699 lines
31 KiB
PowerShell
699 lines
31 KiB
PowerShell
Add-Type -AssemblyName PresentationFramework, PresentationCore, WindowsBase
|
|
|
|
# Import Custom Modules
|
|
Import-Module (Join-Path $PSScriptRoot "Modules\SQLManager.psm1") -Force
|
|
|
|
# Configuration Paths
|
|
$configPath = Join-Path $PSScriptRoot "Config\settings.json"
|
|
|
|
# Load Settings
|
|
if (Test-Path $configPath) {
|
|
$settings = Get-Content $configPath | ConvertFrom-Json
|
|
} else {
|
|
$settings = [pscustomobject]@{ LastServer = ""; Theme = "Light"; Servers = @("localhost") }
|
|
}
|
|
|
|
# Ensure Servers & SplunkHosts are collection and initialized
|
|
if ($null -eq $settings.Servers -or $null -eq $settings.SplunkHosts) {
|
|
$servers = if ($null -ne $settings.Servers) { $settings.Servers } else { @("localhost") }
|
|
$splunkHosts = if ($null -ne $settings.SplunkHosts) { $settings.SplunkHosts } else { @("sf-host-prod-01", "sf-host-prod-02", "sf-host-staging-01") }
|
|
|
|
if ($settings -is [System.Management.Automation.PSCustomObject]) {
|
|
$props = [ordered]@{
|
|
LastServer = $settings.LastServer
|
|
Theme = $settings.Theme
|
|
Servers = $servers
|
|
SplunkHosts = $splunkHosts
|
|
SplunkRealm = if ($null -ne $settings.SplunkRealm) { $settings.SplunkRealm } else { "eu0" }
|
|
SplunkToken = if ($null -ne $settings.SplunkToken) { $settings.SplunkToken } else { "YOUR_TOKEN" }
|
|
}
|
|
$settings = [pscustomobject]$props
|
|
} else {
|
|
$settings = [pscustomobject]@{
|
|
LastServer = ""; Theme = "Light"; Servers = $servers; SplunkHosts = $splunkHosts; SplunkRealm = "eu0"; SplunkToken = "YOUR_TOKEN"
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($settings.Servers -is [string]) {
|
|
$settings.Servers = @($settings.Servers)
|
|
}
|
|
if ($settings.SplunkHosts -is [string]) {
|
|
$settings.SplunkHosts = @($settings.SplunkHosts)
|
|
}
|
|
|
|
# 1. Path to XAML
|
|
$xamlFile = Join-Path $PSScriptRoot "UI\MainWindow.xaml"
|
|
|
|
# 2. Load XAML
|
|
[xml]$xml = Get-Content $xamlFile
|
|
$reader = New-Object System.Xml.XmlNodeReader($xml)
|
|
$Window = [Windows.Markup.XamlReader]::Load($reader)
|
|
|
|
# 3. Extract controls from XAML
|
|
$btnConnect = $Window.FindName("btnConnect")
|
|
$txtServerName = $Window.FindName("txtServerName")
|
|
$txtLogs = $Window.FindName("txtLogs")
|
|
$dgServerInfo = $Window.FindName("dgServerInfo")
|
|
$dgDatabases = $Window.FindName("dgDatabases")
|
|
$dgServerErrors = $Window.FindName("dgServerErrors")
|
|
$tvServers = $Window.FindName("tvServers")
|
|
$btnAddServer = $Window.FindName("btnAddServer")
|
|
$btnRemoveServer = $Window.FindName("btnRemoveServer")
|
|
$gridDetails = $Window.FindName("gridDetails")
|
|
$menuSqlServers = $Window.FindName("menuSqlServers")
|
|
$menuHostSplunk = $Window.FindName("menuHostSplunk")
|
|
$grpDirectory = $Window.FindName("grpDirectory")
|
|
|
|
# Current view state: "SQL" or "Splunk"
|
|
$script:CurrentView = "SQL"
|
|
$script:SplunkHosts = @()
|
|
|
|
# Initialize connection states cache per server
|
|
$script:ServerConnectionStates = @{}
|
|
|
|
# Set initial values from settings
|
|
$txtServerName.Text = $settings.LastServer
|
|
|
|
# 4. Helper function for logging
|
|
function Write-Log {
|
|
param([string]$Message)
|
|
$timestamp = Get-Date -Format "HH:mm:ss"
|
|
$txtLogs.Dispatcher.Invoke({
|
|
$txtLogs.AppendText("[$timestamp] $Message`r`n")
|
|
$txtLogs.ScrollToEnd()
|
|
})
|
|
}
|
|
|
|
# 4.0. Get Splunk API Hosts function
|
|
function Get-SplunkAPIHosts {
|
|
$realm = $settings.SplunkRealm
|
|
if ([string]::IsNullOrWhiteSpace($realm)) { $realm = "eu0" }
|
|
|
|
$token = $env:SF_TOKEN
|
|
if ([string]::IsNullOrWhiteSpace($token)) {
|
|
$token = $settings.SplunkToken
|
|
}
|
|
|
|
# Bezpieczne wymuszenie protokołu TLS 1.2 dla starszych wersji PowerShell
|
|
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
|
|
|
|
# Jeśli brak tokenu lub jest domyślny, spróbujemy poprosić użytkownika o podanie, lub użyjemy demo
|
|
if ([string]::IsNullOrWhiteSpace($token) -or $token -eq "YOUR_TOKEN") {
|
|
Write-Log "DEBUG: Brak tokenu w SF_TOKEN lub settings.json. Pytam użytkownika..."
|
|
try {
|
|
Add-Type -AssemblyName Microsoft.VisualBasic
|
|
$inputToken = [Microsoft.VisualBasic.Interaction]::InputBox("Wprowadź swój token API Splunk Observability (pozostaw puste dla trybu DEMO):", "Wymagana Autoryzacja Splunk", "")
|
|
if (-not [string]::IsNullOrWhiteSpace($inputToken)) {
|
|
$token = $inputToken.Trim()
|
|
$settings.SplunkToken = $token
|
|
$settings | ConvertTo-Json | Set-Content $configPath
|
|
Write-Log "DEBUG: Token został zapisany w settings.json."
|
|
}
|
|
} catch {
|
|
# Ignoruj błędy ładowania VisualBasic
|
|
}
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($token) -or $token -eq "YOUR_TOKEN") {
|
|
Write-Log "DEBUG: Używam symulacji - brak prawidłowego tokenu API."
|
|
Write-Log "DEBUG: Pokazuję pierwszych 20 wyników demonstracyjnych (zgodnie z ograniczeniem)."
|
|
return @(
|
|
"demo-splunk-host-eu-prod-01", "demo-splunk-host-eu-prod-02", "demo-splunk-host-eu-prod-03", "demo-splunk-host-eu-prod-04", "demo-splunk-host-eu-prod-05",
|
|
"demo-splunk-host-eu-prod-06", "demo-splunk-host-eu-prod-07", "demo-splunk-host-eu-prod-08", "demo-splunk-host-eu-prod-09", "demo-splunk-host-eu-prod-10",
|
|
"demo-splunk-host-eu-staging-01", "demo-splunk-host-eu-staging-02", "demo-splunk-host-eu-staging-03", "demo-splunk-host-eu-staging-04", "demo-splunk-host-eu-staging-05",
|
|
"demo-splunk-host-eu-dev-01", "demo-splunk-host-eu-dev-02", "demo-splunk-host-eu-dev-03", "demo-splunk-host-eu-dev-04", "demo-splunk-host-eu-dev-05"
|
|
)
|
|
}
|
|
|
|
$headers = @{
|
|
"X-SF-TOKEN" = $token
|
|
"Content-Type" = "application/json"
|
|
}
|
|
|
|
# Wewnętrzny, kompatybilny helper do zapytań z logowaniem postępu na żywo
|
|
$InvokeSplunkRequest = {
|
|
param([string]$targetUri)
|
|
Write-Log "DEBUG: Zapytanie HTTP GET do: $targetUri"
|
|
|
|
$params = @{
|
|
Uri = $targetUri
|
|
Method = "GET"
|
|
Headers = $headers
|
|
ErrorAction = "Stop"
|
|
}
|
|
|
|
# Kompatybilność parametrów timeout w zależności od wersji PowerShell (PS 5.1 vs Core)
|
|
if ($PSVersionTable.PSVersion.Major -ge 6) {
|
|
$params.TimeoutSec = 10
|
|
} else {
|
|
$params.Timeout = 10000 # 10 sekund w ms dla PS 5.1
|
|
}
|
|
|
|
$startTime = Get-Date
|
|
$responseObj = Invoke-RestMethod @params
|
|
$duration = [Math]::Round(((Get-Date) - $startTime).TotalMilliseconds)
|
|
Write-Log "DEBUG: Odpowiedź otrzymana pomyślnie w $duration ms."
|
|
return $responseObj
|
|
}
|
|
|
|
$hostsSet = New-Object System.Collections.Generic.HashSet[string] ([System.StringComparer]::OrdinalIgnoreCase)
|
|
|
|
# 1. Metoda główna: GET /v2/dimension?query=key:host
|
|
try {
|
|
Write-Log "DEBUG: Rozpoczynam pobieranie wymiarów (Metoda 1: GET /v2/dimension?query=key:host)..."
|
|
$limit = 100
|
|
$offset = 0
|
|
$hasMore = $true
|
|
|
|
while ($hasMore) {
|
|
$uri = "https://api.$realm.observability.splunkcloud.com/v2/dimension?query=key:host&limit=$limit&offset=$offset"
|
|
$response = &$InvokeSplunkRequest -targetUri $uri
|
|
|
|
$results = $null
|
|
if ($null -ne $response) {
|
|
if ($null -ne $response.results) { $results = $response.results }
|
|
elseif ($response -is [System.Array]) { $results = $response }
|
|
}
|
|
|
|
if ($null -ne $results -and $results.Count -gt 0) {
|
|
foreach ($item in $results) {
|
|
if ($item.key -eq "host" -and -not [string]::IsNullOrWhiteSpace($item.value)) {
|
|
[void]$hostsSet.Add($item.value.Trim())
|
|
} elseif (-not [string]::IsNullOrWhiteSpace($item.value)) {
|
|
[void]$hostsSet.Add($item.value.Trim())
|
|
}
|
|
|
|
# Przerywamy natychmiast, gdy zebraliśmy już 20 unikalnych hostów
|
|
if ($hostsSet.Count -ge 20) {
|
|
$hasMore = $false
|
|
break
|
|
}
|
|
}
|
|
Write-Log "DEBUG: Pobrano $($results.Count) obiektów wymiarów (offset: $offset). Dotychczas unikalnych hostów: $($hostsSet.Count)"
|
|
|
|
if ($hostsSet.Count -ge 20) {
|
|
$hasMore = $false
|
|
} elseif ($results.Count -lt $limit) {
|
|
$hasMore = $false
|
|
} else {
|
|
$offset += $limit
|
|
}
|
|
} else {
|
|
$hasMore = $false
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
Write-Log "DEBUG: Metoda 1 zakończyła się błędem: $($_.Exception.Message)"
|
|
}
|
|
|
|
# 2. Metoda Fallback: GET /v2/metrictimeseries?query=host:*
|
|
if ($hostsSet.Count -eq 0) {
|
|
try {
|
|
Write-Log "DEBUG: Metoda 1 nie zwróciła hostów. Uruchamiam Metodę zapasową 2 (GET /v2/metrictimeseries)..."
|
|
$limit = 100
|
|
$offset = 0
|
|
$hasMore = $true
|
|
$maxPages = 5
|
|
$pageCount = 0
|
|
|
|
while ($hasMore -and $pageCount -lt $maxPages) {
|
|
$pageCount++
|
|
$uri = "https://api.$realm.observability.splunkcloud.com/v2/metrictimeseries?query=host:*&limit=$limit&offset=$offset"
|
|
$response = &$InvokeSplunkRequest -targetUri $uri
|
|
|
|
$results = $null
|
|
if ($null -ne $response) {
|
|
if ($null -ne $response.results) { $results = $response.results }
|
|
elseif ($response -is [System.Array]) { $results = $response }
|
|
}
|
|
|
|
if ($null -ne $results -and $results.Count -gt 0) {
|
|
$prevCount = $hostsSet.Count
|
|
foreach ($mts in $results) {
|
|
if ($null -ne $mts.dimensions -and $null -ne $mts.dimensions.host) {
|
|
$val = $mts.dimensions.host
|
|
if (-not [string]::IsNullOrWhiteSpace($val)) {
|
|
[void]$hostsSet.Add($val.Trim())
|
|
}
|
|
}
|
|
|
|
# Przerywamy natychmiast, gdy zebraliśmy już 20 unikalnych hostów
|
|
if ($hostsSet.Count -ge 20) {
|
|
$hasMore = $false
|
|
break
|
|
}
|
|
}
|
|
Write-Log "DEBUG: Pobrano $($results.Count) rekordów MTS (offset: $offset). Dotychczas unikalnych hostów: $($hostsSet.Count)"
|
|
|
|
if ($hostsSet.Count -ge 20) {
|
|
$hasMore = $false
|
|
} elseif ($results.Count -lt $limit) {
|
|
$hasMore = $false
|
|
} else {
|
|
if ($hostsSet.Count -eq $prevCount) {
|
|
Write-Log "DEBUG: Brak nowych unikalnych hostów na tej stronie. Przerywam dalsze skanowanie."
|
|
$hasMore = $false
|
|
} else {
|
|
$offset += $limit
|
|
}
|
|
}
|
|
} else {
|
|
$hasMore = $false
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
Write-Log "DEBUG: Metoda 2 zakończyła się błędem: $($_.Exception.Message)"
|
|
}
|
|
}
|
|
|
|
if ($hostsSet.Count -gt 0) {
|
|
$sorted = $hostsSet | Sort-Object
|
|
# Ograniczenie do pierwszych 20 wyników zgodnie z żądaniem
|
|
$limited = $sorted | Select-Object -First 20
|
|
Write-Log "DEBUG: Pobrano łącznie $($sorted.Count) unikalnych hostów. Ograniczam listę i pokazuję pierwszych $($limited.Count) wyników."
|
|
return $limited
|
|
} else {
|
|
Write-Log "DEBUG: API nie zwróciło żadnych danych. Ładuję listę demonstracyjną (20 hostów)."
|
|
return @(
|
|
"demo-splunk-host-eu-prod-01", "demo-splunk-host-eu-prod-02", "demo-splunk-host-eu-prod-03", "demo-splunk-host-eu-prod-04", "demo-splunk-host-eu-prod-05",
|
|
"demo-splunk-host-eu-prod-06", "demo-splunk-host-eu-prod-07", "demo-splunk-host-eu-prod-08", "demo-splunk-host-eu-prod-09", "demo-splunk-host-eu-prod-10",
|
|
"demo-splunk-host-eu-staging-01", "demo-splunk-host-eu-staging-02", "demo-splunk-host-eu-staging-03", "demo-splunk-host-eu-staging-04", "demo-splunk-host-eu-staging-05",
|
|
"demo-splunk-host-eu-dev-01", "demo-splunk-host-eu-dev-02", "demo-splunk-host-eu-dev-03", "demo-splunk-host-eu-dev-04", "demo-splunk-host-eu-dev-05"
|
|
)
|
|
}
|
|
}
|
|
|
|
# 4.1. TreeView population and refresh
|
|
function Refresh-ServerTree {
|
|
$tvServers.Dispatcher.Invoke({
|
|
$tvServers.Items.Clear()
|
|
|
|
if ($script:CurrentView -eq "SQL") {
|
|
$rootNode = New-Object System.Windows.Controls.TreeViewItem
|
|
$rootNode.Header = "SQL Servers"
|
|
$rootNode.IsExpanded = $true
|
|
$rootNode.Tag = "__ROOT__"
|
|
|
|
foreach ($server in $settings.Servers) {
|
|
$serverNode = New-Object System.Windows.Controls.TreeViewItem
|
|
$serverNode.Header = $server
|
|
$serverNode.Tag = $server
|
|
|
|
# Add a dummy loading node so the expansion arrow is displayed
|
|
$dummyNode = New-Object System.Windows.Controls.TreeViewItem
|
|
$dummyNode.Header = "Loading..."
|
|
$serverNode.Items.Add($dummyNode)
|
|
|
|
# Event handler when a server node is expanded
|
|
$serverNode.Add_Expanded({
|
|
param($sender, $e)
|
|
# Prevent this handler from running for nested child expansions (bubbling)
|
|
if ($e.OriginalSource -ne $sender) { return }
|
|
|
|
$node = $sender
|
|
if ($node.Items.Count -eq 1 -and $node.Items[0].Header -eq "Loading...") {
|
|
$node.Items.Clear()
|
|
$srvName = $node.Tag
|
|
|
|
Write-Log "Fetching databases for $srvName..."
|
|
# Force UI to process the pending write-log before connection blocks the thread
|
|
[System.Windows.Threading.Dispatcher]::CurrentDispatcher.Invoke(
|
|
[System.Windows.Threading.DispatcherPriority]::Background,
|
|
[System.Action]{}
|
|
)
|
|
|
|
try {
|
|
$dbs = Get-SQLDatabases -ServerInstance $srvName
|
|
if ($dbs -and $dbs.Count -gt 0) {
|
|
# Cache the loaded databases
|
|
if ($null -eq $script:ServerConnectionStates[$srvName]) {
|
|
$script:ServerConnectionStates[$srvName] = @{
|
|
Success = $true
|
|
Instance = $srvName
|
|
Version = "Unknown (Expanded)"
|
|
Edition = "Unknown"
|
|
Status = "Online"
|
|
Databases = $dbs
|
|
Errors = $null
|
|
}
|
|
} else {
|
|
$script:ServerConnectionStates[$srvName].Databases = $dbs
|
|
}
|
|
|
|
foreach ($db in $dbs) {
|
|
$dbNode = New-Object System.Windows.Controls.TreeViewItem
|
|
$dbNode.Header = "$($db.Name) ($($db.Status))"
|
|
$dbNode.Tag = $db
|
|
$node.Items.Add($dbNode)
|
|
}
|
|
Write-Log "Successfully loaded $($dbs.Count) databases for $srvName."
|
|
} else {
|
|
$noDbNode = New-Object System.Windows.Controls.TreeViewItem
|
|
$noDbNode.Header = "(No databases found)"
|
|
$node.Items.Add($noDbNode)
|
|
Write-Log "No databases found on $srvName."
|
|
}
|
|
} catch {
|
|
Write-Log "Error fetching databases: $($_.Exception.Message)"
|
|
$errNode = New-Object System.Windows.Controls.TreeViewItem
|
|
$errNode.Header = "(Error loading databases)"
|
|
$node.Items.Add($errNode)
|
|
}
|
|
}
|
|
})
|
|
|
|
$rootNode.Items.Add($serverNode)
|
|
}
|
|
|
|
$tvServers.Items.Add($rootNode)
|
|
} else {
|
|
# Splunk Hosts View
|
|
$rootNode = New-Object System.Windows.Controls.TreeViewItem
|
|
$rootNode.Header = "Splunk Observability Hosts"
|
|
$rootNode.IsExpanded = $true
|
|
$rootNode.Tag = "__ROOT__"
|
|
|
|
foreach ($hostName in $script:SplunkHosts) {
|
|
$hostNode = New-Object System.Windows.Controls.TreeViewItem
|
|
$hostNode.Header = $hostName
|
|
$hostNode.Tag = $hostName
|
|
|
|
# Add a dummy loading node so the expansion arrow is displayed
|
|
$dummyNode = New-Object System.Windows.Controls.TreeViewItem
|
|
$dummyNode.Header = "Loading..."
|
|
$hostNode.Items.Add($dummyNode)
|
|
|
|
# Event handler when a Splunk host node is expanded
|
|
$hostNode.Add_Expanded({
|
|
param($sender, $e)
|
|
if ($e.OriginalSource -ne $sender) { return }
|
|
|
|
$node = $sender
|
|
if ($node.Items.Count -eq 1 -and $node.Items[0].Header -eq "Loading...") {
|
|
$node.Items.Clear()
|
|
$hName = $node.Tag
|
|
|
|
Write-Log "Fetching active dimensions/properties for Splunk host $hName..."
|
|
|
|
# Generate mock indices / indexes for Splunk host representation
|
|
$indexes = @(
|
|
[pscustomobject]@{ Name = "main_index"; Status = "Active"; Size = "1.2 GB"; RecoveryModel = "N/A"; LastFullBackup = "N/A"; LastLogBackup = "N/A" }
|
|
[pscustomobject]@{ Name = "infrastructure_telemetry"; Status = "Active"; Size = "15.4 GB"; RecoveryModel = "N/A"; LastFullBackup = "N/A"; LastLogBackup = "N/A" }
|
|
[pscustomobject]@{ Name = "audit_logs"; Status = "Active"; Size = "0.8 GB"; RecoveryModel = "N/A"; LastFullBackup = "N/A"; LastLogBackup = "N/A" }
|
|
)
|
|
|
|
# Cache Splunk host info
|
|
$script:ServerConnectionStates[$hName] = @{
|
|
Success = $true
|
|
Instance = $hName
|
|
Version = "Splunk Agent v5.4.1"
|
|
Edition = "Enterprise Observability"
|
|
Status = "Active / Online"
|
|
Databases = $indexes
|
|
Errors = @(
|
|
[pscustomobject]@{ LogDate = (Get-Date).AddMinutes(-5).ToString(); Message = "High memory usage alert on host $hName"; Source = "SplunkObservability" }
|
|
[pscustomobject]@{ LogDate = (Get-Date).AddMinutes(-20).ToString(); Message = "Metric forwarder queue processed successfully"; Source = "Forwarder" }
|
|
)
|
|
}
|
|
|
|
foreach ($idx in $indexes) {
|
|
$idxNode = New-Object System.Windows.Controls.TreeViewItem
|
|
$idxNode.Header = "$($idx.Name) ($($idx.Status))"
|
|
$idxNode.Tag = $idx
|
|
$node.Items.Add($idxNode)
|
|
}
|
|
|
|
Write-Log "Loaded active indexes for $hName."
|
|
}
|
|
})
|
|
|
|
$rootNode.Items.Add($hostNode)
|
|
}
|
|
|
|
$tvServers.Items.Add($rootNode)
|
|
}
|
|
})
|
|
}
|
|
|
|
# Handle TreeView selection
|
|
$tvServers.Add_SelectedItemChanged({
|
|
param($sender, $e)
|
|
$selectedItem = $tvServers.SelectedItem
|
|
if ($null -ne $selectedItem -and $null -ne $selectedItem.Tag -and $selectedItem.Tag -ne "__ROOT__") {
|
|
# Show detail panel
|
|
$gridDetails.Visibility = "Visible"
|
|
|
|
if ($selectedItem.Tag -is [string]) {
|
|
# It's a server node, copy its name to TextBox and update its cached connection state
|
|
$srvName = $selectedItem.Tag
|
|
$txtServerName.Text = $srvName
|
|
Write-Log "Selected server: $srvName"
|
|
|
|
# Check cached connection state for this server
|
|
$connState = $script:ServerConnectionStates[$srvName]
|
|
if ($null -ne $connState -and $connState.Success) {
|
|
# Server is connected: show its version/details in the DataGrid
|
|
$dgServerInfo.ItemsSource = @([PSCustomObject]$connState | Select-Object Instance, Version, Edition, Status)
|
|
|
|
# Show cached databases if available
|
|
if ($null -ne $connState.Databases) {
|
|
$dgDatabases.ItemsSource = @($connState.Databases)
|
|
} else {
|
|
$dgDatabases.ItemsSource = $null
|
|
}
|
|
|
|
# Show cached error logs if available
|
|
if ($null -ne $connState.Errors) {
|
|
$dgServerErrors.ItemsSource = @($connState.Errors)
|
|
} else {
|
|
$dgServerErrors.ItemsSource = $null
|
|
}
|
|
} else {
|
|
# Server is not connected: clear the DataGrids so no old server details/errors are visible
|
|
$dgServerInfo.ItemsSource = $null
|
|
$dgDatabases.ItemsSource = $null
|
|
$dgServerErrors.ItemsSource = $null
|
|
}
|
|
} elseif ($selectedItem.Tag -is [System.Management.Automation.PSCustomObject]) {
|
|
# It's a database node
|
|
$db = $selectedItem.Tag
|
|
Write-Log "Selected database: $($db.Name) (Size: $($db.Size), Status: $($db.Status))"
|
|
|
|
# Show database details in the main information grid
|
|
$dgServerInfo.ItemsSource = @($db | Select-Object Name, Status, Size, RecoveryModel, LastFullBackup, LastLogBackup)
|
|
$dgDatabases.ItemsSource = $null
|
|
$dgServerErrors.ItemsSource = $null
|
|
|
|
# Also, update txtServerName with the parent server's tag so they see which server it belongs to
|
|
$parentItem = $selectedItem.Parent
|
|
if ($null -ne $parentItem -and $parentItem.Tag -is [string]) {
|
|
$txtServerName.Text = $parentItem.Tag
|
|
}
|
|
}
|
|
} else {
|
|
# Hide detail panel if selection is null or ROOT
|
|
$gridDetails.Visibility = "Collapsed"
|
|
$dgServerInfo.ItemsSource = $null
|
|
$dgDatabases.ItemsSource = $null
|
|
$dgServerErrors.ItemsSource = $null
|
|
}
|
|
})
|
|
|
|
# Handle Add Server button
|
|
$btnAddServer.Add_Click({
|
|
$server = ""
|
|
$promptMsg = "Enter SQL Server Instance Name (e.g., localhost, srv\SQLEXPRESS):"
|
|
$title = "Add Server"
|
|
if ($script:CurrentView -eq "Splunk") {
|
|
$promptMsg = "Enter Splunk Host Name (e.g., splunk-host-prod-01):"
|
|
$title = "Add Splunk Host"
|
|
}
|
|
|
|
try {
|
|
Add-Type -AssemblyName Microsoft.VisualBasic
|
|
$server = [Microsoft.VisualBasic.Interaction]::InputBox($promptMsg, $title, "")
|
|
} catch {
|
|
# Fallback using current textbox value
|
|
$server = $txtServerName.Text
|
|
if ([string]::IsNullOrWhiteSpace($server)) {
|
|
[System.Windows.MessageBox]::Show("Visual Basic assembly not found. Please type the name in 'Server Name' textbox first, and then click 'Add Server'.", $title, "OK", "Information")
|
|
return
|
|
}
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($server)) { return }
|
|
$server = $server.Trim()
|
|
|
|
if ($script:CurrentView -eq "SQL") {
|
|
if ($settings.Servers -contains $server) {
|
|
[System.Windows.MessageBox]::Show("Server '$server' is already in the list.", "Server Already Exists", "OK", "Information")
|
|
return
|
|
}
|
|
$settings.Servers = $settings.Servers + $server
|
|
Write-Log "Successfully added server to list: $server"
|
|
} else {
|
|
if ($settings.SplunkHosts -contains $server) {
|
|
[System.Windows.MessageBox]::Show("Splunk Host '$server' is already in the list.", "Host Already Exists", "OK", "Information")
|
|
return
|
|
}
|
|
$settings.SplunkHosts = $settings.SplunkHosts + $server
|
|
$script:SplunkHosts = $settings.SplunkHosts
|
|
Write-Log "Successfully added Splunk host: $server"
|
|
}
|
|
|
|
# Save to settings
|
|
$settings | ConvertTo-Json | Set-Content $configPath
|
|
Refresh-ServerTree
|
|
})
|
|
|
|
# Handle Remove Server button
|
|
$btnRemoveServer.Add_Click({
|
|
$selectedItem = $tvServers.SelectedItem
|
|
if ($null -eq $selectedItem -or $selectedItem.Tag -eq "__ROOT__") {
|
|
$msg = if ($script:CurrentView -eq "SQL") { "Please select a server node in the tree to remove." } else { "Please select a Splunk Host node in the tree to remove." }
|
|
[System.Windows.MessageBox]::Show($msg, "Select Node", "OK", "Warning")
|
|
return
|
|
}
|
|
|
|
$serverName = $selectedItem.Tag
|
|
if ($serverName -is [string]) {
|
|
if ($script:CurrentView -eq "SQL" -and ($settings.Servers -contains $serverName)) {
|
|
$confirm = [System.Windows.MessageBox]::Show("Are you sure you want to remove '$serverName' from the list?", "Confirm Server Removal", "YesNo", "Question")
|
|
if ($confirm -eq "Yes") {
|
|
$settings.Servers = $settings.Servers | Where-Object { $_ -ne $serverName }
|
|
Write-Log "Removed server from list: $serverName"
|
|
} else { return }
|
|
} elseif ($script:CurrentView -eq "Splunk" -and ($settings.SplunkHosts -contains $serverName)) {
|
|
$confirm = [System.Windows.MessageBox]::Show("Are you sure you want to remove '$serverName' from the list?", "Confirm Host Removal", "YesNo", "Question")
|
|
if ($confirm -eq "Yes") {
|
|
$settings.SplunkHosts = $settings.SplunkHosts | Where-Object { $_ -ne $serverName }
|
|
$script:SplunkHosts = $settings.SplunkHosts
|
|
Write-Log "Removed Splunk host from list: $serverName"
|
|
} else { return }
|
|
} else {
|
|
[System.Windows.MessageBox]::Show("Only host/server nodes can be removed.", "Invalid Selection", "OK", "Warning")
|
|
return
|
|
}
|
|
|
|
# Save to settings
|
|
$settings | ConvertTo-Json | Set-Content $configPath
|
|
Refresh-ServerTree
|
|
} else {
|
|
[System.Windows.MessageBox]::Show("Only server/host nodes can be removed.", "Invalid Selection", "OK", "Warning")
|
|
}
|
|
})
|
|
|
|
# Handle Main -> SQL Servers menu click
|
|
$menuSqlServers.Add_Click({
|
|
$script:CurrentView = "SQL"
|
|
$grpDirectory.Header = "SQL Server Directory"
|
|
Write-Log "Switched view to SQL Server Directory."
|
|
Refresh-ServerTree
|
|
})
|
|
|
|
# Handle Main -> Host Splunk menu click
|
|
$menuHostSplunk.Add_Click({
|
|
$script:CurrentView = "Splunk"
|
|
$grpDirectory.Header = "Splunk Hosts Directory"
|
|
Write-Log "Switched view to Splunk Observability Cloud."
|
|
|
|
# Fetch Splunk Hosts dynamically from API (or fallback/demo)
|
|
$script:SplunkHosts = Get-SplunkAPIHosts
|
|
Refresh-ServerTree
|
|
})
|
|
|
|
# 5. Connection logic
|
|
$btnConnect.Add_Click({
|
|
$server = $txtServerName.Text
|
|
if ([string]::IsNullOrWhiteSpace($server)) {
|
|
Write-Log "Error: Please enter a server name."
|
|
return
|
|
}
|
|
|
|
$btnConnect.IsEnabled = $false
|
|
Write-Log "Attempting to connect to $server..."
|
|
|
|
# Save last server
|
|
$settings.LastServer = $server
|
|
$settings | ConvertTo-Json | Set-Content $configPath
|
|
|
|
try {
|
|
$result = Test-SQLConnection -ServerInstance $server
|
|
|
|
if ($result.Success) {
|
|
Write-Log "Connected successfully to $($result.Instance)!"
|
|
$dgServerInfo.ItemsSource = @([PSCustomObject]$result | Select-Object Instance, Version, Edition, Status)
|
|
|
|
# Fetch databases list using Get-SQLDatabases
|
|
Write-Log "Fetching databases list for $server..."
|
|
try {
|
|
$dbs = Get-SQLDatabases -ServerInstance $server
|
|
if ($dbs -and $dbs.Count -gt 0) {
|
|
$dgDatabases.ItemsSource = @($dbs)
|
|
$result.Databases = $dbs
|
|
Write-Log "Loaded $($dbs.Count) databases."
|
|
} else {
|
|
$dgDatabases.ItemsSource = $null
|
|
$result.Databases = $null
|
|
Write-Log "No databases found on $server."
|
|
}
|
|
} catch {
|
|
Write-Log "Warning: Could not load databases: $($_.Exception.Message)"
|
|
$dgDatabases.ItemsSource = $null
|
|
$result.Databases = $null
|
|
}
|
|
|
|
# Fetch errors from the last hour using Get-SQLErrors
|
|
Write-Log "Fetching error logs (last hour) for $server..."
|
|
try {
|
|
$errors = Get-SQLErrors -ServerInstance $server
|
|
if ($errors -and $errors.Count -gt 0) {
|
|
$dgServerErrors.ItemsSource = @($errors)
|
|
$result.Errors = $errors
|
|
Write-Log "Loaded $($errors.Count) error log entries."
|
|
} else {
|
|
$dgServerErrors.ItemsSource = $null
|
|
$result.Errors = $null
|
|
Write-Log "No errors found in the last hour."
|
|
}
|
|
} catch {
|
|
Write-Log "Warning: Could not load error log: $($_.Exception.Message)"
|
|
$dgServerErrors.ItemsSource = $null
|
|
$result.Errors = $null
|
|
}
|
|
|
|
# Cache successful connection state with its fetched errors and databases
|
|
$script:ServerConnectionStates[$server] = $result
|
|
} else {
|
|
Write-Log "Connection failed: $($result.Error)"
|
|
$script:ServerConnectionStates[$server] = $null
|
|
$dgServerInfo.ItemsSource = $null
|
|
$dgDatabases.ItemsSource = $null
|
|
$dgServerErrors.ItemsSource = $null
|
|
}
|
|
} catch {
|
|
Write-Log "Critical Error: $($_.Exception.Message)"
|
|
$script:ServerConnectionStates[$server] = $null
|
|
$dgServerInfo.ItemsSource = $null
|
|
$dgDatabases.ItemsSource = $null
|
|
$dgServerErrors.ItemsSource = $null
|
|
} finally {
|
|
$btnConnect.IsEnabled = $true
|
|
}
|
|
})
|
|
|
|
# Startup Dependency Check and Tree Initialization
|
|
try {
|
|
Import-DBAToolBoxDependencies
|
|
Write-Log "DBAToolBox started. dbatools module loaded."
|
|
Refresh-ServerTree
|
|
} catch {
|
|
[System.Windows.MessageBox]::Show($_.Exception.Message, "Dependency Error", "OK", "Error")
|
|
$Window.Close()
|
|
}
|
|
|
|
# 6. Show Window
|
|
$Window.ShowDialog() | Out-Null |