The Script Development Process

Script Development Process
Script Development Process

This script development process is an iterative process designed to help you rapidly write quality scripts. The principle is to understand and define the problem. Break up your solution into small steps. Code one small step at a time. Test each small step and get it right before moving on to the next piece. Repeat these steps until you have your solution. All while managing your time.

When is automation the solution?

The benefits of automation are primarily to save time and to decrease errors associated with manual actions. You will need to make the calculation if a script will benefit your situation. How many hours will it take for you to write these script? How many hours of manual work will the script save?

Suppose you need to install an application on one server, rarely to do it again. Not much benefit of automation in this case.

Now suppose for an example, you need to install the application to one hundred servers after hours. Suppose it would take twenty minutes to manually log in, launch the executable, wait for it to finish, check for errors, then log out. That works out to more than thirty-three hours total for one hundred servers. Let’s say it takes you four hours to manually install to one hundred servers. You could probably write and test this script in four hours. Spend four hours to save thirty-three? Sounds good to me. And now that you have that script, you can recycle it for the next time you have a similar task. Automation is looking a lot better for this case.

Problem (or) Task

The “problem” (or task) often comes as a vague request or is poorly defined. Or maybe you noticed a task that you can automate to save time. Whichever way the task comes to you, think of it as a puzzle to solve.

Understanding the problem

Get the details of the task from the requester. What do they want? What result do they expect? Who, what, when, where, why? Ask questions. Really understand the request. Sometimes the requestor doesn’t fully understand what they want, so you may have to work with spotty information, or even wrong details. It’s your job to figure it out.

Even if you are the origin of this task, you need to fully understand the problem.

You might find out that this is not really a problem, that the situation is necessary for security, legal reasons, or something else. You might not be permitted to “fix it” with a script.

Does your company own a tool that can or should do this task instead? Use the right tool for the task. PowerShell might not be the right tool for your task. For example, if your company has Microsoft System Center Configuration Manager, that would be a better tool to do mass installs than a standalone script.

When new information comes to light, at any stage, re-evaluate your understanding. Document the problem.

Understand the solution

Now that you understand the problem, hopefully the bones of a solution are coming to light. Roughly, decide what kind of solution you will provide. Document the intended solution. It’s also important to note that you should limit your solution to the problem at hand! Don’t over-engineer a solution. Your time is valuable; don’t waste it. An over-engineered script is also more difficult to test and troubleshoot.

Consider if this will be a large effort (more than twenty hours), you may want to break this into several solutions. A partial solution might help right away. Later, you can augment it, making continual improvements. To follow the example of installing software: A script that remotely installs the software to one computer at a time would be better that manually installing. Not as good as full automation, but an improvement.

Divide and conquer

Now that you have a solution in mind, divide it up into bite-size parts. Maybe two parts. Maybe five parts. If it’s a really big task, break it up into major “parts” then divide each part further, into a hierarchy, or a tree, of parts. Ideally each part will be small, either one or a few lines. Usually not more than twenty-five lines. Each part should do one thing. It should be small and simple. Each part should be testable. You might know not how to break it all up at first. As you work on it, your path will become more evident.

Code 1 piece

Start with the core of your solution. The most basic part, if you have defined it that way, to prove that your solution is the right one. If the one piece can be a function, do make it a function. The function should do one thing. If your function is running longer than twenty-five lines, is it really only doing one thing? Consider making multiple, short functions. You’ll find that short functions are easier to understand, test, and troubleshoot.

Test

Test your solution, in a safe manner. Safe? Test in a lab or dev environment, especially if your script changes anything. You can’t allow your script development to cause problems. How embarrassing that would be. If the part you are testing is small and simple, and better yet, a function, testing should be easier. If your task is fully defined, and your solution is fully defined, and your code is simple, the results of your testing should be clear.

Functions are usually easy to test, as they take well defined parameters and (usually) return a value.

Test with good input and bad input. Is it the result you expected? Does your code handle the bad input correctly? Does your code fail gracefully and clearly? Success or failure should be clear. If the code test was successful, code the next part of the puzzle. If the code test was not successful, or you uncovered something that you didn’t realize earlier, then reevaluate your assumptions. Reevaluate your understanding of the problem and the solution. Do you need to change the basic solution? Or was it a simple typo? Go back over it, and then test again.

Is there more?

Keep working on the parts and build up your solution, testing frequently along the way. As your understanding changes, adapt your solution.

Success!

Finally, you’ve got it all working. Congratulations. If this is a frequently used script, you might find value in reevaluating it later and perhaps making improvements, if you feel there will be value in that.

Output the PowerShell assignment command from an array

This function will take an array and output the PowerShell assignment command with the elements from the array. It would be useful if you want to hardcode a string array and don’t want to manually format it.

The function is below, along with an example usage and example output.

function ArrayAssignment ($MyArray) {
    $ArrayContent = "`$Array = @("
    # Output elements except the last
    $MyArray[0..$($MyArray.Count-2)] | foreach {$ArrayContent += "`"$_`","}
    # Output the last element
    $ArrayContent += "`"$($MyArray[$MyArray.Count-1])`")"
    $ArrayContent
    }

# Create an array of the first 20 services names for an example
$Example = (Get-Service).DisplayName[0..19]

# Use the function
ArrayAssignment $Example

# Example Output:
PS C:\>
$Array = @("Adobe Flash Player Update Service","AllJoyn Router Service","Application Layer Gateway Service","AMD External Events Utility","AMD FUEL Service","Application Identity","Application Information","Apple Mobile Device Service","Application Management","App Readiness","AppX Deployment Service (AppXSVC)","ASP.NET State Service","Windows Audio Endpoint Builder","Windows Audio","ActiveX Installer (AxInstSV)","BitLocker Drive Encryption Service","Base Filtering Engine","Background Intelligent Transfer Service","Bonjour Service","Background Tasks Infrastructure Service")

Get the IP addresses of Hyper-V VMs, part 2

A variation on my previous post.

This one-liner uses an expression in a select to expose the IP addresses. The expression is necessary so we can drill down into the NetworkAdapters collection to get to the IPAddresses property.

This will list all IP addresses for each VM:

Get-VM | select Name, State, @{Name="IP";Expression={$_.NetworkAdapters.IPAddresses}}

This next variation displays only the first IP address for each VM. Note the “[0]”. It addresses the first element of “IPAddresses” array.

Get-VM | select Name, State, @{Name="IP";Expression={$_.NetworkAdapters.IPAddresses[0]}}

New-Nano: automating Windows Nano Server lab builds with PowerShell

I wrote this function to automate Nano Server lab builds into Hyper-V. You need to supply the “Windows Server 2016 Technical Preview 5” ISO, which you can get here or here. My script is based partially on ideas presented in the Microsoft TechNet article “Getting Started with Nano Server“.

You will need to have Hyper-V on your computer, have a Hyper-V switch on a network that has DHCP running somewhere (if you have a switch in Hyper-V defined as “External”, this should work fine). You will also need to supply a working directory.

New-Nano copies the required files from the ISO, runs Microsoft’s cmdlet New-NanoServerImage to build a Hyper-V disk image, creates a new VM from that disk image, starts the VM, and gets network information from the VM. The function then runs an example PowerShell Session to demonstrate connectivity to the new Nano VM. Total build time in my tests is a little over three minutes.

If you look to the bottom of the script, you will see an example of how to call the New-Nano function.

Further refinements would be to install features and packages to the Nano, and to do domain joins. Please note that this should only be used for a lab environment because the administrator password is in plaintext. If this was going into a production environment, you would want to encode it.

Enjoy. Please try it out and leave a comment.

Start-Transcript -Path "C:\Lab\NewNano.txt"
function New-Nano
{
    [CmdletBinding()]
    Param
    (
        # Path to the Windows Server 2016 ISO
        [Parameter(Mandatory=$true)]
        $ImagePath,

        # Working Directory
        [Parameter(Mandatory=$true)]
        $WorkingDir,

        # Admin Password
        [SecureString]
        $AdminPassword = (ConvertTo-SecureString -AsPlainText "Password1" -Force),

        # Hyper-V Switch Name
        [String]
        $NetworkSwitch = "External",

        # Nano VM Name
        [String]
        $Name = "nano" + $(Get-Random -Minimum 1000 -Maximum 9999),
        
        # Processor Count
        $ProcessorCount = 1,

        # Memory Startup Bytes
        $MemoryStartupBytes = 512MB
    )

    Process
    {
    # ref: https://technet.microsoft.com/en-us/library/mt126167.aspx
    
    $VirtualHardDiskPath = $(Get-VMHost).VirtualHardDiskPath
    $VirtualMachinePath = $(Get-VMHost).VirtualMachinePath
    $NewNanoPath = $VirtualHardDiskPath + $Name + ".vhdx"
    $StartingDir = (Get-Location).Path
    $MountObject = Mount-DiskImage -ImagePath $ImagePath -PassThru
    $MountDriveLetter = ($MountObject | Get-Volume).DriveLetter + ":"
    $NanoMount = $MountDriveLetter + "\NanoServer\"

    # copy the NanoServer dir to the working dir
    $NanoDir = $WorkingDir + "NanoServer\"
    Copy-Item -Path $NanoMount -Destination $NanoDir -Recurse -Force

    Set-Location $NanoDir

    Import-Module .\NanoServerImageGenerator -Verbose

    # create the VHD
    $NewNanoResult = New-NanoServerImage -DeploymentType Guest -Edition Standard -MediaPath $MountDriveLetter -BasePath .\Base -TargetPath $NewNanoPath -EnableRemoteManagementPort -ComputerName $Name -AdministratorPassword $AdminPassword

    # new VM from the VHD
    New-VM -Name $Name -VHDPath $NewNanoPath -MemoryStartupBytes $MemoryStartupBytes -SwitchName $NetworkSwitch -BootDevice VHD -Generation 2 -Path $VirtualMachinePath  |
    Set-VM -DynamicMemory -ProcessorCount $ProcessorCount -Passthru -AutomaticStopAction ShutDown -AutomaticStartAction Nothing 

    # finish up.
    $MountObject | Dismount-DiskImage
    "Starting $Name"
    Start-VM $Name

    while ((Get-VM $Name).State -ne "Running")
        {Start-Sleep 5}
    "$Name is Running."

    while ((Get-VM $Name).NetworkAdapters.Status[0] -ne "Ok") 
        {Start-Sleep 5}
    "Network adapter reporting Ok."

    $vm = Get-VM $Name
    $vm.NetworkAdapters | select VMName, SwitchName, Status, IPAddresses

    "Demonstrate that PowerShell Session works"
    $AdminUser = "$Name\Administrator"
    $cred = New-Object -Typename System.Management.Automation.PSCredential  -Argumentlist $AdminUser, $AdminPassword
    $pss = New-PSSession -ComputerName $Name -Credential $cred
    Invoke-Command -Session $pss -ScriptBlock {Get-Service | where Status -EQ "Running"| Format-Table} 
    Remove-PSSession $pss

    # return to original directory
    Set-Location $StartingDir
    }
}

$NewNanoParams = @{
    ImagePath = "C:\ISOs\en_windows_server_2016_technical_preview_5_x64_dvd_8512312.iso";
    WorkingDir = "C:\Lab\";
    AdminPassword =  (ConvertTo-SecureString -AsPlainText "Password1" -Force);
    NetworkSwitch = "External"
    Name = "LabNano" + (Get-Random -Minimum 100 -Maximum 999);
    ProcessorCount = 1;
    MemoryStartupBytes = 512MB
    }

New-Nano @NewNanoParams

Stop-Transcript

 

 

Returning custom objects in PowerShell, simplified

I’ve stumbled upon this simplified technique to return custom objects in my PowerShell functions.

My first take on it was this, but I’ve simplified it. In this simplified variation, you just create and return the object in the same hash. For example:

[PsCustomObject]@{
    ComputerName = $ComputerSystem.DNSHostName;
    Domain = $ComputerSystem.Domain;
    ProcessCount = $ProcessCount;
    }

This is a hash, as you probably have seen before, but the [PsCustomObject] is what makes it a custom object.

And in context of a function:

# Example Function
function Get-SystemInfo {

    # Prepare the info
    $OS = Get-CimInstance -ClassName Win32_OperatingSystem # for FreePhysicalMemory, Caption, PSComputerName, SystemDrive
    $LoadPercentage = (Get-CimInstance -ClassName Win32_Processor).LoadPercentage
    $ProcessCount = (Get-Process).Count
    $SystemDrive = Get-CimInstance -ClassName Win32_LogicalDisk -filter "Name = '$($OS.SystemDrive)'" 
    $ComputerSystem = Get-CimInstance -ClassName Win32_ComputerSystem

    # Return the properties in a custom object
    [PsCustomObject]@{
        ComputerName = $ComputerSystem.DNSHostName;
        Domain = $ComputerSystem.Domain;
        FreePhysicalMemory = "{0:n0}" -f ($OS.FreePhysicalMemory /1KB) + " MB";
        OS = $OS.Caption;
        LastBootUpTime = $OS.LastBootUpTime;
        LoadPercentage = $LoadPercentage;
        ProcessCount = $ProcessCount;
        SysVolFree = "{0:n1}" -f ($SystemDrive.FreeSpace /1GB) + " GB"
        }
     
    }

# Example Usage

# Using the results, put them into an object "$Result"
$Result = Get-SystemInfo

# Access the values inside the object
$Result.LastBootUpTime

Get-Subnet, a PowerShell advanced function

I wrote a PowerShell advanced function “Get-Subnet” to return a subnet mask for a given bit or bit range.

The cmdlet, with embedded help, including examples:

<#
.Synopsis
   Returns a subnet mask for a given bit or bit range.
.DESCRIPTION
   For a given number of bits, or range of bits, returns the equivilant subnet mask(s) and number of available hosts in that subnet.
   Returns NetworkBits, SubnetMask, NumberOfHosts for each bit.
   NetworkBits is provided for completeness, in the case of multiple objects returned. 
   SubnetMask string is useful for situations where you know network bits and want to set a subnet mask on a network card. 
   NumberOfHosts is useful for understanding implications of using a selected number of bits.
.EXAMPLE
   PS C:\>Get-Subnet

Returns:
>   NetworkBits SubnetMask      NumberOfHosts
>   ----------- ----------      -------------
>             8 255.0.0.0            16777214
>             9 255.128.0.0           8388606
>            10 255.192.0.0           4194302
>            11 255.224.0.0           2097150
>            12 255.240.0.0           1048574
>            13 255.248.0.0            524286
>            14 255.252.0.0            262142
>            15 255.254.0.0            131070
>            16 255.255.0.0             65534
>            17 255.255.128.0           32766
>            18 255.255.192.0           16382
>            19 255.255.224.0            8190
>            20 255.255.240.0            4094
>            21 255.255.248.0            2046
>            22 255.255.252.0            1022
>            23 255.255.254.0             510
>            24 255.255.255.0             254
>            25 255.255.255.128           126
>            26 255.255.255.192            62
>            27 255.255.255.224            30
>            28 255.255.255.240            14
>            29 255.255.255.248             6
>            30 255.255.255.252             2

   With no parameters, the command lists subnet masks for bit range 8 to 30.
.EXAMPLE
   PS C:\>Get-Subnet -MinBits 20 -MaxBits 24

Returns:
>   NetworkBits SubnetMask    NumberOfHosts
>   ----------- ----------    -------------
>            20 255.255.240.0          4094
>            21 255.255.248.0          2046
>            22 255.255.252.0          1022
>            23 255.255.254.0           510
>            24 255.255.255.0           254
   
   Return a specific range of subnet masks.
.EXAMPLE
   PS C:\>$a = (Get-Subnet -Bits 20).SubnetMask
   PS C:\>Write-Host "20 bit Subnet mask is $a"

   Returns:
>  20 bit Subnet mask is 255.255.240.0

   Store a specific subnet mask to a variable.
#>

function Get-Subnet

{
              [CmdletBinding(DefaultParameterSetName='Parameter Set 1')]
              Param
              (
                [Parameter(ParameterSetName='Parameter Set 1')]
                $Bits,
          
                [Parameter(ParameterSetName='Parameter Set 2')]
                $MinBits,

                [Parameter(ParameterSetName='Parameter Set 2')]
                $MaxBits

              )
    if ($Bits -ne $null) {
        $MinBits = $Bits
        $MaxBits = $Bits
        }
        else {
            if ($MinBits -eq $null) {
                $MinBits = 8
                }
            if ($MaxBits -eq $null) {
                $MaxBits = 30
                }
            }

    foreach ($i in ($MinBits..$MaxBits)) 
    {
        $binary = ([string]"1" * $i).PadRight(32,"0")

        $InvertedBinary = ([string]"0" * $i).PadRight(32,"1")

        $NumberOfHosts = [convert]::ToInt32($InvertedBinary,2) -1 

        $Oct1 = [string][convert]::ToInt32($binary.Substring(0,8),2)
        $Oct2 = [string][convert]::ToInt32($binary.Substring(8,8),2)
        $Oct3 = [string][convert]::ToInt32($binary.Substring(16,8),2)
        $Oct4 = [string][convert]::ToInt32($binary.Substring(24,8),2)

        $SubnetMask = $Oct1 + "." + $Oct2 + "." + $Oct3 + "." + $Oct4

        [PsCustomObject]@{
            "NetworkBits" = $i; 
            "SubnetMask" = $SubnetMask; 
            "NumberOfHosts" = $NumberOfHosts
            }
        } 

}

The XOR Cipher in PowerShell

DISCLAIMER: This is presented for educational purposes only. It is not intended for use as production cryptography. The example here is not secure and is vulnerable to multiple attacks.

Inspired by my son’s recent interest in ciphers, I decided to write a basic XOR cipher in PowerShell. Given your plaintext and key, the script returns the ciphertext.

What does it do?
The plaintext and the key are strings. If the key is shorter then the plaintext, then we repeat the key until it’s as long as the plaintext. A string is made up of bytes. And bytes are made up bits. For each bit in the plaintext, we do an Exclusive Or (XOR) Operation with the coresponding bit in the repeated key. The result is stored in the ciphertext.

What is Exclusive Or? https://en.wikipedia.org/wiki/Exclusive_or

Exclusive Or is an operation that outputs TRUE when the inputs differ.
For example:

Plaintext  01010000
Key        01101011
XOR-----------------
Ciphertext 00111011

A nice feature of XOR is that it is easly reversable. If we XOR the key with the ciphertext, we get the plaintext back.

The code:

function encode($plaintext, $key)
    {
    $cyphertext = ""
    $keyposition = 0
    $KeyArray = $key.ToCharArray()
    $plaintext.ToCharArray() | foreach-object -process {
        $cyphertext += [char]([byte][char]$_ -bxor $KeyArray[$keyposition])
        $keyposition += 1
        if ($keyposition -eq $key.Length) {$keyposition = 0}
        }
    return $cyphertext
    }

PowerShell Hex Functions

I wrote some basic functions to convert a string to hexadecimal, to convert hexadecimal to a string. Also a function to dump a string to it’s component bytes, listing on each line, the character, it’s ASCII code in hexadecimal, and ASCII code in decimal. Example output:

PS C:\> StringToHex "This is an example!@#"
5468697320697320616E206578616D706C65214023
PS C:\> HexToString "4861726B212053686F77206D6520616E206578616D706C653F3F"
Hark! Show me an example??
PS C:\> HexDump "So how is this?"
S 0x53 83
o 0x6F 111
  0x20 32
h 0x68 104
o 0x6F 111
w 0x77 119
  0x20 32
i 0x69 105
s 0x73 115
  0x20 32
t 0x74 116
h 0x68 104
i 0x69 105
s 0x73 115
? 0x3F 63

The code. They are basic functions, not Advanced Functions, so nothing fancy.

function StringToHex($i) {
    $r = ""
    $i.ToCharArray() | foreach-object -process {
        $r += '{0:X}' -f [int][char]$_
        }
    return $r
    }

function HexToString($i) {
    $r = ""
    for ($n = 0; $n -lt $i.Length; $n += 2)
        {$r += [char][int]("0x" + $i.Substring($n,2))}
    return $r
    }

function HexDump($i) {
    $i.ToCharArray() | foreach-object -process {
        $num = [int][char]$_
        $hex = "0x" + ('{0:X}' -f $num)
        "$_ $hex $num"
        }
    }

PowerShell function to calculate data transfer time

This PowerShell function will calculate how long it will take to transfer a given number of bytes at a given speed (in bits per second). Optionally, you can specify transfer overhead, which defaults to ten percent.

Example Usage

PS C:\> Get-TransferTime -FileSize 4.4gb -Speed 2mb


Days         : 0
Hours        : 5
Minutes      : 0
Seconds      : 22
TotalDays    : 0.208587962962963
TotalHours   : 5.00611111111111
TotalMinutes : 300.366666666667
TotalSeconds : 18022

The Function

<#
.Synopsis
Data transfer time calculation.
.DESCRIPTION
Calculate how long it would take to transfer a given number of bytes at a given speed.
.PARAMETER FileSize
File Size in bytes. Can specify with GB, MB, etc.
.PARAMETER Speed
Transfer speed in bits. Can specify with GB, MB, etc.
.PARAMETER OverheadPercent
Transfer overhead percent. Defaults to 10.
.EXAMPLE
Get-TransferTime -FileSize 4.4gb -Speed 2mb
.EXAMPLE
Get-TransferTime -FileSize 50mb -Speed 10mb -OverheadPercent 6
.NOTES
Ken Bradley, 2015-12-10
#>
function Get-TransferTime
{
    [CmdletBinding()]
    [Alias()]
    [OutputType([int])]
    Param
    (
        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=0)]
        [double]$FileSize, # bytes, can specify GB, MB, etc

        #Speed help!
        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=1)]
        [double]$Speed, # bits, can specify GB, MB, etc

        [Parameter(Mandatory=$false,
                   ValueFromPipelineByPropertyName=$true,
                   Position=2)]
        [double]$OverheadPercent=10

        )

    # Calculate seconds
    $Result = ($FileSize * 8) / $Speed

    # Output the result nicely formated
    New-TimeSpan -Seconds $Result | Format-List -Property Days, Hours, Minutes, Seconds, TotalDays, TotalHours, TotalMinutes, TotalSeconds

    }