Automating Lab Buildouts with XenServer PowerShell – Part 4 Roles, Features, and Other Components

Part 1 – Understanding the Requirements
Part 2 – Creating a Custom ISO
Part 3 – Unlimited VM Creation
>>Part 4 – Roles, Features, and Other Components


Creating an automated lab has its benefits, but what about the additional configuration of roles and features after all the servers are built? Building out all of these components can take some time, time that you may not have.

For this reason, AXL has the functionality to add a small subset of additional roles and features to any server that was created. The current roles and features that can be installed and configured with AXL include, Active Directory Domain Services (AD DS), Active Directory Certificate Services (AD CS), and Distributed File System (DFS).

It’s important to note that you can only configure these additional roles and features if the custom ISO you created in part 2 has XenServer Tools in it. If you did not select to put XenServer Tools in the ISO, there will be no way to grab the servers IP address after installation.

Upon completion of server creation, you will be prompted whether or not you want to configure additional roles and features. Upon selecting yes, you will get a prompt as show in Figure 1. If you choose to install any of the additional roles and features, the only requirement is AD DS, as noted by it being automatically checked and grayed out via the component selection form, everything else is optional.

Each of the other roles and features require the server to be part of a domain, which is why AD DS is a requirement. The total additional time of completion will depend on the selected roles and features, each one will take a varying amount of time depending on how large the buildout is.

Figure 1 – Component

AD DS Buildout

Upon selecting to build out additional roles and features, you are required to configure AD DS. The complete configuration includes a mandatory AD DS configuration and an optional User, Group, and OU configuration. I should note that at any time during the configuration of any form you wish to go back and reconfigure something, you can do so by selecting the previous button, if present.

The configuration for AD DS is a lot like the normal configuration you would go through if you were doing it directly on the server, however, there are some other configurations that go along with this form that you would normally do prior to domain creation; notably the IP configuration, as seen in Figure 2. Starting at the top, you will need to configure the local administrator username and password (configured when making the custom ISO), domain name, and safe mode password.

In the next section, you will notice a large list box on the left with all the servers you created in the previous form. Each server will need to be configured with an IP, default gateway, subnet mask, and DNS server(s) and can be done by selecting each server individually from the listbox; the DNS server configuration is important when joining a server to the domain, you will want at least one domain controller IP in the DNS server configuration for proper functionality. As you fill in each of the text boxes for each server, an array will simultaneously be filled in with the information input to allow complete control over the configuration.

Figure 2 – Domain Buildout

Below, you will see a code snippet on how the IP configurations are actually changed.

Function ChangeIPAddresses {
    foreach($XenVMServer in ($Global:AllCreatedServers | sort)) {
    #Define necessary parameters for IP configuration
    $ConnectionPassword = ConvertTo-SecureString -AsPlainText -Force -String $LocalPasswordTextBox.Text
    $ConnectionCreds = New-Object -typename System.Management.Automation.PSCredential -ArgumentList "$($Global:OldIPAddresses[($Global:AllCreatedServers | sort).IndexOf($XenVMServer)])\$($LocalUsernameTextBox.Text)",$ConnectionPassword
    $NewIPAddress = $Global:IPAddresses[($Global:AllCreatedServers | sort).IndexOf($XenVMServer)]
    $PrefixLength = Convert-IpAddressToMaskLength $Global:SubnetMasks[($Global:AllCreatedServers | sort).IndexOf($XenVMServer)]
    $DefaultGateway = $Global:DefaultGateways[($Global:AllCreatedServers | sort).IndexOf($XenVMServer)]
    $DNSServers = "$($Global:PrimaryDNSServers[($Global:AllCreatedServers | sort).IndexOf($XenVMServer)]),$($Global:SecondaryDNSServers[($Global:AllCreatedServers | sort).IndexOf($XenVMServer)])"
        Invoke-Command -ComputerName $Global:OldIPAddresses[($Global:AllCreatedServers | sort).IndexOf($XenVMServer)] -credential $ConnectionCreds -ScriptBlock {
            param ($NewIPAddress, $PrefixLength, $DefaultGateway, $DNSServers)
            #Define the original IP address
            $OriginalIPAddress = ((Get-NetIPConfiguration).IPv4Address).IPAddress
            #Set the DNS Servers
            Set-DnsClientServerAddress -InterfaceAlias (Get-NetIPConfiguration).InterfaceAlias -ServerAddresses $DNSServers
            #Disable IPv6
            Disable-NetAdapterBinding -InterfaceAlias (Get-NetIPConfiguration).InterfaceAlias -ComponentID ms_tcpip6
            #Set the new IP address with the IP, Subnet Mask, and Default Gateway
            New-NetIPAddress -IPAddress $NewIPAddress -InterfaceAlias (Get-NetIPConfiguration).InterfaceAlias -PrefixLength $PrefixLength -DefaultGateway $DefaultGateway
                #Remove the old IP configuration only if the new and old IPs don't match
                if((((Get-NetIPConfiguration).IPv4Address).IPAddress | where {$_ -match $OriginalIPAddress}) -and ($NewIPAddress -NotMatch $OriginalIPAddress)) {
                Remove-NetIPAddress -IPAddress (((Get-NetIPConfiguration).IPv4Address).IPAddress | where {$_ -match $OriginalIPAddress}) -InterfaceAlias (Get-NetIPConfiguration).InterfaceAlias -Confirm:$False
        } -ArgumentList $NewIPAddress, $PrefixLength, $DefaultGateway, $DNSServers -AsJob
    WaitScript 2

After all the aforementioned information is filled in, the next thing to configure would be which servers you want to make Domain Controllers. There must be at least one domain controller, if multiples are selected, you can choose which one will be the primary Domain Controller; the first server selected will automatically become the primary, but this can be changed if desired.

Once everything is configured to your liking, you need to validate the configuration by selecting the validate button. This will verify correct syntax for the domain name, safe mode password, IP schemas, and other minor configurations.

Below is a snippet of code outlining the primary Domain Controller promotion process.

Function PromotePrimaryDomainController {
    foreach($DCServer in ($DomainControllersListBox.Items | where {$_ -match [regex]'\*'})) {
    #Define Domain specific parameters
    $DomainName = $DomainNameTextBox.Text
    $SafeModePassword = ConvertTo-SecureString $SafeModePasswordTextBox.Text -AsPlainText -force
    $ConnectionPassword = ConvertTo-SecureString -AsPlainText -Force -String $LocalPasswordTextBox.Text
    $ConnectionCreds = New-Object -typename System.Management.Automation.PSCredential -argumentlist "$($Global:IPAddresses[($Global:AllCreatedServers | sort).IndexOf($DCServer.Replace("*",''))])\$($LocalUsernameTextBox.Text)",$ConnectionPassword
        if($DFSCheckbox.CheckState -eq "Checked") {
            $VMStatusTextBox.AppendText("`r`nInstalling DFSR Components on $($DCServer.Replace("*"," ")) for DFS Buildout")
            $DFSComponents = Invoke-Command -ComputerName $Global:IPAddresses[($Global:AllCreatedServers | sort).IndexOf($DCServer.Replace("*",""))] -credential $ConnectionCreds -ScriptBlock {
            #Install DFSR components if DFS was selected during component selection, this is necessary for DFS buildout functionality
            Install-WindowsFeature FS-DFS-Replication -IncludeManagementTools
            } -AsJob
            WaitJob $DFSComponents
        $DCPromotion = Invoke-Command -ComputerName $Global:IPAddresses[($Global:AllCreatedServers | sort).IndexOf($DCServer.Replace("*",""))] -credential $ConnectionCreds -ScriptBlock {
        param ($DomainName,$SafeModePassword)
        #Create the AD DS Forest with the paramaeters specified in the AD DS buildout form
        Install-ADDSForest -DomainName $DomainName -SafeModeAdministratorPassword $SafeModePassword -DomainNetBIOSName $DomainName.Remove($DomainName.IndexOf(".")).ToUpper() -SYSVOLPath "C:\Windows\SYSVOL" -LogPath "C:\Windows\NTDS" -DatabasePath "C:\Windows\NTDS" -InstallDNS -Force
        } -ArgumentList $DomainName,$SafeModePassword -AsJob
        WaitJob $DCPromotion
        #If the Domain Controller does not reboot automatically within 15 seconds, reboot the machine
        if(Test-Connection -ComputerName $Global:IPAddresses[($Global:AllCreatedServers | sort).IndexOf($DCServer.Replace("*",""))] -Count 1 -ErrorAction SilentlyContinue) {
        Invoke-XenVM -Name $DCServer -XenAction CleanReboot 

No matter what was chosen on the initial component selection screen, after selecting next on the domain buildout form, you will always get the User, Group, OU buildout form if you want to configure any users, groups, or OUs for your environment.

This form is 100% optional and does not require any of the fields to be filled out. If you do not want to configure any users, groups, or OUs, simply just move onto the next form, if any.

However, if you do choose to fill it out, you will notice three different section, each labeled with their intended purpose. Figure 3 depicts what a filled-out form might look like.

Figure 3 – User Group OU Buildout

Each OU added to the structure can be placed under any OU already created and can be as many levels deep as you wish, though I would not recommend any more than 10 levels for any Active Directory structure. For the Users and Groups, you can input the required information and select add, which will add it to the respective list box.

You will notice there is no validate button for this form, that is because the validation is done before any item is added to a list box. This configuration provides the flexibility to allow you to configure any combination of users, groups, OU’s, or none at all.

AD Certificate Services Buildout

Figure 4 – AD CS Buildout

The next form, if this role was chosen from the form in Figure 1, is AD CS. With this form, seen in Figure 4, you have the ability to completely configure a normal AD CS buildout, as well as AD CS Web Enrollment and OCSP Responder.

Each server added to the list box will need to be configured independently, which can be done by selecting each server from the list box and configuring the required fields.

Each field is entirely separate for each server, meaning you can do different configurations for each one, depending on the CA type chosen. Each Server in the list box can either be a root CA or subordinate CA. If you choose to create a subordinate CA, you will have a more limited selection of fields available compared to a root CA configuration.

This is because the subordinate CA gets all of its configuration from the root CA. Below is a snippet of code that is used to promote the specified CAs.

Function InstallAllServices {
$NonSubordinates = @()
$Subordinates = @()
$AllCAServers = @()
    #Fill arrays with Specified Certificate Authorities
    foreach($CAServer in $CertificateAuthoritiesListBox.Items){
        if($Global:CATypes[$CertificateAuthoritiesListBox.Items.IndexOf($CAServer)] -notmatch "Subordinate") {
        $NonSubordinates += $CAServer
        else {
        $Subordinates += $CAServer
    #Fill primary array starting with all non-subordinate CAs
    foreach($NonSubordinate in $NonSubordinates) {
    $AllCAServers += $NonSubordinate
    #Next, fill primary array with all subordinate CAs
    foreach($Subordinate in $Subordinates) {
    $AllCAServers += $Subordinate
    foreach($CAServer in $AllCAServers){
    #Define necessary connection parameters
    $DomainName = $DomainNameTextBox.Text
    $ConnectionPassword = convertto-securestring -AsPlainText -Force -String $LocalPasswordTextBox.Text
    $DomainAdminCreds = new-object -typename System.Management.Automation.PSCredential -argumentlist "$($DomainName.Remove($DomainName.IndexOf(".")).ToUpper())\Administrator",$ConnectionPassword
        #If the server is not a subordinate CA, define all parameters
        if($Global:CATypes[$CertificateAuthoritiesListBox.Items.IndexOf($CAServer)] -notmatch "Subordinate") {
            $RootCA = Invoke-Command -ComputerName $Global:IPAddresses[($Global:AllCreatedServers | sort).IndexOf($CAServer)] -credential $DomainAdminCreds -ScriptBlock {
            param ($CAType, $CAName, $HashAlgorithm, $KeyLength, $CryptoProvider, $ValidityPeriod, $ValidityPeriodUnits, $DomainAdminCreds, $DomainName)
            Install-AdcsCertificationAuthority -CAType $CAType -CACommonName $CAName -HashAlgorithmName $HashAlgorithm -KeyLength $KeyLength  -CryptoProviderName $CryptoProvider -ValidityPeriod $ValidityPeriod -ValidityPeriodUnits $ValidityPeriodUnits -Credential $DomainAdminCreds -Confirm:$False
            } -ArgumentList $Global:CATypes[$CertificateAuthoritiesListBox.Items.IndexOf($CAServer)], $Global:CANames[$CertificateAuthoritiesListBox.Items.IndexOf($CAServer)], $Global:CAHashAlgorithm[$CertificateAuthoritiesListBox.Items.IndexOf($CAServer)], $Global:CAKeyLength[$CertificateAuthoritiesListBox.Items.IndexOf($CAServer)], $Global:CACryptoProvider[$CertificateAuthoritiesListBox.Items.IndexOf($CAServer)], $Global:CAValidityPeriod[$CertificateAuthoritiesListBox.Items.IndexOf($CAServer)], $Global:CAValidityPeriodUnits[$CertificateAuthoritiesListBox.Items.IndexOf($CAServer)], $DomainAdminCreds, $DomainName -AsJob
            WaitJob $RootCA
        #Else, only create a CA using the parent specified and a few other parameters
        else {
            $SubordinateCA = Invoke-Command -ComputerName $Global:IPAddresses[($Global:AllCreatedServers | sort).IndexOf($CAServer)] -credential $DomainAdminCreds -ScriptBlock {
            param ($CAType, $CAName, $ParentCAName, $ParentCA, $DomainAdminCreds, $DomainName)
            Install-AdcsCertificationAuthority -CAType $CAType -ParentCA "$ParentCA.$DomainName\$ParentCAName" -CACommonName $CAName -Credential $DomainAdminCreds -Confirm:$False
            } -ArgumentList $Global:CATypes[$CertificateAuthoritiesListBox.Items.IndexOf($CAServer)], $Global:CANames[$CertificateAuthoritiesListBox.Items.IndexOf($CAServer)], $Global:CANames[$CertificateAuthoritiesListBox.Items.IndexOf($Global:ParentCA[$CertificateAuthoritiesListBox.Items.IndexOf($CAServer)])], $Global:ParentCA[$CertificateAuthoritiesListBox.Items.IndexOf($CAServer)], $DomainAdminCreds, $DomainName -AsJob
            WaitJob $SubordinateCA
        #If the server was chosen as a web enrollment server, install the role
        if($Global:CAWebEnrollment[$CertificateAuthoritiesListBox.Items.IndexOf($CAServer)] -eq "Checked") {
            $EnrollmentPromotion = Invoke-Command -ComputerName $Global:IPAddresses[($Global:AllCreatedServers | sort).IndexOf($CAServer)] -credential $DomainAdminCreds -ScriptBlock {
            Install-AdcsWebEnrollment -Confirm:$False
            } -AsJob
            WaitJob $EnrollmentPromotion
        #If the server was chosen as an online responder, install the role
        if($Global:CAResponder[$CertificateAuthoritiesListBox.Items.IndexOf($CAServer)] -eq "Checked") {
        $VMStatusTextBox.AppendText("`r`nPromoting $CAServer to an Online Responder")
            $ResponderPromotion = Invoke-Command -ComputerName $Global:IPAddresses[($Global:AllCreatedServers | sort).IndexOf($CAServer)] -credential $DomainAdminCreds -ScriptBlock {
            Install-AdcsOnlineResponder -Confirm:$False
            } -AsJob
            WaitJob $ResponderPromotion
    WaitScript 15

Distributed File System Build

The last form, if chosen to configure the component, is the DFS buildout form. This form allows full configuration of a complete DFS structure, including namespaces, replicated folders, and replication groups. DFS allows for replication of folders and folder contents across multiple servers, this configuration will require at least two servers be chosen for proper replication to take place.

Once the DFS servers are chosen, you need to determine what namespaces you want to create, whether you want to have just one namespace, or split it up for a more complex architecture.

Each DFS folder created in the lower section of the form will need to be in a DFS namespace, specified as DFS root in the form. Each server will get a DFSRoots folder created in the root of the C:\ drive, this will house all of the namespaces created.

Furthermore, each folder created will get created in the DFS root specified; for instance, if you created a DFS root called Common and then created a folder named Backups ­and specified Common as the DFS root, a folder would be created as such, C:\DFSRoots\Common\Backups.

There is an optional parameter for the DFS folder, being the target path. The target path specifies where the DFS folder will point to, if a folder is not specified, it will use the default location in DFSRoots. Using the example before, if you specified a target path of C:\SQL Backups, instead of the DFS folder Backups pointing to C:\DFSRoots\Common\Backups, it gets redirected to C:\SQL Backups when pathing out to the folder.

If you are unfamiliar with DFS, all of these folders live under \\\\. This structure allows for seamless, highly available, and redundant file and folder access, even if one or more servers are down depending on the size of the infrastructure.

Below is a snippet of code used to create the DFS folders. You may notice there are nested Invoke-Commands used for the DFS buildout, this is because the DFSN and DFSR commands would not work when executed remotely directly on the selected servers.

Function CreateDFSFolders {
#Define necessary connection parameters 
$DomainName = $DomainNameTextBox.Text
$ConnectionPassword = convertto-securestring -AsPlainText -Force -String $LocalPasswordTextBox.Text
$DomainAdminCreds = new-object -typename System.Management.Automation.PSCredential -argumentlist "$($DomainName.Remove($DomainName.IndexOf(".")).ToUpper())\Administrator",$ConnectionPassword
#Define the primary domain controller to execute all the commands on
$PrimaryDC = ($DomainControllersListBox.Items | where { $_ -match [regex]"\*" }).ToString().Replace("*","")
    foreach($DFSFolder in $DFSFoldersListBox.Items){
        #If there was a DFS folder target specified, continue with creating that folder and the folder in C:\DFSRoots\<Namespace>
        if($Global:DFSFolderTarget[$Global:DFSFolders.IndexOf($DFSFolder)] -ne $Null) {
            foreach($DFSServer in $DFSServersListBox.Items) {
            $DFSPath = "\\$DomainName\$($Global:DFSFolderRoot[$Global:DFSFolders.IndexOf($DFSFolder)])\$DFSFolder"
                if($DFSServer -match [regex]'\*') {
                $DFSServer = $DFSServer.Replace("*","")
                $DFSFolderCreation = Invoke-Command -ComputerName $Global:IPAddresses[($Global:AllCreatedServers | sort).IndexOf($DFSServer)] -credential $DomainAdminCreds -ScriptBlock {
                param ($DFSFolder,$DFSRoot)
                #Create new DFS folder and share it
                New-Item -ItemType Directory -Path "C:\DFSRoots\$DFSRoot\" -Name "$DFSFolder" -Force
                New-SmbShare -Path "C:\DFSRoots\$DFSRoot\$DFSFolder" -Name "$DFSRoot\$DFSFolder"
                Grant-SmbShareAccess -Name "$DFSRoot\$DFSFolder" -AccountName "Everyone" -AccessRight Full -Force 
                } -ArgumentList $DFSFolder,$Global:DFSFolderRoot[$Global:DFSFolders.IndexOf($DFSFolder)] -AsJob
                WaitJob $DFSFolderCreation
                WaitScript 5
                $FolderTarget = Invoke-Command -ComputerName $Global:IPAddresses[($Global:AllCreatedServers | sort).IndexOf($DFSServer)] -credential $DomainAdminCreds -ScriptBlock {
                param ($DFSPath,$DFSServer,$DFSFolder,$DomainAdminCreds,$PrimaryDC,$OriginalServer)
                    Invoke-Command -ComputerName $PrimaryDC -credential $DomainAdminCreds -ScriptBlock {
                    param ($DFSPath,$DFSServer,$DFSFolder,$OriginalServer)
                        #If this is the primary DFS server, use the DfsnFolder command, otherwise use DfsnFolderTarget
                        if($OriginalServer -match [regex]"\*") {
                        New-DfsnFolder -Path "$DFSPath" -TargetPath "\\$DFSServer\$DFSFolder"
                        else {
                        New-DfsnFolderTarget -Path "$DFSPath" -TargetPath "\\$DFSServer\$DFSFolder"
                    } -ArgumentList $DFSPath,$DFSServer,$DFSFolder,$OriginalServer
                } -ArgumentList $DFSPath,$DFSServer,$DFSFolder,$DomainAdminCreds,$PrimaryDC,($DFSServersListBox.Items | where {$_ -match $DFSServer}) -AsJob
                WaitJob $FolderTarget
        #Else, just make the new folder in C:\DFSRoots\<Namespace>
        else {
            foreach($DFSServer in $DFSServersListBox.Items) {
                if($DFSServer -match [regex]'\*') {
                $DFSServer = $DFSServer.Replace("*","")
                $StandaloneFolder = Invoke-Command -ComputerName $Global:IPAddresses[($Global:AllCreatedServers | sort).IndexOf($DFSServer)] -credential $DomainAdminCreds -ScriptBlock {
                param ($DFSFolder,$DFSRoot)  
                #Create new DFS folder and share it
                New-Item -ItemType Directory -Path "C:\DFSRoots\$DFSRoot\" -Name $DFSFolder -Force
                New-SmbShare -Path "C:\DFSRoots\$DFSRoot\$DFSFolder" -Name "$DFSFolder"
                Grant-SmbShareAccess -Name "$DFSFolder" -AccountName "Everyone" -AccessRight Full -Force
                } -ArgumentList $DFSFolder,$Global:DFSFolderRoot[$Global:DFSFolders.IndexOf($DFSFolder)] -AsJob
                WaitJob $StandaloneFolder

So, as a workaround, I was able to get it working by doing a nested Invoke-Command to essentially run a remote command inside of a remote command. This allowed me to execute the DFSN commands used below on a domain controller from the selected DFS server, the command sequence is as follows, My PC -> DFS Server -> DC. This was very frustrating to figure out because the initial commands gave a very arbitrary and ambiguous error code, but after rigorous testing was finally able to get it to work.

Figure 5 – DFS Buildout


This concludes all segments in this four-part series. We have discussed everything from items needed to begin creating automated labs, how to create a custom ISO for an automated installation, creating VMs rapidly and seamlessly with either default, custom, or blank templates, and finishing it off with how to configure AD DS, AD CS, and DFS all with the AXL tool.

There is a lot of functionality built into AXL and all of it is available on GitHub; you will want to make sure you download not only the PowerShell script, but all the files as well, refer to Part 1 of the series if you want a better understanding of how the files structure should be setup.

I have really enjoyed creating AXL and hope everyone who uses it finds it to be a time saving and useful tool.

Zach Thurmond
IT Consultant
Critical Design Associates

LinkedIn Profile

Automating Lab Builds with XenServer PowerShell – Part 3 Unlimited VM Creation

Part 1 – Understanding the Requirements
Part 2 – Creating a Custom ISO
>>Part 3 – Unlimited VM Creation
Part 4 – Roles, Features, and Other Components


Once you have all the necessary files and have created a custom ISO to complete an unattended installation of Windows, the next step will be to automate the creation of VMs. Doing this manually can take some time via XenCenter, especially if you are creating a large number of VMs. With the VM Build portion of AXL, you can build all 10 VMs, all with the same configuration, from a single screen. You have options to make VMs either from system sysprepped templates, blank templates, or default templates.

NOTE: If you plan on creating VMs in a pool with more than one XenServer host, there are a few considerations before moving forward which includes the following:

  • Rename each host’s local storage repository to something more identifiable than the default name ‘Local Storage’
  • When connecting to the XenServer host to create the VMs, you will need to connect to the pool master

Creating VMs From Sysprepped Templates

Upon first launch of the VM creation form, you’ll notice everything is disabled except for a few areas. This is because a connection to a XenServer host is required for any of the information fields to be populated.

If the XenServer PowerShell module is not found in any subdirectories, you will get a prompt to input the location of the XenServer PowerShell Module, as seen in Figure 1. The PowerShell Module can also be downloaded from GitHub Here .

Figure 1 – Module Location

The process for creating a sysprepped machine using AXL is quite refined compared to the other processes, especially compared to using default templates. Starting at the top and working down, the Select Storage Location drop-down will be where you select what storage repository the VM(s) will get created on which can be either local or network storage.

This information is pulled directly from the host/pool and does not include ISO repositories. In the Input VM Names text box, you can input each VM you want to create and click Add. Multiple machines can be added by separating each name either by a comma or semi-colon, but choose only one or the other type of delimiter, not both.

Selecting the names and clicking Remove can also remove any names if you decide to do so.

Onto the most important part, the Select a Template option. By default, only non-default templates will be populated in the drop down. After selecting which template to use, that’s all that is needed to create VMs from sysprepped templates. After all the information is input, select the validate button to ensure that the configuration is correct and, if it is, click Create to start building the VMs.

There is one caveat to using a sysprepped template though, and it kind of defeats the whole purpose of AXL. That is, you still have to go through the initial configuration of every VM created, whereas with a custom ISO, you don’t have to.

Creating VMs from a Blank Template

You may be asking what I mean by a blank template, and that simply means a template that has no operating system installed. Only the virtual hardware is configured such as the disk, CPU, RAM, and network. The benefit of using a blank template is that it will save you a little bit of time when running through the configuration.

Figure 2 – Sysprepped Buildout Example

Making VMs from a blank template is very similar to that of making them from sysprepped templates. There is one exception though, you have to actually select the ISO repository where the ISO is located, and which ISO you want to be installed. For the ISO repository, you may notice it is disabled in Figure 2, that is because I only had one ISO repository created for my pool.

However, if you had multiples created, the drop down would be enabled, and you could then choose the appropriate repository. To select an ISO, you first need to specify that you would actually like to insert one by selecting the Insert ISO checkbox. You can then select which ISO you want to be inserted into the created VMs and click validate to check your configuration. So long as everything checks out, you can create the configured VMs.

Figure 3 – Blank Buildout Example

NOTE: You can create multiple types of VMs, for instance Windows 10 and Server 2016, but you will need to do create them separately. After you click create, you will receive a prompt asking if you would like to create more VMs; which is when you would click yes if you wanted to make VMs with different operating systems. Doing this will clear the form completely.

Creating VMs From Default Templates

Non-default templates are great, blank and sysprepped, but you also have the option to create VMs via default templates. There is a little more configuration involved with default templates because you have to specify how much RAM, CPU, and disk you want to provide it as well as what network it should be on.

After you have filled out all required information from the blank template example, the only thing needed is for the Default Templates checkbox to be checked. This will populate the drop down with all default templates and will provide some additional drop downs to configure the resources you will allocate to the VMs.

Each drop down will have information pulled from the host to accurately configure the VMs, so you cannot over commit resources that are not there. RAM and Disk Size will be throttled back depending on how many VMs you have listed in the list box above to create.

Say for instance you listed 5 VMs in the list box and you have 32 GB of RAM on your host, taking 2 GB out for the host, you would have a maximum of 6 GB for each VM.

Figure 4 – Default Buildout Example

How it All Works

Knowing how to use the form is important, but so is knowing what is actually happening in the background. The code snippet below is the function that actually creates the VMs. Depending on what was chosen in the form, VMs will be created from either a blank, sysprepped, or default template.

Because of limitations with cloning VMs via the XenServer PowerShell module, the function will determine if the template chosen is on the same SR where the VMs were specified to be created; this would only affect you if you are using pools.

In the event the VMs being created and the template are on a different SRs, a temporary template will be copied to the SR where the VMs are to be created, then they are cloned from there on forward, if creating more than one VM. The temporary template, if created, will get removed after all VMs have been created.

Function BuildVMs {
# Specify DropDown variables 
$VMNames = $NewVMHostnameListBox.Items
$SourceTemplateName = $DropDownTemplates.SelectedItem
$StorageRepositoryName = $DropDownStorage.SelectedItem
$SelectedNetwork = $DropDownNetwork.SelectedItem
    foreach($VMName in $VMNames){
    # Specify general properties 
    $GetSRProperties = Get-XenSR -Name $StorageRepositoryName
    $GetNetworkProperties = Get-XenNetwork $SelectedNetwork
    $TemplateSRLocation = (Get-XenVM -Name $SourceTemplateName | Select -ExpandProperty VBDs | Get-XenVBD | Select -ExpandProperty VDI | Get-XenVDI | Select -ExpandProperty SR | Get-XenSR).name_label
    $ObjSourceTemplate = Get-XenVM -Name $SourceTemplateName
        if($DefaultTemplateCheckbox.CheckState -eq "Checked") { 
        # Specify required VM properties
        $VMRAM = ($DropDownRAMAmmount.SelectedItem*1GB)
        $DiskSize = ($DropDownDiskSize.SelectedItem*1GB)
        $VMCPU = $DropDownCPUCount.SelectedItem
        # Create new VM from all specified properties
        New-XenVM -NameLabel $VMName -MemoryTarget $VMRAM -MemoryStaticMax $VMRAM -MemoryDynamicMax $VMRAM -MemoryDynamicMin $VMRAM -MemoryStaticMin $VMRAM -VCPUsMax $VMCPU -VCPUsAtStartup $VMCPU -HVMBootPolicy "BIOS order" -HVMBootParams @{ order = "dc" } -HVMShadowMultiplier 1 -UserVersion 1 -ActionsAfterReboot restart -ActionsAfterCrash restart -ReferenceLabel $ObjSourceTemplate.reference_label -HardwarePlatformVersion 2 -Platform @{ "cores-per-socket" = "$VMCPU"; hpet = "true"; pae = "true"; vga = "std"; nx = "true"; viridian_time_ref_count = "true"; apic = "true"; viridian_reference_tsc = "true"; viridian = "true"; acpi = "1" } -OtherConfig @{ base_template_name = $ObjSourceTemplate.reference_label }
        $GetVMProperties = Get-XenVM -Name $VMname
        WaitScript 1
        # Create a new Virtual Disk with the same name as the new VM
        New-XenVDI -NameLabel $VMName -VirtualSize $DiskSize -SR $GetSRProperties -Type user
        WaitScript 4
        # Specify VDI and Network locations
        $NewVDI = Get-XenVDI -Name $VMName
        $VIFDevice = (Get-XenVMProperty -VM $GetVMProperties -XenProperty AllowedVIFDevices)[0]
            if($GetVMProperties -and $NewVDI){
                # Create CD drive for the new VM
                New-XenVBD -VM $GetVMProperties -VDI $null -Type CD -mode RO -Userdevice 3 -Bootable $False -Unpluggable $True -Empty $True
                # Attach previously created hard drive into the new VM
                New-XenVBD -VM $GetVMProperties -VDI $NewVDI -Type Disk -mode RW -Userdevice 0 -Bootable $True -Unpluggable $True
                # Create network interface for the new VM
                New-XenVIF -VM $GetVMProperties -Network $GetNetworkProperties -Device $VIFDevice 
                # Mount previously created hard disk
                Get-XenVM -Name $VMName | Select -ExpandProperty VBDs | Get-XenVBD | where {$_.type -eq "Disk"} | Select -ExpandProperty VDI | Set-XenVDI -NameLabel $VMName  
        if($DefaultTemplateCheckbox.CheckState -eq "Unchecked") {
            if($TemplateSRLocation -match $GetSRProperties.name_label) {
            # Create a clone of the template
            Invoke-XenVM -NewName $VMName -VM $ObjSourceTemplate -XenAction Clone
            # Provision the copy into a VM
            Invoke-XenVM -XenAction Provision -Name $VMName
            WaitScript 1
            # Rename the attached disk to the name of the VM
            Get-XenVM -Name $VMName | Select -ExpandProperty VBDs | Get-XenVBD | where {$_.type -eq "Disk"} | Select -ExpandProperty VDI | Set-XenVDI -NameLabel $VMName
            else {
            # Copy the chosen template to the SR where the VMs are being created
            Invoke-XenVM -NewName "$SourceTemplateName - TEMP" -VM $ObjSourceTemplate  -SR $GetSRProperties -XenAction Copy
            # Specify old and new template names
            $SourceTemplateName = "$SourceTemplateName - TEMP"
            $ObjSourceTemplate = Get-XenVM -Name $SourceTemplateName
            # Clone the template that was just coppied to create the first VM
            Invoke-XenVM -NewName $VMName -VM $ObjSourceTemplate -XenAction Clone
            # Provision the copy into a VM
            Invoke-XenVM -XenAction Provision -Name $VMName
            WaitScript 1
            # Rename the attached disk to the name of the VM
            Get-XenVM -Name $VMName | Select -ExpandProperty VBDs | Get-XenVBD | where {$_.type -eq "Disk"} | Select -ExpandProperty VDI | Set-XenVDI -NameLabel $VMName
            # Rename the temporary templates attached disk name
            $ObjSourceTemplate | Select -ExpandProperty VBDs | Get-XenVBD | where {$_.type -eq "Disk"} | Select -ExpandProperty VDI | Set-XenVDI -NameLabel $SourceTemplateName
        if($BlankTemplateCheckbox.CheckState -eq 'Checked' -and $DropDownISOs.SelectedItem) {
        $SelectedBootISO = $DropDownISOs.Text
        # Get the VM, select the CD drive for the VM and attach the ISO
        Get-XenVM -Name $VMName | Select -ExpandProperty VBDs | Get-XenVBD | where {$_.type -eq "CD"} | Invoke-XenVBD -XenAction Insert -VDI (Get-XenVDI -Name $SelectedBootISO).opaque_ref
    # Start the created VM to begin installing the attached ISO
    $VM = Get-XenVM -Name $VMName
    Invoke-XenVM -VM $VM -XenAction Start -Async
    $Global:AllCreatedServers += $VMName
    #If a temporary template was created, remove it and the associated disk
    if($SourceTemplateName -match "- TEMP") {
    WaitScript 1
    $ObjSourceTemplate | Select -ExpandProperty VBDs | Get-XenVBD | where {$_.type -eq "Disk"} | Select -ExpandProperty VDI | Remove-XenVDI
    WaitScript 1
    Remove-XenVM -Name $SourceTemplateName



Figure 5 – VM Creation Process

Figure 5 shows a visual representation of the VM creation process after filling out the form and clicking create.


There is really no limit to creating VMs other than available hardware. With the VM build form, you can rapidly create unlimited number of VMs in just a few minutes. This pairs especially well, and was designed for, the custom ISO created in part 2.

Now that you know all the ways VMs can be created with templates, it’s just a matter of doing it. The blank templates provide the most flexibility as you can do less configuration than default templates while receiving the same results. The blank and default templates used in tandem with the custom ISO provide a seamless way to created VMs.

In Part 4 of this blog post I will be outlining “Roles, Features, and Other Components”.

And don’t forget to check out Part 2 where the ISO creation process will be discussed in further detail.

Zach Thurmond
IT Consultant
Critical Design Associates

LinkedIn Profile

Automating Lab Builds with XenServer PowerShell – Part 2 Creating a Custom ISO

Part 1 – Understanding the Requirements
>>Part 2 – Creating a Custom ISO
Part 3 – Unlimited VM Creation
Part 4 – Roles, Features, and Other Components


After reviewing and staging the required files and folders in Part 1, it’s time to start the custom ISO creation process. The custom ISO is just one part to the whole, but it is probably the most important. Without the custom ISO, you would just have a bunch of bootless VMs.

To build a custom ISO, you need to start with a full Windows operating system ISO which can be downloaded from an MSDN or Visual Studio subscription. Your Microsoft licensing agreement would also include the necessary product keys for the different OS versions.

This base operating system ISO is part of what is used to create the customized, unattended, installation of an OS and will also be used to create the custom ISO. Each of the files and folders (See Part 1) serve their own purpose, which will be discussed in further detail in the following sections along with the ISO creation form itself.

Where the Magic Happens

Upon first launch of AXL, you will get prompted with a choice, (see Figure 1) either to create custom ISO(s), or create VMs.

Figure 1 – First Launch Selection

After selecting to create custom ISO(s), the ISO creation GUI, as shown in Figure 2, will display and is used to define all the information needed to create a custom ISO.

Figure 2 – ISO Creation Form

Responding to each text box and drop down is important to the success of the ISO creation. Below is an explanation of all the fields in the form:

Input ISO Location: This specifies the path where the read-only, base ISO (Windows operating system) is located and will be used for the custom ISO creation.

Input Target Folder Location: This specifies the destination folder. Everything will be copied to the destination folder including the base ISO content, autounattend.xml, and XenServer Tools

Input Autounattended.xml File Location, Input Boot File Location, Input Path to ISO Creation Tool: These three inputs specify the full path to each file and are used in the ISO creation process

Input Path to XenServer Tools contents: This specifies the parent folder that contains the expanded XenServer Tools contents

Input: Product Key, Admin Name, Admin Password, and Time Zone: All of these fields specify information that is required for, and replaced in, the autounattend.xml file

New ISO File Name: This specifies the name of the ISO file you will be creating, NOTE: the file extension is not required in the name

Select Edition: This dropdown is used to specify which Windows edition gets installed and should be used in conjunction with the base ISO selected. One of the check boxes for either Windows 10 or Server 2016 must be checked before this field can be enabled.

After each field is populated, you’ll need to click the Validate button to ensure all the information is entered correctly.

If the validation is successful, the create button is then enabled. Each field has its own validation, most of which use regular expressions (regex) to determine validity. In the example above, clicking “Validate” results in an error as shown in Figure 3. It’s important that each field is populated with the appropriate information.

Figure 3 – Validation Error

For the error shown in Figure 3, the product key text box is evaluated with the following regex:


The ISO Construction form, when loaded, will pre-populate some fields depending on where the tool is launched from. If the oscdimg.exe and files are located in subdirectories of where the tool is launched, the paths for them will be automatically prepopulated on launch.

The same goes for the expanded XenServer Tools folder. After marking the check box to have XenServer Tools installed, all subdirectories will be searched, and if the agent is found, the folder path is automatically populated in the text box.

The Custom ISO

After understanding how to use the form and the purpose of the input fields, we can take a look behind the scenes at how the information is used.

The main file that allows for an unattended installation of a Windows operating system is a file called autounattend.xml. This file contains the instructions for how to install the operating system, including the acceptance of the EULA, the product key, the username and password for the administrator account. Different Windows operating systems require different autounattend.xml files since each Windows platform is unique.

I have created sample autounatend.xml files for both Windows Server 2016 and Windows 10 which are both posted on GitHub.

It’s important to note that the name of the file does matter because when loading the ISO, Windows will search specific locations for this file. Check out this Microsoft article for further information.

The next step is to get this information added to a custom ISO. ISOs are read only, so you cannot simply copy a file into it. You can however, copy the contents of an ISO out of it. But then that leads to another problem:

How do I recreate the ISO after I have the contents expanded into a folder?

The answer is actually quite simple: oscdimg.exe. Oscdimg can be found in the Windows Assessment and Deployment Kit (ADK), which are available here, or simply download the files from the provided GitHub page earlier.

The only files from the ADK that are needed for this tool are oscdimg.exe and Oscdimg allows the creation of ISOs from the contents of a folder, which is exactly what is needed.

ISO Creation Process Explained

I’ve stepped you through what goes into the ISO and how to appropriately fill out the ISO creation form, but I haven’t really touched on the full process yet.

The ISO creation process is fairly straightforward and Figure 4 provides a visual representation.

Figure 4 – ISO Creation Flowchart

Starting at the top, you’ll need to fill out the ISO Construction form. After all the required fields are complete, the next step will be to click validate, which will then validate each populated field to ensure the ISO creation goes off without a hitch.

Should If a field is not filled in correctly, the tool will indicate which field is wrong and the probable cause. If everything checks out during the validation process, the create button will become enabled. Once you click on “Create”, the process cannot be stopped, so double check each field to ensure accuracy.

After the creation process has begun, the selected base Windows ISO is automatically mounted into the next available drive and the contents will arebe copied into the target folder location. A progress bar will track the progress of the file copies.

Once the copy of the Windows ISO contents is complete, the autounattend.xml file and XenServer Tools, if selected, will get copied to the target folder location as well. The code snippet for the copy process is shown below:

Function CopyFiles { 
# Specify the Selected ISO and the mount location
$SelectedISO = $ISOPathTextBox.Text 
$MountedImage =  Mount-DiskImage $SelectedISO -PassThru 
$MountLocation = "$(($MountedImage | Get-Volume).DriveLetter):\" 
$MountedFiles = Get-ChildItem $MountLocation -Recurse 
# Specify variables in coordination with the text boxes
$TargetFolder = $TargetFolderTextBox.Text 
$AutounattendXML = $AutounattendPathTextBox.Text 
$AdminPW = $AdminPasswordTextBox.Text 
$AdminAcct = $AdminNameTextBox.Text 
$ProductKey = $ProductKeyTextBox.Text 
$XenToolsPath = $XenToolsPathTextBox.Text 
$Server2016CheckBox = $Server2016CheckBox.CheckState 
$Windows10Checkbox = $Windows10Checkbox.CheckState 
$DropDownEditionSelection = $DropDownEditionSelection.SelectedItem 
$DropDownTimeZone = $DropDownTimeZones.SelectedItem 
     if($MountLocation) {
        Foreach($MountedFile in $MountedFiles) {
            # Copy all contents from the mounted ISO to the target location keeping the same file/folder structure 
            if ($MountedFile.PSIsContainer) {
            Copy-Item $MountedFile.FullName -Destination (Join-Path $TargetFolder $MountedFile.Parent.FullName.Substring($MountLocation.length))
            else {
            Copy-Item $MountedFile.FullName -Destination (Join-Path $TargetFolder $MountedFile.FullName.Substring($MountLocation.length)) 
    # Copy Autounattend.xml file into the root of the ISO
    if($AutounattendXML -and $TargetFolder) {
    Copy-Item $AutounattendXML -Destination $TargetFolder"\Autounattend.xml"
    # Specify which Windows version is being installed
    if($Server2016CheckBox -eq 'Checked') {
    $WindowsEdition = "Windows Server 2016 $DropDownEditionSelection"
    elseif($Windows10Checkbox -eq 'Checked') {
    $WindowsEdition = "Windows 10 $DropDownEditionSelection"
    # Define the contents of the Autounattend.xml file for later modification
    $DefaultXML = Get-Content $TargetFolder"\Autounattend.xml"
        $DefaultXML | Foreach-Object {
            # Replace the contents of the Autounattend.xml file with the information provided
            $_ -replace '1AdminAccount', $AdminAcct `
            -replace '1AdminPassword', $AdminPW `
            -replace '1ProductKey', $ProductKey `
            -replace '1XenToolsPath', $XenToolsPath.Substring($XenToolsPath.LastIndexOf("\")+1) `
            -replace '1Edition', $WindowsEdition `
            -replace '1TimeZone', $DropDownTimeZones
        } | Set-Content $TargetFolder"\Autounattend.xml"
    # If it was specified to install XenServer Tools, copy the parent folder into the target
    if($XenToolsPath) {
    Copy-Item $XenToolsPath -Destination $TargetFolder -Recurse
    if($MountedImage) {
    Dismount-DiskImage $SelectedISO

When the copy is complete, the ISO will be automatically unmounted as it is no longer needed. After unmounting the ISO, the custom ISO creation process begins by integrating the contents of the target folder.

This process usually takes about 3-5 minutes to complete depending on how large the original ISO is. The code snippet for the ISO creation process is shown below:

Function BuildISO {
$SelectedISO = $ISOPathTextBox.Text
$TargetFolder = $TargetFolderTextBox.Text
$NewISOName = $NewISONameTextBox.Text
$BootFile = $BootFilePathTextBox.Text
$ISOTool = $ISOToolPathTextBox.Text
# List of arguments to pass to oscdimg.exe
$ArgumentList = "-b$BootFile -u2 -h -m $TargetFolder $($SelectedISO.Remove($SelectedISO.LastIndexOf("\")))\$NewISOName.iso"
# Display in the form what ISO is being created and where
$ISOCopyProgressLabel.Text = "Creating $NewISOName.iso at $($SelectedISO.Remove($SelectedISO.LastIndexOf("\")))\"
# Create Custom ISO file. This turns the folder that contains the ISO and unattend into a new ISO file
Start-Process -WindowStyle Hidden -FilePath $ISOTool -ArgumentList $ArgumentList -Wait

To read more about the command line switches used with Oscdimg.exe in the $ArgumentList variable, check out these resources online.

After the custom ISO creation process is complete, you have the option to delete the target folder if desired and you can repeat this process as many times as you like.

The custom ISO is created in the same place where the base Windows ISO was stored. If you have an ISO storage repository in XenServer, I would recommend choosing an ISO from there since the new ISO will automatically get created in that location.


With a full understanding of the ISO Construction form and custom ISO creation process, there shouldn’t be anything holding you back from creating your own ISOs; if you haven’t yet downloaded AXL and the associated files, get them from GitHub here.

This is a fairly seamless process and can be used to create ISOs in under 15 minutes. You should have no problems creating an endless number of ISOs to play around with and this automation allows you to repeatedly build lab environments much faster and easier.

As mentioned in the beginning of this series, check out Part 3 where I will be covering the VM creation process.

Zach Thurmond
IT Consultant
Critical Design Associates

LinkedIn Profile

Checking System Readiness for the Bromium Platform

The Bromium Platform has several hardware and software requirements to fully function on an endpoint. Since the Bromium Client itself does not check many of these requirements until after installation, its difficult know ahead of time what machines require remediation prior to deployment.

To address this issue, I wrote PowerShell scripts to take an inventory of machines in your environment and compile a report using minimal infrastructure.


The solution is designed to be deployed without depending on an endpoint management or software delivery platform. It does however require a scheduled task to run the Endpoint_CDABromiumReadiness.PS1 on each endpoint and a centralized file share where the script can save the collected inventory data. To summarize, the following components are necessary for this solution to work:

  • File Shares – Location for collected data
  • Scheduled Task – Executes the BromiumReadiness script
  • BromiumReadiness PowerShell script – Collects inventory data from endpoint
  • Compiler script – Aggregates collected data into a readable report

File Shares

The Endpoint_CDABromiumReadiness.PS1 collects inventory data from the endpoint and although it could be stored on the machine itself, it would require a significant amount of overhead to log into each machine and gather the data. To facilitate a simpler method of data collection, the script is designed to write the inventory data to a centralized file share. This file share can be one that already exists in your environment or can be created for the purpose of this solution.

The example that I used to create the file share where the script will store inventory data has these properties:

  • Name of folder: TestShare
  • Name of share: TestShare
  • Share permissions: Allow: Change, Read
  • Folder permissions: Allow: Create files / write data, Create folders / append data

Figure 1 – Share Permissions: Allow: Change, Read

NOTE: The name TestShare is used as an example. A more descriptive name would be preferable

Figure 2 – Folder Permissions: Allow: Create files / write data, Create folders / append data

The other file share will be a network location where the Endpoint_CDABromiumReadiness.PS1 PowerShell script can be stored for execution during the Scheduled Task. This file share can be a Read Only location as the script is only read from this location.

The example that I use for a file share location where I store this script is:


Scheduled Task

Since there is no requirement to use a software delivery platform to deploy the Endpoint_CDABromiumReadiness.PS1, the simplest method for deployment and execution of the script is to use a Scheduled Task. Creating the scheduled task on each workstation would be time consuming and inefficient so the better approach would be to simply create the Scheduled Task through an Active Directory Computer Configuration GPO preference. An existing GPO or a new GPO can be used and needs to be linked to the OU or OUs that contain the workstations in the environment.

To create a Scheduled Task as a GPO preference, open the GPO using the Group Policy Management Console (GPMC) and navigate to:

Computer Configuration > Control Panel Settings > Scheduled Tasks

Figure 3 – GPO Preference – Scheduled Tasks

Right-Click “Scheduled Tasks” and choose New > Scheduled Task (Windows Vista and later)

A New Task (Windows Vista and later) Properties window should appear as follows:

Figure 4 – New Task (Windows Vista and later) Properties

Change the Action dropdown from Update to “Create”

Under the General tab, the following parameters should be entered:

  • Name: Bromium Readiness
  • User Account: NT AUTHORITY\System
  • Security Options: Run whether user is logged on or not
  • Security Options: Run with highest privileges
  • Hidden: Enabled

Figure 5 – General tab

Under the Actions tab, click “New” then in the New Action window, enter the following:



Add Arguments(optional):

-ExecutionPolicy Bypass -Command "& '\\<>\Endpoint_CDABromiumReadiness.ps1' -CopyToLocation '\\dc01\testshare\'"

Figure 6 – New Action window

NOTE: The name of the file server and shares are used as an example. Your UNC path would include the location of the Endpoint_CDABromiumReadiness.PS1 in a central file share and the data collection file share as created above. These UNC paths may not necessarily be the same.

Under the Triggers tab, click “New” then in the New Trigger window define the parameters for when to execute the scheduled task:

Figure 7 – New Trigger window

The script should be run at least once but it would be advised to not run the script continuously as the inventory data should only be necessary to collect information to assess the machine’s readiness to deploy the Bromium Client. It is not designed to be a maintenance task.

When the Scheduled Task executes, the Endpoint_CDABromiumReadiness.PS1 PowerShell script will gather the required information from the endpoints, generate a tsv file, and copy the file to the file share you set after the “CopyToLocation” parameter.

BromiumReadiness Script

This PowerShell script collects the inventory data from the endpoints and is contained here:

Figure 8 – BromiumReadiness script

Compiler Script

This PowerShell script aggregates the inventory data located in the file share from all of the individual tsv files generated from each endpoint into a single file that can be reviewed in Excel.

The Compiler_CDABromiumReadiness.PS1 is contained within the zip file.

Figure 9 – Compiler script

It is preferable to keep the Compiler script in the same file share as the tsv files that are generated so that it can be run as needed.

Figure 10 – Compiler script stored in file share

To execute the compiler script, open Windows PowerShell and run:


A finished report will look like this:

Figure 11 – Finished report

Running Multiple Rounds of Readiness Checks (Optional)

If the Scheduled Task runs multiple times, it will overwrite the inventory data that was previously collected for the endpoints. To prevent the data from getting overwritten, a method to keep previous data collections would be to run multiple rounds of readiness checks. This could also be important in a situation where you need to run the inventory more than once and you expect different results.

To do this, simply add the -ReadinessCheckRound parameter to the end of the execution of the BromiumReadiness script with a number indicating the round. This parameter is set to 1 by default and tags the tsv files. Notice in the image that the number 1 precedes the device name:

Figure 12 – Round number in tsv file name

And here is an example of the command line to use. Change the command line in the scheduled task that was created in the GPO to include the -ReadinessCheckRound parameter.

-ExecutionPolicy Bypass -Command “& ‘\\dc01\ScriptShare\Endpoint_CDABromiumReadiness.ps1’ -CopyToLocation ‘\\dc01\testshare\'” -ReadinessCheckRound 2

When the scheduled task runs again, the Endpoint_CDABromiumReadiness.PS1 script will generate tsv files with the round number preceding the name of the device:

Figure 13 – TSV files with multiple rounds

Add the -ReadinessCheckRound parameter when executing the Compiler script and the new report generated will show only data from that round.

.\Compiler_CDABromiumReadiness.ps1 -ReadinessCheckRound 2

Figure 14 – Compiled report from round


Aman Motazedian
Senior Consultant
Critical Design Associates

LinkedIn Profile

Automating Lab Builds with XenServer PowerShell – Part 1 Understanding the Requirements

>>Part 1 – Understanding the Requirements
Part 2 – Creating a Custom ISO
Part 3 – Unlimited VM Creation
Part 4 – Roles, Features, and Other Components


I was introduced to Citrix products in September of 2017 and have been working with it every day since. It seems that on a weekly basis I am exploring some new technology or testing an idea that requires a new lab component. I needed a way to automate tasks. My first target was the creation of new Virtual Machines (VMs).

The XenServer hypervisor is my preferred platform. While not particularly difficult, creating new VMs requires manual processes and time including considerations for machine setup, initial Windows installation, configuration, and other mediums.

With all of the manual steps required to build a lab multiplied by the number of virtual machines being built repeatedly, I decided to create a tool, called Automated XenServer Labs (“AXL”), that takes the heavy lifting and user interaction involved in creating new VMs out of the equation to allow more time for actual lab work.

AXL leverages the XenServer PowerShell module and allows for a wide range of configurations. The PowerShell module allows for the creation and manipulation of VMs, Pools, Storage, Networks, etc. By utilizing this module, you can obtain greater efficiency and automation when, and if, creating new environments.

You may be wondering at this point how I plan to take away a large part of the user interaction to build a Windows VM, and to you I say, a custom-made ISO. An ISO, for those of you who may not know, is used to install an Operating System (OS).

The custom ISO has an unattended answer file in it, which I will talk about in further parts of this series, that allows for no user interaction during the installation process. AXL automates the creation of the Windows ISO to be used during the unattended OS installation and can also allow you to create an Active Directory domain, set IP addresses and names, and install specific server roles and features.

While AXL will automate the installation and configuration of the VMs, some initial user interaction is required to input how the VMs, ISOs, and Windows Features should be configured.  Given some of this configuration, the overall process will still be much quicker than manually building out all of the VMs.

The following sections will describe the components and infrastructure that are necessary for automating the build of a lab environment using AXL, the files and folders that are required to create the custom ISO, and using it to create a virtually unlimited number of VMs.

Components and Infrastructure
There are multiple items needed for AXL to work, the main items being the infrastructure. The first thing you will need is hardware with XenServer installed since AXL specifically uses the XenServer PowerShell module.

The hardware can be anything from a small form factor Intel NUC, to a custom-built or enterprise-grade server. XenServer can be downloaded for free from Citrix. You will need a My Citrix account to complete the process.

For development, I used the following items, though not all are necessary:

– (3x) Intel NUC Core i7 (XenServer Hosts)
– (1x) Ubiquiti EdgeRouter X
– (1x) HP J9028A ProCurve Switch 1800-24G
– (1x) Endpoint (Can be Windows Server or Windows Desktop OS)

As stated above, not all of these components are necessary since all you really need is a single machine to install XenServer, such as an Intel NUC (or some other server type device) and an endpoint to run AXL from.

NUCs have a very small form factor and are great for lab work, however; RAM and HD/SSD are sold separately, so you will have to account for that in the cost if you plan to use them. I use the switch and router to create different VLANs as needed.

A basic network topology of what I use is shown in Figure 1. I RDP to a VM on one of the NUCs and run AXL.
Explanation of VLAN
Explanation of RDP

The most important thing to note is the endpoint where AXL is running must be able to communicate with the XenServer host(s). This means that proper routing and firewall configurations must be in place prior to using AXL. NOTE: Firewall and routing configurations are outside the scope of this post and will not be covered.

Files and Folder Structure
There are a number of files needed for AXL to function. The files use are posted on GitHub, with the most important being the PowerShell script.

The files needed for the ISO creation process are as follows:

– autounattend.xml
– oscdimg.exe
– Windows ISOs and associated licenses (MSDN, Visual Studio, etc)
– Expanded XenServer Tools (this is optional)

I won’t go in depth on any of these files in this part as they will be discussed in Part 2 of this series.

The only files that are required for the VM creation process is the PowerShell module for XenServer, which is actually a folder. Once you have all the required files and folders, you’ll need to put them either in a folder on the root of the C:\ drive, or in a folder on the Desktop.

I would recommend creating a folder just for the PowerShell script and all the necessary files so they are all easily accessible.

The folder structure I use is as follows:


To recap, you will want to get all the required files and folders for AXL and make a suitable folder structure. With the exception of the Windows and XenServer ISOs, all of the files and folders can be downloaded from GitHub.

You will want at least one endpoint (workstation or server) running AXL and one XenServer to host the VMs. With an understanding of the components, files, and folders, you should now have a solid grasp of the overall requirements for using AXL.

And don’t forget to check out Part 2 where the ISO creation process will be discussed in further detail.

Zach Thurmond
IT Consultant
Critical Design Associates

LinkedIn Profile