PowerShell Tiny Project 10 - API Building & Interactions

PowerShell Tiny Project 10 - API Building & Interactions

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:

  1. 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)%"
     }
    
  2. 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
     }
    
  3. 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) %"
     }
    
  4. 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:

  1. 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.

  1. 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 port 8080.

  • $listener.Start(): Starts the HttpListener.

  1. Message Indicating Server Startup:
Write-Host "Listening at http://localhost:8080/"

This just prints a message indicating the URL where the server is listening.

  1. 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 the database.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.

  1. HTTP Request Handling Loop:
while ($listener.IsListening) {
    ...
}

The code within this while loop handles incoming HTTP requests as long as the listener is running.

  1. 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.

  1. 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.

  1. 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 the database.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 the database.json file.

  1. 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.

  1. 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:

  1. Create:

    • Adds a new computer entry with its respective data.
    $uri = "http://localhost:8080/create?computerName=SERVER01&CPUUsage=50&MemoryUsage=2048&currentDateTime=2023-04-01T15:45:00"
    $result = Invoke-RestMethod -Uri $uri -Method Get
    $result
  1. 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
  1. Delete:

    • Deletes a specific entry of a computer based on its timestamp.
    $uri = "http://localhost:8080/delete?computerName=SERVER01&currentDateTime=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:

  1. The $output variable is now always a hashtable (@{}) so it can be easily converted to JSON.

  2. Messages, data, and errors are structured into this hashtable to create a consistent JSON response structure.

  3. Added $response.ContentType = "application/json" to set the content type of the HTTP response to JSON.

  4. 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.

Did you find this article valuable?

Support Application Support by becoming a sponsor. Any amount is appreciated!