自動擴展VDI虛擬桌面集區,並自動指派

自動擴展VDI虛擬桌面集區,並自動指派

這篇是個概念性的文章

要用在Production Server上請三思而後行

基本上,Microsoft的VDI是透過MSDTC來Remote Desktop到Hyper-V上的Virtual Machine

而Microsoft的VDI有分為Virtual Desktop Pool跟One by One Virtaul Desktop

在做Virtual Desktop Pool時會有一個考量點,就是VM的數量會比實際上使用者來的少

如果建的比實際上使用的使用者來的多,就失去了Virtual Desktop Pool的意義

(當然還有節省One by One Virtaul Desktop管理VM的麻煩)

下圖是一張VDI的簡單概念圖,SCVMM並不是必要的,會畫上去是因為這篇會用到

1234

原則上一台Windows 7的Virtual Machine一次只能允許一個RDP Session

如果是One By One的Virtual Desktop,以兩百個使用者為例,就需要建立兩百個Win7的VM

但是如果不會同時達到200人同時使用,其實就可以考慮用Pool的形式

像是一開始只建立50台VM,但一樣實際使用者有200個,這情況下,在第51個使用者使用時,會出現VM不足的問題

如果以手動的方式,我們需要建立一台VM,然後設定VDI相關設定再Win7 VM上,再指派給RDCB的Virtual Pool

這樣子,這台VM才能被使用為VDI的一部VM

 

這篇主要就是要解決這個問題,但是只是概念,如一開始所說,要用請三思:P

 

OK,既然已經知道,RDCB是以Machine的RDP Session來記錄是否有使用

那麼我們要如何得知這台Machine是否有Online的RDP Session呢?

我們可以透過Terminal Services PowerShell Module這個PowerShell Module來取得Online Session

語法是Get-TSSession -ComputerName $ComsputeName.Computername -State Active

我這裡提供一份自動AssinVMPool的Powershell,寫得不是很好,算是個半成品,有興趣的人可以自己改,算是個概念性提供

這裡面會牽涉到VMM的Template的建立、RDCB的Pool Assin、RDP Session的Monitor、也用到了一些Remote Manager的指令

這個指令需要放在VMM Server上,並且VMM需要安裝Terminal Services PowerShell Module

VMM Server對RDCB需要啟用WinRM,這牽涉到送指令給RDCB做AssinVMPool,並且SMB要通,權限要夠

裡面當中,唯一會需要修改的大概只有下面這個PowerShell Script

第五行是取得SCVMM的Server內容,SCVMM是我的Hostname,請自行修改(只要是SCVMM都是)

第九行跟第十行以及第十四行是我針對Win7 VM的Hostname前置名稱,請依個人需求修改

第十五行是網域尾碼,請依自行環境修改

第27行的Win7SP1_Pro_zh-tw_Template是我的Template Name,這需要先在VMM中先建立好

第28行則是我的Hyper-V Host Name

第35行跟第52行則是有vSwitch的名稱,請依個人環境設定

第8行跟第58行及第60行有RDCB的HostName,請自行修改,另外第60行會有Administrator的Password

第7行則是當VM的Online Session比例達到70%(0.7)時,就會建立VM

第29行則是一次要建立幾個VM(範例是指定2個)

第40行則需要修改VM Pools的名稱,我的Pool Nam是POOL VMPOOL

如果要快速建立,可以使用差異磁碟去建立,數分鐘就可以完成幾十個

至於VMM裡的Template需要建立到甚麼樣的程度?

其實只要讓它建立完VM,會自動加入網域就可以了(所以會需要DHCP Server)

Add-PSSnapin Microsoft.SystemCenter.VirtualMachineManager

Import-Module PSTerminalServices

Get-VMMServer -ComputerName SCVMM

Clear-Content C:\1.csv
Clear-Content \\RDCB\C$\Assin.txt
$a=get-vm | where { $_.Computername -Like "*vdiclient*"} |ft|Measure-Object -line
$w=get-vm | where { $_.Computername -Like "*vdiclient*"} 
$b = $a.Lines -4


$computername="vdiclient"
$fqdn=".demo.com.tw"
$w | foreach{
try
{Get-TSSession -ComputerName $_.Computername -State Active}catch{add-content -value "$_.Computername+' Connection Error'" -Path C:\Error.log}}|select Computername|add-content C:\1.csv
$x =Get-Content C:\1.csv|Measure-Object -line
$d=$x.Lines
if($d -gt 0)
{
if($b/$d -gt 0.7 )
	{
$b=$b+1
$s=1
$Template = Get-Template -VMMServer "SCVMM" | where {$_.Name -eq "Win7SP1_Pro_zh-tw_Template"}
$VMHost = Get-VMHost | where {$_.Name -eq "HyperHost1.demo.com.tw"}
while($s -le 2)
		{
$jobgroup1="02ff3a4c-f3a0-43bf-9acc-"
$jobgroup2=100
$jobgroup3=get-random -minimum 100000000 -maximum 999999999
$jobgroup=$jobgroup1+$jobgroup2+$jobgroup3
New-VirtualNetworkAdapter -VMMServer "SCVMM" -JobGroup $jobgroup -PhysicalAddressType Dynamic -VirtualNetwork "VM-Public-10.1.1.0-24" -VLanEnabled $false -Synthetic -VMNetworkOptimizationEnabled $false -MACAddressesSpoofingEnabled $false
 
$tempname=$computername+$b+$fqdn
$tempcomputername=$computername+$b

New-VM -Template $Template -Name $tempname -JobGroup $jobgroup -VMHost $VMHost -Path "C:\ClusterStorage\Volume1\" -RunAsynchronously -ComputerName $tempcomputername -DelayStart 0 -StopAction SaveVM -StartVM 
$TempAssignment=$tempname+" POOL VMPOOL"
$TempAssignment|add-content \\RDCB\C$\Assin.txt

$b=$b+1
$s=$s+1
Start-Sleep -Seconds 5
		}
$jobgroup1="02ff3a4c-f3a0-43bf-9acc-"
$jobgroup2=100
$jobgroup3=get-random -minimum 100000000 -maximum 999999999
$jobgroup=$jobgroup1+$jobgroup2+$jobgroup3
New-VirtualNetworkAdapter -VMMServer "SCVMM" -JobGroup $jobgroup -PhysicalAddressType Dynamic -VirtualNetwork "VM-Public-10.1.1.0-24" -VLanEnabled $false -Synthetic -VMNetworkOptimizationEnabled $false -MACAddressesSpoofingEnabled $false
$tempname=$computername+$b+$fqdn
$tempcomputername=$computername+$b

New-VM -Template $Template -Name $tempname -JobGroup $jobgroup -VMHost $VMHost -Path "C:\ClusterStorage\Volume1\" -ComputerName $tempcomputername -DelayStart 0 -StopAction SaveVM -StartVM
$TempAssignment=$tempname+" POOL VMPOOL"
$TempAssignment|add-content \\RDCB\C$\Assin.txt

winrs -r:RDCB -ad -u:demo.com.tw\administrator -p:!QAZ2wsx "powershell.exe -command C:\Set-VMAssignment.ps1 -AssignmentFileName C:\Assin.txt"
	}
}
exit
exit

因為要透過RDCB執行Assin VM的指令,會需要將Set-VMAssignment.ps1放到RDCB的C:上

Set-VMAssignment.ps1內容如下:

.Synopsis  
    Assigns virtual machines to users or pools. The script, optionally also adds RD Virtualization Host servers to the RD Connection Broker server. 
     
.Description  
    Assigns virtual machines to users or pools. The script, optionally also adds RD Virtualization Host servers to the RD Connection Broker server. While assigning personal virtual desktops, the script prompts for confirmation to continue overriding the current assignment (if any) for the virtual machines. 
    NOTE: This script should be run on the RD Connection Broker server. 
 
.Parameter AssignmentFileName 
    Text file containing assignment information in the format '<VMName> [POOL|USER] <User Name/Pool Name>'. User Name should be in 'domain\user' format. 
     
.Parameter HostFileName 
    Text file containing list of RD Virtualization Host servers that need to be added to the connection broker. Each new line seperated entry in the file is treated as a single RD Virtual Host server's name. 
     
.Parameter PoolFileName 
    Text file containing list of pools that need to be created. Each new line seperated entry in the file is treated as the name of a pool. 
 
.Parameter Credential 
    Credentials that have write acces on the Active Directory for Personal Virtual Desktop assignment. 
 
.Parameter Force 
    Switch: The script seeks confirmation from the user in case it is reassigning a virtual machine to another user unless this switch is used. Providing this switch would force continue the reassignment. 
       
.Example  
    PS C:\> Set-VMAssignment.ps1 -AssignmentFileName "C:\Users\Administrator\Desktop\assninfo.txt" 
     
    The above example performs virtual machine assignment as mentioned in 'assninfo.txt'. 
 
.Example  
    PS C:\> Set-VMAssignment.ps1 -AssignmentFileName "C:\Users\Administrator\Desktop\assninfo.txt" -PoolFileName "C:\Users\Administrator\Desktop\pools.txt" 
     
    The above example creates virtual machine pools with names mentioned in 'pools.txt' and performs virtual machine assignment as mentioned in 'assninfo.txt'. 
 
.Example  
    PS C:\> Set-VMAssignment.ps1 -AssignmentFileName "C:\Users\Administrator\Desktop\assninfo.txt" -PoolFileName "C:\Users\Administrator\Desktop\pools.txt" -HostFileName "C:\Users\Administrator\Desktop\hosts.txt" 
     
    The above example adds entries mentioned in 'hosts.txt' as RD Virtualization Host servers to the RD Connection Borker server on which the script is being executed, creates virtual machine pools with names mentioned in 'pools.txt' and performs virtual machine assignment as mentioned in 'assninfo.txt'. 
 
.Notes  
    Name     : Set-VMAssignment.ps1 
     
#> 
param ( 
    [Parameter(Mandatory=$TRUE, HelpMessage="Text file containing lines in the format '<VM Name> [POOL/USER] <Pool Name/User Name>'.")] 
    [string]$AssignmentFileName, 
     
    [Parameter(Mandatory=$FALSE, HelpMessage="Text file containing servers which should be added to the RD Connection Broker server as RD Virtualization Host servers.")] 
    [string]$HostFileName, 
     
    [Parameter(Mandatory=$FALSE, HelpMessage="Text file containing the names of pools that need to be created on the RD Connection Broker server.")] 
    [string]$PoolFileName, 
     
    [Parameter(Mandatory=$FALSE, HelpMessage="Credentials that have write acces on the Active Directory for Personal Virtual Desktop assignment.")] 
    [System.Management.Automation.PSCredential]$Credential, 
     
    [Parameter(Mandatory=$FALSE, HelpMessage="The script seeks confirmation from the user in case it is reassigning a virtual machine to another user unless this switch is used. Providing this switch would force continue the reassignment.")] 
    [Switch]$Force 
) 
 
function Print-Error([System.String]$Msg) 
{ 
    Write-Host $Msg 
    Exit(1) 
} 
 
function Ask-Confirmation() 
{ 
    Write-Warning "The script will not check for current user assignment of Virtual Machines. A virtual machine already assigned to a user might be reassigned to a new user depending on the entries in the 'AssignmentFile'." 
    Write-Host  "Do you want to continue? : [Y]Yes, [N]No" 
    while($TRUE) 
    { 
return 
    } 
} 
 
#Check if there are proper privileges to run the script 
$LoggedInUser = New-Object -TypeName System.Security.Principal.WindowsPrincipal -ArgumentList ([System.Security.Principal.WindowsIdentity]::GetCurrent()) 
if(!($LoggedInUser.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator))) 
{ 
    Print-Error "Please execute the script with administrative privileges! Exiting..." 
} 
 
Import-Module ServerManager 
$RoleInstalled = (Get-WindowsFeature RDS-Connection-Broker).Installed 
Remove-Module ServerManager 
 
if(!$RoleInstalled) 
{ 
    Print-Error "This script should be run on the RD Connection Broker server! Exiting..." 
} 
 
 
$LineFormat="<VM Name> [POOL/USER] <Pool Name/User Name>" 
 
# File Format 
# <VMName> [POOL/USER] <farm name/user name> 
 
#Add RD Virtualization Host servers if needed 
function Add-VirtualizationHostServer([string]$FileName) 
{ 
    $servers=(Get-Content -Path $FileName) 
    foreach($server in $servers) 
    { 
        New-Item -Path "RDS:\ConnectionBroker\VirtualDesktops\RDVHostServers" -Name $server 
    } 
     
    Start-Sleep -seconds 5 
} 
 
#Create Farms 
function Add-VmFarm([string]$FileName) 
{ 
    $farms=(Get-Content -Path $FileName) 
    foreach($farm in $farms) 
    { 
        New-Item -Path RDS:\ConnectionBroker\VirtualDesktops\Pools -Name $farm -DisplayName $farm 
    } 
} 
 
#Assign function 
function Assign-VirtualMachines([string]$FileName) 
{ 
    if(!$Force.IsPresent) 
    { 
        Ask-Confirmation 
    } 
     
    $i=1 
    foreach($Line in (Get-Content -Path $FileName)) 
    { 
        $tokens = $Line.Trim().Split() 
        if(($tokens) -and ($tokens.Length -eq 3)) 
        { 
            if($tokens[1] -eq "POOL") 
            { 
                New-Item -Path "RDS:\ConnectionBroker\VirtualDesktops\Pools\$($tokens[2])\VirtualMachines" -Name $tokens[0] 
                if(!$?) 
                { 
                    Write-Error "ERROR in Line '$i': Could not create a pool with name $($tokens[0])" 
                } 
            } 
            else 
            { 
                if($tokens[1] -eq "USER") 
                { 
                    if($Credential) 
                    { 
                        Set-PersonalVirtualDesktop -User $tokens[2] -VirtualDesktop $tokens[0] -Credential $Credential 
                    } 
                    else 
                    { 
                        Set-PersonalVirtualDesktop -User $tokens[2] -VirtualDesktop $tokens[0] 
                    } 
                     
                    if($?) 
                    { 
                        $VMObject = Get-VirtualDesktop -User $tokens[2] 
                        Write-Host "Virtual machine: '$($VMObject.Name)' assigned to: '$($tokens[2])'" 
                    } 
                    else 
                    { 
                        Write-Error "ERROR in Line '$i': Could not assign virtual machine: '$($tokens[0])' to '$($tokens[2])'" 
                        Write-Error "You may want to use the script, Verify-VMConfiguration.ps1, to verify the configuration of virtual machine so that it can be used as a virtual desktop in RemoteApp and Desktop Connection." 
                    } 
                } 
                else 
                { 
                    Write-Error "Error in Line $i: Line in unexpected format. Expected Format: $LineFormat" 
                } 
            } 
        } 
        else 
        { 
            if(($tokens) -and ($tokens.Length -gt 0)) 
            { 
                Write-Error "Error in Line $i: Line in unexpected format. Expected Format: $LineFormat" 
            } 
        } 
        $i++ 
    } 
} 
 
Import-Module RemoteDesktopServices 
 
#Add Virtualization Host Servers 
if($HostFileName) 
{ 
    Add-VirtualizationHostServer($HostFileName) 
} 
 
if($PoolFileName) 
{ 
    Add-VmFarm($PoolFileName) 
} 
 
if($AssignmentFileName) 
{ 
    Assign-VirtualMachines($AssignmentFileName) 
} 

然後在透過SCVMM對Win7做Sysprep前先將這個Configure-VirtualMachine PowerShell執行一遍,這樣可以加快設定

如果可以,請將他放入GPO中套用到VDI的VM OU中,並在GPO中預先啟用遠端桌面並指派VDI使用者可登入

param ( 
    [ValidatePattern("(.+)\\(.+)")] 
    [Parameter(Mandatory=$TRUE, Position=0, HelpMessage="RD Virtualization Host server")] 
    [string[]] 
    $RDVHost, 
 
    [ValidatePattern("(.+)\\(.+)")] 
    [Parameter(Mandatory=$FALSE, Position=1, HelpMessage="Remote Desktop Users")] 
    [string[]] 
    $RDUsers, 
 
    [Parameter(Mandatory=$FALSE, Position=2, HelpMessage="File to which events are to be logged")] 
    [string] 
    $LogFile = ".\Configure-VirtualMachine.log", 
     
    [Parameter(Mandatory=$FALSE, HelpMessage="Option not to restart the Remote Desktop Services service")] 
    [switch] 
    $DoNotRestartService, 
 
    [Parameter(Mandatory=$FALSE, HelpMessage="Option to ignore verification of object when credentials are not valid")] 
    [switch] 
    $Force 
) 
 
function Write-Log ([string]$Message = "", [string]$Type = "verbose") 
{ 
    switch ($Type) 
    { 
        "error"       {Write-Error $Message} 
        "warning"     {Write-Warning $Message} 
        "verbose"     {Write-Verbose $Message} 
        "host"        {Write-Host $Message} 
        "initialize"  { "" > $LogFile; return } 
    } 
     
    $Message >> $LogFile 
} 
 
#   Check if the script is running with administrator/elevated privileges 
function Check-Credentials 
{ 
    $principal = New-Object Security.Principal.WindowsPrincipal ([Security.Principal.WindowsIdentity]::GetCurrent()) 
    $elevated = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)   
 
    if (-not $elevated) 
    { 
        Write-Error "Please run this script in an elevated shell!" 
        exit 1 
    } 
} 
 
function Grant-RDPPermissions([String]$RDVHost, [bool]$isXP = $FALSE) 
{ 
    $adsiPath = "WinNT://{0}/{1}" -f ($RDVHost -split "\\") 
    if ((([ADSI]$adsiPath).Class -ne "Group") -AND ($vhost -notmatch '\$$')) 
    { 
        $RDVHost = "$RDVHost$" 
    } 
 
    $nameSpace = if ($isXP) {"root\cimv2"} else {"root\cimv2\terminalservices"} 
 
    $tsAccounts = @(Get-WMIObject -Namespace $nameSpace -Query "SELECT * FROM Win32_TSAccount WHERE (TerminalName = 'RDP-TCP' OR TerminalName = 'Console') AND AccountName = '$($RDVHost.replace("\", "\\"))'") 
 
    if ($tsAccounts -eq $NULL -or $tsaccounts.count -eq 0) 
    { 
        Write-Log "  $RDVHost is being added to the RDP-TCP permissions list" "verbose" 
         
        $permissionSettings = @(Get-WmiObject -Namespace $nameSpace -Query "SELECT * FROM Win32_TSPermissionsSetting WHERE TerminalName = 'RDP-TCP'") 
         
        foreach($setting in $permissionSettings) 
        { 
            $setting.addaccount("$RDVHost", 1) | Out-Null 
            ${script:restartRequired} = $TRUE 
        } 
    } 
 
    $tsAccounts = @(Get-WMIObject -Namespace $nameSpace -Query "SELECT * FROM Win32_TSAccount WHERE (TerminalName = 'RDP-TCP' OR TerminalName = 'Console') AND AccountName = '$($RDVHost.replace("\", "\\"))'") 
     
    foreach($account in $tsAccounts) 
    { 
        if (($account.PermissionsAllowed -band 517) -ne 517) 
        { 
            Write-Log "  Granting permissions : $RDVHost" "verbose" 
             
            $account.ModifyPermissions(0,1) | Out-Null 
            $account.ModifyPermissions(2,1) | Out-Null             
            $account.ModifyPermissions(9,1) | Out-Null 
             
            ${script:restartRequired} = $TRUE 
        } 
    } 
} 
 
 
function Configure-XP() 
{ 
    #   1. Enable Remote Desktop. 
 
    Write-Log "`n  Enabling Remote Desktop..." "verbose" 
 
    $result = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' -Name fDenyTSConnections -ErrorAction SilentlyContinue 
 
    if ($result -eq $NULL) 
    { 
        $result = New-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' -Name fDenyTSConnections -PropertyType DWORD -Value 0 
    } 
    elseif ($result.AllowRemoteRPC -ne 1) 
    { 
        $result = Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' -Name fDenyTSConnections -Value 0 -Passthru 
        ${script:restartRequired} = $TRUE 
    } 
 
    if ($result.fDenyTSConnections -eq 0) 
    { 
        Write-Log "    Done" "verbose" 
    } 
    else 
    { 
        Write-Log "Remote Desktop could not be enabled" "error" 
        $anyFailures = $TRUE 
    } 
 
 
 
    #   2. Enable RPC. 
 
    Write-Log "`n  Enabling RPC..." "verbose" 
     
    $result = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' -Name AllowRemoteRpc -ErrorAction SilentlyContinue 
 
    if ($result -eq $NULL) 
    { 
        $result = New-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' -Name AllowRemoteRPC -PropertyType DWORD -Value 1 
    } 
    elseif ($result.AllowRemoteRPC -ne 1) 
    { 
        $result = Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' -Name AllowRemoteRpc -Value 1 -Passthru 
        ${script:restartRequired} = $TRUE 
    } 
     
    if ($result.AllowRemoteRpc -eq 1) 
    { 
        Write-Log "    Done" "verbose" 
    } 
    else 
    { 
        Write-Log "Remote Desktop could not be enabled" "error" 
        $anyFailures = $TRUE 
    } 
 
 
    #   3. Add users to the Remote Desktop Users group. 
 
    if (($RDUsers -ne $NULL) -AND ($RDUsers.Count -gt 0)) 
    { 
        Write-Log "`n  Adding users to Remote Desktop Users group..." "verbose" 
 
        $RDUsers | foreach {  
            net localgroup 'Remote Desktop Users' $_ /add 2> $Null > $Null 
            if ($LASTEXITCODE -eq 1) 
            { 
                Write-Log "      Failed to add user $_ to 'Remote Desktop Users' group..." "error" 
            } 
        } 
     
        Write-Log "    Done" "verbose" 
    } 
 
 
 
    #   4. Grant RD Virtualization Host servers permissions in the RDP-TCP listener. 
 
    Write-Log "`n  Granting RD Virtualization Host server RDP-TCP permissions..." "verbose" 
 
    $RDVHost | %{Grant-RDPPermissions $_ $TRUE} 
     
    Write-Log "    Done" "verbose" 
 
 
 
    #   5. Enable Windows Firewall to allow an exception for Remote Desktop. 
 
    Write-Log "`n  Enabling firewall for 'remote desktop'..." "verbose" 
 
    netsh firewall set service type=REMOTEDESKTOP mode=ENABLE profile=ALL 2> $Null > $Null 
    if ($LASTEXITCODE -eq 0) 
    { 
        Write-Log "    Done" "verbose" 
    } 
    else 
    { 
        Write-Log "Firewall could not be enabled for 'remote desktop'" "error" 
        $anyFailures = $TRUE 
    } 
 
 
 
    #   6. Enable Windows Firewall to allow an exception for Remote Service Management. 
 
    Write-Log "  Enabling firewall for 'remote service management'..." "verbose" 
 
    netsh firewall set service remoteadmin enable subnet 2> $Null > $Null 
    if ($LASTEXITCODE -eq 0) 
    { 
        Write-Log "  Done" "verbose" 
    } 
    else 
    { 
        Write-Log "Firewall could not be enabled for 'remote service management'" "error" 
        $anyFailures = $TRUE 
    } 
 
 
 
    #   7. Restart the Remote Desktop Services service. 
 
    if ((-not $DoNotRestartService) -AND (${script:restartRequired})) 
    { 
        Write-Log "Rebooting machine..." "verbose" 
 
        shutdown /r /t 10 
    } 
} 
 
 
function Configure-PostXP() 
{ 
    #   1. Enable Remote Desktop. 
 
    Write-Log "`n  Enabling Remote Desktop..." "verbose" 
 
    $result = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' -Name fDenyTSConnections -ErrorAction SilentlyContinue 
 
    if ($result -eq $NULL) 
    { 
        $result = New-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' -Name fDenyTSConnections -PropertyType DWORD -Value 0 
    } 
    elseif ($result.AllowRemoteRPC -ne 1) 
    { 
        $result = Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' -Name fDenyTSConnections -Value 0 -Passthru 
        ${script:restartRequired} = $TRUE 
    } 
 
    if ($result.fDenyTSConnections -eq 0) 
    { 
        Write-Log "    Done" "verbose" 
    } 
    else 
    { 
        Write-Log "Remote Desktop could not be enabled" "error" 
        $anyFailures = $TRUE 
    } 
 
 
 
    #   2. Enable RPC. 
 
    Write-Log "`n  Enabling RPC..." "verbose" 
 
    $result = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' -Name AllowRemoteRpc -ErrorAction SilentlyContinue 
 
    if ($result -eq $NULL) 
    { 
        $result = New-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' -Name AllowRemoteRPC -PropertyType DWORD -Value 1 
    } 
    elseif ($result.AllowRemoteRPC -ne 1) 
    { 
        $result = Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' -Name AllowRemoteRpc -Value 1 -Passthru 
        ${script:restartRequired} = $TRUE 
    } 
     
    if ($result.AllowRemoteRpc -eq 1) 
    { 
        Write-Log "    Done" "verbose" 
    } 
    else 
    { 
        Write-Log "Remote Desktop could not be enabled" "error" 
        $anyFailures = $TRUE 
    } 
 
 
    #   3. Add Users to the Remote Desktop Users group. 
 
    if (($RDUsers -ne $NULL) -AND ($RDUsers.Count -gt 0)) 
    { 
        Write-Log "`n  Adding users to the Remote Desktop Users group..." "verbose" 
 
        $RDUsers | foreach {  
            net localgroup 'Remote Desktop Users' $_ /add 2> $Null > $Null 
            if ($LASTEXITCODE -eq 1) 
            { 
                Write-Log "      Failed to add user $_ to the Remote Desktop Users group..." "error" 
            } 
        } 
     
        Write-Log "    Done" "verbose" 
    } 
 
 
 
    #   4. Grant RD Virtualization Host servers permissions in the RDP-TCP listener. 
 
    Write-Log "`n  Granting RD Virtualization hosts RDP-TCP listener permissions..." "verbose" 
 
    $RDVHost | %{Grant-RDPPermissions $_} 
 
    Write-Log "    Done" "verbose" 
 
 
 
    #   5. Enable Windows Firewall to allow an exception for Remote Desktop. 
 
    Write-Log "`n  Enabling firewall for 'remote desktop'..." "verbose" 
 
    netsh advfirewall firewall set rule group="remote desktop" new enable=yes 2> $Null > $Null 
    if ($LASTEXITCODE -eq 0) 
    { 
        Write-Log "    Done" "verbose" 
    } 
    else 
    { 
        Write-Log "Firewall could not be enabled for 'remote desktop'" "error" 
        $anyFailures = $TRUE 
    } 
 
 
 
    #   6. Enable Windows Firewall to allow an exception for Remote Service Management. 
 
    Write-Log "  Enabling firewall for 'remote service management'..." "verbose" 
 
    netsh advfirewall firewall set rule group="remote service management" new enable=yes 2> $Null > $Null 
    if ($LASTEXITCODE -eq 0) 
    { 
        Write-Log "  Done" "verbose" 
    } 
    else 
    { 
        Write-Log "Firewall could not be enabled for 'remote service management'" "error" 
        $anyFailures = $TRUE 
    } 
 
 
 
    #   7. Restart the Remote Desktop Services service. 
 
    if ((-not $DoNotRestartService) -AND (${script:restartRequired})) 
    { 
        Write-Log "Restarting the Remote Desktop Services service..." "verbose" 
 
        $termService = Get-Service "TermService" 
 
        Write-Log "  Stopping TermService..." "verbose" 
        $termService.Stop() 
        Start-Sleep 5 
 
        Write-Log "  Starting TermService..." "verbose" 
        $termService.Start() 
        Start-Sleep 5 
    } 
} 
 
function Test-ADObject ([string[]]$adEntries, [string[]]$Type) 
{ 
    $toExit = $FALSE 
    foreach($entry in $adEntries) 
    { 
        $adsiPath = "WinNT://{0}/{1}" -f ($entry -split "\\") 
        try  
        { 
            [ADSI]$adsiPath | Select-Object Name | Out-Null 
        } 
        catch 
        { 
            $throwError = !${script:Force} 
             
            if ($Type -contains "Computer") 
            { 
                $adsiPath = "WinNT://{0}/{1}$" -f ($entry -split "\\")                 
                try 
                { 
                    [ADSI]$adsiPath | Select-Object Name | Out-Null 
                    $throwError = $FALSE 
                } 
                catch { } 
            } 
             
            if ($throwError) 
            { 
                $toExit = $TRUE 
                Write-Log "Existence of specified object '$entry' could not be verified. $($_.Exception.InnerException.Message.Trim())" "error"                 
            } 
            continue 
        } 
         
        $adsiObj = [ADSI]$adsiPath 
         
        if ($adsiObj.Name -eq $NULL) 
        { 
            $toExit = $TRUE 
            Write-Log "Specified object '$entry' does not exist" "error" 
        } 
        elseif($Type -notcontains $adsiObj.Class) 
        { 
            $toExit = $TRUE 
            Write-Log "Specified object '$entry' is invalid. Specify an object of type $([string]::Join(" or ", $Type))" "error" 
        } 
    } 
     
    if ($toExit) 
    { 
        exit 1 
    } 
} 
 
Write-Log "" "initialize" 
Check-Credentials 
 
Write-Log "Preparing virtual machine for the RD Virtualization Host server: " "verbose" 
 
Write-Log "  RDV Host  : $([string]::Join(", ", $RDVHost))" "verbose" 
Test-ADObject $RDVHost @("Computer", "Group") 
 
if ($RDUsers.Count -gt 0) 
{ 
    Write-Log "  RD Users  : $([string]::Join(", ", $RDUsers))" "verbose" 
    Test-ADObject $RDUsers @("User", "Group") 
} 
 
$restartRequired = $FALSE 
$anyFailures = $FALSE 
 
$version = New-Object System.Version 6, 0, 0, 0 
if ([System.Environment]::OsVersion.Version -lt $version) 
{ 
    Configure-XP 
} 
else 
{ 
    Configure-PostXP 
} 
 
if ($anyFailures) 
{ 
    Write-Log "ERROR : There were one more errors preparing the virtual machine for the RD Virtualization Host server" "error" 
} 
else 
{ 
    Write-Log "Virtual machine was prepared for use with the RD Virtualization Host server" "verbose" 
    Write-Host "This virtual machine has been successfully configured for use with RD Virtualization." 
}

如果要搭配自動化RemoteApp設定,會需要將RemoteApp的Config先匯出到NAS上,然後將下面這個PowerShell指派到User的Login Script內

這樣就可以自動去套RemoteApp的設定(需要自己寫一個Bat去套用wcx,如Install-RADCConnection.ps1 \\FileServer\RemoteApps.wcx)

<# .SYNOPSIS 
    Installs a connection in RemoteApp and Desktop Connections. 
.DESCRIPTION 
    This script uses a RemoteApp and Desktop Connections bootstrap file(a .wcx file) to set up a connection in Windows 7 workstation. No user interaction is required.It sets up a connection only for the current user. Always run the script in the user's session. 
 
The necessary credentials must be available either as domain credentials or as cached credentials on the local machine. (You can use Cmdkey.exe to cache the credentials.) 
 
Error status information is saved in event log: (Applications and Services\Microsoft\Windows\RemoteApp and Desktop Connections). 
 
.Parameter WCXPath 
    Specifies the path to the .wcx file 
     
.Example 
     
PS C:\> Install-RADCConnection.ps1 c:\test1\work_apps.wcx 
 
Installs the connection in RemoteApp and Desktop Connections using information 
in the specified .wcx file. 
     
#> 
Param( 
    [parameter(Mandatory=$true,Position=0)] 
    [string] 
    $WCXPath 
) 
 
 
function CheckForConnection 
{ 
    Param ( 
        [parameter(Mandatory=$true,Position=0)] 
        [string] 
        $URL 
    ) 
 
    [string] $connectionKey = "" 
    [bool] $found = $false 
 
    foreach ($connectionKey in get-item 'HKCU:\Software\Microsoft\Workspaces\Feeds\*' 2> $null)  
    { 
        
        if ( ($connectionKey | Get-ItemProperty -Name URL).URL -eq $URL) 
        { 
            $found = $true 
            break 
        } 
    } 
 
    return $found 
} 
 
 
# Process the bootstrap file 
[string] $wcxExpanded = [System.Environment]::ExpandEnvironmentVariables($WCXPath)  
[object[]] $wcxPathResults = @(Get-Item $wcxExpanded 2> $null) 
 
if ($wcxPathResults.Count -eq 0) 
{ 
    Write-Host @" 
 
The .wcx file could not be found. 
 
"@ 
 
    exit(1) 
} 
 
if ($wcxPathResults.Count -gt 1) 
{ 
    Write-Host @" 
 
Please specify a single .wcx file. 
 
"@ 
 
    exit(1) 
} 
 
[string] $wcxFile = $wcxPathResults[0].FullName 
[xml] $wcxXml = [string]::Join("", (Get-Content -LiteralPath $wcxFile))  
[string] $connectionUrl = $wcxXml.workspace.defaultFeed.url 
 
if (-not $connectionUrl) 
{ 
    Write-Host @" 
 
The .wcx file is not valid. 
 
"@ 
 
    exit(1) 
} 
 
if ((CheckForConnection $connectionUrl)) 
{ 
    Write-Host @" 
 
The connection in RemoteApp and Desktop Connections already exists. 
 
"@ 
 
    exit(1) 
} 
 
Start-Process -FilePath rundll32.exe -ArgumentList 'tsworkspace,WorkspaceSilentSetup',$wcxFile -NoNewWindow -Wait 
 
# check for the Connection in the registry 
if ((CheckForConnection $connectionUrl)) 
{ 
    Write-Host @" 
 
Connection setup succeeded. 
 
"@ 
} 
else 
{ 
    Write-Host @" 
 
Connection setup failed. 
 
Consult the event log for failure information: 
(Applications and Services\Microsoft\Windows\RemoteApp and Desktop Connections). 
 
"@ 
     
    exit(1) 
} 
 

當測試都OK,也確定可以正常建立VM並指派到Pool中之後

您將第一個PowerShell放入排程中連續執行就可以了,只要一偵測到VM Session不夠,就會建立VM指派到Pools中

其實說穿了,概念很簡單,只是先讓Win7具有自動加入網域,相關的設定都先透過GPO設定好

然後TS PowerShell透過VMM取得全部VM的資訊,取得VDI VM的總數量,然後Monitor Online的Session取得一個Session比例值

透過相同的Hostname前置名稱,後面帶流水號,這樣就不會有重複名稱的問題

當建立完VM,同時製作一個Assin.txt給RDCB,等整個建立過程都完成後,透過WinRM給RDCB下指令去Assin這些VM到Pool內

這是整體運作上的概念,實際運行上其實還有些考量點,像是Memory、Network、CPU、GPU等

這篇只是分享給大家參考,因為Microsoft並沒有自動Assin VM到Pool的機制存在,也歡迎大家透過部落格跟我討論:P