Overview

I developed these PowerShell scripts to generate a daily email report of open tickets within the Software Desk Express software database. The scripts get the content via SQL queries against your SDE SQL database and are piped into my email reporting functions.

At the very bottom of the email a section which tallies up the selected and the other teams total open tickets and sorts them by which team has the most open. To make it a little more friendly to read, I created an array of the “SDE Group” name and the “Team” name which I somewhat made a few guesses on. If the team name is wrong somewhere let me know and I can easily change it.

Basic Functionality

Although the report may look complicated, at its core, each function is simply generating an array of data based off various SQL queries to the database.

There are two types of reports we can invoke. Either one for a specific team (where an example is attached below):


Import-Module "\\scriptserver\Scripts\Invoke-DMGSDEReport\Invoke-DMGSDEReport.psm1" -Force
#Generate The Team Report
$emailaddresses = @("ateam@emailaddress.com")
Invoke-DMGSDEReport -GroupName SDEGROUP2 -GroupDescription "Server Inf. Team" -emailaddresses $emailaddresses

Or a report that combines the functions in a useful way to provide a summary report that would be useful to management:


Import-Module "\\scriptserver\Scripts\Invoke-DMGSDEReport\Invoke-DMGSDEReport.psm1" -Force
#Generate The Manager Report
$emailaddresses = @("managers@emailaddress.com")
Invoke-DMGSDEReport -ManagerReport MANAGERGROUP1 -emailaddresses $emailaddresses

The arrays are combined into a single array of arrays and then passed onto my HTML E-Mail function which is discussed elsewhere on this site:


New-DMGEmailReport `
            -Arrays $OutputArrays `
            -ReportTitle "$GroupDescription Open Ticket Report" `
            -from "SDEReports@emailadress.com" `
            -To $emailaddresses `
            -subject "$GroupDescription SDE Open Ticket Report"

Sample Report

PowerShell Invocation: Invoke-DGMSDEServerReport.ps1


Import-Module "\\SCRIPTSERVER\scripts\New-DMGEmailReport\New-DMGEmailReport.psm1" -Force

Import-Module "\\SCRIPTSERVER\Scripts\Invoke-DMGSDEReport\New-DMGSDEOpenIncidents.psm1" -Force
Import-Module "\\SCRIPTSERVER\Scripts\Invoke-DMGSDEReport\New-DMGSDEOpenWorkOrders.psm1" -Force
Import-Module "\\SCRIPTSERVER\Scripts\Invoke-DMGSDEReport\New-DMGSDETeamsOpenHistory.psm1" -Force
Import-Module "\\SCRIPTSERVER\Scripts\Invoke-DMGSDEReport\New-DMGSDETeamTicketHistory2.psm1" -Force
Import-Module "\\SCRIPTSERVER\Scripts\Invoke-DMGSDEReport\New-DMGSDETeamUserHistory.psm1" -Force
Import-Module "\\SCRIPTSERVER\Scripts\Invoke-DMGSDEReport\New-DMGSDEAreasOfConcern.psm1" -Force

<#
.SYNOPSIS
  Generates an SDE Email Report
.NOTES
  Version:        1.0
  Author:         David Maiolo
  Creation Date:  2018-01-22
  Purpose/Change: Initial script development

#>

#---------------------------------------------------------[Initialisations]--------------------------------------------------------

function Invoke-DMGSDEReport{
    param(
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true,ParameterSetName='TeamReport')]
        [ValidateSet("SDEGROUP1","SDEGROUP2","SDEGROUP3","SDEGROUP4","SDEGROUP5","SDEGROUP6","SDEGROUP7","SDEGROUP8","SDEGROUP9","SDEGROUP10")]
        [String]$GroupName,
        [Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$true,ParameterSetName='TeamReport')]
        [String]$GroupDescription,
        [Parameter(Position=4,Mandatory=$false,ValueFromPipeline=$true,ParameterSetName='TeamReport')]
        [ValidateSet("Black","White","Red","Lime","Blue","Yellow","Cyan","Magenta","Silver","Gray","Maroon","Olive","Green","Purple","Teal","Navy")]
        [String]$Color,
        [ValidateSet("ManagerGroup1","ManagerGroup2","ManagerGroup3","ManagerGroup4","ManagerGroup5","ManagerGroup6")]
        [Parameter(Position=2,Mandatory=$true,ValueFromPipeline=$true,ParameterSetName='ManagerReport')]
        [String]$ManagerReport,
        [Parameter(Position=3,Mandatory=$true,ValueFromPipeline=$true)]
        $emailaddresses
    )
    
    $OutPutArrays = @()

    if([bool]($MyInvocation.BoundParameters.Keys -contains 'ManagerReport')){

        #A Manager Report

        $colors = @("Lime","Blue","Cyan","Magenta","Gray","Maroon","Olive","Purple","Teal","Navy")
        $teams = @()

         switch ($ManagerReport) {
           "ManagerGroup1" {
            $teams += [PSCustomObject] @{'Group' = 'SDEGROUP1'; 'Description' = 'IT Governance'};
            $teams += [PSCustomObject] @{'Group' = 'SDEGROUP2'; 'Description' = 'DS User Request'};
            $teams += [PSCustomObject] @{'Group' = 'SDEGROUP3'; 'Description' = 'Directory Services Administrators'};
            $teams += [PSCustomObject] @{'Group' = 'SDEGROUP4'; 'Description' = 'Move / Add / Change'};
           }
           "ManagerGroup2" {
            $teams += [PSCustomObject] @{'Group' = 'SDEGROUP1'; 'Description' = 'Tech-Ops Channels'};
            $teams += [PSCustomObject] @{'Group' = 'SDEGROUP2'; 'Description' = 'Tech-Ops Core & IPS'};
            $teams += [PSCustomObject] @{'Group' = 'SDEGROUP3'; 'Description' = 'Tech-Ops Lending Credit'};
            $teams += [PSCustomObject] @{'Group' = 'SDEGROUP4'; 'Description' = 'Tech-IT Platform Managers'};
           }
           "ManagerGroup3" {
            $teams += [PSCustomObject] @{'Group' = 'SDEGROUP1'; 'Description' = 'Database Administrators'};
            $teams += [PSCustomObject] @{'Group' = 'SDEGROUP2'; 'Description' = 'Data Warehouse'};
            $teams += [PSCustomObject] @{'Group' = 'SDEGROUP3'; 'Description' = 'EDW Administration'};
            $teams += [PSCustomObject] @{'Group' = 'SDEGROUP4'; 'Description' = 'IBM Administration'};
           }
           "ManagerGroup4" {
            $teams += [PSCustomObject] @{'Group' = 'SDEGROUP1'; 'Description' = 'Enterprise App'};
            $teams += [PSCustomObject] @{'Group' = 'SDEGROUP2'; 'Description' = 'Enterprise Development'};
           }
           "ManagerGroup5" {
            $teams += [PSCustomObject] @{'Group' = 'SDEGROUP1'; 'Description' = 'Network Infr. Team'};
            $teams += [PSCustomObject] @{'Group' = 'SDEGROUP2'; 'Description' = 'Server Infrastructure Team'};
            $teams += [PSCustomObject] @{'Group' = 'SDEGROUP3'; 'Description' = 'Endpoint Admins Team'};
            $teams += [PSCustomObject] @{'Group' = 'SDEGROUP4'; 'Description' = 'Technology Help Desk'};
            $teams += [PSCustomObject] @{'Group' = 'SDEGROUP5'; 'Description' = 'Telecom Team'}
            $teams += [PSCustomObject] @{'Group' = 'SDEGROUP6'; 'Description' = 'Network - General'};
            $teams += [PSCustomObject] @{'Group' = 'SDEGROUP7'; 'Description' = 'Network - Imaging'};
           }
           "ManagerGroup6" {
            $teams += [PSCustomObject] @{'Group' = 'SDEGROUP1'; 'Description' = 'Branch Help Desk'};
            $teams += [PSCustomObject] @{'Group' = 'SDEGROUP2'; 'Description' = 'Corporate Security'};
            $teams += [PSCustomObject] @{'Group' = 'SDEGROUP3'; 'Description' = 'Information Security'};
            $teams += [PSCustomObject] @{'Group' = 'SDEGROUP4'; 'Description' = 'Xerox'};
           }
           default {}
        }

        $OutputArrays+= New-DMGSDETeamsOpenHistory -ShowAllTeams -Color "Blue"

        foreach ($team in $teams){
            #Create a New Random Color For Each Team
            $LastColor = $Color
            do{$Color = $Colors[(Get-Random -Maximum ([array]$Colors).count)]} while ($Color -eq $LastColor)

            #The reports for each team of the manager report
            $OutputArrays+= New-DMGSDETeamUserHistory -GroupName $team.Group -GroupDescription $team.Description -Color $Color
            $OutputArrays+= New-DMGSDEAreasOfConcern -GroupName $team.Group -GroupDescription $team.Description -MaximumNumberOfRowsToDisplay 5 -DaysUntilConcern 45 -Color $Color
            $OutputArrays+= New-DMGSDETeamTicketHistory2 -GroupName $team.Group -GroupDescription $team.Description -NumOfMonthsToShowTrends 8 -Color $Color
        }

        #Email Manager Report
        New-DMGEmailReport `
            -Arrays $OutputArrays `
            -ReportTitle "SDE Manager Report`: $ManagerReport" `
            -from "SDEReports@DMGhawaii.com" `
            -To $emailaddresses `
            -subject "SDE Manager Report`: $ManagerReport"
    }
    else{
        #A Team Report

        if([bool]($MyInvocation.BoundParameters.Keys -contains 'Color')){
            $DMGSDEAreasOfConcern = New-DMGSDEAreasOfConcern -GroupName $GroupName -GroupDescription $GroupDescription -MaximumNumberOfRowsToDisplay 5 -DaysUntilConcern 60 -Color $Color
            $DMGSDEOpenIncidents = New-DMGSDEOpenIncidents -GroupName $GroupName -GroupDescription $GroupDescription -MaximumNumberOfRowsToDisplay 50 -Color $Color
            $DMGSDEOpenWorkOrders = New-DMGSDEOpenWorkOrders -GroupName $GroupName -GroupDescription $GroupDescription -MaximumNumberOfRowsToDisplay 50 -Color $Color
            $DMGSDETeamsOpenHistory = New-DMGSDETeamsOpenHistory -ShowAllTeams -Color $Color
            $DMGSDETeamTicketHistory2 = New-DMGSDETeamTicketHistory2 -GroupName $GroupName -GroupDescription $GroupDescription -NumOfMonthsToShowTrends 12 -Color $Color
            $DMGSDETeamUserHistory = New-DMGSDETeamUserHistory -GroupName $GroupName -GroupDescription $GroupDescription -Color $Color
        }
        else{
            $DMGSDEAreasOfConcern = New-DMGSDEAreasOfConcern -GroupName $GroupName -GroupDescription $GroupDescription -MaximumNumberOfRowsToDisplay 5 -DaysUntilConcern 60
            $DMGSDEOpenIncidents = New-DMGSDEOpenIncidents -GroupName $GroupName -GroupDescription $GroupDescription -MaximumNumberOfRowsToDisplay 50
            $DMGSDEOpenWorkOrders = New-DMGSDEOpenWorkOrders -GroupName $GroupName -GroupDescription $GroupDescription -MaximumNumberOfRowsToDisplay 50
            $DMGSDETeamsOpenHistory = New-DMGSDETeamsOpenHistory -ShowAllTeams
            $DMGSDETeamTicketHistory2 = New-DMGSDETeamTicketHistory2 -GroupName $GroupName -GroupDescription $GroupDescription -NumOfMonthsToShowTrends 12
            $DMGSDETeamUserHistory = New-DMGSDETeamUserHistory -GroupName $GroupName -GroupDescription $GroupDescription
        }

        $OutputArrays+=$DMGSDEAreasOfConcern
        $OutputArrays+=$DMGSDEOpenIncidents
        $OutputArrays+=$DMGSDEOpenWorkOrders
        $OutputArrays+=$DMGSDETeamsOpenHistory
        $OutputArrays+=$DMGSDETeamTicketHistory2
        $OutputArrays+=$DMGSDETeamUserHistory


        if (($DMGSDEOpenIncidents -ne $null) -or ($DMGSDEOpenWorkOrders -ne $null)){
        #Email Multiple Arrays
        New-DMGEmailReport `
            -Arrays $OutputArrays `
            -ReportTitle "$GroupDescription Open Ticket Report" `
            -from "SDEReports@emailadress.com" `
            -To $emailaddresses `
            -subject "$GroupDescription SDE Open Ticket Report"
        }else{
            Write-Host "There were no open incidents or work orders, so the email was not sent."
        }
   }

    
}

PowerShell Module: New-DGMSDEAreasOfConcern.psm1


<#
.SYNOPSIS
  Generates New-DMGSDEOpenIncidents
.NOTES
  Version:        1.0
  Author:         David Maiolo
  Creation Date:  2018-01-22
  Purpose/Change: Initial script development

#>

#---------------------------------------------------------[Initialisations]--------------------------------------------------------

function New-DMGSDEAreasOfConcern{
param(
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
        [ValidateSet("SDEGROUP1","SDEGROUP2","SDEGROUP3","SDEGROUP4","SDEGROUP5","SDEGROUP6","SDEGROUP7","SDEGROUP8","SDEGROUP9","SDEGROUP10")]
        [String]$GroupName,
        [Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$true)]
        [String]$GroupDescription,
        [Parameter(Position=3,Mandatory=$false,ValueFromPipeline=$true)]
        [Int]$MaximumNumberOfRowsToDisplay = 5,
        [Parameter(Position=4,Mandatory=$false,ValueFromPipeline=$true)]
        [Int]$DaysUntilConcern = 30,
        [Parameter(Position=5,Mandatory=$false,ValueFromPipeline=$true)]
        [ValidateSet("Black","White","Red","Lime","Blue","Yellow","Cyan","Magenta","Silver","Gray","Maroon","Olive","Green","Purple","Teal","Navy")]
        [String]$Color,
        [Parameter(Position=6,Mandatory=$false,ValueFromPipeline=$true)]
        [String]$ServerInstance = "SDESQLSERVER"
    )
    
    $OutputArrays =@()
    $sqlquery = $null
    $Array = $null

    $Currentyear = get-date -Format yyyy
    $Currentdate = get-date -Format MM/dd
    $Lastyear = ($Currentyear-1)
    $DateOfConcern=(Get-Date).AddDays(-($DaysUntilConcern)).ToString('yyyy/MM/dd')

    #========================================================================================
    #SQL Query To Find Open Work Orders
    #========================================================================================
    $sqlquery = "SELECT [Incident #],[First Name],[Last Name],[Login ID Assigned To],[Open Date & Time],[Incident Description] ,[LastModified]
                  FROM [SDE_PROD].[_SMDBA_].[Incident]
                  WHERE ([Status ID:] != 'Closed' AND [Status ID:] != 'ADMIN_CLOSE' AND [Status ID:] != 'CANCEL' AND [Status ID:] != 'REJECT' AND [Open Date & Time] <= '$DateOfConcern' AND [Group Name] = '$GroupName')
                  ORDER BY [Open Date & Time] ASC"

    Write-Host $sqlquery

    #Generate Incident Concern Count
    $IncidentConcern = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance  | `
                        Select `
                        @{L='Concern #';E={$_."Incident #"}},`
                        @{L='Customer';E={$_."First Name"+" "+$_."Last Name"}},`
                        @{L='Assigned To';E={$_."Login ID Assigned To"}},
                        @{L='Days Old';E={-((NEW-TIMESPAN –Start (GET-DATE) –End ([datetime]$_."Open Date & Time")).Days)}},`
                        @{L='Brief Description';E={($_."Incident Description").SubString(0,80)}},`
                        @{L='Days Ago Last Modified';E={-((NEW-TIMESPAN –Start (GET-DATE) –End ([datetime]$_."LastModified")).Days)}}
    [int]$IncidentConcernCount = $IncidentConcern.Count

    $sqlquery = "SELECT [Work Order #],[First Name],[Last Name],[Login ID Assigned To],[Open Date & Time],[Incident Description] ,[LastModified]
                  FROM [SDE_PROD].[_SMDBA_].[Work Orders]
                  WHERE ([Status ID:] != 'Closed' AND [Status ID:] != 'ADMIN_CLOSE' AND [Status ID:] != 'CANCEL' AND [Status ID:] != 'REJECT' AND [Open Date & Time] <= '$DateOfConcern' AND [Group Name] = '$GroupName')
                  ORDER BY [Open Date & Time] ASC"

    Write-Host $sqlquery

    #Generate Work Order Concern Count
    $WorkOrderConcern = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance  | `
                        Select `
                        @{L='Concern #';E={$_."Work Order #"}},`
                        @{L='Customer';E={$_."First Name"+" "+$_."Last Name"}},`
                        @{L='Assigned To';E={$_."Login ID Assigned To"}},`
                        @{L='Days Old';E={-((NEW-TIMESPAN –Start (GET-DATE) –End ([datetime]$_."Open Date & Time")).Days)}},`
                        @{L='Brief Description';E={($_."Incident Description").SubString(0,80)}},`
                        @{L='Days Ago Last Modified';E={-((NEW-TIMESPAN –Start (GET-DATE) –End ([datetime]$_."LastModified")).Days)}}
    [int]$WorkOrderConcernCount = $WorkOrderConcern.Count

    $AllConcerns = @()
    $AllConcerns += $IncidentConcern
    $AllConcerns += $WorkOrderConcern

    $AllConcernCount = $IncidentConcernCount+$WorkOrderConcernCount

    if([bool]($MyInvocation.BoundParameters.Keys -contains 'Color')){
        if ($AllConcernCount -le 2){$Message = "There are $IncidentConcernCount Incidents and $WorkOrderConcernCount Work Orders that have been open at least $DaysUntilConcern days. You should take care of these soon, but good work keeping this number low. If you solve all of these, this section will be removed."}
        elseif ($AllConcernCount -le 5){$Message = "There are $IncidentConcernCount Incidents and $WorkOrderConcernCount Work Orders that have been open at least $DaysUntilConcern days. All though this number is low, ideally it should be zero."}
        elseif ($AllConcernCount -le 10){$Message = "There are $IncidentConcernCount Incidents and $WorkOrderConcernCount Work Orders that have been open at least $DaysUntilConcern days. The number of older concerns is becoming critical."}
        elseif ($AllConcernCount -le 15){$Message = "There are $IncidentConcernCount Incidents and $WorkOrderConcernCount Work Orders that have been open at least $DaysUntilConcern days. The number of older concerns is becoming increasingly critical."}
        elseif ($AllConcernCount -le 20){$Message = "There are $IncidentConcernCount Incidents and $WorkOrderConcernCount Work Orders that have been open at least $DaysUntilConcern days. The amount of concerns is almost critital and should be addressed as a top priority."}
        else{$Message = "There are $IncidentConcernCount Incidents and $WorkOrderConcernCount Work Orders that have been open at least $DaysUntilConcern days. This is a critical and should be addressed as a priority."}
    }else{

        if ($AllConcernCount -le 2){$Color = "Green"; $Message = "There are $IncidentConcernCount Incidents and $WorkOrderConcernCount Work Orders that have been open at least $DaysUntilConcern days. You should take care of these soon, but good work keeping this number low. If this number goes above two, we will display this as yellow. If you solve all of these, this section will be removed."}
        elseif ($AllConcernCount -le 5){$Color = "Yellow"; $Message = "There are $IncidentConcernCount Incidents and $WorkOrderConcernCount Work Orders that have been open at least $DaysUntilConcern days. All though this number is low, ideally it should be zero. Once you get this down to two, we will show this area as green."}
        elseif ($AllConcernCount -le 10){$Color = "Yellow"; $Message = "There are $IncidentConcernCount Incidents and $WorkOrderConcernCount Work Orders that have been open at least $DaysUntilConcern days. The number of older concerns is becoming critical. Once you get this down to two, we will show this area as green."}
        elseif ($AllConcernCount -le 15){$Color = "Red"; $Message = "There are $IncidentConcernCount Incidents and $WorkOrderConcernCount Work Orders that have been open at least $DaysUntilConcern days. The number of older concerns is becoming increasingly critical. Once you get this down to ten, we will show this area as yellow."}
        elseif ($AllConcernCount -le 20){$Color = "Red"; $Message = "There are $IncidentConcernCount Incidents and $WorkOrderConcernCount Work Orders that have been open at least $DaysUntilConcern days. The amount of concerns is almost critital and should be addressed as a top priority. Once you get this down to ten, we will show this area as yellow."}
        else{$Color = "Red"; $Message = "There are $IncidentConcernCount Incidents and $WorkOrderConcernCount Work Orders that have been open at least $DaysUntilConcern days. This is a critical and should be addressed as a priority. Once you get this down to ten, we will show this area as yellow."}
    }

    if ($AllConcernCount -gt $MaximumNumberOfRowsToDisplay){
        $Title = "$GroupDescription`: At Least $DaysUntilConcern days old (only showing top $MaximumNumberOfRowsToDisplay)";
    }else{
        $Title = "$GroupDescription`: At Least $DaysUntilConcern days old";
    }

    #Array2
    $output = [PSCustomObject] @{
    'Message' = $Message;
    'Title' = $Title;
    'Color' = $Color;
    'Array' = $AllConcerns | Sort-Object -Property "Days Old" -Descending | Select-Object -First $MaximumNumberOfRowsToDisplay;
    }
    if ($output.Array -ne $NULL){return $output}
}

PowerShell Module: New-DGMSDEOpenIncidents.psm1


<#
.SYNOPSIS
  Generates New-DMGSDEOpenIncidents
.NOTES
  Version:        1.0
  Author:         David Maiolo
  Creation Date:  2018-01-22
  Purpose/Change: Initial script development

#>

#---------------------------------------------------------[Initialisations]--------------------------------------------------------

function New-DMGSDEOpenIncidents{
param(
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
        [ValidateSet("SDEGROUP1","SDEGROUP2","SDEGROUP3","SDEGROUP4","SDEGROUP5","SDEGROUP6","SDEGROUP7","SDEGROUP8","SDEGROUP9","SDEGROUP10")]
        [String]$GroupName,
        [Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$true)]
        [String]$GroupDescription,
        [Parameter(Position=3,Mandatory=$false,ValueFromPipeline=$true)]
        $MaximumNumberOfRowsToDisplay = 50,
        [Parameter(Position=4,Mandatory=$false,ValueFromPipeline=$true)]
        [ValidateSet("Black","White","Red","Lime","Blue","Yellow","Cyan","Magenta","Silver","Gray","Maroon","Olive","Green","Purple","Teal","Navy")]
        [String]$Color = "Blue",
        [Parameter(Position=5,Mandatory=$false,ValueFromPipeline=$true)]
        $ServerInstance = "SDESQLSERVER"
    )
    
    $OutputArrays =@()
    $sqlquery = $null
    $Array = $null

    #========================================================================================
    #SQL Query To Find Open Incidents
    #========================================================================================
    $sqlquery = "SELECT [Incident #],[First Name],[Last Name],[Login ID Assigned To],[Open Date & Time],[Incident Description]
      FROM [SDE_PROD].[_SMDBA_].[Incident]
      WHERE ([Status ID:] != 'Closed' AND [Status ID:] != 'ADMIN_CLOSE' AND [Status ID:] != 'CANCEL' AND [Status ID:] != 'REJECT' AND [Group Name] = '$GroupName')
      ORDER BY [Incident #] DESC"

    #Generate different messages and colors based on the number open
    $Array = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance  | Select `
        "Incident #",`
        @{L='Customer';E={$_."First Name"+" "+$_."Last Name"}},`
        @{L='Assigned To';E={$_."Login ID Assigned To"}},
        @{L='Opened Date';E={($_."Open Date & Time").ToString("MM/dd/yyyy")}},
        "Incident Description"
    [int]$IncidentCount = $Array.Count


    if([bool]($MyInvocation.BoundParameters.Keys -contains 'Color')){
        if ($IncidentCount -le 19){$Message = "These are the $IncidentCount open incidents currently assigned to your group. This small number opened is considered excellent!"}
        elseif ($IncidentCount -le 30){$Message = "These are the $IncidentCount open incidents currently assigned to your group."}
        elseif ($IncidentCount -le 60){$Message = "These are the $IncidentCount open incidents currently assigned to your group. The number of open incidents is getting somewhat critical."}
        elseif ($IncidentCount -le 99){$Message = "These are the $IncidentCount open incidents currently assigned to your group. The number of open incidents is getting critical."}
        elseif ($IncidentCount -le 199){$Message = "These are the $IncidentCount open incidents currently assigned to your group. With over 100 open incidents, the number is critical!"}
        else{$Message = "These are the $IncidentCount open incidents currently assigned to your group. At over 200 tickets, the number of open incidents is severe!"}
    }
    else{
        if ($IncidentCount -le 19){$Color = "Green"; $Message = "These are the $IncidentCount open incidents currently assigned to your group. This small number opened is considered excellent!"}
        elseif ($IncidentCount -le 30){$Color = "Green"; $Message = "These are the $IncidentCount open incidents currently assigned to your group."}
        elseif ($IncidentCount -le 60){$Color = "Yellow"; $Message = "These are the $IncidentCount open incidents currently assigned to your group. The number of open incidents is getting somewhat critical."}
        elseif ($IncidentCount -le 99){$Color = "Red"; $Message = "These are the $IncidentCount open incidents currently assigned to your group. The number of open incidents is getting critical."}
        elseif ($IncidentCount -le 199){$Color = "Red"; $Message = "These are the $IncidentCount open incidents currently assigned to your group. With over 100 open incidents, the number is critical!"}
        else{$Color = "Red"; $Message = "These are the $IncidentCount open incidents currently assigned to your group. At over 200 tickets, the number of open incidents is severe!"}
    }

    if ($Array.Count -gt $MaximumNumberOfRowsToDisplay){
        $Title = "$GroupDescription`: $IncidentCount Open Incidents (only showing top $MaximumNumberOfRowsToDisplay)";
    }else{
        $Title = "$GroupDescription`: $IncidentCount Open Incidents";
    }

    #Array1
    $output = [PSCustomObject] @{
    'Message' = $Message;
    'Title' = $Title;
    'Color' = $Color;
    'Array' = $Array | Select-Object -First $MaximumNumberOfRowsToDisplay;
    }
    if ($output.Array -ne $NULL){return $output}
}

PowerShell Module: New-DGMSDEOpenWorkOrders.psm1


<#
.SYNOPSIS
  Generates New-DMGSDEOpenIncidents
.NOTES
  Version:        1.0
  Author:         David Maiolo
  Creation Date:  2018-01-22
  Purpose/Change: Initial script development

#>

#---------------------------------------------------------[Initialisations]--------------------------------------------------------

function New-DMGSDEOpenWorkOrders{
param(
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
        [ValidateSet("SDEGROUP1","SDEGROUP2","SDEGROUP3","SDEGROUP4","SDEGROUP5","SDEGROUP6","SDEGROUP7","SDEGROUP8","SDEGROUP9","SDEGROUP10")]
        [String]$GroupName,
        [Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$true)]
        [String]$GroupDescription,
        [Parameter(Position=3,Mandatory=$false,ValueFromPipeline=$true)]
        $MaximumNumberOfRowsToDisplay = 50,
        [Parameter(Position=4,Mandatory=$false,ValueFromPipeline=$true)]
        [ValidateSet("Black","White","Red","Lime","Blue","Yellow","Cyan","Magenta","Silver","Gray","Maroon","Olive","Green","Purple","Teal","Navy")]
        [String]$Color = "Blue",
        [Parameter(Position=5,Mandatory=$false,ValueFromPipeline=$true)]
        $ServerInstance = "SDESQLSERVER"
    )
    
    $OutputArrays =@()
    $sqlquery = $null
    $Array = $null

    #========================================================================================
    #SQL Query To Find Open Work Orders
    #========================================================================================
    $sqlquery = "SELECT [Work Order #],[First Name],[Last Name],[Login ID Assigned To],[Open Date & Time],[Incident Description]
      FROM [SDE_PROD].[_SMDBA_].[Work Orders]
      WHERE ([Status ID:] != 'Closed' AND [Status ID:] != 'ADMIN_CLOSE' AND [Status ID:] != 'CANCEL' AND [Status ID:] != 'REJECT' AND [Group Name] = '$GroupName')
      ORDER BY [Work Order #] DESC"

    #Generate different messages and colors based on the number open
    $Array = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance  | Select `
        "Work Order #",`
        @{L='Customer';E={$_."First Name"+" "+$_."Last Name"}},`
        @{L='Assigned To';E={$_."Login ID Assigned To"}},
        @{L='Opened Date';E={($_."Open Date & Time").ToString("MM/dd/yyyy")}},
        "Incident Description"
    [int]$WorkOrderCount = $Array.Count

    if([bool]($MyInvocation.BoundParameters.Keys -contains 'Color')){
        if ($WorkOrderCount -le 19){$Message = "These are the $WorkOrderCount open Work Orders currently assigned to your group. This small number opened is considered excellent!"}
        elseif ($WorkOrderCount -le 30){$Message = "These are the $WorkOrderCount open Work Orders currently assigned to your group."}
        elseif ($WorkOrderCount -le 60){$Message = "These are the $WorkOrderCount open Work Orders currently assigned to your group. The number of open Work Orders is getting somewhat critical."}
        elseif ($WorkOrderCount -le 99){$Message = "These are the $WorkOrderCount open Work Orders currently assigned to your group. The number of open Work Orders is getting critical."}
        elseif ($WorkOrderCount -le 199){$Message = "These are the $WorkOrderCount open Work Orders currently assigned to your group. With over 100 open Work Orders, the number is critical!"}
        else{$Message = "These are the $WorkOrderCount open Work Orders currently assigned to your group. At over 200 tickets, the number of open Work Orders is severe!"}
    }
    else{
        if ($WorkOrderCount -le 19){$Color = "Green"; $Message = "These are the $WorkOrderCount open Work Orders currently assigned to your group. This small number opened is considered excellent!"}
        elseif ($WorkOrderCount -le 30){$Color = "Green"; $Message = "These are the $WorkOrderCount open Work Orders currently assigned to your group."}
        elseif ($WorkOrderCount -le 60){$Color = "Yellow"; $Message = "These are the $WorkOrderCount open Work Orders currently assigned to your group. The number of open Work Orders is getting somewhat critical."}
        elseif ($WorkOrderCount -le 99){$Color = "Red"; $Message = "These are the $WorkOrderCount open Work Orders currently assigned to your group. The number of open Work Orders is getting critical."}
        elseif ($WorkOrderCount -le 199){$Color = "Red"; $Message = "These are the $WorkOrderCount open Work Orders currently assigned to your group. With over 100 open Work Orders, the number is critical!"}
        else{$Color = "Red"; $Message = "These are the $WorkOrderCount open Work Orders currently assigned to your group. At over 200 tickets, the number of open Work Orders is severe!"}
    }

    if ($Array.Count -gt $MaximumNumberOfRowsToDisplay){
        $Title = "$GroupDescription`: $WorkOrderCount Open Work Orders (only showing top $MaximumNumberOfRowsToDisplay)";
    }else{
        $Title = "$GroupDescription`: $WorkOrderCount Open Work Orders";
    }

    #Array2
    $output = [PSCustomObject] @{
    'Message' = $Message;
    'Title' = $Title;
    'Color' = $Color;
    'Array' = $Array | Select-Object -First $MaximumNumberOfRowsToDisplay;
    }
    if ($output.Array -ne $NULL){return $output}
}

PowerShell Module: New-DGMSDETeamsOpenHistory.psm1


<#
.SYNOPSIS
  Generates New-DMGSDEOpenIncidents
.NOTES
  Version:        1.0
  Author:         David Maiolo
  Creation Date:  2018-01-22
  Purpose/Change: Initial script development

#>

#---------------------------------------------------------[Initialisations]--------------------------------------------------------

function New-DMGSDETeamsOpenHistory{
param(
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true,ParameterSetName='Single Team')]
        [ValidateSet("SDEGROUP1","SDEGROUP2","SDEGROUP3","SDEGROUP4","SDEGROUP5","SDEGROUP6","SDEGROUP7","SDEGROUP8","SDEGROUP9","SDEGROUP10")]
        [String]$GroupName,
        [Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$true,ParameterSetName='Single Team')]
        [String]$GroupDescription,
        [Parameter(Position=2,Mandatory=$true,ValueFromPipeline=$true,ParameterSetName='All Teams')]
        [Switch]$ShowAllTeams,
        [Parameter(Position=3,Mandatory=$false,ValueFromPipeline=$true)]
        [ValidateSet("Black","White","Red","Lime","Blue","Yellow","Cyan","Magenta","Silver","Gray","Maroon","Olive","Green","Purple","Teal","Navy")]
        [String]$Color = "Blue",
        [Parameter(Position=4,Mandatory=$false,ValueFromPipeline=$true)]
        $ServerInstance = "SDESQLSERVER"
    )
    
    $OutputArrays =@()
    $sqlquery = $null
    $Array = $null
    $teams = @()
    $Currentyear = get-date -Format yyyy
    $Currentdate = get-date -Format MM/dd
    $Lastyear = ($Currentyear-1)

    #========================================================================================
    #SQL Query To Find Open Tickets For Each Team
    #========================================================================================

    if([bool]($MyInvocation.BoundParameters.Keys -contains 'ShowAllTeams')){

        $teams += [PSCustomObject] @{'Group' = 'BHD'; 'Description' = 'Branch Help Desk'};
        $teams += [PSCustomObject] @{'Group' = 'CCENT'; 'Description' = 'DS User Request'};
        $teams += [PSCustomObject] @{'Group' = 'CORPSEC'; 'Description' = 'Corporate Security'};
        $teams += [PSCustomObject] @{'Group' = 'DBA'; 'Description' = 'Database Administrators'};
        $teams += [PSCustomObject] @{'Group' = 'DS_ADMIN'; 'Description' = 'Directory Services Administrators'};
        $teams += [PSCustomObject] @{'Group' = 'DW'; 'Description' = 'Data Warehouse'};
        $teams += [PSCustomObject] @{'Group' = 'EA'; 'Description' = 'Enterprise Architecture'};
        $teams += [PSCustomObject] @{'Group' = 'EA_DEV'; 'Description' = 'Enterprise Architecture - Dev'};
        $teams += [PSCustomObject] @{'Group' = 'EDW_ADMIN'; 'Description' = 'EDW Administration'};
        $teams += [PSCustomObject] @{'Group' = 'GOVERNANCE'; 'Description' = 'IT Governance'};
        $teams += [PSCustomObject] @{'Group' = 'HELP DESK'; 'Description' = 'Technology Help Desk'};
        $teams += [PSCustomObject] @{'Group' = 'INFOSEC'; 'Description' = 'Information Security'};
        $teams += [PSCustomObject] @{'Group' = 'MAC'; 'Description' = 'Move / Add / Change'};
        $teams += [PSCustomObject] @{'Group' = 'N2K'; 'Description' = 'Network 2000 - General'};
        $teams += [PSCustomObject] @{'Group' = 'N2K_IMAGE'; 'Description' = 'Network 2000 - Imaging'};
        $teams += [PSCustomObject] @{'Group' = 'NOG'; 'Description' = 'Network Team'};
        $teams += [PSCustomObject] @{'Group' = 'SHRSVS'; 'Description' = 'Server Infr. Team'};
        $teams += [PSCustomObject] @{'Group' = 'TECH_OPS_CHANNELS'; 'Description' = 'Tech-Ops Channels'};
        $teams += [PSCustomObject] @{'Group' = 'TECH_OPS_CORE_IPS'; 'Description' = 'Tech-Ops Core & IPS'};
        $teams += [PSCustomObject] @{'Group' = 'TECH_OPS_LENDING_CREDIT'; 'Description' = 'Tech-Ops Lending Credit'};
        $teams += [PSCustomObject] @{'Group' = 'TECH_PLATFORM_MANAGERS'; 'Description' = 'Tech-IT Platform Managers'};
        $teams += [PSCustomObject] @{'Group' = 'TELECOMS'; 'Description' = 'Telecom Team'};
        #$teams += [PSCustomObject] @{'Group' = 'WORKFLOW'; 'Description' = 'Nautilus Workflow'};
        $teams += [PSCustomObject] @{'Group' = 'WSG'; 'Description' = 'Endpoint Admins Team'};
        $teams += [PSCustomObject] @{'Group' = 'XEROX'; 'Description' = 'Xerox'};
    }else{
        $teams += [PSCustomObject] @{'Group' = $GroupName; 'Description' = $GroupDescription};
    }

    
    $Array = @()

    foreach ($team in $teams){
      
      #==Currently Open
      $sqlquery = "SELECT COUNT ([Work Order #])
      FROM [SDE_PROD].[_SMDBA_].[Work Orders]
      WHERE ([Status ID:] != 'Closed' AND [Status ID:] != 'ADMIN_CLOSE' AND [Status ID:] != 'CANCEL' AND [Status ID:] != 'REJECT' AND [Group Name] = '"+($team.Group)+"')"

      $team_WO = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance
      [int]$team_WO_Count = $team_WO.Column1

      $sqlquery = "SELECT COUNT ([Incident #])
      FROM [SDE_PROD].[_SMDBA_].[Incident]
      WHERE ([Status ID:] != 'Closed' AND [Status ID:] != 'ADMIN_CLOSE' AND [Status ID:] != 'CANCEL' AND [Status ID:] != 'REJECT' AND [Group Name] = '"+($team.Group)+"')"

      $team_IN = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance
      [int]$team_IN_Count = $team_IN.Column1

      #==This Year Count
      $sqlquery = "SELECT COUNT ([Incident #])
      FROM [SDE_PROD].[_SMDBA_].[Incident]
      WHERE ([Close Date & Time] between '$Currentyear/01/01' and '$Currentyear/12/31' AND [Group Name] = '"+($team.Group)+"')"

      $Closed_This_Year_IN = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance

      $sqlquery = "SELECT COUNT ([Work Order #])
      FROM [SDE_PROD].[_SMDBA_].[Work Orders]
      WHERE ([Close Date & Time] between '$Currentyear/01/01' and '$Currentyear/12/31' AND [Group Name] = '"+($team.Group)+"')"

      $Closed_This_Year_WO = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance

      [int]$Closed_This_Year_Count = $Closed_This_Year_IN.Column1 + $Closed_This_Year_WO.Column1

      #==Last Year Count
      $sqlquery = "SELECT COUNT ([Incident #])
      FROM [SDE_PROD].[_SMDBA_].[Incident]
      WHERE ([Close Date & Time] between '$Lastyear/01/01' and '$Lastyear/12/31' AND [Group Name] = '"+($team.Group)+"')"

      $Closed_Last_Year_IN = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance

      $sqlquery = "SELECT COUNT ([Work Order #])
      FROM [SDE_PROD].[_SMDBA_].[Work Orders]
      WHERE ([Close Date & Time] between '$Lastyear/01/01' and '$Lastyear/12/31' AND [Group Name] = '"+($team.Group)+"')"

      $Closed_Last_Year_WO = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance

      [int]$Closed_Last_Year_Count = $Closed_Last_Year_IN.Column1 + $Closed_Last_Year_WO.Column1

      #==Last Year to This Day Count
      $sqlquery = "SELECT COUNT ([Incident #])
      FROM [SDE_PROD].[_SMDBA_].[Incident]
      WHERE ([Close Date & Time] between '$Lastyear/01/01' and '$LastYear/$Currentdate' AND [Group Name] = '"+($team.Group)+"')"

      $Closed_Last_Year_To_This_Day_IN = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance

      $sqlquery = "SELECT COUNT ([Work Order #])
      FROM [SDE_PROD].[_SMDBA_].[Work Orders]
      WHERE ([Close Date & Time] between '$Lastyear/01/01' and '$LastYear/$Currentdate' AND [Group Name] = '"+($team.Group)+"')"

      $Closed_Last_Year_To_This_Day_WO = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance

      [int]$Closed_Last_Year_To_This_Day_Count = $Closed_Last_Year_To_This_Day_IN.Column1 + $Closed_Last_Year_To_This_Day_WO.Column1

      #==Tickets Open Exactly a year ago
      $sqlquery = "SELECT COUNT ([Incident #])
      FROM [SDE_PROD].[_SMDBA_].[Incident]
      WHERE ([Open Date & Time] <= '$LastYear/$Currentdate' AND [Close Date & Time] > '$LastYear/$Currentdate' AND [Group Name] = '"+($team.Group)+"')"

      $Open_Exactly_A_Year_Ago_IN = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance

      $sqlquery = "SELECT COUNT ([Work Order #])
      FROM [SDE_PROD].[_SMDBA_].[Work Orders]
      WHERE ([Open Date & Time] <= '$LastYear/$Currentdate' AND [Close Date & Time] > '$LastYear/$Currentdate' AND [Group Name] = '"+($team.Group)+"')"

      $Open_Exactly_A_Year_Ago_WO = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance

      [int]$Open_Exactly_A_Year_Ago_Count = $Open_Exactly_A_Year_Ago_IN.Column1 + $Open_Exactly_A_Year_Ago_WO.Column1

      $output = [PSCustomObject] @{
        'Team' = $team.Description;
        'SDE Group' = $team.Group;
        "Closed Total $Lastyear" = $Closed_Last_Year_Count; 
        "Closed So Far $Lastyear" = $Closed_Last_Year_To_This_Day_Count;
        "Closed So Far $Currentyear" = $Closed_This_Year_Count;
        #'Open Work Orders' = $team_WO_Count;
        #'Open Incidents' = $team_IN_Count;
        "# Open This Day in $Lastyear" = $Open_Exactly_A_Year_Ago_Count;
        'Open Today' = $team_WO_Count+$team_IN_Count;
        }

        $Array+=$output

    }
    
    #Array3
    $output = [PSCustomObject] @{
    'Message' = "This is a look at all of the open tickets and closed ticket trends amongst the other teams. For reference, your SDE Group is $GroupName. This list is sorted by Open Now";
    'Title' = "SDE Group Trends";
    'Color' = $Color;
    'Array' = $Array | Sort-Object -Descending -Property "Open Today";
    }
    if ($output.Array -ne $NULL){return $output}
}

PowerShell Module: New-DGMSDETeamTicketHistory2.psm1


<#
.SYNOPSIS
  Generates New-DMGSDETeamTicketHistory
.NOTES
  Version:        2.0
  Author:         David Maiolo
  Creation Date:  2018-01-26
  Purpose/Change: Initial script development

#>

#---------------------------------------------------------[Initialisations]--------------------------------------------------------

function New-DMGSDETeamTicketHistory2{
param(
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
        [ValidateSet("SDEGROUP1","SDEGROUP2","SDEGROUP3","SDEGROUP4","SDEGROUP5","SDEGROUP6","SDEGROUP7","SDEGROUP8","SDEGROUP9","SDEGROUP10")]
        [String]$GroupName,
        [Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$true)]
        [String]$GroupDescription,
        [Parameter(Position=2,Mandatory=$false,ValueFromPipeline=$true)]
        [Int]$NumOfMonthsToShowTrends = 7,
        [Parameter(Position=3,Mandatory=$false,ValueFromPipeline=$true)]
        [ValidateSet("Black","White","Red","Lime","Blue","Yellow","Cyan","Magenta","Silver","Gray","Maroon","Olive","Green","Purple","Teal","Navy")]
        [String]$Color = "Blue",
        [Parameter(Position=4,Mandatory=$false,ValueFromPipeline=$true)]
        $ServerInstance = "SDESQLSERVER"
    )
    
    $OutputArrays =@()
    $sqlquery = $null
    $Array = $null

    $Currentyear = get-date -Format yyyy
    $Currentdate = get-date -Format MM/dd
    $Lastyear = ($Currentyear-1)

    #========================================================================================
    #SQL Query To Find Ticket History
    #========================================================================================
    $Array = @()
    
    #Create Array Of All The Dates
    $Dates = @()
    $i=$NumOfMonthsToShowTrends
    while ($i -ge 0){
        
        <#
        $Date=(Get-Date).AddMonths(-($i)).ToString('yyyy/MM/dd')
        $DateMinusOne = (Get-Date).AddMonths(-($i+1)).ToString('yyyy/MM/dd')
        #>

        $CurrentDate=(Get-Date).AddMonths(-($i)).ToString('MM/dd/yyyy')

        $FirstDayOfMonth=GET-DATE $CurrentDate -Day 1
        $LastDayOfMonth=GET-DATE $FirstDayOfMonth.AddMonths(1).AddSeconds(-1)

        $Year = $FirstDayOfMonth.ToString('yy')
        $Month = $FirstDayOfMonth.ToString('MM')

        $Date = $LastDayOfMonth.ToString('yyyy/MM/dd')
        $DateMinusOne = $FirstDayOfMonth.ToString('yyyy/MM/dd')

        $output = [PSCustomObject] @{
        'Date' = $Date;
        'DateMinusOne' = $DateMinusOne;
        'Month' = $Month;
        'Year' = $Year;
        }

        $Dates+=$output
        $i--
    }

    foreach ($DateToProcess in $Dates){
        
        $Date = $DateToProcess.Date
        $DateMinusOne = $DateToProcess.DateMinusOne
        $Month = $DateToProcess.Month
        $Month = (Get-Culture).DateTimeFormat.GetMonthName($Month)
        $Year = $DateToProcess.Year

        <#
        Write-Host "Processing..."
        Write-Host "Date: $Date"
        Write-Host "DateMinusOne: $DateMinusOne"
        Write-Host "Month: $Month"
        Write-Host "Year: $Year"
        #>

        #Incidents Opened
        $sqlquery = "SELECT COUNT ([Incident #])
        FROM [SDE_PROD].[_SMDBA_].[Incident]
        WHERE ([Open Date & Time] between '$DateMinusOne' and '$Date' AND [Group Name] = '$GroupName')"
        $IncidentsOpened = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance
        [int]$IncidentsOpenedCount = $IncidentsOpened.Column1

        #Work Orders Opened
        $sqlquery = "SELECT COUNT ([Work Order #])
        FROM [SDE_PROD].[_SMDBA_].[Work Orders]
        WHERE ([Open Date & Time] between '$DateMinusOne' and '$Date' AND [Group Name] = '$GroupName')"
        $WorkOrdersOpened = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance
        [int]$WorkOrdersOpenedCount = $WorkOrdersOpened.Column1

        #Total Opened
        [int]$TotalOpenedCount = $IncidentsOpenedCount+$WorkOrdersOpenedCount

        #Incidents Closed
        $sqlquery = "SELECT COUNT ([Incident #])
        FROM [SDE_PROD].[_SMDBA_].[Incident]
        WHERE ([Close Date & Time] between '$DateMinusOne' and '$Date' AND [Group Name] = '$GroupName')"
        $IncidentsClosed = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance
        [int]$IncidentsClosedCount = $IncidentsClosed.Column1

        #Work Orders Closed
        $sqlquery = "SELECT COUNT ([Work Order #])
        FROM [SDE_PROD].[_SMDBA_].[Work Orders]
        WHERE ([Close Date & Time] between '$DateMinusOne' and '$Date' AND [Group Name] = '$GroupName')"
        $WorkOrdersClosed = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance
        [int]$WorkOrdersClosedCount = $WorkOrdersClosed.Column1

        #Total Closed
        [int]$TotalClosedCount = $IncidentsClosedCount+$WorkOrdersClosedCount

        #Ratio Opened to Closed
        if($TotalOpenedCount -gt 0){
            $RatioOpenedtoClosedCount = ([math]::Round((($TotalClosedCount)/($TotalOpenedCount)),2))
        }else{
            $RatioOpenedtoClosedCount = 0
        }

        $output = [PSCustomObject] @{
        'Month' = $Month+" `'"+$Year;
        #'Incidents Opened' = $IncidentsOpenedCount;
        #'Work Orders Opened' = $WorkOrdersOpenedCount;
        "Total Opened" = $TotalOpenedCount; 
        #"Incidents Closed" = $IncidentsClosedCount;
        #"Work Orders Closed" = $WorkOrdersClosedCount;
        "Total Closed" = $TotalClosedCount;
        'Ratio Opened to Closed' = $RatioOpenedtoClosedCount;
        }

        $Array+=$output

    }

    #Array4
    $output = [PSCustomObject] @{
    'Message' = "This is a look at the ticket trends for this group over the past $NumOfMonthsToShowTrends months. Ratios greater than 1.0 mean more tickets were closed than were opened, whereas those lower than 1.0 mean more tickets were opened than closed. ";
    'Title' = "$GroupDescription`: Open / Close Ratios";
    'Color' = $Color;
    'Array' = $Array;
    }
    if ($output.Array -ne $NULL){return $output}
}

PowerShell Module: New-DGMSDETeamUserHistory.psm1


<#
.SYNOPSIS
  Generates New-DMGSDEOpenIncidents
.NOTES
  Version:        1.0
  Author:         David Maiolo
  Creation Date:  2018-01-22
  Purpose/Change: Initial script development

#>

#---------------------------------------------------------[Initialisations]--------------------------------------------------------

function New-DMGSDETeamUserHistory{
param(
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
        [ValidateSet("SDEGROUP1","SDEGROUP2","SDEGROUP3","SDEGROUP4","SDEGROUP5","SDEGROUP6","SDEGROUP7","SDEGROUP8","SDEGROUP9","SDEGROUP10")]
        [String]$GroupName,
        [Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$true)]
        [String]$GroupDescription,
        [Parameter(Position=2,Mandatory=$false,ValueFromPipeline=$true)]
        [Int]$NumOfMonthsToShowTrends = 7,
        [Parameter(Position=3,Mandatory=$false,ValueFromPipeline=$true)]
        [ValidateSet("Black","White","Red","Lime","Blue","Yellow","Cyan","Magenta","Silver","Gray","Maroon","Olive","Green","Purple","Teal","Navy")]
        [String]$Color = "Blue",
        [Parameter(Position=4,Mandatory=$false,ValueFromPipeline=$true)]
        $ServerInstance = "SDESQLSERVER"
    )
    
    $OutputArrays =@()
    $sqlquery = $null
    $Array = $null

    #Compute Month 0 Variables
    $i=0
    $CurrentDate0=(Get-Date).AddMonths(-($i)).ToString('MM/dd/yyyy')

    $FirstDayOfMonth0=GET-DATE $CurrentDate0 -Day 1
    $LastDayOfMonth0=GET-DATE $FirstDayOfMonth0.AddMonths(1).AddSeconds(-1)

    $Year0 = $FirstDayOfMonth0.ToString('yy')
    $Month0 = $FirstDayOfMonth0.ToString('MM')
    $Month0 = (Get-Culture).DateTimeFormat.GetMonthName($Month0)

    $Date0 = $LastDayOfMonth0.ToString('yyyy/MM/dd')
    $DateMinusOne0 = $FirstDayOfMonth0.ToString('yyyy/MM/dd')

    #Compute Month 1 Variables
    $i=1
    $CurrentDate1=(Get-Date).AddMonths(-($i)).ToString('MM/dd/yyyy')

    $FirstDayOfMonth1=GET-DATE $CurrentDate1 -Day 1
    $LastDayOfMonth1=GET-DATE $FirstDayOfMonth1.AddMonths(1).AddSeconds(-1)

    $Year1 = $FirstDayOfMonth1.ToString('yyy')
    $Month1 = $FirstDayOfMonth1.ToString('MM')
    $Month1 = (Get-Culture).DateTimeFormat.GetMonthName($Month1)

    $Date1 = $LastDayOfMonth1.ToString('yyyy/MM/dd')
    $DateMinusOne1 = $FirstDayOfMonth1.ToString('yyyy/MM/dd')

    #Compute Month 2 Variables
    $i=2
    $CurrentDate2=(Get-Date).AddMonths(-($i)).ToString('MM/dd/yyyy')

    $FirstDayOfMonth2=GET-DATE $CurrentDate2 -Day 1
    $LastDayOfMonth2=GET-DATE $FirstDayOfMonth2.AddMonths(1).AddSeconds(-1)

    $Year2 = $FirstDayOfMonth2.ToString('yyyy')
    $Month2 = $FirstDayOfMonth2.ToString('MM')
    $Month2 = (Get-Culture).DateTimeFormat.GetMonthName($Month2)

    $Date2 = $LastDayOfMonth2.ToString('yyyy/MM/dd')
    $DateMinusOne2 = $FirstDayOfMonth2.ToString('yyyy/MM/dd')

    #Compute Month 3 Variables
    $i=3
    $CurrentDate3=(Get-Date).AddMonths(-($i)).ToString('MM/dd/yyyy')

    $FirstDayOfMonth3=GET-DATE $CurrentDate3 -Day 1
    $LastDayOfMonth3=GET-DATE $FirstDayOfMonth3.AddMonths(1).AddSeconds(-1)

    $Year3 = $FirstDayOfMonth3.ToString('yyyy')
    $Month3 = $FirstDayOfMonth3.ToString('MM')
    $Month3 = (Get-Culture).DateTimeFormat.GetMonthName($Month3)

    $Date3 = $LastDayOfMonth3.ToString('yyyy/MM/dd')
    $DateMinusOne3 = $FirstDayOfMonth3.ToString('yyyy/MM/dd')

    #========================================================================================
    #SQL Query To Find User Trends
    #========================================================================================
    $Array = @()

    $sqlquery = "SELECT [Support Staff ID]
      ,[Full Name]
      FROM [SDE_PROD].[_SMDBA_].[Groups Details]
      WHERE ([Group ID] = '$GroupName')"

    $TeamMmbers = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance


    foreach ($TeamMmber in $TeamMmbers){

        #Query For Incidents Open
        $sqlquery = "SELECT COUNT ([Incident #])
          FROM [SDE_PROD].[_SMDBA_].[Incident]
          WHERE ([Login ID Assigned To] = '"+($TeamMmber."Support Staff ID")+"' AND [Status ID:] != 'Closed' AND [Status ID:] != 'ADMIN_CLOSE' AND [Status ID:] != 'CANCEL' AND [Status ID:] != 'REJECT')"

        $TotalIncidentsOpen = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance

        #Query For Work Orders Open
        $sqlquery = "SELECT COUNT ([Work Order #])
          FROM [SDE_PROD].[_SMDBA_].[Work Orders]
          WHERE ([Login ID Assigned To] = '"+($TeamMmber."Support Staff ID")+"' AND [Status ID:] != 'Closed' AND [Status ID:] != 'ADMIN_CLOSE' AND [Status ID:] != 'CANCEL' AND [Status ID:] != 'REJECT')"

        $TotalWorkOrdersOpen = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance

        #=====
        #Query For Incidents Closed
        $sqlquery = "SELECT COUNT ([Incident #])
          FROM [SDE_PROD].[_SMDBA_].[Incident]
          WHERE ([Login ID Assigned To] = '"+($TeamMmber."Support Staff ID")+"' AND ([Status ID:] = 'Closed' OR [Status ID:] = 'ADMIN_CLOSE'))"

        $TotalIncidentsClosed = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance

        #Query For Work Orders Closed
        $sqlquery = "SELECT COUNT ([Work Order #])
          FROM [SDE_PROD].[_SMDBA_].[Work Orders]
          WHERE ([Login ID Assigned To] = '"+($TeamMmber."Support Staff ID")+"' AND ([Status ID:] = 'Closed' OR [Status ID:] = 'ADMIN_CLOSE'))"

        $TotalWorkOrdersClosed = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance

        #Query For Work Orders Closed At Date0
        $sqlquery = "SELECT COUNT ([Work Order #])
            FROM [SDE_PROD].[_SMDBA_].[Work Orders]
            WHERE ([Close Date & Time] between '$DateMinusOne0' and '$Date0' AND [Login ID Assigned To] = '"+($TeamMmber."Support Staff ID")+"')"

        $TotalWorkOrdersClosedAtDate0 = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance

        #Query For Incidents Closed At Date0
        $sqlquery = "SELECT COUNT ([Incident #])
            FROM [SDE_PROD].[_SMDBA_].[Incident]
            WHERE ([Close Date & Time] between '$DateMinusOne0' and '$Date0' AND [Login ID Assigned To] = '"+($TeamMmber."Support Staff ID")+"')"

        $TotalIncidentsClosedAtDate0 = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance

        #Query For Work Orders Closed At Date1
        $sqlquery = "SELECT COUNT ([Work Order #])
            FROM [SDE_PROD].[_SMDBA_].[Work Orders]
            WHERE ([Close Date & Time] between '$DateMinusOne1' and '$Date1' AND [Login ID Assigned To] = '"+($TeamMmber."Support Staff ID")+"')"

        $TotalWorkOrdersClosedAtDate1 = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance

        #Query For Incidents Closed At Date1
        $sqlquery = "SELECT COUNT ([Incident #])
            FROM [SDE_PROD].[_SMDBA_].[Incident]
            WHERE ([Close Date & Time] between '$DateMinusOne1' and '$Date1' AND [Login ID Assigned To] = '"+($TeamMmber."Support Staff ID")+"')"

        $TotalIncidentsClosedAtDate1 = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance

         #Query For Work Orders Closed At Date2
        $sqlquery = "SELECT COUNT ([Work Order #])
            FROM [SDE_PROD].[_SMDBA_].[Work Orders]
            WHERE ([Close Date & Time] between '$DateMinusOne2' and '$Date2' AND [Login ID Assigned To] = '"+($TeamMmber."Support Staff ID")+"')"

        $TotalWorkOrdersClosedAtDate2 = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance

        #Query For Incidents Closed At Date2
        $sqlquery = "SELECT COUNT ([Incident #])
            FROM [SDE_PROD].[_SMDBA_].[Incident]
            WHERE ([Close Date & Time] between '$DateMinusOne2' and '$Date2' AND [Login ID Assigned To] = '"+($TeamMmber."Support Staff ID")+"')"

        $TotalIncidentsClosedAtDate2 = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance

         #Query For Work Orders Closed At Date3
        $sqlquery = "SELECT COUNT ([Work Order #])
            FROM [SDE_PROD].[_SMDBA_].[Work Orders]
            WHERE ([Close Date & Time] between '$DateMinusOne3' and '$Date3' AND [Login ID Assigned To] = '"+($TeamMmber."Support Staff ID")+"')"

        $TotalWorkOrdersClosedAtDate3 = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance

        #Query For Incidents Closed At Date3
        $sqlquery = "SELECT COUNT ([Incident #])
            FROM [SDE_PROD].[_SMDBA_].[Incident]
            WHERE ([Close Date & Time] between '$DateMinusOne3' and '$Date3' AND [Login ID Assigned To] = '"+($TeamMmber."Support Staff ID")+"')"

        $TotalIncidentsClosedAtDate3 = Invoke-Sqlcmd -Query $sqlquery -ServerInstance $ServerInstance


        $output = [PSCustomObject] @{
            'Team Member' = ($TeamMmber."Full Name");
            #'Incidents Closed' = ($TotalIncidentsClosed.Column1);
            #'Work Orders Closed' = ($TotalWorkOrdersClosed.Column1);
            "Closed $Month3 $Year3" = (($TotalWorkOrdersClosedAtDate3.Column1)+($TotalIncidentsClosedAtDate3.Column1))
            "Closed $Month2 $Year2" = (($TotalWorkOrdersClosedAtDate2.Column1)+($TotalIncidentsClosedAtDate2.Column1))
            "Closed $Month1 $Year1" = (($TotalWorkOrdersClosedAtDate1.Column1)+($TotalIncidentsClosedAtDate1.Column1))
            "Closed $Month0 $Year0" = (($TotalWorkOrdersClosedAtDate0.Column1)+($TotalIncidentsClosedAtDate0.Column1))
            #'Open Incidents' = ($TotalIncidentsOpen.Column1);
            #'Open Work orders' = ($TotalWorkOrdersOpen.Column1);
            'Total Closed' = (($TotalWorkOrdersClosed.Column1)+($TotalIncidentsClosed.Column1));
            'Currently Assigned' = (($TotalIncidentsOpen.Column1)+($TotalWorkOrdersOpen.Column1));
        }
        $Array+=$output
    }

    #Array5
    $output = [PSCustomObject] @{
    'Message' = "This is a look at the user trends over the entire SDE timeline.";
    'Title' = "$GroupDescription`: User Trends";
    'Color' = $Color;
    'Array' = $Array | Sort-Object -Property "Team Member";
    }
    if ($output.Array -ne $NULL){return $output}
}

Overview

I developed this tool, Get-DMGThreadedPingableComputers.ps1, to allow you to ping several computers in your environment at once though multiple threads, severely decreasing the amount of time required to return ping results on a large set of computers. What’s neat about it is you can easily get ping results on hundreds of computers in just seconds.

How To Use This Tool

You can either search directly for computers within the script, pass some computers through on the pipeline or import a .CSV file. You can also export results to a .CSV file.

Pass a list of computers and export results to CSV:


  Get-ADComputer -filter {name -like "*sql*"} | Get-DGMThreadedPingableComputers -csvoutput "C:\Scripts\Get-DGMThreadedPingableComputer\Get-DGMThreadedPingableComputer-output.csv" 

Pass a list of computers (with example output):


  Get-ADComputer -filter {name -like "*DGM06123*"} -Properties Description,OperatingSystem | Get-DGMThreadedPingableComputers 

No Arguments triggers searching for computers within the script (with example output):


  Get-DGMThreadedPingableComputers 

PowerShell Script: Get-DMGThreadedPingableComputers.ps1


<#
.Synopsis
   Threads and threads
.DESCRIPTION
   The tool, Get-DMGThreadedPingableComputers.ps1  was written by David Maiolo.
.EXAMPLE
   Get-DMGThreadedPingableComputers -CSVFile laps_computers_import.csv
#>

function Where-ParallelObject {
    param(
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)] $input,
        [ScriptBlock] $Filter,
        [int] $threads,
        [switch] $progressBar,
        [String] $progressBartext
    )

    $inputQueue = [System.Collections.Queue]::Synchronized( (New-Object System.Collections.Queue) )
    $results = [System.Collections.Queue]::Synchronized( (New-Object System.Collections.Queue) )

    $sessionstate = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
    $sessionstate.Variables.Add(
        (New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry('inputQueue', $inputQueue, $null))
    )
    $sessionstate.Variables.Add(
        (New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry('results', $results, $null))
    )

    $runspacepool = [runspacefactory]::CreateRunspacePool(1, $threads, $sessionstate, $Host)
    $runspacepool.Open()

    foreach ($object in $input) {
        $inputQueue.Enqueue($object)
    }

    $jobs = @()

    $sbpre = '
        while($inputQueue.Count -gt 0) {
            $_ = $inputQueue.Dequeue();
            if('
    $sbpost = ') 
            {
                $results.Enqueue($_);    
            }
        }
    '

    $sb = [ScriptBlock]::Create($sbpre + $Filter.toString() + $sbpost)

    1..$threads | % {
        $job = [PowerShell]::Create().AddScript($sb)
        $job.RunspacePool = $runspacepool
        $jobs += New-Object PSObject -Property @{
            Job = $job
            Result = $job.BeginInvoke()
        }
    }

    do {
        if($progressBar.IsPresent) 
        {
            Write-Progress -Activity ($progressBartext+" " +$input.Count+ " Objects") -status ("" + $($results.Count) + " complete.") -percentComplete ( ($results.Count) / $input.Count * 100) 
        }
        Start-Sleep -Seconds 1
    } while ( $jobs.Result.IsCompleted -contains $false)

    foreach ($job in $jobs) {
        $job.Job.EndInvoke($job.Result)
    }
    $runspacepool.Close()
    $runspacepool.Dispose()

    return $results.ToArray()

}

function LogIt
{
  param (
  [Parameter(Mandatory=$true)]
  $message,
  [Parameter(Mandatory=$true)]
  $component,
  [Parameter(Mandatory=$true)]
  $type )

  switch ($type)
  {
    1 { $type = "Info" }
    2 { $type = "Warning" }
    3 { $type = "Error" }
    4 { $type = "Verbose" }
  }

  if (($type -eq "Verbose") -and ($Global:Verbose))
  {
    $toLog = "{0} `$$<{1}><{2} {3}>" -f ($type + ":" + $message), ($Global:ScriptName + ":" + $component), (Get-Date -Format "MM-dd-yyyy"), (Get-Date -Format "HH:mm:ss.ffffff"), $pid
    $toLog | Out-File -Append -Encoding UTF8 -FilePath ("filesystem::{0}" -f $Global:LogFile)
    Write-Host $message
  }
  elseif ($type -ne "Verbose")
  {
    $toLog = "{0} `$$<{1}><{2} {3}>" -f ($type + ":" + $message), ($Global:ScriptName + ":" + $component), (Get-Date -Format "MM-dd-yyyy"), (Get-Date -Format "HH:mm:ss.ffffff"), $pid
    $toLog | Out-File -Append -Encoding UTF8 -FilePath ("filesystem::{0}" -f $Global:LogFile)
    if ($type -eq 'Info') { Write-Host $message }
    if ($type -eq 'Warning') { Write-Host $message -ForegroundColor Yellow}
    if ($type -eq 'Error') { Write-Host $message -ForegroundColor Red}
    

  }
  if (($type -eq 'Warning') -and ($Global:ScriptStatus -ne 'Error')) { $Global:ScriptStatus = $type }
  if ($type -eq 'Error') { $Global:ScriptStatus = $type }

  if ((Get-Item $Global:LogFile).Length/1KB -gt $Global:MaxLogSizeInKB)
  {
    $log = $Global:LogFile
    Remove-Item ($log.Replace(".log", ".lo_"))
    Rename-Item $Global:LogFile ($log.Replace(".log", ".lo_")) -Force
  }
} 

function GetScriptDirectory
{
  $invocation = (Get-Variable MyInvocation -Scope 1).Value
  Split-Path $invocation.MyCommand.Path
} 

function Get-DMGThreadedPingableComputers {

    param(
        [Parameter(Position=0,Mandatory=$false,ValueFromPipeline=$true)] $input,
        [ValidateScript({(Test-Path $_)})]
        [String] $csvinput,
        [ValidateScript({($_ -le 100 -and $_ -gt 0)})]
        [int] $threads=100,
        [String] $csvoutput
    )

        $path = (get-item -Path .).FullName
        $arraytoping=@()

        #Header
        Write-Host "==========================================" -ForegroundColor Cyan
        Write-Host "Ping Computers in Threads " -ForegroundColor Cyan
        Write-Host "v0.1 (2017-12-28) by dmaiolo" -ForegroundColor Cyan
        Write-Host "Threading function by jreal" -ForegroundColor Cyan
        Write-Host "==========================================" -ForegroundColor Cyan
        LogIt -message ("Starting Logging for $Global:ScriptName") -component "Main()" -type 1

        #Check for what arguments were passed
        if([bool]($MyInvocation.BoundParameters.Keys -match 'csvinput')){
            Write-Host "Importing $csvinput..."
            $csvimport = import-csv $csvinput
            $csvimport | foreach-object {$arraytoping += $_.name; Write-Host "Importing $_.name ..."}       
        }
        elseif([bool]($MyInvocation.BoundParameters.Keys -match 'input')){
            Write-Host "Importing from pipeline..."
            foreach ($object in $input) {
                $arraytoping += $object
            }
        }else{
            Write-Host "Manual Input Selected."
            $arraytoping = Get-DMGSearchQueryComputers
        }
        if([bool]($MyInvocation.BoundParameters.Keys -match 'csvoutput')){
            $csvoutputcheck = $true
        }

        #Ping the computers
        $pingablecomputers = Get-DMGOnlineComputers -ComputerList $arraytoping -Threads $threads

        #Create Pingable Table
        if ($pingablecomputers){
            Write-Host ========================================== -ForegroundColor Cyan
            Write-Host Pingable Computers -ForegroundColor Green
            Write-Host ========================================== -ForegroundColor Cyan
            $pingablecomputers | Select Name,Description,OperatingSystem | Format-Table -AutoSize
        }else{
            Write-Host "No Pingable Computers Were Found."
        }

        #Create Non-Pingable Array
        $nonpingablecomputers = $arraytoping | where {$pingablecomputers -notcontains $_}   

        #Create Non-Pingable Table
        if ($nonpingablecomputers){
            Write-Host ========================================== -ForegroundColor Cyan
            Write-Host Non Pingable Computers -ForegroundColor Red
            Write-Host ========================================== -ForegroundColor Cyan
            $nonpingablecomputers | Select Name,Description,OperatingSystem | Format-Table -AutoSize
        }else{
            Write-Host "No Non-Pingable Computers Were Found."
        }

        #Export to CSV if chosen
        if ($csvoutputcheck){
            Write-Host ========================================== -ForegroundColor Cyan
            Write-Host CSV Output Results -ForegroundColor Cyan
            Write-Host ========================================== -ForegroundColor Cyan
            
            #Build the array
            $results = @()
            $pingablecomputers | select Name,Description,OperatingSystem,@{Name='Pingable';Expression={"True"}}  | %{$results += $_ }
            $nonpingablecomputers | select Name,Description,OperatingSystem,@{Name='Pingable';Expression={"False"}} | %{$results += $_ }

            New-DMGCSVOut -csvoutputpath $csvoutput -arrayforoutput $results 
        }


        #Footer
        Write-Host ========================================== -ForegroundColor Cyan
        Write-Host "Log File of Results Generated" -ForegroundColor Cyan
        Write-Host ========================================== -ForegroundColor Cyan
        Write-Host "Log File`: $Global:LogFile VIEW WITH CMTRACE.EXE"
        LogIt -message ("Ending Logging for $Global:ScriptName") -component "Main()" -type 1

}

function Get-DMGSearchQueryComputers
{
    Write-Host "[1] Search by whole or partial computer name"
    Write-Host "[2] Search by description"
    Write-Host "[3] Search by Operating System"
    do {
        try {$numOk = $true; [int]$GetMyANumber = Read-host "Selection"}
        catch {$numOK = $false}}
    until (($GetMyANumber -ge 1 -and $GetMyANumber -le 3) -and $numOK)

    $validcharacters = "^[a-zA-Z0-9\s]+$"
    do {
        try {$stringOk = $true; [string]$query = Read-host "Enter search query (only letters and numbers)"}
        catch {$stringOk = $false}}
    until (($query -match $validcharacters) -and $stringOk)
    $query = "*"+$query
    $query = $query+"*"

    switch ($GetMyANumber) 
        { 
            1 {$computers = Get-ADComputer -Properties Description,OperatingSystem -filter {name -like $query}} 
            2 {$computers = Get-ADComputer -Properties Description,OperatingSystem -filter {description -like $query}} 
            3 {$computers = Get-ADComputer -Properties Description,OperatingSystem -filter {OperatingSystem -like $query}}
        }

    return $computers
}

function Get-DMGOnlineComputers{

    param(
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
        [int] $threads,
        [Array] $ComputerList
    )
    
    Write-Host ========================================== -ForegroundColor Cyan
    Write-Host Pinging Computers and Building Table -ForegroundColor Cyan
    Write-Host ========================================== -ForegroundColor Cyan
    
    $computers = @()

    if ($ComputerList.Length -gt 0){
        Write-Host "Pinging"($ComputerList.length)"computers in $threads threads."
        $computers = $ComputerList | Where-ParallelObject -Filter {Test-Connection -ComputerName $_.Name -Quiet -Count 1} -Threads $threads -ProgressBar -progressBartext "Pinging"
        <#$computers | foreach-object {
            LogIt -message ("ICMP Response Succesful`: " +($_.Name).ToString() +" - "+($_.Description).ToString()) -component "Main()" -type 1
            }#>
        return $computers
    }else{
        Write-Host "No Computers were found."
        return $false
    }
}

function New-DMGCSVOut{

    param(
            [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
            [String] $csvoutputpath,
            [array] $arrayforoutput
        )

    try{
        $arrayforoutput | export-csv $csvoutputpath -notypeinformation
        LogIt -message ("CSV Export`: CSV Created at $csvoutputpath") -component "Main()" -type 1
    }catch{
        LogIt -message ("CSV Export`: CSV Could NOT be Created at $csvoutputpath") -component "Main()" -type 3
    }


}

$VerboseLogging = "true"
[bool]$Global:Verbose = [System.Convert]::ToBoolean($VerboseLogging)
$Global:LogFile = Join-Path (GetScriptDirectory) "'Get-DMGThreadedPingableComputer.log"
$Global:MaxLogSizeInKB = 10240
$Global:ScriptName = 'Get-DMGThreadedPingableComputer.ps1' 
$Global:ScriptStatus = 'Success'

Overview

I created this series of functions to allow you to conveniently take one or more PowerShell object and have it turn into an email report. For example, if you had a list of computers you have come out of a PowerShell script you wrote, these functions could help you turn them into an email with the same data without much effort.

Create an Email with A Simple Array of Data: EASY

Import-Module \\scriptserver\scripts\Get-DGMEmailReport\Get-DGMEmailReport.psm1 -Force

Import-Module \\pathtoscriptfile\Get-DGMEmailReport.psm1 -Force


$myarray = (Get-ADComputer -properties Description -filter {description -like "*sql*"} | Select Name,Description)

#Single Array Email Report
Get-DGMEmailReport `
    -Array $myarray `
    -ArrayTitle "SQL Servers" `
    -ArrayTitleColor "Blue" `
    -ArrayMessage "These are all the SQL Servers" `
    -ReportTitle "SQL Report 2017" `
    -from "SQLReports@emailaddress.com" `
    -To "c-dmaiolo@emailaddress.com" `
    -subject "SQL Test Report"  

Create an Email With Multiple Arrays of Data: MODERATE DIFFICULTY



Import-Module \\pathtoscriptfile\Get-DGMEmailReport.psm1 -Force 

#Create Some Arrays Of Data To Display in Report. You can create as many as you want.
$OutputArrays = @()
   
#Array1
$output = [PSCustomObject] @{
'Message' = "These are deployments older than 90 days, with lower than an 80% success rate where 100 or more computers were targeted.";
'Title' = "SCCM Problem Deployments";
'Color' = "Red";
'Array' = Get-DGMSCCMProblemDeploymentsArray -Days 90 -PercentSuccessThreshold .8 -NumberOfTargetedThreshold 100;
}

$OutputArrays+=$output

#Array2
$output = [PSCustomObject] @{
'Message' = "These are all deployments that were deployed over a year ago.";
'Title' = "SCCM Deployments Greater Than 1 Year Old";
'Color' = "Green";
'Array' = Get-DGMSCCMProblemDeploymentsArray -Days 365 -PercentSuccessThreshold 1 -NumberOfTargetedThreshold 0;
}
    
$OutputArrays+=$output
    
#Array3
$output = [PSCustomObject] @{
'Message' = "These are all our SQL Servers.";
'Title' = "SQL Servers";
'Array' = Get-ADComputer -properties Description -filter {description -like "*sql*"} | Select Name,Description
}

$OutputArrays+=$output 

#Multiple Arrays Email Report
Get-DGMEmailReport `
    -Arrays $OutputArrays `
    -ReportTitle "SQL Report 2017" `
    -from "SQLReports@emailaddress.com" `
    -To "c-dmaiolo@emailaddress.com" `
    -subject "SQL Test Report" 

Sample Single Array Output

cid:image002.jpg@01D38FBC.5C3028C0

Sample Multiple Array Output Email

cid:image003.jpg@01D38FBC.5C3028C0

PowerShell Functions


<#
.SYNOPSIS
  Generates an Email Report
.NOTES
  Version:        1.0
  Author:         David Maiolo
  Creation Date:  2018-01-02
  Purpose/Change: Initial script development

#>

#---------------------------------------------------------[Initialisations]--------------------------------------------------------

Import-Module \\scriptserver\scripts\DMGSCCM\New-DMGCMTraceLog\New-DMGCMTraceLog.psm1 -Force

function New-DMGCombinedHTMLTable{
 param(
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
        $DMGCombinedHTMLTable
    )
    $CombinedHTML = ""
    
    foreach ($object in $DMGCombinedHTMLTable){

            $CombinedHTML+= "
" if ($object.Title){ if ($object.Color){ $CombinedHTML+= "" #$CombinedHTML+= "

"+($object.Title)+"

" $CombinedHTML+= "

"+($object.Title)+"

" }else{ $CombinedHTML+= "

"+($object.Title)+"

" } } if($object.Message){ $CombinedHTML+= "

➥ "+($object.Message)+"

" } if ($object.Array){ $CombinedHTML+= $object.Array | ConvertTo-Html } $CombinedHTML+= "
" } return $CombinedHTML } function New-DMGHTMLTable{ param( [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)] $Array, [Parameter(Position=1,Mandatory=$false,ValueFromPipeline=$true)] $Title, [Parameter(Position=1,Mandatory=$false,ValueFromPipeline=$true)] $Message, [Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$true)] [ValidateSet("Black","White","Red","Lime","Blue","Yellow","Cyan","Magenta","Silver","Gray","Maroon","Olive","Green","Purple","Teal","Navy")] $Color ) $HTML = "" $HTML+= "
" if([bool]($MyInvocation.BoundParameters.Keys -match 'Title')){ if([bool]($MyInvocation.BoundParameters.Keys -match 'Color')){ $HTML+= "" } $HTML+= "

"+($Title)+"

" } if([bool]($MyInvocation.BoundParameters.Keys -match 'Message')){ $HTML+= "

➥ "+($Message)+"

" } if([bool]($MyInvocation.BoundParameters.Keys -match 'Array')){ $HTML+= $Array | ConvertTo-Html } $HTML+= "
" return $HTML } function New-DMGEmailReport{ param( [Parameter(Position=0,Mandatory=$false,ValueFromPipeline=$true,ParameterSetName='Multiple Arrays')] $Arrays, [Parameter(Position=1,Mandatory=$false,ValueFromPipeline=$true,ParameterSetName='Single Array')] $Array, [Parameter(Position=2,Mandatory=$false,ValueFromPipeline=$true,ParameterSetName='Single Array')] $ArrayMessage, [Parameter(Position=3,Mandatory=$false,ValueFromPipeline=$true,ParameterSetName='Single Array')] $ArrayTitle, [Parameter(Position=9,Mandatory=$false,ValueFromPipeline=$true,ParameterSetName='Single Array')] [ValidateSet("Red","Yellow","Green","Black")] $ArrayTableColor = "Black", [Parameter(Position=4,Mandatory=$true,ValueFromPipeline=$true)] $From, [Parameter(Position=5,Mandatory=$true,ValueFromPipeline=$true)] $To, [Parameter(Position=6,Mandatory=$true,ValueFromPipeline=$true)] $Subject, [Parameter(Position=7,Mandatory=$true,ValueFromPipeline=$true)] $ReportTitle, [Parameter(Position=8,Mandatory=$false,ValueFromPipeline=$true)] $SmtpServer = "mail.emailaddress.com", [Parameter(Position=9,Mandatory=$false,ValueFromPipeline=$true)] [Switch]$AttatchResults, [Parameter(Position=10,Mandatory=$false,ValueFromPipeline=$true)] [Switch]$AttatchSource ) #Set Logging Varibales $invocation = (Get-Variable MyInvocation -Scope 1).Value $ScriptDirectory = Split-Path $invocation.MyCommand.Path $ScriptName = ($MyInvocation.MyCommand.Name)+".psm1" $LogName = ($MyInvocation.MyCommand.Name)+".log" $LogFile = Join-Path $ScriptDirectory $LogName $ScriptFile = Join-Path $ScriptDirectory $ScriptName $ReportDate = Get-Date $WrapperScriptPath = $MyInvocation.PSCommandPath #Map a Drive to UNC Path New-PSDrive -Name UNCPath -PSProvider FileSystem -Root $ScriptDirectory #Log Start of Function New-DMGCMTraceLog -message ("Starting Logging for $ScriptName") -component "Main()" -type 1 -ScriptName $ScriptName -LogFile $LogFile -ScriptFile $ScriptFile #Create Attachments Directory If It Doesn't Exist if (!(Test-Path "UNCPath:\Attachments\")){ New-DMGCMTraceLog -message ("UNCPath:\Attachments\ Not Found. Created.") -component "Main()" -type 1 -ScriptName $ScriptName -LogFile $LogFile -ScriptFile $ScriptFile New-Item "UNCPath:\Attachments\" -ItemType Directory } if([bool]($MyInvocation.BoundParameters.Keys -contains 'Arrays')){ $HTMLTable = New-DMGCombinedHTMLTable -DMGCombinedHTMLTable $Arrays #Log New-DMGCMTraceLog -message ("An Array of Arrays Was Passed") -component "Main()" -type 1 -ScriptName $ScriptName -LogFile $LogFile -ScriptFile $ScriptFile if([bool]($MyInvocation.BoundParameters.Keys -contains 'AttatchResults')){ $attachment = @() foreach ($object in $Arrays){ $AttachmentName = (Remove-DMGInvalidFileNameChars($object.Title))+".csv" $AttachmentFile = Join-Path UNCPath:\Attachments\ $AttachmentName $object.Array | export-csv -path $AttachmentFile -notypeinformation $attachment += $AttachmentFile } New-DMGCMTraceLog -message ("Attatching multiple files: $attachment") -component "Main()" -type 1 -ScriptName $ScriptName -LogFile $LogFile -ScriptFile $ScriptFile } }elseif([bool]($MyInvocation.BoundParameters.Keys -contains 'Array')){ $HTMLTable = New-DMGHTMLTable -Array $Array -Message $ArrayMessage -Title $ArrayTitle -Color $ArrayTableColor #Log New-DMGCMTraceLog -message ("An Single Array Was Passed") -component "Main()" -type 1 -ScriptName $ScriptName -LogFile $LogFile -ScriptFile $ScriptFile if([bool]($MyInvocation.BoundParameters.Keys -contains 'AttatchResults')){ $attachment = @() $AttachmentName = (Remove-DMGInvalidFileNameChars($ArrayTitle))+".csv" $AttachmentFile = Join-Path UNCPath:\Attachments\ $AttachmentName $Array | export-csv -path $AttachmentFile -notypeinformation $attachment += $AttachmentFile } New-DMGCMTraceLog -message ("Attatching the single file: $Attachment") -component "Main()" -type 1 -ScriptName $ScriptName -LogFile $LogFile -ScriptFile $ScriptFile } #Construct HTML for Email $HTMLEmailMessage = @" CodeNarc Report: Sample Project

$Subject

$HTMLTable

Path to script: $ScriptFile

Path to log file: $LogFile

Path to script that called $ScriptName`: $WrapperScriptPath

"@ if([bool]($MyInvocation.BoundParameters.Keys -contains 'AttatchSource')){ if ($attachment -eq $null){$attachment = @()} $AttachmentName = (Remove-DMGInvalidFileNameChars($ReportTitle))+"_html_source.txt" $AttachmentFile = Join-Path UNCPath:\ $AttachmentName $HTMLEmailMessage | Out-File -FilePath $AttachmentFile $attachment += $AttachmentFile } #Send The Email if ($Attachment){ Send-MailMessage -From $from -To $To -Subject $subject -SmtpServer $smtpServer -BodyAsHtml -Body $HTMLEmailMessage -Attachments $Attachment #Log Sent Email New-DMGCMTraceLog -message ("Email Sent`: Subject`:$subject To`:$To From`:$From Attatchments`:$attachment") -component "Main()" -type 1 -ScriptName $ScriptName -LogFile $LogFile -ScriptFile $ScriptFile }else{ Send-MailMessage -From $from -To $To -Subject $subject -SmtpServer $smtpServer -BodyAsHtml -Body $HTMLEmailMessage #Log Sent Email New-DMGCMTraceLog -message ("Email Sent`: Subject`:$subject To`:$To From`:$From") -component "Main()" -type 1 -ScriptName $ScriptName -LogFile $LogFile -ScriptFile $ScriptFile } #Remove Attachments if (Test-Path "UNCPath:\Attachments\"){ Remove-Item "UNCPath:\Attachments\" -Recurse New-DMGCMTraceLog -message ("UNCPath:\Attachments\ Found. Removing files within.") -component "Main()" -type 1 -ScriptName $ScriptName -LogFile $LogFile -ScriptFile $ScriptFile } #Log End Of Function New-DMGCMTraceLog -message ("End Logging for $ScriptName") -component "Main()" -type 1 -ScriptName $ScriptName -LogFile $LogFile -ScriptFile $ScriptFile } function Get-DMGColorHexValue{ param( [Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$true)] [ValidateSet("Black","White","Red","Lime","Blue","Yellow","Cyan","Magenta","Silver","Gray","Maroon","Olive","Green","Purple","Teal","Navy")] $Color ) switch ($Color) { "Black" {"#000000"} "White" {"#FFFFFF"} "Red" {"#FF0000"} "Lime" {"#00FF00"} "Blue" {"#0000FF"} "Yellow" {"#FFFF00"} "Cyan" {"#00FFFF"} "Magenta" {"#FF00FF"} "Silver" {"#C0C0C0"} "Gray" {"#808080"} "Maroon" {"#800000"} "Olive" {"#808000"} "Green" {"#008000"} "Purple" {"#800080"} "Teal" {"#008080"} "Navy" {"#000080"} default {"#DDDDFF"} } } Function Remove-DMGInvalidFileNameChars { param( [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [String]$String ) $pattern = '[^a-zA-Z]' $CleanString = $String -replace $pattern return $CleanString }

PowerShell Code: The Logging Functions

Import this module to enable the logging functions.


function New-DGMCMTraceLog{
  param (
  [Parameter(Mandatory=$true)]
  $ScriptName,
  [Parameter(Mandatory=$true)]
  $LogFile,
  [Parameter(Mandatory=$true)]
  $ScriptFile,
  [Parameter(Mandatory=$true)]
  $message,
  [Parameter(Mandatory=$true)]
  $component,
  [Parameter(Mandatory=$true)]
  $type )

  $VerboseLogging = "true"
  [bool]$Verbose = [System.Convert]::ToBoolean($VerboseLogging)
  $MaxLogSizeInKB = 10240
  $ScriptStatus = 'Success'

  switch ($type)
  {
    1 { $type = "Info" }
    2 { $type = "Warning" }
    3 { $type = "Error" }
    4 { $type = "Verbose" }
  }

  if (($type -eq "Verbose") -and ($Verbose))
  {
    $toLog = "{0} `$$<{1}><{2} {3}>" -f ($type + ":" + $message), ($ScriptName + ":" + $component), (Get-Date -Format "MM-dd-yyyy"), (Get-Date -Format "HH:mm:ss.ffffff"), $pid
    $toLog | Out-File -Append -Encoding UTF8 -FilePath ("filesystem::{0}" -f $LogFile)
    Write-Host $message
  }
  elseif ($type -ne "Verbose")
  {
    $toLog = "{0} `$$<{1}><{2} {3}>" -f ($type + ":" + $message), ($ScriptName + ":" + $component), (Get-Date -Format "MM-dd-yyyy"), (Get-Date -Format "HH:mm:ss.ffffff"), $pid
    $toLog | Out-File -Append -Encoding UTF8 -FilePath ("filesystem::{0}" -f $LogFile)
    if ($type -eq 'Info') { Write-Host $message }
    if ($type -eq 'Warning') { Write-Host $message -ForegroundColor Yellow}
    if ($type -eq 'Error') { Write-Host $message -ForegroundColor Red}
    

  }
  if (($type -eq 'Warning') -and ($ScriptStatus -ne 'Error')) { $ScriptStatus = $type }
  if ($type -eq 'Error') { $ScriptStatus = $type }

  if ((Get-Item $LogFile -ErrorAction SilentlyContinue).Length/1KB -gt $MaxLogSizeInKB)
  {
    $log = $LogFile
    Remove-Item ($log.Replace(".log", ".lo_"))
    Rename-Item $LogFile ($log.Replace(".log", ".lo_")) -Force
  }
} 

Overview

I developed this tool to help you deploy Software Inventory Logger Servers. Software Inventory Logger collects Microsoft software inventory data on a per server basis and reports them to a central Software Inventory Aggregator. This utility can be deployed through SCCM and will setup a Software Inventory Logger Server. Because s Software Inventory Logger requires a trusted key to be distributed to the server, this tool will allow you to automatically deploy the same certificate and store the password in an encypted AES key that can either be deleted after deployment or hidden with ACL’s. This will allow you to automate the deploymenet of Software Inventory Logger Servers in an environment.

How To Use This Tool

Follow the procedures below to begin deploying Software Inventory Logger Servers in your environment.

Creating the AES Key / Secure Password Pair Files

Load the functions below and run this one time to create the AES Key and Secure password files that will be included with your deployment package. The password that you will supply will be the password to your SLI certificate. In our example below, this is the PFX certificate that can be included with the other files for easy distribution.


$cred = Get-Credential
New-DGMAESKey -Path .\ -credentialFile credentialFile -AESKeyFile AESKeyFile -passwordSecureString $cred.Password

This will generate the following two files. Along with the script below, this can be put into an SCCM or other deployment package:

sli_in_view

Deploying Via SCCM

This can easily be turned into an SCCM Application. When creating the application, call it as:


Powershell.exe -ExecutionPolicy ByPass -File "Invoke-DGMSILloggingServer.ps1"

sli_software_center

Sample Output and Logging

The utility will create a log file which is compatible with the CMTrace.exe tool.
Log_File_SLI_Aggregator

PowerShell Code


<#

.SYNOPSIS
  Enable SIL on Servers
.NOTES
  Version:        1.0
  Author:         David Maiolo
  Creation Date:  2018-02-01
  Purpose/Change: Initial script development

Enable SIL on Servers

There are several ways to enable SIL in a distributed server infrastructure, such as in a private cloud of virtual machines. 

You will need a valid client SSL certificate in .pfx format to use these steps. 
The thumbprint of this certificate will need to be added to your SIL Aggregator using the Set-SILAggregator –AddCertificateThumbprint cmdlet. 
This client certificate does not need to match the name of your SIL Aggregator.

#>


function New-DGMCMTraceLog{
  param (
  [Parameter(Mandatory=$true)]
  $ScriptName,
  [Parameter(Mandatory=$true)]
  $LogFile,
  [Parameter(Mandatory=$true)]
  $ScriptFile,
  [Parameter(Mandatory=$true)]
  $message,
  [Parameter(Mandatory=$true)]
  $component,
  [Parameter(Mandatory=$true)]
  $type )

  $VerboseLogging = "true"
  [bool]$Verbose = [System.Convert]::ToBoolean($VerboseLogging)
  $MaxLogSizeInKB = 10240
  $ScriptStatus = 'Success'

  switch ($type)
  {
    1 { $type = "Info" }
    2 { $type = "Warning" }
    3 { $type = "Error" }
    4 { $type = "Verbose" }
  }

  if (($type -eq "Verbose") -and ($Verbose))
  {
    $toLog = "{0} `$$<{1}><{2} {3}>" -f ($type + ":" + $message), ($ScriptName + ":" + $component), (Get-Date -Format "MM-dd-yyyy"), (Get-Date -Format "HH:mm:ss.ffffff"), $pid
    $toLog | Out-File -Append -Encoding UTF8 -FilePath ("filesystem::{0}" -f $LogFile)
    Write-Host $message
  }
  elseif ($type -ne "Verbose")
  {
    $toLog = "{0} `$$<{1}><{2} {3}>" -f ($type + ":" + $message), ($ScriptName + ":" + $component), (Get-Date -Format "MM-dd-yyyy"), (Get-Date -Format "HH:mm:ss.ffffff"), $pid
    $toLog | Out-File -Append -Encoding UTF8 -FilePath ("filesystem::{0}" -f $LogFile)
    if ($type -eq 'Info') { Write-Host $message }
    if ($type -eq 'Warning') { Write-Host $message -ForegroundColor Yellow}
    if ($type -eq 'Error') { Write-Host $message -ForegroundColor Red}
    

  }
  if (($type -eq 'Warning') -and ($ScriptStatus -ne 'Error')) { $ScriptStatus = $type }
  if ($type -eq 'Error') { $ScriptStatus = $type }

  if ((Get-Item $LogFile -ErrorAction SilentlyContinue).Length/1KB -gt $MaxLogSizeInKB)
  {
    $log = $LogFile
    Remove-Item ($log.Replace(".log", ".lo_"))
    Rename-Item $LogFile ($log.Replace(".log", ".lo_")) -Force
  }
} 

function Set-DGMSILLoggingServer{
    param(
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
        [String]$pfxcertificateserverpath,
        [Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$true)]
        [String]$pfxcertificatename,
        [Parameter(Position=2,Mandatory=$true,ValueFromPipeline=$true)]
        [String]$pdxcertificatethumbprint,
        [Parameter(Position=3,Mandatory=$true,ValueFromPipeline=$true)]
        [String]$pfxcertificatelocalpath,
        [Parameter(Position=4,Mandatory=$true,ValueFromPipeline=$true)]
        [String]$SILAggregatorURI,
        [Parameter(Position=5,Mandatory=$false,ValueFromPipeline=$true)]
        $pfxcertificatelocalpathsercurepassword,
        [Parameter(Position=6,Mandatory=$false,ValueFromPipeline=$true)]
        [String]$pfxcertificatelocalpathusername,
        [Parameter(Position=7,Mandatory=$true,ValueFromPipeline=$true)]
        $pfxcertificatesecurepassword,
        [Parameter(Position=8,Mandatory=$true,ValueFromPipeline=$true)]
        [ValidateSet("Install","Uninstall")]
        [String]$InstallationType
    )

    #Set Logging Varibales
    $invocation = (Get-Variable MyInvocation -Scope 1).Value
    #$ScriptDirectory = Split-Path $invocation.MyCommand.Path
    $ScriptDirectory = "c:\admin"
    $ScriptName = ($MyInvocation.MyCommand.Name)+".psm1"
    $LogName = ($MyInvocation.MyCommand.Name)+".log"
    $LogFile = Join-Path $ScriptDirectory $LogName
    $ScriptFile = Join-Path $ScriptDirectory $ScriptName
    $ReportDate = Get-Date 
    $WrapperScriptPath = $MyInvocation.PSCommandPath

    $errorcount = 0
    if ($InstallationType -eq "Install"){

        try{
            #Map Drive to Client PFX Certificate Path
            New-DGMCMTraceLog -message ("Mapping Drive to Client PFX Certificate Path...") -component "Main()" -type 1 -ScriptName $ScriptName -LogFile $LogFile -ScriptFile $ScriptFile
        
            $driveLetters = ([int][char]'C')..([int][char]'Z') | % {[char]$_}
            $occupiedDriveLetters = Get-Volume | % DriveLetter
            $availableDriveLetters = $driveLetters | ? {$occupiedDriveLetters -notcontains $_}
            $firstAvailableDriveLetter = $availableDriveLetters[0]

            if ($pfxcertificatelocalpathusername -ne $null -and $pfxcertificatelocalpathsercurepassword -ne $null){
                $mycreds = New-Object System.Management.Automation.PSCredential ($pfxcertificatelocalpathusername, $pfxcertificatelocalpathsercurepassword)
                New-PSDrive -Name $firstAvailableDriveLetter -PSProvider filesystem -root $pfxcertificateserverpath -credential $mycreds  | Out-Null
            }else{
                New-PSDrive -Name $firstAvailableDriveLetter -PSProvider filesystem -root $pfxcertificateserverpath  | Out-Null
            }
        }catch{
            New-DGMCMTraceLog -message ("Could Not Map Drive to Client PFX Certificate Path.") -component "Main()" -type 3 -ScriptName $ScriptName -LogFile $LogFile -ScriptFile $ScriptFile
            $errorcount++
        }

        try{
            #Copy Client PFX Certificate to Local Path
            New-DGMCMTraceLog -message ("Copying Client PFX Certificate to Local Path...") -component "Main()" -type 1 -ScriptName $ScriptName -LogFile $LogFile -ScriptFile $ScriptFile

            Copy-Item ${firstAvailableDriveLetter}:\$pfxcertificatename $pfxcertificatelocalpath
            Remove-PSDrive –Name $firstAvailableDriveLetter
        }catch{
            New-DGMCMTraceLog -message ("Could Not Copy Client PFX Certificate to Local Path.") -component "Main()" -type 3 -ScriptName $ScriptName -LogFile $LogFile -ScriptFile $ScriptFile
            $errorcount++
        }

        try{
            #Import Client PFX Certificate To Local Store
            New-DGMCMTraceLog -message ("Importing Client PFX Certificate To Local Store...") -component "Main()" -type 1 -ScriptName $ScriptName -LogFile $LogFile -ScriptFile $ScriptFile

            Import-PfxCertificate -FilePath $pfxcertificatelocalpath\$pfxcertificatename cert:\localMachine\my -Password $pfxcertificatesecurepassword | Out-Null
        }catch{
            New-DGMCMTraceLog -message ("Could not Import Client PFX Certificate To Local Store.") -component "Main()" -type 3 -ScriptName $ScriptName -LogFile $LogFile -ScriptFile $ScriptFile
            $errorcount++
        }

        try{
            #Set SIL Logging
            New-DGMCMTraceLog -message ("Setting SIL Logging...") -component "Main()" -type 1 -ScriptName $ScriptName -LogFile $LogFile -ScriptFile $ScriptFile

            Set-sillogging –targeturi $SILAggregatorURI –certificatethumbprint $pdxcertificatethumbprint
        }catch{
            New-DGMCMTraceLog -message ("Could not set SIL Logging.") -component "Main()" -type 3 -ScriptName $ScriptName -LogFile $LogFile -ScriptFile $ScriptFile
            $errorcount++
        }
        try{
            #Start SIL Logging
            New-DGMCMTraceLog -message ("Starting SIL Logging...") -component "Main()" -type 1 -ScriptName $ScriptName -LogFile $LogFile -ScriptFile $ScriptFile

            Start-SilLogging
        }catch{
            New-DGMCMTraceLog -message ("Could not Start SIL Logging.") -component "Main()" -type 3 -ScriptName $ScriptName -LogFile $LogFile -ScriptFile $ScriptFile
            $errorcount++
        }

        if ($errorcount -le 0){
            Set-DGMRegistryInstallValue -InstallationType Install
            New-DGMCMTraceLog -message ("The Set-DGMSILLoggingServer has been installed.") -component "Main()" -type 1 -ScriptName $ScriptName -LogFile $LogFile -ScriptFile $ScriptFile
        }else{
            New-DGMCMTraceLog -message ("The Set-DGMSILLoggingServer could not be installed. $errorcount error(s) were found.") -component "Main()" -type 3 -ScriptName $ScriptName -LogFile $LogFile -ScriptFile $ScriptFile
            Write-Host "This Window Will Close Automatically in 10 Seconds. A log of these errors can be found at $ScriptFile."
            Start-Sleep -Seconds 10
        }
    }
    elseif ($InstallationType -eq "Uninstall"){
        try{
            #Start SIL Logging
            New-DGMCMTraceLog -message ("Stopping SIL Logging...") -component "Main()" -type 1 -ScriptName $ScriptName -LogFile $LogFile -ScriptFile $ScriptFile

            Stop-SilLogging
        }catch{
            New-DGMCMTraceLog -message ("Could not Stop SIL Logging.") -component "Main()" -type 3 -ScriptName $ScriptName -LogFile $LogFile -ScriptFile $ScriptFile
            $errorcount++
        }
        if ($errorcount -le 0){
            Set-DGMRegistryInstallValue -InstallationType Uninstall
            
            New-DGMCMTraceLog -message ("The Set-DGMSILLoggingServer has been uninstalled.") -component "Main()" -type 1 -ScriptName $ScriptName -LogFile $LogFile -ScriptFile $ScriptFile
        }else{
            New-DGMCMTraceLog -message ("The Set-DGMSILLoggingServer could not be uninstalled. $errorcount error(s) were found.") -component "Main()" -type 3 -ScriptName $ScriptName -LogFile $LogFile -ScriptFile $ScriptFile
            Write-Host "This Window Will Close Automatically in 10 Seconds. A log of these errors can be found at $ScriptFile."
            Start-Sleep -Seconds 10
        }
    }

}

function Set-DGMRegistryInstallValue{
    param(
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
        [ValidateSet("Install","Uninstall")]
        [String]$InstallationType
    )

    #Set The Success Bit for SCCM Application Detection
    $Path = "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Set-DGMSILLoggingServer"
    $Name = "IsInstalled"
    $Name2 = "Version"
    $Value2 = "1"

    if(!(Test-Path $Path)){New-Item -Path $Path -Force | Out-Null}

    if ($InstallationType -eq "Install"){
        $Value = "1"
        New-ItemProperty -Path $Path -Name $Name -Value $Value -PropertyType DWORD -Force | Out-Null
        New-ItemProperty -Path $Path -Name $Name2 -Value $Value2 -PropertyType DWORD -Force | Out-Null
        
    }elseif ($InstallationType -eq "Uninstall"){
        $Value = "0"
        New-ItemProperty -Path $Path -Name $Name -Value $Value -PropertyType DWORD -Force | Out-Null
        New-ItemProperty -Path $Path -Name $Name2 -Value $Value2 -PropertyType DWORD -Force | Out-Null
    }
}

function Invoke-DGMSILLoggingServer{
    param(
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
        [ValidateSet("Install","Uninstall")]
        [String]$InstallationType
    )
    #Set DGM Variables
    $pfxcertificateserverpath = ".\"
    $pfxcertificatename = "SILClient2018.pfx"

    #Set Key Variables
    $AESKeyFile = "AESKeyFile"
    $AESKeyFile = "AESKeyFile"

    #Client PFX Certificate Thumbprint and added to your SIL Aggregator using Set-SilAggregator -AddCertificateThumbprint
    #For example, the thumbprint "a9 09 50 2d d8 2a e4 14 33 e6 f8 38 86 b0 0d 42 77 a3 2a 7b" should be specified as "a909502dd82ae41433e6f83886b00d4277a32a7b"
    $pdxcertificatethumbprint = "‎a909502dd82ae41433e6f83886b00d4277a32a7b"

    $pfxcertificatelocalpath = "c:\admin"
    $SILAggregatorURI = "https://VCOMSIL001PRD"

    #Get Credentials to Client PFX Certificate Path
    #$Cred = Get-Credential -Message "Enter DGM Account With Permissions to Client PFX File Path"
    #$pfxcertificatelocalpathsercurepassword = $Cred.Password
    #$pfxcertificatelocalpathusername = $Cred.UserName

    #Get Credentials for Client PFX Certificate Password
    #$cred2 = Get-Credential -Message "Enter Password for PFX Certificate. You Can Make Up a UserName"
    #$pfxcertificatesecurepassword = $cred2.Password


    $pfxcertificatesecurepassword = Get-DGMAESKey -Path $pfxcertificateserverpath -credentialFile credentialFile -AESKeyFile AESKeyFile

    Set-DGMSILLoggingServer `
        -pfxcertificateserverpath $pfxcertificateserverpath `
        -pfxcertificatename $pfxcertificatename `
        -pdxcertificatethumbprint $pdxcertificatethumbprint `
        -pfxcertificatelocalpath $pfxcertificatelocalpath `
        -SILAggregatorURI $SILAggregatorURI `
        -pfxcertificatesecurepassword $pfxcertificatesecurepassword `
        -InstallationType $InstallationType

        #-pfxcertificatelocalpathsercurepassword $pfxcertificatelocalpathsercurepassword `
        #-pfxcertificatelocalpathusername $pfxcertificatelocalpathusername `
}

function Set-DGMAESKey{
    
    param(
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
        $Path,
        [Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$true)]
        $credentialFile,
        [Parameter(Position=2,Mandatory=$true,ValueFromPipeline=$true)]
        $AESKeyFile,
        [Parameter(Position=3,Mandatory=$true,ValueFromPipeline=$true)]
        $passwordSecureString
        
    )

    $credentialFilePath = Join-Path $Path $credentialFile
    $AESKeyFilePath = Join-Path $Path $AESKeyFile

    # Generate a random AES Encryption Key.
    Write-Host "Generating a random AES Encryption Key..."
    $AESKey = New-Object Byte[] 32
    [Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($AESKey)
	
    # Store the AESKey into a file. This file should be protected!  (e.g. ACL on the file to allow only select people to read)
    Write-Host "Storing the AESKey into $AESKeyFilePath. This file should be protected!..."
    Set-Content $AESKeyFilePath $AESKey   # Any existing AES Key file will be overwritten		

    # Store the Credentials into a file
    Write-Host "Storing the Credential File into $credentialFilePath..."
    $password = $passwordSecureString | ConvertFrom-SecureString -Key $AESKey
    Add-Content $credentialFilePath $password

}

function Get-DGMAESKey{

    param(
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
        $Path,
        [Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$true)]
        $credentialFile,
        [Parameter(Position=2,Mandatory=$true,ValueFromPipeline=$true)]
        $AESKeyFile
        
    )

    #To get a new key, run such as:
    #$cred = Get-Credential
    #New-DGMAESKey -Path .\ -credentialFile credentialFile -AESKeyFile AESKeyFile -passwordSecureString $cred.Password

    $AESKeyFilePath = Join-Path $Path $AESKeyFile
    $credentialFilePath = Join-Path $Path $credentialFile

    #Retreive AES Encryption Key
    Write-Host "Retrieving AES Encryption Key from $AESKeyFilePath..."
    $AESKey = Get-Content $AESKeyFilePath
    
    #Retreive Credential File
    Write-Host "Retrieving AES Credential File from $credentialFilePath..."
    $pwdTxt = Get-Content $credentialFilePath

    #Convert and return this a secure string
    Write-Host "ConvertTo-SecureString with  $credentialFile and $AESKeyFile..."
    $securePwd = $pwdTxt | ConvertTo-SecureString -Key $AESKey

    Return $securePwd
}

Overview

I developed this tool, Fix-DMGSCCMWUAPolicy.ps1,to help you mitigate WUA Policy issues that result in error 0x80004005 on the client-side WUAHandler.log file. This tool will attempt to resolve those issues.

Troubleshooting Error 0x80004005

When you deploy software updates, you add the updates to a SUG and then deploy the SUG to your clients. When you create the deployment, the update policy is sent to client computers, and the update content files are downloaded from a DP to the local cache on the client computer. The updates are then available for installation. After the deployment and the deployment policy have been created on the server, clients receive the policy on the next policy evaluation cycle. However, group policy issues can sometimes block these policies.

Root Cause of WSUS Error 0x80004005

If you receive error 0x80004005 in your WUAHandler.log file, then Group policy is likely overriding the local policy, not allowing the workstation to be patched or updated.

Resolution to WSUS Error 0x80004005

If you have this error, a few steps need to be carried out to resolve. The below script will automate this task against remotely against a set of imported computers. If the fix is not required, the utility will pass on to the next computer in the CSV file.

The utility will create a log file which is compatible with the CMTrace.exe tool.

log_file

PowerShell Script: Fix-DMGSCCMWUAPolicy.ps1


<#
.Synopsis
   Fix the WUA Policy Locally On The Client
.DESCRIPTION
   The tool, Fix-DMGSCCMWUAPolicy.ps1 was written by David Maiolo and will automatically fix WUA Policies
.EXAMPLE
   Fix-DMGSCCMWUAPolicy -CSVFile sccm_import.csv
.EXAMPLE
   Fix-DMGSCCMWUAPolicy -Hostname WORKSTATION01
#>


function New-DGMLogFile
{
  param (
  [Parameter(Mandatory=$true)]
  $message,
  [Parameter(Mandatory=$true)]
  $component,
  [Parameter(Mandatory=$true)]
  $type )

  switch ($type)
  {
    1 { $type = "Info" }
    2 { $type = "Warning" }
    3 { $type = "Error" }
    4 { $type = "Verbose" }
  }

  if (($type -eq "Verbose") -and ($Global:Verbose))
  {
    $toLog = "{0} `$$<{1}><{2} {3}>" -f ($type + ":" + $message), ($Global:ScriptName + ":" + $component), (Get-Date -Format "MM-dd-yyyy"), (Get-Date -Format "HH:mm:ss.ffffff"), $pid
    $toLog | Out-File -Append -Encoding UTF8 -FilePath ("filesystem::{0}" -f $Global:LogFile)
    Write-Host $message
  }
  elseif ($type -ne "Verbose")
  {
    $toLog = "{0} `$$<{1}><{2} {3}>" -f ($type + ":" + $message), ($Global:ScriptName + ":" + $component), (Get-Date -Format "MM-dd-yyyy"), (Get-Date -Format "HH:mm:ss.ffffff"), $pid
    $toLog | Out-File -Append -Encoding UTF8 -FilePath ("filesystem::{0}" -f $Global:LogFile)
    if ($type -eq 'Info') { Write-Host $message }
    if ($type -eq 'Warning') { Write-Host $message -ForegroundColor Yellow}
    if ($type -eq 'Error') { Write-Host $message -ForegroundColor Red}
    

  }
  if (($type -eq 'Warning') -and ($Global:ScriptStatus -ne 'Error')) { $Global:ScriptStatus = $type }
  if ($type -eq 'Error') { $Global:ScriptStatus = $type }

  if ((Get-Item $Global:LogFile).Length/1KB -gt $Global:MaxLogSizeInKB)
  {
    $log = $Global:LogFile
    Remove-Item ($log.Replace(".log", ".lo_"))
    Rename-Item $Global:LogFile ($log.Replace(".log", ".lo_")) -Force
  }
} 

function GetScriptDirectory
{
  $invocation = (Get-Variable MyInvocation -Scope 1).Value
  Split-Path $invocation.MyCommand.Path
} 

function Fix-DMGSCCMWUAPolicy
{
    [CmdletBinding()]
    [Alias()]
    [OutputType([int])]
    Param
    (
        # Param1 help description
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   Position=0,
                   ParameterSetName='Parameter Set 1')]
                   [ValidateScript({(Test-Path $_)})]
                   $CSVFile,
        # Param2 help description
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   Position=0,
                   ParameterSetName='Parameter Set 2')]
                   [ValidateScript({(Get-ADComputer -Identity $_).objectclass -eq 'computer' })]
                   [String]$Hostname
    )

    Begin
    {
        $path = (get-item -Path .).FullName 
        
        if ($CSVFile -ne $null){
            Write-Host Importing $CSVFile...
            $csv = import-csv "$CSVFile"
        }else{
            $csv = [PSCustomObject]@{
                Hostname = $Hostname}
        }
        Write-Host ==========================================
        Write-Host SCCM WUA Policy Error 0`x80070002 Fix Tool
        Write-Host ==========================================
        Write-Host "v0.1 (2017-12-20) by dmaiolo"
        New-DGMLogFile -message ("Starting Logging for Fix-DMGSCCMWUAPolicy") -component "Main()" -type 1 
    }
    Process
    {
    

    $computers = @();
        
    $csv | foreach-object {
        $h = $_.Hostname
    

        if(Test-Connection -ComputerName $h -Count 1 -Quiet){


            $g = $null
            $resultsfile = $null

            $resultsfile = "\\$h\c$\Windows\CCM\Logs\WUAHandler.log"
            #$resultsfile = "\\cs-msc\public\Temp\Noah\WUAHandler.log"
            $logLines = @()

            Write-Host $h`: Loading $resultsfile ...
            if (Test-Path $resultsfile){
                $g = Get-Content  $resultsfile -ErrorAction Stop
            }else{
                New-DGMLogFile -message ("$h`: $resultsfile Does Not Exist") -component "Main()" -type 3
            }
            
            $lastLine = $g.Length
            $WUAPolicyErrorFound = $false
            Write-Host $h`: Size of File`: $lastLine Lines

            for($i = $lastLine-1; $i -ge 0; $i--)
            {
                #if ($g[$i] -contains "*0x80004005*") { 
                #    Write-Host $g[$i]
                #    $WUAPolicyErrorFound = $true
                #    break
                $g[$i] -match '(?:.*)(0x80004005)' | out-null
                    #$logLines += ($h + ': ' + $matches[1] + ' on ' + $matches[3] + ' at ' + $matches[2])
                    if ($matches -ne $null){
                        if ($matches[1] -eq "0x80004005") { 
                            $WUAPolicyErrorFound = $true
                            break 
                        }
                    }
               
            }

            if($WUAPolicyErrorFound){
                New-DGMLogFile -message ("$h`: Unable to read existing WUA Group Policy object. 0x80004005. Fix requred.") -component "Main()" -type 2
                $sourcefile = "\\$h\c$\Windows\System32\GroupPolicy\Machine\Registry.pol"
                #$destinationfile = "\\$($h)\c$\Windows\System32\GroupPolicy\Registry_$(Get-Date -Format dd-MM-yyyy).pol"

                Write-Host "$h`: Removing $sourcefile..."
                if (Test-Path $sourcefile){
                    try{
                        Remove-Item -Path $sourcefile -Force
                        New-DGMLogFile -message ("$h`: Removed Policy File`: $sourcefile.") -component "Main()" -type 1
                    }
                    catch{
                        New-DGMLogFile -message ("$h`: Could NOT Remove Policy File`: $sourcefile.") -component "Main()" -type 3
                    }
                }else{
                    New-DGMLogFile -message ("$h`: File Does Not Exist`: $sourcefile.") -component "Main()" -type 3
                }

                Write-Host $h`: Sleeping 5 Seconds...
                sleep 5

                Write-Host "$h`: Running GPUdate `/Force..."
                Try{
                    Invoke-Command -ComputerName $h -ScriptBlock { gpupdate /force } -ErrorAction Stop
                    New-DGMLogFile -message ("$h`: GPUpdate /Force Ran Succesfully") -component "Main()" -type 1
                }
                Catch [System.Management.Automation.Remoting.PSRemotingTransportException]{
                    New-DGMLogFile -message ("$h`: GPUpdate /Force Did NOT Run Succesfully (errorcode 0x80090322 occurred while using Kerberos authentication: An unknown security error occurred.)") -component "Main()" -type 3
                    try {
                        $command = "C:\ADMIN`\PSTools\PsExec.exe \\$h /s /accepteula /nobanner cmd /c `"Winrm quickconfig /q`""
                        Invoke-Expression -Command:"$command"
                        New-DGMLogFile -message ("$h`: WinRM Repair Ran Succesfully") -component "Main()" -type 1
                        }
                    catch{
                        New-DGMLogFile -message ("$h`: WinRM Repair NOT Succesfull") -component "Main()" -type 3
                    }
                }
                Catch{
                    New-DGMLogFile -message ("$h`: GPUpdate /Force Did NOT Run Succesfully (generic error)") -component "Main()" -type 3
                }

                Write-Host $h`: Sleeping 5 Seconds...
                sleep 5

                Write-Host "$h`: Updating the SCCM Store Policy..."
                Try{
                    Invoke-Command -ComputerName $h -ScriptBlock { 
                        $strAction = "{00000000-0000-0000-0000-000000000108}"

                        $WMIPath = "\\" + $Using:h + "\root\ccm:SMS_Client" 
                        $SMSwmi = [wmiclass] $WMIPath 
                        [Void]$SMSwmi.TriggerSchedule($strAction)
                     } -ErrorAction Stop
                    New-DGMLogFile -message ("$h`: Update of the SCCM Store Policy Was Succesful") -component "Main()" -type 1
                }
                Catch [System.Management.Automation.Remoting.PSRemotingTransportException]{
                    New-DGMLogFile -message ("$h`: Update of the SCCM Store Policy Was NOT Succesful (errorcode 0x80090322 occurred while using Kerberos authentication: An unknown security error occurred.)") -component "Main()" -type 3
                }
                Catch{
                    New-DGMLogFile -message ("$h`: Update of the SCCM Store Policy Was NOT Succesful (generic error)") -component "Main()" -type 3
                }

            }else{
                New-DGMLogFile -message ("$h`: No WUA Group Policy Object Errors Found. No Fix requred.") -component "Main()" -type 1
            }

        }
        else{
            New-DGMLogFile -message ("$h`: is offline.") -component "Main()" -type 2
        }
       }
    }
    End
    {
       Write-Host ===============================================================
       Write-Host Log File of Results Generated at $Global:LogFile VIEW WITH CMTRACE.EXE
       New-DGMLogFile -message ("Ending Logging for Fix-DMGSCCMWUAPolicy") -component "Main()" -type 1
    }
}

$VerboseLogging = "true"
[bool]$Global:Verbose = [System.Convert]::ToBoolean($VerboseLogging)
#$Global:LogFile = Join-Path (GetScriptDirectory) "Fix-DMGSCCMWUAPolicy_$(Get-Date -Format dd-MM-yyyy).log"
$Global:LogFile = Join-Path (GetScriptDirectory) "Fix-DMGSCCMWUAPolicy.log"
$Global:MaxLogSizeInKB = 10240
$Global:ScriptName = 'Fix-DMGSCCMWUAPolicy.ps1' 
$Global:ScriptStatus = 'Success'

Overview

I developed this tool, Fix-DMGSCCMStateMessage.ps1, to assit in troubleshooting SCCM “In Progress” and State Message Communication issues. If the UpdateStore.log shows that a particular windows update component is installed, but it is still in progress in the SCCM console, the State Message is likely not communicating properly to the SQL server. State messaging is a mechanism in SCCM which replicates point in time conditions on the client.

Fix-DGMSCCMStateMessage Tool

The tool will automatically update the State Message locally on the SCCM client by invoking the following two commands:


$SCCMUpdatesStore = New-Object -ComObject Microsoft.CCM.UpdatesStore
$SCCMUpdatesStore.RefreshServerComplianceState()

The tool requires the –csvfile argument, which is the path to a .CSV file containing one column, Hostname, with the computer hostnames listed in the column and can be run as in the example below.

Fix-DGMSCCMStateMessage Log File

The utility will create a log file that is compatible with the CMTrace tool, which includes the thread, time, state and component for each process.


<#
.Synopsis
   Update the State Message locally on the SCCM client
.DESCRIPTION
   The tool, Fix-DMGSCCMStateMessage.ps1 was written by David Maiolo which will automatically update the State Message locally on the SCCM client by 
   invoking the following two commands: $SCCMUpdatesStore = New-Object -ComObject Microsoft.CCM.UpdatesStore and $SCCMUpdatesStore.RefreshServerComplianceState()
.EXAMPLE
   Fix-DMGSCCMStateMessage -CSVFile state_message_import.csv
.EXAMPLE
   Fix-DMGSCCMStateMessage -Hostname WORKSTATION01
#>


function New-DGMLogFile
{
  param (
  [Parameter(Mandatory=$true)]
  $message,
  [Parameter(Mandatory=$true)]
  $component,
  [Parameter(Mandatory=$true)]
  $type )

  switch ($type)
  {
    1 { $type = "Info" }
    2 { $type = "Warning" }
    3 { $type = "Error" }
    4 { $type = "Verbose" }
  }

  if (($type -eq "Verbose") -and ($Global:Verbose))
  {
    $toLog = "{0} `$$<{1}><{2} {3}>" -f ($type + ":" + $message), ($Global:ScriptName + ":" + $component), (Get-Date -Format "MM-dd-yyyy"), (Get-Date -Format "HH:mm:ss.ffffff"), $pid
    $toLog | Out-File -Append -Encoding UTF8 -FilePath ("filesystem::{0}" -f $Global:LogFile)
    Write-Host $message
  }
  elseif ($type -ne "Verbose")
  {
    $toLog = "{0} `$$<{1}><{2} {3}>" -f ($type + ":" + $message), ($Global:ScriptName + ":" + $component), (Get-Date -Format "MM-dd-yyyy"), (Get-Date -Format "HH:mm:ss.ffffff"), $pid
    $toLog | Out-File -Append -Encoding UTF8 -FilePath ("filesystem::{0}" -f $Global:LogFile)
    if ($type -eq 'Info') { Write-Host $message }
    if ($type -eq 'Warning') { Write-Host $message -ForegroundColor Yellow}
    if ($type -eq 'Error') { Write-Host $message -ForegroundColor Red}
    

  }
  if (($type -eq 'Warning') -and ($Global:ScriptStatus -ne 'Error')) { $Global:ScriptStatus = $type }
  if ($type -eq 'Error') { $Global:ScriptStatus = $type }

  if ((Get-Item $Global:LogFile).Length/1KB -gt $Global:MaxLogSizeInKB)
  {
    $log = $Global:LogFile
    Remove-Item ($log.Replace(".log", ".lo_"))
    Rename-Item $Global:LogFile ($log.Replace(".log", ".lo_")) -Force
  }
} 

function GetScriptDirectory
{
  $invocation = (Get-Variable MyInvocation -Scope 1).Value
  Split-Path $invocation.MyCommand.Path
} 

function GetStringBetweenTwoStrings($firstString, $secondString, $importString){

    #Get content from file
    $file = $importString

    #Regex pattern to compare two strings
    $pattern = "$firstString(.*?)$secondString"

    #Perform the opperation
    $result = [regex]::Match($file,$pattern).Groups[1].Value

    #Return result
    return $result

}

function Fix-DMGSCCMStateMessage
{
    [CmdletBinding()]
    [Alias()]
    [OutputType([int])]
    Param
    (
        # Param1 help description
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   Position=0,
                   ParameterSetName='Parameter Set 1')]
                   [ValidateScript({(Test-Path $_)})]
                   $CSVFile,
        # Param2 help description
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   Position=0,
                   ParameterSetName='Parameter Set 2')]
                   [ValidateScript({(Get-ADComputer -Identity $_).objectclass -eq 'computer' })]
                   [String]$Hostname
    )

    Begin
    {
        $path = (get-item -Path .).FullName 
        
        if ($CSVFile -ne $null){
            Write-Host Importing $CSVFile...
            $csv = import-csv "$CSVFile"
        }else{
            $csv = [PSCustomObject]@{
                Hostname = $Hostname}
        }
        Write-Host =========================================
        Write-Host SCCM State Message Update Tool
        Write-Host =========================================
        Write-Host "dmaiolo"
        New-DGMLogFile -message ("Starting Logging for Fix-DMGSCCMStateMessage") -component "Main()" -type 1 
    }
    Process
    {
    

    $computers = @();
        
    $csv | foreach-object {
        $h = $_.Hostname
    
        if(Test-Connection -ComputerName $h -Count 1 -ErrorAction SilentlyContinue){

            Try{
               
                $g = $null               
                Invoke-Command -ComputerName $h -ScriptBlock { $SCCMUpdatesStore = New-Object -ComObject Microsoft.CCM.UpdatesStore
                                                               $SCCMUpdatesStore.RefreshServerComplianceState() } -ErrorAction Stop 
                New-DGMLogFile -message ("$h`: State Message Updated Succesfully (RefreshServerComplianceState())") -component "Main()" -type 1
                Write-Host $h`: Sleeping 5 Seconds...
                sleep 5
                $resultsfile = "\\$h\c$\Windows\CCM\Logs\UpdatesStore.log"

                $logLines = @()

                $g = Get-Content  $resultsfile -ErrorAction Stop
                $lastLine = $g.Length
                for($i = $lastLine-1; $i -ge 0; $i--)
                {
                    $g[$i] -match '<\!\[LOG\[(.*?)\]LOG.*

Overview

I developed this tool, Fix-DGMSCCMUpdateStore.ps1, to assist in fixing Windows UpdateStore Corruption (Datastore.edb) on SCCM Client Computers. On the SCCM client machine, the Windows UpdateStore Datastore.edb in Windows\Software Distribution\.. contains scan results. Over time, this may become corrupted which can stop updating from occurring on the client machine. Additionally, an error might be seen on the client application log reporting wuaueng.dll (1668) SUS20ClientDataStore: Database C:\WINDOWS\SoftwareDistribution\DataStore\DataStore.edb requires logfiles xx-yy in order to recover successfully.

Fix-DGMSCCMUpdateStore Tool

The tool ill automatically attempt to fix the Windows Update Store on an array of SCCM client computers imported via a .CSV file. When run, the tool will perform the following tasks on each computer within the .CSV:

  • Stop the Windows Update Service
  • Move SoftwareDistribution to a backup location
  • Start Windows Update Service
  • Recreate SoftwareDistribution

The tool requires the –csvfile argument, which is the path to a .CSV file containing one column, Hostname, with the hostnames listed in the column and can be run as in the example below.

Fix-DGMSCCMUpdateStore Log File

The utility will create a log file that is compatible with the CMTrace tool, which includes the thread, time, state and component for each process.


PowerShell Script: Fix-DGMSCCMUpdateStore.ps1

<# .Synopsis Fix the Windows UpdateStore on an array of SCCM clients. .DESCRIPTION The Windows UpdateStore Datastore.edb in Windows\Software Distribution\.. contains scan results. This may become corrupted. This tool will fix it on an array of computers imported. .EXAMPLE Fix-DMGSCCMUpdateStore -CSVFile .\Fix-DMGSCCMUpdateStore-Import.csv #> function New-DGMLogFile { param ( [Parameter(Mandatory=$true)] $message, [Parameter(Mandatory=$true)] $component, [Parameter(Mandatory=$true)] $type ) switch ($type) { 1 { $type = "Info" } 2 { $type = "Warning" } 3 { $type = "Error" } 4 { $type = "Verbose" } } if (($type -eq "Verbose") -and ($Global:Verbose)) { $toLog = "{0} `$$<{1}><{2} {3}>" -f ($type + ":" + $message), ($Global:ScriptName + ":" + $component), (Get-Date -Format "MM-dd-yyyy"), (Get-Date -Format "HH:mm:ss.ffffff"), $pid $toLog | Out-File -Append -Encoding UTF8 -FilePath ("filesystem::{0}" -f $Global:LogFile) Write-Host $message } elseif ($type -ne "Verbose") { $toLog = "{0} `$$<{1}><{2} {3}>" -f ($type + ":" + $message), ($Global:ScriptName + ":" + $component), (Get-Date -Format "MM-dd-yyyy"), (Get-Date -Format "HH:mm:ss.ffffff"), $pid $toLog | Out-File -Append -Encoding UTF8 -FilePath ("filesystem::{0}" -f $Global:LogFile) if ($type -eq 'Info') { Write-Host $message } if ($type -eq 'Warning') { Write-Host $message -ForegroundColor Yellow} if ($type -eq 'Error') { Write-Host $message -ForegroundColor Red} } if (($type -eq 'Warning') -and ($Global:ScriptStatus -ne 'Error')) { $Global:ScriptStatus = $type } if ($type -eq 'Error') { $Global:ScriptStatus = $type } if ((Get-Item $Global:LogFile).Length/1KB -gt $Global:MaxLogSizeInKB) { $log = $Global:LogFile Remove-Item ($log.Replace(".log", ".lo_")) Rename-Item $Global:LogFile ($log.Replace(".log", ".lo_")) -Force } } function GetScriptDirectory { $invocation = (Get-Variable MyInvocation -Scope 1).Value Split-Path $invocation.MyCommand.Path } function Fix-DMGSCCMUpdateStore { [CmdletBinding()] [Alias()] [OutputType([int])] Param ( # Param1 help description [Parameter(Mandatory=$true, ValueFromPipeline=$true, Position=0, ParameterSetName='Parameter Set 1')] [ValidateScript({(Test-Path $_)})] $CSVFile, # Param2 help description [Parameter(Mandatory=$true, ValueFromPipeline=$true, Position=0, ParameterSetName='Parameter Set 2')] [ValidateScript({(Get-ADComputer -Identity $_).objectclass -eq 'computer' })] [String]$Hostname ) Begin { if ($CSVFile -ne $null){ Write-Host Importing $CSVFile... $csv = import-csv "$CSVFile" }else{ $csv = [PSCustomObject]@{ Hostname = $Hostname} } $service1 = "wuauserv" $service2 = "bits" Write-Host ========================================= Write-Host SCCM Fix Windows Update Store by dmaiolo Write-Host ========================================= Write-Host "v0.2 (2017-12-11)" New-DGMLogFile -message ("Starting Logging for Fix-DMGSCCMUpdateStore") -component "Main()" -type 1 } Process { $computers = @(); $csv | foreach-object { $h = $_.Hostname #Test if machine is online if(Test-Connection -ComputerName $h -count 2 -quiet){ Write-Host "Online: $h" -ForegroundColor Green #Stop Windows Update service try{ (get-service -ComputerName $h -Name $service1 -ErrorAction Stop).Stop() New-DGMLogFile -message ("Stopped $service1 service on $h.") -component "Main()" -type 1 } catch{ New-DGMLogFile -message ("Could NOT Stop $service1 service on $h.") -component "Main()" -type 3 } #Sleep 10 seconds to give service enough time to react Write-Host "Sleeping 5 Seconds..." Start-Sleep -s 5 #Stop BITS service try{ (get-service -ComputerName $h -Name $service2 -ErrorAction Stop).Stop() New-DGMLogFile -message ("Stopped $service2 service on $h.") -component "Main()" -type 1 } catch{ New-DGMLogFile -message ("Could NOT Stop $service2 service on $h.") -component "Main()" -type 3 } #Sleep 5 seconds to give service enough time to react Write-Host "Sleeping 5 Seconds..." Start-Sleep -s 5 #Rename the software update store $sourcepath = "\\$($h)\c$\Windows\SoftwareDistribution" $destinationpath = "\\$($h)\c$\Windows\SoftwareDistribution_$(Get-Date -Format dd-MM-yyyy)" $destinationpath2 = "\\$($h)\c$\Windows\SoftwareDistribution_$(Get-Date -Format dd-MM-yyyy)" #Appending destination path if script already run today if (Test-Path $destinationpath){ $n=0 while ((Test-Path $destinationpath2) -eq $true){ New-DGMLogFile -message ("Backup location $destinationpath2 already exists.") -component "Main()" -type 1 ++$n $destinationpath2 = $destinationpath + "-" + $n } $destinationpath = $destinationpath2 } Write-Host "Renaming $sourcepath..." try{ Move-Item -Path $sourcepath -Destination $destinationpath -Force New-DGMLogFile -message ("Renamed SoftwareDistribution to $destinationpath.") -component "Main()" -type 1 } catch{ New-DGMLogFile -message ("Could NOT Rename SoftwareDistribution to $destinationpath.") -component "Main()" -type 3 } #Start the Windows Update service try{ (get-service -ComputerName $h -Name $service1 -ErrorAction Stop).Start() New-DGMLogFile -message ("Started $service1 service on $h.") -component "Main()" -type 1 } catch{ New-DGMLogFile -message ("Could NOT Start $service1 service on $h.") -component "Main()" -type 3 } #Start the BITS service try{ (get-service -ComputerName $h -Name $service2 -ErrorAction Stop).Start() New-DGMLogFile -message ("Started $service2 service on $h.") -component "Main()" -type 1 } catch{ New-DGMLogFile -message ("Could NOT Start $service2 service on $h.") -component "Main()" -type 3 } #Give the services 5 seconds to wake up and create new folders Start-Sleep -s 5 #Verify new folder was created Write-Host "Checking new folder recreation..." if(Test-Path("\\$($h)\c$\Windows\SoftwareDistribution")){ New-DGMLogFile -message ("\\$($h)\c$\Windows\SoftwareDistribution was recreated.") -component "Main()" -type 1 } else{ New-DGMLogFile -message ("\\$($h)\c$\Windows\SoftwareDistribution could NOT be recreated.") -component "Main()" -type 3 } } else{ #Machine is offline New-DGMLogFile -message ("Offline: $h.") -component "Main()" -type 2 } } } End { Write-Host =============================================================== Write-Host Log File of Results Generated at $path\Fix-DMGSCCMUpdateStore_$(Get-Date -Format dd-MM-yyyy).log VIEW WITH CMTRACE.EXE New-DGMLogFile -message ("Ending Logging for Fix-DMGSCCMUpdateStore") -component "Main()" -type 1 } } $VerboseLogging = "true" [bool]$Global:Verbose = [System.Convert]::ToBoolean($VerboseLogging) $Global:LogFile = Join-Path (GetScriptDirectory) "Fix-DMGSCCMUpdateStore_$(Get-Date -Format dd-MM-yyyy).log" $Global:MaxLogSizeInKB = 10240 $Global:ScriptName = 'Fix-DMGSCCMUpdateStore.ps1' $Global:ScriptStatus = 'Success'

Overview

This upgrade strategy will allow you to update your Server environment to the version of Windows Management 5.1 via SCCM. Use this recommended project management guide to help build your deployment workflow. I used this method to upgrade a 400+ server environment which completed smoothly.

Purpose

The purpose of this Deployment Strategy and Plan article is to help you define a deployment strategy and plan for a Windows Management Framework 5.1 upgrade. This article is comprised of two sections: the Deployment Strategy and the Deployment Plan. The Deployment Strategy section is used to formulate a deployment approach for Windows Management Framework 5.1. The Deployment Plan section contains recommended schedule, resource, technical, and support information necessary for successful deployment of Windows Management Framework 5.1.

About Windows Management Framework

Windows Management Framework (WMF) is the delivery mechanism that provides a management interface across the various versions of Windows and Windows Server. With the installation of WMF 5.1, increases security and feature sets will become available to our servers.

Components For Upgrade

The following components should be scheduled for upgrade during this project to version 5.1. This WMF installation adds and/or updates the following features:

  • Windows PowerShell
  • Windows PowerShell Desired State Configuration (DSC)
  • Windows PowerShell Integrated Script Environment (ISE)
  • Windows Remote Management (WinRM)
  • Windows Management Instrumentation (WMI)
  • Windows PowerShell Web Services (Management OData IIS Extension)
  • Software Inventory Logging (SIL)
  • Server Manager CIM Provider

Deployment Strategy

The Deployment Strategy section of this article provides an overview of the deployment strategy planned for Windows Management Framework 5.1. Included in the deployment strategy is recommended timeline information, a description of the deployment approach, and associated benefits, assumptions and risks.

Deployment Overview

Phases

Sites

Computers

Scheduled Dates

PRE-PILOT

Select Servers

15

October 2, 2017 – October 24, 2017

PILOT

Pilot Server Group

106

January 16, 2018 – January 31, 2018

PRODUCTION

Production Server Group

258

February 6, 2018 – February 28, 2018

Total Servers Targeted: 364

Exclusions to Upgrade

38 systems will not be targeted for the upgrade for various reasons. The exclusions include:

  • Exchange 2010 Mailbox Servers / CAS/HUB Servers (MBX) (CAS)
  • SharePoint 2007, 2010 and 2013 Servers (SPS)
  • Proxy and Application Servers (SAS)
  • SCCM Servers (SCM)
  • VMM Cluster Node, Library and Failover Name Account Servers (VMM)
  • Lync Servers (LNC)
  • Operations Manager 2016 Servers (OPS)

Deployment Phases

The Deployment Dates referenced below are the date Windows Management Framework 5.1 would attempt to begin installation on the selected servers in your environment. This does not indicate the completion date for this phase, which could take an additional 2 weeks.

Pilot Phase

Sub Phases

Sites

Computers

Deployment Date

Server 2008 R2

Pilot Server Group

23

January 16, 2018

Server 2012

Pilot Server Group

12

January 16, 2018

Server 2012 R2

Pilot Server Group

71

January 16, 2018

106

Production Phase

Sub Phases

Sites

Computers

Deployment Date

Server 2008 R2

Production Server Group

45

February 6, 2018

Server 2012

Production Server Group

188

February 6, 2018

Server 2012 R2

Production Server Group

25

February 6, 2018

258

Deployment Plan

Deployment Approach

System Center Configuration Manager (SCCM) will be used to deploy Windows Management Framework 5.1. When each phase is approached, the servers will be instructed to execute the installation in Parallel, within their maintenance window.

Because WMF 5.1 has specific installation requirements based on the Operating System, both the PILOT and PRODUCTION phase can be broken into the 2008 R2, 2012 and 2012 R2 sub phases. This is simply used for application targeting and reporting purposes, and as we can see earlier, does not require a shift in deployment date for the parent phase.

Assumptions and Risks

Assumptions

The servers targeted for deployment are assumed to be left on and connected to your corporate network during their maintenance windows. Additionally it is expected that a reboot will likely occur after the installation, during the maintenance window.

Risks

JEA endpoints and session configurations configured to use virtual accounts in WMF 5.0 will not be configured to use a virtual account after upgrading to WMF 5.1. This means that commands run in JEA sessions will run under the connecting user’s identity instead of a temporary administrator account, potentially preventing the user from running commands which require elevated privileges. To restore the virtual accounts, we would need to unregister and re-register any session configurations that use virtual accounts.

This is unlikely to be an issue in your environment.

Pilot Deployment Statistics

A sample pilot phase might be completed successfully with results broken down in the following phases

PILOT Server 2008 R2

PILOT Server 2012

PILOT Server 2012 R2

Benefits to Upgrade

PowerShell Editions

Starting with version 5.1, PowerShell is available in different editions which denote varying feature sets and platform compatibility.

Catalog Cmdlets

Two new cmdlets have been added in the Microsoft.PowerShell.Security module; these generate and validate Windows catalog files.

  • New-FileCatalog
  • Test0FileCatalog

Module Analysis Cache

Starting with WMF 5.1, PowerShell provides control over the file that is used to cache data about a module, such as the commands it exports.

Specifying module version

In WMF 5.1, using module behaves the same way as other module-related constructions in PowerShell. Previously, you had no way to specify a particular module version; if there were multiple versions present, this resulted in an error.

Engagement and Promotion Strategy

During each deployment phase, you can send an email to communicate the associated deployment phase. Members in your team may choose to notify specific application owners if they feel the need.

Testing Methods and Monitoring

The Windows Management Framework 5.1 deployment should be passed through a pre-pilot and pilot phase, where hopefully no issues would be observed. In the event an issue is determined, a rollback to the previous version can be deployed through the uninstall command on the application.

Monitoring The Deployment

Basic Monitoring

Central monitoring of the Windows Management Framework 5.1 rollout can be viewed from your computer by visiting your SCCM report server and searching for the report ‘All application deployments (basic)’.

Choose By: Application

Select Application Based on OS (Collection):

  • WMF 5.1 (For Windows Server 2008 R2)
  • WMF 5.1 (For Windows Server 2012)
  • WMF 5.1 (For Windows Server 2012 R2)

Select Collection (Application): All

The application metrics will be divided into the respective phases:

Clicking the “View Current” data for the phase will allow you to further drill down, even to the computer and user level if necessary:

The monitoring works by determining the following registry value was created:

Server 2008 R2 and Server 2012 WMF 5.1 Detection

Key: HKLM\SOFTWARE\Microsoft\PowerShell\3\PowerShellEngine

Value: PowerShellVersion [String]

Rule: Must begin with “5.1”

2012 R2 WMF 5.1 Detection

Key: HKLM\SOFTWARE\Microsoft\PowerShell\3\PowerShellEngine

Value: PowerShellVersion [String]

Rule: Must begin with “5.1”

Or

Key: HKLM\SOFTWARE\Microsoft\PowerShell\4\PowerShellEngine

Value: PowerShellVersion [String]

Rule: Must begin with “5.1”

Or

Key: HKLM\SOFTWARE\Microsoft\PowerShell\5\PowerShellEngine

Value: PowerShellVersion [String]

Rule: Must begin with “5.1”

Advanced Monitoring

To assure a technician or technical contact has as much data as possible to troubleshoot Windows Management Framework 5.1 deployment issues, compliance items and baselines were written which assess Windows PowerShell versioning directly in a baseline. To see the advanced monitoring that this baselines provide, again go to your SCCM report server and search for the report: Compliance 1.2 – Compliance Details for all CIs of a specific Baseline (report available through a special Microsoft PFE program).

Configuration Baselines Name: CB.Powershell.Version.5

Clicking ‘View Report’ will allow you to drill down and see each compliance item and reason for failure.

There are similar baselines to track all of the versions prior to the upgrade:

  • CB.Powershell.Version.4
  • CB.Powershell.Version.3
  • CB.Powershell.Version.2

Reference Documents

  • Include Your Reference Documents Here

Overview

Purpose

The purpose of this article is to help you define a deployment strategy and plan for a Cisco AnyConnect upgrade. I used a similar procedure to help a 3000+ client environment transition successfully to the latest version of Cisco Anyconnect by utilizing SCCM and a custom upgrade program I created just for the purpose. For demonstration purposes I will show an upgrade procedure to version 4.5.02036. This article is comprised of two sections: the Deployment Strategy and the Deployment Plan. The Deployment Strategy section is used to formulate a deployment approach for Cisco AnyConnect. The Deployment Plan section contains recommended schedule, resource, technical, and support information necessary for a successful deployment of Cisco AnyConnect.

About AnyConnect

AnyConnect refers to a set of network security tools provided by Cisco that can be used to provide your users VPN access and to prevent non-compliant devices from accessing your network.

This set of tools can installed on all of the workstation computers at your company and is usually visible to the user as a small

Cisco icon which they could also use to open your VPN tunnel.

Components For A Succesful Upgrade

The following sample components are recommended for upgrading to version 4.5.02036:

  • Cisco AnyConnect Start Before Login Module 4.3.03086
  • Cisco AnyConnect Diagnostics and Reporting Tool 4.3.03086
  • Cisco AnyConnect Network Access Manager 4.3.03086
  • Cisco AnyConnect Secure Mobility Client 4.3.03086

The ISE compliance module might not need to be upgraded during your project which is the component used to prevent noncompliant devices:

  • Cisco AnyConnect ISE Compliance Module 4.2.426.0

Deployment Strategy

The Deployment Strategy section of this article provides you the recommended deployment strategy for Cisco AnyConnect 4.5.02036. Included in the deployment strategy is recommended timeline information, a description of the deployment approach, and associated benefits, assumptions and risks.

Deployment Overview

Phases

Sites

Computers

Scheduled Dates

PRE-PILOT

SITE 1

27

October 2, 2020 – October 24, 2020

PILOT

SITE 2

169

October 30, 2020 – November 17, 2020

PRODUCTION

All Locations

1,500

November 15, 2020 – December 20, 2020

The Deployment Date’s referenced below are the date Cisco AnyConnect 4.5.02036 would attempt to begin installation on the selected computers. This does not indicate the completion date for this phase, which could take an additional 2 weeks.

Production Phase 1 (Site 1)

Sub Phases

Sites

Computers

Deployment Date

PHASE 1A

Site 1

243

November 15, 2020

PHASE 1B

Site 2

272

November 20, 2020

PHASE 1C

Site 3

295

November 27, 2020

810

Production Phase 2 (Site 2)

Sub Phases

Sites

Computers

Deployment Date

PHASE 2A

Site A 1/2

246

November 29, 2020

PHASE 2B

Site A 2/2

248

December 4, 2020

PHASE 2C

Other Sites

185

December 6, 2020

679

Production Phase 3 (Executive Staff)

Sub Phases

Sites

Computers

Deployment Date

PHASE 3

Executive Staff

11

Custom Arrangements

Deployment Approach


System Center Configuration Manager (SCCM) should be used to deploy Cisco AnyConnect. When each phase is approached, the computers would be instructed to execute the installation in Parallel, within their maintenance window.

A deployment will require a software reboot once completed. Users have an option to install the software outside of their maintenance window via the Software Center found on the start menu, and if they do, will also require a restart, even if during the middle of the day. The software could be displayed as shown on the right.

Assumptions and Risks

Assumptions

The computers targeted for deployment are assumed to be left on and connected to your corporate network during the maintenance window at least a couple of the nights during the scheduled deployment. Additionally, the computer is assumed to currently have no currently known issues with the version of AnyConnect installed prior to upgrade.

Deployment Targeting and IP Scopes

Deployments can be targeting based on DHCP scopes correlating to client’s active IP addresses. Active scopes and IP address mappings can be seen by having a server administrator run Get-DhcpServerv4Scope –ComputerName ADCSERVER| Select ScopeID, Name

Risks

Because AnyConnect is used as the primary means to authenticate a computer for compliance against your network, failed installations can result in a device not having any network connectivity until the installation is resolved or ISE compliance is turned off on the network port associated to the computer by a Network Engineer.

Benefits to Deployment

A client I worked with was running Cisco AnyConnect 4.3, which was two major versions behind the latest version released in late October 2017, 4.5.02036. Amongst multiple security fixes that were introduced since this version, some important ones include patches for the WPA2 KRACK vulnerability.

Additionally, developing your upgrade strategy will provide a more refined path and plan for future AnyConnect upgrades.

Deployment Plan

The Deployment Plan section provides recommended information on the deployment of Cisco AnyConnect. Included in the Deployment Plan are schedule and resource information, the engagement and promotion strategy, deployment methods, technology infrastructure and support considerations, deployment testing and training requirement, and any known conflicts or issues with the software.

Deployment Schedule and Resources

Pre-Pilot Schedule

Phase

Sites

Computers

Deployment Date*

Server Resource

Network Resource

PRE-PILOT PHASE 1

Site 1

10

October 2, 2020

David Maiolo

Elon Musk

PRE-PILOT PHASE 2

Site 2

3

October 9. 2020

David Maiolo

Elon Musk

PRE-PILOT PHASE 3

Site 3

14

October 20, 2020

David Maiolo

Bill Gates

Pilot Schedule

Phase

Sites

Computers

Deployment Date*

Server Resource

Network Resource

PILOT PHASE 1

Site 1 Pilot

59

October 30, 2020

David Maiolo

Elon Musk

PILOT PHASE 2

Site 2 Pilot

51

November 6. 2020

David Maiolo

Bill Gates

PILOT PHASE 3*

Site 3 Pilot

118

November 13, 2020

David Maiolo

Elon Musk

Production Phase 1 (Site 1)

Sub Phases

Sites

Computers

Deployment Date*

Server Resource

Network Resource

PHASE 1A

Site 1

243

November 15, 2020

David Maiolo

Elon Musk

PHASE 1B

Site 2

272

November 20, 2020

Elon Musk

Elon Musk

PHASE 1C

Site 3

295

November 27, 2020

Elon Musk

Elon Musk

Production Phase 2 (Site 2)

Sub Phases

Sites

Computers

Deployment Date*

Server Resource

Network Resource

PHASE 2A

Produce a xlsx attachment

246

November 29, 2020

Elon Musk

Elon Musk

PHASE 2B

Produce a xlsx attachment

248

December 4, 2020

David Maiolo

Elon Musk

PHASE 2C

See xlsx attachment

185

December 6, 2020

David Maiolo

Elon Musk

Production Phase 3 (Executive Staff)

Sub Phases

Sites

Computers

Deployment Date*

Server Resource

Network Resource

PHASE 3

Executive Staff

7

Custom Arrangements

David Maiolo

Elon Musk

Resource Requirements

Helpdesk Team

Throughout the deployment process it is additionally considered there should be Technology Helpdesk Team resources available to provide immediate remediation efforts via the helpdesk. Your helpdesk technician should walk the user through starting the AUTOMATED REMEDIATION TOOL as shown later in this article and assist with other troubleshooting steps.

Endpoint Team

Additionally, your Endpoint Team can be considered as a Tier 2 resources to assist the helpdesk via requests in the Ticketing system. The Endpoint Team engineer should attempt the steps and tools outlined in the section ADVANCED TECHNICAL SUPPORT

Server Infrastructure Team

Your Network Engineers Team and Server Infrastructure Team resources not listed above should also be thought to be available for emergencies and Tier 3 escalations from either your Endpoint or Helpdesk team. Your Server Infrastructure Team resource should be available for any and all requests for assistance from the Helpdesk Team and Endpoint Team to assist with remediation, and work on additional remediation efforts if these teams do not have the resources available.

Network Engineers Team

Your Network Engineers Team resource should assist in remediation and would likely be the first point of contact to disable ISE on the port where the customer is having a connection issue.

Engagement and Promotion Strategy

This recommended engagement and promotion strategy should be used for deploying Cisco AnyConnect 4.5.02036.

During each production phase, Technology Support should be used as the method to communicate the strategy to associated Managers during the phases and management staff during the executive phases.

E-Mail Template

Colleagues:

The Technology department has successfully completed testing of Cisco AnyConnect 4.5.02036 and is ready to begin the deployment portion of the project. The target date for deployment in your area is [Scheduled time per phase] between the hours of TIME1 and TIME. <--include times from your maintenance window

This deployment is only an upgrade to the preexisting application on the computers in this area.

The Cisco AnyConnect software enables the streamlining of authentication, access controls and privileges, and network systems at this company NNN. For the most part, deployment and streamlined authentication and authorizations services occurs “behind the scenes” with minimal, if any, user disruption.

You should not notice any operational changes when the software is deployed to your computer, other than a reboot during the hours indicated above. However, our engineers and technicians are available to assist in the event the software installation causes an issue with a user accessing our network. Please call the helpdesk at xNNNN immediately if you run into any VPN or network connectivity issues during this deployment.

Thank You for your cooperation,

[Technology Signature]

Testing Methods and Customer Acceptance

You should pass the Cisco AnyConnect 4.5.02036 deployment through a pre-pilot and pilot phase, where some issues could be observed. In those instances, it was of utmost importance that the customers’ issues would be resolved quickly. In the event the Cisco AnyConnect 4.5.02036 installation failed, it would be vitally important that the Network Engineers Team be available to “disable ISE” on the user’s network port so that the AnyConnect requirements would not be needed during the resolution.

With additional support, proper remediation strategies from Endpoint Team, Technology Helpdesk Team and Server Infrastructure Team would likely be required to bring the users’ computer back into compliance with the proper installation of Cisco AnyConnect 4.5.02036.

Monitoring The Deployment

Basic Monitoring

Central monitoring of the Cisco AnyConnect 4.5.02036 rollout could be viewed from your computer by visiting your SCCM SQL report link and searching for the report ‘All application deployments (basic)’.

Choose By: Application

Select Application (Collection): Cisco AnyConnect 4.5.02036 (All Applications)

Select Collection (Application): All

The application metrics should be divided into the respective phases:

Clicking the “View Current” data for the phase would allow you to further drill down, even to the computer and user level if necessary:

The monitoring works by comparing Product installation UIs for each Cisco component with reported installed components on the workstations.

Advanced Monitoring

To assure your technician or technical contact has as much data as possible to troubleshoot Cisco AnyConnect 4.5.02036 deployment issues, several compliance items and baselines were written which assess specific values on the computers. These basslines look to see that certain conflicting software is not installed, required certificates are in place and not expired and all required components are installed successfully. These could be viewed within you SCCM SQL Server by searching for the report: Summary compliance by configuration baseline

Configuration Baselines Name: CB.AnyConnect.4.5.02036.Full.Compliance

Clicking ‘View Report’ will allow you to drill down and see each compliance item and reason for failure.

Advanced Technical Support

If an installation of Cisco AnyConnect 4.5.02036 fails, the user is likely not to have any network access. During the planning and testing of Cisco AnyConnect 4.5.02036, many advanced methods, tools and configurations have been written to help support the rollout, monitor its progress, and provide technical staff (and users’) remediation options.

Compliance Checking

I developed algorithms that can developed as SCCM configuration items to provide a detection service to track centralized deployment success and failures. By use from a technician, these compliance metrics are available to a computer with or without network access to show if a device passed or failed the installation and if so, identify where the installation failed.

This would available to the technician in the Control Panel -> Configuration Manager -> Configurations -> CB.AnyConnect 4.5.02036.Full.Compliance -> Evaluate -> View Report

Success and failures can be further clicked to elaborate on details.

Conflict Resolution Flow-Chart

I developed this flow-chart to help a technician work with customers if they run into issues.

cid:image001.png@01D357E9.F2B60D10

Automated Remediation Tool

I further developed the Cisco AnyConnect Remediation Tool, which should allow a user or technician a first-line defense to components in the flowchart above. The remediation tool was written in PowerShell and attempts to identify and resolve common issues.

The helpdesk or technician should first attempt to use the remediation tool before performing next steps. Network connectivity is NOT required to use the tool.

If pre-deployed to user computers as a required deployment (but far off in the future), it will cache locally on the client. Use of the tool only requires the user to open Software Center and find the Cisco AnyConnect (REMEDIATION TOOL) and clicking Install as shown below. No user interaction is required. If the tool was successful, the user will be asked to restart their computer. If not, the tool will prompt to retry.

Advanced mode

For technicians using the tool, a more advanced and verbose mode is available. The technician would find the tool inside of c:\windows\ccmcache\xx\Invoke-DMGAnyConnect.ps1. If run as an administrate account, the tool will show progress and will log attempts for remediation.

Removing Computers From The Deployment

If a computer needs to be removed from your Cisco AnyConnect deployment, you should include an exclusion collection to your project. This would allow an SCCM administrator add the computer to the collection:

\Assets and Compliance\Overview\Device Collections\Cisco AnyConnect 4.5.02036 Upgrade Project\EXCLUSIONS\Cisco AnyConnect 4.5.02036 Upgrade Project (EXCLUSIONS ONLY)

Further, the Phase collection, such as Cisco AnyConnect 4.5.02036 Upgrade Project (PRODUCTION PHASE 1A), would need to be updated (right click-> Update Membership) to reference the new exclusion.

Reference Documents

  • Be sure to include reference documents

Custom PowerShell Remediation Solution
After the application is deployed through SCCM, the Cisco installation could fail for a multitude of reasons. After working with several clients, and determining the most common reasons for failure, the following “Remediation” application was developed and made available so that end users could install the tool in the event the standard SCCM application did not work.

In my scenarios, this method became so much more successful than the MSI installations provided by Cisco, that it was eventually modified to be the sole Cisco AnyConnect automated deployment through SCCM.


$toolslocation=(get-item -Path .).FullName 
$global:errorsleft = 0
$global:RestartPending = 0
$programversion = "1.4"
$programauthor = "c-dmaiolo"

Start-Transcript -Append -Path "C:\admin\Fix-DMGAnyConnect-$programversion-$(Get-Date -Format dd-MM-yyyy).log"

Function Get-DMGWelcomeScreen($Title){
    Write-Host ==============================================================
    Write-Host Title: $Title                                          
    Write-Host Version: $programversion
    Write-Host Author: $programauthor
    Write-Host ==============================================================

}

Function Get-DMGTitleScreen($Title){
    Write-Host ============================================================== -ForegroundColor Cyan
    Write-Host $Title  -ForegroundColor Cyan
    Write-Host ============================================================== -ForegroundColor Cyan
}

function Get-ConfigurationFileStatus
    {
    [CmdletBinding()]
    
    [OutputType([int])]


    Param
    (
        # Param1 help description
        [Parameter(Mandatory=$false,
                   ValueFromPipeline=$true,
                   Position=0)]
        $ConfigurationFilePath="c:\ProgramData\Cisco\Cisco AnyConnect Secure Mobility Client\Network Access Manager\system\configuration.xml",
        $BadConfigurationFilePath="c:\ProgramData\Cisco\Cisco AnyConnect Secure Mobility Client\Network Access Manager\system\configuration_bad.xml",
        $GoodConfigurationFileDay = 20,
        $GoodConfigurationFileMonth = 10,
        $GoodConfigurationFileYear = 2016

    )


    Begin
    {
    
    }
    Process
    {
        if (Test-Path $ConfigurationFilePath){
            $ConfigurationFilelastModifiedDate = (Get-Item "$ConfigurationFilePath").LastWriteTime
            if ($ConfigurationFilelastModifiedDate.Day.Equals($GoodConfigurationFileDay) -and $ConfigurationFilelastModifiedDate.Month.Equals($GoodConfigurationFileMonth) -and $ConfigurationFilelastModifiedDate.Year.Equals($GoodConfigurationFileYear)){
                $result = 1
                Write-Verbose "Debug: $(Get-Date) - GOOD Configuration File was found: $ConfigurationFilelastModifiedDate"
                Write-Host "Success: $(Get-Date) - Configuration File Found With Good Date: $ConfigurationFilelastModifiedDate (needs to be $GoodConfigurationFileYear-$GoodConfigurationFileMonth-$GoodConfigurationFileDay)" -ForegroundColor Green
            }else {
                $result = 2
                Write-Verbose "Debug: $(Get-Date) - Bad Date Configuration File was found: $ConfigurationFilelastModifiedDate"
                Write-Host "Error: $(Get-Date) - Configuration File Found With Bad Date: $ConfigurationFilelastModifiedDate (needs to be $GoodConfigurationFileYear-$GoodConfigurationFileMonth-$GoodConfigurationFileDay)" -ForegroundColor Red
                $global:RestartPending++
            }
        }
        elseif (Test-Path $BadConfigurationFilePath) {
            $result = 2
            Write-Verbose "Debug: $(Get-Date) - BAD Configuration File was found"
        }
        else{
            $result = 3
            Write-Verbose "Debug: $(Get-Date) - NO Configuration File was found"
        }
    }
    End
    {
        $result
    }

}


function Set-ConfigurationFile
    {
    [CmdletBinding()]
    
    [OutputType([int])]
    Param
    (
        # Param1 help description
        [Parameter(Mandatory=$false,
                   ValueFromPipeline=$true,
                   Position=0)]
                   $var
        
    )

    Begin
    {
        Get-DMGTitleScreen ("CHECKING CISCO ANYCONNECT CONFIGURATION.XML...")
        $ConfigurationFileStatus = Get-ConfigurationFileStatus
        $ConfigurationFileDestination = "c:\ProgramData\Cisco\Cisco AnyConnect Secure Mobility Client\Network Access Manager\system\"
        $ConfigurationFileSource = "$toolslocation\tools\configuration.xml"
        
    }
    Process
    {
        if ($ConfigurationFileStatus -eq 1){
            Write-Host Success: $(Get-Date) - Configuration File is GOOD! -ForegroundColor Green
        }elseif ($ConfigurationFileStatus -eq 2){
            Write-Host Error: $(Get-Date) - Configuration File is Bad. Attempting to fix... -ForegroundColor Yellow
             if (Test-Path $ConfigurationFileDestination){
                Copy-DMGFile -filesource $ConfigurationFileSource -filedestination $ConfigurationFileDestination
                Restart-DMGService -Service nam -Verbose
	        } else{
                Write-Host Error: $(Get-Date) - Could not fix. AnyConnect is not installed! -ForegroundColor Red
            }
        }elseif ($ConfigurationFileStatus -eq 3){
            Write-Host Error: $(Get-Date) - No configuration file was found! Is AnyConnect installed? Attempting to fix... -ForegroundColor Red
             if (Test-Path $ConfigurationFileDestination){
                Copy-DMGFile -filesource $ConfigurationFileSource -filedestination $ConfigurationFileDestination
                Restart-DMGService -Service nam -Verbose
	        }else{
                Write-Host Error: $(Get-Date) - Could not fix. AnyConnect is not installed! -ForegroundColor Red
                }
        }
    }
    End
    {
        $result
    }
    
 }

 function Copy-DMGFile
    {
    [CmdletBinding()]
    
    [OutputType([int])]
    Param
    (
        # Param1 help description
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   Position=0)]
        $filesource,
        $filedestination
    )

    Begin
    {
        Write-Host "Copying $filesource..."
    }
    Process
    {
        try{
            copy $filesource $filedestination
            Write-Host "Success: $(Get-Date) - Copied $filesource" -foregroundcolor green
        }
        catch{
            Write-Host "Error: $(Get-Date) - Could Not Copy $filesource" -foregroundcolor red
            $global:errorsleft++
        }
    }
    End
    {

    }
    
 }


 function Restart-DMGService
    {
    [CmdletBinding()]
    
    [OutputType([int])]
    Param
    (
        # Param1 help description
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   Position=0)]
        $service
    )

    Begin
    {
        Write-Host Restarting $service Service...
    }
    Process
    {
        try{
            Restart-Service $service
            Write-Host "Success: $(Get-Date) - Restarted $service Service" -foregroundcolor green
        }
        catch{
            Write-Host "Error: $(Get-Date) - Could Not Restart $service Service" -foregroundcolor red
        }
    }
    End
    {
        $result
    }
    
 }


 function Is-DMGProgramInstalled {

    [CmdletBinding()]
    
    [OutputType([int])]
    Param
    (
        # Param1 help description
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   Position=0)]
        $program,
        $version
    )

    Begin
    {

    }
    Process
    {
        $x86 = ((Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall" -ErrorAction SilentlyContinue) |
            Where-Object { $_.GetValue( "DisplayName" ) -like "$program" -and $_.GetValue( "DisplayVersion" ) -like "$version"} );

        $x64 = ((Get-ChildItem "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall" -ErrorAction SilentlyContinue) |
            Where-Object { $_.GetValue( "DisplayName" ) -like "$program" -and $_.GetValue( "DisplayVersion" ) -like "$version"} );

        if($x86){
            Write-Host "Success: $(Get-Date) - x86 Version Found at $x86" -foregroundcolor green
            $result = $TRUE
            }
        elseif($x64){
            Write-Host "Success: $(Get-Date) - x64 Version Found at $x64" -foregroundcolor green
            $result = $TRUE
        }
        else{
            Write-Host "Error: $(Get-Date) - No x64 or x86 version found" -foregroundcolor green
            $result = $FALSE
        }
    }
    End
    {
        return $result
    }
}


function Is-DMGAllProgramInstalled {


    Begin
    {

        Get-DMGTitleScreen ("CHECKING FOR INSTALLED COMPONENTS")
        $csv = import-csv $toolslocation\tools\anyconnect_programs.csv 

    }
    Process
    {
        
        $csv | foreach-object {
          $Program = $_.Program
          $Version =$_.Version
          $Required =$_.Required
          $MSI =$_.MSI
          $RestartRequired =$_.RestartRequired

          Write-Host Checking $Program $Version $MSI ...
          
          if (Is-DMGProgramInstalled -program $Program -version $Version){
             Write-Host "Success: $(Get-Date) - $Program $Version is installed. Re-installing anyway just to make sure." -foregroundcolor green
             Install-DMGProgram -Program $Program -Version $Version -Required $Required -MSI $MSI -RestartRequired FALSE
          }else{
             Write-Host "Error: $(Get-Date) - $Program $version NOT installed" -foregroundcolor red
             Install-DMGProgram -Program $Program -Version $Version -Required $Required -MSI $MSI -RestartRequired $RestartRequired
          }
        }

    }
    End
    {
        
    }
}

Function Install-DMGProgram{
    [CmdletBinding()]
    
    [OutputType([int])]
    Param
    (
        # Param1 help description
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   Position=0)]
        $Program,
        $Version,
        $Required,
        $MSI,
        $RestartRequired

    )

    Begin
    {
        $csv = import-csv $toolslocation\tools\anyconnect_programs.csv
        $n=1

    }
    Process
    {
        
                       
        while($n -lt 3){

        Write-Host "Installing $Program $Version $MSI (Try $n of 2)..."
        Start-Process msiexec.exe -Wait -ArgumentList "/i `"$toolslocation\tools\Cisco AnyConnect 4.5.02036\$MSI`" REBOOT=ReallySupress /passive /qb"

        if(Is-DMGProgramInstalled -program $Program -version $Version){
           Write-Host "Success: $(Get-Date) - $Program $Version installed succesfully" -foregroundcolor green
           if ($RestartRequired -eq $TRUE){
                $global:RestartPending++
            }
            $n=4
        }else{
            $n++;
            Write-Host "Error: $(Get-Date) - $Program $Version could not be installed" -foregroundcolor red
            Remove-DMGHKCRRegKey -Program $Program
        }
    }

    }
    End
    {
        Write-Verbose "Debug: $(Get-Date) - Install-DMGProgram Exit Level: $n"
        if($n -eq 3){
            $global:errorsleft++
        }
    }
}


function Get-DMGErrorsLeft{

    Begin
    {

    }
    Process
    {
        if ($global:errorsleft -gt 0){ 
				$return = $TRUE
		}
		else{
			$return = $FALSE
			$registryPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Fix-DMGAnyConnect"
			$name = "Installed"
			$value = "1"
			$name2 = "Version"
			$value2 = "4.5.02036"

			if(!(Test-Path $registryPath))
			{
				New-Item -Path $registryPath -Force | Out-Null

				New-ItemProperty -Path $registryPath -Name $name -Value $value `
				-PropertyType DWORD -Force | Out-Null
				New-ItemProperty -Path $registryPath -Name $name2 -Value $value2 `
				-PropertyType String -Force | Out-Null
			}
			else {
				New-ItemProperty -Path $registryPath -Name $name -Value $value `
				-PropertyType DWORD -Force | Out-Null

				New-ItemProperty -Path $registryPath -Name $name2 -Value $value2 `
				-PropertyType String -Force | Out-Null
			}
		}
    }
    End
    {
        $return
    }

}
 

 function Get-FinalReport{

	 Get-DMGTitleScreen("FINAL REPORT")

	 if (Get-DMGErrorsLeft){
		 Write-Host "Error: $(Get-Date) - There were $global:errorsleft errors that could not be resolved!" -ForegroundColor Red
	 }else{
		 Write-Host "Success: $(Get-Date) - All Errors were resolved!" -ForegroundColor Green
	 }
     if ($global:RestartPending -gt 0){
        Write-Host "Warning: $(Get-Date) - A reboot is required!" -ForegroundColor Yellow
		#[System.Environment]::Exit(3010)
     }else{
        Write-Host "Success: $(Get-Date) - No reboot is required. The user may safely use the computer." -ForegroundColor Green
        #[System.Environment]::Exit(0)
     }
 }
 
 <#
 .Synopsis
    Short description
 .DESCRIPTION
    Long description
 .EXAMPLE
    Example of how to use this cmdlet
 .EXAMPLE
    Another example of how to use this cmdlet
 #>
 function Remove-DMGHKCRRegKey
 {
     [CmdletBinding()]
     
     Param
     (
         # Param1 help description
         [Parameter(Mandatory=$true,
                    ValueFromPipeline=$true,
                    Position=0)]
         $Program
     )
 
     Begin
     {

     }
     Process
     {

        New-PSDrive -PSProvider registry -Name HKCR -Root HKEY_CLASSES_ROOT -ErrorAction SilentlyContinue | Out-Null
        $PRODUCTS = Get-ChildItem "HKCR:Installer\Products"

        foreach ($PRODUCT in $PRODUCTS)
        {
            $PRODUCT_NAME = (Get-ItemProperty -Path ("HKCR:Installer\Products\" + $PRODUCT.PSChildName))."ProductName"

            if ($PRODUCT_NAME -like "*$Program*")
            {
                Write-Host "Removing Key: $Product.PSChildName "-" $PRODUCT_NAME ..." -ForegroundColor Yellow
                Remove-Item ("HKCR:Installer\Products\" + $PRODUCT.PSChildName) -Recurse
                Write-Host "Success: $(Get-Date) - $Product.PSChildName - $PRODUCT_NAME Removed" -foregroundcolor green
            }
        }

     }
     End
     {

     }
 }

function Stop-DMGServices
 {
     Begin
    {
        Get-DMGTitleScreen ("STOPPING SERVICES")
        $csv = import-csv $toolslocation\tools\anyconnect_services.csv
    }
    Process
    {
        $csv | foreach-object {
          $Service = $_.Service
          $Description =$_.Description
          Write-Host "Checking Service: $Description ($Service)..."
          try{
            (get-service -Name $Service).Stop()
            Write-Host "Success: $(Get-Date) - $Description ($Service) Stopped" -foregroundcolor green
            $result = $TRUE
            }
          catch{
            Write-Host "Error: $(Get-Date) - $Description ($Service) could not be Stopped" -foregroundcolor red
            $result = $FALSE
            }
        }
    }
    End
    {
        
    }
 }

function Invoke-DMGRemediateAnyConnect{
 Get-DMGWelcomeScreen("Cisco AnyConnect Fix Utility")
 Stop-DMGServices
 Is-DMGAllProgramInstalled
 Set-ConfigurationFile
 Get-FinalReport
}
 
 

UniversityLite Design Overview

I developed UniversityLite as a rapid deployment e-commerce tool to market university products and information to university students over the web using PHP. In other words, UniversityLite creates, deploys and maintains university websites automatically using custom PHP function. As of 2016, this tool has generated and maintains over 7.000 websites with each receiving about 1,000 views a month. The tool is based on the Model-View-Controller (MVC) architectural programming framework.

UniversityLite as a Function f

UniversityLite as a Function f

In its simplest form, UniversityLite can be understood as a function, where the input is simply the name of a university and the output is a full and complete website tailored to that university. Therefore the job of the function is to automatically create the database and website code for a full and complete website. The method by which this is accomplished is through the systematic gathering of information and the resulting interpretation and presentation of that information based on complex, but predictable logic.

UniversityLite is written primarily in PHP and uses SQL, Javascript and BASH to collect data automatically from sources such as Wikipedia, Google, Bing, Youtube, Amazon, eBay and NCES (and other governmental data sources) to slowly build an understanding about the environment in which to create the site. Most data is gathered through open APIs, such as Bing’s Image API which is used to automatically gather relevant images to a topic, or Amazon’s AWS API to generate products.

UniversityLite Sample REST Request

UniversityLite Sample API REST Request with Amazon AWS

Generating Variables

As the base input for content generation, the tool was fed 7,000 university names that I gathered from the National Center for Education Statistics (NCES).

Let’s look at one of those university names, “Monroe Community College”, as an example to see how the tool generates basic information about the university.

Iterate Through Each University Name

Insert "Monroe Community College" into Algorithm

Insert “Monroe Community College” into Algorithm

First, the tool must determine basic information about the string, “Monroe Community College”. We can reasonably assume this will be the name of a university and will develop an array of queries we will use that represent how the university might be referenced on the internet. First, the tool sees the substring, “Community College” and can reasonably determine we might also call the college by its acronym, “MCC”. This logic is used to generate up to four different unique names each university might go by, such as “Brockport University” from the string “State University of New York at Brockport”.

Now that the tool has an array of search terms, it can get to work using intelligent functions with several of the open APIs mentioned to build a SQL database of variables associated with this university. It would be exhaustive to go through the process of all variables generated as each university name generates thousands of variables. Yet, here are some obvious ones along with the techniques used to gather them:

  • Preferred subdomain such as “mcc.universitylite.com” (query NCES API for website address and strip out the subdomain, ‘mcc’)
  • University Colors such as “Blue and Gold”, as hex values (strippng relevant color names from wikipedia_infobar_request function and sending them to a color_names_to_hex function).
  • City, State and Zip of university, such as “Rochester”, “NY” and “14626” (query NCES API for these values)

…and this process goes on and on to gather such information as demographics, weather, local attractions (such as “Monroe County Museum”), apparel categories (such as “MCC Men’s Pants”), degree programs and product categories based on degree programs (such as “Nursing Supplies”).

UniversityLite builds the SQL database with these variables, organizing each major category into a relevant table. To streamline access and storage, we break each table into the data, a data dictionary describing the data and a variable dictionary (as indicated by the VARS and DD tables below).

Universitylite Database Sample

Universitylite Database Sample

Generating Content

Now that UniversityLite has created our SQL database of a couple thousand variables associated to “Monroe Community College”, we can begin to generate content associated to Monroe County College as a university.

For starters, the functions will begin to create the skeleton content for the website, such as colors, titles, logos and basic content. When displaying products, for example men’s apparel, we use variables such as the university’s name, colors, demographics and degree programs to query the Amazon AWS API.

Function for Sports Weight

Function for Sports Weight

In this example, we determine the university total student population is 52% male, which indicates it is fine to go ahead and generate the REST request to Amazon for a male category (as opposed to an all female university, for example). The variables also show us this university has several sports teams, so we decide to put weight on men’s apparel that is sports related. This weight comes in the form of an array of possible search terms that are given mathematical weights to them based on the funding the sports teams receive (aka their size relative to the overall university’s budget).

The chosen array search term, which let’s say is “MCC Men’s Basketball Activewear” is used to build an Amazon REST request by referencing a custom REST generator build for Men’s Apparel (to assure we are only returning items we want within a certain scope of Men’s apparel). This logic will be repeated across the scope of probability we determine from the sports weight function (and other similarly performing functions) to generate a product list which reflects the weight of sports and other variables actually represented by university statistical data.

REST Request URI Generation

REST Request URI Generation

Once we finally have created our REST request as a string to send to Amazon’s API, we pass it to Amazon, which will in turn provide us an XML database of product information. This will then be imported into the UniversityLite server as a local file for speed in calling the same product string again (dumped after 24 hours for the sake of having relevant pricing models). Each XML comes back between 10MB-30MB, which can quickly grow to over 10GB of XML data in a couple days, depending on how actively the site is being used by actual users. Management of this data with 7,000 active sites (not to mention all the other data generated such as images) requires tight tolerances of data purging to assure the servers that run UniversityLite do so smoothly.

Merging all of the XMLs related to the probability function for sports weight, etc. (using a custom XML Merge function just for this purpose) creates a final XML locally on our server that represents the final collection of information that will be used to display one collection of MCC apparel (in this case, MCC Men’s Apparel).

A sampled section of such a merged XML can be seen below.

Amazon XML Data Return Sample

Amazon XML Data Return Sample

This type of process is used for a couple dozen different product categories which are used through the site. In Monroe Community College’s case, it used for both men and women apparel and merchandise, in addition to several product categories that relate to variables relatable to the university.

Of the greatest importance is applying this logic to textbooks a student could purchase. Because we don’t know what textbooks a student might desire, the search query becomes the basis for the REST request. The only major difference is that the query is passed to a function expecting book titles or ISBNs specifically. One unique aspect, however, is the inclusion of the UniversityLite Aggresearch™. This is a very custom solution I put in place to assure that when UniversityLite Aggresearch™ is chosen, the very lowest priced book opportunities are returned. The basis for this logic is searching several possibilities from multiple vendors and inserting those into an array where we can sort it by the lowest price. The result in many cases is that UniversityLite Aggresearch™ often returns books at competitive prices versus other book searching algorithms found on the internet.

UniversityLite Aggresearch (TM) Search Example

UniversityLite Aggresearch (TM) Search Example

One other major area of the site that falls into the same category is the ability for students to post and sell their own books. Although the process is intended to look seamless to a user, this area has completely different functionality. The buying and selling mechanism is based off the WPAdverts – Classifieds Plugin. A major change I made to the plugin is the ability to automatically fill in the classified posting with Author, Title, ISBN, pictures of the book and suggested prices of the book so the user does not have to. Simply entering in either the ISBN or Title will auto populate all relevant fields. This is a major improvement to the WPAdverts Plugin, and one that makes the plugin unique to UniversityLite. In addition, the e-mail domain variable is used to force users to login with a valid university email to sell a book. This creates a niche and protected market for students at a particular university where they know books are being bought and sold by verified students.

UniversityLite Automated Book Sale Feature

UniversityLite Automated Book Sale Feature

Aside from product generation, UniversityLite also determines useful characteristics about the university that create community pages that automatically update. One such page displays recent YouTube sports videos associated to the university, while another displays recent videos from university students based on a geolocation search to the API. In the former case, the sports team used for the YouTube API search is determined using the wikipedia API and in the geolocation is based on an NCES API call where we query the longitude and latitude. The resulting geolocation is passed to the code snippet below which in turns builds a list of YouTube videos we can insert.

UniversityLite Youtube GeoLocation Code Snippet

UniversityLite Youtube GeoLocation Code Snippet

Another major component of the generated site are dozens of articles, reports and graphs that are automatically “written” for the purpose of displaying information about the university. This is used to drive traffic and interest to the site with unique content. A sample of such an article can be seen below.

UniversityLite News Article Generation Sample

UniversityLite News Article Generation Sample

The many graphs for the article are created using several different methods, most of which use the NCES and Wikipedia variable results now in the SQL Database. In the sample below, we iterate through available variables to build the foundation for a JPEG graph that will be displayed on the site.

A sentence within the article might follow logic something like this code snippet below.

UniversityLite Sentence Generation Sample

UniversityLite Sentence Generation Sample

Load Balancing

UniversityLite is load balanced across three Ubuntu 14.04 LTS virtual servers located in New York, NY with one backup server in Webster, NY. The three primary servers are balanced alphabetically by university name. There are 7,000 websites available as subdomains off of the universitylite.com domain (such as rit.universitylite.com). Each subdomain is given resources by the server only if actively being viewed. As seen in the image below, an arbitrary university named ‘David’s University College’ would be hosted on Server1. Because the functions and algorithms to create any of the A-Z universities resides on each ot the servers, the balancing is done for resource balancing, not necessarily content balancing.

UniversityLite Server Balance

UniversityLite Server Balance

Because of the large set of active subdomains, load balancing through DNS would be cumbersome and time consuming as partial wildcards (such as a*.universitylite.com) in a DNS zone are not defined behavior within the RFC. Manually adding a record for rit.universitylite.com, for example, would also require us to add a manual record for the 6,999 other subdomains. Therefor a DNS zone can only practicaly be used with full wildcards such as *.universitylite.com to to one of the universitylite.com servers. As a result, load balancing is handled first by the Apache virtualhost file, and then subsequently by PHP logic once the subdomain is called.

As seen in the function below, we can query the SQL table once a subdomain passed to the UniversityLite webserver. If the subdomain is found as a SQL entry, we can go ahead and assume the server referenced is the correct load balanced server. However, if it isn’t, we a) either can assume it’s load balanced on one of the other servers or b) it is simply not a valid subdomain anywhere on the site. For example, trying to go to gaboldygooky.universitylite.com will fail all load balancing checks and return an error to the browser.

UniversityLite PHP Load Balance Example

UniversityLite PHP Load Balance Example

In this way, I have created a dynamic load balancing system that allows sites to be added and removed ad-hoc without having to alter DNS. Of course, this is just general domain load balancing, and we must further balance the content itself. There are, afterall, 7,000 different subdomains with uniquely generated HTML, photos, graphs, etc. The easiest way I found to handle this is by having UniversityLite just be one central source of programming code, and to allow other content to be displayed dynamically when required. That is not to say, however, that each site can be dynamically created each time it is viewed, but we can remove some redundancy from the equation.

Below are areas of dialy generated content that CAN be shared between them, assuming the same calculator with description, etc. might be viewed by more than one subdomain. This data alone amounts to about 10GB of freshly generated content for a 24 hour period for the sites on server1. If we did not consolidate this data, this could easily have grown to 23TB of data in just 24 hours on one server.

Directory List of ~/www/universitylite.com/public_html/sites/shared$:

drwxr-xr-x 2 maiolo99 maiolo99 4096 Oct 20 16:49 adpics
drwxr-xr-x 7 maiolo99 maiolo99 4096 Oct 20 21:29 amazon_xml
drwxr-xr-x 2 maiolo99 maiolo99 4096 Sep 23 18:07 apparel_images
drwxrwx--- 2 maiolo99 maiolo99 4096 Sep 18 16:54 avatars
drwxrwx--- 2 maiolo99 maiolo99 4096 Jul 26 23:29 holidays
drwxr-xr-x 356 maiolo99 maiolo99 20480 Oct 8 00:30 ms_images
drwxr-xr-x 2 maiolo99 maiolo99 7966720 Oct 21 16:51 product_images

However, sometimes there is only so much we can do. As seen in the directory listing below, these files are created uniquely for one of the subdomains (mcc.universitylite.com) below. This information is perfectly unique to Monroe Community College, and there is nothing we can really do about it.

Directory Listing of ~/www/universitylite.com/public_html/sites/MCCTextbooks:

./daily_message:
MonroeCommunityCollege_20161019_daily.txt

./graphs-2016-09-19:
graph_MCC_1473753800.jpg
graph_MCC_1473813698.jpg
graph_MCC_1474457188.jpg
graph_MCC_1474581272.jpg
graph_MCC_All Instructional Staff Total_1473662381.jpg
graph_MCC_All Instructional Staff Total_1473662723.jpg
graph_MCC_All Instructional Staff Total_1473722731.jpg
graph_MCC_All Instructional Staff Total_1473773589.jpg
graph_MCC_All Instructional Staff Total_1473791287.jpg
graph_MCC_All Instructional Staff Total_1473813691.jpg
graph_MCC_All Instructional Staff Total_1474227934.jpg
graph_MCC_All Instructional Staff Total_1474457249.jpg
graph_MCC_All Instructional Staff Total.jpg
graph_MCC_Assistant Professor_1473662380.jpg
graph_MCC_Assistant Professor_1473662722.jpg
graph_MCC_Assistant Professor_1473722730.jpg
graph_MCC_Assistant Professor_1473773588.jpg
graph_MCC_Assistant Professor_1473791286.jpg
graph_MCC_Assistant Professor_1473813690.jpg
graph_MCC_Assistant Professor_1474227933.jpg
graph_MCC_Assistant Professor_1474457248.jpg
graph_MCC_Assistant Professor.jpg
graph_MCC_Associate Professor_1473662379.jpg
graph_MCC_Associate Professor_1473662722.jpg
graph_MCC_Associate Professor_1473722730.jpg
graph_MCC_Associate Professor_1473773588.jpg
graph_MCC_Associate Professor_1473791286.jpg
graph_MCC_Associate Professor_1473813690.jpg
graph_MCC_Associate Professor_1474227932.jpg
graph_MCC_Associate Professor_1474457248.jpg
graph_MCC_Associate Professor.jpg
graph_MCC_degrees_by_race.jpg
graph_MCC_demographics_1474316106.jpg
graph_MCC_demographics_1474316169.jpg
graph_MCC_demographics_1474316231.jpg
graph_MCC_demographics_1474316252.jpg
graph_MCC_demographics_1474316321.jpg
graph_MCC_demographics_1474316741.jpg
graph_MCC_demographics_1474316754.jpg
graph_MCC_demographics_1474316757.jpg
graph_MCC_demographics_1474316908.jpg
graph_MCC_demographics_1474317796.jpg
graph_MCC_demographics_1474318228.jpg
graph_MCC_demographics_1474318400.jpg
graph_MCC_demographics_1474318536.jpg
graph_MCC_demographics_1474318968.jpg
graph_MCC_demographics_1474318969.jpg
graph_MCC_demographics_1474321987.jpg
graph_MCC_demographics_1474323294.jpg
graph_MCC_demographics_1474329291.jpg
graph_MCC_demographics_1474329312.jpg
graph_MCC_demographics_1474332642.jpg
graph_MCC_female_students_by_age.jpg
graph_MCC_graduates_to_enrolled_by_race_ratio.jpg
graph_MCC_Instructor_1473662380.jpg
graph_MCC_Instructor_1473662722.jpg
graph_MCC_Instructor_1473722731.jpg
graph_MCC_Instructor_1473773588.jpg
graph_MCC_Instructor_1473791286.jpg
graph_MCC_Instructor_1473813690.jpg
graph_MCC_Instructor_1474227933.jpg
graph_MCC_Instructor_1474457249.jpg
graph_MCC_instructor_annual_income.jpg
graph_MCC_Instructor.jpg
graph_MCC_Lecturer_1473662380.jpg
graph_MCC_Lecturer_1473662722.jpg
graph_MCC_Lecturer_1473722731.jpg
graph_MCC_Lecturer_1473773589.jpg
graph_MCC_Lecturer_1473791287.jpg
graph_MCC_Lecturer_1473813691.jpg
graph_MCC_Lecturer_1474227933.jpg
graph_MCC_Lecturer_1474457249.jpg
graph_MCC_Lecturer.jpg
graph_MCC_male_students_by_age.jpg
graph_MCC_Professor_1473662379.jpg
graph_MCC_Professor_1473662721.jpg
graph_MCC_Professor_1473722730.jpg
graph_MCC_Professor_1473773588.jpg
graph_MCC_Professor_1473791286.jpg
graph_MCC_Professor_1473813690.jpg
graph_MCC_Professor_1474227932.jpg
graph_MCC_Professor_1474457247.jpg
graph_MCC_Professor.jpg
graph_MCC_students_by_age.jpg
staff_sallary_report_graph_MCC_0.jpg

./weather:
Rochester_NY_Weather_2016-09-19.txt

./wiki_infobox-2016-10-19:
Bevier20Memorial20Building(BevierMemorialBuilding)_intro_request_phase2.xml
Bevier20Memorial20Building_intro_request_phase1.xml
Bridge20Square20Historic20District(BridgeSquareHistoricDistrict)_intro_request_phase2.xml
Bridge20Square20Historic20District_intro_request_phase1.xml
Campbell-Whittlesey20House(Campbell-WhittleseyHouse)_intro_request_phase2.xml
Campbell-Whittlesey20House_intro_request_phase1.xml
First20Presbyterian20Church20(Rochester20New20York)(FirstPresbyterianChurch(RochesterNewYork))_intro_request_phase2.xml
First20Presbyterian20Church20Rochester20New20York_intro_request_phase1.xml
Hervey20Ely20House(HerveyElyHouse)_intro_request_phase2.xml
Hervey20Ely20House_intro_request_phase1.xml
Immaculate20Conception20Church20(Rochester20New20York)(ImmaculateConceptionChurch(RochesterNewYork))_intro_request_phase2.xml
Immaculate20Conception20Church20Rochester20New20York_intro_request_phase1.xml
Jonathan20Child20House2020BrewsterBurke20House20Historic20District_intro_request_phase1.xml
Jonathan20Child20House2020BrewsterBurke20House20Historic20District(JonathanChildHouseampBrewsterBurkeHouseHistoricDistrict)_intro_request_phase2.xml
Main2020Oak20RIRTR20station_intro_request_phase1.xml
Main2020Oak20(RIRTR20station)(Main)_intro_request_phase2.xml
Monroe20Community20College20_intro_request_phase1.xml
Monroe20Community20College20(MonroeCommunityCollege)_intro_request_phase2.xml
Monroe20Community20College20Sports_intro_request_phase1.xml
Monroe20Community20College20Sports(UniversityofLouisianaatMonroe)_intro_request_phase2.xml
Monroe20Community20College20Team_intro_request_phase1.xml
Monroe20Community20College20Team(UniversityofLouisianaatMonroe)_intro_request_phase2.xml
Monroe20Community20College20Tribunes20_intro_request_phase1.xml
Monroe20Community20College20Tribunes20(ListofcollegeathleticprogramsinNewYork)_intro_request_phase2.xml
Monroe20Community20College20Tribunes_intro_request_phase1.xml
Monroe20Community20College20Tribunes(ListofcollegeathleticprogramsinNewYork)_intro_request_phase2.xml
Monroe20Community20College_intro_request_phase1.xml
Monroe20Community20College(MonroeCommunityCollege)_intro_request_phase2.xml
MonroeCommunityCollege_infobox.xml
Nick20Tahou20Hots_intro_request_phase1.xml
Nick20Tahou20Hots(NickTahouHots)_intro_request_phase2.xml
RochesterNewYork_infobox.xml
RochesterNY_geosearch.json
Third20Ward20Historic20District20Rochester20New20York_intro_request_phase1.xml
Third20Ward20Historic20District20(Rochester20New20York)(ThirdWardHistoricDistrict(RochesterNewYork))_intro_request_phase2.xml

./youtube-2016-10-19:
BevierMemorialBuildingRochesterNY_youtube_2016-10-19.txt
BridgeSquareHistoricDistrictRochesterNY_youtube_2016-10-19.txt
Campbell-WhittleseyHouseRochesterNY_youtube_2016-10-19.txt
FirstPresbyterianChurchRochesterNewYork_youtube_2016-10-19.txt
geosearch-431012652C-77608488--searchquery-_youtube.txt
geosearch-Rochester-NY--searchquery-RochesterNYMusic_youtube.txt
HerveyElyHouseRochesterNY_youtube_2016-10-19.txt
ImmaculateConceptionChurchRochesterNewYork_youtube_2016-10-19.txt
JonathanChildHouseBrewsterBurkeHouseHistoricDistrictRochesterNY_youtube_2016-10-19.txt
MainOakRIRTRstationRochesterNY_youtube_2016-10-19.txt
MonroeCommunityCollegeSports_youtube_2016-10-19.txt
MonroeCommunityCollege_youtube_2016-10-19.txt
NickTahouHotsRochesterNY_youtube_2016-10-19.txt
ThirdWardHistoricDistrictRochesterNewYork_youtube_2016-10-19.txt

The best we can do with this data is purge it (beyond what we might want to keep for historical purposes). We do this by adding historically usable data into a SQL table for that university, and simply deleting the rest.

The load balancing techniques used on UniversityLite have allowed us to maintain 7,000 unique websites, on only three servers.

Summary

In all, UniversityLite was written in over 800 PHP functions and algorithms, in conjunction with Javascript, SQL, BASH and HTML to provide the automated creation of a website based on a search string such as “Monroe Community College”. The result is a collection of over 7,000 automatically created and maintained websites that generate traffic and income in a most automatically landscape.

Now that I have the groundwork for this structure in place, I am already applying it to other markets such as automobile parts and niche Hawaiian products.

For more information, or a closer demo of my work or code, please feel free to contact me.