Follow-up on Scripting Games 2013 Event 6


As you can see, MVP Richard Siddaway voted so far on more than 1000 script, WOW!!!!!
He left the following comment on my Event 6 entry:

Don’t get me wrong. I really appreciate the feedback. The Scripting Games are a unique occasion to get a review from your peers and most valuable persons, who are all part of the PowerShell community. Richard Siddaway is one of the judges in the Scripting Games 2013 that you can follow on twitter:

Source: http://powershell.org/wp/2013/04/06/2013-scripting-games-judges and http://powershell.org/wp/2013/04/06/2013-scripting-games-mighty-panel-of-celebrity-judges

Let’s get back to the comment of Richard Siddaway and here’s what I’d like to say:

@RichardSiddaway,

Hi, Thanks for the feedback. Please allow me to comment on event 6, answer your questions and explain the choices I made:

Well, the instructions don’t say what hypervisor we should use. I choose Hyper-V because I know it, because it’s installed by a single cmdlet. I could have used VMware but I don’t know it. I could have also used KVM but there isn’t any PowerShell module for it…To continue on the Hypervisor, the instructions don’t say what version, if it’s clustered, if it’s domain joined, if there are other virtual machines running,…

The instructions don’t say if the DHCP is running Windows, if it’s running Windows 2012 that has the built-in DHCP module, if it’s joined to the domain…

Yes, you’re right it would have been easier to use the wildcard for the trusted host. However it doesn’t sound as a good practice for me. I also wanted to have reusable code for handling trusted host additions and removals.

Yes, I’m sure that I can rename and join and reboot the computer the way I did. I started with Add-computer but had some weird issues. So, I switched over the above WMI approach. I’ve tested and it worked better than Add-computer. Add-computer has limited Fjoin flags whereas WMI hasn’t. In my testing, WMI appeared to be more reliable. I’ve a blog post about this https://p0w3rsh3ll.wordpress.com/2013/06/04/2013-scripting-games-event-6/

Event 6 was the hardest event of the Scripting Games. Currently, only Bartek Bielawski shared how he’d have done this event: http://becomelotr.wordpress.com/2013/06/05/event-6-my-way (I’m glad I’m not the only one who noticed that you can get IP and MAC addresses directly from Hyper-V running on Windows 2012 πŸ˜€ )

Nobody asked or commented on it yet, but there are actually 2 reasons why I collected IP Addresses from Hyper-V in my entry:
First, I installed a few VMs, deleted them, installed a new serie,… but DHCP leases of dead VMs were still “active”. (My bad, yeah, I know, I’m not a DHCP guy πŸ˜› )
The second reason was that I wanted to test the remoting with the built-in Test-WSMan cmdlet before trying to use Invoke-Command on the target VMs and join them to the domain.
So, to speed-up WSMan/PSremoting tests, I ended filtering the DHCP data with the active IPs I got from Hyper-V.

2013 Scripting Games Event 6

Instructions for the Event 6, entitled “The Core Configurator”:

Reading the instructions made me realize that I need a test lab.

  • Building the test lab in 60 minutes
  • Here’s how I quickly built a lab from scratch with a PC running a 4-cores processor and 16GB memory.
    To achieve this so fast, I already had an ISO of windows Server 2012 and just downloaded the VM build kit that Thomas Lee realeased on his blog. He wrote the following articles about Building a Hyper-V Test Lab on Windows 8:

    Basically, here are the steps that I followed to get that test lab environment:

    • Boot the ISO image extracted on a bootable USB stick and install a Windows 2012 server Standard edition with a GUI. (I already had it)
    • Install Hyper-V
    • Install-WindowsFeature -Name Hyper-V -Restart -IncludeManagementTools -WhatIf
    • Grab the kit and extract it
    • iwr 'http://www.reskit.net/powershell/vmbuild.zip' -OutFile E:\vmbuild.zip
    • Setup an Internal VM switch
    • New-VMSwitch -Name Internal -SwitchType Internal -WhatIf
    • Change the code execution policy as I’ll run scripts from the VM build kit
    • Set-ExecutionPolicy RemoteSigned
    • Edit unattend.xml and Create-ReferenceVHDX.ps1 from the extracted VM build kit, specify, the Windows Server 2012 ISO file location and change the password
    • Run Create-ReferenceVHDX.ps1

    • (Yes, it took 7 minutes to build the parent partition for virtual machines).

    • Edit Create-VM.ps1

    • Run Create-VM.ps1 to create the non domain joined VM named DC1
    • Edit Configure-DC1-1.ps1 to change passwords and run it
    • I have had an error saying that
      [DC1] Connecting to remote server DC1 failed with the following error message : The WinRM client cannot process the request. If the authentication scheme is different from Kerberos, or if the client computer is not joined to a domain, then HTTPS transport must be used or the destination machine must be added to the TrustedHosts configuration setting. Use winrm.cmd to configure TrustedHosts. Note that computers in the TrustedHosts list might not be authenticated. You can get more information about that by running the following command: winrm help config. For more information, see the about_Remote_Troubleshooting Help topic.
      I fixed it quickly by typing this and finally re-run Configure-DC1-1.ps1 without any issue

      winrm --% set winrm/config/client @{TrustedHosts="DC1"}

      My Last step consisted in setting up a static IP Address on the Internal Hyper-V VM switch:

      # Set a static IP address on Hyper-V switch            
      Get-NetAdapter |             
       ? InterfaceDescription -match "Hyper-V Virtual Ethernet Adapter" |            
       New-NetIPAddress -IPAddress 10.0.0.1 -PrefixLength 24            
      Get-NetAdapter |             
       ? InterfaceDescription -match "Hyper-V Virtual Ethernet Adapter" |            
       Set-DnsClientServerAddress -ServerAddresses 10.0.0.10            
      

    Again, thanks to the awsome work of Thomas Lee and Niklas Goude, I have a virtual machine named DC1, built from a parent reference disk (refWS2012.vhdx), running Windows Server 2012 Standard Core Edition and that it’s the Primary Domain Controller of the reskit.org Active Directory forest and domain. I didn’t need to install a DHCP server as Configure-DC1-1.ps1 already installed and configured it for the 10.0.0.0 scope 😎 Cool, isn’t it?

    Now to get in the conditions specified in the instructions and get some random workgroup based virtual machines, I duplicated and edited the following files from the VM build kit: unattend.xml and create-VM.ps1. Inside the xml file, I removed the XML section where VM get a static IP Address:

    and just left this:

    In my Create-VMEVT6.ps1, I commented lines 69 to 80 where a static IP Address is written in the XML unattend answer file.
    I also defined a new unattend XML template:

    $UnaEVT6 = 'E:\vmbuild\UnAttendEVT6.xml'

    And I finally added the following 4 lines.

    At this point, I have a test lab running with 3 non domain joined virtual machines.
    To make sure the DHCP was running correctly and that the changes I made are effective, I typed at command prompt on DC1:

    Get-DHCPServerv4Scope -ScopeId 10.0.0.0 | Get-DHCPServerv4Lease
  • Writing the tool to solve Event 6
  • I tried to import the DHCP cmdlets from DC1 on the Hyper-V server where the tool should run. I used the following code successfully at command prompt:

    # Import the DHCP cmdlets from the VM DC1            
    try {            
        $session = New-PSSession -ComputerName $DomainController -Authentication Kerberos -Credential $DomainCredential -ErrorAction Stop            
        Import-PSSession -Module DHCPServer -Prefix PSDC -Session  $session -ErrorAction Stop | Out-Null            
    } catch {            
        Write-Warning "Failed to open a PS remote session on domain controller and import DHCP cmdlets"            
        break            
    }

    But for some reasons, it failed when being run inside the script with the following error:
    WARNING: Failed to open PS Remote Session on domain controller DC1 and import DHCP cmdlets because Attribute cannot be
    added because it would cause the variable MacAddress with value to become invalid.


    So, I found a workaround for this as I hadn’t enough time to figure out why and how to fix the issue.

    Again, as the Hyper-V is not joined to a domain, I needed to add the IP address of virtual machines to the trusted hosts list of the Hyper-V WinRM client.

    As the instructions states that:

    You can safely assume that SERVER1, SERVER2, etc. do not already exist in the domain.

    I first went with the following code:

    # Join it to the domain            
    Invoke-Command -ComputerName $_.IPAddress -Credential $credVM -ScriptBlock {            
        $HT = @{            
            NewName = $Using:NewName ;            
            Credential = $Using:DomainCredential ;            
            DomainName = $Using:DomainName ;            
            Force   = $true ;            
            Restart = $true ;            
        }            
        try {            
            Add-Computer @HT -ErrorAction Stop            
            Write-Verbose "Computer $Using:NewName successfully joined the domain $Using:DomainName" -Verbose            
        } catch {            
            Write-Warning "Failed to join the domain because $($_.Exception.Message)"            
        }            
    } # end of scriptblock            
    



    I encountered the following errors with the above code: The account already exists and The directory service is busy.
    The first one is expected as the Add-Computer cmdlet only handles a limited subset of Fjoin options flags. The JoinWithNewName flag is automatically added to the Add-computer cmdlet when you use it with the -NewName parameter. However, this is not enough to overwrite an existing account in Active Directory with the same name.
    For the second error, I don’t have any idea why it fails. I hadn’t enough time to figure out why and how to fix it.

    As the instructions also state that:

    You can assume none of the server names exist on the network (if they do, it’s not your fault if something breaks).
    […]
    It’s fine if you do this for one computer at a time, but when your script finishes running all of the computers must be properly provisioned.

    I changed my mind about the Add-Computer and switched over a slower but more reliable solution based on WMI. The WMI JoinDomainOrWorkgroup method of the Win32_ComputerSystem class allows to specify all the Fjoin options flags listed on this MSDN page: http://msdn.microsoft.com/en-us/library/aa370433%28VS.85%29.aspx

    The last thing I added to my script is a validation script about the prefix name of the computers. SamAccountName are limited to 15 characters and a single Hyper-V server running on Windows Server 2012 can host a maximum of 1024 active virtual machines. I decided to validate the length and check for invalid characters like this:

    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [ValidateScript({
        if (($_ -match '[\"/\\\[\]:;\|,\+\*\?\<\>]') -or ($_.Length -gt 11)) {
            $ErrorMsg = "Invalid name: either length greater than 11 characters or invalid character(s) being used"
            throw (New-Object System.ArgumentException -ArgumentList $ErrorMsg)
        }
        $true
    })]
    [string]$ComputerNamePrefix='SERVER'
    

    This also throws a nicely formatted error:

    Last thing, I wanted to allow MAC Addresses in the following 3 formats: either colon or dash separated or with no separator at all.
    So I used the following regular expression to validate these 3 formats.

    [Parameter(Mandatory,ValueFromPipeline)]            
    [ValidatePattern("^([0-9A-F]{2}[:\-]?){5}([0-9A-F]{2})$")]            
    [string[]]$MacAddress

    And I’ve added the following in the PROCESS bloc because MAC addresses collected from the DHCP server have no separator:

    Process {            
        # Process each MAC Address            
        $MacAddress | ForEach-Object {            
            Switch -Regex ($_) {            
                "-" {            
                    $MacAddressWL += $_ -replace "-","" ;            
                    break ;            
                }            
                ":" {            
                    $MacAddressWL += $_ -replace ":","" ;            
                    break ;            
                }            
                default {            
                    $MacAddressWL += $_            
                }            
            }            
        }            
    }
  • The entry I submitted for this event: http://scriptinggames.org/entrylist_.php?entryid=1137
  • #Requires -Version 3
    #Requires -Modules Hyper-V
    
    Function Add-VMToDomain {
    <#
    .SYNOPSIS
        Adds virtual machines to the domain based on dynamic IP Addresses from the DHCP server.
    
    .DESCRIPTION
        Adds virtual machines to the domain based on dynamic IP Addresses from the DHCP server.
            
    .PARAMETER MacAddress
        Array of MAC Addresses of target virtual machines. 
        Can be either comma or dash separated. No separator is also allowed
    
    .PARAMETER DomainName
        The netbios name of the target active directory domain to join
    
    .PARAMETER DomainFQDN
        The fully qualified domain name to join
    
    .PARAMETER DHCPServer
        The netbios name of the DHCP server.
    
    .PARAMETER DomainAdminAccountName
        The name of a privileged account able to join multiple computers to the domain
    
    .PARAMETER DomainAdminPassword
        String that represents the password of the domain account used to join computers to the domain.
    
    .PARAMETER LocalAdminAccountName
        The name of a local account on the virtual machines, member of the local administrators group.
    
    .PARAMETER LocalAdminPassword
        String that represents the password of the local account used to connect to virtual machines.
    
    .PARAMETER DHCPv4Scope
        Name of the DHCP scope of IPV4 addresses 
    
    .PARAMETER ComputerNamePrefix
        Prefix to rename computers before they join the domain. 
    
    .EXAMPLE
        Add-VMToDomain -MacAddress "00155DCE0001","00:15:5D:CE:00:02","00-15-5D-CE-00-03"
    
    .EXAMPLE
        Get-Content .\MAC.txt | Add-VMToDomain -Verbose -DHCPServer DC1
    
    .NOTES
        Virtual machines being provisionned are running Windows 2012 Core edition and have PSremoting enabled (by default).
        The DHCP server is running on a Windows 2012 Server.
        Prefix for naming computers is limited to 11 characters because Hyper-V 2012 can host a maximum of 1024 active VMs.
        The Hyper-V server doesn't belong to an Active Directory domain, that's why the use of WSMan trusted hosts is required.
        Using WMI method instead of Add-Computer to be able to specify the NETSETUP_JOIN_WITH_NEW_NAME (0x400) join option flag.
    
    .LINK
        http://msdn.microsoft.com/en-us/library/aa370433%28VS.85%29.aspx
        http://msdn.microsoft.com/en-us/library/aa392154%28VS.85%29.aspx
    
    .OUTPUTS
        None
    
    .INPUTS
        [string[]]
    
    #>
    [CmdletBinding()]
    Param(
    [Parameter(Mandatory,ValueFromPipeline)]
    [ValidatePattern("^([0-9A-F]{2}[:\-]?){5}([0-9A-F]{2})$")]
    [string[]]$MacAddress,
    
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string]$DomainName='RESKIT',
    
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string]$DomainFQDN ='Reskit.org',
    
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string]$DHCPServer='DHCP1',
    
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string]$DomainAdminAccountName="administrator",
    
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string]$DomainAdminPassword='P@ssw0rd',
    
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string]$LocalAdminAccountName='administrator',
    
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string]$LocalAdminPassword='Pa$$w0rd',
    
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string]$DHCPv4Scope =  "10.0.0.0",
    
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [ValidateScript({
        if (($_ -match '[\"/\\\[\]:;\|,\+\*\?\<\>]') -or ($_.Length -gt 11)) {
            $ErrorMsg = "Invalid name: either length greater than 11 characters or invalid character(s) being used"
            throw (New-Object System.ArgumentException -ArgumentList $ErrorMsg)
        }
        $true
    })]
    [string]$ComputerNamePrefix='SERVER'
    )
    Begin {
    
        # Save previous trusted hosts
        $PrevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
    
        # Add the DHCP of the VM to the trustedhosts
        if ($PrevTrustedHosts) {
            $TrustedHosts =  $PrevTrustedHosts,$DHCPServer -join ","
        } else {
            $TrustedHosts = $DHCPServer
        }
        Set-Item WSMan:\localhost\Client\TrustedHosts –Value $TrustedHosts -Force
    
        $DomPwd = ConvertTo-SecureString -String  $DomainAdminPassword -AsPlainText -Force
        $DomainCredential = New-Object -Typename PSCredential -Argumentlist "$DomainName\$DomainAdminAccountName",$DomPwd
    
        $DHCPLeaseAr = @(Invoke-Command -ComputerName $DHCPServer -Credential $DomainCredential -ScriptBlock {
            try {
                Import-Module -Name DHCPServer -Scope Local -Prefix PSDC -ErrorAction Stop
            } catch {
                Write-Warning "Failed to import DHCP module on server $Using:DHCPServer"
                break
            }
            Get-PSDCDhcpServerv4Lease -ScopeId $Using:DHCPv4Scope
        })
    
        # Restore trusted hosts
        Set-item WSMan:\localhost\Client\TrustedHosts –Value $PrevTrustedHosts -Force
    
        $MacAddressWL = @()
    }
    Process {
        # Process each MAC Address
        $MacAddress | ForEach-Object {
            Switch -Regex ($_) {
                "-" {
                    $MacAddressWL += $_ -replace "-","" ;
                    break ;
                }
                ":" {
                    $MacAddressWL += $_ -replace ":","" ;
                    break ;
                }
                default {
                    $MacAddressWL += $_
                }
            }
        }
    }
    End {
        if ($MacAddressWL) {
            # Hyper-V can give us the full list of running VMs and their IPAdresses filtered with our MAC addresses
            $TargetVMIPfromHV = @()
            $TargetVMIPfromHV += (Get-VM | Where-Object { 
                $_.State -eq 'Running' -and
                $_.NetworkAdapters.MacAddress -in $MacAddressWL
            }).NetworkAdapters.IPAddresses
    
            # Enum the IPAddresses from the DHCP filtered with our MAC addresses
            $TargetVMfromDHCP = $DHCPLeaseAr | Where-Object {
                $_.AddressState  -eq 'Active'
                $_.ClientId -in $MacAddressWL
            } | Select IPAddress,
                @{l='HostName'  ;e={($_.HostName -split '\.')[0]}},
                @{l='MacAddress';e={$_.ClientId -replace '\-',''}}
    
            # Make sure we can access the VM over PSRemoting (WSman)
            $VM = @()
            $TargetVMfromDHCP | Where-Object { $_.IPAddress -in $TargetVMIPfromHV } | 
            ForEach-Object -Process {
                $Target = $_
                try {
                    Test-WSMan -ComputerName $_.IPAddress -ErrorAction Stop | Out-Null
                    Write-Verbose "Adding $($_.IPAddress) to the array of VMs to provision" 
                    $VM += $Target 
                } catch {
                    Write-Warning "IP $($Target.IPAddress) from DHCP scope doesn't seem to be currently in use"
                }
            }
    
            $Count = 1
            $VM | ForEach-Object -Process {
                
                $VMHost = $_
    
                # New name of the VM
                $NewName = "$ComputerNamePrefix$Count"
            
                # Build credentials to access the VM over PSRemoting
                $LocalPwd = ConvertTo-SecureString -String $LocalAdminPassword -AsPlainText -Force
                $CredVM = New-Object  PSCredential -ArgumentList ("$($VMHost.HostName)\$LocalAdminAccountName",$LocalPwd)
    
                # Add the targetIP of the VM to the trustedhost
                if ($PrevTrustedHosts) {
                    $TrustedHosts =  $PrevTrustedHosts,$($VMHost.IPAddress) -join ","
                } else {
                    $TrustedHosts = $($VMHost.IPAddress)
                }
                Set-Item WSMan:\localhost\Client\TrustedHosts –Value $TrustedHosts -Force
    
                # Join it to the domain
                try {
                    Invoke-Command -ComputerName $VMHost.IPAddress -Credential $CredVM -ErrorAction Stop -ScriptBlock {
    
                        # 1. Rename w/o reboot
                        try {
                            Rename-Computer -NewName $Using:NewName -Restart:$false -Force -ErrorAction Stop
                        } catch {
                            Write-Warning "Failed to rename the computer because $($_.Exception.Message)"
                        }
                        # 2. Join with the new name using FJoinOptions: 0x1 + 0x2 + 0x20 + 0x400
                        try {
                            $result = (Get-WMIObject -Class Win32_ComputerSystem).JoinDomainOrWorkGroup(
                                $Using:DomainFQDN,$Using:DomainAdminPassword,
                                "$($Using:DomainName)\$($Using:DomainAdminAccountName)",$null,(1+2+32+1024)
                            )
                            Write-Verbose "Computer $Using:NewName successfully joined the domain $Using:DomainName" -Verbose
                        } catch {
                            Write-Warning "Computer $Using:NewName failed to join the domain because $($_.Exception.Message)"
                        
                        }
                        if ($result.ReturnValue -eq 0 ) {
                            Write-Verbose "Restarting computer $Using:NewName" -Verbose
                            Restart-Computer -Force
                        } else {
                            Write-Warning "Computer $Using:NewName failed to join the domain and WMI return code is: $($result.ReturnValue)"
                        }
                    } # end of scriptblock
                } catch {
                    Write-Warning "Failed to invoke commands on $($VMHost.IPAddress) because $($_.Exception.Message)"
                    continue
                }
                $Count++
    
            } # end of foreach
        }
    
        # Restore trusted hosts
        Set-item WSMan:\localhost\Client\TrustedHosts –Value $PrevTrustedHosts -Force
    }
    } # end of function
    

    2013 Scripting Games Event 5

    Instructions for the Event 5, the Logfile Labyrinth:

    First let’s see how my entry http://scriptinggames.org/entrylist_.php?entryid=1069 performs:

    • Using the 1MB large log files from the zip file in the instructions
    • Using 4GB of files located on my laptop that doesn’t have a SSD disk 😦

    Although it wasn’t a requirement, I chose to write a function that could parse many files including large files of 25MB,125MB,..as quick as possible without having a negative impact on memory.

    There are many ways to read files content and IIS log files. Here’s a non- exhaustive list I found on the web:

    • Using a SQL DB
    • Using the New-Object -ComObject MSUtil.LogQuery from LogParser
    • Using the built-in Select-String cmdlet
    • Using the built-in switch statement that has a FilePath parameter
    • Using the built-in Get-Content cmdlet that reads lines 1 by 1 by default, or using -ReadCount 0 for all lines, or using a specific number of lines (-Readcount 1000 for example)
    • Using the .Net methods of the class [IO.File]
    • Using the .Net methods associated with the [IO.StreamReader] class
    • Using the [regex] object (my favorite)

    All of these methods perform differently, may consume a lot of RAM,…
    To select the method, I’ve tested the following

    Get-ChildItem -Path .\LogFiles\W3SVC1 | % {             
    New-Object -TypeName psobject -Property @{            
        FileName = $_.FullName            
        'FileSize(MB)' = '{0:N2}'-f($_.Length/1MB)            
        GCMethod = (Measure-Command {            
            Get-Content $_.FullName | Out-Null             
        }).ToString()            
        GC1000Method = (Measure-Command {            
            Get-Content $_.FullName -ReadCount 1000 | % { $_ } | Out-Null             
        }).ToString()            
        StreamMethod = (Measure-Command {            
            $reader = new-object System.IO.StreamReader -ArgumentList $_.FullName            
            while ( ($line = $reader.readline()))  {            
            $line            
            }            
        }).ToString()            
    }}

    Here are a few points that you may notice in my entry.

    • The regular expression in the ValidatePattern attribute will allow IPV4 and IPV6 IP addresses. It means that you can type for example:
      127.0.*
      192.168.*
      *1
      ::1
      2001:*
      fe80*

      I didn’t use the following code because it gave from strange results

      try {            
              [IPAddress]::Parse($_) | Out-Null            
          } catch {            
              throw "An invalid IP address was specified."            
          }

      As I use the -LIKE operator to filter the resulting array of unique IP Addresses, I needed to know whether the Pattern parameter of my function was used so that I can make sure it ends with a wildcard.

      if ($PSBoundParameters.ContainsKey('Pattern')) {            
          if (-not($Pattern.EndsWith("*"))) {            
              $Pattern = "$Pattern*"            
          }            
          Write-Verbose "Using pattern $Pattern to filter the output"            
      }
    • I’ve used the following MSDN page to find out the different file names and understand the different format of IIS log files
    • I’ve also extracted the first four lines in a W3C log file to determine in what column the “c-ip” is located. I used the -TotalCount parameter of the built-in Get-Content for this purpose.
    • You may notice that I also used the new -notin operator of PowerShell version 3.0 to avoid creating a huge array in memory when reading big files containing millions of lines.
    • The total length of my progress bar represents the total size of files. The progress is proportional to the file size.

    Here’s my full entry 😎

    #Requires -Version 3            
                
    Function Get-IPFromIISLog {            
                
    [CmdletBinding()]            
    Param(            
    [Parameter()]            
    [ValidatePattern("^(\d|[a-f]|:|\*|\.|\%)*$")]            
    [string]$Pattern="*",            
                
    [Parameter()]            
    [ValidateScript({            
        Test-Path -Path $_ -PathType Container            
    })]            
    [string]$FilePath = ".\"            
                
    )            
    Begin {            
                
        if ($PSBoundParameters.ContainsKey('Pattern')) {            
            if (-not($Pattern.EndsWith("*"))) {            
                $Pattern = "$Pattern*"            
            }            
           Write-Verbose "Using pattern $Pattern to filter the output"            
        }            
                
        Function Get-LineStream {            
        [CmdLetBinding()]            
        Param(            
            [int32]$Index,            
            [string]$Separator,            
            [String]$Path            
        )            
        Begin {            
            $arIP = @()            
        }            
        Process {            
            try {            
                $StreamReader =  New-object System.IO.StreamReader -ArgumentList (Resolve-Path $Path -ErrorAction Stop).Path            
                Write-Verbose "Reading Stream of file $Path"            
                while ( $StreamReader.Peek() -gt -1 )  {            
                    $Line = $StreamReader.ReadLine()            
                    if ($Line.length -eq 0 -or $Line -match "^#") {            
                        continue            
                    }            
                    $result = ($Line -split $Separator)[$Index]            
                    if ($result -notin $arIP) {            
                        $arIP += $result            
                    }            
                }            
                $StreamReader.Close()            
                $arIP            
            } catch {            
                Write-Warning -Message "Failed to read $Path because $($_.Exception.Message)"            
            }            
        }            
        End {}            
        } # end of function            
                
    }            
    Process {            
                    
        try {            
            $allFiles = Get-ChildItem -Path $FilePath -Filter *.LOG -Recurse -ErrorAction Stop            
        } catch {            
            Write-Warning -Message "Failed to enumerate files under $FilePath because $($_.Exception.Message)"            
            break            
        }            
                
        if ($allFiles) {            
            $IPCollected = @()            
            $Count = 1            
            $FileSizeSum = 0            
            $TotalSize = (($allFiles | ForEach-Object { $_.Length }) | Measure-Object -Sum).Sum            
            $allFiles | ForEach-Object {            
                $File = $_            
                $FileSizeSum += $File.length            
                $WPHT = @{            
                    Activity = "Reading file $($File.Name) of size $('{0:N2}'-f ($File.Length/1MB))MB" ;            
                    Status = '{0} over {1}' -f $Count,($allFiles).Count ;            
                    PercentComplete  = ($FileSizeSum/$TotalSize*100) ;            
                }            
                Write-Progress @WPHT            
                $Count++            
                
                # Based on the file name we know the IIS Format            
                Switch -Regex ($File.Name) {            
                    '^u_ex.*\.log'  { $IISLogFormat = 'W3C'    ; break}            
                    '^ex.*\.log'    { $IISLogFormat = 'W3C'    ; break}            
                    '^in.*\.log'    { $IISLogFormat = 'IIS'    ; break}            
                    '^nc.*\.log'    { $IISLogFormat = 'NCSA'   ; break}            
                    default         { $IISLogFormat = 'Custom' ; break}            
                }            
                Switch ($IISLogFormat) {            
                    'W3C' {            
                        Write-Verbose "Reading W3C formatted file $($File.FullName)"            
                        try {            
                            $First4Lines = Get-Content -Path $($File.FullName) -TotalCount 4 -ErrorAction Stop            
                        } catch {            
                            Write-Warning "Failed to read the content of the file $($File.Name) because $($_.Exception.Message)"            
                        }            
                        if ($First4Lines) {            
                            $i = -1            
                            $Index = ($First4Lines[-1] -split "\s" | ForEach-Object {            
                                [PSObject]@{ Index=$i ; FieldName = $_}            
                                $i++            
                            } | Where-Object { $_.FieldName -eq "c-ip"}).Index                                    
                        }            
                        if ($Index) {            
                            [array]$IPCollected += (Get-LineStream -Path $File.FullName -Separator "\s" -Index $Index)            
                        } else {            
                            Write-Warning "Could not find the c-ip field in the W3C log file $($File.FullName)"            
                        }            
                    }            
                    IIS {            
                        Write-Verbose "Reading IIS formatted file $($File.FullName)"            
                        [array]$IPCollected += (Get-LineStream -Path $File.FullName -Index 0 -Separator ",")            
                    }            
                    NCSA {            
                        Write-Verbose "Reading NCSA formatted file $($File.FullName)"            
                        [array]$IPCollected += (Get-LineStream -Path $File.FullName -Index 0 -Separator "\s")            
                    }            
                    default {            
                        Write-Warning "Cannot parse a custom log file $($File.FullName)"            
                    }            
                }            
                $IPCollected = ($IPCollected | Sort -Unique)            
            }            
            Write-Verbose ("A total of {0} unique IP were collected" -f $($IPCollected.Count))            
            $IPCollected | Where-Object { $_ -like $Pattern }            
        } else {            
            Write-Warning "No file with .LOG extension found in this folder and subtree"            
        }            
    }            
    End {}            
    }            
    

    2013 Scripting Games Event 4

    Instructions:

  • Step 1: Scoping the event
  • The requirements don’t tell:

      • what operating system will be used to run the code
      • what version of powershell will be used to execute the code
      • whether the Active Directory module is present and loaded
      • what version of Windows Domain Controllers are running
      • how the Active Directory is configured: replication,…

    I made 3 assumptions for this event:

      • We won’t use PowerShell V1 because Get-Random cmdlet isn’t available. Writing a piece of code to simulate the behavior of Get-Random would have been interesting but was totally out-of-scope for this event. Anyway this doesn’t mean that we cannot come up with clever ways of selecting 20 pseudo randomly users.
      • If the domain admin that will run the code has installed Active Directory modules for Powershell (using RSAT) and still has only Active Directory running on Windows 2003, he has also taken care of installing Active Directory Management Gateway Service. The domain admin will not run the code on a broken plateform.
      • The instructions say “A Domain Admin will always run the command”. It doesn’t tell if he will run the code from a computer that doesn’t belong to Active Directory. I assumed he will select a machine (client or server) that belongs to an Active Directory because that’s just common sense. Checking that the computer is a member of a domain and not a workgroup isn’t a requirement.
  • Step 2 Gathering information on Active Directory properties
  • One of the most difficult tasks in this event is that there are many ways to query Active Directory, some techniques return the raw value and some display a nicely formatted value. They don’t have the same properties name and count returned by default.

    The Get-ADUser cmdlet returns by default 10 properties. Using core cmdlets like Get-Item on a user object while navigating the AD drive will return only 4 properties by default.
    Get-ADUser will display a nicely formatted property PasswordLastSet that is derived from the 64bit integer value named pwdLastSet…

    To make it even more difficult, you should find out how accurrate these properties are, whether they are replicated across all domain controllers, how active directory stores them and how some of these properties are calculated. One of the most common mistake is to use the LastLogon property. This property isn’t replicated across DCs, it’s thus unreliable. The lastlogontimestamp is replicated but it’s not 100% accurate.

    To illustrate that and to get more information about Active Directory, I’ve got some really nice links to share from the following sources:

  • Step 3: Organize scripting tasks
    • Testing whether the Active Directory module is present or not
    • To be absolutely sure that the orginal module from Microsoft is present and being used, I first remove any module named “ActiveDirectory”.

      Remove-Module -Name "ActiveDirectory" -Force -ErrorAction Stop

      Then I try to load the original module from Microsoft. As I don’t know if the $env:PSModulePath has been modified, I try to load the module from its original path. Yes, the Import-Module cmdlet allows filepath as input.

      Get-Item "$env:systemroot\system32\WindowsPowerShell\v1.0\Modules\ActiveDirectory" -EA Stop |            
      Import-Module -ErrorAction Stop            
      
    • Testing whether if the Active Directory drive is mounted or not
    • For fun, I decided to use the core cmdlets (Get-ChildItem, Get-Item,…) to query the AD: drive. I needed to know whether the drive was mounted along with the module import or disabled on purpose using the $Env:ADPS_LoadDefaultDrive variable:

      if (Test-Path Env:ADPS_LoadDefaultDrive) {             
          if ([bool]($Env:ADPS_LoadDefaultDrive)) {            
              # The drive is loaded because the value is not 0            
              $ADDriveLoaded = $true            
              Write-Verbose "AD drive mounted by default using environment variable"            
          } else {            
              # $Env:ADPS_LoadDefaultDrive = 0 -> don't load            
              Write-Verbose "AD drive mounting was disabled using environment variable"            
              if ($Fun) {            
                  try {            
                      # Mount the drive using a Global Catalog            
                      New-PSDrive -PSProvider ActiveDirectory -Name AD -Root "" -server (            
                          Get-ADDomainController -Discover -Service 2 -ErrorAction Stop            
                      ).Name -ErrorAction Stop            
                      $ADDriveLoaded = $true            
                      Write-Verbose "AD drive mounted in current scope"            
                  } catch {            
                      $ADDriveLoaded = $false            
                      Write-Warning -Message "Failed to mount AD drive in current scope because  $($_.Exception.Message)"            
                  }            
              } else {            
                  $ADDriveLoaded = $false            
              }            
          }            
      } else {            
          # The AD drive is loaded by default            
          $ADDriveLoaded = $true            
          Write-Verbose "AD drive mounted by default"            
      }
    • Avoid the recursive search gotcha
    • I have noticed during the testing phase, that the Active Directory provider no longer allow a Recursive search in Powershell 3.0 as it did in Powershell 2.0
      I had to add a mandatory path to an OU to workaround that issue.

      if ($Fun -and $ADDriveLoaded) {            
          Write-Verbose "Using the Active Directory module in fun mode"            
          Switch ($PSVersionTable.PSVersion.Major) {            
              2 {            
                  $GCIHT = @{            
                      Path = "AD:\$Path"            
                      Recurse = $true            
                  }            
                  break            
              }            
              default {            
                  $GCIHT = @{Path = "AD:\$Path"}            
              }            
          }            
      $allusers = @(Get-ChildItem -Filter "(&(objectClass=user)(objectCategory=person))" @GCIHT)            
      
    • Validating an OU path
    • I could not validate the path of the OU in the parameter block as I needed to know whether the AD drive was mounted or not. I added the following in the Begin block:

      # Validate the OU            
      if ($Fun -and $ADDriveLoaded) {            
          if (-not(Test-Path -Path "AD:\$($Path)" -PathType Container)) {            
              Write-Warning "Invalid OU path provided"            
              break            
          }            
      }
    • Solving the case of the User-Account-Control attribute and other Active Directory attibutes
    • Can the userAccountControl attribute be used to determine if the user account is locked or if his password is expired?
      The KB article kb305144 says that:

      Here’s also a comment on the MSDN page about the userAccountControl

      Thanks to the Powershell magazine PSTip I have seen that the msDS-User-Account-Control-Computed AD attribute is a constructed attribute

      Thanks to Richard Mueller and his wiki page we also know how these attributes are calculated:








      As you can see, this wiki page is just awsome, it actually answers many questions 😎

      When using ADSI or the AD: drive, I’ve used this kind of code to display nicely formatted properties:

      1..$MaxUsers | ForEach-Object {            
          $i = (Get-Random -Maximum $MaxRandom  -Minimum 0)            
          $propsar= 'sAMAccountName','department','title','userAccountControl','lastLogonTimestamp',            
                      'pwdLastSet','msDS-User-Account-Control-Computed'            
          $user = Get-Item -Path "AD:\$($allusers[$i].distinguishedName)" -Properties $propsar            
          New-Object -TypeName PSObject -Property @{            
              UserName = $user.sAMAccountName            
              Enabled = -not($user.userAccountControl -band 2)            
              Department = $user.department            
              Title = $user.title            
              LastLogonDate = switch ($user.lastLogonTimestamp) {            
                  $null {            
                      'Never' ; break            
                  }            
                  default {            
                      [datetime]::FromFileTime([int64]::Parse($_))            
                  }            
              }            
              LockedOut = [bool]($user.'msDS-User-Account-Control-Computed' -band 16)            
              PasswordExpired = [bool]($user.'msDS-User-Account-Control-Computed'-band 8388608)            
              PasswordLastSet = switch ($user.pwdLastSet) {            
                  0 {            
                          if ($user.userAccountControl -band 65536) {            
                              0 ; break            
                          } else {            
                              'Must Change password at next logon' ; break            
                          }            
                  }            
                  default {            
                      [datetime]::FromFileTime([int64]::Parse($_))            
                  }            
              }            
          }            
      } |             
      Select -Property UserName,Enabled,Department,Title,LastLogonDate,LockedOut,PasswordExpired,PasswordLastSet |            
      Sort -Descending -Property PasswordLastSet

      Having the PwdLastSet attribute set to 0 doesn’t necessarily means that the user must change his password at next logon
      (source)

    • Making sure to get results quickly
    • Performance wasn’t a requirement. But I didn’t know how many user objects are present in Active Directory. We could have 1000, 10 000 or even more. To get results more quickly, I added a range to the MaxUsers parameter and used its upper limit as a value for the ResultSetSize parameter of the Get-ADUser cmdlet.

      [parameter()]            
      [ValidateRange(0,1000)]            
      [int]$MaxUsers=20
      $allusers = ActiveDirectory\Get-ADUser -Filter * -ResultSetSize 1000

      ADSI has also a way to set the maximum number of objects returned.

      $Searcher = [System.DirectoryServices.DirectorySearcher]$Root            
      $Searcher.SizeLimit = 1000

      Unfortunately, we cannot instruct core cmdlets to get a certain number of items. As I discovered the issue with the recursive search and made the OU path a mandatory parameter, I could get 0 or more AD users objects. Yes, I could get 0 object if you type a valid OU path where there isn’t any user object. Worse, I noticed another problem when there’s only 1 user object. My Get-Random cmdlet was throwing an error because you can’t do:

      Get-Random -Maximum 1 -Minimum 1

    • (Re)using code to produce the HTML page
    • In the previous event, I used the awsome work of Don Jones who wrote a free ebook named “Creating HTML Reports in PowerShell” that you can download on this page: http://powershell.org/wp/newsletter/

    • Working on parameter validation
    • The instruction said that:

      Get PowerShell to do as much of that validation as possible – Dr.Scripto has to review your code, and doesn’t want to read a lot of manual validation
      script.

      I spent some time testing and reading the help about ValidateScript.
      I think I found a way to put as much as validation as possible in the ValidateScript attribute, keep it easy to read, grasp the logic and of course maintain.
      The only thing I wanted was to make the error behave as if it was thrown by a cmdlet.
      I wanted to avoid the full ValidateScript content to be displayed in the console when an error occurs because having too many lines could be somehow “counter-productive”. I’m also not a big fan of using the throw keyword… but sometimes you just need it πŸ˜‰

      [parameter()]            
      [ValidateScript({            
          # Test that the parent path exists            
          if (-not(Test-Path -Path (Split-Path -Path $_ -Parent) -PathType Container)) {            
              throw New-Object System.ArgumentException -ArgumentList "Filepath doesn't exist"            
          }            
          # Validate the filename extension            
          if ( (Split-Path -Path $_ -Leaf) -notmatch ".*\.html?$") {            
              throw New-Object System.ArgumentException -ArgumentList "File extension must end by .html or .htm"            
          }            
          return $true            
      })]            
      [string]$FilePath = "$Env:TEMP\audit.report.html"

      And here’s the result

      Not bad? What do you think?

    Final word
    This event was really hard but also fun. It was a good opportunity to learn many many things about Active Directory, like ADSI,…
    I’m not super proud of how my entry looks like but I know it does the job.
    You can have a look at it: http://scriptinggames.org/entrylist_.php?entryid=914
    If you see anything that’s wrong or that I can improve, I welcome your feedback. If you find something you don’t understand, leave a comment on this blog post, I’ll try to explain it.

    2013 Scripting Games Event 3

    As much as I like feedback on my Scripting Games entries, I still see voters using the BOFH mode.

    I’m sure my entry http://scriptinggames.org/entrylist.php?entryid=729 shouldn’t be rated with 1 star according to the scoring guidelines that you absolutely need to read before voting.

    Really, you’d fire me? Did I fail?
    I think you should see what it looks like as sometimes a picture is worth a thousand words:

    First I’ve had a look at the built-in cmdlet Get-Volume that comes with the Storage module in Windows 8 / Windows 2012 and that has already a pretty output in the console:

    But, this cmdlet cannot target older version of Windows because it queries a WMI namespace and class that only exists on Windows 8 based operating systems.

    Get-Content Function:\Get-Volume


    This cmdlet is great but not appropriate for the scenario.

    Then, I started the traditional way. I created a header, added some HTML data…

    $HTMLheader = @"
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
    <title>DRIVE FREESPACE REPORT</title>
    </head><body>
    
    "@
    

    There’s nothing wrong with this approach. It will work but it’s a bit cumbersome.

    Then I remembered that Don Jones wrote a free ebook named “Creating HTML Reports in PowerShell” that you can download on this page: http://powershell.org/wp/newsletter/

    I’ve just copied and used the awsome function he wrote. He already did the hard work, why would I reinvent the wheel? 😎

    The only thing I changed is the colors in the report. I extracted the blue colors from Excel with MSPaint and translated RGB colors to Hexadecimal with this website: http://www.rgbtohex.net/hextorgb/

    I also didnt’ know how to create an horizontal bar in HTML. I googled the “horizontal bar Html” keywords and found the answer on this page: http://www.w3schools.com/tags/tag_hr.asp

    Mike F Robbins won the Advanced Event2 2nd with this entry http://scriptinggames.org/entrylist_.php?entryid=552 where he demonstrated how to use the CIM (Common Information Model) to query computers over the WSMAN protocol or failing over DCOM for older versions of Windows.

    I liked his approach but I also like sometimes a very straightforward approach because it would make a difference, a boost on performance when you query thousands of computers/servers.
    I’ve seen many entries that use first the Test-Connection cmdlet before using the Get-WmiObject cmdlet to retrieve data from remote computers. They actually probably don’t realize that Test-Connection uses WMI behind the scene. It even has a -Credential parameter. Here’s what the NOTES in the help of the Test-Connection cmdlet say:
    . The more WMI classes you query, the longer it will last.

    You may wonder why I remove Credentials when creating a CIM session over WSMAN that targets the localhost. It isn’t really required. Well, I just remove them because my vanilla Windows 8 computer has WinRM turned off by default 😦

    So, my PROCESS block has 2 parts. I first try to establish a CIM session over WSMAN with the remote computer and then failover DCOM if WSMAN doesn’t work. I collect all the successfully opened CIM sessions into an array. When writing this code I wondered whether CIM sessions are throttled like PSSessions. The Get-Volume cmdlet has a -ThrottleLimit parameter whereas the New-CIMSession and New-CimSessionOption cmdlets don’t.

    In the second part of the PROCESS block, I query the Win32_Volume WMI class using the -Filter parameter of the Get-CIMinstance cmdlet.
    Why Win32_Volume ? Well, it’s more exhaustive than Win32_LogicalDisk. The Win32_Volume WMI class will list the system reserved partition, a NTFS hidden volume (see above image of the HTML report). It will also list local disks that have been mounted (mountpoints, in other words) as well as mounted VHD drives. My C: drive is actually a VHD located on the F: drive. Boe Prox wrote a nice blog post on mountpoints that you can find here.

    The other “cool” thing in this second part is that I’m using some of the properties of the CIM session object to display the target computer name (the one passed as parameter to the function) and the protocol being used to communicate with the remote computer.

    I also extracted the real NetBIOS name of the computer from the CIM data retreived that the person who rated my script with one star didn’t notice or understand 😦 I forgive you, I’m using some of the new Powershell Version 3.0 syntax enhancements πŸ˜€

    $NetBiosName = $CimData.SystemName | Select-Object -Unique

    Mike uses the following classic code to achieve the same thing from WMI data:

    $MachineName = $DiskSpace | Select-Object -ExpandProperty SystemName -Unique

    See his approach for Event3: http://mikefrobbins.com/2013/05/16/2013-powershell-scripting-games-advanced-event-3-bringing-bad-habits-from-the-gui-to-powershell/

    2013 Scripting Games Event 2

    As I write this post, Event 2 winners have been announced. Event 3 voting period started and is opened till 2013-05-21 00:00:00 (GMT). In other words, we just passed mid-point in Scripting Games 2013

    My entry for Event 2 in Advanced category was awarded place #5 by the voters’ community.

    … and it was ranked #1 by the expert judges and the mighty panel of celebrity judges

    I’d like to thank first all the anonymous voters on my entry, those who left a comment on my entry (it’s important to get feedback).
    I’d like to address a special thank to the judges. As I’m also a voter, I understand now how difficult and time consuming it may be to appreciate others’ work and leave a comment that help others improve their PowerShell scripting skills.

    My entry for Event 2 in the Advanced category won: http://scriptinggames.org/entrylist.php?entryid=465
    I’m sure it didn’t win because it was superior to others, I’ve seen some greats entries and learned many things by looking, voting and commenting these entries.
    I believe it won because there are probably some great learning points in it (hopefully, that’s what you’ll find).
    I actually wrote this entry twice.
    When I wrote my first version, I wanted to keep it simple. I started by the beginner entry instructions, wrote and validated some commands.
    Then I changed it to make it an advanced function. As I was using the Win32_ComputerSystem WMI class, I also read the following limitations on this MSDN page:

    I’ve left the code 2 days and before testing it on various systems, I read the instructions again. I didn’t pay enough attention the first time. I missed the “reliable” word.
    We were supposed to find reliable info whatever the operating system. I googled the following keywords “WMI list sockets” and found this page
    I had a look at these two knowledgebase articles:

    After reading the “more information” section of these KB articles, the word “reliable” in the instructions made fully sense.
    I decided to test every versions of Windows, check if the KB is present for Windows 2003 and XP… I actually even used the word “reliable” as a variable name to make sure the logic in the script is easier to follow.
    I still wanted to keep it simple and easy to read. That’s why I implemented a huge switch block for all the versions of Windows.
    What’s cool with this $Reliable variable is that it has 2 types depending on the OS version, either a boolean or an array of WMI object with 1 element or none.

    The $Reliable could be $null if the specific KB isn’t found by querying the Win32_QuickFixEngineering WMI class.
    The $Reliable could be hold 1 System.Management.ManagementObject#\Win32_QuickFixEngineering object if the specific KB is retreived from WMI.
    This trick allowed me to just use a single if statement block.

    if ($Reliable) {            
        Write-Verbose -Message "Reliable set to true"            
        $CS = Get-WmiObject -Class Win32_ComputerSystem  -Property $CSprops @WMIHT            
        $ProcessorsCount = $CS.NumberOfProcessors            
        $CoresCount      = $CS.NumberOfLogicalProcessors            
    } else {            
        Write-Verbose -Message "Reliable set to false"            
        $CS = Get-WmiObject -Class Win32_ComputerSystem  -Property "TotalPhysicalMemory","NumberOfProcessors" @WMIHT            
        $CoresCount      = $CS.NumberOfProcessors            
        $ProcessorsCount = 0            
    }

    To make sure to understand how this work, you can test the following in your console:

    if ($true) { $true } else { $false }            
                
    if (0) { $true } else { $false }            
                
    if (1) { $true } else { $false }            
                
    # test an empty array            
    if (@()) { $true } else { $false }            
                
    # test an array with 1 element            
    if (@('1')) { $true } else { $false }


    As you can see in the above image, Mike F Robbins and Francois-Xavier Cat, the top 2 crowdscores on Advanced Event 2, already wrote blog articles that you can find here:

    You can also read and learn many things from the Expert judges:

    or by other competitors blogging on Scriptings Games that Mike started referencing on this landing page:

    In case you missed it, a motivating prizes list is available on this page: http://powershell.org/wp/2013/04/17/scripting-games-2013-prize-list/

    Follow-up on monitoring the Scripting Games 2013 leaderboard

    I know I should spend more time polishing my Scripting Games entries but there’s also so much fun coding a way to monitor the leaderboard πŸ˜›
    Since my previous post many things have changed. The online page displays only the top 10 users in each track. It means I cannot get your current position if the event isn’t closed (voting included) unless you appear in the top 10 users of the ongoing event.

    When I wrote my previous entry, I took the wrong approach to find your name. Select-String will find all matches by default. Now that there are two events, if your name appears in both top 10 users, I got 2 matches. Of course, I could have used Select-String with the -Context parameter but it will capture the specified number of lines before and after the line with the match:

    $page | sls -Pattern "Advanced 2" -Context 10


    At the end, I didn’t use Select-String as I’ve found a quicker way to extract the top 10 users from the online page with a while loop 😎

    I’ve updated the script. The Get-PSH2013Leaderboard function’s features remain unchanged. I’ve created 2 “child” functions that will either read the top 10 users online or download and read all closed entries in your temp directory:

    Join-Path -Path $env:temp -ChildPath "SG2013"

    To get things run quicker and as voting ended, it’s downloaded only once.

    To use the version 2.0 of the Scripting Games 2013 monitoring script, I load all the functions by dot-sourcing the main script like this:

    . E:\Monitor-SG2013.ps1


    Now,
    For event 1: I can see the top 5 users and my own rank. The total number of votes property was available and I’ve added it.
    If it’s the first time I use it, I will see the following progress bar indicating that entries are downloaded in the %temp%\SG2013 directory:

    If I run the same command again, I get results more quickly as entries are being read locally.

    Get-PSH2013Learder -Category Advanced -EventNumber 1 -TopUsers 5 -UserName "_Emin_" |            
    Select Rank,Name,Score,Votes |            
    ft -AutoSize

    For event 2: as long as it isn’t closed, it makes sense to query the top 10 users online.

    Get-PSH2013Learder -Category Advanced -EventNumber 2 -TopUsers 5 -UserName "_Emin_"  -Repeat

    As you can see in the above image, I got only 16 votes on my first entry whereas Mike F. Robbins got 74. Sure, Mike has a fan club πŸ˜€ and I don’t 😦

    In the meantime, scoring guides lines have been published by Don Jones: http://powershell.org/wp/the-scripting-games/scoring-guidelines/ and a nice reminder apprears in the standard voting mode:
    …as well as in Beta voting mode:

    If you read this post that far and before voting ends on event 2 (2013-05-14 00:00:00 (all times GMT)), you actually owe me a vote on my entry (if you didn’t rate it yet): http://scriptinggames.org/entrylist.php?entryid=465. Don’t forget to write a comment on what you like or don’t like. The whole point of the Scripting Games is to get feeback on my work and have fun.

    Sure, you voted on my entry so that I can beat Mike? πŸ˜‰ Here’s the link to the version 2.0 of Monitor-SG2013.ps1