© Adam Bertram 2020
A. BertramBuilding Better PowerShell Codehttps://doi.org/10.1007/978-1-4842-6388-4_14

14. Build Tools

Adam Bertram1  
(1)
Evansville, IN, USA
 

As you begin to write more PowerShell, you’ll probably find that you keep reinventing the wheel. This is natural. You keep repeating yourself over and over again because you’re not building upon the code you had previously created. You’re not building script libraries, modules, and tools. You’re essentially creating disposable code. Stop that!

Instead of wasting time writing a brand new script from scratch, write code in a way you can reuse it. Build tools you can then put to use to build larger automation frameworks.

Think Ahead and Build Abstraction “Layers”

This tip isn’t necessarily PowerShell specific, but it’s one of the most important tips of this entire book. When you build reusable tools in PowerShell, always think and build with abstraction in mind.

What’s abstraction? Abstraction is a term software developers are familiar with but you may not be. Abstraction means writing code that interacts with something just not directly. Abstraction means writing code in “layers” that then refer back to one another eventually building an entire framework around a concept.

Building in abstraction layers means you’re coding not for the now but for the future. You’re writing code that can be reused across many different use cases in various situations without actually changing the code; you just call it differently.

Think about the code you write now. The code may work in the current context, but will it work if you run it from another computer, another environment, or even move it to the cloud? Does it need to have that kind of resiliency? If so and your code breaks when it’s moved out of its pristine, current environment, you’re probably not building in enough abstraction.

I know the term abstraction is a bit vague, so let’s put some context and examples behind it when you’re building tools in PowerShell. Let’s say you’ve built some scripts that start, stop, and manage Hyper-V virtual machines (VMs). Your team manages one of the company’s hypervisors which, in this case, is Hyper-V.

Since the VMs are Hyper-V, you decide to use the HyperV PowerShell module. This module is solely focused on only Hyper-V VMs. Perhaps you have some code that looks like the following:
#requires -Module HyperV
[CmdletBinding()]
param(
    [Parameter(Mandatory)]
    [string]$VmName
)
## Use the Start-Vm cmdlet in the HyperV module to start a VM
Start-Vm -Name $VmName
## Do some stuff to the VM here
## Use the Stop-Vm cmdlet in the HyperV module to stop the VM
Stop-Vm -Name $VmName

This script works just fine. You’ve already created a parameter which is a great first start. This parameter allows you to run this script against many different VMs without changing any code inside of the script. Good job!

With this script, you’ve already created one layer of abstraction. Instead of running Start-Vm and Stop-VM directly on the PowerShell console, you decided you needed a script to those things and run some other code in between. You created it and now, instead of typing Start-VM and Stop-VM at the console, you’re simply running .-MyVM.ps1 or something like that. You’re not calling the Start-VM and Stop-VM cmdlets directly. You’ve abstracted away the need to do that and instead just interface with the script and not the cmdlets themselves.

One day, your boss comes along and tells you your team has now inherited management of the company’s VmWare cluster. Uh oh! Now you think you need to create an entirely new set of scripts or module to manage VmWare VMs. You might have to but you could decide to integrate VmWare functionality into your current solution.

A VM is a VM, right? VmWare VMs and Hyper-V VMs are similar; they just run on different hypervisors. What are the commonalities between them? You can start and stop both kinds of VMs. Instead of creating another script, why not integrate them into your current generic Set-MyVM.ps1 script. One way to do that would be to add a parameter called Hypervisor and build the logic needed to determine if your script should run some VmWare code of HyperV code.

Notice that the following code snippet has a required module called VmWare. You’ll need to ensure your script now supports both scenarios. Also, it now has some conditional logic in the form of a switch statement that does some different things based on the type of hypervisor chosen.
#requires -Module HyperV, VmWare
[CmdletBinding()]
param(
    [Parameter(Mandatory)]
    [string]$VmName,
    [Parameter()]
    [ValidateSet('HyperV','VmWare')]
    [string]$Hypervisor
)
switch ($Hypervisor) {
    'HyperV' {
        ## Use the Start-Vm cmdlet in the HyperV module to start a VM
        Start-Vm -Name $VmName
        ## Do some stuff to the VM here
        ## Use the Stop-Vm cmdlet in the HyperV module to stop the VM
        Stop-Vm -Name $VmName
        break
    }
    'VmWare' {
        ## Use whatever command the VmWare module has in it to start the VM
        Start-VmWareVm -Name $VmName
        ## do stuff to the VmWare VM
        ## Use whatever command the VmWare module has in it to stop the VM
        Stop-VmWareVm -Name $VmName
        break
    }
    default {
        "The hypervisor you passed [$_] is not supported"
    }
}
Alternatively and albeit better approach, if possible, you could dynamically figure out what type of VM was passed and then make a decision on the code to run based on the result of that code. See in the following an example of how to do that. Notice now you leave the decision-making up to the code (which is always better). The script now automatically knows what hypervisor the VM is running on.
#requires -Module HyperV, VmWare
[CmdletBinding()]
param(
    [Parameter(Mandatory)]
    [string]$VmName
)
function Get-Hypervisor {
    ## simple helper function to determine the hypervisor
    param(
        [Parameter()]
        [string]$VmName
    )
    ## Some code here to figure out what type of hypervisor this VM is running on
    ## Return either 'HyperV' or 'VmWare'
}
$hypervisor = Get-Hypervisor -VmName $VmName
switch ($Hypervisor) {
    'HyperV' {
        ## Use the Start-Vm cmdlet in the HyperV module to start a VM
        Start-Vm -Name $VmName
        ## Do some stuff to the VM here
        ## Use the Stop-Vm cmdlet in the HyperV module to stop the VM
        Stop-Vm -Name $VmName
        break
    }
    'VmWare' {
        ## Use whatever command the VmWare module has in it to start the VM
        Start-VmWareVm -Name $VmName
        ## do stuff to the VmWare VM
        ## Use whatever command the VmWare module has in it to stop the VM
        Stop-VmWareVm -Name $VmName
        break
    }
    default {
        "The hypervisor you passed [$_] is not supported"
    }
}

You’ve now discovered the commonalities between Hyper-V and VmWare VMs and have built a solution to manage them both! You’ve now created a layer of abstraction that allows you to manage two different VM types with a single script.

Further Learning

Wrap Command-Line Utilities in Functions

If you have to use a command-line utility, wrap it in a PowerShell function. If it returns output, parse the output and make it return a pscustomobject. This allows a CLI utility to act like any other PowerShell command. Once you’ve abstracted away all of the “CLIness”, the command can be easily integrated with other PowerShell tools.

This tip is great for standardization. When building tools, it’s important to standardize everything you can. It makes your code cleaner and easier to understand and work with. A great example of this is the command-line utility netstat. This is a utility that tells you all of the ports that are closed, listening, etc. on your local Windows computer. This utility, like all command-line utilities, returns a bunch of text. This text has no structure to it as you can see in Figure 14-1. You can’t pipe this output to another command or check on object properties.
../images/501963_1_En_14_Chapter/501963_1_En_14_Fig1_HTML.jpg
Figure 14-1

No object structure to netstat

To build reusable tools, all commands whatever they are, PowerShell cmdlets or command-line tools, should return similar if not the same kind of object. Check out this script called Get-LocalPort.ps1. You’ll notice this script uses various methods to parse the output from netstat. It then returns a standard PowerShell pscustomobject for each entry as you can see in Figure 14-2.
../images/501963_1_En_14_Chapter/501963_1_En_14_Fig2_HTML.jpg
Figure 14-2

The netstat command-line output in objects

Once you have a script or function returning a PowerShell object, you can treat that command’s output just like you would any other object.

Further Learning

Make Module Functions Return Common Object Types

If you have different PowerShell functions that work together, be sure they always return the same type of object. This object type is usually a pscustomobject because it’s generic and easy to create. If functions are all returning a pscustomobject type, you’ll know what to expect when it returns information. It also makes troubleshooting easier.

Perhaps you have a module with a couple of functions like in the following code snippet. What those functions do is irrelevant. Regardless of what you do inside of each function, ensure each function that returns an object to the pipeline returns the same type of object.
function Get-Thing {
    [CmdletBinding()]
    param()
    ## Do some stuff
    [pscustomobject]@{
        'Name' = 'XXXX'
        'Type' = 'XXXX'
    }
}
function Get-OtherThing {
    [CmdletBinding()]
    param()
        [pscustomobject]@{
            'Name' = 'XXXX'
            'Type' = 'XXXX'
            'Property1' = 'Value1'
            'Property2' = 'Value2'
        }
}

When you or someone else now runs Get-Thing, they’ll then expect Get-OtherThing to return the same type of object. The properties may be different because they are, after all, different functions that perform different tasks, but you should always strive to return objects of the same type and as similar as possible.

Further Learning

Ensure Module Functions Cover All the Verbs

If you have an immediate need to accomplish a task like creating a user account, removing a file, or modifying a database record, don’t just create a single function. Instead, create four functions that cover the complete life cycle of that object New, Set, Get, and Remove.

For example, perhaps you’re creating a module for a monitoring appliance that has an API. You decide on an “object” noun of Monitor. If you need to create a new monitor in an automation script, don’t just create the New-Monitor function. Instead, create Get-Monitor, Set-Monitor, and Remove-Monitor to ensure you have support for the monitor’s full life cycle.

Even if you just have the need to create a new monitor at this point, always, at least create a Get-Monitor function. You’re going to inevitably need to confirm if the monitor you think you just created was actually created or ensure the monitor does not exist before attempting to create it.

If you’re tight on time, create the function you immediately need to perform some change on an object like New, Set, or Remove. Then, once you have built that function, build the Get function alongside of it. Eventually, you should go back and build out the entire set of functions which ensures you have a tool that can manage whatever thing you’re working with through its entire life cycle.

Tip Source: https://twitter.com/JimMoyle

Further Learning

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.186.72