A Better PowerShell Automation Philosophy

Last Update: Sep 04, 2024 | Published: Sep 15, 2015

SHARE ARTICLE

Today’s article is a continuation of a thread that started out discussing the best approach for selecting data from PowerShell, to automating that process. I’m going to continue with the same scenario and code samples, so take a few minutes to peruse my previous posts if you’re just digging in.


As I mentioned last time, the script file I ended up with was basically a playback of PowerShell commands you typed in an interactive session. But there’s much more to PowerShell automation than this example. I’d like to get you thinking about creating reusable tools as modules. These tools will be the building blocks that you can combine to meet what I’m sure are ever-changing needs. In much the same way that a cmdlet like Start-VM is a building block, you can create your own through PowerShell functions. Please keep in mind that this article is more conceptual in nature and not exactly a tutorial on how to write a function.
The most important concept is that a PowerShell function only does one thing and if it sends objects to the pipeline, it only sends one type of object. You would not write a function that emitted service and process objects. In the original scenario, there were a few distinct actions with different types of results. So I would create separate tools. First, here is a function to simply getting running virtual machines that start with a particular string.

#requires -version 4.0
#requires -module Hyper-V
Function Get-RunningVM {
[cmdletbinding()]
Param(
[Parameter(Position=0,HelpMessage="Enter a virtual machine name")]
[ValidateNotNullorEmpty()]
[string]$Name = "CHI*",
[Alias("cn")]
[ValidateNotNullorEmpty()]
[string]$Computername = $env:Computername
)
Try {
 $vms = Get-VM -name $name -ComputerName $Computername -ErrorAction Stop | Where {$_.state -eq "running"}
 #Only continue if there were matching virtual machines
 if ($vms.count) {
  #adding Hyper-V host name to output
   $vms | Select-Object -property @{Name="Date";Expression={Get-Date}},Status,Uptime,State,Name,Computername
 }
 else {
    Write-Warning "No running virtual machines found on $computername that match the name $Name."
 }
} #Try
Catch {
    #$_ is the exception object
    Write-Warning "Failed to get virtual machines on $computername. $($_.exception.message)"
} #Catch
} #end function

I have added a few advanced function embellishments, such as parameter validation. Once this function is loaded into my PowerShell session, I can use it like any other PowerShell command.

get-runningvm -Computername chi-hvr2

Using a custom function (Image Credit: Jeff Hicks)
Using a custom function (Image Credit: Jeff Hicks)

I’ve added a default value for the VM name that might meet 99% of my use cases. If I need to test something for the other 1%, that’s pretty easy to do.
Using the function for other situations (Image Credit: Jeff Hicks)
Using the function for other situations (Image Credit: Jeff Hicks)


One important point to note is that I did not include formatting with this function. The function writes objects to the pipeline. If I want a table, then I can tell PowerShell to create a table.
Formatting function results as a table (Image Credit: Jeff Hicks)
Formatting function results as a table (Image Credit: Jeff Hicks)

Or send formatted results to a file:

get-runningvm -Computername chi-hvr2 | format-table -AutoSize | out-file D:workvms.txt

Or perhaps my manager says she needs a CSV version of the information.

get-runningvm -Computername chi-hvr2 | export-csv -Path d:workrunningvm.csv –NoTypeInformation

The function is written in such a way as to provide maximum flexibility.
Now, what about the second part of starting virtual machines? That’s an easy one-line command.

Get-VM -name $name -computername $computername | where {$_.state -ne "running"} | Start-VM

I started using variable names as a means of testing and also to make it easier when I move this to a function, which I’ll show you in a moment. I wanted to point out that in this particularly Hyper-V scenario, you might also want to use a regular expression pattern to find virtual machines that are Off or Saved.

Get-VM -name $name -computername $computername | where {$_.state -match "off|saved"} | Start-VM

While you might be happy with this, wrapping it in a function provides better mechanisms for tasks like error handling and validation.

#requires -version 4.0
#requires -module Hyper-V
Function Start-MyVM {
[cmdletbinding(SupportsShouldProcess)]
Param(
[Parameter(Position=0,HelpMessage="Enter a virtual machine name")]
[ValidateNotNullorEmpty()]
[string]$Name = "*",
[Alias("cn")]
[ValidateNotNullorEmpty()]
[string]$Computername = $env:Computername,
[switch]$AsJob
)
#Add ErrorAction to PSBoundParameters
$PSBoundParameters.Add("ErrorAction","Stop")
#remove AsJob from PSBoundParameters
if ($AsJob) {
    $PSBoundParameters.Remove("AsJob")
}
Try {
 $vms = Get-VM @PSBoundParameters | Where {$_.state -match "off|saved"}
 if ($vms) {
    if ($AsJob) {
        $vms | Start-VM -AsJob
    }
    else {
        $vms | Start-VM
    }
 }
} #Try
Catch {
    #$_ is the exception object
    Write-Warning "Failed to get virtual machines on $computername. $($_.exception.message)"
} #Catch
} #end function

I also added a feature to pass the –AsJob parameter to Start-VM. The bottom line with all of this is that I know have two re-usable and flexible tools. I could even create a module to make them even easier to use. Coming full circle, I can still create a script to automate the use of these tools.

#requires -version 4.0
#requires -module Hyper-V
[cmdletbinding(SupportsShouldProcess)]
Param(
[Parameter(Position=0,HelpMessage="Enter a virtual machine name")]
[ValidateNotNullorEmpty()]
[string]$Name = "*",
[Alias("cn")]
[ValidateNotNullorEmpty()]
[string]$Computername = $env:Computername,
[ValidateNotNullorEmpty()]
[string]$Path = "D:workvms.txt"
)
#insert code to dot source or load modules with Get-RunningVM and Start-MyVM functions
Get-RunningVM -Name $Name -Computername $Computername | Format-Table -AutoSize |
Out-File -FilePath $Path -Encoding ascii
Start-MyVM -Name $Name -Computername $Computername

The script file wraps up my underlying tools with some parameter validation and support for ShouldProcess.
I appreciate this module approach from a theoretical perspective. This scenario leaves something to be desired, however, from a practical perspective.
When I create PowerShell tools, I try to not repeat steps or commands. In this scenario even though it is in separate commands, I am essentially grabbing virtual machines that match a given name twice. Once to log those that are running and again to start off or saved VMs. On all but the busiest of Hyper-V hosts, this is probably a minor point, but why make two connections when one will do?
With this is mind, the original scripted solution may be more appropriate. Your design decisions may come down to what you need to automate and who will be running it. As you hopefully have recognized by now, there is usually more than one way to accomplish a task in PowerShell.
In fact, I’ll leave you with yet another solution to this particular problem. This is a multi-step solution. There’s no rule that says everything has to be one long pipeline.

$vmhash = (get-vm -name $name -ComputerName $computername).where({$_.state -match "running|off|saved"}) |
group-object -Property State -AsHashTable -AsString
$vmhash.running | Select @{Name="Date";Expression={Get-Date}},Status,Uptime,State,Name,Computername |
Format-Table -AutoSize | Out-File -FilePath $Path -Encoding ascii
$vmhash.off | Start-VM -WhatIf
$vmhash.saved | Start-VM -whatif

This would be easy to wrap up in a script so that you could make it easier to re-run and parameterize items, such as VMName and Computername. If I were to do that, I’d most likely give the script a name like Invoke-DailyVMTask.

This has been a complex series of articles, and I fear I may have left you more confused. If so, please post your questions in the comments and stay connected with the Petri IT Knowledgebase.

SHARE ARTICLE