linux/powershell/PSCloudShellUtility/PSCloudShellUtility.psm1 (1,562 lines of code) (raw):

#region Variables Microsoft.PowerShell.Core\Set-StrictMode -Version Latest Microsoft.PowerShell.Utility\Import-LocalizedData LocalizedData -filename PSCloudShellUtility.Resource.psd1 $script:IsWindowsOS = ($PSVersionTable.PSEdition -eq 'Desktop') -or $IsWindows Enum OsType { Windows = 1; Linux = 2 } $script:acronymLookup = $null $script:CurrentConsoleHostHistorycount = $null $script:HistoryIndex = 1 # keep the historycount up to 500 at the time of launching cloudshell. # PSReadline hardcoded MaximumHistoryCount = 4096, otherwise we could use $MaximumHistoryCount directly $script:MaximumHistoryCount = 500 $script:IsCore = $PSVersionTable.PSEdition -eq 'Core' # PowerShell Session Option when connecting to Windows targets # This is required since we connect to a WinRM_HTTPS endpoint configured using a self-signed certificate $script:sessionOption = [System.Management.Automation.Remoting.PSSessionOption]::new() $script:sessionOption.SkipCACheck = $true $script:sessionOption.SkipCNCheck = $true #region Telemetry $script:AppInsightTrackMetric = New-Object Microsoft.ApplicationInsights.TelemetryClient # Use the same instrumentationKey from bash $script:AppInsightTrackMetric.InstrumentationKey = '2d98668f-09f0-48a2-9df0-ba68f2ec3466' # track commands $script:AppInsightsTrackEvent = New-Object Microsoft.ApplicationInsights.TelemetryClient $script:AppInsightsTrackEvent.InstrumentationKey = 'f48a52b4-cb01-4f54-8733-9bebfdeee1dd' # properties can be used for data querying later on $script:CommonMetricProperties = New-Object "System.Collections.Generic.Dictionary``2[System.String,System.String]" $script:CommonMetricProperties.Add('ACC_VERSION', $env:ACC_VERSION) $script:CommonMetricProperties.Add('ACC_CLUSTER', $env:ACC_CLUSTER) $script:CommonMetricProperties.Add('PSVersion', $PSVersionTable.PSVersion) $script:CommonMetricProperties.Add('SessionId', (New-Guid)) # modules, tools and aliases are installed by default in the CloudShell $script:DefaultInstalledModules=@('Pester', 'PackageManagement', 'PowerShellGet', 'PSReadline', 'PSCloudShellUtility') $script:Tools=@('vim', 'git', 'pwsh', 'nano', 'code', 'emacs', 'az', 'ssh', 'sqlcmd', 'vi') $script:BuiltinModuleNames=@('Microsoft.PowerShell.Core', 'Microsoft.PowerShell.Management', 'Microsoft.PowerShell.Archive', 'Microsoft.PowerShell.Host', 'Microsoft.PowerShell.Utility', 'Microsoft.PowerShell.Management', 'Microsoft.PowerShell.Security', 'Microsoft.PowerShell.Diagnostics', 'PSDesiredStateConfiguration', 'Microsoft.WSMan.Management', 'CimCmdlets', 'PSDiagnostics') $script:BuiltinAliases=@( 'ac','asnp', 'cat', 'cd', 'CFS', 'chdir', 'clc', 'clear', 'clhy', 'cli', 'clp', 'cls', 'clv', 'cnsn', 'compare', 'copy', 'cp', 'cpi','cpp','curl','cvpa', 'dbp', 'del', 'diff', 'dir', 'dnsn', 'ebp', 'echo', 'epal', 'epcsv', 'epsn', 'erase', 'etsn', 'exsn', 'fc', 'fhx', 'fl', 'foreach', 'ft', 'fw', 'gal', 'gbp', 'gc', 'gcb', 'gci', 'gcm', 'gcs', 'gdr', 'ghy', 'gi', 'gin', 'gjb', 'gl', 'gm', 'gmo',' gp', 'gps', 'gpv', 'group', 'gsn','gsnp','gsv', 'gtz', 'gu', 'gv', 'gwmi', 'h', 'history', 'icm','iex', 'ihy', 'ii', 'ipal', 'ipcsv', 'ipmo','ipsn','irm', 'ise','iwmi', 'iwr', 'kill', 'lp', 'ls', 'man', 'md', 'measure', 'mi', 'mount', 'move','mp', 'mv', 'nal','ndr', 'ni', 'nmo','npssc', 'nsn', 'nv', 'ogv', 'oh', 'popd', 'ps', 'pushd', 'pwd', 'r', 'rbp', 'rcjb', 'rcsn', 'rd', 'rdr', 'refreshenv', 'ren','ri', 'rjb', 'rm', 'rmdir', 'rmo', 'rni','rnp', 'rp', 'rsn', 'rsnp', 'rujb','rv', 'rvpa', 'rwmi', 'sajb', 'sal', 'saps', 'sasv', 'sbp', 'sc', 'scb', 'select', 'set', 'shcm', 'si', 'sl', 'sleep','sls', 'sort', 'sp', 'spjb', 'spps','spsv', 'start', 'stz', 'sujb', 'sv', 'swmi', 'tee', 'trcm', 'type', 'wget', 'where', 'wjb', 'write', '%', '?' ) #endregion #region CloudShell commands function Add-CloudShellTelemetry { param( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$Name, [Parameter(Mandatory=$true)] [double]$Value ) # Calling TrackMetric API to log the metric to AppInsights service. $script:AppInsightTrackMetric.TrackMetric($Name, $Value, $script:CommonMetricProperties) } function Set-CloudShellPSReadLineKeyHandler { # PSReadline module may be unloaded or gets deleted from user profiles if(-not (Microsoft.PowerShell.Core\Get-Module -Name PSReadline)) { return } # PSReadline follows Emacs keybindings (https://en.wikipedia.org/wiki/GNU_Readline), `Alt+F` and `Alt+B` are used for # moving backward and forward word by word. However MacBook enables Ctrl+ArrowLeft` and `Ctrl+ArrowRight` by default and cloudshell # Bash supports them too. Therefore we add these keyhandlers in PowerShell cloudshell. PSReadline\Set-PSReadLineKeyHandler -Chord CTRL+LeftArrow BackwardWord PSReadline\Set-PSReadLineKeyHandler -Chord CTRL+RightArrow ForwardWord # The darkgray color currently in use is not readable for Parameters and Operators so change them to light gray. PSReadline\Set-PSReadLineOption -Colors @{ 'Parameter'="$([char]0x1b)[38;2;150;150;150m" 'Operator'="$([char]0x1b)[38;2;150;150;150m" } # Fix Bug 2271907 - OS and Browser coverage: cut&paste to PowerShell Cloudshell reverse ordered on Mac/Chrome, firefox # Root Cause: Firefox/Linux/Mac uses LF(LineFeed) for newline; while other cases use CRLF(Carriage Return & Line Feed). # LF is interpreted as Ctrl+Enter and triggers InsertLineAbove which pushes each text line down as the paste is getting processed one char at a time, # CRLF works fine because CR triggers AcceptLine. # To fix it, need to remove the keybinding for Ctrl+Enter of InsertLineAbove. # This keyBound doesn't exist DefaultEmacsBindings, only in DefaultWindowsBindings. This explains why bash works fine. PSReadline\Remove-PSReadlineKeyHandler -Key Ctrl+Enter # Set historyhandler for telemetry if(-not (PSReadline\Get-PSReadlineOption).AddToHistoryHandler) { # Control the size of ConsoleHost_history.txt to workaround the limit its size issue https://github.com/lzybkr/PSReadLine/issues/537 try { # turn off history PSReadline\Set-PSReadlineOption -HistorySaveStyle SaveNothing $historyPath = (PSReadline\Get-PSReadlineOption).HistorySavePath if(Microsoft.PowerShell.Management\Test-Path -Path $historyPath) { $historyContent = Microsoft.PowerShell.Management\Get-Content -Path $historyPath # 'Count' property does not exist if the PSReadline HistorySavePath contains 1 record only $script:CurrentConsoleHostHistorycount = if($historyContent.GetType().Name -eq 'String'){1} else{$historyContent.Count} if($script:CurrentConsoleHostHistorycount -gt $script:MaximumHistoryCount) { $tempContent = Microsoft.PowerShell.Management\Get-Content -Path $historyPath -Tail $script:MaximumHistoryCount -ReadCount $script:MaximumHistoryCount Microsoft.PowerShell.Management\Set-Content -Path $historyPath -Value $tempContent -Force $script:CurrentConsoleHostHistorycount = $script:MaximumHistoryCount } } } catch { Write-Warning "$_" return } finally { # reset back to default PSReadline\Set-PSReadlineOption -HistorySaveStyle SaveIncrementally } PSReadline\Set-PSReadlineOption -AddToHistoryHandler $PSReadlineHistoryHandler } } # Set a callback to PSReadline $PSReadlineHistoryHandler={ param ( [Parameter(Mandatory=$true)] [string]$Line ) try { # Skip commands from ConsoleHost_history.txt if ($script:HistoryIndex -le $script:CurrentConsoleHostHistorycount) { $script:HistoryIndex++ return $true } try { $cmdline=[ScriptBlock]::Create($Line) } catch{ # ignore commands PowerShell cannot interpret such as $env:, $home/helloworld.ps1 return $true } $commandInfo=$cmdline.Ast.FindAll({$args[0] -is [System.Management.Automation.Language.CommandAst]}, $true) if(-not $commandInfo) { # returning true means the command goes to the history that's the default behavior. return $true } $commandName=$commandInfo.GetCommandName() if(-not $commandName) { return $true } if($commandInfo.Count -gt 1) { # $commandName can be Object[] $commandName.foreach{ if(-not [System.String]::IsNullOrWhiteSpace($_)) { # call the function if the command is not empty or whitespaces Add-CloudShellCustomEvent -CommandName $_ } } } else { Add-CloudShellCustomEvent -CommandName $commandName } } catch { Write-Warning "`n$_" } return $true } function Add-CloudShellCustomEvent { param ( [Parameter(Mandatory=$true)] [string]$CommandName ) $command = Microsoft.PowerShell.Core\Get-Command -Name $commandName -ErrorAction Ignore if(-not $command) { # does nothing for null command return $true } # Builtin aliases and tools if(($script:BuiltinAliases -contains $command.Name) -or ($script:Tools -contains $command.Name)) { $script:AppInsightsTrackEvent.TrackEvent($command.Name, $script:CommonMetricProperties, $null) return $true } # Workaround a PowerShell issue: Run 'Get-Command ?' returns an array object, expected a single command object if($CommandName -eq '?') { $script:AppInsightsTrackEvent.TrackEvent($CommandName, $script:CommonMetricProperties, $null) return $true } $eventName = $null # Check $command.Source for some builtin cmdlets such as Get-Module that returns null from (get-command get-module).Module # On linux, the builtin commands are listed in BuiltinModuleNames if($command.Source -and ($script:BuiltinModuleNames -contains $command.Source)) { # Use $command.Name instead of $commandName because it's normalized for case sensitivity $eventName = Microsoft.PowerShell.Management\Join-Path $command.Source $command.Name } # Handling PowerShell modules elseif($command.Module) { # Builtin modules from programfiles if($script:DefaultInstalledModules -contains $command.ModuleName) { $eventName = Microsoft.PowerShell.Management\Join-Path $command.ModuleName $command.Name } else { # Handling inbox modules or those from PowerShellGallery $modulePath=$command.Module.ModuleBase if($modulePath) { if (($modulePath -like '/usr/local/share/powershell/Modules/*') -or ($modulePath -like '/opt/microsoft/powershell/*')) { $eventName = Microsoft.PowerShell.Management\Join-Path $command.ModuleName $command.Name } else { $psgetModuleInfoFile=Microsoft.PowerShell.Management\Join-Path -Path $modulePath -ChildPath PSGetModuleInfo.xml if(Microsoft.PowerShell.Management\Test-Path $psgetModuleInfoFile) { $content = Microsoft.PowerShell.Management\Get-Content $psgetModuleInfoFile if(($content -match '<S.*RepositorySourceLocation.*powershellgallery.com.*/S>') -or ($content -match '<S.*RepositorySourceLocation.*www.poshtestgallery.com.*/S>')) { $eventName = Microsoft.PowerShell.Management\Join-Path $command.ModuleName $command.Name } } } } } } #Handling scripts elseif($command.Source -and $command.Name) { $psScript=[System.IO.Path]::GetFileNameWithoutExtension($command.Name) $psgetScriptInfoFile=[System.IO.Path]::Combine((Microsoft.PowerShell.Management\Split-Path $command.Source -Parent), 'InstalledScriptInfos', "$($psScript)_InstalledScriptInfo.xml") if(Microsoft.PowerShell.Management\Test-Path $psgetScriptInfoFile) { $content = Microsoft.PowerShell.Management\Get-Content $psgetScriptInfoFile if(($content -match '<S.*RepositorySourceLocation.*powershellgallery.com.*/S>') -or ($content -match '<S.*RepositorySourceLocation.*dtlgalleryint.cloudapp.net.*/S>')) { $eventName = $command.Name } } } if($eventName) { #Syntax: TrackEvent(name, properties, metrics) $script:AppInsightsTrackEvent.TrackEvent($eventName, $script:CommonMetricProperties, $null) return $true } } $script:cloudshellTempDir = $null $script:tempPath = [System.IO.Path]::GetTempPath(); function Export-File { <# .SYNOPSIS Exports files and directories from your cloudshell to your local machine. .PARAMETER Path Specifies, as a string array, the path to the items to be exported. Supports multiple items. .PARAMETER LiteralPath Specifies, as a string array, the path to the items to be exported. Unlike Path, the value of the LiteralPath is used exactly as it is typed. Supports multiple items. .PARAMETER InputObject Specifies the object to be exported. Enter a variable that contains the objects, or type a command or expression that gets the objects. You can also pipe objects to Export-File. .EXAMPLE Export-File -path ~/hello.ps1 Export-File -path ~/hello*.ps1 Export-File -path ~/h1.ps1, ~/h2.ps1 Export-File -path ~/hellofolder dir *.txt | Export-File -Verbose "Hello World!" | Export-File #> [CmdletBinding(DefaultParameterSetName='ByPath', SupportsShouldProcess = $true)] param ( [Parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ByPath')] [string[]]$Path, [Parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ByLiteralPath')] [string[]]$LiteralPath, [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ParameterSetName = 'ByObject')] [object]$InputObject ) Begin { if ($($PSVersionTable.PSEdition -eq 'Desktop') -or $IsWindows) { $message = $LocalizedData.OSNotSupported -f ('Export-File', 'Windows', 'Linux') ThrowError -ExceptionName "System.NotSupportedException" ` -ExceptionMessage $message ` -ErrorId "OSNotSupported" ` -CallerPSCmdlet $PSCmdlet ` -ErrorCategory InvalidOperation ` -ExceptionObject $PSCmdlet } $objects=@() $isLiteralPath = $false } Process { if ($PSCmdlet.ParameterSetName -eq 'ByObject') { if ($InputObject -is [System.IO.FileSystemInfo]) { # Handling cases like dir *.txt | Export-File; Get-Item hello.ps1 | Export-File Copy-FromCloudShell -Item $InputObject } else { if ($InputObject.PSObject.Properties['Path']) { # Handling cases where Path is from pipeline. # Note when both ValueFromPipeline and ValueFromPipelineByPropertyName exist, ValueFromPipeline takes precedence. # Thus we need to handle them explicitly here $Path += $InputObject.Path } elseif ($InputObject.PSObject.Properties['LiteralPath']) { # Handling cases where Path is from pipeline $Path += $InputObject.LiteralPath $isLiteralPath = $true } else { # Handling cases like "hello world | Export-File $objects += $InputObject } } } } End { if ($LiteralPath) { $isLiteralPath = $true $Path = $LiteralPath } if ($Path) { foreach ($each in $Path) { $items = if ($isLiteralPath ){Microsoft.PowerShell.Management\Get-Item -LiteralPath $each} else {Microsoft.PowerShell.Management\Get-Item -Path $each} # Note the $items can be a single item or a collection if $each contains wildcard. foreach ($item in $items) { Copy-FromCloudShell -Item $item } } } elseif ($objects.Count -ne 0) { # Exporting content texts. The temp file name follows cloudshell-<randomname> format. Set-TempDirectory $newName = 'cloudshell-' + [System.IO.Path]::GetRandomFileName() $tmp = Microsoft.PowerShell.Management\Join-Path -Path $script:cloudshellTempDir -ChildPath $newName Microsoft.PowerShell.Management\Set-Content -Path $tmp -Value $objects -Force if (Microsoft.PowerShell.Management\Test-Path -Path $tmp) { Write-Verbose "Downloading $tmp" -Verbose download $tmp } } } } function Copy-FromCloudShell { <# .SYNOPSIS An internal private help function that downloads files and directories from the cloudshell. #> param ( [System.IO.FileSystemInfo] $Item ) # Handle files under filesystem provider only if (-not (($Item.PSPath).StartsWith('Microsoft.PowerShell.Core\FileSystem'))) { return } # Limitation: The download command supoprts $home or tmp directory only. if (-not ($Item.FullName.StartsWith($HOME) -or $Item.FullName.StartsWith($script:tempPath))) { $message = $LocalizedData.PathNotSupported -f ('Export-File', "$HOME or $script:tempPath") ThrowError -ExceptionName "System.NotSupportedException" ` -ExceptionMessage $message ` -ErrorId "PathNotSupported" ` -CallerPSCmdlet $PSCmdlet ` -ErrorCategory InvalidOperation ` -ExceptionObject $PSCmdlet return } if ($item -is [System.IO.FileInfo]) { # item is a file, go ahead to download. Note: No check is necessary because get-item has done that. $fullPath = $Item.FullName Write-Verbose "Downloading $fullPath" -Verbose download "$fullPath" # Workaround due to the bug in download command Microsoft.PowerShell.Utility\Start-Sleep -Seconds 1 } elseif ($item -is [System.IO.DirectoryInfo]) { # Exporting a directory. The directory will be zipped and filename is cloudshell-<foldername>.zip Set-TempDirectory $fullPath = $Item.FullName $filename = 'cloudshell-' + $Item.Name + '.zip' $tmp = Microsoft.PowerShell.Management\Join-Path -Path $script:cloudshellTempDir -ChildPath $filename write-verbose "running jar cMvf $tmp -C $fullPath ." # Zip the directory. Note using jar instead of zip is to ignore original top-level folder structure. $null = jar cMvf $tmp -C $fullPath . if ((Microsoft.PowerShell.Management\Test-Path -Path $tmp) -or (Microsoft.PowerShell.Management\Test-Path -LiteralPath $tmp)) { Write-Verbose "Downloading $tmp" -Verbose download $tmp } } else { # If -path contains text that should be treated -LiteralPath, but a user does not specify -LiteralPath # Get-Item will return empty results. We'll end up here. } } function Set-TempDirectory { <# .SYNOPSIS An internal private help function that sets up the directory for storing temporally files. #> if (-not $script:cloudshellTempDir) { # If the directory, $home\.tmp, does not exist, create it. # Note that the download command only works for files under home directory $script:cloudshellTempDir = Microsoft.PowerShell.Management\Join-Path -Path $Home -ChildPath '.tmp' } if (-not (Microsoft.PowerShell.Management\Test-Path -Path $script:cloudshellTempDir)) { $null = Microsoft.PowerShell.Management\New-Item -Path $script:cloudshellTempDir -ItemType Directory -Verbose } else { # Clean up old files $items = Microsoft.PowerShell.Management\Get-ChildItem -Path $script:cloudshellTempDir foreach ($item in $items) { if(-not $item.Name.StartsWith('cloudshell-')) { continue } # Files are expected to be downloaded less than 120 minutes $duration = ([System.DateTime]::Now - $item.LastAccessTime).TotalMinutes if ($duration -ge 120) { Write-Verbose "Removing $item" Remove-Item $item -Force -ErrorAction SilentlyContinue } } } } function Dismount-CloudDrive { <# .SYNOPSIS Dismounts Azure File storage share from the current session. .PARAMETER Force When this switch is set, no prompt to user for the confirmation before unmounting the fileshare. .INPUTS None You cannot pipe input to this command. .OUTPUTS None. .EXAMPLE Dismount-CloudDrive #> [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter()] [switch] $Force ) # We are not calling Get-CloudDrive before dismounting it because something like the following cases that changed in browsers # don't actually impact the user storageprofile information stored in backend (via RP). Thus we should allow to dismount even if # the user's curerent session is not in good state. # # Cases: # * In the cloudshell, a user deleted the clouddrive through net use /delete # * manually modified the env:ACC_STORAGE_PROFILE # $driveInfo = $LocalizedData.DismountingClouddrive if ($PSCmdlet.ShouldProcess($driveInfo)) { if ($Force -or $PSCmdlet.ShouldContinue($LocalizedData.DismountQueryMessage, $LocalizedData.DismountCaption)) { # These HTTP requests are in sync with bash src/images/agent/linux/clouddrive try { Write-Verbose $LocalizedData.UpdatingUserSettings $null=Invoke-WebRequest -Uri 'http://127.0.0.1:8888/userSettings' -Method 'DELETE' -UseBasicParsing -ContentType 'application/json' Write-Verbose $LocalizedData.DismountingClouddrive $null=Invoke-WebRequest -Uri 'http://127.0.0.1:8888/cloudshell' -Method 'DELETE' -UseBasicParsing -ContentType 'application/json' } catch { $message = $LocalizedData.DismountCloudDriveFailed -f $_ ThrowError -ExceptionName "System.InvalidOperationException" ` -ExceptionMessage $message ` -ErrorId "DismountCloudDriveFailed" ` -CallerPSCmdlet $PSCmdlet ` -ErrorCategory InvalidOperation ` -ExceptionObject $PSCmdlet } } } } function Get-CloudDrive { <# .SYNOPSIS List information of the Azure File storage share that is mounted as 'CloudDrive'. .PARAMETER None. .INPUTS None You cannot pipe input to this command. .OUTPUTS Returns the mounted file share information including subscription, resourcegroup and storageaccount. .EXAMPLE Get-CloudDrive #> [CmdletBinding()] param ( ) $clouddrivePath = Microsoft.PowerShell.Management\Join-Path -Path $HOME -ChildPath 'clouddrive' # Check if the clouddrive exists. This case will unlikely happen because there is a pop dialogbox during logon. # for Persist Account files https://docs.microsoft.com/en-us/azure/cloud-shell/overview. But we do check here anyway. if (-not (Test-Path $clouddrivePath)) { Write-Warning $LocalizedData.ClouddriveNotMounted return } # Check if the clouddrive folder exists. This case may occur when a user on purposely deleted the clouddrive through net use Y: /delete $null = Get-ChildItem $clouddrivePath -ErrorAction SilentlyContinue -ErrorVariable ev if($ev){ Write-Warning $LocalizedData.ClouddriveNotMounted return } # If a user delete $env:ACC_STORAGE_PROFILE, this error case can happen $AccStorageProfile = Microsoft.PowerShell.Management\Get-Item -Path env:ACC_STORAGE_PROFILE -ErrorAction SilentlyContinue -ErrorVariable ev if(-not $AccStorageProfile -or $ev){ $message = $LocalizedData.StorageProfileDoesnotExist -f $ev ThrowError -ExceptionName "System.InvalidOperationException" ` -ExceptionMessage $message ` -ErrorId "StorageProfileDoesnotExist" ` -CallerPSCmdlet $PSCmdlet ` -ErrorCategory InvalidData ` -ExceptionObject $PSCmdlet } # Check if the env is valid try { $json = Microsoft.PowerShell.Utility\ConvertFrom-Json $AccStorageProfile.Value } catch { $message = $LocalizedData.StorageProfileHasInvalidContent -f $_ ThrowError -ExceptionName "System.InvalidOperationException" ` -ExceptionMessage $message ` -ErrorId "StorageProfileHasInvalidContent" ` -CallerPSCmdlet $PSCmdlet ` -ErrorCategory InvalidData ` -ExceptionObject $AccStorageProfile } # storageProfile contract: $storageProfileFormat="{`"storageAccountResourceId`":`"/subscriptions/<id>/resourceGroups/<rgname>/providers/Microsoft.Storage/storageAccounts/<saName>`", `"fileShareName`": `"<myshare>`", `"diskSizeInGB`":5}." try { $fileshare = $json.fileShareName $objArray = $json.storageAccountResourceId -split "/" } catch { $message = $LocalizedData.StorageProfileHasUnsupportedJsonFormat -f ($storageProfileFormat, $_.Exception.Message) ThrowError -ExceptionName "System.InvalidOperationException" ` -ExceptionMessage $message ` -ErrorId "StorageProfileHasUnsupportedJsonFormat" ` -CallerPSCmdlet $PSCmdlet ` -ErrorCategory InvalidData ` -ExceptionObject $AccStorageProfile } # we expected subscription, resourcegroup and storageaccount are set in the storageAccountResourceId. Thus the length of $objArray is at least 6 if(-not $objArray -or $objArray.Count -lt 6) { $message = $LocalizedData.StorageAccountResourceIdHasUnsupportedJsonFormat -f ($storageProfileFormat) ThrowError -ExceptionName "System.InvalidOperationException" ` -ExceptionMessage $message ` -ErrorId "StorageAccountResourceIdHasUnsupportedJsonFormat" ` -CallerPSCmdlet $PSCmdlet ` -ErrorCategory InvalidData ` -ExceptionObject $AccStorageProfile } $subscription=$null $resourceGroup=$null $storageAccount=$null $stack = [system.collections.stack]::new() for($i=$objArray.Count - 1; $i -ge 0; $i--) { $stack.Push($objArray[$i]); } while ($stack.Count -gt 0) { $resourceInfo = $stack.Pop() switch($resourceInfo) { "subscriptions" { $subscription = $stack.Pop(); break } "resourceGroups" { $resourceGroup = $stack.Pop(); break } "storageAccounts" { $storageAccount = $stack.Pop(); break } } } # Check if all these variables are set. if(-not ($subscription -and $resourceGroup -and $storageAccount -and $fileshare)) { $message=$LocalizedData.MissingStorageProfileProperty -f ($subscription, $resourceGroup, $storageAccount, $fileshare) Write-Error -Message $message -Category InvalidData -ErrorId "MissingStorageProfileProperty" } $dirSeparatorChar = [System.IO.Path]::DirectorySeparatorChar $object= New-Object PSCustomObject -Property ([Ordered]@{ "FileShareName" = "$fileshare" "FileSharePath" = $dirSeparatorChar + [System.IO.Path]::Combine($dirSeparatorChar, "$storageAccount.file.core.windows.net", $fileshare) "MountPoint" = "$clouddrivePath" "Name" = "$storageAccount" "ResourceGroupName" = "$resourceGroup" "StorageAccountName" = "$StorageAccount" "SubscriptionId" = "$subscription" }) $object.pstypenames.Insert(0,'PSCloudShell.CloudDrive') return $object } function Get-CloudShellTip { <# .SYNOPSIS List CloudShell PowerShell Tips. .PARAMETER All Switch parameter to show All tips. When not specified, show only one tip randomly. .EXAMPLE Get-CloudShellTip Show one tip randomly. .EXAMPLE Get-CloudShellTip -All Show all tips. #> [CmdletBinding()] param ( [switch]$All ) $tipFilePath = Microsoft.PowerShell.Management\Join-Path -Path $PSScriptRoot -ChildPath "tips.json" $message = $null $tipsJson = Get-Content $tipFilePath | Microsoft.PowerShell.Utility\ConvertFrom-Json if($All) { $message = $tipsJson.items } else { $randomTip = $tipsJson.items | Microsoft.PowerShell.Utility\Get-Random if(-not [string]::IsNullOrEmpty($randomTip)) { $message = "MOTD: " + $randomTip $message = "`r`n" + $message + "`r`n"; } } return $message } #endregion #region Az commands function Get-AzVMPublicIPAddress { <# .DESCRIPTION Returns the public IPAddress or DNS Name of the Azure VM .PARAMETER Name Name of the Azure VM .PARAMETER ResourceGroupName Name of the resource group the VM belongs to .EXAMPLE Get-AzVMPublicIPAddress -Name AzureVmName -ResourceGroupName ResourceGroupName #> param ( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string]$Name, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string]$ResourceGroupName ) $azVM = Az.Compute\Get-AzVM -ResourceGroupName $ResourceGroupName -Name $Name -ErrorAction SilentlyContinue if(-not $azVM) { $message = $LocalizedData.GetAzureVMError -f ($Name) Write-Error -Message $message -ErrorId "AzureVMNameNotAvailable" -Category InvalidArgument return $azVM } $azVMNetInterfacesId = Split-Path -Leaf $azVM.NetworkProfile.NetworkInterfaces[0].Id $azVMInferface = Az.Network\Get-AzNetworkInterface -Name $azVMNetInterfacesId -ResourceGroupName $ResourceGroupName $azVMPublicIPId = Split-Path -Leaf $azVMInferface.IpConfigurations.PublicIpAddress.id $ipAddressObj = Az.Network\Get-AzPublicIpAddress -ResourceGroupName $ResourceGroupName -Name $azVMPublicIPId # Ip address can be 'not assigned', # e.g. when computer is shut down if($ipAddressObj.IpAddress -eq 'Not Assigned') { $message = $LocalizedData.GetAzureVMShutDown -f ($Name) Write-Error -Message $message -ErrorId "AzureVMShutDown" -Category ConnectionError return $null } $ComputerName = $ipAddressObj.IpAddress # If FQDN is not set, return the IP Address if($ipAddressObj.DnsSettings) { $fqdn = $ipAddressObj.DnsSettings.Fqdn if($fqdn) {$ComputerName = $fqdn} } $verboseMessage = $LocalizedData.GetAzureVMIPVerboseMsg -f ($Name, $ComputerName) Write-Verbose -Message $verboseMessage # Add this ComputerName to Trusted Host only on Windows OS # This ComputerName can be IP or FQDN Update-WinRMTrustedHosts -ComputerNameOrIPAddress $ComputerName $ComputerName } function Update-WinRMTrustedHosts { param( [Parameter(Mandatory=$true)] $ComputerNameOrIPAddress ) # WSMan:\ is applicable only in Windows environment if (-not $script:IsWindowsOS) { return } $trustedHosts = Get-Item WSMan:\localhost\Client\TrustedHosts if($trustedHosts.Value.Split(",") -notcontains $ComputerNameOrIPAddress) { $verboseMsgAddComputer = $LocalizedData.VerboseMsgAddComputer -f ($ComputerNameOrIPAddress) Write-Verbose -Message $verboseMsgAddComputer Set-Item WSMan:\localhost\Client\TrustedHosts -Value $ComputerNameOrIPAddress -Concatenate -Force } } function Invoke-AzVMCommand { <# .DESCRIPTION Invokes the given command on the list of given Azure VMs .PARAMETER Name Specifies the computername .PARAMETER ResourceGroupName Provide the name of the resource group .PARAMETER ScriptBlock The ScriptBlock that needs to be executed .PARAMETER Credential Provide Credential when connecting to Windows Targets .PARAMETER UserName Provide UserName when connecting to Linux Targets. When used with KeyFilePath parameter, identifies the user on the remote computer .PARAMETER KeyFilePath Provide SSH KeyFile Path when connecting to Linux Targets, if connection uses Key based authentication .EXAMPLE Invoke-AzVMCommand -Name WindowsVM -ResourceGroupName ResourceGroupName -ScriptBlock ScriptBlock -Credential credential .EXAMPLE Invoke-AzVMCommand -Name LinuxVM -ResourceGroupName ResourceGroupName -ScriptBlock ScriptBlock -UserName username .EXAMPLE Invoke-AzVMCommand -Name LinuxVM -ResourceGroupName ResourceGroupName -ScriptBlock ScriptBlock -UserName username -KeyFilePath ~/.ssh/id_rsa #> [CmdletBinding()] param ( [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string]$Name, [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string]$ResourceGroupName, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [ScriptBlock]$ScriptBlock, [Parameter(Mandatory, ParameterSetName='wsman')] [ValidateNotNullOrEmpty()] [PSCredential]$Credential, [Parameter(Mandatory, ParameterSetName='ssh')] [ValidateNotNullOrEmpty()] [string]$UserName, [Parameter(ParameterSetName='ssh')] [ValidateNotNullOrEmpty()] [string]$KeyFilePath ) if(-not (Test-AzResourceGroup -ResourceGroupName $ResourceGroupName)) { $badResourceGroup = $LocalizedData.TestAzResourceGroup -f ($ResourceGroupName) throw [System.ArgumentException] $badResourceGroup } $OsType = Get-OsType -Name $Name -ResourceGroupName $ResourceGroupName if (-not $OsType) { $badTargetVM = $LocalizedData.BadTargetVM throw [System.ArgumentException] $badTargetVM } $testAzVMParams = @{'Name'=$Name;'ResourceGroupName'=$ResourceGroupName;'OsType'=$OsType} if ([OStype]::Windows -eq $OsType) { if (-not $Credential) { $message = $LocalizedData.CredentialError throw [System.ArgumentException] $message } } $cName = Test-AzVM @testAzVMParams if(-not $cName) { $message = $LocalizedData.GetAzureVMError -f ($Name) throw [System.ArgumentException] $message } if ([OStype]::Windows -eq $OsType) { $invokeCommandParams = @{ ComputerName = $cName UseSSL = $true Credential = $Credential SessionOption = $script:sessionOption ScriptBlock = $ScriptBlock Authentication = 'Basic' } } elseif ([OStype]::Linux -eq $OsType) { $invokeCommandParams = @{'HostName'=$cName;'ScriptBlock'=$ScriptBlock} if ($KeyFilePath) { $invokeCommandParams.Add('KeyFilePath',$KeyFilePath) } if ($Credential -and $Credential.UserName) { $invokeCommandParams.Add('UserName',$Credential.UserName) } elseif ($UserName) { $invokeCommandParams.Add('UserName',$UserName) } else { $message = $LocalizedData.UserNameError throw [System.ArgumentException] $message } } Invoke-Command @invokeCommandParams -ErrorAction Stop } function Get-AzVmNsg { param ( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string]$Name, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string]$ResourceGroupName ) $azVM = Az.Compute\Get-AzVM -ResourceGroupName $ResourceGroupName -Name $Name if (-not $azVM) { $badTarget = $LocalizedData.GetAzureVMError -f ($Name) throw [System.ArgumentException] $badTarget } $nsg = @() $azVMNetInterfacesId = $azVM.NetworkProfile.NetworkInterfaces[0].Id $networkInterface = Split-Path $azVMNetInterfacesId -Leaf # Get Interface level NSG Az.Network\Get-AzNetworkSecurityGroup -ResourceGroupName $ResourceGroupName ` | Foreach-Object ` { ` if ($_.NetworkInterfaces -and ($_.NetworkInterfaces.Id -eq $azVMNetInterfacesId)) ` { $nsg+=$_ } ` } # Get Subnet Level NSG $vnets = Az.Network\Get-AzVirtualNetwork -ResourceGroupName $ResourceGroupName foreach ($vnet in $vnets) { $subnets = Az.Network\Get-AzVirtualNetworkSubnetConfig -VirtualNetwork $vnet foreach ($subnet in $subnets) { foreach ($ipConfiguration in $subnet.IpConfigurations) { if ($ipConfiguration.Id.Contains($networkInterface)) { if ($subnet.NetworkSecurityGroup) { $subnetNsgName = Split-Path $subnet.NetworkSecurityGroup.Id -Leaf if ($subnetNsgName) { $subnetNsg = Az.Network\Get-AzNetworkSecurityGroup -ResourceGroupName $ResourceGroupName -Name $subnetNsgName $nsg+=$subnetNsg break } } } } } } return $nsg } function Get-AzVMPSRemoting { <# .DESCRIPTION Gets the status of PowerShell Remoting on the given VM .PARAMETER Name Name of the Azure VM .PARAMETER ResourceGroupName Name of the Azure resource group .PARAMETER Nsg Network Security group object .EXAMPLE Get-AzVMPSRemoting -Name VmName -ResourceGroupName ResourceGroupName -Nsg <nsgObject> #> param ( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string]$Name, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string]$ResourceGroupName, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] $Nsg ) $protocol = @{https = $false; http = $false; ssh = $false} if($Nsg){ # Check if Nsg has inbound allow security rules with protocol (TCP) for WinRM Az.Network\Get-AzNetworkSecurityRuleConfig -NetworkSecurityGroup $Nsg | Where-Object { ($_.Access -eq 'Allow') -and ($_.Protocol -eq 'TCP') -and ($_.Direction -eq 'Inbound') -and ($_.SourceAddressPrefix -eq '*') -and ($_.SourcePortRange -eq '*') -and ($_.DestinationAddressPrefix -eq '*') } | ForEach-Object { # Port 5985 for http and 5986 for https and 22 for ssh if($_.DestinationPortRange -eq 5986){$protocol.https = $true} if($_.DestinationPortRange -eq 5985){$protocol.http = $true} if($_.DestinationPortRange -eq 22){$protocol.ssh = $true} } } $protocol } function Enable-AzVMPSRemoting { <# .DESCRIPTION Enables the Azure PSRemoting .PARAMETER Name Specifies the computername .PARAMETER ResourceGroupName Provide the name of the resource group .PARAMETER Protocol Provide the type of Protocol for nsg rule setup - http/https/ssh .PARAMETER OsType Option of windows/linux .EXAMPLE Enable-AzVMPSRemoting -Name vmName -ResourceGroup resourceGroup .EXAMPLE Enable-AzVMPSRemoting -Name vmName -ResourceGroup resourceGroup -OsType linux #> param ( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string]$Name, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string]$ResourceGroupName, [ValidateSet('http','https','ssh')] [string]$Protocol, [ValidateSet('windows','linux')] [OSType]$OsType ) # Since this is a standalone cmdlet and OsType is optional, it may not be supplied # So we make a REST call to retrieve the target OsType if (-not $OsType) { $OsType = Get-OsType -Name $Name -ResourceGroupName $ResourceGroupName } # Obtain all Network Security Groups associated with the VM (Interface level Nsg, Subnet Level Nsg) $azVMNsgs = Get-AzVmNsg -Name $Name -ResourceGroupName $ResourceGroupName $parameters = @{NetworkSecurityGroup = $null Protocol = 'Tcp' Direction = 'Inbound' Access = 'Allow' SourcePortRange = '*' SourceAddressPrefix = '*' DestinationAddressPrefix = '*' Priority = (Get-Random -Minimum 100 -Maximum 4096) } $azureVM = Az.Compute\Get-AzVM -Name $Name -ResourceGroupName $ResourceGroupName if ([OStype]::Windows -eq $OsType) { if (-not $Protocol) { # Only WinRM_HTTPS protocol is supported for Windows Target $Protocol = 'https' } foreach ($azVMNsg in $azVMNsgs) { # Get PSRemoting status for a given Nsg $psremoting = Get-AzVMPSRemoting -Name $Name -ResourceGroupName $ResourceGroupName -Nsg $azVMNsg $parameters['NetworkSecurityGroup'] = $azVMNsg # If Https is not enabled, enable it if($Protocol -eq 'https') { if (-not $psremoting.Https) { $null = Az.Network\Add-AzNetworkSecurityRuleConfig -Name 'allow-winrm-https' -DestinationPortRange 5986 @parameters | Az.Network\Set-AzNetworkSecurityGroup } # Setup WinRM HTTPS based remoting using Self-Signed Certificate $runCommandParameters = @{ commandId = 'RunPowerShellScript' script = @( 'Set-Item WSMan:\localhost\Service\Auth\Basic $true -Force;$selfSignedCert = New-SelfSignedCertificate -CertStoreLocation Cert:\LocalMachine\My -DnsName $env:COMPUTERNAME;Enable-PSRemoting -SkipNetworkProfileCheck -Force;Remove-Item -Path WSMan:\Localhost\listener\Listener* -Recurse -Force;New-Item -Path WSMan:\LocalHost\Listener -Transport HTTPS -Address * -CertificateThumbPrint $selfSignedCert.Thumbprint -Force;New-NetFirewallRule -DisplayName WindowsRemoteManagement_HTTPS_In -Name WindowsRemoteManagement_HTTPS_In -Profile Any -LocalPort 5986 -Protocol TCP -RemoteAddress Any;' ) } $null = Az.Resources\Invoke-AzResourceAction -ResourceId $azureVM.Id -Action runCommand -Parameters $runCommandParameters -ApiVersion 2017-03-30 -Force } # Future code path, if we support both http/https protocols for Windows target # If Http is not enabled, enable it if($Protocol -eq 'http') { if (-not $psremoting.Http) { $null = Az.Network\Add-AzNetworkSecurityRuleConfig -Name 'allow-winrm-http' -DestinationPortRange 5985 @parameters | Az.Network\Set-AzNetworkSecurityGroup } ################################################################### # Enable PowerShell remoting on a target Windows computer ################################################################### # Enable-PSRemoting -Force # Enable-PSRemoting configures the computer to receive Windows PowerShell remote commands that are sent by using WSMan protocol # Enable-PSRemoting performs the following operations: # 1) Runs the Set-WSManQuickConfig cmdlet, to: # Start the WinRM service. # Set the startup type on the WinRM service to Automatic. # Create a listener to accept requests on any IP address. # Enable a firewall exception for WS-Management communications. # Register the Microsoft.PowerShell and Microsoft.PowerShell.Workflow session configurations, if it they are not already registered. # Register the Microsoft.PowerShell32 session configuration on 64-bit computers, if it is not already registered. # Enable all session configurations. # Change the security descriptor of all session configurations to allow remote access. # 2) Restart the WinRM service to make the preceding changes effective. # Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name 'LocalAccountTokenFilterPolicy' -Value 1 -Type DWord -Force # When using local computer account for remoting, UAC (User Account Control) does not allow access to WinRM service. # Setting LocalAccountTokenFilterPolicy to 1 ensures UAC filtering for local accounts is disabled and access to WinRM service is granted. # Side Note: When using domain account for remoting, this account needs to be a member of the remote computer Administrators group. $runCommandParameters = @{ commandId = 'RunPowerShellScript' script = @( 'Set-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System -Name LocalAccountTokenFilterPolicy -Value 1 -Type DWord -Force;Enable-PSRemoting -Force;Set-NetFirewallRule -Name WINRM-HTTP-In-TCP-PUBLIC -RemoteAddress Any;' ) } $null = Az.Resources\Invoke-AzResourceAction -ResourceId $azureVM.Id -Action runCommand -Parameters $runCommandParameters -ApiVersion 2017-03-30 -Force } } } elseif ([OStype]::Linux -eq $OsType) { if (-not $Protocol) { # Only SSH protocol is supported for Linux Target $Protocol = 'ssh' } foreach ($azVMNsg in $azVMNsgs) { # Get PSRemoting status for a given Nsg $psremoting = Get-AzVMPSRemoting -Name $Name -ResourceGroupName $ResourceGroupName -Nsg $azVMNsg $parameters['NetworkSecurityGroup'] = $azVMNsg # If SSH is not enabled, enable it if($Protocol -eq 'ssh') { if (-not $psremoting.ssh) { $null = Az.Network\Add-AzNetworkSecurityRuleConfig -Name 'allow-ssh' -DestinationPortRange 22 @parameters | Az.Network\Set-AzNetworkSecurityGroup } # ThisRunCommand step does following: # 1) Install powershellcore in linux, if not already present # 2) backup current sshd_config, configure sshd_config to enable PasswordAuthentication, register powershell subsystem with ssh daemon # (#2 is required to support interactive username/password authentication over powershell-ssh) # 3) Restart the ssh daemon service to pick up the new config changes $runCommandParameters = @{ commandId = 'RunShellScript' script = @( 'sudo wget -q https://packages.microsoft.com/config/ubuntu/18.04/packages-microsoft-prod.deb;sudo dpkg -i packages-microsoft-prod.deb;sudo apt-get update;sudo apt-get install -y powershell;sshdconfigfile=/etc/ssh/sshd_config;sudo sed -re "s/^(\#)(PasswordAuthentication)([[:space:]]+)(.*)/\2\3\4/" -i.`date -I` "$sshdconfigfile";sudo sed -re "s/^(PasswordAuthentication)([[:space:]]+)no/\1\2yes/" -i.`date -I` "$sshdconfigfile";subsystem="Subsystem powershell /usr/bin/pwsh -sshs -NoLogo -NoProfile";sudo grep -qF -- "$subsystem" "$sshdconfigfile" || sudo echo "$subsystem" | sudo tee --append "$sshdconfigfile";sudo service sshd restart' ) } $null = Az.Resources\Invoke-AzResourceAction -ResourceId $azureVM.Id -Action runCommand -Parameters $runCommandParameters -ApiVersion 2017-03-30 -Force } } } } function Disable-AzVMPSRemoting { <# .DESCRIPTION Disables the Azure PSRemoting .PARAMETER Name Specifies the computername .PARAMETER ResourceGroupName Provide the name of the resource group .PARAMETER Protocol Provide the type of Protocol for nsg rule setup - http/https/ssh .PARAMETER OsType Option of windows/linux .EXAMPLE Disable-AzVMPSRemoting -Name VmName -ResourceGroup resourceGroup .EXAMPLE Disable-AzVMPSRemoting -Name VmName -ResourceGroup resourceGroup -Protocol http -OsType windows .EXAMPLE Disable-AzVMPSRemoting -Name VmName -ResourceGroup resourceGroup -Protocol ssh -OsType linux #> param ( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string]$Name, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string]$ResourceGroupName, [ValidateSet('http','https','ssh')] [string]$Protocol, [ValidateSet('windows','linux')] [OSType]$OsType ) # Since this is a standalone cmdlet and OsType is optional, it may not be supplied # So we make a REST call to retrieve the target OsType if (-not $OsType) { $OsType = Get-OsType -Name $Name -ResourceGroupName $ResourceGroupName } $azVMNsgs = Get-AzVmNsg -Name $Name -ResourceGroupName $ResourceGroupName if ([OStype]::Windows -eq $OsType) { if (-not $Protocol) { # Only WinRM_HTTPS protocol is supported for Windows Target $Protocol = 'https' } foreach ($azVMNsg in $azVMNsgs) { #Check if Nsg has allow security rules for port (5986 or 5985) and protocol (TCP) for WinRM Az.Network\Get-AzNetworkSecurityRuleConfig -NetworkSecurityGroup $azVMNsg | Where-Object {($_.Access -eq 'Allow') -and ($_.Protocol -eq 'TCP') -and ($_.Direction -eq 'Inbound') } | ForEach-Object { if( (($Protocol -eq 'http') -and ($_.DestinationPortRange -eq 5985)) -or (($Protocol -eq 'https') -and ($_.DestinationPortRange -eq 5986)) ){ $null = Az.Network\Remove-AzNetworkSecurityRuleConfig -Name $_.Name -NetworkSecurityGroup $azVMNsg | Az.Network\Set-AzNetworkSecurityGroup } } } # Disable PowerShell Remoting and restore UAC (User Account Control) setting $azureVM = Az.Compute\Get-AzVM -Name $Name -ResourceGroupName $ResourceGroupName $parameters = @{ commandId = 'RunPowerShellScript' script = @( "Disable-PSRemoting -Force; Set-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System -Name LocalAccountTokenFilterPolicy -Value 0 -Type DWord -Force;" ) } $null = Az.Resources\Invoke-AzResourceAction -ResourceId $azureVM.Id -Action runCommand -Parameters $parameters -ApiVersion 2017-03-30 -Force } elseif ([OStype]::Linux -eq $OsType) { if (-not $Protocol) { # Only SSH protocol is supported for Linux Target $Protocol = 'ssh' } foreach ($azVMNsg in $azVMNsgs) { # Remove Allow-ssh inbound rule for TCP, Port 22 Az.Network\Get-AzNetworkSecurityRuleConfig -NetworkSecurityGroup $azVMNsg | Where-Object {($_.Access -eq 'Allow') -and ($_.Protocol -eq 'TCP') -and ($_.Direction -eq 'Inbound') } | ForEach-Object { if( (($Protocol -eq 'ssh') -and ($_.DestinationPortRange -eq 22)) ){ $null = Az.Network\Remove-AzNetworkSecurityRuleConfig -Name $_.Name -NetworkSecurityGroup $azVMNsg | Az.Network\Set-AzNetworkSecurityGroup } } } # Restore to original SSH Daemon Config, restart sshd service to pick the config $azureVM = Az.Compute\Get-AzVM -Name $Name -ResourceGroupName $ResourceGroupName $parameters = @{ commandId = 'RunShellScript' script = @( 'sudo cp -f /etc/ssh/sshd_config_orig /etc/ssh/sshd_config;sudo service sshd restart' ) } $null = Az.Resources\Invoke-AzResourceAction -ResourceId $azureVM.Id -Action runCommand -Parameters $parameters -ApiVersion 2017-03-30 -Force } } function Get-AzCommand { <# .DESCRIPTION Gets the Azure Command relevant to the current path .PARAMETER Keyword Specifies the keyword to be used to find the Az Command .EXAMPLE Get-AzCommand Get-AzCommand -Keyword azureKeyWord #> [cmdletbinding()] param ( [Parameter(Mandatory=$false)] [string]$Keyword ) if($Keyword) { return GetCommand -Keyword $Keyword.ToLower() } $path = (Get-Location).Path $array = $path.Split([System.IO.Path]::DirectorySeparatorChar,[System.StringSplitOptions]::RemoveEmptyEntries) $start = $array.Count - 1 for($index = $start; $index -gt 1 ; $index--){ $curr = $array[$index] $str = $curr -replace ':' # Remove the Preceding 'Microsoft.' if($str.ToLower().StartsWith("microsoft.")) { $str = $str.ToLower() -replace "microsoft." } # If the string is a plural (ends with a s) # Remove the last s # Some cmdlets have singular names in them $str = $str.ToLower().TrimEnd('s') $commands = GetCommand -Keyword $str if($commands) { return $commands } } # Generic help $commands = GetCommand return $commands } function Test-AzResourceGroup { [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory=$true)] [string]$ResourceGroupName ) # Check if the resourcegroup exists $resourceGroup = Az.Resources\Get-AzResource -ErrorAction SilentlyContinue | Where-Object {$_.ResourceGroupName -eq $ResourceGroupName} if(-not $resourceGroup) { return $false } $verboseMessage = $LocalizedData.TestResourceGroup -f ($ResourceGroupName) Write-Verbose -Message $verboseMessage return $true } function Test-AzVM { param ( [Parameter(Mandatory=$true)] [string]$Name, [Parameter(Mandatory=$true)] [string]$ResourceGroupName, [Parameter(Mandatory=$true)] [OSType]$OsType ) $azVMNsgs = Get-AzVmNsg -Name $Name -ResourceGroupName $ResourceGroupName foreach ($azVMNsg in $azVMNsgs) { # Check if Remoting is enabled for the VM $psRemoting = Get-AzVMPSRemoting -Name $Name -ResourceGroupName $ResourceGroupName -Nsg $azVMNsg if ((([OStype]::Windows -eq $OsType) -and (-not $psRemoting.https)) -or (([OStype]::Linux -eq $OsType) -and (-not $psRemoting.ssh))) { $errMsg = $LocalizedData.TestAzPsRemotingError -f ($Name, $ResourceGroupName) throw $errMsg } } # Check the communication with the remote machine $cName = Get-AzVMPublicIPAddress -Name $Name -ResourceGroupName $ResourceGroupName if ($cName) { return $cName } return $null } #endregion #region Common Functions function Get-OsType { param ( [Parameter(Mandatory=$true)] [string]$Name, [Parameter(Mandatory=$true)] [string]$ResourceGroupName ) $vmInfo = Az.Compute\Get-AzVM -ResourceGroupName $ResourceGroupName -Name $Name if(-not $vmInfo) { $message = $LocalizedData.GetAzureVMError -f ($Name) throw [System.ArgumentException] $message } if ($vmInfo.OSProfile.WindowsConfiguration) { return [OStype]::Windows } elseif ($vmInfo.OSProfile.LinuxConfiguration) { return [OStype]::Linux } return $null } function Get-Help { [CmdletBinding(DefaultParameterSetName='AllUsersView', HelpUri='http://go.microsoft.com/fwlink/?LinkID=113316')] param( [Parameter(Position=0, ValueFromPipelineByPropertyName=$true)] [string]$Name, [string]$Path, [ValidateSet('Alias','Cmdlet','Provider','General','FAQ','Glossary','HelpFile','ScriptCommand','Function','Filter','ExternalScript','All','DefaultHelp','Workflow','DscResource','Class','Configuration')] [string[]]$Category, [string[]]$Component, [string[]]$Functionality, [string[]]$Role, [Parameter(ParameterSetName='DetailedView', Mandatory=$true)] [switch]$Detailed, [Parameter(ParameterSetName='AllUsersView')] [switch]$Full, [Parameter(ParameterSetName='Examples', Mandatory=$true)] [switch]$Examples, [Parameter(ParameterSetName='Parameters', Mandatory=$true)] [string]$Parameter, [Parameter(ParameterSetName='Online', Mandatory=$true)] [switch]$Online ) # If this function is called without -Name parameter, then show Cloud Shell help if (-not $Name) { $helpFilePath = Join-Path $PSScriptRoot "PSCloudShellUtility.Help.txt" if(Test-Path -Path $helpFilePath) { Get-Content -Path $helpFilePath } } # For all other cases, call built-in Get-Help cmdlet else{ if (($Name -eq "get-help") -and (-not $Category)){ $PSBoundParameters['Name'] = "Microsoft.PowerShell.Core\Get-Help"; } if($Online) { RedirectOnlineHelp $PSBoundParameters } else { Microsoft.PowerShell.Core\Get-Help @PSBoundParameters } } <# .ForwardHelpTargetName Get-Help .ForwardHelpCategory Cmdlet #> } function ThrowError { param ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.Management.Automation.PSCmdlet] $CallerPSCmdlet, [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $ExceptionName, [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $ExceptionMessage, [System.Object] $ExceptionObject, [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $ErrorId, [parameter(Mandatory = $true)] [ValidateNotNull()] [System.Management.Automation.ErrorCategory] $ErrorCategory ) $exception = New-Object $ExceptionName $ExceptionMessage; $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $ErrorId, $ErrorCategory, $ExceptionObject $CallerPSCmdlet.ThrowTerminatingError($errorRecord) } function GetCommand { param ( [parameter(Mandatory = $false)] $Keyword ) if(-not $Keyword) { $commands = Get-Command -Module Az.* return $commands } if(-not $script:acronymLookup) { $acronymContent = GetAcronymContent if(-not $acronymContent) { return $null } $script:acronymLookup = @{} $acronymContent.psobject.properties | ForEach-Object { $script:acronymLookup[$_.Name] = $_.Value } } $currKeyWord = if($script:acronymLookup.ContainsKey($Keyword)) { $script:acronymLookup.$Keyword } else { $Keyword } $currKeyWord = "*$currKeyWord*" $commands = Get-Command -Module Az.* -Name $currKeyWord -ErrorAction SilentlyContinue return $commands } function GetAcronymContent { # Get the acronyms file $path = Join-Path -Path $PSScriptRoot -ChildPath 'PSCloudShellUtilityAcronyms.json' if(Test-Path -Path $path) { # Get the content from the acronyms file $content = Get-Content -Path $path | ConvertFrom-Json return $content } return $null } function RedirectOnlineHelp($Parameters) { try { Microsoft.PowerShell.Core\Get-Help @Parameters } catch [System.Management.Automation.PSInvalidOperationException] { $err = $_ $matchFound = $err.Exception.Message -match "No program or browser is associated to open the URI (?<link>\w+:\/\/[\w@][\w.:@]+\/?[\w\.?=%&=\-@/$,]*)." if(($matchFound -eq $True) -and (-not [string]::IsNullOrEmpty($env:ACC_TERM_ID))) { $targetLink = $matches["link"] Write-Verbose "Redirecting to $targetLink" $uri = "http://localhost:8888/openLink/$($env:ACC_TERM_ID)" try{ $null = Invoke-RestMethod -Method Post -Uri $uri -Body "{""url"":""$targetLink""}" -ContentType "application/json" }catch{ Write-Warning -Message "Redirecting to $targetLink failed, please open the link in another page." throw } } else { # If the failure was caused by other reason, just throw it AS-IS. throw } } } function New-PackageInfo() { param( [string]$name, [string]$version, [string]$type ) return [PSCustomObject]@{ Name = $name Version = $version Type = $type } } function Get-PackageVersion() { <# .DESCRIPTION Report versions of all installed packages .EXAMPLE Get-PackageVersion | Where-Object Name -like "*emacs*" #> [CmdletBinding()] param() # Apt and some other programs write to stderr, which fails tests without this $ErrorActionPreference = "Continue" # Enumerate all APT packages with versions $packages = New-Object -TypeName System.Collections.ArrayList # TODO - find the regular expression to seperate the package name from the package version # apt list --installed 2> /dev/null | % { # Write-Verbose "Apt: $_" # if ($_ -match "([^/]*)/[^ ]* ([^ ]*)") { # $p = New-PackageInfo -Name $matches[1] -Version $matches[2] -Type "Apt" # $null = $packages.Add($p) # } # } # enumerate special packages $pwsh = New-PackageInfo -Name "PowerShell" -Version $PSVersionTable.PSVersion.ToString() -type "Special" $null = $packages.Add($pwsh) function Get-VersionFromCommand($package) { try { $output = Invoke-Expression "& $($package.command) $($package.Args) 2>/dev/null" } catch { return "Error" } $version = ($output | % { if ($_ -match $package.match) { Write-Verbose "matched $_" $matches[1]; } }) if ($null -eq $version) { $version = "Unknown"} return $version } $packageVersionDetections = @( @{displayname = "Node.JS"; command = "node"; args = "--version"; match = "v(.*)"}, @{displayname = "Cloud Foundry CLI"; command = "cf"; args = "-v"; match = "cf version (.*)"}, @{displayname = "Ansible"; command = "ansible"; args = "--version"; match = "ansible \[core ([\d\.]+)\]"}, @{displayname = "Istio"; command = "istioctl"; args = "version -s --remote=false"; match = "(.+)"}, @{displayname = "Go"; command = "go"; args = "version"; match = "go version go(\S+) .*"}, @{displayname = "DC/OS CLI"; command = "dcos"; args = "--version"; match = "dcoscli.version=(.*)"}, @{displayname = "Ripgrep"; command = "rg"; args = "--help | head"; match = "ripgrep ([\d\.]+)$"}, @{displayname = "Helm"; command = "helm"; args = "version --short"; match = "v(.+)"}, @{displayname = "AZCopy"; command = "azcopy"; args = "--version"; match = "azcopy version (.+)"}, @{displayname = "Azure CLI"; command = "az"; args = "version "; match = "`"azure-cli`": `"(.+)`""}, @{displayname = "Kubectl"; command = "kubectl"; args = "version --client=true | head -n 1"; match = "Client Version: v(.+)"} @{displayname = "Terraform"; command = "terraform"; args = "version"; match = "Terraform v(.+)"}, @{displayname = "GitHub CLI"; command = "gh"; args = "--version"; match = "gh version (.+) \(.*"}, @{displayname = "Azure Developer CLI"; command = "azd"; args = "version"; match = "azd version (\d+\.\d+\.\d+(-[\w\d\.]*)?).*"} ) foreach ($package in $packageVersionDetections) { Write-Verbose "$($package.displayname)" $version = Get-VersionFromCommand -Package $package $p = New-PackageInfo -Name $package.displayname -Version $version -type "Special" $null = $packages.Add($p) } # PIP3 packages & pip3 list | % { if ($_ -match "(\w+)\s+(.*)") { $p = New-PackageInfo -Name $matches[1] -Version $matches[2] -Type "PIP" $null = $packages.Add($p) } } # TODO Ruby Gems # $rubypackages = [ordered]@{} # & gem list --local | % { if ($_ -match "(\w+)\s+\((.*)\)") { $pippackages[$matches[1]] = $matches[2]}}} # PowerShell modules Get-Module -ListAvailable | % { $p = New-PackageInfo -name $_.Name -version $_.Version -type "PowerShell" $null = $packages.Add($p) } # NPM global modules $packages | sort-object -Property Type, Name }