Best Practices Analyzer(s) for Multiple Servers

Earlier this week I posted an article on how to run the various Best Practices Analyzers (BPAs for short) on a single server. For this post we are going to take what we learned from that and apply it to multiple servers. The hardest part about this script is that the BPA cmdlets need to be run locally. That’s right, we cannot reference another server name from the cmdlets. Running cmdlets remotely adds a layer of complexity not only from the perspective of connecting to a remote server that may or may not allow that connection, but also how to process the reports locally and copy them to a central source to be utilized for a health check.

Step One: PowerShell Connection To Server – To Run BPA Locally

One of the important steps in creating this script is creating a connection to the server in order to run the BPAs locally. The reason is that the BPAs cannot be connected to remotely. In order to do this, we need to first get a valid set of credentials to use for authenticating with a remote server:

write-host "Enter a user name and password for a Domain Admin.  "
$Credentials = Get-Credential


Using the above credentials, we can connect to the remote server using these two lines:

$Session = New-PSSession -ComputerName $Server -credential $Credentials
Invoke-Command -Session $Session -Scriptblock $ScriptBlock2008

Notice in the Scriptblock being executed is for Windows 2008. An additional script block for 2012+ servers will need to be created because the BPAs for Windows 2008 is different from 2012 and 2016:

Invoke-Command -Session $Session -Scriptblock $ScriptBlock2012

If the server is local, then we don’t need to run use the session parameter and instead use these lines (depending on the local server version):

Invoke-Command -Scriptblock $ScriptBlock2008 
Invoke-Command -Scriptblock $ScriptBlock2012

In order to check if the server where the BPAs need to be run on is we do two checks. One is to see if the server is remote or local and the other is if the server is available. First we check if the server is local, then we can assume its available. If it is not, then check to see if it is available with the ‘Test-Connection’ cmdlets.

# Check server availability
$LocalServer = $env:computername
if ($LocalServer -eq $Server) {
    $LiveServer = $True    
} Else {
    $LiveServer = Test-Connection -Cn $Server -BufferSize 16 -Count 1 -ea 0 -quiet
}

Step Two: Operating System Check

Next we’ll need to build an OS version check so that the appropriate script block is run and the correct BPA check code is run.

#Check OS Version
$Version = ((Get-WMIObject win32_OperatingSystem -ComputerName $Server).Version).Split([char]0x002E)
$PartOne = $Version[0];$PartTwo = $Version[1]
$Ver = $PartOne+"."+$PartTwo

Step Three: Loop for Multiple Servers

Because we need to run BPAs on multiple servers, a Foreach loop will be needed to loop through each server. First a variable will be needed to get a list of all the Domain Controllers in Active Directory (single Forest) and store it in the $DCs variable. Then the loop is constructed off that:

$DCs = Get-ADDomainController -Filter *

Foreach ($line in $DCs) {
....code bulk goes here ....
}

Final Step: Cleanup and Reporting

Once all of the BPA files have been generated on the remote servers, we need to copy them to a central location as well as remove the files after they are copied off the servers. For this, we’ll loop back through the DCs, copy them and store then locally.

Foreach ($line in $DCs) {

    # Variables for Loop
    $Server = $Line.Name

    # Copy remote results files to local server [HTML only]
    write-host " "
    write-host "Copying files from $Server......." -ForegroundColor Green
    write-host " "
    write-host "Files will be copied to $ResultsPath" -Foregroundcolor Yellow
    Copy-Item \\$Server\c$\BPATesting\*.html $ResultsPath
    # Uncomment the below line if you want the CSV files too
    # Copy-Item \\$Server\c$\BPATesting\*.CSV $FilePath

    # Cleanup of remotely generated files
    write-host " "
    Write-Host "Performing cleanup, removing test directory and files on server $Server......" -ForegroundColor Yellow
    Remove-Item \\$Server\c$\BPATesting\*.* -erroraction SilentlyContinue
    Remove-Item \\$Server\c$\BPATesting -erroraction SilentlyContinue

    # Pausing for report creation (on remote server)
    Write-host " "
    Write-host "Pausing for reports to finish on the remote servers......" -ForegroundColor Yellow
    Start-Sleep 60

}

Final Code
Putting all of the above steps together (and adding a few extras), we now have a script that can query Windows 2008 R2, Windows 2012, Windows 2012 R2 and Windows 2016 servers for the Best Practices Analyzers. The caveat is that it only works with Domain controllers, but in theory, if you modify the $DCs variable and provide your own list of servers, the script would work against those as well.

#############################################################
# BPA Report for local Server                               #
#                                                           #
# Author:     Damian Scoles                                 #
# Purpose:    To generate an HTML version of all available  #
#                  BPA reports on the local server.         #
#                                                           #
# Version:      1.8                                         #
# History:      1.8 - Fixed bugs, test connections, etc.    #
#               1.7 - MultiServer Support (2012 and 2008)   #
#               1.6 - Correct some bugs                     #
#               1.5 - Added support for Windows 2008 R2     #
#                                                           #
# Wish List                                                 #
#                                                           #
#############################################################

# Define Code to be executed remotely on 2008 R2 DCs

$ScriptBlock2008 = {

    Function GenerateHTMLReport {

        param($name,$CSVFile,$FilePath)

        # Variables
        $css = "<style>"
        $css = $css + "TABLE{border-width: 1px;border-style: solid;border-color: black;border-collapse: collapse;}"
        $css= $css + "TH{border-width: 1px;padding: 2px;border-style: solid;border-color: black;}"
        $css = $css + "TD{border-width: 1px;padding: 2px;border-style: solid;border-color: black;}"
        $css = $css + "</style>"
        $Server = $env:computername
        $Date = get-date -Format "MM.dd.yyyy-hh.mm-tt"
        # $Path = $FilePath+"\Test\$name-BPA-Report-$Date.html"
        $Path = $FilePath+"\$name-BPA-Report-$Server-$Date.html"

        Write-Verbose "HTML report will be saved $FilePath"
        # $CSVFile
        $Info = $CSVFile | ConvertTo-Html -Fragment -As Table | Out-String
        $Report = ConvertTo-Html -Title "BPA Report - $Name - $Server" -Head "<h1>BPA Report - $Name for $Server</h1><br>This report was run on: $Date" -Body "$Info $Css"
        $Report | Out-File $path
        write-host "The file " -ForegroundColor White -nonewline
        write-host "$Path" -foregroundcolor Cyan -nonewline
        write-host " has been created." -ForegroundColor White
    }

    # Load PowerShell Modules
    Import-Module ServerManager
    Import-Module BestPractices

    # Initialize test Array variable
    $BPA = @() 

    # Check the BPAs on the remote server
    if ((Get-WindowsFeature Application-Server).Installed) {$BPA += "Microsoft/Windows/ApplicationServer"} 
    if ((Get-WindowsFeature AD-Certificate).Installed) {$BPA += "Microsoft/Windows/CertificateServices"} 
    if ((Get-WindowsFeature DHCP).Installed) {$BPA += "Microsoft/Windows/DHCP"} 
    if ((Get-WindowsFeature AD-Domain-Services).Installed) {$BPA += "Microsoft/Windows/DirectoryServices"} 
    if ((Get-WindowsFeature DNS).Installed) {$BPA += "Microsoft/Windows/DNSServer"} 
    if ((Get-WindowsFeature File-Services).Installed) {$BPA += "Microsoft/Windows/FileServices"} 
    if ((Get-WindowsFeature Hyper-V).Installed) {$BPA += "Microsoft/Windows/Hyper-V"} 
    If ((Get-WindowsFeature NPAS).Installed) {$BPA += "Microsoft/Windows/NPAS"} 
    If ((Get-WindowsFeature Remote-Desktop-Services).Installed) {$BPA += "Microsoft/Windows/TerminalServices"} 
    If ((Get-WindowsFeature Web-Server).Installed) {$BPA += "Microsoft/Windows/WebServer"}
    If ((Get-WindowsFeature OOB-WSUS).Installed) {$BPA += "Microsoft/Windows/WSUS"}
            
    # Create test result path
    $PathTest = Test-Path C:\BPATesting
    if ($PathTest -eq $False) {
        MD C:\BPATesting
        $Verify = Test-Path C:\BPATesting
        if ($verify) {
            write-host "The directory C:\BPATesting has been created on $Server." -ForegroundColor White
        }
    }

    # Change Current Directory
    cd c:\BPATesting
            
    # Variables
    $Filepath = (Get-Item -Path ".\" -Verbose).FullName
    $Server = $env:computername

    foreach ($line in $bpa) {
        $Name = $Line.Replace("Microsoft/Windows/","") 
        $Model = $Line
        $CSV = "BPA-Results-$Name-$Server.csv"
        Invoke-BPAModel $Model -ErrorAction SilentlyContinue
        Get-BPAResult -BestPracticesModelId $Model -ErrorAction SilentlyContinue | Where-Object {$_.Problem -ne $Null} | Select-Object ResultNumber,Severity,Category,Title,Problem,Impact,Resolution | Export-Csv $CSV -NoTypeInformation -Encoding UTF8
        $CSVFile = Import-CSV $CSV
        GenerateHTMLReport $Name $CSVFile $Filepath
    }


}

$ScriptBlock2012 = {

    # Variables for the loop
    $Server = $Line.Name
    $LiveServer = $False
    $PathTest = $False

    Function GenerateHTMLReport {

        param($name,$CSVFile,$FilePath)

        # Variables
        $css = "<style>"
        $css = $css + "TABLE{border-width: 1px;border-style: solid;border-color: black;border-collapse: collapse;}"
        $css= $css + "TH{border-width: 1px;padding: 2px;border-style: solid;border-color: black;}"
        $css = $css + "TD{border-width: 1px;padding: 2px;border-style: solid;border-color: black;}"
        $css = $css + "</style>"
        $Server = $env:computername
        $Date = get-date -Format "MM.dd.yyyy-hh.mm-tt"
        # $Path = $FilePath+"\Test\$name-BPA-Report-$Date.html"
        $Path = $FilePath+"\$name-BPA-Report-$Server-$Date.html"

        Write-Verbose "HTML report will be saved $FilePath"
        # $CSVFile
        $Info = $CSVFile | ConvertTo-Html -Fragment -As Table | Out-String
        $Report = ConvertTo-Html -Title "BPA Report - $Name - $Server" -Head "<h1>BPA Report - $Name for $Server</h1><br>This report was run on: $Date" -Body "$Info $Css"
        $Report | Out-File $path
        write-host "The file " -ForegroundColor White -nonewline
        write-host "$Path" -foregroundcolor Cyan -nonewline
        write-host " has been created." -ForegroundColor White
    }

    $PathTest = Test-Path C:\BPATesting
    if ($PathTest -eq $False) {
        MD C:\BPATesting
        $Verify = Test-Path C:\BPATesting
        if ($verify) {
            write-host "The directory C:\BPATesting has been created." -ForegroundColor White
        }
    }

    # Change Current Directory
    cd c:\BPATesting
            
    # Variables
    $Filepath = (Get-Item -Path ".\" -Verbose).FullName
    $Server = $env:computername

    $BPA = (Get-WindowsFeature).BestPracticesModelId

    foreach ($line in $bpa) {
        $Installed = $False

        if ($line -ne "") {
            $Installed = (Get-WindowsFeature | where {$_.BestPracticesModelID -eq $Line}).Installed
        }

        if ($Installed -eq $True) {
            [string]$Test = $Line
            if ($Installed = $True) {
                $BPA = Get-BPAModel $Test
	            $Name = $BPA.Name
                $Model = $BPA.ID
                $CSV = "BPA-Results-$Name.csv"
                Invoke-BPAModel $Model -ErrorAction SilentlyContinue
                Get-BPAResult -Model $Model -ErrorAction SilentlyContinue | Where-Object {$_.Problem -ne $Null} | Select-Object ResultNumber,Severity,Category,Title,Problem,Impact,Resolution | Export-Csv $CSV -NoTypeInformation -Encoding UTF8
                $CSVFile = Import-CSV $CSV
                GenerateHTMLReport $Name $CSVFile $Filepath
            }
        } 
    }
}

# Clear the screen
CLS

# Import the Active Directory module
Import-Module activedirectory

# Variable definition
# $RootPath = 'C:\BPATesting'
$ResultsPath = (Get-Item -Path ".\" -Verbose).FullName
$DCs = Get-ADDomainController -Filter *
$Server = $DCs.Name
$LocalServer = $env:computername

# Prompt for credentials ('flashing' red / white)
$N = 1
Do {
    If (($N -eq 1) -or ($N -eq 3) -or ($N -eq 5) -or ($N -eq 7)) {
        [system.console]::ForegroundColor = "White"
        write-host "Enter a user name and password for a Domain Admin`r" -nonewline
        sleep 1
        $N++
    }
    If (($N -eq 2) -or ($N -eq 4) -or ($N -eq 6)) {
        [system.console]::ForegroundColor = "red"
        write-host "Enter a user name and password for a Domain Admin`r" -nonewline
        sleep 1
        $N++
    }
} While ($N -lt 8)
write-host "Enter a user name and password for a Domain Admin." -ForegroundColor Yellow
$Credentials = Get-Credential

Foreach ($line in $DCs) {

    # Variables for the loop
    $Server = $Null
    $Server = $Line.Name
    $LiveServer = $False
    $BPA = $Null
    $CSVFile = $Null
    $CSV = $Null
    $Model = $Null
    $PathTest = $False

    # Check server availability
    if ($LocalServer -eq $Server) {
        $LiveServer = $True    
    } Else {
        $LiveServer = Test-Connection -Cn $Server -BufferSize 16 -Count 1 -ea 0 -quiet
    }

    If ($LiveServer) {

        Write-Host " "
        Write-Host "Processing BPAs on the server " -foregroundcolor White -NoNewLine
        Write-Host "$Server." -foregroundcolor Green
        Write-Host " "

        #Check OS Version
        # $Ver = (Get-WMIObject win32_OperatingSystem).Version
        $Version = ((Get-WMIObject win32_OperatingSystem -ComputerName $Server).Version).Split([char]0x002E)
        $PartOne = $Version[0];$PartTwo = $Version[1]
        $Ver = $PartOne+"."+$PartTwo

        # Create PS Sesstion to remote server
        $Session = New-PSSession -ComputerName $Server -credential $Credentials

        if ([Decimal]$ver -ge 6.1) {
            if ([Decimal]$ver -lt 6.2) {

                #######################
                # Windows 2008 R2 SP1 #
                #######################
                If ($Server -ne $LocalServer) {

                    # Enter-PSSession $Server -credential $Credentials
                    Invoke-Command -Session $Session -Scriptblock $ScriptBlock2008

                    # Close PS Session to remote server
                    Remove-PSSession $Session
        
                } Else {
    
                    # Invoke the BPA Code locally
                    Invoke-Command -Scriptblock $ScriptBlock2008
                }
            }
        }

        if ([Decimal]$ver -ge 6.2) {

            #################################
            # Windows 2012 and 2016 Section #
            #################################

            If ($Server -ne $LocalServer) {

                # Enter-PSSession $Server -credential $Credentials
                Invoke-Command -Session $Session -Scriptblock $ScriptBlock2012

                # Close PS Session to remote server
                Remove-PSSession $Session
        
            } Else {
    
                # Invoke the BPA Code locally
                Invoke-Command -Scriptblock $ScriptBlock2012
            }
        }
    } else {
        Write-Host " "
        Write-Host "The server $Server cannot be contacted. Please check the server." -ForegroundColor Red
        Write-Host " "
    }
}

# Pausing for report creation (on remote server)
Write-host " "
Write-host "Pausing for reports to finish on the remote servers......" -ForegroundColor Yellow
Start-Sleep 60

Foreach ($line in $DCs) {

    # Variables for Loop
    $Server = $Line.Name
#   $Filepath = (Get-Item -Path ".\" -Verbose).FullName
#   Write-host "The filepath is $FilePAth." -foregroundcolor white

    # Copy remote results files to local server [HTML only]
    write-host " "
    write-host "Copying files from $Server......." -ForegroundColor Green
    write-host " "
    # $Filepath = (Get-Item -Path ".\" -Verbose).FullName
    # dir \\$Server\c$\BPATesting\
    write-host "Files will be copied to $ResultsPath" -Foregroundcolor Yellow
    Copy-Item \\$Server\c$\BPATesting\*.html $ResultsPath
    # Uncomment the below line if you want the CSV files too
    # Copy-Item \\$Server\c$\BPATesting\*.CSV $FilePath

    # Cleanup of remotely generated files
    write-host " "
    Write-Host "Performing cleanup, removing test directory and files on server $Server......" -ForegroundColor Yellow
    Remove-Item \\$Server\c$\BPATesting\*.* -erroraction SilentlyContinue
    Remove-Item \\$Server\c$\BPATesting -erroraction SilentlyContinue
    
    # Remove local CSV generated files (if a DC)
    # Remove-Item c:\BPATesting\BPA*.csv

    # Pausing for report creation (on remote server)
    Write-host " "
    Write-host "Pausing for reports to finish on the remote servers......" -ForegroundColor Yellow
    Start-Sleep 60

}

# Change Current Directory to Results Directory
cd $ResultsPath

# Finalize
write-host " "
Write-Host "Script is complete!" -foregroundcolor Yellow
Write-Host "The HTML files from each server's BPAs are located in the $ResultsPath directory." -ForegroundColor Green
write-host " "

Reports Generated

Just like the single server script, a stack of HTML reports is generated:

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s