Getting Started - Utilizing the Web: Part 3 (More Invoke-RestMethod)
Welcome to my Getting Started with Windows PowerShell series!
In case you missed the earlier posts, you can check them out here:
- Customizing your environment
- Command discovery
- Using the ISE and basic function creation
- A deeper dive into functions
- Loops
- Modules
- Help
- Accepting pipeline input
- Strings
- Jobs
- Error handling
- Creating Custom Objects
- Working with data
- Utilizing the Web: Part 1 (Invoke-WebRequest)
- Utilizing the Web: Part 2 (Invoke-RestMethod)
We will be exploring:
- A continuation of using Invoke-RestMethod
- API key in header (LIFX API)
- Sending data to a REST API via Body (LIFX API)
- Username/Password in request URL (PRTG API)
- Sending data via URL (PRTG API)
- Homework
Keep an eye out for part 4!
Part 4 will contain:
- Controlling the Internet Explorer COM Object
- Navigating to pages
- Logging in to pages
Continuing With Invoke-RestMethod
In part 2, we went over Invoke-RestMethod's basics, including:
In this post, I will be going over some more ways to use Invoke-RestMethod to authenticate to different APIs. I will also go over how to send information to the API, and work with the results we get back.
API Key In Header
Some APIs will require you to authenticate with a key in the header information. I happen to own a LIFX light, and their API uses that method of authentication.
If you own a LIFX light, and want to follow along, you can get a personal access token here: https://cloud.lifx.com/settings.
Information recap:
Now let's get to work! I have my key, which let's just say is: 'YouNeedYourOwnKey123321$'
Let's store that in a variable.
$apiKey = 'YouNeedYourOwnKey123321$'
The next step here is to store that key in the header information (as a hashtable), with the key name of "Authorization".
$headers = @{Authorization=("Bearer {0}" -f $apiKey)}
Let's take a peek at $headers to ensure it contains what we need.
Looks good! Now to check it out. The URL in the documentation to get the lights available is 'https://api.lifx.com/v1/lights/all'. I like to store information like this in variables.
$allURL = 'https://api.lifx.com/v1/lights/all'
Now to put it all to use! We'll be utilizing Invoke-RestMethod with the following parameters/arguments:
- Headers
- Since we already have our headers stored in a nifty hashtable, we'll simply specify $headers
- Uri
- We'll pass along $allURL, and attempt to get a list of available lights
Here's all of the code so far.
We'll be storing the results of the Invoke-RestMethod command in $ninjaLights.
$apiKey = 'YouNeedYourOwnKey123321$' $headers = @{Authorization=("Bearer {0}" -f $apiKey)} $allURL = 'https://api.lifx.com/v1/lights/all' $ninjaLights = Invoke-RestMethod -Headers $headers -Uri $allURL -ContentType 'application/json'
Success!
Sending Data to a REST API via Body (LIFX API)
Now that we've authenticated to LIFX, we can work on sending information over, and control the light. Here are the steps I generally follow when using an API for the first time:
- Review documentation
- Review documentation (This can't be emphasized enough)
- In this case, reviewing: https://api.developer.lifx.com/docs/set-state
- Test out a bunch of different ways to utilize the API
- Get errors, handle errors, see what's expected, clean up the code, and build a function
Here is a snippet from their documentation:
What that tells us is how the text is to be formatted (JSON), and that we'll be using the PUT method.
Here are the parameters they accept:
Let's get started by ensuring we have working code, and then build a function around it.
The URL is 'https://api.lifx.com/v1/lights/all/state', which we'll store in $lightStateURL. We'll also setup some variables for the accepted parameters, as follows:
- $state
- on or off, we'll choose on
- $color
- We'll set this to red
- $brightness
- We'll set this to .5, or 50%
- $duration
- This will be set to .0, which is indefinitely
After setting up these variables, we'll use a here-string to build the payload, and finally use Invoke-RestMethod with the following parameters/arguments:
- Uri
- The $lightStateURL variable
- Method
- We'll be using PUT, per their documentation
- Headers
- We'll use the headers from the Base64 example above
- Body
- We will set this to $payloadBuilder (which is the here-string that contains the parameters we're passing to the LIFX API)
Here is the full code so far to test this:
$apiKey = 'YouNeedYourOwnKey123321$' $headers = @{Authorization=("Bearer {0}" -f $apiKey)} $allURL = 'https://api.lifx.com/v1/lights/all' $lightStateURL = 'https://api.lifx.com/v1/lights/all/state' $state = 'on' $color = 'red' $brightness = 0.5 $duration = 0.0 $payload = [PSCustomObject] ${ power = $state, color = $color, brightness = $brightness, duration = $duration } $jsonPayload = $payload | ConvertTo-Json $setResults = Invoke-RestMethod -Uri $lightStateURL -Method Put -Headers $headers -Body $jsonPayload -ContentType 'application/json'
Let's go ahead and run that to see if it worked:
This is an easy one to verify!
We can also take a look at the results property of $setResults.
Now that we know that it works, it can be used in a function to control the light.
Here is the code for an example of how to build a function with this information:
$apiKey = 'YouNeedYourOwnKey123321' $headers = @{Authorization=("Bearer {0}" -f $apiKey)} $baseURL = 'https://api.lifx.com/v1/lights' function Set-LightState { #Begin function Set-LightState [cmdletbinding()] Param ( [Parameter(Mandatory)] $headers, [String] $state = 'on', [String] $color = 'White', [Double] $brightness = 0.5, [Double] $duration = 0.0 ) #Get lights $ninjaLights = Invoke-RestMethod -Headers $headers -Uri $allURL #If our light shows as connected... if ($ninjaLights.Connected) { #Set the selected as the ID of the light (per LIFX documentation) $selector = $ninjaLights.id #Construct the URL to include the selector and /state $fullURL = "$baseurl/$selector/state" #Build the payload $payload = [PSCustomObject] @{ power = $state color = $color brightness = $brightness duration = $duration } #Convert payload to JSON $jsonPayload = $payload | ConvertTo-Json Write-Host "Changing light from:" Write-Host `t"State : $($ninjaLights.Power)" Write-Host `t"Color : $($ninjaLights.Color)" Write-Host `t"Brightness: $($($ninjaLights.Brightness * 100))%"`n Write-Host "Changing light to:" Write-Host `t"State : $state" Write-Host `t"Color : $color" Write-Host `t"Brightness: $($brightness * 100)%" Write-Host `t"Duration : $duration"`n #Store results in $setResults $setResults = Invoke-RestMethod -Uri $fullURL -Method Put -Headers $headers -Body $jsonPayload -ContentType 'application/json' Write-Host "API status:" Write-Host `t"Light :" $setResults.results.label Write-Host `t"Status:" $setResults.results.status `n } } #End function Set-LightState Set-LightState -headers $headers -color blue
Let's see what happens when we run it!
(NOTE) I use Write-Host here merely as a tool to give you a visual of the data you are working with. Ultimately it would be best to create some custom objects, and return the results as you need to with the code you're writing.
The information here is provided to show you what you can do, how you can build a payload, and then work with the data you are returned. You can do anything you put your mind to, from silly things (the next example), to even using it with another script (that may get weather information), and then flash the light red a few times if there is a weather alert.
For this next example, I will utilize a loop, and our new function, to confuse my girlfriend. Well, with all the testing I've been doing... maybe not anymore ;)
Here's the code:
[array]$colors = @('white','red','orange','yellow','cyan','green','blue','purple','pink') $originalBrightness = $ninjaLights.Brightness $originalColor = $ninjalights.Color $originalState = $ninjalights.Power $colorString = "hue:" + $originalcolor.hue + " saturation:" + $originalcolor.saturation + " kelvin:" + $originalColor.Kelvin $i = 0 While ($i -le 20) { $color = Get-Random $colors [double]$brightness = "{0:n2}" -f (Get-Random -Maximum 1 -Minimum 0.00) Set-LightState -headers $headers -color $color -brightness $brightness -state $state Start-Sleep -seconds 1 $i++ } Set-LightState -headers $headers -state $originalState -color $colorString -brightness $originalBrightness
And off we go!
It worked!
At the end it will set the light back to the value it was discovered with:
If you'd like a deeper dive into this, check out my post on using the Lifx API with PowerShell, here: http://www.gngrninja.com/script-ninja/2016/2/6/powershell-control-your-lifx-light
Username/Password in Request URL (PRTG API)
Some APIs authenticate you via including a username/password (hopefully eventually password hash) in the request URL. PRTG has one of those APIs. PRTG is one of my favorite monitoring tools, as not only is it great out of the box, but it also has great synergy with PowerShell.
PRTG information:
Let's start by getting the credentials of the account you want to use with the API. This account will be an account that has access to do what you need to do in PRTG. I will use the PRTG admin account, but you'll want to ensure you use one setup just to use with the API.
$prtgCredential = Get-Credential
If you're demoing PRTG, and are using a self-signed cert, you'll need the following code to allow Invoke-RestMethod to work with the self-signed cert. This code will only affect your current session.
Add-Type @" using System.Net; using System.Security.Cryptography.X509Certificates; public class TemporarySelfSignedCert : ICertificatePolicy { public TemporarySelfSignedCert() {} public bool CheckValidationResult( ServicePoint sPoint, X509Certificate cert, WebRequest wRequest, int certProb) { return true; } } "@ [System.Net.ServicePointManager]::CertificatePolicy = New-Object TemporarySelfSignedCert
Now let's setup a variable that contains our PRTG hostname, and a custom object with information for the API URLs.
$hostName = 'localhost' $prtgInfo = [PSCustomObject]@{ BaseURL = "https://$hostName/api" SensorCountURL = "https://$hostName/api/table.xml?content=sensors&columns=sensor" }
Next, per PRTG's documenation, we'll construct the URL we need to use to get the password hash.
$getHashURL = "$($prtgInfo.BaseURL)/getpasshash.htm?username=$($prtgCredential.userName)&password=$($prtgCredential.GetNetworkCredential().Password)"
Now we can finally use Invoke-RestMethod to get the hash:
$getHash = Invoke-RestMethod -Uri $getHashURL
Let's make sure $getHash has our password hash.
Alright, got it! Now we can create a new PS Credential object to store our username, and password hash (instead of our password).
First. we'll convert the hash we created into a secure string. Then, we will create a new PS Credential object using the username we specified in $prtgCredential, and the secure string we just created as the password.
$passwordHash = ConvertTo-SecureString $getHash -AsPlainText -Force $prtgCredentialwHash = New-Object System.Management.Automation.PSCredential ($($prtgCredential.UserName), $passwordHash)
We can then verify the information by displaying the contents of $prtgCredentialwHash, and also using the GetNetworkCredential().Password method.
Now that we know $prtgCredentialwHash contains what we need, we can construct a URL to test out the API.
Let's set some variables up:
$prtgUser = $prtgCredentialwHash.UserName $prtgPass = $prtgCredentialwHash.GetNetworkCredential().Password $credentialAppend = "&username=$prtgUser&passhash=$prtgPass"
We can check the value of $credentialAppend to ensure it is correct:
Now to construct the URL, and test out the API via Invoke-RestMethod.
Invoke-RestMethod -uri ($prtgInfo.SensorCountURL + $credentialAppend)
Note: The full URL that is constructed is (I cut out my actual password hash on purpose, you'll see your hash after the = if all went well):
If all went well, you will see the results of the request (if not, you'll see an unauthorized message).
Success!
Sending Data via URL (PRTG API)
The PRTG API accepts data in the URL of the request. The below example will pause a sensor for a specific duration, with a specific message:
#Set the $duration (in minutes) $duration = 10 #Set $sensorID to the sensor we want to pause $sensorID = 45 #Set the pause message $message = "Paused via PS PRTG" #Construct the $pauseURL $pauseURL = "$($prtgInfo.baseURL)/pauseobjectfor.htm?id=$sensorID&pausemsg=$message&duration=$duration" #Use Invoke-RestMethod with the $pauseURL + $credentialAppend #We will always append $credentialAppend as this is how the API accepts authentication Invoke-RestMethod -Uri ($pauseURL + $credentialAppend)
Awesome, it worked!
Mega Example With Concept Code
This last example contains some concept code for a project I'm working on. Feel free to judge and use it as you wish, however I will note now that it is nowhere near finalized. I'm still in the exploration, see what's possible, and try to get it all to work phase.
Requirements for concept code to work:
- PRTG Installaction
- Folder structure
- Module
- NinjaLogging.psm1 (included in ZIP file below)
NOTE: I have not fully cleaned up or officially released any of the code yet. That includes the logging module.
Here is my TODO list for the code:
- Upload it to GitHub
- Clean up/add error handling
- Create readme files
- Add parameter sets to some of the psprtg.ps1 functions
- Fully convert psprtg.ps1 to a module
- Add functionality to psprtg.ps1 to include an 'oh crap!' undo feature
- This will include exporting a custom object with the device ID and action taken
With that said, here is a link to download it (or you can skip to the code if that's all you want to see):
How to get it working/examples:
- Import the script as a module via Import-Module .\psprtg.ps1
- The first time you do this, it will ask for your PRTG credentials. The module will then export the credential object to the input folder, and then use that exported credential the next time the module is used
- Contents of machine/user encrypted credential file stored in the input directory:
- Find a device and pause it via Invoke-SensorModification -findDevice
- Note: You can also specify -Duration and -Message
- Use a text file list of devices to find and take an action on
Pausing example:
Resuming example:
Log File Contents:
Code
psprtg.ps1
#Setup $scriptPath = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition $moduleDir = "$scriptPath\modules" $logDir = "$scriptPath\logs" $outputDir = "$scriptPath\output" $inputDir = "$scriptPath\input" $hashFile = "$inputDir\prtgCredentialwHash.xml" $hostName = 'localhost' $modules = ('ninjaLogging') $prtgInfo = [PSCustomObject]@{ BaseURL = "https://$hostName/api" SensorCountURL = "https://$hostName/api/table.xml?content=sensors&columns=sensor" } function Invoke-ModuleImport { #Begin function Invoke-ModuleImport [cmdletbinding()] param($modules) Push-Location ForEach ($module in $modules) { if (Get-Module -ListAvailable -Name $module) { Import-Module $module } elseif (!(Get-Module -Name $module)) { Write-Verbose "Importing module: [$module] from: [$moduleDir\$module]" Import-Module "$moduleDir\$module" } Else { Write-Verbose "Module [$module] already loaded!" } } Pop-Location } #End function Invoke-ModuleImport function Invoke-CredentialCheck { #Begin function Invoke-CredentialCheck [cmdletbinding()] Param() Try { #Begin try for credential path existence #Check if our hashed credential object exists if (Test-Path $hashFile) { #Begin if for testing credential path existence #Import credentials from hash file Write-LogFile -logPath $logFile -logValue "Importing credentials from [$hashFile]" -Verbose $prtgCredentialwHash = Import-Clixml $hashFile #Add to info hash $prtgInfo | Add-Member -Type NoteProperty -Name Credentials -Value $prtgCredentialwHash #create credential string for URLs $prtgUser = $prtgCredentialwHash.UserName $prtgPass = $prtgCredentialwHash.GetNetworkCredential().Password $credentialAppend = "&username=$prtgUser&passhash=$prtgPass" Return $credentialAppend #If it doesn't exist, attempt to create it } else { Write-Host "We need to get your hashed password to use with the API!" `n Write-Host "Please enter your PRTG credentials"`n #Store credential in $prtgCredential $prtgCredential = Get-Credential #Get the password hash via ConvertTo-SecureString and the Get-PRTGPasswordHash function. #Note: If you are not using a self-signed certificate (hopefully not, but if it is a demo/test install you may be): use -selfSignedCert:$True $passwordHash = ConvertTo-SecureString "$(Get-PRTGPasswordHash -PRTGCredential $prtgCredential -selfSignedCert:$True)" -AsPlainText -Force #Create new credential object and export it to the input folder #This allows us to use it when the script runs $prtgCredentialwHash = New-Object System.Management.Automation.PSCredential ($($prtgCredential.UserName), $passwordHash) Write-LogFile -logPath $logFile -logValue "Exporting hashed credentials to [$hashFile]." -Verbose #Export to file $prtgCredentialwHash | Export-Clixml $hashFile #create credential string for URLs $prtgUser = $prtgCredentialwHash.UserName $prtgPass = $prtgCredentialwHash.GetNetworkCredential().Password $credentialAppend = "&username=$prtgUser&passhash=$prtgPass" Return $credentialAppend } #End if for testing credential path existence } #End try for credential path existence Catch { $errorMessage = $_.Exception.Message Write-LogFileError -logPath $logFile -errorDesc "Error while checking for credentials/importing credentials: [$errorMessage]!" #Resolve the log file Resolve-LogFile -logPath $logFile Break } } #End function Invoke-CredentialCheck function Get-PRTGPasswordHash { #Begin function Get-PRTGPasswordHash [cmdletbinding()] param( [Parameter(Mandatory)] [PSCredential] $PRTGCredential, [Parameter()] [Boolean] $selfSignedCert = $false ) if ($selfSignedCert) { Try { Add-Type @" using System.Net; using System.Security.Cryptography.X509Certificates; public class TemporarySelfSignedCert : ICertificatePolicy { public TemporarySelfSignedCert() {} public bool CheckValidationResult( ServicePoint sPoint, X509Certificate cert, WebRequest wRequest, int certProb) { return true; } } "@ [System.Net.ServicePointManager]::CertificatePolicy = New-Object TemporarySelfSignedCert } Catch { $errorMessage = $_.Exception.Message Write-LogFileError -logPath $logFile -errorDesc "Error while allowing self signed certs: [$errorMessage]" -ForegroundColor Red -BackgroundColor DarkBlue #Resolve the log file Resolve-LogFile -logPath $logFile Break } } Try { $getHashURL = "$($prtgInfo.BaseURL)/getpasshash.htm?username=$($prtgCredential.userName)&password=$($prtgCredential.GetNetworkCredential().Password)" $getHash = Invoke-RestMethod -Uri $getHashURL Return $getHash } Catch { $errorMessage = $_.Exception.Message Write-LogFileError -logPath $logFile -errorDesc "Error while getting hash: [$errorMessage]" -Verbose Break } } #End function Get-PRTGPasswordHash function Invoke-DeviceSearch { #Begin function Invoke-DeviceSearch [cmdletbinding()] Param( [Parameter(Mandatory)] $findMe ) [System.Collections.ArrayList]$foundDeviceIDs = @() ForEach ($find in $findMe) { Try { $getDeviceURL = "$($prtgInfo.BaseURL)/table.json?content=devices&output=json&columns=objid,probe,group,device,host,downsens,partialdownsens,downacksens,upsens,warnsens,pausedsens,unusualsens,undefinedsens" $devices = Invoke-RestMethod -Uri ($getDeviceURL + $credentialAppend) $findDeviceWildCard = '*' + $find + '*' ForEach ($device in $devices.devices) { Switch ($device.device) { {$_ -like $findDeviceWildCard} { $foundDeviceIDs.Add($device.objid) | Out-Null } } } } Catch { $errorMessage = $_.Exception.Message Write-LogFileError -logPath $logFile -errorDesc "Unable to get device list: [$errorMessage]" -Verbose } } Return $foundDeviceIDs } #End function Invoke-DeviceSearch function Invoke-SensorModification { #Begin function Invoke-SensorModification [cmdletbinding()] Param( [parameter(Mandatory)] [string] $action, [parameter()] [string] $findDevice, [Parameter()] [int] $sensorID, [Parameter()] [String] $message, [Parameter()] [int] $duration, [Parameter()] $List ) $logFile = New-logFile -logPath $logDir -logName 'PSPRTG.log' -addDate:$true if (!$duration) { $duration = 30 } if (!$message) { $message = "Sensor paused by: [$((Get-ChildItem Env:\USERNAME).Value)] via PS PRTG." } if ($List) { Switch ($List | Get-Member | Select-Object -ExpandProperty TypeName -Unique) { {$_ -eq 'System.String'} { if ($list -like '*.txt') { Write-Host "File detected" $deviceList = Get-Content $list $foundDeviceIDs = Invoke-DeviceSearch -findMe $deviceList } else { Write-Host 'Single string or array detected' $deviceList = $list $foundDeviceIDs = Invoke-DeviceSearch -findMe $deviceList } } {$_ -eq 'System.Management.Automation.PSCustomObject'} { Write-Host 'Custom Object detected' } } } if ($findDevice) { Try { $foundDeviceIDs = Invoke-DeviceSearch -findMe $findDevice } Catch { $errorMessage = $_.Exception.Message Write-LogFileError -logPath $logFile -errorDesc "Unable to get device list: [$errorMessage]" -Verbose #Resolve the log file Resolve-LogFile -logPath $logFile Break } } Switch ($action) { #Begin switch for PRTG action {$_ -eq 'Pause'} { if ($findDevice -or $List) { ForEach ($id in $foundDeviceIDs) { $sensorID = $null $sensorID = $id $pauseURL = "$($prtgInfo.baseURL)/pauseobjectfor.htm?id=$sensorID&pausemsg=$message&duration=$duration" Write-LogFile -logPath $logFile -Value "Attempting to pause sensor ID: [$sensorID], for duration of [$duration], with message [$message]" -Verbose $pauseAttempt = Invoke-WebRequest -Uri ($pauseURL + $credentialAppend) } #Resolve the log file Resolve-LogFile -logPath $logFile } Else { $pauseURL = "$($prtgInfo.baseURL)/pauseobjectfor.htm?id=$sensorID&pausemsg=$message&duration=$duration" Write-LogFile -logPath $logFile -Value "Attempting to pause sensor ID: [$sensorID], for duration of [$duration], with message [$message]" -Verbose $pauseAttempt = Invoke-WebRequest -Uri ($pauseURL + $credentialAppend) #Resolve the log file Resolve-LogFile -logPath $logFile } } {$_ -eq 'Resume'} { if ($findDevice -or $List) { ForEach ($id in $foundDeviceIDs) { $sensorID = $null $sensorID = $id $resumeURL = "$($prtgInfo.baseURL)/pause.htm?id=$sensorID&action=1" Write-LogFile -logPath $logFile -Value "Attempting to resume sensor ID: [$sensorID]." -Verbose $resumeAttempt = Invoke-WebRequest -Uri ($resumeURL + $credentialAppend) } #Resolve the log file Resolve-LogFile -logPath $logFile } Else { $resumeURL = "$($prtgInfo.baseURL)/pause.htm?id=$sensorID&action=1" Write-LogFile -logPath $logFile -Value "Attempting to resume sensor ID: [$sensorID]." -Verbose $resumeAttempt = Invoke-WebRequest -Uri ($resumeURL + $credentialAppend) #Resolve the log file Resolve-LogFile -logPath $logFile } } Default { Write-LogFileError -logPath $logFile -errorDesc "Unable to perform action [$action], as it does not match any valid actions!" -Verbose Resolve-LogFile -logPath $logFile Break } } #End switch for PRTG action } #End function Invoke-SensorModification #Test API Try{ #Import modules ForEach ($module in $modules) {Invoke-ModuleImport -modules $module} #Create log file $logFile = New-logFile -logPath $logDir -logName 'PSPRTG.log' -addDate:$true #Attempt to import/set credentials and get the returned string to append to the request for authentication $credentialAppend = Invoke-CredentialCheck #Attempt to get information as a test to see if the API will work Invoke-RestMethod -uri ($prtgInfo.SensorCountURL + $credentialAppend) | Out-Null #If it works, set TestPassed as true $prtgInfo | Add-Member -MemberType NoteProperty -Name TestPassed -Value $true } Catch { $errorMessage = $_.Exception.Message Write-LogFileError -logPath $logFile -errorDesc "Error while attempting to use API: [$errorMessage]" -Verbose #If it doesn't work, set TestPassed as false and break out $prtgInfo | Add-Member -MemberType NoteProperty -Name TestPassed -Value $false #Resolve the log file Resolve-LogFile -logPath $logFile Break }
NinjaLogging.psm1
Set-StrictMode -Version Latest $scriptPath = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition Switch ($MyInvocation.PSCommandPath) { {$_ -match '\\\\'} { $scriptName = $_.SubString($_.LastIndexOf('\')+1) } Default { $scriptName = Get-ChildItem $MyInvocation.PSCommandPath | Select-Object -ExpandProperty BaseName } } if ('Count' -in ($scriptName.psobject.Members.Name)) { $scriptName = 'ScriptLog' } function New-LogFile { <# .SYNOPSIS New-LogFile will create a log file. .DESCRIPTION New-LogFile will create a log file. You can specify different paramaters to change the file's name, and where it is stored. By default it will attempt to get the name of the calling function or script via $scriptName = (Get-ChildItem $MyInvocation.PSCommandPath | Select-Object -ExpandProperty BaseName). It will also attempt to get the path via $scriptPath = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition. You can also specify the path and name, as well as if you'd like to append the date in the following format: MM-dd-yy_HHmm. Use the -Verbose parameter to display what is happening to the host. .PARAMETER logPath Alias: Path Type : String Specify the path to the logfile .PARAMETER logName Alias: Name Type : String Specify the name of the log file. Be sure to include the extension if specifying the name. .PARAMETER scriptVersion Type : Double Specify the version of your script being run. If left blank, will default to 0.1 .PARAMETER addDate Type : Boolean Specify if you'd like to add the date to the file name. If you're specifying logName, you can use addDate to append the current date/time in the format: MM-dd-yy_HHmm. .NOTES Name: New-LogFile Version: 1.0 Author: Ginger Ninja (Mike Roberts) DateCreated: 5/11/16 .LINK http://www.gngrninja.com .EXAMPLE $logFile = New-LogFile ----------------------------- gngrNinja> $logFile C:\PowerShell\logs\ScriptLog_05-11-16_1612.log .EXAMPLE $logFile = New-LogFile -Verbose ----------------------------- VERBOSE: No path specified. Using: C:\PowerShell\logs VERBOSE: VERBOSE: No log name specified. Setting log name to: ScriptLog.log and adding date. VERBOSE: VERBOSE: Adding date to log file with an extension! New file name: ScriptLog_05-11-16_1613.log VERBOSE: VERBOSE: Created C:\PowerShell\logs\ScriptLog_05-11-16_1613.log VERBOSE: VERBOSE: File C:\PowerShell\logs\ScriptLog_05-11-16_1613.log created and verified to exist. VERBOSE: VERBOSE: Adding the following information to: C:\PowerShell\logs\ScriptLog_05-11-16_1613.log VERBOSE: VERBOSE: ----------------------------------------------------------------- VERBOSE: Started logging at [05/11/2016 16:13:11] VERBOSE: Script (Version 0.1) executed by: [thegn] on computer: [GINJA10] VERBOSE: ----------------------------------------------------------------- VERBOSE: gngrNinja> .EXAMPLE $logfile = New-LogFile -Name 'testName.log' -path 'c:\temp' -addDate $true -Verbose ----------------------------- VERBOSE: Adding date to log file with an extension! New file name: testName_05-11-16_1615.log VERBOSE: VERBOSE: Created c:\temp\testName_05-11-16_1615.log VERBOSE: VERBOSE: File C:\temp\testName_05-11-16_1615.log created and verified to exist. VERBOSE: VERBOSE: Adding the following information to: C:\temp\testName_05-11-16_1615.log VERBOSE: VERBOSE: ----------------------------------------------------------------- VERBOSE: Started logging at [05/11/2016 16:15:13] VERBOSE: Script (Version 0.1) executed by: [thegn] on computer: [GINJA10] VERBOSE: ----------------------------------------------------------------- VERBOSE: .OUTPUTS Full path to the log file created. #> [cmdletbinding()] param( [Parameter(Mandatory = $false, Position = 0)] [Alias('Path')] [string] $logPath, [Parameter(Mandatory = $false, Position = 1)] [Alias('Name')] [string] $logName, [Parameter(Mandatory = $false, Position = 2)] [double] $scriptVersion = 0.1, [Parameter(Mandatory = $false, Position = 3)] [boolean] $addDate = $false ) #Check if file/path are set if (!$logPath) { $logPath = "$scriptPath\logs" Write-Verbose "No path specified. Using: $logPath" Write-Verbose "" } if (!$logName) { $logName = $scriptName + '.log' $addDate = $true Write-Verbose "No log name specified. Setting log name to: $logName and adding date." Write-Verbose "" } #Check if $addDate is $true, take action if so if ($addDate) { if ($logName.Contains('.')) { $logName = $logName.SubString(0,$logName.LastIndexOf('.')) + "_{0:MM-dd-yy_HHmm}" -f (Get-Date) + $logName.Substring($logName.LastIndexOf('.')) Write-Verbose "Adding date to log file with an extension! New file name: $logName" Write-Verbose "" } else { $logName = $logName + "_{0:MM-dd-yy_HHmm}" -f (Get-Date) Write-Verbose "Adding date to log file. New file name: $logName" Write-Verbose "" } } #Variable set up $time = Get-Date $fullPath = $logPath + '\' + $logName $curUser = (Get-ChildItem Env:\USERNAME).Value $curComp = (Get-ChildItem Env:\COMPUTERNAME).Value #Checking paths / Creating directory if needed if (!(Test-Path $logPath)) { Try { New-Item -Path $logPath -ItemType Directory -ErrorAction Stop | Out-Null Write-Verbose "Folder $logPath created as it did not exist." Write-Verbose "" } Catch { $message = $_.Exception.Message Write-Output "Could not create folder due to an error. Aborting. (See error details below)" Write-Error $message Break } } #Checking to see if a file with the name name exists, renaming it if so. if (Test-Path $fullPath) { Try { $renFileName = ($fullPath + (Get-Random -Minimum ($time.Second) -Maximum 999) + 'old') Rename-Item $fullPath -NewName ($renFileName.Substring($renFileName.LastIndexOf('\')+1)) -Force -ErrorAction Stop | Out-Null Write-Verbose "Renamed $fullPath to $($renFileName.Substring($renFileName.LastIndexOf('\')+1))" Write-Verbose "" } Catch { $message = $_.Excetion.Message Write-Output "Could not rename existing file due to an error. Aborting. (See error details below)" Write-Error $message Break } } #File creation Try { New-Item -Path $fullPath -ItemType File -ErrorAction Stop | Out-Null Write-Verbose "Created $fullPath" Write-Verbose "" } Catch { $message = $_.Exception.Message Write-Output "Could not create directory due to an error. Aborting. (See error details below)" Write-Error $message Break } #Get the full path in case of dot sourcing $fullPath = (Get-ChildItem $fullPath).FullName if (Test-Path $fullPath) { $flairLength = ("Script (Version $scriptVersion) executed by: [$curUser] on computer: [$curComp]").Length + 1 Write-Verbose "File $fullPath created and verified to exist." Write-Verbose "" Write-Verbose "Adding the following information to: $fullPath" Write-Verbose "" Write-Verbose ('-'*$flairLength) Write-Verbose "Started logging at [$time]" Write-Verbose "Script (Version $scriptVersion) executed by: [$curUser] on computer: [$curComp]" Write-Verbose ('-'*$flairLength) Write-Verbose "" Add-Content -Path $fullPath -Value ('-'*$flairLength) Add-Content -Path $fullPath -Value "Started logging at [$time]" Add-Content -Path $fullPath -Value "Script (Version $scriptVersion) executed by: [$curUser] on computer: [$curComp]" Add-Content -Path $fullPath -Value ('-'*$flairLength) Add-Content -Path $fullPath -Value "" Return [string]$fullPath } else { Write-Error "File $fullPath does not exist. Aborting script." Break } } function Write-LogFile { <# .SYNOPSIS Write-LogFile will add information to a log file created with New-LogFile. .DESCRIPTION Write-LogFile will add information to a log file created with New-LogFile. By default additions to the log file will include a timestamp, unless you specify -addTimeStamp $false. This function accepts values from the pipeline, as demonstrated in an example. Use the -Verbose parameter to display what is being logged to the host. .PARAMETER logPath Alias: Path Type : String Specify the full path to the log file, including the name. .PARAMETER logValue Alias: Value Type : String Specify the value(s) you'd like logged. .PARAMETER addTimeStamp Type : Boolean Defaults to true, set to false if you'd like to omit the timestamp. .NOTES Name: Write-LogFile Version: 1.0 Author: Ginger Ninja (Mike Roberts) DateCreated: 5/11/16 .LINK http://www.gngrninja.com .EXAMPLE For this example we'll assume you use: $logFile = New-LogFile Write-LogFile -logPath $logFile -logValue 'test log value!' ----------------------------- gngrNinja> more $logfile ----------------------------------------------------------------- Started logging at [05/11/2016 16:19:37] Script (Version 0.1) executed by: [thegn] on computer: [GINJA10] ----------------------------------------------------------------- [05-11-16 16:23:24] test log value! .EXAMPLE For this example we'll assume you use: $logFile = New-LogFile Write-LogFile -logPath $logFile -logValue 'test log value!' -Verbose ----------------------------- VERBOSE: Adding [05-11-16 16:25:19] test log value! to C:\PowerShell\logs\ScriptLog_05-11-16_1619.log VERBOSE: .EXAMPLE For this example we'll assume you use: $logFile = New-LogFile Get-Process | Write-LogFile $logFile -Verbose ----------------------------- ... VERBOSE: Adding [05-11-16 16:26:51] System.Diagnostics.Process (wininit) to C:\PowerShell\logs\ScriptLog_05-11-16_1619.log VERBOSE: VERBOSE: Adding [05-11-16 16:26:51] System.Diagnostics.Process (winlogon) to C:\PowerShell\logs\ScriptLog_05-11-16_1619.log VERBOSE: VERBOSE: Adding [05-11-16 16:26:51] System.Diagnostics.Process (WmiPrvSE) to C:\PowerShell\logs\ScriptLog_05-11-16_1619.log VERBOSE: VERBOSE: Adding [05-11-16 16:26:51] System.Diagnostics.Process (WmiPrvSE) to C:\PowerShell\logs\ScriptLog_05-11-16_1619.log VERBOSE: VERBOSE: Adding [05-11-16 16:26:51] System.Diagnostics.Process (WUDFHost) to C:\PowerShell\logs\ScriptLog_05-11-16_1619.log VERBOSE: ... .EXAMPLE For this example we'll assume you use: $logFile = New-LogFile Write-LogFile -logPath $logFile -logValue 'test without timestamp' -addTimeStamp $false -Verbose ----------------------------- VERBOSE: Adding test without timestamp to C:\PowerShell\logs\ScriptLog_05-11-16_1631.log VERBOSE: #> [cmdletbinding()] param( [Parameter(Mandatory = $true, Position = 0)] [Alias('Path')] [string] $logPath, [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 1)] [Alias('Value')] [string] $logValue, [Parameter(Mandatory = $false, Position = 2)] [boolean] $addTimeStamp = $true ) Begin { if (!(Test-Path $logPath)) { Write-Error "Unable to access $logPath" Break } } Process { ForEach ($value in $logValue) { $timeStamp = "[{0,0:MM}-{0,0:dd}-{0,0:yy} {0,0:HH}:{0,0:mm}:{0,0:ss}]" -f (Get-Date) if ($addTimeStamp) { $value = "$($timeStamp + ' ' + $value)" } Write-Verbose "Adding $value to $logPath" Write-Verbose "" Add-Content -Path $logPath -Value $value Add-Content -Path $logPath -Value '' } } } function Write-LogFileError { <# .SYNOPSIS Write-LogFileError will add information to a log file created with New-LogFile. The information will be prepended with [ERROR]. .DESCRIPTION Write-LogFileError will add information to a log file created with New-LogFile. The information will be prepended with [ERROR]. By default additions to the log file will include a timestamp, unless you specify -addTimeStamp $false. This function accepts values from the pipeline, as demonstrated in an example. Use the -Verbose parameter to display what is being logged to the host. .PARAMETER logPath Alias: Path Type : String Specify the full path to the log file, including the name. .PARAMETER errorDesc Alias: Value Type : String Specify the value(s) you'd like logged as errors. .PARAMETER addTimeStamp Type : Boolean Defaults to true, set to false if you'd like to omit the timestamp. .PARAMETER exitScript Alias: Exit Type : Boolean This parameter let's you specify $true if you'd like to exit the script after the error is logged. It defaults to $false. .NOTES Name: Write-LogFileError Version: 1.0 Author: Ginger Ninja (Mike Roberts) DateCreated: 5/11/16 .LINK http://www.gngrninja.com .EXAMPLE For this example we'll assume you use: $logFile = New-LogFile Write-LogFileError -logPath $logFile -errorDesc 'test log value error!' ----------------------------- gngrNinja> more $logFile ----------------------------------------------------------------- Started logging at [05/11/2016 16:31:44] Script (Version 0.1) executed by: [thegn] on computer: [GINJA10] ----------------------------------------------------------------- [05-11-16 16:31:51] [ERROR ENCOUNTERED]: test log value error! #> [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0)] [Alias('Path')] [string] $logPath, [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 1)] [string] $errorDesc, [Parameter(Mandatory = $false, Position = 2)] [boolean] $addTimeStamp = $true, [Parameter(Mandatory = $false, Position = 3)] [Alias('Exit')] [boolean] $exitScript = $false ) Begin { if (!(Test-Path $logPath)) { Write-Error "Unable to access $logPath" Break } } Process { ForEach ($value in $errorDesc) { $timeStamp = "[{0,0:MM}-{0,0:dd}-{0,0:yy} {0,0:HH}:{0,0:mm}:{0,0:ss}]" -f (Get-Date) $value = "[ERROR ENCOUNTERED]: $value" if ($addTimeStamp) { $value = "$($timeStamp + ' ' + $value)" } Write-Verbose "Adding $value to $logPath" Write-Verbose "" Add-Content -Path $logPath -Value $value Add-Content -Path $logPath -Value '' } } End { if ($exitScript) { Write-Verbose "Performing log file close command: Resolve-LogFile -logPath $logPath -exitonCompletion $true" Write-Verbose "" Resolve-LogFile -logPath $logPath -exitScript $true } } } function Write-LogFileWarning { <# .SYNOPSIS Write-LogFileWarning will add information to a log file created with New-LogFile. The information will be prepended with [ERROR]. .DESCRIPTION Write-LogFileWarning will add information to a log file created with New-LogFile. The information will be prepended with [ERROR]. By default additions to the log file will include a timestamp, unless you specify -addTimeStamp $false. This function accepts values from the pipeline, as demonstrated in an example. Use the -Verbose parameter to display what is being logged to the host. .PARAMETER logPath Alias: Path Type : String Specify the full path to the log file, including the name. .PARAMETER warningDesc Alias: Value Type : String Specify the value(s) you'd like logged as errors. .PARAMETER addTimeStamp Type : Boolean Defaults to true, set to false if you'd like to omit the timestamp. .PARAMETER exitScript Alias: Exit Type : Boolean This parameter let's you specify $true if you'd like to exit the script after the error is logged. It defaults to $false. .NOTES Name: Write-LogFileWarning Version: 1.0 Author: Ginger Ninja (Mike Roberts) DateCreated: 5/11/16 .LINK http://www.gngrninja.com .EXAMPLE For this example we'll assume you use: $logFile = New-LogFile Write-LogFileWarning -logPath $logFile -warningDesc 'test log value warning!' ----------------------------- gngrNinja> more $logFile ----------------------------------------------------------------- Started logging at [05/11/2016 16:38:29] Script (Version 0.1) executed by: [thegn] on computer: [GINJA10] ----------------------------------------------------------------- [05-11-16 16:38:48] [WARNING]: test log value warning! #> [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0)] [Alias('Path')] [string] $logPath, [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 1)] [string] $warningDesc, [Parameter(Mandatory = $false, Position = 2)] [boolean] $addTimeStamp = $true, [Parameter(Mandatory = $false, Position = 3)] [Alias('Exit')] [boolean] $exitScript = $false ) Begin { if (!(Test-Path $logPath)) { Write-Error "Unable to access $logPath" Break } } Process { ForEach ($value in $warningDesc) { $timeStamp = "[{0,0:MM}-{0,0:dd}-{0,0:yy} {0,0:HH}:{0,0:mm}:{0,0:ss}]" -f (Get-Date) $value = "[WARNING]: $value" if ($addTimeStamp) { $value = "$($timeStamp + ' ' + $value)" } Write-Verbose "Adding $value to $logPath" Write-Verbose "" Add-Content -Path $logPath -Value $value Add-Content -Path $logPath -Value '' } } End { if ($exitScript) { Write-Verbose "Performing log file close command: Resolve-LogFile -logPath $logPath -exitonCompletion $true" Write-Verbose "" Resolve-LogFile -logPath $logPath -exitScript $true } } } function Resolve-LogFile { <# .SYNOPSIS Resolve-LogFile will resolve a created log file. .DESCRIPTION Resolve-LogFile will resolve a created log file. Use the -Verbose parameter to display what is happening to the host. .PARAMETER logPath Alias: Path Type : String Specify the full path, including name, to the log file to be resolved. .PARAMETER logName Alias: Name Type : String Specify the name of the log file. Be sure to include the extension if specifying the name. .PARAMETER exitScript Alias: Exit Type : Boolean Specify $true if you'd like to exit the script after the log file is resolved. It defaults to $false. .NOTES Name: Resolve-LogFile Version: 1.0 Author: Ginger Ninja (Mike Roberts) DateCreated: 5/11/16 .LINK http://www.gngrninja.com .EXAMPLE $logFile = New-LogFile ----------------------------- gngrNinja> $logFile C:\PowerShell\logs\ScriptLog_05-11-16_1612.log .EXAMPLE $logFile = New-LogFile Get-Process | Write-LogFile $logFile Resolve-LogFile $logFile ----------------------------- ... [05-11-16 16:43:58] System.Diagnostics.Process (wininit) [05-11-16 16:43:58] System.Diagnostics.Process (winlogon) [05-11-16 16:43:58] System.Diagnostics.Process (WmiPrvSE) [05-11-16 16:43:58] System.Diagnostics.Process (WmiPrvSE) [05-11-16 16:43:58] System.Diagnostics.Process (WUDFHost) --------------------------------------------- Ended logging at [05/11/2016 16:44:01] --------------------------------------------- #> [cmdletbinding()] param( [parameter(Mandatory = $true, Position = 0)] [Alias('Path')] [string] $logPath, [Parameter(Mandatory = $false, Position = 1)] [Alias('Exit')] [boolean] $exitScript = $false ) $time = Get-Date if (Test-Path $logPath) { $flairLength = ("Finished processing at [$time]").Length + 1 Write-Verbose "Adding the following content to: $logPath" Write-Verbose ('-'*$flairLength) Write-Verbose "Ended logging at [$time]" Write-Verbose ('-'*$flairLength) Write-Verbose "" Add-Content -Path $logPath -Value ('-'*$flairLength) Add-Content -Path $logPath -Value "Ended logging at [$time]" Add-Content -Path $logPath -Value ('-'*$flairLength) } else { Write-Error "Unable to access $logPath" Break } if ($exitScript) { Write-Verbose "Exiting on completion specified, exiting..." Exit } } function Out-LogFile { <# .SYNOPSIS Out-LogFile will create, add to, and resolve a logfile. .DESCRIPTION Out-LogFile will create, add to, and resolve a logfile. Value from the pipeline is accepted. Use the -Verbose parameter to display what is happening to the host. .PARAMETER logPath Alias: Path Type : String Specify the path to the logFile you'd like created. .PARAMETER logName Alias: Name Type : String Specify the name of the log file. Be sure to include the extension if specifying the name. .PARAMETER logValue Alias: Value Type : String Specify the value(s) you'd like logged. .PARAMETER addTimeStamp Type : Boolean Defaults to true, set to false if you'd like to omit the timestamp. .NOTES Name: Out-LogFile Version: 1.0 Author: Ginger Ninja (Mike Roberts) DateCreated: 5/11/16 .LINK http://www.gngrninja.com .EXAMPLE $outLog = Get-Process | Out-LogFile -logPath c:\temp -logName 'outlog.log' -Verbose ----------------------------- VERBOSE: Created c:\temp\outlog.log VERBOSE: VERBOSE: File C:\temp\outlog.log created and verified to exist. VERBOSE: VERBOSE: Adding the following information to: C:\temp\outlog.log VERBOSE: VERBOSE: ----------------------------------------------------------------- VERBOSE: Started logging at [05/11/2016 16:58:43] VERBOSE: Script (Version 0.1) executed by: [thegn] on computer: [GINJA10] VERBOSE: ----------------------------------------------------------------- VERBOSE: VERBOSE: Adding [05-11-16 16:58:43] System.Diagnostics.Process (AdobeUpdateService) to C:\temp\outlog.log VERBOSE: .EXAMPLE $outLog = Get-Process | Out-LogFile -Verbose ----------------------------- gngrNinja> more $outLog ... [05-11-16 16:56:02] System.Diagnostics.Process (winlogon) [05-11-16 16:56:02] System.Diagnostics.Process (WmiPrvSE) [05-11-16 16:56:02] System.Diagnostics.Process (WmiPrvSE) [05-11-16 16:56:02] System.Diagnostics.Process (WUDFHost) --------------------------------------------- Ended logging at [05/11/2016 16:56:02] --------------------------------------------- gngrNinja> #> [cmdletbinding()] param( [Parameter(Mandatory = $false, Position = 0)] [Alias('Path')] [string] $logPath, [Parameter(Mandatory = $false, Position = 1)] [Alias('Name')] [string] $logName, [Parameter(Mandatory = $true, ValueFromPipeLine = $true, ValueFromPipelineByPropertyName = $true, Position = 2)] [Alias('Value')] [string] $logValue, [Parameter(Mandatory = $false, Position = 3)] [boolean] $addTimeStamp = $true ) Begin { $logFile = New-LogFile -logPath $logPath -logName $logName } Process { ForEach ($value in $logValue) { Write-LogFile -logPath $logFile -logValue $value -addTimeStamp $addTimeStamp } } End { Resolve-LogFile $logFile Return $logFile } } function Send-LogEmail { [cmdletbinding()] param( [Parameter(Mandatory=$true)] [string] $To, [Parameter(Mandatory=$true)] [string] $Subject, [Parameter(Mandatory=$true)] [string] $Body, [Parameter(Mandatory=$true)] [string] $emailFrom, [Parameter(Mandatory=$true)] [string] $emailUser, [Parameter(Mandatory=$false)] [string] $provider = 'gmail', [Parameter(Mandatory=$true)] $password = (Read-Host "Password?" -AsSecureString) ) if (!$to) {Write-Error "No recipient specified";break} if (!$subject) {Write-Error "No subject specified";break} if (!$body) {Write-Error "No body specified";break} if (!$emailFrom) {$emailFrom = 'Ninja_PS_Logging@gngrninja.com'} Switch ($provider) { {$_ -eq 'gmail'} { $SMTPServer = "smtp.gmail.com" $SMTPPort = 587 } {$_ -eq 'custom'} { $SMTPServer = 'Your.SMTP.Server' $SMTPPort = 'Your.SMTP.Server.Port' } } $gmailCredential = New-Object System.Management.Automation.PSCredential($emailUser,$password) Send-MailMessage -To $to -From $emailFrom -Body $body -BodyAsHtml:$true -Subject $Subject -SmtpServer $smtpServer -Port $smtpPort -UseSsl -Credential $gmailCredential }
Notes:
- I've spent some time on the help with the NinjaLogging.psm1 module
- Feel free to browse through and use it as you wish, let me know if you have any problems!
- I have not finished the email sending feature
- This code will eventually find its way to Github, and be a lot more polished
Homework
- Find an API for an application you'd like control with PowerShell
- Study up on the documentation, and find ways to send data to it
- Automate something using what you've learned today