UMN-Azure.psm1

###

# Copyright 2017 University of Minnesota, Office of Information Technology

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with Foobar. If not, see <http://www.gnu.org/licenses/>.

#region Basic Azure VM build

function New-AzureRGTempateComplete
{
    <#
        .Synopsis
            Create Resource Group Templete to build VM
        
        .DESCRIPTION
            Create Resource Group Templete to build VM

        .PARAMETER resourceGroupName,
            Resource group VM is to belong to.

        .PARAMETER Location
            Azure zone, Central US and so forth.

         .PARAMETER vm
            Name of the VM

        .PARAMETER localUserName
            Name of the new local user

        .PARAMETER localPswd
            Local administrator password

        .PARAMETER storageAccountName
            Storage account name for VM storage

        .PARAMETER storageAccountKey
            Storage account key

        .PARAMETER vmSize
            Basic size of VM, such as A0

        .PARAMETER sku
            OS SKU -- such as 2012-R2-Datacenter

        .PARAMETER netSecGroup
            network security group - plan ahead

        .PARAMETER netSecRG
            network security resource group template

        .PARAMETER virtNetName
            Name of virtual network to be access on

        .PARAMETER vnetRG
            Name of virtual network gateway resource group
                   
        .PARAMETER scriptPath
            Path in Azure Storeage where Powershell file resides that will be run on vm at build time

        .PARAMETER scriptFile
            Name of File in scriptPath location that will be run on vm at build time

        .EXAMPLE
            New-AzureRGTempateComplete -resourceGroupName $resourceGroupName -Location $Location -vm $vm -vmSize $vmSize -storageAccountName $storageAccountName -netSecGroup $netSecGroup -netSecRG $netSecRG -virtNetName $virtNetName -vnetRG $vnetRG -localUserName $localUserName -localPswd $localPswd
        
        .EXAMPLE
            Another example of how to use this cmdlet
                    
        .Notes
            Author: Travis Sobeck
    #>


    [CmdletBinding()]
    Param
    (
        [ValidateNotNullOrEmpty()]
        [string]$resourceGroupName,

        [ValidateSet("eastus", "eastus2", "westus","centralus")]
        [string]$Location,

        [ValidateNotNullOrEmpty()]
        [string]$vm,

        [ValidateNotNullOrEmpty()]
        [string]$localUserName,

        [ValidateNotNullOrEmpty()]
        [string]$localPswd,

        [ValidateNotNullOrEmpty()]
        [string]$storageAccountName,
        
        [ValidateNotNullOrEmpty()]
        [string]$storageAccountKey,

        [string]$vmSize,

        [string]$sku,

        [string]$netSecGroup,

        [string]$netSecRG,

        [string]$virtNetName,

        [string]$vnetRG,
        
        [string]$scriptPath,

        [string]$scriptFile

    )

    $virtNetObject = Get-AzureRmVirtualNetwork -ResourceGroupName $vnetRG -Name $virtNetName
    $ipPrefix = $virtNetObject.AddressSpace.AddressPrefixes
    $subnets = $virtNetObject.Subnets
    foreach ($subnet in $subnets){
        if ($subnet.Name -eq 'default'){$subnetPrifix = $subnet.AddressPrefix;$subnetRef = $subnet.Id}
    }
    if ($subnetPrifix -eq $null){throw "Failed to find default subnet"}

    $netSecGroupID = (Get-AzureRmNetworkSecurityGroup -Name $netSecGroup -ResourceGroupName $netSecRG).Id
    $netIntName = $vm + "-nic-1"# + (Get-Random -Minimum 100 -Maximum 999)
    $pubIPName = $vm + "-PubIP"# + (Get-Random -Minimum 100 -Maximum 999)
    $pubDNSName = $vm# + "umn"
    $diskUri = "https://$storageAccountName" + ".blob.core.windows.net/vhds/$vm" + (Get-Random -Minimum 10000 -Maximum 99999) + '.vhd'

    $vmTemplate = @{type = "Microsoft.Compute/virtualMachines";name = $vm;apiVersion = "2015-06-15";location = $location;dependsOn = @("Microsoft.Network/networkInterfaces/$netIntName");
        properties = @{osProfile = @{computerName = $vm;adminUsername = $localUserName;adminPassword = $localPswd;windowsConfiguration = @{provisionVmAgent = 'true'}};
            hardwareProfile = @{vmSize = $vmSize};
            networkProfile = @{networkInterfaces = @(@{id="[resourceId('Microsoft.Network/networkInterfaces', '$netIntName')]"})};
            storageProfile = @{imageReference = @{publisher = "MicrosoftWindowsServer"; offer = "WindowsServer"; sku = $sku; version = "latest"};
                osDisk = @{name = $vm;vhd = @{uri = $diskUri};createOption = "fromImage"};
                dataDisks = @()}
        }
    }
    if ($scriptFile -and $scriptPath){
        $customScript = @{type = "Microsoft.Compute/virtualMachines/extensions";name = "$vm/BuildScript";apiVersion = "2016-03-30";location = $location;dependsOn = @("Microsoft.Compute/virtualMachines/$vm");
                            properties = @{publisher = "Microsoft.Compute";type = "CustomScriptExtension";typeHandlerVersion = "1.8";autoUpgradeMinorVersion = "true";
                                settings = @{fileUris = @(($scriptPath+$scriptFile));commandToExecute = "powershell.exe -ExecutionPolicy Unrestricted -File $scriptFile"};
                                protectedSettings = @{storageAccountName = $storageAccountName;storageAccountKey = $storageAccountKey}
                            }
                        }
    }
    $netInterfaceTemplate = @{type = "Microsoft.Network/networkInterfaces";name = $netIntName;apiVersion = "2015-06-15";location = $location;dependsOn = @("Microsoft.Network/publicIpAddresses/$pubIPName");
       properties = @{primary = $true;
        ipConfigurations = @(@{
            name = "ipconfig1";properties = @{subnet = @{id = $subnetRef};privateIPAllocationMethod = "Dynamic";publicIpAddress = @{id = "[resourceId('$resourceGroupName','Microsoft.Network/publicIpAddresses', '$pubIPName')]"}}
        });
        networkSecurityGroup = @{id = $netSecGroupID}
       }
    }

    $pubIpTemplate = @{type = "Microsoft.Network/publicIpAddresses";name = $pubIPName;apiVersion = "2015-06-15";location = $location;
       properties = @{publicIpAllocationMethod = "Dynamic";dnsSettings = @{domainNameLabel = $pubDNSName}} 
    }

    @{'$schema'='http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#';contentVersion="1.0.0.0";
        parameters = @{};
        variables = @{};
        resources = @($vmTemplate;$customScript;$netInterfaceTemplate;$pubIpTemplate);
        outputs = @{};
    } | ConvertTo-Json -Depth 7 | Out-File -FilePath .\$vm.json -Force 

    Test-AzureRmResourceGroupDeployment -ResourceGroupName $resourceGroupName -TemplateFile .\$vm.json -Mode Incremental -Verbose

}

## Build VM
function New-AzureVMcomplete
{
    <#
        .Synopsis
            Build VM
        
        .DESCRIPTION
            Build a VM along with required resources

        .PARAMETER resourceGroupName,
            Resource group VM is to belong to.

        .PARAMETER Location
            Azure zone, Central US and so forth.

         .PARAMETER vm
            Name of the VM

        .PARAMETER localUserName
            Name of the new local user

        .PARAMETER localPswd
            Local administrator password

        .PARAMETER storageAccountName
            Storage account name for VM storage

        .PARAMETER storageAccountKey
            Storage account key

        .PARAMETER vmSize
            Basic size of VM, such as A0

        .PARAMETER sku
            OS SKU -- such as 2012-R2-Datacenter

        .PARAMETER netSecGroup
            network security group - plan ahead

        .PARAMETER netSecRG
            network security resource group template

        .PARAMETER virtNetName
            Name of virtual network to be access on

        .PARAMETER vnetRG
            Name of virtual network gateway resource group
                
        .EXAMPLE
            $result = New-AzureVMcomplete -ResourceGroupName "VPN-GW" -Location eastus -vmname "mynewtest" -VMSize Basic_A0

        .Notes
            Author: Travis Sobeck
    #>

    [CmdletBinding()]
    Param
    (
        [ValidateNotNullOrEmpty()]
        [string]$resourceGroupName,

        [ValidateSet("eastus", "eastus2", "westus","centralus")]
        [string]$Location,

        [ValidateNotNullOrEmpty()]
        [string]$vm,

        [ValidateNotNullOrEmpty()]
        [string]$localUserName,

        [ValidateNotNullOrEmpty()]
        [string]$localPswd,

        [ValidateNotNullOrEmpty()]
        [string]$storageAccountName,
        
        [ValidateNotNullOrEmpty()]
        [string]$storageAccountKey,

        [string]$vmSize,

        [string]$sku,

        [string]$netSecGroup,

        [string]$netSecRG,

        [string]$virtNetName,

        [string]$vnetRG        

    )

    New-AzureRGTempateComplete -resourceGroupName $resourceGroupName -Location $Location -vm $vm -vmSize $vmSize -storageAccountName $storageAccountName -netSecGroup $netSecGroup -netSecRG $netSecRG -virtNetName $virtNetName -vnetRG $vnetRG -localUserName $localUserName -localPswd $localPswd
    New-AzureRmResourceGroupDeployment -ResourceGroupName $resourceGroupName -TemplateFile .\$vm.json
    Remove-Item -Path .\$vm.json -Force
}

function Remove-AzureVMcomplete
{
    <#
        .Synopsis
            Delete VM
        
        .DESCRIPTION
            Delete a VM and its NIC/PublicIP/osDisk
        
        .PARAMETER resourceGroupName
            The name of the resource group the VM belongs to

        .PARAMETER vm
            The name of the VM

        .PARAMETER storageRGname
            The resource group name of the storage account

        .EXAMPLE
            $result = Remove-AzureVMcomplete -ResourceGroupName "VPN-GW" -vm "mynewtest"
            
        .Notes
            Author: Travis Sobeck
    #>

    [CmdletBinding()]
    Param
    (
        [ValidateNotNullOrEmpty()]
        [string]$ResourceGroupName,

        [ValidateNotNullOrEmpty()]
        [string]$vm,

        [ValidateNotNullOrEmpty()]
        [string]$storageRGname = 'RG_Template'
    )

    $vmObj = Get-AzureRmVM -ResourceGroupName $ResourceGroupName -Name $vm
    #### should probably stop vm
    $null = Stop-AzureRmVM -ResourceGroupName $ResourceGroupName -Name $vm -Force # Status : Succeeded
    ## get NetworkInterface info
    $netID = $vmObj.NetworkInterfaceIDs[0]
    $net = Get-AzureRmResource -ResourceId $netID
    $netIpConfigID = $net.Properties.ipConfigurations[0].id
    ## get Public IP info
    $pubIP = Get-AzureRmPublicIpAddress -ResourceGroupName $ResourceGroupName
    $pubIpName = ($pubIP | Where-Object {$_.IpConfiguration[0].Id -eq $netIpConfigID}).Name

    ### Deletion order is very important
    ## remove vm
    $null = Remove-AzureRmVM -ResourceGroupName $ResourceGroupName -Name $vm -Force ## Status = Succeeded

    ## remove network Interface
    $null = Remove-AzureRmNetworkInterface -ResourceGroupName $resourceGroupName -Name $net.Name -Force

    ## remove public IP
    if ($pubIpName -ne $null){$null = Remove-AzureRmPublicIpAddress -ResourceGroupName $resourceGroupName -Name $pubIpName -Force}
    else{write-host "No Public IP"}

    ## Delete Disk
    [array]$diskArray = ($vmObj.StorageProfile.OsDisk.vhd.Uri).Split('/')
    $container = $diskArray[-2] ## check this equals vhds
    $diskName = $diskArray[-1] ## check this equals $vm<rand>.vhd
    $StorageAccountName = ($diskArray[-3]).Split('.')[0] # do a Find-AzureRmResource -ResourceGroupNameContains $resourceGroupName -ResourceType 'Microsoft.Storage/storageAccounts' make sure its in there
    $StorageAccountKey = (Get-AzureRmStorageAccountKey -ResourceGroupName $storageRGname -Name $StorageAccountName)[0].Value
    $ctx = New-AzureStorageContext -StorageAccountName $StorageAccountName -StorageAccountKey $StorageAccountKey
    $null = Remove-AzureStorageBlob -Blob $diskName -Container $container -Context $ctx

    ## clean out of AD
    Remove-ADComputer -Identity $vm -Confirm:$false
}

#endregion

#region Azure Log Analytics

function Get-AzureLogAnalytics
{
    <#
        .Synopsis
            Query Azure Log Analytics
        
        .DESCRIPTION
            Requires having identity set in Azure AD to allow access to Log Analytics API, and an Azure AD Application registered to get an API OAuth token from.
        
        .PARAMETER workspaceID
            The workspaceID reference for this API is the subscription which has the Log Analytics account.

        .PARAMETER accessToken
            An OAuth accessToken. See Get-AzureOAuthTokenUser as a possible source.

        .PARAMETER query
            A valid Log Analytics query. Example = 'AzureDiagnostics | where ResultType == "Failed" | where RunbookName_s == "Name of runbook" |where TimeGenerated > ago(1h)'

        .EXAMPLE
            $result = Get-AzureLogAnalytics -workspaceID <Subscription ID> -accessToken $accessToken -query $query
               
        .Notes
            Author: Kyle Weeks
    #>


    param 
    (
        [Parameter(Mandatory=$true)]
        [string] $workspaceID,

        [Parameter(Mandatory=$true)]
        [string] $accessToken,

        [Parameter(Mandatory=$true)]
        [string] $query
    )
    
    Begin
    {
        $contentType = 'application/json'
        $uri = "https://api.loganalytics.io/v1/workspaces/"+$workspaceID+"/query"
        $header = @{"Authorization"="Bearer $accessToken"}
        $body = @{"query"=$query} |ConvertTo-Json
    }

    Process
    {
        $response = Invoke-RestMethod -Method Post -Uri $uri -Body $body -Headers $header -ContentType $contentType
    }

    End
    {
        return $response
    }
}

function Get-AzureLogAnalyticsSignature
{
    <#
        .Synopsis
            Create signature for posting to Log Analytics
        
        .DESCRIPTION
            Create signature for posting to Log Analytics
        
        .PARAMETER data
            JSON formatted data to be posted to Log Analytics

        .PARAMETER primaryKey
            The Primary Workspace KEY for your workspace.

        .PARAMETER workspaceID
            Workspace ID associated to your Log Analytics space.

        .EXAMPLE
            $signature = Get-AzureLogAnalyticsSignature -date $json -primaryKey $key -workspaceID $workspaceID
               
        .Notes
            Author: Kyle Weeks
    #>


    param 
    (
        [Parameter(Mandatory=$true)]
        [string] $data,

        [Parameter(Mandatory=$true)]
        [string] $primaryKey,

        [Parameter(Mandatory=$true)]
        [string] $workspaceID
    )
    
    Begin
    {
        $method = "POST"
        $contentType = "application/json"
        $api = "/api/logs"        
    }

    Process
    {
        $xMSDate = "x-ms-date:" + ([DateTime]::UtcNow.ToString("r"))
        $contentLength = $data.Length
        $stringToSign = $method + "`n" + $contentLength + "`n" + $contentType + "`n" + $xMSDate + "`n" + $api
        $stringBytes = [Text.Encoding]::UTF8.GetBytes($stringToSign)
        $primaryKeyBytes = [Convert]::FromBase64String($primaryKey)
        
        $hmac256 = New-Object System.Security.Cryptography.HMACSHA256
        $hmac256.Key = $primaryKeyBytes
        $hash = $hmac256.ComputeHash($stringBytes)
        $hashBase64 = [Convert]::ToBase64String($hash)
        $signature = 'SharedKey {0}:{1}' -f $workspaceID,$hashBase64
    }

    End
    {
        return $signature
    }
}


function New-AzureLogAnalyticsData
{
    <#
        .Synopsis
            For posting data to Log Analytics.
        
        .DESCRIPTION
            The post portion of pushing data to Log Analytics. Requires a signed signature.

        .PARAMETER data
            JSON Formatted data to be sent to Log Analytics custom log.
        
        .PARAMETER logtype
            The custom log name for indexing. Will have _CL appended to it automatically by Log Analytics (Custom Log)

        .PARAMETER primaryKey
            The Primary Workspace KEY for your workspace.

        .PARAMETER workspaceID
            Workspace ID associated to your Log Analytics space.

        .EXAMPLE
            New-AzureLogAnalyticsData -data $data -logType $logType -primaryKey $primaryKey -workspaceID $workspaceID
               
        .Notes
            Author: Kyle Weeks
    #>


    param 
    (
        [Parameter(Mandatory=$true)]
        [string] $data,
        
        [Parameter(Mandatory=$true)]
        [string] $logType,
 
        [Parameter(Mandatory=$true)]
        [string] $primaryKey,       

        [Parameter(Mandatory=$true)]
        [string] $workspaceID
    )
    
    Begin
    {
        $method = "POST"
        $contentType = "application/json"
        $api = "/api/logs"        
        $uri = "https://" + $workspaceID + ".ods.opinsights.azure.com" + $api + "?api-version=2016-04-01"
        $date = [DateTime]::UtcNow.ToString("r")
        $signature = Get-AzureLogAnalyticsSignature -data $data -primaryKey $primaryKey -workspaceID $workspaceID
        $headers = @{"Authorization" = $signature;"Log-Type" = $logType;"x-ms-date" = $date;"time-generated-field"=$date}
    }

    Process
    {
        $response = Invoke-WebRequest -Uri $uri -Method $method -ContentType $contentType -Headers $headers -Body $data -UseBasicParsing      
    }

    End
    {
        return $response
    }
}


#endregion

#region Azure Graph API
## A starter point for the graph API
## Based on https://developer.microsoft.com/en-us/graph/graph-explorer
## oData Filtering/paging is supported. See https://msdn.microsoft.com/en-us/library/azure/ad/graph/howto/azure-ad-graph-api-supported-queries-filters-and-paging-options
## Example: $uri = "https://graph.microsoft.com/v1.0/users?" + '$filter' + "=userPrincipalName eq '$userPrincipalName'"

function Get-AzureGraphUsers
{
    <#
        .Synopsis
            Query Azure Graph API for basic user details
        
        .DESCRIPTION
            Requires having identity set in Azure AD to allow access to Graph API, and an Azure AD Application registered to get an API OAuth token from.

        .PARAMETER accessToken
            An OAuth accessToken. See Get-AzureOAuthTokenUser as a possible source.

        .PARAMETER userPrincipalNames
            A valid user userPrincipalName

        .PARAMETER query
            Optional to query specified information in relation to the user object.

        .EXAMPLE
            $result = Get-AzureGraphUsers -accessToken $accessToken -userPrincipalName 'jemina@somedomain.onmicrosoft.com' -query
               
        .Notes
            Author: Kyle Weeks
    #>


    param 
    (
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        [array] $userPrincipalNames,

        [Parameter(Mandatory=$true)]
        [string] $accessToken,

        [string]$query
    )
    
    Begin
    {
        $header = @{"Authorization"="Bearer $accessToken"}
        $response = @{}
    }

    Process
    {
        Foreach ($PSItem in $userPrincipalNames)
            {
                $results = $null
                $uri = "https://graph.microsoft.com/v1.0/users/$PSItem/$query"
                $results = Invoke-RestMethod -Method Get -Uri $uri -Headers $header
                $response.Add($PSItem,$results)
            }
    }

    End
    {
        return $response
    }
}

Function Get-AzureGraphObject
{

    <#
        .Synopsis
            Query Azure Graph API for object details
        
        .DESCRIPTION
            Use the $top oData filter to query objects in bulk using paging.

        .PARAMETER accessToken
            An OAuth accessToken. See Get-AzureOAuthTokenUser as a possible source.

        .PARAMETER apiVersion
            Some of the API versions in Graph are 'beta' - default to 1.0

        .PARAMETER batchSize
            Used to determine how many records to return per page. Microsoft Graph behaviors are per api...
            
        .PARAMETER objectType
            The object type to query.
            Paging with the $top filter is supported for all /users, but the $top filter is rejected.

        .EXAMPLE
            $results = Get-AzureGraphObject -accessToken $accessToken -objectType ''

        .EXAMPLE
            $results = Get-AzureGraphObject -accessToken $accessToken -apiVersion 'Beta' -batchSize 500 -objectType ''

        .Notes
            Author: Kyle Weeks
    #>


    param
        (
        [Parameter(Mandatory=$true)]
        [string] $accessToken,
        
        [Parameter(Mandatory=$false)]
        [string]$apiVersion,
        
        [Parameter(Mandatory=$false)]
        [int]$batchSize = '200',

        [Parameter(Mandatory=$true)]
        [string]$objectType
        )
  
    Begin
    {
        if(-not $apiVersion) 
            {$apiVersion='v1.0'}

        $header = @{"Authorization"="Bearer $accessToken"}
        $uri = "https://graph.microsoft.com/$apiVersion/$objectType`?top eq $batchSize"
        If($objectType -eq 'users')
            {
                $uri = "https://graph.microsoft.com/$apiVersion/$objectType"
            }

        ## Testing
        $i=0
    }

    Process
    {
        $results= @()
        do
        {
            $return = $null
            $return = Invoke-RestMethod -Method Get -Uri $uri -Headers $header
            $uri = $return.'@odata.nextlink'
            $results = $results + $return.value
            $i++
            write-host "Pass number $i"

        }
        until ($uri -eq $null)
    }

    End
    {
        return $results
    }

}

function Get-AzureOneDriveID 
{
<#
    .Synopsis
        Gets One Drive ID by User
    
    .DESCRIPTION
        Gets One Drive ID by User
    
    .PARAMETER accessToken
        oAuth Access token with API permissions allowed for One Drive on the https://graph.microsoft.com resource.

    .PARAMETER apiVersion
        Defaults to 1.0. Can set for beta or other as they allow.

    .PARAMETER userPrincipalName
        User Principal Name of the user's one drive.

    .EXAMPLE
        Get-AzureOneDriveID -accessToken $accessToken -userPrincipalName 'moon@domain.edu'
            
    .Notes
        Author: Kyle Weeks
#>
  

    param
        (
        [Parameter(Mandatory=$true)]
        [string]$accessToken,

        [string]$apiVersion = 'v1.0',

        [Parameter(Mandatory=$true)]
        [string]$userPrincipalName
        )

    Begin
    {
        $header = @{"Authorization"="Bearer $accessToken"}
    }
    Process
    {    
        $uri = "https://graph.microsoft.com/$apiVersion/users/$userPrincipalName/drives"
        $return = Invoke-RestMethod -Method Get -Uri $uri -Headers $header
    }

    End
    {
        return $return.value.Id
    }
}
function Get-AzureOneDriveFiles
{
<#
    .Synopsis
        Function to query One Drive for files
    
    .DESCRIPTION
        Needed in order to upload large files to One Drive via the Graph API.
    
    .PARAMETER accessToken
        oAuth Access token with API permissions allowed for One Drive on the https://graph.microsoft.com resource.

    .PARAMETER apiVersion
        Defaults to 1.0. Can set for beta or other as they allow.

    .PARAMETER driveID
        The OneDrive ID of the O365 User. See Get-AzureOneDriveID.

    .PARAMETER itemIDs
        An array of file/folder item IDs to be downloaded. See Get-AzureOneDriveRootContent as a starting place.

    .PARAMETER outPutPath
        Local path to store the files.

    .PARAMETER rootCreated
        A switch for when looping through from the root of a one drive to gather the entire one drive.

    .PARAMETER userPrincipalName
        The Azure AD UserPrincipalName of the OneDrive account owner.

    .EXAMPLE
        Get-AzureOneDriveFiles -accessToken $accessToken -driveID $driveID -itemIDs $arrayOfItemIds -outPutPath c:\temp -rootCreated $False -userPrincipalName 'moon@domain.edu'
            
    .Notes
        Author: Kyle Weeks
#>
    
    param 
        (
        [Parameter(Mandatory=$true)]
        [string]$accessToken,

        [string]$apiVersion = 'V1.0',

        [Parameter(Mandatory=$true)]
        [string]$driveID,
        
        [Parameter(Mandatory=$true)]
        [array]$itemIDs,

        [Parameter(Mandatory=$true)]
        [string]$outPutPath,

        [string]$rootCreated = 'needed',

        [Parameter(Mandatory=$true)]
        [string]$userPrincipalName
        )

    Begin
    {
        $user = ($userPrincipalName -split ("@"))[0]
        $header = @{"Authorization"="Bearer $accessToken"}

        # Check and create output directory
        If ($outPutPath -notmatch '.+?\\$') {$outPutPath += '\'}
        Try {Get-ChildItem -path $outputPath\$user -ErrorAction stop}
        Catch {New-Item -ItemType directory -Path $outPutPath\$user}

        # Get first folder structure of root:/ and create if needed.
        If($rootCreated -eq 'needed')
            {
                $itemIDs | ForEach-Object {
                    $child = $_
                    $return = Get-AzureOneDriveItem -accessToken $accessToken -driveID $driveID -itemID $child
                    
                    # Folder / File Loop
                    if ($return.folder){
                        $parent = $return.parentReference.path -replace ("/drives/$driveID/root:","$outPutPath\$user")
                        $parent = $parent -replace ("/","\")
                        New-Item -ItemType directory -Path ($parent + '\' + $return.name) -Force
                        }
                    if ($return.file){
                        $fileName = $return.name
                        If ($outPutPath -match '.+?\\$') {$outPutPath = $outPutPath.Substring(0,$outPutPath.Length-1)}
                        $filePath = $return.parentReference.path -replace ("/drives/$driveID/root:","$outPutPath\$user")
                        $filePath = $filePath -replace ("/","\")
                        $outfile = $filePath + '\' + $fileName
                        $download = $return.'@microsoft.graph.downloadUrl'
                        Invoke-WebRequest -Method Get -Uri $download -OutFile $outfile
                        }   
                    if ($return.package.type)
                        {
                            $type = $return.package.type
                            $location = $return.parentReference
                            Write-Host "$type found at location $location. Unable to download for user $user"
                        }
                }        
            }

        }


    Process{
        Foreach ($PSItem in $itemIDs){    
                # Get Children of Item
                $uri = "https://graph.microsoft.com/$apiVersion/drives/$driveID/items/$PSItem"+"?expand=children(select=id,name)"
                $return = Invoke-RestMethod -Method Get -Uri $uri -Headers $header 
                $children = $return.children

                # Process each item
                $children | ForEach-Object{
                        $child = $_.id
                        $return = Get-AzureOneDriveItem -accessToken $accessToken -driveID $driveID -itemID $child
                        
                        # Folder / File Loop
                        if ($return.folder){
                            $parent = $return.parentReference.path -replace ("/drives/$driveID/root:","$outPutPath\$user")
                            $parent = $parent -replace ("/","\")
                            New-Item -ItemType Directory -Path ($parent + '\' + $return.name) -Force
                            Try {
                                $newArray = New-Object System.Collections.ArrayList($null)
                                $return | foreach-object {$null = $newArray.Add($_.id)} 
                                Start-Sleep -Seconds 1
                                Get-AzureOneDriveFiles -accessToken $accessToken -driveID $driveID -itemIDs $newArray -user $user -outPutPath $outPutPath -rootCreated 'done'
                            }
                            Catch{}                  
                            }
                        if ($return.file){
                                $fileName = $return.name
                                If ($outPutPath -match '.+?\\$') {$outPutPath = $outPutPath.Substring(0,$outPutPath.Length-1)}
                                $filePath = $return.parentReference.path -replace ("/drives/$driveID/root:","$outPutPath\$user")
                                $filePath = $filePath -replace ("/","\")
                                $outfile = $filePath + '\' + $fileName
                                $download = $return.'@microsoft.graph.downloadUrl'
                                Invoke-WebRequest -Method Get -Uri $download -OutFile $outfile
                            }
                        if ($return.package.type)
                        {
                            $type = $return.package.type
                            $location = $return.parentReference
                            Write-Host "$type found at location $location. Unable to download for user $user"
                        }       
                    }
            }
        }
    end{}
}

function Get-AzureOneDriveItem
{
<#
    .Synopsis
        Gets One Drive Item by ID
    
    .DESCRIPTION
        Gets One Drive Item by ID
    
    .PARAMETER accessToken
        oAuth Access token with API permissions allowed for One Drive on the https://graph.microsoft.com resource.

    .PARAMETER apiVersion
        Defaults to 1.0. Can set for beta or other as they allow.

    .PARAMETER driveID
        The OneDrive ID of the O365 User. See Get-AzureOneDriveID.

    .PARAMETER itemID
        The itemID of the folder/file.

    .EXAMPLE
        Get-AzureOneDriveItem -accessToken $accessToken -driveID $driveID -itemID $itemID
            
    .Notes
        Author: Kyle Weeks
#>
  
    param
        (
        [Parameter(Mandatory=$true)]
        [string]$accessToken,

        [string]$apiVersion = 'v1.0',

        [Parameter(Mandatory=$true)]
        [string]$driveID,

        [Parameter(Mandatory=$true)]
        [string]$itemID
        )
    Begin{}
    Process
        {
            $header = @{"Authorization"="Bearer $accessToken"}
            $uri = "https://graph.microsoft.com/$apiVersion/drives/$driveID/items/$itemID"
            $return = Invoke-RestMethod -Method Get -Uri $uri -Headers $header
        }
    End{return $return}
}

function Get-AzureOneDriveRootContent 
{
<#
    .Synopsis
        Gets the One Drive Root Content
    
    .DESCRIPTION
        Will get all IDs of folders and files at the root of a one Drive with Child item info.
    
    .PARAMETER accessToken
        oAuth Access token with API permissions allowed for One Drive on the https://graph.microsoft.com resource.

    .PARAMETER apiVersion
        Defaults to 1.0. Can set for beta or other as they allow.

    .PARAMETER driveID
        The drive ID to be queried.

    .EXAMPLE
        Get-AzureOneDriveRootContent -accessToken $accessToken -driveID $driveID
            
    .Notes
        Author: Kyle Weeks
#>
  

    param 
        (
        [Parameter(Mandatory=$true)]
        [string]$accessToken,

        [string]$apiVersion = 'V1.0',

        [Parameter(Mandatory=$true)]
        [string]$driveID
        )

    Begin
    {
        $header = @{"Authorization"="Bearer $accessToken"}
    }

    Process
    {
        $uri = "https://graph.microsoft.com/$apiVersion/drives/$driveID/root?expand=children(select=id,name,type,property)"
        $return = Invoke-RestMethod -Method Get -Uri $uri -Headers $header
    }
    End
    {
        return $return.children
    }
}

function New-OneDriveFolder
{
<#
    .Synopsis
        Creates a new folder
    
    .DESCRIPTION
        Provide a item ID of parent folder or create new folder at root of OneDrive
    
    .PARAMETER accessToken
        oAuth Access token with API permissions allowed for One Drive on the https://graph.microsoft.com resource.

    .PARAMETER apiVersion
        Defaults to 1.0. Can set for beta or other as they allow.

    .PARAMETER folderName
        Name of the new folder

    .PARAMETER parentID
        Item of the parent folder

    .PARAMETER root
        Boolean switch. If true - no parent ID is needed, and will create folder in root of One Drive.

    .PARAMETER userPrincipalName
        UserPrincipalName of the OneDrive account owner.

    .EXAMPLE
        New-OneDriveFolder -accessToken $accessToken -folderName 'New Folder' -root $true -userPrincipalName 'moon@domain.edu'

    .EXAMPLE
        New-OneDriveFolder -accessToken $accessToken -folderName 'New Folder' -parentID $parentID -userPrincipalName 'moon@domain.edu'
            
    .Notes
        Author: Kyle Weeks
#>
  

    param 
        (
        [Parameter(Mandatory=$true)]
        [string]$accessToken,

        [string]$apiVersion = 'V1.0',

        [Parameter(Mandatory=$true)]
        [string]$folderName,

        [string]$parentID,

        [boolean]$root = $false,

        [Parameter(Mandatory=$true)]
        [string]$userPrincipalName
        )
    Begin
    {
        $body = @{folder=@{"@odata.type"="microsoft.graph.folder"};name="$folderName"} |ConvertTo-Json
        $header = @{"Authorization"="Bearer $accessToken"}
        If ($root -eq $false)
        {
            $uri = "https://graph.microsoft.com/$apiVersion/users/$userPrincipalName/drive/items/$parentID/children"
        }
        else 
        {
            $uri = "https://graph.microsoft.com/$apiVersion/users/$userPrincipalName/drive/root/children"   
        }
        
        
    }
    Process
    {
        $return = Invoke-RestMethod -Method Post -Uri $uri -Headers $header -Body $body -ContentType 'application/json'
    }
    End
    {
        return $return
    }

}

function New-OneDriveLargeFileUpload 
{
<#
    .Synopsis
        Upload large files to OneDrive
    
    .DESCRIPTION
        Will break down a large file into chunks for upload to OneDrive for Business account. Requires prep work for administrative control.
    
    .PARAMETER chunkSize
        The byte chunk size to break the file into. Has to be a multiple of 327680 or OneDrive API will reject.

    .PARAMETER localFilePath
        Path to the local file to be uploaded. Include the file name with extension.

    .PARAMETER uploadURL
        The upload URL provided from the upload session request. See New-AzureOneDriveLargeFileSession call to retrieve.

    .EXAMPLE
        New-OneDriveLargeFileUpload -localFilePath c:\temp\aVeryLargeFile.vhd -uploadURL $uploadURL
            
    .Notes
        Author: Kyle Weeks
#>

    param
        (
        [int]$chunkSize=4915200,
        
        [Parameter(Mandatory=$true)]
        [string]$LocalFilePath,

        [Parameter(Mandatory=$true)]
        [string]$uploadURL
        )
    Begin
    {    
        $reader = [System.IO.File]::OpenRead($LocalFilePath)
        $fileLength = $reader.Length
        $buffer = New-Object Byte[] $chunkSize
        $moreChunks = $true
        $byteCount = 0
    }
    Process
    {    
        while($moreChunks)
        {
            ## Test for end of file
            If (($reader.Position + $buffer.Length) -gt $fileLength)
                {
                    $bits = ($fileLength - $reader.Position)
                    $buffer = New-Object Byte[] $bits
                    $bytesRead = $reader.Read($buffer, 0, $bits)
                    $moreChunks = $false
                }
            Else {$bytesRead = $reader.Read($buffer, 0, $buffer.Length)} 

            $output = $buffer
            $contentLength = $bytesread
            $uploadRange = ($reader.Position -1)
            $contentRange = "$bytecount"+"-"+$uploadRange+"/$fileLength"
            $headerUpload = @{
                    "Content-Length"=$contentLength;
                    "Content-Range"="bytes $contentRange"
                    }
            $return = Invoke-RestMethod -Method Put -Uri $uploadURL -Headers $headerUpload -Body $output
            $byteCount =  $byteCount + $chunkSize
            $return.nextExpectedRanges

        }
    }
    End
    {
        $reader.Close()
        return $return
    }
}
    
function New-AzureOneDriveLargeFileSession
{
<#
    .Synopsis
        Generates a One Drive large file upload session.
    
    .DESCRIPTION
        Needed in order to upload large files to One Drive via the Graph API.
    
    .PARAMETER accessToken
        oAuth Access token with API permissions allowed for One Drive on the https://graph.microsoft.com resource.

    .PARAMETER apiVersion
        Defaults to 1.0. Can set for beta or other as they allow.

    .PARAMETER driveID
        The OneDrive ID of the O365 User. See Get-AzureOneDriveID.

    .PARAMETER OneDriveFilePath
        The One Drive folder path with file name. "New Folder/Microsoft.jpg"

    .EXAMPLE
        New-AzureOneDriveLargeFileSession -accessToken $accessToken -driveID $driveID -oneDriveFilePath $oneDriveFilePath
            
    .Notes
        Author: Kyle Weeks
#>
    
    param
        (
        [Parameter(Mandatory=$true)]    
        [string]$accessToken,

        [string]$apiVersion = "v1.0",

        [Parameter(Mandatory=$true)]
        [string]$driveID,

        [Parameter(Mandatory=$true)]
        [string]$OneDriveFilePath
        )
    Begin
    {
        $header = @{"Authorization"="Bearer $accessToken"}
        $method = 'POST'
        $uri = "https://graph.microsoft.com/$apiVersion/drives/$driveID/root:/$OneDriveFilePath"+":/createUploadSession"
    }
    Process
    {
        $response = Invoke-RestMethod -Method $method -Uri $uri -Headers $header
        $uploadURL = $response.uploadurl

    }
    End
    {
        return $uploadURL
    }
}

#endregion


#region Azure Marketplace billing

function Get-AzureMarketplaceCharges
{
    <#
        .Synopsis
            Get azure marketplace usage
        
        .DESCRIPTION
            For getting marketplace usage data.
        
        .PARAMETER key
            API key gathered from the EA portal for use with billing API.

        .PARAMETER enrollment
            Your Enterprise Enrollment number. Available form the EA portal.

        .PARAMETER billingPeriodID
            An optional parameter to specify that you wish to get from the following year and month. Format YYYYMM
        
        .PARAMETER startDate
            Start date time of the query - ####-##-## year, month, day = 2017-01-28
        
        .PARAMETER endDate
            End date time of the query - ####-##-## year, month, day = 2017-01-28
        
        .EXAMPLE
            $result = Get-AzureMarketplaceCharges -key 'apiKeyFromEAPortal' -enrollment 'EAEnrollmentNumber'
        
        .EXAMPLE
            $result = Get-AzureMarketplaceCharges -key 'apiKeyFromEAPortal' -enrollment 'EAEnrollmentNumber' -billingPeriodID '201701'
        
        .EXAMPLE
            $result = Get-AzureMarketplaceCharges -key 'apiKeyFromEAPortal' -enrollment 'EAEnrollmentNumber' -startDate '20170515' -endDate '20170602'
            
        .Notes
            Author: Kyle Weeks
    #>


    param 
    (
        [Parameter(Mandatory=$true)]
        [string] $enrollment,

        [Parameter(Mandatory=$true)]
        [string] $key,

        [string]$billingPeriodID,

        [ValidateLength(1,10)]
        [string]$startDate,

        [ValidateLength(1,10)]
        [string]$endDate
    )
    
    Begin{
    $header = @{"authorization"="bearer $key"}

    if ($billingPeriodID -eq '') {$uri = "https://consumption.azure.com/v2/enrollments/$enrollment/marketplacecharges"}
    Else {$uri = "https://consumption.azure.com/v2/enrollments/$enrollment/billingPeriods/$billingPeriodID/marketplacecharges"}

    if ($startDate -ne '') {if ($endDate -ne ''){$uri = "https://consumption.azure.com/v2/enrollments/$enrollment/marketplacechargesbycustomdate?startTime=$startDate&endTime=$endDate"}}
    }

    Process
    {
        $response = Invoke-WebRequest $uri -Headers $header -ErrorAction Stop
    }

    End
    {
        return $response
    }
}

#endregion

#region Azure OAuth authentication

function Get-AzureOAuthTokenService{
    <#
        .Synopsis
           Get Valid OAuth Token. The access token is good for an hour, and there is no refresh token.
        
        .DESCRIPTION
            This OAuth token is intended for use with CLI, automation, and service calls. No user interaction is required.
            Requires an application to be registered in Azure AD with appropriate API permissions configured.

        .PARAMETER tenantID
            Azure AD Directory ID/TenantID

        .PARAMETER clientid
            Azure AD Custom Application ID

        .PARAMETER accessKey
            Azure AD Custom Application access key


        .PARAMETER resource
            Resource to be interacted with. Example = https://api.loganalytics.io. Use the clientID here if authenticating a token to your own custom app.
        
        .PARAMATER scope
            An alternate to url resource to provide security scope to actions of an API such as with OneDrive.

        .EXAMPLE
            $tokenInfo = Get-AzureOAuthTokenService -tenantID 'Azure AD Tenant ID' -clientid 'Application ID' -accessKey 'Preset key for app' -resource 'MS API Resource'
        
        .Notes
            Author: Kyle Weeks
    #>

    [CmdletBinding()]
    [OutputType([array])]
    Param
    (
        [Parameter(Mandatory)]
        [string]$tenantID,

        [Parameter(Mandatory)]
        [string]$clientid,

        [Parameter(Mandatory)]
        [string]$accessKey,

        [string]$resource,

        [string]$scope = ''
    )     
     
    Begin
    {
        $uri = "https://login.microsoftonline.com/$tenantID/oauth2/token"
    }

    Process
    {
        If ($scope -ne '')
            {$body = @{grant_type="client_credentials";client_id=$clientid;client_secret=$accessKey;scope=$scope}}
        else 
            {$body = @{grant_type="client_credentials";client_id=$clientid;client_secret=$accessKey;resource=$resource}}        

        $response = Invoke-RestMethod -Method Post -Uri $uri -ContentType "application/x-www-form-urlencoded" -Body $body
        $accessToken = $response.access_token
    }

    End
    {
        return $accessToken
    }
}

function Get-AzureOAuthTokenUser{
    <#
        .Synopsis
           Get Valid OAuth Token. The access token is good for an hour, the refresh token is mostly permanent and can be used to get a new access token without having to re-authenticate
        
        .DESCRIPTION
            This is based on authenticating against a custom Web/API Application registered in Azure AD which has permissions to Azure AD, Azure Management, and other APIs.

        .PARAMETER tenantID
            Azure AD Directory ID/TenantID

        .PARAMETER clientid
            Azure AD Custom Application ID

        .PARAMETER accessKey
            Azure AD Custom Application access key

        .PARAMETER redirectUri
            For return stream of claims

        .PARAMETER resource
            Resource to be interacted with. Example = https://api.loganalytics.io, or https://graph.microsoft.com

        .PARAMETER refreshtoken
            Supply a refresh token to get a new valid token for use after expiring

        .PARAMETER prompt
            Define if your app login should prompt the user for consent in the Azure portal on login. none = will never request and rely on SSO (web apps)
                    
        .EXAMPLE
            $tokenInfo = Get-AzureOAuthTokenUser -tenantID 'Azure AD Tenant ID' -clientid 'Application ID' -accessKey 'Preset key for app' -redirectUri 'https redirect uri of app' -resource 'MS API Resource'
        
        .EXAMPLE
            $tokenInfo = Get-AzureOAuthTokenUser -tenantID 'Azure AD Tenant ID' -clientid 'Application ID' -accessKey 'Preset key for app' -redirectUri 'https redirect uri of app' -resource 'MS API Resource' -refreshtoken 'your refresh token from a previous call'

        .Notes
            Author: Kyle Weeks
    #>

    [CmdletBinding()]
    [OutputType([array])]
    Param
    (
        [Parameter(Mandatory)]
        [string]$tenantID,

        [Parameter(Mandatory)]
        [string]$clientid,

        [Parameter(Mandatory)]
        [string]$accessKey,

        [Parameter(Mandatory)]
        [string]$redirectUri,

        $resource,

        $scope = '',

        [ValidateSet('login','none','consent')]
        [string]$prompt = "consent",

        [string]$refreshtoken
        )     
     
    Begin
    {
        # Build Azure REST Endpoints
        $baseURI = "https://login.microsoftonline.com/$tenantID"
        $tokenEndpoint = $baseURI + "/oauth2/token"
        $authorizeEndpoint = $baseURI + "/oauth2/authorize"
    }

    Process
    {
        If (!$refreshtoken){

            # Get a claim code which is used to get a token
            $responseType = 'code'
            $grantType = "authorization_code"

            # Construct the claim authorization endpoint
            If ($scope -ne ''){
                $uri = $authorizeEndpoint+"?client_id=$clientid&response_type=$responseType&scope=$scope&redirect_uri=$redirectUri&prompt=$prompt"
            }
            else {
                $uri = $authorizeEndpoint+"?client_id=$clientid&response_type=$responseType&resource=$resource&redirect_uri=$redirectUri&prompt=$prompt"
            }
            # OAuth is generally used interactive for users... not core friendly.
            ## Popup a new IE window, log in, authorize app as needed, and collect claim code
            $ie = New-Object -comObject InternetExplorer.Application
            $ie.visible = $true
            $null = $ie.navigate($uri)

            #Wait for user interaction in IE, manual approval
            do{Start-Sleep 1}until($ie.LocationURL -match 'code=([^&]*)')
            $null = $ie.LocationURL -match 'code=([^&]*)'
            $authorizationCode = $matches[1]
            $null = $ie.Quit()

            # exchange the authorization code for tokens
            $uri = $tokenEndpoint
            If ($scope -ne ''){
                $body = @{client_id=$clientid;grant_type=$grantType;code=$authorizationCode;redirect_uri=$redirectUri;client_secret=$accessKey;resource=$resource}
            }
            Else {
                $body = @{client_id=$clientid;grant_type=$grantType;code=$authorizationCode;redirect_uri=$redirectUri;client_secret=$accessKey;resource=$resource}
            }
            
            $response = Invoke-RestMethod -Method Post -Uri $uri -ContentType "application/x-www-form-urlencoded" -Body $body

            $properties = @{
                accessToken = $response.access_token
                refreshToken = $response.refresh_token
                jwt = $response.id_token
        }
        }

        Else {
            ## Exchange a refresh token for new tokens
            $grantType = "refresh_token"
            $uri = $tokenEndpoint
            $body = @{client_id=$clientid;grant_type=$grantType;client_secret=$accessKey;refresh_token=$refreshtoken}
 
            $response = Invoke-RestMethod -Method Post -Uri $uri -ContentType "application/x-www-form-urlencoded" -Body $body
            $properties = @{
                accessToken = $response.access_token
                refreshToken = $response.refresh_token
                jwt = $response.id_token
        }

    }
    }

    End
    {
        return $properties
    }
}

#endregion


#region Azure Price Sheets

function Get-AzurePriceSheet {
    <#
        .Synopsis
            Get current price sheet from enterprise portal
        
        .DESCRIPTION
            Use this call to get a price sheet of resources from the EA portal using an API key
        
        .PARAMETER key
            API key gathered from the EA portal for use with billing API.

        .PARAMETER enrollment
            Your Enterprise Enrollment number. Available form the EA portal.

        .PARAMETER billingPeriodID
            An optional parameter to specify that you wish to get from the following year and month. Format YYYYMM

        .EXAMPLE
            $result = Get-AzurePriceSheet -key 'apiKeyFromEAPortal' -enrollment 'EAEnrollmentNumber'
        
        .EXAMPLE
            $result = Get-AzurePriceSheet -key 'apiKeyFromEAPortal' -enrollment 'EAEnrollmentNumber' -billingPeriodID '201701'

        .Notes
            Author: Kyle Weeks
    #>


    param
    (
        [Parameter(Mandatory=$true)]
        [string]$key,

        [Parameter(Mandatory=$true)]
        [string]$enrollment,

        [string]$billingPeriodID
    )

    Begin{
        $header = @{"authorization"="bearer $key"}
        If (-not $billingPeriodID){$uri = "https://consumption.azure.com/v2/enrollments/$enrollment/pricesheet"}
        Else {$uri = "https://consumption.azure.com/v2/enrollments/$enrollment/billingPeriods/$billingPeriodID/pricesheet"}
    }

    Process
    {
        $response = Invoke-WebRequest -Uri $uri -Headers $header
        $return = $response.Content |ConvertFrom-Json
    }

    End
    {
        return $return

    }
}


function Get-AzureBillingPeriods {
    <#
        .Synopsis
            Get current available billing periods, and some metadata around them.
    
        .DESCRIPTION
            A call to get available billing periods from your EA portal.
        
        .PARAMETER key
            API key gathered from the EA portal for use with billing API.

        .PARAMETER enrollment
            Your Enterprise Enrollment number. Available form the EA portal.

        .EXAMPLE
            $result = Get-AzureBillingPeriods -key 'apiKeyFromEAPortal' -enrollment 'EAEnrollmentNumber'
        
        .Notes
            Author: Kyle Weeks
    #>



    param 
    (
        [Parameter(Mandatory=$true)]
        [string]$key,

        [Parameter(Mandatory=$true)]
        [string]$enrollment
    )

    Begin{
        $header = @{"authorization"="bearer $key"}
        $uri = "https://consumption.azure.com/v2/enrollments/$enrollment/billingPeriods"
    }

    Process
    {
        $response = Invoke-WebRequest -Uri $uri -Headers $header
        $return = $response.Content |ConvertFrom-Json 
    }

    End
    {
        return $return
    }
}

#endregion

#region Azure Usage

function Get-AzureUsageJSON
{
    <#
        .Synopsis
            Get azure usage in a JSON format directly
        
        .DESCRIPTION
            There are other options for retrieving usage information. Directly as a CSV non-polling, polling, or JSON.
            If no billing period is included. The current month cycle will be retreived.

        .PARAMETER key
            API key gathered from the EA portal for use with billing API.

        .PARAMETER enrollment
            Your Enterprise Enrollment number. Available form the EA portal.

        .PARAMETER billingPeriodID
            An optional parameter to specify that you wish to get from the following year and month. Format YYYYMM

        .PARAMETER startDate
            Start date time of the query - ####-##-## year, month, day = 2017-01-28
        
        .PARAMETER endDate
            End date time of the query - ####-##-## year, month, day = 2017-01-28
       
        .EXAMPLE
            $result = Get-AzureUsageJSON -key 'apiKeyFromEAPortal' -enrollment 'EAEnrollmentNumber'
        
        .EXAMPLE
            $result = Get-AzureUsageJSON -key 'apiKeyFromEAPortal' -enrollment 'EAEnrollmentNumber' -billingPeriodID '201701'
        
        .EXAMPLE
            $result = Get-AzureUsageJSON -key 'apiKeyFromEAPortal' -enrollment 'EAEnrollmentNumber' -startDate '20170515' -endDate '20170602'

        .Notes
            Author: Kyle Weeks
    #>


    param 
    (
        [Parameter(Mandatory=$true)]
        [string] $enrollment,

        [Parameter(Mandatory=$true)]
        [string] $key,

        [string]$billingPeriod, ## year + month

        [ValidateLength(1,10)]
        [string]$startDate, ####-##-## year, month, day = 2017-01-28

        [ValidateLength(1,10)]
        [string]$endDate ####-##-## year, month, day = 2017-01-28
    )

    Begin{
        $header = @{"authorization"="bearer $key"}

        If ($billingPeriod -eq ''){$uri = "https://consumption.azure.com/v2/enrollments/$enrollment/usagedetails"}
        Else {$uri = "https://consumption.azure.com/v2/enrollments/$enrollment/billingPeriods/$billingPeriod/usagedetails"}

        if ($startDate -ne '') {if ($endDate -ne ''){$uri = "https://consumption.azure.com/v2/enrollments/$enrollment/usagedetailsbycustomdate?startTime=$startDate&endTime=$endDate"}}
    
    }

    Process
    {
        $usage = @()
        while ($uri -ne $null)
        {
            $response = Invoke-WebRequest $uri -Headers $header -ErrorAction Stop
            if ($response.StatusCode -eq 200) {
                $usage += ($response.Content | ConvertFrom-Json).Data

                # get next page link - loop for more data
                $uri = ($response.Content | ConvertFrom-Json).nextLink
            }
        }
    }

    End
    {
        return $usage
    }
}

function Get-AzureUsageCSV
{
    <#
        .Synopsis
            Get azure usage directly as a CSV (as if downloading from the web UI)
        
        .DESCRIPTION
            There are other options for retrieving usage information. Directly as a CSV non-polling, polling, or JSON.
        
        .PARAMETER key
            API key gathered from the EA portal for use with billing API.

        .PARAMETER enrollment
            Your Enterprise Enrollment number. Available form the EA portal.

        .PARAMETER billingPeriodID
            An optional parameter to specify that you wish to get from the following year and month. Format YYYYMM

        .PARAMETER outputDir
            A directory for outputing a CSV of collected data.
 
        .EXAMPLE
        $result = Get-AzureUsageCSV -key 'apiKeyFromEAPortal' -enrollment 'EAEnrollmentNumber' -billingPeriodID '201701' -outputDir 'c:\'

        .Notes
            Author: Kyle Weeks
    #>


    param 
    (
        [Parameter(Mandatory=$true)]
        [string] $enrollment,

        [Parameter(Mandatory=$true)]
        [string] $key,

        [Parameter(Mandatory=$true)]
        [string]$billingPeriod,  ## year+month

        [Parameter(Mandatory=$true)]
        [string]$outputDir
    )

    Begin{
        $header = @{"authorization"="bearer $key"}
        $uri = "https://consumption.azure.com/v2/enrollments/$enrollment/usagedetails/download?billingPeriod=$billingPeriod"
        If ($outputDir -notlike '*\') {$outputDir = $outputDir + '\'}
        $outfile = $outputDir+"AzureUsage-"+"$billingPeriod"+".csv"
    }

    Process
    {
        $response = Invoke-WebRequest $uri -Headers $header -OutFile $outfile |Out-Null
    }

    End
    {
        return $response
    }

}

function Get-AzureUsageCSVcustomDate
{
    <#
        .Synopsis
            Get azure usage in a CSV format directly - providing a custom date range
        
        .DESCRIPTION
            There are other options for retrieving usage information. Directly as a CSV non-polling, polling, or JSON.
            This method requests a custom data file be generated (up to 36 months of data). It is saved to a blob storage point.
            The function will poll that location until it is available, then output the csv.
        
        .PARAMETER key
            API key gathered from the EA portal for use with billing API.

        .PARAMETER enrollment
            Your Enterprise Enrollment number. Available form the EA portal.

        .PARAMETER billingPeriodID
            An optional parameter to specify that you wish to get from the following year and month. Format YYYYMM
        
        .PARAMETER startDate
            Start date time of the query - ####-##-## year, month, day = 2017-01-28
        
        .PARAMETER endDate
            End date time of the query - ####-##-## year, month, day = 2017-01-28
        
        .EXAMPLE
            $result = Get-AzureUsageCSV -key 'apiKeyFromEAPortal' -enrollment 'EAEnrollmentNumber' -startDate '20170515' -endDate '20170602' -outputDir 'c:\'

        .Notes
            Author: Kyle Weeks
    #>


    param 
    (
        [Parameter(Mandatory=$true)]
        [string] $enrollment,

        [Parameter(Mandatory=$true)]
        [string] $key,

        [Parameter(Mandatory=$true)]
        [string]$outputDir,

        [Parameter(Mandatory=$true)]
        [ValidateLength(1,10)]
        [string]$startDate, ####-##-## year, month, day = 2017-01-28

        [Parameter(Mandatory=$true)]
        [ValidateLength(1,10)]
        [string]$endDate ####-##-## year, month, day = 2017-01-28
    )

    Begin{
        $header = @{"authorization"="bearer $key"}
        $uri = "https://consumption.azure.com/v2/enrollments/$enrollment/usagedetails/submit?startTime=$startDate&endTime=$endDate"
        If ($outputDir -notlike '*\') {$outputDir = $outputDir + '\'}
        $outfile = $outputDir+"AzureUsage-"+"$startDate"+"_"+"$endDate"+".csv"
    }

    Process
    {
        $results = Invoke-WebRequest -Method Post $uri -Headers $header
        $temp = $results.Content |ConvertFrom-Json
        $status = ''

        while ($status -eq '')
            {
            $test = Invoke-WebRequest -Method get -Uri ($temp.reportUrl) -Headers $header
            $json = $test.Content |Convertfrom-Json    
            $status = $json.status
            }

        If ($status -eq '3')
            {
            $results = Invoke-WebRequest -Method post -Uri ($json.blobPath) -Headers $header -OutFile $outfile
            }
    }

    End
    {
        return $results
    }
}
 
#endregion
# SIG # Begin signature block
# MIIaxgYJKoZIhvcNAQcCoIIatzCCGrMCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUVAUSP8RvOuIy4BIw4pSs+gDG
# dpKgghW3MIIEmTCCA4GgAwIBAgIPFojwOSVeY45pFDkH5jMLMA0GCSqGSIb3DQEB
# BQUAMIGVMQswCQYDVQQGEwJVUzELMAkGA1UECBMCVVQxFzAVBgNVBAcTDlNhbHQg
# TGFrZSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxITAfBgNV
# BAsTGGh0dHA6Ly93d3cudXNlcnRydXN0LmNvbTEdMBsGA1UEAxMUVVROLVVTRVJG
# aXJzdC1PYmplY3QwHhcNMTUxMjMxMDAwMDAwWhcNMTkwNzA5MTg0MDM2WjCBhDEL
# MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE
# BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKjAoBgNVBAMT
# IUNPTU9ETyBTSEEtMSBUaW1lIFN0YW1waW5nIFNpZ25lcjCCASIwDQYJKoZIhvcN
# AQEBBQADggEPADCCAQoCggEBAOnpPd/XNwjJHjiyUlNCbSLxscQGBGue/YJ0UEN9
# xqC7H075AnEmse9D2IOMSPznD5d6muuc3qajDjscRBh1jnilF2n+SRik4rtcTv6O
# KlR6UPDV9syR55l51955lNeWM/4Og74iv2MWLKPdKBuvPavql9LxvwQQ5z1IRf0f
# aGXBf1mZacAiMQxibqdcZQEhsGPEIhgn7ub80gA9Ry6ouIZWXQTcExclbhzfRA8V
# zbfbpVd2Qm8AaIKZ0uPB3vCLlFdM7AiQIiHOIiuYDELmQpOUmJPv/QbZP7xbm1Q8
# ILHuatZHesWrgOkwmt7xpD9VTQoJNIp1KdJprZcPUL/4ygkCAwEAAaOB9DCB8TAf
# BgNVHSMEGDAWgBTa7WR0FJwUPKvdmam9WyhNizzJ2DAdBgNVHQ4EFgQUjmstM2v0
# M6eTsxOapeAK9xI1aogwDgYDVR0PAQH/BAQDAgbAMAwGA1UdEwEB/wQCMAAwFgYD
# VR0lAQH/BAwwCgYIKwYBBQUHAwgwQgYDVR0fBDswOTA3oDWgM4YxaHR0cDovL2Ny
# bC51c2VydHJ1c3QuY29tL1VUTi1VU0VSRmlyc3QtT2JqZWN0LmNybDA1BggrBgEF
# BQcBAQQpMCcwJQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLnVzZXJ0cnVzdC5jb20w
# DQYJKoZIhvcNAQEFBQADggEBALozJEBAjHzbWJ+zYJiy9cAx/usfblD2CuDk5oGt
# Joei3/2z2vRz8wD7KRuJGxU+22tSkyvErDmB1zxnV5o5NuAoCJrjOU+biQl/e8Vh
# f1mJMiUKaq4aPvCiJ6i2w7iH9xYESEE9XNjsn00gMQTZZaHtzWkHUxY93TYCCojr
# QOUGMAu4Fkvc77xVCf/GPhIudrPczkLv+XZX4bcKBUCYWJpdcRaTcYxlgepv84n3
# +3OttOe/2Y5vqgtPJfO44dXddZhogfiqwNGAwsTEOYnB9smebNd0+dmX+E/CmgrN
# Xo/4GengpZ/E8JIh5i15Jcki+cPwOoRXrToW9GOUEB1d0MYwggV3MIIEX6ADAgEC
# AhAT6ihwW/Ts7Qw2YwmAYUM2MA0GCSqGSIb3DQEBDAUAMG8xCzAJBgNVBAYTAlNF
# MRQwEgYDVQQKEwtBZGRUcnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJu
# YWwgVFRQIE5ldHdvcmsxIjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJv
# b3QwHhcNMDAwNTMwMTA0ODM4WhcNMjAwNTMwMTA0ODM4WjCBiDELMAkGA1UEBhMC
# VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4w
# HAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVz
# dCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA
# A4ICDwAwggIKAoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzO
# iZ/MPans9s/B3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwW
# IJAJPuLodMkYtJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YU
# VD+8M/5+bJz/Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1da
# t//O+T23LLb2VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+
# UzeQc0PzMsNT79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/z
# JSZrM233bkf6c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLa
# qUkL39iAigmTYo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxb
# gtNMs+1b/97lc6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9A
# qURE9JnnV4eeUB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+
# eLf8ZxXhyVeEHg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwID
# AQABo4H0MIHxMB8GA1UdIwQYMBaAFK29mHo0tCb3+sQmVO8DveAky1QaMB0GA1Ud
# DgQWBBRTeb9aqitKz1SA4dibwJ3ysgNmyzAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0T
# AQH/BAUwAwEB/zARBgNVHSAECjAIMAYGBFUdIAAwRAYDVR0fBD0wOzA5oDegNYYz
# aHR0cDovL2NybC51c2VydHJ1c3QuY29tL0FkZFRydXN0RXh0ZXJuYWxDQVJvb3Qu
# Y3JsMDUGCCsGAQUFBwEBBCkwJzAlBggrBgEFBQcwAYYZaHR0cDovL29jc3AudXNl
# cnRydXN0LmNvbTANBgkqhkiG9w0BAQwFAAOCAQEAk2X2N4OVD17Dghwf1nfnPIrA
# qgnw6Qsm8eDCanWhx3nJuVJgyCkSDvCtA9YJxHbf5aaBladG2oJXqZWSxbaPAyJs
# M3fBezIXbgfOWhRBOgUkG/YUBjuoJSQOu8wqdd25cEE/fNBjNiEHH0b/YKSR4We8
# 3h9+GRTJY2eR6mcHa7SPi8BuQ33DoYBssh68U4V93JChpLwt70ZyVzUFv7tGu25t
# N5m2/yOSkcZuQPiPKVbqX9VfFFOs8E9h6vcizKdWC+K4NB8m2XsZBWg/ujzUOAai
# 0+aPDuO0cW1AQsWEtECVK/RloEh59h2BY5adT3Xg+HzkjqnR8q2Ks4zHIc3C7zCC
# BawwggSUoAMCAQICEHJNXiAT1cKRQFXzfFSJVHEwDQYJKoZIhvcNAQELBQAwfDEL
# MAkGA1UEBhMCVVMxCzAJBgNVBAgTAk1JMRIwEAYDVQQHEwlBbm4gQXJib3IxEjAQ
# BgNVBAoTCUludGVybmV0MjERMA8GA1UECxMISW5Db21tb24xJTAjBgNVBAMTHElu
# Q29tbW9uIFJTQSBDb2RlIFNpZ25pbmcgQ0EwHhcNMTcxMjE0MDAwMDAwWhcNMjAx
# MjEzMjM1OTU5WjCByzELMAkGA1UEBhMCVVMxDjAMBgNVBBEMBTU1NDU1MRIwEAYD
# VQQIDAlNaW5uZXNvdGExFDASBgNVBAcMC01pbm5lYXBvbGlzMRgwFgYDVQQJDA8x
# MDAgVW5pb24gU3QgU0UxIDAeBgNVBAoMF1VuaXZlcnNpdHkgb2YgTWlubmVzb3Rh
# MSQwIgYDVQQLDBtDb21wdXRlciBhbmQgRGV2aWNlIFN1cHBvcnQxIDAeBgNVBAMM
# F1VuaXZlcnNpdHkgb2YgTWlubmVzb3RhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
# MIIBCgKCAQEAwk6kLE9u+tWv0JUkIJSn5pWfa09g6cqFLucCXomNj9NYj8t+JfPn
# a3gC6LHv3OQAUDHOoC5H+8N3ea7qVGYIiwPRHzXOGqG/tVaiU5s5hG3vBhfRX8W1
# /2g4/hpgeXUzrxYn/2c5SOGGy0MU1ZJyUSFEdsjXHEV7HXK4qmFGV9RJxtiLZH1q
# UldCglxcj7zw0QnUdG6oAxpwTCeVp057/WXbnIR8a0gPse+y/new5+CBUGTAvrw6
# K2BrJQVsdIIVn/q+BbcZxh9PpeZfTtsi6lgkvy0bUWtl5sSpd75+hvw4Sl3HAaWZ
# toWN7LPmbDbbVRO2Arv4doh4Chod4wJ5xQIDAQABo4IB2DCCAdQwHwYDVR0jBBgw
# FoAUrjUjF///Bj2cUOCMJGUzHnAQiKIwHQYDVR0OBBYEFF4LEhElVUvT8n5txOJS
# NAczooSAMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoG
# CCsGAQUFBwMDMBEGCWCGSAGG+EIBAQQEAwIEEDBmBgNVHSAEXzBdMFsGDCsGAQQB
# riMBBAMCATBLMEkGCCsGAQUFBwIBFj1odHRwczovL3d3dy5pbmNvbW1vbi5vcmcv
# Y2VydC9yZXBvc2l0b3J5L2Nwc19jb2RlX3NpZ25pbmcucGRmMEkGA1UdHwRCMEAw
# PqA8oDqGOGh0dHA6Ly9jcmwuaW5jb21tb24tcnNhLm9yZy9JbkNvbW1vblJTQUNv
# ZGVTaWduaW5nQ0EuY3JsMH4GCCsGAQUFBwEBBHIwcDBEBggrBgEFBQcwAoY4aHR0
# cDovL2NydC5pbmNvbW1vbi1yc2Eub3JnL0luQ29tbW9uUlNBQ29kZVNpZ25pbmdD
# QS5jcnQwKAYIKwYBBQUHMAGGHGh0dHA6Ly9vY3NwLmluY29tbW9uLXJzYS5vcmcw
# GQYDVR0RBBIwEIEOb2l0bXB0QHVtbi5lZHUwDQYJKoZIhvcNAQELBQADggEBAENR
# lesMKmBaZ0g68lttYEMtaPiz+DaNpOlXBs1gH66aghB1aP6iiRJcFVasGLUVFncd
# G1xbw503LTrBUc5PECMVDVF7KKCfHA1OeFV9vOWyvdVgbe3paDy1sj4CADO2D0gn
# xcGiZoFhEZiBkTvSsj4S3GXZEvoFHJxJLw2kvdLnzy0gH/b/b/yblwA1fKXw4loc
# UpDM6qTwM7SiKgkQ5W7/280EYu8BI6c8rpiJmqM1tZLcpswuavB00T52Y+ZZmz3t
# MMVgFHn9pFFltYr3s3bEek7I6pU8unISbiyQzxqhIUKaBi8hy8LgoY5UnGjX5jHs
# IvINzms+JX5Ity02sL0wggXrMIID06ADAgECAhBl4eLj1d5QRYXzJiSABeLUMA0G
# CSqGSIb3DQEBDQUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMKTmV3IEplcnNl
# eTEUMBIGA1UEBxMLSmVyc2V5IENpdHkxHjAcBgNVBAoTFVRoZSBVU0VSVFJVU1Qg
# TmV0d29yazEuMCwGA1UEAxMlVVNFUlRydXN0IFJTQSBDZXJ0aWZpY2F0aW9uIEF1
# dGhvcml0eTAeFw0xNDA5MTkwMDAwMDBaFw0yNDA5MTgyMzU5NTlaMHwxCzAJBgNV
# BAYTAlVTMQswCQYDVQQIEwJNSTESMBAGA1UEBxMJQW5uIEFyYm9yMRIwEAYDVQQK
# EwlJbnRlcm5ldDIxETAPBgNVBAsTCEluQ29tbW9uMSUwIwYDVQQDExxJbkNvbW1v
# biBSU0EgQ29kZSBTaWduaW5nIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
# CgKCAQEAwKAvix56u2p1rPg+3KO6OSLK86N25L99MCfmutOYMlYjXAaGlw2A6O2i
# gTXrC/Zefqk+aHP9ndRnec6q6mi3GdscdjpZh11emcehsriphHMMzKuHRhxqx+85
# Jb6n3dosNXA2HSIuIDvd4xwOPzSf5X3+VYBbBnyCV4RV8zj78gw2qblessWBRyN9
# EoGgwAEoPgP5OJejrQLyAmj91QGr9dVRTVDTFyJG5XMY4DrkN3dRyJ59UopPgNwm
# ucBMyvxR+hAJEXpXKnPE4CEqbMJUvRw+g/hbqSzx+tt4z9mJmm2j/w2nP35MViPW
# Cb7hpR2LB8W/499Yqu+kr4LLBfgKCQIDAQABo4IBWjCCAVYwHwYDVR0jBBgwFoAU
# U3m/WqorSs9UgOHYm8Cd8rIDZsswHQYDVR0OBBYEFK41Ixf//wY9nFDgjCRlMx5w
# EIiiMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMBMGA1UdJQQM
# MAoGCCsGAQUFBwMDMBEGA1UdIAQKMAgwBgYEVR0gADBQBgNVHR8ESTBHMEWgQ6BB
# hj9odHRwOi8vY3JsLnVzZXJ0cnVzdC5jb20vVVNFUlRydXN0UlNBQ2VydGlmaWNh
# dGlvbkF1dGhvcml0eS5jcmwwdgYIKwYBBQUHAQEEajBoMD8GCCsGAQUFBzAChjNo
# dHRwOi8vY3J0LnVzZXJ0cnVzdC5jb20vVVNFUlRydXN0UlNBQWRkVHJ1c3RDQS5j
# cnQwJQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLnVzZXJ0cnVzdC5jb20wDQYJKoZI
# hvcNAQENBQADggIBAEYstn9qTiVmvZxqpqrQnr0Prk41/PA4J8HHnQTJgjTbhuET
# 98GWjTBEE9I17Xn3V1yTphJXbat5l8EmZN/JXMvDNqJtkyOh26owAmvquMCF1pKi
# QWyuDDllxR9MECp6xF4wnH1Mcs4WeLOrQPy+C5kWE5gg/7K6c9G1VNwLkl/po9OR
# PljxKKeFhPg9+Ti3JzHIxW7LdyljffccWiuNFR51/BJHAZIqUDw3LsrdYWzgg4x0
# 6tgMvOEf0nITelpFTxqVvMtJhnOfZbpdXZQ5o1TspxfTEVOQAsp05HUNCXyhznlV
# Lr0JaNkM7edgk59zmdTbSGdMq8Ztuu6VyrivOlMSPWmay5MjvwTzuNorbwBv0DL+
# 7cyZBp7NYZou+DoGd1lFZN0jU5IsQKgm3+00pnnJ67crdFwfz/8bq3MhTiKOWEb0
# 4FT3OZVp+jzvaChHWLQ8gbCORgClaZq1H3aqI7JeRkWEEEp6Tv4WAVsr/i7LoXU7
# 2gOb8CAzPFqwI4Excdrxp0I4OXbECHlDqU4sTInqwlMwofmxeO4u94196qIqJQl+
# 8Sykl06VktqMux84Iw3ZQLH08J8LaJ+WDUycc4OjY61I7FGxCDkbSQf3npXeRFm0
# IBn8GiW+TRDk6J2XJFLWEtVZmhboFlBLoUlqHUCKu0QOhU/+AEOqnY98j2zRMYIE
# eTCCBHUCAQEwgZAwfDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAk1JMRIwEAYDVQQH
# EwlBbm4gQXJib3IxEjAQBgNVBAoTCUludGVybmV0MjERMA8GA1UECxMISW5Db21t
# b24xJTAjBgNVBAMTHEluQ29tbW9uIFJTQSBDb2RlIFNpZ25pbmcgQ0ECEHJNXiAT
# 1cKRQFXzfFSJVHEwCQYFKw4DAhoFAKB4MBgGCisGAQQBgjcCAQwxCjAIoAKAAKEC
# gAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwG
# CisGAQQBgjcCARUwIwYJKoZIhvcNAQkEMRYEFB7xQcI/ovBphBttaFkvc+9IEjid
# MA0GCSqGSIb3DQEBAQUABIIBAFaZMAy0msLam/mD9cFKvaMitGDmd23th+PCS7UP
# mXTi85WvPlHpInf7I8gY+itW0z3nlA6d5JCKA+4+FNu5CU1rJ+8TtiXMUeZgbhZU
# mnPH2k7eYg+etlHLouGZRJb62ggYZD6AjEetZcWCCl4waTec646PbIAuSYLC8lYF
# y63vyswfI28eJoEG/zOfez9ShZKm9+l5545owLmI+AONbJrwa7ZUvjcgLPLfE/xX
# P0X8F3jJ63nxl5RG8I8YOrAjLK2bShMTOzR7KO5yvn8CbbpDeecok/7DxoPvdtFs
# SrI6O9eBMrZT1+6W3f5zz41JkhVQzi7j2d1rSt2DS0TsLDmhggJDMIICPwYJKoZI
# hvcNAQkGMYICMDCCAiwCAQEwgakwgZUxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJV
# VDEXMBUGA1UEBxMOU2FsdCBMYWtlIENpdHkxHjAcBgNVBAoTFVRoZSBVU0VSVFJV
# U1QgTmV0d29yazEhMB8GA1UECxMYaHR0cDovL3d3dy51c2VydHJ1c3QuY29tMR0w
# GwYDVQQDExRVVE4tVVNFUkZpcnN0LU9iamVjdAIPFojwOSVeY45pFDkH5jMLMAkG
# BSsOAwIaBQCgXTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJ
# BTEPFw0xODA3MTcyMDM0MTZaMCMGCSqGSIb3DQEJBDEWBBTBcJ8SfV3QVIqNnPn4
# rdnhjbOZBjANBgkqhkiG9w0BAQEFAASCAQCpudQAJoYhQVMRSCjoEs1O44oIRp5m
# IT6AKjTcwI7rpKhgCkvrYnrPACQ1NDCCcyH6gEPVObWESXfy+e5IXK57Vb3GHZdJ
# EyUJs18ZaDOXSf2Pn83966fwM1qSeznky/wEZCQlZQga6PGaTLlE8qhL6Yrtguqq
# LxJtUvEvJdG4Fn1sNOpl3h6YLi4q9eENgB+l3lVv6y4f6A/C2rVLlpH6iuZeVml9
# Nkn31yDjZobItoYi8y0KkHgj1t5WIiXVeMssR7V80A33betIlICcxLj0uYW76j//
# gwhIE2zK0nlhoVXNpKKk9mGv241+RvYpZgiFiwkz/D0MDFRFB4njhSUJ
# SIG # End signature block