lib/private/utils.ps1
<#
.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) } } # New-LabException <# .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 } } # Invoke-LabDownloadAndUnzipFile <# .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 xNetworking ` -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 } # Invoke-LabDownloadResourceModule <# .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 } # New-LabCredential <# .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 } # Enable-LabWSMan <# .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 } ) } } } # Install-LabHyperV <# .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-ValidConfigurationXMLSchema -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-ValidConfigurationXMLSchema { [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 } # Assert-ValidConfigurationXMLSchema <# .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, [Byte] $Step = 1 ) Return [System.String]::Format("{0:X}", [Convert]::ToUInt64($MACAddress, 16) + $Step).PadLeft(12, '0') } # Get-NextMacAddress <# .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-NextIpAddress -IpAddress '192.168.123.44' -Step 2 Returns the IP Address '192.168.123.44' .EXAMPLE Get-NextIpAddress -IpAddress 'fe80::15b4:b934:5d23:1a2f' -Step 2 Returns the IP Address 'fe80::15b4:b934:5d23:1a31' .OUTPUTS The increased IP Address. #> function Get-NextIpAddress { [CmdLetBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $IpAddress, [Parameter()] [System.Byte] $Step = 1 ) # Check the IP Address is valid $ip = Assert-ValidIpAddress -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 } # Get-NextIpAddress <# .SYNOPSIS Validates the IP Address. .PARAMETER IpAddress Contains the IP Address to validate. .EXAMPLE Assert-ValidIpAddress -IpAddress '192.168.123.44' Does not throw an exception and returns '192.168.123.44'. .EXAMPLE Assert-ValidIpAddress -IpAddress '192.168.123.4432' Throws an exception. .OUTPUTS The IP address if valid. #> function Assert-ValidIpAddress { [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 } # Assert-ValidIpAddress <# .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 } # Install-LabPackageProvider <# .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 } # Register-LabPackageSource <# .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 } # Write-LabMessage |