LabBuilder.psm1
<#
.EXTERNALHELP LabBuilder-help.xml #> #Requires -version 5.1 #Requires -RunAsAdministrator $moduleRoot = Split-Path ` -Path $MyInvocation.MyCommand.Path ` -Parent #region LocalizedData $Culture = 'en-us' if (Test-Path -Path (Join-Path -Path $moduleRoot -ChildPath $PSUICulture)) { $Culture = $PSUICulture } Import-LocalizedData ` -BindingVariable LocalizedData ` -Filename 'LabBuilder_LocalizedData.psd1' ` -BaseDirectory $moduleRoot ` -UICulture $Culture #endregion #region Types Enum LabOStype { Server = 1 Nano = 2 Client = 3 } # Enum LabOStype Enum LabVHDType { Fixed = 1 Dynamic = 2 Differencing = 3 } # Enum LabVHDType Enum LabVHDFormat { VHD = 1 VHDx = 2 } # Enum LabVHDFormat Enum LabSwitchType { Private = 1 Internal = 2 External = 3 NAT = 4 } # Enum LabSwitchType Enum LabPartitionStyle { MBR = 1 GPT = 2 } # Enum LabPartitionStyle Enum LabFileSystem { FAT32 = 1 exFAT = 2 NTFS = 3 ReFS = 4 } # Enum LabFileSystem Enum LabCertificateSource { Guest = 1 Host = 2 } # Enum LabCertificateSource class LabResourceModule:System.ICloneable { [System.String] $Name [System.String] $URL [System.String] $Folder [System.String] $MinimumVersion [System.String] $RequiredVersion LabResourceModule() {} LabResourceModule($Name) { $this.Name = $Name } # Constructor [Object] Clone () { $New = [LabResourceModule]::New() foreach ($Property in ($this | Get-Member -MemberType Property)) { $New.$($Property.Name) = $this.$($Property.Name) } # foreach return $New } # Clone } # class LabResourceModule class LabResourceMSU:System.ICloneable { [System.String] $Name [System.String] $URL [System.String] $Path [System.String] $Filename LabResourceMSU() {} LabResourceMSU($Name) { $this.Name = $Name } # Constructor LabResourceMSU($Name,$URL) { $this.Name = $Name $this.URL = $URL } # Constructor [Object] Clone () { $New = [LabResourceMSU]::New() foreach ($Property in ($this | Get-Member -MemberType Property)) { $New.$($Property.Name) = $this.$($Property.Name) } # foreach return $New } # Clone } # class LabResourceMSU class LabResourceISO:System.ICloneable { [System.String] $Name [System.String] $URL [System.String] $Path LabResourceISO() {} LabResourceISO($Name) { $this.Name = $Name } # Constructor [Object] Clone () { $New = [LabResourceISO]::New() foreach ($Property in ($this | Get-Member -MemberType Property)) { $New.$($Property.Name) = $this.$($Property.Name) } # foreach return $New } # Clone } # class LabResourceISO class LabSwitchAdapter:System.ICloneable { [System.String] $Name [System.String] $MACAddress [Byte] $Vlan LabSwitchAdapter() {} LabSwitchAdapter($Name) { $this.Name = $Name } # Constructor LabSwitchAdapter($Name,$Type) { $this.Name = $Name $this.Type = $Type } # Constructor [Object] Clone () { $New = [LabSwitchAdapter]::New() foreach ($Property in ($this | Get-Member -MemberType Property)) { $New.$($Property.Name) = $this.$($Property.Name) } # foreach return $New } # Clone } # class LabSwitchAdapter class LabVMAdapterIPv4:System.ICloneable { [System.String] $Address [System.String] $DefaultGateway [Byte] $SubnetMask [System.String] $DNSServer LabVMAdapterIPv4() {} LabVMAdapterIPv4($Address,$SubnetMask) { $this.Address = $Address $this.SubnetMask = $SubnetMask } # Constructor [Object] Clone () { $New = [LabVMAdapterIPv4]::New() foreach ($Property in ($this | Get-Member -MemberType Property)) { $New.$($Property.Name) = $this.$($Property.Name) } # foreach return $New } # Clone } # class LabVMAdapterIPv4 class LabVMAdapterIPv6:System.ICloneable { [System.String] $Address [System.String] $DefaultGateway [Byte] $SubnetMask [System.String] $DNSServer LabVMAdapterIPv6() {} LabVMAdapterIPv6($Address,$SubnetMask) { $this.Address = $Address $this.SubnetMask = $SubnetMask } # Constructor [Object] Clone () { $New = [LabVMAdapterIPv6]::New() foreach ($Property in ($this | Get-Member -MemberType Property)) { $New.$($Property.Name) = $this.$($Property.Name) } # foreach return $New } # Clone } # class LabVMAdapterIPv6 class LabVMAdapter:System.ICloneable { [System.String] $Name [System.String] $SwitchName [System.String] $MACAddress [System.Boolean] $MACAddressSpoofing [Byte] $Vlan [LabVMAdapterIPv4] $IPv4 [LabVMAdapterIPv6] $IPv6 LabVMAdapter() {} LabVMAdapter($Name) { $this.Name = $Name } # Constructor [Object] Clone () { $New = [LabVMAdapter]::New() foreach ($Property in ($this | Get-Member -MemberType Property)) { $New.$($Property.Name) = $this.$($Property.Name) } # foreach return $New } # Clone } # class LabVMAdapter class LabDataVHD:System.ICloneable { [System.String] $VHD [LabVHDType] $VHDType [Uint64] $Size [System.String] $SourceVHD [System.String] $ParentVHD [System.Boolean] $MoveSourceVHD [System.String] $CopyFolders [LabFileSystem] $FileSystem [LabPartitionStyle] $PartitionStyle [System.String] $FileSystemLabel [System.Boolean] $Shared = $false [System.Boolean] $SupportPR = $false LabDataVHD() {} LabDataVHD($VHD) { $this.VHD = $VHD } # Constructor [Object] Clone () { $New = [LabDataVHD]::New() foreach ($Property in ($this | Get-Member -MemberType Property)) { $New.$($Property.Name) = $this.$($Property.Name) } # foreach return $New } # Clone } # class LabDataVHD class LabDVDDrive:System.ICloneable { [System.String] $ISO [System.String] $Path LabDVDDrive() {} LabDVDDrive($ISO) { $this.ISO = $ISO } # Constructor [Object] Clone () { $New = [LabDVDDrive]::New() foreach ($Property in ($this | Get-Member -MemberType Property)) { $New.$($Property.Name) = $this.$($Property.Name) } # foreach return $New } # Clone } # class LabDVDDrive class LabVMTemplateVHD:System.ICloneable { [System.String] $Name [System.String] $ISOPath [System.String] $VHDPath [LabOStype] $OSType = [LabOStype]::Server [System.String] $Edition [Byte] $Generation = 2 [LabVHDFormat] $VHDFormat = [LabVHDFormat]::VHDx [LabVHDType] $VHDType = [LabVHDType]::Dynamic [Uint64] $VHDSize = 0 [System.String[]] $Packages [System.String[]] $Features LabVMTemplateVHD() {} LabVMTemplateVHD($Name) { $this.Name = $Name } # Constructor [Object] Clone () { $New = [LabVMTemplateVHD]::New() foreach ($Property in ($this | Get-Member -MemberType Property)) { $New.$($Property.Name) = $this.$($Property.Name) } # foreach return $New } # Clone } # class LabVMTemplateVHD class LabVMTemplate:System.ICloneable { [System.String] $Name [System.String] $VHD [System.String] $SourceVHD [System.String] $ParentVHD [System.String] $TemplateVHD [Uint64] $MemoryStartupBytes = 1GB [System.Boolean] $DynamicMemoryEnabled = $true [System.Boolean] $ExposeVirtualizationExtensions = $false [Byte] $ProcessorCount = 1 [System.String] $AdministratorPassword [System.String] $ProductKey [System.String] $Timezone="Pacific Standard Time" [LabOStype] $OSType = [LabOStype]::Server [System.String[]] $IntegrationServices = @('Guest Service Interface','Heartbeat','Key-Value Pair Exchange','Shutdown','Time Synchronization','VSS') [System.String[]] $Packages [ValidateRange(1,2)][Byte] $Generation = 2 [ValidateSet("5.0","6.2","7.0","7.1","8.0","254.0","255.0")][System.String] $Version = '8.0' LabVMTemplate() {} LabVMTemplate($Name) { $this.Name = $Name } # Constructor [Object] Clone () { $New = [LabVMTemplate]::New() foreach ($Property in ($this | Get-Member -MemberType Property)) { $New.$($Property.Name) = $this.$($Property.Name) } # foreach return $New } # Clone } # class LabVMTemplate class LabSwitch:System.ICloneable { [System.String] $Name [LabSwitchType] $Type [Byte] $VLAN [System.String] $BindingAdapterName [System.String] $BindingAdapterMac [System.String] $NatSubnet [System.String] $NatGatewayAddress [LabSwitchAdapter[]] $Adapters LabSwitch() {} LabSwitch($Name) { $this.Name = $Name } # Constructor LabSwitch($Name,$Type) { $this.Name = $Name $this.Type = $Type } # Constructor [Object] Clone () { $New = [LabSwitch]::New() foreach ($Property in ($this | Get-Member -MemberType Property)) { $New.$($Property.Name) = $this.$($Property.Name) } # foreach return $New } # Clone } # class LabSwitch class LabDSC:System.ICloneable { [System.String] $ConfigName [System.String] $ConfigFile [System.String] $Parameters [System.Boolean] $Logging = $false LabDSC() {} LabDSC($ConfigName) { $this.ConfigName = $ConfigName } # Constructor LabDSC($ConfigName,$ConfigFile) { $this.ConfigName = $ConfigName $this.ConfigFile = $ConfigFile } # Constructor [Object] Clone () { $New = [LabDSC]::New() foreach ($Property in ($this | Get-Member -MemberType Property)) { $New.$($Property.Name) = $this.$($Property.Name) } # foreach return $New } # Clone } # class LabDSC class LabVM:System.ICloneable { [System.String] $Name [System.String] $Template [System.String] $ComputerName [Byte] $ProcessorCount [Uint64] $MemoryStartupBytes = 1GB [System.Boolean] $DynamicMemoryEnabled = $true [System.Boolean] $ExposeVirtualizationExtensions = $false [System.String] $ParentVHD [System.Boolean] $UseDifferencingDisk = $true [System.String] $AdministratorPassword [System.String] $ProductKey [System.String] $Timezone="Pacific Standard Time" [LabOStype] $OSType = [LabOStype]::Server [System.String] $UnattendFile [System.String] $SetupComplete [System.String[]] $Packages [ValidateRange(1,2)][Byte] $Generation = 2 [ValidateSet("5.0","6.2","7.0","7.1","8.0","254.0","255.0")][System.String] $Version = '8.0' [System.Int32] $BootOrder [System.String[]] $IntegrationServices = @('Guest Service Interface','Heartbeat','Key-Value Pair Exchange','Shutdown','Time Synchronization','VSS') [LabVMAdapter[]] $Adapters [LabDataVHD[]] $DataVHDs [LabDVDDrive[]] $DVDDrives [LabDSC] $DSC [System.String] $VMRootPath [System.String] $LabBuilderFilesPath [LabCertificateSource] $CertificateSource = [LabCertificateSource]::Guest [System.String] $NanoODJPath LabVM() {} LabVM($Name) { $this.Name = $Name } # Constructor LabVM($Name,$ComputerName) { $this.Name = $Name $this.ComputerName = $ComputerName } # Constructor [Object] Clone () { $New = [LabVM]::New() foreach ($Property in ($this | Get-Member -MemberType Property)) { $New.$($Property.Name) = $this.$($Property.Name) } # foreach return $New } # Clone } # class LabVM class LabDSCModule:System.ICloneable { [System.String] $ModuleName [Version] $ModuleVersion [Version] $MinimumVersion LabDSCModule() {} LabDSCModule($ModuleName) { $this.ModuleName = $ModuleName } # Constructor LabDSCModule($ModuleName,$ModuleVersion) { $this.ModuleName = $ModuleName $this.ModuleVersion = [Version] $ModuleVersion } # Constructor [Object] Clone () { $New = [LabDSCModule]::New() foreach ($Property in ($this | Get-Member -MemberType Property)) { $New.$($Property.Name) = $this.$($Property.Name) } # foreach return $New } # Clone } # class LabDSCModule #endregion #region ModuleVariables [System.String] $Script:WorkingFolder = $ENV:Temp # Supporting files [System.String] $Script:SupportConvertWindowsImagePath = Join-Path ` -Path $PSScriptRoot ` -ChildPath 'support\Convert-WindowsImage.ps1' [System.String] $Script:SupportGertGenPath = Join-Path ` -Path $PSScriptRoot ` -ChildPath 'support\New-SelfSignedCertificateEx.ps1' # DSC Library [System.String] $Script:DSCLibraryPath = Join-Path ` -Path $PSScriptRoot ` -ChildPath 'dsclibrary' # Virtual Networking Parameters [System.Int32] $Script:DefaultManagementVLan = 99 # Self-signed Certificate Parameters [System.Int32] $Script:SelfSignedCertKeyLength = 2048 # Warning - using KSP causes the Private Key to not be accessible to PS. [System.String] $Script:SelfSignedCertProviderName = 'Microsoft Enhanced Cryptographic Provider v1.0' # 'Microsoft Software Key Storage Provider' [System.String] $Script:SelfSignedCertAlgorithmName = 'RSA' # 'ECDH_P256' Or 'ECDH_P384' Or 'ECDH_P521' [System.String] $Script:SelfSignedCertSignatureAlgorithm = 'SHA256' # 'SHA1' [System.String] $Script:DSCEncryptionCert = 'DSCEncryption.cer' [System.String] $Script:DSCEncryptionPfxCert = 'DSCEncryption.pfx' [System.String] $Script:DSCCertificateFriendlyName = 'DSC Credential Encryption' [System.String] $Script:DSCCertificatePassword = 'E3jdNkd903mDn43NEk2nbDENjw' [System.Int32] $Script:RetryConnectSeconds = 5 [System.Int32] $Script:RetryHeartbeatSeconds = 1 [System.Int32] $Script:StartupTimeout = 90 # System Info [System.Int32] $Script:CurrentBuild = (Get-ItemProperty ` -Path 'hklm:\SOFTWARE\Microsoft\Windows NT\CurrentVersion').CurrentBuild # XML Stuff [System.String] $Script:ConfigurationXMLSchema = Join-Path ` -Path $PSScriptRoot ` -ChildPath 'schema\labbuilderconfig-schema.xsd' [System.String] $Script:ConfigurationXMLTemplate = Join-Path ` -Path $PSScriptRoot ` -ChildPath 'template\labbuilderconfig-template.xml' # Nano Stuff [System.String] $Script:NanoPackageCulture = 'en-us' #region Functions <# .SYNOPSIS Validates the provided configuration XML against the Schema. .DESCRIPTION This function will ensure that the provided Configration XML is compatible with the LabBuilderConfig.xsd Schema file. .PARAMETER ConfigPath Contains the path to the Configuration XML file. .EXAMPLE Assert-LabValidConfigurationXMLSchema -ConfigPath c:\mylab\config.xml Validates the XML configuration and downloads any resources required by it. .OUTPUTS None. If the XML is invalid an exception will be thrown. #> function Assert-LabValidConfigurationXMLSchema { [CmdLetBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $ConfigPath ) # Define these variables so they are accesible inside the event handler. $Script:XMLErrorCount = 0 $Script:XMLFirstError = '' $Script:XMLPath = $ConfigPath $Script:ConfigurationXMLValidationMessage = $LocalizedData.ConfigurationXMLValidationMessage # Perform the XSD Validation $readerSettings = New-Object -TypeName System.Xml.XmlReaderSettings $readerSettings.ValidationType = [System.Xml.ValidationType]::Schema $null = $readerSettings.Schemas.Add("labbuilderconfig", $Script:ConfigurationXMLSchema) $readerSettings.ValidationFlags = [System.Xml.Schema.XmlSchemaValidationFlags]::ProcessInlineSchema -bor [System.Xml.Schema.XmlSchemaValidationFlags]::ProcessSchemaLocation $readerSettings.add_ValidationEventHandler( { # Triggered each time an error is found in the XML file if ([System.String]::IsNullOrWhitespace($Script:XMLFirstError)) { $Script:XMLFirstError = $_.Message } # if Write-LabMessage -Message ($Script:ConfigurationXMLValidationMessage ` -f $Script:XMLPath, $_.Message) $Script:XMLErrorCount++ }) $reader = [System.Xml.XmlReader]::Create([System.String] $ConfigPath, $readerSettings) try { while ($reader.Read()) { } # while } # try catch { # XML is NOT valid $exceptionParameters = @{ errorId = 'ConfigurationXMLValidationError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.ConfigurationXMLValidationError ` -f $ConfigPath, $_.Exception.Message) } New-LabException @exceptionParameters } # catch finally { $null = $reader.Close() } # finally # Verify the results of the XSD validation if ($script:XMLErrorCount -gt 0) { # XML is NOT valid $exceptionParameters = @{ errorId = 'ConfigurationXMLValidationError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.ConfigurationXMLValidationError -f $ConfigPath, $Script:XMLFirstError) } New-LabException @exceptionParameters } # if } <# .SYNOPSIS Validates the IP Address. .PARAMETER IpAddress Contains the IP Address to validate. .EXAMPLE Assert-LabValidIpAddress -IpAddress '192.168.123.44' Does not throw an exception and returns '192.168.123.44'. .EXAMPLE Assert-LabValidIpAddress -IpAddress '192.168.123.4432' Throws an exception. .OUTPUTS The IP address if valid. #> function Assert-LabValidIpAddress { [CmdLetBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $IpAddress ) $ip = [System.Net.IPAddress]::Any if (-not [System.Net.IPAddress]::TryParse($IpAddress, [ref] $ip)) { $exceptionParameters = @{ errorId = 'IPAddressError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.IPAddressError -f $IpAddress) } New-LabException @exceptionParameters } return $ip } <# .SYNOPSIS Uploads Precreated ODJ files to Nano systems or others as required. .DESCRIPTION This function will perform the following tasks: 1. Connect to the VM via remoting. 2. Upload the ODJ file to c:\windows\setup\ODJFiles folder of the VM. If the ODJ file does not exist in the LabFiles folder for the VM then the copy will not be performed. .PARAMETER Lab Contains the Lab object that was produced by the Get-Lab cmdlet. .PARAMETER VM A LabVM object pulled from the Lab Configuration file using Get-LabVM .PARAMETER Timeout The maximum amount of time that this function can take to perform the copy. If the timeout is reached before the process is complete an error will be thrown. The timeout defaults to 300 seconds. .EXAMPLE $Lab = Get-Lab -ConfigPath c:\mylab\config.xml $VMs = Get-LabVM -Lab $Lab Copy-LabOdjFile -Lab $Lab -VM $VMs[0] .OUTPUTS None. #> function Copy-LabOdjFile { [CmdLetBinding()] param ( [Parameter(Mandatory = $true)] $Lab, [Parameter(Mandatory = $true)] [LabVM] $VM, [System.Int32] $Timeout = 300 ) $startTime = Get-Date $session = $null $complete = $false $odjCopyComplete = $false $odjFilename = Join-Path ` -Path $vmLabBuilderFiles ` -ChildPath "$($VM.ComputerName).txt" # If ODJ file does not exist then return if (-not (Test-Path -Path $odjFilename)) { return } # if # Get Path to LabBuilder files $vmLabBuilderFiles = $VM.LabBuilderFilesPath while ((-not $complete) ` -and (((Get-Date) - $startTime).TotalSeconds) -lt $TimeOut) { # Connect to the VM $session = Connect-LabVM ` -VM $VM ` -ErrorAction Continue # Failed to connnect to the VM if (-not $session) { $exceptionParameters = @{ errorId = 'ODJCopyError' errorCategory = 'OperationTimeout' errorMessage = $($LocalizedData.ODJCopyError ` -f $VM.Name,$odjFilename) } New-LabException @exceptionParameters return } # if if (($session) ` -and ($session.State -eq 'Opened') ` -and (-not $odjCopyComplete)) { $CopyParameters = @{ Destination = 'C:\Windows\Setup\ODJFiles\' ToSession = $session Force = $true ErrorAction = 'Stop' } # Connection has been made OK, upload the ODJ files While ((-not $odjCopyComplete) ` -and (((Get-Date) - $startTime).TotalSeconds) -lt $TimeOut) { Try { Write-LabMessage -Message $($LocalizedData.CopyingFilesToVMMessage ` -f $VM.Name,'ODJ') Copy-Item ` @CopyParameters ` -Path (Join-Path ` -Path $vmLabBuilderFiles ` -ChildPath "$($VM.ComputerName).txt") ` -Verbose $odjCopyComplete = $true } Catch { Write-LabMessage -Message $($LocalizedData.CopyingFilesToVMFailedMessage ` -f $VM.Name,'ODJ',$Script:RetryConnectSeconds) Start-Sleep -Seconds $Script:RetryConnectSeconds } # try } # while } # if # If the copy didn't complete and we're out of time throw an exception if ((-not $odjCopyComplete) ` -and (((Get-Date) - $startTime).TotalSeconds) -ge $TimeOut) { # Disconnect from the VM Disconnect-LabVM ` -VM $VM ` -ErrorAction Continue $exceptionParameters = @{ errorId = 'ODJCopyError' errorCategory = 'OperationTimeout' errorMessage = $($LocalizedData.ODJCopyError ` -f $VM.Name,$odjFilename) } New-LabException @exceptionParameters } # if # Disconnect from the VM Disconnect-LabVM ` -VM $VM ` -ErrorAction Continue $complete = $true } # while } <# .SYNOPSIS Ensures the WS-Man is configured on this system. .DESCRIPTION If WS-Man is not enabled on this system it will be enabled. This is required to communicate with the managed Lab Virtual Machines. .EXAMPLE Enable-LabWSMan Enables WS-Man on this machine. .OUTPUTS None #> function Enable-LabWSMan { [CmdLetBinding()] param ( [Parameter()] [Switch] $Force ) if (-not (Get-PSPRovider -PSProvider WSMan -ErrorAction SilentlyContinue)) { Write-LabMessage -Message ($LocalizedData.EnablingWSManMessage) try { Start-Service -Name WinRm -ErrorAction Stop } catch { $null = Enable-PSRemoting ` @PSBoundParameters ` -SkipNetworkProfileCheck ` -ErrorAction Stop } # Check WS-Man was enabled if (-not (Get-PSProvider -PSProvider WSMan -ErrorAction SilentlyContinue)) { $exceptionParameters = @{ errorId = 'WSManNotEnabledError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.WSManNotEnabledError) } New-LabException @exceptionParameters } # if } # if } <# .SYNOPSIS Assemble the the PowerShell commands required to create a self-signed certificate. .DESCRIPTION This function creates the content that can be written into a PS1 file to create a self-signed certificate. .EXAMPLE $Lab = Get-Lab -ConfigPath c:\mylab\config.xml $VMs = Get-LabVM -Lab $Lab $CertificatePS = Get-LabCertificatePsFileContent -Lab $Lab -VM $VMs[0] Return the Create Self-Signed Certificate script for the first VM in the Lab c:\mylab\config.xml for DSC configuration. .PARAMETER Lab Contains the Lab object that was produced by the Get-Lab cmdlet. .PARAMETER VM A LabVM object pulled from the Lab Configuration file using Get-LabVM .PARAMETER CertificateSource A CertificateSource to use instead of the one contained in the VM. .OUTPUTS A string containing the Create Self-Signed Certificate PowerShell code. #> function Get-LabCertificatePsFileContent { [CmdLetBinding()] [OutputType([System.String])] param ( [Parameter(Mandatory = $true)] $Lab, [Parameter(Mandatory = $true)] [LabVM] $VM, [Parameter()] [LabCertificateSource] $CertificateSource ) # If a CertificateSource is not provided get it from the VM. if (-not $CertificateSource) { $CertificateSource = $VM.CertificateSource } # if if ($CertificateSource -eq [LabCertificateSource]::Guest) { $createCertificatePs = @" `$CertificateFriendlyName = '$($Script:DSCCertificateFriendlyName)' `$Cert = Get-ChildItem -Path cert:\LocalMachine\My `` | Where-Object { `$_.FriendlyName -eq `$CertificateFriendlyName } `` | Select-Object -First 1 if (-not `$Cert) { . `"`$(`$ENV:SystemRoot)\Setup\Scripts\New-SelfSignedCertificateEx.ps1`" New-SelfsignedCertificateEx `` -Subject 'CN=$($VM.ComputerName)' `` -EKU '1.3.6.1.4.1.311.80.1','1.3.6.1.5.5.7.3.1','1.3.6.1.5.5.7.3.2' `` -KeyUsage 'DigitalSignature, KeyEncipherment, DataEncipherment' `` -SAN '$($VM.ComputerName)' `` -FriendlyName `$CertificateFriendlyName `` -Exportable `` -StoreLocation 'LocalMachine' `` -StoreName 'My' `` -KeyLength $($Script:SelfSignedCertKeyLength) `` -ProviderName '$($Script:SelfSignedCertProviderName)' `` -AlgorithmName $($Script:SelfSignedCertAlgorithmName) `` -SignatureAlgorithm $($Script:SelfSignedCertSignatureAlgorithm) # There is a slight delay before new cert shows up in Cert: # So wait for it to show. While (-not `$Cert) { `$Cert = Get-ChildItem -Path cert:\LocalMachine\My `` | Where-Object { `$_.FriendlyName -eq `$CertificateFriendlyName } } } Export-Certificate `` -Type CERT `` -Cert `$Cert `` -FilePath `"`$(`$ENV:SystemRoot)\$Script:DSCEncryptionCert`" Add-Content `` -Path `"`$(`$ENV:SystemRoot)\Setup\Scripts\SetupComplete.log`" `` -Value 'Encryption Certificate Imported from CER ...' `` -Encoding Ascii "@ } else { [System.String] $createCertificatePs = @" if (Test-Path -Path `"`$(`$ENV:SystemRoot)\$Script:DSCEncryptionPfxCert`") { `$CertificatePassword = ConvertTo-SecureString `` -String '$Script:DSCCertificatePassword' `` -Force `` -AsPlainText Add-Content `` -Path `"`$(`$ENV:SystemRoot)\Setup\Scripts\SetupComplete.log`" `` -Value 'Importing Encryption Certificate from PFX ...' `` -Encoding Ascii Import-PfxCertificate `` -Password '$Script:DSCCertificatePassword' `` -FilePath `"`$(`$ENV:SystemRoot)\$Script:DSCEncryptionPfxCert`" `` -CertStoreLocation cert:\localMachine\root Add-Content `` -Path `"`$(`$ENV:SystemRoot)\Setup\Scripts\SetupComplete.log`" `` -Value 'Encryption Certificate from PFX Imported...' `` -Encoding Ascii } "@ } # if return $createCertificatePs } <# .SYNOPSIS Assemble the content of the Networking DSC config file. .DESCRIPTION This function creates the content that will be written to the Networking DSC Config file from the networking details stored in the VM object. .EXAMPLE $Lab = Get-Lab -ConfigPath c:\mylab\config.xml $VMs = Get-LabVM -Lab $Lab $NetworkingDsc = Get-LabDSCNetworkingConfig -Lab $Lab -VM $VMs[0] Return the Networking DSC for the first VM in the Lab c:\mylab\config.xml for DSC configuration. .PARAMETER Lab Contains the Lab object that was produced by the Get-Lab cmdlet. .PARAMETER VM A LabVM object pulled from the Lab Configuration file using Get-LabVM .OUTPUTS A string containing the DSC Networking config. #> function Get-LabDSCNetworkingConfig { [CmdLetBinding()] [OutputType([System.String])] param ( [Parameter(Mandatory = $true)] $Lab, [Parameter(Mandatory = $true)] [LabVM] $VM ) $NetworkingDscVersion = (` Get-Module -Name NetworkingDsc -ListAvailable ` | Sort-Object version -Descending ` | Select-Object -First 1 ` ).Version.ToString() $dscNetworkingConfig = @" Configuration Networking { Import-DscResource -ModuleName NetworkingDsc -ModuleVersion $NetworkingDscVersion "@ $adapterCount = 0 foreach ($Adapter in $VM.Adapters) { $adapterCount++ if ($adapter.IPv4) { if (-not [System.String]::IsNullOrWhitespace($adapter.IPv4.Address)) { $dscNetworkingConfig += @" IPAddress IPv4_$adapterCount { InterfaceAlias = '$($adapter.Name)' AddressFamily = 'IPv4' IPAddress = '$($adapter.IPv4.Address.Replace(',',"','"))/$($adapter.IPv4.SubnetMask)' } "@ if (-not [System.String]::IsNullOrWhitespace($adapter.IPv4.DefaultGateway)) { $dscNetworkingConfig += @" DefaultGatewayAddress IPv4G_$adapterCount { InterfaceAlias = '$($adapter.Name)' AddressFamily = 'IPv4' Address = '$($adapter.IPv4.DefaultGateway)' } "@ } else { $dscNetworkingConfig += @" DefaultGatewayAddress IPv4G_$adapterCount { InterfaceAlias = '$($adapter.Name)' AddressFamily = 'IPv4' } "@ } # if } else { $dscNetworkingConfig += @" NetIPInterface IPv4DHCP_$adapterCount { InterfaceAlias = '$($adapter.Name)' AddressFamily = 'IPv4' Dhcp = 'Enabled' } "@ } # if if (-not [System.String]::IsNullOrWhitespace($adapter.IPv4.DNSServer)) { $dscNetworkingConfig += @" DnsServerAddress IPv4D_$adapterCount { InterfaceAlias = '$($adapter.Name)' AddressFamily = 'IPv4' Address = '$($adapter.IPv4.DNSServer.Replace(',',"','"))' } "@ } # if } # if if ($adapter.IPv6) { if (-not [System.String]::IsNullOrWhitespace($adapter.IPv6.Address)) { $dscNetworkingConfig += @" IPAddress IPv6_$adapterCount { InterfaceAlias = '$($adapter.Name)' AddressFamily = 'IPv6' IPAddress = '$($adapter.IPv6.Address.Replace(',',"','"))/$($adapter.IPv6.SubnetMask)' } "@ if (-not [System.String]::IsNullOrWhitespace($adapter.IPv6.DefaultGateway)) { $dscNetworkingConfig += @" DefaultGatewayAddress IPv6G_$adapterCount { InterfaceAlias = '$($adapter.Name)' AddressFamily = 'IPv6' Address = '$($adapter.IPv6.DefaultGateway)' } "@ } else { $dscNetworkingConfig += @" DefaultGatewayAddress IPv6G_$adapterCount { InterfaceAlias = '$($adapter.Name)' AddressFamily = 'IPv6' } "@ } # if } else { $dscNetworkingConfig += @" NetIPInterface IPv6DHCP_$adapterCount { InterfaceAlias = '$($adapter.Name)' AddressFamily = 'IPv6' Dhcp = 'Enabled' } "@ } # if if (-not [System.String]::IsNullOrWhitespace($adapter.IPv6.DNSServer)) { $dscNetworkingConfig += @" DnsServerAddress IPv6D_$adapterCount { InterfaceAlias = '$($adapter.Name)' AddressFamily = 'IPv6' Address = '$($adapter.IPv6.DNSServer.Replace(',',"','"))' } "@ } # if } # if } # endfor $dscNetworkingConfig += @" } "@ return $dscNetworkingConfig } # Get-LabDSCNetworkingConfig [DSCLocalConfigurationManager()] Configuration ConfigLCM { param ( [System.String] $ComputerName, [System.String] $Thumbprint ) Node $ComputerName { Settings { RefreshMode = 'Push' ConfigurationMode = 'ApplyAndAutoCorrect' CertificateId = $Thumbprint ConfigurationModeFrequencyMins = 15 RefreshFrequencyMins = 30 RebootNodeIfNeeded = $true ActionAfterReboot = 'ContinueConfiguration' } } } <# .SYNOPSIS Get list of Integration Service names (localized) .DESCRIPTION This cmdlet will get the list of Integration services available on a Hyper-V host. The list of Integration Services will contain the localized names. .EXAMPLE Get-LabIntegrationServiceName .OUTPUTS An array of localized Integration Serivce names. #> function Get-LabIntegrationServiceName { [CmdLetBinding()] param ( ) $captions = @() $classes = @( 'Msvm_VssComponentSettingData' 'Msvm_ShutdownComponentSettingData' 'Msvm_TimeSyncComponentSettingData' 'Msvm_HeartbeatComponentSettingData' 'Msvm_GuestServiceInterfaceComponentSettingData' 'Msvm_KvpExchangeComponentSettingData' ) <# This Integration Service is registered in CIM but is not exposed in Hyper-V: 'Msvm_RdvComponentSettingData' #> foreach ($class in $classes) { $captions += (Get-CimInstance ` -Class $class ` -Namespace Root\Virtualization\V2 ` -Property Caption | Select-Object -First 1).Caption } # foreach return $captions } <# .SYNOPSIS Returns the name of the Management Switch to use for this lab. .DESCRIPTION Each lab has a unique private management switch created for it. All Virtual Machines in the Lab are connected to the switch. This function returns the name of this swtich for the provided lab configuration. .PARAMETER Lab Contains the Lab object that was produced by the Get-Lab cmdlet. .EXAMPLE $Lab = Get-Lab -ConfigPath c:\mylab\config.xml $ManagementSwitch = Get-LabManagementSwitchName -Lab $Lab Returns the Management Switch for the Lab c:\mylab\config.xml. .OUTPUTS A management switch name. #> function Get-LabManagementSwitchName { [CmdLetBinding()] [OutputType([System.String])] param ( [Parameter(Mandatory = $true)] $Lab ) $LabId = $Lab.labbuilderconfig.settings.labid if (-not $LabId) { $LabId = $Lab.labbuilderconfig.name } # if $managementSwitchName = ('{0}Lab Management' -f $LabId) return $managementSwitchName } <# .SYNOPSIS Get a list of all Resources imported in a DSC Config .DESCRIPTION Uses RegEx to pull a list of Resources that are imported in a DSC Configuration using the Import-DSCResource cmdlet. If The -ModuleVersion parameter is included then the ModuleVersion property in the returned LabDSCModule object will be set, otherwise it will be null. .PARAMETER DscConfigFile Contains the path to the DSC Config file to extract resource module names from. .PARAMETER DscConfigContent Contains the content of the DSC Config to extract resource module names from. .EXAMPLE Get-LabModulesInDSCConfig -DscConfigFile c:\mydsc\Server01.ps1 Return the DSC Resource module list from file c:\mydsc\server01.ps1 .EXAMPLE Get-LabModulesInDSCConfig -DscConfigContent $DSCConfig Return the DSC Resource module list from the DSC Config in $DSCConfig. .OUTPUTS An array of LabDSCModule objects containing the DSC Resource modules required by this DSC configuration file. #> function Get-LabModulesInDSCConfig { [CmdLetBinding(DefaultParameterSetName = "Content")] [OutputType([Object[]])] param ( [parameter( Position = 1, ParameterSetName = "Content", Mandatory = $true)] [System.String] $DscConfigContent, [parameter( Position = 2, ParameterSetName = "File", Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $DscConfigFile ) [LabDSCModule[]] $modules = $null if ($PSCmdlet.ParameterSetName -eq 'File') { $DscConfigContent = Get-Content -Path $DscConfigFile -Raw } # if $regex = "[ \t]*?Import\-DscResource[ \t]+(?:\-ModuleName[ \t])?'?`"?([A-Za-z0-9._-]+)`"?'?(([ \t]+-ModuleVersion)?[ \t]+'?`"?([0-9.]+)`"?`?)?[ \t]*?[\r\n]+?" $moduleMatches = [regex]::matches($DscConfigContent, $regex, 'IgnoreCase') foreach ($moduleMatch in $moduleMatches) { $moduleName = $moduleMatch.Groups[1].Value $moduleVersion = $moduleMatch.Groups[4].Value # Make sure this module isn't already in the list if ($moduleName -notin $Modules.ModuleName) { $module = [LabDSCModule]::New($moduleName) if (-not [System.String]::IsNullOrWhitespace($moduleVersion)) { $module.moduleVersion = [Version] $moduleVersion } # if $modules += @( $module ) } # if } # foreach return $modules } <# .SYNOPSIS Increases the IP Address. .PARAMETER IpAddress Contains the IP Address to increase. .PARAMETER Step Contains the number of steps to increase the IP address by. .EXAMPLE Get-LabNextIpAddress -IpAddress '192.168.123.44' -Step 2 Returns the IP Address '192.168.123.44' .EXAMPLE Get-LabNextIpAddress -IpAddress 'fe80::15b4:b934:5d23:1a2f' -Step 2 Returns the IP Address 'fe80::15b4:b934:5d23:1a31' .OUTPUTS The increased IP Address. #> function Get-LabNextIpAddress { [CmdLetBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $IpAddress, [Parameter()] [System.Byte] $Step = 1 ) # Check the IP Address is valid $ip = Assert-LabValidIpAddress -IpAddress $IpAddress # This code will increase the next IP address by the step amount. # It uses the IP Address byte array to do this. $bytes = $ip.GetAddressBytes() $position = $bytes.Length - 1 while ($Step -gt 0) { if ($bytes[$position] + $Step -gt 255) { $bytes[$position] = $bytes[$position] + $Step - 256 $Step = $Step - $bytes[$position] $position-- } else { $bytes[$position] = $bytes[$position] + $Step $Step = 0 } # if } # while return [System.Net.IPAddress]::new($bytes).IPAddressToString } <# .SYNOPSIS Increases the MAC Address. .PARAMETER MACAddress Contains the MAC Address to increase. .PARAMETER Step Contains the number of steps to increase the MAC address by. .EXAMPLE Get-NextMacAddress -MacAddress '00155D0106ED' -Step 2 Returns the MAC Address '00155D0106EF' .OUTPUTS The increased MAC Address. #> function Get-NextMacAddress { [CmdLetBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $MacAddress, [System.Byte] $Step = 1 ) return [System.String]::Format("{0:X}", [Convert]::ToUInt64($MACAddress, 16) + $Step).PadLeft(12, '0') } <# .SYNOPSIS Assembles the content of a Unattend XML file that should be used to initialize Windows on the specified VM. .DESCRIPTION This function will return the content of a standard Windows Unattend XML file that can be written to an VHD containing a copy of Windows that is still in OOBE mode. .PARAMETER Lab Contains the Lab object that was produced by the Get-Lab cmdlet. .PARAMETER VM A LabVM object pulled from the Lab Configuration file using Get-LabVM .EXAMPLE $Lab = Get-Lab -ConfigPath c:\mylab\config.xml $VMs = Get-LabVM -Lab $Lab Get-LabUnattendFileContent -Lab $Lab -VM $VMs[0] Returns the content of the Unattend File for the first VM in the Lab c:\mylab\config.xml. .OUTPUTS The content of the Unattend File for the VM. #> function Get-LabUnattendFileContent { [CmdLetBinding()] [OutputType([System.String])] param ( [Parameter(Mandatory = $true)] $Lab, [Parameter(Mandatory = $true)] [LabVM] $VM ) if ($VM.UnattendFile) { $unattendContent = Get-Content -Path $VM.UnattendFile } else { $domainName = $Lab.labbuilderconfig.settings.domainname $email = $Lab.labbuilderconfig.settings.email $unattendContent = @" <?xml version="1.0" encoding="utf-8"?> <unattend xmlns="urn:schemas-microsoft-com:unattend"> <settings pass="offlineServicing"> <component name="Microsoft-Windows-LUA-Settings" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <EnableLUA>false</EnableLUA> </component> </settings> <settings pass="generalize"> <component name="Microsoft-Windows-Security-SPP" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <SkipRearm>1</SkipRearm> </component> </settings> <settings pass="specialize"> <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <InputLocale>0409:00000409</InputLocale> <SystemLocale>en-US</SystemLocale> <UILanguage>en-US</UILanguage> <UILanguageFallback>en-US</UILanguageFallback> <UserLocale>en-US</UserLocale> </component> <component name="Microsoft-Windows-Security-SPP-UX" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <SkipAutoActivation>true</SkipAutoActivation> </component> <component name="Microsoft-Windows-SQMApi" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <CEIPEnabled>0</CEIPEnabled> </component> <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <ComputerName>$($VM.ComputerName)</ComputerName> </component> "@ if ($VM.OSType -eq [LabOSType]::Client) { $unattendContent += @" <component name="Microsoft-Windows-Deployment" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <RunSynchronous> <RunSynchronousCommand wcm:action="add"> <Order>1</Order> <Path>net user administrator /active:yes</Path> </RunSynchronousCommand> </RunSynchronous> </component> "@ } # If $unattendContent += @" </settings> <settings pass="oobeSystem"> <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <OOBE> <HideEULAPage>true</HideEULAPage> <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen> <HideOnlineAccountScreens>true</HideOnlineAccountScreens> <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE> <NetworkLocation>Work</NetworkLocation> <ProtectYourPC>1</ProtectYourPC> <SkipUserOOBE>true</SkipUserOOBE> <SkipMachineOOBE>true</SkipMachineOOBE> </OOBE> <UserAccounts> <AdministratorPassword> <Value>$($VM.AdministratorPassword)</Value> <PlainText>true</PlainText> </AdministratorPassword> </UserAccounts> <RegisteredOrganization>$($domainName)</RegisteredOrganization> <RegisteredOwner>$($email)</RegisteredOwner> <DisableAutoDaylightTimeSet>false</DisableAutoDaylightTimeSet> <TimeZone>$($VM.TimeZone)</TimeZone> </component> <component name="Microsoft-Windows-ehome-reg-inf" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="NonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <RestartEnabled>true</RestartEnabled> </component> <component name="Microsoft-Windows-ehome-reg-inf" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="NonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <RestartEnabled>true</RestartEnabled> </component> </settings> </unattend> "@ } return $unattendContent } <# .SYNOPSIS Gets the Management IP Address for a running Lab VM. .DESCRIPTION This function will return the IPv4 address assigned to the network adapter that is connected to the Management switch for the specified VM. The VM must be running, otherwise an error will be thrown. .PARAMETER Lab Contains the Lab object that was produced by the Get-Lab cmdlet. .PARAMETER VM A LabVM object pulled from the Lab Configuration file using Get-LabVM .EXAMPLE $Lab = Get-Lab -ConfigPath c:\mylab\config.xml $VMs = Get-LabVM -Lab $Lab $IPAddress = Get-LabVMManagementIPAddress -Lab $Lab -VM $VM[0] .OUTPUTS The IP Managment IP Address. #> function Get-LabVMManagementIPAddress { [CmdLetBinding()] [OutputType([System.String])] param ( [Parameter(Mandatory = $true)] $Lab, [Parameter(Mandatory = $true)] [LabVM] $VM ) $managementSwitchName = Get-LabManagementSwitchName -Lab $Lab $managementAdapter = Get-VMNetworkAdapter -VMName $VM.Name | Where-Object -Property SwitchName -EQ -Value $managementSwitchName $managementAdapterIpAddresses = $managementAdapter.IPAddresses $managementAdapterIpAddress = $managementAdapterIpAddresses | Where-Object -FilterScript { $_.Contains('.') } if (-not $managementAdapterIpAddress) { $exceptionParameters = @{ errorId = 'ManagmentIPAddressError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.ManagmentIPAddressError ` -f $managementSwitchName,$VM.Name) } New-LabException @exceptionParameters } # if return $managementAdapterIpAddress } <# .SYNOPSIS Initialized a VM VHD for first boot by applying any required files to the image. .DESCRIPTION This function mounts a VM boot VHD image and applies the following files from the LabBuilder Files folder to it: 1. Unattend.xml - a Windows Unattend.xml file. 2. SetupComplete.cmd - the command file that gets run after the Windows OOBE is complete. 3. SetupComplete.ps1 - this PowerShell script file that is run at the the end of the SetupComplete.cmd. The files should have already been prepared by the New-LabVMInitializationFile function. The VM VHD image should contain an installed copy of Windows still in OOBE mode. This function also applies optional MSU package files from the Lab resource folder if specified in the packages list in the VM. .PARAMETER Lab Contains the Lab object that was produced by the Get-Lab cmdlet. .PARAMETER VM A VMLab object pulled from the Lab Configuration file using Get-LabVM. .EXAMPLE $Lab = Get-Lab -ConfigPath c:\mylab\config.xml $VMs = Get-LabVM -Lab $Lab Initialize-LabBootVHD ` -Lab $Lab ` -VM $VMs[0] ` -VMBootDiskPath $BootVHD[0] Prepare the boot VHD in for the first VM in the Lab c:\mylab\config.xml for initial boot. .OUTPUTS None. #> function Initialize-LabBootVHD { [CmdLetBinding()] param ( [Parameter(Mandatory = $true)] $Lab, [Parameter(Mandatory = $true)] [LabVM] $VM, [Parameter(Mandatory = $true)] [System.String] $VMBootDiskPath ) # Get path to Lab [System.String] $LabPath = $Lab.labbuilderconfig.settings.labpath # Get Path to LabBuilder files [System.String] $VMLabBuilderFiles = $VM.LabBuilderFilesPath # Mount the VMs Boot VHD so that files can be loaded into it Write-LabMessage -Message $($LocalizedData.MountingVMBootDiskMessage ` -f $VM.Name,$VMBootDiskPath) # Create a mount point for mounting the Boot VHD [System.String] $MountPoint = Join-Path ` -Path $VMLabBuilderFiles ` -ChildPath 'Mount' if (-not (Test-Path -Path $MountPoint -PathType Container)) { $null = New-Item ` -Path $MountPoint ` -ItemType Directory } # Mount the VHD to the Mount point $null = Mount-WindowsImage ` -ImagePath $VMBootDiskPath ` -Path $MountPoint ` -Index 1 try { $Packages = $VM.Packages if ($VM.OSType -eq [LabOSType]::Nano) { # Now specify the Nano Server packages to add. [System.String] $NanoPackagesFolder = Join-Path ` -Path $LabPath ` -ChildPath 'NanoServerPackages' if (-not (Test-Path -Path $NanoPackagesFolder)) { $exceptionParameters = @{ errorId = 'NanoServerPackagesFolderMissingError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.NanoServerPackagesFolderMissingError ` -f $NanoPackagesFolder) } New-LabException @exceptionParameters } # Add DSC Package to packages list if missing if ([System.String]::IsNullOrWhitespace($Packages)) { $Packages = 'Microsoft-NanoServer-DSC-Package.cab' } else { if (@($Packages -split ',') -notcontains 'Microsoft-NanoServer-DSC-Package.cab') { $Packages = "$Packages,Microsoft-NanoServer-DSC-Package.cab" } # if } # if } # if # Apply any listed packages to the Image if (-not [System.String]::IsNullOrWhitespace($Packages)) { # Get the list of Lab Resource MSUs $ResourceMSUs = Get-LabResourceMSU ` -Lab $Lab foreach ($Package in @($Packages -split ',')) { if (([System.IO.Path]::GetExtension($Package) -eq '.cab') ` -and ($VM.OSType -eq [LabOSType]::Nano)) { # This is a Nano Server .CAB package # Generate the path to the Nano Package $PackagePath = Join-Path ` -Path $NanoPackagesFolder ` -ChildPath $Package # Does it exist? if (-not (Test-Path -Path $PackagePath)) { $exceptionParameters = @{ errorId = 'NanoPackageNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.NanoPackageNotFoundError ` -f $PackagePath) } New-LabException @exceptionParameters } # Add the package Write-LabMessage -Message $($LocalizedData.ApplyingVMBootDiskFileMessage ` -f $VM.Name,$Package,$PackagePath) $null = Add-WindowsPackage ` -PackagePath $PackagePath ` -Path $MountPoint # Generate the path to the Nano Language Package $PackageLangFile = $Package -replace '.cab',"_$($Script:NanoPackageCulture).cab" $PackageLangFile = Join-Path ` -Path $NanoPackagesFolder ` -ChildPath "$($Script:NanoPackageCulture)\$PackageLangFile" # Does it exist? if (-not (Test-Path -Path $PackageLangFile)) { $exceptionParameters = @{ errorId = 'NanoPackageNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.NanoPackageNotFoundError ` -f $PackageLangFile) } New-LabException @exceptionParameters } Write-LabMessage -Message $($LocalizedData.ApplyingVMBootDiskFileMessage ` -f $VM.Name,$Package,$PackageLangFile) # Add the package $null = Add-WindowsPackage ` -PackagePath $PackageLangFile ` -Path $MountPoint } else { # Tihs is a ResourceMSU type package [System.Boolean] $Found = $false foreach ($ResourceMSU in $ResourceMSUs) { if ($ResourceMSU.Name -eq $Package) { # Found the package $Found = $true break } # if } # foreach if (-not $Found) { $exceptionParameters = @{ errorId = 'PackageNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.PackageNotFoundError ` -f $Package) } New-LabException @exceptionParameters } # if $PackagePath = $ResourceMSU.Filename if (-not (Test-Path -Path $PackagePath)) { $exceptionParameters = @{ errorId = 'PackageMSUNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.PackageMSUNotFoundError ` -f $Package,$PackagePath) } New-LabException @exceptionParameters } # if # Apply a Package Write-LabMessage -Message $($LocalizedData.ApplyingVMBootDiskFileMessage ` -f $VM.Name,$Package,$PackagePath) $null = Add-WindowsPackage ` -PackagePath $PackagePath ` -Path $MountPoint } # if } # foreach } # if } catch { # Dismount Disk Image before throwing exception Write-LabMessage -Message $($LocalizedData.DismountingVMBootDiskMessage ` -f $VM.Name,$VMBootDiskPath) $null = Dismount-WindowsImage -Path $MountPoint -Save $null = Remove-Item -Path $MountPoint -Recurse -Force Throw $_ } # try # Create the scripts folder where setup scripts will be put $null = New-Item ` -Path "$MountPoint\Windows\Setup\Scripts" ` -ItemType Directory # Create the ODJ folder where Offline domain join files can be put $null = New-Item ` -Path "$MountPoint\Windows\Setup\ODJFiles" ` -ItemType Directory # Apply an unattended setup file Write-LabMessage -Message $($LocalizedData.ApplyingVMBootDiskFileMessage ` -f $VM.Name,'Unattend','Unattend.xml') if (-not (Test-Path -Path "$MountPoint\Windows\Panther" -PathType Container)) { Write-LabMessage -Message $($LocalizedData.CreatingVMBootDiskPantherFolderMessage ` -f $VM.Name) $null = New-Item ` -Path "$MountPoint\Windows\Panther" ` -ItemType Directory } # if $null = Copy-Item ` -Path (Join-Path -Path $VMLabBuilderFiles -ChildPath 'Unattend.xml') ` -Destination "$MountPoint\Windows\Panther\Unattend.xml" ` -Force # If a Certificate PFX file is available, copy it into the c:\Windows # folder of the VM. $CertificatePfxPath = Join-Path ` -Path $VMLabBuilderFiles ` -ChildPath $Script:DSCEncryptionPfxCert if (Test-Path -Path $CertificatePfxPath) { # Apply the CMD Setup Complete File Write-LabMessage -Message $($LocalizedData.ApplyingVMBootDiskFileMessage ` -f $VM.Name,'Credential Certificate PFX',$Script:DSCEncryptionPfxCert) $null = Copy-Item ` -Path $CertificatePfxPath ` -Destination "$MountPoint\Windows\$Script:DSCEncryptionPfxCert" ` -Force } # Apply the CMD Setup Complete File Write-LabMessage -Message $($LocalizedData.ApplyingVMBootDiskFileMessage ` -f $VM.Name,'Setup Complete CMD','SetupComplete.cmd') $null = Copy-Item ` -Path (Join-Path -Path $VMLabBuilderFiles -ChildPath 'SetupComplete.cmd') ` -Destination "$MountPoint\Windows\Setup\Scripts\SetupComplete.cmd" ` -Force # Apply the PowerShell Setup Complete file Write-LabMessage -Message $($LocalizedData.ApplyingVMBootDiskFileMessage ` -f $VM.Name,'Setup Complete PowerShell','SetupComplete.ps1') $null = Copy-Item ` -Path (Join-Path -Path $VMLabBuilderFiles -ChildPath 'SetupComplete.ps1') ` -Destination "$MountPoint\Windows\Setup\Scripts\SetupComplete.ps1" ` -Force # Apply the Certificate Generator script if not a Nano Server if ($VM.OSType -ne [LabOSType]::Nano) { $CertGenFilename = Split-Path -Path $Script:SupportGertGenPath -Leaf Write-LabMessage -Message $($LocalizedData.ApplyingVMBootDiskFileMessage ` -f $VM.Name,'Certificate Create Script',$CertGenFilename) $null = Copy-Item ` -Path $Script:SupportGertGenPath ` -Destination "$MountPoint\Windows\Setup\Scripts\"` -Force } # Dismount the VHD in preparation for boot Write-LabMessage -Message $($LocalizedData.DismountingVMBootDiskMessage ` -f $VM.Name,$VMBootDiskPath) $null = Dismount-WindowsImage -Path $MountPoint -Save $null = Remove-Item -Path $MountPoint -Recurse -Force } <# .SYNOPSIS This function prepares all files require to configure a VM using Desired State Configuration (DSC). .DESCRIPTION Calling this function will cause the LabBuilder folder to be populated/updated with all files required to configure a Virtual Machine with DSC. This includes: 1. Required DSC Resouce Modules. 2. DSC Credential Encryption certificate. 3. DSC Configuration files. 4. DSC MOF Files for general config and for LCM config. 5. Start up scripts. .PARAMETER Lab Contains the Lab object that was produced by the Get-Lab cmdlet. .PARAMETER VM A LabVM object pulled from the Lab Configuration file using Get-LabVM .EXAMPLE $Lab = Get-Lab -ConfigPath c:\mylab\config.xml $VMs = Get-LabVM -Lab $Lab Initialize-LabDSC -Lab $Lab -VM $VMs[0] Prepares all files required to start up Desired State Configuration for the first VM in the Lab c:\mylab\config.xml for DSC start up. .OUTPUTS None. #> function Initialize-LabDSC { [CmdLetBinding()] param ( [Parameter(Mandatory = $true)] $Lab, [Parameter(Mandatory = $true)] [LabVM] $VM ) # Are there any DSC Settings to manage? Update-LabDSC -Lab $Lab -VM $VM # Generate the DSC Start up Script file Set-LabDSC -Lab $Lab -VM $VM } <# .SYNOPSIS Create the LabBuilder Management Network switch and assign VLAN .DESCRIPTION Each lab needs a unique private management switch created for it. All Virtual Machines in the Lab are connected to the switch. This function creates the virtual switch and attaches an adapter to it and assigns it to a VLAN. .PARAMETER Lab Contains the Lab object that was produced by the Get-Lab cmdlet. .EXAMPLE $Lab = Get-Lab -ConfigPath c:\mylab\config.xml Initialize-LabManagementSwitch -Lab $Lab Creates or updates the Management Switch for the Lab c:\mylab\config.xml. .OUTPUTS None. #> function Initialize-LabManagementSwitch { [CmdLetBinding()] param ( [Parameter(Mandatory = $true)] $Lab ) # Used by host to communicate with Lab VMs $managementSwitchName = Get-LabManagementSwitchName -Lab $Lab Write-LabMessage -Message $($LocalizedData.InitializingLabManagementVirtualNetworkMesage ` -f $managementSwitchName) if ($Lab.labbuilderconfig.switches.ManagementVlan) { $requiredManagementVlan = $Lab.labbuilderconfig.switches.ManagementVlan } else { $requiredManagementVlan = $Script:DefaultManagementVLan } $managementSwitch = Get-VMSwitch | Where-Object -Property Name -eq $managementSwitchName if ($managementSwitch.Count -eq 0) { $null = New-VMSwitch ` -SwitchType Internal ` -Name $managementSwitchName ` -ErrorAction Stop Write-LabMessage -Message $($LocalizedData.CreatingLabManagementSwitchMessage ` -f $managementSwitchName, $requiredManagementVlan) } # Check the Vlan ID of the adapter on the switch $existingManagementAdapter = Get-VMNetworkAdapter ` -ManagementOS ` -Name $managementSwitchName ` -SwitchName $managementSwitchName ` -ErrorAction SilentlyContinue if ($null -eq $existingManagementAdapter) { $existingManagementAdapter = Add-VMNetworkAdapter ` -ManagementOS ` -Name $managementSwitchName ` -SwitchName $managementSwitchName ` -ErrorAction Stop } $existingManagementAdapterVlan = Get-VMNetworkAdapterVlan ` -VMNetworkAdapter $existingManagementAdapter $existingManagementVlan = $existingManagementAdapterVlan.AccessVlanId if ($existingManagementVlan -ne $requiredManagementVlan) { Write-LabMessage -Message $($LocalizedData.UpdatingLabManagementSwitchMessage ` -f $managementSwitchName, $requiredManagementVlan) Set-VMNetworkAdapterVlan ` -VMNetworkAdapter $existingManagementAdapter ` -Access ` -VlanId $requiredManagementVlan ` -ErrorAction Stop } } <# .SYNOPSIS This function mounts the VHDx passed and ensures it is OK to be written to. .DESCRIPTION The function checks that the disk has been paritioned and that it contains a volume that has been formatted. This function will work for the following situations: 0. VHDx is not mounted. 1. VHDx is not initialized and PartitionStyle is passed. 2. VHDx is initialized but has 0 partitions and FileSystem is passed. 3. VHDx has 1 partition but 0 volumes and FileSystem is passed. 4. VHDx has 1 partition and 1 volume that is unformatted and FileSystem is passed. 5. VHDx has 1 partition and 1 volume that is formatted. If the VHDx is any other state an exception will be thrown. If the FileSystemLabel passed is different to the current label then it will be updated. This function will not change the File System and/or Partition Type on the VHDx if it is different to the values provided. .PARAMETER Path This is the path to the VHD/VHDx file to mount and initialize. .PARAMETER PartitionStyle The Partition Style to set an uninitialized VHD/VHDx to. It can be MBR or GPT. If it is not passed and the VHD is not initialized then an exception will be thrown. .PARAMETER FileSystem The File System to format the new parition with on an VHD/VHDx. It can be FAT, FAT32, exFAT, NTFS, ReFS. If it is not passed and the VHD does not contain any formatted volumes then an exception will be thrown. .PARAMETER FileSystemLabel This parameter will allow the File System Label of the disk to be changed to this value. .PARAMETER DriveLetter Setting this parameter to a drive letter that is not in use will cause the VHD to be assigned to this drive letter. .PARAMETER AccessPath Setting this parameter to an existing folder will cause the VHD to be assigned to the AccessPath defined. The folder must already exist otherwise an exception will be thrown. .EXAMPLE Initialize-LabVHD -Path c:\VMs\Tools.VHDx -AccessPath c:\mount The VHDx c:\VMs\Tools.VHDx will be mounted and and assigned to the c:\mount folder if it is initialized and contains a formatted partition. .EXAMPLE Initialize-LabVHD -Path c:\VMs\Tools.VHDx -PartitionStyle GPT -FileSystem NTFS The VHDx c:\VMs\Tools.VHDx will be mounted and initialized with GPT if not already initialized. It will also be partitioned and formatted with NTFS if no partitions already exist. .EXAMPLE Initialize-LabVHD ` -Path c:\VMs\Tools.VHDx ` -PartitionStyle GPT ` -FileSystem NTFS ` -FileSystemLabel ToolsDisk -DriveLetter X The VHDx c:\VMs\Tools.VHDx will be mounted and initialized with GPT if not already initialized. It will also be partitioned and formatted with NTFS if no partitions already exist. The File System label will also be set to ToolsDisk and the disk will be mounted to X drive. .OUTPUTS It will return the Volume object that can then be mounted to a Drive Letter or path. #> function Initialize-LabVHD { [OutputType([Microsoft.Management.Infrastructure.CimInstance])] [CmdletBinding(DefaultParameterSetName = 'AssignDriveLetter')] param ( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [System.String] $Path, [Parameter()] [LabPartitionStyle] $PartitionStyle, [Parameter()] [LabFileSystem] $FileSystem, [Parameter()] [ValidateNotNullOrEmpty()] [System.String] $FileSystemLabel, [Parameter(ParameterSetName = 'DriveLetter')] [ValidateNotNullOrEmpty()] [System.String] $DriveLetter, [Parameter(ParameterSetName = 'AccessPath')] [ValidateNotNullOrEmpty()] [System.String] $AccessPath ) # Check file exists if (-not (Test-Path -Path $Path)) { $exceptionParameters = @{ errorId = 'FileNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.FileNotFoundError ` -f "VHD",$Path) } New-LabException @exceptionParameters } # if # Check disk is not already mounted $VHD = Get-VHD ` -Path $Path if (-not $VHD.Attached) { Write-LabMessage -Message ($LocalizedData.InitializeVHDMountingMessage ` -f $Path) $null = Mount-VHD ` -Path $Path ` -ErrorAction Stop $VHD = Get-VHD ` -Path $Path } # if # Check partition style $DiskNumber = $VHD.DiskNumber if ((Get-Disk -Number $DiskNumber).PartitionStyle -eq 'RAW') { if (-not $PartitionStyle) { $exceptionParameters = @{ errorId = 'InitializeVHDNotInitializedError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.InitializeVHDNotInitializedError ` -f $Path) } New-LabException @exceptionParameters } # if Write-LabMessage -Message ($LocalizedData.InitializeVHDInitializingMessage ` -f $Path,$PartitionStyle) $null = Initialize-Disk ` -Number $DiskNumber ` -PartitionStyle $PartitionStyle ` -ErrorAction Stop } # if # Check for a partition that is not 'reserved' $Partitions = @(Get-Partition ` -DiskNumber $DiskNumber ` -ErrorAction SilentlyContinue) if (-not ($Partitions) ` -or (($Partitions | Where-Object -Property Type -ne 'Reserved').Count -eq 0)) { Write-LabMessage -Message ($LocalizedData.InitializeVHDCreatePartitionMessage ` -f $Path) $Partitions = @(New-Partition ` -DiskNumber $DiskNumber ` -UseMaximumSize ` -ErrorAction Stop) } # if # Find the best partition to work with # This will usually be the one just created if it was # Otherwise we'll try and match by FileSystem and then # format and failing that the first partition. foreach ($Partition in $Partitions) { $VolumeFileSystem = (Get-Volume ` -Partition $Partition).FileSystem if ($FileSystem) { if (-not [System.String]::IsNullOrWhitespace($VolumeFileSystem)) { # Found a formatted partition $FoundFormattedPartition = $Partition } # if if ($FileSystem -eq $VolumeFileSystem) { # Found a parition with a matching file system $FoundPartition = $Partition break } # if } else { if (-not [System.String]::IsNullOrWhitespace($VolumeFileSystem)) { # Found an formatted partition $FoundFormattedPartition = $Partition break } # if } # if } # foreach if ($FoundPartition) { # Use the formatted partition $Partition = $FoundPartition } elseif ($FoundFormattedPartition) { # An unformatted partition was found $Partition = $FoundFormattedPartition } else { # There are no formatted partitions so use the first one $Partition = $Partitions[0] } # if $PartitionNumber = $Partition.PartitionNumber # Check for volume $Volume = Get-Volume ` -Partition $Partition # Check for file system if ([System.String]::IsNullOrWhitespace($Volume.FileSystem)) { # This volume is not formatted if (-not $FileSystem) { # A File System wasn't specified so can't continue $exceptionParameters = @{ errorId = 'InitializeVHDNotFormattedError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.InitializeVHDNotFormattedError ` -f $Path) } New-LabException @exceptionParameters } # Format the volume Write-LabMessage -Message ($LocalizedData.InitializeVHDFormatVolumeMessage ` -f $Path,$FileSystem,$PartitionNumber) $FormatProperties = @{ InputObject = $Volume FileSystem = $FileSystem } if ($FileSystemLabel) { $FormatProperties += @{ NewFileSystemLabel = $FileSystemLabel } } $Volume = Format-Volume ` @FormatProperties ` -ErrorAction Stop } else { # Check the File System Label if (($FileSystemLabel) -and ` ($Volume.FileSystemLabel -ne $FileSystemLabel)) { Write-LabMessage -Message ($LocalizedData.InitializeVHDSetLabelVolumeMessage ` -f $Path,$FileSystemLabel) $Volume = Set-Volume ` -InputObject $Volume ` -NewFileSystemLabel $FileSystemLabel ` -ErrorAction Stop } } # Assign an access path or Drive letter if ($DriveLetter -or $AccessPath) { switch ($PSCmdlet.ParameterSetName) { 'DriveLetter' { # Mount the partition to a Drive Letter $null = Set-Partition ` -DiskNumber $Disknumber ` -PartitionNumber $PartitionNumber ` -NewDriveLetter $DriveLetter ` -ErrorAction Stop $Volume = Get-Volume ` -Partition $Partition Write-LabMessage -Message ($LocalizedData.InitializeVHDDriveLetterMessage ` -f $Path,$DriveLetter.ToUpper()) } 'AccessPath' { # Check the Access folder exists if (-not (Test-Path -Path $AccessPath -Type Container)) { $exceptionParameters = @{ errorId = 'InitializeVHDAccessPathNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.InitializeVHDAccessPathNotFoundError ` -f $Path,$AccessPath) } New-LabException @exceptionParameters } # Add the Partition Access Path $null = Add-PartitionAccessPath ` -DiskNumber $DiskNumber ` -PartitionNumber $partitionNumber ` -AccessPath $AccessPath ` -ErrorAction Stop Write-LabMessage -Message ($LocalizedData.InitializeVHDAccessPathMessage ` -f $Path,$AccessPath) } } } # Return the Volume to the pipeline return $Volume } <# .SYNOPSIS Creates the folder structure that will contain a Lab Virtual Machine. .DESCRIPTION Creates a standard Hyper-V Virtual Machine folder structure as well as additional folders for containing configuration files for DSC. .PARAMETER vmpath The path to the folder where the Virtual Machine files are stored. .EXAMPLE Initialize-LabVMPath -VMPath 'c:\VMs\Lab\Virtual Machine 1' The command will create the Virtual Machine structure for a Lab VM in the folder: 'c:\VMs\Lab\Virtual Machine 1' .OUTPUTS None. #> function Initialize-LabVMPath { [CmdLetBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $VMPath ) if (-not (Test-Path -Path $VMPath)) { $null = New-Item ` -Path $VMPath ` -ItemType Directory } # if if (-not (Test-Path -Path "$VMPath\Virtual Machines")) { $null = New-Item ` -Path "$VMPath\Virtual Machines" ` -ItemType Directory } # if if (-not (Test-Path -Path "$VMPath\Virtual Hard Disks")) { $null = New-Item ` -Path "$VMPath\Virtual Hard Disks" ` -ItemType Directory } # if if (-not (Test-Path -Path "$VMPath\LabBuilder Files")) { $null = New-Item ` -Path "$VMPath\LabBuilder Files" ` -ItemType Directory } # if if (-not (Test-Path -Path "$VMPath\LabBuilder Files\DSC Modules")) { $null = New-Item ` -Path "$VMPath\LabBuilder Files\DSC Modules" ` -ItemType Directory } # if } <# .SYNOPSIS Ensures the Hyper-V features are installed onto the system. .DESCRIPTION If the Hyper-V features are not installed onto this system they will be installed. .EXAMPLE Install-LabHyperV Installs the appropriate Hyper-V features if they are not currently installed. .OUTPUTS None #> function Install-LabHyperV { [CmdLetBinding()] param ( ) # Install Hyper-V Components if ((Get-CimInstance Win32_OperatingSystem).ProductType -eq 1) { # Desktop OS [Array] $feature = Get-WindowsOptionalFeature -Online -FeatureName '*Hyper-V*' ` | Where-Object -Property State -Eq 'Disabled' if ($feature.Count -gt 0 ) { Write-LabMessage -Message ($LocalizedData.InstallingHyperVComponentsMesage ` -f 'Desktop') $feature.Foreach( { Enable-WindowsOptionalFeature -Online -FeatureName $_.FeatureName } ) } } Else { # Server OS [Array] $feature = Get-WindowsFeature -Name Hyper-V ` | Where-Object -Property Installed -EQ $false if ($feature.Count -gt 0 ) { Write-LabMessage -Message ($LocalizedData.InstallingHyperVComponentsMesage ` -f 'Desktop') $feature.Foreach( { Install-WindowsFeature -IncludeAllSubFeature -IncludeManagementTools -Name $_.Name } ) } } } <# .SYNOPSIS Ensures the Package Providers required by LabBuilder are installed. .DESCRIPTION This function will check that both the NuGet and the PowerShellGet package providers are installed. If either of them are missing the function will attempt to install them. .EXAMPLE Install-LabPackageProvider Ensures the required Package Providers for LabBuilder are installed. .OUTPUTS None #> function Install-LabPackageProvider { [CmdLetBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param ( [Parameter()] [Switch] $Force ) $requiredPackageProviders = @('PowerShellGet', 'NuGet') $currentPackageProviders = Get-PackageProvider ` -ListAvailable ` -ErrorAction Stop foreach ($requiredPackageProvider in $requiredPackageProviders) { $packageProvider = $currentPackageProviders | Where-Object { $_.Name -eq $requiredPackageProvider } if (-not $packageProvider) { # The Package provider is not installed so install it if ($Force -or $PSCmdlet.ShouldProcess( 'LocalHost', ` ($LocalizedData.ShouldInstallPackageProvider ` -f $packageProvider ))) { Write-LabMessage -Message ($LocalizedData.InstallPackageProviderMessage ` -f $requiredPackageProvider) $null = Install-PackageProvider ` -Name $requiredPackageProvider ` -ForceBootstrap ` -Force ` -ErrorAction Stop } else { # Can't continue if the package provider is not installed. $exceptionParameters = @{ errorId = 'PackageProviderNotInstalledError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.PackageProviderNotInstalledError ` -f $requiredPackageProvider) } New-LabException @exceptionParameters } # if } # if } # foreach } <# .SYNOPSIS Download the a file to a folder and optionally unzip it. .DESCRIPTION If the file is a zip file the file will be downloaded to a temporary working folder and then unzipped to the destination, otherwise it will be downloaded straight to the destination folder. #> function Invoke-LabDownloadAndUnzipFile { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $URL, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $DestinationPath ) $fileName = [System.IO.Path]::GetFileName($URL) if (-not (Test-Path -Path $DestinationPath)) { $exceptionParameters = @{ errorId = 'DownloadFolderDoesNotExistError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.DownloadFolderDoesNotExistError ` -f $DestinationPath, $fileName) } New-LabException @exceptionParameters } $extension = [System.IO.Path]::GetExtension($fileName) if ($extension -eq '.zip') { # Download to a temp folder and unzip $downloadPath = Join-Path -Path $Script:WorkingFolder -ChildPath $fileName } else { # Download to a temp folder and unzip $downloadPath = Join-Path -Path $DestinationPath -ChildPath $fileName } Write-LabMessage -Message ($LocalizedData.DownloadingFileMessage ` -f $fileName, $URL, $downloadPath) try { Invoke-WebRequest ` -Uri $URL ` -OutFile $downloadPath ` -ErrorAction Stop } catch { $exceptionParameters = @{ errorId = 'FileDownloadError' errorCategory = 'InvalidOperation' errorMessage = $($LocalizedData.FileDownloadError -f $fileName, $URL, $_.Exception.Message) } New-LabException @exceptionParameters } # try if ($extension -eq '.zip') { Write-LabMessage -Message ($LocalizedData.ExtractingFileMessage ` -f $fileName, $downloadPath) # Extract this to the destination folder try { Expand-Archive ` -Path $downloadPath ` -DestinationPath $DestinationPath ` -Force ` -ErrorAction Stop } catch { $exceptionParameters = @{ errorId = 'FileExtractError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.FileExtractError -f $fileName, $_.Exception.Message) } New-LabException @exceptionParameters } finally { # Remove the downloaded zip file Remove-Item -Path $downloadPath } # try } } <# .SYNOPSIS Downloads a resource module. .DESCRIPTION It will download a specific resource module, either from PowerShell Gallery or from a URL if the module does not already exist. .PARAMETER Name Contains the Name of the module to download. .PARAMETER URL If this parameter is specified, the resource module will be downloaded from a URL rather than via PowerShell Gallery. This is a the URL to use to download a zip file containing this resource module. .PARAMETER Folder If this resource module is downloaded using a URL, this is the folder in the zip file that contains the resource and will need to be renamed to the name of the resource. .PARAMETER RequiredVersion This is the required version of the Resource Module that is required. If this version is not installed the a new version will be downloaded. .PARAMETER MinimumVersion This is the minimum version of the Resource Module that is required. If at least this version is not installed then a new version will be downloaded. .EXAMPLE Invoke-LabDownloadResourceModule ` -Name NetworkingDsc ` -RequiredVersion 2.7.0.0 Downloads the Resource Module xNetowrking version 2.7.0.0 .OUTPUTS None. #> function Invoke-LabDownloadResourceModule { [CmdLetBinding()] param ( [Parameter( position = 1, Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Name, [Parameter( position = 2)] [System.String] $URL, [Parameter( position = 3)] [System.String] $Folder, [Parameter( position = 4)] [System.String] $RequiredVersion, [Parameter( position = 5)] [System.String] $MinimumVersion ) $installedModules = @(Get-Module -ListAvailable) # Determine a query that will be used to decide if the module is already installed if ($RequiredVersion) { [ScriptBlock] $Query = { ($_.Name -eq $Name) -and ($_.Version -eq $RequiredVersion) } $versionMessage = $RequiredVersion } elseif ($MinimumVersion) { [ScriptBlock] $Query = { ($_.Name -eq $Name) -and ($_.Version -ge $MinimumVersion) } $versionMessage = "min ${MinimumVersion}" } else { [ScriptBlock] $Query = { $_.Name -eq $Name } $versionMessage = 'any version' } # Is the module installed? if ($installedModules.Where($Query).Count -eq 0) { Write-LabMessage -Message ($LocalizedData.ModuleNotInstalledMessage ` -f $Name, $versionMessage) # If a URL was specified, download this module via HTTP if ($URL) { # The module is not installed - so download it # This is usually for downloading modules directly from github Write-LabMessage -Message ($LocalizedData.DownloadingLabResourceWebMessage ` -f $Name, $versionMessage, $URL) $modulesFolder = "$($ENV:ProgramFiles)\WindowsPowerShell\Modules\" Invoke-LabDownloadAndUnzipFile ` -URL $URL ` -DestinationPath $modulesFolder ` -ErrorAction Stop if ($Folder) { # This zip file contains a folder that is not the name of the module so it must be # renamed. This is usually the case with source downloaded directly from GitHub $modulePath = Join-Path -Path $modulesFolder -ChildPath $Name if (Test-Path -Path $modulePath) { Remove-Item -Path $modulePath -Recurse -Force } Rename-Item ` -Path (Join-Path -Path $modulesFolder -ChildPath $Folder) ` -NewName $Name ` -Force } # if Write-LabMessage -Message ($LocalizedData.InstalledLabResourceWebMessage ` -f $Name, $versionMessage, $modulePath) } else { # Install the package via PowerShellGet from the PowerShellGallery # Make sure the Nuget Package provider is initialized. $null = Get-PackageProvider ` -name nuget ` -ForceBootStrap ` -Force # Make sure PSGallery is trusted Set-PSRepository ` -Name PSGallery ` -InstallationPolicy Trusted # Install the module $installModuleParameters = [PSObject] @{ Name = $Name } if ($RequiredVersion) { # Is a specific module version required? $installModuleParameters += [PSObject] @{ RequiredVersion = $RequiredVersion } } elseif ($MinimumVersion) { # Is a specific module version minimum version? $installModuleParameters += [PSObject] @{ MinimumVersion = $MinimumVersion } } try { Install-Module @installModuleParameters -Force -ErrorAction Stop } catch { $exceptionParameters = @{ errorId = 'ModuleNotAvailableError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.ModuleNotAvailableError ` -f $Name, $versionMessage, $_.Exception.Message) } New-LabException @exceptionParameters } } # If } # If } <# .SYNOPSIS Generates a credential object from a username and password. #> function New-LabCredential() { [CmdletBinding()] [OutputType([PSCredential])] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Username, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Password ) $credential = New-Object ` -TypeName System.Management.Automation.PSCredential ` -ArgumentList ($Username, (ConvertTo-SecureString $Password -AsPlainText -Force)) return $credential } <# .SYNOPSIS Throws a custom exception. .DESCRIPTION This cmdlet throws a terminating or non-terminating exception. .PARAMETER errorId The Id of the exception. .PARAMETER errorCategory The category of the exception. It must be a valid [System.Management.Automation.ErrorCategory] value. .PARAMETER errorMessage The exception message. .PARAMETER terminate This switch will cause the exception to terminate the cmdlet. .EXAMPLE $exceptionParameters = @{ errorId = 'ConnectionFailure' errorCategory = 'ConnectionError' errorMessage = 'Could not connect' } New-LabException @exceptionParameters Throw a ConnectionError exception with the message 'Could not connect'. .OUTPUTS None #> function New-LabException { [CmdLetBinding()] param ( [Parameter(Mandatory = $true)] [System.String] $ErrorId, [Parameter(Mandatory = $true)] [System.Management.Automation.ErrorCategory] $ErrorCategory, [Parameter(Mandatory = $true)] [System.String] $ErrorMessage, [Switch] $Terminate ) $exception = New-Object -TypeName System.Exception ` -ArgumentList $errorMessage $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord ` -ArgumentList $exception, $errorId, $errorCategory, $null if ($Terminate) { # This is a terminating exception. throw $errorRecord } else { # Note: Although this method is called ThrowTerminatingError, it doesn't terminate. $PSCmdlet.ThrowTerminatingError($errorRecord) } } <# .SYNOPSIS Generate a new credential encryption certificate on the Host for a VM. .DESCRIPTION This function will create a new self-signed certificate on the host that can be uploaded to the VM that it is created for. The certificate will be created in the LabBuilder files folder for the specified VM. .PARAMETER Lab Contains the Lab object that was produced by the Get-Lab cmdlet. .PARAMETER VM A LabVM object pulled from the Lab Configuration file using Get-LabVM .EXAMPLE $Lab = Get-Lab -ConfigPath c:\mylab\config.xml $VMs = Get-LabVM -Lab $Lab New-LabHostSelfSignedCertificate -Lab $Lab -VM $VMs[0] Causes a new self-signed certificate for the VM and stores it to the Labbuilder files folder of th VM. .OUTPUTS The path to the certificate file that was created. #> function New-LabHostSelfSignedCertificate { [CmdLetBinding()] [OutputType([System.IO.FileInfo])] param ( [Parameter(Mandatory = $true)] $Lab, [Parameter(Mandatory = $true)] [LabVM] $VM ) # Get Path to LabBuilder files $vmLabBuilderFiles = $VM.LabBuilderFilesPath $certificateFriendlyName = $Script:DSCCertificateFriendlyName $certificateSubject = "CN=$($VM.ComputerName)" # Create the self-signed certificate for the destination VM . $Script:SupportGertGenPath New-SelfsignedCertificateEx ` -Subject $certificateSubject ` -EKU 'Document Encryption','Server Authentication','Client Authentication' ` -KeyUsage 'DigitalSignature, KeyEncipherment, DataEncipherment' ` -SAN $VM.ComputerName ` -FriendlyName $certificateFriendlyName ` -Exportable ` -StoreLocation 'LocalMachine' ` -StoreName 'My' ` -KeyLength $Script:SelfSignedCertKeyLength ` -ProviderName $Script:SelfSignedCertProviderName ` -AlgorithmName $Script:SelfSignedCertAlgorithmName ` -SignatureAlgorithm $Script:SelfSignedCertSignatureAlgorithm ` -ErrorAction Stop # Locate the newly created certificate $certificate = Get-ChildItem -Path cert:\LocalMachine\My ` | Where-Object { ($_.FriendlyName -eq $certificateFriendlyName) ` -and ($_.Subject -eq $certificateSubject) } | Select-Object -First 1 # Export the certificate with the Private key in # preparation for upload to the VM $certificatePassword = ConvertTo-SecureString ` -String $Script:DSCCertificatePassword ` -Force ` -AsPlainText $certificatePfxDestination = Join-Path ` -Path $vmLabBuilderFiles ` -ChildPath $Script:DSCEncryptionPfxCert $null = Export-PfxCertificate ` -FilePath $certificatePfxDestination ` -Cert $certificate ` -Password $certificatePassword ` -ErrorAction Stop # Export the certificate without a private key $certificateDestination = Join-Path ` -Path $vmLabBuilderFiles ` -ChildPath $Script:DSCEncryptionCert $null = Export-Certificate ` -Type CERT ` -FilePath $certificateDestination ` -Cert $certificate ` -ErrorAction Stop # Remove the certificate from the Local Machine store $certificate | Remove-Item return (Get-Item -Path $certificateDestination) } <# .SYNOPSIS Prepares the the files for initializing a new VM. .DESCRIPTION This function creates the following files in the LabBuilder Files for the a VM in preparation for them to be applied to the VM VHD before it is booted up for the first time: 1. Unattend.xml - a Windows Unattend.xml file. 2. SetupComplete.cmd - the command file that gets run after the Windows OOBE is complete. 3. SetupComplete.ps1 - this PowerShell script file that is run at the the end of the SetupComplete.cmd. .PARAMETER Lab Contains the Lab object that was produced by the Get-Lab cmdlet. .PARAMETER VM A LabVM object pulled from the Lab Configuration file using Get-LabVM .EXAMPLE $Lab = Get-Lab -ConfigPath c:\mylab\config.xml $VMs = Get-LabVM -Lab $Lab New-LabVMInitializationFile -Lab $Lab -VM $VMs[0] Prepare the first VM in the Lab c:\mylab\config.xml for initial boot. .OUTPUTS None. #> function New-LabVMInitializationFile { [CmdLetBinding()] param ( [Parameter(Mandatory = $true)] $Lab, [Parameter(Mandatory = $true)] [LabVM] $VM ) # Get Path to LabBuilder files $vmLabBuilderFiles = $VM.LabBuilderFilesPath # Generate an unattended setup file $unattendFile = Get-LabUnattendFileContent ` -Lab $Lab ` -VM $VM $null = Set-Content ` -Path (Join-Path -Path $vmLabBuilderFiles -ChildPath 'Unattend.xml') ` -Value $unattendFile -Force # Assemble the SetupComplete.* scripts. $setupCompleteCmd = '' # Write out the PS1 Setup Complete File if ($VM.OSType -eq [LabOSType]::Nano) { # For a Nano Server we also need to create the certificates # to upload to it (because it Nano Server can't generate them) $null = New-LabHostSelfSignedCertificate ` -Lab $Lab ` -VM $VM # PowerShell currently can't find any basic Cmdlets when executed by # SetupComplete.cmd during the initialization phase, so create an empty # a SetupComplete.ps1 $setupCompletePs = '' } else { if ($VM.CertificateSource -eq [LabCertificateSource]::Host) { # Generate the PFX certificate on the host $null = New-LabHostSelfSignedCertificate ` -Lab $Lab ` -VM $VM } $getCertPs = Get-LabCertificatePsFileContent ` -Lab $Lab ` -VM $VM $setupCompletePs = @" Add-Content `` -Path "C:\WINDOWS\Setup\Scripts\SetupComplete.log" `` -Value 'SetupComplete.ps1 Script Started...' `` -Encoding Ascii Start-Sleep -Seconds 30 $getCertPs Add-Content `` -Path `"`$(`$ENV:SystemRoot)\Setup\Scripts\SetupComplete.log`" `` -Value 'Certificate identified and saved to C:\Windows\$Script:DSCEncryptionCert ...' `` -Encoding Ascii Enable-PSRemoting -SkipNetworkProfileCheck -Force Add-Content `` -Path `"`$(`$ENV:SystemRoot)\Setup\Scripts\SetupComplete.log`" `` -Value 'Windows Remoting Enabled ...' `` -Encoding Ascii "@ } # if if ($VM.SetupComplete) { $setupComplete = $VM.SetupComplete if (-not (Test-Path -Path $setupComplete)) { $exceptionParameters = @{ errorId = 'SetupCompleteScriptMissingError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.SetupCompleteScriptMissingError ` -f $VM.name,$setupComplete) } New-LabException @exceptionParameters } $extension = [System.IO.Path]::GetExtension($setupComplete) switch ($extension.ToLower()) { '.ps1' { $setupCompletePs += Get-Content -Path $setupComplete Break } # 'ps1' '.cmd' { $setupCompleteCmd += Get-Content -Path $setupComplete Break } # 'cmd' } # Switch } # If # Write out the CMD Setup Complete File if ($VM.OSType -eq [LabOSType]::Nano) { $setupCompleteCmd = @" @echo SetupComplete.cmd Script Started... >> %SYSTEMROOT%\Setup\Scripts\SetupComplete.log $setupCompleteCmd certoc.exe -ImportPFX -p $Script:DSCCertificatePassword root $ENV:SystemRoot\$Script:DSCEncryptionPfxCert >> %SYSTEMROOT%\Setup\Scripts\SetupComplete.log @echo SetupComplete.cmd Script Finished... >> %SYSTEMROOT%\Setup\Scripts\SetupComplete.log @echo Initial Setup Completed - this file indicates that setup has completed. >> %SYSTEMROOT%\Setup\Scripts\InitialSetupCompleted.txt "@ } else { $setupCompleteCmd = @" @echo SetupComplete.cmd Script Started... >> %SYSTEMROOT%\Setup\Scripts\SetupComplete.log`r $setupCompleteCmd @echo SetupComplete.cmd Execute SetupComplete.ps1... >> %SYSTEMROOT%\Setup\Scripts\SetupComplete.log`r powerShell.exe -ExecutionPolicy Unrestricted -Command `"%SYSTEMROOT%\Setup\Scripts\SetupComplete.ps1`" `r @echo SetupComplete.cmd Script Finished... >> %SYSTEMROOT%\Setup\Scripts\SetupComplete.log @echo Initial Setup Completed - this file indicates that setup has completed. >> %SYSTEMROOT%\Setup\Scripts\InitialSetupCompleted.txt "@ } $null = Set-Content ` -Path (Join-Path -Path $vmLabBuilderFiles -ChildPath 'SetupComplete.cmd') ` -Value $setupCompleteCmd -Force # Write out the PowerShell Setup Complete file $setupCompletePs = @" Add-Content `` -Path `"$($ENV:SystemRoot)\Setup\Scripts\SetupComplete.log`" `` -Value 'SetupComplete.ps1 Script Started...' `` -Encoding Ascii $setupCompletePs Add-Content `` -Path `"$($ENV:SystemRoot)\Setup\Scripts\SetupComplete.log`" `` -Value 'SetupComplete.ps1 Script Finished...' `` -Encoding Ascii "@ $null = Set-Content ` -Path (Join-Path -Path $vmLabBuilderFiles -ChildPath 'SetupComplete.ps1') ` -Value $setupCompletePs -Force # If ODJ file specified copy it to the labuilder path. if ($VM.OSType -eq [LabOSType]::Nano ` -and -not [System.String]::IsNullOrWhiteSpace($VM.NanoODJPath)) { if ([System.IO.Path]::IsPathRooted($VM.NanoODJPath)) { $nanoODJPath = $VM.NanoODJPath } else { $nanoODJPath = Join-Path ` -Path $Lab.labbuilderconfig.settings.fullconfigpath ` -ChildPath $VM.NanoODJPath } # if $null = Copy-Item ` -Path (Join-Path -Path $nanoODJPath -ChildPath "$($VM.ComputerName).txt") ` -Destination $vmLabBuilderFiles ` -ErrorAction Stop } # if Write-LabMessage -Message $($LocalizedData.CreatedVMInitializationFiles ` -f $VM.Name) } <# .SYNOPSIS Download the existing self-signed certificate from a running VM. .DESCRIPTION This function uses PS Remoting to connect to a running VM and download the an existing Self-Signed certificate file that was written to the c:\windows folder of the guest operating system by the SetupComplete.ps1 script on the. The certificate will be downloaded to the VM's Labbuilder files folder. .PARAMETER Lab Contains the Lab object that was produced by the Get-Lab cmdlet. .PARAMETER VM A LabVM object pulled from the Lab Configuration file using Get-LabVM .PARAMETER Timeout The maximum amount of time that this function can take to download the certificate. If the timeout is reached before the process is complete an error will be thrown. The timeout defaults to 300 seconds. .EXAMPLE $Lab = Get-Lab -ConfigPath c:\mylab\config.xml $VMs = Get-LabVM -Lab $Lab Recieve-LabSelfSignedCertificate -Lab $Lab -VM $VMs[0] Downloads the existing Self-signed certificate for the VM to the Labbuilder files folder of the VM. .OUTPUTS The path to the certificate file that was downloaded. #> function Recieve-LabSelfSignedCertificate { [CmdLetBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory = $true)] $Lab, [Parameter(Mandatory = $true)] [LabVM] $VM, [Parameter()] [System.Int32] $Timeout = 300 ) $startTime = Get-Date $session = $null $complete = $false # Get Path to LabBuilder files $vmLabBuilderFiles = $VM.LabBuilderFilesPath while ((-not $complete) ` -and (((Get-Date) - $startTime).TotalSeconds) -lt $TimeOut) { $session = Connect-LabVM ` -VM $VM ` -ErrorAction Continue # Failed to connnect to the VM if (-not $session) { $exceptionParameters = @{ errorId = 'CertificateDownloadError' errorCategory = 'OperationTimeout' errorMessage = $($LocalizedData.CertificateDownloadError ` -f $VM.Name) } New-LabException @exceptionParameters return } # if if (($session) ` -and ($session.State -eq 'Opened') ` -and (-not $complete)) { # We connected OK - download the Certificate file while ((-not $complete) ` -and (((Get-Date) - $startTime).TotalSeconds) -lt $TimeOut) { try { $null = Copy-Item ` -Path "c:\windows\$Script:DSCEncryptionCert" ` -Destination $vmLabBuilderFiles ` -FromSession $session ` -ErrorAction Stop $complete = $true } catch { Write-LabMessage -Message $($LocalizedData.WaitingForCertificateMessage ` -f $VM.Name,$Script:RetryConnectSeconds) Start-Sleep -Seconds $Script:RetryConnectSeconds } # try } # while } # if # If the copy didn't complete and we're out of time throw an exception if ((-not $complete) ` -and (((Get-Date) - $startTime).TotalSeconds) -ge $TimeOut) { # Disconnect from the VM Disconnect-LabVM ` -VM $VM ` -ErrorAction Continue $exceptionParameters = @{ errorId = 'CertificateDownloadError' errorCategory = 'OperationTimeout' errorMessage = $($LocalizedData.CertificateDownloadError ` -f $VM.Name) } New-LabException @exceptionParameters } # if # Close the Session if it is opened and the download is complete if (($session) ` -and ($session.State -eq 'Opened') ` -and ($complete)) { # Disconnect from the VM Disconnect-LabVM ` -VM $VM ` -ErrorAction Continue } # if } # while return (Get-Item -Path "$vmLabBuilderFiles\$($Script:DSCEncryptionCert)") } <# .SYNOPSIS Ensures the Package Sources required by LabBuilder are registered. .DESCRIPTION This function will check that both the NuGet.org and the PSGallery package sources are registered. If either of them are missing the function will attempt to register them. .EXAMPLE Register-LabPackageSource Ensures the required Package Sources for LabBuilder are required. .OUTPUTS None #> function Register-LabPackageSource { [CmdLetBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param ( [Parameter()] [Switch] $Force ) $requiredPackageSources = @( @{ Name = 'nuget.org' ProviderName = 'NuGet' Location = 'https://www.nuget.org/api/v2/' }, @{ Name = 'PSGallery' ProviderName = 'PowerShellGet' Location = 'https://www.powershellgallery.com/api/v2/' } ) $currentPackageSources = Get-PackageSource -ErrorAction Stop foreach ($requiredPackageSource in $requiredPackageSources) { $packageSource = $currentPackageSources | Where-Object -FilterScript { $_.Name -eq $requiredPackageSource.Name } if ($packageSource) { if (-not $packageSource.IsTrusted) { if ($Force -or $PSCmdlet.ShouldProcess( 'Localhost', ` ($LocalizedData.ShouldTrustPackageSource ` -f $requiredPackageSource.Name, $requiredPackageSource.Location ))) { # The Package source is not trusted so trust it Write-LabMessage -Message ($LocalizedData.RegisterPackageSourceMessage ` -f $requiredPackageSource.Name, $requiredPackageSource.Location) $null = Set-PackageSource ` -Name $requiredPackageSource.Name ` -Trusted ` -Force ` -ErrorAction Stop } else { # Can't continue if the package source is not trusted. $exceptionParameters = @{ errorId = 'PackageSourceNotTrustedError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.PackageSourceNotTrustedError ` -f $requiredPackageSource.Name) } New-LabException @exceptionParameters } # if } # if } else { # The Package source is not registered so register it if ($Force -or $PSCmdlet.ShouldProcess( 'Localhost', ` ($LocalizedData.ShouldRegisterPackageSource ` -f $requiredPackageSource.Name, $requiredPackageSource.Location ))) { Write-LabMessage -Message ($LocalizedData.RegisterPackageSourceMessage ` -f $requiredPackageSource.Name, $requiredPackageSource.Location) $null = Register-PackageSource ` -Name $requiredPackageSource.Name ` -Location $requiredPackageSource.Location ` -ProviderName $requiredPackageSource.ProviderName ` -Trusted ` -Force ` -ErrorAction Stop } else { # Can't continue if the package source is not registered. $exceptionParameters = @{ errorId = 'PackageSourceNotRegisteredError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.PackageSourceNotRegisteredError ` -f $requiredPackageSource.Name) } New-LabException @exceptionParameters } # if } # if } # foreach } <# .SYNOPSIS Generate and download a new credential encryption certificate from a running VM. .DESCRIPTION This function uses PS Remoting to connect to a running VM and upload the GetDSCEncryptionCert.ps1 script and then run it. This wil create a new self-signed certificate that is written to the c:\windows folder of the guest operating system. The certificate will be downloaded to the VM's Labbuilder files folder. .PARAMETER Lab Contains the Lab object that was produced by the Get-Lab cmdlet. .PARAMETER VM A LabVM object pulled from the Lab Configuration file using Get-LabVM .PARAMETER Timeout The maximum amount of time that this function can take to download the certificate. If the timeout is reached before the process is complete an error will be thrown. The timeout defaults to 300 seconds. .EXAMPLE $Lab = Get-Lab -ConfigPath c:\mylab\config.xml $VMs = Get-LabVM -Lab $Lab Request-LabSelfSignedCertificate -Lab $Lab -VM $VMs[0] Causes a new self-signed certificate on the VM and download it to the Labbuilder files folder of th VM. .OUTPUTS The path to the certificate file that was downloaded. #> function Request-LabSelfSignedCertificate { [CmdLetBinding()] [OutputType([System.IO.FileInfo])] param ( [Parameter(Mandatory = $true)] $Lab, [Parameter(Mandatory = $true)] [LabVM] $VM, [Parameter()] [System.Int32] $Timeout = 300 ) $startTime = Get-Date $session = $null $complete = $false # Get Path to LabBuilder files $vmLabBuilderFiles = $VM.LabBuilderFilesPath # Ensure the certificate generation script has been created $getCertPs = Get-LabCertificatePsFileContent ` -Lab $Lab ` -VM $VM ` -CertificateSource Guest $null = Set-Content ` -Path "$VMLabBuilderFiles\GetDSCEncryptionCert.ps1" ` -Value $getCertPs ` -Force while ((-not $complete) ` -and (((Get-Date) - $startTime).TotalSeconds) -lt $TimeOut) { $session = Connect-LabVM ` -VM $VM ` -ErrorAction Continue # Failed to connnect to the VM if (-not $session) { $exceptionParameters = @{ errorId = 'CertificateDownloadError' errorCategory = 'OperationTimeout' errorMessage = $($LocalizedData.CertificateDownloadError ` -f $VM.Name) } New-LabException @exceptionParameters return } # if $complete = $false if (($session) ` -and ($session.State -eq 'Opened') ` -and (-not $complete)) { # We connected OK - Upload the script while ((-not $complete) ` -and (((Get-Date) - $startTime).TotalSeconds) -lt $TimeOut) { try { Copy-Item ` -Path "$VMLabBuilderFiles\GetDSCEncryptionCert.ps1" ` -Destination 'c:\windows\setup\scripts\' ` -ToSession $session ` -Force ` -ErrorAction Stop $complete = $true } catch { Write-LabMessage -Message $($LocalizedData.FailedToUploadCertificateCreateScriptMessage ` -f $VM.Name,$Script:RetryConnectSeconds) Start-Sleep -Seconds $Script:RetryConnectSeconds } # try } # while } # if $complete = $false if (($session) ` -and ($session.State -eq 'Opened') ` -and (-not $complete)) { # Script uploaded, run it while ((-not $complete) ` -and (((Get-Date) - $startTime).TotalSeconds) -lt $TimeOut) { try { Invoke-Command -Session $session -ScriptBlock { C:\Windows\Setup\Scripts\GetDSCEncryptionCert.ps1 } $complete = $true } catch { Write-LabMessage -Message $($LocalizedData.FailedToExecuteCertificateCreateScriptMessage ` -f $VM.Name,$Script:RetryConnectSeconds) Start-Sleep -Seconds $Script:RetryConnectSeconds } # try } # while } # if $complete = $false if (($session) ` -and ($session.State -eq 'Opened') ` -and (-not $complete)) { # Now download the Certificate while ((-not $complete) ` -and (((Get-Date) - $startTime).TotalSeconds) -lt $TimeOut) { try { $null = Copy-Item ` -Path "c:\windows\$($Script:DSCEncryptionCert)" ` -Destination $vmLabBuilderFiles ` -FromSession $session ` -ErrorAction Stop $complete = $true } catch { Write-LabMessage -Message $($LocalizedData.FailedToDownloadCertificateMessage ` -f $VM.Name,$Script:RetryConnectSeconds) Start-Sleep -Seconds $Script:RetryConnectSeconds } # Try } # While } # If # If the process didn't complete and we're out of time throw an exception if ((-not $complete) ` -and (((Get-Date) - $startTime).TotalSeconds) -ge $TimeOut) { if ($session) { Remove-PSSession -Session $session } $exceptionParameters = @{ errorId = 'CertificateDownloadError' errorCategory = 'OperationTimeout' errorMessage = $($LocalizedData.CertificateDownloadError ` -f $VM.Name) } New-LabException @exceptionParameters } # Close the Session if it is opened and the download is complete if (($session) ` -and ($session.State -eq 'Opened') ` -and ($complete)) { Remove-PSSession -Session $session } # If } # While return (Get-Item -Path "$vmLabBuilderFiles\$($Script:DSCEncryptionCert)") } <# .SYNOPSIS This function prepares the PowerShell scripts used for starting up DSC on a VM. .DESCRIPTION Two PowerShell scripts will be created by this function in the LabBuilder Files folder of the VM: 1. StartDSC.ps1 - the script that is called automatically to start up DSC. 2. StartDSCDebug.ps1 - a debug script that will start up DSC in debug mode. These scripts will contain code to perform the following operations: 1. Configure the names of the Network Adapters so that they will match the names in the DSC Configuration files. 2. Enable/Disable DSC Event Logging. 3. Apply Configuration to the Local Configuration Manager. 4. Start DSC. .PARAMETER Lab Contains the Lab object that was produced by the Get-Lab cmdlet. .PARAMETER VM A LabVM object pulled from the Lab Configuration file using Get-LabVM. .EXAMPLE $Lab = Get-Lab -ConfigPath c:\mylab\config.xml $VMs = Get-LabVM -Lab $Lab Set-LabDSC -Lab $Lab -VM $VMs[0] Prepare the first VM in the Lab c:\mylab\config.xml for DSC start up. .OUTPUTS None. #> function Set-LabDSC { [CmdLetBinding()] param ( [Parameter(Mandatory = $true)] $Lab, [Parameter(Mandatory = $true)] [LabVM] $VM ) $dscStartPs = '' # Get Path to LabBuilder files $vmLabBuilderFiles = $VM.LabBuilderFilesPath <# Relabel the Network Adapters so that they match what the DSC Networking config will use This is because unfortunately the Hyper-V Device Naming feature doesn't work. #> $managementSwitchName = Get-LabManagementSwitchName -Lab $Lab $adapters = [System.String[]] ($VM.Adapters).Name $adapters += @($managementSwitchName) foreach ($adapter in $adapters) { $netAdapter = Get-VMNetworkAdapter -VMName $($VM.Name) -Name $adapter if (-not $netAdapter) { $exceptionParameters = @{ errorId = 'NetworkAdapterNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.NetworkAdapterNotFoundError ` -f $adapter, $VM.Name) } New-LabException @exceptionParameters } # if $macAddress = $netAdapter.MacAddress if (-not $macAddress) { $exceptionParameters = @{ errorId = 'NetworkAdapterBlankMacError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.NetworkAdapterBlankMacError ` -f $adapter, $VM.Name) } New-LabException @exceptionParameters } # If $dscStartPs += @" Get-NetAdapter `` | Where-Object { `$_.MacAddress.Replace('-','') -eq '$macAddress' } `` | Rename-NetAdapter -NewName '$($adapter)' "@ } # Foreach <# Enable DSC logging (as long as it hasn't been already) Nano Server doesn't have the Microsoft-Windows-Dsc/Analytic channels so Logging can't be enabled. #> if ($VM.OSType -ne [LabOSType]::Nano) { $logging = ($VM.DSC.Logging).ToString() $dscStartPs += @" `$Result = & "wevtutil.exe" get-log "Microsoft-Windows-Dsc/Analytic" if (-not (`$Result -like '*enabled: true*')) { & "wevtutil.exe" set-log "Microsoft-Windows-Dsc/Analytic" /q:true /e:$logging } `$Result = & "wevtutil.exe" get-log "Microsoft-Windows-Dsc/Debug" if (-not (`$Result -like '*enabled: true*')) { & "wevtutil.exe" set-log "Microsoft-Windows-Dsc/Debug" /q:true /e:$logging } "@ } # if # Start the actual DSC Configuration $dscStartPs += @" Set-DscLocalConfigurationManager `` -Path `"`$(`$ENV:SystemRoot)\Setup\Scripts\`" `` -Verbose *>> `"`$(`$ENV:SystemRoot)\Setup\Scripts\DSC.log`" Start-DSCConfiguration `` -Path `"`$(`$ENV:SystemRoot)\Setup\Scripts\`" `` -Force `` -Verbose *>> `"`$(`$ENV:SystemRoot)\Setup\Scripts\DSC.log`" "@ $null = Set-Content ` -Path (Join-Path -Path $vmLabBuilderFiles -ChildPath 'StartDSC.ps1') ` -Value $dscStartPs -Force $dscStartPsDebug = @" param ( [System.Boolean] `$WaitForDebugger ) Set-DscLocalConfigurationManager `` -Path `"`$(`$ENV:SystemRoot)\Setup\Scripts\`" `` -Verbose if (`$WaitForDebugger) { Enable-DscDebug `` -BreakAll } Start-DSCConfiguration `` -Path `"`$(`$ENV:SystemRoot)\Setup\Scripts\`" `` -Force `` -Debug `` -Wait `` -Verbose if (`$WaitForDebugger) { Disable-DscDebug } "@ $null = Set-Content ` -Path (Join-Path -Path $vmLabBuilderFiles -ChildPath 'StartDSCDebug.ps1') ` -Value $dscStartPsDebug -Force } <# .SYNOPSIS Sets the Modules Resources that should be imported in a DSC Config. .DESCRIPTION It will completely replace the list of Imported DSCResources with this new list. .PARAMETER DscConfigFile Contains the path to the DSC Config file to set resource module names in. .PARAMETER DscConfigContent Contains the content of the DSC Config to set resource module names in. .PARAMETER Modules Contains an array of LabDSCModule objects to replace set in the Configuration. .EXAMPLE Set-LabModulesInDSCConfig -DscConfigFile c:\mydsc\Server01.ps1 -Modules $Modules Set the DSC Resource module in the content from file c:\mydsc\server01.ps1 .EXAMPLE Set-LabModulesInDSCConfig -DscConfigContent $DSCConfig -Modules $Modules Set the DSC Resource module in the content $DSCConfig .OUTPUTS A string containing the content of the DSC Config file with the updated module names in it. #> function Set-LabModulesInDSCConfig { [CmdLetBinding(DefaultParameterSetName = "Content")] [OutputType([System.String])] param ( [parameter( Position = 1, ParameterSetName = "Content", Mandatory = $true)] [System.String] $DscConfigContent, [parameter( Position = 2, ParameterSetName = "File", Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $DscConfigFile, [parameter( Position = 3, Mandatory = $true)] [ValidateNotNullOrEmpty()] [LabDSCModule[]] $Modules ) if ($PSCmdlet.ParameterSetName -eq 'File') { $DscConfigContent = Get-Content -Path $DscConfigFile -Raw } # if $regex = "[ \t]*?Import\-DscResource[ \t]+(?:\-ModuleName[ \t]+)?'?`"?([A-Za-z0-9._-]+)`"?'?([ \t]+(-ModuleVersion[ \t]+)?'?`"?([0-9.]+)`"?'?)?[ \t]*[\r\n]+" $moduleMatches = [regex]::matches($DscConfigContent, $regex, 'IgnoreCase') foreach ($module in $Modules) { $importCommand = "Import-DscResource -ModuleName '$($module.ModuleName)'" if ($module.ModuleVersion) { $importCommand = "$importCommand -ModuleVersion '$($module.ModuleVersion)'" } # if $importCommand = " $importCommand`r`n" # is this module already in there? $found = $false foreach ($moduleMatch in $moduleMatches) { if ($moduleMatch.Groups[1].Value -eq $module.ModuleName) { # Found the module - so replace it $DscConfigContent = ("{0}{1}{2}" -f ` $DscConfigContent.Substring(0, $moduleMatch.Index), ` $importCommand, ` $DscConfigContent.Substring($moduleMatch.Index + $moduleMatch.Length)) $moduleMatches = [regex]::matches($DscConfigContent, $regex, 'IgnoreCase') $found = $true break } # if } # foreach if (-not $found) { if ($moduleMatches.Count -gt 0) { # Add this to the end of the existing Import-DSCResource lines $moduleMatch = $moduleMatches[$moduleMatches.count - 1] } else { # There are no existing DSC Resource lines, so add it after # Configuration ... { line $moduleMatch = [regex]::matches($DscConfigContent, "[ \t]*?Configuration[ \t]+?'?`"?[A-Za-z0-9._-]+`"?'?[ \t]*?[\r\n]*?{[\r\n]*?", 'IgnoreCase') if (-not $moduleMatch) { $exceptionParameters = @{ errorId = 'DSCConfiguartionMissingError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.DSCConfiguartionMissingError) } New-LabException @exceptionParameters } } # if $DscConfigContent = ("{0}{1}{2}" -f ` $DscConfigContent.Substring(0, $moduleMatch.Index + $moduleMatch.Length), ` $importCommand, ` $DscConfigContent.Substring($moduleMatch.Index + $moduleMatch.Length)) $moduleMatches = [regex]::matches($DscConfigContent, $regex, 'IgnoreCase') } # Module not found so add it to the end } # foreach return $DscConfigContent } <# .SYNOPSIS Ensures that the virtual adapter is attached to a Virtual Switch and configured correctly. .DESCRIPTION This function is used to add or update the specified virtual network adapter that is used by the Management OS to connect to the specifed virtual switch. .PARAMETER Name Contains the name of the virtual network adapter to add. .PARAMETER SwitchName Contains the name of the virtual switch to connect this adapter to. .PARAMETER ManagementOS Whether or not this adapter is attached to the Management OS. .PARAMETER StaticMacAddress This optional parameter contains the static MAC address to assign to the virtual network adapter. .PARAMETER VlanId This optional parameter contains the VLan Id to assign to this network adapter. .EXAMPLE Set-LabSwitchAdapter -Name 'Domain Nat SMB' -SwitchName 'Domain Nat' -VlanId 25 .OUTPUTS None. #> function Set-LabSwitchAdapter { [CmdLetBinding()] param ( [Parameter(Mandatory = $true)] [System.String] $Name, [Parameter(Mandatory = $true)] [System.String] $SwitchName, [Parameter()] [Switch] $ManagementOS, [Parameter()] [System.String] $StaticMacAddress, [Parameter()] [AllowNull()] [Nullable[System.Int32]] $VlanId ) # Determine if we should set the MAC address and VLan Id $setVlanId = $PSBoundParameters.ContainsKey('VlanId') $setMacAddress = $PSBoundParameters.ContainsKey('StaticMacAddress') # Remove VlanId Parameter so this can be splatted $null = $PSBoundParameters.Remove('VlanId') $null = $PSBoundParameters.Remove('StaticMacAddress') $existingManagementAdapter = Get-VMNetworkAdapter ` @PSBoundParameters ` -ErrorAction SilentlyContinue if (-not $existingManagementAdapter) { # Adapter does not exist so add it if ($SetMacAddress) { # For a management adapter a Static MAC address can only be assigned at creation time. if (-not ([System.String]::IsNullOrEmpty($StaticMacAddress))) { $PSBoundParameters.Add('StaticMacAddress', $StaticMacAddress) } } $existingManagementAdapter = Add-VMNetworkAdapter ` @PSBoundParameters ` -Passthru } else { <# The MAC Address for an existing Management Adapter can not be changed This shouldn't ever happen unless the configuration is changed. Not sure of the solution to this problem. #> } # Set or clear the VlanId if ($setVlanId) { $existingManagementAdapterVlan = Get-VMNetworkAdapterVlan ` -VMNetworkAdapter $existingManagementAdapter $existingManagementVlan = $existingManagementAdapterVlan.AccessVlanId if ($null -eq $VlanId) { if ($null -eq $existingManagementVlan) { $setVMNetworkAdapterVlanParameters = @{ VMNetworkAdapter = $existingManagementAdapter Untagged = $true } $null = Set-VMNetworkAdapterVlan ` @setVMNetworkAdapterVlanParameters ` -ErrorAction Stop } } else { if ($VlanId -ne $existingManagementVlan) { $setVMNetworkAdapterVlanParameters = @{ VMNetworkAdapter = $existingManagementAdapter Access = $true VlanId = $VlanId } $null = Set-VMNetworkAdapterVlan ` @setVMNetworkAdapterVlanParameters ` -ErrorAction Stop } } } } <# .SYNOPSIS Uploads prepared Modules and MOF files to a VM and starts up Desired State Configuration (DSC) on it. .DESCRIPTION This function will perform the following tasks: 1. Connect to the VM via remoting. 2. Upload the DSC and LCM MOF files to the c:\windows\setup\scripts folder of the VM. 3. Upload DSC Start up scripts to the c:\windows\setup\scripts folder of the VM. 4. Upload all required modules to the c:\program files\WindowsPowerShell\Modules\ folder of the VM. 5. Invoke the StartDSC.ps1 script on the VM to start DSC processing. .PARAMETER Lab Contains the Lab object that was produced by the Get-Lab cmdlet. .PARAMETER VM A LabVM object pulled from the Lab Configuration file using Get-LabVM. .PARAMETER Timeout The maximum amount of time that this function can take to perform DSC start-up. If the timeout is reached before the process is complete an error will be thrown. The timeout defaults to 300 seconds. .EXAMPLE $Lab = Get-Lab -ConfigPath c:\mylab\config.xml $VMs = Get-LabVM -Lab $Lab Start-LabDSC -Lab $Lab -VM $VMs[0] Starts up Desired State Configuration for the first VM in the Lab c:\mylab\config.xml. .OUTPUTS None. #> function Start-LabDSC { [CmdLetBinding()] param ( [Parameter(Mandatory = $true)] $Lab, [Parameter(Mandatory = $true)] [LabVM] $VM, [Parameter()] [System.Int32] $Timeout = 300 ) $startTime = Get-Date $session = $null $complete = $false $configCopyComplete = $false $moduleCopyComplete = $false # Get Path to LabBuilder files $vmLabBuilderFiles = $VM.LabBuilderFilesPath While ((-not $complete) ` -and (((Get-Date) - $startTime).TotalSeconds) -lt $TimeOut) { # Connect to the VM $session = Connect-LabVM ` -VM $VM ` -ErrorAction Continue # Failed to connnect to the VM if (-not $session) { $exceptionParameters = @{ errorId = 'DSCInitializationError' errorCategory = 'OperationTimeout' errorMessage = $($LocalizedData.DSCInitializationError ` -f $VM.Name) } New-LabException @exceptionParameters return } if (($session) ` -and ($session.State -eq 'Opened') ` -and (-not $configCopyComplete)) { $copyParameters = @{ Destination = 'c:\Windows\Setup\Scripts' ToSession = $session Force = $true ErrorAction = 'Stop' } # Connection has been made OK, upload the DSC files While ((-not $configCopyComplete) ` -and (((Get-Date) - $startTime).TotalSeconds) -lt $TimeOut) { Try { Write-LabMessage -Message $($LocalizedData.CopyingFilesToVMMessage ` -f $VM.Name, 'DSC') $null = Copy-Item ` @copyParameters ` -Path (Join-Path -Path $vmLabBuilderFiles -ChildPath "$($VM.ComputerName).mof") if (Test-Path ` -Path "$vmLabBuilderFiles\$($VM.ComputerName).meta.mof") { $null = Copy-Item ` @copyParameters ` -Path (Join-Path -Path $vmLabBuilderFiles -ChildPath "$($VM.ComputerName).meta.mof") } # If $null = Copy-Item ` @copyParameters ` -Path (Join-Path -Path $vmLabBuilderFiles -ChildPath 'StartDSC.ps1') $null = Copy-Item ` @copyParameters ` -Path (Join-Path -Path $vmLabBuilderFiles -ChildPath 'StartDSCDebug.ps1') $configCopyComplete = $true } catch { Write-LabMessage -Message $($LocalizedData.CopyingFilesToVMFailedMessage ` -f $VM.Name, 'DSC', $Script:RetryConnectSeconds) Start-Sleep -Seconds $Script:RetryConnectSeconds } # try } # while } # if # If the copy didn't complete and we're out of time throw an exception if ((-not $configCopyComplete) ` -and (((Get-Date) - $startTime).TotalSeconds) -ge $TimeOut) { # Disconnect from the VM Disconnect-LabVM ` -VM $VM ` -ErrorAction Continue $exceptionParameters = @{ errorId = 'DSCInitializationError' errorCategory = 'OperationTimeout' errorMessage = $($LocalizedData.DSCInitializationError ` -f $VM.Name) } New-LabException @exceptionParameters } # if # Upload any required modules to the VM if (($session) ` -and ($session.State -eq 'Opened') ` -and (-not $moduleCopyComplete)) { $dscContent = Get-Content ` -Path $($VM.DSC.ConfigFile) ` -Raw [LabDSCModule[]] $dscModules = Get-LabModulesInDSCConfig -DSCConfigContent $dscContent # Add the NetworkingDsc DSC Resource because it is always used $module = [LabDSCModule]::New('NetworkingDsc') $dscModules += @( $module ) foreach ($dscModule in $dscModules) { $moduleName = $dscModule.ModuleName # Upload all but PSDesiredStateConfiguration because it # should always exist on client node. if ($moduleName -ne 'PSDesiredStateConfiguration') { try { Write-LabMessage -Message $($LocalizedData.CopyingFilesToVMMessage ` -f $VM.Name, "DSC Module $moduleName") $null = Copy-Item ` -Path (Join-Path -Path $vmLabBuilderFiles -ChildPath "DSC Modules\$moduleName\") ` -Destination "$($env:ProgramFiles)\WindowsPowerShell\Modules\" ` -ToSession $session ` -Force ` -Recurse ` -ErrorAction Stop } catch { Write-LabMessage -Message $($LocalizedData.CopyingFilesToVMFailedMessage ` -f $VM.Name, "DSC Module $moduleName", $Script:RetryConnectSeconds) Start-Sleep -Seconds $Script:RetryConnectSeconds } # try } # if } # foreach $moduleCopyComplete = $true } # if # If the copy didn't complete and we're out of time throw an exception if ((-not $moduleCopyComplete) ` -and (((Get-Date) - $startTime).TotalSeconds) -ge $TimeOut) { # Disconnect from the VM Disconnect-LabVM ` -VM $VM ` -ErrorAction Continue $exceptionParameters = @{ errorId = 'DSCInitializationError' errorCategory = 'OperationTimeout' errorMessage = $($LocalizedData.DSCInitializationError ` -f $VM.Name) } New-LabException @exceptionParameters } # if # Finally, Start DSC up! if (($session) ` -and ($session.State -eq 'Opened') ` -and ($configCopyComplete) ` -and ($moduleCopyComplete)) { Write-LabMessage -Message $($LocalizedData.StartingDSCMessage ` -f $VM.Name) Invoke-Command -Session $session { c:\windows\setup\scripts\StartDSC.ps1 } # Disconnect from the VM Disconnect-LabVM ` -VM $VM ` -ErrorAction Continue $complete = $true } # if } # while } <# .SYNOPSIS This function prepares all the files and modules necessary for a VM to be configured using Desired State Configuration (DSC). .DESCRIPTION This funcion performs the following tasks in preparation for starting Desired State Configuration on a Virtual Machine: 1. Ensures the folder structure for the Virtual Machine DSC files is available. 2. Gets a list of all Modules required by the DSC configuration to be applied. 3. Download and Install any missing DSC modules required for the DSC configuration. 4. Copy all modules required for the DSC configuration to the VM folder. 5. Cause a self-signed cetficiate to be created and downloaded on the Lab VM. 6. Create a Networking DSC configuration file and ensure the DSC config file calss it. 7. Create the MOF file from the config and an LCM config. .PARAMETER Lab Contains the Lab object that was produced by the Get-Lab cmdlet. .PARAMETER VM A LabVM object pulled from the Lab Configuration file using Get-LabVM. .EXAMPLE $Lab = Get-Lab -ConfigPath c:\mylab\config.xml $VMs = Get-LabVM -Lab $Lab Update-LabDSC -Lab $Lab -VM $VMs[0] Prepare the first VM in the Lab c:\mylab\config.xml for DSC configuration. .OUTPUTS None. #> function Update-LabDSC { [CmdLetBinding()] param ( [Parameter(Mandatory = $true)] $Lab, [Parameter(Mandatory = $true)] [LabVM] $VM ) $dscMOFFile = '' $dscMOFMetaFile = '' # Get Path to LabBuilder files $vmLabBuilderFiles = $VM.LabBuilderFilesPath if (-not $VM.DSC.ConfigFile) { # This VM doesn't have a DSC Configuration return } # Make sure all the modules required to create the MOF file are installed $installedModules = Get-Module -ListAvailable Write-LabMessage -Message $($LocalizedData.DSCConfigIdentifyModulesMessage ` -f $VM.DSC.ConfigFile, $VM.Name) $dscConfigContent = Get-Content ` -Path $($VM.DSC.ConfigFile) ` -Raw [LabDSCModule[]] $dscModules = Get-LabModulesInDSCConfig ` -DSCConfigContent $dscConfigContent # Add the NetworkingDsc DSC Resource because it is always used $module = [LabDSCModule]::New('NetworkingDsc') # It must be 7.0.0.0 or greater $module.MinimumVersion = [Version] '7.0.0.0' $dscModules += @( $module ) foreach ($dscModule in $dscModules) { $moduleName = $dscModule.ModuleName $moduleParameters = @{ Name = $ModuleName } $moduleVersion = $dscModule.ModuleVersion $minimumVersion = $dscModule.MinimumVersion if ($moduleVersion) { $filterScript = { ($_.Name -eq $ModuleName) -and ($moduleVersion -eq $_.Version) } $moduleParameters += @{ RequiredVersion = $moduleVersion } } elseif ($minimumVersion) { $filterScript = { ($_.Name -eq $ModuleName) -and ($_.Version -ge $minimumVersion) } $moduleParameters += @{ MinimumVersion = $minimumVersion } } else { $filterScript = { $_.Name -eq $ModuleName } } $module = ($installedModules | Where-Object -FilterScript $filterScript | Sort-Object -Property Version -Descending | Select-Object -First 1) if ($module) { # The module already exists, load the version number into the Module # to force the version number to be set in the DSC Config file $dscModule.ModuleVersion = $module.Version } else { # The Module isn't available on this computer, so try and install it Write-LabMessage -Message $($LocalizedData.DSCConfigSearchingForModuleMessage ` -f $VM.DSC.ConfigFile, $VM.Name, $ModuleName) $newModule = Find-Module ` @moduleParameters if ($newModule) { Write-LabMessage -Message $($LocalizedData.DSCConfigInstallingModuleMessage ` -f $VM.DSC.ConfigFile, $VM.Name, $ModuleName) try { $newModule | Install-Module } catch { $exceptionParameters = @{ errorId = 'DSCModuleDownloadError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.DSCModuleDownloadError ` -f $VM.DSC.ConfigFile, $VM.Name, $ModuleName) } New-LabException @exceptionParameters } } else { $exceptionParameters = @{ errorId = 'DSCModuleDownloadError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.DSCModuleDownloadError ` -f $VM.DSC.ConfigFile, $VM.Name, $ModuleName) } New-LabException @exceptionParameters } $dscModule.ModuleVersion = $newModule.Version } # if Write-LabMessage -Message $($LocalizedData.DSCConfigSavingModuleMessage ` -f $VM.DSC.ConfigFile, $VM.Name, $ModuleName) # Find where the module is actually stored $modulePath = '' foreach ($Path in $ENV:PSModulePath.Split(';')) { if (-not [System.String]::IsNullOrEmpty($Path)) { $modulePath = Join-Path ` -Path $Path ` -ChildPath $ModuleName if (Test-Path -Path $modulePath) { break } # If } } # Foreach if (-not (Test-Path -Path $modulePath)) { $exceptionParameters = @{ errorId = 'DSCModuleNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.DSCModuleNotFoundError ` -f $VM.DSC.ConfigFile, $VM.Name, $ModuleName) } New-LabException @exceptionParameters } $destinationPath = Join-Path -Path $vmLabBuilderFiles -ChildPath 'DSC Modules\' if (-not (Test-Path -Path $destinationPath)) { # Create the DSC Modules folder if it doesn't exist. $null = New-Item -Path $destinationPath -ItemType Directory -Force } # if Write-LabMessage -Message $($LocalizedData.DSCConfigCopyingModuleMessage ` -f $VM.DSC.ConfigFile, $VM.Name, $ModuleName, $modulePath, $destinationPath) Copy-Item ` -Path $modulePath ` -Destination $destinationPath ` -Recurse ` -Force ` -ErrorAction Continue } # Foreach if ($VM.CertificateSource -eq [LabCertificateSource]::Guest) { # Recreate the certificate if it the source is the Guest if (-not (Request-LabSelfSignedCertificate -Lab $Lab -VM $VM)) { $exceptionParameters = @{ errorId = 'CertificateCreateError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.CertificateCreateError ` -f $VM.Name) } New-LabException @exceptionParameters } # Remove any old self-signed certifcates for this VM Get-ChildItem -Path cert:\LocalMachine\My ` | Where-Object { $_.FriendlyName -eq $Script:DSCCertificateFriendlyName } ` | Remove-Item } # if # Add the VM Self-Signed Certificate to the Local Machine store and get the Thumbprint $certificateFile = Join-Path ` -Path $vmLabBuilderFiles ` -ChildPath $Script:DSCEncryptionCert $certificate = Import-Certificate ` -FilePath $certificateFile ` -CertStoreLocation 'Cert:LocalMachine\My' $certificateThumbprint = $certificate.Thumbprint # Set the predicted MOF File name $dscMOFFile = Join-Path ` -Path $ENV:Temp ` -ChildPath "$($VM.ComputerName).mof" $dscMOFMetaFile = ([System.IO.Path]::ChangeExtension($dscMOFFile, 'meta.mof')) # Generate the LCM MOF File Write-LabMessage -Message $($LocalizedData.DSCConfigCreatingLCMMOFMessage -f $dscMOFMetaFile, $VM.Name) $null = ConfigLCM ` -OutputPath $($ENV:Temp) ` -ComputerName $($VM.ComputerName) ` -Thumbprint $certificateThumbprint if (-not (Test-Path -Path $dscMOFMetaFile)) { $exceptionParameters = @{ errorId = 'DSCConfigMetaMOFCreateError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.DSCConfigMetaMOFCreateError ` -f $VM.Name) } New-LabException @exceptionParameters } # If # A DSC Config File was provided so create a MOF File out of it. Write-LabMessage -Message $($LocalizedData.DSCConfigCreatingMOFMessage -f $VM.DSC.ConfigFile, $VM.Name) # Now create the Networking DSC Config file $dscNetworkingConfig = Get-LabDSCNetworkingConfig ` -Lab $Lab -VM $VM $NetworkingDscFile = Join-Path ` -Path $vmLabBuilderFiles ` -ChildPath 'DSCNetworking.ps1' $null = Set-Content ` -Path $NetworkingDscFile ` -Value $dscNetworkingConfig . $NetworkingDscFile $dscFile = Join-Path ` -Path $vmLabBuilderFiles ` -ChildPath 'DSC.ps1' # Set the Modules List in the DSC Configuration $dscConfigContent = Set-LabModulesInDSCConfig ` -DSCConfigContent $dscConfigContent ` -Modules $dscModules if (-not ($dscConfigContent -match 'Networking Network {}')) { # Add the Networking Configuration item to the base DSC Config File # Find the location of the line containing "Node $AllNodes.NodeName {" [System.String] $Regex = '\s*Node\s.*{.*' $Matches = [regex]::matches($dscConfigContent, $Regex, 'IgnoreCase') if ($Matches.Count -eq 1) { $dscConfigContent = $dscConfigContent.` Insert($Matches[0].Index + $Matches[0].Length, "`r`nNetworking Network {}`r`n") } else { $exceptionParameters = @{ errorId = 'DSCConfigMoreThanOneNodeError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.DSCConfigMoreThanOneNodeError ` -f $VM.DSC.ConfigFile, $VM.Name) } New-LabException @exceptionParameters } # if } # if # Save the DSC Content $null = Set-Content ` -Path $dscFile ` -Value $dscConfigContent ` -Force # Hook the Networking DSC File into the main DSC File . $dscFile $dscConfigName = $VM.DSC.ConfigName Write-LabMessage -Message $($LocalizedData.DSCConfigPrepareMessage -f $dscConfigName, $VM.Name) # Generate the Configuration Nodes data that always gets passed to the DSC configuration. $dscConfigData = @" @{ AllNodes = @( @{ NodeName = '$($VM.ComputerName)' CertificateFile = '$certificateFile' Thumbprint = '$certificateThumbprint' LocalAdminPassword = '$($VM.administratorpassword)' $($VM.DSC.Parameters) } ) } "@ # Write it to a temp file $dscConfigFile = Join-Path ` -Path $vmLabBuilderFiles ` -ChildPath 'DSCConfigData.psd1' if (Test-Path -Path $dscConfigFile) { $null = Remove-Item ` -Path $dscConfigFile ` -Force } $null = Set-Content -Path $dscConfigFile -Value $dscConfigData # Read the config data into a Hash Table $dscConfigData = Import-LocalizedData -BaseDirectory $vmLabBuilderFiles -FileName 'DSCConfigData.psd1' # Generate the MOF file from the configuration $null = & $dscConfigName ` -OutputPath $($ENV:Temp) ` -ConfigurationData $dscConfigData ` -ErrorAction Stop if (-not (Test-Path -Path $dscMOFFile)) { $exceptionParameters = @{ errorId = 'DSCConfigMOFCreateError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.DSCConfigMOFCreateError -f $VM.DSC.ConfigFile, $VM.Name) } New-LabException @exceptionParameters } # If # Remove the VM Self-Signed Certificate from the Local Machine Store $null = Remove-Item ` -Path "Cert:LocalMachine\My\$certificateThumbprint" ` -Force Write-LabMessage -Message $($LocalizedData.DSCConfigMOFCreatedMessage -f $VM.DSC.ConfigFile, $VM.Name) # Copy the files to the LabBuilder Files folder $dscMOFDestinationFile = Join-Path -Path $vmLabBuilderFiles -ChildPath "$($VM.ComputerName).mof" $null = Copy-Item ` -Path $dscMOFFile ` -Destination $dscMOFDestinationFile ` -Force if (-not $VM.DSC.MOFFile) { # Remove Temporary files created by DSC $null = Remove-Item ` -Path $dscMOFFile ` -Force } if (Test-Path -Path $dscMOFMetaFile) { $dscMOFMetaDestinationFile = Join-Path -Path $vmLabBuilderFiles -ChildPath "$($VM.ComputerName).meta.mof" $null = Copy-Item ` -Path $dscMOFMetaFile ` -Destination $dscMOFMetaDestinationFile ` -Force if (-not $VM.DSC.MOFFile) { # Remove Temporary files created by DSC $null = Remove-Item ` -Path $dscMOFMetaFile ` -Force } } # if } <# .SYNOPSIS Updates the VM Data Disks to match the VM Configuration. .DESCRIPTION This cmdlet will take the VM configuration provided and ensure that that data disks that are attached to the VM. The function will use the array of items in the DataVHDs property of the VM to create and attach any data disk VHDs that are missing. If the data disk VHD file exists but is not attached it will be attached to the VM. If the data disk VHD file does not exist then it will be created and attached. .EXAMPLE $Lab = Get-Lab -ConfigPath c:\mylab\config.xml $VMs = Get-LabVM -Lab $Lab Update-LabVMDataDisk -Lab $Lab -VM VM[0] This will update the data disks for the first VM in the configuration file c:\mylab\config.xml. .PARAMETER Lab Contains the Lab object that was produced by the Get-Lab cmdlet. .PARAMETER VM A LabVM object pulled from the Lab Configuration file using Get-LabVM .OUTPUTS None. #> function Update-LabVMDataDisk { [CmdLetBinding()] param ( [Parameter( Mandatory, Position=0)] [ValidateNotNullOrEmpty()] $Lab, [Parameter( Mandatory, Position=1)] [ValidateNotNullOrEmpty()] [LabVM] $VM ) # If there are no data VHDs just return if (-not $VM.DataVHDs) { return } # Get the root path of the VM [System.String] $VMRootPath = $VM.VMRootPath # Get the Virtual Hard Disk Path [System.String] $VHDPath = Join-Path ` -Path $VMRootPath ` -ChildPath 'Virtual Hard Disks' foreach ($DataVhd in @($VM.DataVHDs)) { $Vhd = $DataVhd.Vhd if (Test-Path -Path $Vhd) { Write-LabMessage -Message $($LocalizedData.VMDiskAlreadyExistsMessage ` -f $VM.Name,$Vhd,'Data') # Check the parameters of the VHD match $ExistingVhd = Get-VHD -Path $Vhd # Check the VHD Type if (($DataVhd.VhdType) ` -and ($ExistingVhd.VhdType.ToString() -ne $DataVhd.VhdType.ToString())) { # The type of disk can't be changed. $exceptionParameters = @{ errorId = 'VMDataDiskVHDConvertError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMDataDiskVHDConvertError ` -f $VM.name,$Vhd,$DataVhd.VhdType) } New-LabException @exceptionParameters } # Check the size if ($DataVhd.Size) { if ($ExistingVhd.Size -lt $DataVhd.Size) { # Expand the disk Write-LabMessage -Message $($LocalizedData.ExpandingVMDiskMessage ` -f $VM.Name,$Vhd,'Data',$DataVhd.Size) $null = Resize-VHD ` -Path $Vhd ` -SizeBytes $DataVhd.Size } elseif ($ExistingVhd.Size -gt $DataVhd.Size) { # The disk size can't be reduced. # This could be revisited later. $exceptionParameters = @{ errorId = 'VMDataDiskVHDShrinkError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMDataDiskVHDShrinkError ` -f $VM.name,$Vhd,$DataVhd.Size) } New-LabException @exceptionParameters } # if } # if } else { # The data disk VHD does not exist so create it $SourceVhd = $DataVhd.SourceVhd if ($SourceVhd) { # A source VHD was specified to create the new VHD using if (! (Test-Path -Path $SourceVhd)) { $exceptionParameters = @{ errorId = 'VMDataDiskSourceVHDNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMDataDiskSourceVHDNotFoundError ` -f $VM.name,$SourceVhd) } New-LabException @exceptionParameters } # if # Should the Source VHD be copied or moved if ($DataVhd.MoveSourceVHD) { Write-LabMessage -Message $($LocalizedData.CreatingVMDiskByMovingSourceVHDMessage ` -f $VM.Name,$Vhd,$SourceVhd) $null = Move-Item ` -Path $SourceVhd ` -Destination $VHDPath ` -Force ` -ErrorAction Stop } else { Write-LabMessage -Message $($LocalizedData.CreatingVMDiskByCopyingSourceVHDMessage ` -f $VM.Name,$Vhd,$SourceVhd) $null = Copy-Item ` -Path $SourceVhd ` -Destination $VHDPath ` -Force ` -ErrorAction Stop } # if } else { $Size = $DataVhd.size switch ($DataVhd.VhdType) { 'fixed' { # Create a new Fixed VHD Write-LabMessage -Message $($LocalizedData.CreatingVMDiskMessage ` -f $VM.Name,$Vhd,'Fixed Data') $null = New-VHD ` -Path $Vhd ` -SizeBytes $Size ` -Fixed ` -ErrorAction Stop break; } # 'fixed' 'dynamic' { # Create a new Dynamic VHD Write-LabMessage -Message $($LocalizedData.CreatingVMDiskMessage ` -f $VM.Name,$Vhd,'Dynamic Data') $null = New-VHD ` -Path $Vhd ` -SizeBytes $Size ` -Dynamic ` -ErrorAction Stop break; } # 'dynamic' 'differencing' { # A differencing disk is specified so check the Parent VHD # is specified and exists $ParentVhd = $DataVhd.ParentVhd if (-not $ParentVhd) { $exceptionParameters = @{ errorId = 'VMDataDiskParentVHDMissingError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMDataDiskParentVHDMissingError ` -f $VM.name) } New-LabException @exceptionParameters } # if if (-not (Test-Path -Path $ParentVhd)) { $exceptionParameters = @{ errorId = 'VMDataDiskParentVHDNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMDataDiskParentVHDNotFoundError ` -f $VM.name,$ParentVhd) } New-LabException @exceptionParameters } # if # Create a new Differencing VHD Write-LabMessage -Message $($LocalizedData.CreatingVMDiskMessage ` -f $VM.Name,$Vhd,"Differencing Data using Parent '$ParentVhd'") $null = New-VHD ` -Path $Vhd ` -SizeBytes $Size ` -Differencing ` -ParentPath $ParentVhd ` -ErrorAction Stop break; } # 'differencing' default { $exceptionParameters = @{ errorId = 'VMDataDiskUnknownTypeError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMDataDiskUnknownTypeError ` -f $VM.Name,$Vhd,$DataVhd.VhdType) } New-LabException @exceptionParameters } # default } # switch } # if # Do folders need to be copied to this Data Disk? if ($null -ne $DataVhd.CopyFolders) { # Files need to be copied to this Data VHD so # set up a mount folder for it to be mounted to. # Get Path to LabBuilder files [System.String] $VMLabBuilderFiles = $VM.LabBuilderFilesPath [System.String] $MountPoint = Join-Path ` -Path $VMLabBuilderFiles ` -ChildPath 'VHDMount' if (-not (Test-Path -Path $MountPoint -PathType Container)) { $null = New-Item ` -Path $MountPoint ` -ItemType Directory } # Yes, initialize the disk (or check it is) $initializeLabVHDParams = @{ Path = $VHD AccessPath = $MountPoint } # Are we allowed to initialize/format the disk? if ($DataVHD.PartitionStyle -and $DataVHD.FileSystem) { # Yes, initialize the disk $initializeLabVHDParams += @{ PartitionStyle = $DataVHD.PartitionStyle FileSystem = $DataVHD.FileSystem } # Set a FileSystemLabel too? if ($DataVHD.FileSystemLabel) { $initializeLabVHDParams += @{ FileSystemLabel = $DataVHD.FileSystemLabel } } } Write-LabMessage -Message $($LocalizedData.InitializingVMDiskMessage ` -f $VM.Name,$VHD) Initialize-LabVHD ` @initializeLabVHDParams ` -ErrorAction Stop # Copy each folder to the VM Data Disk foreach ($CopyFolder in @($DataVHD.CopyFolders)) { Write-LabMessage -Message $($LocalizedData.CopyingFoldersToVMDiskMessage ` -f $VM.Name,$VHD,$CopyFolder) Copy-item ` -Path $CopyFolder ` -Destination $MountPoint ` -Recurse ` -Force } # Dismount the VM Data Disk Write-LabMessage -Message $($LocalizedData.DismountingVMDiskMessage ` -f $VM.Name,$VHD) Dismount-VHD ` -Path $VHD ` -ErrorAction Stop } else { # No folders need to be copied but check if we # need to initialize the new disk. if ($DataVHD.PartitionStyle -and $DataVHD.FileSystem) { $InitializeVHDParams = @{ Path = $VHD PartitionStyle = $DataVHD.PartitionStyle FileSystem = $DataVHD.FileSystem } if ($DataVHD.FileSystemLabel) { $InitializeVHDParams += @{ FileSystemLabel = $DataVHD.FileSystemLabel } } # if Write-LabMessage -Message $($LocalizedData.InitializingVMDiskMessage ` -f $VM.Name,$VHD) Initialize-LabVHD ` @InitializeVHDParams ` -ErrorAction Stop # Dismount the VM Data Disk Write-LabMessage -Message $($LocalizedData.DismountingVMDiskMessage ` -f $VM.Name,$VHD) Dismount-VHD ` -Path $VHD ` -ErrorAction Stop } # if } # if } # if # Get a list of disks attached to the VM $VMHardDiskDrives = Get-VMHardDiskDrive ` -VMName $VM.Name # The data disk VHD will now exist so ensure it is attached if (($VMHardDiskDrives | Where-Object -Property Path -eq $Vhd).Count -eq 0) { # The data disk is not yet attached Write-LabMessage -Message $($LocalizedData.AddingVMDiskMessage ` -f $VM.Name,$Vhd,'Data') # Determine the ControllerLocation and ControllerNumber to # attach the VHD to. $ControllerLocation = ($VMHardDiskDrives | Measure-Object -Property ControllerLocation -Maximum).Maximum + 1 $NewHardDiskParams = @{ VMName = $VM.Name Path = $Vhd ControllerType = 'SCSI' ControllerLocation = $ControllerLocation ControllerNumber = 0 ErrorAction = 'Stop' } if ($DataVhd.Shared -or $DataVHD.SupportPR) { $NewHardDiskParams += @{ SupportPersistentReservations = $true } } # if $null = Add-VMHardDiskDrive @NewHardDiskParams } # if } # foreach } <# .SYNOPSIS Updates the VM DVD Drives to match the VM Configuration. .DESCRIPTION This cmdlet will take the VM configuration provided and ensure that the DVD Drives are attached to the VM and with the specified ISO. The function will use the array of items in the DVDDrives property of the VM to create and attach any DVD Drives that are missing. If an ISO File is specified in the DVD Drive then it will be mounted to the DVD Drive. .EXAMPLE $Lab = Get-Lab -ConfigPath c:\mylab\config.xml $VMs = Get-LabVM -Lab $Lab Update-LabVMDvdDrive -Lab $Lab -VM VM[0] This will update the DVD Drives for the first VM in the configuration file c:\mylab\config.xml. .PARAMETER Lab Contains the Lab object that was produced by the Get-Lab cmdlet. .PARAMETER VM A LabVM object pulled from the Lab Configuration file using Get-LabVM .OUTPUTS None. #> function Update-LabVMDvdDrive { [CmdLetBinding()] param ( [Parameter( Mandatory, Position=0)] [ValidateNotNullOrEmpty()] $Lab, [Parameter( Mandatory, Position=1)] [ValidateNotNullOrEmpty()] [LabVM] $VM ) # If there are no DVD Drives just return if (-not $VM.DVDDrives) { return } [System.Int32] $DVDDriveCount = 0 foreach ($DVDDrive in @($VM.DVDDrives)) { # Get a list of DVD Drives attached to the VM $VMDVDDrives = @(Get-VMDVDDrive ` -VMName $VM.Name) # The DVD Drive will now exist so ensure it is attached if ($VMDVDDrives[$DVDDriveCount]) { # The DVD Drive is already attached then make sure the correct ISO if ($VMDVDDrives[$DVDDriveCount].Path -ne $DVDDrive.Path) { if ($DVDDrive.Path) { Write-LabMessage -Message $($LocalizedData.MountingVMDVDDriveISOMessage ` -f $VM.Name,$DVDDrive.Path) } else { Write-LabMessage -Message $($LocalizedData.DismountingVMDVDDriveISOMessage ` -f $VM.Name,$VMDVDDrives[$DVDDriveCount].Path) } # if Set-VMDVDDrive ` -VMName $VM.Name ` -ControllerNumber $VMDVDDrives[$DVDDriveCount].ControllerNumber ` -ControllerLocation $VMDVDDrives[$DVDDriveCount].ControllerLocation ` -Path $DVDDrive.Path } # if } else { # The DVD Drive does not exist Write-LabMessage -Message $($LocalizedData.AddingVMDVDDriveMessage ` -f $VM.Name) $NewDVDDriveParams = @{ VMName = $VM.Name ErrorAction = 'Stop' } if ($DVDDrive.Path) { Write-LabMessage -Message $($LocalizedData.MountingVMDVDDriveISOMessage ` -f $VM.Name,$DVDDrive.Path) $NewDVDDriveParams += @{ Path = $DVDDrive.Path } } # if $null = Add-VMDVDDrive @NewDVDDriveParams } # if $DVDDriveCount++ } # foreach } <# .SYNOPSIS Updates the VM Integration Services to match the VM Configuration. .DESCRIPTION This cmdlet will take the VM object provided and ensure the integration services specified in it are enabled. The function will use comma delimited list of integration services in the VM object passed and enable the integration services listed for this VM. If the IntegrationServices property of the VM is not set or set to null then ALL integration services will be ENABLED. If the IntegrationServices property of the VM is set but is blank then ALL integration services will be DISABLED. The IntegrationServices property should contain a comma delimited list of Integration Services that should be enabled. The currently available Integration Services are: - Guest Service Interface - Heartbeat - Key-Value Pair Exchange - Shutdown - Time Synchronization - VSS .EXAMPLE $Lab = Get-Lab -ConfigPath c:\mylab\config.xml $VMs = Get-LabVM -Lab $Lab Update-LabVMIntegrationService -VM VM[0] This will update the Integration Services for the first VM in the configuration file c:\mylab\config.xml. .PARAMETER VM A LabVM object pulled from the Lab Configuration file using Get-LabVM .OUTPUTS None. #> function Update-LabVMIntegrationService { [CmdLetBinding()] param ( [Parameter( Mandatory, Position=1)] [ValidateNotNullOrEmpty()] [LabVM] $VM ) # Configure the Integration services $integrationServices = $VM.IntegrationServices if ($null -eq $integrationServices) { # Get the full list of Integration Service names localized $integrationServices = ((Get-LabIntegrationServiceName) -Join ',') } $enabledIntegrationServices = $integrationServices -split ',' $existingIntegrationServices = Get-VMIntegrationService ` -VMName $VM.Name ` -ErrorAction Stop # Loop through listed integration services and enable them foreach ($existingIntegrationService in $existingIntegrationServices) { if ($existingIntegrationService.Name -in $enabledIntegrationServices) { # This integration service should be enabled if (-not $existingIntegrationService.Enabled) { # It is disabled so enable it $existingIntegrationService | Enable-VMIntegrationService Write-LabMessage -Message $($LocalizedData.EnableVMIntegrationServiceMessage ` -f $VM.Name,$existingIntegrationService.Name) } # if } else { # This integration service should be disabled if ($existingIntegrationService.Enabled) { # It is enabled so disable it $existingIntegrationService | Disable-VMIntegrationService Write-LabMessage -Message $($LocalizedData.DisableVMIntegrationServiceMessage ` -f $VM.Name,$existingIntegrationService.Name) } # if } # if } # foreach } <# .SYNOPSIS Waits for a VM to complete setup. .DESCRIPTION When a VM starts up for the first time various scripts are run that prepare the Virtual Machine to be managed as part of a Lab. This function will wait for these scripts to complete. It determines if the setup has been completed by using PowerShell remoting to connect to the VM and downloading the c:\windows\Setup\Scripts\InitialSetupCompleted.txt file. If this file does not exist then the initial setup has not been completed. The cmdlet will wait for a maximum of 300 seconds for this process to be completed. .PARAMETER VM A LabVM object pulled from the Lab Configuration file using Get-LabVM .PARAMETER Timeout The maximum amount of time that this function will wait for the setup to complete. If the timeout is reached before the process is complete an error will be thrown. The timeout defaults to 300 seconds. .EXAMPLE $Lab = Get-Lab -ConfigPath c:\mylab\config.xml $VMs = Get-LabVM -Lab $Lab Wait-LabVMInitializationComplete -VM $VMs[0] Waits for the initial setup to complete on the first VM in the config.xml. .OUTPUTS The path to the local copy of the Initial Setup complete file in the Labbuilder files folder for this VM. #> function Wait-LabVMInitializationComplete { [CmdLetBinding()] [OutputType([System.String])] param ( [Parameter(Mandatory = $true)] [LabVM] $VM, [Parameter()] [System.Int32] $Timeout = 300 ) [DateTime] $StartTime = Get-Date [System.Management.Automation.Runspaces.PSSession] $Session = $null [System.Boolean] $Complete = $false # Get the root path of the VM [System.String] $VMRootPath = $VM.VMRootPath # Get Path to LabBuilder files [System.String] $VMLabBuilderFiles = $VM.LabBuilderFilesPath # Make sure the VM has started Wait-LabVMStarted -VM $VM [System.String] $InitialSetupCompletePath = Join-Path ` -Path $VMLabBuilderFiles ` -ChildPath 'InitialSetupCompleted.txt' # Check the initial setup on this VM hasn't already completed if (Test-Path -Path $InitialSetupCompletePath) { Write-LabMessage -Message $($LocalizedData.InitialSetupIsAlreadyCompleteMessaage ` -f $VM.Name) return $InitialSetupCompletePath } while ((-not $Complete) ` -and (((Get-Date) - $StartTime).TotalSeconds) -lt $TimeOut) { # Connect to the VM $Session = Connect-LabVM ` -VM $VM ` -ErrorAction Continue # Failed to connnect to the VM if (-not $Session) { $exceptionParameters = @{ errorId = 'InitialSetupCompleteError' errorCategory = 'OperationTimeout' errorMessage = $($LocalizedData.InitialSetupCompleteError ` -f $VM.Name) } New-LabException @exceptionParameters return } if (($Session) ` -and ($Session.State -eq 'Opened') ` -and (-not $Complete)) { # We connected OK - Download the script while ((-not $Complete) ` -and (((Get-Date) - $StartTime).TotalSeconds) -lt $TimeOut) { try { $null = Copy-Item ` -Path "c:\windows\Setup\Scripts\InitialSetupCompleted.txt" ` -Destination $VMLabBuilderFiles ` -FromSession $Session ` -Force ` -ErrorAction Stop $Complete = $true } catch { Write-LabMessage -Message $($LocalizedData.WaitingForInitialSetupCompleteMessage ` -f $VM.Name, $Script:RetryConnectSeconds) Start-Sleep ` -Seconds $Script:RetryConnectSeconds } # try } # while } # if # If the process didn't complete and we're out of time throw an exception if ((-not $Complete) ` -and (((Get-Date) - $StartTime).TotalSeconds) -ge $TimeOut) { # Disconnect from the VM Disconnect-LabVM ` -VM $VM ` -ErrorAction Continue $exceptionParameters = @{ errorId = 'InitialSetupCompleteError' errorCategory = 'OperationTimeout' errorMessage = $($LocalizedData.InitialSetupCompleteError ` -f $VM.Name) } New-LabException @exceptionParameters } # Close the Session if it is opened if (($Session) ` -and ($Session.State -eq 'Opened')) { # Disconnect from the VM Disconnect-LabVM ` -VM $VM ` -ErrorAction Continue } # if } # while return $InitialSetupCompletePath } <# .SYNOPSIS Wait for VM to enter the Off state. .PARAMETER VM A LabVM object pulled from the Lab Configuration file using Get-LabVM. .OUTPUTS None. #> function Wait-LabVMOff { [CmdLetBinding()] param ( [Parameter(Mandatory = $true)] [LabVM] $VM ) $runningVM = Get-VM -Name $VM.Name while ($runningVM.State -ne 'Off') { $runningVM = Get-VM -Name $VM.Name Start-Sleep -Seconds $Script:RetryHeartbeatSeconds } # while } <# .SYNOPSIS Wait for the VM to enter the running state. .PARAMETER VM A LabVM object pulled from the Lab Configuration file using Get-LabVM .OUTPUTS None. #> function Wait-LabVMStarted { [CmdLetBinding()] param ( [Parameter(Mandatory = $true)] [LabVM] $VM ) # If the VM is not running then throw an exception if ((Get-VM -VMName $VM.Name).State -ne 'Running') { $exceptionParameters = @{ errorId = 'VMNotRunningHeartbeatMessage' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMNotRunningHeartbeatMessage ` -f $VM.name) } New-LabException @exceptionParameters } # if # Names of IntegrationServices are not culture neutral, but have an ID $heartbeatCultureNeutral = ( Get-VMIntegrationService -VMName $VM.Name | Where-Object { $_.ID -match "84EAAE65-2F2E-45F5-9BB5-0E857DC8EB47" } ).Name $heartbeat = Get-VMIntegrationService -VMName $VM.Name -Name $heartbeatCultureNeutral while (($heartbeat.PrimaryStatusDescription -ne 'OK') -and (-not [System.String]::IsNullOrEmpty($heartbeat.PrimaryStatusDescription))) { $heartbeat = Get-VMIntegrationService -VMName $VM.Name -Name $heartbeatCultureNeutral Write-LabMessage -Message $($LocalizedData.WaitingForVMHeartbeatMessage ` -f $VM.Name,$Script:RetryHeartbeatSeconds) Start-Sleep -Seconds $Script:RetryHeartbeatSeconds } # while } <# .SYNOPSIS Writes a Message of the specified Type. .DESCRIPTION This cmdlet will write a message along with the time to the specified output stream. .PARAMETER Type This can be one of the following: Error - Writes to the Error Stream. Warning - Writes to the Warning Stream. Verbose - Writes to the Verbose Stream (default) Debug - Writes to the Debug Stream. Information - Writes to the Information Stream. Output - Writes to the Output Stream (so should be used for a terminating message) .PARAMETER Message The Message to output. .PARAMETER ForegroundColor The foreground color of the message if being writen to the output stream. .EXAMPLE Write-LabMessage -Type Verbose -Message 'Downloading file' New-LabException @exceptionParameters Outputs the message 'Downloading file' to the Verbose stream. .OUTPUTS None #> function Write-LabMessage { [CmdLetBinding()] param ( [Parameter()] [ValidateSet('Error', 'Warning', 'Verbose', 'Debug', 'Info', 'Alert')] [System.String] $Type = 'Verbose', [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Message, [Parameter()] [System.String] $ForegroundColor = 'Yellow' ) $time = Get-Date -UFormat %T switch ($Type) { 'Error' { Write-Error -Message $Message break } 'Warning' { Write-Warning -Message ('[{0}]: {1}' -f $time, $Message) break } 'Verbose' { Write-Verbose -Message ('[{0}]: {1}' -f $time, $Message) break } 'Debug' { Write-Debug -Message ('[{0}]: {1}' -f $time, $Message) break } 'Info' { Write-Information -MessageData ('INFO: [{0}]: {1}' -f $time, $Message) break } 'Alert' { Write-Host ` -ForegroundColor $ForegroundColor ` -Object $Message break } } # switch } function Connect-LabVM { [OutputType([System.Management.Automation.Runspaces.PSSession])] [CmdLetBinding()] param ( [Parameter( Position=1, Mandatory=$true)] [LabVM] $VM, [Parameter( Position=2)] [System.Int32] $ConnectTimeout = 300 ) $startTime = Get-Date $session = $null $adminCredential = New-LabCredential ` -Username '.\Administrator' ` -Password $VM.AdministratorPassword $fatalException = $false while (($null -eq $session) ` -and (((Get-Date) - $startTime).TotalSeconds) -lt $ConnectTimeout ` -and -not $fatalException) { try { <# Get the Management IP Address of the VM We repeat this because the IP Address will only be assiged once the VM is fully booted. #> $ipAddress = Get-LabVMManagementIPAddress ` -Lab $Lab ` -VM $VM <# Add the IP Address to trusted hosts if not already in it This could be avoided if able to use SSL or if PS Direct is used. Also, don't add if TrustedHosts is already * #> $trustedHosts = (Get-Item -Path WSMAN::localhost\Client\TrustedHosts).Value if (($trustedHosts -notlike "*$ipAddress*") -and ($trustedHosts -ne '*')) { if ([System.String]::IsNullOrWhitespace($trustedHosts)) { $trustedHosts = "$ipAddress" } else { $trustedHosts = "$trustedHosts,$ipAddress" } Set-Item ` -Path WSMAN::localhost\Client\TrustedHosts ` -Value $trustedHosts ` -Force Write-LabMessage -Message $($LocalizedData.AddingIPAddressToTrustedHostsMessage ` -f $VM.Name,$ipAddress) } Write-LabMessage -Message $($LocalizedData.ConnectingVMMessage ` -f $VM.Name,$ipAddress) $session = New-PSSession ` -Name 'LabBuilder' ` -ComputerName $ipAddress ` -Credential $adminCredential ` -ErrorAction Stop } catch { if (-not $ipAddress) { Write-LabMessage -Message $($LocalizedData.WaitingForIPAddressAssignedMessage ` -f $VM.Name,$Script:RetryConnectSeconds) } else { Write-LabMessage -Message $($LocalizedData.ConnectingVMFailedMessage ` -f $VM.Name,$Script:RetryConnectSeconds,$_.Exception.Message) } Start-Sleep -Seconds $Script:RetryConnectSeconds } # Try } # While <# If a fatal exception occured or the connection just couldn't be established then throw an exception so it can be caught by the calling code. #> if ($fatalException -or ($null -eq $session)) { # The connection failed so throw an error $exceptionParameters = @{ errorId = 'RemotingConnectionError' errorCategory = 'ConnectionError' errorMessage = $($LocalizedData.RemotingConnectionError ` -f $VM.Name) } New-LabException @exceptionParameters } return $session } # Connect-LabVM function Disconnect-LabVM { [CmdLetBinding()] param ( [Parameter( Position=1, Mandatory=$true)] [LabVM] $VM ) $adminCredential = New-LabCredential ` -Username '.\Administrator' ` -Password $VM.AdministratorPassword # Get the Management IP Address of the VM $ipAddress = Get-LabVMManagementIPAddress ` -Lab $Lab ` -VM $VM try { # Look for the session $session = Get-PSSession ` -Name 'LabBuilder' ` -ComputerName $ipAddress ` -Credential $adminCredential ` -ErrorAction Stop if (-not $session) { # No session found to this machine so nothing to do. Write-LabMessage -Message $($LocalizedData.VMSessionDoesNotExistMessage ` -f $VM.Name) } else { if ($session.State -eq 'Opened') { # Disconnect the session $null = $session | Disconnect-PSSession Write-LabMessage -Message $($LocalizedData.DisconnectingVMMessage ` -f $VM.Name,$IPAddress) } # Remove the session $null = $session | Remove-PSSession -ErrorAction SilentlyContinue } } catch { Throw $_ } finally { # Remove the entry from TrustedHosts $trustedHosts = (Get-Item -Path WSMAN::localhost\Client\TrustedHosts).Value if (($trustedHosts -like "*$ipAddress*") -and ($trustedHosts -ne '*')) { $ipAddresses = @($trustedHosts -split ',') $trustedHosts = ($ipAddresses | Where-Object -FilterScript { $_ -ne $ipAddress }) -join ',' Set-Item ` -Path WSMAN::localhost\Client\TrustedHosts ` -Value $trustedHosts ` -Force Write-LabMessage -Message $($LocalizedData.RemovingIPAddressFromTrustedHostsMessage ` -f $VM.Name,$ipAddress) } } # try } # Disconnect-LabVM function Get-Lab { [CmdLetBinding()] [OutputType([XML])] param ( [Parameter( Position=1, Mandatory=$true)] [ValidateNotNullOrEmpty()] [System.String] $ConfigPath, [Parameter( Position=2)] [ValidateNotNullOrEmpty()] [System.String] $LabPath, [Parameter( Position=3)] [Switch] $SkipXMLValidation ) # Param # If a relative path to the config has been specified # then convert it to absolute path if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { $ConfigPath = Join-Path ` -Path (Get-Location).Path ` -ChildPath $ConfigPath } # if if (-not (Test-Path -Path $ConfigPath)) { $exceptionParameters = @{ errorId = 'ConfigurationFileNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.ConfigurationFileNotFoundError ` -f $ConfigPath) } New-LabException @exceptionParameters } # if $Content = Get-Content -Path $ConfigPath -Raw if (-not $Content) { $exceptionParameters = @{ errorId = 'ConfigurationFileEmptyError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.ConfigurationFileEmptyError ` -f $ConfigPath) } New-LabException @exceptionParameters } # if if (-not $SkipXMLValidation) { # Validate the XML Assert-LabValidConfigurationXMLSchema ` -ConfigPath $ConfigPath ` -ErrorAction Stop } # The XML passes the Schema check so load it. $Lab = New-Object -TypeName System.Xml.XmlDocument $Lab.PreserveWhitespace = $true $Lab.LoadXML($Content) # Check the Required Windows Build $RequiredWindowsBuild = $Lab.labbuilderconfig.settings.requiredwindowsbuild if ($RequiredWindowsBuild -and ` ($Script:CurrentBuild -lt $RequiredWindowsBuild)) { $exceptionParameters = @{ errorId = 'RequiredBuildNotMetError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.RequiredBuildNotMetError ` -f $Script:CurrentBuild,$RequiredWindowsBuild) } New-LabException @exceptionParameters } # if # Figure out the Config path and load it into the XML object (if we can) # This path is used to find any additional configuration files that might # be provided with config [System.String] $ConfigPath = [System.IO.Path]::GetDirectoryName($ConfigPath) [System.String] $XMLConfigPath = $Lab.labbuilderconfig.settings.configpath if ($XMLConfigPath) { if (-not [System.IO.Path]::IsPathRooted($XMLConfigurationPath)) { # A relative path was provided in the config path so add the actual path of the # XML to it [System.String] $FullConfigPath = Join-Path ` -Path $ConfigPath ` -ChildPath $XMLConfigPath } # if } else { [System.String] $FullConfigPath = $ConfigPath } $Lab.labbuilderconfig.settings.setattribute('fullconfigpath',$FullConfigPath) # if the LabPath was passed as a parameter, set it in the config if ($LabPath) { $Lab.labbuilderconfig.settings.SetAttribute('labpath',$LabPath) } else { [System.String] $LabPath = $Lab.labbuilderconfig.settings.labpath } # Get the VHDParentPathFull - if it isn't supplied default [System.String] $VHDParentPath = $Lab.labbuilderconfig.settings.vhdparentpath if (-not $VHDParentPath) { $VHDParentPath = 'Virtual Hard Disk Templates' } # if the resulting parent path is not rooted make the root the Lab Path if (-not ([System.IO.Path]::IsPathRooted($VHDParentPath))) { $VHDParentPath = Join-Path ` -Path $LabPath ` -ChildPath $VHDParentPath } # if $Lab.labbuilderconfig.settings.setattribute('vhdparentpathfull',$VHDParentPath) # Get the DSCLibraryPathFull - if it isn't supplied default [System.String] $DSCLibraryPath = $Lab.labbuilderconfig.settings.dsclibrarypath if (-not $DSCLibraryPath) { $DSCLibraryPath = 'DSCLibrary' } # if # if the resulting parent path is not rooted make the root the Full config path if (-not [System.IO.Path]::IsPathRooted($DSCLibraryPath)) { $DSCLibraryPath = Join-Path ` -Path $Lab.labbuilderconfig.settings.fullconfigpath ` -ChildPath $DSCLibraryPath } # if $Lab.labbuilderconfig.settings.setattribute('dsclibrarypathfull',$DSCLibraryPath) # Get the ResourcePathFull - if it isn't supplied default [System.String] $ResourcePath = $Lab.labbuilderconfig.settings.resourcepath if (-not $ResourcePath) { $ResourcePath = 'Resource' } # if # if the resulting Resource path is not rooted make the root the Lab Path if (-not [System.IO.Path]::IsPathRooted($ResourcePath)) { $ResourcePath = Join-Path ` -Path $LabPath ` -ChildPath $ResourcePath } # if $Lab.labbuilderconfig.settings.setattribute('resourcepathfull',$ResourcePath) # Determine the ModulePath where alternate Lab PowerShell Modules can be found. # If a path is specified but it is relative, make it relative to the lab path. # Otherwise use it as is. [System.String] $ModulePath = $Lab.labbuilderconfig.settings.modulepath if ($ModulePath) { if (-not [System.IO.Path]::IsPathRooted($ModulePath)) { $ModulePath = Join-Path ` -Path $LabPath ` -ChildPath $ModulePath } # if # If the path is not included in the PSModulePath add it if (-not $env:PSModulePath.ToLower().Contains($ModulePath.ToLower() + ';')) { $env:PSModulePath = "$ModulePath;" + $env:PSModulePath } # if } # if Return $Lab } # Get-Lab function Get-LabResourceISO { [OutputType([LabResourceISO[]])] [CmdLetBinding()] param ( [Parameter( Position = 1, Mandatory = $true)] [ValidateNotNullOrEmpty()] $Lab, [Parameter( Position = 2)] [ValidateNotNullOrEmpty()] [System.String[]] $Name ) # Determine the ISORootPath where the ISO files should be found. # If no path is specified then look in the resource path. # If a path is specified but it is relative, make it relative to the resource path. # Otherwise use it as is. [System.String] $ISORootPath = $Lab.labbuilderconfig.Resources.ISOPath if ($ISORootPath) { if (-not [System.IO.Path]::IsPathRooted($ISORootPath)) { $ISORootPath = Join-Path ` -Path $Lab.labbuilderconfig.settings.resourcepathfull ` -ChildPath $ISORootPath } # if } else { $ISORootPath = $Lab.labbuilderconfig.settings.resourcepathfull } # if [LabResourceISO[]] $ResourceISOs = @() if ($Lab.labbuilderconfig.resources) { foreach ($ISO in $Lab.labbuilderconfig.resources.iso) { $ISOName = $ISO.Name if ($Name -and ($ISOName -notin $Name)) { # A names list was passed but this ISO wasn't included continue } # if if ($ISOName -eq 'iso') { $exceptionParameters = @{ errorId = 'ResourceISONameIsEmptyError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.ResourceISONameIsEmptyError) } New-LabException @exceptionParameters } # if $ResourceISO = [LabResourceISO]::New($ISOName) $Path = $ISO.Path if ($Path) { if (-not [System.IO.Path]::IsPathRooted($Path)) { $Path = Join-Path ` -Path $ISORootPath ` -ChildPath $Path } # if } else { # A Path is not provided $exceptionParameters = @{ errorId = 'ResourceISOPathIsEmptyError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.ResourceISOPathIsEmptyError ` -f $ISOName) } New-LabException @exceptionParameters } if ($ISO.URL) { $ResourceISO.URL = $ISO.URL } # if $ResourceISO.Path = $Path $ResourceISOs += @( $ResourceISO ) } # foreach } # if return $ResourceISOs } # Get-LabResourceISO function Get-LabResourceModule { [OutputType([LabResourceModule[]])] [CmdLetBinding()] param ( [Parameter( Position = 1, Mandatory = $true)] [ValidateNotNullOrEmpty()] $Lab, [Parameter( Position = 2)] [ValidateNotNullOrEmpty()] [System.String[]] $Name ) [LabResourceModule[]] $ResourceModules = @() if ($Lab.labbuilderconfig.resources) { foreach ($Module in $Lab.labbuilderconfig.resources.module) { $ModuleName = $Module.Name if ($Name -and ($ModuleName -notin $Name)) { # A names list was passed but this Module wasn't included continue } # if if ($ModuleName -eq 'module') { $exceptionParameters = @{ errorId = 'ResourceModuleNameIsEmptyError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.ResourceModuleNameIsEmptyError) } New-LabException @exceptionParameters } # if $ResourceModule = [LabResourceModule]::New($ModuleName) $ResourceModule.URL = $Module.URL $ResourceModule.Folder = $Module.Folder $ResourceModule.MinimumVersion = $Module.MinimumVersion $ResourceModule.RequiredVersion = $Module.RequiredVersion $ResourceModules += @( $ResourceModule ) } # foreach } # if return $ResourceModules } # Get-LabResourceModule function Get-LabResourceMSU { [OutputType([LabResourceMSU[]])] [CmdLetBinding()] param ( [Parameter( Position = 1, Mandatory = $true)] [ValidateNotNullOrEmpty()] $Lab, [Parameter( Position = 2)] [ValidateNotNullOrEmpty()] [System.String[]] $Name ) [LabResourceMSU[]] $ResourceMSUs = @() if ($Lab.labbuilderconfig.resources) { foreach ($MSU in $Lab.labbuilderconfig.resources.msu) { $MSUName = $MSU.Name if ($Name -and ($MSUName -notin $Name)) { # A names list was passed but this MSU wasn't included continue } # if if ($MSUName -eq 'msu') { $exceptionParameters = @{ errorId = 'ResourceMSUNameIsEmptyError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.ResourceMSUNameIsEmptyError) } New-LabException @exceptionParameters } # if $ResourceMSU = [LabResourceMSU]::New($MSUName, $MSU.URL) $Path = $MSU.Path if ($Path) { if (-not [System.IO.Path]::IsPathRooted($Path)) { $Path = Join-Path ` -Path $Lab.labbuilderconfig.settings.resourcepathfull ` -ChildPath $Path } } else { $Path = $Lab.labbuilderconfig.settings.resourcepathfull } $FileName = Join-Path ` -Path $Path ` -ChildPath $MSU.URL.Substring($MSU.URL.LastIndexOf('/') + 1) $ResourceMSU.Path = $Path $ResourceMSU.Filename = $Filename $ResourceMSUs += @( $ResourceMSU ) } # foreach } # if return $ResourceMSUs } # Get-LabResourceMSU function Get-LabSwitch { [OutputType([LabSwitch[]])] [CmdLetBinding()] param ( [Parameter( Position = 1, Mandatory = $true)] [ValidateNotNullOrEmpty()] $Lab, [Parameter( Position = 2)] [ValidateNotNullOrEmpty()] [System.String[]] $Name ) [System.String] $LabId = $Lab.labbuilderconfig.settings.labid [LabSwitch[]] $Switches = @() $ConfigSwitches = $Lab.labbuilderconfig.Switches.Switch foreach ($ConfigSwitch in $ConfigSwitches) { # It can't be switch because if the name attrib/node is missing the name property on the # XML object defaults to the name of the parent. So we can't easily tell if no name was # specified or if they actually specified 'switch' as the name. $SwitchName = $ConfigSwitch.Name if ($Name -and ($SwitchName -notin $Name)) { # A names list was passed but this swtich wasn't included continue } # if if ($SwitchName -eq 'switch') { $exceptionParameters = @{ errorId = 'SwitchNameIsEmptyError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.SwitchNameIsEmptyError) } New-LabException @exceptionParameters } # Convert the switch type string to a LabSwitchType $SwitchType = [LabSwitchType]::$($ConfigSwitch.Type) # If the SwitchType string doesn't match any enum value it will be # set to null. if (-not $SwitchType) { $exceptionParameters = @{ errorId = 'UnknownSwitchTypeError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.UnknownSwitchTypeError ` -f $ConfigSwitch.Type, $SwitchName) } New-LabException @exceptionParameters } # if # if a LabId is set for the lab, prepend it to the Switch name as long as it isn't # an external switch. if ($LabId -and ($SwitchType -ne [LabSwitchType]::External)) { $SwitchName = "$LabId$SwitchName" } # if # Assemble the list of Mangement OS Adapters if any are specified for this switch # Only Intenal and External switches are allowed Management OS adapters. if ($ConfigSwitch.Adapters) { [LabSwitchAdapter[]] $ConfigAdapters = @() foreach ($Adapter in $ConfigSwitch.Adapters.Adapter) { $AdapterName = $Adapter.Name # if a LabId is set for the lab, prepend it to the adapter name. # But only if it is not an External switch. if ($LabId -and ($SwitchType -ne [LabSwitchType]::External)) { $AdapterName = "$LabId$AdapterName" } $ConfigAdapter = [LabSwitchAdapter]::New($AdapterName) $ConfigAdapter.MACAddress = $Adapter.MacAddress $ConfigAdapters += @( $ConfigAdapter ) } # foreach if (($ConfigAdapters.Count -gt 0) ` -and ($SwitchType -notin [LabSwitchType]::External, [LabSwitchType]::Internal)) { $exceptionParameters = @{ errorId = 'AdapterSpecifiedError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.AdapterSpecifiedError ` -f $SwitchType, $SwitchName) } New-LabException @exceptionParameters } # if } else { $ConfigAdapters = $null } # if # Create the new Switch object [LabSwitch] $NewSwitch = [LabSwitch]::New($SwitchName, $SwitchType) $NewSwitch.VLAN = $ConfigSwitch.VLan $NewSwitch.BindingAdapterName = $ConfigSwitch.BindingAdapterName $NewSwitch.BindingAdapterMac = $ConfigSwitch.BindingAdapterMac $NewSwitch.BindingAdapterMac = $ConfigSwitch.BindingAdapterMac $NewSwitch.NatSubnet = $ConfigSwitch.NatSubnet $NewSwitch.NatGatewayAddress = $ConfigSwitch.NatGatewayAddress $NewSwitch.Adapters = $ConfigAdapters $Switches += @( $NewSwitch ) } # foreach return $Switches } # Get-LabSwitch function Get-LabVM { [OutputType([LabVM[]])] [CmdLetBinding()] param ( [Parameter( Position=1, Mandatory=$true)] [ValidateNotNullOrEmpty()] $Lab, [Parameter( Position=2)] [ValidateNotNullOrEmpty()] [System.String[]] $Name, [Parameter( Position=3)] [LabVMTemplate[]] $VMTemplates, [Parameter( Position=4)] [LabSwitch[]] $Switches ) # if VMTeplates array not passed, pull it from config. if (-not $PSBoundParameters.ContainsKey('VMTemplates')) { [LabVMTemplate[]] $VMTemplates = Get-LabVMTemplate ` -Lab $Lab } # if Switches array not passed, pull it from config. if (-not $PSBoundParameters.ContainsKey('Switches')) { [LabSwitch[]] $Switches = Get-LabSwitch ` -Lab $Lab } [LabVM[]] $LabVMs = @() [System.String] $LabPath = $Lab.labbuilderconfig.settings.labpath [System.String] $VHDParentPath = $Lab.labbuilderconfig.settings.vhdparentpathfull [System.String] $LabId = $Lab.labbuilderconfig.settings.labid $VMs = $Lab.labbuilderconfig.vms.vm foreach ($VM in $VMs) { if ($VM.Name -eq 'VM') { $exceptionParameters = @{ errorId = 'VMNameError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMNameError) } New-LabException @exceptionParameters } # if # Get the Instance Count attribute $InstanceCount = $VM.InstanceCount if (-not $InstanceCount) { $InstanceCount = 1 } foreach ($Instance in 1..$InstanceCount) { # If InstanceCount is 1 then don't increment the IP or MAC addresses or append count to the name if ($InstanceCount -eq 1) { $VMName = $VM.Name $ComputerName = $VM.ComputerName $IncNetIds = 0 } else { $VMName = "$($VM.Name)$Instance" $ComputerName = "$($VM.ComputerName)$Instance" # This value is used to increment IP and MAC addresses $IncNetIds = $Instance - 1 } # if if ($Name -and ($VMName -notin $Name)) { # A names list was passed but this VM wasn't included continue } # if # if a LabId is set for the lab, prepend it to the VM name. if ($LabId) { $VMName = "$LabId$VMName" } if (-not $VM.Template) { $exceptionParameters = @{ errorId = 'VMTemplateNameEmptyError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMTemplateNameEmptyError ` -f $VMName) } New-LabException @exceptionParameters } # if # Find the template that this VM uses and get the VHD Path [System.String] $ParentVHDPath = '' [System.Boolean] $Found = $false foreach ($VMTemplate in $VMTemplates) { if ($VMTemplate.Name -eq $VM.Template) { $ParentVHDPath = $VMTemplate.ParentVHD $Found = $true Break } # if } # foreach if (-not $Found) { $exceptionParameters = @{ errorId = 'VMTemplateNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMTemplateNotFoundError ` -f $VMName,$VM.template) } New-LabException @exceptionParameters } # if # Get path to Offline Domain Join file if it exists [System.String]$NanoODJPath = $null if ($VM.NanoODJPath) { $NanoODJPath = $VM.NanoODJPath } # if # Assemble the Network adapters that this VM will use [LabVMAdapter[]] $VMAdapters = @() [System.Int32] $AdapterCount = 0 foreach ($VMAdapter in $VM.Adapters.Adapter) { $AdapterCount++ $AdapterName = $VMAdapter.Name $AdapterSwitchName = $VMAdapter.SwitchName if ($AdapterName -eq 'adapter') { $exceptionParameters = @{ errorId = 'VMAdapterNameError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMAdapterNameError ` -f $VMName) } New-LabException @exceptionParameters } # if if (-not $AdapterSwitchName) { $exceptionParameters = @{ errorId = 'VMAdapterSwitchNameError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMAdapterSwitchNameError ` -f $VMName,$AdapterName) } New-LabException @exceptionParameters } # if # if a LabId is set for the lab, prepend it to the adapter name # name and switch name. if ($LabId) { $AdapterName = "$LabId$AdapterName" $AdapterSwitchName = "$LabId$AdapterSwitchName" } # if # Check the switch is in the switch list [System.Boolean] $Found = $false foreach ($Switch in $Switches) { # Match the switch name to the Adapter Switch Name or # the LabId and Adapter Switch Name if ($Switch.Name -eq $AdapterSwitchName) { # The switch is found in the switch list - record the VLAN (if there is one) $Found = $true $SwitchVLan = $Switch.Vlan Break } # if elseif ($Switch.Name -eq $VMAdapter.SwitchName) { # The switch is found in the switch list - record the VLAN (if there is one) $Found = $true $SwitchVLan = $Switch.Vlan if ($Switch.Type -eq [LabSwitchType]::External) { $AdapterName = $VMAdapter.Name $AdapterSwitchName = $VMAdapter.SwitchName } # if Break } } # foreach if (-not $Found) { $exceptionParameters = @{ errorId = 'VMAdapterSwitchNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMAdapterSwitchNotFoundError ` -f $VMName,$AdapterName,$AdapterSwitchName) } New-LabException @exceptionParameters } # if # Figure out the VLan - If defined in the VM use it, otherwise use the one defined in the Switch, otherwise keep blank. [System.String] $VLan = $VMAdapter.VLan if (-not $VLan) { $VLan = $SwitchVLan } # if [System.Boolean] $MACAddressSpoofing = ($VMAdapter.macaddressspoofing -eq 'On') # Have we got any IPv4 settings? Remove-Variable -Name IPv4 -ErrorAction SilentlyContinue if ($VMAdapter.IPv4) { if ($VMAdapter.IPv4.Address) { $IPv4 = [LabVMAdapterIPv4]::New(` (Get-LabNextIpAddress ` -IpAddress $VMAdapter.IPv4.Address` -Step $IncNetIds)` ,$VMAdapter.IPv4.SubnetMask) } # if $IPv4.defaultgateway = $VMAdapter.IPv4.DefaultGateway $IPv4.dnsserver = $VMAdapter.IPv4.DNSServer } # if # Have we got any IPv6 settings? Remove-Variable -Name IPv6 -ErrorAction SilentlyContinue if ($VMAdapter.IPv6) { if ($VMAdapter.IPv6.Address) { $IPv6 = [LabVMAdapterIPv6]::New(` (Get-LabNextIpAddress ` -IpAddress $VMAdapter.IPv6.Address` -Step $IncNetIds)` ,$VMAdapter.IPv6.SubnetMask) } # if $IPv6.defaultgateway = $VMAdapter.IPv6.DefaultGateway $IPv6.dnsserver = $VMAdapter.IPv6.DNSServer } # if $NewVMAdapter = [LabVMAdapter]::New($AdapterName) $NewVMAdapter.SwitchName = $AdapterSwitchName if($VMAdapter.macaddress) { $NewVMAdapter.MACAddress = Get-NextMacAddress ` -MacAddress $VMAdapter.macaddress ` -Step $IncNetIds } # if $NewVMAdapter.MACAddressSpoofing = $MACAddressSpoofing $NewVMAdapter.VLan = $VLan $NewVMAdapter.IPv4 = $IPv4 $NewVMAdapter.IPv6 = $IPv6 $VMAdapters += @( $NewVMAdapter ) } # foreach # Assemble the Data Disks this VM will use [LabDataVHD[]] $DataVhds = @() [System.Int32] $DataVhdCount = 0 foreach ($VMDataVhd in $VM.DataVhds.DataVhd) { $DataVhdCount++ # Load all the VHD properties and check they are valid [System.String] $Vhd = $VMDataVhd.Vhd if (-not $VMDataVhd.Vhd) { $exceptionParameters = @{ errorId = 'VMDataDiskVHDEmptyError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMDataDiskVHDEmptyError ` -f $VMName) } New-LabException @exceptionParameters } # if # Adjust the path to be relative to the Virtual Hard Disks folder of the VM # if it doesn't contain a root (e.g. c:\) if (-not [System.IO.Path]::IsPathRooted($Vhd)) { $Vhd = Join-Path ` -Path $LabPath ` -ChildPath "$($VMName)\Virtual Hard Disks\$Vhd" } # if # Does the VHD already exist? $Exists = Test-Path ` -Path $Vhd # Create the new Data VHD object $NewDataVHD = [LabDataVHD]::New($Vhd) # Get the Parent VHD and check it exists if passed if ($VMDataVhd.ParentVHD) { $NewDataVHD.ParentVhd = $VMDataVhd.ParentVHD # Adjust the path to be relative to the Virtual Hard Disks folder of the VM # if it doesn't contain a root (e.g. c:\) if (-not [System.IO.Path]::IsPathRooted($NewDataVHD.ParentVhd)) { $NewDataVHD.ParentVhd = Join-Path ` -Path $Lab.labbuilderconfig.settings.fullconfigpath ` -ChildPath $NewDataVHD.ParentVhd } if (-not (Test-Path -Path $NewDataVHD.ParentVhd)) { $exceptionParameters = @{ errorId = 'VMDataDiskParentVHDNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMDataDiskParentVHDNotFoundError ` -f $VMName,$NewDataVHD.ParentVhd) } New-LabException @exceptionParameters } # if } # if # Get the Source VHD and check it exists if passed if ($VMDataVhd.SourceVHD) { $NewDataVHD.SourceVhd = $VMDataVhd.SourceVHD # Adjust the path to be relative to the Virtual Hard Disks folder of the VM # if it doesn't contain a root (e.g. c:\) if (-not [System.IO.Path]::IsPathRooted($NewDataVHD.SourceVhd)) { $NewDataVHD.SourceVhd = Join-Path ` -Path $Lab.labbuilderconfig.settings.fullconfigpath ` -ChildPath $NewDataVHD.SourceVhd } # if if (-not (Test-Path -Path $NewDataVHD.SourceVhd)) { $exceptionParameters = @{ errorId = 'VMDataDiskSourceVHDNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMDataDiskSourceVHDNotFoundError ` -f $VMName,$NewDataVHD.SourceVhd) } New-LabException @exceptionParameters } # if } # if # Get the disk size if provided if ($VMDataVhd.Size) { $NewDataVHD.Size = (Invoke-Expression $VMDataVhd.Size) } # if # Get the Shared flag $NewDataVHD.Shared = ($VMDataVhd.Shared -eq 'Y') # Get the Support Persistent Reservations $NewDataVHD.SupportPR = ($VMDataVhd.SupportPR -eq 'Y') # Validate the data disk type specified if ($VMDataVhd.Type) { switch ($VMDataVhd.Type) { 'fixed' { break; } 'dynamic' { break; } 'differencing' { if (-not $NewDataVHD.ParentVhd) { $exceptionParameters = @{ errorId = 'VMDataDiskParentVHDMissingError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMDataDiskParentVHDMissingError ` -f $VMName) } New-LabException @exceptionParameters } # if if ($NewDataVHD.Shared) { $exceptionParameters = @{ errorId = 'VMDataDiskSharedDifferencingError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMDataDiskSharedDifferencingError ` -f $VMName,$VHD) } New-LabException @exceptionParameters } # if } Default { $exceptionParameters = @{ errorId = 'VMDataDiskUnknownTypeError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMDataDiskUnknownTypeError ` -f $VMName,$VHD,$VMDataVhd.Type) } New-LabException @exceptionParameters } } # switch $NewDataVHD.VHDType = [LabVHDType]::$($VMDataVhd.Type) } # if # Get Partition Style for the new disk. if ($VMDataVhd.PartitionStyle) { $PartitionStyle = [LabPartitionStyle]::$($VMDataVhd.PartitionStyle) if (-not $PartitionStyle) { $exceptionParameters = @{ errorId = 'VMDataDiskPartitionStyleError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMDataDiskPartitionStyleError ` -f $VMName,$VHD,$VMDataVhd.PartitionStyle) } New-LabException @exceptionParameters } # if $NewDataVHD.PartitionStyle = $PartitionStyle } # if # Get file system for the new disk. if ($VMDataVhd.FileSystem) { $FileSystem = [LabFileSystem]::$($VMDataVhd.FileSystem) if (-not $FileSystem) { $exceptionParameters = @{ errorId = 'VMDataDiskFileSystemError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMDataDiskFileSystemError ` -f $VMName,$VHD,$VMDataVhd.FileSystem) } New-LabException @exceptionParameters } # if $NewDataVHD.FileSystem = $FileSystem } # if # Has a file system label been provided? if ($VMDataVhd.FileSystemLabel) { $NewDataVHD.FileSystemLabel = $VMDataVhd.FileSystemLabel } # if # if the Partition Style, File System or File System Label has been # provided then ensure Partition Style and File System are set. if ($NewDataVHD.PartitionStyle ` -or $NewDataVHD.FileSystem ` -or $NewDataVHD.FileSystemLabel) { if (-not $NewDataVHD.PartitionStyle) { $exceptionParameters = @{ errorId = 'VMDataDiskPartitionStyleMissingError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMDataDiskPartitionStyleMissingError ` -f $VMName,$VHD) } New-LabException @exceptionParameters } # if if (-not $NewDataVHD.FileSystem) { $exceptionParameters = @{ errorId = 'VMDataDiskFileSystemMissingError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMDataDiskFileSystemMissingError ` -f $VMName,$VHD) } New-LabException @exceptionParameters } # if } # if # Get the Folder to copy and check it exists if passed if ($VMDataVhd.CopyFolders) { foreach ($CopyFolder in ($VMDataVhd.CopyFolders -Split ',')) { # Adjust the path to be relative to the configuration folder # if it doesn't contain a root (e.g. c:\) if (-not [System.IO.Path]::IsPathRooted($CopyFolder)) { $CopyFolder = Join-Path ` -Path $Lab.labbuilderconfig.settings.fullconfigpath ` -ChildPath $CopyFolder } # if if (-not (Test-Path -Path $CopyFolder -Type Container)) { $exceptionParameters = @{ errorId = 'VMDataDiskCopyFolderMissingError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMDataDiskCopyFolderMissingError ` -f $VMName,$VHD,$CopyFolder) } New-LabException @exceptionParameters } } # foreach $NewDataVHD.CopyFolders = $VMDataVhd.CopyFolders } # if # Should the Source VHD be moved rather than copied if ($VMDataVhd.MoveSourceVHD) { $NewDataVHD.MoveSourceVHD = ($VMDataVhd.MoveSourceVHD -eq 'Y') if (-not $NewDataVHD.SourceVHD) { $exceptionParameters = @{ errorId = 'VMDataDiskSourceVHDIfMoveError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMDataDiskSourceVHDIfMoveError ` -f $VMName,$VHD) } New-LabException @exceptionParameters } # if } # if # if the data disk file doesn't exist then some basic parameters MUST be provided if (-not $Exists ` -and ( ( ( -not $NewDataVHD.VhdType ) -or ( $NewDataVHD.Size -eq 0) ) ` -and -not $NewDataVHD.SourceVhd ) ) { $exceptionParameters = @{ errorId = 'VMDataDiskCantBeCreatedError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMDataDiskCantBeCreatedError ` -f $VMName,$VHD) } New-LabException @exceptionParameters } # if $DataVHDs += @( $NewDataVHD ) } # foreach # Assemble the DVD Drives this VM will use [LabDVDDrive[]] $DVDDrives = @() [System.Int32] $DVDDriveCount = 0 foreach ($VMDVDDrive in $VM.DVDDrives.DVDDrive) { $DVDDriveCount++ # Create the new DVD Drive object $NewDVDDrive = [LabDVDDRive]::New() # Load all the DVD Drive properties and check they are valid if ($VMDVDDrive.ISO) { # Look the ISO up in the ISO Resources # Pull the list of Resource ISOs available if not already pulled from Lab. if (-not $ResourceISOs) { $ResourceISOs = Get-LabResourceISO ` -Lab $Lab } # if # Lookup the Resource ISO record $ResourceISO = $ResourceISOs | Where-Object -Property Name -eq $VMDVDDrive.ISO if (-not $ResourceISO) { # The ISO Resource was not found $exceptionParameters = @{ errorId = 'VMDVDDriveISOResourceNotFOundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMDVDDriveISOResourceNotFOundError ` -f $VMName,$VMDVDDrive.ISO) } New-LabException @exceptionParameters } # if # The ISO resource was found so populate the ISO details $NewDVDDrive.ISO = $VMDVDDrive.ISO $NewDVDDrive.Path = $ResourceISO.Path } # if $DVDDrives += @( $NewDVDDrive ) } # foreach # Does the VM have an Unattend file specified? [System.String] $UnattendFile = '' if ($VM.UnattendFile) { if ([System.IO.Path]::IsPathRooted($VM.UnattendFile)) { $UnattendFile = $VM.UnattendFile } else { $UnattendFile = Join-Path ` -Path $Lab.labbuilderconfig.settings.fullconfigpath ` -ChildPath $VM.UnattendFile } # if if (-not (Test-Path $UnattendFile)) { $exceptionParameters = @{ errorId = 'UnattendFileMissingError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.UnattendFileMissingError ` -f $VMName,$UnattendFile) } New-LabException @exceptionParameters } # if } # if # Does the VM specify a Setup Complete Script? [System.String] $SetupComplete = '' if ($VM.SetupComplete) { if ([System.IO.Path]::IsPathRooted($VM.SetupComplete)) { $SetupComplete = $VM.SetupComplete } else { $SetupComplete = Join-Path ` -Path $Lab.labbuilderconfig.settings.fullconfigpath ` -ChildPath $VM.SetupComplete } # if if ([System.IO.Path]::GetExtension($SetupComplete).ToLower() -notin '.ps1','.cmd' ) { $exceptionParameters = @{ errorId = 'SetupCompleteFileBadTypeError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.SetupCompleteFileBadTypeError ` -f $VMName,$SetupComplete) } New-LabException @exceptionParameters } # if if (-not (Test-Path $SetupComplete)) { $exceptionParameters = @{ errorId = 'SetupCompleteFileMissingError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.SetupCompleteFileMissingError ` -f $VMName,$SetupComplete) } New-LabException @exceptionParameters } # if } # if # Create the Lab DSC object $LabDSC = [LabDSC]::New($VM.DSC.ConfigName) # Load the DSC Config File setting and check it [System.String] $LabDSC.ConfigFile = '' if ($VM.DSC.ConfigFile) { if (-not [System.IO.Path]::IsPathRooted($VM.DSC.ConfigFile)) { $LabDSC.ConfigFile = Join-Path ` -Path $Lab.labbuilderconfig.settings.dsclibrarypathfull ` -ChildPath $VM.DSC.ConfigFile } else { $LabDSC.ConfigFile = $VM.DSC.ConfigFile } # if if ([System.IO.Path]::GetExtension($LabDSC.ConfigFile).ToLower() -ne '.ps1' ) { $exceptionParameters = @{ errorId = 'DSCConfigFileBadTypeError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.DSCConfigFileBadTypeError ` -f $VMName,$LabDSC.ConfigFile) } New-LabException @exceptionParameters } # if if (-not (Test-Path $LabDSC.ConfigFile)) { $exceptionParameters = @{ errorId = 'DSCConfigFileMissingError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.DSCConfigFileMissingError ` -f $VMName,$LabDSC.ConfigFile) } New-LabException @exceptionParameters } # if if (-not $VM.DSC.ConfigName) { $exceptionParameters = @{ errorId = 'DSCConfigNameIsEmptyError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.DSCConfigNameIsEmptyError ` -f $VMName) } New-LabException @exceptionParameters } # if } # if # Load the DSC Parameters [System.String] $LabDSC.Parameters = '' if ($VM.DSC.Parameters) { # Correct any LFs into CRLFs to ensure the new line format is the same when # pulled from the XML. $LabDSC.Parameters = ($VM.DSC.Parameters -replace "`r`n","`n") -replace "`n","`r`n" } # if # Load the DSC Parameters [System.Boolean] $LabDSC.Logging = ($VM.DSC.Logging -eq 'Y') # Get the Memory Startup Bytes (from the template or VM) [Int64] $MemoryStartupBytes = 1GB if ($VM.memorystartupbytes) { $MemoryStartupBytes = (Invoke-Expression $VM.memorystartupbytes) } elseif ($VMTemplate.memorystartupbytes) { $MemoryStartupBytes = $VMTemplate.memorystartupbytes } # if # Get the Dynamic Memory Enabled flag [System.Boolean] $DynamicMemoryEnabled = $true if ($VM.DynamicMemoryEnabled) { $DynamicMemoryEnabled = ($VM.DynamicMemoryEnabled -eq 'Y') } elseif ($VMTemplate.DynamicMemoryEnabled) { $DynamicMemoryEnabled = $VMTemplate.DynamicMemoryEnabled } # if # Get the Number of vCPUs (from the template or VM) [System.Int32] $ProcessorCount = 1 if ($VM.processorcount) { $ProcessorCount = (Invoke-Expression $VM.processorcount) } elseif ($VMTemplate.processorcount) { $ProcessorCount = $VMTemplate.processorcount } # if # Get the Expose Virtualization Extensions flag if ($VM.ExposeVirtualizationExtensions) { $ExposeVirtualizationExtensions = ($VM.ExposeVirtualizationExtensions -eq 'Y') } elseif ($VMTemplate.ExposeVirtualizationExtensions) { $ExposeVirtualizationExtensions = $VMTemplate.ExposeVirtualizationExtensions } # if # If VM requires ExposeVirtualizationExtensions but # it is not supported on Host then throw an exception. if ($ExposeVirtualizationExtensions -and ($Script:CurrentBuild -lt 10565)) { $exceptionParameters = @{ errorId = 'VMVirtualizationExtError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMVirtualizationExtError ` -f $VMName) } New-LabException @exceptionParameters } # if [System.Boolean] $UseDifferencingDisk = $true if ($VM.UseDifferencingDisk -eq 'N') { $UseDifferencingDisk = $false } # if # Get the Integration Services flags if ($null -ne $VM.IntegrationServices) { $IntegrationServices = $VM.IntegrationServices } elseif ($null -ne $VMTemplate.IntegrationServices) { $IntegrationServices = $VMTemplate.IntegrationServices } # if # Get the Administrator password (from the template or VM) [System.String] $AdministratorPassword = '' if ($VM.administratorpassword) { $AdministratorPassword = $VM.administratorpassword } elseif ($VMTemplate.administratorpassword) { $AdministratorPassword = $VMTemplate.administratorpassword } # if # Get the Product Key (from the template or VM) [System.String] $ProductKey = '' if ($VM.productkey) { $ProductKey = $VM.productkey } elseif ($VMTemplate.productkey) { $ProductKey = $VMTemplate.productkey } # if # Get the Timezone (from the template or VM) [System.String] $Timezone = 'Pacific Standard Time' if ($VM.timezone) { $Timezone = $VM.timezone } elseif ($VMTemplate.timezone) { $Timezone = $VMTemplate.timezone } # if # Get the OS Type $OSType = [LabOStype]::Server if ($VM.OSType) { $OSType = $VM.OSType } elseif ($VMTemplate.OSType) { $OSType = $VMTemplate.OSType } # if # Get the Bootorder [Byte] $Bootorder = [Byte]::MaxValue if ($VM.bootorder) { $Bootorder = $VM.bootorder } # if # Get the Packages [System.String] $Packages = $null if ($VM.packages) { $Packages = $VM.packages } elseif ($VMTemplate.packages) { $Packages = $VMTemplate.packages } # if # Get the Version (from the template or VM) [System.String] $Version = '8.0' if ($VM.version) { $Version = $VM.version } elseif ($VMTemplate.version) { $Version = $VMTemplate.version } # if # Get the Generation (from the template or VM) [System.String] $Generation = 2 if ($VM.generation) { $Generation = $VM.generation } elseif ($VMTemplate.generation) { $Generation = $VMTemplate.generation } # if # Get the Certificate Source $CertificateSource = [LabCertificateSource]::Guest if ($OSType -eq [LabOSType]::Nano) { # Nano Server can't generate certificates so must always be set to Host $CertificateSource = [LabCertificateSource]::Host } elseif ($VM.CertificateSource) { $CertificateSource = $VM.CertificateSource } # if $LabVM = [LabVM]::New($VMName,$ComputerName) $LabVM.Template = $VM.Template $LabVM.ParentVHD = $ParentVHDPath $LabVM.UseDifferencingDisk = $UseDifferencingDisk $LabVM.MemoryStartupBytes = $MemoryStartupBytes $LabVM.DynamicMemoryEnabled = $DynamicMemoryEnabled $LabVM.ProcessorCount = $ProcessorCount $LabVM.ExposeVirtualizationExtensions = $ExposeVirtualizationExtensions $LabVM.IntegrationServices = $IntegrationServices $LabVM.AdministratorPassword = $AdministratorPassword $LabVM.ProductKey = $ProductKey $LabVM.TimeZone =$Timezone $LabVM.UnattendFile = $UnattendFile $LabVM.SetupComplete = $SetupComplete $LabVM.OSType = $OSType $LabVM.CertificateSource = $CertificateSource $LabVM.Bootorder = $Bootorder $LabVM.Packages = $Packages $LabVM.Version = $Version $LabVM.Generation = $Generation $LabVM.Adapters = $VMAdapters $LabVM.DataVHDs = $DataVHDs $LabVM.DVDDrives = $DVDDrives $LabVM.DSC = $LabDSC $LabVM.NanoODJPath = $NanoODJPath $LabVM.VMRootPath = Join-Path ` -Path $LabPath ` -ChildPath $VMName $LabVM.LabBuilderFilesPath = Join-Path ` -Path $LabPath ` -ChildPath "$VMName\LabBuilder Files" $LabVMs += @( $LabVM ) } # foreach } # foreach Return $LabVMs } # Get-LabVM function Get-LabVMTemplate { [OutputType([LabVMTemplate[]])] [CmdLetBinding()] param ( [Parameter( Position = 1, Mandatory = $true)] [ValidateNotNullOrEmpty()] $Lab, [Parameter( Position = 2)] [ValidateNotNullOrEmpty()] [System.String[]] $Name, [Parameter( Position = 3)] [LabVMTemplateVHD[]] $VMTemplateVHDs ) # if VMTeplateVHDs array not passed, pull it from config. if (-not $PSBoundParameters.ContainsKey('VMTemplateVHDs')) { [LabVMTemplateVHD[]] $VMTemplateVHDs = Get-LabVMTemplateVHD ` -Lab $Lab } # if [LabVMTemplate[]] $VMTemplates = @() [System.String] $VHDParentPath = $Lab.labbuilderconfig.settings.vhdparentpathfull # Get a list of all templates in the Hyper-V system matching the phrase found in the fromvm # config setting [System.String] $FromVM = $Lab.labbuilderconfig.templates.fromvm if ($FromVM) { $Templates = @(Get-VM -Name $FromVM) foreach ($Template in $Templates) { if ($Name -and ($Template.Name -notin $Name)) { # A names list was passed but this VM Template wasn't included continue } # if [System.String] $VHDFilepath = (Get-VMHardDiskDrive -VMName $Template.Name).Path [System.String] $VHDFilename = [System.IO.Path]::GetFileName($VHDFilepath) [LabVMTemplate] $VMTemplate = [LabVMTemplate]::New($Template.Name) $VMTemplate.Vhd = $VHDFilename $VMTemplate.SourceVhd = $VHDFilepath $VMTemplate.ParentVhd = (Join-Path -Path $VHDParentPath -ChildPath $VHDFilename) $VMTemplates += @( $VMTemplate ) } # foreach } # if # Read the list of templates from the configuration file $Templates = $Lab.labbuilderconfig.templates.template foreach ($Template in $Templates) { # It can't be template because if the name attrib/node is missing the name property on # the XML object defaults to the name of the parent. So we can't easily tell if no name # was specified or if they actually specified 'template' as the name. $TemplateName = $Template.Name if ($Name -and ($TemplateName -notin $Name)) { # A names list was passed but this VM Template wasn't included continue } # if if ($TemplateName -eq 'template') { $exceptionParameters = @{ errorId = 'EmptyTemplateNameError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.EmptyTemplateNameError) } New-LabException @exceptionParameters } # if # Does the template already exist in the list? [System.Boolean] $Found = $false foreach ($VMTemplate in $VMTemplates) { if ($VMTemplate.Name -eq $TemplateName) { # The template already exists - so don't add it again $Found = $true Break } # if } # foreach if (-not $Found) { # The template wasn't found in the list of templates so add it $VMTemplate = [LabVMTemplate]::New($TemplateName) # Add the new Template to the Templates Array $VMTemplates += @( $VMTemplate ) } # if # Determine the Source VHD, Template VHD and VHD [System.String] $SourceVHD = $Template.SourceVHD [System.String] $TemplateVHD = $Template.TemplateVHD # Throw an error if both a TemplateVHD and SourceVHD are provided if ($TemplateVHD -and $SourceVHD) { $exceptionParameters = @{ errorId = 'TemplateSourceVHDAndTemplateVHDConflictError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.TemplateSourceVHDAndTemplateVHDConflictError ` -f $TemplateName) } New-LabException @exceptionParameters } # if if ($TemplateVHD) { # A TemplateVHD was provided so look it up. $VMTemplateVHD = ` $VMTemplateVHDs | Where-Object -Property Name -EQ $TemplateVHD if ($VMTemplateVHD) { # The TemplateVHD was found $VMTemplate.Sourcevhd = $VMTemplateVHD.VHDPath # if a VHD filename wasn't specified in the TemplateVHD # Just use the leaf of the SourceVHD if ($VMTemplateVHD.VHD) { $VMTemplate.Vhd = $VMTemplateVHD.VHD } else { $VMTemplate.Vhd = Split-Path ` -Path $VMTemplate.sourcevhd ` -Leaf } # if } else { # The TemplateVHD could not be found in the list $exceptionParameters = @{ errorId = 'TemplateTemplateVHDNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.TemplateTemplateVHDNotFoundError ` -f $TemplateName, $TemplateVHD) } New-LabException @exceptionParameters } # if } elseif ($SourceVHD) { # A Source VHD was provided so use that. # if this is a relative path, add it to the config path if ([System.IO.Path]::IsPathRooted($SourceVHD)) { $VMTemplate.SourceVhd = $SourceVHD } else { $VMTemplate.SourceVhd = Join-Path ` -Path $Lab.labbuilderconfig.settings.fullconfigpath ` -ChildPath $SourceVHD } # A Source VHD file was specified - does it exist? if (-not (Test-Path -Path $VMTemplate.sourcevhd)) { $exceptionParameters = @{ errorId = 'TemplateSourceVHDNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.TemplateSourceVHDNotFoundError ` -f $TemplateName, $VMTemplate.sourcevhd) } New-LabException @exceptionParameters } # if # if a VHD filename wasn't specified in the Template # Just use the leaf of the SourceVHD if ($Template.VHD) { $VMTemplate.vhd = $Template.VHD } else { $VMTemplate.vhd = Split-Path ` -Path $VMTemplate.sourcevhd ` -Leaf } # if } elseif ($VMTemplate.SourceVHD) { # A SourceVHD is already set # Usually because it was pulled From a Hyper-V VM template. } else { # Neither a SourceVHD or TemplateVHD was provided # So throw an exception $exceptionParameters = @{ errorId = 'TemplateSourceVHDandTemplateVHDMissingError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.TemplateSourceVHDandTemplateVHDMissingError ` -f $TemplateName) } New-LabException @exceptionParameters } # if # Ensure the ParentVHD is up-to-date $VMTemplate.parentvhd = Join-Path ` -Path $VHDParentPath ` -ChildPath ([System.IO.Path]::GetFileName($VMTemplate.vhd)) # Write any template specific default VM attributes [Int64] $MemoryStartupBytes = 1GB if ($Template.MemoryStartupBytes) { $MemoryStartupBytes = (Invoke-Expression $Template.MemoryStartupBytes) } # if if ($MemoryStartupBytes -gt 0) { $VMTemplate.memorystartupbytes = $MemoryStartupBytes } # if if ($Template.DynamicMemoryEnabled) { $VMTemplate.DynamicMemoryEnabled = ($Template.DynamicMemoryEnabled -eq 'Y') } elseif (-not $VMTemplate.DynamicMemoryEnabled) { $VMTemplate.DynamicMemoryEnabled = $true } # if if ($Template.version) { $VMTemplate.version = $Template.version } elseif (-not $Template.version) { $VMTemplate.version = "8.0" } # if if ($Template.generation) { $VMTemplate.generation = $Template.generation } elseif (-not $Template.generation) { $VMTemplate.generation = 2 } # if if ($Template.ProcessorCount) { $VMTemplate.ProcessorCount = $Template.ProcessorCount } # if if ($Template.ExposeVirtualizationExtensions) { $VMTemplate.ExposeVirtualizationExtensions = ($Template.ExposeVirtualizationExtensions -eq 'Y') } # if if ($Template.AdministratorPassword) { $VMTemplate.AdministratorPassword = $Template.AdministratorPassword } # if if ($Template.ProductKey) { $VMTemplate.ProductKey = $Template.ProductKey } # if if ($Template.TimeZone) { $VMTemplate.TimeZone = $Template.TimeZone } # if if ($Template.OSType) { $VMTemplate.OSType = [LabOSType]::$($Template.OSType) } elseif (-not $VMTemplate.OSType) { $VMTemplate.OSType = [LabOStype]::Server } # if if ($Template.IntegrationServices) { $VMTemplate.IntegrationServices = $Template.IntegrationServices } else { $VMTemplate.IntegrationServices = $null } # if if ($Template.Packages) { $VMTemplate.Packages = $Template.Packages } else { $VMTemplate.Packages = $null } # if } # foreach Return $VMTemplates } # Get-LabVMTemplate function Get-LabVMTemplateVHD { [OutputType([LabVMTemplateVHD[]])] [CmdLetBinding()] param ( [Parameter ( Position = 1, Mandatory = $true)] [ValidateNotNullOrEmpty()] $Lab, [Parameter( Position = 2)] [ValidateNotNullOrEmpty()] [System.String[]] $Name ) # return null if the TemplateVHDs node does not exist if (-not $Lab.labbuilderconfig.TemplateVHDs) { return } # Determine the ISORootPath where the ISO files should be found # if no path is specified then look in the same path as the config # if a path is specified but it is relative, make it relative to the # config path. Otherwise use it as is. [System.String] $ISORootPath = $Lab.labbuilderconfig.TemplateVHDs.ISOPath if (-not $ISORootPath) { $ISORootPath = $Lab.labbuilderconfig.settings.fullconfigpath } else { if (-not [System.IO.Path]::IsPathRooted($ISORootPath)) { $ISORootPath = Join-Path ` -Path $Lab.labbuilderconfig.settings.fullconfigpath ` -ChildPath $ISORootPath } # if } # if if (-not (Test-Path -Path $ISORootPath -Type Container)) { $exceptionParameters = @{ errorId = 'VMTemplateVHDISORootPathNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMTemplateVHDISORootPathNotFoundError ` -f $ISORootPath) } New-LabException @exceptionParameters } # if # Determine the VHDRootPath where the VHD files should be put # if no path is specified then look in the same path as the config # if a path is specified but it is relative, make it relative to the # config path. Otherwise use it as is. [System.String] $VHDRootPath = $Lab.labbuilderconfig.TemplateVHDs.VHDPath if (-not $VHDRootPath) { $VHDRootPath = $Lab.labbuilderconfig.settings.fullconfigpath } else { if (-not [System.IO.Path]::IsPathRooted($VHDRootPath)) { $VHDRootPath = Join-Path ` -Path $Lab.labbuilderconfig.settings.fullconfigpath ` -ChildPath $VHDRootPath } # if } # if if (-not (Test-Path -Path $VHDRootPath -Type Container)) { $exceptionParameters = @{ errorId = 'VMTemplateVHDRootPathNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMTemplateVHDRootPathNotFoundError ` -f $VHDRootPath) } New-LabException @exceptionParameters } # if $TemplatePrefix = $Lab.labbuilderconfig.templatevhds.prefix # Read the list of templateVHD from the configuration file $TemplateVHDs = $Lab.labbuilderconfig.templatevhds.templatevhd [LabVMTemplateVHD[]] $VMTemplateVHDs = @() foreach ($TemplateVHD in $TemplateVHDs) { # It can't be template because if the name attrib/node is missing the name property on # the XML object defaults to the name of the parent. So we can't easily tell if no name # was specified or if they actually specified 'templatevhd' as the name. $TemplateVHDName = $TemplateVHD.Name if ($Name -and ($TemplateVHDName -notin $Name)) { # A names list was passed but this VM Template VHD wasn't included continue } # if if (($TemplateVHDName -eq 'TemplateVHD') ` -or ([System.String]::IsNullOrWhiteSpace($TemplateVHDName))) { $exceptionParameters = @{ errorId = 'EmptyVMTemplateVHDNameError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.EmptyVMTemplateVHDNameError) } New-LabException @exceptionParameters } # if # Get the ISO Path [System.String] $ISOPath = $TemplateVHD.ISO if (-not $ISOPath) { $exceptionParameters = @{ errorId = 'EmptyVMTemplateVHDISOPathError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.EmptyVMTemplateVHDISOPathError ` -f $TemplateVHD.Name) } New-LabException @exceptionParameters } # if # Adjust the ISO Path if required if (-not [System.IO.Path]::IsPathRooted($ISOPath)) { $ISOPath = Join-Path ` -Path $ISORootPath ` -ChildPath $ISOPath } # if # Does the ISO Exist? if (-not (Test-Path -Path $ISOPath)) { $URL = $TemplateVHD.URL if ($URL) { Write-LabMessage ` -Type Alert ` -Message $($LocalizedData.ISONotFoundDownloadURLMessage ` -f $TemplateVHD.Name, $ISOPath, $URL) } # if $exceptionParameters = @{ errorId = 'VMTemplateVHDISOPathNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMTemplateVHDISOPathNotFoundError ` -f $TemplateVHD.Name, $ISOPath) } New-LabException @exceptionParameters } # if # Get the VHD Path [System.String] $VHDPath = $TemplateVHD.VHD if (-not $VHDPath) { $exceptionParameters = @{ errorId = 'EmptyVMTemplateVHDPathError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.EmptyVMTemplateVHDPathError ` -f $TemplateVHD.Name) } New-LabException @exceptionParameters } # if # Adjust the VHD Path if required if (-not [System.IO.Path]::IsPathRooted($VHDPath)) { $VHDPath = Join-Path ` -Path $VHDRootPath ` -ChildPath $VHDPath } # if # Add the template prefix to the VHD name. if (-not ([System.String]::IsNullOrWhitespace($TemplatePrefix))) { $VHDPath = Join-Path ` -Path (Split-Path -Path $VHDPath)` -ChildPath ("$TemplatePrefix$(Split-Path -Path $VHDPath -Leaf)") } # if # Get the Template OS Type $OSType = [LabOStype]::Server if ($TemplateVHD.OSType) { $OSType = [LabOStype]::$($TemplateVHD.OSType) } # if if (-not $OSType) { $exceptionParameters = @{ errorId = 'InvalidVMTemplateVHDOSTypeError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.InvalidVMTemplateVHDOSTypeError ` -f $TemplateVHD.Name, $TemplateVHD.OSType) } New-LabException @exceptionParameters } # if # Get the Template Wim Image to use $Edition = $null if ($TemplateVHD.Edition) { $Edition = $TemplateVHD.Edition } # if # Get the Template VHD Format $VHDFormat = [LabVHDFormat]::VHDx if ($TemplateVHD.VHDFormat) { $VHDFormat = [LabVHDFormat]::$($TemplateVHD.VHDFormat) } # if if (-not $VHDFormat) { $exceptionParameters = @{ errorId = 'InvalidVMTemplateVHDVHDFormatError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.InvalidVMTemplateVHDVHDFormatError ` -f $TemplateVHD.Name, $TemplateVHD.VHDFormat) } New-LabException @exceptionParameters } # Get the Template VHD Type $VHDType = [LabVHDType]::Dynamic if ($TemplateVHD.VHDType) { $VHDType = [LabVHDType]::$($TemplateVHD.VHDType) } # if if (-not $VHDType) { $exceptionParameters = @{ errorId = 'InvalidVMTemplateVHDVHDTypeError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.InvalidVMTemplateVHDVHDTypeError ` -f $TemplateVHD.Name, $TemplateVHD.VHDType) } New-LabException @exceptionParameters } # if # Get the disk size if provided [Int64] $VHDSize = 25GB if ($TemplateVHD.VHDSize) { $VHDSize = (Invoke-Expression $TemplateVHD.VHDSize) } # if # Get the Template VM Generation [System.Int32] $Generation = 2 if ($TemplateVHD.Generation) { $Generation = $TemplateVHD.Generation } # if if ($Generation -notin @(1, 2) ) { $exceptionParameters = @{ errorId = 'InvalidVMTemplateVHDGenerationError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.InvalidVMTemplateVHDGenerationError ` -f $TemplateVHD.Name, $Generation) } New-LabException @exceptionParameters } # Get the Template Packages if ($TemplateVHD.packages) { $Packages = $TemplateVHD.Packages } # if # Get the Template Features if ($TemplateVHD.features) { $Features = $TemplateVHD.Features } # if # Add template VHD to the list $NewVMTemplateVHD = [LabVMTemplateVHD]::New($TemplateVHDName) $NewVMTemplateVHD.ISOPath = $ISOPath $NewVMTemplateVHD.VHDPath = $VHDPath $NewVMTemplateVHD.OSType = $OSType $NewVMTemplateVHD.Edition = $Edition $NewVMTemplateVHD.Generation = $Generation $NewVMTemplateVHD.VHDFormat = $VHDFormat $NewVMTemplateVHD.VHDType = $VHDType $NewVMTemplateVHD.VHDSize = $VHDSize $NewVMTemplateVHD.Packages = $Packages $NewVMTemplateVHD.Features = $Features $VMTemplateVHDs += @( $NewVMTemplateVHD ) } # foreach Return $VMTemplateVHDs } # Get-LabVMTemplateVHD function Initialize-LabResourceISO { [CmdLetBinding()] param ( [Parameter( Position = 1, Mandatory = $true)] [ValidateNotNullOrEmpty()] $Lab, [Parameter( Position = 2)] [ValidateNotNullOrEmpty()] [System.String[]] $Name, [Parameter( Position = 3)] [LabResourceISO[]] $ResourceISOs ) # if resource ISOs was not passed, pull it. if (-not $PSBoundParameters.ContainsKey('resourceisos')) { $ResourceMSUs = Get-LabResourceISO ` @PSBoundParameters } # if if ($ResourceISOs) { foreach ($ResourceISO in $ResourceISOs) { if (-not (Test-Path -Path $ResourceISO.Path)) { # The Resource ISO does not exist if (-not ($ResourceISO.URL)) { $exceptionParameters = @{ errorId = 'ResourceISOFileNotFoundAndNoURLError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.ResourceISOFileNotFoundAndNoURLError ` -f $ISOName, $Path) } New-LabException @exceptionParameters } # if $URLLeaf = [System.IO.Path]::GetFileName($ResourceISO.URL) $URLExtension = [System.IO.Path]::GetExtension($URLLeaf) if ($URLExtension -in @('.zip', '.iso')) { Write-LabMessage -Message $($LocalizedData.DownloadingResourceISOMessage ` -f $ResourceISO.Name, $ResourceISO.URL) Invoke-LabDownloadAndUnzipFile ` -URL $ResourceISO.URL ` -DestinationPath (Split-Path -Path $ResourceISO.Path) } elseif ([System.String]::IsNullOrEmpty($URLExtension)) { Write-LabMessage ` -Type Alert ` -Message $($LocalizedData.ISONotFoundDownloadURLMessage ` -f $ResourceISO.Name, $ResourceISO.Path, $ResourceISO.URL) } # if if (-not (Test-Path -Path $ResourceISO.Path)) { $exceptionParameters = @{ errorId = 'ResourceISOFileNotDownloadedError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.ResourceISOFileNotDownloadedError ` -f $ResourceISO.Name, $ResourceISO.Path, $ResourceISO.URL) } New-LabException @exceptionParameters } # if } # if } # foreach } # if } # Initialize-LabResourceISO function Initialize-LabResourceModule { [CmdLetBinding()] param ( [Parameter( Position = 1, Mandatory = $true)] [ValidateNotNullOrEmpty()] $Lab, [Parameter( Position = 2)] [ValidateNotNullOrEmpty()] [System.String[]] $Name, [Parameter( Position = 3)] [LabResourceModule[]] $ResourceModules ) # if resource modules was not passed, pull it. if (-not $PSBoundParameters.ContainsKey('resourcemodules')) { $ResourceModules = Get-LabResourceModule ` @PSBoundParameters } if ($ResourceModules) { foreach ($Module in $ResourceModules) { $Splat = [PSObject] @{ Name = $Module.Name } if ($Module.URL) { $Splat += [PSObject] @{ URL = $Module.URL } } if ($Module.Folder) { $Splat += [PSObject] @{ Folder = $Module.Folder } } if ($Module.RequiredVersion) { $Splat += [PSObject] @{ RequiredVersion = $Module.RequiredVersion } } if ($Module.MiniumVersion) { $Splat += [PSObject] @{ MiniumVersion = $Module.MiniumVersion } } Write-LabMessage -Message $($LocalizedData.DownloadingResourceModuleMessage ` -f $Name, $URL) Invoke-LabDownloadResourceModule @Splat } # foreach } # if } # Initialize-LabResourceModule function Initialize-LabResourceMSU { [CmdLetBinding()] param ( [Parameter( Position = 1, Mandatory = $true)] [ValidateNotNullOrEmpty()] $Lab, [Parameter( Position = 2)] [ValidateNotNullOrEmpty()] [System.String[]] $Name, [Parameter( Position = 3)] [LabResourceMSU[]] $ResourceMSUs ) # if resource MSUs was not passed, pull it. if (-not $PSBoundParameters.ContainsKey('resourcemsus')) { $ResourceMSUs = Get-LabResourceMSU ` @PSBoundParameters } if ($ResourceMSUs) { foreach ($MSU in $ResourceMSUs) { if (-not (Test-Path -Path $MSU.Filename)) { Write-LabMessage -Message $($LocalizedData.DownloadingResourceMSUMessage ` -f $MSU.Name, $MSU.URL) Invoke-LabDownloadAndUnzipFile ` -URL $MSU.URL ` -DestinationPath (Split-Path -Path $MSU.Filename) } # if } # foreach } # if } # Initialize-LabResourceMSU function Initialize-LabSwitch { [CmdLetBinding()] param ( [Parameter( Position = 1, Mandatory = $true)] [ValidateNotNullOrEmpty()] $Lab, [Parameter( Position = 2)] [ValidateNotNullOrEmpty()] [System.String[]] $Name, [Parameter( Position = 3)] [LabSwitch[]] $Switches ) # if switches was not passed, pull it. if (-not $PSBoundParameters.ContainsKey('switches')) { [LabSwitch[]] $Switches = Get-LabSwitch ` @PSBoundParameters } # Create Hyper-V Switches foreach ($VMSwitch in $Switches) { if ($Name -and ($VMSwitch.name -notin $Name)) { # A names list was passed but this swtich wasn't included continue } # if if ((Get-VMSwitch | Where-Object -Property Name -eq $($VMSwitch.Name)).Count -eq 0) { [System.String] $SwitchName = $VMSwitch.Name if (-not $SwitchName) { $exceptionParameters = @{ errorId = 'SwitchNameIsEmptyError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.SwitchNameIsEmptyError) } New-LabException @exceptionParameters } [LabSwitchType] $SwitchType = $VMSwitch.Type Write-LabMessage -Message $($LocalizedData.CreatingVirtualSwitchMessage ` -f $SwitchType, $SwitchName) Switch ($SwitchType) { 'External' { # Determine which Physical Adapter to bind this switch to if ($VMSwitch.BindingAdapterMac) { $BindingAdapter = Get-NetAdapter | Where-Object { ($_.MacAddress -replace '-', '') -eq $VMSwitch.BindingAdapterMac } $ErrorDetail = "with a MAC address '$($VMSwitch.BindingAdapterMac)' " } elseif ($VMSwitch.BindingAdapterName) { $BindingAdapter = Get-NetAdapter ` -Name $VMSwitch.BindingAdapterName ` -ErrorAction SilentlyContinue $ErrorDetail = "with a name '$($VMSwitch.BindingAdapterName)' " } else { $BindingAdapter = Get-NetAdapter | ` Where-Object { ($_.Status -eq 'Up') ` -and (-not $_.Virtual) ` } | Select-Object -First 1 $ErrorDetail = '' } # if # Check that a Binding Adapter was found if (-not $BindingAdapter) { $exceptionParameters = @{ errorId = 'BindingAdapterNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.BindingAdapterNotFoundError ` -f $SwitchName, $ErrorDetail) } New-LabException @exceptionParameters } # if # Check this adapter is not already bound to a switch $VMSwitchNames = (Get-VMSwitch | Where-Object { $_.SwitchType -eq 'External' }).Name $MacAddress = @() foreach ($VmSwitchName in $VmSwitchNames) { $MacAddress += (Get-VMNetworkAdapter ` -ManagementOS ` -SwitchName $VmSwitchName ` -Name $VmSwitchName ` -ErrorAction SilentlyContinue).MacAddress } # foreach $UsedAdapters = @((Get-NetAdapter | Where-Object { ($_.MacAddress -replace '-', '') -in $MacAddress }).Name) if ($BindingAdapter.Name -in $UsedAdapters) { $exceptionParameters = @{ errorId = 'BindingAdapterUsedError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.BindingAdapterUsedError ` -f $SwitchName, $BindingAdapter.Name) } New-LabException @exceptionParameters } # if # Create the swtich $null = New-VMSwitch ` -Name $SwitchName ` -NetAdapterName $BindingAdapter.Name break } # 'External' 'Private' { $null = New-VMSwitch ` -Name $SwitchName ` -SwitchType Private Break } # 'Private' 'Internal' { $null = New-VMSwitch ` -Name $SwitchName ` -SwitchType Internal Break } # 'Internal' 'NAT' { if ($Script:CurrentBuild -lt 14295) { $exceptionParameters = @{ errorId = 'NatSwitchNotSupportedError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.NatSwitchNotSupportedError -f $SwitchName) } New-LabException @exceptionParameters } $NatSubnet = $VMSwitch.NatSubnet # Check Nat Subnet is set if (-not $NatSubnet) { $exceptionParameters = @{ errorId = 'NatSubnetEmptyError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.NatSubnetEmptyError ` -f $SwitchName) } New-LabException @exceptionParameters } # if # Ensure Nat Subnet looks valid if ($NatSubnet -notmatch '[0-9]+.[0-9]+.[0-9]+.[0-9]+/[0-9]+') { $exceptionParameters = @{ errorId = 'NatSubnetInvalidError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.NatSubnetInvalidError ` -f $SwitchName, $NatSubnet) } New-LabException @exceptionParameters } # if $NatSubnetComponents = ($NatSubnet -split '/') $NatSubnetAddress = $NatSubnetComponents[0] # Validate the Nat Subnet Address if (-not ([System.Net.Ipaddress]::TryParse($NatSubnetAddress, [ref]0))) { $exceptionParameters = @{ errorId = 'NatSubnetAddressInvalidError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.NatSubnetAddressInvalidError ` -f $SwitchName, $NatSubnetAddress) } New-LabException @exceptionParameters } # if # Validate the Nat Subnet Prefix Length [System.Int32] $NatSubnetPrefixLength = $NatSubnetComponents[1] if (($NatSubnetPrefixLength -lt 1) -or ($NatSubnetPrefixLength -gt 31)) { $exceptionParameters = @{ errorId = 'NatSubnetPrefixLengthInvalidError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.NatSubnetPrefixLengthInvalidError ` -f $SwitchName, $NatSubnetPrefixLength) } New-LabException @exceptionParameters } # if $NatGatewayAddress = $VMSwitch.NatGatewayAddress # Create the Internal Switch $null = New-VMSwitch ` -Name $SwitchName ` -SwitchType Internal ` -ErrorAction Stop # Set the IP Address on the default adapter connected to the NAT switch $MacAddress = (Get-VMNetworkAdapter ` -ManagementOS ` -SwitchName $SwitchName ` -Name $SwitchName ` -ErrorAction Stop).MacAddress if ([System.String]::IsNullOrEmpty($MacAddress)) { $exceptionParameters = @{ errorId = 'NatSwitchDefaultAdapterMacEmptyError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.NatSwitchDefaultAdapterMacEmptyError ` -f $SwitchName) } New-LabException @exceptionParameters } # if $Adapter = Get-NetAdapter | Where-Object { ($_.MacAddress -replace '-', '') -eq $MacAddress } if (-not $Adapter) { $exceptionParameters = @{ errorId = 'NatSwitchDefaultAdapterNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.NatSwitchDefaultAdapterNotFoundError ` -f $SwitchName) } New-LabException @exceptionParameters } $null = $Adapter | New-NetIPAddress ` -IPAddress $NatGatewayAddress ` -PrefixLength $NatSubnetPrefixLength ` -ErrorAction Stop # Does the NAT already exist? $NetNat = Get-NetNat ` -Name $SwitchName ` -ErrorAction SilentlyContinue if ($NetNat) { # If the NAT already exists, remove it so it can be recreated $null = $NetNat | Remove-NetNat -Confirm:$false } # Create the new NAT $null = New-NetNat ` -Name $SwitchName ` -InternalIPInterfaceAddressPrefix $NatSubnet ` -ErrorAction Stop Break } # 'NAT' Default { $exceptionParameters = @{ errorId = 'UnknownSwitchTypeError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.UnknownSwitchTypeError ` -f $SwitchType, $SwitchName) } New-LabException @exceptionParameters } } # switch if ($SwitchType -ne 'Private') { # Configure the VLan on the default Management Adapter $setLabSwitchAdapterParameters = @{ Name = $SwitchName SwitchName = $SwitchName } if ($VMSwitch.VLan) { $setLabSwitchAdapterParameters += @{ VlanId = $VMSwitch.Vlan } } # if Set-LabSwitchAdapter @setLabSwitchAdapterParameters # Add any management OS adapters to the switch if ($VMSwitch.Adapters) { foreach ($Adapter in $VMSwitch.Adapters) { $setLabSwitchAdapterParameters = @{ Name = $Adapter.Name SwitchName = $SwitchName } if ($Adapter.MacAddress) { $setLabSwitchAdapterParameters += @{ StaticMacAddress = $Adapter.MacAddress } } # if if ($VMSwitch.VLan) { $setLabSwitchAdapterParameters += @{ VlanId = $VMSwitch.Vlan } } # if Set-LabSwitchAdapter @setLabSwitchAdapterParameters } # foreach } # if } # if } # if } # foreach } # Initialize-LabSwitch function Initialize-LabVM { [CmdLetBinding()] param ( [Parameter( Position=1, Mandatory=$true)] [ValidateNotNullOrEmpty()] $Lab, [Parameter( Position=2)] [ValidateNotNullOrEmpty()] [System.String[]] $Name, [Parameter( Position=3)] [LabVM[]] $VMs ) # if VMs array not passed, pull it from config. if (-not $PSBoundParameters.ContainsKey('VMs')) { [LabVM[]] $VMs = Get-LabVM ` @PSBoundParameters } # if # if there are not VMs just return if (-not $VMs) { return } # if $CurrentVMs = Get-VM [System.String] $LabPath = $Lab.labbuilderconfig.settings.labpath # Figure out the name of the LabBuilder control switch $ManagementSwitchName = Get-LabManagementSwitchName ` -Lab $Lab if ($Lab.labbuilderconfig.switches.ManagementVlan) { [Int32] $ManagementVlan = $Lab.labbuilderconfig.switches.ManagementVlan } else { [Int32] $ManagementVlan = $Script:DefaultManagementVLan } # if foreach ($VM in $VMs) { if ($Name -and ($VM.Name -notin $Name)) { # A names list was passed but this VM wasn't included continue } # if # Get the root path of the VM [System.String] $VMRootPath = $VM.VMRootPath # Get the Virtual Machine Path [System.String] $VMPath = Join-Path ` -Path $VMRootPath ` -ChildPath 'Virtual Machines' # Get the Virtual Hard Disk Path [System.String] $VHDPath = Join-Path ` -Path $VMRootPath ` -ChildPath 'Virtual Hard Disks' # Get Path to LabBuilder files [System.String] $VMLabBuilderFiles = $VM.LabBuilderFilesPath if (($CurrentVMs | Where-Object -Property Name -eq $VM.Name).Count -eq 0) { Write-LabMessage -Message $($LocalizedData.CreatingVMMessage ` -f $VM.Name) # Make sure the appropriate folders exist Initialize-LabVMPath ` -VMPath $VMRootPath # Create the boot disk $VMBootDiskPath = "$VHDPath\$($VM.Name) Boot Disk.vhdx" if (-not (Test-Path -Path $VMBootDiskPath)) { if ($VM.UseDifferencingDisk) { Write-LabMessage -Message $($LocalizedData.CreatingVMDiskMessage ` -f $VM.Name,$VMBootDiskPath,'Differencing Boot') $null = New-VHD ` -Differencing ` -Path $VMBootDiskPath ` -ParentPath $VM.ParentVHD } else { Write-LabMessage -Message $($LocalizedData.CreatingVMDiskMessage ` -f $VM.Name,$VMBootDiskPath,'Boot') $null = Copy-Item ` -Path $VM.ParentVHD ` -Destination $VMBootDiskPath } # Create all the required initialization files for this VM New-LabVMInitializationFile ` -Lab $Lab ` -VM $VM # Because this is a new boot disk apply any required initialization Initialize-LabBootVHD ` -Lab $Lab ` -VM $VM ` -VMBootDiskPath $VMBootDiskPath } else { Write-LabMessage -Message $($LocalizedData.VMDiskAlreadyExistsMessage ` -f $VM.Name,$VMBootDiskPath,'Boot') } # if # Create New VM from settings if ($VM.Version -and ($Script:CurrentBuild -ge 14352)) { $null = New-VM ` -Name $VM.Name ` -MemoryStartupBytes $VM.MemoryStartupBytes ` -Generation $VM.Generation ` -Path $LabPath ` -VHDPath $VMBootDiskPath ` -Version $VM.Version } else { $null = New-VM ` -Name $VM.Name ` -MemoryStartupBytes $VM.MemoryStartupBytes ` -Generation $VM.Generation ` -Path $LabPath ` -VHDPath $VMBootDiskPath ` } # Remove the default network adapter created with the VM because we don't need it Remove-VMNetworkAdapter ` -VMName $VM.Name ` -Name 'Network Adapter' } # Set the processor count if different to default and if specified in config file if ($VM.ProcessorCount) { if ($VM.ProcessorCount -ne (Get-VM -Name $VM.Name).ProcessorCount) { Set-VM ` -Name $VM.Name ` -ProcessorCount $VM.ProcessorCount } # if } # if # Enable/Disable Dynamic Memory if ($VM.DynamicMemoryEnabled -ne (Get-VMMemory -VMName $VM.Name).DynamicMemoryEnabled) { Set-VMMemory ` -VMName $VM.Name ` -DynamicMemoryEnabled:$($VM.DynamicMemoryEnabled) } # if # Is ExposeVirtualizationExtensions supported? if ($Script:CurrentBuild -lt 10565) { # No, it is not supported - is it required by VM? if ($VM.ExposeVirtualizationExtensions) { # ExposeVirtualizationExtensions is required for this VM $exceptionParameters = @{ errorId = 'VMVirtualizationExtError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMVirtualizationExtError ` -f $VM.Name) } New-LabException @exceptionParameters } # if } else { # Yes, it is - is the setting different? if ($VM.ExposeVirtualizationExtensions ` -ne (Get-VMProcessor -VMName $VM.Name).ExposeVirtualizationExtensions) { if ($Script:CurrentBuild -ge 14352 -and ($VM.Version -eq "8.0")) { Set-VMSecurity ` -VMName $VM.Name ` -VirtualizationBasedSecurityOptOut $true } # if # Try and update it Set-VMProcessor ` -VMName $VM.Name ` -ExposeVirtualizationExtensions:$VM.ExposeVirtualizationExtensions ` -ErrorAction Stop } # if } # if # Enable/Disable the Integration Services Update-LabVMIntegrationService ` -VM $VM # Update the data disks for the VM Update-LabVMDataDisk ` -Lab $Lab ` -VM $VM # Update the DVD Drives for the VM Update-LabVMDvdDrive ` -Lab $Lab ` -VM $VM # Create/Update the Management Network Adapter if ((Get-VMNetworkAdapter -VMName $VM.Name | Where-Object -Property Name -EQ $ManagementSwitchName).Count -eq 0) { Write-LabMessage -Message $($LocalizedData.AddingVMNetworkAdapterMessage ` -f $VM.Name,$ManagementSwitchName,'Management') Add-VMNetworkAdapter ` -VMName $VM.Name ` -SwitchName $ManagementSwitchName ` -Name $ManagementSwitchName } $VMNetworkAdapter = Get-VMNetworkAdapter ` -VMName $VM.Name ` -Name $ManagementSwitchName $null = $VMNetworkAdapter | Set-VMNetworkAdapterVlan ` -Access ` -VlanId $ManagementVlan Write-LabMessage -Message $($LocalizedData.SettingVMNetworkAdapterVlanMessage ` -f $VM.Name,$ManagementSwitchName,'Management',$ManagementVlan) # Create any network adapters foreach ($VMAdapter in $VM.Adapters) { if ((Get-VMNetworkAdapter -VMName $VM.Name | Where-Object -Property Name -EQ $VMAdapter.Name).Count -eq 0) { Write-LabMessage -Message $($LocalizedData.AddingVMNetworkAdapterMessage ` -f $VM.Name,$VMAdapter.SwitchName,$VMAdapter.Name) Add-VMNetworkAdapter ` -VMName $VM.Name ` -SwitchName $VMAdapter.SwitchName ` -Name $VMAdapter.Name } # if $VMNetworkAdapter = Get-VMNetworkAdapter ` -VMName $VM.Name ` -Name $VMAdapter.Name if ($VMAdapter.VLan) { $null = $VMNetworkAdapter | Set-VMNetworkAdapterVlan ` -Access ` -VlanId $VMAdapter.VLan Write-LabMessage -Message $($LocalizedData.SettingVMNetworkAdapterVlanMessage ` -f $VM.Name,$VMAdapter.Name,'',$VMAdapter.VLan) } else { $null = $VMNetworkAdapter | Set-VMNetworkAdapterVlan ` -Untagged Write-LabMessage -Message $($LocalizedData.ClearingVMNetworkAdapterVlanMessage ` -f $VM.Name,$VMAdapter.Name,'') } # if if ([System.String]::IsNullOrWhitespace($VMAdapter.MACAddress)) { $null = $VMNetworkAdapter | Set-VMNetworkAdapter ` -DynamicMacAddress } else { $null = $VMNetworkAdapter | Set-VMNetworkAdapter ` -StaticMacAddress $VMAdapter.MACAddress } # if # Enable Device Naming if supported by VM version and generation if (((Get-Command -Name Set-VMNetworkAdapter).Parameters.ContainsKey('DeviceNaming')) -and (($VM.Version -ge "6.2") -and ($VM.Generation -eq 2))) { $null = $VMNetworkAdapter | Set-VMNetworkAdapter ` -DeviceNaming On } # if if ($VMAdapter.MACAddressSpoofing -ne $VMNetworkAdapter.MACAddressSpoofing) { $MACAddressSpoofing = if ($VMAdapter.MACAddressSpoofing) {'On'} else {'Off'} $null = $VMNetworkAdapter | Set-VMNetworkAdapter ` -MacAddressSpoofing $MACAddressSpoofing } # if } # foreach Install-LabVM ` -Lab $Lab ` -VM $VM } # foreach } # Initialize-LabVM function Initialize-LabVMTemplate { [CmdLetBinding()] param ( [Parameter( Position = 1, Mandatory = $true)] [ValidateNotNullOrEmpty()] $Lab, [Parameter( Position = 2)] [ValidateNotNullOrEmpty()] [System.String[]] $Name, [Parameter( Position = 3)] [LabVMTemplate[]] $VMTemplates, [Parameter( Position = 4)] [LabVMTemplateVHD[]] $VMTemplateVHDs ) # if VMTeplates array not passed, pull it from config. if (-not $PSBoundParameters.ContainsKey('VMTemplates')) { [LabVMTemplate[]] $VMTemplates = Get-LabVMTemplate ` @PSBoundParameters } [System.String] $LabPath = $Lab.labbuilderconfig.settings.labpath # Check each Parent VHD exists in the Parent VHDs folder for the # Lab. If it isn't, try and copy it from the SourceVHD # Location. foreach ($VMTemplate in $VMTemplates) { if ($Name -and ($VMTemplate.Name -notin $Name)) { # A names list was passed but this VM Template wasn't included continue } # if if (-not (Test-Path $VMTemplate.ParentVhd)) { # The Parent VHD isn't in the VHD Parent folder # so copy it there, optimize it and mark it read-only. if (-not (Test-Path $VMTemplate.SourceVhd)) { # The source VHD could not be found. $exceptionParameters = @{ errorId = 'TemplateSourceVHDNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.TemplateSourceVHDNotFoundError ` -f $VMTemplate.Name, $VMTemplate.sourcevhd) } New-LabException @exceptionParameters } Write-LabMessage -Message $($LocalizedData.CopyingTemplateSourceVHDMessage ` -f $VMTemplate.SourceVhd, $VMTemplate.ParentVhd) Copy-Item ` -Path $VMTemplate.SourceVhd ` -Destination $VMTemplate.ParentVhd # Add any packages to the template if required if (-not [System.String]::IsNullOrWhitespace($VMTemplate.Packages)) { if ($VMTemplate.OSType -ne [LabOStype]::Nano) { # Mount the Template Boot VHD so that files can be loaded into it Write-LabMessage -Message $($LocalizedData.MountingTemplateBootDiskMessage ` -f $VMTemplate.Name, $VMTemplate.ParentVhd) # Create a mount point for mounting the Boot VHD [System.String] $MountPoint = Join-Path ` -Path (Split-Path -Path $VMTemplate.ParentVHD) ` -ChildPath 'Mount' if (-not (Test-Path -Path $MountPoint -PathType Container)) { $null = New-Item ` -Path $MountPoint ` -ItemType Directory } # Mount the VHD to the Mount point $null = Mount-WindowsImage ` -ImagePath $VMTemplate.parentvhd ` -Path $MountPoint ` -Index 1 # Get the list of Packages to apply $ApplyPackages = @($VMTemplate.Packages -split ',') # Get the list of Lab Resource MSUs $ResourceMSUs = Get-LabResourceMSU ` -Lab $Lab foreach ($Package in $ApplyPackages) { # Find the package in the Resources [System.Boolean] $Found = $false foreach ($ResourceMSU in $ResourceMSUs) { if ($ResourceMSU.Name -eq $Package) { # Found the package $Found = $true break } # if } # foreach if (-not $Found) { # Dismount before throwing the error Write-LabMessage -Message $($LocalizedData.DismountingTemplateBootDiskMessage ` -f $VMTemplate.Name, $VMTemplate.parentvhd) $null = Dismount-WindowsImage ` -Path $MountPoint ` -Save $null = Remove-Item ` -Path $MountPoint ` -Recurse ` -Force $exceptionParameters = @{ errorId = 'PackageNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.PackageNotFoundError ` -f $Package) } New-LabException @exceptionParameters } # if $PackagePath = $ResourceMSU.Filename if (-not (Test-Path -Path $PackagePath)) { # Dismount before throwing the error Write-LabMessage -Message $($LocalizedData.DismountingTemplateBootDiskMessage ` -f $VMTemplate.Name, $VMTemplate.ParentVhd) $null = Dismount-WindowsImage ` -Path $MountPoint ` -Save $null = Remove-Item ` -Path $MountPoint ` -Recurse ` -Force $exceptionParameters = @{ errorId = 'PackageMSUNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.PackageMSUNotFoundError ` -f $Package, $PackagePath) } New-LabException @exceptionParameters } # if # Apply a Pacakge Write-LabMessage -Message $($LocalizedData.ApplyingTemplateBootDiskFileMessage ` -f $VMTemplate.Name, $Package, $PackagePath) $null = Add-WindowsPackage ` -PackagePath $PackagePath ` -Path $MountPoint } # foreach # Dismount the VHD Write-LabMessage -Message $($LocalizedData.DismountingTemplateBootDiskMessage ` -f $VMTemplate.Name, $VMTemplate.parentvhd) $null = Dismount-WindowsImage ` -Path $MountPoint ` -Save $null = Remove-Item ` -Path $MountPoint ` -Recurse ` -Force } # if } # if Write-LabMessage -Message $($LocalizedData.OptimizingParentVHDMessage ` -f $VMTemplate.parentvhd) Set-ItemProperty ` -Path $VMTemplate.parentvhd ` -Name IsReadOnly ` -Value $false Optimize-VHD ` -Path $VMTemplate.parentvhd ` -Mode Full Write-LabMessage -Message $($LocalizedData.SettingParentVHDReadonlyMessage ` -f $VMTemplate.parentvhd) Set-ItemProperty ` -Path $VMTemplate.parentvhd ` -Name IsReadOnly ` -Value $true } Else { Write-LabMessage -Message $($LocalizedData.SkipParentVHDFileMessage ` -f $VMTemplate.Name, $VMTemplate.parentvhd) } # if this is a Nano Server template, we need to ensure that the # NanoServerPackages folder is copied to our Lab folder if ($VMTemplate.OSType -eq [LabOStype]::Nano) { [System.String] $VHDPackagesFolder = Join-Path ` -Path (Split-Path -Path $VMTemplate.SourceVhd -Parent)` -ChildPath 'NanoServerPackages' [System.String] $NanoPackagesFolder = Join-Path ` -Path $LabPath ` -ChildPath 'NanoServerPackages' if (-not (Test-Path -Path $NanoPackagesFolder -Type Container)) { Write-LabMessage -Message $($LocalizedData.CachingNanoServerPackagesMessage ` -f $VHDPackagesFolder, $NanoPackagesFolder) Copy-Item ` -Path $VHDPackagesFolder ` -Destination $LabPath ` -Recurse ` -Force } } } } function Initialize-LabVMTemplateVHD { param ( [Parameter( Position = 1, Mandatory = $true)] [ValidateNotNullOrEmpty()] $Lab, [Parameter( Position = 2)] [ValidateNotNullOrEmpty()] [System.String[]] $Name, [Parameter( Position = 3)] [LabVMTemplateVHD[]] $VMTemplateVHDs ) # if VMTeplateVHDs array not passed, pull it from config. if (-not $PSBoundParameters.ContainsKey('VMTemplateVHDs')) { [LabVMTemplateVHD[]] $VMTemplateVHDs = Get-LabVMTemplateVHD ` @PSBoundParameters } # if # if there are no VMTemplateVHDs just return if ($null -eq $VMTemplateVHDs) { return } # if [System.String] $LabPath = $Lab.labbuilderconfig.settings.labpath # Is an alternate path to DISM specified? if ($Lab.labbuilderconfig.settings.DismPath) { $DismPath = Join-Path ` -Path $Lab.labbuilderconfig.settings.DismPath ` -ChildPath 'dism.exe' if (-not (Test-Path -Path $DismPath)) { $exceptionParameters = @{ errorId = 'FileNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.FileNotFoundError ` -f 'alternate DISM.EXE', $DismPath) } New-LabException @exceptionParameters } } foreach ($VMTemplateVHD in $VMTemplateVHDs) { [System.String] $TemplateVHDName = $VMTemplateVHD.Name if ($Name -and ($TemplateVHDName -notin $Name)) { # A names list was passed but this VM Template VHD wasn't included continue } # if [System.String] $VHDPath = $VMTemplateVHD.VHDPath if (Test-Path -Path ($VHDPath)) { # The SourceVHD already exists Write-LabMessage -Message $($LocalizedData.SkipVMTemplateVHDFileMessage ` -f $TemplateVHDName, $VHDPath) continue } # if # Create the VHD Write-LabMessage -Message $($LocalizedData.CreatingVMTemplateVHDMessage ` -f $TemplateVHDName, $VHDPath) # Check the ISO exists. [System.String] $ISOPath = $VMTemplateVHD.ISOPath if (-not (Test-Path -Path $ISOPath)) { $exceptionParameters = @{ errorId = 'VMTemplateVHDISOPathNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMTemplateVHDISOPathNotFoundError ` -f $TemplateVHDName, $ISOPath) } New-LabException @exceptionParameters } # if # Mount the ISO so we can read the files. Write-LabMessage -Message $($LocalizedData.MountingVMTemplateVHDISOMessage ` -f $TemplateVHDName, $ISOPath) $null = Mount-DiskImage ` -ImagePath $ISOPath ` -StorageType ISO ` -Access Readonly # Refresh the PS Drive list to make sure the new drive can be detected Get-PSDrive ` -PSProvider FileSystem $DiskImage = Get-DiskImage -ImagePath $ISOPath $Volume = Get-Volume -DiskImage $DiskImage if (-not $Volume) { $exceptionParameters = @{ errorId = 'VolumeNotAvailableAfterMountError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VolumeNotAvailableAfterMountError ` -f $ISOPath) } New-LabException @exceptionParameters } [System.String] $DriveLetter = $Volume.DriveLetter if (-not $DriveLetter) { $exceptionParameters = @{ errorId = 'DriveLetterNotAssignedError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.DriveLetterNotAssignedError ` -f $ISOPath) } New-LabException @exceptionParameters } [System.String] $ISODrive = "$([System.String]$DriveLetter):" # Determine the path to the WIM [System.String] $SourcePath = "$ISODrive\Sources\Install.WIM" if ($VMTemplateVHD.OSType -eq [LabOStype]::Nano) { $SourcePath = "$ISODrive\Nanoserver\NanoServer.WIM" } # if # This will have to change depending on the version # of Convert-WindowsImage being used. [System.String] $VHDFormat = $VMTemplateVHD.VHDFormat [System.String] $VHDType = $VMTemplateVHD.VHDType [System.String] $VHDDiskLayout = 'UEFI' if ($VMTemplateVHD.Generation -eq 1) { $VHDDiskLayout = 'BIOS' } # if [System.String] $Edition = $VMTemplateVHD.Edition # if edition is not set then use Get-WindowsImage to get the name # of the first image in the WIM. if ([System.String]::IsNullOrWhiteSpace($Edition)) { $Edition = (Get-WindowsImage ` -ImagePath $SourcePath ` -Index 1).ImageName } # if $ConvertParams = @{ sourcepath = $SourcePath vhdpath = $VHDpath vhdformat = $VHDFormat # Convert-WindowsImage doesn't support creating different VHDTypes # vhdtype = $VHDType edition = $Edition disklayout = $VHDDiskLayout erroraction = 'Stop' } # Set the size if ($null -ne $VMTemplateVHD.VHDSize) { $ConvertParams += @{ sizebytes = $VMTemplateVHD.VHDSize } } # if # Are any features specified? if (-not [System.String]::IsNullOrWhitespace($VMTemplateVHD.Features)) { $Features = @($VMTemplateVHD.Features -split ',') $ConvertParams += @{ feature = $Features } } # if # Is an alternate path to DISM specified? if ($DismPath) { $ConvertParams += @{ DismPath = $DismPath } } # Perform Nano Server package prep if ($VMTemplateVHD.OSType -eq [LabOStype]::Nano) { # Make a copy of the all the Nano packages in the VHD root folder # So that if any VMs need to add more packages they are accessible # once the ISO has been dismounted. [System.String] $VHDFolder = Split-Path ` -Path $VHDPath ` -Parent [System.String] $NanoPackagesFolder = Join-Path ` -Path $VHDFolder ` -ChildPath 'NanoServerPackages' if (-not (Test-Path -Path $NanoPackagesFolder -Type Container)) { Write-LabMessage -Message $($LocalizedData.CachingNanoServerPackagesMessage ` -f "$ISODrive\Nanoserver\Packages", $NanoPackagesFolder) Copy-Item ` -Path "$ISODrive\Nanoserver\Packages" ` -Destination $VHDFolder ` -Recurse ` -Force Rename-Item ` -Path "$VHDFolder\Packages" ` -NewName 'NanoServerPackages' } # if } # if # Do we need to add any packages? if (-not [System.String]::IsNullOrWhitespace($VMTemplateVHD.Packages)) { $Packages = @() # Get the list of Lab Resource MSUs $ResourceMSUs = Get-LabResourceMSU ` -Lab $Lab try { foreach ($Package in @($VMTemplateVHD.Packages -split ',')) { if (([System.IO.Path]::GetExtension($Package) -eq '.cab') ` -and ($VMTemplateVHD.OSType -eq [LabOSType]::Nano)) { # This is a Nano Server .CAB pacakge # Generate the path to the Nano Package $PackagePath = Join-Path ` -Path $NanoPackagesFolder ` -ChildPath $Package # Does it exist? if (-not (Test-Path -Path $PackagePath)) { $exceptionParameters = @{ errorId = 'NanoPackageNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.NanoPackageNotFoundError ` -f $PackagePath) } New-LabException @exceptionParameters } $Packages += @( $PackagePath ) # Generate the path to the Nano Language Package $PackageLangFile = $Package -replace '.cab', "_$($Script:NanoPackageCulture).cab" $PackageLangPath = Join-Path ` -Path $NanoPackagesFolder ` -ChildPath "$($Script:NanoPackageCulture)\$PackageLangFile" # Does it exist? if (-not (Test-Path -Path $PackageLangPath)) { $exceptionParameters = @{ errorId = 'NanoPackageNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.NanoPackageNotFoundError ` -f $PackageLangPath) } New-LabException @exceptionParameters } $Packages += @( $PackageLangPath ) } else { # Tihs is a ResourceMSU type package [System.Boolean] $Found = $false foreach ($ResourceMSU in $ResourceMSUs) { if ($ResourceMSU.Name -eq $Package) { # Found the package $Found = $true break } # if } # foreach if (-not $Found) { $exceptionParameters = @{ errorId = 'PackageNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.PackageNotFoundError ` -f $Package) } New-LabException @exceptionParameters } # if $PackagePath = $ResourceMSU.Filename if (-not (Test-Path -Path $PackagePath)) { $exceptionParameters = @{ errorId = 'PackageMSUNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.PackageMSUNotFoundError ` -f $Package, $PackagePath) } New-LabException @exceptionParameters } # if $Packages += @( $PackagePath ) } } # foreach $ConvertParams += @{ Package = $Packages } } catch { # Dismount Disk Image before throwing exception $null = Dismount-DiskImage ` -ImagePath $ISOPath Throw $_ } # try } # if Write-LabMessage -Message ($LocalizedData.ConvertingWIMtoVHDMessage ` -f $SourcePath, $VHDPath, $VHDFormat, $Edition, $VHDPartitionStyle, $VHDType) # Work around an issue with Convert-WindowsImage not seeing the drive Get-PSDrive ` -PSProvider FileSystem # Dot source the Convert-WindowsImage script # Should only be done once if (-not (Test-Path -Path Function:Convert-WindowsImage)) { . $Script:SupportConvertWindowsImagePath } # if try { # Call the Convert-WindowsImage script Convert-WindowsImage @ConvertParams } # try catch { $exceptionParameters = @{ errorId = 'ConvertWindowsImageError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.ConvertWindowsImageError ` -f $ISOPath, $SourcePath, $Edition, $VHDFormat, $_.Exception.Message) } New-LabException @exceptionParameters } # catch finally { # Dismount the ISO. Write-LabMessage -Message $($LocalizedData.DismountingVMTemplateVHDISOMessage ` -f $TemplateVHDName, $ISOPath) $null = Dismount-DiskImage ` -ImagePath $ISOPath } # finally } # endfor } # Initialize-LabVMTemplateVHD function Install-Lab { [CmdLetBinding(DefaultParameterSetName="Lab")] param ( [parameter( Position=1, ParameterSetName="File", Mandatory=$true)] [ValidateNotNullOrEmpty()] [System.String] $ConfigPath, [parameter( Position=2, ParameterSetName="File")] [ValidateNotNullOrEmpty()] [System.String] $LabPath, [Parameter( Position=3, ParameterSetName="Lab", Mandatory=$true, ValueFromPipeline=$true)] [ValidateNotNullOrEmpty()] $Lab, [Parameter( Position=4)] [Switch] $CheckEnvironment, [Parameter( Position=5)] [Switch] $Force, [Parameter( Position=6)] [Switch] $OffLine ) # Param begin { # Create a splat array containing force if it is set $ForceSplat = @{} if ($PSBoundParameters.ContainsKey('Force')) { $ForceSplat = @{ Force = $true } } # if # Remove some PSBoundParameters so we can Splat $null = $PSBoundParameters.Remove('CheckEnvironment') $null = $PSBoundParameters.Remove('Force') if ($CheckEnvironment) { # Check Hyper-V Install-LabHyperV ` -ErrorAction Stop } # if # Ensure WS-Man is enabled Enable-LabWSMan ` @ForceSplat ` -ErrorAction Stop if (!($PSBoundParameters.ContainsKey('OffLine'))) { # Install Package Providers Install-LabPackageProvider ` @ForceSplat ` -ErrorAction Stop # Register Package Sources Register-LabPackageSource ` @ForceSplat ` -ErrorAction Stop } $null = $PSBoundParameters.Remove('Offline') if ($PSCmdlet.ParameterSetName -eq 'File') { # Read the configuration $Lab = Get-Lab ` @PSBoundParameters ` -ErrorAction Stop } # if } # begin process { # Initialize the core Lab components # Check Lab Folder structure Write-LabMessage -Message $($LocalizedData.InitializingLabFoldersMesage) # Check folders are defined [System.String] $LabPath = $Lab.labbuilderconfig.settings.labpath if (-not (Test-Path -Path $LabPath)) { Write-LabMessage -Message $($LocalizedData.CreatingLabFolderMessage ` -f 'LabPath',$LabPath) $null = New-Item ` -Path $LabPath ` -Type Directory } [System.String] $VHDParentPath = $Lab.labbuilderconfig.settings.vhdparentpathfull if (-not (Test-Path -Path $VHDParentPath)) { Write-LabMessage -Message $($LocalizedData.CreatingLabFolderMessage ` -f 'VHDParentPath',$VHDParentPath) $null = New-Item ` -Path $VHDParentPath ` -Type Directory } [System.String] $ResourcePath = $Lab.labbuilderconfig.settings.resourcepathfull if (-not (Test-Path -Path $ResourcePath)) { Write-LabMessage -Message $($LocalizedData.CreatingLabFolderMessage ` -f 'ResourcePath',$ResourcePath) $null = New-Item ` -Path $ResourcePath ` -Type Directory } # Initialize the Lab Management Switch Initialize-LabManagementSwitch ` -Lab $Lab ` -ErrorAction Stop # Download any Resource Modules required by this Lab $ResourceModules = Get-LabResourceModule ` -Lab $Lab Initialize-LabResourceModule ` -Lab $Lab ` -ResourceModules $ResourceModules ` -ErrorAction Stop # Download any Resource MSUs required by this Lab $ResourceMSUs = Get-LabResourceMSU ` -Lab $Lab Initialize-LabResourceMSU ` -Lab $Lab ` -ResourceMSUs $ResourceMSUs ` -ErrorAction Stop # Initialize the Switches $Switches = Get-LabSwitch ` -Lab $Lab Initialize-LabSwitch ` -Lab $Lab ` -Switches $Switches ` -ErrorAction Stop # Initialize the VM Template VHDs $VMTemplateVHDs = Get-LabVMTemplateVHD ` -Lab $Lab Initialize-LabVMTemplateVHD ` -Lab $Lab ` -VMTemplateVHDs $VMTemplateVHDs ` -ErrorAction Stop # Initialize the VM Templates $VMTemplates = Get-LabVMTemplate ` -Lab $Lab Initialize-LabVMTemplate ` -Lab $Lab ` -VMTemplates $VMTemplates ` -ErrorAction Stop # Initialize the VMs $VMs = Get-LabVM ` -Lab $Lab ` -VMTemplates $VMTemplates ` -Switches $Switches Initialize-LabVM ` -Lab $Lab ` -VMs $VMs ` -ErrorAction Stop Write-LabMessage -Message $($LocalizedData.LabInstallCompleteMessage ` -f $Lab.labbuilderconfig.name,$Lab.labbuilderconfig.settings.labpath) } # process end { } # end } # Install-Lab function Install-LabVM { [CmdLetBinding()] param ( [Parameter( Position=1, Mandatory=$true)] [ValidateNotNullOrEmpty()] $Lab, [Parameter( Position=2)] [ValidateNotNullOrEmpty()] [LabVM] $VM ) [System.String] $LabPath = $Lab.labbuilderconfig.settings.labpath # The VM is now ready to be started if ((Get-VM -Name $VM.Name).State -eq 'Off') { Write-LabMessage -Message $($LocalizedData.StartingVMMessage ` -f $VM.Name) Start-VM -VMName $VM.Name } # if # We only perform this section of VM Initialization (DSC, Cert, etc) with Server OS if ($VM.OSType -in ([LabOStype]::Server,[LabOStype]::Nano)) { # Has this VM been initialized before (do we have a cert for it) if (-not (Test-Path "$LabPath\$($VM.Name)\LabBuilder Files\$Script:DSCEncryptionCert")) { # No, so check it is initialized and download the cert if required if (Wait-LabVMInitializationComplete -VM $VM -ErrorAction Continue) { Write-LabMessage -Message $($LocalizedData.CertificateDownloadStartedMessage ` -f $VM.Name) if ($VM.CertificateSource -eq [LabCertificateSource]::Guest) { if (Recieve-LabSelfSignedCertificate -Lab $Lab -VM $VM) { Write-LabMessage -Message $($LocalizedData.CertificateDownloadCompleteMessage ` -f $VM.Name) } else { $exceptionParameters = @{ errorId = 'CertificateDownloadError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.CertificateDownloadError ` -f $VM.name) } New-LabException @exceptionParameters } # if } # if } else { $exceptionParameters = @{ errorId = 'InitializationDidNotCompleteError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.InitializationDidNotCompleteError ` -f $VM.name) } New-LabException @exceptionParameters } # if } # if if ($VM.OSType -in ([LabOStype]::Nano)) { # Copy ODJ Files if it Exists Copy-LabOdjFile ` -Lab $Lab ` -VM $VM } # if # Create any DSC Files for the VM Initialize-LabDSC ` -Lab $Lab ` -VM $VM # Attempt to start DSC on the VM Start-LabDSC ` -Lab $Lab ` -VM $VM } # if } # Install-LabVM function New-Lab { [CmdLetBinding( SupportsShouldProcess = $true)] [OutputType([XML])] param ( [Parameter( Position=1, Mandatory=$true)] [ValidateNotNullOrEmpty()] [System.String] $ConfigPath, [Parameter( Position=2, Mandatory=$true)] [ValidateNotNullOrEmpty()] [System.String] $LabPath, [Parameter( Position=3, Mandatory=$true)] [ValidateNotNullOrEmpty()] [System.String] $Name, [Parameter( Position=4)] [ValidateNotNullOrEmpty()] [System.String] $Version = '1.0', [Parameter( Position=5)] [ValidateNotNullOrEmpty()] [System.String] $Id, [Parameter( Position=6)] [ValidateNotNullOrEmpty()] [System.String] $Description, [Parameter( Position=7)] [ValidateNotNullOrEmpty()] [System.String] $DomainName, [Parameter( Position=8)] [ValidateNotNullOrEmpty()] [System.String] $Email ) # Param # Determine the full Lab Path if (-not [System.IO.Path]::IsPathRooted($LabPath)) { $LabPath = Join-Path ` -Path Get-Location ` -ChildPath $LabPath } # if # Does the Lab Path exist? if (Test-Path -Path $LabPath -Type Container) { # It does - exit if the user declines if (-not $PSCmdlet.ShouldProcess( 'LocalHost', ` ($LocalizedData.ShouldOverwriteLab ` -f $LabPath ))) { return } } else { Write-LabMessage -Message $($LocalizedData.CreatingLabFolderMessage ` -f 'LabPath',$LabPath) $null = New-Item ` -Path $LabPath ` -Type Directory } # if # Determine the full Lab configuration Path if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { $ConfigPath = Join-Path ` -Path $LabPath ` -ChildPath $ConfigPath } # if # Does the lab configuration path already exist? if (Test-Path -Path $ConfigPath) { # It does - exit if the user declines if (-not $PSCmdlet.ShouldProcess( 'LocalHost', ` ($LocalizedData.ShouldOverwriteLabConfig ` -f $ConfigPath ))) { return } } # if # Get the Config Template into a variable $Content = Get-Content ` -Path $Script:ConfigurationXMLTemplate # The XML passes the Schema check so load it. [XML] $Lab = New-Object System.Xml.XmlDocument $Lab.PreserveWhitespace = $true $Lab.LoadXML($Content) # Populate the Lab Entries $Lab.labbuilderconfig.name = $Name $Lab.labbuilderconfig.version = $Version $Lab.labbuilderconfig.settings.labpath = $LabPath if ($PSBoundParameters.ContainsKey('Id')) { $Lab.labbuilderconfig.settings.SetAttribute('Id',$Id) } # if if ($PSBoundParameters.ContainsKey('Description')) { $Lab.labbuilderconfig.description = $Description } # if if ($PSBoundParameters.ContainsKey('DomainName')) { $Lab.labbuilderconfig.settings.SetAttribute('DomainName',$DomainName) } # if if ($PSBoundParameters.ContainsKey('Email')) { $Lab.labbuilderconfig.settings.SetAttribute('Email',$Email) } # if # Save Configiration XML $Lab.Save($ConfigPath) # Create ISOFiles folder $null = New-Item ` -Path (Join-Path -Path $LabPath -ChildPath 'ISOFiles')` -Type Directory ` -ErrorAction SilentlyContinue # Create VDFFiles folder $null = New-Item ` -Path (Join-Path -Path $LabPath -ChildPath 'VHDFiles')` -Type Directory ` -ErrorAction SilentlyContinue # Copy the DSCLibrary $null = Copy-Item ` -Path $Script:DSCLibraryPath ` -Destination $LabPath ` -Recurse ` -Force ` -ErrorAction SilentlyContinue Return (Get-Lab ` -ConfigPath $ConfigPath ` -LabPath $LabPath) } # New-Lab function Remove-LabSwitch { [CmdLetBinding()] param ( [Parameter( Position = 1, Mandatory = $true)] [ValidateNotNullOrEmpty()] $Lab, [Parameter( Position = 2)] [ValidateNotNullOrEmpty()] [System.String[]] $Name, [Parameter( Position = 3)] [LabSwitch[]] $Switches, [Parameter( Position = 4)] [Switch] $RemoveExternal ) $PSBoundParameters.Remove('RemoveExternal') # if switches were not passed so pull them if (-not $PSBoundParameters.ContainsKey('switches')) { [LabSwitch[]] $Switches = Get-LabSwitch ` @PSBoundParameters } # Delete Hyper-V Switches foreach ($VMSwitch in $Switches) { if ($Name -and ($VMSwitch.name -notin $Name)) { # A names list was passed but this swtich wasn't included continue } # if $existingVMSwitch = Get-VMSwitch | Where-Object -Property Name -eq $VMSwitch.Name if ($existingVMSwitch.Count -ne 0) { $SwitchName = $VMSwitch.Name if (-not $SwitchName) { $exceptionParameters = @{ errorId = 'SwitchNameIsEmptyError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.SwitchNameIsEmptyError) } New-LabException @exceptionParameters } [LabSwitchType] $SwitchType = $VMSwitch.Type Write-LabMessage -Message $($LocalizedData.DeleteingVirtualSwitchMessage ` -f $SwitchType, $SwitchName) Switch ($SwitchType) { 'External' { if ($VMSwitch.Adapters) { $VMSwitch.Adapters.foreach( { $null = Remove-VMNetworkAdapter ` -SwitchName $SwitchName ` -Name $_.Name ` -ManagementOS ` -ErrorAction SilentlyContinue } ) } # if if ($RemoveExternal) { Remove-VMSwitch ` -Name $SwitchName } break } # 'External' 'Private' { Remove-VMSwitch ` -Name $SwitchName break } # 'Private' 'Internal' { if ($VMSwitch.Adapters) { $VMSwitch.Adapters.foreach( { $null = Remove-VMNetworkAdapter ` -SwitchName $SwitchName ` -Name $_.Name ` -ManagementOS ` -ErrorAction SilentlyContinue } ) } # if Remove-VMSwitch ` -Name $SwitchName break } # 'Internal' 'NAT' { Remove-NetNat ` -Name $SwitchName Remove-VMSwitch ` -Name $SwitchName break } # 'Internal' Default { $exceptionParameters = @{ errorId = 'UnknownSwitchTypeError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.UnknownSwitchTypeError ` -f $SwitchType, $SwitchName) } New-LabException @exceptionParameters } } # Switch } # if } # foreach } function Remove-LabVM { [CmdLetBinding()] param ( [Parameter( Position=1, Mandatory=$true)] [ValidateNotNullOrEmpty()] $Lab, [Parameter( Position=2)] [ValidateNotNullOrEmpty()] [System.String[]] $Name, [Parameter( Position=3)] [LabVM[]] $VMs, [Parameter( Position=4)] [Switch] $RemoveVMFolder ) # if VMs array not passed, pull it from config. if (-not $PSBoundParameters.ContainsKey('VMs')) { $null = $PSBoundParameters.Remove('RemoveVMFolder') [LabVM[]] $VMs = Get-LabVM ` @PSBoundParameters } # if $CurrentVMs = Get-VM # Get the LabPath [System.String] $LabPath = $Lab.labbuilderconfig.settings.labpath foreach ($VM in $VMs) { if ($Name -and ($VM.Name -notin $Name)) { # A names list was passed but this VM wasn't included continue } # if if (($CurrentVMs | Where-Object -Property Name -eq $VM.Name).Count -ne 0) { # if the VM is running we need to shut it down. if ((Get-VM -Name $VM.Name).State -eq 'Running') { Write-LabMessage -Message $($LocalizedData.StoppingVMMessage ` -f $VM.Name) Stop-VM ` -Name $VM.Name # Wait for it to completely shut down and report that it is off. Wait-LabVMOff ` -VM $VM } Write-LabMessage -Message $($LocalizedData.RemovingVMMessage ` -f $VM.Name) # Now delete the actual VM Get-VM ` -Name $VM.Name | Remove-VM -Force -Confirm:$false Write-LabMessage -Message $($LocalizedData.RemovedVMMessage ` -f $VM.Name) } else { Write-LabMessage -Message $($LocalizedData.VMNotFoundMessage ` -f $VM.Name) } } # Should we remove the VM Folder? if ($RemoveVMFolder) { if (Test-Path -Path $VM.VMRootPath) { Write-LabMessage -Message $($LocalizedData.DeletingVMFolderMessage ` -f $VM.Name) Remove-Item ` -Path $VM.VMRootPath ` -Recurse ` -Force } } } # Remove-LabVM function Remove-LabVMTemplate { [CmdLetBinding()] param ( [Parameter( Position = 1, Mandatory = $true)] [ValidateNotNullOrEmpty()] $Lab, [Parameter( Position = 2)] [ValidateNotNullOrEmpty()] [System.String[]] $Name, [Parameter( Position = 3)] [LabVMTemplate[]] $VMTemplates ) # if VMTeplates array not passed, pull it from config. if (-not $PSBoundParameters.ContainsKey('VMTemplates')) { $VMTemplates = Get-LabVMTemplate ` @PSBoundParameters } # if foreach ($VMTemplate in $VMTemplates) { if ($Name -and ($VMTemplate.Name -notin $Name)) { # A names list was passed but this VM Template wasn't included continue } # if if (Test-Path $VMTemplate.ParentVhd) { Set-ItemProperty ` -Path $VMTemplate.parentvhd ` -Name IsReadOnly ` -Value $false Write-LabMessage -Message $($LocalizedData.DeletingParentVHDMessage ` -f $VMTemplate.ParentVhd) Remove-Item ` -Path $VMTemplate.ParentVhd ` -Confirm:$false ` -Force } # if } # foreach } function Remove-LabVMTemplateVHD { param ( [Parameter( Position = 1, Mandatory = $true)] [ValidateNotNullOrEmpty()] $Lab, [Parameter( Position = 2)] [ValidateNotNullOrEmpty()] [System.String[]] $Name, [Parameter( Position = 3)] [LabVMTemplateVHD[]] $VMTemplateVHDs ) # if VMTeplateVHDs array not passed, pull it from config. if (-not $PSBoundParameters.ContainsKey('VMTemplateVHDs')) { [LabVMTemplateVHD[]] $VMTemplateVHDs = Get-LabVMTemplateVHD ` @PSBoundParameters } # if # if there are no VMTemplateVHDs just return if ($null -eq $VMTemplateVHDs) { return } # if [System.String] $LabPath = $Lab.labbuilderconfig.settings.labpath foreach ($VMTemplateVHD in $VMTemplateVHDs) { [System.String] $TemplateVHDName = $VMTemplateVHD.Name if ($Name -and ($TemplateVHDName -notin $Name)) { # A names list was passed but this VM Template VHD wasn't included continue } # if [System.String] $VHDPath = $VMTemplateVHD.VHDPath if (Test-Path -Path ($VHDPath)) { Remove-Item ` -Path $VHDPath ` -Force Write-LabMessage -Message $($LocalizedData.DeletingVMTemplateVHDFileMessage ` -f $TemplateVHDName, $VHDPath) } # if } # endfor } # Remove-LabVMTemplateVHD function Start-Lab { [CmdLetBinding(DefaultParameterSetName="Lab")] param ( [parameter( Position=1, ParameterSetName="File", Mandatory=$true)] [ValidateNotNullOrEmpty()] [System.String] $ConfigPath, [parameter( Position=2, ParameterSetName="File")] [ValidateNotNullOrEmpty()] [System.String] $labPath, [Parameter( Position=3, ParameterSetName="Lab", Mandatory=$true, ValueFromPipeline=$true)] [ValidateNotNullOrEmpty()] $lab, [Parameter( Position=4)] [System.Int32] $StartupTimeout = $Script:StartupTimeout ) # Param begin { # Remove some PSBoundParameters so we can Splat $null = $PSBoundParameters.Remove('StartupTimeout') if ($PSCmdlet.ParameterSetName -eq 'File') { # Read the configuration $lab = Get-Lab @PSBoundParameters } # if } # begin process { # Get the VMs $vms = Get-LabVM ` -Lab $lab # Get the bootorders by lowest first and ignoring 0 and call $bootOrders = @( ($vms | Where-Object -FilterScript { ($_.Bootorder -gt 0) } ).Bootorder ) $bootPhases = @( ($bootOrders | Sort-Object -Unique) ) # Step through each of these "Bootphases" waiting for them to complete foreach ($bootPhase in $bootPhases) { # Process this "Bootphase" Write-LabMessage -Message $($LocalizedData.StartingBootPhaseVMsMessage ` -f $bootPhase) # Get all VMs in this "Bootphase" $bootVMs = @( $vms | Where-Object -FilterScript { ($_.BootOrder -eq $bootPhase) } ) $startPhase = Get-Date $phaseComplete = $false $phaseAllBooted = $true $vmCount = $bootVMs.Count $vmNumber = 0 <# Loop through all the VMs in this "Bootphase" repeatedly until timeout occurs or PhaseComplete is marked as complete #> while (-not $phaseComplete ` -and ((Get-Date) -lt $startPhase.AddSeconds($StartupTimeout))) { # Get the VM to boot/check $vm = $bootVMs[$vmNumber] $vmName = $vm.Name # Get the actual Hyper-V VM object $vmObject = Get-VM ` -Name $vmName ` -ErrorAction SilentlyContinue if ($vmObject) { # Start the VM if it is off if ($vmObject.State -eq 'Off') { Write-LabMessage -Message $($LocalizedData.StartingVMMessage -f $vmName) Start-VM -VM $vmObject } # if <# Use the allocation of a Management IP Address as an indicator the machine has booted #> $managementIP = Get-LabVMManagementIPAddress ` -Lab $lab ` -VM $vm ` -ErrorAction SilentlyContinue if (-not ($managementIP)) { # It has not booted $phaseAllBooted = $false } # if } else { # if the VM does not exist then throw a non-terminating exception $exceptionParameters = @{ errorId = 'VMDoesNotExistError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMDoesNotExistError ` -f $vmName) } New-LabException @exceptionParameters } # if $vmNumber++ if ($vmNumber -eq $vmCount) { <# We have stepped through all VMs in this Phase so check if all have booted, otherwise reset the loop. #> if ($phaseAllBooted) { <# If we have gone through all VMs in this "Bootphase" and they're all marked as booted then we can mark this phase as complete and allow moving on to the next one #> Write-LabMessage -Message $($LocalizedData.AllBootPhaseVMsStartedMessage -f $bootPhase) $phaseComplete = $true } else { $phaseAllBooted = $true } # if # Reset the VM Loop $vmNumber = 0 } # if } # while # Did we timeout? if (-not ($phaseComplete)) { # Yes, throw an exception $exceptionParameters = @{ errorId = 'BootPhaseVMsTimeoutError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.BootPhaseStartVMsTimeoutError ` -f $bootPhase) } New-LabException @exceptionParameters } # if } # foreach Write-LabMessage -Message $($LocalizedData.LabStartCompleteMessage ` -f $lab.labbuilderconfig.name,$lab.labbuilderconfig.settings.fullconfigpath) } # process end { } # end } # Start-Lab function Stop-Lab { [CmdLetBinding(DefaultParameterSetName="Lab")] param ( [parameter( Position=1, ParameterSetName="File", Mandatory=$true)] [ValidateNotNullOrEmpty()] [System.String] $ConfigPath, [parameter( Position=2, ParameterSetName="File")] [ValidateNotNullOrEmpty()] [System.String] $LabPath, [Parameter( Position=3, ParameterSetName="Lab", Mandatory=$true, ValueFromPipeline=$true)] [ValidateNotNullOrEmpty()] $Lab ) # Param begin { # Remove some PSBoundParameters so we can Splat if ($PSCmdlet.ParameterSetName -eq 'File') { # Read the configuration $Lab = Get-Lab ` @PSBoundParameters } # if } # begin process { # Get the VMs $vms = Get-LabVM ` -Lab $Lab # Get the bootorders by highest first and ignoring 0 $bootOrders = @( ($vms | Where-Object -FilterScript { ($_.Bootorder -gt 0) } ).Bootorder ) $bootPhases = @( ($bootOrders | Sort-Object -Unique -Descending) ) # Step through each of these "Bootphases" waiting for them to complete foreach ($bootPhase in $bootPhases) { # Process this "Bootphase" Write-LabMessage -Message $($LocalizedData.StoppingBootPhaseVMsMessage ` -f $bootPhase) # Get all VMs in this "Bootphase" $bootVMs = @( $vms | Where-Object -FilterScript { ($_.BootOrder -eq $bootPhase) } ) $phaseComplete = $false $phaseAllStopped = $true $vmCount = $bootVMs.Count $vmNumber = 0 # Loop through all the VMs in this "Bootphase" repeatedly while (-not $phaseComplete) { # Get the VM to boot/check $VM = $bootVMs[$vmNumber] $vmName = $VM.Name # Get the actual Hyper-V VM object $vmObject = Get-VM ` -Name $vmName ` -ErrorAction SilentlyContinue if (-not $vmObject) { # if the VM does not exist then throw a non-terminating exception $exceptionParameters = @{ errorId = 'VMDoesNotExistError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.VMDoesNotExistError ` -f $vmName) } New-LabException @exceptionParameters } # if # Shutodwn the VM if it is off if ($vmObject.State -eq 'Running') { Write-LabMessage -Message $($LocalizedData.StoppingVMMessage ` -f $VMName) $null = Stop-VM ` -VM $vmObject ` -Force ` -ErrorAction Continue } # if # Determine if the VM has stopped. if ($vmObject -and (Get-VM -VMName $vmName).State -ne 'Off') { # It has not stopped $phaseAllStopped = $false } # if $vmNumber++ if ($vmNumber -eq $vmCount) { <# We have stepped through all VMs in this Phase so check if all have stopped, otherwise reset the loop. #> if ($phaseAllStopped) { <# if we have gone through all VMs in this "Bootphase" and they're all marked as stopped then we can mark this phase as complete and allow moving on to the next one #> Write-LabMessage -Message $($LocalizedData.AllBootPhaseVMsStoppedMessage ` -f $bootPhase) $phaseComplete = $true } else { $phaseAllStopped = $true } # if # Reset the VM Loop $vmNumber = 0 } # if } # while } # foreach Write-LabMessage -Message $($LocalizedData.LabStopCompleteMessage ` -f $Lab.labbuilderconfig.name,$Lab.labbuilderconfig.settings.fullconfigpath) } # process end { } # end } # Stop-Lab function Uninstall-Lab { [CmdLetBinding(DefaultParameterSetName="Lab", SupportsShouldProcess = $true, ConfirmImpact = 'High')] param ( [parameter( Position=1, ParameterSetName="File", Mandatory=$true)] [ValidateNotNullOrEmpty()] [System.String] $ConfigPath, [parameter( Position=2, ParameterSetName="File")] [ValidateNotNullOrEmpty()] [System.String] $LabPath, [Parameter( Position=3, ParameterSetName="Lab", Mandatory=$true, ValueFromPipeline=$true)] [ValidateNotNullOrEmpty()] $Lab, [Parameter( Position=4)] [Switch] $RemoveSwitch, [Parameter( Position=5)] [Switch] $RemoveVMTemplate, [Parameter( Position=6)] [Switch] $RemoveVMFolder, [Parameter( Position=7)] [Switch] $RemoveVMTemplateVHD, [Parameter( Position=8)] [Switch] $RemoveLabFolder ) # Param begin { # Remove some PSBoundParameters so we can Splat $null = $PSBoundParameters.Remove('RemoveSwitch') $null = $PSBoundParameters.Remove('RemoveVMTemplate') $null = $PSBoundParameters.Remove('RemoveVMFolder') $null = $PSBoundParameters.Remove('RemoveVMTemplateVHD') $null = $PSBoundParameters.Remove('RemoveLabFolder') if ($PSCmdlet.ParameterSetName -eq 'File') { # Read the configuration $Lab = Get-Lab ` @PSBoundParameters } # if } # begin process { if ($PSCmdlet.ShouldProcess( 'LocalHost', ` ($LocalizedData.ShouldUninstallLab ` -f $Lab.labbuilderconfig.name,$Lab.labbuilderconfig.settings.labpath ))) { # Remove the VMs $VMSplat = @{} if ($RemoveVMFolder) { $VMSplat += @{ RemoveVMFolder = $true } } # if $null = Remove-LabVM ` -Lab $Lab ` @VMSplat # Remove the VM Templates if ($RemoveVMTemplate) { if ($PSCmdlet.ShouldProcess( 'LocalHost', ` ($LocalizedData.ShouldRemoveVMTemplate ` -f $Lab.labbuilderconfig.name,$Lab.labbuilderconfig.settings.labpath ))) { $null = Remove-LabVMTemplate ` -Lab $Lab } # if } # if # Remove the VM Switches if ($RemoveSwitch) { if ($PSCmdlet.ShouldProcess( 'LocalHost', ` ($LocalizedData.ShouldRemoveSwitch ` -f $Lab.labbuilderconfig.name,$Lab.labbuilderconfig.settings.labpath ))) { $null = Remove-LabSwitch ` -Lab $Lab } # if } # if # Remove the VM Template VHDs if ($RemoveVMTemplateVHD) { if ($PSCmdlet.ShouldProcess( 'LocalHost', ` ($LocalizedData.ShouldRemoveVMTemplateVHD ` -f $Lab.labbuilderconfig.name,$Lab.labbuilderconfig.settings.labpath ))) { $null = Remove-LabVMTemplateVHD ` -Lab $Lab } # if } # if # Remove the Lab Folder if ($RemoveLabFolder) { if (Test-Path -Path $Lab.labbuilderconfig.settings.labpath) { if ($PSCmdlet.ShouldProcess( 'LocalHost', ` ($LocalizedData.ShouldRemoveLabFolder ` -f $Lab.labbuilderconfig.name,$Lab.labbuilderconfig.settings.labpath ))) { Remove-Item ` -Path $Lab.labbuilderconfig.settings.labpath ` -Recurse ` -Force } # if } # if } # if # Remove the LabBuilder Management Network switch [System.String] $ManagementSwitchName = Get-LabManagementSwitchName ` -Lab $Lab if ((Get-VMSwitch | Where-Object -Property Name -eq $ManagementSwitchName).Count -ne 0) { $null = Remove-VMSwitch ` -Name $ManagementSwitchName Write-LabMessage -Message $($LocalizedData.RemovingLabManagementSwitchMessage ` -f $ManagementSwitchName) } Write-LabMessage -Message $($LocalizedData.LabUninstallCompleteMessage ` -f $Lab.labbuilderconfig.name,$Lab.labbuilderconfig.settings.labpath ) } # if } # process end { } # end } # Uninstall-Lab function Update-Lab { [CmdLetBinding(DefaultParameterSetName="Lab")] param ( [parameter( Position=1, ParameterSetName="File", Mandatory=$true)] [ValidateNotNullOrEmpty()] [System.String] $ConfigPath, [parameter( Position=2, ParameterSetName="File")] [ValidateNotNullOrEmpty()] [System.String] $LabPath, [Parameter( Position=3, ParameterSetName="Lab", Mandatory=$true, ValueFromPipeline=$true)] [ValidateNotNullOrEmpty()] $Lab ) # Param begin { if ($PSCmdlet.ParameterSetName -eq 'File') { # Read the configuration $Lab = Get-Lab ` @PSBoundParameters } # if } # begin process { Install-Lab ` @PSBoundParameters Write-LabMessage -Message $($LocalizedData.LabUpdateCompleteMessage ` -f $Lab.labbuilderconfig.name,$Lab.labbuilderconfig.settings.fullconfigpath) } # process end { } # end } # Update-Lab #endregion |