linux/powershell/PSCloudShellStartup.ps1 (361 lines of code) (raw):

# Startupscript for PowerShell Cloud Shell #region Script Variables $script:Startuptime = [System.DateTime]::Now $script:AzureADModuleName = 'AzureAD.Standard.Preview' # Set SkipAzInstallationChecks to avoid az check for AzInstallationChecks.json [System.Environment]::SetEnvironmentVariable('SkipAzInstallationChecks', $true) # Get SkipMSIAuth for local testing. If skipped, auth will be taken care of by testing script. $script:SkipMSIAuth = [System.Environment]::GetEnvironmentVariable('SkipMSIAuth') Microsoft.PowerShell.Core\Import-Module -Name PSCloudShellUtility $script:PSCloudShellUtilityModuleInfo = Microsoft.PowerShell.Core\Get-Module PSCloudShellUtility # This Cloud env map is for Connect-AzureAD $script:CloudEnvironmentMapAzureAD = @{ PROD = 'AzureCloud'; Fairfax = 'AzureUSGovernment'; Mooncake = 'AzureChinaCloud'; BlackForest = 'AzureGermanCloud'; dogfood = 'dogfood'; USNat = 'AzureUSGovernment2'; USSec = 'AzureUSGovernment3' } # This Cloud Env Map is for Connect-AzAccount # We need to have two because the mapping is slightly different for the two commands $script:CloudEnvironmentMapAzAccount = @{ PROD = 'AzureCloud'; Fairfax = 'AzureUSGovernment'; Mooncake = 'AzureChinaCloud'; BlackForest = 'AzureGermanCloud'; dogfood = 'dogfood'; } # For the Az.Tools.Predictor PSReadline\Set-PSReadLineOption -Colors @{ InLinePrediction = '#8d8d8d'} Microsoft.PowerShell.Core\Import-Module Az.Tools.Predictor -Force # Using the new set of az cmdlets Microsoft.PowerShell.Core\Import-Module Az.Accounts Az.Accounts\Enable-AzureRmAlias # On Linux, we are not loading the profile from the clouddrive since we are already mounted on the Linux OS image: \clouddrive\.cloudconsole\acc_<user>.img # For Pwsh profile, see https://docs.microsoft.com/en-us/powershell/scripting/whats-new/what-s-new-in-powershell-core-60?view=powershell-6#filesystem $script:UserDefaultPath = $HOME $script:CurrentHostProfilePath = (Microsoft.PowerShell.Management\Join-Path -Path $script:UserDefaultPath -ChildPath '.config/PowerShell/Microsoft.PowerShell_profile.ps1') $script:AllHostsProfilePath = (Microsoft.PowerShell.Management\Join-Path -Path $script:UserDefaultPath -ChildPath '.config/PowerShell/profile.ps1') # To ensure that the installed script is immediately usable, we need to add the scope path to the PATH enviroment variable. $scriptPath = Microsoft.PowerShell.Management\Join-Path $env:HOME '.local/share/powershell/Scripts' $existingPath = [System.Environment]::GetEnvironmentVariable('PATH', [System.EnvironmentVariableTarget]::Process) if(($existingPath -split ':') -notcontains $scriptPath) { [System.Environment]::SetEnvironmentVariable('PATH', $existingPath + ':' + $scriptPath, [System.EnvironmentVariableTarget]::Process) } #endregion #region Utility Functions # Helper to do telemetry logging. function Invoke-CloudShellTelemetry { param ( [Parameter(Mandatory=$true)] [string] $LogLabel, [Parameter(Mandatory=$true)] [System.DateTime] $StartTime ) $elapsed = [System.Math]::Round(([System.DateTime]::Now - $StartTime).TotalMilliseconds) # Calling a scoped module private function. Format: & ($moduleinfo){} & ($script:PSCloudShellUtilityModuleInfo){param([string]$Label, [double]$Elapsed)Add-CloudShellTelemetry -Name "ACC.POWERSHELL.$Label" -Value $Elapsed} -Label $LogLabel -Elapsed $elapsed } # Extract default subscriptionId from Storage Profile environment variable # Format of Storage Profile- {"storageAccountResourceId":"/subscriptions/<subscriptionGuid>/resourcegroups/<resourceGroup>/providers/Microsoft.Storage/storageAccounts/<storageAccountId>","fileShareName":"<Blob File Share>","diskSizeInGB":<diskSize>} function Get-SubscriptionIdFromStorageProfile { $subscriptionId = '' $startTime = [System.DateTime]::Now try { if ($env:ACC_STORAGE_PROFILE) { $storageProfile = $env:ACC_STORAGE_PROFILE | Microsoft.PowerShell.Utility\ConvertFrom-Json $storageAccountResourceId = $storageProfile.storageAccountResourceId if ($storageAccountResourceId) { # storageAccountResourceId is organized by the delimiter '/' $storageAccountResourceIdTokens = $storageAccountResourceId.Split('/') if ($storageAccountResourceIdTokens.Count) { # SubscriptionId is the next token after the keyword 'subscriptions' # This way of picking ensures that any change in the subscriptionId token location is future proofed $subscriptionId = $storageAccountResourceIdTokens[$storageAccountResourceIdTokens.IndexOf('subscriptions') + 1] } } } } finally { Invoke-CloudShellTelemetry -LogLabel "GETSUBSCRIPTIONID" -StartTime $startTime } $subscriptionId } # Authenticate to Azure Resource Manager Service using Identity (MSI) based auth # This is a one time authentication at Shell startup # The Identity endpoint $env:MSI_ENDPOINT takes care of keeping the auth current function Connect-AzService { param ( [string]$currentSubscriptionId ) # Enable Az Data collection # Else User is prompted when Connect-AzAccount is invoked Set-PSCloudShellTelemetry $startTime = [System.DateTime]::Now try { Microsoft.PowerShell.Core\Import-Module Az.Accounts # Removed AccountId as it's not required. When it is provided, it indicates using a user-assigned, rather than a system-assigned identity. # In the case where the user provides an account id, there are three possibilities: (1) It represents a clientId; # (2) It represents and ObjectId; (3) It represents the resource-id of a user-assigned identity. # Since (1) and (2) are both guids, when AccountId is a guid, we attempt authentication both using the 'client_id' query string # value in our request, and, if that fails, the 'object_id' query string value. So, if the msi service is set up to authenticate # using a particular user-assigned identity, and the user passes the appropriate object id or client id setting as AccountId, # the authentication will be successful. $envName = $env:ACC_CLOUD if ($CloudEnvironmentMapAzAccount.ContainsKey($env:ACC_CLOUD)) { $envName = $script:CloudEnvironmentMapAzAccount[$env:ACC_CLOUD] } $addAzAccountParameters = @{'Identity' = $true; 'TenantId' = $env:ACC_TID; 'EnvironmentName' = $envName} if($currentSubscriptionId) { $addAzAccountParameters.Add('SubscriptionId', $currentSubscriptionId) } $azAccount = Az.Accounts\Connect-AzAccount @addAzAccountParameters -ErrorAction SilentlyContinue -ErrorVariable azError # Log any errors from Azure authentication if ($azError) { $errorFolderPath = $script:UserDefaultPath $azureFolderPath = (Microsoft.PowerShell.Management\Join-Path -Path $script:UserDefaultPath -ChildPath '.azure') if (Microsoft.PowerShell.Management\Test-Path -Path $azureFolderPath) { $errorFolderPath = $azureFolderPath } # Use $script:UserDefaultPath Path if .azure folder does not exist $azErrorPath = Microsoft.PowerShell.Management\Join-Path -Path $errorFolderPath -ChildPath 'azError.err' $addAzAccountParameters.Keys > $azErrorPath $addAzAccountParameters.Values >> $azErrorPath $azError >> $azErrorPath } } finally { Invoke-CloudShellTelemetry -LogLabel "CONNECTAZURERMSERVICE" -StartTime $startTime } return $azAccount } # Authenticate to Azure Active Directory Service # This function needs to be run once per shell startup and everytime we get a new token from the RP # This deliberately shadows the Connect-AzureAD cmdlet because most of the time you want our version, and their error messages tell you to run this function Connect-AzureAD { $startTime = [System.DateTime]::Now try { $envName = $env:ACC_CLOUD if ($CloudEnvironmentMapAzureAD.ContainsKey($env:ACC_CLOUD)) { $envName = $script:CloudEnvironmentMapAzureAD[$env:ACC_CLOUD] } # Remove AccountId from parameters since it's missing for some users; Plus, it doesn't affect the authorization. $azureADParameters = @{'Identity' = $true; 'TenantId' = $env:ACC_TID; 'AzureEnvironmentName' = $envName} # This call sets the local process context with the token, account and tenant information & $script:AzureADModuleName\Connect-AzureAD @azureADParameters -ErrorAction SilentlyContinue -ErrorVariable azureADError | Microsoft.PowerShell.Core\Out-Null # Log any errors from AzureAD authentication if ($azureADError) { $errorFolderPath = $script:UserDefaultPath $azureFolderPath = (Microsoft.PowerShell.Management\Join-Path -Path $script:UserDefaultPath -ChildPath '.azure') Microsoft.PowerShell.Utility\Write-Warning -Message "An error occurred while authenticating to $($script:AzureADModuleName). Check $azureFolderPath for logs" if (Microsoft.PowerShell.Management\Test-Path -Path $azureFolderPath) { $errorFolderPath = $azureFolderPath } # Use $script:UserDefaultPath Path if .azure folder does not exist $azureADError > (Microsoft.PowerShell.Management\Join-Path -Path $errorFolderPath -ChildPath 'azureADError.err') } } finally { Invoke-CloudShellTelemetry -LogLabel "CONNECTAZUREADSERVICE" -StartTime $startTime } } function Set-PSCloudShellTelemetry { $startTime = [System.DateTime]::Now try { # Default value in case PSCloudShellUtility is not loaded Microsoft.PowerShell.Core\Import-Module -Name Az.Accounts $productVersion = '0.1.0' $productName = 'ps-cloud-shell' [Microsoft.Azure.Common.Authentication.AzureSession]::ClientFactory.AddUserAgent($productName, $productVersion) Az.Accounts\Enable-AzDataCollection -WarningAction SilentlyContinue } finally { Invoke-CloudShellTelemetry -LogLabel "ENABLECLOUDSHELLTELEMETRY" -StartTime $startTime } } function Invoke-PSCloudShellUserProfile { $start = [System.DateTime]::Now # First run all hosts profile if(Microsoft.PowerShell.Management\Test-Path -Path $script:AllHostsProfilePath) { try { Microsoft.PowerShell.Utility\Write-Verbose -Message 'Loading AllHosts profile ...' -Verbose # As the startupscript.ps1 gets executed with "." for global scope, we use "." here to use the startupscript's scope, i.e., global. . $script:AllHostsProfilePath } catch { # Log a warning and continue if encountering any terminating errors from the running user profile Microsoft.PowerShell.Utility\Write-Warning -Message "$_" } } # Second run current host profile if(Microsoft.PowerShell.Management\Test-Path -Path $script:CurrentHostProfilePath) { try { Microsoft.PowerShell.Utility\Write-Verbose -Message 'Loading CurrentHost profile ...' -Verbose # As the startupscript.ps1 gets executed with "." for global scope, we use "." here to use the startupscript's scope, i.e., global. . $script:CurrentHostProfilePath } catch { # Log a warning and continue if encountering any terminating errors from the running user profile Microsoft.PowerShell.Utility\Write-Warning -Message "$_" } } Invoke-CloudShellTelemetry -LogLabel "USERPROFILELOAD" -StartTime $start # display time if it's greater than 1 second if($elapsed -gt '1000') { Microsoft.PowerShell.Utility\Write-Verbose -Message "Loading user profile took $elapsed ms." -Verbose } } # Define a custom prompt function for Azure drive (Azure:) only # Since it is defined before user profile(s) are loaded, users can still customize prompt via profile function prompt { # If inside Azure PSDrive, show the current path above the prompt if(($pwd.Drive).Name -eq 'Azure' -and ($pwd.Provider).Name -eq 'SHiPS') { # There is a double prompt issue on pwsh bash using write-host here. See https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences for more details. # Gray #969696 is chosen because it's passed color contrast check for both bash and PowerShell blue # PS blue: #012456 vs #969696 5.11:1 # Bash: #000000 vs #969696 7.09:1 $CSI=[char]0x1b + '[' "${CSI}38;2;150;150;150m$($pwd)${CSI}00m`nPS Azure:\> " } # else use the default prompt else { "PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) "; } # .Link # https://go.microsoft.com/fwlink/?LinkID=225750 # .ExternalHelp System.Management.Automation.dll-help.xml } #endregion if (! $env:ACC_CLOUD) { # we are running locally, not in azure - skip setup steps return } #region Initialization # show MOTD & ($script:PSCloudShellUtilityModuleInfo){Get-CloudShellTip -ErrorAction SilentlyContinue} # Set the user profile path to clouddrive Microsoft.PowerShell.Utility\Set-Variable -Name PROFILE -Value $script:CurrentHostProfilePath -Scope Global $PROFILE = $PROFILE | Microsoft.PowerShell.Utility\Add-Member -MemberType NoteProperty -Name CurrentUserAllHosts -Value $script:AllHostsProfilePath -PassThru $PROFILE = $PROFILE | Microsoft.PowerShell.Utility\Add-Member -MemberType NoteProperty -Name CurrentUserCurrentHost -Value $script:CurrentHostProfilePath -PassThru # Dogfood initialization script if ($env:ACC_CLOUD -eq 'dogfood') { Microsoft.PowerShell.Utility\Write-Warning -Message "You are running in a dogfood environment. Please supply a URI for Azure dogfood environment initialization script." $dfEnvInitScriptURI = Microsoft.PowerShell.Utility\Read-Host -Prompt "Supply the URI" $dfEnvInitScript = Microsoft.PowerShell.Utility\Invoke-WebRequest -Uri $dfEnvInitScriptURI -UseBasicParsing | ForEach-Object Content $null = [ScriptBlock]::Create($dfEnvInitScript).Invoke() } if($script:SkipMSIAuth -eq $true) { Microsoft.PowerShell.Utility\Write-Debug -Message "Skip authenticating with MSI, instead test scripts will take care of Auth." } else { Microsoft.PowerShell.Utility\Write-Debug -Message "Authenticating with MSI ..." $AuthStartTime = [System.DateTime]::Now try { # Authenticate to Azure services # Use the default subscriptionId from Storage Profile to optimize authenticating to Azure Services using Connect-AzAccount Microsoft.PowerShell.Utility\Write-Verbose -Verbose -Message 'Authenticating to Azure ...' if (-not (Connect-AzService -currentSubscriptionId (Get-SubscriptionIdFromStorageProfile))) { Microsoft.PowerShell.Utility\Write-Warning -Message 'Azure Authentication failed.' . Invoke-PSCloudShellUserProfile return } } finally { # Measure the time spent on the Azure authentication Invoke-CloudShellTelemetry -LogLabel "AZUREAUTHENTICATION" -StartTime $AuthStartTime } } #endregion #region AzureAD # Import AzureAD module so cmdlets are visible, they are not currently being auto-discovered $azureADLoadStartTime = [System.DateTime]::Now try { Microsoft.PowerShell.Core\Import-Module -Name $script:AzureADModuleName } finally { Invoke-CloudShellTelemetry -LogLabel "AZUREADLOAD" -StartTime $azureADLoadStartTime } #endregion #region User Specific . Invoke-PSCloudShellUserProfile # Set PSDefaultParameterValues for cmdlets $PSDefaultParameterValues = @{'Install-Module:Scope' = 'CurrentUser'; 'Install-Script:Scope' = 'CurrentUser'} #region Initialize AzurePSDrive $startLoadingModules = [System.DateTime]::Now try { Microsoft.PowerShell.Core\Import-Module -Name AzurePSDrive Microsoft.PowerShell.Utility\Write-Verbose -Verbose -Message 'Building your Azure drive ...' } finally { Invoke-CloudShellTelemetry -LogLabel "LOADCLOUDSHELLMODULES" -StartTime $startLoadingModules } $startBuildingShips = [System.DateTime]::Now try { $null = Microsoft.PowerShell.Management\New-PSDrive -Name Azure -PSProvider SHiPS -Root "AzurePSDrive#Azure" -Scope Global if(-not $?) { Microsoft.PowerShell.Utility\Write-Warning -Message 'Something went wrong while creating Azure drive. You can still use this shell to run Azure PowerShell commands.' } } finally { Invoke-CloudShellTelemetry -LogLabel "BUILDSHIPS" -StartTime $startBuildingShips } # Set the PSReadline key handler for CloudShell key bindings and telemetry. # Note: Set-CloudShellPSReadLineKeyHandler has to be after loading user profiles & ($script:PSCloudShellUtilityModuleInfo){Set-CloudShellPSReadLineKeyHandler} Invoke-CloudShellTelemetry -LogLabel "STARTUPTIME" -StartTime $Startuptime #endregion #region Clean up temp Variables # Clean up variables since this startup script runs at global scope Microsoft.PowerShell.Utility\Remove-Variable -Name AllHostsProfilePath, CurrentHostProfilePath, Startuptime, elapsed, UserDefaultPath, startBuildingShips, startLoadingModules, AuthStartTime -ErrorAction Ignore Microsoft.PowerShell.Management\Remove-Item -Path env:ACC_CLUSTER -ErrorAction Ignore #endregion