AzureRM.Bootstrapper.psm1

$RollUpModule = "AzureRM"
$PSProfileMapEndpoint = "https://azureprofile.azureedge.net/powershell/profilemap.json"
$script:BootStrapRepo = "BootStrap"
$RepoLocation = "https://www.powershellgallery.com/api/v2/"
$existingRepos = Get-PSRepository | Where-Object {$_.SourceLocation -eq $RepoLocation}
if ($null -eq $existingRepos)
{
  Register-PSRepository -Name $BootStrapRepo -SourceLocation $RepoLocation -PublishLocation $RepoLocation -ScriptSourceLocation $RepoLocation -ScriptPublishLocation $RepoLocation -InstallationPolicy Trusted -PackageManagementProvider NuGet
}
else
{
  $script:BootStrapRepo = $existingRepos[0].Name
}

# Is it Powershell Core edition?
$Script:IsCoreEdition = ($PSVersionTable.PSEdition -eq 'Core')

# Check if current user is Admin to decide on cache path
$script:IsAdmin = $false
if ((-not $Script:IsCoreEdition) -or ($IsWindows))
{
  If (([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator"))
  {
    $script:IsAdmin = $true
  }
}
else {
  # on Linux, tests run via sudo will generally report "root" for whoami
  if ( (whoami) -match "root" ) 
  {
    $script:IsAdmin = $true
  }
}

# Get profile cache path
function Get-ProfileCachePath
{
  if ((-not $Script:IsCoreEdition) -or ($IsWindows))
  {
    $ProfileCache = Join-Path -path $env:LOCALAPPDATA -childpath "Microsoft\AzurePowerShell\ProfileCache"
    if ($script:IsAdmin)
    {
      $ProfileCache = Join-Path -path $env:ProgramData -ChildPath "Microsoft\AzurePowerShell\ProfileCache"
    }
  }
  else {
    $ProfileCache = "$HOME/.config/Microsoft/AzurePowerShell/ProfileCache" 
  }

  # If profile cache directory does not exist, create one.
  if(-Not (Test-Path $ProfileCache))
  {
    New-Item -ItemType Directory -Force -Path $ProfileCache | Out-Null
  }

  return $ProfileCache
}

# Function to find the latest profile map from cache
function Get-LatestProfileMapPath
{
  $ProfileCache = Get-ProfileCachePath
  $ProfileMapPaths = Get-ChildItem $ProfileCache
  if ($null -eq $ProfileMapPaths)
  {
    return
  }

  $LargestNumber = Get-LargestNumber -ProfileCache $ProfileCache
  if ($null -eq $LargestNumber)
  {
    return
  }

  $LatestMapPath = $ProfileMapPaths | Where-Object { $_.Name.Startswith($LargestNumber.ToString() + '-') }
  return $LatestMapPath
}

# Function to get the largest number in profile cache profile map names: This helps to find the latest map
function Get-LargestNumber
{
  param($ProfileCache)
  
  $ProfileMapPaths = Get-ChildItem $ProfileCache
  $LargestNumber = $ProfileMapPaths | ForEach-Object { if($_.Name -match "\d+-") { $matches[0] -replace '-' } } | Measure-Object -Maximum 
  if ($null -ne $LargestNumber)
  {
    return $LargestNumber.Maximum
  }
}

# Find the latest ProfileMap
$script:LatestProfileMapPath = Get-LatestProfileMapPath

# Make Web-Call
function Get-AzureStorageBlob
{
  $retryCount = 0
  Do
  {
    $retryCount = $retryCount + 1
    try {
      $WebResponse =  Invoke-WebRequest -uri $PSProfileMapEndpoint -ErrorVariable RestError
      $Status = "success"
    }
    catch {    
      if ($retryCount -le 3)
      {
        Start-Sleep -Seconds 3
      }
      else {
        throw $_
      }
    }
  } 
  while ($Status -ne "success")
  return $WebResponse
}

# Get-Content with retry logic; to handle parallel requests
function RetryGetContent
{
  param([string]$FilePath)
  $retryCount = 0
  Do
  {
    $retryCount = $retryCount + 1
    try {
      $ProfileMap = Get-Content -Raw -Path $FilePath -ErrorAction stop | ConvertFrom-Json 
      $Status = "success"
    }
    catch {
      Start-Sleep -Seconds 3
    }
  } 
  while (($Status -ne "success") -and ($retryCount -lt 3))
  return $ProfileMap
}

# Get-ProfileMap from Azure Endpoint
function Get-AzureProfileMap
{
  Write-Verbose "Updating profiles"
  $ProfileCache = Get-ProfileCachePath

  # Get online profile data using Web request
  $WebResponse = Get-AzureStorageBlob  

  # Get ETag value for OnlineProfileMap
  $OnlineProfileMapETag = $WebResponse.Headers["ETag"]

  # If profilemap json exists, compare online Etag and cached Etag; if not different, don't replace cache.
  if (($null -ne $script:LatestProfileMapPath) -and ($script:LatestProfileMapPath -match "(\d+)-(.*.json)"))
  {
    [string]$ProfileMapETag = [System.IO.Path]::GetFileNameWithoutExtension($Matches[2])
    if (($ProfileMapETag -eq $OnlineProfileMapETag) -and (Test-Path $script:LatestProfileMapPath.FullName))
    {
      $ProfileMap = RetryGetContent -FilePath $script:LatestProfileMapPath.FullName
      if ($null -ne $ProfileMap)
      {
        return $ProfileMap
      }
    }
  }

  # If profilemap json doesn't exist, or if online ETag and cached ETag are different, cache online profile map
  $LargestNoFromCache = Get-LargestNumber -ProfileCache $ProfileCache 
  if ($null -eq $LargestNoFromCache)
  {
    $LargestNoFromCache = 0
  }
  
  $ChildPathName = ($LargestNoFromCache+1).ToString() + '-' + ($OnlineProfileMapETag) + ".json"
  $CacheFilePath = (Join-Path $ProfileCache -ChildPath $ChildPathName)
  $OnlineProfileMap = RetrieveProfileMap -WebResponse $WebResponse
  $OnlineProfileMap | ConvertTo-Json -Compress | Out-File -FilePath $CacheFilePath
   
  # Update $script:LatestProfileMapPath
  $script:LatestProfileMapPath = Get-ChildItem $ProfileCache | Where-Object { $_.FullName.equals($CacheFilePath)}
  return $OnlineProfileMap
}

# Helper to retrieve profile map from http response
function RetrieveProfileMap
{
  param($WebResponse)
  $OnlineProfileMap = ($WebResponse.Content -replace "`n|`r|`t") | ConvertFrom-Json
  return $OnlineProfileMap
}

# Get ProfileMap from Cache, online or embedded source
function Get-AzProfile
{
  [CmdletBinding()]
  param([Switch]$Update)

  $Update = $PSBoundParameters.Update
  # If Update is present, download ProfileMap from online source
  if ($Update.IsPresent)
  {
    return (Get-AzureProfileMap)
  }

  # Check the cache
  if(($null -ne $script:LatestProfileMapPath) -and (Test-Path $script:LatestProfileMapPath.FullName))
  {
    $ProfileMap = RetryGetContent -FilePath $script:LatestProfileMapPath.FullName
    if ($null -ne $ProfileMap)
    {
      return $ProfileMap
    }
  }
     
  # If cache doesn't exist, Check embedded source
  $defaults = [System.IO.Path]::GetDirectoryName($PSCommandPath)
  $ProfileMap = RetryGetContent -FilePath (Join-Path -Path $defaults -ChildPath "ProfileMap.json")
  if($null -eq $ProfileMap)
  {
    # Cache & Embedded source empty; Return error and stop
    throw [System.IO.FileNotFoundException] "Profile meta data does not exist. Use 'Get-AzureRmProfile -Update' to download from online source."
  }

  return $ProfileMap
}


# Lists the profiles available for install from gallery
function Get-ProfilesAvailable
{
  param([parameter(Mandatory = $true)] [PSCustomObject] $ProfileMap)
  $ProfileList = ""
  foreach ($Profile in $ProfileMap) 
  {
    foreach ($Module in ($Profile | get-member -MemberType NoteProperty).Name)
    {
      $ProfileList += "Profile: $Module`n"
      $ProfileList += "----------------`n"
      $ProfileList += ($Profile.$Module | Format-List | Out-String)
    } 
  }
  return $ProfileList
}

# Lists the profiles that are installed on the machine
function Get-ProfilesInstalled
{
  param([parameter(Mandatory = $true)] [PSCustomObject] $ProfileMap, [REF]$IncompleteProfiles)
  $result = @{}
  $AllProfiles = ($ProfileMap | Get-Member -MemberType NoteProperty).Name
  foreach ($key in $AllProfiles)
  {
    Write-Verbose "Checking if profile $key is installed"
    foreach ($module in ($ProfileMap.$key | Get-Member -MemberType NoteProperty).Name)
    {
      $ModulesList = (Get-Module -Name $Module -ListAvailable)
      $versionList = $ProfileMap.$key.$module
      foreach ($version in $versionList)
      {
        if ($null -ne ($ModulesList | Where-Object { $_.Version -eq $version}))
        {
          if ($result.ContainsKey($key))
          {
            $result[$key] += @{$module = $ProfileMap.$Key.$module}
          }
          else
          {
            $result.Add($key, @{$module = $ProfileMap.$Key.$module})
          }
        }
      }
    }

    # If not all the modules from a profile are installed, add it to $IncompleteProfiles
    if(($result.$key.Count -gt 0) -and ($result.$key.Count -ne ($ProfileMap.$key | Get-Member -MemberType NoteProperty).Count))
    {
      if ($result.$key.Contains($RollUpModule))
      {
        continue
      }
      
      $result.Remove($key)
      if ($null -ne $IncompleteProfiles) 
      {
        $IncompleteProfiles.Value += $key
      }
    }
  }
  return $result
}

# Function to remove Previous profile map
function Remove-ProfileMapFile
{
  [CmdletBinding()]
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    param([string]$ProfileMapPath)

  if (Test-Path -Path $ProfileMapPath)
  {
    RemoveWithRetry -Path $ProfileMapPath -Force
  }
}

# Remove-Item command with Retry
function RemoveWithRetry
{
  param ([string]$Path)

  $retries = 3
  $secondsDelay = 2
  $retrycount = 0
  $completedSuccessfully = $false

  while (-not $completedSuccessfully) 
  {
    try 
    {
      Remove-Item @PSBoundParameters -ErrorAction Stop
      $completedSuccessfully = $true
    } 
    catch 
    {
      if ($retrycount -ge $retries) 
      {
        throw
      } 
      else 
      {
        Start-Sleep $secondsDelay
        $retrycount++
      }
    }
  }
}

# Get profiles installed associated with the module version
function Test-ProfilesInstalled
{
  param([String]$Module, [String]$Profile, [PSObject]$PMap, [hashtable]$AllProfilesInstalled)

  # Profiles associated with the particular module version - installed?
  $profilesAssociated = @()
  $versionList = $PMap.$Profile.$Module
  foreach ($version in $versionList)
  {
    foreach ($profileInAllProfiles in $AllProfilesInstalled[$Module + $version])
    {
      $profilesAssociated += $profileInAllProfiles
    }
  }
  return $profilesAssociated
}

# Function to uninstall module
function Uninstall-ModuleHelper
{
  [CmdletBinding(SupportsShouldProcess = $true)] 
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidShouldContinueWithoutForce", "")]
  param([String]$Profile, $Module, [System.Version]$version, [Switch]$RemovePreviousVersions)

  $Remove = $PSBoundParameters.RemovePreviousVersions
  Do
  {
    $moduleInstalled = Get-Module -Name $Module -ListAvailable | Where-Object { $_.Version -eq $version} 
    if ($PSCmdlet.ShouldProcess("$module version $version", "Remove module")) 
    {
      if (($null -ne $moduleInstalled) -and ($Remove.IsPresent -or $PSCmdlet.ShouldContinue("Remove module $Module version $version", "Removing Modules for profile $Profile"))) 
      {
        Write-Verbose "Removing module from session"
        Remove-Module -Name $module -Force -ErrorAction "SilentlyContinue"
        try 
        {
          Write-Verbose "Uninstalling module $module version $version"
          Uninstall-Module -Name $module -RequiredVersion $version -Force -ErrorAction Stop
        }
        catch
        {
          Write-Error $_.Exception.Message
          break
        }
      }
      else {
        break
      }
    }
    else {
      break
    }
  }
  While($null -ne $moduleInstalled);
}

# Help function to uninstall a profile
function Uninstall-ProfileHelper
{
  [CmdletBinding()]
  param([PSObject]$PMap, [String]$Profile, [Switch]$Force)
  $modules = ($PMap.$Profile | Get-Member -MemberType NoteProperty).Name

  # Get-Profiles installed across all hashes. This is to avoid uninstalling modules that are part of other installed profiles
  $AllProfilesInstalled = Get-AllProfilesInstalled

  foreach ($module in $modules)
  {
    if ($Force.IsPresent)
    {
      Invoke-UninstallModule -PMap $PMap -Profile $Profile -Module $module -AllProfilesInstalled $AllProfilesInstalled -RemovePreviousVersions
    }
    else {
      Invoke-UninstallModule -PMap $PMap -Profile $Profile -Module $module -AllProfilesInstalled $AllProfilesInstalled 
    }
  }
}

# Checks if the module is part of other installed profiles. Calls Uninstall-ModuleHelper if not.
function Invoke-UninstallModule
{
  [CmdletBinding()]
  param([PSObject]$PMap, [String]$Profile, $Module, [hashtable]$AllProfilesInstalled, [Switch]$RemovePreviousVersions)

  # Check if the profiles associated with the module version are installed.
  Write-Verbose "Checking module dependency to any other profile installed"
  $profilesAssociated = Test-ProfilesInstalled -Module $Module -Profile $Profile -PMap $PMap -AllProfilesInstalled $AllProfilesInstalled
      
  # If more than one profile is installed for the same version of the module, do not uninstall
  if ($profilesAssociated.Count -gt 1) 
  {
    return
  }

  $PSBoundParameters.Remove('AllProfilesInstalled') | Out-Null
  $PSBoundParameters.Remove('PMap') | Out-Null

  # Uninstall module
  $versionList = $PMap.$Profile.$module
  foreach ($version in $versionList)
  {
    Uninstall-ModuleHelper -version $version @PSBoundParameters
  }  
}

# Helps to uninstall previous versions of modules in the profile
function Remove-PreviousVersion
{
  [CmdletBinding()]
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")]
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
  param([PSObject]$PreviousMap, [PSObject]$LatestMap, [hashtable]$AllProfilesInstalled, [String]$Profile, [Array]$Module, [Switch]$RemovePreviousVersions)

  $Remove = $PSBoundParameters.RemovePreviousVersions
  $Modules = $PSBoundParameters.Module

  $PreviousProfiles = ($PreviousMap | Get-Member -MemberType NoteProperty).Name
  
  Write-Verbose "Checking if previous versions of modules are installed"
  # If the profile was not in $PreviousProfiles, return
  if($Profile -notin $PreviousProfiles)
  {
    return
  }

  if ($null -eq $Modules)
  {
    $Modules = ($PreviousMap.$Profile | Get-Member -MemberType NoteProperty).Name
  }

  foreach ($module in $Modules)
  {
    # If the latest version is same as the previous version, do not uninstall.
    if (($PreviousMap.$Profile.$module | Out-String) -eq ($LatestMap.$Profile.$module | Out-String))
    {
      continue
    }
      
    # Is that module installed? If not skip
    $versionList = $PreviousMap.$Profile.$module
    foreach ($version in $versionList)
    {
      if ($null -eq (Get-Module -Name $Module -ListAvailable | Where-Object { $_.Version -eq $version} ))
      {
        continue
      }

      Write-Verbose "Previous versions of modules were found. Trying to uninstall..."
      # Modules are different. Uninstall previous version.
      if ($Remove.IsPresent)
      {
        Invoke-UninstallModule -PMap $PreviousMap -Profile $Profile -Module $module -AllProfilesInstalled $AllProfilesInstalled -RemovePreviousVersions
      }
      else {
        Invoke-UninstallModule -PMap $PreviousMap -Profile $Profile -Module $module -AllProfilesInstalled $AllProfilesInstalled         
      }
    }
      
    # Uninstall removes module from session; import latest version again
    $versions = $LatestMap.$Profile.$module
    $versionEnum = $versions.GetEnumerator()
    $toss = $versionEnum.MoveNext()
    $version = $versionEnum.Current
    Import-Module $Module -RequiredVersion $version -Global
  }
}

# Gets profiles installed from all the profilemaps from cache
function Get-AllProfilesInstalled
{
  $AllProfilesInstalled = @{}
  $ProfileCache = Get-ProfileCachePath
  $ProfileMapHashes = Get-ChildItem $ProfileCache 
  foreach ($ProfileMapHash in $ProfileMapHashes)
  {
    $ProfileMap = RetryGetContent -FilePath (Join-Path $ProfileCache $ProfileMapHash.Name) 
    $profilesInstalled = (Get-ProfilesInstalled -ProfileMap $ProfileMap)
    foreach ($Profile in $profilesInstalled.Keys)
    {
      foreach ($Module in ($ProfileMap.$Profile | Get-Member -MemberType NoteProperty).Name)
      {
        $versionList = $ProfileMap.$Profile.$Module
        foreach ($version in $versionList)
        {
          if ($AllProfilesInstalled.ContainsKey(($Module + $version)))
          {
            if ($Profile -notin $AllProfilesInstalled[($Module + $version)])
            {
              $AllProfilesInstalled[($Module + $version)] += $Profile
            }
          }
          else {
            $AllProfilesInstalled.Add(($Module + $version), @($Profile))
          }
        }
      }
    }
  }
  return $AllProfilesInstalled
}

# Helps to remove-previous versions of the update-profile and clean up cache, if none of the old hash profiles are installed
function Update-ProfileHelper
{
  [CmdletBinding()]
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
  param([String]$Profile, [Array]$Module, [Switch]$RemovePreviousVersions)

  Write-Verbose "Attempting to clean up previous versions"
  # Get all the hash files (ProfileMaps) from cache
  $ProfileCache = Get-ProfileCachePath
  $ProfileMapHashes = Get-ChildItem $ProfileCache

  $LatestProfileMap = RetryGetContent -FilePath $script:LatestProfileMapPath.FullName 

  # Get-Profiles installed across all hashes.
  $AllProfilesInstalled = Get-AllProfilesInstalled

  foreach ($ProfileMapHash in $ProfileMapHashes)
  {
    # Set flag to delete hash
    $deleteHash = $true
    
    # Do not process the latest hash; we don't want to remove the latest hash
    if ($ProfileMapHash.Name -eq $script:LatestProfileMapPath.Name)
    {
      continue
    }

    # Compare previous & latest map for the update profile. Uninstall previous if they are different
    $previousProfileMap = RetryGetContent -FilePath (Join-Path $profilecache $ProfileMapHash) 
    Remove-PreviousVersion -PreviousMap $previousProfileMap -LatestMap $LatestProfileMap -AllProfilesInstalled $AllProfilesInstalled @PSBoundParameters

    # If the previous map has profiles installed, do not delete it.
    $profilesInstalled = (Get-ProfilesInstalled -ProfileMap $previousProfileMap)
    foreach ($PreviousProfile in $profilesInstalled.Keys)
    {
      # Map can be deleted if the only profile installed is the updated one.
      if ($PreviousProfile -eq $Profile)
      {
        continue
      }
      else {
        $deleteHash = $false
      }
    }

    # If none were installed, remove the hash
    if ($deleteHash -ne $false)
    {
      Write-Verbose "Cleaning up cache"
      Remove-ProfileMapFile -ProfileMapPath (Join-Path $profilecache $ProfileMapHash)
    }
  }
}

# If cmdlets were installed at a different scope, warn users of the potential conflict
function Find-PotentialConflict
{
  [CmdletBinding(SupportsShouldProcess = $true)] 
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")]
  param([string]$Module, [switch]$Force)
  
  Write-Verbose "Checking if there is a potential conflict for module installation"
  $availableModules = Get-Module $Module -ListAvailable
  $IsPotentialConflict = $false

  Write-Information "Modules installed: $availableModules"

  if ($null -eq $availableModules)
  {
    return $false
  }

  # If Admin, check CurrentUser Module folder path and vice versa
  if ($script:IsAdmin)
  {
    $availableModules | ForEach-Object { if (($null -ne $_.Path) -and $_.Path.Contains($env:HOMEPATH)) { $IsPotentialConflict = $true } }
  }
  else {
    $availableModules | ForEach-Object { if (($null -ne $_.Path) -and $_.Path.Contains($env:ProgramFiles)) { $IsPotentialConflict = $true } }
  }

  if ($IsPotentialConflict)
  {
    if (($Force.IsPresent) -or ($PSCmdlet.ShouldContinue(`
      "The Cmdlets from module $Module are already present on this device. Proceeding with the installation might cause conflicts. Would you like to continue?", "Detected $Module cmdlets")))
    {
      return $false
    }
    else 
    {
      return $true
    }
  }
  return $false
}

# Helper function to invoke install-module
function Invoke-InstallModule
{
  param($module, $version, $scope)
  $installCmd = Get-Command Install-Module
  if($installCmd.Parameters.ContainsKey('AllowClobber'))
  {
    if (-not $scope)
    {
      Install-Module $Module -RequiredVersion $version -AllowClobber -Repository $script:BootStrapRepo
    }
    else {
      Install-Module $Module -RequiredVersion $version -Scope $scope -AllowClobber -Repository $script:BootStrapRepo
    }
  }
  else {
     if (-not $scope)
    {
      Install-Module $Module -RequiredVersion $version -Force -Repository $script:BootStrapRepo
    }
    else {
      Install-Module $Module -RequiredVersion $version -Scope $scope -Force -Repository $script:BootStrapRepo
    }
  }
}

# Add Scope parameter to the cmdlet
function Add-ScopeParam
{
  param([System.Management.Automation.RuntimeDefinedParameterDictionary]$params, [string]$set = "__AllParameterSets")
  $Keys = @('CurrentUser', 'AllUsers')
  $scopeValid = New-Object -Type System.Management.Automation.ValidateSetAttribute($Keys)
  $scopeAttribute = New-Object -Type System.Management.Automation.ParameterAttribute
  $scopeAttribute.ParameterSetName = 
  $scopeAttribute.Mandatory = $false
  $scopeAttribute.Position = 1
  $scopeCollection = New-object -Type System.Collections.ObjectModel.Collection[System.Attribute]
  $scopeCollection.Add($scopeValid)
  $scopeCollection.Add($scopeAttribute)
  $scopeParam = New-Object -Type System.Management.Automation.RuntimeDefinedParameter("Scope", [string], $scopeCollection)
  $params.Add("Scope", $scopeParam)
}

# Add the profile parameter to the cmdlet
function Add-ProfileParam
{
  param([System.Management.Automation.RuntimeDefinedParameterDictionary]$params, [string]$set = "__AllParameterSets")
  $ProfileMap = (Get-AzProfile)
  $AllProfiles = ($ProfileMap | Get-Member -MemberType NoteProperty).Name
  $profileAttribute = New-Object -Type System.Management.Automation.ParameterAttribute
  $profileAttribute.ParameterSetName = $set
  $profileAttribute.Mandatory = $true
  $profileAttribute.Position = 0
  $validateProfileAttribute = New-Object -Type System.Management.Automation.ValidateSetAttribute($AllProfiles)
  $profileCollection = New-object -Type System.Collections.ObjectModel.Collection[System.Attribute]
  $profileCollection.Add($profileAttribute)
  $profileCollection.Add($validateProfileAttribute)
  $profileParam = New-Object -Type System.Management.Automation.RuntimeDefinedParameter("Profile", [string], $profileCollection)
  $params.Add("Profile", $profileParam)
}

function Add-ForceParam
{
  param([System.Management.Automation.RuntimeDefinedParameterDictionary]$params, [string]$set = "__AllParameterSets")
  Add-SwitchParam $params "Force" $set
}

function Add-RemoveParam
{
  param([System.Management.Automation.RuntimeDefinedParameterDictionary]$params, [string]$set = "__AllParameterSets")
  $name = "RemovePreviousVersions" 
  $newAttribute = New-Object -Type System.Management.Automation.ParameterAttribute
  $newAttribute.ParameterSetName = $set
  $newAttribute.Mandatory = $false
  $newCollection = New-object -Type System.Collections.ObjectModel.Collection[System.Attribute]
  $newCollection.Add($newAttribute)
  $newParam = New-Object -Type System.Management.Automation.RuntimeDefinedParameter($name, [switch], $newCollection)
  $params.Add($name, [Alias("r")]$newParam)
}

function Add-SwitchParam
{
  param([System.Management.Automation.RuntimeDefinedParameterDictionary]$params, [string]$name, [string] $set = "__AllParameterSets")
  $newAttribute = New-Object -Type System.Management.Automation.ParameterAttribute
  $newAttribute.ParameterSetName = $set
  $newAttribute.Mandatory = $false
  $newCollection = New-object -Type System.Collections.ObjectModel.Collection[System.Attribute]
  $newCollection.Add($newAttribute)
  $newParam = New-Object -Type System.Management.Automation.RuntimeDefinedParameter($name, [switch], $newCollection)
  $params.Add($name, $newParam)
}

function Add-ModuleParam
{
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")]
  param([System.Management.Automation.RuntimeDefinedParameterDictionary]$params, [string]$name, [string] $set = "__AllParameterSets")
  $ProfileMap = (Get-AzProfile)
  $Profiles = ($ProfileMap | Get-Member -MemberType NoteProperty).Name
  if ($Profiles.Count -gt 1)
  {
    $enum = $Profiles.GetEnumerator()
    $toss = $enum.MoveNext()
    $Current = $enum.Current
    $Keys = ($($ProfileMap.$Current) | Get-Member -MemberType NoteProperty).Name
  }
  else {
    $Keys = ($($ProfileMap.$Profiles[0]) | Get-Member -MemberType NoteProperty).Name
  }
  $moduleValid = New-Object -Type System.Management.Automation.ValidateSetAttribute($Keys)
  $AllowNullAttribute = New-Object -Type System.Management.Automation.AllowNullAttribute
  $AllowEmptyStringAttribute = New-Object System.Management.Automation.AllowEmptyStringAttribute
  $moduleAttribute = New-Object -Type System.Management.Automation.ParameterAttribute
  $moduleAttribute.ParameterSetName = 
  $moduleAttribute.Mandatory = $false
  $moduleAttribute.Position = 1
  $moduleCollection = New-object -Type System.Collections.ObjectModel.Collection[System.Attribute]
  $moduleCollection.Add($moduleValid)
  $moduleCollection.Add($moduleAttribute)
  $moduleCollection.Add($AllowNullAttribute)
  $moduleCollection.Add($AllowEmptyStringAttribute)
  $moduleParam = New-Object -Type System.Management.Automation.RuntimeDefinedParameter("Module", [array], $moduleCollection)
  $params.Add("Module", $moduleParam)
}

<#
.ExternalHelp help\AzureRM.Bootstrapper-help.xml
#>

function Get-AzureRmModule 
{
  [CmdletBinding()]
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")]
  param()
  DynamicParam
  {
    $params = New-Object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
    Add-ProfileParam $params
    $ProfileMap = (Get-AzProfile)
    $Profiles = ($ProfileMap | Get-Member -MemberType NoteProperty).Name
    if ($Profiles.Count -gt 1)
    {
      $enum = $Profiles.GetEnumerator()
      $toss = $enum.MoveNext()
      $Current = $enum.Current
      $Keys = ($($ProfileMap.$Current) | Get-Member -MemberType NoteProperty).Name
    }
    else {
      $Keys = ($($ProfileMap.$Profiles[0]) | Get-Member -MemberType NoteProperty).Name
    }
    $moduleValid = New-Object -Type System.Management.Automation.ValidateSetAttribute($Keys)
    $moduleAttribute = New-Object -Type System.Management.Automation.ParameterAttribute
    $moduleAttribute.ParameterSetName = 
    $moduleAttribute.Mandatory = $true
    $moduleAttribute.Position = 1
    $moduleCollection = New-object -Type System.Collections.ObjectModel.Collection[System.Attribute]
    $moduleCollection.Add($moduleValid)
    $moduleCollection.Add($moduleAttribute)
    $moduleParam = New-Object -Type System.Management.Automation.RuntimeDefinedParameter("Module", [string], $moduleCollection)
    $params.Add("Module", $moduleParam)
    return $params
  }
  
  PROCESS
  {
    $ProfileMap = (Get-AzProfile)
    $Profile = $PSBoundParameters.Profile
    $Module = $PSBoundParameters.Module
    $versionList = $ProfileMap.$Profile.$Module
    Write-Verbose "Getting the version of $module from $profile"
    $moduleList = Get-Module -Name $Module -ListAvailable | Where-Object {$null -ne $_.RepositorySourceLocation}
    foreach ($version in $versionList)
    {
      foreach ($module in $moduleList)
      {
        if ($version -eq $module.Version)
        {
          return $version
        }
      }
    }
    return $null
  }
}

<#
.ExternalHelp help\AzureRM.Bootstrapper-help.xml
#>

function Get-AzureRmProfile
{
  [CmdletBinding(DefaultParameterSetName="ListAvailableParameterSet")]
  param()
  DynamicParam
  {
    $params = New-Object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
    Add-SwitchParam $params "ListAvailable" "ListAvailableParameterSet"
    Add-SwitchParam $params "Update"
    return $params
  }
  PROCESS
  {
    # ListAvailable helps to display all profiles available from the gallery
    [switch]$ListAvailable = $PSBoundParameters.ListAvailable
    $PSBoundParameters.Remove('ListAvailable') | Out-Null
    $ProfileMap = (Get-AzProfile @PSBoundParameters)
    if ($ListAvailable.IsPresent)
    {
      Write-Verbose "Getting all the profiles available for install"
      Get-ProfilesAvailable $ProfileMap
    }
    else
    {
      # Just display profiles installed on the machine
      Write-Verbose "Getting profiles installed on the machine and available for import"
      $IncompleteProfiles = @()
      $profilesInstalled = Get-ProfilesInstalled -ProfileMap $ProfileMap ([REF]$IncompleteProfiles)
      $Output = @()
      foreach ($key in $profilesInstalled.Keys)
      {
        $Output += "Profile : $key"
        $Output += "-----------------"
        $Output += ($profilesInstalled.$key | Format-Table -HideTableHeaders | Out-String)
      }
      if ($IncompleteProfiles.Count -gt 0)
      {
        Write-Warning "Some modules from profile(s) $(@($IncompleteProfiles) -join ', ') were not installed. Use Install-AzureRmProfile to install missing modules."
      }
      $Output
    }
  }
}

<#
.ExternalHelp help\AzureRM.Bootstrapper-help.xml
#>

function Use-AzureRmProfile
{
  [CmdletBinding(SupportsShouldProcess=$true)] 
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")]
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidShouldContinueWithoutForce", "")]
  param()
  DynamicParam
  {
    $params = New-Object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
    Add-ProfileParam $params
    Add-ForceParam $params
    Add-ScopeParam $params
    Add-ModuleParam $params
    return $params
  }
  PROCESS 
  {
    $Force = $PSBoundParameters.Force
    $ProfileMap = (Get-AzProfile)
    $Profile = $PSBoundParameters.Profile
    $Scope = $PSBoundParameters.Scope
    $Modules = $PSBoundParameters.Module

    # If user hasn't provided modules, use the module names from profile
    if ($null -eq $Modules)
    {
      $Modules = ($ProfileMap.$Profile | Get-Member -MemberType NoteProperty).Name
    }

    # If AzureRM $RollUpModule is present in that profile, it will install all the dependent modules; no need to specify other modules
    if ($Modules.Contains($RollUpModule))
    {
      $Modules = @($RollUpModule)
    }
    
    $PSBoundParameters.Remove('Profile') | Out-Null
    $PSBoundParameters.Remove('Scope') | Out-Null
    $PSBoundParameters.Remove('Module') | Out-Null

    # Variable to track progress
    $ModuleCount = 0
    Write-Output "Loading Profile $Profile"
    foreach ($Module in $Modules)
    {
      $ModuleCount = $ModuleCount + 1
      if (Find-PotentialConflict -Module $Module @PSBoundParameters) 
      {
        continue
      }

      $version = Get-AzureRmModule -Profile $Profile -Module $Module
      if (($null -eq $version) -and $PSCmdlet.ShouldProcess($module, "Installing module for profile $profile in the current scope"))
      {
        Write-Verbose "$module was not found on the machine. Trying to install..."
        if (($Force.IsPresent -or $PSCmdlet.ShouldContinue("Install Module $module for Profile $Profile from the gallery?", "Installing Modules for Profile $Profile")))
        {
          $versions = $ProfileMap.$Profile.$Module
          $versionEnum = $versions.GetEnumerator()
          $toss = $versionEnum.MoveNext()
          $version = $versionEnum.Current
          Write-Progress -Activity "Installing Module $Module version: $version" -Status "Progress:" -PercentComplete ($ModuleCount/($Modules.Length)*100)
          Write-Verbose "Installing module $module"
          Invoke-InstallModule -module $Module -version $version -scope $scope
        }
      }

      # Block user if they try to import a module to the session where a different version of the same module is already imported
      if ($null -ne (Get-Module -Name $Module | Where-Object { $_.Version -ne $version} ))
      {
        Write-Error "A different version of module $module is already imported in this session. Start a new PowerShell session and retry the operation."
        return
      }

      if ($PSCmdlet.ShouldProcess($module, "Importing module for profile $profile in the current scope"))
      {
        Write-Verbose "Importing module $module"
        Import-Module -Name $Module -RequiredVersion $version -Global
      }
    }
  }
}

<#
.ExternalHelp help\AzureRM.Bootstrapper-help.xml
#>

function Install-AzureRmProfile
{
  [CmdletBinding()]
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")]
  param()
  DynamicParam
  {
    $params = New-Object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
    Add-ProfileParam $params
    Add-ScopeParam $params
    Add-ForceParam $params
    return $params
  }

  PROCESS {
    $ProfileMap = (Get-AzProfile)
    $Profile = $PSBoundParameters.Profile
    $Scope = $PSBoundParameters.Scope
    $Modules = ($ProfileMap.$Profile | Get-Member -MemberType NoteProperty).Name

    # If AzureRM $RollUpModule is present in $profile, it will install all the dependent modules; no need to specify other modules
    if ($Modules.Contains($RollUpModule))
    {
      $Modules = @($RollUpModule)
    }
      
    $PSBoundParameters.Remove('Profile') | Out-Null
    $PSBoundParameters.Remove('Scope') | Out-Null

    $ModuleCount = 0
    foreach ($Module in $Modules)
    {
      $ModuleCount = $ModuleCount + 1
      if (Find-PotentialConflict -Module $Module @PSBoundParameters) 
      {
        continue
      }

      $version = Get-AzureRmModule -Profile $Profile -Module $Module
      if ($null -eq $version) 
      {
        $versions = $ProfileMap.$Profile.$Module
        $versionEnum = $versions.GetEnumerator()
        $toss = $versionEnum.MoveNext()
        $version = $versionEnum.Current
        Write-Progress -Activity "Installing Module $Module version: $version" -Status "Progress:" -PercentComplete ($ModuleCount/($Modules.Length)*100)
        Write-Verbose "Installing module $module" 
        Invoke-InstallModule -module $Module -version $version -scope $scope
      }
    }
  }
}

<#
.ExternalHelp help\AzureRM.Bootstrapper-help.xml
#>

function Uninstall-AzureRmProfile
{
  [CmdletBinding(SupportsShouldProcess = $true)]
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidShouldContinueWithoutForce", "")]
  param()
  DynamicParam
  {
    $params = New-Object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
    Add-ProfileParam $params
    Add-ForceParam $params
    return $params
  }

  PROCESS {
    $ProfileMap = (Get-AzProfile)
    $Profile = $PSBoundParameters.Profile
    $Force = $PSBoundParameters.Force

    if ($PSCmdlet.ShouldProcess("$Profile", "Uninstall Profile")) 
    {
      if (($Force.IsPresent -or $PSCmdlet.ShouldContinue("Uninstall Profile $Profile", "Removing Modules for profile $Profile")))
      {
        Write-Verbose "Trying to uninstall profile $profile"
        Uninstall-ProfileHelper -PMap $ProfileMap @PSBoundParameters
      }
    }
  }
}

<#
.ExternalHelp help\AzureRM.Bootstrapper-help.xml
#>

function Update-AzureRmProfile
{
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")]
  [CmdletBinding(SupportsShouldProcess = $true)]
  param()
  DynamicParam
  {
    $params = New-Object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
    Add-ProfileParam $params
    Add-ForceParam $params
    Add-RemoveParam $params 
    Add-ModuleParam $params
    Add-ScopeParam $params
    return $params
  }

  PROCESS {
    # Update Profile cache, if not up-to-date
    $ProfileMap = (Get-AzProfile -Update)
    $profile = $PSBoundParameters.Profile
    $Remove = $PSBoundParameters.RemovePreviousVersions

    $PSBoundParameters.Remove('RemovePreviousVersions') | Out-Null

    # Install & import the required version
    Use-AzureRmProfile @PSBoundParameters
    
    $PSBoundParameters.Remove('Force') | Out-Null
    $PSBoundParameters.Remove('Scope') | Out-Null

    # Remove previous versions of the profile?
    if ($Remove.IsPresent -and $PSCmdlet.ShouldProcess($profile, "Remove previous versions of profile")) 
    {
      # Remove-PreviousVersions and clean up cache
      Update-ProfileHelper @PSBoundParameters -RemovePreviousVersions
    }
  }
}

function Set-BootstrapRepo
{
    [CmdletBinding()]
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    param([string]$Repo)
    $script:BootStrapRepo = $Repo
}

# SIG # Begin signature block
# MIIkGwYJKoZIhvcNAQcCoIIkDDCCJAgCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCB/0liwKKlI1nlN
# aJqGK5EdUhm1Ik0gDdvcWyxN/UWIc6CCDZMwggYRMIID+aADAgECAhMzAAAAjoeR
# pFcaX8o+AAAAAACOMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMTYxMTE3MjIwOTIxWhcNMTgwMjE3MjIwOTIxWjCBgzEL
# MAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1v
# bmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjENMAsGA1UECxMETU9Q
# UjEeMBwGA1UEAxMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMIIBIjANBgkqhkiG9w0B
# AQEFAAOCAQ8AMIIBCgKCAQEA0IfUQit+ndnGetSiw+MVktJTnZUXyVI2+lS/qxCv
# 6cnnzCZTw8Jzv23WAOUA3OlqZzQw9hYXtAGllXyLuaQs5os7efYjDHmP81LfQAEc
# wsYDnetZz3Pp2HE5m/DOJVkt0slbCu9+1jIOXXQSBOyeBFOmawJn+E1Zi3fgKyHg
# 78CkRRLPA3sDxjnD1CLcVVx3Qv+csuVVZ2i6LXZqf2ZTR9VHCsw43o17lxl9gtAm
# +KWO5aHwXmQQ5PnrJ8by4AjQDfJnwNjyL/uJ2hX5rg8+AJcH0Qs+cNR3q3J4QZgH
# uBfMorFf7L3zUGej15Tw0otVj1OmlZPmsmbPyTdo5GPHzwIDAQABo4IBgDCCAXww
# HwYDVR0lBBgwFgYKKwYBBAGCN0wIAQYIKwYBBQUHAwMwHQYDVR0OBBYEFKvI1u2y
# FdKqjvHM7Ww490VK0Iq7MFIGA1UdEQRLMEmkRzBFMQ0wCwYDVQQLEwRNT1BSMTQw
# MgYDVQQFEysyMzAwMTIrYjA1MGM2ZTctNzY0MS00NDFmLWJjNGEtNDM0ODFlNDE1
# ZDA4MB8GA1UdIwQYMBaAFEhuZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEsw
# SaBHoEWGQ2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0Nv
# ZFNpZ1BDQTIwMTFfMjAxMS0wNy0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsG
# AQUFBzAChkVodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01p
# Y0NvZFNpZ1BDQTIwMTFfMjAxMS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkq
# hkiG9w0BAQsFAAOCAgEARIkCrGlT88S2u9SMYFPnymyoSWlmvqWaQZk62J3SVwJR
# avq/m5bbpiZ9CVbo3O0ldXqlR1KoHksWU/PuD5rDBJUpwYKEpFYx/KCKkZW1v1rO
# qQEfZEah5srx13R7v5IIUV58MwJeUTub5dguXwJMCZwaQ9px7eTZ56LadCwXreUM
# tRj1VAnUvhxzzSB7pPrI29jbOq76kMWjvZVlrkYtVylY1pLwbNpj8Y8zon44dl7d
# 8zXtrJo7YoHQThl8SHywC484zC281TllqZXBA+KSybmr0lcKqtxSCy5WJ6PimJdX
# jrypWW4kko6C4glzgtk1g8yff9EEjoi44pqDWLDUmuYx+pRHjn2m4k5589jTajMW
# UHDxQruYCen/zJVVWwi/klKoCMTx6PH/QNf5mjad/bqQhdJVPlCtRh/vJQy4njpI
# BGPveJiiXQMNAtjcIKvmVrXe7xZmw9dVgh5PgnjJnlQaEGC3F6tAE5GusBnBmjOd
# 7jJyzWXMT0aYLQ9RYB58+/7b6Ad5B/ehMzj+CZrbj3u2Or2FhrjMvH0BMLd7Hald
# G73MTRf3bkcz1UDfasouUbi1uc/DBNM75ePpEIzrp7repC4zaikvFErqHsEiODUF
# he/CBAANa8HYlhRIFa9+UrC4YMRStUqCt4UqAEkqJoMnWkHevdVmSbwLnHhwCbww
# ggd6MIIFYqADAgECAgphDpDSAAAAAAADMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYD
# VQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEe
# MBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3Nv
# ZnQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxMTAeFw0xMTA3MDgyMDU5
# MDlaFw0yNjA3MDgyMTA5MDlaMH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo
# aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y
# cG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIw
# MTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCr8PpyEBwurdhuqoIQ
# TTS68rZYIZ9CGypr6VpQqrgGOBoESbp/wwwe3TdrxhLYC/A4wpkGsMg51QEUMULT
# iQ15ZId+lGAkbK+eSZzpaF7S35tTsgosw6/ZqSuuegmv15ZZymAaBelmdugyUiYS
# L+erCFDPs0S3XdjELgN1q2jzy23zOlyhFvRGuuA4ZKxuZDV4pqBjDy3TQJP4494H
# DdVceaVJKecNvqATd76UPe/74ytaEB9NViiienLgEjq3SV7Y7e1DkYPZe7J7hhvZ
# PrGMXeiJT4Qa8qEvWeSQOy2uM1jFtz7+MtOzAz2xsq+SOH7SnYAs9U5WkSE1JcM5
# bmR/U7qcD60ZI4TL9LoDho33X/DQUr+MlIe8wCF0JV8YKLbMJyg4JZg5SjbPfLGS
# rhwjp6lm7GEfauEoSZ1fiOIlXdMhSz5SxLVXPyQD8NF6Wy/VI+NwXQ9RRnez+ADh
# vKwCgl/bwBWzvRvUVUvnOaEP6SNJvBi4RHxF5MHDcnrgcuck379GmcXvwhxX24ON
# 7E1JMKerjt/sW5+v/N2wZuLBl4F77dbtS+dJKacTKKanfWeA5opieF+yL4TXV5xc
# v3coKPHtbcMojyyPQDdPweGFRInECUzF1KVDL3SV9274eCBYLBNdYJWaPk8zhNqw
# iBfenk70lrC8RqBsmNLg1oiMCwIDAQABo4IB7TCCAekwEAYJKwYBBAGCNxUBBAMC
# AQAwHQYDVR0OBBYEFEhuZOVQBdOCqhc3NyK1bajKdQKVMBkGCSsGAQQBgjcUAgQM
# HgoAUwB1AGIAQwBBMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1Ud
# IwQYMBaAFHItOgIxkEO5FAVO4eqnxzHRI4k0MFoGA1UdHwRTMFEwT6BNoEuGSWh0
# dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0Nl
# ckF1dDIwMTFfMjAxMV8wM18yMi5jcmwwXgYIKwYBBQUHAQEEUjBQME4GCCsGAQUF
# BzAChkJodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0Nl
# ckF1dDIwMTFfMjAxMV8wM18yMi5jcnQwgZ8GA1UdIASBlzCBlDCBkQYJKwYBBAGC
# Ny4DMIGDMD8GCCsGAQUFBwIBFjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtp
# b3BzL2RvY3MvcHJpbWFyeWNwcy5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcA
# YQBsAF8AcABvAGwAaQBjAHkAXwBzAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZI
# hvcNAQELBQADggIBAGfyhqWY4FR5Gi7T2HRnIpsLlhHhY5KZQpZ90nkMkMFlXy4s
# PvjDctFtg/6+P+gKyju/R6mj82nbY78iNaWXXWWEkH2LRlBV2AySfNIaSxzzPEKL
# UtCw/WvjPgcuKZvmPRul1LUdd5Q54ulkyUQ9eHoj8xN9ppB0g430yyYCRirCihC7
# pKkFDJvtaPpoLpWgKj8qa1hJYx8JaW5amJbkg/TAj/NGK978O9C9Ne9uJa7lryft
# 0N3zDq+ZKJeYTQ49C/IIidYfwzIY4vDFLc5bnrRJOQrGCsLGra7lstnbFYhRRVg4
# MnEnGn+x9Cf43iw6IGmYslmJaG5vp7d0w0AFBqYBKig+gj8TTWYLwLNN9eGPfxxv
# FX1Fp3blQCplo8NdUmKGwx1jNpeG39rz+PIWoZon4c2ll9DuXWNB41sHnIc+BncG
# 0QaxdR8UvmFhtfDcxhsEvt9Bxw4o7t5lL+yX9qFcltgA1qFGvVnzl6UJS0gQmYAf
# 0AApxbGbpT9Fdx41xtKiop96eiL6SJUfq/tHI4D1nvi/a7dLl+LrdXga7Oo3mXkY
# S//WsyNodeav+vyL6wuA6mk7r/ww7QRMjt/fdW1jkT3RnVZOT7+AVyKheBEyIXrv
# QQqxP/uozKRdwaGIm1dxVk5IRcBCyZt2WwqASGv9eZ/BvW1taslScxMNelDNMYIV
# 3jCCFdoCAQEwgZUwfjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjEoMCYGA1UEAxMfTWljcm9zb2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAA
# AI6HkaRXGl/KPgAAAAAAjjANBglghkgBZQMEAgEFAKCBzDAZBgkqhkiG9w0BCQMx
# DAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkq
# hkiG9w0BCQQxIgQgfQzuIW5RG2FoeUqq6BVRutu1xFbDVILBU5X8juy/sbMwYAYK
# KwYBBAGCNwIBDDFSMFCgNoA0AE0AaQBjAHIAbwBzAG8AZgB0ACAAQQB6AHUAcgBl
# ACAAUABvAHcAZQByAFMAaABlAGwAbKEWgBRodHRwOi8vQ29kZVNpZ25JbmZvIDAN
# BgkqhkiG9w0BAQEFAASCAQA7AFSBOQ0M5drxebgd2tF4PS5u1iECumj4C1TF/oXs
# fti+cqTWUSkevOuaXqRCgb39KpSMrgozVvYO07RgJ47Yg5KCFbdYNaQkiNkZH/+o
# 6PKjZnLVeX0EpXmNOGshN/cdba0ZaMDcEz6hxRtkcMtatXw0BhTx3Mr0/JwNug/v
# Vd+SijDtWQx1E579QwhsK/BT3eDOOxnPL4rVwsDy1PeZGfaDyUGfSFTe6Je3KRqA
# mb0THIjhHhx12UOcW6EZVlGRI4b/21EVoBW/aflbz9ghxTamErOhgXUmB+ufKrqV
# 9f5VnOlFY2cUuP7Mi0SMPVMptSnPOKIG3aGFFH1Ork/WoYITSjCCE0YGCisGAQQB
# gjcDAwExghM2MIITMgYJKoZIhvcNAQcCoIITIzCCEx8CAQMxDzANBglghkgBZQME
# AgEFADCCAT0GCyqGSIb3DQEJEAEEoIIBLASCASgwggEkAgEBBgorBgEEAYRZCgMB
# MDEwDQYJYIZIAWUDBAIBBQAEICBNUHI6J+jb42j6qiIUmEKpTqGx9a9CrqpunQWU
# 5qsVAgZYr7LWvykYEzIwMTcwMzE0MDExMTMyLjA5MVowBwIBAYACAfSggbmkgbYw
# gbMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xDTALBgNVBAsT
# BE1PUFIxJzAlBgNVBAsTHm5DaXBoZXIgRFNFIEVTTjpCOEVDLTMwQTQtNzE0NDEl
# MCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaCCDs0wggZxMIIE
# WaADAgECAgphCYEqAAAAAAACMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9v
# dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxMDAeFw0xMDA3MDEyMTM2NTVaFw0y
# NTA3MDEyMTQ2NTVaMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9u
# MRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRp
# b24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMIIBIjAN
# BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqR0NvHcRijog7PwTl/X6f2mUa3RU
# ENWlCgCChfvtfGhLLF/Fw+Vhwna3PmYrW/AVUycEMR9BGxqVHc4JE458YTBZsTBE
# D/FgiIRUQwzXTbg4CLNC3ZOs1nMwVyaCo0UN0Or1R4HNvyRgMlhgRvJYR4YyhB50
# YWeRX4FUsc+TTJLBxKZd0WETbijGGvmGgLvfYfxGwScdJGcSchohiq9LZIlQYrFd
# /XcfPfBXday9ikJNQFHRD5wGPmd/9WbAA5ZEfu/QS/1u5ZrKsajyeioKMfDaTgaR
# togINeh4HLDpmc085y9Euqf03GS9pAHBIAmTeM38vMDJRF1eFpwBBU8iTQIDAQAB
# o4IB5jCCAeIwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFNVjOlyKMZDzQ3t8
# RhvFM2hahW1VMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1UdDwQEAwIB
# hjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNX2VsuP6KJcYmjRPZSQW9fO
# mhjEMFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9w
# a2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNybDBaBggr
# BgEFBQcBAQROMEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93d3cubWljcm9zb2Z0LmNv
# bS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3J0MIGgBgNVHSAB
# Af8EgZUwgZIwgY8GCSsGAQQBgjcuAzCBgTA9BggrBgEFBQcCARYxaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL1BLSS9kb2NzL0NQUy9kZWZhdWx0Lmh0bTBABggrBgEF
# BQcCAjA0HjIgHQBMAGUAZwBhAGwAXwBQAG8AbABpAGMAeQBfAFMAdABhAHQAZQBt
# AGUAbgB0AC4gHTANBgkqhkiG9w0BAQsFAAOCAgEAB+aIUQ3ixuCYP4FxAz2do6Eh
# b7Prpsz1Mb7PBeKp/vpXbRkws8LFZslq3/Xn8Hi9x6ieJeP5vO1rVFcIK1GCRBL7
# uVOMzPRgEop2zEBAQZvcXBf/XPleFzWYJFZLdO9CEMivv3/Gf/I3fVo/HPKZeUqR
# UgCvOA8X9S95gWXZqbVr5MfO9sp6AG9LMEQkIjzP7QOllo9ZKby2/QThcJ8ySif9
# Va8v/rbljjO7Yl+a21dA6fHOmWaQjP9qYn/dxUoLkSbiOewZSnFjnXshbcOco6I8
# +n99lmqQeKZt0uGc+R38ONiU9MalCpaGpL2eGq4EQoO4tYCbIjggtSXlZOz39L9+
# Y1klD3ouOVd2onGqBooPiRa6YacRy5rYDkeagMXQzafQ732D8OE7cQnfXXSYIghh
# 2rBQHm+98eEA3+cxB6STOvdlR3jo+KhIq/fecn5ha293qYHLpwmsObvsxsvYgrRy
# zR30uIUBHoD7G4kqVDmyW9rIDVWZeodzOwjmmC3qjeAzLhIp9cAvVCch98isTtoo
# uLGp25ayp0Kiyc8ZQU3ghvkqmqMRZjDTu3QyS99je/WZii8bxyGvWbWu3EQ8l1Bx
# 16HSxVXjad5XwdHeMMD9zOZN+w2/XU/pnR4ZOC+8z1gFLu8NoFA12u8JJxzVs341
# Hgi62jbb01+P3nSISRIwggTaMIIDwqADAgECAhMzAAAAn2fytagjBlt7AAAAAACf
# MA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5n
# dG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9y
# YXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4X
# DTE2MDkwNzE3NTY0N1oXDTE4MDkwNzE3NTY0N1owgbMxCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xDTALBgNVBAsTBE1PUFIxJzAlBgNVBAsTHm5D
# aXBoZXIgRFNFIEVTTjpCOEVDLTMwQTQtNzE0NDElMCMGA1UEAxMcTWljcm9zb2Z0
# IFRpbWUtU3RhbXAgU2VydmljZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
# ggEBALkI8SOc3cQCLwKFoaMnl2T5A5wSVD9Tglq4Put9bhjFcsEn1XApDPCWS9aP
# hMcWOWKe+7ENI4Si4zD30nVQC9PZ0NDu+pK9XV83OfjGchFkKzOBRddOhpsQkxFg
# MF3RfLTNXAEqffnNaReXwtVUkiGEJvW6KmABixzP0aeUVmJ6MHnJnmo+TKZdoVl7
# cg6TY6LCoze/F6rhOXmi/P3X/K3jHtmAaxL9Ou53jjDgO5Rjxt6ZEamdEsGF2SWZ
# 6wH6Dmg9G6iZPxgw+mjODwReL6jwh7H2XhsvzoFMrSERMzIIf2eJGAM9C0GR0BZH
# yRti17QqL5TaCuWPjMxTKXX4DlkCAwEAAaOCARswggEXMB0GA1UdDgQWBBT9ixsi
# w30jR3amHt/gZtRS6bb5oDAfBgNVHSMEGDAWgBTVYzpcijGQ80N7fEYbxTNoWoVt
# VTBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtp
# L2NybC9wcm9kdWN0cy9NaWNUaW1TdGFQQ0FfMjAxMC0wNy0wMS5jcmwwWgYIKwYB
# BQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20v
# cGtpL2NlcnRzL01pY1RpbVN0YVBDQV8yMDEwLTA3LTAxLmNydDAMBgNVHRMBAf8E
# AjAAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMA0GCSqGSIb3DQEBCwUAA4IBAQBlEMFs
# a88VHq8PSDbr3y0LvAAA5pFmGlCWZbkxD2WMqfF0y8fnlvgb874z8sz8QZzByCmY
# 1jHyHTc98Zekz7L2Y5SANUIa8jyU36c64Ck5fY6Pe9hUA1RG/1zP+eq080chUPCF
# 2zezhfwuz9Ob0obO64BwW0GZgYYz1hjsq+DBkSCBRV59ryFpzgKRwhWF8quXtHDp
# imiJx+ds2VZSwEVk/QRY7pLuUvedN8P5DNuLaaRw3oJcs2Wxh2jWS5T8Y3JevUo3
# K3VTtHPi2IBWISkEG7TOnNEUcUXDMGSOeZ27kuPFzKkDVbtzvwEVepkGrsZ1W+1x
# uDYPQ1b3BMG8C79HoYIDdjCCAl4CAQEwgeOhgbmkgbYwgbMxCzAJBgNVBAYTAlVT
# MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xDTALBgNVBAsTBE1PUFIxJzAlBgNVBAsT
# Hm5DaXBoZXIgRFNFIEVTTjpCOEVDLTMwQTQtNzE0NDElMCMGA1UEAxMcTWljcm9z
# b2Z0IFRpbWUtU3RhbXAgU2VydmljZaIlCgEBMAkGBSsOAwIaBQADFQBs0ycI8vnZ
# qMv5Gd6SS0qt2xmjwaCBwjCBv6SBvDCBuTELMAkGA1UEBhMCVVMxEzARBgNVBAgT
# Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m
# dCBDb3Jwb3JhdGlvbjENMAsGA1UECxMETU9QUjEnMCUGA1UECxMebkNpcGhlciBO
# VFMgRVNOOjU3RjYtQzFFMC01NTRDMSswKQYDVQQDEyJNaWNyb3NvZnQgVGltZSBT
# b3VyY2UgTWFzdGVyIENsb2NrMA0GCSqGSIb3DQEBBQUAAgUA3HG2hDAiGA8yMDE3
# MDMxNDAwMjMzMloYDzIwMTcwMzE1MDAyMzMyWjB0MDoGCisGAQQBhFkKBAExLDAq
# MAoCBQDccbaEAgEAMAcCAQACAgHgMAcCAQACAhjiMAoCBQDccwgEAgEAMDYGCisG
# AQQBhFkKBAIxKDAmMAwGCisGAQQBhFkKAwGgCjAIAgEAAgMW42ChCjAIAgEAAgMH
# oSAwDQYJKoZIhvcNAQEFBQADggEBABtn9P7C84fsSh4DE34mW3y1R7gHtZKM56zC
# MJyn4gQiNTI8hwHbMlbHay6IfIK76OckXh5m21yg/YNydUMFpZbd5JydrVCCUoZC
# 3BNu7B74vw6EQEZzxy/4XQh5CT9+oelzNfuxSoFZi7Kmwd5pvHbAkkrXu9bNoVg5
# Kt8uxeUHmRGYFIlUIp/mKRosWOTS3/48nQ3APdQC/GS5CNJU6t0FEVwRX65lr6+2
# ysP1n5h79j8bP86DJOtvB4Qqb2mDwZOl4H/KmvYpwdcvhJ1NoeA7l9cIPWjIz7Hd
# 6CZKBYH5THtUjw4kwrmvOEPmNPRceSRCnR/G1rXS3jI4x9SStQwxggL1MIIC8QIB
# ATCBkzB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD
# VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAJ9n8rWoIwZb
# ewAAAAAAnzANBglghkgBZQMEAgEFAKCCATIwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3
# DQEJEAEEMC8GCSqGSIb3DQEJBDEiBCDGR5InNcY0fLZ2P9mV60dGksBmdydMAqRx
# zJzzd0FucTCB4gYLKoZIhvcNAQkQAgwxgdIwgc8wgcwwgbEEFGzTJwjy+dmoy/kZ
# 3pJLSq3bGaPBMIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp
# bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw
# b3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAC
# EzMAAACfZ/K1qCMGW3sAAAAAAJ8wFgQU6iI0UNHA7VG5/D1DM/22WW7XeqkwDQYJ
# KoZIhvcNAQELBQAEggEAikKeGnHdZt6Yd3nUiOVdkWaWys5ee9vOod5XcN5OyB5C
# f+s22E9hzEHHRAienv/Iep8fw7s8CnF0vxiMJnfCal3vWdBi/DknjbNCLTbRW1er
# tQl7sKX95KYUMl9ykn4bs6+IqJCY6R9cfpydNxibtUXUQQ1wdWwE5sgfL8SD//L8
# bcp7m8pFGm9lX5t8H/l6QhgtO0STQvMsUGEYOycpajuZeclSYk7dEA+7tRzuKDbY
# ohNSaIdyYj6P3UoV4fpGaID5983touW8woF7BF79Dc5vBIMSb3AjGxRA7RtCqE1V
# TMGUunAyz5vhwV7nBuUpRn3fPEVKt3+RDP1YpeDMKQ==
# SIG # End signature block