Stickers, What!?

When I started tommymaynard.com, I never considered that I would ever brand anything with my URL, and yet, as of this weekend I have. There’s a couple reasons as to why.

One, I’ll be in Phoenix in a couple weekends to give a session about my newest advanced function template (the 2.0 version). As a part of the session, I’m also producing a related blog post, so people at the session can refer to it in case they miss something during my session, or in case I run short on time. I couldn’t believe how fast an hour went when I spoke last year. Additionally, I’ll use my blog to make the advanced function template available — think, downloadable.

Two, it’s the PowerShell symbol and who, that’s willing to lose some time to PowerShell on a Saturday, wouldn’t want a sticker with that logo? So, for those that attend, I’ll see you there, and for those that don’t, let me see you here after 11 a.m. on Saturday, October 14, 2017 to check out the post.

Read-Host with a Previous Value Part II

It’s simply not everyday that Rob Sewell (@sqldbawithbeard) places a link on Twitter to your blog. Yours, as in mine, but here we are. That was yesterday.

Today, knowing this is out there, I decided to add a bit more to this short post with a second installment. In this post, I’ve written a few changes to the single If statement from Part I. While it was a simple statement to see if I could reuse a previously stored value in the first installment, it’s a bit more full featured now.

We’re getting away from statically setting the default user. We’ll actually let the user assign that value. This determination — whether there’s a default user or not — defines our prompt message, which is most of what’s been added.

If ($User) {
    $Prompt = "Press Enter for the last user [$User], or enter a new user"
} Else {
    $Prompt = "Please enter a new user"
}

If (($Result = Read-Host -Prompt $Prompt) -eq '' -or $Result -eq $User) {
    "You're using the previously used ""$User"" user."
} Else {
    "You're using the previously unused ""$Result"" user."
    $User = $Result
}

I won’t bother including any examples of the running code, but do run the example yourself if you’re interested. Remember to clear or remove the $User, $Prompt, and $Result variables if you want to start fresh. The following command will remove those variables, if you find you need it before a new run.

Remove-Variable -Name User,Prompt,Result -ErrorAction SilentlyContinue

And that’s it. Thanks for the link to the blog, Rob!

Three Ways to Set $PSDefaultParameterValues Part II

In part one of this post, I indicated three different ways to populate the $PSDefaultParameterValues preference variable. The $PSDefaultParameterValues variable allow one to assign default values to the parameters of cmdlets and functions without ever needing to type them when they invoke the cmdlet or function. Here’s the two sets of examples from that post.

$PSDefaultParameterValues.Add('Get-Help:ShowWindow',$true)

$PSDefaultParameterValues = @{'Get-Help:ShowWindow' = $true}

$PSDefaultParameterValues['Get-Help:ShowWindow'] = $true


$PSDefaultParameterValues.Add('Out-Default:OutVariable','__')

$PSDefaultParameterValues = @{'Out-Default:OutVariable' = '__'}

$PSDefaultParameterValues['Out-Default:OutVariable'] = '__'

In the first section of the above example, we set the ShowWindow parameter of Get-Help to $true. Now, whenever I use Get-Help, it will always include the ShowWindow parameter, and I don’t have to type a thing. Keep in mind, if it’s not clear, that the first three commands all do the same thing. You’d only need to run one of them. The same goes for the second section of the above example.

So, I recently wrote a function at work. It’s another PowerShell random password generator, as if the world needed one more. In my version, there’s a CharacterType parameter that can accept one or more of four predefined values: Lowercase, Number, Symbol, and Uppercase. These values can be used alone, or in combination with each other. As I have a coworker that didn’t want symbols, I obliged by adding this character type feature. I should mention that if the CharacterType parameter isn’t included at all, that it will default to use all four character types. As it should.

Now, my coworker doesn’t have to type the below command over and over in order to include three of the four possible values, every time.

PS > New-RandomPassword -CharacterType Lowercase,Number,Uppercase

That’s when it dawned on me. I’m not sure I’ve ever used the $PSDefaultParameterValues preference variable with multiple parameter values. It’s up there, but to review, the function is New-RandomPassword, and the parameter where we want default values is called CharacterType. The default values we always want included are Lowercase, Number, and Uppercase. As you’d expect, we’re avoiding symbols in our random passwords for this example.

$PSDefaultParameterValues = @{'New-RandomPassword:CharacterType'='Number','Uppercase','Lowercase'}

$PSDefaultParameterValues['New-RandomPassword:CharacterType'] = 'Number','Uppercase','Lowercase'

$PSDefaultParameterValues.Add('New-RandomPassword:CharacterType',@('Number','Uppercase','Lowercase'))

Although you’d only need to run one of them, each of the above examples will modify $PSDefaultParameterValues the identical way.

PS > $PSDefaultParameterValues | Format-Table -AutoSize

Name                             Value
----                             -----
New-RandomPassword:CharacterType {Number, Uppercase, Lowercase}

Now, whenever New-RandomPassword is invoked (on a system where this entry is a part of $PSDefaultParameterValues, obviously), it’ll include those values, for that parameter, without the need to actually type the parameter name and the values — neat. As of now, I can say I’ve include multiple values for a single parameter using the $PSDefaultParameterValues variable. You too, or at least you can say you know it’s an option.

Read-Host with a Previous Value

Update: There’s a Part II, read it next.

Here’s a quick one. For an upcoming project, I decided I may need a way to use Read-Host in combination with a previously set, default value. Here’s the working code I threw together in case it’s needed.

$User = 'tommymaynard'

If (($Result = Read-Host -Prompt "Press Enter for the last user [$User], or enter a new user") -eq '' -or $Result -eq $User) {
    "You're using the previously used ""$User"" user."
} Else {
    "You're using the previously unused ""$Result"" user."
}

I set my user to “tommymaynard,” and then the If statement executes. The execution pauses while the Read-Host cmdlet waits for my input. If I press Enter, the If portion runs indicating that I’m using the previously set value of “tommymaynard.” It’ll do that if I enter tommymaynard again, too. If I enter a new value, it indicates I’m using the new value.

While we’re here, let’s run the code with three possible values: nothing entered, tommymaynard entered, and a different user entered.

Press Enter for the last user [tommymaynard], or enter a new user:
You’re using the previously used “tommymaynard” user.

Press Enter for the last user [tommymaynard], or enter a new user: tommymaynard
You’re using the previously used “tommymaynard” user.

Press Enter for the last user [tommymaynard], or enter a new user: lsmith
You’re using the previously unused “lsmith” user.

And there it is, a super short post about using a previously set value for Read-Host.

AWS UserData Multiple Run Framework Part III

Update: There’s a fourth version of this article series. See the link at the bottom of this post.

At the bottom of the below post (Part II), was a function that created code for what I called the AWS UserData Multiple Run Framework. The code produced by this function can be added to the AWS CloudFormation UserData section allowing for Windows EC2 instance configuration between a controlled number of restarts. You know… configure, restart, configure, restart, configure, restart, etc. Without this in place, or some other configuration tool, you only get one time to utilize the UserData section, and that’s when an EC2 instance launches for the first time.

That version, however, required the use of text files at the root of the C:\ drive. These were used so that the UserData code would know which section of the code to run after each restart. I always worried that someone wouldn’t know the importance of the text files at the root of the C:\ drive and remove them. Therefore, I wrote a newer version. I believe I previously mentioned that as a possibility, in one of the two other related posts.

AWS UserData Multiple Run Framework Part II

This version—the newest version—makes use of the Windows Registry to make the determination of what code to run next. There are no more scattered files on the root of the C:\, and instead, everything is better protected, and hidden, in the Windows Registry. Let’s briefly discuss each section first. Then, use the function at the bottom of this post. It creates the code that you would add to the AWS CloudFormation UserData section. It isn’t the code you’d add to the UserData section. It’s still PowerShell creating PowerShell. For reference, think of the Microsoft function New-IseSnippet of old, and the New-ModuleManifest cmdlet. They’re both PowerShell, that create PowerShell.

Function Set-SystemForNextRun {
    Param (
        [string]$Pass,
        [switch]$UserData,
        [switch]$Restart
    )
    If ($Pass) {
        [System.Void](New-ItemProperty -Path 'HKLM:\SOFTWARE\DEPT' -Name "Pass$Pass" -Value 'Complete')
    }
    If ($UserData) {
        $Path = "$env:ProgramFiles\Amazon\Ec2ConfigService\Settings\config.xml"
        [xml]$ConfigXml = Get-Content -Path $Path
        ($ConfigXml.Ec2ConfigurationSettings.Plugins.Plugin |
            Where-Object -Property Name -eq 'Ec2HandleUserData').State = 'Enabled'
        $ConfigXml.Save($Path)
    }
    If ($Restart) {
        Restart-Computer -Force
    }
}

The Set-SystemForNextRun function—the first part of the code that’s created—has some minor changes. First, the PassFile parameter is now just Pass. This was changed because, well, we’re not using files anymore. Second, the code inside the If ($Pass) section no longer creates text files and instead, creates registry values. Remember, as this code runs inside the AWS CloudFormation UserData section, this function is written to memory before it’s ever used. The two upcoming sections are actually run, or rather, do things we can see, first.

# Check if Registry Subkey does not exist.
If (-Not(Get-Item -Path 'HKLM:\SOFTWARE\DEPT' -ErrorAction SilentlyContinue)) {
    # Create Registry Subkey.
    [System.Void](New-Item -Path 'HKLM:\SOFTWARE\' -Name 'DEPT')
}

The above code is the first run code in UserData, as again our Set-SystemForNextRun function is written to memory and yet to be invoked. Its purpose is to create a Windows Registry subkey DEPT if it doesn’t already exist. You can change DEPT to whatever makes the most sense for your use. This is the location where we’ll store our values that indicate which section in the upcoming If-ElseIf (ElseIf, ElseIf, etc.) statement we’ll run.

If (-Not((Get-ItemProperty -Path 'HKLM:\SOFTWARE\DEPT').Pass1 -eq 'Complete')) {

    # Place code here (1).

    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -Pass '1' -UserData -Restart

} ElseIf (-Not((Get-ItemProperty -Path 'HKLM:\SOFTWARE\DEPT').Pass2 -eq 'Complete')) {

    # Place code here (2).

    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -Pass '2'

}

This is the meat of what’s produced by the upcoming function. It’s the If-ElseIf statement. On the first run of the UserData, the If statement will fire. This is because we won’t have a value called Pass1 set to the string “Complete” in the HKLM:\SOFTWARE\DEPT subkey. After its code is done, it’ll invoke the Set-SystemForNextRun function which will add the Pass1 value to the Registry, set UserData to enabled, and restart the instance. On the next run, it’ll execute the code in the first (and only) ElseIf section because the value Pass1 will have been created, but the value Pass2 will not have been created. It gets created after the code in this section is complete.

That’s it. Hopefully, this can be helpful for more than just me and those around me at work.

Function New-AWSMultiRunTemplate {
    [CmdletBinding()]
    Param (
        [Parameter()]
        [ValidateRange(1,10)]
        [int]$CodeSectionCount = 2,

        [Parameter()]
        [ValidateSet('All','AllButLast')]
        [string]$EnableUserData = 'AllButLast',

        [Parameter()]
        [ValidateSet('All','AllButLast')]
        [string]$EnableRestart = 'AllButLast'
    )

    DynamicParam {
        # Create dynamic, Log parameter.
        If ($PSBoundParameters['Verbose']) {
            $SingleAttribute = New-Object System.Management.Automation.ParameterAttribute
            $SingleAttribute.Position = 1

            $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
            $AttributeCollection.Add($SingleAttribute)

            $LogParameter = New-Object System.Management.Automation.RuntimeDefinedParameter('Log',[switch],$AttributeCollection)
            $LogParameter.Value = $true
 
            $ParamDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
            $ParamDictionary.Add('Log',$LogParameter)
            return $ParamDictionary
        } # End If.
    } # End DynamicParam.

    Begin {
        #region Create logs directory and Write-Verbose function.
        If ($PSBoundParameters['Verbose'] -and $PSBoundParameters['Log']) {
            $LogDirectory = "$($MyInvocation.MyCommand.Name)"
            $LogPath = "$env:SystemDrive\support\Logs\$LogDirectory"
            If (-Not(Test-Path -Path $LogPath)) {
                [System.Void](New-Item -Path $LogPath -ItemType Directory)
            }
            $LogFilePath = "$LogPath\$(Get-Date -Format 'DyyyyMMddThhmmsstt').txt"

            Function Write-Verbose {
                Param ($Message)
                Microsoft.PowerShell.Utility\Write-Verbose -Message $Message
                Microsoft.PowerShell.Utility\Write-Verbose -Message "[$(Get-Date -Format G)]: $Message" 4>> $LogFilePath
            }
        }

        # Set Write-Verbose block location.
        $BlockLocation = '[BEGIN  ]'
        Write-Verbose -Message "$BlockLocation Entering the Begin block [Function: $($MyInvocation.MyCommand.Name)]."
        #endregion

        Write-Verbose -Message "$BlockLocation Storing the template's function to memory."
        $TemplateFunction = @"
Function Set-SystemForNextRun {
    Param (
        [string]`$Pass,
        [switch]`$UserData,
        [switch]`$Restart
    )
    If (`$Pass) {
        [System.Void](New-ItemProperty -Path 'HKLM:\SOFTWARE\DEPT' -Name "Pass`$Pass" -Value 'Complete')
    }
    If (`$UserData) {
        `$Path = "`$env:ProgramFiles\Amazon\Ec2ConfigService\Settings\config.xml"
        [xml]`$ConfigXml = Get-Content -Path `$Path
        (`$ConfigXml.Ec2ConfigurationSettings.Plugins.Plugin |
            Where-Object -Property Name -eq 'Ec2HandleUserData').State = 'Enabled'
        `$ConfigXml.Save(`$Path)
    }
    If (`$Restart) {
        Restart-Computer -Force
    }
}

# Check if Registry Subkey does not exist.
If (-Not(Get-Item -Path 'HKLM:\SOFTWARE\DEPT' -ErrorAction SilentlyContinue)) {
    # Create Registry Subkey.
    [System.Void](New-Item -Path 'HKLM:\SOFTWARE\' -Name 'DEPT')
}


"@
    } # End Begin.

    Process {
        #region Set Write-Verbose block location.
        $BlockLocation = '[PROCESS]'
        Write-Verbose -Message "$BlockLocation Entering the Process block [Function: $($MyInvocation.MyCommand.Name)]."
        #endregion

        Write-Verbose -Message "$BlockLocation Beginning to create the If-ElseIf code for the template."
        1..$CodeSectionCount | ForEach-Object {
            If ($_ -eq 1) {
                $Start = 'If'
            } Else {
                $Start = 'ElseIf'
            }

            If ($EnableUserData -eq 'All') {
                $UserData = '-UserData '
            } ElseIf ($_ -eq $CodeSectionCount) {
                $UserData = $null
            } Else {
                $UserData = '-UserData '
            }

            If ($EnableRestart -eq 'All') {
                $Restart = '-Restart'
            } ElseIf ($_ -eq $CodeSectionCount) {
                $Restart = $null
            } Else {
                $Restart = '-Restart'
            }

            $TemplateIfElseIf += @"
$Start (-Not((Get-ItemProperty -Path 'HKLM:\SOFTWARE\DEPT').Pass$_ -eq 'Complete')) {
    
    # Place code here ($_).

    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -Pass '$_' $UserData$Restart

} $End
"@
        } # End ForEach-Object.
    } # End Process.

    End {
        #region Set Write-Verbose block location.
        $BlockLocation = '[END    ]'
        Write-Verbose -Message "$BlockLocation Entering the End block [Function: $($MyInvocation.MyCommand.Name)]."
        #endregion

        Write-Verbose -Message "$BlockLocation Displaying the DEPT AWS mulitple run code with $CodeSectionCount code section(s)."
        "$TemplateFunction$TemplateIfElseIf"
    } # End End.
} # End Function: New-AWSMultiRunTemplate.

Read IV

On Disk Credentials

This article has been written by plenty of people, plenty of times. But even so, I’m writing it again. This is partly because I want to have a place to find this information written by myself. That said, if it helps anyone else too, then it’s doing the other part, and that’s helping those around me.

What I want to do today, is create an on disk credential file. I’ve done it a few times now, for a few projects, but there’s something about this topic, I can’t forever nail down in my own brain. Well, as best as I see it, those days are numbered.

Once my on disk credential file is created, I want to run an automated task(s) as a user other than that which is running my outlying automation. It’s kind of like this: User1 runs an automated task and it needs to complete some work it can’t do on it’s own. Therefore, it does a part of what it needs as User2. That’s the user that can do what User1 can’t.

The first step is creating the credential file. Now, when you think of credentials, you should think of the combination of a username and a corresponding password, together. That said, this credential file is actually only going to hold the password. While I’m going to continue to call it a credential file, keep in mind that we’re only storing the password inside this file.

In the below example, we’re taking a password, as a standard string, converting it to a secure string, converting that (the secure string) into an encrypted standard string, and writing it out to an on disk file. Before we begin, we need to open our PowerShell host (the ConsoleHost, the ISE, etc.). Based on User1 and User2, we’d open PowerShell as User1. Remember, User1 is going to run our script, function, automation, etc., but it’s going to require creating a credential object — a username and password — for User2.

One other consideration to keep in mind is the computer on which this password file will be used. You have to use the credential file on the computer in which it’s created, and with the user that created it.

$PasswordAsString = '1234)(#DGTh@ppYMedixM.'
$PassOut = ConvertTo-SecureString -String $PasswordAsString -AsPlainText -Force
$PassOut | ConvertFrom-SecureString | Out-File -FilePath 'C:\Users\tommymaynard\Desktop\cred.txt'

Here’s what each line in the above example accomplishes:

Line 1: Creates a variable to hold the password as a standard string. A standard string is like any other string, such as the word dog, cat, or house. Each of those is a standard string, and most often just called a string. We’re including the word “standard,” as a way to differentiate among the other string types we’ll discuss.
Line 2: Creates a variable to hold the password once it’s been converted to a secure string. The AsPlainText and Force parameters are required for ConvertTo-SecureString to accept a standard sting for conversion.
Line 3: Converts the password as stored in the variable created in Line 2 as a secure string, to an encrypted standard string and saves it in an on disk file called cred.txt on my desktop.

Here’s the thing: The above process is a one time thing. Once the on disk credential file is created, then that step will never have to be duplicated (unless you change User2’s password). Remember, you have to open PowerShell as the user that will use the password, not as the account to which the password corresponds.

The next bit of code is how we use the on disk credential file.

$User = 'domain\User2'
$PassIn = Get-Content -Path 'C:\Users\tommymaynard\Desktop\cred.txt' | ConvertTo-SecureString
$Credential = New-Object System.Management.Automation.PSCredential -ArgumentList $User,$PassIn

Here’s what each line in this example accomplishes:

Line 1: Creates a variable to hold the username of the user that corresponds to this password.
Line 2: Creates a variable to hold the password. This command will read in the contents of our cred.txt file from our first example, and convert the encrypted standard string in the file to secure string.
Line 3: Creates a variable to store the credential object. This includes both the username from line 1, and the password we brought in from line 2.

That’s it. Again, this is a two step process. The first example was how to prestage our credential file, and the second example allows us to make use of it. The first example would likely be done once, manually, and as part of a prestaging effort. The second example would be done inside of the script, etc., to make use of running something as a user (User2) other than the one running the script, etc. itself (User1).

Okay Tommy, you now have a place to turn once you’re forgotten how to do this. You too, Internet.

Technical Fact at PowerShell Launch

When I read books, or websites, and find worthy facts, I aim to try and keep them. If I read a book, my bookmark is often a few pieces of paper stapled together with info and page numbers from the book. Well, I’m scrapping that technique, which went hand-in-hand with folding down page corners. I’m also ditching the small pieces of paper that litter my desk with random bits of information: “PowerShell objects come from classes,” and “LastLogonDate is the converted version of LastLogonTimeStamp.” Now, it’s all slated to go in a single file called InfoLines.txt in Dropbox, here: C:\Users\tommymaynard\Dropbox\PowerShell\Profile.

If you’re wondering why I use a Dropbox folder it’s because I want this file and its worthy facts to be available regardless of whether I’m on my work, or home computer. You can read more in a post I wrote that uses Dropbox to sync my profile script between work and home, here: http://tommymaynard.com/sync-profile-script-from-work-to-home-2017. It may help make what I’m doing here make more sense.

For now, because I just started this today, I only have a few lines of worthy information. Here’s the contents of my InfoLines.txt file so far. If you can’t tell, I’m finishing up Amazon Web Services in Action. I only have 70 more pages to go!

The auto-scaling group is responsible for connecting a newly launched EC2 instance with the load balancer (ELB). (AWS in Action, p.315)
DevOps is an approach driven by software development to bring development and operations closer together. (AWS in Action, p.93)
Auto-scaling is a part of the EC2 service and helps you to ensure that a specified number of virtual servers are running. (AWS in Action, p.294)

Each time I open the ConsoleHost, the ISE, or Visual Studio Code, I want a random line from the file to be shared with me. The below code will return a single, random line with asterisks both above and below it. This is in order to help separate it from the (totally unnecessary, unwelcome, and shouldn’t even be there) message that tells me how long my “personal and system profiles” took to load, and my prompt. They need to put that message in a variable and not on my screen without permission. Don’t tell us what you think we need to know, guys.

'**************'
Get-Content -Path "$env:USERPROFILE\Dropbox\PowerShell\Profile\InfoLines.txt" | Get-Random
'**************'

That’s it. Now, whenever I open one of these PowerShell hosts, I’ll get a quick reminder about something I found important, and want to keep fresh in my mind.

Update: I decided I wanted the asterisks above and below my technical fact to go from one end of the PowerShell host to other. Here’s how I did that.

'*' * ($Host.UI.RawUI.BufferSize.Width - 1)
Get-Content -Path "$env:USERPROFILE\Dropbox\PowerShell\Profile\InfoLines.txt" | Get-Random
'*' * ($Host.UI.RawUI.BufferSize.Width - 1)

AWS EC2 Instance CSV File: Let’s Have it, Amazon

A while back I found something that AWS began offering that made the below paragraph meaningless. I’ll be back with more soon. I know I said that before, but I’ll try extra hard this time…

I’ve wanted it for some time now… an always up-to-date CSV file that I can programmatically download—using PowerShell, duh—directly from AWS. But not just any CSV. I need a continually updated CSV that includes all the possible information on every AWS EC2 instance type. It can even include the previous generation instances, providing there’s a column that indicates whether it’s current or not. It would probably contain information I’ll never need, or want, and I’d still want the file, as I can easily filter against the contents of the document. I’d love to write the website post that shows how to do that if we can get this file to fruition.

 

Forcing a Switch Parameter to be False

The coworker that sits next to me at the office has upped his PowerShell game tremendously since he’s moved up from Operations to the Windows Team. He often has well-thought-out questions and he’s long shown a great desire to learn about PowerShell. It’s addictive and rewarding, so there’s no question as to why. He may not know it, but I look forward to the questions. The need for PowerShell answers here and there keeps me fresh, and so while it benefits him, it benefits me too.

As he does, he asked a question recently. It was in regard to the Confirm parameter. Why does it need the colon and $false to not be true? Why does it need to be written like this: -Confirm:$false? Why can’t it be written like this: -Confirm $false?

If a switch parameter is included, then its value is $true. If it’s not included, its value is $false. Knowing that alone, makes it clear that we’d have to force something to be false if its default is $true. Okay, knowing that, why can’t we do this then: -Confirm $false?

It’s got to do with the PowerShell parser. It’s how the engine evaluates the statement. Remember, it’s a switch parameter. The parameter is either included or it’s not. The switch parameter is inherently never dependent on a value to be included with it. It doesn’t work that way. The PowerShell engine will see -Confirm and make it $true, long before it even sees the $false value. The colon and value being attached to the Confirm parameter ensures the parser knows you want it to be of that specific value.

AWS UserData Multiple Run Framework Part II

My first post on the topic of using AWS UserData (via CloudFormation and PowerShell), was posted recently (http://tommymaynard.com/aws-userdata-multiple-run-framework-2017). While it didn’t light up Twitter and Facebook, where it was shared, there was still some value to it for me, and at least one other person. Therefore, I’ve decided to do part two. This is one of these using-PowerShell-to-create-PowerShell posts. I just love these.

A quick recap on the first post. I use the below PowerShell function in all the UserData sections of all my CloudFormation documents (when building Windows systems). Additionally, I also include an If-ElseIf language construct to do specific actions against an EC2 instance, as it’s coming online. The best part here is that I can do instance restarts between configuration steps. While I’ll include the full example (actually put together), from Part I below, it might be best to read that first post (link above).

Function Set-SystemForNextRun {
    Param (
        [string]$PassFile,
        [switch]$UserData,
        [switch]$Restart
    )
    If ($PassFile) {
        [System.Void](New-Item -Path "$env:SystemDrive\passfile$PassFile.txt" -ItemType File)
    }
    If ($UserData) {
        $Path = "$env:ProgramFiles\Amazon\Ec2ConfigService\Settings\config.xml"
        $ConfigXml = Get-Content -Path $Path
        ($ConfigXml.Ec2ConfigurationSettings.Plugins.Plugin |
            Where-Object -Property Name -eq 'Ec2HandleUserData').State = 'Enabled'
        $ConfigXml.Save($Path)
    }
    If ($Restart) {
        Restart-Computer -Force
    }
}

If (-Not(Test-Path -Path "$env:SystemDrive\passfile1.txt")) {
 
    # Place code here (1).
 
    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -PassFile '1' -UserData -Restart
 
} ElseIf (-Not(Test-Path -Path "$env:SystemDrive\passfile2.txt")) {
 
    # Place code here (2).
 
    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -PassFile '2' -UserData -Restart
 
} ElseIf (-Not(Test-Path -Path "$env:SystemDrive\passfile3.txt")) {
 
    # Place code here (3).
 
    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -PassFile '3' -UserData -Restart
 
} ElseIf (-Not(Test-Path -Path "$env:SystemDrive\passfile4.txt")) {
 
    # Place code here (4).
 
    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -PassFile '4'
 
}

Here’s the thing, the above example is going to restart my computer three times: after the If portion, after the first ElseIf, and after the second ElseIf portion. These three restarts effectively give me four areas in which to configure an EC2 instance in sequence and again, between restarts.

There’s no way for me to predict how many code runs you or I are going to need with each new project, whether or not we’ll want to enable UserData after the final code run, or even if we’ll need a final, final restart. Therefore, I’ve written a PowerShell function to help with this process. Why manually edit this If-ElseIf statement if you don’t have to, right? With this newer function, you can decide how many code runs you need and whether you need to enable UserData or issue a restart after your final code section executes. Here are a few examples with the full function, New-AWSMultiRunTemplateat the end of today’s post.

In this first example, we’ll just run the function with its default settings. This will create the Set-SystemForNextRun function and an If-ElseIf statement (with two places to configure the instance and a single restart in between them. If this is what you needed, you’d take the output of the New-AWSMultiRunTemplate function invocation and paste it into your AWS CloudFormation’s UserData section, between an open and close PowerShell tag: <powershell> and </powershell>.

New-AWSMultiRunTemplate
Function Set-SystemForNextRun {
    Param (
        [string]$PassFile,
        [switch]$UserData,
        [switch]$Restart
    )
    If ($PassFile) {
        [System.Void](New-Item -Path "$env:SystemDrive\passfile$PassFile.txt" -ItemType File)
    }
    If ($UserData) {
        $Path = "$env:ProgramFiles\Amazon\Ec2ConfigService\Settings\config.xml"
        [xml]$ConfigXml = Get-Content -Path $Path
        ($ConfigXml.Ec2ConfigurationSettings.Plugins.Plugin |
            Where-Object -Property Name -eq 'Ec2HandleUserData').State = 'Enabled'
        $ConfigXml.Save($Path)
    }
    If ($Restart) {
        Restart-Computer -Force
    }
}

If (-Not(Test-Path -Path "$env:SystemDrive\passfile1.txt")) {

    # Place code here (1).

    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -PassFile '1' -UserData -Restart

} ElseIf (-Not(Test-Path -Path "$env:SystemDrive\passfile2.txt")) {

    # Place code here (2).

    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -PassFile '2'

}

This next run will create a single If statement. I suppose if you’re only going to do a single configuration pass, you don’t need this function at all. Again, I said I use it for all my CloudFormation templates. Say you edit your CloudFormation document later and add more configuration and restarts, you’ll already have most of what you need to be included in your CloudFormation document.

New-AWSMultiRunTemplate -CodeSectionCount 1
Function Set-SystemForNextRun {
    Param (
        [string]$PassFile,
        [switch]$UserData,
        [switch]$Restart
    )
    If ($PassFile) {
        [System.Void](New-Item -Path "$env:SystemDrive\passfile$PassFile.txt" -ItemType File)
    }
    If ($UserData) {
        $Path = "$env:ProgramFiles\Amazon\Ec2ConfigService\Settings\config.xml"
        [xml]$ConfigXml = Get-Content -Path $Path
        ($ConfigXml.Ec2ConfigurationSettings.Plugins.Plugin |
            Where-Object -Property Name -eq 'Ec2HandleUserData').State = 'Enabled'
        $ConfigXml.Save($Path)
    }
    If ($Restart) {
        Restart-Computer -Force
    }
}

If (-Not(Test-Path -Path "$env:SystemDrive\passfile1.txt")) {

    # Place code here (1).

    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -PassFile '1'

}

Now, let’s get crazy! We’re going to add a bunch of ElseIf statements in this example. Additionally, we’re going to highlight a couple of other features. There are parameters to use if you wish to include a final UserData reset and a final restart. Take a look at the below parameters used with the New-AWSMultiRunTemplate.

New-AWSMultiRunTemplate -CodeSectionCount 6 -EnableUserData All -EnableRestart All
Function Set-SystemForNextRun {
    Param (
        [string]$PassFile,
        [switch]$UserData,
        [switch]$Restart
    )
    If ($PassFile) {
        [System.Void](New-Item -Path "$env:SystemDrive\passfile$PassFile.txt" -ItemType File)
    }
    If ($UserData) {
        $Path = "$env:ProgramFiles\Amazon\Ec2ConfigService\Settings\config.xml"
        [xml]$ConfigXml = Get-Content -Path $Path
        ($ConfigXml.Ec2ConfigurationSettings.Plugins.Plugin |
            Where-Object -Property Name -eq 'Ec2HandleUserData').State = 'Enabled'
        $ConfigXml.Save($Path)
    }
    If ($Restart) {
        Restart-Computer -Force
    }
}

If (-Not(Test-Path -Path "$env:SystemDrive\passfile1.txt")) {

    # Place code here (1).

    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -PassFile '1' -UserData -Restart

} ElseIf (-Not(Test-Path -Path "$env:SystemDrive\passfile2.txt")) {

    # Place code here (2).

    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -PassFile '2' -UserData -Restart

} ElseIf (-Not(Test-Path -Path "$env:SystemDrive\passfile3.txt")) {

    # Place code here (3).

    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -PassFile '3' -UserData -Restart

} ElseIf (-Not(Test-Path -Path "$env:SystemDrive\passfile4.txt")) {

    # Place code here (4).

    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -PassFile '4' -UserData -Restart

} ElseIf (-Not(Test-Path -Path "$env:SystemDrive\passfile5.txt")) {

    # Place code here (5).

    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -PassFile '5' -UserData -Restart

} ElseIf (-Not(Test-Path -Path "$env:SystemDrive\passfile6.txt")) {

    # Place code here (6).

    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -PassFile '6' -UserData -Restart

}

Notice that in the final ElseIf above, we have the UserData and Restart parameters. These were added by including the EnableUserData and EnableRestart parameters, and All as the parameters’ value. By default, they’re both set to the AllButLast values. And that it’s! Here’s the New-AWSMultiRunTemplate function, in case it’s something you might find helpful! Enjoy!!

Function New-AWSMultiRunTemplate {
    [CmdletBinding()]
    Param (
        [Parameter()]
        [ValidateRange(1,10)]
        [int]$CodeSectionCount = 2,

        [Parameter()]
        [ValidateSet('All','AllButLast')]
        [string]$EnableUserData = 'AllButLast',

        [Parameter()]
        [ValidateSet('All','AllButLast')]
        [string]$EnableRestart = 'AllButLast'
    )

    DynamicParam {
        # Create dynamic, Log parameter.
        If ($PSBoundParameters['Verbose']) {
            $SingleAttribute = New-Object System.Management.Automation.ParameterAttribute
            $SingleAttribute.Position = 1

            $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
            $AttributeCollection.Add($SingleAttribute)

            $LogParameter = New-Object 
System.Management.Automation.RuntimeDefinedParameter('Log',[switch],$AttributeCollection)
            $LogParameter.Value = $true
 
            $ParamDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
            $ParamDictionary.Add('Log',$LogParameter)
            return $ParamDictionary
        } # End If.
    } # End DynamicParam.

    Begin {
        #region Create logs directory and Write-Verbose function.
        If ($PSBoundParameters['Verbose'] -and $PSBoundParameters['Log']) {
            $LogDirectory = "$($MyInvocation.MyCommand.Name)"
            $LogPath = "$env:SystemDrive\support\Logs\$LogDirectory"
            If (-Not(Test-Path -Path $LogPath)) {
                [System.Void](New-Item -Path $LogPath -ItemType Directory)
            }
            $LogFilePath = "$LogPath\$(Get-Date -Format 'DyyyyMMddThhmmsstt').txt"

            Function Write-Verbose {
                Param ($Message)
                Microsoft.PowerShell.Utility\Write-Verbose -Message $Message
                Microsoft.PowerShell.Utility\Write-Verbose -Message "[$(Get-Date -Format G)]: $Message" 4&amp;gt;&amp;gt; 
$LogFilePath
            }
        }

        # Set Write-Verbose block location.
        $BlockLocation = '[BEGIN  ]'
        Write-Verbose -Message "$BlockLocation Entering the Begin block [Function: $($MyInvocation.MyCommand.Name)]."
        #endregion

        Write-Verbose -Message "$BlockLocation Storing the template's function to memory."
        $TemplateFunction = @"
Function Set-SystemForNextRun {
    Param (
        [string]`$PassFile,
        [switch]`$UserData,
        [switch]`$Restart
    )
    If (`$PassFile) {
        [System.Void](New-Item -Path "`$env:SystemDrive\passfile`$PassFile.txt" -ItemType File)
    }
    If (`$UserData) {
        `$Path = "`$env:ProgramFiles\Amazon\Ec2ConfigService\Settings\config.xml"
        [xml]`$ConfigXml = Get-Content -Path `$Path
        (`$ConfigXml.Ec2ConfigurationSettings.Plugins.Plugin |
            Where-Object -Property Name -eq 'Ec2HandleUserData').State = 'Enabled'
        `$ConfigXml.Save(`$Path)
    }
    If (`$Restart) {
        Restart-Computer -Force
    }
}


"@
    } # End Begin.

    Process {
        #region Set Write-Verbose block location.
        $BlockLocation = '[PROCESS]'
        Write-Verbose -Message "$BlockLocation Entering the Process block [Function: $($MyInvocation.MyCommand.Name)]."
        #endregion

        Write-Verbose -Message "$BlockLocation Beginning to create the If-ElseIf code for the template."
        1..$CodeSectionCount | ForEach-Object {
            If ($_ -eq 1) {
                $Start = 'If'
            } Else {
                $Start = 'ElseIf'
            }

            If ($EnableUserData -eq 'All') {
                $UserData = '-UserData '
            } ElseIf ($_ -eq $CodeSectionCount) {
                $UserData = $null
            } Else {
                $UserData = '-UserData '
            }

            If ($EnableRestart -eq 'All') {
                $Restart = '-Restart'
            } ElseIf ($_ -eq $CodeSectionCount) {
                $Restart = $null
            } Else {
                $Restart = '-Restart'
            }

            $TemplateIfElseIf += @"
$Start (-Not(Test-Path -Path "`$env:SystemDrive\passfile$_.txt")) {
    
    # Place code here ($_).

    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -PassFile '$_' $UserData$Restart

} $End
"@
        } # End ForEach-Object.
    } # End Process.

    End {
        #region Set Write-Verbose block location.
        $BlockLocation = '[END    ]'
        Write-Verbose -Message "$BlockLocation Entering the End block [Function: $($MyInvocation.MyCommand.Name)]."
        #endregion

        Write-Verbose -Message "$BlockLocation Displaying the AWS mulitple run code with $CodeSectionCount code 
section(s)."
        "$TemplateFunction$TemplateIfElseIf"
    } # End End.
} # End New-AWSMultiRunTemplate function.

Read III