diff --git a/.vscode/analyzersettings.psd1 b/.vscode/analyzersettings.psd1 index 78312d2..d925a62 100644 --- a/.vscode/analyzersettings.psd1 +++ b/.vscode/analyzersettings.psd1 @@ -1,44 +1,115 @@ @{ - CustomRulePath = '.\output\RequiredModules\DscResource.AnalyzerRules' - includeDefaultRules = $true - IncludeRules = @( - # DSC Resource Kit style guideline rules. - 'PSAvoidDefaultValueForMandatoryParameter', - 'PSAvoidDefaultValueSwitchParameter', - 'PSAvoidInvokingEmptyMembers', - 'PSAvoidNullOrEmptyHelpMessageAttribute', - 'PSAvoidUsingCmdletAliases', - 'PSAvoidUsingComputerNameHardcoded', - 'PSAvoidUsingDeprecatedManifestFields', - 'PSAvoidUsingEmptyCatchBlock', - 'PSAvoidUsingInvokeExpression', - 'PSAvoidUsingPositionalParameters', - 'PSAvoidShouldContinueWithoutForce', - 'PSAvoidUsingWMICmdlet', - 'PSAvoidUsingWriteHost', - 'PSDSCReturnCorrectTypesForDSCFunctions', - 'PSDSCStandardDSCFunctionsInResource', - 'PSDSCUseIdenticalMandatoryParametersForDSC', - 'PSDSCUseIdenticalParametersForDSC', - 'PSMisleadingBacktick', - 'PSMissingModuleManifestField', - 'PSPossibleIncorrectComparisonWithNull', - 'PSProvideCommentHelp', - 'PSReservedCmdletChar', - 'PSReservedParams', - 'PSUseApprovedVerbs', - 'PSUseCmdletCorrectly', - 'PSUseOutputTypeCorrectly', - 'PSAvoidGlobalVars', - 'PSAvoidUsingConvertToSecureStringWithPlainText', - 'PSAvoidUsingPlainTextForPassword', - 'PSAvoidUsingUsernameAndPasswordParams', - 'PSDSCUseVerboseMessageInDSCResource', - 'PSShouldProcess', - 'PSUseDeclaredVarsMoreThanAssignments', - 'PSUsePSCredentialType', + CustomRulePath = @( + './output/RequiredModules/DscResource.AnalyzerRules' + './output/RequiredModules/Indented.ScriptAnalyzerRules' + ) + IncludeDefaultRules = $true + IncludeRules = @( + # DSC Community style guideline rules from the module ScriptAnalyzer. + 'PSAvoidDefaultValueForMandatoryParameter' + 'PSAvoidDefaultValueSwitchParameter' + 'PSAvoidInvokingEmptyMembers' + 'PSAvoidNullOrEmptyHelpMessageAttribute' + 'PSAvoidUsingCmdletAliases' + 'PSAvoidUsingComputerNameHardcoded' + 'PSAvoidUsingDeprecatedManifestFields' + 'PSAvoidUsingEmptyCatchBlock' + 'PSAvoidUsingInvokeExpression' + 'PSAvoidUsingPositionalParameters' + 'PSAvoidShouldContinueWithoutForce' + 'PSAvoidUsingWMICmdlet' + 'PSAvoidUsingWriteHost' + 'PSDSCReturnCorrectTypesForDSCFunctions' + 'PSDSCStandardDSCFunctionsInResource' + 'PSDSCUseIdenticalMandatoryParametersForDSC' + 'PSDSCUseIdenticalParametersForDSC' + 'PSMisleadingBacktick' + 'PSMissingModuleManifestField' + 'PSPossibleIncorrectComparisonWithNull' + 'PSProvideCommentHelp' + 'PSReservedCmdletChar' + 'PSReservedParams' + 'PSUseApprovedVerbs' + 'PSUseCmdletCorrectly' + 'PSUseOutputTypeCorrectly' + 'PSAvoidGlobalVars' + 'PSAvoidUsingConvertToSecureStringWithPlainText' + 'PSAvoidUsingPlainTextForPassword' + 'PSAvoidUsingUsernameAndPasswordParams' + 'PSDSCUseVerboseMessageInDSCResource' + 'PSShouldProcess' + 'PSUseDeclaredVarsMoreThanAssignments' + 'PSUsePSCredentialType' + + # Additional rules from the module ScriptAnalyzer + 'PSUseConsistentWhitespace' + 'UseCorrectCasing' + 'PSPlaceOpenBrace' + 'PSPlaceCloseBrace' + 'AlignAssignmentStatement' + 'AvoidUsingDoubleQuotesForConstantString' + 'UseShouldProcessForStateChangingFunctions' + # Rules from the modules DscResource.AnalyzerRules 'Measure-*' + + # Rules from the module Indented.ScriptAnalyzerRules + 'AvoidCreatingObjectsFromAnEmptyString' + 'AvoidDashCharacters' + 'AvoidEmptyNamedBlocks' + 'AvoidFilter' + 'AvoidHelpMessage' + 'AvoidNestedFunctions' + 'AvoidNewObjectToCreatePSObject' + 'AvoidParameterAttributeDefaultValues' + 'AvoidProcessWithoutPipeline' + 'AvoidSmartQuotes' + 'AvoidThrowOutsideOfTry' + 'AvoidWriteErrorStop' + 'AvoidWriteOutput' + 'UseSyntacticallyCorrectExamples' ) + <# + The following types are not rules but parse errors reported by PSScriptAnalyzer + so they cannot be ecluded. They need to be filtered out from the result of + Invoke-ScriptAnalyzer. + + TypeNotFound - Because classes in the project cannot be found unless built. + RequiresModuleInvalid - Because 'using module' in prefix.ps1 cannot be resolved as source file. + #> + ExcludeRules = @() + + Rules = @{ + PSUseConsistentWhitespace = @{ + Enable = $true + CheckOpenBrace = $true + CheckInnerBrace = $true + CheckOpenParen = $true + CheckOperator = $false + CheckSeparator = $true + CheckPipe = $true + CheckPipeForRedundantWhitespace = $true + CheckParameter = $false + } + + PSPlaceOpenBrace = @{ + Enable = $true + OnSameLine = $false + NewLineAfter = $true + IgnoreOneLineBlock = $false + } + + PSPlaceCloseBrace = @{ + Enable = $true + NoEmptyLineBefore = $true + IgnoreOneLineBlock = $false + NewLineAfter = $true + } + + PSAlignAssignmentStatement = @{ + Enable = $true + CheckHashtable = $true + } + } } diff --git a/.vscode/settings.json b/.vscode/settings.json index 72f9013..0c162ce 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,8 +10,9 @@ "powershell.codeFormatting.pipelineIndentationStyle": "IncreaseIndentationForFirstPipeline", "powershell.codeFormatting.preset": "Custom", "powershell.codeFormatting.alignPropertyValuePairs": true, + "powershell.codeFormatting.useConstantStrings": true, "powershell.developer.bundledModulesPath": "${cwd}/output/RequiredModules", - "powershell.scriptAnalysis.settingsPath": ".vscode\\analyzersettings.psd1", + "powershell.scriptAnalysis.settingsPath": "/.vscode/analyzersettings.psd1", "powershell.scriptAnalysis.enable": true, "files.trimTrailingWhitespace": true, "files.trimFinalNewlines": true, @@ -19,6 +20,9 @@ "files.associations": { "*.ps1xml": "xml" }, + "cSpell.dictionaries": [ + "powershell" + ], "cSpell.words": [ "COMPANYNAME", "ICONURI", @@ -34,8 +38,21 @@ "pscmdlet", "steppable" ], + "cSpell.ignorePaths": [ + ".git" + ], "[markdown]": { "files.trimTrailingWhitespace": false, "files.encoding": "utf8" - } + }, + "powershell.pester.useLegacyCodeLens": false, + "pester.testFilePath": [ + "[tT]ests/[qQ][aA]/*.[tT]ests.[pP][sS]1", + "[tT]ests/[uU]nit/**/*.[tT]ests.[pP][sS]1", + "[tT]ests/[uU]nit/*.[tT]ests.[pP][sS]1" + ], + "pester.runTestsInNewProcess": true, + "pester.pesterModulePath": "./output/RequiredModules/Pester", + "powershell.pester.codeLens": true, + "pester.suppressCodeLensNotice": true, } diff --git a/CHANGELOG.md b/CHANGELOG.md index 7184a62..68d06c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ For older change log history see the [historic changelog](HISTORIC_CHANGELOG.md) - Fix multiple DNS IP adresses does not work #190 - NetworkSetting parameter is now optional and no default actions are taken if not specified - Switch to use VM image `windows-latest` to build phase. + - Use latest DscCommunity scripts and files + - Changed `HyperVDsc.Common` to a buildable module. ## [3.18.0] - 2022-06-04 diff --git a/RequiredModules.psd1 b/RequiredModules.psd1 index c875d80..9a10704 100644 --- a/RequiredModules.psd1 +++ b/RequiredModules.psd1 @@ -1,5 +1,5 @@ @{ - PSDependOptions = @{ + PSDependOptions = @{ AddToPath = $true Target = 'output\RequiredModules' Parameters = @{ @@ -7,18 +7,26 @@ } } - InvokeBuild = 'latest' - PSScriptAnalyzer = 'latest' - Pester = '4.10.1' - Plaster = 'latest' - ModuleBuilder = 'latest' - ChangelogManagement = 'latest' - Sampler = 'latest' - 'Sampler.GitHubTasks' = 'latest' - MarkdownLinkCheck = 'latest' - 'DscResource.Test' = 'latest' - 'DscResource.AnalyzerRules' = 'latest' - 'DscResource.DocGenerator' = 'latest' - 'DscResource.Common' = 'latest' - xDscResourceDesigner = 'latest' + InvokeBuild = 'latest' + PSScriptAnalyzer = 'latest' + Pester = '4.10.1' + Plaster = 'latest' + ModuleBuilder = 'latest' + ChangelogManagement = 'latest' + Sampler = 'latest' + 'Sampler.GitHubTasks' = 'latest' + MarkdownLinkCheck = 'latest' + 'DscResource.Test' = 'latest' + xDscResourceDesigner = 'latest' + + # Build dependencies needed for using the module + 'DscResource.Common' = 'latest' + + # Analyzer rules + 'DscResource.AnalyzerRules' = 'latest' + 'Indented.ScriptAnalyzerRules' = 'latest' + + # Prerequisite modules for documentation. + 'DscResource.DocGenerator' = 'latest' + PlatyPS = 'latest' } diff --git a/Resolve-Dependency.ps1 b/Resolve-Dependency.ps1 index e207b3e..17cc98e 100644 --- a/Resolve-Dependency.ps1 +++ b/Resolve-Dependency.ps1 @@ -9,7 +9,7 @@ .PARAMETER PSDependTarget Path for PSDepend to be bootstrapped and save other dependencies. Can also be CurrentUser or AllUsers if you wish to install the modules in - such scope. The default value is './output/RequiredModules' relative to + such scope. The default value is 'output/RequiredModules' relative to this script's path. .PARAMETER Proxy @@ -46,6 +46,21 @@ .PARAMETER WithYAML Not yet written. + .PARAMETER UseModuleFast + Specifies to use ModuleFast instead of PowerShellGet to resolve dependencies + faster. + + .PARAMETER ModuleFastBleedingEdge + Specifies to use ModuleFast code that is in the ModuleFast's main branch + in its GitHub repository. The parameter UseModuleFast must also be set to + true. + + .PARAMETER UsePSResourceGet + Specifies to use the new PSResourceGet module instead of the (now legacy) PowerShellGet module. + + .PARAMETER PSResourceGetVersion + String specifying the module version for PSResourceGet if the `UsePSResourceGet` switch is utilized. + .NOTES Load defaults for parameters values from Resolve-Dependency.psd1 if not provided as parameter. @@ -59,7 +74,7 @@ param [Parameter()] [System.String] - $PSDependTarget = (Join-Path -Path $PSScriptRoot -ChildPath './output/RequiredModules'), + $PSDependTarget = (Join-Path -Path $PSScriptRoot -ChildPath 'output/RequiredModules'), [Parameter()] [System.Uri] @@ -96,7 +111,39 @@ param [Parameter()] [System.Management.Automation.SwitchParameter] - $WithYAML + $WithYAML, + + [Parameter()] + [System.Collections.Hashtable] + $RegisterGallery, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $UseModuleFast, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $ModuleFastBleedingEdge, + + [Parameter()] + [System.String] + $ModuleFastVersion, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $UsePSResourceGet, + + [Parameter()] + [System.String] + $PSResourceGetVersion, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $UsePowerShellGetCompatibilityModule, + + [Parameter()] + [System.String] + $UsePowerShellGetCompatibilityModuleVersion ) try @@ -107,17 +154,6 @@ try { Import-Module -Name Microsoft.PowerShell.Utility -RequiredVersion '3.1.0.0' } - - <# - Making sure the imported PackageManagement module is not from PS7 module - path. The VSCode PS extension is changing the $env:PSModulePath and - prioritize the PS7 path. This is an issue with PowerShellGet because - it loads an old version if available (or fail to load latest). - #> - Get-Module -ListAvailable PackageManagement | - Where-Object -Property 'ModuleBase' -NotMatch 'powershell.7' | - Select-Object -First 1 | - Import-Module -Force } Write-Verbose -Message 'Importing Bootstrap default parameters from ''$PSScriptRoot/Resolve-Dependency.psd1''.' @@ -138,7 +174,7 @@ try { if (-not $PSBoundParameters.Keys.Contains($parameterName) -and $resolveDependencyDefaults.ContainsKey($parameterName)) { - Write-Verbose -Message "Setting $parameterName with $($resolveDependencyDefaults[$parameterName])." + Write-Verbose -Message "Setting parameter '$parameterName' to value '$($resolveDependencyDefaults[$parameterName])'." try { @@ -151,7 +187,7 @@ try $PSBoundParameters.Add($parameterName, $variableValue) - Set-Variable -Name $parameterName -value $variableValue -Force -ErrorAction 'SilentlyContinue' + Set-Variable -Name $parameterName -Value $variableValue -Force -ErrorAction 'SilentlyContinue' } catch { @@ -165,253 +201,860 @@ catch Write-Warning -Message "Error attempting to import Bootstrap's default parameters from '$resolveDependencyConfigPath': $($_.Exception.Message)." } -Write-Progress -Activity 'Bootstrap:' -PercentComplete 0 -CurrentOperation 'NuGet Bootstrap' +# Handle when both ModuleFast and PSResourceGet is configured or/and passed as parameter. +if ($UseModuleFast -and $UsePSResourceGet) +{ + Write-Information -MessageData 'Both ModuleFast and PSResourceGet is configured or/and passed as parameter.' -InformationAction 'Continue' -# TODO: This should handle the parameter $AllowOldPowerShellGetModule. -$powerShellGetModule = Import-Module -Name 'PowerShellGet' -MinimumVersion '2.0' -ErrorAction 'SilentlyContinue' -PassThru + if ($PSVersionTable.PSVersion -ge '7.2') + { + $UsePSResourceGet = $false -# Install the package provider if it is not available. -$nuGetProvider = Get-PackageProvider -Name 'NuGet' -ListAvailable | Select-Object -First 1 + Write-Information -MessageData 'PowerShell 7.2 or higher being used, prefer ModuleFast over PSResourceGet.' -InformationAction 'Continue' + } + else + { + $UseModuleFast = $false -if (-not $powerShellGetModule -and -not $nuGetProvider) -{ - $providerBootstrapParameters = @{ - Name = 'nuget' - Force = $true - ForceBootstrap = $true - ErrorAction = 'Stop' + Write-Information -MessageData 'Windows PowerShell or PowerShell <=7.1 is being used, prefer PSResourceGet since ModuleFast is not supported on this version of PowerShell.' -InformationAction 'Continue' } +} - switch ($PSBoundParameters.Keys) +# Only bootstrap ModuleFast if it is not already imported. +if ($UseModuleFast -and -not (Get-Module -Name 'ModuleFast')) +{ + try { - 'Proxy' + $moduleFastBootstrapScriptBlockParameters = @{} + + if ($ModuleFastBleedingEdge) { - $providerBootstrapParameters.Add('Proxy', $Proxy) + Write-Information -MessageData 'ModuleFast is configured to use Bleeding Edge (directly from ModuleFast''s main branch).' -InformationAction 'Continue' + + $moduleFastBootstrapScriptBlockParameters.UseMain = $true } + elseif($ModuleFastVersion) + { + if ($ModuleFastVersion -notmatch 'v') + { + $ModuleFastVersion = 'v{0}' -f $ModuleFastVersion + } - 'ProxyCredential' + Write-Information -MessageData ('ModuleFast is configured to use version {0}.' -f $ModuleFastVersion) -InformationAction 'Continue' + + $moduleFastBootstrapScriptBlockParameters.Release = $ModuleFastVersion + } + else { - $providerBootstrapParameters.Add('ProxyCredential', $ProxyCredential) + Write-Information -MessageData 'ModuleFast is configured to use latest released version.' -InformationAction 'Continue' } - 'Scope' - { - $providerBootstrapParameters.Add('Scope', $Scope) + $moduleFastBootstrapUri = 'bit.ly/modulefast' # cSpell: disable-line + + Write-Debug -Message ('Using bootstrap script at {0}' -f $moduleFastBootstrapUri) + + $invokeWebRequestParameters = @{ + Uri = $moduleFastBootstrapUri + ErrorAction = 'Stop' } + + $moduleFastBootstrapScript = Invoke-WebRequest @invokeWebRequestParameters + + $moduleFastBootstrapScriptBlock = [ScriptBlock]::Create($moduleFastBootstrapScript) + + & $moduleFastBootstrapScriptBlock @moduleFastBootstrapScriptBlockParameters } + catch + { + Write-Warning -Message ('ModuleFast could not be bootstrapped. Reverting to PSResourceGet. Error: {0}' -f $_.Exception.Message) - if ($AllowPrerelease) + $UseModuleFast = $false + $UsePSResourceGet = $true + } +} + +if ($UsePSResourceGet) +{ + $psResourceGetModuleName = 'Microsoft.PowerShell.PSResourceGet' + + # If PSResourceGet was used prior it will be locked and we can't replace it. + if ((Test-Path -Path "$PSDependTarget/$psResourceGetModuleName" -PathType 'Container') -and (Get-Module -Name $psResourceGetModuleName)) { - $providerBootstrapParameters.Add('AllowPrerelease', $true) + Write-Information -MessageData ('{0} is already bootstrapped and imported into the session. If there is a need to refresh the module, open a new session and resolve dependencies again.' -f $psResourceGetModuleName) -InformationAction 'Continue' } + else + { + Write-Debug -Message ('{0} do not exist, saving the module to RequiredModules.' -f $psResourceGetModuleName) - Write-Information -MessageData 'Bootstrap: Installing NuGet Package Provider from the web (Make sure Microsoft addresses/ranges are allowed).' + $psResourceGetDownloaded = $false - $null = Install-PackageProvider @providerBootstrapParams + try + { + if (-not $PSResourceGetVersion) + { + # Default to latest version if no version is passed in parameter or specified in configuration. + $psResourceGetUri = "https://www.powershellgallery.com/api/v2/package/$psResourceGetModuleName" + } + else + { + $psResourceGetUri = "https://www.powershellgallery.com/api/v2/package/$psResourceGetModuleName/$PSResourceGetVersion" + } - $nuGetProvider = Get-PackageProvider -Name 'NuGet' -ListAvailable | Select-Object -First 1 + $invokeWebRequestParameters = @{ + # TODO: Should support proxy parameters passed to the script. + Uri = $psResourceGetUri + OutFile = "$PSDependTarget/$psResourceGetModuleName.nupkg" # cSpell: ignore nupkg + ErrorAction = 'Stop' + } - $nuGetProviderVersion = $nuGetProvider.Version.ToString() + $previousProgressPreference = $ProgressPreference + $ProgressPreference = 'SilentlyContinue' - Write-Information -MessageData "Bootstrap: Importing NuGet Package Provider version $nuGetProviderVersion to current session." + # Bootstrapping Microsoft.PowerShell.PSResourceGet. + Invoke-WebRequest @invokeWebRequestParameters - $Null = Import-PackageProvider -Name 'NuGet' -RequiredVersion $nuGetProviderVersion -Force -} + $ProgressPreference = $previousProgressPreference -Write-Progress -Activity 'Bootstrap:' -PercentComplete 10 -CurrentOperation "Ensuring Gallery $Gallery is trusted" + $psResourceGetDownloaded = $true + } + catch + { + Write-Warning -Message ('{0} could not be bootstrapped. Reverting to PowerShellGet. Error: {1}' -f $psResourceGetModuleName, $_.Exception.Message) + } -# Fail if the given PSGallery is not registered. -$previousGalleryInstallationPolicy = (Get-PSRepository -Name $Gallery -ErrorAction 'Stop').InstallationPolicy + $UsePSResourceGet = $false -Set-PSRepository -Name $Gallery -InstallationPolicy 'Trusted' -ErrorAction 'Ignore' + if ($psResourceGetDownloaded) + { + # On Windows PowerShell the command Expand-Archive do not like .nupkg as a zip archive extension. + $zipFileName = ((Split-Path -Path $invokeWebRequestParameters.OutFile -Leaf) -replace 'nupkg', 'zip') -try + $renameItemParameters = @{ + Path = $invokeWebRequestParameters.OutFile + NewName = $zipFileName + Force = $true + } + + Rename-Item @renameItemParameters + + $psResourceGetZipArchivePath = Join-Path -Path (Split-Path -Path $invokeWebRequestParameters.OutFile -Parent) -ChildPath $zipFileName + + $expandArchiveParameters = @{ + Path = $psResourceGetZipArchivePath + DestinationPath = "$PSDependTarget/$psResourceGetModuleName" + Force = $true + } + + Expand-Archive @expandArchiveParameters + + Remove-Item -Path $psResourceGetZipArchivePath + + Import-Module -Name $expandArchiveParameters.DestinationPath -Force + + # Successfully bootstrapped PSResourceGet, so let's use it. + $UsePSResourceGet = $true + } + } + + if ($UsePSResourceGet) + { + $psResourceGetModule = Get-Module -Name $psResourceGetModuleName + + $psResourceGetModuleVersion = $psResourceGetModule.Version.ToString() + + if ($psResourceGetModule.PrivateData.PSData.Prerelease) + { + $psResourceGetModuleVersion += '-{0}' -f $psResourceGetModule.PrivateData.PSData.Prerelease + } + + Write-Information -MessageData ('Using {0} v{1}.' -f $psResourceGetModuleName, $psResourceGetModuleVersion) -InformationAction 'Continue' + + if ($UsePowerShellGetCompatibilityModule) + { + $savePowerShellGetParameters = @{ + Name = 'PowerShellGet' + Path = $PSDependTarget + Repository = 'PSGallery' + TrustRepository = $true + } + + if ($UsePowerShellGetCompatibilityModuleVersion) + { + $savePowerShellGetParameters.Version = $UsePowerShellGetCompatibilityModuleVersion + + # Check if the version is a prerelease. + if ($UsePowerShellGetCompatibilityModuleVersion -match '\d+\.\d+\.\d+-.*') + { + $savePowerShellGetParameters.Prerelease = $true + } + } + + Save-PSResource @savePowerShellGetParameters + + Import-Module -Name "$PSDependTarget/PowerShellGet" + } + } +} + +# Check if legacy PowerShellGet and PSDepend must be bootstrapped. +if (-not ($UseModuleFast -or $UsePSResourceGet)) { - Write-Progress -Activity 'Bootstrap:' -PercentComplete 25 -CurrentOperation 'Checking PowerShellGet' + if ($PSVersionTable.PSVersion.Major -le 5) + { + <# + Making sure the imported PackageManagement module is not from PS7 module + path. The VSCode PS extension is changing the $env:PSModulePath and + prioritize the PS7 path. This is an issue with PowerShellGet because + it loads an old version if available (or fail to load latest). + #> + Get-Module -ListAvailable PackageManagement | + Where-Object -Property 'ModuleBase' -NotMatch 'powershell.7' | + Select-Object -First 1 | + Import-Module -Force + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 0 -CurrentOperation 'NuGet Bootstrap' + + $importModuleParameters = @{ + Name = 'PowerShellGet' + MinimumVersion = '2.0' + MaximumVersion = '2.8.999' + ErrorAction = 'SilentlyContinue' + PassThru = $true + } - # Ensure the module is loaded and retrieve the version you have. - $powerShellGetVersion = (Import-Module -Name 'PowerShellGet' -PassThru -ErrorAction 'SilentlyContinue').Version + if ($AllowOldPowerShellGetModule) + { + $importModuleParameters.Remove('MinimumVersion') + } - Write-Verbose -Message "Bootstrap: The PowerShellGet version is $powerShellGetVersion" + $powerShellGetModule = Import-Module @importModuleParameters - # Versions below 2.0 are considered old, unreliable & not recommended - if (-not $powerShellGetVersion -or ($powerShellGetVersion -lt [System.Version] '2.0' -and -not $AllowOldPowerShellGetModule)) + # Install the package provider if it is not available. + $nuGetProvider = Get-PackageProvider -Name 'NuGet' -ListAvailable -ErrorAction 'SilentlyContinue' | + Select-Object -First 1 + + if (-not $powerShellGetModule -and -not $nuGetProvider) { - Write-Progress -Activity 'Bootstrap:' -PercentComplete 40 -CurrentOperation 'Installing newer version of PowerShellGet' - - $installPowerShellGetParameters = @{ - Name = 'PowerShellGet' - Force = $true - SkipPublisherCheck = $true - AllowClobber = $true - Scope = $Scope - Repository = $Gallery + $providerBootstrapParameters = @{ + Name = 'NuGet' + Force = $true + ForceBootstrap = $true + ErrorAction = 'Stop' + Scope = $Scope } switch ($PSBoundParameters.Keys) { 'Proxy' { - $installPowerShellGetParameters.Add('Proxy', $Proxy) + $providerBootstrapParameters.Add('Proxy', $Proxy) } 'ProxyCredential' { - $installPowerShellGetParameters.Add('ProxyCredential', $ProxyCredential) + $providerBootstrapParameters.Add('ProxyCredential', $ProxyCredential) } - 'GalleryCredential' + 'AllowPrerelease' { - $installPowerShellGetParameters.Add('Credential', $GalleryCredential) + $providerBootstrapParameters.Add('AllowPrerelease', $AllowPrerelease) } } - Write-Progress -Activity 'Bootstrap:' -PercentComplete 60 -CurrentOperation 'Installing newer version of PowerShellGet' - - Install-Module @installPowerShellGetParameters + Write-Information -MessageData 'Bootstrap: Installing NuGet Package Provider from the web (Make sure Microsoft addresses/ranges are allowed).' - Remove-Module -Name 'PowerShellGet' -Force -ErrorAction 'SilentlyContinue' - Remove-Module -Name 'PackageManagement' -Force + $null = Install-PackageProvider @providerBootstrapParameters - $powerShellGetModule = Import-Module PowerShellGet -Force -PassThru + $nuGetProvider = Get-PackageProvider -Name 'NuGet' -ListAvailable | Select-Object -First 1 - $powerShellGetVersion = $powerShellGetModule.Version.ToString() + $nuGetProviderVersion = $nuGetProvider.Version.ToString() - Write-Information -MessageData "Bootstrap: PowerShellGet version loaded is $powerShellGetVersion" - } + Write-Information -MessageData "Bootstrap: Importing NuGet Package Provider version $nuGetProviderVersion to current session." - # Try to import the PSDepend module from the available modules. - $getModuleParameters = @{ - Name = 'PSDepend' - ListAvailable = $true + $Null = Import-PackageProvider -Name 'NuGet' -RequiredVersion $nuGetProviderVersion -Force } - $psDependModule = Get-Module @getModuleParameters - - if ($PSBoundParameters.ContainsKey('MinimumPSDependVersion')) + if ($RegisterGallery) { - try + if ($RegisterGallery.ContainsKey('Name') -and -not [System.String]::IsNullOrEmpty($RegisterGallery.Name)) { - $psDependModule = $psDependModule | Where-Object -FilterScript { $_.Version -ge $MinimumPSDependVersion } + $Gallery = $RegisterGallery.Name } - catch + else { - throw ('There was a problem finding the minimum version of PSDepend. Error: {0}' -f $_) + $RegisterGallery.Name = $Gallery } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 7 -CurrentOperation "Verifying private package repository '$Gallery'" -Completed + + $previousRegisteredRepository = Get-PSRepository -Name $Gallery -ErrorAction 'SilentlyContinue' + + if ($previousRegisteredRepository.SourceLocation -ne $RegisterGallery.SourceLocation) + { + if ($previousRegisteredRepository) + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 9 -CurrentOperation "Re-registrering private package repository '$Gallery'" -Completed + + Unregister-PSRepository -Name $Gallery + + $unregisteredPreviousRepository = $true + } + else + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 9 -CurrentOperation "Registering private package repository '$Gallery'" -Completed + } + + Register-PSRepository @RegisterGallery + } + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 10 -CurrentOperation "Ensuring Gallery $Gallery is trusted" + + # Fail if the given PSGallery is not registered. + $previousGalleryInstallationPolicy = (Get-PSRepository -Name $Gallery -ErrorAction 'Stop').Trusted + + $updatedGalleryInstallationPolicy = $false + + if ($previousGalleryInstallationPolicy -ne $true) + { + $updatedGalleryInstallationPolicy = $true + + # Only change policy if the repository is not trusted + Set-PSRepository -Name $Gallery -InstallationPolicy 'Trusted' -ErrorAction 'Ignore' } +} - if (-not $psDependModule) +try +{ + # Check if legacy PowerShellGet and PSDepend must be used. + if (-not ($UseModuleFast -or $UsePSResourceGet)) { - # PSDepend module not found, installing or saving it. - if ($PSDependTarget -in 'CurrentUser', 'AllUsers') + Write-Progress -Activity 'Bootstrap:' -PercentComplete 25 -CurrentOperation 'Checking PowerShellGet' + + # Ensure the module is loaded and retrieve the version you have. + $powerShellGetVersion = (Import-Module -Name 'PowerShellGet' -PassThru -ErrorAction 'SilentlyContinue').Version + + Write-Verbose -Message "Bootstrap: The PowerShellGet version is $powerShellGetVersion" + + # Versions below 2.0 are considered old, unreliable & not recommended + if (-not $powerShellGetVersion -or ($powerShellGetVersion -lt [System.Version] '2.0' -and -not $AllowOldPowerShellGetModule)) { - Write-Debug -Message "PSDepend module not found. Attempting to install from Gallery $Gallery." + Write-Progress -Activity 'Bootstrap:' -PercentComplete 40 -CurrentOperation 'Fetching newer version of PowerShellGet' + + # PowerShellGet module not found, installing or saving it. + if ($PSDependTarget -in 'CurrentUser', 'AllUsers') + { + Write-Debug -Message "PowerShellGet module not found. Attempting to install from Gallery $Gallery." + + Write-Warning -Message "Installing PowerShellGet in $PSDependTarget Scope." + + $installPowerShellGetParameters = @{ + Name = 'PowerShellGet' + Force = $true + SkipPublisherCheck = $true + AllowClobber = $true + Scope = $Scope + Repository = $Gallery + MaximumVersion = '2.8.999' + } + + switch ($PSBoundParameters.Keys) + { + 'Proxy' + { + $installPowerShellGetParameters.Add('Proxy', $Proxy) + } + + 'ProxyCredential' + { + $installPowerShellGetParameters.Add('ProxyCredential', $ProxyCredential) + } + + 'GalleryCredential' + { + $installPowerShellGetParameters.Add('Credential', $GalleryCredential) + } + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 60 -CurrentOperation 'Installing newer version of PowerShellGet' + + Install-Module @installPowerShellGetParameters + } + else + { + Write-Debug -Message "PowerShellGet module not found. Attempting to Save from Gallery $Gallery to $PSDependTarget" + + $saveModuleParameters = @{ + Name = 'PowerShellGet' + Repository = $Gallery + Path = $PSDependTarget + Force = $true + MaximumVersion = '2.8.999' + } - Write-Warning -Message "Installing PSDepend in $PSDependTarget Scope." + Write-Progress -Activity 'Bootstrap:' -PercentComplete 60 -CurrentOperation "Saving PowerShellGet from $Gallery to $Scope" - $installPSDependParameters = @{ - Name = 'PSDepend' - Repository = $Gallery - Force = $true - Scope = $PSDependTarget - SkipPublisherCheck = $true - AllowClobber = $true + Save-Module @saveModuleParameters } - if ($MinimumPSDependVersion) + Write-Debug -Message 'Removing previous versions of PowerShellGet and PackageManagement from session' + + Get-Module -Name 'PowerShellGet' -All | Remove-Module -Force -ErrorAction 'SilentlyContinue' + Get-Module -Name 'PackageManagement' -All | Remove-Module -Force + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 65 -CurrentOperation 'Loading latest version of PowerShellGet' + + Write-Debug -Message 'Importing latest PowerShellGet and PackageManagement versions into session' + + if ($AllowOldPowerShellGetModule) { - $installPSDependParameters.Add('MinimumVersion', $MinimumPSDependVersion) + $powerShellGetModule = Import-Module -Name 'PowerShellGet' -Force -PassThru } + else + { + Import-Module -Name 'PackageManagement' -MinimumVersion '1.4.8.1' -Force - Write-Progress -Activity 'Bootstrap:' -PercentComplete 75 -CurrentOperation "Installing PSDepend from $Gallery" + $powerShellGetModule = Import-Module -Name 'PowerShellGet' -MinimumVersion '2.2.5' -Force -PassThru + } - Install-Module @installPSDependParameters + $powerShellGetVersion = $powerShellGetModule.Version.ToString() + + Write-Information -MessageData "Bootstrap: PowerShellGet version loaded is $powerShellGetVersion" + } + + # Try to import the PSDepend module from the available modules. + $getModuleParameters = @{ + Name = 'PSDepend' + ListAvailable = $true } - else - { - Write-Debug -Message "PSDepend module not found. Attempting to Save from Gallery $Gallery to $PSDependTarget" - $saveModuleParameters = @{ - Name = 'PSDepend' - Repository = $Gallery - Path = $PSDependTarget - Force = $true + $psDependModule = Get-Module @getModuleParameters + + if ($PSBoundParameters.ContainsKey('MinimumPSDependVersion')) + { + try + { + $psDependModule = $psDependModule | Where-Object -FilterScript { $_.Version -ge $MinimumPSDependVersion } + } + catch + { + throw ('There was a problem finding the minimum version of PSDepend. Error: {0}' -f $_) } + } + + if (-not $psDependModule) + { + Write-Debug -Message 'PSDepend module not found.' - if ($MinimumPSDependVersion) + # PSDepend module not found, installing or saving it. + if ($PSDependTarget -in 'CurrentUser', 'AllUsers') { - $saveModuleParameters.add('MinimumVersion', $MinimumPSDependVersion) + Write-Debug -Message "Attempting to install from Gallery '$Gallery'." + + Write-Warning -Message "Installing PSDepend in $PSDependTarget Scope." + + $installPSDependParameters = @{ + Name = 'PSDepend' + Repository = $Gallery + Force = $true + Scope = $PSDependTarget + SkipPublisherCheck = $true + AllowClobber = $true + } + + if ($MinimumPSDependVersion) + { + $installPSDependParameters.Add('MinimumVersion', $MinimumPSDependVersion) + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 75 -CurrentOperation "Installing PSDepend from $Gallery" + + Install-Module @installPSDependParameters + } + else + { + Write-Debug -Message "Attempting to Save from Gallery $Gallery to $PSDependTarget" + + $saveModuleParameters = @{ + Name = 'PSDepend' + Repository = $Gallery + Path = $PSDependTarget + Force = $true + } + + if ($MinimumPSDependVersion) + { + $saveModuleParameters.add('MinimumVersion', $MinimumPSDependVersion) + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 75 -CurrentOperation "Saving PSDepend from $Gallery to $PSDependTarget" + + Save-Module @saveModuleParameters } + } - Write-Progress -Activity 'Bootstrap:' -PercentComplete 75 -CurrentOperation "Saving & Importing PSDepend from $Gallery to $Scope" + Write-Progress -Activity 'Bootstrap:' -PercentComplete 80 -CurrentOperation 'Importing PSDepend' - Save-Module @saveModuleParameters + $importModulePSDependParameters = @{ + Name = 'PSDepend' + ErrorAction = 'Stop' + Force = $true + } + + if ($PSBoundParameters.ContainsKey('MinimumPSDependVersion')) + { + $importModulePSDependParameters.Add('MinimumVersion', $MinimumPSDependVersion) } - } - Write-Progress -Activity 'Bootstrap:' -PercentComplete 80 -CurrentOperation 'Loading PSDepend' + # We should have successfully bootstrapped PSDepend. Fail if not available. + $null = Import-Module @importModulePSDependParameters - $importModulePSDependParameters = @{ - Name = 'PSDepend' - ErrorAction = 'Stop' - Force = $true + Write-Progress -Activity 'Bootstrap:' -PercentComplete 81 -CurrentOperation 'Invoke PSDepend' + + if ($WithYAML) + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 82 -CurrentOperation 'Verifying PowerShell module PowerShell-Yaml' + + if (-not (Get-Module -ListAvailable -Name 'PowerShell-Yaml')) + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 85 -CurrentOperation 'Installing PowerShell module PowerShell-Yaml' + + Write-Verbose -Message "PowerShell-Yaml module not found. Attempting to Save from Gallery '$Gallery' to '$PSDependTarget'." + + $SaveModuleParam = @{ + Name = 'PowerShell-Yaml' + Repository = $Gallery + Path = $PSDependTarget + Force = $true + } + + Save-Module @SaveModuleParam + } + else + { + Write-Verbose -Message 'PowerShell-Yaml is already available' + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 88 -CurrentOperation 'Importing PowerShell module PowerShell-Yaml' + } } - if ($PSBoundParameters.ContainsKey('MinimumPSDependVersion')) + if (Test-Path -Path $DependencyFile) { - $importModulePSDependParameters.Add('MinimumVersion', $MinimumPSDependVersion) - } + if ($UseModuleFast -or $UsePSResourceGet) + { + $requiredModules = Import-PowerShellDataFile -Path $DependencyFile - # We should have successfully bootstrapped PSDepend. Fail if not available. - $null = Import-Module @importModulePSDependParameters + $requiredModules = $requiredModules.GetEnumerator() | + Where-Object -FilterScript { $_.Name -ne 'PSDependOptions' } - if ($WithYAML) - { - Write-Progress -Activity 'Bootstrap:' -PercentComplete 82 -CurrentOperation 'Verifying PowerShell module PowerShell-Yaml' + if ($UseModuleFast) + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 90 -CurrentOperation 'Invoking ModuleFast' - if (-not (Get-Module -ListAvailable -Name 'PowerShell-Yaml')) - { - Write-Progress -Activity 'Bootstrap:' -PercentComplete 85 -CurrentOperation 'Installing PowerShell module PowerShell-Yaml' + Write-Progress -Activity 'ModuleFast:' -PercentComplete 0 -CurrentOperation 'Restoring Build Dependencies' - Write-Verbose -Message "PowerShell-Yaml module not found. Attempting to Save from Gallery $Gallery to $PSDependTarget" + $modulesToSave = @( + 'PSDepend' # Always include PSDepend for backward compatibility. + ) - $SaveModuleParam = @{ - Name = 'PowerShell-Yaml' - Repository = $Gallery - Path = $PSDependTarget - Force = $true + if ($WithYAML) + { + $modulesToSave += 'PowerShell-Yaml' + } + + if ($UsePowerShellGetCompatibilityModule) + { + Write-Debug -Message 'PowerShellGet compatibility module is configured to be used.' + + # This is needed to ensure that the PowerShellGet compatibility module works. + $psResourceGetModuleName = 'Microsoft.PowerShell.PSResourceGet' + + if ($PSResourceGetVersion) + { + $modulesToSave += ('{0}:[{1}]' -f $psResourceGetModuleName, $PSResourceGetVersion) + } + else + { + $modulesToSave += $psResourceGetModuleName + } + + $powerShellGetCompatibilityModuleName = 'PowerShellGet' + + if ($UsePowerShellGetCompatibilityModuleVersion) + { + $modulesToSave += ('{0}:[{1}]' -f $powerShellGetCompatibilityModuleName, $UsePowerShellGetCompatibilityModuleVersion) + } + else + { + $modulesToSave += $powerShellGetCompatibilityModuleName + } + } + + foreach ($requiredModule in $requiredModules) + { + # If the RequiredModules.psd1 entry is an Hashtable then special handling is needed. + if ($requiredModule.Value -is [System.Collections.Hashtable]) + { + if (-not $requiredModule.Value.Version) + { + $requiredModuleVersion = 'latest' + } + else + { + $requiredModuleVersion = $requiredModule.Value.Version + } + + if ($requiredModuleVersion -eq 'latest') + { + $moduleNameSuffix = '' + + if ($requiredModule.Value.Parameters.AllowPrerelease -eq $true) + { + <# + Adding '!' to the module name indicate to ModuleFast + that is should also evaluate pre-releases. + #> + $moduleNameSuffix = '!' + } + + $modulesToSave += ('{0}{1}' -f $requiredModule.Name, $moduleNameSuffix) + } + else + { + $modulesToSave += ('{0}:[{1}]' -f $requiredModule.Name, $requiredModuleVersion) + } + } + else + { + if ($requiredModule.Value -eq 'latest') + { + $modulesToSave += $requiredModule.Name + } + else + { + # Handle different nuget version operators already present. + if ($requiredModule.Value -match '[!|:|[|(|,|>|<|=]') + { + $modulesToSave += ('{0}{1}' -f $requiredModule.Name, $requiredModule.Value) + } + else + { + # Assuming the version is a fixed version. + $modulesToSave += ('{0}:[{1}]' -f $requiredModule.Name, $requiredModule.Value) + } + } + } + } + + Write-Debug -Message ("Required modules to retrieve plan for:`n{0}" -f ($modulesToSave | Out-String)) + + $installModuleFastParameters = @{ + Destination = $PSDependTarget + DestinationOnly = $true + NoPSModulePathUpdate = $true + NoProfileUpdate = $true + Update = $true + Confirm = $false + } + + $moduleFastPlan = Install-ModuleFast -Specification $modulesToSave -Plan @installModuleFastParameters + + Write-Debug -Message ("Missing modules that need to be saved:`n{0}" -f ($moduleFastPlan | Out-String)) + + if ($moduleFastPlan) + { + # Clear all modules in plan from the current session so they can be fetched again. + $moduleFastPlan.Name | Get-Module | Remove-Module -Force + + $moduleFastPlan | Install-ModuleFast @installModuleFastParameters + } + else + { + Write-Verbose -Message 'All required modules were already up to date' + } + + Write-Progress -Activity 'ModuleFast:' -PercentComplete 100 -CurrentOperation 'Dependencies restored' -Completed } - Save-Module @SaveModuleParam + if ($UsePSResourceGet) + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 90 -CurrentOperation 'Invoking PSResourceGet' + + $modulesToSave = @( + @{ + Name = 'PSDepend' # Always include PSDepend for backward compatibility. + } + ) + + if ($WithYAML) + { + $modulesToSave += @{ + Name = 'PowerShell-Yaml' + } + } + + # Prepare hashtable that can be concatenated to the Save-PSResource parameters. + foreach ($requiredModule in $requiredModules) + { + # If the RequiredModules.psd1 entry is an Hashtable then special handling is needed. + if ($requiredModule.Value -is [System.Collections.Hashtable]) + { + $saveModuleHashtable = @{ + Name = $requiredModule.Name + } + + if ($requiredModule.Value.Version -and $requiredModule.Value.Version -ne 'latest') + { + $saveModuleHashtable.Version = $requiredModule.Value.Version + } + + if ($requiredModule.Value.Parameters.AllowPrerelease -eq $true) + { + $saveModuleHashtable.Prerelease = $true + } + + $modulesToSave += $saveModuleHashtable + } + else + { + if ($requiredModule.Value -eq 'latest') + { + $modulesToSave += @{ + Name = $requiredModule.Name + } + } + else + { + $modulesToSave += @{ + Name = $requiredModule.Name + Version = $requiredModule.Value + } + } + } + } + + $percentagePerModule = [System.Math]::Floor(100 / $modulesToSave.Length) + + $progressPercentage = 0 + + Write-Progress -Activity 'PSResourceGet:' -PercentComplete $progressPercentage -CurrentOperation 'Restoring Build Dependencies' + + foreach ($currentModule in $modulesToSave) + { + Write-Progress -Activity 'PSResourceGet:' -PercentComplete $progressPercentage -CurrentOperation 'Restoring Build Dependencies' -Status ('Saving module {0}' -f $savePSResourceParameters.Name) + + $savePSResourceParameters = @{ + Path = $PSDependTarget + TrustRepository = $true + Confirm = $false + } + + # Concatenate the module parameters to the Save-PSResource parameters. + $savePSResourceParameters += $currentModule + + # Modules that Sampler depend on that cannot be refreshed without a new session. + $skipModule = @('PowerShell-Yaml') + + if ($savePSResourceParameters.Name -in $skipModule -and (Get-Module -Name $savePSResourceParameters.Name)) + { + Write-Progress -Activity 'PSResourceGet:' -PercentComplete $progressPercentage -CurrentOperation 'Restoring Build Dependencies' -Status ('Skipping module {0}' -f $savePSResourceParameters.Name) + + Write-Information -MessageData ('Skipping the module {0} since it cannot be refresh while loaded into the session. To refresh the module open a new session and resolve dependencies again.' -f $savePSResourceParameters.Name) -InformationAction 'Continue' + } + else + { + # Clear all module from the current session so any new version fetched will be re-imported. + Get-Module -Name $savePSResourceParameters.Name | Remove-Module -Force + + Save-PSResource @savePSResourceParameters -ErrorVariable 'savePSResourceError' + + if ($savePSResourceError) + { + Write-Warning -Message 'Save-PSResource could not save (replace) one or more dependencies. This can be due to the module is loaded into the session (and referencing assemblies). Close the current session and open a new session and try again.' + } + } + + $progressPercentage += $percentagePerModule + } + + Write-Progress -Activity 'PSResourceGet:' -PercentComplete 100 -CurrentOperation 'Dependencies restored' -Completed + } } else { - Write-Verbose "PowerShell-Yaml is already available" + Write-Progress -Activity 'Bootstrap:' -PercentComplete 90 -CurrentOperation 'Invoking PSDepend' + + Write-Progress -Activity 'PSDepend:' -PercentComplete 0 -CurrentOperation 'Restoring Build Dependencies' + + $psDependParameters = @{ + Force = $true + Path = $DependencyFile + } + + # TODO: Handle when the Dependency file is in YAML, and -WithYAML is specified. + Invoke-PSDepend @psDependParameters + + Write-Progress -Activity 'PSDepend:' -PercentComplete 100 -CurrentOperation 'Dependencies restored' -Completed } } + else + { + Write-Warning -Message "The dependency file '$DependencyFile' could not be found." + } - Write-Progress -Activity 'Bootstrap:' -PercentComplete 90 -CurrentOperation 'Invoke PSDepend' - - Write-Progress -Activity "PSDepend:" -PercentComplete 0 -CurrentOperation "Restoring Build Dependencies" + Write-Progress -Activity 'Bootstrap:' -PercentComplete 100 -CurrentOperation 'Bootstrap complete' -Completed +} +finally +{ + if ($RegisterGallery) + { + Write-Verbose -Message "Removing private package repository '$Gallery'." + Unregister-PSRepository -Name $Gallery + } - if (Test-Path -Path $DependencyFile) + if ($unregisteredPreviousRepository) { - $psDependParameters = @{ - Force = $true - Path = $DependencyFile + Write-Verbose -Message "Reverting private package repository '$Gallery' to previous location URI:s." + + $registerPSRepositoryParameters = @{ + Name = $previousRegisteredRepository.Name + InstallationPolicy = $previousRegisteredRepository.InstallationPolicy } - # TODO: Handle when the Dependency file is in YAML, and -WithYAML is specified. - Invoke-PSDepend @psDependParameters + if ($previousRegisteredRepository.SourceLocation) + { + $registerPSRepositoryParameters.SourceLocation = $previousRegisteredRepository.SourceLocation + } + + if ($previousRegisteredRepository.PublishLocation) + { + $registerPSRepositoryParameters.PublishLocation = $previousRegisteredRepository.PublishLocation + } + + if ($previousRegisteredRepository.ScriptSourceLocation) + { + $registerPSRepositoryParameters.ScriptSourceLocation = $previousRegisteredRepository.ScriptSourceLocation + } + + if ($previousRegisteredRepository.ScriptPublishLocation) + { + $registerPSRepositoryParameters.ScriptPublishLocation = $previousRegisteredRepository.ScriptPublishLocation + } + + Register-PSRepository @registerPSRepositoryParameters } - Write-Progress -Activity "PSDepend:" -PercentComplete 100 -CurrentOperation "Dependencies restored" -Completed + if ($updatedGalleryInstallationPolicy -eq $true -and $previousGalleryInstallationPolicy -ne $true) + { + # Only try to revert installation policy if the repository exist + if ((Get-PSRepository -Name $Gallery -ErrorAction 'SilentlyContinue')) + { + # Reverting the Installation Policy for the given gallery if it was not already trusted + Set-PSRepository -Name $Gallery -InstallationPolicy 'Untrusted' + } + } - Write-Progress -Activity 'Bootstrap:' -PercentComplete 100 -CurrentOperation "Bootstrap complete" -Completed -} -finally -{ - # Reverting the Installation Policy for the given gallery - Set-PSRepository -Name $Gallery -InstallationPolicy $previousGalleryInstallationPolicy - Write-Verbose -Message "Project Bootstrapped, returning to Invoke-Build" + Write-Verbose -Message 'Project Bootstrapped, returning to Invoke-Build.' } diff --git a/Resolve-Dependency.psd1 b/Resolve-Dependency.psd1 index 2ae8c0d..07945f8 100644 --- a/Resolve-Dependency.psd1 +++ b/Resolve-Dependency.psd1 @@ -2,4 +2,14 @@ Gallery = 'PSGallery' AllowPrerelease = $false WithYAML = $true + + #UseModuleFast = $true + #ModuleFastVersion = '0.1.2' + #ModuleFastBleedingEdge = $true + + UsePSResourceGet = $true + #PSResourceGetVersion = '1.0.1' + + UsePowerShellGetCompatibilityModule = $true + UsePowerShellGetCompatibilityModuleVersion = '3.0.23-beta23' } diff --git a/azure-pipelines.yml b/azure-pipelines.yml index fef6bcc..6d46e39 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -28,7 +28,7 @@ stages: vmImage: 'windows-latest' steps: - pwsh: | - dotnet tool install --global GitVersion.Tool + dotnet tool install --global GitVersion.Tool --version 5.* $gitVersionObject = dotnet-gitversion | ConvertFrom-Json $gitVersionObject.PSObject.Properties.ForEach{ Write-Host -Object "Setting Task Variable '$($_.Name)' with value '$($_.Value)'." diff --git a/build.ps1 b/build.ps1 index b40a72e..f4a0fae 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,6 +1,6 @@ <# .DESCRIPTION - Bootstrap and build script for PowerShell module CI/CD pipeline + Bootstrap and build script for PowerShell module CI/CD pipeline. .PARAMETER Tasks The task or tasks to run. The default value is '.' (runs the default task). @@ -56,6 +56,19 @@ .PARAMETER AutoRestore Not yet written. + + .PARAMETER UseModuleFast + Specifies to use ModuleFast instead of PowerShellGet to resolve dependencies + faster. + + .PARAMETER UsePSResourceGet + Specifies to use PSResourceGet instead of PowerShellGet to resolve dependencies + faster. This can also be configured in Resolve-Dependency.psd1. + + .PARAMETER UsePowerShellGetCompatibilityModule + Specifies to use the compatibility module PowerShellGet. This parameter + only works then the method of downloading dependencies is PSResourceGet. + This can also be configured in Resolve-Dependency.psd1. #> [CmdletBinding()] param @@ -121,7 +134,19 @@ param [Parameter()] [System.Management.Automation.SwitchParameter] - $AutoRestore + $AutoRestore, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $UseModuleFast, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $UsePSResourceGet, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $UsePowerShellGetCompatibilityModule ) <# @@ -132,7 +157,6 @@ param process { - if ($MyInvocation.ScriptName -notLike '*Invoke-Build.ps1') { # Only run the process block through InvokeBuild (look at the Begin block at the bottom of this script). @@ -178,7 +202,7 @@ process ConvertFrom-Yaml -Yaml (Get-Content -Raw $configFile) } - # Native Support for JSON and JSONC (by Removing comments) + # Support for JSON and JSONC (by Removing comments) when module PowerShell-Yaml is available '\.[json|jsonc]' { $jsonFile = Get-Content -Raw -Path $configFile @@ -336,7 +360,7 @@ process } } -Begin +begin { # Find build config if not specified. if (-not $BuildConfig) @@ -356,7 +380,8 @@ Begin $BuildConfig = $config[0] } - else { + else + { $BuildConfig = $config } } @@ -449,7 +474,8 @@ Begin if ($ResolveDependency) { - Write-Host -Object "[pre-build] Resolving dependencies." -ForegroundColor Green + Write-Host -Object "[pre-build] Resolving dependencies using preferred method." -ForegroundColor Green + $resolveDependencyParams = @{ } # If BuildConfig is a Yaml file, bootstrap powershell-yaml via ResolveDependency. diff --git a/build.yaml b/build.yaml index c3e77f2..f3e0655 100644 --- a/build.yaml +++ b/build.yaml @@ -5,20 +5,48 @@ CopyPaths: - en-US - DSCResources - - Modules Encoding: UTF8 -VersionedOutputDirectory: true +BuiltModuleSubdirectory: builtModule + +ModuleBuildTasks: + Sampler: + - '*.build.Sampler.ib.tasks' + Sampler.GitHubTasks: + - '*.ib.tasks' + DscResource.DocGenerator: + - 'Task.*' + DscResource.Test: + - 'Task.*' + +TaskHeader: | + param($Path) + "" + "=" * 79 + Write-Build Cyan "`t`t`t$($Task.Name.replace("_"," ").ToUpper())" + Write-Build DarkGray "$(Get-BuildSynopsis $Task)" + "-" * 79 + Write-Build DarkGray " $Path" + Write-Build DarkGray " $($Task.InvocationInfo.ScriptName):$($Task.InvocationInfo.ScriptLineNumber)" + "" #################################################### # ModuleBuilder Submodules Configuration # #################################################### - NestedModule: - DscResource.Common: - CopyOnly: true - Path: ./output/RequiredModules/DscResource.Common - AddToManifest: false - Exclude: PSGetModuleInfo.xml + DscResource.Common: + CopyOnly: true + Path: ./output/RequiredModules/DscResource.Common + AddToManifest: false + Exclude: PSGetModuleInfo.xml + HyperVDsc.Common: + Prefix: prefix.ps1 + VersionedOutputDirectory: false + CopyPaths: + - en-US + Encoding: UTF8 + + AddToManifest: false + Exclude: PSGetModuleInfo.xml #################################################### # Pipeline Configuration # @@ -33,11 +61,17 @@ BuildWorkflow: - Build_Module_ModuleBuilder - Build_NestedModules_ModuleBuilder - Create_Changelog_Release_Output + + docs: - Generate_Conceptual_Help - Generate_Wiki_Content + - Generate_Wiki_Sidebar + - Clean_Markdown_Metadata + - Package_Wiki_Content pack: - build + - docs - package_module_nupkg hqrmtest: @@ -45,6 +79,7 @@ BuildWorkflow: test: - Pester_Tests_Stop_On_Fail + - Convert_Pester_Coverage - Pester_If_Code_Coverage_Under_Threshold publish: @@ -67,6 +102,9 @@ Pester: CodeCoverageOutputFileEncoding: ascii CodeCoverageThreshold: 80 +#################################################### +# Pester Configuration (DscResource.Test) # +#################################################### DscTest: OutputFormat: NUnitXML ExcludeTag: @@ -77,25 +115,9 @@ DscTest: - Modules/DscResource.Common MainGitBranch: main -ModuleBuildTasks: - Sampler: - - '*.build.Sampler.ib.tasks' - Sampler.GitHubTasks: - - '*.ib.tasks' - DscResource.DocGenerator: - - 'Task.*' - -TaskHeader: | - param($Path) - "" - "=" * 79 - Write-Build Cyan "`t`t`t$($Task.Name.replace("_"," ").ToUpper())" - Write-Build DarkGray "$(Get-BuildSynopsis $Task)" - "-" * 79 - Write-Build DarkGray " $Path" - Write-Build DarkGray " $($Task.InvocationInfo.ScriptName):$($Task.InvocationInfo.ScriptLineNumber)" - "" - +#################################################### +# GitHub Configuration # +#################################################### GitHubConfig: GitHubFilesToAdd: - 'CHANGELOG.md' @@ -115,3 +137,18 @@ DscResource.DocGenerator: - '_(.+?)_' # Match Italic (underscore) - '\*\*(.+?)\*\*' # Match bold - '\*(.+?)\*' # Match Italic (asterisk) + Publish_GitHub_Wiki_Content: + Debug: false + Generate_Markdown_For_DSC_Resources: + MofResourceMetadata: + Type: MofResource + Category: Resources + ClassResourceMetadata: + Type: ClassResource + Category: Resources + CompositeResourceMetadata: + Type: CompositeResource + Category: Resources + Generate_Wiki_Sidebar: + Debug: false + AlwaysOverwrite: true diff --git a/source/HyperVDsc.psd1 b/source/HyperVDsc.psd1 index f172ad5..39837f6 100644 --- a/source/HyperVDsc.psd1 +++ b/source/HyperVDsc.psd1 @@ -1,4 +1,7 @@ @{ + # Script module or binary module file associated with this manifest. + RootModule = 'HyperVDsc.psm1' + # Version number of this module. moduleVersion = '0.0.1' diff --git a/source/HyperVDsc.psm1 b/source/HyperVDsc.psm1 new file mode 100644 index 0000000..932b798 --- /dev/null +++ b/source/HyperVDsc.psm1 @@ -0,0 +1 @@ +# Empty file diff --git a/source/Modules/HyperVDsc.Common/HyperVDsc.Common.psd1 b/source/Modules/HyperVDsc.Common/HyperVDsc.Common.psd1 index 1ad8b7f..c4b09e8 100644 --- a/source/Modules/HyperVDsc.Common/HyperVDsc.Common.psd1 +++ b/source/Modules/HyperVDsc.Common/HyperVDsc.Common.psd1 @@ -30,14 +30,7 @@ Description = 'Functions used by the DSC resources in HyperVDsc.' # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. - FunctionsToExport = @( - 'Set-VMProperty' - 'Set-VMState' - 'Wait-VMIPAddress' - 'ConvertTo-TimeSpan' - 'ConvertFrom-TimeSpan' - 'Get-VMHyperV' - ) + FunctionsToExport = @() # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. CmdletsToExport = @() @@ -54,4 +47,3 @@ } # End of PSData hashtable } # End of PrivateData hashtable } - diff --git a/source/Modules/HyperVDsc.Common/HyperVDsc.Common.psm1 b/source/Modules/HyperVDsc.Common/HyperVDsc.Common.psm1 deleted file mode 100644 index 6795cf4..0000000 --- a/source/Modules/HyperVDsc.Common/HyperVDsc.Common.psm1 +++ /dev/null @@ -1,341 +0,0 @@ -$script:dscResourceCommonModulePath = Join-Path -Path $PSScriptRoot -ChildPath '../DscResource.Common' - -Import-Module -Name $script:dscResourceCommonModulePath - -$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' - -<# - .SYNOPSIS - Sets one or more virtual machine properties, powering the VM - off if required. - - .PARAMETER Name - Name of the virtual machine to apply the changes to. - - .PARAMETER VMName - Name of the virtual machine to apply the changes to. - - .PARAMETER VMCommand - The Hyper-V cmdlet name to call to enact the changes. - - .PARAMETER ChangeProperty - The collection of cmdlet parameter names and values to pass to the command. - - .PARAMETER WaitForIP - Waits for the virtual machine to report an IP address when transitioning - into a running state. - - .PARAMETER RestartIfNeeded - Power cycle the virtual machine if changes are required. -#> -function Set-VMProperty -{ - [CmdletBinding(DefaultParameterSetName = 'Name')] - param - ( - [Parameter(Mandatory = $true, ParameterSetName = 'Name')] - [System.String] - $Name, - - [Parameter(Mandatory = $true, ParameterSetName = 'VMName')] - [System.String] - $VMName, - - [Parameter(Mandatory = $true)] - [System.String] - $VMCommand, - - [Parameter(Mandatory = $true)] - [System.Collections.Hashtable] - $ChangeProperty, - - [Parameter()] - [System.Boolean] - $WaitForIP, - - [Parameter()] - [System.Boolean] - $RestartIfNeeded - ) - - if ($PSBoundParameters.ContainsKey('VMName')) - { - # Add the -Name property to the ChangeProperty hashtable for splatting - $ChangeProperty['VMName'] = $VMName - - # Set the common parameters for splatting against Get-VM and Set-VMState - $vmCommonProperty = @{ - Name = $VMName - } - - # Ensure that the name parameter is set for verbose messages - $Name = $VMName - } - else - { - # Add the -Name property to the ChangeProperty hashtable for splatting - $ChangeProperty['Name'] = $Name - - # Set the common parameters for splatting against Get-VM and Set-VMState - $vmCommonProperty = @{ - Name = $Name - } - } - - $vmObject = Get-VM @vmCommonProperty - $vmOriginalState = $vmObject.State - - if ($vmOriginalState -ne 'Off' -and $RestartIfNeeded) - { - # Turn the vm off to make changes - Set-VMState @vmCommonProperty -State Off - - Write-Verbose -Message ($script:localizedData.UpdatingVMProperties -f $Name) - # Make changes using the passed hashtable - & $VMCommand @ChangeProperty - - # Cannot move an off VM to a paused state - only to running state - if ($vmOriginalState -eq 'Running') - { - Set-VMState @vmCommonProperty -State Running -WaitForIP $WaitForIP - } - - Write-Verbose -Message ($script:localizedData.VMPropertiesUpdated -f $Name) - - # Cannot restore a vm to a paused state - if ($vmOriginalState -eq 'Paused') - { - Write-Warning -Message ($script:localizedData.VMStateWillBeOffWarning -f $Name) - } - } - elseif ($vmOriginalState -eq 'Off') - { - Write-Verbose -Message ($script:localizedData.UpdatingVMProperties -f $Name) - & $VMCommand @ChangeProperty - Write-Verbose -Message ($script:localizedData.VMPropertiesUpdated -f $Name) - } - else - { - $errorMessage = $script:localizedData.CannotUpdatePropertiesOnlineError -f $Name, $vmOriginalState - New-InvalidOperationException -Message $errorMessage - } -} #end function - -<# - .SYNOPSIS - Sets one or more virtual machine properties, powering the VM - off if required. - - .PARAMETER Name - Name of the virtual machine to apply the changes to. - - .PARAMETER State - The target power state of the virtual machine. - - .PARAMETER ChangeProperty - The collection of cmdlet parameter names and values to pass to the command. - - .PARAMETER WaitForIP - Waits for the virtual machine to be report an IP address when transitioning - into a running state. -#> -function Set-VMState -{ - [CmdletBinding()] - param - ( - [Parameter(Mandatory = $true)] - [Alias('VMName')] - [System.String] - $Name, - - [Parameter(Mandatory = $true)] - [ValidateSet('Running','Paused','Off')] - [System.String] - $State, - - [Parameter()] - [System.Boolean] - $WaitForIP - ) - - switch ($State) - { - 'Running' { - $vmCurrentState = (Get-VM -Name $Name).State - if ($vmCurrentState -eq 'Paused') - { - # If VM is in paused state, use resume-vm to make it running - Write-Verbose -Message ($script:localizedData.ResumingVM -f $Name) - Resume-VM -Name $Name - } - elseif ($vmCurrentState -eq 'Off') - { - # If VM is Off, use start-vm to make it running - Write-Verbose -Message ($script:localizedData.StartingVM -f $Name) - Start-VM -Name $Name - } - - if ($WaitForIP) - { - Wait-VMIPAddress -Name $Name -Verbose - } - } - 'Paused' { - if ($vmCurrentState -ne 'Off') - { - Write-Verbose -Message ($script:localizedData.SuspendingVM -f $Name) - Suspend-VM -Name $Name - } - } - 'Off' { - if ($vmCurrentState -ne 'Off') - { - Write-Verbose -Message ($script:localizedData.StoppingVM -f $Name) - Stop-VM -Name $Name -Force -WarningAction SilentlyContinue - } - } - } -} #end function - -<# - .SYNOPSIS - Waits for a virtual machine to be assigned an IP address. - - .PARAMETER Name - Name of the virtual machine to apply the changes to. - - .PARAMETER Timeout - Number of seconds to wait before timing out. Defaults to 300 (5 minutes). -#> -function Wait-VMIPAddress -{ - [CmdletBinding()] - param - ( - [Parameter(Mandatory = $true)] - [Alias('VMName')] - [System.String] - $Name, - - [Parameter()] - [System.Int32] - $Timeout = 300 - ) - - [System.Int32] $elapsedSeconds = 0 - while ((Get-VMNetworkAdapter -VMName $Name).IpAddresses.Count -lt 2) - { - Write-Verbose -Message ($script:localizedData.WaitingForVMIPAddress -f $Name) - Start-Sleep -Seconds 3 - - $elapsedSeconds += 3 - if ($elapsedSeconds -gt $Timeout) - { - $errorMessage = $script:localizedData.WaitForVMIPAddressTimeoutError -f $Name, $Timeout - - New-ObjectNotFoundException -Message $errorMessage - } - } -} #end function - -<# - .SYNOPSIS - Converts a number of seconds, minutes, hours or days into a System.TimeSpan object. - - .PARAMETER TimeInterval - The total number of seconds, minutes, hours or days to convert. - - .PARAMETER TimeSpanType - Convert using specified interval type. -#> -function ConvertTo-TimeSpan -{ - [CmdletBinding()] - [OutputType([System.TimeSpan])] - param - ( - [Parameter(Mandatory = $true)] - [System.UInt32] - $TimeInterval, - - [Parameter(Mandatory = $true)] - [ValidateSet('Seconds','Minutes','Hours','Days')] - [System.String] - $TimeIntervalType - ) - - $newTimeSpanParams = @{ } - switch ($TimeIntervalType) - { - 'Seconds' { $newTimeSpanParams['Seconds'] = $TimeInterval } - 'Minutes' { $newTimeSpanParams['Minutes'] = $TimeInterval } - 'Hours' { $newTimeSpanParams['Hours'] = $TimeInterval } - 'Days' { $newTimeSpanParams['Days'] = $TimeInterval } - } - return (New-TimeSpan @newTimeSpanParams) -} #end function ConvertTo-TimeSpan - -<# - .SYNOPSIS - Converts a System.TimeSpan into the number of seconds, minutes, hours or days. - - .PARAMETER TimeSpan - TimeSpan to convert into an integer - - .PARAMETER TimeSpanType - Convert timespan into the total number of seconds, minutes, hours or days. -#> -function ConvertFrom-TimeSpan -{ - [CmdletBinding()] - [OutputType([System.Int32])] - param - ( - [Parameter(Mandatory = $true)] - [System.TimeSpan] - $TimeSpan, - - [Parameter(Mandatory = $true)] - [ValidateSet('Seconds','Minutes','Hours','Days')] - [System.String] - $TimeSpanType - ) - - switch ($TimeSpanType) - { - 'Seconds' { return $TimeSpan.TotalSeconds -as [System.UInt32] } - 'Minutes' { return $TimeSpan.TotalMinutes -as [System.UInt32] } - 'Hours' { return $TimeSpan.TotalHours -as [System.UInt32] } - 'Days' { return $TimeSpan.TotalDays -as [System.UInt32] } - } -} #end function ConvertFrom-TimeSpan - -<# - .SYNOPSIS - Helper function for retrieving a virtual machine, ensuring only one VM is resolved - - .PARAMETER VMName - Name of the Hyper-V virtual machine to return -#> -function Get-VMHyperV -{ - [CmdletBinding()] - param - ( - [Parameter(Mandatory = $true)] - [System.String] - $VMName - ) - - $vm = Get-VM -Name $VMName -ErrorAction SilentlyContinue - - # Check if 1 or 0 VM with name = $name exist - if ($vm.count -gt 1) - { - $errorMessage = $script:localizedData.MoreThanOneVMExistsError -f $VMName - New-InvalidResultException -Message $errorMessage - } - - return $vm -} #end function Get-VMHyperV diff --git a/source/Modules/HyperVDsc.Common/Private/Wait-VMIPAddress.ps1 b/source/Modules/HyperVDsc.Common/Private/Wait-VMIPAddress.ps1 new file mode 100644 index 0000000..c077b89 --- /dev/null +++ b/source/Modules/HyperVDsc.Common/Private/Wait-VMIPAddress.ps1 @@ -0,0 +1,40 @@ +<# + .SYNOPSIS + Waits for a virtual machine to be assigned an IP address. + + .PARAMETER Name + Name of the virtual machine to apply the changes to. + + .PARAMETER Timeout + Number of seconds to wait before timing out. Defaults to 300 (5 minutes). +#> +function Wait-VMIPAddress +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [Alias('VMName')] + [System.String] + $Name, + + [Parameter()] + [System.Int32] + $Timeout = 300 + ) + + [System.Int32] $elapsedSeconds = 0 + while ((Get-VMNetworkAdapter -VMName $Name).IpAddresses.Count -lt 2) + { + Write-Verbose -Message ($script:localizedData.WaitingForVMIPAddress -f $Name) + Start-Sleep -Seconds 3 + + $elapsedSeconds += 3 + if ($elapsedSeconds -gt $Timeout) + { + $errorMessage = $script:localizedData.WaitForVMIPAddressTimeoutError -f $Name, $Timeout + + New-ObjectNotFoundException -Message $errorMessage + } + } +} #end function diff --git a/source/Modules/HyperVDsc.Common/Public/ConvertFrom-TimeSpan.ps1 b/source/Modules/HyperVDsc.Common/Public/ConvertFrom-TimeSpan.ps1 new file mode 100644 index 0000000..9c095b0 --- /dev/null +++ b/source/Modules/HyperVDsc.Common/Public/ConvertFrom-TimeSpan.ps1 @@ -0,0 +1,34 @@ +<# + .SYNOPSIS + Converts a System.TimeSpan into the number of seconds, minutes, hours or days. + + .PARAMETER TimeSpan + TimeSpan to convert into an integer + + .PARAMETER TimeSpanType + Convert timespan into the total number of seconds, minutes, hours or days. +#> +function ConvertFrom-TimeSpan +{ + [CmdletBinding()] + [OutputType([System.Int32])] + param + ( + [Parameter(Mandatory = $true)] + [System.TimeSpan] + $TimeSpan, + + [Parameter(Mandatory = $true)] + [ValidateSet('Seconds','Minutes','Hours','Days')] + [System.String] + $TimeSpanType + ) + + switch ($TimeSpanType) + { + 'Seconds' { return $TimeSpan.TotalSeconds -as [System.UInt32] } + 'Minutes' { return $TimeSpan.TotalMinutes -as [System.UInt32] } + 'Hours' { return $TimeSpan.TotalHours -as [System.UInt32] } + 'Days' { return $TimeSpan.TotalDays -as [System.UInt32] } + } +} #end function ConvertFrom-TimeSpan diff --git a/source/Modules/HyperVDsc.Common/Public/ConvertTo-TimeSpan.ps1 b/source/Modules/HyperVDsc.Common/Public/ConvertTo-TimeSpan.ps1 new file mode 100644 index 0000000..6da0a3d --- /dev/null +++ b/source/Modules/HyperVDsc.Common/Public/ConvertTo-TimeSpan.ps1 @@ -0,0 +1,36 @@ +<# + .SYNOPSIS + Converts a number of seconds, minutes, hours or days into a System.TimeSpan object. + + .PARAMETER TimeInterval + The total number of seconds, minutes, hours or days to convert. + + .PARAMETER TimeSpanType + Convert using specified interval type. +#> +function ConvertTo-TimeSpan +{ + [CmdletBinding()] + [OutputType([System.TimeSpan])] + param + ( + [Parameter(Mandatory = $true)] + [System.UInt32] + $TimeInterval, + + [Parameter(Mandatory = $true)] + [ValidateSet('Seconds','Minutes','Hours','Days')] + [System.String] + $TimeIntervalType + ) + + $newTimeSpanParams = @{ } + switch ($TimeIntervalType) + { + 'Seconds' { $newTimeSpanParams['Seconds'] = $TimeInterval } + 'Minutes' { $newTimeSpanParams['Minutes'] = $TimeInterval } + 'Hours' { $newTimeSpanParams['Hours'] = $TimeInterval } + 'Days' { $newTimeSpanParams['Days'] = $TimeInterval } + } + return (New-TimeSpan @newTimeSpanParams) +} #end function ConvertTo-TimeSpan diff --git a/source/Modules/HyperVDsc.Common/Public/Get-VMHyperV.ps1 b/source/Modules/HyperVDsc.Common/Public/Get-VMHyperV.ps1 new file mode 100644 index 0000000..011beb2 --- /dev/null +++ b/source/Modules/HyperVDsc.Common/Public/Get-VMHyperV.ps1 @@ -0,0 +1,28 @@ +<# + .SYNOPSIS + Helper function for retrieving a virtual machine, ensuring only one VM is resolved + + .PARAMETER VMName + Name of the Hyper-V virtual machine to return +#> +function Get-VMHyperV +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $VMName + ) + + $vm = Get-VM -Name $VMName -ErrorAction SilentlyContinue + + # Check if 1 or 0 VM with name = $name exist + if ($vm.count -gt 1) + { + $errorMessage = $script:localizedData.MoreThanOneVMExistsError -f $VMName + New-InvalidResultException -Message $errorMessage + } + + return $vm +} #end function Get-VMHyperV diff --git a/source/Modules/HyperVDsc.Common/Public/Set-VMProperty.ps1 b/source/Modules/HyperVDsc.Common/Public/Set-VMProperty.ps1 new file mode 100644 index 0000000..75d1ff8 --- /dev/null +++ b/source/Modules/HyperVDsc.Common/Public/Set-VMProperty.ps1 @@ -0,0 +1,116 @@ +<# + .SYNOPSIS + Sets one or more virtual machine properties, powering the VM + off if required. + + .PARAMETER Name + Name of the virtual machine to apply the changes to. + + .PARAMETER VMName + Name of the virtual machine to apply the changes to. + + .PARAMETER VMCommand + The Hyper-V cmdlet name to call to enact the changes. + + .PARAMETER ChangeProperty + The collection of cmdlet parameter names and values to pass to the command. + + .PARAMETER WaitForIP + Waits for the virtual machine to report an IP address when transitioning + into a running state. + + .PARAMETER RestartIfNeeded + Power cycle the virtual machine if changes are required. +#> +function Set-VMProperty +{ + [CmdletBinding(DefaultParameterSetName = 'Name')] + param + ( + [Parameter(Mandatory = $true, ParameterSetName = 'Name')] + [System.String] + $Name, + + [Parameter(Mandatory = $true, ParameterSetName = 'VMName')] + [System.String] + $VMName, + + [Parameter(Mandatory = $true)] + [System.String] + $VMCommand, + + [Parameter(Mandatory = $true)] + [System.Collections.Hashtable] + $ChangeProperty, + + [Parameter()] + [System.Boolean] + $WaitForIP, + + [Parameter()] + [System.Boolean] + $RestartIfNeeded + ) + + if ($PSBoundParameters.ContainsKey('VMName')) + { + # Add the -Name property to the ChangeProperty hashtable for splatting + $ChangeProperty['VMName'] = $VMName + + # Set the common parameters for splatting against Get-VM and Set-VMState + $vmCommonProperty = @{ + Name = $VMName + } + + # Ensure that the name parameter is set for verbose messages + $Name = $VMName + } + else + { + # Add the -Name property to the ChangeProperty hashtable for splatting + $ChangeProperty['Name'] = $Name + + # Set the common parameters for splatting against Get-VM and Set-VMState + $vmCommonProperty = @{ + Name = $Name + } + } + + $vmObject = Get-VM @vmCommonProperty + $vmOriginalState = $vmObject.State + + if ($vmOriginalState -ne 'Off' -and $RestartIfNeeded) + { + # Turn the vm off to make changes + Set-VMState @vmCommonProperty -State Off + + Write-Verbose -Message ($script:localizedData.UpdatingVMProperties -f $Name) + # Make changes using the passed hashtable + & $VMCommand @ChangeProperty + + # Cannot move an off VM to a paused state - only to running state + if ($vmOriginalState -eq 'Running') + { + Set-VMState @vmCommonProperty -State Running -WaitForIP $WaitForIP + } + + Write-Verbose -Message ($script:localizedData.VMPropertiesUpdated -f $Name) + + # Cannot restore a vm to a paused state + if ($vmOriginalState -eq 'Paused') + { + Write-Warning -Message ($script:localizedData.VMStateWillBeOffWarning -f $Name) + } + } + elseif ($vmOriginalState -eq 'Off') + { + Write-Verbose -Message ($script:localizedData.UpdatingVMProperties -f $Name) + & $VMCommand @ChangeProperty + Write-Verbose -Message ($script:localizedData.VMPropertiesUpdated -f $Name) + } + else + { + $errorMessage = $script:localizedData.CannotUpdatePropertiesOnlineError -f $Name, $vmOriginalState + New-InvalidOperationException -Message $errorMessage + } +} #end function diff --git a/source/Modules/HyperVDsc.Common/Public/Set-VMState.ps1 b/source/Modules/HyperVDsc.Common/Public/Set-VMState.ps1 new file mode 100644 index 0000000..d1d62eb --- /dev/null +++ b/source/Modules/HyperVDsc.Common/Public/Set-VMState.ps1 @@ -0,0 +1,76 @@ +<# + .SYNOPSIS + Sets one or more virtual machine properties, powering the VM + off if required. + + .PARAMETER Name + Name of the virtual machine to apply the changes to. + + .PARAMETER State + The target power state of the virtual machine. + + .PARAMETER ChangeProperty + The collection of cmdlet parameter names and values to pass to the command. + + .PARAMETER WaitForIP + Waits for the virtual machine to be report an IP address when transitioning + into a running state. +#> +function Set-VMState +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [Alias('VMName')] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateSet('Running','Paused','Off')] + [System.String] + $State, + + [Parameter()] + [System.Boolean] + $WaitForIP + ) + + switch ($State) + { + 'Running' { + $vmCurrentState = (Get-VM -Name $Name).State + if ($vmCurrentState -eq 'Paused') + { + # If VM is in paused state, use resume-vm to make it running + Write-Verbose -Message ($script:localizedData.ResumingVM -f $Name) + Resume-VM -Name $Name + } + elseif ($vmCurrentState -eq 'Off') + { + # If VM is Off, use start-vm to make it running + Write-Verbose -Message ($script:localizedData.StartingVM -f $Name) + Start-VM -Name $Name + } + + if ($WaitForIP) + { + Wait-VMIPAddress -Name $Name -Verbose + } + } + 'Paused' { + if ($vmCurrentState -ne 'Off') + { + Write-Verbose -Message ($script:localizedData.SuspendingVM -f $Name) + Suspend-VM -Name $Name + } + } + 'Off' { + if ($vmCurrentState -ne 'Off') + { + Write-Verbose -Message ($script:localizedData.StoppingVM -f $Name) + Stop-VM -Name $Name -Force -WarningAction SilentlyContinue + } + } + } +} #end function diff --git a/source/Modules/HyperVDsc.Common/prefix.ps1 b/source/Modules/HyperVDsc.Common/prefix.ps1 new file mode 100644 index 0000000..0d47385 --- /dev/null +++ b/source/Modules/HyperVDsc.Common/prefix.ps1 @@ -0,0 +1,5 @@ +$script:dscResourceCommonModulePath = Join-Path -Path $PSScriptRoot -ChildPath '../DscResource.Common' + +Import-Module -Name $script:dscResourceCommonModulePath + +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' diff --git a/tests/Integration/DSC_VMHost_set.config.ps1 b/tests/Integration/DSC_VMHost_set.config.ps1 index e4d3fae..8a81404 100644 --- a/tests/Integration/DSC_VMHost_set.config.ps1 +++ b/tests/Integration/DSC_VMHost_set.config.ps1 @@ -15,7 +15,7 @@ configuration DSC_VMHost_Set_Config $EnableEnhancedSessionMode ) - Import-DscResource -ModuleName 'xHyperV' + Import-DscResource -ModuleName 'HyperVDsc' node localhost { VMHost Integration_Test { diff --git a/tests/Integration/DSC_VMProcessor_set.config.ps1 b/tests/Integration/DSC_VMProcessor_set.config.ps1 index 12c269b..6832121 100644 --- a/tests/Integration/DSC_VMProcessor_set.config.ps1 +++ b/tests/Integration/DSC_VMProcessor_set.config.ps1 @@ -3,7 +3,7 @@ configuration DSC_VMProcessor_Set_Config { Import-DscResource -ModuleName 'HyperVDsc' node localhost { - xVMProcessor Integration_Test { + VMProcessor Integration_Test { VMName = $Node.VMName CompatibilityForMigrationEnabled = $Node.CompatibilityForMigrationEnabled CompatibilityForOlderOperatingSystemsEnabled = $Node.CompatibilityForOlderOperatingSystemsEnabled diff --git a/tests/Unit/HyperVDsc.Common.Tests.ps1 b/tests/Unit/HyperVDsc.Common.Tests.ps1 deleted file mode 100644 index 69a8d01..0000000 --- a/tests/Unit/HyperVDsc.Common.Tests.ps1 +++ /dev/null @@ -1,315 +0,0 @@ -#region HEADER -$script:projectPath = "$PSScriptRoot\..\.." | Convert-Path -$script:projectName = (Get-ChildItem -Path "$script:projectPath\*\*.psd1" | Where-Object -FilterScript { - ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and - $(try - { - Test-ModuleManifest -Path $_.FullName -ErrorAction Stop - } - catch - { - $false - }) - }).BaseName - -$script:parentModule = Get-Module -Name $script:projectName -ListAvailable | Select-Object -First 1 -$script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' -Remove-Module -Name $script:parentModule -Force -ErrorAction 'SilentlyContinue' - -$script:subModuleName = (Split-Path -Path $PSCommandPath -Leaf) -replace '\.Tests.ps1' -$script:subModuleFile = Join-Path -Path $script:subModulesFolder -ChildPath "$($script:subModuleName)" - -Import-Module $script:subModuleFile -Force -ErrorAction 'Stop' -#endregion HEADER - -# Import the stub functions. -#Get-Module -Name 'Hyper-V' -All | Remove-Module -Force -Import-Module -Name "$PSScriptRoot/Stubs/Hyper-V.stubs.psm1" -Force - -InModuleScope $script:subModuleName { - Describe 'HyperVDsc.Common\Set-VMProperty' { - It "Should throw if VM is running and 'RestartIfNeeded' is False" { - $mockVMName = 'Test' - - Mock -CommandName Get-VM -MockWith { - return @{ - State = 'Running' - } - } - - $setVMPropertyParams = @{ - VMName = $mockVMName - VMCommand = 'Set-VMProcessor' - ChangeProperty = @{ - ResourcePoolName = 'Dummy' - } - } - - { Set-VMProperty @setVMPropertyParams } | Should -Throw ( - $script:localizedData.CannotUpdatePropertiesOnlineError -f $mockVMName, 'Running' - ) - } - - It "Should stop and restart VM when running and 'RestartIfNeeded' is True" { - Mock -CommandName Stop-VM - Mock -CommandName Set-VMProcessor - Mock -CommandName Set-VMState - Mock -CommandName Get-VM -MockWith { - return @{ - State = 'Running' - } - } - - $setVMPropertyParams = @{ - VMName = 'Test' - VMCommand = 'Set-VMProcessor' - ChangeProperty = @{ - ResourcePoolName = 'Dummy' - } - RestartIfNeeded = $true - } - Set-VMProperty @setVMPropertyParams - - Assert-MockCalled -CommandName Set-VMState -ParameterFilter { - $State -eq 'Off' - } -Scope It - - Assert-MockCalled -CommandName Set-VMState -ParameterFilter { - $State -eq 'Running' - } -Scope It - } - - } - - Describe 'HyperVDsc.Common\Set-VMState' { - It 'Should resume VM when current "State" is "Paused" and target state is "Running"' { - Mock -CommandName Resume-VM - Mock -CommandName Wait-VMIPAddress - Mock -CommandName Get-VM -MockWith { - return @{ - State = 'Paused' - } - } - - Set-VMState -Name 'TestVM' -State 'Running' - - Assert-MockCalled -CommandName Resume-VM -Scope It - Assert-MockCalled -CommandName Wait-VMIPAddress -Scope It -Exactly 0 - } - - It 'Should resume VM and wait when current "State" is "Paused" and target state is "Running"' { - Mock -CommandName Resume-VM - Mock -CommandName Wait-VMIPAddress - Mock -CommandName Get-VM -MockWith { - return @{ - State = 'Paused' - } - } - - Set-VMState -Name 'TestVM' -State 'Running' -WaitForIP $true - - Assert-MockCalled -CommandName Resume-VM -Scope It - Assert-MockCalled -CommandName Wait-VMIPAddress -Scope It - } - - It 'Should start VM when current "State" is "Off" and target state is "Running"' { - Mock -CommandName Start-VM - Mock -CommandName Wait-VMIPAddress - Mock -CommandName Get-VM -MockWith { - return @{ - State = 'Off' - } - } - - Set-VMState -Name 'TestVM' -State 'Running' - - Assert-MockCalled -CommandName Start-VM -Scope It - Assert-MockCalled -CommandName Wait-VMIPAddress -Scope It -Exactly 0 - } - - It 'Should start VM and wait when current "State" is "Off" and target state is "Running"' { - Mock -CommandName Start-VM - Mock -CommandName Wait-VMIPAddress - Mock -CommandName Get-VM -MockWith { - return @{ - State = 'Off' - } - } - - Set-VMState -Name 'TestVM' -State 'Running' -WaitForIP $true - - Assert-MockCalled -CommandName Start-VM -Scope It - Assert-MockCalled -CommandName Wait-VMIPAddress -Scope It - } - - It 'Should suspend VM when current "State" is "Running" and target state is "Paused"' { - Mock -CommandName Suspend-VM - Mock -CommandName Get-VM -MockWith { - return @{ - State = 'Running' - } - } - - Set-VMState -Name 'TestVM' -State 'Paused' - - Assert-MockCalled -CommandName Suspend-VM -Scope It - } - - It 'Should stop VM when current "State" is "Running" and target state is "Off"' { - Mock -CommandName Stop-VM - Mock -CommandName Get-VM -MockWith { - return @{ - State = 'Running' - } - } - - Set-VMState -Name 'TestVM' -State 'Off' - - Assert-MockCalled -CommandName Stop-VM -Scope It - } - - It 'Should stop VM when current "State" is "Paused" and target state is "Off"' { - Mock -CommandName Stop-VM - Mock -CommandName Get-VM -MockWith { - return @{ - State = 'Paused' - } - } - - Set-VMState -Name 'TestVM' -State 'Off' - - Assert-MockCalled -CommandName Stop-VM -Scope It - } - } # describe HyperVDsc.Common\Set-VMState -} - -Describe 'HyperVDsc.Common\Wait-VMIPAddress' { - Context 'When VM network adapter reports 2 IP addresses'{ - BeforeAll { - Mock -CommandName Get-VMNetworkAdapter -ModuleName 'HyperVDsc.Common' -MockWith { - return @{ - IpAddresses = @('192.168.0.1', '172.16.0.1') - } - } - } - - It 'Should return without throwing an exception' { - $result = Wait-VMIPAddress -Name 'Test' -Verbose - - $result | Should -BeNullOrEmpty - } - } - - Context 'When VM network adapter reports 2 IP addresses'{ - BeforeAll { - Mock -CommandName Start-Sleep -ModuleName 'HyperVDsc.Common' - Mock -CommandName Get-VMNetworkAdapter -ModuleName 'HyperVDsc.Common' -MockWith { - return $null - } - } - - It 'Should throw the correct exception' { - { Wait-VMIPAddress -Name 'Test' -Timeout 2 -Verbose } | Should -Throw ( - $script:localizedData.WaitForVMIPAddressTimeoutError -f 'Test', '2' - ) - } - } -} # describe HyperVDsc.Common\WaitVMIPAddress - -Describe 'HyperVDsc.Common\ConvertTo-TimeSpan' { - It 'Should convert 60 seconds to "System.TimeSpan" of 1 minute' { - $testSeconds = 60 - - $result = ConvertTo-TimeSpan -TimeInterval $testSeconds -TimeIntervalType Seconds - - $result.TotalMinutes | Should -Be 1 - } - - It 'Should convert 60 minutes to "System.TimeSpan" of 60 minutes' { - $testMinutes = 60 - - $result = ConvertTo-TimeSpan -TimeInterval $testMinutes -TimeIntervalType Minutes - - $result.TotalHours | Should -Be 1 - } - - It 'Should convert 48 hours to "System.TimeSpan" of 2 days' { - $testHours = 48 - - $result = ConvertTo-TimeSpan -TimeInterval $testHours -TimeIntervalType Hours - - $result.TotalDays | Should -Be 2 - } - -} # describe HyperVDsc.Common\ConvertTo-TimeSpan - -Describe 'HyperVDsc.Common\ConvertFrom-TimeSpan' { - It 'Should convert a "System.TimeSpan" of 1 minute to 60 seconds' { - $testTimeSpan = New-TimeSpan -Minutes 1 - - $result = ConvertFrom-TimeSpan -TimeSpan $testTimeSpan -TimeSpanType Seconds - - $result | Should -Be 60 - } - - It 'Should convert a "System.TimeSpan" of 1 hour to 60 minutes' { - $testTimeSpan = New-TimeSpan -Hours 1 - - $result = ConvertFrom-TimeSpan -TimeSpan $testTimeSpan -TimeSpanType Minutes - - $result | Should -Be 60 - } - - It 'Should convert a "System.TimeSpan" of 2 dayes to 48 hours' { - $testTimeSpan = New-TimeSpan -Days 2 - - $result = ConvertFrom-TimeSpan -TimeSpan $testTimeSpan -TimeSpanType Hours - - $result | Should -Be 48 - } - -} # describe HyperVDsc.Common\ConvertFrom-TimeSpan - -Describe 'HyperVDsc.Common\Get-VMHyperV' { - BeforeAll { - $mockVMName = 'TestVM' - } - - # Guard mocks - It 'Should not throw when no VM is found' { - Mock -CommandName Get-VM -ModuleName 'HyperVDsc.Common' - - $result = Get-VMHyperV -VMName $mockVMName - - $result | Should -BeNullOrEmpty - } - - It 'Should not throw when one VM is found' { - Mock -CommandName Get-VM -ModuleName 'HyperVDsc.Common' -MockWith { - [PSCustomObject] @{ - Name = $VMName - } - } - - $result = Get-VMHyperV -VMName $mockVMName - - $result.Name | Should -Be $mockVMName - } - - It 'Should throw when more than one VM is found' { - Mock -CommandName Get-VM -ModuleName 'HyperVDsc.Common' -MockWith { - @( - [PSCustomObject] @{ - Name = $VMName - }, - [PSCustomObject] @{ - Name = $VMName - } - ) - } - - { Get-VMHyperV -VMName $mockVMName } | Should -Throw ( - $script:localizedData.MoreThanOneVMExistsError -f $mockVMName - ) - } -} # describe HyperVDsc.Common\Get-VMHyperV diff --git a/tests/Unit/HyperVDsc.Common/Private/Wait-VMIPAddress.Tests.ps1 b/tests/Unit/HyperVDsc.Common/Private/Wait-VMIPAddress.Tests.ps1 new file mode 100644 index 0000000..b29ae07 --- /dev/null +++ b/tests/Unit/HyperVDsc.Common/Private/Wait-VMIPAddress.Tests.ps1 @@ -0,0 +1,62 @@ +#region HEADER +$script:projectPath = "$PSScriptRoot\..\..\..\.." | Convert-Path +$script:projectName = (Get-ChildItem -Path "$script:projectPath\*\*.psd1" | Where-Object -FilterScript { + ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and + $(try + { + Test-ModuleManifest -Path $_.FullName -ErrorAction Stop + } + catch + { + $false + }) + }).BaseName + +$script:parentModule = Get-Module -Name $script:projectName -ListAvailable | Select-Object -First 1 +$script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' +Remove-Module -Name $script:parentModule -Force -ErrorAction 'SilentlyContinue' + +$script:subModuleName = ('{0}.Common' -f $script:projectName) +$script:subModuleFile = Join-Path -Path $script:subModulesFolder -ChildPath "$($script:subModuleName)" + +Import-Module $script:subModuleFile -Force -ErrorAction 'Stop' +#endregion HEADER + +# Import the stub functions. +#Get-Module -Name 'Hyper-V' -All | Remove-Module -Force +Import-Module -Name "$PSScriptRoot/../../Stubs/Hyper-V.stubs.psm1" -Force -DisableNameChecking + +InModuleScope $script:subModuleName { + Describe 'Private\Wait-VMIPAddress' { + Context 'When VM network adapter reports 2 IP addresses' { + BeforeAll { + Mock -CommandName Get-VMNetworkAdapter -MockWith { + return @{ + IpAddresses = @('192.168.0.1', '172.16.0.1') + } + } + } + + It 'Should return without throwing an exception' { + $result = Wait-VMIPAddress -Name 'Test' -Verbose + + $result | Should -BeNullOrEmpty + } + } + + Context 'When VM network adapter reports 2 IP addresses' { + BeforeAll { + Mock -CommandName Start-Sleep + Mock -CommandName Get-VMNetworkAdapter -MockWith { + return $null + } + } + + It 'Should throw the correct exception' { + { Wait-VMIPAddress -Name 'Test' -Timeout 2 -Verbose } | Should -Throw ( + $script:localizedData.WaitForVMIPAddressTimeoutError -f 'Test', '2' + ) + } + } + } +} diff --git a/tests/Unit/HyperVDsc.Common/Public/ConvertFrom-TimeSpan.Tests.ps1 b/tests/Unit/HyperVDsc.Common/Public/ConvertFrom-TimeSpan.Tests.ps1 new file mode 100644 index 0000000..2254053 --- /dev/null +++ b/tests/Unit/HyperVDsc.Common/Public/ConvertFrom-TimeSpan.Tests.ps1 @@ -0,0 +1,56 @@ +#region HEADER +$script:projectPath = "$PSScriptRoot\..\..\..\.." | Convert-Path +$script:projectName = (Get-ChildItem -Path "$script:projectPath\*\*.psd1" | Where-Object -FilterScript { + ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and + $(try + { + Test-ModuleManifest -Path $_.FullName -ErrorAction Stop + } + catch + { + $false + }) + }).BaseName + +$script:parentModule = Get-Module -Name $script:projectName -ListAvailable | Select-Object -First 1 +$script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' +Remove-Module -Name $script:parentModule -Force -ErrorAction 'SilentlyContinue' + +$script:subModuleName = ('{0}.Common' -f $script:projectName) +$script:subModuleFile = Join-Path -Path $script:subModulesFolder -ChildPath "$($script:subModuleName)" + +Import-Module $script:subModuleFile -Force -ErrorAction 'Stop' +#endregion HEADER + +# Import the stub functions. +#Get-Module -Name 'Hyper-V' -All | Remove-Module -Force +Import-Module -Name "$PSScriptRoot/../../Stubs/Hyper-V.stubs.psm1" -Force -DisableNameChecking + +InModuleScope $script:subModuleName { + Describe 'Public\ConvertFrom-TimeSpan' { + It 'Should convert a "System.TimeSpan" of 1 minute to 60 seconds' { + $testTimeSpan = New-TimeSpan -Minutes 1 + + $result = ConvertFrom-TimeSpan -TimeSpan $testTimeSpan -TimeSpanType Seconds + + $result | Should -Be 60 + } + + It 'Should convert a "System.TimeSpan" of 1 hour to 60 minutes' { + $testTimeSpan = New-TimeSpan -Hours 1 + + $result = ConvertFrom-TimeSpan -TimeSpan $testTimeSpan -TimeSpanType Minutes + + $result | Should -Be 60 + } + + It 'Should convert a "System.TimeSpan" of 2 dayes to 48 hours' { + $testTimeSpan = New-TimeSpan -Days 2 + + $result = ConvertFrom-TimeSpan -TimeSpan $testTimeSpan -TimeSpanType Hours + + $result | Should -Be 48 + } + + } +} diff --git a/tests/Unit/HyperVDsc.Common/Public/ConvertTo-TimeSpan.Tests.ps1 b/tests/Unit/HyperVDsc.Common/Public/ConvertTo-TimeSpan.Tests.ps1 new file mode 100644 index 0000000..3369a32 --- /dev/null +++ b/tests/Unit/HyperVDsc.Common/Public/ConvertTo-TimeSpan.Tests.ps1 @@ -0,0 +1,56 @@ +#region HEADER +$script:projectPath = "$PSScriptRoot\..\..\..\.." | Convert-Path +$script:projectName = (Get-ChildItem -Path "$script:projectPath\*\*.psd1" | Where-Object -FilterScript { + ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and + $(try + { + Test-ModuleManifest -Path $_.FullName -ErrorAction Stop + } + catch + { + $false + }) + }).BaseName + +$script:parentModule = Get-Module -Name $script:projectName -ListAvailable | Select-Object -First 1 +$script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' +Remove-Module -Name $script:parentModule -Force -ErrorAction 'SilentlyContinue' + +$script:subModuleName = ('{0}.Common' -f $script:projectName) +$script:subModuleFile = Join-Path -Path $script:subModulesFolder -ChildPath "$($script:subModuleName)" + +Import-Module $script:subModuleFile -Force -ErrorAction 'Stop' +#endregion HEADER + +# Import the stub functions. +#Get-Module -Name 'Hyper-V' -All | Remove-Module -Force +Import-Module -Name "$PSScriptRoot/../../Stubs/Hyper-V.stubs.psm1" -Force -DisableNameChecking + +InModuleScope $script:subModuleName { + Describe 'Public\ConvertTo-TimeSpan' { + It 'Should convert 60 seconds to "System.TimeSpan" of 1 minute' { + $testSeconds = 60 + + $result = ConvertTo-TimeSpan -TimeInterval $testSeconds -TimeIntervalType Seconds + + $result.TotalMinutes | Should -Be 1 + } + + It 'Should convert 60 minutes to "System.TimeSpan" of 60 minutes' { + $testMinutes = 60 + + $result = ConvertTo-TimeSpan -TimeInterval $testMinutes -TimeIntervalType Minutes + + $result.TotalHours | Should -Be 1 + } + + It 'Should convert 48 hours to "System.TimeSpan" of 2 days' { + $testHours = 48 + + $result = ConvertTo-TimeSpan -TimeInterval $testHours -TimeIntervalType Hours + + $result.TotalDays | Should -Be 2 + } + + } +} diff --git a/tests/Unit/HyperVDsc.Common/Public/Get-VMHyperV.Tests.ps1 b/tests/Unit/HyperVDsc.Common/Public/Get-VMHyperV.Tests.ps1 new file mode 100644 index 0000000..ffd1fba --- /dev/null +++ b/tests/Unit/HyperVDsc.Common/Public/Get-VMHyperV.Tests.ps1 @@ -0,0 +1,73 @@ +#region HEADER +$script:projectPath = "$PSScriptRoot\..\..\..\.." | Convert-Path +$script:projectName = (Get-ChildItem -Path "$script:projectPath\*\*.psd1" | Where-Object -FilterScript { + ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and + $(try + { + Test-ModuleManifest -Path $_.FullName -ErrorAction Stop + } + catch + { + $false + }) + }).BaseName + +$script:parentModule = Get-Module -Name $script:projectName -ListAvailable | Select-Object -First 1 +$script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' +Remove-Module -Name $script:parentModule -Force -ErrorAction 'SilentlyContinue' + +$script:subModuleName = ('{0}.Common' -f $script:projectName) +$script:subModuleFile = Join-Path -Path $script:subModulesFolder -ChildPath "$($script:subModuleName)" + +Import-Module $script:subModuleFile -Force -ErrorAction 'Stop' +#endregion HEADER + +# Import the stub functions. +#Get-Module -Name 'Hyper-V' -All | Remove-Module -Force +Import-Module -Name "$PSScriptRoot/../../Stubs/Hyper-V.stubs.psm1" -Force -DisableNameChecking + +InModuleScope $script:subModuleName { + Describe 'Public\Get-VMHyperV' { + BeforeAll { + $mockVMName = 'TestVM' + } + + # Guard mocks + It 'Should not throw when no VM is found' { + Mock -CommandName Get-VM + + $result = Get-VMHyperV -VMName $mockVMName + + $result | Should -BeNullOrEmpty + } + + It 'Should not throw when one VM is found' { + Mock -CommandName Get-VM -MockWith { + [PSCustomObject] @{ + Name = $VMName + } + } + + $result = Get-VMHyperV -VMName $mockVMName + + $result.Name | Should -Be $mockVMName + } + + It 'Should throw when more than one VM is found' { + Mock -CommandName Get-VM -MockWith { + @( + [PSCustomObject] @{ + Name = $VMName + }, + [PSCustomObject] @{ + Name = $VMName + } + ) + } + + { Get-VMHyperV -VMName $mockVMName } | Should -Throw ( + $script:localizedData.MoreThanOneVMExistsError -f $mockVMName + ) + } + } +} diff --git a/tests/Unit/HyperVDsc.Common/Public/Set-VMProperty.Tests.ps1 b/tests/Unit/HyperVDsc.Common/Public/Set-VMProperty.Tests.ps1 new file mode 100644 index 0000000..d7d0270 --- /dev/null +++ b/tests/Unit/HyperVDsc.Common/Public/Set-VMProperty.Tests.ps1 @@ -0,0 +1,82 @@ +#region HEADER +$script:projectPath = "$PSScriptRoot\..\..\..\.." | Convert-Path +$script:projectName = (Get-ChildItem -Path "$script:projectPath\*\*.psd1" | Where-Object -FilterScript { + ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and + $(try + { + Test-ModuleManifest -Path $_.FullName -ErrorAction Stop + } + catch + { + $false + }) + }).BaseName + +$script:parentModule = Get-Module -Name $script:projectName -ListAvailable | Select-Object -First 1 +$script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' +Remove-Module -Name $script:parentModule -Force -ErrorAction 'SilentlyContinue' + +$script:subModuleName = ('{0}.Common' -f $script:projectName) +$script:subModuleFile = Join-Path -Path $script:subModulesFolder -ChildPath "$($script:subModuleName)" + +Import-Module $script:subModuleFile -Force -ErrorAction 'Stop' +#endregion HEADER + +# Import the stub functions. +#Get-Module -Name 'Hyper-V' -All | Remove-Module -Force +Import-Module -Name "$PSScriptRoot/../../Stubs/Hyper-V.stubs.psm1" -Force -DisableNameChecking + +InModuleScope $script:subModuleName { + Describe 'Public\Set-VMProperty' { + It "Should throw if VM is running and 'RestartIfNeeded' is False" { + $mockVMName = 'Test' + + Mock -CommandName Get-VM -MockWith { + return @{ + State = 'Running' + } + } + + $setVMPropertyParams = @{ + VMName = $mockVMName + VMCommand = 'Set-VMProcessor' + ChangeProperty = @{ + ResourcePoolName = 'Dummy' + } + } + + { Set-VMProperty @setVMPropertyParams } | Should -Throw ( + $script:localizedData.CannotUpdatePropertiesOnlineError -f $mockVMName, 'Running' + ) + } + + It "Should stop and restart VM when running and 'RestartIfNeeded' is True" { + Mock -CommandName Stop-VM + Mock -CommandName Set-VMProcessor + Mock -CommandName Set-VMState + Mock -CommandName Get-VM -MockWith { + return @{ + State = 'Running' + } + } + + $setVMPropertyParams = @{ + VMName = 'Test' + VMCommand = 'Set-VMProcessor' + ChangeProperty = @{ + ResourcePoolName = 'Dummy' + } + RestartIfNeeded = $true + } + Set-VMProperty @setVMPropertyParams + + Assert-MockCalled -CommandName Set-VMState -ParameterFilter { + $State -eq 'Off' + } -Scope It + + Assert-MockCalled -CommandName Set-VMState -ParameterFilter { + $State -eq 'Running' + } -Scope It + } + } +} diff --git a/tests/Unit/HyperVDsc.Common/Public/Set-VMState.Tests.ps1 b/tests/Unit/HyperVDsc.Common/Public/Set-VMState.Tests.ps1 new file mode 100644 index 0000000..46a0dd2 --- /dev/null +++ b/tests/Unit/HyperVDsc.Common/Public/Set-VMState.Tests.ps1 @@ -0,0 +1,130 @@ +#region HEADER +$script:projectPath = "$PSScriptRoot\..\..\..\.." | Convert-Path +$script:projectName = (Get-ChildItem -Path "$script:projectPath\*\*.psd1" | Where-Object -FilterScript { + ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and + $(try + { + Test-ModuleManifest -Path $_.FullName -ErrorAction Stop + } + catch + { + $false + }) + }).BaseName + +$script:parentModule = Get-Module -Name $script:projectName -ListAvailable | Select-Object -First 1 +$script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' +Remove-Module -Name $script:parentModule -Force -ErrorAction 'SilentlyContinue' + +$script:subModuleName = ('{0}.Common' -f $script:projectName) +$script:subModuleFile = Join-Path -Path $script:subModulesFolder -ChildPath "$($script:subModuleName)" + +Import-Module $script:subModuleFile -Force -ErrorAction 'Stop' +#endregion HEADER + +# Import the stub functions. +#Get-Module -Name 'Hyper-V' -All | Remove-Module -Force +Import-Module -Name "$PSScriptRoot/../../Stubs/Hyper-V.stubs.psm1" -Force -DisableNameChecking + +InModuleScope $script:subModuleName { + Describe 'Public\Set-VMState' { + It 'Should resume VM when current "State" is "Paused" and target state is "Running"' { + Mock -CommandName Resume-VM + Mock -CommandName Wait-VMIPAddress + Mock -CommandName Get-VM -MockWith { + return @{ + State = 'Paused' + } + } + + Set-VMState -Name 'TestVM' -State 'Running' + + Assert-MockCalled -CommandName Resume-VM -Scope It + Assert-MockCalled -CommandName Wait-VMIPAddress -Scope It -Exactly 0 + } + + It 'Should resume VM and wait when current "State" is "Paused" and target state is "Running"' { + Mock -CommandName Resume-VM + Mock -CommandName Wait-VMIPAddress + Mock -CommandName Get-VM -MockWith { + return @{ + State = 'Paused' + } + } + + Set-VMState -Name 'TestVM' -State 'Running' -WaitForIP $true + + Assert-MockCalled -CommandName Resume-VM -Scope It + Assert-MockCalled -CommandName Wait-VMIPAddress -Scope It + } + + It 'Should start VM when current "State" is "Off" and target state is "Running"' { + Mock -CommandName Start-VM + Mock -CommandName Wait-VMIPAddress + Mock -CommandName Get-VM -MockWith { + return @{ + State = 'Off' + } + } + + Set-VMState -Name 'TestVM' -State 'Running' + + Assert-MockCalled -CommandName Start-VM -Scope It + Assert-MockCalled -CommandName Wait-VMIPAddress -Scope It -Exactly 0 + } + + It 'Should start VM and wait when current "State" is "Off" and target state is "Running"' { + Mock -CommandName Start-VM + Mock -CommandName Wait-VMIPAddress + Mock -CommandName Get-VM -MockWith { + return @{ + State = 'Off' + } + } + + Set-VMState -Name 'TestVM' -State 'Running' -WaitForIP $true + + Assert-MockCalled -CommandName Start-VM -Scope It + Assert-MockCalled -CommandName Wait-VMIPAddress -Scope It + } + + It 'Should suspend VM when current "State" is "Running" and target state is "Paused"' { + Mock -CommandName Suspend-VM + Mock -CommandName Get-VM -MockWith { + return @{ + State = 'Running' + } + } + + Set-VMState -Name 'TestVM' -State 'Paused' + + Assert-MockCalled -CommandName Suspend-VM -Scope It + } + + It 'Should stop VM when current "State" is "Running" and target state is "Off"' { + Mock -CommandName Stop-VM + Mock -CommandName Get-VM -MockWith { + return @{ + State = 'Running' + } + } + + Set-VMState -Name 'TestVM' -State 'Off' + + Assert-MockCalled -CommandName Stop-VM -Scope It + } + + It 'Should stop VM when current "State" is "Paused" and target state is "Off"' { + Mock -CommandName Stop-VM + Mock -CommandName Get-VM -MockWith { + return @{ + State = 'Paused' + } + } + + Set-VMState -Name 'TestVM' -State 'Off' + + Assert-MockCalled -CommandName Stop-VM -Scope It + } + } +}