From 0f2d4dbe624da080638507bcfd57726d80b21221 Mon Sep 17 00:00:00 2001 From: brodriguezmsft <103538983+brodriguezmsft@users.noreply.github.com> Date: Fri, 14 Oct 2022 15:38:47 -0400 Subject: [PATCH 1/6] Users/brod/zipoption (#177) * option to zip logs or leave unzipped * Added switch for -ExcludeLocaleMetadata * don't compact if zip files is false * cleaned up print statements * shorten msdbg. to msdbg * addressed comments * add try/catch statements * trim trailing whitespace * small style change in !zipfiles section; update CAU debug section for constrained language mode --- .../PrivateCloud.DiagnosticInfo.psm1 | 622 +++++++++++------- 1 file changed, 388 insertions(+), 234 deletions(-) diff --git a/PrivateCloud.DiagnosticInfo/PrivateCloud.DiagnosticInfo.psm1 b/PrivateCloud.DiagnosticInfo/PrivateCloud.DiagnosticInfo.psm1 index 3686ee6..ff247d8 100644 --- a/PrivateCloud.DiagnosticInfo/PrivateCloud.DiagnosticInfo.psm1 +++ b/PrivateCloud.DiagnosticInfo/PrivateCloud.DiagnosticInfo.psm1 @@ -40,7 +40,7 @@ $CommonFuncBlock = { Import-Module Storage # - # Shows error, cancels script + # Shows error # function Show-Error( [string] $Message, @@ -407,7 +407,7 @@ $CommonFuncBlock = { $tal = (Get-Date) - $tal # Emit results - [pscustomobject] @{ + [ordered] @{ EventFile = $EventFile LogName = $p.LogName RecordCount = $p.RecordCount @@ -642,7 +642,7 @@ $CommonFuncBlock = { # the gathering node should retrieve. Source paths must be UNC. NoCopy + Delete is used # to scrub away a capture directory. - [PSCustomObject] @{ + [Ordered] @{ Source = $Source NoCopy = $NoCopy Delete = $Delete @@ -819,37 +819,44 @@ function Start-CopyJob foreach ($childJob in $Job.ChildJobs) { - # Receive set of copy tasks from job - these are built by NewCopyTask - $copy = @(Receive-Job $childJob) - if ($copy.Count -eq 0) { continue } - - # create/use a specific job destination if present - # ex: "foo" -> \node_xxx\foo\ v. the default \node_xxx\ - $Destination = (Get-NodePath $Path $childJob.Location) - if (Get-Member -InputObject $_ -Name Destination) { - $Destination = Join-Path $Destination $childJob.Destination - if (-not (Test-Path $Destination)) { - $null = mkdir $Destination -Force -ErrorAction Continue + try + { + # Receive set of copy tasks from job - these are built by NewCopyTask + $copy = @(Receive-Job $childJob) + if ($copy.Count -eq 0) { continue } + + # create/use a specific job destination if present + # ex: "foo" -> \node_xxx\foo\ v. the default \node_xxx\ + $Destination = (Get-NodePath $Path $childJob.Location) + if (Get-Member -InputObject $_ -Name Destination) { + $Destination = Join-Path $Destination $childJob.Destination + if (-not (Test-Path $Destination)) { + $null = mkdir $Destination -Force -ErrorAction Continue + } } - } - $jobName = "Copy $($Job.Name) $($childjob.Location)" - start-job -Name $jobName -ArgumentList $copy,$Destination { + $jobName = "Copy $($Job.Name) $($childjob.Location)" + start-job -Name $jobName -ArgumentList $copy,$Destination { - param($copy,$Destination) + param($copy,$Destination) - $copy |% { + $copy |% { - # allow errors to propagte for triage - if (-not $_.NoCopy) - { - Copy-Item -Recurse $_.Source $Destination -Force -ErrorAction Continue - } - if ($_.Delete) { - Remove-Item -Recurse $_.Source -Force -ErrorAction Continue + # allow errors to propagte for triage + if (-not $_.NoCopy) + { + Copy-Item -Recurse $_.Source $Destination -Force -ErrorAction Continue + } + if ($_.Delete) { + Remove-Item -Recurse $_.Source -Force -ErrorAction Continue + } } } } + catch + { + Show-Warning("Exception in start-copyjob. `nError="+$_.Exception.Message) + } } } } @@ -1101,7 +1108,7 @@ function Get-NodeList( Path to read content from for summary health report generation. .PARAMETER TemporaryPath -Temporary path to stage capture content to, prior to ZIP creation. +Temporary path to stage capture content to, prior to ZIP creation. Only use if you want output to be zipped. .PARAMETER ClusterName Cluster to capture content from. @@ -1121,7 +1128,7 @@ Specify -1 to capture the complete archive - NOTE: this may be very large. Specify 0 to disable capture of the archive. .PARAMETER ZipPrefix -Path for the resulting ZIP file: --.ZIP will be appended. +Path for the resulting ZIP file: --.ZIP will be appended. Only use if you want output to be zipped. .PARAMETER MonitoringMode Run in a limited monitoring mode (deprecated) @@ -1201,6 +1208,15 @@ physical disks due to varying overhead in their internal handling of SMART queri SessionConfigurationName to connect to other nodes in cluster. Null if default configuration is to be used. +.PARAMETER DestPath +The destination path for output. Only use if you do not want output to be zipped. + +.PARAMETER ZipFiles +Determine if output should be zipped. If false, will extract internal .cab files, as well. + +.PARAMETER ExcludeLocaleMetadata +Determine if contents in "LocaleMetadata" should not be collected. + #> function Get-SddcDiagnosticInfo @@ -1355,7 +1371,19 @@ function Get-SddcDiagnosticInfo [parameter(ParameterSetName="WriteC", Mandatory=$false)] [parameter(ParameterSetName="WriteN", Mandatory=$false)] - [string] $SessionConfigurationName = $null + [string] $SessionConfigurationName = $null, + + [parameter(ParameterSetName="WriteC", Mandatory=$false)] + [parameter(ParameterSetName="WriteN", Mandatory=$false)] + [string] $DestPath = $($env:userprofile + "\HealthTest"), + + [parameter(ParameterSetName="WriteC", Mandatory=$false)] + [parameter(ParameterSetName="WriteN", Mandatory=$false)] + [bool] $ZipFiles = $true, + + [parameter(ParameterSetName="WriteC", Mandatory=$false)] + [parameter(ParameterSetName="WriteN", Mandatory=$false)] + [bool] $ExcludeLocaleMetadata = $false ) # @@ -1669,12 +1697,33 @@ function Get-SddcDiagnosticInfo # # Verify zip location # - if (-not (Test-PrefixFilePath ([ref] $ZipPrefix))) { Write-Error "$ZipPrefix is not a valid prefix for ZIP: $ZipPrefix.ZIP must be creatable" return } + if ($ZipFiles) + { + if ($PSBoundParameters.ContainsKey("DestPath")) + { + Write-Error "Can't use DestPath parameter if ZipFiles parameter is true" + return + } + } + else + { + if ($PSBoundParameters.ContainsKey("TemporaryPath")) + { + Write-Error "Can't use TemporaryPath parameter if ZipFiles parameter is false" + return + } + if ($PSBoundParameters.ContainsKey("ZipPrefix")) + { + Write-Error "Can't use ZipPrefix parameter if ZipFiles parameter is false" + return + } + } + # # Veriyfing path # @@ -1683,7 +1732,15 @@ function Get-SddcDiagnosticInfo $Path = $ReadFromPath $Read = $true } else { - $Path = $TemporaryPath + if ($ZipFiles) + { + $Path = $TemporaryPath + } + else + { + $Path = $DestPath + } + $Read = $false } @@ -1735,7 +1792,7 @@ function Get-SddcDiagnosticInfo try { - Show-Update "Temporary write path : $Path" + Show-Update "Write path : $Path" # # Handle parameters to archive/pass into the summary report generator. @@ -1968,8 +2025,8 @@ function Get-SddcDiagnosticInfo try { # SCDT returns a fileinfo object for the saved ZIP on the pipeline; discard (allow errors/warnings to flow as normal) - - if ((Get-Command Save-CauDebugTrace).Parameters.ContainsKey("FeatureUpdateLogs")) { + $parameters = (Get-Command Save-CauDebugTrace).Parameters.Keys + if ($parameters -contains "FeatureUpdateLogs") { $null = Save-CauDebugTrace -Cluster $using:AccessNode -FeatureUpdateLogs All -FilePath $using:Path } else { @@ -2014,18 +2071,25 @@ function Get-SddcDiagnosticInfo # however, dividing these into distinct jobs helps when triaging hangs or sources of error - its a tradeoff Show-Update "Start gather of verifier ..." - $JobGather += Invoke-CommonCommand -ClusterNodes $($ClusterNodes).Name -JobName Verifier -SessionConfigurationName $SessionConfigurationName -InitBlock $CommonFunc { # Verifier - $LocalFile = Join-Path $env:temp "verifier-query.txt" - verifier /query > $LocalFile - NewCopyTask -Delete (Get-AdminSharePathFromLocal $env:COMPUTERNAME $LocalFile) + try + { + $LocalFile = Join-Path $env:temp "verifier-query.txt" + verifier /query > $LocalFile + NewCopyTask -Delete (Get-AdminSharePathFromLocal $env:COMPUTERNAME $LocalFile) + + $LocalFile = Join-Path $env:temp "verifier-querysettings.txt" + verifier /querysettings > $LocalFile + NewCopyTask -Delete (Get-AdminSharePathFromLocal $env:COMPUTERNAME $LocalFile) + } + catch + { + Show-Warning("Exception in verifier script block. `nError="+$_.Exception.Message) + } - $LocalFile = Join-Path $env:temp "verifier-querysettings.txt" - verifier /querysettings > $LocalFile - NewCopyTask -Delete (Get-AdminSharePathFromLocal $env:COMPUTERNAME $LocalFile) } Show-Update "Start gather of filesystem filter status ..." @@ -2033,21 +2097,36 @@ function Get-SddcDiagnosticInfo $JobGather += Invoke-CommonCommand -ClusterNodes $($ClusterNodes).Name -JobName 'Filesystem Filter Manager' -SessionConfigurationName $SessionConfigurationName -InitBlock $CommonFunc { # Filter Manager + try + { + $LocalFile = Join-Path $env:temp "fltmc.txt" + fltmc > $LocalFile + NewCopyTask -Delete (Get-AdminSharePathFromLocal $env:COMPUTERNAME $LocalFile) - $LocalFile = Join-Path $env:temp "fltmc.txt" - fltmc > $LocalFile - NewCopyTask -Delete (Get-AdminSharePathFromLocal $env:COMPUTERNAME $LocalFile) + $LocalFile = Join-Path $env:temp "fltmc-instances.txt" + fltmc instances > $LocalFile + NewCopyTask -Delete (Get-AdminSharePathFromLocal $env:COMPUTERNAME $LocalFile) + } + catch + { + Show-Warning("Exception in filter manager script block. `nError="+$_.Exception.Message) + } - $LocalFile = Join-Path $env:temp "fltmc-instances.txt" - fltmc instances > $LocalFile - NewCopyTask -Delete (Get-AdminSharePathFromLocal $env:COMPUTERNAME $LocalFile) } $JobGather += Invoke-CommonCommand -ClusterNodes $($ClusterNodes).Name -JobName 'Copy WER ReportArchive' -SessionConfigurationName $SessionConfigurationName -InitBlock $CommonFunc { - # ReportArchive copy (one-shot, we'll recursively copy) + try + { + # ReportArchive copy (one-shot, we'll recursively copy) + + NewCopyTask -Delete:$false (Get-AdminSharePathFromLocal $env:COMPUTERNAME $env:ProgramData\Microsoft\Windows\WER\ReportArchive) + } + catch + { + Show-Warning("Exception in Copy WER ReportArchive script block. `nError="+$_.Exception.Message) + } - NewCopyTask -Delete:$false (Get-AdminSharePathFromLocal $env:COMPUTERNAME $env:ProgramData\Microsoft\Windows\WER\ReportArchive) } if ($IncludeDumps -eq $true) { @@ -2141,57 +2220,76 @@ function Get-SddcDiagnosticInfo if ($IncludeGetNetView) { Show-Update "Start gather of Get-NetView ..." + $GetNetViewArguments = @($SkipVM, $ZipFiles) - $JobGather += Invoke-CommonCommand -ArgumentList $SkipVm -ClusterNodes $($ClusterNodes).Name -JobName 'GetNetView' -SessionConfigurationName $SessionConfigurationName -InitBlock $CommonFunc { - - Param($SkipVM) + $JobGather += Invoke-CommonCommand -ArgumentList $GetNetViewArguments -ClusterNodes $($ClusterNodes).Name -JobName 'GetNetView' -SessionConfigurationName $SessionConfigurationName -InitBlock $CommonFunc { - $NodePath = $env:Temp + Param($SkipVM,$ZipFiles) + try + { + $NodePath = $env:Temp - # create a directory to capture GNV + # create a directory to capture GNV - $gnvDir = Join-Path $NodePath 'GetNetView' - Remove-Item -Recurse -Force $gnvDir -ErrorAction SilentlyContinue - $null = md $gnvDir -Force -ErrorAction SilentlyContinue + $gnvDir = Join-Path $NodePath 'GetNetView' + Remove-Item -Recurse -Force $gnvDir -ErrorAction SilentlyContinue + $null = md $gnvDir -Force -ErrorAction SilentlyContinue - # run inside a child session so we can sink output to the transcript - # we must pass the GNV dir since $using is statically evaluated in the - # outermost scope and $gnvDir is inside the Invoke call. + # run inside a child session so we can sink output to the transcript + # we must pass the GNV dir since $using is statically evaluated in the + # outermost scope and $gnvDir is inside the Invoke call. - $j = Start-Job -ArgumentList $gnvDir,$SkipVM { + $j = Start-Job -ArgumentList $gnvDir,$SkipVM { - param($gnvDir,$SkipVM) + param($gnvDir,$SkipVM) - # start gather transcript to the GNV directory + # start gather transcript to the GNV directory - $transcriptFile = Join-Path $gnvDir "0_GetNetViewGatherTranscript.log" - Start-Transcript -Path $transcriptFile -Force + $transcriptFile = Join-Path $gnvDir "0_GetNetViewGatherTranscript.log" + Start-Transcript -Path $transcriptFile -Force - if (Get-Command Get-NetView -ErrorAction SilentlyContinue) { - if ($SkipVM) { - Get-NetView -OutputDirectory $gnvDir -SkipLogs -SkipVM + if (Get-Command Get-NetView -ErrorAction SilentlyContinue) { + if ($SkipVM) { + Get-NetView -OutputDirectory $gnvDir -SkipLogs -SkipVM + } else { + Get-NetView -OutputDirectory $gnvDir -SkipLogs + } } else { - Get-NetView -OutputDirectory $gnvDir -SkipLogs + Write-Host "Get-NetView command not available" } - } else { - Write-Host "Get-NetView command not available" + + Stop-Transcript } - Stop-Transcript - } + # do not receive job - sunk to transcript for offline analysis + # gnv produces a very large quantity of host output + $null = $j | Wait-Job + $j | Remove-Job - # do not receive job - sunk to transcript for offline analysis - # gnv produces a very large quantity of host output - $null = $j | Wait-Job - $j | Remove-Job - # wipe all non-file content (gnv produces zip + uncompressed dir, don't need the dir) - dir $gnvDir -Directory |% { - Remove-Item -Recurse -Force $_.FullName + # If chose to zip files, wipe all non-file content (gnv produces zip + uncompressed dir, don't need the dir) + if ($ZipFiles) + { + Write-Host "User selected to zip output. Remove uncompressed directory msdbg" + dir $gnvDir -Directory |% { + Remove-Item -Recurse -Force $_.FullName + } + } + else + { + # if not zipping content, keep the uncompressed dir to copy later, and remove the zipped directory + Write-Host "User selected not to zip output. Remove compressed directory msdbg*.zip" + Get-ChildItem -Path $gnvDir -Filter 'msdbg*.zip' | Remove-Item + } + + # gather all remaining content (will be the zip + transcript) in GNV directory + NewCopyTask -Delete (Get-AdminSharePathFromLocal $env:COMPUTERNAME $gnvDir) + } + catch + { + Show-Warning("Exception in GetNetView script block. `nError="+$_.Exception.Message) } - # gather all remaining content (will be the zip + transcript) in GNV directory - NewCopyTask -Delete (Get-AdminSharePathFromLocal $env:COMPUTERNAME $gnvDir) } } @@ -2200,7 +2298,8 @@ function Get-SddcDiagnosticInfo $JobStatic += Start-Job -Name ClusterLogs { $null = Get-ClusterLog -Node $using:ClusterNodes.Name -Destination $using:Path -UseLocalTime - if ((Get-Command Get-ClusterLog).Parameters.ContainsKey("NetFt")) + $parameters = (Get-Command Get-ClusterLog).Parameters.Keys + if ($parameters -contains "NetFt") { $null = Get-ClusterLog -Node $using:ClusterNodes.Name -Destination $using:Path -UseLocalTime -Netft } @@ -2217,18 +2316,19 @@ function Get-SddcDiagnosticInfo $NodeName = $_ Invoke-CommonCommand -JobName "System Info: $NodeName" -InitBlock $CommonFunc -SessionConfigurationName $SessionConfigurationName -ScriptBlock { + try + { + $Node = "$using:NodeName" + if ($using:ClusterDomain.Length) { + $Node += ".$using:ClusterDomain" + } - $Node = "$using:NodeName" - if ($using:ClusterDomain.Length) { - $Node += ".$using:ClusterDomain" - } + $LocalNodeDir = Get-NodePath $using:Path $using:NodeName - $LocalNodeDir = Get-NodePath $using:Path $using:NodeName - - # Text-only conventional commands - # - # Gather SYSTEMINFO.EXE output for a given node - SystemInfo.exe /S $using:NodeName > (Join-Path (Get-NodePath $using:Path $using:NodeName) "SystemInfo.TXT") + # Text-only conventional commands + # + # Gather SYSTEMINFO.EXE output for a given node + SystemInfo.exe /S $using:NodeName > (Join-Path (Get-NodePath $using:Path $using:NodeName) "SystemInfo.TXT") # Cmdlets to drop in TXT and XML forms # @@ -2280,115 +2380,120 @@ function Get-SddcDiagnosticInfo @{ C = 'Get-StorageFaultDomain -CimSession _A_ -Type StorageScaleUnit |? FriendlyName -eq _N_ | Get-StorageFaultDomain -CimSession _A_'; F = $null }, @{ C = 'Get-WindowsFeature -ComputerName _C_'; F = $null } - # These commands are specific to optional modules, add only if present - # - DcbQos: RoCE environments primarily - # - Hyper-V: may be ommitted in SOFS-only cases - if (Get-Module DcbQos -ErrorAction SilentlyContinue) { - $CmdsToLog += - @{ C = 'Get-NetQosDcbxSetting -CimSession _C_'; F = $null }, - @{ C = 'Get-NetQosFlowControl -CimSession _C_'; F = $null }, - @{ C = 'Get-NetQosTrafficClass -CimSession _C_'; F = $null } - } + # These commands are specific to optional modules, add only if present + # - DcbQos: RoCE environments primarily + # - Hyper-V: may be ommitted in SOFS-only cases + if (Get-Module DcbQos -ErrorAction SilentlyContinue) { + $CmdsToLog += + @{ C = 'Get-NetQosDcbxSetting -CimSession _C_'; F = $null }, + @{ C = 'Get-NetQosFlowControl -CimSession _C_'; F = $null }, + @{ C = 'Get-NetQosTrafficClass -CimSession _C_'; F = $null } + } - if (Get-Module Hyper-V -ErrorAction SilentlyContinue) { - $CmdsToLog += - @{ C = 'Get-VM -CimSession _C_ -ErrorAction SilentlyContinue'; F = $null }, - @{ C = 'Get-VMNetworkAdapter -All -CimSession _C_ -ErrorAction SilentlyContinue'; F = $null }, - @{ C = 'Get-VMSwitch -CimSession _C_ -ErrorAction SilentlyContinue'; F = $null } - } + if (Get-Module Hyper-V -ErrorAction SilentlyContinue) { + $CmdsToLog += + @{ C = 'Get-VM -CimSession _C_ -ErrorAction SilentlyContinue'; F = $null }, + @{ C = 'Get-VMNetworkAdapter -All -CimSession _C_ -ErrorAction SilentlyContinue'; F = $null }, + @{ C = 'Get-VMSwitch -CimSession _C_ -ErrorAction SilentlyContinue'; F = $null } + } - foreach ($cmd in $CmdsToLog) { + foreach ($cmd in $CmdsToLog) { - $cmdstr = $cmd.C - $file = $cmd.F + $cmdstr = $cmd.C + $file = $cmd.F - # Default rule: base cmdlet name no dash - if ($null -eq $file) { - $LocalFile = (Join-Path $LocalNodeDir (($cmdstr.split(' '))[0] -replace "-","")) - } else { - $LocalFile = (Join-Path $LocalNodeDir $file) - } + # Default rule: base cmdlet name no dash + if ($null -eq $file) { + $LocalFile = (Join-Path $LocalNodeDir (($cmdstr.split(' '))[0] -replace "-","")) + } else { + $LocalFile = (Join-Path $LocalNodeDir $file) + } - try { + try { - $cmdex = $cmdstr -replace '_C_',$using:NodeName -replace '_N_',$using:NodeName -replace '_A_',$using:AccessNode - $out = iex $cmdex + $cmdex = $cmdstr -replace '_C_',$using:NodeName -replace '_N_',$using:NodeName -replace '_A_',$using:AccessNode + $out = iex $cmdex - # capture as txt and xml for quick analysis according to taste - $out | ft -AutoSize | Out-File -Width 9999 -Encoding ascii -FilePath "$LocalFile.txt" - $out | Export-Clixml -Path "$LocalFile.xml" + # capture as txt and xml for quick analysis according to taste + $out | ft -AutoSize | Out-File -Width 9999 -Encoding ascii -FilePath "$LocalFile.txt" + $out | Export-Clixml -Path "$LocalFile.xml" - } catch { - Show-Warning "'$cmdex' failed for node $Node ($($_.Exception.Message))" + } catch { + Show-Warning "'$cmdex' failed for node $Node ($($_.Exception.Message))" + } } - } - $NodeSystemRootPath = Invoke-Command -ComputerName $using:NodeName -ConfigurationName $using:SessionConfigurationName { $env:SystemRoot } + $NodeSystemRootPath = Invoke-Command -ComputerName $using:NodeName -ConfigurationName $using:SessionConfigurationName { $env:SystemRoot } - # Avoid to use 'Join-Path' because the drive of path may not exist on the local machine. - if ($using:IncludeDumps -eq $true) { + # Avoid to use 'Join-Path' because the drive of path may not exist on the local machine. + if ($using:IncludeDumps -eq $true) { - $NodeMinidumpsPath = Invoke-Command -ComputerName $using:NodeName -ConfigurationName $using:SessionConfigurationName { (Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\CrashControl').MinidumpDir } -ErrorAction SilentlyContinue - $NodeLiveKernelReportsPath = Invoke-Command -ComputerName $using:NodeName -ConfigurationName $using:SessionConfigurationName { (Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\CrashControl\LiveKernelReports').LiveKernelReportsPath } -ErrorAction SilentlyContinue - ## - # Minidumps - ## + $NodeMinidumpsPath = Invoke-Command -ComputerName $using:NodeName -ConfigurationName $using:SessionConfigurationName { (Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\CrashControl').MinidumpDir } -ErrorAction SilentlyContinue + $NodeLiveKernelReportsPath = Invoke-Command -ComputerName $using:NodeName -ConfigurationName $using:SessionConfigurationName { (Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\CrashControl\LiveKernelReports').LiveKernelReportsPath } -ErrorAction SilentlyContinue + ## + # Minidumps + ## - try { - # Use the registry key value if it exists. - if ($NodeMinidumpsPath) { - $RPath = (Get-AdminSharePathFromLocal $using:NodeName "$NodeMinidumpsPath\*.dmp") - } - else { - $RPath = (Get-AdminSharePathFromLocal $using:NodeName "$NodeSystemRootPath\Minidump\*.dmp") + try { + # Use the registry key value if it exists. + if ($NodeMinidumpsPath) { + $RPath = (Get-AdminSharePathFromLocal $using:NodeName "$NodeMinidumpsPath\*.dmp") + } + else { + $RPath = (Get-AdminSharePathFromLocal $using:NodeName "$NodeSystemRootPath\Minidump\*.dmp") + } + + $DmpFiles = Get-ChildItem -Path $RPath -Recurse -ErrorAction SilentlyContinue } + catch { $DmpFiles = ""; Show-Warning "Unable to get minidump files for node $using:NodeName" } - $DmpFiles = Get-ChildItem -Path $RPath -Recurse -ErrorAction SilentlyContinue - } - catch { $DmpFiles = ""; Show-Warning "Unable to get minidump files for node $using:NodeName" } + $DmpFiles |% { + try { Copy-Item $_.FullName $LocalNodeDir } + catch { Show-Warning("Could not copy minidump file $_.FullName") } + } - $DmpFiles |% { - try { Copy-Item $_.FullName $LocalNodeDir } - catch { Show-Warning("Could not copy minidump file $_.FullName") } - } + ## + # Live Kernel Reports + ## - ## - # Live Kernel Reports - ## + try { + # Use the registry key value if it exists. + if ($NodeLiveKernelReportsPath) { + $RPath = (Get-AdminSharePathFromLocal $using:NodeName "$NodeLiveKernelReportsPath\*.dmp") + } + else { + $RPath = (Get-AdminSharePathFromLocal $using:NodeName "$NodeSystemRootPath\LiveKernelReports\*.dmp") + } - try { - # Use the registry key value if it exists. - if ($NodeLiveKernelReportsPath) { - $RPath = (Get-AdminSharePathFromLocal $using:NodeName "$NodeLiveKernelReportsPath\*.dmp") - } - else { - $RPath = (Get-AdminSharePathFromLocal $using:NodeName "$NodeSystemRootPath\LiveKernelReports\*.dmp") + $DmpFiles = Get-ChildItem -Path $RPath -Recurse -ErrorAction SilentlyContinue } + catch { $DmpFiles = ""; Show-Warning "Unable to get LiveKernelReports files for node $using:NodeName" } - $DmpFiles = Get-ChildItem -Path $RPath -Recurse -ErrorAction SilentlyContinue + $DmpFiles |% { + try { Copy-Item $_.FullName $LocalNodeDir } + catch { Show-Warning "Could not copy LiveKernelReports file $($_.FullName)" } + } } - catch { $DmpFiles = ""; Show-Warning "Unable to get LiveKernelReports files for node $using:NodeName" } - $DmpFiles |% { - try { Copy-Item $_.FullName $LocalNodeDir } - catch { Show-Warning "Could not copy LiveKernelReports file $($_.FullName)" } + try { + $RPath = (Get-AdminSharePathFromLocal $using:NodeName "$NodeSystemRootPath\Cluster\Reports\*.*") + $RepFiles = Get-ChildItem -Path $RPath -Recurse -ErrorAction SilentlyContinue } + catch { $RepFiles = ""; Show-Warning "Unable to get reports for node $using:NodeName" } + + $LocalReportDir = Join-Path $LocalNodeDir "ClusterReports" + md $LocalReportDir | Out-Null + + # Copy logs from the Report directory; exclude cluster/health logs which we're getting seperately + $RepFiles |% { + if (($_.Name -notlike "Cluster.log") -and ($_.Name -notlike "ClusterHealth.log")) { + try { Copy-Item $_.FullName $LocalReportDir } + catch { Show-Warning "Could not copy report file $($_.FullName)" } + } } } - - try { - $RPath = (Get-AdminSharePathFromLocal $using:NodeName "$NodeSystemRootPath\Cluster\Reports\*.*") - $RepFiles = Get-ChildItem -Path $RPath -Recurse -ErrorAction SilentlyContinue } - catch { $RepFiles = ""; Show-Warning "Unable to get reports for node $using:NodeName" } - - $LocalReportDir = Join-Path $LocalNodeDir "ClusterReports" - md $LocalReportDir | Out-Null - - # Copy logs from the Report directory; exclude cluster/health logs which we're getting seperately - $RepFiles |% { - if (($_.Name -notlike "Cluster.log") -and ($_.Name -notlike "ClusterHealth.log")) { - try { Copy-Item $_.FullName $LocalReportDir } - catch { Show-Warning "Could not copy report file $($_.FullName)" } - } + catch + { + Show-Warning("Exception in System Info: NodeName $node `nError="+$_.Exception.Message) } } } @@ -2398,54 +2503,74 @@ function Get-SddcDiagnosticInfo $JobGather += Invoke-CommonCommand -ArgumentList $IncludeLiveDump,$IncludeStorDiag -ClusterNodes $AccessNode -SessionConfigurationName $SessionConfigurationName -InitBlock $CommonFunc -JobName StorageDiagnosticInfoAndLiveDump { Param($IncludeLiveDump,$IncludeStorDiag) + try + { + $Node = $env:COMPUTERNAME + $NodePath = $env:Temp - $Node = $env:COMPUTERNAME - $NodePath = $env:Temp + $destinationPath = Join-Path -Path $NodePath -ChildPath 'StorageDiagnosticDump' - $destinationPath = Join-Path -Path $NodePath -ChildPath 'StorageDiagnosticDump' + if (Test-Path -Path $destinationPath) { + Remove-Item -Path $destinationPath -Recurse -Force + } - if (Test-Path -Path $destinationPath) { - Remove-Item -Path $destinationPath -Recurse -Force - } + $clusterSubsystem = (Get-StorageSubSystem |? Model -eq 'Clustered Windows Storage').FriendlyName - $clusterSubsystem = (Get-StorageSubSystem |? Model -eq 'Clustered Windows Storage').FriendlyName + if ($IncludeLiveDump) { + Get-StorageDiagnosticInfo -StorageSubSystemFriendlyName $clusterSubsystem -IncludeLiveDump -DestinationPath $destinationPath - if ($IncludeLiveDump) { - Get-StorageDiagnosticInfo -StorageSubSystemFriendlyName $clusterSubsystem -IncludeLiveDump -DestinationPath $destinationPath + # Copy storage diagnostic and live dump information (one-shot, we'll recursively copy) + NewCopyTask -Delete (Get-AdminSharePathFromLocal $Node $destinationPath) + } + elseif ($IncludeStorDiag) { + Get-StorageDiagnosticInfo -StorageSubSystemFriendlyName $clusterSubsystem -DestinationPath $destinationPath - # Copy storage diagnostic and live dump information (one-shot, we'll recursively copy) - NewCopyTask -Delete (Get-AdminSharePathFromLocal $Node $destinationPath) + # Copy storage diagnostic and live dump information (one-shot, we'll recursively copy) + NewCopyTask -Delete (Get-AdminSharePathFromLocal $Node $destinationPath) + } } - elseif ($IncludeStorDiag) { - Get-StorageDiagnosticInfo -StorageSubSystemFriendlyName $clusterSubsystem -DestinationPath $destinationPath - - # Copy storage diagnostic and live dump information (one-shot, we'll recursively copy) - NewCopyTask -Delete (Get-AdminSharePathFromLocal $Node $destinationPath) + catch + { + Show-Warning("Exception in StorageDiagnosticInfoAndLiveDump `nError="+$_.Exception.Message) } } Show-Update "Starting export of events ..." + $EventsArguments = @($HoursOfEvents, $ExcludeLocaleMetadata) + $JobGather += Invoke-CommonCommand -ArgumentList $EventsArguments -ClusterNodes $($ClusterNodes).Name -SessionConfigurationName $SessionConfigurationName -InitBlock $CommonFunc -JobName Events { - $JobGather += Invoke-CommonCommand -ArgumentList $HoursOfEvents -ClusterNodes $($ClusterNodes).Name -SessionConfigurationName $SessionConfigurationName -InitBlock $CommonFunc -JobName Events { + Param([int] $Hours,[bool] $ExcludeLocaleMetadata) + try + { + $Node = $env:COMPUTERNAME - Param([int] $Hours) + # use temporary directory with compression on to minimize capture footprint + $NodePath = New-TemporaryFile + Remove-Item $NodePath + $null = New-Item -ItemType Directory $NodePath + $null = compact /c $NodePath - $Node = $env:COMPUTERNAME + # Flatten the captured events + local metadata into the per-node directory on the gatherer + Get-SddcCapturedEvents $NodePath $Hours |% { + NewCopyTask -Delete (Get-AdminSharePathFromLocal $Node $_) + } - # use temporary directory with compression on to minimize capture footprint - $NodePath = New-TemporaryFile - Remove-Item $NodePath - $null = New-Item -ItemType Directory $NodePath - $null = compact /c $NodePath + if ($ExcludeLocaleMetadata) + { + NewCopyTask -Delete (Get-AdminSharePathFromLocal $Node (Join-Path $NodePath "LocaleMetaData")) -NoCopy + } + else + { + NewCopyTask -Delete (Get-AdminSharePathFromLocal $Node (Join-Path $NodePath "LocaleMetaData")) + } - # Flatten the captured events + local metadata into the per-node directory on the gatherer - Get-SddcCapturedEvents $NodePath $Hours |% { - NewCopyTask -Delete (Get-AdminSharePathFromLocal $Node $_) + # And remove the capture directory at the end of copy + NewCopyTask -Delete (Get-AdminSharePathFromLocal $Node $NodePath) -NoCopy + } + catch + { + Show-Warning("Exception in JobName Events `nError="+$_.Exception.Message) } - NewCopyTask -Delete (Get-AdminSharePathFromLocal $Node (Join-Path $NodePath "LocaleMetaData")) - - # And remove the capture directory at the end of copy - NewCopyTask -Delete (Get-AdminSharePathFromLocal $Node $NodePath) -NoCopy } if ($IncludeAssociations -and $ClusterName.Length) { @@ -2643,7 +2768,6 @@ function Get-SddcDiagnosticInfo $Volumes | Export-Clixml ($Path + "GetVolume.XML") } catch { Show-Error("Unable to get Volumes. `nError="+$_.Exception.Message) } - # Virtual disk health # Used in S2D-specific gather below @@ -2685,7 +2809,6 @@ function Get-SddcDiagnosticInfo catch { Show-Warning("Unable to get Storage Tiers. `nError="+$_.Exception.Message) } # Storage pool health - try { $StoragePools = @(Get-StoragePool -IsPrimordial $False -CimSession $AccessNode -StorageSubSystem $Subsystem -ErrorAction SilentlyContinue) $StoragePools | Export-Clixml ($Path + "GetStoragePool.XML") } @@ -3146,7 +3269,31 @@ function Get-SddcDiagnosticInfo # Phase 4 # - Show-Update "<<< Phase 4 - Compacting files for transport >>>" -ForegroundColor Cyan + if ($ZipFiles) + { + Show-Update "<<< Phase 4 - Compacting files for transport >>>" -ForegroundColor Cyan + } + else + { + Show-Update "<<< Phase 4 - Extract cab files + Final Cleanup >>>" -ForegroundColor Cyan + } + + if (!$ZipFiles) + { + Show-Update "Rename msdbg. to msdbg. Do this to shorten overall filepath." + $items = Get-ChildItem -Recurse -Path $Path -Filter "msdbg.*" + foreach ($item in $items) + { + if ($item.FullName -Match "msdbg(.*)") + { + $childFolder = Split-Path $item.FullName -Leaf + $parentFolder = Split-Path $item.FullName -Parent + $childFolder = $childFolder -Replace $matches[1], "" + $renamedFolder = Join-Path -Path $parentFolder -ChildPath $childFolder + Rename-Item $item.FullName $renamedFolder + } + } + } # # Force GC so that any pending file references are @@ -3156,32 +3303,35 @@ function Get-SddcDiagnosticInfo [System.GC]::Collect() - # time/extension suffix - $ZipSuffix = '-' + (Format-SddcDateTime $TodayDate) + '.ZIP' - - # prepend clustername if live, domain name trimmed away - # we could use $Cluster.Name since it will exist if $ClusterName was created from it, - # but that may seem excessively mysterious) - if ($ClusterName.Length) { - $ZipSuffix = '-' + ($ClusterName.Split('.',2)[0]) + $ZipSuffix - } else { - $ZipSuffix = '-OFFLINECLUSTER' + $ZipSuffix - } + if ($ZipFiles) + { + # time/extension suffix + $ZipSuffix = '-' + (Format-SddcDateTime $TodayDate) + '.ZIP' + + # prepend clustername if live, domain name trimmed away + # we could use $Cluster.Name since it will exist if $ClusterName was created from it, + # but that may seem excessively mysterious) + if ($ClusterName.Length) { + $ZipSuffix = '-' + ($ClusterName.Split('.',2)[0]) + $ZipSuffix + } else { + $ZipSuffix = '-OFFLINECLUSTER' + $ZipSuffix + } - # ... and full path - $ZipPath = $ZipPrefix + $ZipSuffix + # ... and full path + $ZipPath = $ZipPrefix + $ZipSuffix - try { - Add-Type -Assembly System.IO.Compression.FileSystem - [System.IO.Compression.ZipFile]::CreateFromDirectory($Path, $ZipPath, [System.IO.Compression.CompressionLevel]::Optimal, $false) - $ZipPath = Convert-Path $ZipPath - Show-Update "Zip File Name : $ZipPath" + try { + Add-Type -Assembly System.IO.Compression.FileSystem + [System.IO.Compression.ZipFile]::CreateFromDirectory($Path, $ZipPath, [System.IO.Compression.CompressionLevel]::Optimal, $false) + $ZipPath = Convert-Path $ZipPath + Show-Update "Zip File Name : $ZipPath" - Show-Update "Cleaning up temporary directory $Path" - Remove-Item -Path $Path -ErrorAction SilentlyContinue -Recurse + Show-Update "Cleaning up temporary directory $Path" + Remove-Item -Path $Path -ErrorAction SilentlyContinue -Recurse - } catch { - Show-Error("Error creating the ZIP file!`nContent remains available at $Path") + } catch { + Show-Error("Error creating the ZIP file!`nContent remains available at $Path") + } } Show-Update "Cleaning up CimSessions" @@ -5297,6 +5447,10 @@ function Get-StorageLatencyReport } } } + catch + { + Show-Warning("Exception in get-stroage latency report . `nError="+$_.Exception.Message) + } finally { # And remove sessions from jobs From f723bde7c1d07f961082701c4fe6dd6d4f4a44e1 Mon Sep 17 00:00:00 2001 From: Dan Lovinger Date: Wed, 16 Nov 2022 10:15:31 -0800 Subject: [PATCH 2/6] remove [ref] usage from test-prefixfilepath add pester test for t-pfp and regular full-language invocation --- .../PrivateCloud.DiagnosticInfo.Tests.ps1 | 244 ++++++++++++++++++ .../PrivateCloud.DiagnosticInfo.psm1 | 41 ++- 2 files changed, 261 insertions(+), 24 deletions(-) create mode 100644 PrivateCloud.DiagnosticInfo/PrivateCloud.DiagnosticInfo.Tests.ps1 diff --git a/PrivateCloud.DiagnosticInfo/PrivateCloud.DiagnosticInfo.Tests.ps1 b/PrivateCloud.DiagnosticInfo/PrivateCloud.DiagnosticInfo.Tests.ps1 new file mode 100644 index 0000000..92253d8 --- /dev/null +++ b/PrivateCloud.DiagnosticInfo/PrivateCloud.DiagnosticInfo.Tests.ps1 @@ -0,0 +1,244 @@ +# +# +# + +Set-StrictMode -Version 3.0 + +$zpfx = Join-Path $env:TEMP "PesterTest" + +function RunGetSddcDiagnosticInfo +{ + param( + [System.Management.Automation.Runspaces.PSSession] + $Session, + + [switch] + $ShouldBeSuccessful + ) + + { Invoke-Command -Session $Session -ErrorVariable script:e { Get-SddcDiagnosticInfo -ZipPrefix $using:zpfx }} | Should Not Throw + + if ($ShouldBeSuccessful) + { + $script:e | Should BeNullOrEmpty Becaue "the invocation should be successful without errors" + } + else + { + $script:e | Should Not BeNullOrEmpty Becaue "the invocation should be unsuccessful" + } +} + +function CleanupSddcDiagnosticInfo +{ + $z = Get-Item "$zpfx*" + + # $z | Should Not BeNullOrEmpty + $z | Remove-Item +} + +<# +# +# Preserving rough start at a constrained language mode test. This is not correct as written since clm +# requires using Device Guard/AppLocker to express constrained language exceptions for signed modules +# (not limited to but inclusive of system modules) and/or modules located under secured paths. However, +# it may still lay out useful bones of what it will eventually look like. +# + +Describe "VerifyConstrainedLanguageMode" { + + BeforeAll { + $session = New-PSSession -EnableNetworkAccess + $script:e = $null + } + + It "ParentFullBefore" { + $ExecutionContext.SessionState.LanguageMode | Should Be ([System.Management.Automation.PSLanguageMode]::FullLanguage) + } + + It "IsFull" { + $mode = Invoke-Command -Session $session { $ExecutionContext.SessionState.LanguageMode } + $mode | Should Be ([System.Management.Automation.PSLanguageMode]::FullLanguage) + } + + It "SetConstrained" { + Invoke-Command -Session $session { $ExecutionContext.SessionState.LanguageMode = 'ConstrainedLanguage' } + $mode = Invoke-Command -Session $session { $ExecutionContext.SessionState.LanguageMode } + $mode | Should Be ([System.Management.Automation.PSLanguageMode]::ConstrainedLanguage) + } + + # + # do it + # + + RunGetSddcDiagnosticInfo $session -ShouldBeSuccessful:$true + CleanupSddcDiagnosticInfo + + It "RemainedConstrained" { + $mode = Invoke-Command -Session $session { $ExecutionContext.SessionState.LanguageMode } + $mode | Should Be ([System.Management.Automation.PSLanguageMode]::ConstrainedLanguage) + } + + It "ParentFullAfter" { + $ExecutionContext.SessionState.LanguageMode | Should Be ([System.Management.Automation.PSLanguageMode]::FullLanguage) + } + + AfterAll { + Remove-PSSession $session + } +} +#> + +Describe "VerifyFullLanguageMode" { + + BeforeAll { + $session = New-PSSession -EnableNetworkAccess + $script:e = $null + } + + It "ParentFullBefore" { + $ExecutionContext.SessionState.LanguageMode | Should Be ([System.Management.Automation.PSLanguageMode]::FullLanguage) + } + + It "IsFull" { + $mode = Invoke-Command -Session $session { $ExecutionContext.SessionState.LanguageMode } + $mode | Should Be ([System.Management.Automation.PSLanguageMode]::FullLanguage) + } + + # + # do it + # + + RunGetSddcDiagnosticInfo $session -ShouldBeSuccessful:$true + CleanupSddcDiagnosticInfo + + It "RemainedFull" { + $mode = Invoke-Command -Session $session { $ExecutionContext.SessionState.LanguageMode } + $mode | Should Be ([System.Management.Automation.PSLanguageMode]::FullLanguage) + } + + It "ParentFullAfter" { + $ExecutionContext.SessionState.LanguageMode | Should Be ([System.Management.Automation.PSLanguageMode]::FullLanguage) + } + + AfterAll { + Remove-PSSession $session + } +} + +Describe "TestPrefixFilePath" { + + It "SystemDriveIsDriveLetterColon" { + $env:SystemDrive.Length | Should Be 2 + ($env:SystemDrive[0] -match '[A-Z]') | Should Be $true + $env:SystemDrive[1] | Should Be ':' + } + + InModuleScope PrivateCloud.DiagnosticInfo { + + Context "InScope" { + + BeforeAll { + + # Get the first lexical existing name in the directory and duplicate the first + # character, which guarantees creating a non-existent name. If none exists + # use a trivial name. + function MakeAvailableName + { + param( + [string] + $Path + ) + + $firstName = Get-ChildItem $Path | Select-Object -First 1 + + if ($null -ne $firstName) + { + return $firstName.Name[0] + $firstName.Name + } + else + { + return 'a' + } + + } + + # An available name at the root of the system drive + $sysdAvailableName = MakeAvailableName (Join-Path $env:SystemDrive '') + + # An available name in the Windows directory + $windAvailableName = MakeAvailableName $env:windir + + # An available name in the current directory + $cwdAvailableName = MakeAvailableName '.' + + # Simplify later expressions building UNC paths + $cName = $env:COMPUTERNAME + $sysDL = $env:SystemDrive[0] + } + + It "NoDriveLetter" { + Test-PrefixFilePath $env:SystemDrive | Should BeNullOrEmpty Because "this is a bare drive letter with no possible file prefix" + } + + It "NoDriveLetterSlash" { + Test-PrefixFilePath (Join-Path $env:SystemDrive '') | Should BeNullOrEmpty Because "this is a trailing seperator with no possible file prefix" + } + + # at root of drive + # c:\somenewname -> yes + # c:\somenewname\test -> no + It "YesAvailableNameAtRoot" { + Test-PrefixFilePath (Join-Path $env:SystemDrive $sysdAvailableName) | Should Not BeNullOrEmpty Because "this is an available name in the system drive and should work" + } + + It "NoChildOfAvailableNameAtRoot" { + Test-PrefixFilePath (Join-Path (Join-Path $env:SystemDrive $sysdAvailableName) 'test') | Should BeNullOrEmpty Because "this is an available name, and cannot be a directory to put test into" + } + + It "NoAvailableNameAtRootAsDirectory" { + Test-PrefixFilePath (Join-Path (Join-Path $env:SystemDrive $sysdAvailableName) '') | Should BeNullOrEmpty Because "this is an available name as a directory with no possible file prefix" + } + + + # in child directory, using windir as a convenient must-exist directory + # c:\dir\somenewname -> yes + # c:\dir\somenewname\test -> no + It "YesAvailableNameInChild" { + Test-PrefixFilePath (Join-Path $env:windir $windAvailableName) | Should Not BeNullOrEmpty Because "this is an available name in the windows directory and should work" + } + + It "NoChildOfAvailableNameInChild" { + Test-PrefixFilePath (Join-Path (Join-Path $env:windir $windAvailableName) 'test') | Should BeNullOrEmpty Because "this is an available name, and cannot be a directory to put test into" + } + + It "NoAvailableNameInChildAsDirectory" { + Test-PrefixFilePath (Join-Path (Join-Path $env:windir $windAvailableName) '') | Should BeNullOrEmpty Because "this is an available name as a directory with no possible file prefix" + } + + # unc cases + # \\foo -> no + # \\foo\bar -> no + # \\foo\bar\somenewname -> yes + It "NoUNCComputerName" { + Test-PrefixFilePath "\\$cName" | Should BeNullOrEmpty Because "this is just a computer name, not a potential filename prefix" + } + + It "NoUNCShare" { + Test-PrefixFilePath "\\$cName\$sysDL$" | Should BeNullOrEmpty Because "this is just a share, not a potential filename prefix" + } + + It "YesUNCShareWithName" { + Test-PrefixFilePath "\\$cName\$sysDL$\$sysdAvailableName" | Should Not BeNullOrEmpty Because "this is just a share, not a potential filename prefix" + } + + # relative cases using current working directory + It "YesCWDAvailableName" { + Test-PrefixFilePath "$cwdAvailableName" | Should Not BeNullOrEmpty Because "this is an available name in the cwd" + } + + It "NoChildOfCWDAvailableName" { + Test-PrefixFilePath (Join-Path $cwdAvailableName "test") | Should BeNullOrEmpty Because "this is an available name, and cannot be a directory to put test into" + } + } + } +} \ No newline at end of file diff --git a/PrivateCloud.DiagnosticInfo/PrivateCloud.DiagnosticInfo.psm1 b/PrivateCloud.DiagnosticInfo/PrivateCloud.DiagnosticInfo.psm1 index ff247d8..8bb5a12 100644 --- a/PrivateCloud.DiagnosticInfo/PrivateCloud.DiagnosticInfo.psm1 +++ b/PrivateCloud.DiagnosticInfo/PrivateCloud.DiagnosticInfo.psm1 @@ -672,16 +672,17 @@ $CommonFunc = [scriptblock]::Create($( # # This tests whether a path is a valid prefix name for a new file (e.g., $path + .ZIP) -# A ref is provided so it can be rewritten to an absolute path if specified in drive or -# directory relative forms - .NET callouts have cwd = WINDIR, which confuses things v. -# normal expectations -# +# The path is returned if valid, normalized to an absolute path if specified in +# relative form. If $null is returned it is not a valid prefix path. -function Test-PrefixFilePath( - [ref] $path - ) +function Test-PrefixFilePath { - $p = $path.Value + param( + [string] + $Path + ) + + $p = $Path $elements = @($p -split '\\') # we need to tear off the last element and test the parent. before doing that, @@ -699,7 +700,7 @@ function Test-PrefixFilePath( if ($lastempty -or ($islocabs -and $elements.Count -eq 1) -or ($isunc -and $elements.Count -lt 5)) { - return $false + return $null } # if not local absolute or unc, it is local relative @@ -719,15 +720,10 @@ function Test-PrefixFilePath( # drive relative single element (no test needed, return immediately) (e.g., \foo, NOT \foo\bar) if ($elements.Count -eq 2) { - - $path.Value = $p - return $true + return $p } # ... must be multi-element (test needed) - # resplit for the prefix test (e.g., got foo\bar, must set up so we test c:\the\cwd\foo) - $elements = @($p -split '\\') - } else { # prepend cwd @@ -735,15 +731,13 @@ function Test-PrefixFilePath( # local single element (no test needed, return immediately) if ($elements.Count -eq 1) { - - $path.Value = $p - return $true + return $p } # ... must be local relative multi-element (test needed) - # resplit ... - $elements = @($path.Value -split '\\') } + + $elements = @($p -split '\\') } # rejoin without the tail and test @@ -751,11 +745,10 @@ function Test-PrefixFilePath( # return potentially updated path, but only modify on success if (Test-Path $tp) { - $path.Value = $p - $true - } else { - $false + return $p } + + return $null } function Check-ExtractZip( From 644562f25238201d7152e1d5a5f270961c713d9a Mon Sep 17 00:00:00 2001 From: Thomas Wohllaib Date: Thu, 23 May 2024 13:44:36 -0700 Subject: [PATCH 3/6] Update PrivateCloud.DiagnosticInfo.psm1 Run get-clusterlog sequentially. --- .../PrivateCloud.DiagnosticInfo.psm1 | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/PrivateCloud.DiagnosticInfo/PrivateCloud.DiagnosticInfo.psm1 b/PrivateCloud.DiagnosticInfo/PrivateCloud.DiagnosticInfo.psm1 index 3686ee6..6e366de 100644 --- a/PrivateCloud.DiagnosticInfo/PrivateCloud.DiagnosticInfo.psm1 +++ b/PrivateCloud.DiagnosticInfo/PrivateCloud.DiagnosticInfo.psm1 @@ -2204,10 +2204,7 @@ function Get-SddcDiagnosticInfo { $null = Get-ClusterLog -Node $using:ClusterNodes.Name -Destination $using:Path -UseLocalTime -Netft } - } - - if ($S2DEnabled) { - $JobStatic += Start-Job -Name ClusterHealthLogs { + if ($S2DEnabled) { $null = Get-ClusterLog -Node $using:ClusterNodes.Name -Destination $using:Path -Health -UseLocalTime } } @@ -6224,4 +6221,4 @@ Export-ModuleMember -Alias * -Function 'Get-SddcDiagnosticInfo', 'Show-StorageCounters', 'Get-SpacesTimeline', 'Set-SddcDiagnosticArchiveJobParameters', - 'Get-SddcDiagnosticArchiveJobParameters' \ No newline at end of file + 'Get-SddcDiagnosticArchiveJobParameters' From 36bbf7d54ec04294284eed2a276ccbc41c08ced1 Mon Sep 17 00:00:00 2001 From: Thomas Wohllaib Date: Thu, 23 May 2024 13:48:53 -0700 Subject: [PATCH 4/6] Update PrivateCloud.DiagnosticInfo.psm1 update variable $S2DEnabled to $using:S2DEnabled --- PrivateCloud.DiagnosticInfo/PrivateCloud.DiagnosticInfo.psm1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PrivateCloud.DiagnosticInfo/PrivateCloud.DiagnosticInfo.psm1 b/PrivateCloud.DiagnosticInfo/PrivateCloud.DiagnosticInfo.psm1 index 6e366de..c422c29 100644 --- a/PrivateCloud.DiagnosticInfo/PrivateCloud.DiagnosticInfo.psm1 +++ b/PrivateCloud.DiagnosticInfo/PrivateCloud.DiagnosticInfo.psm1 @@ -2204,7 +2204,7 @@ function Get-SddcDiagnosticInfo { $null = Get-ClusterLog -Node $using:ClusterNodes.Name -Destination $using:Path -UseLocalTime -Netft } - if ($S2DEnabled) { + if ($using:S2DEnabled) { $null = Get-ClusterLog -Node $using:ClusterNodes.Name -Destination $using:Path -Health -UseLocalTime } } From b774bba289f4c059fbfe5fda25a19bb2eb10051a Mon Sep 17 00:00:00 2001 From: Thomas Wohllaib Date: Tue, 4 Jun 2024 10:55:32 -0700 Subject: [PATCH 5/6] Update PrivateCloud.DiagnosticInfo.psm1 CAU log collection is also using the same log collection interface as the collection for the netft, health and cluster logs, moved CAU log collection to the same job so that it would be done sequentially for reliability --- .../PrivateCloud.DiagnosticInfo.psm1 | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/PrivateCloud.DiagnosticInfo/PrivateCloud.DiagnosticInfo.psm1 b/PrivateCloud.DiagnosticInfo/PrivateCloud.DiagnosticInfo.psm1 index c422c29..0e24155 100644 --- a/PrivateCloud.DiagnosticInfo/PrivateCloud.DiagnosticInfo.psm1 +++ b/PrivateCloud.DiagnosticInfo/PrivateCloud.DiagnosticInfo.psm1 @@ -1962,23 +1962,8 @@ function Get-SddcDiagnosticInfo Export-Clixml ($using:Path + "GetClusterQuorum.XML") } catch { Show-Warning("Unable to get Cluster Quorum. `nError="+$_.Exception.Message) } - } - - $JobStatic += start-job -Name CauDebugTrace { - try { - - # SCDT returns a fileinfo object for the saved ZIP on the pipeline; discard (allow errors/warnings to flow as normal) - - if ((Get-Command Save-CauDebugTrace).Parameters.ContainsKey("FeatureUpdateLogs")) { - $null = Save-CauDebugTrace -Cluster $using:AccessNode -FeatureUpdateLogs All -FilePath $using:Path - } - else { - $null = Save-CauDebugTrace -Cluster $using:AccessNode -FilePath $using:Path - } - } - catch { Show-Warning("Unable to get CAU debug trace. `nError="+$_.Exception.Message) } - } - + } + } else { Show-Update "... Skip gather of cluster configuration since cluster is not available" } @@ -2196,7 +2181,7 @@ function Get-SddcDiagnosticInfo } # Events, cmd, reports, et.al. - Show-Update "Start gather of system info, cluster/netft/health logs, reports and dump files ..." + Show-Update "Start gather of system info, cluster/netft/health/CAU logs, reports and dump files ..." $JobStatic += Start-Job -Name ClusterLogs { $null = Get-ClusterLog -Node $using:ClusterNodes.Name -Destination $using:Path -UseLocalTime @@ -2207,6 +2192,17 @@ function Get-SddcDiagnosticInfo if ($using:S2DEnabled) { $null = Get-ClusterLog -Node $using:ClusterNodes.Name -Destination $using:Path -Health -UseLocalTime } + + try { + # SCDT returns a fileinfo object for the saved ZIP on the pipeline; discard (allow errors/warnings to flow as normal) + if ((Get-Command Save-CauDebugTrace).Parameters.ContainsKey("FeatureUpdateLogs")) { + $null = Save-CauDebugTrace -Cluster $using:AccessNode -FeatureUpdateLogs All -FilePath $using:Path + } + else { + $null = Save-CauDebugTrace -Cluster $using:AccessNode -FilePath $using:Path + } + } + catch { Show-Warning("Unable to get CAU debug trace. `nError="+$_.Exception.Message) } } $JobStatic += $ClusterNodes.Name |% { From 279f8a38e872cf400fc91077e5655fef4a707798 Mon Sep 17 00:00:00 2001 From: PR-Liu <53802017+PR-Liu@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:03:06 -0700 Subject: [PATCH 6/6] Fix not gathering event logs, etc on node lists (no cluster) (#181) --- .../PrivateCloud.DiagnosticInfo.psm1 | 283 +++++++++--------- 1 file changed, 145 insertions(+), 138 deletions(-) diff --git a/PrivateCloud.DiagnosticInfo/PrivateCloud.DiagnosticInfo.psm1 b/PrivateCloud.DiagnosticInfo/PrivateCloud.DiagnosticInfo.psm1 index 8bb5a12..82b4eed 100644 --- a/PrivateCloud.DiagnosticInfo/PrivateCloud.DiagnosticInfo.psm1 +++ b/PrivateCloud.DiagnosticInfo/PrivateCloud.DiagnosticInfo.psm1 @@ -872,7 +872,7 @@ function Invoke-CommonCommand ( { $Sessions = @() - if ($ClusterNodes.Count -eq 0) + if ($null -eq $ClusterNodes -or $ClusterNodes.Count -eq 0) { $Sessions = New-PSSession -Cn localhost -EnableNetworkAccess -ConfigurationName $SessionConfigurationName } @@ -1881,7 +1881,7 @@ function Get-SddcDiagnosticInfo # $DedupEnabled = $true - if ($(Invoke-Command -ComputerName $AccessNode -ConfigurationName $SessionConfigurationName {(-not (Get-Command -Module Deduplication))} )) { + if ($null -eq $AccessNode -or $(Invoke-Command -ComputerName $AccessNode -ConfigurationName $SessionConfigurationName {(-not (Get-Command -Module Deduplication))} )) { $DedupEnabled = $false } @@ -2670,188 +2670,195 @@ function Get-SddcDiagnosticInfo } } - # - # SMB share health/status - # - - Show-Update "SMB Shares" + if($null -eq $AccessNode) + { + Show-Update "Skipping gathering of cluster storage as no access node was found" + } + else + { + # + # SMB share health/status + # - try { $SmbShares = Get-SmbShare -CimSession $AccessNode } - catch { Show-Error("Unable to get SMB Shares. `nError="+$_.Exception.Message) } + Show-Update "SMB Shares" + try { $SmbShares = Get-SmbShare -CimSession $AccessNode } + catch { Show-Error("Unable to get SMB Shares. `nError="+$_.Exception.Message) } - # XXX only sharepath and health are added in, why are we selecting down to just these four as opposed to add-member? - $ShareStatus = $SmbShares |? ContinuouslyAvailable | Select-Object ScopeName, Name, SharePath, Health - $Count1 = 0 - $Total1 = NCount($ShareStatus) + # XXX only sharepath and health are added in, why are we selecting down to just these four as opposed to add-member? + $ShareStatus = $SmbShares |? ContinuouslyAvailable | Select-Object ScopeName, Name, SharePath, Health + $Count1 = 0 + $Total1 = NCount($ShareStatus) - if ($Total1 -gt 0) - { - $ShareStatus |% { - $Progress = $Count1 / $Total1 * 100 - $Count1++ - Write-Progress -Activity "Testing file share access" -PercentComplete $Progress + if ($Total1 -gt 0) + { + $ShareStatus |% { + $Progress = $Count1 / $Total1 * 100 + $Count1++ + Write-Progress -Activity "Testing file share access" -PercentComplete $Progress - if ($ClusterDomain -ne "") - { - $_.SharePath = "\\" + $_.ScopeName + "." + $ClusterDomain + "\" + $_.Name - } - else - { - $_.SharePath = "\\" + $_.ScopeName + "\" + $_.Name - } - try { if (Test-Path -Path $_.SharePath -ErrorAction SilentlyContinue) { - $_.Health = "Accessible" - } else { - $_.Health = "Inaccessible" + if ($ClusterDomain -ne "") + { + $_.SharePath = "\\" + $_.ScopeName + "." + $ClusterDomain + "\" + $_.Name + } + else + { + $_.SharePath = "\\" + $_.ScopeName + "\" + $_.Name } + try { if (Test-Path -Path $_.SharePath -ErrorAction SilentlyContinue) { + $_.Health = "Accessible" + } else { + $_.Health = "Inaccessible" + } + } + catch { $_.Health = "Accessible: "+$_.Exception.Message } } - catch { $_.Health = "Accessible: "+$_.Exception.Message } + Write-Progress -Activity "Testing file share access" -Completed } - Write-Progress -Activity "Testing file share access" -Completed - } - $ShareStatus | Export-Clixml ($Path + "ShareStatus.XML") + $ShareStatus | Export-Clixml ($Path + "ShareStatus.XML") - Show-Update "SMB Share Open Files" + Show-Update "SMB Share Open Files" - try { - $o = Get-SmbOpenFile -CimSession $AccessNode - $o | Export-Clixml ($Path + "GetSmbOpenFile.XML") } - catch { Show-Error("Unable to get SMB open files. `nError="+$_.Exception.Message) } - - Show-Update "SMB Share Witness" + try { + $o = Get-SmbOpenFile -CimSession $AccessNode + $o | Export-Clixml ($Path + "GetSmbOpenFile.XML") } + catch { Show-Error("Unable to get SMB open files. `nError="+$_.Exception.Message) } - try { - $o = Get-SmbWitnessClient -CimSession $AccessNode - $o | Export-Clixml ($Path + "GetSmbWitness.XML") } - catch { Show-Error("Unable to get SMB Witness state. `nError="+$_.Exception.Message) } + Show-Update "SMB Share Witness" - Show-Update "Clustered Subsystem" + try { + $o = Get-SmbWitnessClient -CimSession $AccessNode + $o | Export-Clixml ($Path + "GetSmbWitness.XML") } + catch { Show-Error("Unable to get SMB Witness state. `nError="+$_.Exception.Message) } - # NOTE: $Subsystem is reused several times below - try { - $Subsystem = Get-StorageSubsystem Cluster* -CimSession $AccessNode - $Subsystem | Export-Clixml ($Path + "GetStorageSubsystem.XML") - } - catch { Show-Warning("Unable to get Clustered Subsystem.`nError="+$_.Exception.Message) } + Show-Update "Clustered Subsystem" - # Automatic triage is dependent on the cluster (Health Resource), avoid spurious - # errors if not available - if ($Subsystem.HealthStatus -notlike "Healthy" -and $ClusterName.Length) { - Show-Update "Triage for Clustered Subsystem (HealthStatus = $($Subsystem.HealthStatus))" + # NOTE: $Subsystem is reused several times below try { - $cmdlet = Get-Command Get-HealthFault -ErrorAction SilentlyContinue - if ($null -ne $cmdlet -and $cmdlet.Source -eq 'FailoverClusters') { - Get-HealthFault -CimSession $AccessNode | - Export-Clixml (Join-Path $Path "HeathFault.XML") - } else { - $Subsystem | Debug-StorageSubsystem -CimSession $AccessNode | - Export-Clixml (Join-Path $Path "DebugStorageSubsystem.XML") + $Subsystem = Get-StorageSubsystem Cluster* -CimSession $AccessNode + $Subsystem | Export-Clixml ($Path + "GetStorageSubsystem.XML") + } + catch { Show-Warning("Unable to get Clustered Subsystem.`nError="+$_.Exception.Message) } + + # Automatic triage is dependent on the cluster (Health Resource), avoid spurious + # errors if not available + if ($Subsystem.HealthStatus -notlike "Healthy" -and $ClusterName.Length) { + Show-Update "Triage for Clustered Subsystem (HealthStatus = $($Subsystem.HealthStatus))" + try { + $cmdlet = Get-Command Get-HealthFault -ErrorAction SilentlyContinue + if ($null -ne $cmdlet -and $cmdlet.Source -eq 'FailoverClusters') { + Get-HealthFault -CimSession $AccessNode | + Export-Clixml (Join-Path $Path "HeathFault.XML") + } else { + $Subsystem | Debug-StorageSubsystem -CimSession $AccessNode | + Export-Clixml (Join-Path $Path "DebugStorageSubsystem.XML") + } } + catch { Show-Error "Unable to get Get-HealthFault or Debug-StorageSubsystem for unhealthy StorageSubsystem.`nError=" $_ } } - catch { Show-Error "Unable to get Get-HealthFault or Debug-StorageSubsystem for unhealthy StorageSubsystem.`nError=" $_ } - } - Show-Update "Volumes & Virtual Disks" + Show-Update "Volumes & Virtual Disks" - # Volume status + # Volume status - try { - $Volumes = Get-Volume -CimSession $AccessNode -StorageSubSystem $Subsystem - $Volumes | Export-Clixml ($Path + "GetVolume.XML") } - catch { Show-Error("Unable to get Volumes. `nError="+$_.Exception.Message) } + try { + $Volumes = Get-Volume -CimSession $AccessNode -StorageSubSystem $Subsystem + $Volumes | Export-Clixml ($Path + "GetVolume.XML") } + catch { Show-Error("Unable to get Volumes. `nError="+$_.Exception.Message) } - # Virtual disk health - # Used in S2D-specific gather below + # Virtual disk health + # Used in S2D-specific gather below - try { - $VirtualDisk = Get-VirtualDisk -CimSession $AccessNode -StorageSubSystem $Subsystem - $VirtualDisk | Export-Clixml ($Path + "GetVirtualDisk.XML") - } - catch { Show-Warning("Unable to get Virtual Disks.`nError="+$_.Exception.Message) } + try { + $VirtualDisk = Get-VirtualDisk -CimSession $AccessNode -StorageSubSystem $Subsystem + $VirtualDisk | Export-Clixml ($Path + "GetVirtualDisk.XML") + } + catch { Show-Warning("Unable to get Virtual Disks.`nError="+$_.Exception.Message) } - # Deduplicated volume health - # XXX the counts/healthy likely not needed once phase 2 shifted into summary report + # Deduplicated volume health + # XXX the counts/healthy likely not needed once phase 2 shifted into summary report - if ($DedupEnabled) - { - Show-Update "Dedup Volume Status" + if ($DedupEnabled) + { + Show-Update "Dedup Volume Status" - try { - $DedupVolumes = Invoke-Command -ComputerName $AccessNode -ConfigurationName $SessionConfigurationName { Get-DedupStatus } - $DedupVolumes | Export-Clixml ($Path + "GetDedupVolume.XML") } - catch { Show-Error("Unable to get Dedup Volumes.`nError="+$_.Exception.Message) } + try { + $DedupVolumes = Invoke-Command -ComputerName $AccessNode -ConfigurationName $SessionConfigurationName { Get-DedupStatus } + $DedupVolumes | Export-Clixml ($Path + "GetDedupVolume.XML") } + catch { Show-Error("Unable to get Dedup Volumes.`nError="+$_.Exception.Message) } - $DedupTotal = NCount($DedupVolumes) - $DedupHealthy = NCount($DedupVolumes |? LastOptimizationResult -eq 0 ) + $DedupTotal = NCount($DedupVolumes) + $DedupHealthy = NCount($DedupVolumes |? LastOptimizationResult -eq 0 ) - } else { + } else { - $DedupVolumes = @() - $DedupTotal = 0 - $DedupHealthy = 0 - } + $DedupVolumes = @() + $DedupTotal = 0 + $DedupHealthy = 0 + } - Show-Update "Storage Pool & Tiers" + Show-Update "Storage Pool & Tiers" - # Storage tier information + # Storage tier information - try { - Get-StorageTier -CimSession $AccessNode | - Export-Clixml ($Path + "GetStorageTier.XML") } - catch { Show-Warning("Unable to get Storage Tiers. `nError="+$_.Exception.Message) } + try { + Get-StorageTier -CimSession $AccessNode | + Export-Clixml ($Path + "GetStorageTier.XML") } + catch { Show-Warning("Unable to get Storage Tiers. `nError="+$_.Exception.Message) } - # Storage pool health - try { - $StoragePools = @(Get-StoragePool -IsPrimordial $False -CimSession $AccessNode -StorageSubSystem $Subsystem -ErrorAction SilentlyContinue) - $StoragePools | Export-Clixml ($Path + "GetStoragePool.XML") } - catch { Show-Error("Unable to get Storage Pools. `nError="+$_.Exception.Message) } + # Storage pool health - Show-Update "Storage Jobs" + try { + $StoragePools = @(Get-StoragePool -IsPrimordial $False -CimSession $AccessNode -StorageSubSystem $Subsystem -ErrorAction SilentlyContinue) + $StoragePools | Export-Clixml ($Path + "GetStoragePool.XML") } + catch { Show-Error("Unable to get Storage Pools. `nError="+$_.Exception.Message) } - try { - # cannot subsystem scope Get-StorageJob at this time - icm $AccessNode -ConfigurationName $SessionConfigurationName { Get-StorageJob } | - Export-Clixml ($Path + "GetStorageJob.XML") } - catch { Show-Warning("Unable to get Storage Jobs. `nError="+$_.Exception.Message) } + Show-Update "Storage Jobs" - Show-Update "Clustered PhysicalDisks and SNV" + try { + # cannot subsystem scope Get-StorageJob at this time + icm $AccessNode -ConfigurationName $SessionConfigurationName { Get-StorageJob } | + Export-Clixml ($Path + "GetStorageJob.XML") } + catch { Show-Warning("Unable to get Storage Jobs. `nError="+$_.Exception.Message) } - # Physical disk health + Show-Update "Clustered PhysicalDisks and SNV" - try { - $PhysicalDisks = Get-PhysicalDisk -CimSession $AccessNode -StorageSubSystem $Subsystem - $PhysicalDisks | Export-Clixml ($Path + "GetPhysicalDisk.XML") } - catch { Show-Error("Unable to get Physical Disks. `nError="+$_.Exception.Message) } + # Physical disk health - try { - Get-PhysicalDisk -CimSession $AccessNode -StorageSubSystem $Subsystem | Get-PhysicalDiskSNV -CimSession $AccessNode | - Export-Clixml ($Path + "GetPhysicalDiskSNV.XML") } - catch { Show-Error("Unable to get Physical Disk Storage Node View. `nError="+$_.Exception.Message) } + try { + $PhysicalDisks = Get-PhysicalDisk -CimSession $AccessNode -StorageSubSystem $Subsystem + $PhysicalDisks | Export-Clixml ($Path + "GetPhysicalDisk.XML") } + catch { Show-Error("Unable to get Physical Disks. `nError="+$_.Exception.Message) } - # Reliability counters - # These may cause a latency burst on some devices due to device-specific requirements for lifting/generating - # the SMART data which underlies them. Decline to do this by default. + try { + Get-PhysicalDisk -CimSession $AccessNode -StorageSubSystem $Subsystem | Get-PhysicalDiskSNV -CimSession $AccessNode | + Export-Clixml ($Path + "GetPhysicalDiskSNV.XML") } + catch { Show-Error("Unable to get Physical Disk Storage Node View. `nError="+$_.Exception.Message) } - if ($IncludeReliabilityCounters -eq $true) { + # Reliability counters + # These may cause a latency burst on some devices due to device-specific requirements for lifting/generating + # the SMART data which underlies them. Decline to do this by default. - Show-Update "Storage Reliability Counters" + if ($IncludeReliabilityCounters -eq $true) { - try { - $PhysicalDisks | Get-StorageReliabilityCounter -CimSession $AccessNode | - Export-Clixml ($Path + "GetReliabilityCounter.XML") } - catch { Show-Error("Unable to get Storage Reliability Counters. `nError="+$_.Exception.Message) } + Show-Update "Storage Reliability Counters" - } + try { + $PhysicalDisks | Get-StorageReliabilityCounter -CimSession $AccessNode | + Export-Clixml ($Path + "GetReliabilityCounter.XML") } + catch { Show-Error("Unable to get Storage Reliability Counters. `nError="+$_.Exception.Message) } + + } - # Storage enclosure health + # Storage enclosure health - Show-Update "Storage Enclosures" + Show-Update "Storage Enclosures" - try { - Get-StorageEnclosure -CimSession $AccessNode -StorageSubSystem $Subsystem | - Export-Clixml ($Path + "GetStorageEnclosure.XML") } - catch { Show-Error("Unable to get Enclosures. `nError="+$_.Exception.Message) } + try { + Get-StorageEnclosure -CimSession $AccessNode -StorageSubSystem $Subsystem | + Export-Clixml ($Path + "GetStorageEnclosure.XML") } + catch { Show-Error("Unable to get Enclosures. `nError="+$_.Exception.Message) } + } # Undo changes as this is failing in AzureStack environment. # SDDC cim objects