Importing Azure RDC Files into RDCMan.exe’s RDG
This is really over-engineering. Azure’s “connect” link sends you a .rdc file to download or open. The registered handler for .rdc files is mstsc.exe, a.k.a. Remote Desktop Connection.
The key line in the .rdc file is “Full Address:s:FQDN:PORT”. The ‘s’ column in the colon-delimited value is short for ‘string,’ nothing more. What we want are the FQDN, the port, and the .rdc file’s name. Why the filename? It turns out that the Azure Cloud Service is the FQDN in the .rdc file. The machine name is just the filename. In other words, if all your VMs are in the same Azure Cloud Service, then the FQDN for each .rdc will be identical. Only the port will differentiate one VM from the other.
Anyhow, all this does is look for the Full Address line, extract out the FQDN and port data, then create a server element under the specified group in the RDG file.
function Import-AzureRdpToRdg
{
<#
.Synopsis
Import a saved Remote Desktop Connection configuration file (RDC) into a Remote Desktop Connection Manager (RDCMan) configuration file (RDG)
.description
The name says 'Azure', but this will import the FQDN, the displayed name, and the port from any RDC file. It's useful for Azure because the ports in their RDCs are not consistent.
If an existing server shares the same FQDN and port, regardless of the displayed name, it is marked redundant and will be removed after a warning and delay prompt.
.parameter Path
Location of one or more RDC files to import.
.parameter Group
Name of RDG group into which to import the RDC servers. If multiple groups with the same name are found, the first one is used. Defaults to 'Azure'. If specified group does not exist, it will be created after a warning and delay prompt.
.parameter RdgPath
Name of RDG file into which to import the RDC servers. If not specified, it defaults to the value stored in $Host.Rdg.XML. If that is not set, script will error out. If file does not exist, it will be created after a warning and delay prompt.
.parameter Force
Do not warn nor delay prompt when creating missing RdgPath, -Group group value, nor removing redundant hosts.
#>
param (
[parameter(ValueFromPipeline=$true)][string[]]$Path,
[string]$Group = 'Azure',
[string]$RdgPath = $Host.Rdg.Path,
[switch]$Force
);
begin
{
#region header
##########
$ErrorActionPreference = 'SilentlyContinue';
trap { Write-Warning $_.Exception.Message; return; }
##########
#endregion
#region validate $RdgPath
##########
if ($RdgPath)
{
if (!($host.Rdg.Path = $RdgPath))
{
Add-Member -InputObject $Host -MemberType NoteProperty -Name Rdg -Value @{
Path = $RdgPath;
} -Force -ErrorVariable errorVariable;
if ($errorVariable) { return ( Write-Warning $errorVariable.Exception.Message ); }
} # if (!($host.Rdg.Path = $RdgPath))
} # if (!$RdgPath)
else
{
$message = "-RdgPath not specified and `$Host.Rdg.Path is not set.";
Write-Error -Message $message -ErrorAction SilentlyContinue
return ( Write-Warning -Message $message );
} # if (!$RdgPath) ... else
if (Test-Path -Path $RdgPath)
{ # if we can find the file. back it up
$backupPath = ($RdgPath -replace "\.rdg$") + " ($(Get-Date -Format 'yyyy-MM-dd_HH-mm-ss')).rdg"
Copy-Item $RdgPath $backupPath;
}
else
{ # if we can't find the file, try to create it
if (!$Force)
{
Write-Warning "-RdgPath $Rdgpath not found. Creating in 5 seconds.";
Start-Sleep -Seconds 5;
} # if (!$Force)
Set-Content -Path $RdgPath -Value "
<?xml version='1.0' encoding='utf-8'?>
<RDCMan schemaVersion='1'>
<version>2.2</version>
<file>
<properties>
<name>AzureRDG</name>
</properties>
<group>
<properties>
<name>$group</name>
</properties>
</group>
</file>
</RDCMan>
" -ErrorAction SilentlyContinue
} # if (Test-Path -Path $RdgPath) ... else
if (!($Host.Rdg.XML = (Get-Content -Path $RdgPath -ErrorVariable errorVariable) -as [xml]))
{
$message = "-RdgPath $RdgPath cannot be parsed as XML.";
Write-Error -Message $message -ErrorAction SilentlyContinue
return ( Write-Warning -Message $message );
} # if (!($Host.Rdg.XML = (Get-Content -Path $RdgPath) -as [xml]))
if ($errorVariable) { return ( Write-Warning $errorVariable.Exception.Message ); }
##########
#endregion
#region validate $Group
##########
$groupNameHash = @{}; # need a [HashTable] to make the group name handling case-insensitive
$Host.Rdg.XML.SelectNodes("//group/properties/name") | % { $groupNameHash.($_.'#text') = $_.'#text'; }
if ($groupNameHash.$Group)
{ # if the group name is in the [HashTable], correct the case
$Group = $groupNameHash.$Group;
} # if ($groupNameHash.$Group) ... else
elseif (!$Force)
{ # otherwise, the group name, no matter the case, is not in the list of groups in the RDG file
Write-Warning -Message "-RdgPath $RdgPath -Group $Group cannot be found. Creating in 5 seconds.";
Start-Sleep -Seconds 5;
} # if ($groupNameHash.$Group) ... else
if (!($azureGroup = $Host.Rdg.XML.SelectSingleNode("//group/properties/name[text()='Azure']/../..") |
Select-Object -First 1
))
{ # if we can't find the Azure group, create it
$azureGroup = $Host.Rdg.XML.CreateElement("group");
$azureGroup.InnerXml = "
<properties>
<name>$group</name>
</properties>
";
$Host.Rdg.XML.SelectSingleNode("//file").AppendChild($azureGroup) | Out-Null;
} # if (($azureGroup = !$Host.Rdg.XML.SelectSingleNode(...
#endregion
##########
} # begin
process
{
#region validate each file in $Path
##########
foreach ($file in $Path)
{
if (Test-Path -Path $Path -ErrorVariable errorVariable)
{
$fqdn, $port = (
(
Get-Content -Path $Path |
? { $_ -match '^full address:' } |
Select-Object -First 1
) -split ":"
)[2,3]
} # if (Test-Path -Path $Path -ErrorVariable errorVariable)
elseif ($errorVariable)
{
Write-Warning $errorVariable.Exception.Message;
continue;
} # if (Test-Path -Path $Path -ErrorVariable errorVariable) ... else
} # foreach ($file in $Path)
if (!$fqdn -or !$port -or !($port -as [int]))
{
$message = "-Path $file is not a valid RDP file.";
Write-Error -Message $message -ErrorAction SilentlyContinue
Write-Warning -Message $message;
continue;
} # if (!$fqdn -or !$port -or !($port -as [int]))
##########
#endregion
#region create server node
##########
$Host.Rdg.XML.SelectNodes("//server/name[text()='$fqdn']/../connectionSettings/port[text()='$port']/../..") |
% {
$serverNode = $_;
if (!$Force)
{
$groupNode = $serverNode
while ($groupNode = $sgroupNode.ParentNode)
{ $groupString = "/" + $serverNode.ParentNode.Properties.Name.'#text'; }
Write-Warning "$groupString/$displayname points to ${fqdn}:$port. Overwriting in 5 seconds.";
Start-sleep -Seconds 5;
} # if (!$Force)
$serverNode.ParentNode.RemoveChild($serverNode) | Out-Null;
}
$displayName = (Split-Path -Path $file -Leaf) -replace '\.rdp$';
$serverNode = $Host.Rdg.XML.CreateElement("server");
$serverNode.InnerXml = "
<name>$fqdn</name>
<displayName>$displayName</displayName>
<comment />
<connectionSettings inherit='None'>
<port>$port</port>
</connectionSettings>
";
$azureGroup.AppendChild($serverNode) | Out-Null;
$Host.Rdg.XML.SelectNodes("//server/name[text()='$fqdn']/../connectionsettings/port[text()='$port']/../..")
Write-Verbose -Verbose "$displayName (${fqdn}:$port) added.";
##########
#endregion
} # process
end
{ $host.Rdg.XML.Save($Host.Rdg.Path); }
} # function Import-AzureRdpToRdg