Start-AzureLocalServer.ps1
|
<# .SYNOPSIS Start the Azure Local Inventory web dashboard. .DESCRIPTION Launches a local HTTP server on the specified port and opens a browser-ready dashboard for exploring Azure Local (Azure Stack HCI) inventory. The dashboard provides authentication flow, WAF assessment, cost analysis, and executive PDF export. .PARAMETER Port TCP port for the local web server. Defaults to 8081. .EXAMPLE Start-AzureLocalServer Starts the dashboard on the default port (8081). .EXAMPLE Start-AzureLocalServer -Port 9090 Starts the dashboard on port 9090. #> function Start-AzureLocalServer { [CmdletBinding()] param( [int]$Port = 8081 ) $moduleRoot = $PSScriptRoot Write-Host "🚀 Starting Azure Local Inventory Server..." -ForegroundColor Cyan Write-Host "✓ PowerShell Version: $($PSVersionTable.PSVersion)" -ForegroundColor Green # Check Azure connection (private helper) function Test-AzureConnection { try { $context = Get-AzContext if ($null -eq $context) { return $false } return $true } catch { return $false } } # Server-scoped state $isAuthenticated = Test-AzureConnection $inventoryData = @{} $lastUpdate = $null $wafConfig = $null # Load WAF Configuration $wafConfigPath = Join-Path $moduleRoot "waf-config.json" if (Test-Path $wafConfigPath) { try { $wafConfig = Get-Content $wafConfigPath -Raw | ConvertFrom-Json Write-Host "✓ WAF Configuration loaded (Version: $($wafConfig.version))" -ForegroundColor Green } catch { Write-Host "⚠️ Failed to load WAF configuration: $($_.Exception.Message)" -ForegroundColor Yellow } } else { Write-Host "⚠️ WAF configuration file not found at: $wafConfigPath" -ForegroundColor Yellow } Write-Host "🔐 Azure Authentication Status: $(if ($isAuthenticated) { 'Connected ✓' } else { 'Not Connected ✗' })" -ForegroundColor $(if ($isAuthenticated) { 'Green' } else { 'Yellow' }) # HTTP Listener $listener = New-Object System.Net.HttpListener $listener.Prefixes.Add("http://localhost:$Port/") $listener.Start() Write-Host "🌐 Server started at http://localhost:$Port" -ForegroundColor Green Write-Host "📊 Access the Azure Local Inventory Dashboard in your browser" -ForegroundColor Cyan Write-Host "Press Ctrl+C to stop the server" -ForegroundColor Gray Write-Host "" # CancellationTokenSource used by the Ctrl+C handler to signal shutdown $cts = [System.Threading.CancellationTokenSource]::new() # Register Ctrl+C so it stops the listener gracefully instead of hard-killing $cancelHandler = { param($sender, $args) $args.Cancel = $true # prevent the process from being terminated immediately $cts.Cancel() $listener.Stop() } [Console]::add_CancelKeyPress($cancelHandler) try { while ($listener.IsListening) { # GetContextAsync + 500 ms polling allows Ctrl+C to be processed # between requests rather than blocking indefinitely. $task = $listener.GetContextAsync() try { while (-not $task.Wait(500)) { if ($cts.IsCancellationRequested) { break } } } catch { } if ($cts.IsCancellationRequested -or -not $listener.IsListening) { break } $context = $task.Result $request = $context.Request $response = $context.Response $path = $request.Url.AbsolutePath $method = $request.HttpMethod Write-Host "$(Get-Date -Format 'HH:mm:ss') $method $path" -ForegroundColor Gray # Content type and response $content = "" $contentType = "text/html; charset=utf-8" # Route handling switch -Regex ($path) { '^/$' { # Serve main page $indexPath = Join-Path $moduleRoot "index.html" if (Test-Path $indexPath) { $content = Get-Content $indexPath -Raw } else { $content = "<html><body><h1>Azure Local Inventory Server</h1><p>index.html not found</p></body></html>" } } '^/styles\.css$' { $cssPath = Join-Path $moduleRoot "styles.css" if (Test-Path $cssPath) { $content = Get-Content $cssPath -Raw $contentType = "text/css" } } '^/app\.js$' { $jsPath = Join-Path $moduleRoot "app.js" if (Test-Path $jsPath) { $content = Get-Content $jsPath -Raw $contentType = "application/javascript" } } '^/api/auth/status$' { $isAuthenticated = Test-AzureConnection $authStatus = @{ authenticated = $isAuthenticated context = if ($isAuthenticated) { $ctx = Get-AzContext @{ account = $ctx.Account.Id subscription = $ctx.Subscription.Name tenant = $ctx.Tenant.Id } } else { $null } } $content = $authStatus | ConvertTo-Json $contentType = "application/json" } '^/api/auth/login$' { try { Connect-AzAccount -UseDeviceAuthentication | Out-Null $isAuthenticated = $true $content = @{ success = $true; message = "Authentication successful" } | ConvertTo-Json } catch { $content = @{ success = $false; message = $_.Exception.Message } | ConvertTo-Json } $contentType = "application/json" } '^/api/waf/config$' { # Serve WAF configuration try { if ($null -ne $wafConfig) { Write-Host " 📋 Serving WAF configuration" -ForegroundColor Cyan $content = $wafConfig | ConvertTo-Json -Depth 20 -Compress:$false Write-Host " ✅ WAF config sent ($(($content.Length / 1KB).ToString('N2')) KB)" -ForegroundColor Green } else { Write-Host " ⚠️ WAF configuration not available" -ForegroundColor Yellow $response.StatusCode = 503 $content = @{ error = "WAF configuration not loaded" } | ConvertTo-Json } } catch { Write-Host " ❌ Error serving WAF config: $($_.Exception.Message)" -ForegroundColor Red $response.StatusCode = 500 $content = @{ error = $_.Exception.Message } | ConvertTo-Json } $contentType = "application/json" } '^/api/inventory/progress$' { # Return current inventory collection progress try { $progress = Get-CollectionProgress $content = $progress | ConvertTo-Json } catch { $response.StatusCode = 500 $content = @{ message = "Initializing..." percent = 0 lastUpdate = (Get-Date).ToString('o') error = $_.Exception.Message } | ConvertTo-Json } $contentType = "application/json" } '^/api/inventory/data$' { if ($isAuthenticated) { try { Write-Host " 📊 Collecting Azure Local inventory..." -ForegroundColor Cyan # Reset progress before starting Update-CollectionProgress "Starting inventory collection..." 0 $inventoryData = Get-AzureLocalInventory $lastUpdate = Get-Date $content = $inventoryData | ConvertTo-Json -Depth 10 } catch { $content = @{ error = $_.Exception.Message } | ConvertTo-Json } } else { $response.StatusCode = 401 $content = @{ error = "Not authenticated" } | ConvertTo-Json } $contentType = "application/json" } '^/api/inventory/refresh$' { if ($isAuthenticated) { try { Write-Host " 🔄 Refreshing inventory..." -ForegroundColor Cyan # Reset progress before starting Update-CollectionProgress "Starting inventory refresh..." 0 $inventoryData = Get-AzureLocalInventory $lastUpdate = Get-Date $content = @{ success = $true lastUpdate = $lastUpdate.ToString('o') } | ConvertTo-Json } catch { $content = @{ success = $false; error = $_.Exception.Message } | ConvertTo-Json } } else { $response.StatusCode = 401 $content = @{ success = $false; error = "Not authenticated" } | ConvertTo-Json } $contentType = "application/json" } default { $response.StatusCode = 404 $content = "<html><body><h1>404 - Not Found</h1></body></html>" } } # Send response $buffer = [System.Text.Encoding]::UTF8.GetBytes($content) $response.ContentLength64 = $buffer.Length $response.ContentType = $contentType $response.Headers.Add("Access-Control-Allow-Origin", "*") $response.OutputStream.Write($buffer, 0, $buffer.Length) $response.Close() } } catch [System.Net.HttpListenerException] { # Thrown when the listener is stopped while GetContextAsync is waiting — expected on Ctrl+C. } catch { Write-Host "❌ Server error: $($_.Exception.Message)" -ForegroundColor Red } finally { Write-Host "`n🛑 Stopping server..." -ForegroundColor Yellow [Console]::remove_CancelKeyPress($cancelHandler) if ($listener.IsListening) { $listener.Stop() } $listener.Close() $cts.Dispose() Write-Host "✓ Server stopped" -ForegroundColor Green } } |