Scripts/Core.ps1

##############################################################################
#.SYNOPSIS
# Stores the credentials for accessing the ScienceLogic EM7 API in-memory
# and tests the connection to the web service.
##############################################################################
function Connect-EM7 {

    [CmdletBinding()]
    param(

        # The API root URI
        [Parameter(Position=0, Mandatory=$true)]
        [Uri]$URI,

        # The credentials to use when calling the API.
        [Parameter(Mandatory=$true)]
        [PSCredential]$Credential,

        # Specify this when you'll be using a HTTP debugger like Fiddler.
        # It will cause the JSON to be formatted with whitespace for easier
        # reading, but is more likely to result in errors with larger responses.
        [Parameter()]
        [Switch]$Formatted,

        # If specified, SSL errors will be ignored in all SSL requests made
        # from this PowerShell session. This is an awful hacky way of doing
        # this and it should only be used for testing.
        [Parameter()]
        [Switch]$IgnoreSSLErrors

    )

    # Force trailing slash
    if ($URI -notlike '*/') { $URI = "$URI/" }

    $Globals.IgnoreSSLErrors = $IgnoreSSLErrors.IsPresent
    if ($IgnoreSSLErrors) {
        [Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
    }

    if (Test-Path $Globals.CredentialPath) { Remove-Item $Globals.CredentialPath }

    $Globals.ApiRoot = $URI
    $Globals.FormatResponse = $Formatted.IsPresent
    $Globals.Credentials = $Credential
    $Globals.Credentials | Add-Member NoteProperty URI $URI
    $Globals.Credentials | Add-Member NoteProperty FormatResponse $Formatted.IsPresent

    # Will throw not-authorized if the credentials are invalid
    HttpInvoke $URI | Out-Null

    $Globals.Credentials | Export-Clixml $Globals.CredentialPath

}

##############################################################################
#.SYNOPSIS
# Retrieves a specific EM7 object by its ID.
##############################################################################
function Get-EM7Object {

    [Alias('gslo')]
    [CmdletBinding(DefaultParameterSetName="ByURI")]
    param(

        # The URI of a single ScienceLogic object, such as /api/device/1
        [Parameter(ParameterSetName="ByURI", Position=0, Mandatory=$True, ValueFromPipeline=$true)]
        [String[]]$URI,

        # The name of the resource index to query.
        # See the documentation for value values.
        # Examples include, device, device_group, organization, account...
        [Parameter(ParameterSetName="ByID", Position=0, Mandatory=$true)]
        [String]$Resource,

        # The ID of a specific entity to retrieve.
        [Parameter(ParameterSetName="ByID", Position=1, Mandatory=$true)]
        [Int32[]]$ID,

        # Specifies one or more property names that ordinarily contain a link
        # to a related object to automatically retrieve and place in the
        # returned object.
        [Parameter()]
        [String[]]$ExpandProperty

    )

    begin {

        EnsureConnected -ErrorAction Stop

    }

    process {

        # If URI specified, convert URI into Resource + ID.
        # All URIs must refer to resources of the same type in a given call.
        # This is a hack due to a design flaw and I'll correct it later.
        if ($URI.Length) {
            if ($URI[0] -match $Globals.UriPattern) {
                $Resource = $Matches.r
                $ID = $URI -replace $Globals.UriPattern,'$3'
            }
        }

        $FindArgs = @{
            Resource = $Resource
            ExpandProperty = $ExpandProperty
            Filter = @{}
            Limit = $Globals.DefaultPageSize
        }

        $IDQueue = New-Object System.Collections.Generic.Queue[Int32] -ArgumentList (,$ID)
        $IDBatch = New-Object System.Collections.Generic.List[Int32] -ArgumentList ($Globals.DefaultPageSize)

        do {

            $IDBatch.Clear()
            for ($i=0;$i -lt $Globals.DefaultPageSize -and $IDQueue.Count;$i++) {
                $IDBatch.Add($IDQueue.Dequeue())
            }

            if ($IDBatch.Count) {
                $FindArgs.Filter['_id.in'] = $IDBatch -join ','
            }

            Find-EM7Object @FindArgs

        } while ($IDQueue.Count);

    }

}

##############################################################################
#.SYNOPSIS
# Queries the specified resource index for resources matching an optional
# filter specification.
##############################################################################
function Find-EM7Object {

    [Alias('fslo')]
    [CmdletBinding()]
    param(

        # The name of the resource index to query.
        # See the documentation for value values.
        # Examples include, device, device_group, organization, account...
        [Parameter(Position=0, Mandatory=$true)]
        [String]$Resource,

        # If specifieed, the keys of this hashtable are prefixed with
        # 'filter.' and used as filters. For example: @{organization=6}
        [Parameter()]
        [Hashtable]$Filter = @{},

        # Limits the results to the specified number. The default is 1000.
        [Parameter()]
        [Int32]$Limit = $Globals.DefaultLimit,

        # The starting offset in the results to return.
        # If retrieving objects in pages of 100, you would specify 0 for page 1,
        # 100 for page 2, 200 for page 3, and so on.
        [Parameter()]
        [Int32]$Offset = 0,

        # Optionally sorts the results by this field in ascending order, or if
        # the field is prefixed with a dash (-) in descending order.
        # You can also pipe the output to PowerShell's Sort-Object cmdlet, but
        # this parameter is processed on the server, which will affect how
        # results are paginated when there are more results than fit in a
        # single page.
        [Parameter()]
        [String]$OrderBy,

        # Specifies one or more property names that ordinarily contain a link
        # to a related object to automatically retrieve and place in the
        # returned object.
        [Parameter()]
        [String[]]$ExpandProperty

    )

    begin {
    
        EnsureConnected -ErrorAction Stop

    }

    process {

        $UriArgs = @{
            Resource = $Resource
            Filter = $Filter
            Limit = $Limit
            Offset = $Offset
            Extended = $true
            OrderBy = $OrderBy
        }

        if ($UriArgs.Limit -gt $Globals.DefaultPageSize) {
            $UriArgs.Limit = $Globals.DefaultPageSize
        }

        $OutputCount = 0

        do {

            $URI = CreateUri @UriArgs
        
            $Response = HttpInvoke $URI
        
            $TotalMatched = $Response.total_matched -as [Int32]
            $TotalReturned = $Response.total_returned -as [Int32]

            $Result = @($Response | UnrollArray)
            if ($ExpandProperty.Length) {
                $Cache = @{}
                foreach ($Obj in $Result) {
                    ExpandProperty $Obj $ExpandProperty -Cache:$Cache
                }
            }

            if ($Result.Length) {
            
                Write-Output $Result

                $OutputCount += $Result.Length
                $UriArgs.Offset += $Result.Length

                if ($UriArgs.Offset + $UriArgs.Limit -gt $Limit) {
                    $UriArgs.Limit = $Limit - $UriArgs.Offset
                }

            }
            else {
                break
            }

        } while ($OutputCount -lt $Limit -and $OutputCount -lt $TotalMatched);

    }

}

##############################################################################
#.SYNOPSIS
# Updates the properties of a EM7 object at the specified URI. Only the
# properties specified in the -InputObject parameter will be updated.
##############################################################################
function Set-EM7Object {
    
    [Alias('sslo')]
    [CmdletBinding(SupportsShouldProcess=$True)]
    param(

        # A custom object (which may be a Hashtable or other PSObject
        # such as a deserialized JSON object or PSCustomObject.)
        [ValidateNotNull()]
        [Parameter(Position=0)]
        [PSObject]$InputObject,

        # The relative or absolute URI of the resource, such as
        # /api/organization/1 or https://servername/api/device/9.
        [Alias("__URI")]
        [ValidateNotNull()]
        [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [Uri]$URI,

        # If specified, the output of the update will be deserialized
        # and written to the pipeline.
        [Parameter()]
        [Switch]$PassThru

    )

    begin {

        EnsureConnected -ErrorAction Stop

    }

    process {

        # Cmdlet takes an absolute or relative URL
        # If relative was specified, make it absolute against the API root
        if (!$URI.IsAbsoluteUri) {
            $URI = New-Object Uri ($Globals.ApiRoot, $URI.OriginalString)
        }

        if ($InputObject -is [Hashtable]) {

            if ($InputObject.Count) {

                $Compress = !$Globals.FormatResponse

                # Typically Hashtables were passed in as an argument
                # They are expected to only contain the properties we want to
                # update. No scrubbing will be done.
                $JSON = ConvertTo-Json -InputObject:$InputObject -Compress:$Compress

            }
            else {
                Write-Warning "No properties were updated."
                return
            }

        }
        else {

            # If it's another PSObject, it's probably been piped in from
            # another command such as Get-EM7Object and modified.
            # We need to exclude aliases and internal properties.

            $Properties = @(
                $InputObject | 
                Get-Member -MemberType NoteProperty | 
                Where Name -NotLike __* | 
                Select -ExpandProperty Name
            )

            if ($Properties.Length) {
                $InputObject = $InputObject | Select $Properties
                $JSON = $InputObject | ConvertTo-Json
            }
            else {
                Write-Warning "No writable properties specified."
                return
            }

        }

        if ($PSCmdlet.ShouldProcess($URI, "POST: $JSON")) {

            $Result = HttpInvoke $URI -Method POST -PostData $JSON

            if ($PassThru) {

                if ($URI.AbsolutePath -match $Globals.UriPattern) {
                    $TypeName = $Matches.t
                    $ID = $Matches.id
                    if ($ID -as [Int32]) { $ID = $ID -as [Int32] }
                    $Result | Add-Member -TypeName $TypeName
                    $Result | Add-Member NoteProperty __ID $ID
                    $Result | Add-Member NoteProperty __URI $URI.AbsolutePath
                }

                Write-Output $Result

            }

        }

    }

}

##############################################################################
#.SYNOPSIS
# Creates a new EM7 object at the specified URI using the properties of the
# input object.
#
#.RETURNS
# The object that was created.
##############################################################################
function Add-EM7Object {
    
    [CmdletBinding(SupportsShouldProcess=$True)]
    param(

        # A custom object (which may be a Hashtable or other PSObject
        # such as a deserialized JSON object or PSCustomObject.)
        [ValidateNotNull()]
        [Parameter(Position=0)]
        [PSObject]$InputObject,

        # The relative or absolute URI of the resource index, such as /api/organization/.
        # Do not include an identity value in this property, as the ID will be provided.
        [Alias("__URI")]
        [ValidateNotNull()]
        [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [Uri]$URI,

        # If specified, the output of the post will be deserialized
        # and written to the pipeline.
        [Parameter()]
        [Switch]$PassThru

    )

    begin {

        EnsureConnected -ErrorAction Stop

    }

    process {

        if ($InputObject -is [Hashtable]) {

            if ($InputObject.Count) {

                $Compress = !$Globals.FormatResponse

                # Typically Hashtables were passed in as an argument
                # They are expected to only contain the properties we want to
                # update. No scrubbing will be done.
                $JSON = ConvertTo-Json -InputObject:$InputObject -Compress:$Compress

            }
            else {
                Write-Warning "No properties were updated."
                return
            }

        }
        else {

            # If it's another PSObject, it's probably been piped in from
            # another command such as Get-EM7Object and modified.
            # We need to exclude aliases and internal properties.

            $Properties = @(
                $InputObject | 
                Get-Member -MemberType NoteProperty | 
                Where Name -NotLike __* | 
                Select -ExpandProperty Name
            )

            if ($Properties.Length) {
                $InputObject = $InputObject | Select $Properties
                $JSON = $InputObject | ConvertTo-Json
            }
            else {
                Write-Warning "No writable properties specified."
                return
            }

        }

        if ($PSCmdlet.ShouldProcess($URI, "POST: $JSON")) {

            $Result = HttpInvoke $URI -Method POST -PostData $JSON

            if ($PassThru) {

                #if ($URI.AbsolutePath -match '^(.*)/([A-Za-z0-9_\-\.]+)$') {
                # $TypeName = $Matches[1]
                # $ID = $Matches[2]
                # if ($ID -as [Int32]) { $ID = $ID -as [Int32] }
                # $Result | Add-Member -TypeName $TypeName
                # $Result | Add-Member NoteProperty __ID $ID
                # $Result | Add-Member NoteProperty __URI $URI.AbsolutePath
                #}

                Write-Output $Result

            }

        }

    }

}