We can use PowerShell remoting to collect data from a bunch of servers. Powershell provides a rich set of cmdlets that can be used to gather information from remote servers. Here are some common ways of collecting metrics:
Use WMI (Windows Management Instrumentation):
$servers = "Server1", "Server2" foreach ($server in $servers) { $cpuLoad = Get-WmiObject win32_processor -ComputerName $server | Measure-Object -property LoadPercentage -Average | Select Average Write-Output "Average CPU Load for $server: $($cpuLoad.Average)%" }
Use CIM (Common Information Model): It's a newer alternative to WMI.
$servers = "Server1", "Server2" foreach ($server in $servers) { $session = New-CimSession -ComputerName $server $os = Get-CimInstance -CimSession $session -ClassName Win32_OperatingSystem Write-Output "Free Space on $server: $($os.FreePhysicalMemory) MB" Remove-CimSession -CimSession $session }
Performance Counters: You can fetch performance metrics using performance counters.
$servers = "Server1", "Server2" foreach ($server in $servers) { $counter = "\Processor(_Total)\% Processor Time" $cpuUsage = Get-Counter -ComputerName $server -Counter $counter Write-Output "CPU usage on $server: $($cpuUsage.CounterSamples.CookedValue) %" }
Remote Sessions with
Invoke-Command
: If you need to run a specific script on a remote server and gather its output.$servers = "Server1", "Server2" $scriptBlock = { # This block will be executed on the remote server. return Get-Process | Where-Object {$_.CPU -gt 10} } foreach ($server in $servers) { $highCpuProcesses = Invoke-Command -ComputerName $server -ScriptBlock $scriptBlock Write-Output "Processes with high CPU on $server:" $highCpuProcesses | ForEach-Object {Write-Output $_.Name} }
As you can see there are multiple ways of doingt the same thing when it comes to remote communication.
In this tiny project, I would like to propose a different approach which might be less common but I found it to be very pleasing. In this approach, we'll have a web server with a set of APIs that allows our script from the remote host to interact with it. In the methodology, the scripts are scheduled to run locally or remotely on a bunch of machines and the collected outputs are to be sent and processed by our server.
PowerShell can be used to create a simple web server, primarily through the use of the 'httpListener' class available in .NET.
Add-Type -AssemblyName System.Net.HttpListener
$listener = New-Object System.Net.HttpListener
$listener.Prefixes.Add("http://*:8080/")
$listener.Start()
Write-Host "Listening at http://localhost:8080/"
# Database file path
$dbPath = "database.json"
# Ensure the JSON file exists and initialize if not
if (-not (Test-Path $dbPath)) {
@() | ConvertTo-Json | Out-File $dbPath
}
while ($listener.IsListening) {
$context = $listener.GetContext()
$request = $context.Request
$response = $context.Response
$output = ""
# Load the data from the JSON file
$db = @(Get-Content $dbPath | ConvertFrom-Json)
switch ($request.Url.AbsolutePath) {
"/create" {
$computerName = $request.QueryString["computerName"]
$CPUUsage = $request.QueryString["CPUUsage"]
$MemoryUsage = $request.QueryString["MemoryUsage"]
$currentDateTime = $request.QueryString["currentDateTime"]
$newEntry = @{
computerName = $computerName
CPUUsage = $CPUUsage
MemoryUsage = $MemoryUsage
currentDateTime = $currentDateTime
}
$db += $newEntry
$db | ConvertTo-Json | Out-File $dbPath
$output = "Added entry for $computerName at $currentDateTime"
}
"/read" {
$computerName = $request.QueryString["computerName"]
$entries = $db | Where-Object { $_.computerName -eq $computerName }
$output = $entries | ConvertTo-Json
}
"/delete" {
$computerName = $request.QueryString["computerName"]
$currentDateTime = $request.QueryString["currentDateTime"]
$db = $db | Where-Object { $_.computerName -ne $computerName -or $_.currentDateTime -ne $currentDateTime }
$db | ConvertTo-Json | Out-File $dbPath
$output = "Deleted entry for $computerName at $currentDateTime"
}
default {
$output = "Invalid request!"
}
}
$buffer = [System.Text.Encoding]::UTF8.GetBytes($output)
$response.ContentLength64 = $buffer.Length
$response.OutputStream.Write($buffer, 0, $buffer.Length)
$response.OutputStream.Close()
}
# To stop, use: $listener.Stop()
Let's break down the script section by section:
- Loading Required Libraries:
Add-Type -AssemblyName System.Net.HttpListener
This command loads the System.Net
.HttpListener
.NET assembly into the current PowerShell session. This assembly provides the capabilities to create a simple HTTP server.
- Setting Up the HTTP Listener:
$listener = New-Object System.Net.HttpListener
$listener.Prefixes.Add("http://*:8080/")
$listener.Start()
$listener = New-Object
System.Net
.HttpListener
: Creates a new instance of the HttpListener class.$listener.Prefixes.Add("http://*:8080/")
: Specifies the URLs that the HttpListener object will listen on. In this case, it's listening on all network interfaces (*
) on port8080
.$listener.Start()
: Starts the HttpListener.
- Message Indicating Server Startup:
Write-Host "Listening at http://localhost:8080/"
This just prints a message indicating the URL where the server is listening.
- Database Initialization:
$dbPath = "database.json"
if (-not (Test-Path $dbPath)) {
@() | ConvertTo-Json | Out-File $dbPath
}
$dbPath = "database.json"
: Specifies the file path for the "database" (a simple JSON file in this case).The
if
block checks if thedatabase.json
file doesn't exist, and if it doesn't, it creates an empty JSON array (@()
represents an empty array) and writes it to the file.
- HTTP Request Handling Loop:
while ($listener.IsListening) {
...
}
The code within this while
loop handles incoming HTTP requests as long as the listener is running.
- Retrieve HTTP Context, Request, and Response Objects:
$context = $listener.GetContext()
$request = $context.Request
$response = $context.Response
$output = ""
This code retrieves the context of an incoming request (GetContext()
), and from that context, it gets the request and response objects. $output
is initialized to an empty string and will be used to store the response text.
- Load Data from JSON "Database":
$db = @(Get-Content $dbPath | ConvertFrom-Json)
This line reads the content of the database.json
file, converts the JSON data to a PowerShell object (or array of objects), and stores it in the $db
variable.
- Handle Different URL Paths (CRUD operations): The
switch
statement evaluates the URL path of the incoming request to determine what action to take (Create, Read, Delete).
For example:
/create: Retrieves computer data from the query parameters, adds a new entry to the
$db
array, and writes the updated data back to thedatabase.json
file./read: Retrieves all entries that match a given computer name and returns them as a JSON string.
/delete: Removes entries that match a given computer name and date/time from the
$db
array and updates thedatabase.json
file.
- Send Response Back to Client:
$buffer = [System.Text.Encoding]::UTF8.GetBytes($output)
$response.ContentLength64 = $buffer.Length
$response.OutputStream.Write($buffer, 0, $buffer.Length)
$response.OutputStream.Close()
The response data ($output
) is converted to a byte array and then written to the response stream. After that, the response stream is closed, sending the data back to the client.
- Stopping the HTTP Listener:
# To stop, use: $listener.Stop()
This is a comment indicating how you can stop the listener if needed. If you're running the script interactively, you can stop the listener by executing $listener.Stop()
in the same session.
Now lets see how we can interact with our API:
Of course! The Invoke-RestMethod
cmdlet automatically parses JSON content, which makes it more suited for REST APIs. Here's how you can use Invoke-RestMethod
for the CRUD operations:
Create:
- Adds a new computer entry with its respective data.
$uri = "http://localhost:8080/create?computerName=SERVER01&CPUUsage=50&MemoryUsage=2048¤tDateTime=2023-04-01T15:45:00"
$result = Invoke-RestMethod -Uri $uri -Method Get
$result
Read:
- Retrieves all entries associated with a given computer name.
$uri = "http://localhost:8080/read?computerName=SERVER01"
$entries = Invoke-RestMethod -Uri $uri -Method Get
$entries
Delete:
- Deletes a specific entry of a computer based on its timestamp.
$uri = "http://localhost:8080/delete?computerName=SERVER01¤tDateTime=2023-04-01T15:45:00"
$result = Invoke-RestMethod -Uri $uri -Method Get
$result
Using Invoke-RestMethod
, the results are automatically parsed from JSON (if the response content type is application/json
). This means that, for instance, when you retrieve entries with the Read example, $entries
will contain the actual PowerShell objects instead of a raw JSON string.
Let's update our API to porvide JSON responses. The core idea for making the responses JSON-based is to always return JSON strings as the output, even for the success and error messages.
Add-Type -AssemblyName System.Net.HttpListener
$listener = New-Object System.Net.HttpListener
$listener.Prefixes.Add("http://*:8080/")
$listener.Start()
Write-Host "Listening at http://localhost:8080/"
# Database file path
$dbPath = "database.json"
# Ensure the JSON file exists and initialize if not
if (-not (Test-Path $dbPath)) {
@() | ConvertTo-Json | Out-File $dbPath
}
while ($listener.IsListening) {
$context = $listener.GetContext()
$request = $context.Request
$response = $context.Response
$output = @{}
# Load the data from the JSON file
$db = @(Get-Content $dbPath | ConvertFrom-Json)
switch ($request.Url.AbsolutePath) {
"/create" {
$computerName = $request.QueryString["computerName"]
$CPUUsage = $request.QueryString["CPUUsage"]
$MemoryUsage = $request.QueryString["MemoryUsage"]
$currentDateTime = $request.QueryString["currentDateTime"]
$newEntry = @{
computerName = $computerName
CPUUsage = $CPUUsage
MemoryUsage = $MemoryUsage
currentDateTime = $currentDateTime
}
$db += $newEntry
$db | ConvertTo-Json | Out-File $dbPath
$output = @{ message = "Added entry for $computerName at $currentDateTime" }
}
"/read" {
$computerName = $request.QueryString["computerName"]
$entries = $db | Where-Object { $_.computerName -eq $computerName }
$output = @{ data = $entries }
}
"/delete" {
$computerName = $request.QueryString["computerName"]
$currentDateTime = $request.QueryString["currentDateTime"]
$db = $db | Where-Object { $_.computerName -ne $computerName -or $_.currentDateTime -ne $currentDateTime }
$db | ConvertTo-Json | Out-File $dbPath
$output = @{ message = "Deleted entry for $computerName at $currentDateTime" }
}
default {
$output = @{ error = "Invalid request!" }
}
}
$jsonOutput = $output | ConvertTo-Json
$buffer = [System.Text.Encoding]::UTF8.GetBytes($jsonOutput)
$response.ContentType = "application/json"
$response.ContentLength64 = $buffer.Length
$response.OutputStream.Write($buffer, 0, $buffer.Length)
$response.OutputStream.Close()
}
# To stop, use: $listener.Stop()
Key changes:
The
$output
variable is now always a hashtable (@{}
) so it can be easily converted to JSON.Messages, data, and errors are structured into this hashtable to create a consistent JSON response structure.
Added
$response.ContentType = "application/json"
to set the content type of the HTTP response to JSON.The actual JSON response is generated with
$jsonOutput = $output | ConvertTo-Json
before sending it back to the client.
Now, all responses from this server will be in JSON format.