Abstraction Is Your Friend When You Have Tight Deadlines
We recently took on a very bespoke project for a company who wanted to automate their Active Directory User creation from their MIS database and they also wanted to detect any changes to users within the MIS database and automatically update Active Directory… oh, and they need this in 2 weeks time!
A team of us got together on a conference call with the client to go through their requirements and to get a handle on what would be needed to complete the task on time. Once we’d agreed the plan, we spun up a replica of the client’s environment in our lab which allowed me to work on the project without any risk to the clients live environment. I chose PowerShell to achieve what was required as it was most suited to the task although we did discuss Azure Function Apps.
When you’re working to a tight deadline, it’s sometimes easy to get carried away with the requirements and just create a linear, procedural script which simply follows a path, but it’s so much more elegant to abstract each section into a function and then call the functions when necessary. As well as elegance, this approach also allows you to organise and standardise what would otherwise be chaotic code. This approach, in turn, speeds up any debugging you need to do… in short: it puts things in exactly the right place for when you need to get to them. Abstract whenever you can – it will make your life easier in the future!
With this in mind, before I wrote any code for the customer, I put together a framework that I would be able to use to build the final product. My framework was created in the following order:
- Boilerplate Start with some brief information about the script. Don’t go into too much detail here – this shouldn’t be a biography!
- Command Line Parameters This section is to allow for any parameters you may wish to accept.
- Script Variables Keeping all your variables together in one place makes it much easier to keep track of things as things progress.
- Script Environment Setup This section is to add anything that has to run each time this script runs, but before any of the functions are created.
- Framework Functions These are functions that are part of the framework and not actually to do with the eventual task this script will perform. This includes things like writing to the event log and sending emails.
- Script Functions This is where all the script functions will live. Each task the script will undertake is created in a function and then called in the running section.
- Running Section This is where the functions are actually called. This is where all the script logic will exist.
By creating and using a framework like this, it allowed me to focus on coding each specific task, without trying to sift through a mess of code. Each section is clearly laid out and easy to read. Working this way will also help in the future when someone has to go back through the code to make changes – everything should be easy to find. I also created some documentation for the script which was given to the customer which will also assist future alterations.
Here’s the framework:
<#
.DESCRIPTION
Description of script.
.PARAMETER VerboseLogging
Enables Verbose Logging to the Application Log.
.NOTES
Version: X.Y
Author: Somerset Admin
Email: info@somersetsysadmin.co.uk
Creation Date: 17/08/2020
#>
################################
# Script Parameters #
################################
[CmdletBinding()]
param (
[Switch]$VerboseLogging = $false # Enable Verbose Logging
)
################################
# Set Script Variables #
################################
[string]$EventLogName = "AD Automation" # What source name do you want to give to Event Logs
[string]$EmailFromAddress = "email@domain.com" # Email Address to send any email alerts from
[string[]]$EmailToAddress = "email1@domain.com", # Email Address to send any email alerts to
"email2@domain.com", # Email Address to send any email alerts to
"email3@domain.com" # Email Address to send any email alerts to
[string]$EmailSmtpServer = "smtp.server.local" # FQDN of email relay server
################################
# Setup Script Environment #
################################
New-EventLog -LogName Application -Source $EventLogName -ErrorAction SilentlyContinue
################################################################
# Start of Framework Functions #
################################################################
################################
# Event Log Write Function #
################################
Function Add-LogEntry{
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[String]$Message,
[Parameter(Mandatory)]
[Int] $EventID,
[Bool] $IsError = $false,
[Bool] $IsVerbose = $false
)
if($IsError){
Write-EventLog -LogName "Application" -Source $EventLogName -EventID $EventID -EntryType Error -Message $Message -ErrorAction SilentlyContinue
}else{
if($VerboseLogging){
Write-EventLog -LogName "Application" -Source $EventLogName -EventID $EventID -EntryType Information -Message $Message -ErrorAction SilentlyContinue
}else{
if(!($IsVerbose)){
Write-EventLog -LogName "Application" -Source $EventLogName -EventID $EventID -EntryType Information -Message $Message -ErrorAction SilentlyContinue
}
}
}
}
################################
# Check User Is Elevated #
################################
Function Test-Elevated {
$CurrentUser = [Security.Principal.WindowsIdentity]::GetCurrent();
(New-Object Security.Principal.WindowsPrincipal $CurrentUser).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
}
################################
# Send Email #
################################
Function New-MailMessage {
[CmdletBinding()]
param(
[parameter(Mandatory)]
[string]$Subject,
[parameter(Mandatory)]
[string]$Body
)
writeLog -Message "Starting $($MyInvocation.MyCommand) Function" -EventID 800 -IsVerbose:$True
Try {
Send-MailMessage -from $EmailFromAddress -to $EmailToAddress -SmtpServer $EmailSmtpServer -Subject $Subject -Body $Body
} catch {
$ErrorDetails = "$($MyInvocation.MyCommand)`n`n$($_.Exception.message)`n$($_.InvocationInfo.MyCommand.Name)`n$($_.ErrorDetails.Message)`n$($_.InvocationInfo.PositionMessage)`n$($_.CategoryInfo.ToString())`n$($_.FullyQualifiedErrorId)"
writeLog -Message $ErrorDetails -EventID 800 -IsError:$true
}
writeLog -Message "Finished $($MyInvocation.MyCommand) Function" -EventID 800 -IsVerbose:$True
}
################################################################
# End of Framework Functions #
################################################################
################################################################
# Start of Functions #
################################################################
################################
# Blank Function Template #
################################
Function New-FunctionName {
[CmdletBinding()]
param(
[parameter(Mandatory)]
[string]$String,
[bool]$Bool
)
writeLog -Message "Starting $($MyInvocation.MyCommand) Function" -EventID 800 -IsVerbose:$True
Try {
### SCRIPT FUNCTION GOES HERE ###
} catch {
$ErrorDetails = "$($MyInvocation.MyCommand)`n`n$($_.Exception.message)`n$($_.InvocationInfo.MyCommand.Name)`n$($_.ErrorDetails.Message)`n$($_.InvocationInfo.PositionMessage)`n$($_.CategoryInfo.ToString())`n$($_.FullyQualifiedErrorId)"
writeLog -Message $ErrorDetails -EventID 800 -IsError:$true
}
writeLog -Message "Finished $($MyInvocation.MyCommand) Function" -EventID 800 -IsVerbose:$True
}
################################################################
# End of Functions #
################################################################
################################################################
# Start of Running Script #
################################################################
writeLog -Message "Starting Running All Tasks" -EventID 800
################################
# Run Processes #
################################
################################################################
# End of Running Script #
################################################################
writeLog -Message "Finished Running All Tasks" -EventID 800