azure-cli
azure-cli copied to clipboard
Add opt-in/opt-out for multiple monitors on `az network bastion rdp` command
Related command
az network bastion rdp --name MyBastionHost --resource-group MyResourceGroup --target-resource-id vmResourceId
Is your feature request related to a problem? Please describe. While connecting to an Azure resource through Azure Bastion Standard native client, generated RDP file will always use all monitors by default, which is not always desired when working with multiple screens.
Generated RDP file excerpt:
hostname:s:<serverName>
full address:s:<serverName>
alternate full address:s:<serverName>
use multimon:i:1
serverport:i:3389
gatewaycredentialssource:i:5
gatewayusagemethod:i:1
gatewayprofileusagemethod:i:1
gatewayhostname:s:<bastionPIPName>.bastion.azure.com
gatewayaccesstoken:s:<gatewayAccessToken>
signscope:s:Full Address,Alternate Full Address,GatewayHostname,GatewayUsageMethod,GatewayProfileUsageMethod,GatewayCredentialsSource
signature:s:<AuthSignature>
Describe the solution you'd like
Would be interesting to give option to administrator while connecting to bastion to opt-in or opt-out for the multiple monitor options based in a command switch (eg. az network bastion rdp --name MyBastionHost --resource-group MyResourceGroup --target-resource-id vmResourceId --use-multimon)
Describe alternatives you've considered
As generated RDP file comes from managed Remote Desktop Gateway, the solution would be to modify conn.rdp in rdp_bastion_host function to replace the integer value of use multimon to 0 in the presence or absence of a parameter to use multiple monitors, which should be modified in src/azure-cli/azure/cli/command_modules/network/custom.py.
As a workaround, whenever needed to use a single monitor, we can cancel connection and then replace value in conn.rdp file as snippet below
(Get-Content .\conn.rdp).Replace("use multimon:i:1","use multimon:i:0") | Set-Content .\conn.rdp
mstsc .\conn.rdp
Additional context N/A
route to CXP team
Thanks for the feedback! We are routing this to the appropriate team for follow-up. cc @aznetsuppgithub.
Issue Details
Related command
az network bastion rdp --name MyBastionHost --resource-group MyResourceGroup --target-resource-id vmResourceId
Is your feature request related to a problem? Please describe. While connecting to an Azure resource through Azure Bastion Standard native client, generated RDP file will always use all monitors by default, which is not always desired when working with multiple screens.
Generated RDP file excerpt:
hostname:s:<serverName>
full address:s:<serverName>
alternate full address:s:<serverName>
use multimon:i:1
serverport:i:3389
gatewaycredentialssource:i:5
gatewayusagemethod:i:1
gatewayprofileusagemethod:i:1
gatewayhostname:s:<bastionPIPName>.bastion.azure.com
gatewayaccesstoken:s:<gatewayAccessToken>
signscope:s:Full Address,Alternate Full Address,GatewayHostname,GatewayUsageMethod,GatewayProfileUsageMethod,GatewayCredentialsSource
signature:s:<AuthSignature>
Describe the solution you'd like
Would be interesting to give option to administrator while connecting to bastion to opt-in or opt-out for the multiple monitor options based in a command switch (eg. az network bastion rdp --name MyBastionHost --resource-group MyResourceGroup --target-resource-id vmResourceId --use-multimon)
Describe alternatives you've considered
As generated RDP file comes from managed Remote Desktop Gateway, the solution would be to modify conn.rdp in rdp_bastion_host function to replace the integer value of use multimon to 0 in the presence or absence of a parameter to use multiple monitors, which should be modified in src/azure-cli/azure/cli/command_modules/network/custom.py.
As a workaround, whenever needed to use a single monitor, we can cancel connection and then replace value in conn.rdp file as snippet below
(Get-Content .\conn.rdp).Replace("use multimon:i:1","use multimon:i:0") | Set-Content .\conn.rdp
mstsc .\conn.rdp
Additional context N/A
| Author: | davi-cruz |
|---|---|
| Assignees: | - |
| Labels: |
|
| Milestone: | Backlog |
@davi-cruz Thanks for the feedback! Looks like the RDP file content is retrieved from an API on the service side, so we are routing this to the service team for review.
I would like to upvote this, if possible. It's annoying to have to connect to get a conn.rdp file, then disconnect, edit the file and reconnect. Would be nice to be able to modify those settings as part of the command.
The issue still persist with Azure CLI version 2.40
Adding my voice to please allow customizing the RDP session.
Adding a vote for me and my team. Still not possible with Azure CLI v2.4.1. Forcing multiple monitors makes support calls particularly challenging.
It's really bothersome, please prioritize this. It was working in older version looks like a regression.
Thank you for your feedback. This has been routed to the support team for assistance.
Just a workaround works for me.
$ResourceGroupName = "xxxxxxxx"
$BastionName = "bast-xxxxxxxx"
$ResourceId = "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/xxxxxxxx/providers/Microsoft.Compute/virtualMachines/vm-xxxxxxxx"
$Token = (Get-AzAccessToken).Token
$Headers = @{
'Content-Type' = 'application/json'
'Authorization' = "Bearer $token"
}
$BastionDnsName = (Get-AzBastion -ResourceGroupName $ResourceGroupName -Name $BastionName).DnsName
$(Invoke-RestMethod -Method Get `
-Uri "https://$BastionDnsName/api/rdpfile?resourceId=$ResourceId&format=rdp" `
-Headers $headers).Replace("use multimon:i:1","") | Out-File .\conn.rdp
mstsc .\conn.rdp
Just a workaround works for me.
$ResourceGroupName = "xxxxxxxx" $BastionName = "bast-xxxxxxxx" $ResourceId = "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/xxxxxxxx/providers/Microsoft.Compute/virtualMachines/vm-xxxxxxxx" $Token = (Get-AzAccessToken).Token $Headers = @{ 'Content-Type' = 'application/json' 'Authorization' = "Bearer $token" } $BastionDnsName = (Get-AzBastion -ResourceGroupName $ResourceGroupName -Name $BastionName).DnsName $(Invoke-RestMethod -Method Get ` -Uri "https://$BastionDnsName/api/rdpfile?resourceId=$ResourceId&format=rdp" ` -Headers $headers).Replace("use multimon:i:1","") | Out-File .\conn.rdp mstsc .\conn.rdp
' Thanks for sharing your code with us @skmkzyk. My expectation is that a similar adjustment be made in az-cli source code, not requiring us to need this kind of workaround anymore, which is simpler to implement than any service API change.
Thanks all for the feedback and @skmkzyk for the workaround! We have worked up a PR that adds support for a new flag (--configure) that would open up the Remote Desktop Connection UI instead of directly connecting, allowing you make changes to the session configuration.
@PramodValavala-MSFT would it be possible to add an option to simply output the .RDP file to a specified location? This would be useful for integration in Remote Desktop Manager. That code snippet shared by @skmkzyk looks much better than the original az cli command, I think I'll use this for now.
I made a PowerShell code snippet derived from @skmkzyk that filters out unwanted .RDP file properties and that should be relatively easy to modify to inject custom options. As a bonus, it doesn't use the az cli at all. Only edit the few variables at the beginning of the script for the (VMName, ResourceGroupName, BastionName), the rest is generic:
$VMName = "RDM-WOA"
$ResourceGroupName = "TestWoa"
$BastionName = "TestWoA-vnet-bastion"
@('Az.Accounts','Az.Network') | Import-Module
$SubscriptionId = (Get-AzContext).Subscription.Id
$VMResourceID = "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.Compute/virtualMachines/$VMName"
$BastionDnsName = (Get-AzBastion -ResourceGroupName $ResourceGroupName -Name $BastionName).DnsName
$Params = @{
Method = 'GET'
Uri = "https://$BastionDnsName/api/rdpfile?resourceId=$VMResourceId&format=rdp"
Headers = @{
'Content-Type' = 'application/json'
'Authorization' = "Bearer $((Get-AzAccessToken).Token)"
}
}
$RdpExclude = @('use multimon','signscope','signature')
$RdpFile = (Invoke-RestMethod @Params).Split("`n") | Where-Object {
if (-Not [string]::IsNullOrEmpty($_)) {
($Name, $Type, $Value) = $_.Split(':', 3, [System.StringSplitOptions]::TrimEntries)
(-Not $RdpExclude.Contains($Name))
}
}
$RdpFilePath = Join-Path $Env:TEMP "$VMName.rdp"
$RdpFile | Out-File $RdpFilePath
& 'mstsc.exe' $RdpFilePath
@PramodValavala-MSFT would it be possible to add an option to simply output the .RDP file to a specified location? This would be useful for integration in Remote Desktop Manager. That code snippet shared by @skmkzyk looks much better than the original az cli command, I think I'll use this for now.
This should be possible. Could you create a new issue to track this feature request?
Based on the snippet that you created, I would say an option to simply fetch the RDP file and save it to a location without opening it is what you need and you would modify it based on your needs as part a script. Correct?
@PramodValavala-MSFT would it be possible to add an option to simply output the .RDP file to a specified location? This would be useful for integration in Remote Desktop Manager. That code snippet shared by @skmkzyk looks much better than the original az cli command, I think I'll use this for now.
This should be possible. Could you create a new issue to track this feature request?
Based on the snippet that you created, I would say an option to simply fetch the RDP file and save it to a location without opening it is what you need and you would modify it based on your needs as part a script. Correct?
Actually, the only reason I've looked into az cli and not Azure PowerShell is because it's not supported in Azure PowerShell, which is really annoying. I think it should be supported in both, with the ability to fetch the .RDP file directly, without launching an external program like mstsc. Now that I can make the calls directly with Azure PowerShell, I don't need az cli anymore.
I made an initial Azure Bastion native client access integration in Remote Desktop Manager using a pre-connection PowerShell script, which is good enough for a lot of customers that are waiting for a better integration and just need something that works today: https://twitter.com/awakecoding/status/1586069007119003648
Here is the forum thread where I have posted the solution for customers that were asking for it: https://forum.devolutions.net/topics/37294/rdp-via-azure-bastion-native-client#167612
I've been looking for a contact at Microsoft who could help us get a proper integration in place. What I built today is only a temporary solution, now we need to plan the work for a proper built-in integration that doesn't rely on Azure PowerShell, az cli or external scripts. Our goal is to make it possible to attach Azure Bastion to supported connection entries in Remote Desktop Manager such that the user really has no additional manual steps to open connections. In the end, it should be as simple as "double click and it just connects".
I don't want to hijack this issue much further and would like to take this discussion offline. Can you help me get in touch with the right people in the Azure Bastion team to discuss third-party integration? Either share your email, or send me an email at mamoreau [at] devolutions.net. I've asked some of my contacts at Microsoft who to contact and tried sending an email to Sandeep Deo two months ago about this but never got a reply. I would love to collaborate with Microsoft on Remote Desktop Manager integration, but also provide feedback on how Azure Bastion usability could be improved with just a few tweaks.
Just adding below code to your profile.ps1, I can use Connect-MyAzBastionRdp (or any name you want) cmdlet to connect to a VM.
Thanks @awakecoding!
Function Connect-MyAzBastionRdp() {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)] [string] $VMName,
[Parameter(Mandatory = $true)] [string] $ResourceGroupName,
[Parameter(Mandatory = $true)] [string] $BastionName
)
@('Az.Accounts', 'Az.Network') | Import-Module
$SubscriptionId = (Get-AzContext).Subscription.Id
$VMResourceID = "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.Compute/virtualMachines/$VMName"
$BastionDnsName = (Get-AzBastion -ResourceGroupName $ResourceGroupName -Name $BastionName).DnsName
$Params = @{
Method = 'GET'
Uri = "https://$BastionDnsName/api/rdpfile?resourceId=$VMResourceId&format=rdp"
Headers = @{
'Content-Type' = 'application/json'
'Authorization' = "Bearer $((Get-AzAccessToken).Token)"
}
}
$RdpExclude = @('use multimon', 'signscope', 'signature')
$RdpFile = (Invoke-RestMethod @Params).Split("`n") | Where-Object {
if (-Not [string]::IsNullOrEmpty($_)) {
($Name, $Type, $Value) = $_.Split(':', 3, [System.StringSplitOptions]::TrimEntries)
(-Not $RdpExclude.Contains($Name))
}
}
$RdpFilePath = Join-Path $Env:TEMP "$VMName.rdp"
$RdpFile | Out-File $RdpFilePath
& 'mstsc.exe' $RdpFilePath
}
To use it;
Connect-MyAzBastionRdp -VMName vm-ad01 -ResourceGroupName active-directory -BastionName bast-hub00
@skmkzyk that's a nice cleaned-up version of the code snippet! In an effort to try and reduce the amount of (long) parameters one has to type in, I've assumed that the resource group name of the Azure Bastion host and the target Azure VM is the same, but I guess it's technically possible that they would be in two separate resource groups? I'm trying to figure out the best way to handle all the defaults without having to be so explicit about it, yet allow explicitly specifying non-default values. I also wonder if there would be a clever way to figure out which Azure Bastion instance is associated with a specific Azure VM, such that one could pass only Azure VM information, and let the cmdlet throw an error if no Azure Bastion instance can be found.
As far as I investigated, Azure Portal utilizes Resource Graph to examine whether there's available Bastion to connect the VM. (I don't sure in detail, but it returns a Bastion on peered VNet.)
Resources
| where type =~ 'Microsoft.Network/bastionHosts' and properties.ipConfigurations[0].properties.subnet.id startswith '/subscriptions/xxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/resoure-group02/providers/Microsoft.Network/virtualNetworks/vnet-xxxx/'
| project id, location, name, sku, properties, type, vnetid = '/subscriptions/xxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/resoure-group02/providers/Microsoft.Network/virtualNetworks/vnet-xxxx'
| union (Resources
| where id =~ '/subscriptions/xxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/resoure-group02/providers/Microsoft.Network/virtualNetworks/vnet-xxxx'
| mv-expand peering=properties.virtualNetworkPeerings
| project vnetid = tolower(tostring(peering.properties.remoteVirtualNetwork.id))
| join kind=inner (Resources
| where type =~ 'microsoft.network/bastionHosts'
| extend vnetid=tolower(extract('(.*/virtualnetworks/[^/]+)/', 1, tolower(tostring(properties.ipConfigurations[0].properties.subnet.id)))))
on vnetid)
And as you know, it needs to get VNet info from its NIC beforehand.
I think it'll be too much, so I'll go simpler to add optional $VMResourceGroupName parameter. And if not present, set it same as $ResourceGroupName.
Function Connect-MyAzBastionRdp() {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)] [string] $VMName,
[string] $VMResourceGroupName,
[Parameter(Mandatory = $true)] [string] $ResourceGroupName,
[Parameter(Mandatory = $true)] [string] $BastionName
)
if ([string]::IsNullOrEmpty($VMResourceGroupName)) {
$VMResourceGroupName = $ResourceGroupName
}
@('Az.Accounts', 'Az.Network') | Import-Module
$SubscriptionId = (Get-AzContext).Subscription.Id
$VMResourceID = "/subscriptions/$SubscriptionId/resourceGroups/$VMResourceGroupName/providers/Microsoft.Compute/virtualMachines/$VMName"
$BastionDnsName = (Get-AzBastion -ResourceGroupName $ResourceGroupName -Name $BastionName).DnsName
$Params = @{
Method = 'GET'
Uri = "https://$BastionDnsName/api/rdpfile?resourceId=$VMResourceId&format=rdp"
Headers = @{
'Content-Type' = 'application/json'
'Authorization' = "Bearer $((Get-AzAccessToken).Token)"
}
}
$RdpExclude = @('use multimon', 'signscope', 'signature')
$RdpFile = (Invoke-RestMethod @Params).Split("`n") | Where-Object {
if (-Not [string]::IsNullOrEmpty($_)) {
($Name, $Type, $Value) = $_.Split(':', 3, [System.StringSplitOptions]::TrimEntries)
(-Not $RdpExclude.Contains($Name))
}
}
$RdpFilePath = Join-Path $Env:TEMP "$VMName.rdp"
$RdpFile | Out-File $RdpFilePath
& 'mstsc.exe' $RdpFilePath
}
Sharing my modified version below.
You just need to provide the resource ID for the VM and Bastion. In my case, I've only got one Bastion and so there's an option to hardcode the resource ID for your one and only Bastion and default to using that.
I've also added a few lines to force the RDP session to not go full screen and go in window mode, so that it doesn't cover the start menu of your local machine, as well as allow clipboard and local drives redirection. Haven't fully tested though, but should work as I took these RDP settings directly out of my existing and working RDP file. More info on this can be found here - https://www.donkz.nl/overview-rdp-file-settings/
Function Connect-BastionRDP() {
[CmdletBinding()]
param (
[Parameter(Mandatory = $false)] [string] $VM_Resource_ID,
[Parameter(Mandatory = $false)] [string] $Bastion_Resource_ID
)
@('Az.Accounts', 'Az.Compute', 'Az.Network') | Import-Module
# $SubscriptionId = (Get-AzContext).Subscription.Id
#Write-host "`$VM_Resource_ID --- " $VM_Resource_ID
#Write-host "`$Bastion_Resource_ID --- " $Bastion_Resource_ID
if ($VM_Resource_ID -eq $null -OR $VM_Resource_ID -eq "")
{
Write-host ""
$VM_Resource_ID = $(Write-Host "Provide the VM's resource ID here:" -foregroundcolor cyan; Read-Host)
While ($VM_Resource_ID -eq $null -OR $VM_Resource_ID.length -lt 20)
{
Write-host ""
$VM_Resource_ID = $(Write-Host "Invalid resource ID! Please the VM's resource ID here:" -foregroundcolor red; Read-Host)
Write-host ""
}
}
if ($Bastion_Resource_ID -eq $null -OR $Bastion_Resource_ID -eq "")
{
Write-host ""
$Bastion_Resource_ID = $(Write-Host "Provide the Bastion's resource ID here, or press 'Y' if you want to use 'bas-connectivity-xxxxxxxxxxxxxxxxxxxxxx' by default:" -foregroundcolor cyan; Read-Host)
While ($Bastion_Resource_ID -eq $null -OR (($Bastion_Resource_ID -ne "Y" -AND $Bastion_Resource_ID.length -lt 20)))
{
Write-host ""
$Bastion_Resource_ID = $(Write-Host "Invalid resource ID! Please the Bastion's resource ID here, or press 'Y' if you want to use 'bas-connectivity-xxxxxxxxxxxxxxxxxxxxxx' by default:" -foregroundcolor red; Read-Host)
Write-host ""
}
if ($Bastion_Resource_ID -eq "Y")
{
$Bastion_Resource_ID = "/subscriptions/xxxxxxxxxxxxxxxxxxxxxx/resourceGroups/rg-connectivity-xxxxxxxxxxxxxxxxxxxxxx/providers/Microsoft.Network/bastionHosts/bas-connectivity-xxxxxxxxxxxxxxxxxxxxxx"
}
}
$VM_Subscription_ID = ($VM_Resource_ID -split("/"))[2]
$CurrentContext = (Get-AZContext).Subscription.id
if ($CurrentContext -ne $VM_Subscription_ID) {
Write-host ""
Write-host "Changing context to VM subscription..." -foregroundcolor magenta
Set-AZContext $VM_Subscription_ID
Get-AZContext
}
$VMName = (Get-AZVM -resourceID $VM_Resource_ID).name
$Bastion_Subscription_ID = ($Bastion_Resource_ID -split("/"))[2]
$CurrentContext = (Get-AZContext).Subscription.id
if ($CurrentContext -ne $Bastion_Subscription_ID) {
Write-host ""
Write-host "Changing context to Bastion subscription..." -foregroundcolor magenta
Set-AZContext $Bastion_Subscription_ID
Get-AZContext
}
$BastionDnsName = (Get-AZBastion -resourceid $Bastion_Resource_ID).DnsName
$Params = @{
Method = 'GET'
Uri = "https://$BastionDnsName/api/rdpfile?resourceId=$VM_Resource_ID&format=rdp"
Headers = @{
'Content-Type' = 'application/json'
'Authorization' = "Bearer $((Get-AzAccessToken).Token)"
}
}
$RdpExclude = @('use multimon', 'signscope', 'signature')
$RdpFile = (Invoke-RestMethod @Params).Split("`n") | Where-Object {
if (-Not [string]::IsNullOrEmpty($_)) {
($Name, $Type, $Value) = $_.Split(':', 3, [System.StringSplitOptions]::TrimEntries)
(-Not $RdpExclude.Contains($Name))
}
}
$RdpFile += "screen mode id:i:1"
$RdpFile += "use multimon:i:0"
$RdpFile += "desktopwidth:i:1920"
$RdpFile += "desktopheight:i:1080"
$RdpFile += "keyboardhook:i:1"
$RdpFile += "redirectclipboard:i:1"
$RdpFile += "drivestoredirect:s:*"
$RdpFile += "smart sizing:i:1*"
#$RdpFile += "enablerdsaadauth:i:0*"
#$RdpFile += "enablecredsspsupport:i:0*"
$RdpFilePath = Join-Path $Env:TEMP "$VMName.rdp"
<#
$profilePath = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::UserProfile)
$profilePath = Join-Path $profilePath 'Downloads'
$RdpFilePath = Join-Path $downloadsPath "$VMName.rdp"
#>
$RdpFile | Out-File $RdpFilePath
& 'mstsc.exe' $RdpFilePath
}
#Connect-BastionRDP -VM_Resource_ID "/subscriptions/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/resourceGroups/rg-data-xxxxxxxxxxxxxxxxx/providers/Microsoft.Compute/virtualMachines/vm-xxxxxxxxxxxxxxxxxxxxx" -Bastion_Resource_ID "/subscriptions/xxxxxxxxxxxxxxxxxxxxxx/resourceGroups/rg-connectivity-xxxxxxxxxxxxxxxxxxxxxx/providers/Microsoft.Network/bastionHosts/bas-connectivity-xxxxxxxxxxxxxxxxxxxxxx"
#Connect-BastionRDP -VM_Resource_ID "/subscriptions/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/resourceGroups/rg-data-xxxxxxxxxxxxxxxxx/providers/Microsoft.Compute/virtualMachines/vm-xxxxxxxxxxxxxxxxxxxxx"
Connect-BastionRDP