Private/AzStackHci.ModuleManagement.Helpers.ps1
|
# //////////////////////////////////////////////////////////////////////////// # Function to check if a command exists # Timeout (seconds) for PSGallery version checks — prevents hangs when gallery is unreachable $script:FindModuleTimeoutSeconds = 15 Function Test-CommandExists { Param ($command) begin { # Write-Debug "Test-CommandExists: Beginning command existence check for '$command'" $oldPreference = $ErrorActionPreference $ErrorActionPreference = "Stop" [bool]$CommandExists = $false } process { try { if(Get-Command $command){ $CommandExists = $true } } Catch { $CommandExists = $false } Finally { $ErrorActionPreference=$oldPreference } } # End of process block end { # Write-Debug "Test-CommandExists: Command existence check completed" # Return the result return $CommandExists } } # End function Test-CommandExists # //////////////////////////////////////////////////////////////////////////// Function Get-AzStackHciEnvironmentCheckerModule { begin { # Write-Debug "Get-AzStackHciEnvironmentCheckerModule: Beginning Environment Checker module verification" # Check if the AzStackHci.EnvironmentChecker module is installed and running the latest version $Module = "AzStackHci.EnvironmentChecker" } process { # Check if the module is installed [Array]$InstalledVersions = @() $InstalledVersions = (Get-Module -Name $Module -ListAvailable -ErrorAction SilentlyContinue) | Sort-Object -Property Version # If only one version is installed, set the installed version if($InstalledVersions){ if($InstalledVersions.Count -eq 1) { [version]$InstalledVersion = $InstalledVersions.Version } elseif($InstalledVersions.Count -gt 1) { # Multiple versions installed, use the latest version [version]$InstalledVersion = $InstalledVersions[0].Version Write-Warning "There are $($InstalledVersions.Count) versions of 'AzStackHci.EnvironmentChecker' module installed" } Write-HostAzS "'AzStackHci.EnvironmentChecker' module version $($InstalledVersion.ToString()) is installed" -ForegroundColor "Green" # Do nothing, continue with the script } elseif(-not($InstalledVersions)){ # Module not found, ask user to install the module Write-HostAzS "Azure Stack HCI Environment Checker module is not installed" -ForegroundColor Red if ($script:SilentMode) { $InstallModulePrompt = "Y" } else { $InstallModulePrompt = Read-Host "Would you like to install the dependant module '$Module' now? (Y/N)" } if(([string]::IsNullOrWhiteSpace($InstallModulePrompt)) -or ($InstallModulePrompt -ne "Y")){ Throw "Error: Null or not 'Y' response. Exiting script, please install the '$Module' module and re-run the function" } elseif($InstallModulePrompt -eq "Y"){ Write-HostAzS "Installing module '$Module'...." -ForegroundColor "Green" try { # Requires -AllowClobber as some functions are included in Az.StackHCI Install-Module -Name $Module -Repository PSGallery -Force -ErrorVariable ModuleInstallError -AllowClobber -Scope AllUsers -Confirm:$false } catch { Throw "Error installing module '$Module' - $($_.Exception.Message)" } if(-not($ModuleInstallError)){ Write-HostAzS "Module '$Module' installed successfully" -ForegroundColor "Green" } else { Throw "Error installing module '$Module' - $ModuleInstallError" } } else { # All other responses Write-HostAzS "Invalid response, please enter 'Y' to install the module..." -ForegroundColor Red Throw "Exiting script, please install the '$Module' module and re-run the function" } } Import-Module -Name $Module -Force # Load URLs from environment checker (Targets.json), check if multiple module version are installed, if so, use the latest version $Location = ((Get-Module -Name $Module -ListAvailable) | Sort-Object -Property Version -Descending | Select-Object -First 1).ModuleBase if(-not($Location)){ Write-HostAzS "Azure Stack HCI Environment Checker module not found, please install the module" -ForegroundColor Red Write-HostAzS "Run: 'Install-Module -Name AzStackHci.EnvironmentChecker' and troubleshoot any installation issues" -ForegroundColor Green Exit } } # End of process block end { # Write-Debug "Get-AzStackHciEnvironmentCheckerModule: Environment Checker module verification completed" # Return the module location Return $Location } } # End Function Get-AzStackHciEnvironmentCheckerModule # //////////////////////////////////////////////////////////////////////////// # Function to get the latest version of a module from PSGallery # This function will check for the latest version of a module in the PowerShell Gallery Function Get-LatestModuleVersion { [CmdletBinding()] param ( [Parameter(Mandatory=$true, Position=0)] [ValidateNotNullOrEmpty()] [string]$ModuleName ) begin { # Write-Debug "Get-LatestModuleVersion: Beginning latest module version check for '$ModuleName'" # //////////////////////////////////////////// # ///// Check for latest module version ////// # //////////////////////////////////////////// [bool]$ModuleNotInstalled = $false } process { # Get Existing Most Recent Module Version: [version]$ExistingModuleVersion = (Get-InstalledModule -Name $ModuleName -ErrorAction SilentlyContinue).Version if(-not $ExistingModuleVersion) { Write-Debug "Module '$ModuleName' is not installed via PSGallery. Skipping update check." $ModuleNotInstalled = $true return } # //////////////////////////////////////////////////////////////////////////// # Version discovery strategy: # # Step 1 (Fast path): Use Find-Module without -RequiredVersion to check for # the latest *listed* version in PSGallery. This is a single network call # and works for any module that is publicly listed. If it returns a version # higher than the installed version, use it immediately. # # Step 2 (Fallback for unlisted modules): If Step 1 does not find a newer # version, fall back to probing exact version numbers with -RequiredVersion. # This is required because Find-Module without -RequiredVersion does NOT # return unlisted packages. The only way to discover unlisted versions is # by probing exact version numbers. # Originally written for a module where all versions were "Unlisted" in # the PowerShell Gallery, this fallback has been retained for portability. # # Fallback version increment strategy: # 1. First, increment the Build (z) digit: x.y.z -> x.y.(z+1) # 2. If incrementing build finds no match, try rolling over to the next # Minor version: x.y.z -> x.(y+1).0 # 3. If that also finds no match, try rolling over to the next Major # version: x.y.z -> (x+1).0.0 # 4. Only stop if all three probes miss, meaning no further version exists. # //////////////////////////////////////////////////////////////////////////// # --- Step 1: Fast path — single call for latest listed version --- # Note: Find-Module can return multiple results when multiple repositories # are registered (e.g. PSGallery + a local repo). Sort descending and pick # the highest version across all repositories. # Uses Start-Job/Wait-Job with timeout to prevent hangs if PSGallery is unreachable. Write-Debug "Checking PowerShell Gallery for latest listed version of '$ModuleName' (timeout: $($script:FindModuleTimeoutSeconds)s)..." [version]$ListedVersion = $null [bool]$galleryReachable = $false try { $job = Start-Job -ScriptBlock { param($Name) (Find-Module -Name $Name -ErrorAction SilentlyContinue | Sort-Object -Property Version -Descending | Select-Object -First 1).Version } -ArgumentList $ModuleName $completed = Wait-Job -Job $job -Timeout $script:FindModuleTimeoutSeconds -ErrorAction SilentlyContinue if ($completed -and $completed.State -eq 'Completed') { $ListedVersion = Receive-Job -Job $job -ErrorAction SilentlyContinue $galleryReachable = $true } else { Write-Debug "Version check timed out after $($script:FindModuleTimeoutSeconds) seconds" } } catch { Write-Debug "Version check failed: $($_.Exception.Message)" } finally { if ($job) { Remove-Job -Job $job -Force -ErrorAction SilentlyContinue } } if($ListedVersion -and $ListedVersion -gt $ExistingModuleVersion) { # Latest listed version is newer than installed — use it directly Write-Debug "Latest listed version v$($ListedVersion.ToString()) is newer than installed v$($ExistingModuleVersion.ToString())" [version]$LatestModuleVersion = $ListedVersion } elseif (-not $galleryReachable) { # PSGallery was unreachable (timeout or error) — skip probing, continue with installed version Write-Debug "PSGallery unreachable; skipping unlisted version probing. Continuing with installed v$($ExistingModuleVersion.ToString())" } else { # --- Step 2: Fallback — incremental probing for unlisted versions --- if($ListedVersion) { Write-Debug "Listed version v$($ListedVersion.ToString()) is not newer than installed v$($ExistingModuleVersion.ToString()). Checking for unlisted versions..." } else { Write-Debug "No listed version found in PSGallery. Module may be unlisted. Probing exact versions..." } [bool]$StopModuleVersion = $false [version]$ModuleVersion = $ExistingModuleVersion # Loop and check if a new unlisted version exists: Do { [int]$Major = $ModuleVersion.Major [int]$Minor = $ModuleVersion.Minor [int]$Build = $ModuleVersion.Build # --- Attempt 1: Increment Build (z) by 1 --- [version]$NewModuleVersion = "$Major.$Minor.$($Build + 1)" Write-Debug "Checking if module v$($NewModuleVersion) exists in PowerShell Gallery..." [version]$FoundVersion = (Find-Module -Name "$ModuleName" -RequiredVersion $NewModuleVersion -ErrorAction SilentlyContinue).Version if(-not $FoundVersion) { # --- Attempt 2: Roll over to next Minor version (x.(y+1).0) --- [version]$NewModuleVersion = "$Major.$($Minor + 1).0" Write-Debug "Build increment miss. Checking minor rollover v$($NewModuleVersion)..." [version]$FoundVersion = (Find-Module -Name "$ModuleName" -RequiredVersion $NewModuleVersion -ErrorAction SilentlyContinue).Version } if(-not $FoundVersion) { # --- Attempt 3: Roll over to next Major version ((x+1).0.0) --- [version]$NewModuleVersion = "$($Major + 1).0.0" Write-Debug "Minor rollover miss. Checking major rollover v$($NewModuleVersion)..." [version]$FoundVersion = (Find-Module -Name "$ModuleName" -RequiredVersion $NewModuleVersion -ErrorAction SilentlyContinue).Version } if($FoundVersion) { Write-Debug "Higher module version found v$($FoundVersion.ToString())" [version]$LatestModuleVersion = $FoundVersion # Continue searching from the found version [version]$ModuleVersion = $FoundVersion } else { # All three probes missed — no further version exists in PSGallery Write-Debug "No match for v$($Major).$($Minor).$($Build + 1), v$($Major).$($Minor + 1).0, or v$($Major + 1).0.0" $StopModuleVersion = $true } } While (-not($StopModuleVersion)) } } # End of process block end { # Write-Debug "Get-LatestModuleVersion: Latest module version check completed" # If module was not installed via PSGallery, return null (no version to report) if($ModuleNotInstalled) { return $null } # Return Function output if($LatestModuleVersion) { Return [version]$LatestModuleVersion } else { Return [version]$ExistingModuleVersion } } } # End Function Get-LatestModuleVersion # //////////////////////////////////////////////////////////////////////////// # Auto Update Module Function Function Update-ModuleVersion { [CmdletBinding()] param ( [Parameter(Mandatory=$true, Position=0)] [ValidateNotNullOrEmpty()] [string]$ModuleName ) begin { # Write-Debug "Update-ModuleVersion: Beginning module update process for '$ModuleName'" [bool]$ModuleUpdated = $false } process { # Check for Updates to the Module using PSGallery [version]$InstalledModuleVersion = (Get-Module -Name "$ModuleName" -ListAvailable | Sort-Object -Property Version -Descending | Select-Object -First 1).Version # Check if the module is installed if($InstalledModuleVersion) { Write-HostAzS -ForegroundColor Green "`n`tChecking for updates..." # Call function to determine the latest version of module in PSGallery [version]$LatestModule = Get-LatestModuleVersion -ModuleName $ModuleName # Check if the installed version is less than the latest version if($InstalledModuleVersion -lt $LatestModule) { # Update required Write-HostAzS -ForegroundColor Yellow "`n`tINFO: " -NoNewLine if($InstalledModuleVersion){ Write-HostAzS "`t$ModuleName module needs updating from v$($InstalledModuleVersion.ToString()) to v$($LatestModule.ToString())`n" Write-HostAzS -ForegroundColor Green "`n`t`tAuto-updating....`n" } else { Write-HostAzS "$ModuleName module is not installed`n" } # Install latest module version try { Install-Module -Name $ModuleName -RequiredVersion $LatestModule -ErrorAction Continue -Force -ErrorVariable ModuleInstallError } catch { Write-Warning "Error installing latest version $($LatestModule.ToString()) of module $ModuleName. $($_.Exception.Message)" } # Attempt to Uninstall the previous version try { Uninstall-Module -Name $ModuleName -RequiredVersion $InstalledModuleVersion -Force -ErrorAction SilentlyContinue -ErrorVariable ModuleUninstallError } catch { Write-Warning "Error uninstalling previous version $($InstalledModuleVersion.ToString()) of module $ModuleName. $($_.Exception.Message)" } # Import the new module version and validate try { Import-Module -Name $ModuleName -RequiredVersion $LatestModule -Force -ErrorAction Stop -ErrorVariable ModuleImportError # Validate the imported module version matches what we installed $loadedVersion = (Get-Module -Name $ModuleName).Version if ($loadedVersion -ne $LatestModule) { Write-Warning "Installed v$($LatestModule.ToString()) but loaded v$($loadedVersion.ToString()). Update may be incomplete." } } catch { Write-Warning "Error importing latest version $($LatestModule.ToString()) of module $ModuleName. $($_.Exception.Message)" } if(-not($ModuleInstallError)) { [bool]$ModuleUpdated = $true Write-HostAzS -ForegroundColor Green "`t`tModule updated successfully!`n" Write-HostAzS -ForegroundColor Green "`tPlease re-run the function to continue.`n`n" # Clean up stale module versions now that the new version is installed $StaleVersions = Get-Module -Name $ModuleName -ListAvailable | Where-Object { $PSItem.Version -ne $LatestModule } foreach($StaleModule in $StaleVersions) { try { Uninstall-Module -Name $ModuleName -RequiredVersion $StaleModule.Version -Force -ErrorAction Stop } catch { # Fallback: if Uninstall-Module fails (e.g. module was manually copied, not PSGallery-tracked), # remove the folder directly as a last resort Write-Debug "Uninstall-Module failed for v$($StaleModule.Version): $($_.Exception.Message). Removing folder directly." Remove-Item -Path $StaleModule.ModuleBase -Force -Recurse -ErrorAction SilentlyContinue } } } elseif($ModuleUninstallError) { Write-Warning "Error uninstalling previous version $($InstalledModuleVersion.ToString()) of module $ModuleName. $ModuleUninstallError" [bool]$ModuleUpdated = $false } else { Write-Warning "Error updating module $ModuleName. $ModuleInstallError" [bool]$ModuleUpdated = $false } # /// Work-around for Updated Module NOT importing automatically # Unload existing module from the active Pwsh session Remove-Module -Name $ModuleName -ErrorAction SilentlyContinue # Force Import of the Updated Module version by file path (Get-Module -Name $ModuleName -ListAvailable -ErrorAction SilentlyContinue | Sort-Object Version -Descending | Select-Object -First 1).Path | Import-Module -Force } else { # No Update Required Write-HostAzS -ForegroundColor Green "`n`tINFO: " -NoNewLine Write-HostAzS "$ModuleName module is up-to-date at v$($InstalledModuleVersion.ToString())`n" } # Module not installed } else { Write-HostAzS -ForegroundColor Red "`n`tError: " -NoNewLine Write-HostAzS "Module '$ModuleName' is not installed.`n" -ForegroundColor Yellow [bool]$ModuleUpdated = $false } } # End of process block end { # Write-Debug "Update-ModuleVersion: Module update process completed" # Return Function with boolean for Module Updated or not Return $ModuleUpdated } } # End Function Update-ModuleVersion # //////////////////////////////////////////////////////////////////////////// # Function to display an ASCII art banner # This function displays an ASCII art banner in the console # Font = Slant, credit web-site: https://patorjk.com/software/taag/ Function Show-ASCIIArtBanner { begin { # Write-Debug "Show-ASCIIArtBanner: Beginning ASCII art banner display" } process { # ASCI Art Banner variable [string]$banner=@' //////////////////////////////////////////////////////////////////////////////////////// ___ __ __ / |____ __ __________ / / ____ _________ _/ / / /| /_ / / / / / ___/ _ \ / / / __ \/ ___/ __ `/ / / ___ |/ /_/ /_/ / / / __/ / /___/ /_/ / /__/ /_/ / / /_/ |_/___/\__,_/_/ \___/ /_____/\____/\___/\__,_/_/ ______ __ _ _ __ ______ __ / ____/___ ____ ____ ___ _____/ /_(_) __(_) /___ __ /_ __/__ _____/ /______ / / / __ \/ __ \/ __ \/ _ \/ ___/ __/ / | / / / __/ / / / / / / _ \/ ___/ __/ ___/ / /___/ /_/ / / / / / / / __/ /__/ /_/ /| |/ / / /_/ /_/ / / / / __(__ ) /_(__ ) \____/\____/_/ /_/_/ /_/\___/\___/\__/_/ |___/_/\__/\__, / /_/ \___/____/\__/____/ /____/ //////////////////////////////////////////////////////////////////////////////////////// '@ Write-HostAzS -ForegroundColor Green `n$banner`n } end { # Write-Debug "Show-ASCIIArtBanner: ASCII art banner display completed" } } # End Function Show-ASCIIArtBanner |