Public/NA.Runtime.ps1
|
function Import-PreferredModule { <# .SYNOPSIS Imports a module preferring a development manifest when available. .DESCRIPTION Tries to import a module from DevManifestPath first (if requested and found), otherwise imports the installed module by name. .PARAMETER ModuleName Module name to import. .PARAMETER DevManifestPath Optional path to the development module manifest (*.psd1). .PARAMETER PreferDev Prefer DevManifestPath when it exists. Enabled by default. .PARAMETER Force Force module reload. .EXAMPLE Import-PreferredModule -ModuleName 'Nebula.Automations' -DevManifestPath 'C:\Temp\Nebula.Automations\Nebula.Automations.psd1' .EXAMPLE Import-PreferredModule -ModuleName 'Nebula.Log' -PreferDev $false -Force .LINK https://kb.gioxx.org/Nebula/Automations/usage/import-preferredmodule #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$ModuleName, [string]$DevManifestPath, [bool]$PreferDev = $true, [switch]$Force ) $result = [pscustomobject]@{ Success = $false ModuleName = $ModuleName Source = 'Installed' Version = $null Path = $null Message = $null } try { if ($Force.IsPresent) { Remove-Module $ModuleName -Force -ErrorAction SilentlyContinue } $useDev = $PreferDev -and -not [string]::IsNullOrWhiteSpace($DevManifestPath) -and (Test-Path -LiteralPath $DevManifestPath) $resolvedDevManifestPath = $null if ($useDev) { $resolvedDevManifestPath = (Resolve-Path -LiteralPath $DevManifestPath -ErrorAction Stop).Path Import-Module $DevManifestPath -Force:$Force.IsPresent -WarningAction SilentlyContinue -ErrorAction Stop } else { Import-Module $ModuleName -Force:$Force.IsPresent -WarningAction SilentlyContinue -ErrorAction Stop } $loadedModule = Get-Module -Name $ModuleName | Sort-Object Version -Descending | Select-Object -First 1 if (-not $loadedModule) { throw "Module '$ModuleName' appears not loaded after import attempt." } $loadedModuleDir = Split-Path -Path $loadedModule.Path -Parent if (-not [string]::IsNullOrWhiteSpace($resolvedDevManifestPath)) { $devModuleDir = Split-Path -Path $resolvedDevManifestPath -Parent if ([string]::Equals($loadedModuleDir, $devModuleDir, [System.StringComparison]::OrdinalIgnoreCase)) { $result.Source = 'DEV' } else { $result.Source = 'Installed' } } else { $result.Source = 'Installed' } $result.Success = $true $result.Version = $loadedModule.Version $result.Path = $loadedModule.Path $result.Message = "Module '$ModuleName' loaded ($($result.Source)): v$($result.Version) from $($result.Path)" } catch { $result.Message = $_.Exception.Message } return $result } function Initialize-ScriptRuntime { <# .SYNOPSIS Initializes common script runtime prerequisites. .DESCRIPTION Imports requested modules, loads an XML configuration file and optionally ensures that a log directory exists. .PARAMETER ConfigPath Full path to the XML configuration file. .PARAMETER ModulesToImport Modules to import before script execution. .PARAMETER LogDirectory Log directory path. .PARAMETER EnsureLogDirectory Create LogDirectory if it does not exist. .EXAMPLE Initialize-ScriptRuntime -ConfigPath 'C:\Config\tenant.config.xml' -ModulesToImport @('Nebula.Log','Nebula.Automations') -LogDirectory 'C:\Logs' -EnsureLogDirectory .EXAMPLE $runtime = Initialize-ScriptRuntime -ConfigPath 'C:\Config\tenant.config.xml' if (-not $runtime.Success) { throw $runtime.Message } .LINK https://kb.gioxx.org/Nebula/Automations/usage/initialize-scriptruntime #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$ConfigPath, [string[]]$ModulesToImport = @('Nebula.Log', 'Nebula.Automations'), [string]$LogDirectory, [switch]$EnsureLogDirectory ) $result = [pscustomobject]@{ Success = $false Config = $null ConfigPath = $ConfigPath LogDirectory = $LogDirectory Message = $null } try { foreach ($moduleName in $ModulesToImport) { if ([string]::IsNullOrWhiteSpace($moduleName)) { continue } Import-Module $moduleName -Force -WarningAction SilentlyContinue -ErrorAction Stop } if (-not (Test-Path -LiteralPath $ConfigPath)) { throw "Configuration file not found: $ConfigPath" } [xml]$config = Get-Content -LiteralPath $ConfigPath -ErrorAction Stop $result.Config = $config if ($EnsureLogDirectory.IsPresent -and -not [string]::IsNullOrWhiteSpace($LogDirectory)) { if (-not (Test-Path -LiteralPath $LogDirectory)) { $null = New-Item -ItemType Directory -Path $LogDirectory -Force -ErrorAction Stop } } $result.Success = $true $result.Message = 'Runtime initialization completed.' } catch { $result.Message = $_.Exception.Message } return $result } function Resolve-ScriptConfigPaths { <# .SYNOPSIS Builds common script paths (config/log/output) from a script root. .DESCRIPTION Resolves a reusable path set used by automation scripts: - Config root + config file path - Log directory path - Output file path .PARAMETER ScriptRoot Root folder of the calling script (typically $PSScriptRoot). .PARAMETER ConfigRelativePath Relative path of the configuration file under ConfigRoot. .PARAMETER ConfigRootPath Optional explicit config root. If omitted, parent of ScriptRoot is used. .PARAMETER LogRelativePath Optional relative log directory under ScriptRoot. .PARAMETER OutputRelativePath Optional relative output file path under ScriptRoot. .PARAMETER EnsureDirectories Creates log directory and output parent directory if missing. .EXAMPLE Resolve-ScriptConfigPaths -ScriptRoot $PSScriptRoot -ConfigRelativePath 'Config\tenant.config.xml' -LogRelativePath 'Logs\MyScript' -OutputRelativePath 'Export\result.json' .EXAMPLE Resolve-ScriptConfigPaths -ScriptRoot $PSScriptRoot -ConfigRootPath 'C:\AutomationRoot' -ConfigRelativePath 'Config\tenant.config.xml' -EnsureDirectories .LINK https://kb.gioxx.org/Nebula/Automations/usage/resolve-scriptconfigpaths #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$ScriptRoot, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$ConfigRelativePath, [string]$ConfigRootPath, [string]$LogRelativePath, [string]$OutputRelativePath, [switch]$EnsureDirectories ) $result = [pscustomobject]@{ Success = $false ScriptRoot = $null ConfigRoot = $null ConfigPath = $null LogDirectory = $null OutputPath = $null Message = $null } try { if (-not (Test-Path -LiteralPath $ScriptRoot)) { throw "ScriptRoot not found: $ScriptRoot" } $resolvedScriptRoot = (Resolve-Path -LiteralPath $ScriptRoot -ErrorAction Stop).Path $resolvedConfigRoot = if (-not [string]::IsNullOrWhiteSpace($ConfigRootPath)) { if (-not (Test-Path -LiteralPath $ConfigRootPath)) { throw "ConfigRootPath not found: $ConfigRootPath" } (Resolve-Path -LiteralPath $ConfigRootPath -ErrorAction Stop).Path } else { Split-Path -Path $resolvedScriptRoot -Parent } $result.ScriptRoot = $resolvedScriptRoot $result.ConfigRoot = $resolvedConfigRoot $result.ConfigPath = Join-Path -Path $resolvedConfigRoot -ChildPath $ConfigRelativePath if (-not [string]::IsNullOrWhiteSpace($LogRelativePath)) { $result.LogDirectory = Join-Path -Path $resolvedScriptRoot -ChildPath $LogRelativePath } if (-not [string]::IsNullOrWhiteSpace($OutputRelativePath)) { $result.OutputPath = Join-Path -Path $resolvedScriptRoot -ChildPath $OutputRelativePath } if ($EnsureDirectories.IsPresent) { if (-not [string]::IsNullOrWhiteSpace($result.LogDirectory) -and -not (Test-Path -LiteralPath $result.LogDirectory)) { $null = New-Item -ItemType Directory -Path $result.LogDirectory -Force -ErrorAction Stop } if (-not [string]::IsNullOrWhiteSpace($result.OutputPath)) { $outputParent = Split-Path -Path $result.OutputPath -Parent if (-not [string]::IsNullOrWhiteSpace($outputParent) -and -not (Test-Path -LiteralPath $outputParent)) { $null = New-Item -ItemType Directory -Path $outputParent -Force -ErrorAction Stop } } } $result.Success = $true $result.Message = 'Path resolution completed.' } catch { $result.Message = $_.Exception.Message } return $result } function Test-ScriptActivityLog { <# .SYNOPSIS Verifies that activity logging is available for a script. .DESCRIPTION Uses Test-ActivityLog when available (typically from Nebula.Log). Falls back to a direct write/delete test in the log directory. .PARAMETER LogLocation Log directory path to validate. .EXAMPLE Test-ScriptActivityLog -LogLocation 'C:\Logs\MyScript' .EXAMPLE $status = Test-ScriptActivityLog -LogLocation 'C:\Logs\MyScript' if ($status.Status -ne 'OK') { throw $status.Message } .LINK https://kb.gioxx.org/Nebula/Automations/usage/test-scriptactivitylog #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$LogLocation ) $result = [pscustomobject]@{ Success = $false Status = 'KO' LogLocation = $LogLocation Message = $null } try { if (-not (Test-Path -LiteralPath $LogLocation)) { $null = New-Item -ItemType Directory -Path $LogLocation -Force -ErrorAction Stop } $testActivityLogCommand = Get-Command -Name 'Test-ActivityLog' -ErrorAction SilentlyContinue if ($testActivityLogCommand) { $status = Test-ActivityLog -LogLocation $LogLocation if ($status -eq 'OK') { $result.Success = $true $result.Status = 'OK' $result.Message = 'Activity log is ready and writable.' } else { $result.Message = "Test-ActivityLog returned '$status'." } } else { $testFile = Join-Path -Path $LogLocation -ChildPath '.na-activitylog.test' Set-Content -Path $testFile -Value 'ok' -Encoding UTF8 -ErrorAction Stop Remove-Item -Path $testFile -Force -ErrorAction Stop $result.Success = $true $result.Status = 'OK' $result.Message = 'Activity log fallback test passed.' } if (Get-Command -Name 'Write-NALog' -ErrorAction SilentlyContinue) { $level = if ($result.Success) { 'INFO' } else { 'ERROR' } Write-NALog -Message $result.Message -Level $level -LogLocation $LogLocation } } catch { $result.Message = $_.Exception.Message if (Get-Command -Name 'Write-NALog' -ErrorAction SilentlyContinue) { Write-NALog -Message "Activity log check failed: $($result.Message)" -Level ERROR -LogLocation $LogLocation } } return $result } function Start-ScriptTranscript { <# .SYNOPSIS Starts a transcript safely for automation scripts. .DESCRIPTION Optionally clears old transcript files and starts a new transcript in the specified output directory. .PARAMETER OutputDirectory Output directory where transcript files are written. .PARAMETER CleanupOld Remove older transcript files matching CleanupPattern. .PARAMETER CleanupPattern Pattern used when CleanupOld is enabled. .PARAMETER IncludeInvocationHeader Include invocation header in the transcript. .EXAMPLE Start-ScriptTranscript -OutputDirectory 'C:\Logs\MyScript' -CleanupOld .EXAMPLE Start-ScriptTranscript -OutputDirectory 'C:\Logs\MyScript' -IncludeInvocationHeader .LINK https://kb.gioxx.org/Nebula/Automations/usage/start-scripttranscript #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$OutputDirectory, [switch]$CleanupOld, [string]$CleanupPattern = 'PowerShell*.txt', [switch]$IncludeInvocationHeader ) $result = [pscustomobject]@{ Success = $false TranscriptPath = $null Message = $null } try { if (-not (Test-Path -LiteralPath $OutputDirectory)) { $null = New-Item -ItemType Directory -Path $OutputDirectory -Force -ErrorAction Stop } if ($CleanupOld.IsPresent -and -not [string]::IsNullOrWhiteSpace($CleanupPattern)) { Get-ChildItem -Path $OutputDirectory -Filter $CleanupPattern -File -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue } $startParams = @{ OutputDirectory = $OutputDirectory } if ($IncludeInvocationHeader.IsPresent) { $startParams.IncludeInvocationHeader = $true } $transcript = Start-Transcript @startParams $result.TranscriptPath = $transcript.Path $result.Success = $true $result.Message = 'Transcript started successfully.' } catch { $result.Message = $_.Exception.Message } return $result } function Stop-ScriptTranscriptSafe { <# .SYNOPSIS Stops transcript safely without throwing if no transcript is active. .DESCRIPTION Attempts to stop transcript and returns a status object. .EXAMPLE Stop-ScriptTranscriptSafe .EXAMPLE $stop = Stop-ScriptTranscriptSafe Write-Output $stop.Message .LINK https://kb.gioxx.org/Nebula/Automations/usage/stop-scripttranscriptsafe #> [CmdletBinding()] param() $result = [pscustomobject]@{ Success = $false Message = $null } try { Stop-Transcript -ErrorAction Stop | Out-Null $result.Success = $true $result.Message = 'Transcript stopped successfully.' } catch { if ($_.Exception.Message -match 'transcription has not been started') { $result.Success = $true $result.Message = 'No active transcript. Nothing to stop.' } else { $result.Message = $_.Exception.Message } } return $result } |