Functions/Public/Locations.ps1

# Location management and geography functions

Function Get-NectarLocation {
    <#
        .SYNOPSIS
        Returns a list of Nectar DXP locations
         
        .DESCRIPTION
        Returns a list of Nectar DXP locations
 
        .PARAMETER SearchQuery
        The name of the location to get information on based on either network, networkName, City, StreetAddress, State, SiteName or SiteCode. Can be a partial match, and may return more than one entry.
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
                 
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .EXAMPLE
        Get-NectarLocation
        Returns the first 10 locations
         
        .EXAMPLE
        Get-NectarLocation -ResultSize 100
        Returns the first 100 locations
 
        .EXAMPLE
        Get-NectarLocation -LocationName Location2
        Returns up to 10 locations that contains "location2" anywhere in the name. The search is not case-sensitive. This example would return Location2, Location20, Location214, MyLocation299 etc
 
        .EXAMPLE
        Get-NectarLocation -LocationName ^Location2
        Returns up to 10 locations that starts with "location2" in the name. The search is not case-sensitive. This example would return Location2, Location20, Location214 etc, but NOT MyLocation299
 
        .EXAMPLE
        Get-NectarLocation -LocationName ^Location2$
        Returns a location explicitly named "Location2". The search is not case-sensitive.
 
        .NOTES
        Version 1.1
    #>

    
    [Alias("gnl")]
    Param (
        [Parameter(Mandatory=$False)]
        [string]$SearchQuery,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize = 5000
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { 
                $TenantName = $Global:NectarTenantName 
            } ElseIf ($TenantName) {
                If ($TenantName -NotIn $Global:NectarTenantList) {
                    $TList = $Global:NectarTenantList -join ', '
                    Throw "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)"
                }
            }
            
            $URI = "https://$Global:NectarCloud/aapi/config/locations?pageNumber=1&tenant=$TenantName&pageSize=$ResultSize&searchQuery=$SearchQuery"
            Write-Verbose $URI
        
            $Response = [Text.Encoding]::UTF8.GetString((Invoke-WebRequest $URI -UseBasicParsing -Method 'GET' -Headers $Global:NectarAuthHeader).RawContentStream.ToArray())  # This is required to get the data in UTF8 format
            $JSON = $Response | ConvertFrom-JSON
            
            If (!$JSON.elements) {
                Write-Error "Location $SearchQuery not found."
            }
            Else {
                If ($TenantName) { $JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty } # Add the tenant name to the output which helps pipelining
                $JSON.elements | Add-Member -TypeName 'Nectar.LocationList'
                Return $JSON.elements
            }
        }
        Catch {
            Write-Error "Unable to get location details. $($_.Exception.Message)"
            If ($PSCmdlet.MyInvocation.BoundParameters["ErrorAction"] -ne "SilentlyContinue") { Get-JSONErrorStream -JSONResponse $_ }
        }
    }
}


Function Set-NectarLocation {
    <#
        .SYNOPSIS
        Update a Nectar DXP location in the location database
         
        .DESCRIPTION
        Update a Nectar DXP location in the location database. This command can use the Google Geocode API to automatically populate the latitude/longitude for each location. You can register for an API key and save it as persistent environment variable called GoogleGeocode_API_Key on this machine. This command will prompt for the GeoCode API key and will save it in the appropriate location. Follow this link to get an API Key - https://developers.google.com/maps/documentation/geocoding/get-api-key. If this is not an option, then use the -SkipGeoLocate switch
 
        .PARAMETER SearchQuery
        A string to search for. Will search in Network, NetworkName, City, Street Address, Region etc.
         
        .PARAMETER Network
        The IP subnet of the network
         
        .PARAMETER NetworkRange
        The subnet mask of the network
         
        .PARAMETER ExtNetwork
        The IP subnet of the external/public network. Optional. Used to help differentiate calls from corporate locations that use common home subnets (192.168.x.x)
         
        .PARAMETER ExtNetworkRange
        The subnet mask of the external/public network. Optional. Used to help differentiate calls from corporate locations that use common home subnets (192.168.x.x)
         
        .PARAMETER NetworkName
        The name to give to the network
         
        .PARAMETER SiteName
        The name to give to the siteCode
         
        .PARAMETER SiteCode
        A site code to assign to the site
         
        .PARAMETER Region
        The name of the region. Typically is set to country name or whatever is appropriate for the company
         
        .PARAMETER StreetAddress
        The street address of the location
         
        .PARAMETER City
        The city of the location
         
        .PARAMETER State
        The state/province of the location
         
        .PARAMETER PostCode
        The postal/zip code of the location
         
        .PARAMETER Country
        The 2-letter ISO country code of the location
         
        .PARAMETER Description
        A description to apply to the location
         
        .PARAMETER IsWireless
        True or false if the network is strictly wireless
         
        .PARAMETER IsExternal
        True or false if the network is outside the corporate network
         
        .PARAMETER IsVPN
        True or false if the network is a VPN
 
        .PARAMETER NetworkRangeType
        The type of network range. Choose from Sequential or Subnet
         
        .PARAMETER Latitude
        The geographical latitude of the location. If not specified, will attempt automatic geolocation.
         
        .PARAMETER Longitude
        The geographical longitude of the location. If not specified, will attempt automatic geolocation.
         
        .PARAMETER SkipGeoLocate
        Don't attempt geolocation. Do this if you don't have a valid Google Maps API key.
         
        .PARAMETER Identity
        The numerical ID of the location to update. Can be obtained via Get-NectarLocation and pipelined to Set-NectarLocation
         
        .EXAMPLE
        Set-NectarLocation HeadOffice -Region WestUS
        Changes the region for HeadOffice to WestUS
 
        .EXAMPLE
        Get-NectarLocation | Set-NectarLocation
        Will go through each location and update the latitude/longitude. Useful if a Google Geocode API key was obtained after initial location loading
         
        .NOTES
        Version 1.11
    #>

    
    [Alias("snl")]
    Param (
        [Parameter(Mandatory=$False)]
        [string]$SearchQuery,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("subnet")]
        [string]$Network,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateRange(0,32)]
        [string]$NetworkRange,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias('ExternalNetwork')]
        [string]$ExtNetwork,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias('ExternalNetworkRange')]
        [ValidateRange(0,32)]
        [string]$ExtNetworkRange,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateLength(1,99)]
        [Alias("Network Name")]
        [string]$NetworkName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$SiteName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("Site Code")]
        [string]$SiteCode,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Region,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("address")]
        [string]$StreetAddress,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$City,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("province")]
        [string]$State,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("zipcode")]
        [string]$PostCode,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateLength(0,2)]
        [string]$Country,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Description,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet("True","False","Yes","No",0,1, IgnoreCase=$True)]
        [Alias("isWirelessNetwork","Wireless(Yes/No)","Wireless","Wifi")]
        [string]$IsWireless,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet("True","False","Yes","No",0,1, IgnoreCase=$True)]
        [Alias("External(Yes/No)","External")]
        [string]$IsExternal,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet("True","False","Yes","No",0,1, IgnoreCase=$True)]
        [Alias("VPN","VPN(Yes/No)")]
        [string]$IsVPN,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('Sequential','Subnet','Virtual', IgnoreCase=$True)]
        [string]$NetworkRangeType,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateRange(-90,90)]
        [Alias("CoordinatesLatitude")]
        [double]$Latitude,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateRange(-180,180)]
        [Alias("CoordinatesLongitude")]
        [double]$Longitude,
        [Parameter(Mandatory=$False)]
        [switch]$ForceGeoLocate,
        [Parameter(Mandatory=$False)]
        [switch]$SkipGeoLocate,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("id")]
        [int]$Identity
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { 
                $TenantName = $Global:NectarTenantName 
            } ElseIf ($TenantName) {
                If ($TenantName -NotIn $Global:NectarTenantList) {
                    $TList = $Global:NectarTenantList -join ', '
                    Throw "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)"
                }
            }

        Try {
            If ($SearchQuery) {
                $LocationInfo = Get-NectarLocation -SearchQuery $SearchQuery -Tenant $TenantName -ResultSize 1
                $Identity = $LocationInfo.id #.ToString()
            }
            
            If (-not $Network) {$Network = $LocationInfo.network}
            If (-not $NetworkRange) {$NetworkRange = $LocationInfo.networkRange}
            If (-not $ExtNetwork) {$ExtNetwork = $LocationInfo.externalNetwork}
            If (-not $ExtNetworkRange) {$ExtNetworkRange = $LocationInfo.externalNetworkRange}
            If (-not $NetworkRangeType) {$NetworkRangeType = $LocationInfo.networkRangeType}
            If (-not $NetworkName) {$NetworkName = $LocationInfo.networkName}
            If (-not $SiteName) {$SiteName = $LocationInfo.siteName}
            If (-not $SiteCode) {$SiteCode = $LocationInfo.siteCode}
            If (-not $Region) {$Region = $LocationInfo.region}
            If (-not $StreetAddress) {$StreetAddress = $LocationInfo.streetAddress}
            If (-not $City) {$City = $LocationInfo.city}
            If (-not $State) {$State = $LocationInfo.state}
            If (-not $PostCode) {$PostCode = $LocationInfo.zipCode}
            If (-not $Country) {$Country = $LocationInfo.country}
            If (-not $Description) {$Description = $LocationInfo.description}
            If (-not $IsWireless) {$IsWireless = $LocationInfo.isWirelessNetwork}
            If (-not $IsExternal) {$IsExternal = $LocationInfo.isExternal}
            If (-not $IsVPN) {$IsVPN = $LocationInfo.vpn}
            If ($NULL -eq $Latitude -or $Latitude -eq 0) {$Latitude = $LocationInfo.latitude}
            If ($NULL -eq $Longitude -or $Longitude -eq 0) {$Longitude = $LocationInfo.longitude}
            If (-not $IsVPN) {$IsVPN = $LocationInfo.vpn}

            If ((($NULL -eq $Latitude -Or $NULL -eq $Longitude) -Or ($Latitude -eq 0 -And $Longitude -eq 0)) -Or $ForceGeoLocate -And !$SkipGeoLocate) {
                Write-Verbose "Lat/Long missing. Getting Lat/Long."
                $LatLong = Get-LatLong "$StreetAddress, $City, $State, $PostCode, $Region"
                $Latitude = $LatLong.Latitude
                $Longitude = $LatLong.Longitude
            }

            $URI = "https://$Global:NectarCloud/aapi/config/location/$($Identity)?tenant=$TenantName"
            Write-Verbose $URI

            $Body = @{
                city = $City
                description = $Description
                id = $Identity
                isExternal = ParseBool $IsExternal
                isWirelessNetwork = ParseBool $IsWireless
                latitude = $Latitude.ToString()
                longitude = $Longitude.ToString()
                network = $Network
                networkRange = $NetworkRange
                externalNetwork = $ExtNetwork
                externalNetworkRange = $ExtNetworkRange
                networkName = $NetworkName
                networkRangeIpEnd = $NULL
                networkRangeIpStart = $NULL
                networkRangeType = $NetworkRangeType
                region = $Region
                siteCode = $SiteCode
                siteName = $SiteName
                state = $State
                streetAddress = $StreetAddress
                country = $Country
                vpn = ParseBool $IsVPN
                zipCode = $PostCode
            }
            
            $JSONBody = $Body | ConvertTo-Json

            Try {
                Write-Verbose $JSONBody
                $NULL = Invoke-RestMethod -Method PUT -URI $URI -Headers $Global:NectarAuthHeader -Body $JSONBody -ContentType 'application/json; charset=utf-8' 
            }
            Catch {
                If ($NetworkName) {
                    $IDText = $NetworkName
                }
                Else {
                    $IDText = "with ID $Identity"
                }
                
                Write-Error "Unable to apply changes for location $IDText. $($_.Exception.Message)"
                If ($PSCmdlet.MyInvocation.BoundParameters["ErrorAction"] -ne "SilentlyContinue") { Get-JSONErrorStream -JSONResponse $_ }
            }
        }
        Catch {
            Write-Error $($_.Exception.Message)
        }
    }
}


Function New-NectarLocation {
    <#
        .SYNOPSIS
        Creates a Nectar DXP location in the location database
         
        .DESCRIPTION
        Creates a Nectar DXP location in the location database. This command can use the Google Geocode API to automatically populate the latitude/longitude for each location. You can register for an API key and save it as persistent environment variable called GoogleGeocode_API_Key on this machine. This command will prompt for the GeoCode API key and will save it in the appropriate location. Follow this link to get an API Key - https://developers.google.com/maps/documentation/geocoding/get-api-key. If this is not an option, then use the -SkipGeoLocate switch
 
        .PARAMETER Network
        The IP subnet of the network
         
        .PARAMETER NetworkRange
        The subnet mask of the network
         
        .PARAMETER ExtNetwork
        The IP subnet of the external/public network. Optional. Used to help differentiate calls from corporate locations that use common home subnets (192.168.x.x)
         
        .PARAMETER ExtNetworkRange
        The subnet mask of the external/public network. Optional. Used to help differentiate calls from corporate locations that use common home subnets (192.168.x.x)
         
        .PARAMETER NetworkName
        The name to give to the network
         
        .PARAMETER SiteName
        The name to give to the site
         
        .PARAMETER SiteCode
        A site code to assign to the site
         
        .PARAMETER Region
        The name of the region. Typically is set to country name or whatever is appropriate for the company
         
        .PARAMETER StreetAddress
        The street address of the location
         
        .PARAMETER City
        The city of the location
         
        .PARAMETER State
        The state/province of the location
         
        .PARAMETER PostCode
        The postal/zip code of the location
         
        .PARAMETER Country
        The 2-letter ISO country code of the location
         
        .PARAMETER Description
        A description to apply to the location
         
        .PARAMETER IsWireless
        True or false if the network is strictly wireless
         
        .PARAMETER IsExternal
        True or false if the network is outside the corporate network
         
        .PARAMETER IsVPN
        True or false if the network is a VPN
 
        .PARAMETER NetworkRangeType
        The type of network range. Choose from Sequential, Subnet or Virtual
         
        .PARAMETER SkipGeoLocate
        Don't attempt geolocation. Do this if you don't have a valid Google Maps API key.
         
        .PARAMETER Latitude
        The geographical latitude of the location. If not specified, will attempt automatic geolocation.
         
        .PARAMETER Longitude
        The geographical longitude of the location. If not specified, will attempt automatic geolocation.
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        New-NectarLocation -Network 10.14.3.0 -NetworkRange 24 -NetworkName Corp5thFloor -SiteName 'Head Office'
        Creates a new location using the minimum required information
         
        .EXAMPLE
        New-NectarLocation -Network 10.15.1.0 -NetworkRange 24 -ExtNetwork 79.23.155.71 -ExtNetworkRange 28 -NetworkName Corp3rdFloor -SiteName 'Head Office' -SiteCode HO3 -IsWireless True -IsVPN False -Region EastUS -StreetAddress '366 North Broadway' -City Jericho -State 'New York' -Country US -PostCode 11753 -Description 'Head office 3rd floor' -Latitude 40.7818283 -Longitude -73.5351438
        Creates a new location using all available fields
                 
        .EXAMPLE
        Import-Csv LocationData.csv | New-NectarLocation
        Imports a CSV file called LocationData.csv and creates new locations
         
        .EXAMPLE
        Import-Csv LocationData.csv | New-NectarLocation -SkipGeolocate
        Imports a CSV file called LocationData.csv and creates new locations but will not attempt geolocation
 
        .NOTES
        Version 1.1
    #>

    
    [Alias("nnl")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias('subnet','searchquery')]
        [string]$Network,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateRange(0,32)]
        [string]$NetworkRange,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias('ExternalNetwork')]
        [string]$ExtNetwork,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias('ExternalNetworkRange')]
        [ValidateRange(0,32)]
        [string]$ExtNetworkRange,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [ValidateLength(1,99)]
        [Alias('Network Name')]
        [string]$NetworkName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias('Site Name')]
        [string]$SiteName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias('Site Code')]
        [string]$SiteCode,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Region,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias('Street Address', 'address')]
        [string]$StreetAddress,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$City,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias('province')]
        [string]$State,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias('Zip Code', 'zipcode')]
        [string]$PostCode,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateLength(0,2)]
        [string]$Country,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Description,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('True','False','Yes','No',0,1, IgnoreCase=$True)]
        [Alias('isWirelessNetwork','Wireless(Yes/No)','Wireless(True/False)','Wireless','Wifi')]
        [string]$IsWireless,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('True','False','Yes','No',0,1, IgnoreCase=$True)]
        [Alias('External(Yes/No)','External(True/False)','External','Internal/External')]
        [string]$IsExternal,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('True','False','Yes','No',0,1, IgnoreCase=$True)]
        [Alias('VPN','VPN(Yes/No)','VPN(True/False)')]
        [string]$IsVPN,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('Sequential','Subnet','Virtual', IgnoreCase=$True)]
        [string]$NetworkRangeType = 'Subnet',
        [Parameter(Mandatory=$False)]
        [switch]$SkipGeoLocate,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateRange(-90,90)]
        [Alias('Coordinates Latitude','CoordinatesLatitude')]
        [double]$Latitude,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateRange(-180,180)]
        [Alias('Coordinates Longitude','CoordinatesLongitude')]
        [double]$Longitude,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName        
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { 
                $TenantName = $Global:NectarTenantName 
            } ElseIf ($TenantName) {
                If ($TenantName -NotIn $Global:NectarTenantList) {
                    $TList = $Global:NectarTenantList -join ', '
                    Throw "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)"
                }
            }
        
        $URI = "https://$Global:NectarCloud/aapi/config/location?tenant=$TenantName"
        Write-Verbose $URI

        If (-not $Latitude -Or -not $Longitude -And !$SkipGeoLocate) {
            $LatLong = Get-LatLong "$StreetAddress, $City, $State, $PostCode, $Country"
            [double]$Latitude = $LatLong.Latitude
            [double]$Longitude = $LatLong.Longitude
        }

        $Body = @{
            city = $City
            description = $Description
            isExternal = ParseBool $IsExternal
            isWirelessNetwork = ParseBool $IsWireless
            latitude = $Latitude.ToString()
            longitude = $Longitude.ToString()
            network = $Network
            networkName = $NetworkName
            externalNetwork = $ExtNetwork
            externalNetworkRange = $ExtNetworkRange
            region = $Region
            siteCode = $SiteCode
            siteName = $SiteName
            state = $State
            streetAddress = $StreetAddress
            country = $Country
            vpn = ParseBool $IsVPN
            zipCode = $PostCode
            networkRangeType = $NetworkRangeType
        }

        # This is to get around a bug where Nectar DXP throws an error if a location is built around externalNetworkRange
        # DXP expects that networkRange be explictly NULL, but PowerShell sends an empty string which causes a 500 error
        # The error doesn't occur if networkRange is simply not present.
        If ($NetworkRange) {
            $Body.Add('networkRange', $NetworkRange)
        }

        $JSONBody = $Body | ConvertTo-Json

        Try {
            Write-Verbose $JSONBody
            $NULL = Invoke-RestMethod -Method POST -URI $URI -Headers $Global:NectarAuthHeader -Body $JSONBody -ContentType 'application/json; charset=utf-8'
        }
        Catch {
            Write-Error "Unable to create location $NetworkName with network $Network/$NetworkRange. $($_.Exception.Message)"
            If ($PSCmdlet.MyInvocation.BoundParameters["ErrorAction"] -ne "SilentlyContinue") { Get-JSONErrorStream -JSONResponse $_ }
        }
    }
}


Function Remove-NectarLocation {
    <#
        .SYNOPSIS
        Removes a Nectar DXP location from the location database
         
        .DESCRIPTION
        Removes a Nectar DXP location from the location database
 
        .PARAMETER SearchQuery
        The name of the location to remove. Can be a partial match. To return an exact match and to avoid ambiguity, enclose location name with ^ at the beginning and $ at the end.
 
        .PARAMETER NetworkRangeType
        The type of network range to delete.
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .PARAMETER Identity
        The numerical ID of the location to remove. Can be obtained via Get-NectarLocation and pipelined to Remove-NectarLocation
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("rnl")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("networkName")]
        [string]$SearchQuery,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('Sequential','Subnet','Virtual', IgnoreCase=$True)]
        [string]$NetworkRangeType = 'Subnet',
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("id")]
        [string]$Identity
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { 
                $TenantName = $Global:NectarTenantName 
            } ElseIf ($TenantName) {
                If ($TenantName -NotIn $Global:NectarTenantList) {
                    $TList = $Global:NectarTenantList -join ', '
                    Throw "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)"
                }
            }
            
        If ($SearchQuery -And !$Identity) {
            $LocationInfo = Get-NectarLocation -SearchQuery $SearchQuery -Tenant $TenantName -ResultSize 1 -ErrorVariable GetLocationError
            $Identity = $LocationInfo.id
            $NetworkName = $LocationInfo.networkName
        }
            
        If (!$GetLocationError) {
            $URI = "https://$Global:NectarCloud/aapi/config/location/$($Identity)?networkRangeType=$NetworkRangeType&tenant=$TenantName"
            Write-Verbose $URI

            Try {
                $NULL = Invoke-RestMethod -Method DELETE -URI $URI -Headers $Global:NectarAuthHeader
                Write-Verbose "Successfully deleted $LocationName."
            }
            Catch {
                Write-Error "Unable to delete location $NetworkName. Ensure you typed the name of the location correctly. $($_.Exception.Message)"
                If ($PSCmdlet.MyInvocation.BoundParameters["ErrorAction"] -ne "SilentlyContinue") { Get-JSONErrorStream -JSONResponse $_ }
            }
        }
    }
}


Function Import-NectarLocations {
    <#
        .SYNOPSIS
        Imports a CSV list of locations into Nectar DXP
         
        .DESCRIPTION
        Import a CSV list of locations into Nectar DXP. This will overwrite any existing locations with the same network ID. Useful for making wholesale changes without wiping and replacing everything.
        Assumes you are working from an export from the existing Nectar DXP location list.
 
        .PARAMETER Path
        The path to the CSV file to import into Nectar DXP. The CSV file must use the standard column heading template used by Nectar DXP exports.
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER SkipGeoLocate
        Don't attempt geolocation. Do this if you don't have a valid Google Maps API key or the lat/long is already included in the CSV.
         
        .NOTES
        Version 1.1
    #>

    
    Param (
        [Parameter(Mandatory=$True)]
        [string]$Path, 
        [Parameter(Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [switch]$SkipGeoLocate    
    )
    
    $LocTable = ((Get-Content -Path $Path -Raw) -replace '\(Yes/No\)','')
    $LocTable = $LocTable -replace '\"?Network\"?\,',"""SearchQuery""," 
    
    $LocationList = ConvertFrom-Csv $LocTable | Select-Object 'Network','NetworkRange','ExtNetwork','ExtNetworkRange','NetworkRangeType','NetworkName','SiteName','SiteCode','City','PostCode','Country','State','Region','Description','IsExternal','IsVPN','IsWireless','Latitude','Longitude'

    ForEach ($Location in $LocationList) {
        $LocationHashTable = @{}
        $Location.psobject.properties | ForEach-Object { $LocationHashTable[$_.Name] = $_.Value }
        
        If ($TenantName) { $LocationHashTable += @{TenantName = $TenantName } } # Add the tenant name to the hashtable
        If ($SkipGeoLocate) { $LocationHashTable += @{SkipGeoLocate = $TRUE} }
        
        Try {
            Write-Host "Updating location with subnet $($Location.SearchQuery)"
            Write-Verbose $LocationHashTable
            Set-NectarLocation @LocationHashTable -ErrorAction:Stop
        }
        Catch {
            Write-Host "Location does not exist. Creating location $($Location.SearchQuery)"
            New-NectarLocation @LocationHashTable
        }
    }
}


Function Import-MSTeamsLocations {
    <#
        .SYNOPSIS
        Imports a CSV list of locations downloaded from Microsoft CQD into Nectar DXP
         
        .DESCRIPTION
        Import a CSV list of locations downloaded from Microsoft CQD into Nectar DXP. This will overwrite any existing locations with the same network ID. Useful for making wholesale changes without wiping and replacing everything.
 
        .PARAMETER Path
        The path to the CSV file to import into Nectar DXP. The CSV file must be in the same format as downloaded from Microsoft CQD as per https://docs.microsoft.com/en-us/microsoftteams/cqd-upload-tenant-building-data
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER SkipGeoLocate
        Don't attempt geolocation. Do this if you don't have a valid Google Maps API key.
         
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(Mandatory=$True)]
        [string]$Path, 
        [Parameter(Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [switch]$SkipGeoLocate    
    )
    
    $Header = 'SearchQuery', 'NetworkName', 'NetworkRange', 'SiteName', 'OwnershipType', 'BuildingType', 'BuildingOfficeType', 'City', 'PostCode', 'Country', 'State', 'Region', 'IsExternal', 'ExpressRoute', 'IsVPN'
    $LocationList = Import-Csv $Path -Header $Header | Select-Object 'SearchQuery','NetworkRange','NetworkName','SiteName','City','PostCode','Country','State','Region','IsExternal','IsVPN'
    
    ForEach ($Location in $LocationList) {
        If ($Location.IsExternal -eq 0) { $Location.IsExternal = 1 } Else { $Location.IsExternal = 0 }
        If ($Location.IsVPN -eq 1) { $Location.IsVPN = 1 } Else { $Location.IsVPN = 0 }
    
        $LocationHashTable = @{}
        $Location.psobject.properties | ForEach-Object { $LocationHashTable[$_.Name] = $_.Value }
        
        If ($TenantName) { $LocationHashTable += @{TenantName = $TenantName } }# Add the tenant name to the hashtable
        If ($SkipGeoLocate) { $LocationHashTable += @{SkipGeoLocate = $TRUE} }
                
        Try {
            Write-Host "Updating location with subnet $($Location.SearchQuery)"
            Write-Verbose $LocationHashTable
            Set-NectarLocation @LocationHashTable -ErrorAction:Stop
        }
        Catch {
            Write-Host "Location does not exist. Creating location $($Location.SearchQuery)"
            New-NectarLocation @LocationHashTable
        }
    }
}




#################################################################################################################################################
# #
# Tenant Alert Functions #
# #


Function Get-NectarExtCities {
    <#
        .SYNOPSIS
        Returns a list of cities found via IP geolocation
         
        .DESCRIPTION
        Most call records include the user's external IP address. Nectar DXP does a geo-IP lookup of the external IP address and stores the geographic information for later use. This command will return all the cities where Nectar DXP was able to successfully geolocate an external IP address.
 
        .PARAMETER SearchQuery
        The name of the city to locate. Can be a partial match, and may return more than one entry.
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
                 
        .PARAMETER ResultSize
        The number of results to return. Defaults to 10000.
 
        .EXAMPLE
        Get-NectarExtCities
        Returns the first 1000 cities sorted alphabetically.
 
        .EXAMPLE
        Get-NectarExtCities -ResultSize 5000
        Returns the first 5000 cities sorted alphabetically.
         
        .EXAMPLE
        Get-NectarExtCities -SearchQuery Gu
        Returns all cities that contain the letters 'gu'
         
        .NOTES
        Version 1.0
    #>

    
    [Alias("gneci")]
    Param (
        [Parameter(Mandatory=$False)]
        [string]$SearchQuery,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize = 10000
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { 
                $TenantName = $Global:NectarTenantName 
            } ElseIf ($TenantName) {
                If ($TenantName -NotIn $Global:NectarTenantList) {
                    $TList = $Global:NectarTenantList -join ', '
                    Throw "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)"
                }
            }
            
            $URI = "https://$Global:NectarCloud/dapi/info/external/cities"
            Write-Verbose $URI

            $Params = @{ 'pageSize' = $ResultSize }    
            If ($SearchQuery) { $Params.Add('searchQuery',$SearchQuery) }
            If ($TenantName) { $Params.Add('Tenant',$TenantName) }
            
            $JSON = Invoke-RestMethod -Method GET -URI $URI -Headers $Global:NectarAuthHeader -Body $Params
            
            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            $JSON.elements
        }
        Catch {
            Write-Error "No results. Try specifying a less-restrictive filter. $($_.Exception.Message)"
            If ($PSCmdlet.MyInvocation.BoundParameters["ErrorAction"] -ne "SilentlyContinue") { Get-JSONErrorStream -JSONResponse $_ }
        }
    }
}


Function Get-NectarExtCountries {
    <#
        .SYNOPSIS
        Returns a list of 2-letter country codes found via IP geolocation
         
        .DESCRIPTION
        Most call records include the user's external IP address. Nectar DXP does a geo-IP lookup of the external IP address and stores the geographic information for later use. This command will return all the countries where Nectar DXP was able to successfully geolocate an external IP address.
 
        .PARAMETER SearchQuery
        The 2-letter country code to locate. Can be a partial match, and may return more than one entry.
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
                 
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
 
        .EXAMPLE
        Get-NectarExtCountries
        Returns all country codes sorted alphabetically.
         
        .EXAMPLE
        Get-NectarExtCountries -SearchQuery US
        Returns all country codes that contain the letters 'US'
         
        .NOTES
        Version 1.0
    #>

    
    [Alias("gneco")]
    Param (
        [Parameter(Mandatory=$False)]
        [string]$SearchQuery,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize = 1000
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { 
                $TenantName = $Global:NectarTenantName 
            } ElseIf ($TenantName) {
                If ($TenantName -NotIn $Global:NectarTenantList) {
                    $TList = $Global:NectarTenantList -join ', '
                    Throw "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)"
                }
            }
            
            $URI = "https://$Global:NectarCloud/dapi/info/external/countries"
            Write-Verbose $URI

            $Params = @{ 'pageSize' = $ResultSize }    
            If ($SearchQuery) { $Params.Add('searchQuery',$SearchQuery) }
            If ($TenantName) { $Params.Add('Tenant',$TenantName) }
            
            $JSON = Invoke-RestMethod -Method GET -URI $URI -Headers $Global:NectarAuthHeader -Body $Params
            
            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            $JSON.elements
        }
        Catch {
            Write-Error "No results. Try specifying a less-restrictive filter. $($_.Exception.Message)"
            If ($PSCmdlet.MyInvocation.BoundParameters["ErrorAction"] -ne "SilentlyContinue") { Get-JSONErrorStream -JSONResponse $_ }
        }
    }
}


Function Get-NectarExtISPs {
    <#
        .SYNOPSIS
        Returns a list of ISPs found via IP geolocation
         
        .DESCRIPTION
        Most call records include the user's external IP address. Nectar DXP does a geo-IP lookup of the external IP address and stores the geographic information for later use. This command will return all the ISPs where Nectar DXP was able to successfully geolocate an external IP address.
 
        .PARAMETER SearchQuery
        The name of the city to locate. Can be a partial match, and may return more than one entry.
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
                 
        .PARAMETER ResultSize
        The number of results to return. Defaults to 10000.
 
        .EXAMPLE
        Get-NectarExtISPs
        Returns the first 1000 ISPs sorted alphabetically.
 
        .EXAMPLE
        Get-NectarExtISPs -ResultSize 5000
        Returns the first 5000 ISPs sorted alphabetically.
         
        .EXAMPLE
        Get-NectarExtISPs -SearchQuery Be
        Returns all ISPs that contain the letters 'be'
         
        .NOTES
        Version 1.0
    #>

    
    [Alias("gneci")]
    Param (
        [Parameter(Mandatory=$False)]
        [string]$SearchQuery,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize = 10000
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { 
                $TenantName = $Global:NectarTenantName 
            } ElseIf ($TenantName) {
                If ($TenantName -NotIn $Global:NectarTenantList) {
                    $TList = $Global:NectarTenantList -join ', '
                    Throw "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)"
                }
            }
            
            $URI = "https://$Global:NectarCloud/dapi/info/external/isps"
            Write-Verbose $URI

            $Params = @{ 'pageSize' = $ResultSize }    
            If ($SearchQuery) { $Params.Add('searchQuery',$SearchQuery) }
            If ($TenantName) { $Params.Add('Tenant',$TenantName) }
            
            $JSON = Invoke-RestMethod -Method GET -URI $URI -Headers $Global:NectarAuthHeader -Body $Params
            
            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            $JSON.elements
        }
        Catch {
            Write-Error "No results. Try specifying a less-restrictive filter. $($_.Exception.Message)"
            If ($PSCmdlet.MyInvocation.BoundParameters["ErrorAction"] -ne "SilentlyContinue") { Get-JSONErrorStream -JSONResponse $_ }
        }
    }
}



#################################################################################################################################################
#################################################################################################################################################
## ##
## Endpoint Client Functions ##
## ##
#################################################################################################################################################
#################################################################################################################################################

#################################################################################################################################################
# #
# Controller Connection Functions #
# #