Scripts/Internal.ps1

##############################################################################
#.SYNOPSIS
# Builds a URI from several well-known components of a EM7 API URL.
##############################################################################
function CreateUri {
    
    [CmdletBinding()]
    param(

        # The resource type, such as device or organization.
        [Parameter()]
        [String]$Resource,

        # Specifies the ID of a resource if requesting a specific entity.
        # If not specified, the device index will be queried instead.
        [Parameter()]
        [String]$ID,

        # Specifies a limit on the number of returned results. If this parameter
        # is not specified, no results will be returned.
        [Parameter()]
        [Int32]$Limit,

        # 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,

        # True to request an expanded result, instead of a result containing
        # a list of links to resources.
        [Parameter()]
        [Boolean]$Extended,

        # The keys in this hashtable will be prefixed with 'filter.' and appended
        # to the query string for filtering on resource indexes.
        [Parameter()]
        [Hashtable]$Filter,

        # 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

    )

    $Query = @{}

    if ($Globals.HideFilterInfo) { $Query['hide_filterinfo'] = $Globals.HideFilterInfo }
    if ($Extended) { $Query['extended_fetch'] = 1 }
    if ($Limit) { $Query['limit'] = $Limit }
    if ($Offset) { $Query['offset'] = $Offset }
    if ($OrderBy) {
        if ($OrderBy -like '-*') { $Query["order.$($OrderBy.Substring(1))"] = 'DESC' }
        else { $Query["order.$OrderBy"] = 'ASC' }
    }

    foreach ($Name in $Filter.Keys) {
        $Query["filter.$Name"] = $Filter[$Name]
    }

    $URI = %New-Uri "$Resource/$ID" -BaseUri $Globals.ApiRoot -QueryString $Query
    Return $URI

}

##############################################################################
#.SYNOPSIS
# The ScienceLogic EM7 API returns multiple results as a single root object
# container whose properties are URIs and values are the actual object that
# we want. This is much more usable as an array, so this function simply
# enumerates the properties that are URIs and returns the values of those
# properties, essentially turning a JSON object into an array of its values.
##############################################################################
function UnrollArray {

    [CmdletBinding()]
    param(

        # The objects whose properties are to be enumerated and unrolled
        # into an array of those properties' values.
        [Parameter(Position=0, ValueFromPipeline=$true)]
        [PSObject]$InputObject

    )

    if ($InputObject.result_set) {
        $InputObject = $InputObject.result_set
        
    }

    $AllKeys = @($InputObject | Get-Member -MemberType NoteProperty | Select -ExpandProperty Name)
    $UriKeys = @($AllKeys -like '/api/*')
    if ($AllKeys.Count) {
        if ($AllKeys.Count -eq $UriKeys.Count) {
            $UriKeys | ForEach { 
                $Item = $InputObject.$_ 
                if ($_ -match '^(.*)/([A-Za-z0-9_\-\.]+)$') {
                    $TypeName = $Matches[1]
                    $ID = $Matches[2]
                    if ($ID -as [Int32]) { $ID = $ID -as [Int32] }
                    $Item | Add-Member -TypeName $TypeName
                    $Item | Add-Member NoteProperty __ID $ID
                    $Item | Add-Member NoteProperty __URI $_
                }
                Write-Output $Item
            }
        }
        else {
            # Not sure what this object is.
            # It has properties, but not all of them are URI keys
            # Just return it as-is.
            Write-Output $InputObject
        }
    }

}

##############################################################################
#.SYNOPSIS
# The ScienceLogic EM7 API returns objects that contain links to other
# resources. For example, a device has a link to its organization. This
# property is represented as a relative URI to the related object. This
# function takes an input object and one or more of these link property names
# to 'expand' by making HTTP requests for them and replacing the properties
# of the original object with the results of those HTTP requests.
# In other words, a device object that has an organization property which is
# a link to its organization will now have an organization property which is
# the organization object itself.
##############################################################################
function ExpandProperty {

    [CmdletBinding()]
    param(

        # The object that contains links to other objects to be expanded.
        [Parameter(Position=0)]
        [PSObject]$InputObject,

        # One or more property names to expand by making requests for those
        # objects.
        [Parameter(Position=1)]
        [String[]]$Property,

        # When ExpandProperty is being called on multiple objects (for example,
        # a list of devices in an organization), many or all of those objects
        # may have links that reference the same object. Rather than requesting
        # the object multiple times, a Hashtable can be created up front and
        # passed into each call to ExpandProperty. If the URI is found in the
        # hashtable as a key, that object will be returned immediately instead
        # of being requested again.
        # It is recommended that you do not reuse the cache between batches,
        # unless there is a specific reason to do so.
        [Parameter()]
        [Hashtable]$Cache

    )

    if ($Cache -eq $Null) { $Cache  =@{} }

    foreach ($Prop in $Property) {

        Write-Verbose "Expanding: $Prop"

        $P,$S = $Prop -split '/'

        $URI = $Null

        if ($InputObject.$P -is [String]) {
            
            # URI is a simple property
            # ie. 'snmp_cred_id': '/api/credential/snmp/1'
            $URI = $InputObject.$P

        }
        elseif ($InputObject.$P -is [Array]) {

            if (!($InputObject.$P -notlike '/api/*')) {

                $URI = $InputObject.$P

                $InputObject.$P = @()

            }

        }
        else {

            # URI is a complex property
            # ie. "notes": {
            # "URI": "/api/device/3066/note/?hide_filterinfo=1&limit=1000",
            # "description": "Notes"
            # },

            if ($InputObject.$P.URI -is [String]) {
                $URI = $InputObject.$P.URI
            }

        }

        foreach ($U in $URI) {

            # First check the cache
            if (!$Cache[$U]) {
                # Not there? Go get it.
                $Cache[$U] = HttpInvoke (%New-Uri $U -BaseUri $Globals.ApiRoot -QueryString @{extended_fetch=1;limit=10}) | UnrollArray
            }

            if ($URI -is [Array]) {
                $InputObject.$P += $Cache[$U]
            }
            else {
                $InputObject.$P = $Cache[$U]
            }

        }

        # Are there subproperties to expand?
        if ($S.Length) {
            ExpandProperty ($InputObject.$P) ($S -join '/') -Cache:$Cache
        }

    }

}

##############################################################################
#.SYNOPSIS
# Checks to make sure that Connect-EM7 has been called. In other words, that
# we have an API root and credentials. Does not verify credentials.
##############################################################################
function EnsureConnected {
    
    if (!$Globals.ApiRoot -or !$Globals.Credentials) {
        Write-Error "Connect-EM7 must be called first."
    }

}

##############################################################################
#.SYNOPSIS
# Makes a HTTP request for a particular URL, passing in the required
# authentication headers and other global options used with the ScienceLogic
# EM7 REST API.
##############################################################################
function HttpInvoke {

    [CmdletBinding(DefaultParameterSetName="GET")]
    param(
        
        # The URI of the resource
        [Parameter(Position=0, Mandatory=$true)]
        [URI]$URI,

        # Specifies the HTTP verb to use.
        # The default is GET
        [Parameter(ParameterSetName="Advanced")]
        [String]$Method = "GET",

        # The POST data to include in the request.
        [Parameter(ParameterSetName="Advanced")]
        [String]$PostData,

        # Not currently implemented
        [Parameter()]
        [Switch]$ThrowIfNotFound

    )

    Write-Verbose "$Method $URI"

    [System.Net.HttpWebRequest]$Request = $Null
    [System.Net.HttpWebResponse]$Response = $Null
    [System.IO.Stream]$RequestStream = $Null
    [System.IO.StreamWriter]$RequestWriter = $Null
    [System.IO.Stream]$ResponseStream = $Null
    [System.IO.StreamReader]$ResponseReader = $Null

    try {

        $Cred    = $Globals.Credentials.GetNetworkCredential()
    
        $Request = [System.Net.HttpWebRequest]([System.Net.WebRequest]::Create($URI))
        $Request.Method = $Method
        $Request.Accept = 'application/json'
        $Request.Headers.Add('Authorization', "Basic $([Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Cred.UserName + ':' + $Cred.Password)))")

        if ($Globals.FormatResponse) {
            $Request.Headers.Add('x-em7-beautify-response', '1')
        }

        $Request.AllowAutoRedirect = $false

        if ($PostData) {
            $Request.ContentType = "application/json"
            $RequestStream = $Request.GetRequestStream()
            $RequestWriter = New-Object System.IO.StreamWriter ($RequestStream)
            $RequestWriter.Write($PostData)
            $RequestWriter.Flush()
            $RequestWriter.Close()
            $RequestStream.Close()
        }

        $Response = $Request.GetResponse()
        $ResponseStream = $Response.GetResponseStream()
        $ResponseReader = New-Object System.IO.StreamReader ($ResponseStream)

        [String]$JSON = $ResponseReader.ReadToEnd()

        $Result = ConvertFrom-Json $JSON

        Return $Result

    }
    finally {

        if ($RequestWriter) { $RequestWriter.Dispose() }
        if ($RequestStream) { $RequestStream.Dispose() }
        if ($ResponseReader) { $ResponseReader.Dispose() }
        if ($ResponseStream) { $ResponseStream.Dispose() }
        if ($Response) { $Response.Dispose() }

    }

}

##############################################################################
#.SYNOPSIS
# Verifies that the current user is an administrator and the process is
# currently elevated under UAC.
##############################################################################
function %Test-Administrator {

    try {
        
        $Identity = [Security.Principal.WindowsIdentity]::GetCurrent()
        $Principal = New-Object Security.Principal.WindowsPrincipal $Identity

        if ($Principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { 
            return $true
        } 
        
    }
    finally {
        if ($Identity) { $Identity.Dispose() }
    }

}

##############################################################################
#.SYNOPSIS
# Creates a new URI based on the specified base and relative URI's.
#
#.EXAMPLE
# $Uri = New-Uri /images -BaseUri http://www.google.com
##############################################################################
function %New-Uri {

    [OutputType([System.Uri])]
    [CmdletBinding()]
    param (
    
        # A string that contains a valid URI. If -BaseUri is specified, this may be
        # relative to the BaseUri.
        [Alias('u')]
        [Parameter(Position=0, Mandatory=$true)]
        [String]$Uri,
        
        # The URI to use as the base upon which the Uri builds.
        [Alias('Base', 'b')]
        [Parameter()]
        [Uri]$BaseUri,
        
        # A list of key/value pairs that are appended to the URI's query string.
        [Alias('q')]
        [Parameter()]
        [Hashtable]$QueryString

    )

    if ($BaseUri) {
        $UriBuilder = New-Object UriBuilder(New-Object Uri($BaseUri,$Uri))
    }
    else {
        $UriBuilder = New-Object UriBuilder(New-Object Uri($Uri))
    }
    
    if ($QueryString.Count) {
        
        # Work around a bug in UriBuilder's Query property
        # which can result in redundant ? characters.
        [String]$Query = $UriBuilder.Query.TrimStart('?')

        foreach ($Key in $QueryString.Keys) {
            $Value = $QueryString[$Key]
            $Query += [String]::Concat(
                '&',
                [Uri]::EscapeDataString($Key),
                '=',
                [Uri]::EscapeDataString($Value)
            )
        }

        $UriBuilder.Query = $Query.TrimStart('&')

    }

    $UriBuilder.Uri

}