Microsoft.AzLocal.CSSTools.psm1
|
################################################################# # # # Copyright (C) Microsoft Corporation. All rights reserved. # # # ################################################################# [CmdletBinding()] param ( [Parameter(Mandatory = $false, Position = 0)] [Bool]$SkipStartupActions = $false, [Parameter(Mandatory = $false, Position = 1)] [Bool]$SkipUpdateCheck = $false ) $skipExplicitlySet = $PSBoundParameters.ContainsKey('SkipStartupActions') [void]$PSBoundParameters.Remove('SkipStartupActions') [void]$PSBoundParameters.Remove('SkipUpdateCheck') # check if we are within a PSSession or runspace # if we are and SkipStartupActions wasn't explicitly set, we skip the startup actions if ($PSSenderInfo -and -not $skipExplicitlySet) { $SkipStartupActions = $true $SkipUpdateCheck = $true } ################################################################# # # # STARTUP ACTIONS ON IMPORT # # # ################################################################# Import-LocalizedData -BindingVariable 'msg' -BaseDirectory "$PSScriptRoot\locale" -UICulture (Get-Culture) -WarningAction SilentlyContinue $configurationData = Import-PowerShellDataFile -Path "$PSScriptRoot\config\Microsoft.AzLocal.CSSTools.Config.psd1" New-Variable -Name 'CSSTools_AzsSupport' -Scope 'Global' -Force -Value @{ Cache = @{} Config = $configurationData EnvironmentInfo = @{ WindowsProductName = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name 'ProductName' -ErrorAction Ignore).ProductName OSDisplayVersion = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name 'DisplayVersion' -ErrorAction Ignore).DisplayVersion CloudName = [string]::Empty # this is set later DisconnectedOps = $false SolutionVersion = [string]::Empty # this is set later SolutionInstalled = $false # this is set later Platform = [string]::Empty # this is set later and will be HCI_OS or HCI_OS_AZLOCAL_SOLUTION } ModuleVersion = $null } function Get-AzsSupportInstalledModuleVersion { <# .SYNOPSIS Returns the highest installed version of a module under the Windows PowerShell modules path. .DESCRIPTION Inspects $env:ProgramFiles\WindowsPowerShell\Modules\<Name> for version-named subfolders and returns the highest [System.Version], or $null when the module is not installed there. .PARAMETER Name The module name to inspect. .OUTPUTS System.Version. The highest installed version, or $null when none is found. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String]$Name ) $modulePath = Join-Path -Path (Join-Path -Path $env:ProgramFiles -ChildPath 'WindowsPowerShell\Modules') -ChildPath $Name if (-not (Test-Path -Path $modulePath -PathType Container)) { return $null } $highestVersion = $null foreach ($versionDirectory in (Get-ChildItem -Path $modulePath -Directory -ErrorAction Ignore)) { $parsedVersion = $null if ([System.Version]::TryParse($versionDirectory.Name, [ref]$parsedVersion)) { if ($null -eq $highestVersion -or $parsedVersion -gt $highestVersion) { $highestVersion = $parsedVersion } } } return $highestVersion } function Get-AzsSupportPackagePath { <# .SYNOPSIS Resolves the full filesystem path to a package bundled with Microsoft.AzLocal.CSSTools. .DESCRIPTION Reads PackageManifest.psd1, locates the requested package entry, and returns the full path to the highest shipped '<Package>.<Version>' folder under the package Root. This is primarily intended for packages flagged with Install = $false, which ship with the module but are NOT copied to the Windows PowerShell modules path (for example, to avoid interop issues with other features). The returned path can be passed directly to Import-Module so the specific bundled package can be loaded on demand. .PARAMETER Name The package name. Must match a Package value in the manifest. .PARAMETER ManifestPath Optional explicit path to the packages manifest. Defaults to PackageManifest.psd1 next to this module. .OUTPUTS System.String. The full path to the shipped package version folder, or $null when the package source folder or a versioned folder cannot be found. .EXAMPLE PS> Import-Module -Name (Get-AzsSupportPackagePath -Name 'SdnDiagnostics') #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String]$Name, [Parameter(Mandatory = $false)] [System.String]$ManifestPath ) $resolvedManifestPath = $ManifestPath if ([System.String]::IsNullOrWhiteSpace($resolvedManifestPath)) { $resolvedManifestPath = Join-Path -Path $PSScriptRoot -ChildPath 'PackageManifest.psd1' } if (-not (Test-Path -Path $resolvedManifestPath -PathType Leaf)) { throw ("Unable to locate packages manifest '{0}'." -f $resolvedManifestPath) } $manifest = Import-PowerShellDataFile -LiteralPath $resolvedManifestPath $manifestRoot = Split-Path -Path $resolvedManifestPath -Parent # Locate the requested package entry (case-insensitive match on the manifest Package value). $package = $manifest.Packages.Where({ $_.Package -ieq $Name }, 'First')[0] if ($null -eq $package) { throw ("Package '{0}' was not found in the packages manifest '{1}'." -f $Name, $resolvedManifestPath) } # Root is a path prefix relative to the manifest folder; the shipped folder is '<Root>.<Version>' # (for example packages\SdnDiagnostics.4.2604.29.191). $rootResolved = Join-Path -Path $manifestRoot -ChildPath $package.Root $searchParent = Split-Path -Path $rootResolved -Parent if (-not (Test-Path -Path $searchParent -PathType Container)) { Trace-Output -Level 'Warning' -Message ("Package source folder '{0}' for '{1}' was not found." -f $searchParent, $package.Package) return $null } # Discover the highest shipped '<Package>.<Version>' folder, deriving the version by stripping the # known package name prefix (handles variable-length version strings). $sourceDirectory = $null $sourceVersion = $null foreach ($candidate in (Get-ChildItem -Path $searchParent -Directory -Filter ("{0}.*" -f $package.Package) -ErrorAction Ignore)) { $versionString = $candidate.Name.Substring($package.Package.Length + 1) $parsedVersion = $null if ([System.Version]::TryParse($versionString, [ref]$parsedVersion)) { if ($null -eq $sourceVersion -or $parsedVersion -gt $sourceVersion) { $sourceVersion = $parsedVersion $sourceDirectory = $candidate } } } if ($null -eq $sourceDirectory) { Trace-Output -Level 'Warning' -Message ("No shipped '{0}.<Version>' folder was found under '{1}'." -f $package.Package, $searchParent) return $null } return $sourceDirectory.FullName } function Install-AzsSupportPackage { <# .SYNOPSIS Installs bundled packages declared in the central packages manifest into the Windows PowerShell modules path. .DESCRIPTION Reads PackageManifest.psd1 and, for every entry flagged with Install = $true, locates the shipped '<Package>.<Version>' folder under the package Root, derives the version by stripping the known package name prefix, and copies it to $env:ProgramFiles\WindowsPowerShell\Modules\<Package>\<Version>. The copy is performed only when the shipped version is newer than the highest version already installed (or when nothing is installed). When the same or a newer version is already installed the copy is skipped. Specify -Force to always overwrite the matching version. Entries flagged with Install = $false ship with the module but are not installed to the system modules path and are skipped. .PARAMETER Name Optional package name. When supplied, only the matching manifest entry is processed. .PARAMETER ManifestPath Optional explicit path to the packages manifest. Defaults to PackageManifest.psd1 next to this module. .PARAMETER Force Overwrites the matching installed version even when it already exists. .OUTPUTS System.String. The install path for each package that was installed or already present. #> [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [System.String]$Name, [Parameter(Mandatory = $false)] [System.String]$ManifestPath, [Parameter(Mandatory = $false)] [Switch]$Force ) $resolvedManifestPath = $ManifestPath if ([System.String]::IsNullOrWhiteSpace($resolvedManifestPath)) { $resolvedManifestPath = Join-Path -Path $PSScriptRoot -ChildPath 'PackageManifest.psd1' } if (-not (Test-Path -Path $resolvedManifestPath -PathType Leaf)) { throw ("Unable to locate packages manifest '{0}'." -f $resolvedManifestPath) } $manifest = Import-PowerShellDataFile -LiteralPath $resolvedManifestPath $manifestRoot = Split-Path -Path $resolvedManifestPath -Parent $modulesBasePath = Join-Path -Path $env:ProgramFiles -ChildPath 'WindowsPowerShell\Modules' foreach ($package in $manifest.Packages) { # Honor an explicit package filter when supplied. if (-not [System.String]::IsNullOrWhiteSpace($Name) -and $package.Package -ine $Name) { continue } # Entries not flagged for installation ship with the module but are not copied to the # system modules path. if (-not $package.Install) { Trace-Output -Level 'Verbose' -Message ("Package '{0}' is not flagged for installation. Skipping." -f $package.Package) continue } try { # Root is a path prefix relative to the manifest (config) folder; the shipped folder is # '<Root>.<Version>' (for example packages\SdnDiagnostics.4.2604.29.191). $rootResolved = Join-Path -Path $manifestRoot -ChildPath $package.Root $searchParent = Split-Path -Path $rootResolved -Parent if (-not (Test-Path -Path $searchParent -PathType Container)) { Trace-Output -Level 'Warning' -Message ("Package source folder '{0}' for '{1}' was not found. Skipping installation." -f $searchParent, $package.Package) continue } # Discover the highest shipped '<Package>.<Version>' folder, deriving the version by # stripping the known package name prefix (handles variable-length version strings). $sourceDirectory = $null $sourceVersion = $null foreach ($candidate in (Get-ChildItem -Path $searchParent -Directory -Filter ("{0}.*" -f $package.Package) -ErrorAction Ignore)) { $versionString = $candidate.Name.Substring($package.Package.Length + 1) $parsedVersion = $null if ([System.Version]::TryParse($versionString, [ref]$parsedVersion)) { if ($null -eq $sourceVersion -or $parsedVersion -gt $sourceVersion) { $sourceVersion = $parsedVersion $sourceDirectory = $candidate } } } if ($null -eq $sourceDirectory) { Trace-Output -Level 'Warning' -Message ("No shipped '{0}.<Version>' folder was found under '{1}'. Skipping installation." -f $package.Package, $searchParent) continue } $installPath = Join-Path -Path (Join-Path -Path $modulesBasePath -ChildPath $package.Package) -ChildPath $sourceVersion.ToString() $installedVersion = Get-AzsSupportInstalledModuleVersion -Name $package.Package # Copy when forced, when nothing is installed, or when the shipped version is newer than # the highest installed version. Otherwise the current (same or newer) version is kept. $shouldCopy = $Force.IsPresent -or ($null -eq $installedVersion) -or ($sourceVersion -gt $installedVersion) if (-not $shouldCopy) { Trace-Output -Level 'Verbose' -Message ("Package '{0}' version '{1}' already satisfied by installed version '{2}'. Skipping copy." -f $package.Package, $sourceVersion, $installedVersion) $installPath continue } if (Test-Path -Path $installPath -PathType Container) { Trace-Output -Level 'Verbose' -Message ("Removing existing install for package '{0}' at '{1}'." -f $package.Package, $installPath) Remove-Item -Path $installPath -Recurse -Force -ErrorAction Stop } New-Item -Path $installPath -ItemType Directory -Force -ErrorAction Stop | Out-Null Copy-Item -Path (Join-Path -Path $sourceDirectory.FullName -ChildPath '*') -Destination $installPath -Recurse -Force -ErrorAction Stop Trace-Output -Level 'Verbose' -Message ("Installed package '{0}' version '{1}' to '{2}'." -f $package.Package, $sourceVersion, $installPath) $installPath } catch { $_ | Trace-Exception Trace-Output -Level 'Warning' -Message ("Failed to install package '{0}'. {1}" -f $package.Package, $_.Exception.Message) } } } function Import-AzsSupportModule { <# .SYNOPSIS Imports the module dependencies declared in the central modules manifest. .DESCRIPTION Reads ModuleManifest.psd1 and imports each declared dependency by splatting its Parameters hashtable to Import-Module. When a Parameters entry defines RequiredVersion the specific version is loaded; otherwise the latest available version is used. Imports honor per-entry Import-Module parameters from the manifest (including ErrorAction). Failed imports are treated as optional by default (logged as warnings and loading continues). If a dependency entry sets Parameters.ErrorAction = 'Stop', a failed import is rethrown as a critical failure that stops module loading. When a dependency entry sets ImportFromBundle = $true, the module is imported from its bundled package location that ships with CSSTools (the shipped '<Package>.<Version>' folder resolved from PackageManifest.psd1 via Get-AzsSupportPackagePath) instead of by name. Use this for packages that are intentionally NOT installed to the Windows PowerShell modules path (Install = $false). A bundled dependency always takes precedence over a same-named module already present in the runspace (for example a native module shipped with the OS): the already-loaded short-circuit compares against the bundled version and force-reimports the bundled module when a different version is loaded, so the bundled version wins. .PARAMETER Name Optional dependency name (matching the manifest Package value). When supplied, only the matching entry is imported. .PARAMETER ModuleFile Optional leaf file name to import from the resolved, installed package folder instead of importing the module by name. Use this to import a module's .psm1 directly (for access to non-exported functions). Requires the package to be installed under the Windows PowerShell modules path. .PARAMETER ManifestPath Optional explicit path to the modules manifest. Defaults to ModuleManifest.psd1 next to this module. .PARAMETER Force Forces a reimport even when the module is already loaded. .PARAMETER Scope Optional override to control module import scope. When specified, overrides the Scope parameter from the manifest for the imported module(s). By default, the manifest setting is used. Use 'Local' to import into the local/script scope (restricting visibility of non-exported functions to the importing script). .OUTPUTS System.Management.Automation.PSModuleInfo. The imported module(s); nothing for entries that were skipped. #> [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [System.String]$Name, [Parameter(Mandatory = $false)] [System.String]$ModuleFile, [Parameter(Mandatory = $false)] [System.String]$ManifestPath, [Parameter(Mandatory = $false)] [Switch]$Force, [Parameter(Mandatory = $false)] [System.String]$Scope ) $resolvedManifestPath = $ManifestPath if ([System.String]::IsNullOrWhiteSpace($resolvedManifestPath)) { $resolvedManifestPath = Join-Path -Path $PSScriptRoot -ChildPath 'ModuleManifest.psd1' } if (-not (Test-Path -Path $resolvedManifestPath -PathType Leaf)) { throw ("Unable to locate modules manifest '{0}'." -f $resolvedManifestPath) } $manifest = Import-PowerShellDataFile -LiteralPath $resolvedManifestPath $modulesBasePath = Join-Path -Path $env:ProgramFiles -ChildPath 'WindowsPowerShell\Modules' foreach ($dependency in $manifest.ModuleDependencies) { # Honor an explicit dependency filter when supplied. if (-not [System.String]::IsNullOrWhiteSpace($Name) -and $dependency.Package -ine $Name) { continue } # Build the Import-Module splat from the manifest Parameters. $importParams = @{} if ($null -ne $dependency.Parameters) { foreach ($key in $dependency.Parameters.Keys) { $importParams[$key] = $dependency.Parameters[$key] } } # Apply Scope override if explicitly specified by the caller. if (-not [System.String]::IsNullOrWhiteSpace($Scope)) { $importParams['Scope'] = $Scope } $moduleName = $importParams['Name'] if ([System.String]::IsNullOrWhiteSpace($moduleName)) { $moduleName = $dependency.Package $importParams['Name'] = $moduleName } $errorAction = 'Continue' if ($importParams.ContainsKey('ErrorAction') -and -not [System.String]::IsNullOrWhiteSpace([System.String]$importParams['ErrorAction'])) { $errorAction = [System.String]$importParams['ErrorAction'] } # A dependency is considered mandatory when the manifest requests terminating behavior # for import failures. $mustSucceed = ($errorAction -ieq 'Stop') $requiredVersion = $null $hasRequiredVersion = -not [System.String]::IsNullOrWhiteSpace([System.String]$importParams['RequiredVersion']) if ($hasRequiredVersion) { $requiredVersion = [System.Version]$importParams['RequiredVersion'] } # When the dependency is imported by name from the standard module path (not from a bundled # package and not as a specific module file), confirm it is actually available before # attempting the import. Optional Azure Local solution components are absent on plain HCI OS, # and importing a missing module by name emits a non-terminating error that is not catchable # here. Skip quietly with a verbose trace so the module loads cleanly on systems without the # Azure Local solution installed. Mandatory dependencies (ErrorAction = Stop) are left to the # import attempt so their absence still surfaces as a terminating failure. if (-not $mustSucceed -and -not $dependency.ImportFromBundle -and [System.String]::IsNullOrWhiteSpace($ModuleFile)) { if (-not (Get-Module -Name $moduleName -ListAvailable -ErrorAction Ignore)) { Trace-Output -Level 'Verbose' -Message ($msg.dependencyModuleNotAvailable -f $moduleName) continue } # Some optional dependencies cannot initialize without other modules, declared via the # DependsOn array in ModuleManifest.psd1 (for example Support.AksArc requires MOC). Skip # the dependency when any required module is absent if ($null -ne $dependency.DependsOn) { $missingDependency = @($dependency.DependsOn).Where({ -not (Get-Module -Name $_ -ListAvailable -ErrorAction Ignore) }, 'First')[0] if (-not [System.String]::IsNullOrWhiteSpace($missingDependency)) { Trace-Output -Level Warning -Message ($msg.dependencyModuleSkippedMissingDependency -f $dependency.Package, $missingDependency) continue } } } try { # For bundled dependencies, resolve the shipped package path and derive its version up # front so the already-loaded short-circuit below can compare against the bundled # version. A bundled module must supersede any same-named module already present in the # runspace (for example a native SdnDiagnostics shipped with the OS), so this resolution # happens before the skip check rather than only at import time. $bundledPackagePath = $null $bundleVersion = $null if ($dependency.ImportFromBundle) { $bundledPackagePath = Get-AzsSupportPackagePath -Name $dependency.Package if ([System.String]::IsNullOrWhiteSpace($bundledPackagePath)) { throw ("Bundled package '{0}' could not be resolved from the packages manifest; cannot import from the bundled location." -f $dependency.Package) } # The shipped folder is named '<Package>.<Version>'; derive the version by stripping # the known package-name prefix from the leaf folder name. $bundleLeaf = Split-Path -Path $bundledPackagePath -Leaf $bundleVersionString = $bundleLeaf.Substring($dependency.Package.Length + 1) $parsedBundleVersion = $null if ([System.Version]::TryParse($bundleVersionString, [ref]$parsedBundleVersion)) { $bundleVersion = $parsedBundleVersion } } # Skip when the module is already loaded, unless a reimport is forced or a specific # module file is being imported. Bundled dependencies are compared against the bundled # version and force-reimported when a different version is loaded, so the bundled module # always supersedes a same-named module already present in the runspace. $forceBundleReimport = $false if (-not $Force -and [System.String]::IsNullOrWhiteSpace($ModuleFile)) { $loadedModules = @(Get-Module -Name $moduleName) if ($loadedModules.Count -gt 0) { # For bundled dependencies, compare against the bundled version (RequiredVersion # is ignored for bundle entries) rather than the manifest RequiredVersion. $compareVersion = $requiredVersion $hasCompareVersion = $hasRequiredVersion if ($dependency.ImportFromBundle -and $null -ne $bundleVersion) { $compareVersion = $bundleVersion $hasCompareVersion = $true } if ($hasCompareVersion) { if ($loadedModules.Where({ $_.Version -eq $compareVersion }, 'First')) { Trace-Output -Level 'Verbose' -Message ("Module '{0}' required version '{1}' is already loaded. Skipping import." -f $moduleName, $compareVersion) continue } $loadedVersions = ($loadedModules | Select-Object -ExpandProperty Version | ForEach-Object { $_.ToString() }) -join ', ' Trace-Output -Level 'Verbose' -Message ("Module '{0}' is loaded (version(s): {1}), but required version '{2}' is not loaded. Proceeding with import." -f $moduleName, $loadedVersions, $compareVersion) # A same-named module from a different source/version is loaded; force the # reimport so the bundled module supersedes it. if ($dependency.ImportFromBundle) { $forceBundleReimport = $true } } elseif ($dependency.ImportFromBundle) { # Bundled dependency whose shipped version could not be parsed; force the # reimport so the bundled module still supersedes the already-loaded one. $loadedVersions = ($loadedModules | Select-Object -ExpandProperty Version | ForEach-Object { $_.ToString() }) -join ', ' Trace-Output -Level 'Verbose' -Message ("Bundled module '{0}' is already loaded (version(s): {1}); forcing reimport so the bundled version is preferred." -f $moduleName, $loadedVersions) $forceBundleReimport = $true } else { Trace-Output -Level 'Verbose' -Message ("Module '{0}' is already loaded. Skipping import." -f $moduleName) continue } } } # When the dependency is flagged to import from the bundled package location, import from # the shipped '<Package>.<Version>' folder (resolved above) rather than by name. This # supports packages that are intentionally NOT installed to the Windows PowerShell # modules path (Install = $false in PackageManifest.psd1). if ($dependency.ImportFromBundle) { if (-not [System.String]::IsNullOrWhiteSpace($ModuleFile)) { # An explicit module file was requested; target it within the bundled folder. $importTarget = Join-Path -Path $bundledPackagePath -ChildPath $ModuleFile if (-not (Test-Path -Path $importTarget -PathType Leaf)) { throw ("Module file '{0}' was not found in bundled package folder '{1}' for module '{2}'." -f $ModuleFile, $bundledPackagePath, $moduleName) } } else { # The bundled folder is named '<Package>.<Version>', which does not match the module # manifest name, so Import-Module cannot auto-discover the manifest from the folder. # Resolve the actual module file: prefer the manifest (.psd1), then the script module # (.psm1), matching the package name first and otherwise the only such file present. $importTarget = $null foreach ($extension in @('psd1', 'psm1')) { $namedFile = Join-Path -Path $bundledPackagePath -ChildPath ("{0}.{1}" -f $moduleName, $extension) if (Test-Path -Path $namedFile -PathType Leaf) { $importTarget = $namedFile break } $candidates = @(Get-ChildItem -Path $bundledPackagePath -Filter ("*.{0}" -f $extension) -File -ErrorAction Ignore) if ($candidates.Count -eq 1) { $importTarget = $candidates[0].FullName break } } if ([System.String]::IsNullOrWhiteSpace($importTarget)) { throw ("No module manifest (.psd1) or script module (.psm1) was found in bundled package folder '{0}' for module '{1}'." -f $bundledPackagePath, $moduleName) } } $importParams['Name'] = $importTarget $importParams.Remove('RequiredVersion') } # When a specific module file is requested, resolve it from the installed package folder # (RequiredVersion when declared, otherwise the highest installed version) and import # that file directly rather than importing the module by name. elseif (-not [System.String]::IsNullOrWhiteSpace($ModuleFile)) { $targetVersion = $null if ($hasRequiredVersion) { $targetVersion = $requiredVersion } else { $targetVersion = Get-AzsSupportInstalledModuleVersion -Name $moduleName } if ($null -eq $targetVersion) { throw ("Module '{0}' is not installed under the Windows PowerShell modules path; cannot import module file '{1}'." -f $moduleName, $ModuleFile) } $packageFolder = Join-Path -Path (Join-Path -Path $modulesBasePath -ChildPath $moduleName) -ChildPath $targetVersion.ToString() $importTarget = Join-Path -Path $packageFolder -ChildPath $ModuleFile if (-not (Test-Path -Path $importTarget -PathType Leaf)) { throw ("Module file '{0}' was not found in package folder '{1}' for module '{2}'." -f $ModuleFile, $packageFolder, $moduleName) } $importParams['Name'] = $importTarget $importParams.Remove('RequiredVersion') } $importParams['PassThru'] = $true if (-not $importParams.ContainsKey('ErrorAction')) { $importParams['ErrorAction'] = 'Continue' } if (-not $importParams.ContainsKey('DisableNameChecking')) { $importParams['DisableNameChecking'] = $true } # Force the reimport when the caller requested it, or when a bundled dependency must # supersede a different same-named module already loaded in the runspace. if ($Force -or $forceBundleReimport) { $importParams['Force'] = $true } # A bundled dependency is imported by path, but a same-named module already loaded by # name (for example a native SdnDiagnostics) is a distinct module to Import-Module, so # even -Force leaves the existing version loaded and both versions coexist. Explicitly # remove every already-loaded copy of the module by name first so the bundled version is # the only one left in the runspace. if ($forceBundleReimport) { Trace-Output -Level 'Verbose' -Message ("Removing already-loaded module '{0}' so the bundled version supersedes it." -f $moduleName) Remove-Module -Name $moduleName -Force -ErrorAction SilentlyContinue } Trace-Output -Level 'Verbose' -Message ("Importing module '{0}'." -f $importParams['Name']) # Suppress verbose (4) and warning (3) streams emitted by the imported module on load. Import-Module @importParams 4> $null 3> $null } catch { if ($mustSucceed) { throw ("Failed to import module '{0}'. {1}" -f $moduleName, $_.Exception.Message) } $_ | Trace-Exception Trace-Output -Level 'Warning' -Message ("Failed to import module '{0}'. {1}" -f $moduleName, $_.Exception.Message) } } } # Ensure the module is always imported from an elevated (administrator) session, regardless of # whether startup actions are skipped. Confirm-IsAdmin throws a localized error instructing the # user to re-import as administrator when not elevated. Confirm-IsAdmin # Install bundled packages declared in the packages manifest into the Windows PowerShell modules # path. This runs for all imports, including remote/runspace sessions, so that by-name imports # below can resolve installed versions from the standard PSModulePath. try { Install-AzsSupportPackage | Out-Null } catch { $_ | Trace-Exception Trace-Output -Level:Warning -Message ("Failed to install one or more bundled packages. {0}" -f $_.Exception.Message) } # Import the module dependencies declared in the modules manifest (always, regardless of startup # actions) so dependent modules such as Microsoft.AzureStack.Lcm.PowerShell are available in both # interactive and remote sessions. A failed import aborts module loading when that dependency sets # ErrorAction = Stop in ModuleManifest.psd1; otherwise it is skipped with a warning. Import-AzsSupportModule | Out-Null # Determine whether the Azure Local solution is installed by probing for the solution-provided # cmdlets. On plain HCI OS these are absent, and the module operates in a reduced HCI_OS mode. The # result is cached on the global so Test-AzsSupportSolutionInstalled and other modules can read it. $solutionInstalled = $true foreach ($solutionCommand in @('Get-StampInformation', 'Get-SolutionUpdateEnvironment')) { if (-not (Get-Command -Name $solutionCommand -ErrorAction Ignore)) { $solutionInstalled = $false break } } $Global:CSSTools_AzsSupport.EnvironmentInfo.SolutionInstalled = $solutionInstalled $Global:CSSTools_AzsSupport.EnvironmentInfo.Platform = if ($solutionInstalled) { 'HCI_OS_AZLOCAL_SOLUTION' } else { 'HCI_OS' } # The solution update environment cmdlet is only available when the Azure Local solution is # installed; query it only in that mode to avoid a CommandNotFoundException on HCI OS. if ($solutionInstalled) { $currentVersion = (Get-SolutionUpdateEnvironment -ErrorAction Ignore).CurrentVersion if ($currentVersion) { $Global:CSSTools_AzsSupport.EnvironmentInfo.SolutionVersion = $currentVersion.ToString() } } ################################################################# # # # ENUMS AND CLASSES # # # ################################################################# enum Component { OS AzureArcResourceBridge } ################################################################# # # # FUNCTIONS # # # ################################################################# function Get-AzsSupportOSStampInformation { <# .SYNOPSIS Builds a stamp-information view for systems without the Azure Local solution. .DESCRIPTION On plain HCI OS the solution-provided Get-StampInformation cmdlet is unavailable. This helper synthesizes an ordered set of stamp-style properties from operating-system, computer, domain, cluster, and time-zone data. Properties that are only meaningful when the Azure Local solution is installed are returned as $null so consumers can render them as not-applicable. .OUTPUTS System.Collections.Specialized.OrderedDictionary. The synthesized stamp information. #> [CmdletBinding()] [OutputType([System.Collections.Specialized.OrderedDictionary])] param () $environmentInfo = $Global:CSSTools_AzsSupport.EnvironmentInfo $computerSystem = $null try { $computerSystem = Get-CimInstance -ClassName 'Win32_ComputerSystem' -ErrorAction Stop } catch { $_ | Trace-Exception } $domainFqdn = $null if ($null -ne $computerSystem -and $computerSystem.PartOfDomain) { $domainFqdn = $computerSystem.Domain } # Determine the node count from the failover cluster when the feature is present; default to the # single local node otherwise. $numberOfNodes = 1 if (Get-Command -Name 'Get-ClusterNode' -ErrorAction Ignore) { try { $clusterNodes = @(Get-ClusterNode -ErrorAction Stop) if ($clusterNodes.Count -gt 0) { $numberOfNodes = $clusterNodes.Count } } catch { $_ | Trace-Exception } } $timeZoneId = $null try { $timeZoneId = (Get-TimeZone -ErrorAction Stop).Id } catch { $_ | Trace-Exception } return [Ordered]@{ Platform = $environmentInfo.Platform WindowsProductName = $environmentInfo.WindowsProductName OSDisplayVersion = $environmentInfo.OSDisplayVersion ComputerName = $env:COMPUTERNAME DomainFQDN = $domainFqdn DomainNetBIOSName = $env:USERDOMAIN NumberOfNodes = $numberOfNodes TimeZone = $timeZoneId RegionName = $environmentInfo.Region DeploymentID = $null OemVersion = $null StampVersion = $null ServicesVersion = $null PlatformVersion = $null InitialDeployedVersion = $null NetworkSchemaVersion = $null CloudID = $null } } function Get-AzsSupportStampInformation { <# .SYNOPSIS Gets common stamp information .DESCRIPTION Queries for common stamp information properties such as DeploymentID, OEMVersion and CloudID. On plain HCI OS, where the Azure Local solution is not installed, an equivalent view is synthesized from operating-system, computer, and cluster data instead. .EXAMPLE PS> Get-AzsSupportStampInformation .OUTPUTS Outputs the Stamp Information #> [CmdletBinding()] param() # On plain HCI OS the Azure Local solution - and its Get-StampInformation cmdlet - is not present. # Synthesize an equivalent view so callers and the entry banner still receive a populated object. if (-not (Test-AzsSupportSolutionInstalled)) { return Get-AzsSupportOSStampInformation } try { $stampInfo = Get-StampInformation -ErrorAction Stop $stampInformation = [Ordered]@{ DeploymentID = $stampInfo.DeploymentID OemVersion = $stampInfo.OemVersion StampVersion = $stampInfo.StampVersion ServicesVersion = $stampInfo.ServicesVersion PlatformVersion = $stampInfo.PlatformVersion InitialDeployedVersion = $stampInfo.InitialDeployedVersion NetworkSchemaVersion = $stampInfo.NetworkSchemaVersion Prefix = $stampInfo.Prefix CompanyName = $stampInfo.CompanyName ServerSku = $stampInfo.ServerSku Topology = $stampInfo.Topology TimeZone = $stampInfo.TimeZone HardwareOEM = $stampInfo.HardwareOEM RegionName = $stampInfo.RegionName DomainNetBIOSName = $stampInfo.DomainNetBIOSName DomainFQDN = $stampInfo.DomainFQDN NumberOfNodes = $stampInfo.NumberOfNodes CloudID = $stampInfo.CloudID RingName = $stampInfo.RingName InstallationMethod = $stampInfo.InstallationMethod HardwareClass = $stampInfo.HardwareClass } } catch { $_ | Write-Error } return $stampInformation } function Get-ModuleVersion { $manifest = Test-ModuleManifest -Path "$PSScriptRoot\Microsoft.AzLocal.CSSTools.psd1" $Global:CSSTools_AzsSupport.ModuleVersion = $manifest.Version.ToString() return $manifest.Version.ToString() } function Get-ModuleUpdateStatus { try { $galleryModule = Find-Module -Name 'Microsoft.AzLocal.CSSTools' -Repository 'PSGallery' -ErrorAction Stop $localVersion = [version]$Global:CSSTools_AzsSupport.ModuleVersion $galleryVersion = [version]$galleryModule.Version if ([version]$galleryVersion -gt [version]$localVersion) { return @{ UpdateAvailable = $true CurrentVersion = $localVersion.ToString() LatestVersion = $galleryVersion.ToString() } } } catch { Trace-Output -Level:Warning -Message "Unable to check for module updates: $_" } return @{ UpdateAvailable = $false } } function Test-AzsSupportSolutionInstalled { <# .SYNOPSIS Determines whether the Azure Local solution is present on the current system. .DESCRIPTION Returns the cached platform state recorded on the CSSTools_AzsSupport global during module import: $false when running on plain HCI OS, $true when the Azure Local solution is also installed. When the global has not yet been populated - for example when this function is called in isolation before the main module has finished loading - it falls back to a live probe for the solution-provided cmdlets so the result is still correct. .PARAMETER Terminating If specified, the function will throw a terminating error if the solution is not installed. .OUTPUTS System.Boolean. $true when the Azure Local solution is installed; otherwise $false. .EXAMPLE PS> Test-AzsSupportSolutionInstalled True #> [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory=$false)] [switch]$Terminating ) $environmentInfo = $Global:CSSTools_AzsSupport.EnvironmentInfo if ($null -ne $environmentInfo -and $environmentInfo.ContainsKey('SolutionInstalled')) { if (-not $environmentInfo.SolutionInstalled -and $Terminating) { throw New-Object System.NotSupportedException($msg.errSolutionNotInstalled) } return [System.Boolean]$environmentInfo.SolutionInstalled } # The global state has not been populated yet; probe for the solution-provided cmdlets directly. $solutionInstalled = $true foreach ($command in @('Get-StampInformation', 'Get-SolutionUpdateEnvironment')) { if (-not (Get-Command -Name $command -ErrorAction Ignore)) { $solutionInstalled = $false break } } if (-not $solutionInstalled -and $Terminating) { throw New-Object System.NotSupportedException($msg.errSolutionNotInstalled) } return $solutionInstalled } function Get-EntryText { @' ################################################################################# _ _ _ / \ _____ _ _ __ ___ | | ___ ___ __ _| | / _ \ |_ / | | | '__/ _ \ | | / _ \ / __/ _` | | / ___ \ / /| |_| | | | __/ | |__| (_) | (_| (_| | | /_/ \_\/___|\__,_|_| \___| |_____\___/ \___\__,_|_| ################################################################################# Provide feedback or suggestions to azlocaldiagfeedback@microsoft.com. Tool Tips: List CSSTools commands: Get-Command -Module Microsoft.AzLocal.CSSTools Get help with examples: Get-Help 'Verb-Command' -Full Check for common issues: Invoke-AzsSupportInsight Check for new updates: Find-Module -Name Microsoft.AzLocal.CSSTools Install latest version: Update-Module -Name Microsoft.AzLocal.CSSTools -Force Clean up the working directory after you are done to reclaim disk space: Clear-AzsSupportDirectory '@ | Write-Host -ForegroundColor:Green # display primary stamp info properties $stampInfo = Get-AzsSupportStampInformation if ($stampInfo) { $stampInfo += @{ CSSToolsVersion = $Global:CSSTools_AzsSupport.ModuleVersion Region = $Global:CSSTools_AzsSupport.EnvironmentInfo.Region DisconnectedOps = $Global:CSSTools_AzsSupport.EnvironmentInfo.DisconnectedOps } $maxKeyLength = ($stampInfo.Keys | Measure-Object -Property Length -Maximum).Maximum $stampInfo.Keys | ForEach-Object { $key = $_.PadRight($maxKeyLength) $value = "N/A" if (![string]::IsNullOrEmpty($stampInfo[$_])) { $value = $stampInfo[$_] } # Print aligned output "{0} --> {1}" -f $key, $value | Write-Host -ForegroundColor:Gray } } else { $msg.stampInfoNotAvailable | Write-Warning } "" | Write-Host # display remediation history if available if ($Global:AzStack_Insights.RemediationHistory.Count -gt 0) { "Remediation History Location: $($Global:AzStack_Insights.RemediationHistoryFolder)" | Write-Host -ForegroundColor:Cyan "Remediation History:" | Write-Host -ForegroundColor:Cyan $Global:AzStack_Insights.RemediationHistory | Format-Table -Property RemediationScript, Name, TimeStamp, Status, ChangesApplied -AutoSize | Out-String | Write-Host -ForegroundColor:Gray } } function New-AzsSupportDataBundle { <# .SYNOPSIS Creates a support data bundle for Azure Stack HCI. .DESCRIPTION This function collects various types of diagnostic data from an Azure Stack HCI cluster and compiles it into a support data bundle. The data collected can include cluster commands, node commands, events, registry information, and specific folders. The function supports both automatic data collection based on predefined components and manual data collection based on user-specified parameters. .PARAMETER Component Specifies the Azure Stack HCI component for which to automatically collect data. Valid values are defined in the Component enum. .PARAMETER ClusterCommands An array of cluster-level commands to run and include in the data bundle. .PARAMETER NodeCommands An array of node-level commands to run on each cluster node and include in the data bundle. .PARAMETER NodeEvents An array of event log queries to run on each cluster node and include in the data bundle. .PARAMETER NodeRegistry An array of registry paths to query on each cluster node and include in the data bundle. .PARAMETER NodeFolders An array of folder paths to collect from each cluster node and include in the data bundle. .PARAMETER ComputerName An array of computer names (cluster nodes) on which to run the specified commands and collect data. If not specified, defaults to all cluster nodes. .EXAMPLE PS C:\> New-AzsSupportDataBundle -Component "OS" Automatically collects a predefined set of diagnostic data related to the operating system component of Azure Stack HCI and compiles it into a support data bundle. .EXAMPLE PS C:\> New-AzsSupportDataBundle -NodeCommands @("Get-Process", "Get-Service") -ComputerName @("Node01", "Node02") Collects the output of "Get-Process" and "Get-Service" commands from Node01 and Node02 and includes it in the support data bundle. .NOTES This function requires administrative privileges to run. The collected data may contain sensitive information, so ensure that the support data bundle is handled securely and shared only with authorized personnel. #> [CmdletBinding()] param ( # AUTOMATIC DATA COLLECTION SPECS [Parameter(ParameterSetName = 'DataCollectAuto')] [Component] $Component, # MANUAL DATA COLLECTION SPECS [Parameter(ParameterSetName = 'DataCollectManual')] [array] $ClusterCommands, [Parameter(ParameterSetName = 'DataCollectManual')] [array] $NodeCommands, [Parameter(ParameterSetName = 'DataCollectManual')] [array] $NodeEvents, [Parameter(ParameterSetName = 'DataCollectManual')] [array] $NodeRegistry, [Parameter(ParameterSetName = 'DataCollectManual')] [array] $NodeFolders, [Parameter(ParameterSetName = 'DataCollectManual')] [array] $ComputerName ) Trace-Output -Level:Information -Message $msg.startingNewDataBundle $runtime = Register-CommandRuntime switch ($PSCmdlet.ParameterSetName.ToLower()) { "datacollectmanual" { if(($NodeCommands -or $NodeEvents -or $NodeRegistry -or $NodeFolders) -and (-Not $ComputerName)) { Trace-Output -Level:Error -Message $msg.errComputerNameNotSupplied } else { Invoke-DataCollection ` -runtime $runtime ` -clusterCommands $ClusterCommands ` -nodeCommands $NodeCommands ` -nodeEvents $NodeEvents ` -nodeRegistry $NodeRegistry ` -nodeFolders $NodeFolders ` -ComputerName $ComputerName } } "datacollectauto" { Trace-Output -Level:Information -Message $msg.startingAutomaticCollection Invoke-AutoDataCollection -runtime $runtime -Component $Component } Default {} } } function Invoke-DataCollection() { param( [string] $runtime, [array] $ClusterCommands, [array] $NodeCommands, [array] $NodeEvents, [array] $NodeRegistry, [array] $NodeFolders, [array] $ComputerName ) Trace-Output -Level:Information -Message $msg.startingDataCollection $name = "SupportDataBundle" # important default information we want to always collect. $NodeCommands = $NodeCommands + @("Get-ChildItem env:*", "gpresult /h STORAGE_DEST/gpresultoutput.html") # manually collecting data Collect-SupportData ` -runtime $runtime ` -clusterCommands $ClusterCommands ` -nodeCommands $NodeCommands ` -nodeEvents $NodeEvents ` -nodeRegistry $NodeRegistry ` -nodeFolders $NodeFolders ` -ComputerName $ComputerName ` -customName $name } function Invoke-AutoDataCollection() { param( [string] $runtime, [Component] $Component ) $storage = Get-WorkingDirectory $storageTemp = ('{0}\temp' -f $storage) if((Test-Path -path $storageTemp) -eq $false) { New-Item -Type:Directory -Path $storageTemp | Out-Null } Trace-Output -Level:Verbose -Message $Component switch ($Component) { ([Component]::OS) { # Automatic OS log collection $cmd = Get-Command -Name "Send-DiagnosticData" -ErrorAction Ignore if($cmd) { Collect-SupportData ` -nodeCommands @('Send-DiagnosticData -SaveToPath STORAGE_DEST -FromDate ((Get-Date).AddDays(-1)) -ToDate (Get-Date) -CollectSddc:$true') ` -ComputerName (Get-ClusterNode) } else { Trace-Output -Level:Error -Message $msg.errSendDiagDataNotAvailable } } ([Component]::AzureArcResourceBridge) { Trace-Output -Level:Verbose -Message "Starting Resource Bridge Data Collection" Trace-Output -Level:Information -Message $msg.arcApplianceLogStart $destPath = ("{0}\SupportDataBundle-{1}" -f $storage, (Get-Date -Format "HH-mm_dd-MM-yyyy")) $azTenant = Read-Host -Prompt $msg.questionTenantLogin Trace-Output -Level:Information -Message $msg.arcApplianceLogConfig # get environment information $azStackInfo = Get-AzureStackHCI $azStackUri = $azStackInfo.AzureResourceUri.split("/") $azSub = $azStackUri[2] # xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx $azReg = $azStackUri[4] # sample-rg $arcHciConfigObject = Get-ArcHciConfig $arcHciConfigPath = ("{0}/hci-resource.yaml" -f $arcHciConfigObject.workingDir) $arcHciConfig = Get-Content -Path $arcHciConfigPath $arcHciConfig = $arcHciConfig.trim() $arcHciConfigApplianceName = "" foreach($arcHciConfigEntry in $arcHciConfig) { if($arcHciConfigEntry.indexOf("name:") -ne -1){ $arcHciConfigApplianceName = $arcHciConfigEntry.split(":")[1].trim() } } Trace-Output -Level:Information -Message $msg.arcApplianceLogLogin az login --tenant $azTenant --use-device-code az account set --subscription $azSub Trace-Output -Level:Information -Message $msg.arcApplianceLogCollectionStart az arcappliance get-credentials -g $azReg -n $arcHciConfigApplianceName --overwrite-existing --credentials-dir $storageTemp Get-ChildItem -path $storageTemp | ForEach-Object { Icacls $storageTemp /c /t /Inheritance:d | Out-Null Icacls $storageTemp /c /t /Grant ${env:UserName}:F | Out-Null TakeOwn /F $storageTemp | Out-Null Icacls $storageTemp /c /t /Grant:r ${env:UserName}:F | Out-Null Icacls $storageTemp /c /t /Remove:g Administrator "Authenticated Users" BUILTIN\Administrators BUILTIN Everyone System Users | Out-Null } | Out-Null az arcappliance logs hci ` --cloudagent $arcHciConfigObject.cloudFqdn ` --credentials-dir $storageTemp ` --ip $arcHciConfigObject.controlPlaneIP ` --kubeconfig ("{0}\kubeconfig" -f $arcHciConfigObject.workingDir) ` --loginconfigfile ("{0}\kvatoken.tok" -f $arcHciConfigObject.workingDir) ` --out-dir $destPath if($destPath) { $destZipPath = ("{0}.zip" -f $destPath) Compress-Archive -Path $destPath -DestinationPath ("{0}.zip" -f $destZipPath) Remove-Item -Path $destPath -Recurse -Force Trace-Output -Level:Success -Message ($msg.CollectionDataEnd -f $destZipPath) } else { Trace-Output -Level:Error -Message $msg.arcApplianceLogCollectionError } Remove-Item -Path $storageTemp -Recurse -Force } Default { Trace-Output -Level:Error -Message $msg.errComponentNotFound } } } function Confirm-AzsSupportOSVersion { <# .SYNOPSIS Validates the OS version against a specified version or minimum version. .DESCRIPTION This function checks the current OS version against a specified version or minimum version. It throws an error if the current OS version does not match the specified version or is below the minimum version. .PARAMETER Version The exact OS version to confirm against the current OS version. .PARAMETER MinimumVersion The minimum OS version that the current OS version must meet or exceed. .EXAMPLE PS> Confirm-AzsSupportOSVersion -Version "23H2" .OUTPUTS System.Void This cmdlet does not return objects. It throws an error if the OS version check fails. #> param( [Parameter(Mandatory = $true, ParameterSetName = 'ConfirmOSVersion')] [ValidateSet("22H2", "23H2", "24H2", "25H2")] [string]$Version, [Parameter(Mandatory = $true, ParameterSetName = 'ConfirmMinimumOSVersion')] [ValidateSet("22H2", "23H2", "24H2", "25H2")] [string]$MinimumVersion ) $osOrder = @("22H2", "23H2", "24H2", "25H2") $currentVersion = $Global:CSSTools_AzsSupport.EnvironmentInfo.OSDisplayVersion switch ($PSCmdlet.ParameterSetName) { 'ConfirmOSVersion' { if ($currentVersion -ne $Version) { throw ($msg.osNotSupported -f $Version) } } 'ConfirmMinimumOSVersion' { $minIndex = $osOrder.IndexOf($MinimumVersion) $curIndex = $osOrder.IndexOf($currentVersion) if ($curIndex -lt 0) { throw ($msg.osNotSupportedGeneric) } if ($curIndex -lt $minIndex) { throw ($msg.osNotSupported -f $MinimumVersion) } } } } function Install-AzsSupportModule { <# .SYNOPSIS Install the AzsSupport Module to remote computers if not installed or version mismatch. .DESCRIPTION This function checks if the Microsoft.AzLocal.CSSTools module is installed on the specified remote computers and if the version matches the local version. If the module is not installed or there is a version mismatch, it copies the module from the local computer to the remote computer. It also has an option to force the installation, which will copy the module regardless of the current state on the remote computer. .PARAMETER ComputerName Type the NetBIOS name, an IP address, or a fully qualified domain name of one or more remote computers. .PARAMETER Credential Specifies a user account that has permission to perform this action. The default is the current user. Type a user name, such as User01 or Domain01\User01, or enter a PSCredential object generated by the Get-Credential cmdlet. If you type a user name, you're prompted to enter the password. .PARAMETER Force Forces a cleanup and re-install of the module on the remote computer. .EXAMPLE PS> Install-AzsSupportModule -ComputerName @('Node01', 'Node02') Checks each remote computer for the installed module version and copies the local module if missing or outdated. .EXAMPLE PS> $cred = Get-Credential PS> Install-AzsSupportModule -ComputerName 'Node01' -Credential $cred -Force Forces a reinstall of the module on the remote computer by copying the local module regardless of remote version. .OUTPUTS System.Void This cmdlet does not return objects. It writes progress and status/error messages. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String[]]$ComputerName, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential = [System.Management.Automation.PSCredential]::Empty, [Parameter(Mandatory = $false)] [switch]$Force ) begin { $moduleName = 'Microsoft.AzLocal.CSSTools' $moduleDirpath = (Split-Path -Path (Get-Module -Name $moduleName).Path -Parent) # if we have multiple modules installed on the current workstation, # abort the operation because side by side modules can cause some interop issues to the remote nodes $localModule = Get-Module -Name $moduleName if ($localModule.Count -gt 1) { throw ($msg.installSupportModuleMultipleVersions -f $moduleName) } $getModuleVersionSB = { param ([string]$arg0) try { # Get the latest version of Microsoft.AzLocal.CSSTools Module installed $version = (Get-Module -Name $arg0 -ListAvailable -ErrorAction Ignore | Sort-Object Version -Descending)[0].Version.ToString() } catch { # in some instances, the module will not be available and as such we want to skip the noise and return # a string back to the remote call command which we can do proper comparison against $version = '0.0.0.0' } return $version } # typically PowerShell modules will be installed in the following directory configuration: # $env:ProgramFiles\WindowsPowerShell\Modules\{module}\{version} # $env:USERPROFILE\Documents\WindowsPowerShell\Modules\{module}\{version} # so we default to Leaf of the path being {module} as PSGet will handle the versioning so we only ever do import in the following format: # Import-Module {module} (if using default PowerShell module path) # Import-Module C:\{path}\{module} (if using custom PowerShell module path) # so we need to ensure that we are copying the module to the correct path on the remote computer [System.String]$destinationPathDir = $moduleDirpath } process { $totalComputers = $ComputerName.Count $currentComputer = 0 $ComputerName | ForEach-Object { $computer = $_ $currentComputer++ $percentComplete = [math]::Min([math]::Round(($currentComputer / $totalComputers) * 100, 0), 99) Write-Progress -Activity "Installing $moduleName" -Status "Processing $computer ($currentComputer of $totalComputers)" -PercentComplete $percentComplete -Id 1 try { # check to see if the computer is local, if so, we will skip the operation if (Test-ComputerNameIsLocal -ComputerName $computer) { ($msg.installSupportModuleSkipUpdate -f $computer) | Trace-Output return # exit the current iteration of the loop and move to the next computer } if (!$Force) { Write-Progress -Activity "Installing $moduleName" -Status "Checking version on $computer" -PercentComplete $percentComplete -CurrentOperation "Getting installed version" -Id 1 ($msg.installSupportModuleGetVersion -f $computer, $moduleName) | Trace-Output # use Invoke-Command here, as we do not want to create a cached session for the remote computers # as it will impact scenarios where we need to import the module on the remote computer for remote sessions try { $invokeParams = @{ ComputerName = $computer ScriptBlock = $getModuleVersionSB ArgumentList = @($moduleName) ErrorAction = 'Stop' } if ($Credential -ne [System.Management.Automation.PSCredential]::Empty -and $null -ne $Credential) { $invokeParams.Add('Credential', $Credential) } $remoteModuleVersion = Invoke-Command @invokeParams } catch { # if we are unable to connect to the remote computer, we will skip the operation $_ | Trace-Exception ($msg.installSupportModuleUnableToConnect -f $computer) | Trace-Output return # exit the current iteration of the loop and move to the next computer } if ($remoteModuleVersion) { # if the remote module version is greater or equal to the local module version, then we do not need to update ($msg.installSupportModuleVersionCheck -f $computer, $remoteModuleVersion, $localModule.Version.ToString()) | Trace-Output if ([version]$remoteModuleVersion -ge [version]$localModule.Version) { return # exit the current iteration of the loop and move to the next computer } } } Write-Progress -Activity "Installing $moduleName" -Status "Updating $computer" -PercentComplete $percentComplete -CurrentOperation "Copying module files (version $($localModule.Version))" -Id 1 ($msg.installSupportModuleUpdateStart -f $computer, $moduleName, $localModule.Version.ToString()) | Trace-Output Copy-FileToRemoteComputer -Path $localModule.ModuleBase -ComputerName $computer -Destination $destinationPathDir -Credential $Credential -Recurse -Force Write-Progress -Activity "Installing $moduleName" -Status "Cleaning up $computer" -PercentComplete $percentComplete -CurrentOperation "Removing PS sessions" -Id 1 # ensure that we destroy the current pssessions for the computer to prevent any caching issues # we will want to remove any existing PSSessions for the remote computers Remove-AzsSupportPSSession -ComputerName $computer } catch { $_ | Trace-Exception $_ | Write-Error } } } end { Write-Progress -Activity "Installing $moduleName" -Completed -Id 1 } } function Invoke-AzsSupportScript { <# .SYNOPSIS Invokes an AzsSupport script. .DESCRIPTION This function is used to execute a script located in the scripts directory of the AzsSupport module. .PARAMETER ScriptName The name of the script to execute. .PARAMETER Parameters A hashtable of parameters to pass to the script. .EXAMPLE Invoke-AzsSupportScript -ScriptName "MyScript" -Parameters @{Param1 = "Value1"; Param2 = "Value2"} #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ArgumentCompleter({ param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) $possibleValues = Get-ChildItem -Path $PSScriptRoot\scripts -Filter "*.ps1" -ErrorAction Stop | Select-Object -ExpandProperty BaseName if ([string]::IsNullOrEmpty($wordToComplete)) { return $possibleValues | Sort-Object } return $possibleValues | Where-Object { $_ -like "*$wordToComplete*" } | Sort-Object })] [string]$ScriptName, [Parameter(Mandatory = $false)] [hashtable]$Parameters ) $scriptPath = Join-Path -Path $PSScriptRoot -ChildPath "scripts\$ScriptName.ps1" if (-not (Test-Path -Path $scriptPath)) { throw "Script '$ScriptName' not found at path: $scriptPath" } try { "Executing script: {0}" -f $ScriptName | Trace-Output if ($Parameters) { "Passing arguments: {0}" -f ($Parameters | Out-String) | Trace-Output & $scriptPath @Parameters } else { & $scriptPath } } catch { $_ | Trace-Exception "Error occurred while executing script: {0}" -f $ScriptName | Trace-Output -Level:Error throw $_ } } ################################################################# # # # SECONDARY STARTUP ACTIONS ON IMPORT # # # ################################################################# $null = Get-ModuleVersion # The Azure Connected Machine (Arc) agent is independent of the Azure Local solution and can be # present on plain HCI OS as well. Gate the probe on the agent's actual availability - not on # SolutionInstalled - so the region is resolved on Arc-enabled HCI OS systems too. The catch still # covers the case where azcmagent is installed but the invocation fails (agent stopped or not yet # onboarded). try { if (Get-Command -Name 'azcmagent' -ErrorAction Ignore) { $cloudData = azcmagent show -j | ConvertFrom-Json if ($cloudData) { $Global:CSSTools_AzsSupport.EnvironmentInfo.Region = $cloudData.location if ($cloudData.location -notin $Global:CSSTools_AzsSupport.Config.AzureRegions) { $Global:CSSTools_AzsSupport.EnvironmentInfo.DisconnectedOps = $true } } } } catch { Trace-Output -Level:Warning -Message $msg.azcmagentFailed } # print the entry text if we are not skipping the startup actions if (!$SkipStartupActions) { Get-EntryText # check for module updates from PSGallery unless skipped or in a remote session if (-not $SkipUpdateCheck) { $updateStatus = Get-ModuleUpdateStatus if ($updateStatus.UpdateAvailable) { Write-Host "" # just a spacer line Write-Host "A newer version of Microsoft.AzLocal.CSSTools is available: v$($updateStatus.LatestVersion) (installed: v$($updateStatus.CurrentVersion))." -ForegroundColor Yellow Write-Host "To update, remove the current module from your session and install the latest version:" -ForegroundColor Yellow Write-Host "`tRemove-Module -Name Microsoft.AzLocal.CSSTools -Force" -ForegroundColor Yellow Write-Host "`tUpdate-Module -Name Microsoft.AzLocal.CSSTools -Force" -ForegroundColor Yellow Write-Host "`tImport-Module -Name Microsoft.AzLocal.CSSTools -Force" -ForegroundColor Yellow Write-Host "" # just a spacer line } } Write-Host "" # just a spacer line } Set-Location -Path (Get-AzsSupportWorkingDirectory) # Remove any existing PSSessions that were created by this module # This is to ensure that we do not have any stale sessions that could cause issues # when running commands in the module. Remove-AzsSupportPSSession # if we are running in disconnected setup, we want to import the AzStack.Disconnected module into the global runspace # to expose unique functions related to management of disconnected AzLocal systems if ($Global:CSSTools_AzsSupport.EnvironmentInfo.DisconnectedOps) { $disconnectedModulePath = Join-Path -Path $PSScriptRoot -ChildPath "Modules\AzStack.Disconnected" if (Test-Path -Path $disconnectedModulePath) { Import-Module -Name $disconnectedModulePath -Force -ErrorAction Ignore -Scope Global } } ################################################# # DO NOT EDIT BELOW THIS LINE # ################################################# # SIG # Begin signature block # MIInNwYJKoZIhvcNAQcCoIInKDCCJyQCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCD9VmTk75T3MQtJ # mvRNDMz9/NBuEdl5Y/4HAxAoFk8TJqCCDMkwggYEMIID7KADAgECAhMzAAACHPrN # xZvoL37EAAAAAAIcMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNVBAYTAlVTMR4wHAYD # VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBD # b2RlIFNpZ25pbmcgUENBIDIwMjQwHhcNMjYwNDE2MTg1OTQxWhcNMjcwNDE1MTg1 # OTQxWjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYD # VQQDExVNaWNyb3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IB # DwAwggEKAoIBAQDVsZfgOKmM31HPfoWOoNEiw0SlCiIxUMC0I9NMWbucKOw/e9lP # oAoehQVu6SG65V4EPzrYsnBnFPNoi4/HoOdjhz1qkrEt4I6tEcxXU6oOeY9zGveC # /3iBeuhLYxM3M/PkcUoebF+Nednm8OkdSPoDu8imViHPQq/8CQUu0WRR4rE+dMRf # rpVqfmNi2qWCX94T4MsepijGVkwE//tJg0ryAiYdHT34LSnlG/RSBZmQRGWZ5g8j # qnKjRParSqMft1gvjuUTVgtWNZfgcLFSK5Wa0myrq8OPcgTGGsRgun+tnSS+IxDT # xVsAPH1OzvPjwomguByhUe/OcvUN0D5Wmp7xAgMBAAGjggGqMIIBpjAOBgNVHQ8B # Af8EBAMCB4AwHwYDVR0lBBgwFgYKKwYBBAGCN0wIAQYIKwYBBQUHAwMwHQYDVR0O # BBYEFNoH7a2YDjOSwpkp6DHcmUS7J+0yMFQGA1UdEQRNMEukSTBHMS0wKwYDVQQL # EyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxFjAUBgNVBAUT # DTIzMDAxMis1MDc1NjkwHwYDVR0jBBgwFoAUf1k/VCHarU/vBeXmo9ctBpQSCDEw # YAYDVR0fBFkwVzBVoFOgUYZPaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9w # cy9jcmwvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNy # bDBtBggrBgEFBQcBAQRhMF8wXQYIKwYBBQUHMAKGUWh0dHA6Ly93d3cubWljcm9z # b2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmcl # MjBQQ0ElMjAyMDI0LmNydDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IC # AQAUnEqhaRXe0T3hIJjvdQErEkrA/7bByjn6t5IArODkkRjzkYwtKMc2yYj2quaN # rLutWw2YZcngKPy1b71YyDJQTy4NDRwaSh9Tw5thrk3NmcPrAHia5vtcBJ1CgtKK # 7mQbIcQ22d/N3813ayCDDFewu1+jsZmX+r/aTEqaOM4TVxVtRSkuCy8nAXKuChOK # Li/zA4XuH8iEYqIsj2YoNaeSxVmeGiERXpKdo3dDmYi0kO5w2D8VS4c3+9h6gElY # BaAAg/dYErBg27qT3vv0zRDJhJufvCNylA8S7/+8H5E/PV5cng6na9VV/w9OV3qu # uND6zdGa2EX38Glp50F9AIQk3p2xXmcvorDeM4XJ7UlWYBi6g80J1SSOQnInCYFE # msfUNn3+1AaTJKSJL83quKArTac2pKhu0Yzzzrzo6HrsRiQKzpnRBb1/dMa6P3hz # 75XbMRBctNsFhZC07WCmjExdLg2eHW5uV0TY8D5+6wozJf7vF3+WHkYPO85Z+BC6 # U4FkNbYNycZ9cE4j1tXRdyDCfml6c0HWPHjNVDObrv9lKt3qUqFpX38VCqVCyNOO # 1UcXfQiVjJw32U2WUKZjt/neJKHEBsm9kFsLuWzkQ53+qcaSaytmsCnk2gOglrlD # 5d3kKyvvAw+rzm0lT8K38P6PLxfZQHhu4W8dV7Av8N2ZmDCCBr0wggSloAMCAQIC # EzMAAAA5O7Y3Gb8GHWcAAAAAADkwDQYJKoZIhvcNAQEMBQAwgYgxCzAJBgNVBAYT # AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD # VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBS # b290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDExMB4XDTI0MDgwODIwNTQxOFoX # DTM2MDMyMjIyMTMwNFowVzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29m # dCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9zb2Z0IENvZGUgU2lnbmluZyBQ # Q0EgMjAyNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANgBnB7jOMeq # lRYHNa265v4IY9fH8TKhemHfPINe1gpLaV3dhg324WwH06LcHbpnsBukCDNitryo # 0dtS/EW6I/yEL/bLSY8hKpbfQuWusBPr9qazYcDxCW/qnjb5JsI1s8bNOg3bVATv # QVL4tcf03aTycsz8QeCdM0l/yHRObJ9QqazM1r6VPEOJ7LL+uEEb73w6QCuhs89a # 1uv1zerOYMnsneRRwCbpyW11IcggU0cRKDDq1pjVJzIbIF6+oiXXbReOsgeI8zu1 # FyQfK0fVkaya8SmVHQ/tOf23mZ4W9k0Ri22QW9p3UgSC5OUDktKxxcCmGL6tXLfO # GSWHIIV4YrTJTT6PNty5REojHJuZHArkF9VnHTERWoTjAzfI3kP+5b4alUdhgAZ7 # ttOu1bVnXfHaqPYl2rPs20ji03LOVWsh/radgE17es5hL+t6lV0eVHrVhsssROWJ # uz2MXMCt7iw7lFPG9LXKGjsmonn2gotGdHIuEg5JnJMJVmixd5LRlkmgYRZKzhxS # CwyoGIq0PhaA7Y+VPct5pCHkijcIIDm0nlkK+0KyepolcqGm0T/GYQRMhHJlGOOm # VQop36wUVUYklUy++vDWeEgEo4s7hxN6mIbf2MSIQ/iIfMZgJxC69oukMUXCrOC3 # SkE/xIkgpfl22MM1itkZ35nNXkMolU1lAgMBAAGjggFOMIIBSjAOBgNVHQ8BAf8E # BAMCAYYwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFH9ZP1Qh2q1P7wXl5qPX # LQaUEggxMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMA8GA1UdEwEB/wQFMAMB # Af8wHwYDVR0jBBgwFoAUci06AjGQQ7kUBU7h6qfHMdEjiTQwWgYDVR0fBFMwUTBP # oE2gS4ZJaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMv # TWljUm9vQ2VyQXV0MjAxMV8yMDExXzAzXzIyLmNybDBeBggrBgEFBQcBAQRSMFAw # TgYIKwYBBQUHMAKGQmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMv # TWljUm9vQ2VyQXV0MjAxMV8yMDExXzAzXzIyLmNydDANBgkqhkiG9w0BAQwFAAOC # AgEAFJQfOChP7onn6fLIMKrSlN1WYKwDFgAddymOUO3FrM8d7B/W/iQ6DxXsDn7D # 5W4wMwYeLystcEqfkjz4NURRgazyMu5yRzQh4LqjA4tStTcJh1opExo7nn5PuPBY # nbu0+THSuVHTe0VTTPVhily/piFrDo3axQ9P4C+Ol5yet+2gTfekICS5xS+cYfSI # vgn0JksVBVMYVI5QFu/qhnLhsEFEUzG8fvv0hjgkO+lkpV9ty6GkN4vdnd7ya6Q6 # aR9y34aiM1qmxaxBi6OUnyNl6fkuun/diTFnYDLTppOkr/mg5WSfCiDVMNCxtj4w # PKC5OmHm1DQIt/MNokbbH3UGsFP1QbzsLocuSqLCvH09Io3fDPTmscR9Y75G4qX7 # RTX8AdBPo0I6OEojf39zuFZt0qOHm65YWQE69cZM2ueE1MB05dNNgHK9gTE7zKvK # /fg8B2qjW88MT/WF5V5uvZGtqa9FSL2RazArA+rDPuf6JGYz4HpgMZHB4S6szWSK # YBv0VisCzfxgeU+dquXW9bd0auYlOB58DPcOYKdc3Se94g+xL4pcEhbB54JOgAkw # YTu/9dLeH2pDqeJZAABVDWRQCaXfO5LgyKwKCLYXpigrZYCjUSBcr+Ve8PFWMhVT # Ql0v4q8J/AUmQN5W4n101cY2L4A7GTQG1h32HHAvfQESWP0xghnEMIIZwAIBATBu # MFcxCzAJBgNVBAYTAlVTMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x # KDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMjQCEzMAAAIc # +s3Fm+gvfsQAAAAAAhwwDQYJYIZIAWUDBAIBBQCggZAwGQYJKoZIhvcNAQkDMQwG # CisGAQQBgjcCAQQwLwYJKoZIhvcNAQkEMSIEIFuPaW8qGlDe1RVrcm3VUojFCjHY # 8UhYRWAc0DW1bC12MEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8AcwBv # AGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEBBQAE # ggEABm7ePqp+6/fUrwSdojS+c5weexvksWT0QTeK1giUuGjhbBAHqb2RYk2ZKNDC # DXfJfixz67s6xfJcFn6m+ZfZ/QM0fUL2cjI+hOdELwG2+vo5SxN9zzKdNi5y5m9y # zP+RwBz9KTB4VikxZBu3IkEPSq9I285dUFRfP8PT9HsVnNPYhgmTgohGNSA6lq7I # Oa+mF86rtR3hSGf41iNBMvXop9zo28J8uXx0Qqptz4yknhKojm4plNaeItI8eGx0 # 3BKTmPVZzrZP/loSAcUWZsGUHI4SV14PYoLPMy8ftPXmBHes/14w97mDEW+YuS9A # g2UxLBBh9m7HuxttoB5Cz8ozWqGCF5QwgheQBgorBgEEAYI3AwMBMYIXgDCCF3wG # CSqGSIb3DQEHAqCCF20wghdpAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFSBgsqhkiG # 9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFlAwQC # AQUABCDTYRcshsw9cPsOJiS1wRDEUj5Qj0lsto5bTrsIPhB5OgIGal9aBddhGBMy # MDI2MDcyOTEzMTc1MS4xMDFaMASAAgH0oIHRpIHOMIHLMQswCQYDVQQGEwJVUzET # MBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV # TWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmlj # YSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046MzcwMy0wNUUw # LUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2WgghHq # MIIHIDCCBQigAwIBAgITMwAAAh86cGnkojAulQABAAACHzANBgkqhkiG9w0BAQsF # ADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH # UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD # Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0yNjAyMTkxOTM5NTFa # Fw0yNzA1MTcxOTM5NTFaMIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu # Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv # cmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMScw # JQYDVQQLEx5uU2hpZWxkIFRTUyBFU046MzcwMy0wNUUwLUQ5NDcxJTAjBgNVBAMT # HE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIiMA0GCSqGSIb3DQEBAQUA # A4ICDwAwggIKAoICAQDLO8XFOcfGqAqgiz0+AmQmFl3dZ0aTG4UFJkqqNdMHy28D # aheCBs6ONufukye5x42CWkzgRIy9kE2VWwEntZ8ZkgyrykC0bIqsID7+6FxguseT # Xf1Vwvm1D8104VmetoBJlJ4uGbuyJZUvXDx55nVh50ygLTzZ24WkQsnPpvRZv2kP # c39f3bhLyHVtnHsa/W/86Vrftd+AfFveA+qN/EY+XGj5c/DPMXCYECb0arYb92dD # JWtwzpyBrp4gfHlgY1UEpc4l4AGELrf2J4wrxTzTW+SM8XhV1dOOPrYjD080IbZq # L8B+IF0RCdn269YXrGK6QIHipznKZcCS8jN30YAHnTJVN5Zzs6t/2YsqBGDquvDa # d7934FFTwzvUcO3VoIyd93XWwvP8/SCFVJh21W8oGQTptGHyly+Fl4henVMVZF1v # 6osOtirX8GFTiEhnf8nRdOg7yZYAJ0xy9CtDfbXaTn/cf3Lq3N/GCYKFjC+5mUCE # +AJhmxMuMdvSUGmKiAFdiPAjUTqsWWBBZJm0eCwgeGJFmmQA+V7/98BKcE+gUL7O # 9eWRDQwKeAcvo6rxNv2Y4jKrHA6Z/wi3a/fKUhLCNZES8qGdrpDAm7qh+6FjYxyt # AbkiKM6uTNy/ULPlwtlYZoAJDDQP7eYCywwVbNTbHXRBSS+NccC0sSB4W7U67wID # AQABo4IBSTCCAUUwHQYDVR0OBBYEFNk72sGDlH0r5DwvfGR5XwJI8B7bMB8GA1Ud # IwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1UdHwRYMFYwVKBSoFCGTmh0 # dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUyMFRp # bWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggrBgEFBQcBAQRgMF4wXAYI # KwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMv # TWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3J0MAwGA1Ud # EwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwDgYDVR0PAQH/BAQDAgeA # MA0GCSqGSIb3DQEBCwUAA4ICAQBlbu3IoynnPz0K1iPbeNnsej2b15l5sdl2FAFB # BGT9lRdc2gNV8LAIusPYHHhUvRDcsx4lbMNhVKPGu4TDLaqNt/CI+SFtGuqdRLpV # P1XE9cCLyKrKPpcJFJCqPpV+efoAtYBmIUQcxxwT7WIQ7gag8+rkKvrMkCoRqKS0 # mKv8J1sKfi85+G2uhZ/1RteSVdYZOZOj+Sb4wzonTCTj7EtgMN/BX35W5dTzd7wJ # dGepYkVi871dSrC2Tr1ZFzAR7S44drCWZpJ6phJabVNOsNxFJKgSykugOGWzQ318 # Rr3MTPg2s3Bns+pUPVgMijd4bUOH2BlEsLMMwOcolTTZqg1HYrdY1jxpUAI9ipjB # QRINL/O705Z+/f2LjNmJQooCVJVX24adpZ519SsfazGoqXGt91bmqKo0fI09Il4s # UHh4ih6rpiQDBlyL7vmvCejwVxYevY4qVwTZ/o3gvl+R0lFxYS9feIM4NeG0+WsD # Z7jLci5MFeuNwosQY3z26Xg1oj0U9u+ncR9uTU+xBmJ8BtlCdhQ13RNMX5P+krRY # PB3XCp9Jm6XaO1995q32AIZm1mzBGI6yHlviXaEC5TzGiO1LXuPtXZU2X93oQJbM # oe3v8+5CPKrQalGWyYuh2a3V1pwbj+W0FEmEFPpu8TI+qYO1IIQWUSRvFjXth5Ob # 02hMMjCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZIhvcN # AQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYD # VQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAw # BgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDEw # MB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDELMAkGA1UEBhMCVVMx # EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT # FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUt # U3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDk # 4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg4r25PhdgM/9c # T8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aORmsHFPPFdvWG # UNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41JmTamDu6Gnsz # rYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOORj7I5LFGc6XBpDco2 # LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnopN6zL64NF50ZuyjLV # wIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0gz3N9QZpGdc3EXzTd # EonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/BQOj0XOmTTd0lBw0 # gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8QmguEOqEUUbi0b1qGFph # AXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0kZSU2LlQ+QuJ # YfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y7435UsSFF5PAPBXb # GjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQABo4IB3TCCAdkwEgYJ # KwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTEmr6CkTxGNSnP # EP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwGA1UdIARVMFMw # UQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWljcm9z # b2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNVHSUEDDAKBggr # BgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYw # DwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvXzpoY # xDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtp # L2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYIKwYB # BQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20v # cGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDANBgkqhkiG9w0B # AQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4sQaTlz0xM7U5 # 18JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6th542DYunKmCVgAD # sAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRXud2f8449xvNo # 32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBewVIVCs/wMnosZ # iefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0DLzskYDSPeZK # PmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+CljdQDzHVG2dY3RI # LLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5aGZFrDZ+kKNxnGSgk # ujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+fKFhbHP+CrvsQWY9 # af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7ntdAoGokLjzba # ukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGePu1+oDEzfbzL6Xu/ # OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNYs6FwZvKhggNNMIIC # NQIBATCB+aGB0aSBzjCByzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0 # b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3Jh # dGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEnMCUG # A1UECxMeblNoaWVsZCBUU1MgRVNOOjM3MDMtMDVFMC1EOTQ3MSUwIwYDVQQDExxN # aWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQBLIMg1 # P7sNuCXpmbH2IXT2tXeEEKCBgzCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQI # EwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv # ZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBD # QSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA7hRjVDAiGA8yMDI2MDcyOTExMzIzNloY # DzIwMjYwNzMwMTEzMjM2WjB0MDoGCisGAQQBhFkKBAExLDAqMAoCBQDuFGNUAgEA # MAcCAQACAgOHMAcCAQACAhMZMAoCBQDuFbTUAgEAMDYGCisGAQQBhFkKBAIxKDAm # MAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJKoZIhvcN # AQELBQADggEBAMviH8Xc+lr0iATL4YakwJC4YHBj8hN4xGVHg5cLPdT/8DwBws6s # rhkAtvKD6sQM3AaFb0cfBVDqvHme9P6xJHQa4TjcaZmGIfUCZFZcMLGfqYfw4SI8 # B9GQP+7IhuZmvoTGj3We6awNPRmRbmPlSHvVwu8KnFujERMw1nBzjqGT+AQtd+Jc # w1TcillQnmMgRTPmV8zUmaZUDCgmYyQA5BptVJWEZvLwz/D/B6rTJ4+AFQMCwIs6 # DtgLLhLj9xXJ6YBG/8BPcgQkCXT6tDrO339b8kgNzLS5wg6nv0ukhizayiWVCNgz # qfmZ+9OA+rooFhVXt/9hT8yV5r1iRFCpZ7cxggQNMIIECQIBATCBkzB8MQswCQYD # VQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEe # MBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3Nv # ZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAh86cGnkojAulQABAAACHzANBglg # hkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8GCSqG # SIb3DQEJBDEiBCBU5O3xTaXbDA+yAfg8Is4GmjpJP/p22iL3plfHe0oBVDCB+gYL # KoZIhvcNAQkQAi8xgeowgecwgeQwgb0EILAkCt9WkCsMtURkFu6TY0P3UXdRnCiY # uPZhe3ykLfwUMIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp # bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw # b3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAC # EzMAAAIfOnBp5KIwLpUAAQAAAh8wIgQgvjJrIFDtRplTmDzVZOa/1ZuBy+NuRfPA # cz0oDsSjESMwDQYJKoZIhvcNAQELBQAEggIACMYOXnCcuosEXrRhfQ64/FBXNjIN # 6g2fc+dGdjnBsZeCzH3jditIXLVqdv6yraHPXw09u8bqg8fUpLDh9ZUyeW/1MM2p # JrniaCSY+k3BHqywiM2jUARokTyb1dEf2+AaCVrhV/hKeq1eoReNnmKVFEq40oxy # tka6iJGDJdTQAiF2it6BD9TBSsNoqpY603tHaQJrRedwI1mG+YldTofrfQgiA1Ks # eMpMTLEj0ubZq8ncFTt1hxiW2QTYNbju92tzCaAFfRH9IIZL1fGCjxBU3sld2dVx # 1b/c0vkLYu4UXqtT3qpE27AYE5P6LxrJxcTOU1z+cLtj1H6J+0zoDvNwlVJFQQ1w # ht45SS2P4z7wccH/DDk5IQZDRaEMdlmEY+ttdrw3lx/LsPHjL9Kb2POQ/yFi58yh # k4OfwcC6+6wYgTGga6Sc+8gXBukFXpsAST3DA5xDdpOjERcf6y67j2tLMEzsA6vu # D9rlcqj4aIEbHSqo/U9L6HiJ9mGuUNM+ZVej7htJ/YIWj7U5srwNVSj8ceCWzEoQ # 6jxMkOTPPzP77aK7GcpXy2NyOnhwpsuV1emzjeHPlD6DVDtGHrYc8iP3p8lrTyrs # qzpGIW1uQ6Yj9LEaaw8GXKhyulzpcOUEvM2qLOssJUo7oNMV+kwa+3RNEaW4F7wV # 39R20tYirGssDlg= # SIG # End signature block |