Saturday, 28 March 2015

Detecting file change management in PowerShell

For some time Windows has supported in the Win32 API various events that are triggered when files or directories in a filesystem are changed and this appears to be supported in .NET as well. This then leads us to a natural question of whether we can support that in PowerShell. Due to PowerShell being essentially a scripting extension to .NET, we indeed can use these capabilities in our scripts.

After looking on the internet it appears from several sources mentioned below I can have a script that looks something like this:
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = $searchPath
$watcher.IncludeSubdirectories = $true
$watcher.EnableRaisingEvents = $true


Starting off here the FileSystemWatcher object is instanced for this script and parameters have been passed to define the path to be searched and whether to include subdirectories. The last line tells the FileSystemWatcher to raise an event when a change is detected. 


$changed = Register-ObjectEvent $watcher "Changed" -Action {
   write-host "Changed: $($eventArgs.FullPath)"
}
$created = Register-ObjectEvent $watcher "Created" -Action {
   write-host "Created: $($eventArgs.FullPath)"
}
$deleted = Register-ObjectEvent $watcher "Deleted" -Action {
   write-host "Deleted: $($eventArgs.FullPath)"
}
$renamed = Register-ObjectEvent $watcher "Renamed" -Action {
   write-host "Renamed: $($eventArgs.FullPath)"
}
This section registers the event handlers for different types of events that are raised. Once this piece of code has executed then these actions will be fired each time the FileSystemWatcher detects a notifiable activity.

Unregister-Event $changed.Id
Unregister-Event $created.Id
Unregister-Event $deleted.Id
Unregister-Event $renamed.Id
Finishing off here with unregistering the event handlers and there should also be a call to dispose of the $watcher object or at least disable it when no longer needed. Closing the PowerShell session will have the same effect.
The source for this script came from here: http://dereknewton.com/2011/05/monitoring-file-system-changes-with-powershell/

There is a comment to that article mentioning the Change event can be fired multiple times for the same file depending on how the application doing the change works with the file. To limit the firing of multiple events some other kind of code may be needed in an event handler to determine whether the file is currently opened or closed by the application which is writing to it. I still have to do some testing with the setup I am looking at to determine how these events would be fired and which events to use. An option is to have the creating script rename the file once it has finished with it and the rename event will then be fired.

Well, testing it works more or less as expected. One major issue to be aware of is to do with errors; the code inside the braces for the Action won’t be evaluated by the Powershell ISE for syntax errors and if a mistake occurs in execution the usual messages on the Powershell console window will not be displayed. So we really only have the option of live testing. The situation I am working with uses a script that causes an email to be sent to a group whenever the action files, so I will be watching for emails coming in for sure.
 

Using PowerShell to manage Google Apps

With the limitations of Google’s console to manage Google Apps for system admins, scripting and command line tools have naturally become a topic for the system admin community. Previous posts here have discussed the options for use of various tools and so far I have used both Dito GAM and PowerShell with extensions as a Google Apps management tool. Dito GAM is a command line tool specifically and is well suited for batch operations managing many aspects of Google Apps. It is well thought out, completely free and proved quite invaluable for managing our first Google Apps deployment in a New Zealand school.

However it does not specifically integrate to PowerShell in the way that a PowerShell extension would be capable of achieving as it is a separate executable application and so I have more recently looked at the gShell extension module to accomplish scripted tasks within PowerShell in a different school. Having mentioned various PowerShell extensions that were available in an earlier post, gShell has been the one I have focused on to date and it has a supportive user community and open development model which has made it easy to implement to date.

I now turn specifically to some of the tasks I have achieved in Google Apps experimentally to manage specifically the synchronisation of user accounts between Active Directory and Google Apps so far. The scripts examined in this post are:
  • Provision-Students: Provision accounts in Google Apps from Active Directory
  • Classify-Students and Reclassify-Students: Organise student accounts in Google Apps to match the class structure of the school
  • Deprovision-Students: Move accounts of non-current students to a different suborganisation in Google Apps.
Provision-Students
Provision-Users is a simple script that generates accounts in Google Apps based on the list of accounts it can find by searching a specific tree in Active Directory.
import-module ActiveDirectory
import-module gShell

#Retrieve all students from Active Directory
$ADStudents = Get-ADUser -Filter * -SearchBase "ou=Z,ou=HCS Students,ou=HCS,DC=hcs,DC=local" -SearchScope Subtree -Properties EmailAddress
foreach ($S in $ADStudents)
{
    # Check account is enabled
    if ($S.Enabled -eq $false)
    {
        continue
    }

    $Email = $S.EmailAddress  
    if ($Email -eq $null)
    {
        Write-Host ($S.SamAccountName + " does not have an email address set. Skipping.")
        continue
    }
In the first section of the script we are searching AD using a subtree filter and it has used one specific OU (named for for students whose surnames start with Z) but it can use a higher level OU to search multiple lower levels. We have to specify in the query that the EmailAddress is being returned from the query as it isn’t one of the default user object properties that are returned from this cmdlet. After checking the account is enabled and has an email address set we can fall through to the next section of the script, otherwise that user account is skipped.
   #Look up in Google Apps
    Write-Host ("Looking up Google Apps for " + $Email + "...") -NoNewline
    $User = $null
    try
    {
        $User = Get-GAUser -UserName $Email -ErrorAction Stop
    }
    catch
    {
    }
    if ($User -eq $null)
    {
        #Add if not there
        New-GAUser -UserName $Email -GivenName $S.GivenName -FamilyName $S.Surname -PasswordLength 8 -IncludeInDirectory $true `
        -OrgUnitPath "/Students From AD" -ChangePasswordAtNextLogin $false
        Write-Host ("Added " + $Email + " to Google Apps")
    }
    else
    {
        Write-Host ""
    }
}
The rest of the script is concerned with finding an account in Google Apps and creating a new account if the existing one isn’t found. Get-GAUser will throw an exception if the account isn’t found so I have to catch this with a try-catch block although I could also have used a Trap block. I haven’t tried to determine which exception is being thrown and occasionally there will be an instance of an exception even though the account does exist indicating some other type of exception other than “not found”. This results in an error in the next section in these few cases. The code around this block assumes the $User instance will be null if Get-GAUser doesn’t find the account. The last section simply calls New-GAUser to set up the new account. We didn’t specify a password and have let Google create a random one. Later we will provision passwords into Active Directory and let Google Apps Password Sync automatically provision them into Google Apps, so this isn’t an issue.

Deprovision-Students
A simple script that moves students accounts to a different sub organisation in Google Apps if they are not found or disabled in AD.
import-module ActiveDirectory
import-module gShell

$PC = New-GAUserPropertyCollection
#Retrieve all students from Google Apps
$GAStudents = Get-GAUser -All

foreach ($G in $GAStudents)
{
    # Skip if they are not in the Students organisation
    if ($G.OrgUnitPath -notlike "/Students From AD*")
    {
        continue
    }

    $Email = $G.PrimaryEmail
    # Look them up in AD
    $ADStudents = Get-ADUser -Filter {EmailAddress -eq $Email} -SearchBase "ou=HCS Students,ou=HCS,dc=hcs,dc=local" -SearchScope Subtree
    if ($ADStudents -eq $null)
    {
        Set-GAUser -Username $Email -PropertyCollection $PC -OrgUnitPath "/Students Not In AD"
        Write-Host ($Email + " is not in AD")
    }
    else
    { # If found but disabled then move them as well
        foreach ($S in $ADStudents)
        {
            if ($S.Enabled -eq $False)
            {
                Set-GAUser -Username $Email -PropertyCollection $PC -OrgUnitPath "/Students Not In AD"
                Write-Host ($Email + " is not in AD")
            }
        }
    }
}
We are using Set-GAUser to move the account in Google Apps. Due to a bug in this cmdlet we have to create a redundant GAUserPropertyCollection object and use that in our calls. It is important if you use the extra properties in your Google Apps accounts to make sure that calls to Set-GAUser do not erase these properties by passing an empty instance (as in this case). I don’t know if this is a risk or not because I haven’t checked in the documentation to see what happens in such a case.

Classify-Students / Reclassify-Students
These two scripts are both concerned with reorganising students between different sub organisations in Google and are essentially identical apart from one line which I will mention below.
import-module ActiveDirectory
import-module gShell

$PC = New-GAUserPropertyCollection
#Retrieve all students from Google Apps
$GAStudents = Get-GAUser -All

foreach ($G in $GAStudents)
{
    # Skip if they are not in the Students organisation
    if ($G.OrgUnitPath -ne "/Students From AD")
    {
        continue
    }


    $Email = $G.PrimaryEmail
    # Look them up in AD
    $ADStudents = Get-ADUser -Filter {EmailAddress -eq $Email} -SearchBase "ou=HCS Students,ou=HCS,dc=hcs,dc=local" -SearchScope Subtree -Properties Division
    if ($ADStudents -ne $null)
    {
        foreach ($S in $ADStudents)
        {
            $OrgPath = "/Students From AD/" + $S.Division
            Set-GAUser -Username $Email -PropertyCollection $PC -OrgUnitPath $OrgPath
            Write-Host ($Email + " moved to " + $OrgPath)
        }
    }
}
These scripts only differ in the Google Apps OrgUnitPath which in the Classify script doesn’t have the wildclass * on the end of it and in the case of Reclassify, does. The former script is for first time classification of new accounts and the second for reclassifying existing accounts but you could just use Reclassify for both functions, however if you have a lot of accounts it needlessly wastes time calling gShell to essentially do nothing to the account since it doesn’t need moving. (Note however my comment about GA-UserPropertyCollection objects from the previous scripts)

We use AD calls here to retrieve the classification property (in this case the class that the student is enrolled in) and move the account in Google Apps to a sub organisation named after that class.