Harnessing the Power of PowerShell Advanced Functions

Recently, I published (https://github.com/snoopj123/NXAPI) a community-based PowerShell module so that PowerShell aficionados could interact with Cisco NX-OS switches (specifically the Nexus 5000 and 7000 families) that were running an API package called NX-API.  This API package allowed for sending NX-OS CLI to these switches, but instead of forcing either a telnet or SSH session, you could do this through HTTP or HTTPS.  The entire module shows how to initialize the connection, including building the right HTTP(S) headers, body, and URI (uniform resource identifier) to the switch endpoint.

I built this library because I was tired of some of the techniques Cisco had deployed within the automation and orchestration framework of Cisco UCS Director.  For the past four years, interaction with NX-OS was done through Java libraries, built by Cisco, that encapsulated SSH connectivity and then screen scraped the responses from the SSH session as returns, whether as success/fail criteria or as inventory information to update Cisco UCS Director’s database.  Overall, these components added massive overhead to the process, especially when you consider multiple switches to have to communicate with in a large-scale fabric.

So, the final goal of this project was to rip away UCS Director’s overhead and get back to what we wanted done:  a way to touch multiple switches in as little time as possible.

What Does this have to do with PowerShell?

Well, PowerShell is my scripting language of choice.  This project also forced me to get much more intimate with advanced function techniques, along with getting more proficient with the Invoke-RestMethod and Invoke-WebRequest cmdlets.   For the sake of this post, we are going to focus on some of the techniques used for crafting a function that I will be using regularly (Add-NXAPIVlan).  Let’s go through the code:

Let’s start with one of the first lines of code in the function:

[CmdletBinding()][OutputType('PSCustomObject')]

What exactly is this small block of code trying to convey?  The CmdletBinding() declaration is what tells PowerShell that this is an advanced function.  We are able to start adding certain parameter designations, like -WhatIf and -Confirm, which almost treats the function like a full-fledged cmdlet.  It’s simply required for advanced function capabilities.

Now, the OutputType() declaration is more of a cosmetic declaration.  This is used, in the beginning of PowerShell functions, to declare the expected return type of the object the function will return.  However, this declaration is not actually performed and validated by this declaration.  In this example, we are cosmetically declaring we are returning a .NET object type of PSCustomObject (the PowerShell custom object).

Working with Parameters

Moving on, we see the param() section of the code.  I won’t list all of these, but some of the better examples of some of the advanced functions within:

param(

[parameter(Mandatory = $true, ValueFromPipeline = $true)]
[ValidateNotNullOrEmpty()]
[string]$Switch,

[parameter(Mandatory = $true)]
[ValidateNotNullorEmpty()]
[ValidateRange(1, 4094)]
[int]$VLANID,

[parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[ValidateLength(1, 32)]
[ValidateScript( {$_ -match "^[a-zA-Z0-9_]+$"})]
[string]$VLANName

)

Inside the param() section, you’ll see a list of multiple declared parameters for this function.  Each have been given some specific validation functions to be compared against.  Let’s look at the first parameter, Switch.

[parameter(Mandatory = $true, ValueFromPipeline = $true)]
[ValidateNotNullorEmpty()]
[string]$Switch,

For this specific parameter, we’ve added two conditions to the parameter itself in the form of Mandatory and ValueFromPipeline.  Mandatory is there to ensure that the parameter is always present when calling this function.  Without that parameter, a critical error will occur and running of the function will never happen.  As far as ValueFromPipeline, this means we are declaring that a string object can be passed to this function via the PowerShell pipeline.  Here’s an example:

$switch = @(“myswitch.domain.org”,"myswitch2.domain.org","myswitch3.domain.org")
$switch | Add-NXAPIVlan -VLANID 1001 -VLANName TestVLAN -Username admin -Password $password

Notice that I did not need to explicitly declare the Switch parameter.  The reason is due to ValueFromPipeline.  By using the pipeline, the assumption was that we were sending a value for the Switch parameter.

Lastly, we have the ValidateNotNullorEmpty declaration.  This is a quick validation to make sure that the object being passed is not set to $null or does not have a declared value associated to it.  There’s no point in processing through the function if the parameter has no value!

Later on in the param() section, you’ll notice a few more validation declarations.  ValidateRange allows for the function author to set a range in which the object can have a value.  In the case of this function, we are stating that the integer for VLANID must be between 1 and 4094.  Any attempt to provide a value outside of this range will net in the function returning an error.  The same goes for ValidateLength, however, this one is used to specific the minimum and maximum character length the parameter VLANName can have.  Lastly, there’s a ValidateScript declaration.  This declaration allows authors to produce their own validation script.  In this example, we are checking the characters in VLANName against an approved list of character values, specified in a Regex format.  Each character much be an upper-case letter (A-Z), a lower-case letter (a-z), or a numeric digit (0-9).  All other characters are considered invalid to this function.

You might notice that there are some other parameters in which I’ve specifically set the Mandatory declaration to $false.  This is because I want those parameters to be optional.  In the overall functions, they are there for very specific returns, whether verbose logging or optional defined functionality that I do not want to be executed by default.

Begin/Process/End

Lastly, you may notice that there’s a particular form to the actual meat of the advanced function.  If you’ve worked with a Try/Catch/Finally error handling block, you can kind of get the idea what the meaning behind Begin/Process/End is all about.  The Begin/Process/End block is a requirement for working with arrays or multiple objects coming into the function.  The reasoning will become apparent further in the explanation.

A Begin block is used for a very specific purpose.  In the event that you are going to be handing multiple objects (as an example, from the pipeline), this block of code is used for a single execution of code before the main body of code is processed.  As an example, I include a lot of EnableVerbose parameters on these functions.  In my Begin block, I’ll check to see if the parameter has been passed and set the VerbosePreference for the entire execution time of the function.  Having that setting run in the Process block for every object being passed is a waste of execution time and resources.

A Process block is used to specify the code you want executed for every single object that might be passed to the function.  Not much really needs to be explained about this section.  Your biggest hurdle might be determining what code needs to go to the Begin or End blocks instead of continually performing that operation on every object, especially if you plan on sending quite a few objects to this function.

Lastly, we have the End block.  Similar to the Begin block, you get a one-time run of code contained within when the function is complete.  If I’m setting the VerbosePreference in the Begin block, then I’m setting the value back to what it was after completion.  Please note, that if you break out of the function for any reason or have a critical stop somewhere in the code, the End block will not process.  This deviates from the Try/Catch/Finally code block, where Finally is always processed.

Now, why do we use this block?  You want to get multiple returns from your function!  If you do not use the Begin/Process/End block, what happens is that the function only returns information on the last object processed within.  If you wanted success or fail criteria from all the objects you sent through the pipeline, you will be sorely disappointed when all you receive is the last object without the block

Conclusion

This was a fun project, from multiple fronts.  I feel like I got a greater idea of what advanced functions within PowerShell are capable of.  I also feel I’ve grown in better identifying how to carve up my code for single/global execution within an advanced function.  I can’t wait to learn more!

Unknown's avatar

About snoopj

vExpert 2014/2015/2016/2017, Cisco Champion 2015/2016/2017, NetApp United 2017. Virtualization and data center enthusiast. Working too long and too hard in the technology field since college graduation in 2000.
This entry was posted in Technical and tagged , , , , , . Bookmark the permalink.

Leave a comment