Uncategorized

Launching Windows Admin Center from the Configuration Manager Console

September 3, 2020

It is no secret that I am a fan of Windows Admin Center (WAC). I wrote an extension to handle CM client tasks within WAC a year or so back that you can find more information about here. I have finally got around to a small addition that kept getting de-prioritized on my to do list.

I have written two small “right click tools” to loosely integrate WAC back into the Configuration Manager console.

The first is available on devices and simply opens the device in WAC.

The second is available on collections and adds all devices in the collection (limited to online and the console user has access to) in to the WAC console. The added devices are tagged with the collection ID of the collection you ran the script from.

In both cases, the script determines if the target device is a workstation or server to set the device up in WAC correctly.

To install the two tools, grab the script below. You will run the script on the system the console you want the tools in is installed on. You need to provide two pieces of information:
1. Name of the WAC gateway system
2. What port you use for WAC

Should you want to remove the tools but don’t feel like digging through the AdminConsole\XmlStorage\Extensions\Actions folder, simply run the script with an -uninstall switch.

Param(
    [Parameter()][String]$WACNAME,
    [Parameter()][String]$WACPort,
    [Parameter()][Switch]$uninstall
)


$DeviceScript = @'

Param(
    [Parameter(Mandatory=$True, Position=0)][String]$computer
)

$WACName = "WAC_NAME"
$WACPort = "WAC_Port"


$WACFQDN = ([System.Net.Dns]::GetHostByName(($WACName))).Hostname

$producttrype = $NULL
$producttrype = (Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName $computer -ErrorAction SilentlyContinue).ProductType
$FQDN = ([System.Net.Dns]::GetHostByName(($computer))).Hostname


if($producttrype -eq 1){$URL = "https://$($WACFQDN):$($WACPort)/computerManagement/connections/computer/$($FQDN)/tools/overview"}
else{$URL = "https://$($WACFQDN):$($WACPort)/servermanager/connections/server/$($FQDN)/tools/overview"}

Start-Process $URL

'@

$basexmlST = @'
<ActionDescription Class="Group" DisplayName="Windows Admin Center" MnemonicDisplayName="Windows Admin Center" Description="Windows Admin Center" SqmDataPoint="53">
  <ImagesDescription>
    <ResourceAssembly>
		<Assembly>AdminUI.UIResources.dll</Assembly> 
		<Type>Microsoft.ConfigurationManagement.AdminConsole.UIResources.Properties.Resources.resources</Type> 
    </ResourceAssembly>
    <ImageResourceName>ClientCheck</ImageResourceName> 
  </ImagesDescription>
  <ShowOn>
    <string>ContextMenu</string>
  </ShowOn>
  <ActionGroups>

<!--Site-->


  </ActionGroups>
</ActionDescription>
'@

$siteXMLST = @'
        
<ActionDescription Class="Executable" DisplayName="Open in Windows Admin Center" MnemonicDisplayName="Open in Windows Admin Center" Description="Open in Windows Admin Center">
<ImagesDescription>
 <ResourceAssembly>
   <Assembly>AdminUI.UIResources.dll</Assembly> 
     <Type>Microsoft.ConfigurationManagement.AdminConsole.UIResources.Properties.Resources.resources</Type> 
  </ResourceAssembly>
 <ImageResourceName>InstallClient</ImageResourceName>
 </ImagesDescription> 
 <Executable>
   <FilePath>ENTEREXEPATH</FilePath>
   <Parameters>ENTERPARAMETERS</Parameters>
 </Executable>
 <ShowOn>
   <string>ContextMenu</string>
 </ShowOn>
</ActionDescription>

<!--Site-->
'@

$CollScript = @'
Param(
    [Parameter(Mandatory=$True, Position=0)][String]$collid
)

$WACName = "WAC_NAME"
$WACPort = "WAC_Port"

add-type @"
    using System.Net;
    using System.Security.Cryptography.X509Certificates;
    public class TrustAllCertsPolicy : ICertificatePolicy {
        public bool CheckValidationResult(
            ServicePoint srvPoint, X509Certificate certificate,
            WebRequest request, int certificateProblem) {
            return true;
        }
    }
"@

Function Get-Params {
    param(
        [Parameter(Mandatory = $false)]
        [Uri]
        $GatewayEndpoint,
        [Parameter(Mandatory = $false)]
        [pscredential]
        $Credential
    )
    if ( $GatewayEndpoint -eq $null ) {
        try
        {
            $GatewayEndpoint = [Uri] ( Get-ItemPropertyValue 'HKCU:\Software\Microsoft\ServerManagementGateway' 'SmeDesktopEndpoint' )
        }
        catch
        {
            throw (New-Object System.Exception -ArgumentList 'No endpoint was specified so a local gateway was assumed and it must be run at least once.')
        }
    }
    $params = @{useBasicParsing = $true; userAgent = "PowerShell"}
    [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
    $clientCertificateThumbprint = ''
    $IsLocal = $GatewayEndpoint.IsLoopback -or ( $GatewayEndpoint.Host -eq $Env:ComputerName )
    if ( ( $GatewayEndpoint.Scheme -eq [Uri]::UriSchemeHttps ) -and $IsLocal ) {  
        $clientCertificateThumbprint = (Get-ChildItem 'Cert:\CurrentUser\My' | Where-Object { $_.Subject -eq 'CN=Windows Admin Center Client' }).Thumbprint
    }
    if ($clientCertificateThumbprint) {
        $params.certificateThumbprint = "$clientCertificateThumbprint"
    }
    else {
        if ($Credential) {
            $params.credential = $Credential
        }
        else {
            $params.useDefaultCredentials = $True
        }
    }
    $params.uri = "$($GatewayEndpoint)/api/connections"
    return $params
}

Function Get-Connections {
    param(
        [Parameter(Mandatory = $false)]
        [Uri]
        $GatewayEndpoint,
        [Parameter(Mandatory = $false)]
        [pscredential]
        $Credential
    )
    $params = Get-Params $GatewayEndpoint $Credential
    $params.method = "Get"
    $response = Invoke-WebRequest @params
    if ($response.StatusCode -ne 200 ) {
        throw "Failed to get the connections"
    }
    $connections = ConvertFrom-Json $response.Content
    return $connections
}

Function Remove-Connection {
    param(
        [Parameter(Mandatory = $false)]
        [Uri]
        $GatewayEndpoint,
        [Parameter(Mandatory = $true)]
        [String]
        $connectionId,
        [Parameter(Mandatory = $false)]
        [pscredential]
        $Credential
    )
    $params = Get-Params $GatewayEndpoint $Credential
    $params.method = "Delete"
    $params.uri = $params.uri + "/" + $connectionId
    $response = Invoke-WebRequest @params
    if ($response.StatusCode -ge 300) {
        throw "Failed to remove the connection"
    }
}

<#
.SYNOPSIS
Import the connections available in a file to the Windows Admin Center Gateway.

.DESCRIPTION
The function import all the connections especified into a file

.PARAMETER GatewayEndpoint
Required. Provide the gateway name.

.PARAMETER Credential
Optional. If you wish to provide credentials to the Windows Admin Center gateway which are different from your credentials on the computer where you're executing the script, provide a PSCredential by using Get-Credential. You can also provide just the username for this parameter and you will be prompted to enter the corresponding password (which gets stored as a SecureString).

.PARAMETER fileName
Required. File name to export the results. If is not provided the result is show in console.

.PARAMETER prune
Optional. If it is present, the connections not included in the file will be removed.

.EXAMPLE
C:\PS> Import-Connection "http://localhost:4100" -fileName "lista.csv"
#>
Function Import-Connection {
    param(
        [Parameter(Mandatory = $false)]
        [Uri]
        $GatewayEndpoint,
        [Parameter(Mandatory = $false)]
        [pscredential]
        $Credential,
        [Parameter(Mandatory = $true)]
        [String]
        $fileName,
        [Parameter(Mandatory = $false)]
        [switch]
        $prune
    )

    $connections = Import-Csv $fileName
    $connections | ForEach-Object {
        if ([string]::IsNullOrEmpty($_.type)) {
            $_.type = "msft.sme.connection-type.server"
        }

        $id = ($_.type + "!" + $_.name)
        if ($_.groupId)  {
            $id = ($id + "!" + $_.groupId)
        }
        $_ | Add-Member "id" $id
        if ($_.tags) {
            $_.tags = $_.tags.split("|")
        }
    }

    $params = Get-Params $GatewayEndpoint $Credential
    $params.method = "Put"
    $params.body = ConvertTo-Json @($connections)

    Try {
        $response = Invoke-WebRequest @params
    } 
    Catch {
        $error = ConvertFrom-Json $_
        throw $error.error.message
    }

    if ($response) {
        if ($response.StatusCode -ne 200 ) {
            throw "Failed to import the connections"
        }
        $content = ConvertFrom-Json $response
        if ($content -and $content.errors) {
            Write-Host "The operation partially succeeded with errors:"
            Write-Warning ($content.errors | fl * | Out-String)                 
        } else {
            Write-Host "Import connections succeeded"
        }
        if ($prune) {
            $connectionsResult = Get-Connections $GatewayEndpoint $Credential
            $connectionsImported = $connections.id 
            $connectionsRemove = $connectionsResult.value | Where-Object { $_.name -notin $connectionsImported} 
            $connectionsRemove | ForEach-Object {
                Remove-Connection $GatewayEndpoint $_.name $Credential
            }
        }
    }

    return $connections
}


$WACFQDN = ([System.Net.Dns]::GetHostByName(($WAC))).Hostname
$localFQDN = ([System.Net.Dns]::GetHostByName(($env:computerName))).Hostname
$tempfile = "$($ENV:temp)\wac.csv"

if($WACFQDN -eq $localFQDN){}

if((Get-Module ConfigurationManager) -eq $null) {Import-Module "$($ENV:SMS_ADMIN_UI_PATH)\..\ConfigurationManager.psd1"}

$psdrive = Get-PSDrive -PSProvider CMSite
$sitecode = $psdrive.SiteCode
$provider = $psdrive.Connection.NamedValueDictionary["ProviderLocation"].Machine
$namespace = "root\sms\site_"+$sitecode

Set-Location "$(($psdrive).Name):\"


Add-Content -Path $tempfile  -Value '"name","type","tags","groupId"'
$TargetMachines = (Get-CMDevice -CollectionId $collid | Select-Object -ExpandProperty Name)
ForEach($TargetMachine in $TargetMachines){
    if(test-wsman $TargetMachine -ErrorAction SilentlyContinue){
        $producttrype = $NULL
        $PT = $NULL
        $producttrype = (Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName $TargetMachine -ErrorAction SilentlyContinue).ProductType
        $FQDN = ([System.Net.Dns]::GetHostByName(($TargetMachine))).Hostname
        if($producttrype -eq 1){$PT = "msft.sme.connection-type.windows-client"}
        elseif($producttrype -eq $NULL){}
        else{$PT = "msft.sme.connection-type.server"}
        if($PT){Add-Content -Path $tempfile -Value "`"$($FQDN)`",`"$($PT)`",`"$($collid)`""}
    }
}

Import-Connection "https://$($WACFQDN):$($WACPort)" -fileName $tempfile
remove-item $tempfile

Write-Host -NoNewLine 'Press any key to close...';
$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown');
'@

$baseXMLCT = @'
<ActionDescription Class="Group" DisplayName="Windows Admin Center" MnemonicDisplayName="Windows Admin Center" Description="Windows Admin Center" SqmDataPoint="53">
  <ImagesDescription>
    <ResourceAssembly>
		<Assembly>AdminUI.UIResources.dll</Assembly> 
		<Type>Microsoft.ConfigurationManagement.AdminConsole.UIResources.Properties.Resources.resources</Type> 
    </ResourceAssembly>
    <ImageResourceName>ClientCheck</ImageResourceName> 
  </ImagesDescription>
  <ShowOn>
    <string>ContextMenu</string>
  </ShowOn>
  <ActionGroups>
    
        <!--Site-->
	
	
	
  </ActionGroups>	
</ActionDescription>
'@

$siteXMLCT = @'

<ActionDescription Class="Executable" DisplayName="Add to Windows Admin Center" MnemonicDisplayName="Add to Windows Admin Center" Description="Add to Windows Admin Center">
<ImagesDescription>
 <ResourceAssembly>
   <Assembly>AdminUI.UIResources.dll</Assembly> 
     <Type>Microsoft.ConfigurationManagement.AdminConsole.UIResources.Properties.Resources.resources</Type> 
  </ResourceAssembly>
 <ImageResourceName>InstallClient</ImageResourceName>
 </ImagesDescription> 
 <Executable>
   <FilePath>ENTEREXEPATH</FilePath>
   <Parameters>ENTERPARAMETERS</Parameters>
 </Executable>
 <ShowOn>
   <string>ContextMenu</string>
 </ShowOn>
</ActionDescription>

<!--Site-->
'@

#build required strings for install/uninstall
$ConsoleDir = $Env:SMS_ADMIN_UI_PATH.Substring(0,$Env:SMS_ADMIN_UI_PATH.Length-8)
$ConsoleTools = $ConsoleDir + 'XmlStorage\Tools'
$RCTPathST = $ConsoleTools + '\WAC_Launch.ps1'
$RCTPathCT = $ConsoleTools + '\AddCollToWAC.ps1'
$ConsoleActions = $ConsoleDir + 'XmlStorage\Extensions\Actions'
$system1 = $ConsoleActions + '\3fd01cd1-9e01-461e-92cd-94866b8d1f39'
$systemfile1 = $system1 + '\WAC_Launch.xml'
$system2 = $ConsoleActions + '\0770186d-ea57-4276-a46b-7344ae081b58'
$systemfile2 = $system2 + '\WAC_Launch.xml'
$system3 = $ConsoleActions + '\ed9dee86-eadd-4ac8-82a1-7234a4646e62'
$systemfile3 = $system3 + '\WAC_Launch.xml'
$coll1 = $ConsoleActions + '\3785759b-db2c-414e-a540-e879497c6f97'
$collfile1 = $coll1 + '\WAC_Launch.xml'
$coll2 = $ConsoleActions + '\a92615d6-9df3-49ba-a8c9-6ecb0e8b956b'
$collfile2 = $coll2 + '\WAC_Launch.xml'
$PSPath = "Powershell.exe"
$STXML = $basexmlST
$COLLXML = $baseXMLCT
$STParam = "-noexit -windowstyle hidden -executionpolicy bypass -file `"$RCTPathST`" ##SUB:Name## "
$CRParam = "-executionpolicy bypass -file `"$RCTPathCT`" ##SUB:CollectionID## "
$STXML = $STXML -replace '<!--Site-->', $siteXMLST ` -Replace 'ENTEREXEPATH', $PSPath -Replace 'ENTERPARAMETERS', $STParam
$COLLXML = $COLLXML -Replace '<!--Site-->', $siteXMLCT -Replace 'ENTEREXEPATH', $PSPath -Replace 'ENTERPARAMETERS', $CRParam


if($Uninstall){
    #remove files
    if(test-path $RCTPathST){Remove-Item $RCTPathST -force}
    if(test-path $RCTPathCT){Remove-Item $RCTPathCT -force}
    if(test-path $systemfile1){Remove-Item $systemfile1 -force}
    if(test-path $systemfile2){Remove-Item $systemfile2 -force}
    if(test-path $systemfile3){Remove-Item $systemfile3 -force}
    if(test-path $collfile1){Remove-Item $collfile1 -force}
    if(test-path $collfile1){Remove-Item $collfile1 -force}
}
else{
    #create scripts
    if(!(test-path $ConsoleTools)){New-item ConsoleTools -ItemType directory | out-null}
    if(test-path $RCTPathST){Remove-Item $RCTPathST -force}
    $DeviceScript -replace "WAC_NAME",$WACNAME -replace "WAC_Port",$WACPort | out-file $RCTPathST -Force

    if(test-path $RCTPathCT){Remove-Item $RCTPathCT -force}
    $CollScript -replace "WAC_NAME",$WACNAME -replace "WAC_Port",$WACPort | out-file $RCTPathCT -Force

    #build / populate extension directories
    if(!(test-path $system1)){New-item $system1 -ItemType directory | out-null}
    $STXML | out-file $systemfile1

    if(!(test-path $system2)){New-item $system2 -ItemType directory | out-null}
    $STXML | out-file $systemfile2

    if(!(test-path $system3)){New-item $system3 -ItemType directory | out-null}
    $STXML | out-file $systemfile3

    if(!(test-path $coll1)){New-item $coll1  -ItemType directory | out-null}
    $COLLXML | out-file $collfile1

    if(!(test-path $coll2)){New-item $coll2 -ItemType directory | out-null}
    $COLLXML | out-file $collfile2
}

Fine! One more little teaser, end credits scene, etc. I am nearing completion on the next release of the WAC extension. There are a couple bug fixes, but the new feature is the addition of the Deployment Monitoring Tool.