Wilco van Bragt - LinkeIn Wilco van Bragt - Twitter rssa 

Unattended Installation Citrix Provisioning Services 6.1 Part 3

In the previous Unattended Installation Citrix Provisioning Services articles I explained how PVS can be installed automated via scripts. In most cases these articles are enough for most infrastructures. However if you would like to set-up more PVS environments it would be nice if could reuse the scripts and/or the answerfiles by making them variable. For one of my customers this was the case and I did that using PowerShell (their default scripting language). So actually this third part is more a PowerShell for dummies articles (I do not have much PowerShell knowledge, but this assignment showed me how powerful PowerShell is) than about the unattended installation but it gives a good insight in the possibilities.


To use reuse the scripts and answer files you need to make them variable. Making scripts variable implies that you need to have somewhere the information that need to be filled in at the sections where variables are used. As I earlier created this kind of tasks with CMD scripting where using INI files for storing the information. Although PowerShell offers also other possibilities like XML file, I stayed at the INI files as I’m used with that and it does the job. Which component you would like to create variable depends on how flexible you would like to have the solution and the available component within the infrastructure. In this case I had a specific instance on the SQL database server and the choice about PXE/TFTP was by design and should be implemented the same on all flavours that will be installed with the scripts.

A INI file could be like this.




PVSFarm=Farm   VanBragt

PVSSite=VBN   Site

PVSCollection=XenApp   6.5







The next step is read this information out of this INI file, so it can be used within the scripts to fill in these variables. As with all new things Google is your friend, so I started searching for a command that fulfills. Quickly I found out that the Get-Content with a ForEach was the way to go and with that info I shortly found the full command line that does the job. With this command followed by adding the read information into PowerShell variables this part was done, the code is shown in the following table.

Get-Content   "$VariableFile" | ForEach-Object -Begin {$settings=@{}} -Process   {$store = [regex]::split($_,'='); if(($store[0].CompareTo("") -ne   0) -and ($store[0].StartsWith("[") -ne $True) -and   ($store[0].StartsWith("#") -ne $True)) {$settings.Add($store[0],   $store[1])}}

$PVSDatabaseServerValue   = $settings.Get_Item("PVSDatabaseServer")

$PVSDatabaseNameValue   = $settings.Get_Item("PVSDatabaseName")

$PVSFarmValue   = $settings.Get_Item("PVSFarm")

$PVSSiteValue   = $settings.Get_Item("PVSSite")

$PVSCollectionValue   = $settings.Get_Item("PVSCollection")

$PVSADGroupValue   = $settings.Get_Item("PVSADGroup")

$PVSStoreValue   = $settings.Get_Item("PVSStore")

$PVSDefaultPathValue   = $settings.Get_Item("PVSDefaultPath")

$CtxLicenseServerValue   = $settings.Get_Item("CtxLicenseServer")

$CtxLicenseServerPortValue=   $settings.Get_Item("CtxLicPort")

At this customer there were two NICs within the PVS Server, where only one NIC will be used for the PVS streaming. Therefore I need to find a way to determine which NIC should be used for the PVS Streaming. I first decided to use a part of the IP address, assuming that the second NIC will be on a separate network/subnet. However in the test environment the subnets were that small that the two VLANS were using the same first three octets, so I needed to find another way. Luckily they were renaming the default Local Connection NIC name to their function, so I could use the name of the NIC to determine which I would use. As both methods can be useful I will show the code, first based on the IP address. I used a variable here, to show the possibility. In the final version this should be moved to the INI file so the range can also be made variable.


$IPConfigset   = Get-WmiObject Win32_NetworkAdapterConfiguration


foreach   ($IPConfig in $IPConfigSet) {

                        if   ($IPConfig.Ipaddress) {

                                               foreach   ($addr in $IPConfig.Ipaddress) {

                                               if   ($addr.StartsWith("$IPRangePRD")) {

                                                                       $PVSNetworkAdapterValue   = "$addr"






The second based on the NIC name I struggled with to separate the IPV4 and IPV6 address (I only need to have the IPV4 address). I found out can use that the AddressFamily object (yes, Google again) can be used to determine the IP address type. InterNetwork is used for IPV6. Also the netconnectionID is based on another WMI object than the IP address.

$NIC=gwmi   win32_networkadapter -Filter 'netconnectionid = "LAN"'



$PVSNetworkAdapterValue=Get-WmiObject   win32_networkadapterconfiguration -Filter "index='$NICID'" |

                        Select-Object -Expand   IPAddress | Where-Object { ([Net.IPAddress]$_).AddressFamily -eq   "InterNetwork" }


Now we have all the information we need to create the answer file. What I did is using a template where I defined which parts should be replaced by the values we have defined in the INI file and are now connected to PowerShell variables.

















BootstrapFile=C:\ProgramData\Citrix\Provisioning   Services\Tftpboot\ARDBP32.BIN


Out of this template the parts started with a # should be replaced. I just used the # to make it clear which parts of the template will be replaced and which parts are hardcoded. The variables $AnswerFileSource and $AnswerFile should be defined before running this part. The $AnswerFileSource (which is displayed in the table above) will we read and the values will be replaced with the variables out of the INI file. This is text based, so do not define a replace part that is has the same name. For example #CtxLicenseServer and #CtxLicenceServerPort will both replaced with the value defined at $CtxLicenseServerValue as #CtxLicenseServer is completely defined in both words.

(Get-Content   "$AnswerFileSource") -replace "#PVSDatabaseServer",   "$PVSDatabaseServerValue" -replace   "#PVSDatabaseInstance", "$PVSDatabaseInstanceValue"   -replace "#PVSFarm", "$PVSFarmValue" -replace   "#PVSSite", "$PVSSiteValue" -replace   "#PVSStore", "$PVSStoreValue" -replace   "#PVSDefaultPath", "$PVSDefaultPathValue" -replace   "#CtxLicenseServer", "$CtxLicenseServerValue" -replace   "#CtxLicServerPort", "$CtxLicenseServerPortValue"   -replace "#PVSNetworkAdapter", "$PVSNetworkAdapterValue"   | Set-Content -Encoding Unicode $AnswerFile


Now we have the answerfile completed. However in this case we were not allowed to create the database with the PVS software, but it needed to be defined by the SQL administrator. PVS have an executable dbscript to create a SQL script for setting-up the database but you need to define the values already for several PVS components like the farm, site, collection and more. I decided to use the same concept for the .SQL script. I created one with the placeholder values that should be replaced by the actual values. You should do this very carefully because you are editing .SQL script created by the PVS software. It’s the most easy to create the .SQL script which easy to find settings, so you can search and replace. One really important thing is that the dbscript.exe is adding the GUID of the group specified as the PVS Farm Administrator Group. If you are using different groups or domains (which is logically when setting this part variable), you need to change the GUID. I used the adfind tool to determine the GUID and make a PowerShell variable of it. The full command is showed in below shown table. I will go a bit more in detail about this in an upcoming part 4.

#Determine the GUID of the PVS AD Local Admin Group

$adfindresult=(&$adfindexe -b dc="$env:USERDOMAIN,dc=local" -f "(sAMAccountName=$PVSLocalAdminGroup)" objectGUID)
$PVSLocalAdminGuid = [regex]::match($adfindresult,'\{([^\{]+)\}').Groups[1].Value

By using the same command line, reading this template SQL file and replacing the placeholders with the actual values, we are creating an usable .SQL script again. This SQL script can be then be used by the SQL administrator with SQLCMD or the SQL Management Console to create the database.

(Get-Content   "$SQLScriptSource") -replace "#PVSDatabaseName",   "$PVSDatabaseNameValue" -replace "#PVSFarm",   "$PVSFarmValue" -replace "#PVSSite",   "$PVSSiteValue" -replace "#PVSCollection",   "$PVSCollectionValue" -replace "#PVSADGroup",   "$PVSADGroupValue" –replace “#ADGUID”,”$PVSADGUID” | Set-Content   $SQLScript


With the part three article I don’t actually discussed the unattended installation possibilities anymore, but showed how to use template files to make the unattended installation using PowerShell command lines if you would to create scripts that can be re-used for different infrastructures. A final and last part will follow describing the PVS peculiarities with the unattended installation.