IPerfAutomate.psm1

$iperfFileName = 'iperf3.exe'
$Defaults = @{
    IPerfSharedFolderPath = 'C:\Program Files\WindowsPowerShell\Modules\Iperf'
    IperfServerFolderPath = 'C:\Program Files\WindowsPowerShell\Modules\Iperf\bin'
    EmailNotificationRecipients = 'foo@var.com','ghi@whaev.com'
    SmtpServer = 'foo.test.local'
    InvokeIPerfPSSessionSuffix = 'iPerf'
}

$SiteServerMap = @{
    Reno = 'CLIENT1'
    Mcpherson = 'DC'
    Wichita = 'LABSQL'
    Carlisle = 'FOO'
    McDonough = 'FOO'
    Nashua = 'FOO'
    Broomfield = 'FOO'
}

Set-StrictMode -Version Latest

function ConvertToUncPath
{
    [OutputType([string])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$LocalFilePath,

        [Parameter(Mandatory)]
        [string]$ComputerName
    )

    $RemoteFilePathDrive = ($LocalFilePath | Split-Path -Qualifier).TrimEnd(':')
    "\\$ComputerName\$RemoteFilePathDrive`$$($LocalFilePath | Split-Path -NoQualifier)"
}

function TestServerAvailability
{
    [OutputType([pscustomobject])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string[]]$ComputerName
    )
    begin
    {
        $ErrorActionPreference = 'Stop'
    }
    process
    {
        try
        {
            foreach ($computer in $ComputerName) {
                $output = @{
                    ComputerName = $computer
                    Online = $false
                }
                if (Test-Connection -ComputerName $computer -Quiet -Count 1) {
                    $output.Online = $true
                }
                [pscustomobject]$output
            }
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function InvokeIperf
{
    [OutputType([void])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string[]]$ComputerName,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Arguments
    )
    begin
    {
        $ErrorActionPreference = 'Stop'
    }
    process
    {
        try
        {
            $iperfServerFilePath = Join-Path -Path $Defaults.IperfServerFolderPath -ChildPath $iperfFileName

            $mode = ConvertArgsToMode -IPerfArgs $Arguments

            $icmParams = @{
                ComputerName = $ComputerName
                ArgumentList = $iperfServerFilePath, $Arguments
            }

            if ($mode -eq 'Server') {
                ## Do not invoke server mode for servers that already have it running
                if ($runningServers = @($ComputerName).where({ TestIPerfServerSession -ComputerName $_ })) {
                    Write-Verbose -Message "The server(s) [$(($runningServers -join ','))] are already running."
                    [string[]]$ComputerName = @($ComputerName).where({ $_ -notin $runningServers})
                }
                $icmParams.InDisconnectedSession = $true
            }

            Write-Verbose -Message "Invoking iPerf in [$($mode)] mode on computer(s) [$ComputerName] using args [$($Arguments)]..."
            $ComputerName | ForEach-Object {
                if ($mode -eq 'Server') {
                    $icmParams.SessionName = "$_ - $mode - $($Defaults.InvokeIPerfPSSessionSuffix)"
                }
                Invoke-Command @icmParams -ScriptBlock {
                    $VerbosePreference = 'Continue'
                    ## Convert to short name so that IEX doesn't puke when using quotes
                    $fileShortPath = (New-Object -com scripting.filesystemobject).GetFile($args[0]).ShortPath
                    $cliString = "$fileShortPath $($args[1])"
                    Invoke-Expression -Command $cliString
                }
            }
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function StartIperfServer
{
    [OutputType([void])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string[]]$ComputerName
    )
    begin
    {
        $ErrorActionPreference = 'Stop'
    }
    process
    {
        try
        {
            if ($runningServers = @($ComputerName).where({ TestIPerfServerSession -ComputerName $_ })) {
                Write-Verbose -Message "The server(s) [$(($runningServers -join ','))] are already running."
                $ComputerName = @($ComputerName).where({ $_ -notin $runningServers})
            }

            $null = InvokeIperf -ComputerName $ComputerName -Arguments '-s'
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function ConvertArgsToMode
{
    [OutputType([string])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$IPerfArgs
    )
    switch ($IPerfArgs) {
        '-s' {
            'Server'
        }
        {$_ -like '*-c*'} {
            'Client'
        }
        default {
            throw "Unrecognized input: [$_]"
        }
    }
}

function StopIPerfServer
{
    [OutputType([void])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string[]]$ComputerName
    )
    begin
    {
        $ErrorActionPreference = 'Stop'
    }
    process
    {
        try
        {
            $icmParams = @{
                ComputerName = $ComputerName
                ScriptBlock = { Get-Process -Name $args[0] -ErrorAction SilentlyContinue | Stop-Process }
                ArgumentList = [System.IO.Path]::GetFileNameWithoutExtension($iperfFileName)
            }
            Invoke-Command @icmParams
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($_)
        } finally {
            $ComputerName | ForEach-Object {
                Get-PSSession -Name "$_ - Server*" -ErrorAction SilentlyContinue | Remove-PSSession
            }
            
        }
    }
}

function TestIPerfServerSession
{
    [OutputType([bool])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$ComputerName
    )
    begin
    {
        $ErrorActionPreference = 'Stop'
    }
    process
    {
        try
        {
            $cimParams = @{
                ComputerName = $ComputerName
                ClassName = 'Win32_Process'
                Filter = "Name = 'iperf3.exe'"
                Property = 'CommandLine'
            }
            if (($serverProc =  Get-CimInstance @cimParams) -and ($serverProc.CommandLine -match '-s$')) {
                $true
            } else {
                $false
            }
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function New-IperfSchedule
{
    <#
        .SYNOPSIS
            This function find the server mapped to FromSite and creates one or more Windows scheduled task on that server to
            kick off Iperf pointed to all of the sites specified. If more than one site is specified in ToSite, it will
            create that respective number of scheduled tasks.

        .PARAMETER FromSite
             A mandatory string parameter representing a single Viega site. This can be Reno, Mchpherson, Wichita, Carlisle,
             McDonough,Nashua or Broomfield. This is the site in which Iperf will be invoked from.

        .PARAMETER ToSite
             A mandatory string parameter representing a single Viega site. This can be Reno, Mchpherson, Wichita, Carlisle,
             McDonough,Nashua or Broomfield. This is the site IPerf will reach out to.

        .PARAMETER Daily
             A optional bool parameter to use if the scheduled task is to be executed every day. By default, this is set
             to $true.

        .PARAMETER Time
             A optional datetime parameter representing the time to kick off the scheduled task(s). By default, this is set
             to 6AM.
    
        .EXAMPLE
            PS> New-IperfSchedule -FromSite Reno -ToSite 'Mchpherson','Carlisle','Nashua'
    
    #>

    [OutputType([void])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory,ParameterSetName = 'Site')]
        [ValidateNotNullOrEmpty()]
        [ValidateSet('Reno','Mcpherson','Wichita','Carlisle','McDonough','Nashua','Broomfield')]
        [string]$FromSite,

        [Parameter(Mandatory,ParameterSetName = 'Site')]
        [ValidateNotNullOrEmpty()]
        [ValidateSet('Reno','Mcpherson','Wichita','Carlisle','McDonough','Nashua','Broomfield')]
        [string[]]$ToSite,

        [Parameter(Mandatory,ParameterSetName = 'Server')]
        [ValidateNotNullOrEmpty()]
        [string]$FromServerName,

        [Parameter(Mandatory,ParameterSetName = 'Server')]
        [ValidateNotNullOrEmpty()]
        [string[]]$ToServerName,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [bool]$Daily = $true,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [datetime]$Time = '06:00'
    )
    begin
    {
        $ErrorActionPreference = 'Stop'
    }
    process
    {
        try
        {
            if ($PSCmdlet.ParameterSetName -eq 'Site') {
                $ToServerName = $ToSite | ForEach-Object { $SiteServerMap.$_ }
                $FromServerName = $SiteServerMap.$FromSite
            }

            $localIperfFilePath = Join-Path -Path $Defaults.IperfServerFolderPath -ChildPath $iperfFileName

            ## Ensure the latest copy is on the remote computer
            Install-IperfModule -ComputerName $FromServerName
            
            Invoke-Command -ComputerName $FromServerName -ScriptBlock {
                $trigParams = @{
                    At = $args[3]
                }
                if ($args[2]) {
                    $trigParams.Daily = $true
                }
                $trigger = New-ScheduledTaskTrigger @trigParams
                $settings = New-ScheduledTaskSettingsSet
                
                $toServers = $args[1]
                $psCommand = "
                    try {
                        `$results = Start-IPerfMonitorTest -FromServerName $(hostname) -ToServerName $toServers;
                        Send-MailMessage -SmtpServer $($Defaults.SmtpServer) -To $($args[4]) -From `"Network Test From $(hostname) to $($args[1] -join ',')`" -Subject 'Network Monitor Test' -Body `$results
                    } catch {
                        Add-Content -Path `$env:TEMP\IperfMonitor.log -Value `$_.Exception.Message;
                        `$Host.SetShouldExit(1)
                    } finally {
                        Add-Content -Path `$env:TEMP\IperfMonitor.log -Value `$results;
                    }
                "

                $action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Unrestricted -Command `"$psCommand`""
                $task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings
                Register-ScheduledTask "IPerf Network Test - [$($args[1] -join ',')]" -InputObject $task
            } -ArgumentList $localIperfFilePath,$ToServerName,$Daily,$Time,$Defaults.EmailNotificationRecipients
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Install-IPerfModule
{
    [OutputType([void])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$ComputerName
    )
    begin
    {
        $ErrorActionPreference = 'Stop'
    }
    process
    {
        try
        {
            $modulePath = ConvertToUncPath -LocalFilePath 'C:\Program Files\WindowsPowerShell\Modules' -ComputerName $ComputerName
            Write-Verbose -Message "Copying IPerf module to [$($modulePath)]..."
            Copy-Item -Path $PSScriptRoot -Destination $modulePath -Recurse -Force
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function NewTestFile
{
    [OutputType([System.IO.FileInfo])]
    [CmdletBinding()]
    param
    ()
    
    $testFilePath = "$env:temp\testfile.txt"
    $file = [IO.File]::Create($testFilePath)
    $file.SetLength((Invoke-Expression -Command $FileSize))
    $file.Close()
    Get-Item -Path $testFilePath
}

function Start-IPerfMonitorTest
{
    <#
        .SYNOPSIS
            This function invokes Iperf from the server at site FromSite to all of the sites specified in ToSite.

        .PARAMETER FromSite
             A mandatory string parameter representing a single Viega site. This can be Reno, Mchpherson, Wichita, Carlisle,
             McDonough,Nashua or Broomfield. This is the site that IPerf will be invoked from.
            
        .PARAMETER ToSite
             A mandatory string parameter representing a single Viega site. This can be Reno, Mchpherson, Wichita, Carlisle,
             McDonough,Nashua or Broomfield. This is the site(s) that Iperf will connect to.

        .EXAMPLE
            PS> Start-IPerfMonitorTest -FromSite Reno -ToSite 'Wichita','Broomfield'
    
    #>

    [OutputType([void])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory,ParameterSetName = 'Site')]
        [ValidateNotNullOrEmpty()]
        [ValidateSet('Reno','Mcpherson','Wichita','Carlisle','McDonough','Nashua','Broomfield')]
        [string]$FromSite,

        [Parameter(Mandatory,ParameterSetName = 'Site')]
        [ValidateNotNullOrEmpty()]
        [ValidateSet('Reno','Mcpherson','Wichita','Carlisle','McDonough','Nashua','Broomfield')]
        [string[]]$ToSite,

        [Parameter(Mandatory,ParameterSetName = 'Server')]
        [ValidateNotNullOrEmpty()]
        [string]$FromServerName,

        [Parameter(Mandatory,ParameterSetName = 'Server')]
        [ValidateNotNullOrEmpty()]
        [string[]]$ToServerName,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            if ($_ -notmatch 'KB$') {
                throw "FileSize must end with 'KB' to indicate kilobytes"
            } else {
                $true
            }
        })]
        [string]$WindowSize = '712KB',

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            if ($_ -notmatch 'MB$') {
                throw "FileSize must end with 'MB' to indicate megabytes"
            } else {
                $true
            }
        })]
        [string]$FileSize
    )
    begin
    {
        $ErrorActionPreference = 'Stop'
    }
    process
    {
        try
        {
            if ($PSCmdlet.ParameterSetName -eq 'Site') {
                $ToServerName = $ToSite | ForEach-Object { $SiteServerMap.$_ }
                $FromServerName = $SiteServerMap.$FromSite
            }

            ## Ensure all servers are available
            if ($notavail = @(TestServerAvailability -ComputerName (@($FromServerName) + $ToServerName)).where({ -not $_.Online })) {
                throw "The server(s) [$(($notavail.ComputerName -join ','))] could not be contacted."
            }

            ## Ensure the iPerf module is installed on all servers (if being invoked remotely)
            (@($FromServerName) + $ToServerName).where({ $_ -notlike "$env:COMPUTERNAME*"}) | ForEach-Object {
                Install-IperfModule -ComputerName $_
            }

            ## Ensure all To Servers have a server instance running
            if ($noservers = @($ToServerName).where({ -not (TestIPerfServerSession -ComputerName $_) })) {
                $noservers | ForEach-Object {
                    Write-Verbose -Message "IPerf server not running on [$($_)]. Starting server..."
                    StartIperfServer -ComputerName $_
                }
            }

            ## Create the test file and copy it to clients, if necessary
            if ($PSBoundParameters.ContainsKey('FileSize'))
            {
                $testFile = NewTestFile
                $localTestFilePath = 'C:\{0}' -f $testFile.Name
                $copiedTestFiles = [System.Collections.ArrayList]@()
                @($FromServerName).foreach({
                    $uncTestFilePath = ConvertToUncPath -LocalFilePath $localTestFilePath -ComputerName $_
                    Write-Verbose -Message "Copying test file [$($testFile.FullName)] to $uncTestFilePath..."
                    $null = $copiedTestFiles.Add((Copy-Item -Path $testFile.FullName -Destination $uncTestFilePath -PassThru))
                })
                
            }
            
            $ToServerName | ForEach-Object {
                $iPerfArgs = ('-c {0} -w {1}' -f $_,$WindowSize)
                if ($PSBoundParameters.ContainsKey('FileSize'))
                {
                    $iPerfArgs += " -F `"$localTestFilePath`""
                }
                InvokeIperf -ComputerName $FromServerName -Arguments $iPerfArgs
            }
        } catch {
            $PSCmdlet.ThrowTerminatingError($_)
        } finally {
            StopIperfServer -ComputerName $ToServerName
            if (Get-Variable -Name testFile -ErrorAction Ignore) {
                Write-Verbose -Message "Removing local test file [$($testFile.FullName)]"
                Remove-Item -Path $testFile.FullName -ErrorAction Ignore
            }

            if (Get-Variable -Name copiedTestFiles -ErrorAction Ignore) {
                Write-Verbose -Message "Removing copied test files [$($copiedTestFiles.FullName -join ',')]"
                Remove-Item -Path $copiedTestFiles.FullName -ErrorAction Ignore
            }
        }
    }
}