Detect ARS cmdlet versions

Here’s a script I found on the internet, and modified to show me not only the PowerShell version numbers but the ARS version details – if installed that is….

You can use this in any script that needs to check the version numbers meet the minimum requirements by accessing the objects parameters and comparing version numbers.

function Get-PSVersion {
<#
.SYNOPSIS

Returns a custom PS Object listing the PowerShell and ARS versions

.DESCRIPTION
Returns a custom PS Object listing the PowerShell and ARS versions

.PARAMETER Credential

  Optional credentials that can be used to connect to the remote computer
.PARAMETER ComputerName

A array of computers to connect to and collect the version information

.INPUTS

Array of computer Names

[System.Management.Automation.PSCredential]

.OUTPUTS

Custom PS Object

ComputerName

PSBuildVersion

PSVersion

OSName

PowerGUIVersion

QuestCmdLetVersion
ARSVersion
.EXAMPLE

“localhost” | Get-PSVersion

Using the pipeline to pass the list of computer names to the function

.EXAMPLE

Get-PSVersion “localhost”

.EXAMPLE

Get-PSVersion
Calling the function with no parameters connects to the local host.
.NOTES

Version : 1.0 DATE HERE Author : Lee Andrews

.LINK
https://clan8blog.wordpress.com/

#>

[CmdletBinding()]
param
(
[parameter(Position=0,ValueFromPipeline=$true)][ValidateNotNullOrEmpty()]
[String[]]$ComputerName = @(‘.’),
[Parameter()][ValidateNotNull()] [System.Management.Automation.Credential()]  
$Credential
= [System.Management.Automation.PSCredential]::Empty
)

process {
if
(Test-Connection -ComputerName $computername -Count 1 -ErrorAction SilentlyContinue) {
$WMIcommonParameters
= @{
Namespace=“root\CIMV2”
ComputerName=$computername
Credential=$credential
ErrorAction=“SilentlyContinue”   }
# get the ARS version details

  try { $products = Get-WmiObject @WMIcommonParameters -class Win32_Product }
  catch { }
if
( $products ) {
$ADCmdLetVersion
= “Not Found”
$PowerGUIVersion
= “Not Found”
$ARSVersion
= “Not Found”
ForEach ( $product in $products ) {
switch
-wildcard ( $product.name ) {
“Quest ActiveRoles Management Shell for Active Directory*” {
$ADCmdLetVersion
= $product.version
}
“Quest PowerGUI*” {
$PowerGUIVersion = $product.version
}
“Quest ActiveRoles Server*” {
$ARSVersion
= $product.version
}
} # switch -wildcard ( $product.name )
} # ForEach ( $product in $products )
} # if ($product)
else
{
$ADCmdLetVersion = “Unable connect to WMI”
$PowerGUIVersion = “Unable connect to WMI”
$ARSVersion = “Unable connect to WMI”
} # else if ($product)
try
{ $OS = Get-WmiObject @WMIcommonParameters -Class Win32_OperatingSystem }
catch { }
if
($OS) {
$path
= “$($OS.SystemDirectory -replace ‘\\’,’\\’)\\WindowsPowerShell\\v1.0\\powershell.exe”
$OSName = $OS.Name.Split(‘|’)[0]
} # if ($OS) {
else
{ $OSName = “Unable to connect to WMI” }
# now get the PS Version

$query = “SELECT Version FROM CIM_DataFile WHERE Name = ‘$path’
try { $PSEXE = Get-WmiObject -Query $query -ComputerName $computername -Credential $credential }
catch
{ }
if
( ( $PSEXE ) -and ( $PSEXE.Version ) ) {
$buildversion
= $PSEXE.Version.Split()[0]
$versionPresent = [version]$buildversion
$versionRequired
= [version]’6.0.6002.18111′
if ($versionPresent -ge $versionRequired) { $psversion = “V2 RTM”}
elseif ($versionPresent.Major -ge 6) {$psversion = “V2 CTP Prerelease – Update to V2 RTM!”}
else {$psversion = “V1”}
}
else
{
$psversion
= “Unable to Detect or Not Installed”
$BuildVersion
=[version]$null $Version=‘n/a’ $Description=‘Unable to connect to computer – ping fail to connect to WMI’
} # else if ($PSEXE.Version) {
} # if (Test-Connection
else {
$ADCmdLetVersion
= “Unable to connect to computer – ping fail”
$PowerGUIVersion
= “Unable to connect to computer – ping fail”
$ARSVersion = “Unable to connect to computer – ping fail”
$OSName
=”Unable to connect to computer – ping fail”
$psversion
= “Unable to connect to computer – ping fail”
$BuildVersion
=[version]$null
$Version=’Unable to connect to computer – ping fail’ }
$Properties
= @{
ComputerName=$computername[0]
PSBuildVersion=$buildversion
PSVersion=$psversion
OSName=$OSName
PowerGUIVersion = $PowerGUIVersion
QuestCmdLetVersion = $ADCmdLetVersion
ARSVersion = $ARSVersion
}
New-Object
PSObject -Property $($Properties | Sort-Object)
} # end process
} # end function

Get-PSVersion “localhost” | Get-PSVersion
Get-PSVersion “localhost”
Get-PSVersion
“serverName2”, “ServerName1” | Get-PSVersion

Advertisements

Calling an External Script from an Active Roles Server Scheduled Task

Some tasks on an ARS server can be batched up which suits a scheduled task nicely, especially if the task takes a fair while to run.  My little trick when doing this is to use a single scheduled task script and just change the parameters to call different external scripts.  I also figured out how to pass completion messages between the scheduled task and the external script.

In my environment I also like to be able to quickly see who was the last person to run the task, if it was run manually as some of my scripts are.  I do this by reading the event logs and finding the last scheduled run.   This may not be 100% reliable so should not be used for audit purposes as it’s possible some other action happens in-between the script run and the event log being read.

I also came up with a way of providing a one shot capability, i.e. I might want the scheduled task to do a report on a single user when run manually but the daily schedule would report on all users.   Rather than edit the code I add an additional parameter which the called script will clear when the external script is run.  This prevents someone forgetting to clear the attribute so that the scheduled run will report on all users.

I prefix any parameters that are to be oneshot with “a_”.  This makes sure the parameters all show at the top of the scheduled task parameter list making them easier to find.

How the script works is it calls a PowerShell job that will run in the background on the ARS server.

There are only there mandatory parameters.  The PowerShell script name, the PowerShell script path and the PowerShell job name – used to managed the PowerShell jobs.

The scheduled task can have many other parameters but these are not passed as arguments.  The external script will be passed the DN of the task and it will directly read the additional parameters.

Once the external script is called the script will execute a loop that checks if the PowerShell job has completed and uses the return codes to update the scheduled task last run message by throwing an error.

#===========================================================================
# run-ExternalScript
$scriptversion = "2.1"
#===========================================================================
#
# This ActiveRoles Server Scheduled Task Script runs a Powershell Job
# when it is invoked by the ARS Scheduled Task that specifies it.
#
# MANDATORY PARAMETERS:
#
#  nameForPowershellJobs - used to clean up running tasks
#
#  powershellScriptName - the name of the external PowerShell script to run
#                         must include the extention .ps1
#
#  powershellScriptPath - the path to the external script
#
# OPTIONAL PARAMETERS:
#
#  TimeOutMinutes - used to wait for the scrip to finish and return
#                   an exit code
#
#===========================================================================
#
# Version 1.0 : calls an external PosH script with all paramters set in pairs
#               adds the TaskDN and instigator so the called script
#               can use these if required.
#
# Version 2.0 : only sends the TaskDN and instigator parameters the rest are
#               processed in the external script using the TaskDN and
#               get-qadobject
#
#===========================================================================
#
#Region Helper Functions
function Clean-Memory {
 if ($Global:startupvariables) {
	 # if the startup variable exists we assume the call is to clean up after a script run
	 Get-Variable |
	 Where-Object { $Global:startupVariables -notcontains $_.Name } |
		ForEach-Object {
		 try { Remove-Variable -Name "$($_.Name)" -Force -Scope "global" -ErrorAction SilentlyContinue -WarningAction SilentlyContinue}
			catch { }
		}
		# now clean the startupVariables
	 try {Remove-Variable -Name startupvariables  -Scope "Global" -Force -ErrorAction SilentlyContinue }
		catch { }
		# just in case this is an inital run after the script had failed in the last run lets set up the variable again
		New-Variable -name startupVariables -Force -Scope "Global" -value ( Get-Variable | ForEach-Object { $_.Name } )
	}
	else {
	 # Store all the start up variables so you can clean up when the script finishes.
  New-Variable -name startupVariables -Force -Scope "Global" -value ( Get-Variable | ForEach-Object { $_.Name } )
	}
}
Clean-Memory # reset any variables
# prevents command line polution from previous runs
# especially when working in an ISE

function Send-AlertEmail {
<#
.SYNOPSIS
   Sends an alert email when a script failure occurs.
.DESCRIPTION
   Sends an alert email when a script failure occurs
   Uses Splatting to simplyfy the parameter list.
.PARAMETER to
  The email address (array of email addresses) to send the alert to
.PARAMETER from
  Sets the Email From used to replying to the Alert
.PARAMETER BodyText
  The bodytext of the alert email
.PARAMETER subject
  The subject line of the alert email
 .PARAMETER exitMessage
  This is used to display a message on the lastrun message on an ARS task
 .EXAMPLE
     Send-AlertEmail @mailParameters -smtpServer "smtp.mydomain.com"
    .INPUTS
    .OUTPUTS
    .NOTES
        NAME     : Send-AlertEmail
        VERSION  : Version 1.0
        AUTHOR   : Lee Andrews
        CREATED  : 13th November 2012
        LASTEDIT : 13th November 2012 - Version 1.0
    .LINK
        https://clan8blog.wordpress.com

#>
#
[CmdletBinding(SupportsShouldProcess=$False,ConfirmImpact="None")]
param(
[Parameter(HelpMessage="An Array of email addresses to send the alert to",Position=0)]
[string[]]$to="SupportEmail@clan8.com" ,
[Parameter(HelpMessage="The replyto email address",Position=1)]
[string]$from="DoNotReply@clan8.com" ,
[Parameter(HelpMessage="The email body text",Position=2)]
[string]$BodyText="Error in script run" ,
[Parameter(HelpMessage="The email subject text",Position=3)]
[string]$subject="Error in script Run" ,
[Parameter(HelpMessage="Defaults to smtp.clan8.com",Position=4)]
[string]$smtpServer="smtp.clan8.com",
[Parameter(HelpMessage="Text to be displayed when the script exits"Position=5)]
[string]$exitMessage="Error in Script Run"
)
 if ( $debug -eq $true ) {
  $To = "lee.andrews@MyDomain.com"
 }
 $mailAlertParameterDefaults = @{
  "To"         = $To
  "From"       = $from
  "SmtpServer" = $smtpServer
  "Body"       = $BodyText
  "Subject"    = $subject
 }
 Send-MailMessage @mailAlertParameterDefaults
 Clean-Memory
 throw $exitMessage
}
function Clear-Jobs {
 if ( $jobList ) { Remove-Variable -Name jobList -ErrorAction SilentlyContinue }
 if ( $psJob ) { Remove-Variable -Name psJob -ErrorAction SilentlyContinue }
 # get the list of running PosH jobs.
 $jobList = Get-Job -name $nameForPowershellJobs -ErrorAction SilentlyContinue
 # clean up the jobs
 if ( ($jobList) -and ($($jobList | Measure-Object).count -gt 0  ) ) {
  foreach ( $psJob in $jobList ) {
   if ( $psJob.state -eq 'blocked' ) { Stop-Job -Id $psJob.id }
   if (
    ( $psJob.state -eq 'Stopped' ) -or
    ( $psJob.State -eq 'Completed' ) -or
    ( $psJob.State -eq 'Failed' )
   ) {
    try { Remove-Job -Id $psJob.Id } catch {}
   }
  }
 }
 # release the variables from memory
 if ( $jobList ) { Remove-Variable -Name jobList -ErrorAction SilentlyContinue }
 if ( $psJob )   { Remove-Variable -Name psJob   -ErrorAction SilentlyContinue }
}
#endregion
# get the details of the person calling the scheduled task - this assumes that
# no other transaction took place after this one, which is reasonable but
# not perfect so treat this as info only not as an audit trail
$instigator = $(Get-EventLog -logname "EDM Server"  -Newest 1 -InstanceID 2692).username
$instigator = $instigator.Substring($instigator.IndexOf("\")+1)
$DN = $null
#region Set Debug variables
#===========================================================================
# uncomment the path below when running a debug
#$debug = $true
#$DebugPreference = "Continue"
#$taskDN = "CN=Run-ExternanlScript,CN=Service Desk Tasks,CN=Clan8,CN=Scheduled Tasks,CN=Server Configuration,CN=Configuration"
#===========================================================================
#endregion
# Retrieve Scheduled Task Parameters
try {
 $Task.DirObj.GetInfo()
 $Task.DirObj.GetInfoEx(@("edsaParameters"),0)
 $parameters = $Task.DirObj.Get("edsaParameters")
 $DN = $Task.DirObj.DN
 $DebugPreference = "SilentlyContinue"
}
catch { }
if ( $DN -eq $null ) {
 $Task = Get-QADObject -Identity $TaskDN  -Proxy -IncludedProperties edsaParameters
 if ( $Task -ne $null ) {
  $parameters = $Task.edsaParameters
  $DN = $TaskDN
 }
 else { throw "Unable to get TASK DN script cannot continue" }
}
# now get the mandatory parameters
$nameForPowershellJobs = $null
$powershellScriptName = $null
$powershellScriptPath = $null
$timeOutMinutes = $null

foreach ( $parameter in $parameters ) {
 switch ( $($parameter.Substring( 1,$parameter.IndexOf(">")-1)) ) {
  "nameForPowershellJobs" {
   $nameForPowershellJobs = $($parameter.Substring( $parameter.IndexOf(">")+1,$parameter.IndexOf("</")-($parameter.IndexOf(">")+1)))
   break
  }
  "powershellScriptName" {
   $powershellScriptName  = $($parameter.Substring( $parameter.IndexOf(">")+1,$parameter.IndexOf("</")-($parameter.IndexOf(">")+1)))
   break
  }
  "powershellScriptPath" {
   $powershellScriptPath  = $($parameter.Substring( $parameter.IndexOf(">")+1,$parameter.IndexOf("</")-($parameter.IndexOf(">")+1)))
   break
  }
  "TimeOutMinutes" {
   $timeOutMinutes  = $($parameter.Substring( $parameter.IndexOf(">")+1,$parameter.IndexOf("</")-($parameter.IndexOf(">")+1)))
   break
  }
 }
}
# now check we got all 3 parameters
if (
    ( $nameForPowershellJobs -eq $null) -or
    ( $powershellScriptName -eq $null ) -or
    ( $powershellScriptPath -eq $null ) -or
    ( $nameForPowershellJobs -eq "" )   -or
    ( $powershellScriptName -eq "" )    -or
    ( $powershellScriptPath -eq "" )
   ) {
 throw "Missing one or more of the mandatory parameters, nameForPowershellJobs, powershellScriptName or powershellScriptPath"
}
# set a timer to periodiclaly check if the external script has finished
if ( ( $timeOutMinutes -eq $null  ) -or ( $timeOutMinutes -eq "" ) ) {
 $timeOutMinutes = 10 # default is 10 minutes
}
# check that the PS path does not include the PS script name
if ( $powershellScriptPath.EndsWith(".ps1" ) )  { $powershellScriptPath = $powershellScriptPath.Substring(0,$powershellScriptPath.LastIndexOf("\")) }
# check if the path ends with a backslash if not add it
if ( ! ( $powershellScriptPath.EndsWith("\") 	) ) { $powershellScriptPath = "$powershellScriptPath\" }
$powershellScript =  "$powershellScriptPath$powershellScriptName"
# call the powershell job
# first do some cleanup - get the list of running jobs
Clear-Jobs
# build the powershell command parameters
$argumentlist = ""
$argumentlist += """Instigator"",""$instigator"",""TaskDN"",""$DN"""
if ( $debug -eq $true ) {
 $DebugLogParameters = @{
  "filePath"      = "c:\temp\RunExternalTask.txt"
  "InputObject"   = "Start-Job -filepath $powershellScript  -name $nameForPowershellJobs -Argumentlist $argumentlist"
 }
 Out-File @DebugLogParameters
}
$powershellScript = """$powershellScript"""
$command = '$job=' + "Start-Job -filepath $powershellScript  -name $nameForPowershellJobs -Argumentlist $argumentlist"
Invoke-Expression $command
$iteration = 0
$Interations = ($timeOutMinutes * 60)/60
$waitinterval = 60 # seconds
while ( ($job.State.ToString() -eq "Running") -and ( $iteration -le $Interations ) ) {
 if ( $debug -eq $true ) {
  $DebugLogParameters = @{
   "filePath"      = "c:\temp\RunExternalTask.txt"
   "InputObject"   = "Job Status: $($job.State.ToString()) - iteration $iteration"
  }
  Out-File @DebugLogParameters
 }
 $iteration++
	Write-Debug "Job State = $($job.State.ToString())"
 Start-Sleep -Seconds $waitinterval
}
$exitMessage = $($job.ChildJobs[0].JobStateInfo.Reason.Message)
$jobStatus = $job.State.ToString()
Clear-jobs
# output the completion message by Throwing an error so it shows on the last run message status
switch ( $jobStatus ) {
 "Failed" {
  throw $exitMessage
 }
 "Running" {
  $BodyText = "ERROR Script running for more than 1 hour: $instigator called $powershellScriptName"
  $subject  = "ERROR Script running for more than 1 hour: $instigator called $powershellScriptName"
  Send-AlertEmail -Body $BodyText -Subject $subject
  throw "ERROR Script running for more than 1 hour: $instigator called $powershellScriptName"
 }
 "Blocked" {
  # end the job - this means something in the script is awaiting user input and therefore the script will never complete
  $BodyText = "ERROR Script BLOCKED: $instigator called $powershellScriptName"
  $subject  = "ERROR Script BLOCKED: $instigator called $powershellScriptName"
  Send-AlertEmail -Body $BodyText -Subject $subject
  throw "ERROR Script BLOCKED: $instigator called $powershellScriptName"
  break
 }
 "Stopped" {
  $BodyText = "ERROR Script STOPPED: $instigator called $powershellScriptName"
  $subject  = "ERROR Script STOPPED: $instigator called $powershellScriptName"
  Send-AlertEmail -Body $BodyText -Subject $subject
  throw "ERROR Script STOPPED: $instigator called $powershellScriptName"
  break
 }
 "Stopping" {
  $BodyText = "ERROR Script STOPPING: $instigator called $powershellScriptName"
  $subject  = "ERROR Script STOPPING: $instigator called $powershellScriptName"
  Send-AlertEmail -Body $BodyText -Subject $subject
  throw "ERROR Script STOPPING: $instigator called $powershellScriptName"
  break
 }
 "Suspended" {
  $BodyText = "ERROR Script SUSPENDED: $instigator called $powershellScriptName"
  $subject  = "ERROR Script SUSPENDED: $instigator called $powershellScriptName"
  Send-AlertEmail -Body $BodyText -Subject $subject
  throw "ERROR Script SUSPENDED: $instigator called $powershellScriptName"
  break
 }
 "Suspending" {
  $BodyText = "ERROR Script SUSPENDING: $instigator called $powershellScriptName"
  $subject  = "ERROR Script SUSPENDING: $instigator called $powershellScriptName"
  Send-AlertEmail -Body $BodyText -Subject $subject
  throw "ERROR Script SUSPENDED: $instigator called $powershellScriptName"
  break
 }
}
throw "NO RETURN CODE : $instigator called $powershellScriptName"

Active Roles Scheduled Task Parameters

This is an example  script, which could be turned into a function, that I use to get the task parameters from the ARS scheduled task that calls the script.  When the script is called directly from the scheduled task I can just grab the parameters but when debugging I set the TaskDN to the DN of the scheduled task.

#Region  Retrieve Scheduled Task Parameters
try {
 $Task.DirObj.GetInfo()
$Task.DirObj.GetInfoEx(@("edsaParameters"),0)
$parameters = $Task.DirObj.Get("edsaParameters")
$DN = $Task.DirObj.DN
$DebugPreference = "SilentlyContinue"
}
catch { }
if ( $DN -eq $null ) {
 $Task = Get-QADObject -Identity $TaskDN  -Proxy -IncludedProperties edsaParameters
 if ( $Task -ne $null ) {
  $parameters = $Task.edsaParameters
  $DN = $TaskDN
} # if ( $Task -ne $null ) {
 else { throw "Unable to get TASK DN script cannot continue" }
} # if ( $DN -eq $null ) {
# now get the mandatory parameters
if ( $parameters -ne $null ) {
 foreach ( $parameter in $parameters ) {
  switch ( $($parameter.Substring( 1,$parameter.IndexOf(">")-1)) ) {
   "GroupToManage" {
    $groupToManage = $($parameter.Substring( $parameter.IndexOf(">")+1,$parameter.IndexOf("</")-($parameter.IndexOf(">")+1)))
    break
   } # "GroupToManage" {
   "UserOU" {
    $targetOUs  = $($parameter.Substring( $parameter.IndexOf(">")+1,$parameter.IndexOf("</")-($parameter.IndexOf(">")+1)))
    if ( $targetOUs -ne "" ) {
     $targetOUs = $targetOUs.split(";")
    }
    break
   }
   "emailTo" {
    $emailTo  = $($parameter.Substring( $parameter.IndexOf(">")+1,$parameter.IndexOf("</")-($parameter.IndexOf(">")+1)))
    if ( $emailTo -ne "" ) {
     $emailTo = $emailTo.split(";")
    }
    break
   } #  "emailTo" {
  }  # switch ( $($parameter.Substring( 1,$parameter.IndexOf(">")-1)) ) {
} # foreach ( $parameter in $parameters ) {
}
# now check we got all 3 mandatory parameters
if ( ( $groupToManage -eq "") -or ( $targetOUs.length -eq 0 ) -or ( $emailTo.length -eq 0 ) ) {
 $alertParameters =@{
  "BodyText"    = "Missing one or more of the mandatory parameters, GroupToManage, emailTo or UserOU"
  "subject"     = "Missing one or more of the mandatory parameters, GroupToManage, emailTo or UserOU"
  "exitMessage" = "Missing one or more of the mandatory parameters, GroupToManage, emailTo or UserOU"
}
Send-AlertEmail @alertParameters
} # if ( ( $groupToManage -eq $null) -or ( $targetOUs -eq $null ) ) {

Dynamically manage group memberships

Today I  was asked to create an email enabled group whos’ membership would always be up to date.   I’m lucky enough to be using Active Roles Server so my initial thought was to use that rather than a query based distribution list.  Active Roles has a built in capability to dynamically manage a groups’ membership and it’s pretty flexible and really easy to set up.  Best of all it’s completely transparent to AD that the group is anything other than a normal group.

All of the users that needed to be in the group, I was told, were in the same OU so that made it even easier.    I quickly setup the dynamic group so that all the users in the OU were added to the group.  Job done!  Obviously not else I wouldn’t be writing this 🙂

It turns out this OU contains the admin accounts for a group of system administrators.  Adding these users to the group is not going to be a lot of use because none of them have email accounts configured and even if they did, it’s likely these users will not be monitoring the mail boxes associated with their personal functional accounts.  What we really want is to add the personal accounts of these admin users to the group.  Fairly easy to do manually but remember this group needs to be dynamic.  When a new admin account is added to the OU I need to add the personal account to the group and I want to avoid the SD forgetting to do this.

My solution to this was to use a scheduled task script, actually an ARS scheduled task script to do this.  The benefit of the latter is that the task parameters are visible in the ARS MMC and can be modified without having to modify the script.  I have a separate post on how to get the task parameters and use them in a script.

Here are a few tips for writing scripts that I use:

Tip one: I always add a script version variable at the top of my script listing.  If I’m following my own best practice and creating a log file I can use this in my log file header.  $scriptversion = “1.0”

Tip two: put any debug variables at the top of the script so you can see them easily – in this instance it’s in a separate section a little further down because I call the Clean-Memory function and this wiped the debug variables.

Tip Three: before you write the script write the script file header and document the script logic in outline – this will help  you actually write the code and gather the requirements too.

Tip four: if it’s going to be a big script then using #region <section details> #endregion will allow you to quickly hide section of code when you are writing / debugging – you can even use the notes you just wrote in Tip Three

Tip five: Clean Up variables as a matter of course especially when working in an ISE.  I’ve been caught out many times by not doing this.  Between runs variables stay in memory which can mean your code works fine as you are debugging it but fails in production.

Tip six: When making bulk updates – as well as a log file always create a rollback file something that can be used to quickly reverse the changes made by your script. The log file is to assist you in debugging when things go wrong and also so you can check that the script is running properly and doing what it’s supposed to be doing.

Tip six might literally save your job one day so don’t forget it!

Tip Seven: when working at the command prompt, using the shorter command alias’ is great as it saves typing but in a production script don’t do it!  Use the full command line verbs – it will make the script easier to read and follow later on.  Who wants to remember that %  is the same as ForEach-Object.

it seems that Don Jones a Windows PowerShell MVP agrees with me

In classes, I tell my students to type whatever they want when they’re using the shell. After all, saving yourself typing saves you time. That’s what the shell is all about. However, I also tell them they should copy and paste a command into a script. It makes sense to use the full cmdlet name and the full parameter names. Doing so makes the eventual script easier to read

http://technet.microsoft.com/en-us/magazine/gg675931.aspx

In that same blog Don also recommends the use of splatting to make your code more readable

So the next time you’re placing a command in a script, consider splatting. If nothing else, it sounds really cool

Nip over to Dons’ blog linked above for more details on splatting but you will see I’ve made use of it in my script to dynamically update the group membership.

To keep the blog short I’ve split out the explanations of the function into other blogs and you can jump between them using the links below:

function Send-AlertEmail – sends an email alert to the script support team so they are aware that the script is not functioning as expected – uses splatting

function Clean-Memory – a simple one liner (split over a few lines for readability reasons) that I use to remove all the variables I set up in a script – this helps prevent debug errors as it makes sure you instantiate all the variables your script needs rather than using one that you previously instantiated in a previous run.  I find it best to make sure I’ve not got any lingering variables when I run my scripts so I purposely kill them off at the beginning of the script.  I modified this function recently so it can be called at the start of the script to store all of the existing variable and then again at the end to release them all.  As part of this I made use of the global variable scope so that the variable would persist during the script run.

Getting Variables from the ARS scheduled task.

In my environment we use the employeeID and employeeNumber attributes to link personal employee accounts to personal functional accounts (the admin accounts).  This allows me to search AD for a matching personal employee account with the same employeeID as the value in the admin accounts employeeNumber attribute.

As some of the admin accounts might have emails associated I could have just put them in the group but the design was to put the personal employee accounts.  I also allowed for the OU to contain other distribution lists.

I’ve added a download link for the code as well as shown it here because annoyingly when I post the code it adds a line number and when you cut an paste it into any editor you will spend ages deleting the numbers 😦

DynamicGroups.ps1

Since I did my initial post I revisited this script and decided to allow it even more flexibility and to add similar capabilities that the ARS Dynamic Group configuration allows.  I still need to add in the exclude options and I might even allow for multiple LDAP queries but for now one is enough don’t you think?

#===========================================================================
# Group-UpdateGroupsMembersBasedOnOU
$scriptversion = "2.0"
#===========================================================================
#
# This ActiveRoles Server Scheduled Task Script manages a groups 'shadow' membership
# i.e. it locates target users and if they are personal functional accounts
# determined by the employeeNumber attribute holding the employeeID of the
# personal employee account it adds the users personal employee account to the
# target group.
#
# The script will also add any personal functional accounts it finds as well as
# an optional list of explicit users.
#
# The script can also target only personal functional accounts if the
# -FunctionalAccountsOnly parameter is set to YES
#
# Version 3 will look at having exclude queries as with the ARS dynamic group functionality
#
# For each user object found in a specified OU(s) the script checks:
# if the user has an employeeID the script adds it to the group if it's not already a member
# if the user does not have an employeeID then the script checks:
# if the user has an employeeNumber set and then searches for a matching user based on the employeeNumber
# if it finds a match it adds the user to the group if it's not already a member
# if both searches above fail it checks if the object has an associated email address
# if it has it adds the object to the group if it's not already a member
#
# Version 1.0 : Initial Script Version
#
# Version 2.0 adds in LDAP query and group members
#
########################################################################################################
#
#region Helper Functions
function Clean-Memory {
 if ($Global:startupvariables) {
  # if the startup variable exists we assume the call is to clean up after a script run
  Get-Variable |
   Where-Object { $Global:startupVariables -notcontains $_.Name } |
   ForEach-Object {
    try { Remove-Variable -Name "$($_.Name)" -Force -Scope "global" -ErrorAction SilentlyContinue -WarningAction SilentlyContinue}
    catch { }
   }
  # now clean the startupVariables
  try {Remove-Variable -Name startupvariables  -Scope "Global" -Force -ErrorAction SilentlyContinue }
  catch { }
  # just in case this is an initial run after the script had failed in the last run lets set up the variable again
  New-Variable -name startupVariables -Force -Scope "Global" -value ( Get-Variable | ForEach-Object { $_.Name } )
 }
 else {
  # Store all the start up variables so you can clean up when the script finishes.
  New-Variable -name startupVariables -Force -Scope "Global" -value ( Get-Variable | ForEach-Object { $_.Name } )
 }
}
Clean-Memory # reset any variables - prevents command line polution from previous runs especially when working in an ISE
function Send-AlertEmail {
 param ( $BodyText,$subject,$exitMessage )
 # set alert defaults if script fails completely
 if ( ( $emailTo -eq "" ) -or ($emailTo -eq $null) ) {
  $emailTo = "ITSupport@DOMAIN-NAME.com"
 }
 if ( $debug -eq $true ) {
  $emailTo = "lee.andrews@MyDomain.com"
 }
 $mailAlertParameterDefaults = @{
  "To"         = $emailTo;
  "From"       = "ARS-Group-UpdateGroupsMembersBasedOnOU@clan8.com";
  "SmtpServer" = "smtp.clan8.com"
  "Body"       = $BodyText
  "Subject"    = $subject
 }
 Send-MailMessage @mailAlertParameterDefaults
 Clean-Memory
 throw $exitMessage
}
#endregion
#region initialise script variables
#region debug variables
# uncomment the required debug variables below when running a debug
$debug = $true
#$DebugPreference = "Continue"
$taskDN = "CN=Update Mail Distribution Group,CN=Scheduled Tasks,CN=Server Configuration,CN=Configuration"
#endregion
$DN = $null
$GroupToManage = $null
$targetOUs = $null
$emailTo = $null
$includeQuery = $null
$searchRoot = $null
$includeGroups = $null
$explicitMembers = $null
$OUObjects = @()
$usersToAdd = ""
$usersToRemove = ""
$personalEmployeeAccounts = @()
$UserOU = $null
$logFolder = "C:\Scripts\Identity Management\Logs\DynamicGroups"
$removedUsers = @()
$addedUsers = @()
$OperationReason = $null
$LogFileRetentionDays = 30
#endregion
#Region  Retrieve Scheduled Task Parameters
try {
 $Task.DirObj.GetInfo()
 $Task.DirObj.GetInfoEx(@("edsaParameters"),0)
 $parameters = $Task.DirObj.Get("edsaParameters")
 $DN = $Task.DirObj.DN
 $DebugPreference = "SilentlyContinue"
}
catch { }
if ( $DN -eq $null ) {
 $Task = Get-QADObject -Identity $TaskDN  -Proxy -IncludedProperties edsaParameters
 if ( $Task -ne $null ) {
  $parameters = $Task.edsaParameters
  $DN = $TaskDN
 } # if ( $Task -ne $null ) {
 else { throw "Unable to get TASK DN script cannot continue" }
} # if ( $DN -eq $null ) {
# now get the mandatory parameters
if ( $parameters -ne $null ) {
 foreach ( $parameter in $parameters ) {
  switch ( $($parameter.Substring( 1,$parameter.IndexOf(">")-1)) ) {
   "GroupToManage" {
    $groupToManage = $($parameter.Substring( $parameter.IndexOf(">")+1,$parameter.IndexOf("")+1)))
    if ( $groupToManage.Length -ge 1 ) {
     # we only support DOMAIN-NAME so check if the group is prefixed with DOMAIN-NAME
     # first check if there is a backslash in the group name
     if ( $groupToManage.indexOf("\") -gt 0 ) {
      # now check if it's DOMAIN-NAME if not bail out!
      if ( $groupToManage.indexOf("DOMAIN-NAME\",[System.StringComparison]::OrdinalIgnoreCase) -ne 0 ) {
       $groupToManage = $null
      }
     }
     else {
      # prefix the group name with DOMAIN-NAME if it was missing
      $groupToManage = "DOMAIN-NAME\$groupToManage"
     }
    }
    break
   } # "GroupToManage" {
   "UserOU" {
    $targetOUs  = $($parameter.Substring( $parameter.IndexOf(">")+1,$parameter.IndexOf("")+1)))
    if ( $targetOUs -ne "" ) {
     $targetOUs = $targetOUs.split(";")
    }
    break
   }
   "emailTo" {
    $emailTo  = $($parameter.Substring( $parameter.IndexOf(">")+1,$parameter.IndexOf("")+1)))
    if ( $emailTo -ne "" ) {
     $emailTo = $emailTo.split(";")
    }
    break
   } #  "emailTo" {
   "IncludeQuery" {
    $includeQuery  = $($parameter.Substring( $parameter.IndexOf(">")+1,$parameter.IndexOf("")+1)))
    break
   } #  "IncludeQuery" {
   "QueryScope" {
    $searchRoot  = $($parameter.Substring( $parameter.IndexOf(">")+1,$parameter.IndexOf("")+1)))
    break
   } #  "QueryScope" {
   "includeGroups" {
    $includeGroups  = $($parameter.Substring( $parameter.IndexOf(">")+1,$parameter.IndexOf("")+1)))
    if ( $includeGroups -ne "" ) {
     $includeGroups = $includeGroups.split(";")
    }
    break
   } #  "includeGroups" {
   "explicitMembers" {
    $explicitMembers  = $($parameter.Substring( $parameter.IndexOf(">")+1,$parameter.IndexOf("")+1)))
    if ( $explicitMembers -ne "" ) {
     $explicitMembers = $explicitMembers.split(";")
    }
    break
   } #  "includeGroups" {
   "OperationReason" {
    $OperationReason  = $($parameter.Substring( $parameter.IndexOf(">")+1,$parameter.IndexOf("")+1)))
    break
   } #  "OperationReason" {
   "LogFileRetentionDays" {
    $LogFileRetentionDays  = $($parameter.Substring( $parameter.IndexOf(">")+1,$parameter.IndexOf("")+1)))
    if ( $LogFileRetentionDays -eq "" ) { $LogFileRetentionDays = 30 }
    break
   }
  }  # switch ( $($parameter.Substring( 1,$parameter.IndexOf(">")-1)) ) {
 } # foreach ( $parameter in $parameters ) {
}
# now check we got the minimum No. of parameters
if (
    ( $groupToManage.length -ge 1     ) -and
    ( $emailTo.length -ge 1           ) -and
    ( !
     (
      ( $targetOUs.length -ge 1       ) -or
      ( $includeQuery.length -ge 1    ) -or
      ( $includeGroups.length -ge 1   ) -or
      ( $explicitMembers.length -ge 1 )
     )
    )
   )
{
 $alertParameters =@{
  "BodyText"    = "Missing one or more of the mandatory parameters, GroupToManage, emailTo or UserOU - NOTE the script only supports groups in the DOMAIN-NAME Domain"
  "subject"     = "Missing one or more of the mandatory parameters, GroupToManage, emailTo or UserOU"
  "exitMessage" = "Missing one or more of the mandatory parameters, GroupToManage, emailTo or UserOU"
 }
 Send-AlertEmail @alertParameters
} # if ( ( $groupToManage -eq $null) -or ( $targetOUs -eq $null ) ) {
if ( $OperationReason.length -lt 1 ) { $OperationReason = "$DN" }
#endregion
#Region get users to process
If ( $targetOUs.Length -ge 1 ) {
 foreach ( $OU in $targetOUs ) {
  if ( ($OU.Length -gt 3 ) -and ( $OU.substring(0,3) -eq "OU=" ) ) {
   $foundObjects = Get-QADObject -SearchRoot $OU -Proxy -IncludedProperties employeeNumber,employeeID,ProxyAddresses |
    where { ( ($_.gettype().name -eq "ArsUserObject") -or ($_.gettype().name -eq "ArsGroupObject")  ) }
   if ( $foundObjects -ne $null ) {  $OUObjects += $foundObjects }
  }
 } # foreach ( $OU in $targetOUs ) {
} # If ( $targetOUs.Length -ge 1 ) {
if ( $includeQuery.length -ge 1 ) {
 if ( $searchRoot.length -ge 1 ) {
  $foundObjects = Get-QADObject -SearchRoot $searchRoot -LdapFilter $includeQuery -Proxy -IncludedProperties employeeNumber,employeeID,ProxyAddresses
 }
 else {
  $searchRoot = "DC=AD,DC=COM"
  $foundObjects = Get-QADObject -SearchRoot $searchRoot -LdapFilter $includeQuery -Proxy -IncludedProperties employeeNumber,employeeID,ProxyAddresses
 }
 if ( $foundObjects -ne $null ) {  $OUObjects += $foundObjects }
}
if ( $includeGroups.Length -ge 1 ) {
 ForEach ( $group in $includeGroups ) {
  $foundObjects = Get-QADGroupMember -Identity "DOMAIN-NAME\$group" -Proxy -Type user -IncludedProperties employeeNumber,employeeID,ProxyAddresses
  if ( $foundObjects -ne $null ) {  $OUObjects += $foundObjects }
 }
}
#endregion
#Region get the groups current members
$currentGroupMembers = Get-QADGroupMember -Identity $groupToManage -Proxy | select -ExpandProperty NTAccountName
foreach ( $object in $OUObjects ) {
 # get the objects in the Target OUs
 $personalEmployeeAccount = $null
 # check if the
 # ( object is not mail enabled  AND employeeID is not present and it's a user object )
 # OR
 # ( the object has email but no employeeID but does have an employeeNumber )
 if (
     (
      ( ! ( $object.ProxyAddresses ) ) -and
      ( $object.employeeID -eq $null ) -and
      ( $object.gettype().name -eq "ArsUserObject" )
     ) -or
     (
      ( $object.ProxyAddresses ) -and
      ( $object.employeeID -eq $null ) -and
      ( $object.employeenumber -ne $null )
     )
    )
 {
  # see if the account has an employeeNumber otherwise there is nothing we can do
  if ( $object.employeeNumber -eq $null ) { continue }
  $personalEmployeeAccount = $(Get-QADUser -SearchAttributes @{employeeID=$object.employeeNumber} -SearchRoot "OU=User Accounts,DC=ad,DC=com" -Proxy).NTAccountName
 } # check if it a Personal Employee or Personal Functional account
 else {
  # if the object has a mail address then add it to the group
  if ( $object.ProxyAddresses ) {
   $personalEmployeeAccount = $object.NTAccountName
 }
}
if ( $personalEmployeeAccount -ne $null ) {
 if ( $personalEmployeeAccounts -contains $personalEmployeeAccount ) { continue } # make sure we do not add the same user twice!
  $personalEmployeeAccounts += $personalEmployeeAccount
  if ( $currentGroupMembers -notcontains $personalEmployeeAccount ) {
   $addedUsers += $personalEmployeeAccount
   if ( $usersToAdd -eq "" ) { $usersToAdd += """$personalEmployeeAccount""" }
   else { $usersToAdd += ",""$personalEmployeeAccount""" }
  } # if ( $currentGroupMembers -notcontains $personalEmployeeAccount ) {
 } # if ( $personalEmployeeAccount -ne $null ) {
} # Work out which users, if any, to add to the group
foreach ( $currentGroupMember in $currentGroupMembers ) {
 if ( $personalEmployeeAccounts -notcontains $currentGroupMember ) {
  $removedUsers += $currentGroupMember
  if ( $usersToRemove -eq "" ) { $usersToRemove += """$currentGroupMember""" }
  else { $usersToRemove += ",""$currentGroupMember""" }
 } # if ( $personalEmployeeAccounts -notcontains $currentGroupMember ) {
} # Work out which users, if any, to remove from the group
#endregion
#Region Optionally create a rollback log
if ( ( $usersToRemove.length -gt 0 ) -or ( $usersToAdd.length -gt 0 ) ) {
 $dateFormat = "yyyy-MM-dd-HH-mm"
 $unique = $(Get-Date -Format $dateFormat)
 $rollBackLogfile = "$logFolder\$($GroupToManage.Substring(0,$GroupToManage.indexOf("\")))-$($GroupToManage.Substring(15))-"+$unique+".csv"
 if ( ! ( Test-Path $logFolder ) ) {
  try { New-Item -Path $logFolder }
  catch {  }
 }
 if ( Test-Path $logFolder ) {
  Out-File -Encoding "UTF8" -FilePath $rollBackLogfile -InputObject "Domain,SamAccountName"
  ForEach ( $member in $currentGroupMembers ) {
   Out-File -Encoding "UTF8" -FilePath $rollBackLogfile -InputObject "$($member.subString(0,$member.indexOf("\"))),$($member.subString($member.indexOf("\")+1))" -Append
  }
 }
 else {
  $alertParameters =@{
   "BodyText"    = "Unable to create a rollback file script halted"
   "subject"     = "Unable to create a rollback file script halted"
   "exitMessage" = "Unable to create a rollback file script halted"
  }
  Send-AlertEmail @alertParameters
 }
}
#endregion
#region Add users to the group
$Error.Clear()
if ( $usersToAdd.length -gt 0 ) {
 $usersToAdd = "Set-QADGroup -Identity ""$groupToManage"" -Member @{append=@($usersToAdd)}"+' -Control @{OperationReason="' + $operationReason + '"}' + " -proxy"
 Invoke-Expression $usersToAdd
 if ( $Error.Count -gt 0 ) {
  $alertParameters =@{
   "BodyText"    = "Unable to add new members script halted"
   "subject"     = "Unable to add new members"
   "exitMessage" = "Unable to add new members"
  }
  Send-AlertEmail @alertParameters
 }
 $addedLogfile = "$logFolder\$($GroupToManage.Substring(0,$GroupToManage.indexOf("\")))-$($GroupToManage.Substring(15))-"+$unique+"-ADDED.csv"
 Out-File -Encoding "UTF8" -FilePath $addedLogfile -InputObject "Domain,SamAccountName"
 if ( Test-Path ( $addedLogfile ) ) {
  ForEach ( $member in $addedUsers ) {
   Out-File -Encoding "UTF8" -FilePath $addedLogfile -InputObject "$($member.subString(0,$member.indexOf("\"))),$($member.subString($member.indexOf("\")+1))" -Append
  }
 }
 else {
  $alertParameters =@{
   "BodyText"    = "Unable to create a Added file script halted"
   "subject"     = "Unable to create a Added file script halted"
   "exitMessage" = "Unable to create a Added file script halted"
  }
  Send-AlertEmail @alertParameters
 }
}
#endregion
#region Remove Users from the group
if ( $usersToRemove.length -gt 0 ) {
 $usersToRemove = "Set-QADGroup -Identity ""$groupToManage"" -Member @{delete=@($usersToRemove)}"+' -Control @{OperationReason="' + $operationReason + '"}' + " -proxy"
 Invoke-Expression $usersToRemove
 if ( $Error.Count -gt 0 ) {
  $alertParameters =@{
   "BodyText"    = "Unable to remove members script halted"
   "subject"     = "Unable to remove members"
   "exitMessage" = "Unable to remove members"
  }
  Send-AlertEmail @alertParameters
 }
 $removedLogfile = "$logFolder\$($GroupToManage.Substring(0,$GroupToManage.indexOf("\")))-$($GroupToManage.Substring(15))-"+$unique+"-REMOVED.csv"
 Out-File -Encoding "UTF8" -FilePath $removedLogfile -InputObject "Domain,SamAccountName"
 if ( Test-Path ( $removedLogfile ) ) {
  ForEach ( $member in $removedUsers ) {
   Out-File -Encoding "UTF8" -FilePath $removedLogfile -InputObject "$($member.subString(0,$member.indexOf("\"))),$($member.subString($member.indexOf("\")+1))" -Append
  }
 }
 else {
  $alertParameters =@{
   "BodyText"    = "Unable to create a Removed file script halted"
   "subject"     = "Unable to create a Removed file script halted"
   "exitMessage" = "Unable to create a Removed file script halted"
  }
  Send-AlertEmail @alertParameters
 }
}
#endregion
#region Clean Up
# clean up old log files...
$Now = Get-Date
$LastWrite = $Now.AddDays(-$LogFileRetentionDays)
$Files = Get-ChildItem $logFolder -include *.* -recurse | Where {$_.LastWriteTime -le "$LastWrite"}
if ($i) { if ($i -is [IDisposable]) {try {$p.Dispose() | Out-Null } catch { }}; Remove-Variable -Name i -ErrorAction SilentlyContinue | Out-Null }
$i = 0
if ($Files.count -gt 0) {
 foreach ($File in $Files) {
  $i++
  Remove-Item $File | Out-Null
 }
}
Clean-Memory
#endregion
# end script

Hello Good Evening and Welcome…..The Answer is 42!

I’ve been putting off writing a blog for too long and today it stops.  It seems like everything with me is work in progress.  I write a reusable powershell function and every time I reuse it  in another script I revisit it and enhance it.   Sometimes I know there are things I want to do with the function but as with any business you have to consider the ROI and who will see the brilliant job you made of it or the pigs ear you made of it for that matter.

Am I an expert in PowerShell, Active Roles Server or Active Directory? When I’m asked that I always remember that someone once told me that an expert is for ex, as in has been and spurt, as in drip under pressure.  I did a quick search to see if this was a common saying and came across a blog where it’s claimed it comes from the Latin “ex” meaning “a has-been”, and “spurt” meaning “a drip under pressure”.  I doubt that but you never know.  I usually avoid the question with a smile or move the conversation to a more interesting topic.  I don’t consider myself an expert because I learn something new every day doesn’t an expert know everything?  That’s both the best and worst thing about this job.  It’s  always changing and you are always playing catch up.  An expert?  I can google as good as anyone I guess.  Then I can put 2 and 2 together and make it into any number I like 🙂

So why PowerShell or PosH as some people like to call it.  the name Posh probably originates from when Mrs. Hyacinth Bucket (http://en.wikipedia.org/wiki/Hyacinth_Bucket)  tried to write a script.  I used to be an avid VBScript user but since I started my new job and was asked to automate everything (still working on automating the tea making) and discovered that Active Roles used PowerShell and I saw it as an opportunity to lean something new and I have never looked back.  It’s a fantastic scripting language and just a few lines of code will make you look like an expert in other peoples eyes.

If you are thinking of taking it up  then get your a*** to Mars … er I mean you should go download the powergui editor from here  http://www.powergui.org/

If you are also running Active Roles make sure you get the correct version of the cmdlets.  If you load the latest cmdlets I’m sure you will be bristling with pride with all the clever stuff you can do with them….. but you won’t be communicating with your ARS service because it will ignore you completely.

Date Posted Name Version File Type Size
May 16 2011 ActiveRoles Management Shell for Active Directory 32-bitAD Management Shell 1.5.1 is compatible with only ARS 6.7.0 1.5.1 20.08 MB
Oct 29 2012 Quest One ActiveRoles Management Shell for Active Directory 32-bit – ZipAD Management Shell 1.6.0 is compatible with only ARS 6.8.0 1.6.0 21.95 MB
May 16 2011 ActiveRoles Management Shell for Active Directory 64-bitAD Management Shell 1.5.1 is compatible with only ARS 6.7.0 1.5.1 32.73 MB
Oct 29 2012 Quest One ActiveRoles Management Shell for Active Directory 64-bit – ZipAD Management Shell 1.6.0 is compatible with only ARS 6.8.0 1.6.0 36.79 MB

Once you have all this downloaded and installed pop back and I should have started posting some of my ARS functions and things I have learnt over the last 3 years.  I’m due to upgrade my version of ARS as it’s a little old, like me.  I’m running version 6.5 still.  What I’m really saying here is that the stuff I post here may not work with your version so make sure you test in a safe environment – you do have a test environment don’t you?     Also I figured out a few tricks to do stuff that may now be easier to do in the later versions of ARS – you are welcome to tell me if it has as it may save me some time regression testing all my scripts which is my usual argument for not upgrading along with if it ain’t broke don’t fix it!

P.S  another gotcha is you cannot talk to the ARS service unless you have the ARS MMC installed.  This info may save you hours of head scratching like me.

One last thing before I sign off.  If you can’t wait for my scripts you could do worse than follow this blogger.  We both post on the quest forums and she has a lot of useful information in her blog, which can be found here:

http://insideactiveroles.com/