Posted On April 27, 2021

PowerShell: Hyper-V Servers Capacity Report

kimconnect 0 comments
blog.KimConnect.com >> Windows >> PowerShell: Hyper-V Servers Capacity Report

Current Version:

# HyperVCapacityReport.ps1
# Version 0.0.2

# Report parameters
$workingDirectory='C:\scripts\googleSheets'
$selectFields='node,model,os,cores,cpuUtilizationPercent,ramGb,ramUsedPercent'
$domainObjects=@(
    @{domain='intranet.kimconnect.com';dc='lax-dc02.intranet.kimconnect.com';username='intranet\clusterReport';password=$env:clusterReportPass}
    @{domain='intranet.dragoncoin.com';dc='lax-dc02.intranet.dragoncoin.com';username='intranet\clusterReport';password=$env:clusterReportPass}
    )

# Update this Google Sheets
$spreadsheetID='SOMEHASHHERE'
$spreadsheetUrl='https://docs.google.com/spreadsheets/d/'+$spreadsheetID

# Google API Authozation
$scope = "https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.file"
$certPath = 'C:\scripts\googleSheets\plated-campaign-SOMEHASHERE.p12'
$iss = '[email protected]'
$certPassword = 'notasecret'

# Email parameters
$emailFrom='[email protected]'
$emailTo='[email protected]','[email protected]'
$subject='Hyper-V Hosts Capacity Report'
$smtpRelayServer='relay02.dragoncoin.com'
$emailDay='Monday'

function getAllHyperVClusters($domainObjects){
    function getHyperVHosts($domainObjects){
        function includeRSAT{
            $ErrorActionPreference='stop'
            [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
            #$rsatWindows7x32='https://download.microsoft.com/download/4/F/7/4F71806A-1C56-4EF2-9B4F-9870C4CFD2EE/Windows6.1-KB958830-x86-RefreshPkg.msu'
            $rsatWindows7x64='https://download.microsoft.com/download/4/F/7/4F71806A-1C56-4EF2-9B4F-9870C4CFD2EE/Windows6.1-KB958830-x64-RefreshPkg.msu'
            $rsatWindows81='https://download.microsoft.com/download/1/8/E/18EA4843-C596-4542-9236-DE46F780806E/Windows8.1-KB2693643-x64.msu'
            $rsat1709 = "https://download.microsoft.com/download/1/D/8/1D8B5022-5477-4B9A-8104-6A71FF9D98AB/WindowsTH-RSAT_WS_1709-x64.msu"
            $rsat1803 = "https://download.microsoft.com/download/1/D/8/1D8B5022-5477-4B9A-8104-6A71FF9D98AB/WindowsTH-RSAT_WS_1803-x64.msu"
            $rsatWs2016 = "https://download.microsoft.com/download/1/D/8/1D8B5022-5477-4B9A-8104-6A71FF9D98AB/WindowsTH-RSAT_WS2016-x64.msu"
    
            # This command does not work on Windows 2012R2
            #$releaseId=(Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name ReleaseId).ReleaseId
            #Get-ItemProperty : Property ReleaseId does not exist at path HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows
            #NT\CurrentVersion.
            #At line:1 char:2
            #+ (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Na ...
            #+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
            #    + CategoryInfo          : InvalidArgument: (ReleaseId:String) [Get-ItemProperty], PSArgumentException
            #    + FullyQualifiedErrorId : System.Management.Automation.PSArgumentException,Microsoft.PowerShell.Commands.GetItemPropertyCommand
    
            $releaseId=(Get-Item "HKLM:SOFTWARE\Microsoft\Windows NT\CurrentVersion").GetValue('ReleaseID')
            $osVersion=[System.Environment]::OSVersion.Version
            [double]$osVersionMajorMinor="$($osVersion.Major).$($osVersion.Minor)" 
            $osName=(Get-WmiObject Win32_OperatingSystem).Name
            #$osType=switch ((Get-CimInstance -ClassName Win32_OperatingSystem).ProductType){
            #    1 {'client'}
            #    2 {'domaincontroller'}
            #    3 {'memberserver'}
            #    }
    
            $windowsVersion=(Get-CimInstance Win32_OperatingSystem).Version
    
            switch ($releaseId){
                1607{write-host 'Windows Server 2016 Release 1607 detected';$link=$rsatWs2016;break}
                1709{write-host 'Windows Server 2016 Release 1709 detected';$link=$rsat1709;break}
                1803{write-host 'Windows Server 2016 Release 1803 detected';$link=$rsat1803}
            }
        
            switch ($osVersionMajorMinor){
                {$_ -eq 6.0}{write-host 'Windows Server 2008 or Windows Vista detected';$link=$rsat1709;break}
                {$_ -eq 6.1}{write-host 'Windows Server 2008 R2 or Windows 7 detected';$link=$rsatWindows7x64;break}
                {$_ -eq 6.2}{write-host 'Windows Server 2012 or Windows 8.1 detected';$link=$rsatWindows81;break}
                {$_ -eq 6.3}{write-host 'Windows Server 2012 R2 detected';$link=$rsatWindows81}
            }
    
            if (!(Get-Module -ListAvailable -Name ActiveDirectory -EA SilentlyContinue)){
                Write-host "Prerequisite checks: module ActiveDirectory NOT currently available on this system. Please wait while the program adds that plugin..."
                try{
                    # If OS is Windows Server, then install RSAT using a different method
                    if ($osName -match "^Microsoft Windows Server") {
                        # This sequence has confirmed to be valid on Windows Server 2008 R2 and above
                        Write-Verbose "Importing Windows Feature: RSAT-AD-PowerShell"
                        Import-Module ServerManager
                        Add-WindowsFeature RSAT-AD-PowerShell
                        }
                    else{
                        Write-Verbose "This sequence targets Windows Client versions"
                        $destinationFile= ($ENV:USERPROFILE) + "\Downloads\" + (split-path $link -leaf)
                        Write-Host "Downloading RSAT from $link..."
                        Start-BitsTransfer -Source $link -Destination $destinationFile
                        $fileCheck=Get-AuthenticodeSignature $destinationFile
                        if($fileCheck.status -ne "valid") {write-host "$destinationFile is not valid. Please try again...";break}
                        $wusaCommand = $destinationFile + " /quiet"
                        Write-host "Installing RSAT - please wait..."
                        Start-Process -FilePath "C:\Windows\System32\wusa.exe" -ArgumentList $wusaCommand -Wait
                        }
                    return $true
                    }
                catch{
                    write-warning "$($error[0].Exception)"
                    return $false
                    }
            }else{
                Write-host "Prerequisite checks: module ActiveDirectory IS currently available on this system." -ForegroundColor Green
                return $true
                }
        }
        function listAllHyperVNodes($domainObjects){
            try{
                #if(!$domains){$domains=(Get-ADForest).Name|%{(Get-ADForest -Identity $_).Name}}
                $allHyperVNodes=@()
                foreach ($domainObject in $domainObjects){
                    #[string]$dc=(get-addomaincontroller -DomainName "$domain" -Discover -NextClosestSite).HostName
                    $domain=$domainObject.domain
                    $username=$domainObject.username
                    $password=$domainObject.password
                    $encryptedPassword=ConvertTo-securestring $password -AsPlainText -Force
                    $credential=New-Object -TypeName System.Management.Automation.PSCredential -Args $username,$encryptedPassword
                    $dc=if(!($domainObject.dc)){
                        $domainNode=.{try{get-addomain -Server $domain}catch{$false}}
                        if (!$domainNode){return $null}
                        if($domainNode.ReplicaDirectoryServers[1]){
                                $domainNode.ReplicaDirectoryServers[1]
                            }else{
                                $domainNode.PDCEmulator
                            }
                    }else{
                        $domainObject.dc
                    }
                    $session=.{
                        try{
                            new-pssession -computername $dc -credential $credential -EA Stop
                            write-host "Connected to $dc..."
                        }catch{
                            write-warning $_
                            return $false
                        }
                    }
                    if($session.state -eq 'Opened'){
                        write-host "Collecting all Hyper-V Clusters in $domain. This may take a while, depending on cluster sizes."
                        $allClusters=.{              
                            $clusters=invoke-command -session $session -scriptblock{
                                $rsatClusteringPowershell=get-WindowsFeature RSAT-Clustering-PowerShell
                                if(!$rsatClusteringPowershell.Installed){Add-WindowsFeature RSAT-Clustering-PowerShell}
                                (get-cluster -domain $env:USERDNSDOMAIN).Name
                            }
                            return $clusters
                        }
                        Remove-PSSession $session
                        foreach ($cluster in $allClusters){
                            $clusterName="$cluster.$domain"
                            $retries=3
                            do{
                                $newSession=.{
                                    try{
                                        new-pssession -computername $clusterName -credential $credential -EA Stop
                                        write-host "Connected to $clusterName..."
                                    }catch{
                                        return $_
                                    }
                                }
                                $retries--
                            }until($newSession.state -eq 'Opened' -OR $retries -le 0)
                            if($newSession.state -eq 'Opened'){
                                write-host "Checking $clusterName"
                                try{
                                    $nodes=try{
                                            invoke-command -session $newSession -scriptblock{
                                            param($clustername)
                                            #$rsatClusteringPowershell=get-WindowsFeature RSAT-Clustering-PowerShell
                                            #if(!$rsatClusteringPowershell.Installed){Add-WindowsFeature RSAT-Clustering-PowerShell}
                                            $x=Get-ClusterNode -Cluster $clustername -ea SilentlyContinue
                                            if($x){
                                                $x|Where-Object{$_.State -eq 'Up'}|Select-Object Name,@{name='Cluster';e={"$clustername.$env:USERDNSDOMAIN"}}
                                            }else{
                                                $false
                                            }
                                        } -Args $cluster -EA Stop|select Name,Cluster
                                    }catch{
                                        write-warning $_
                                    }
                                    if($nodes){
                                        $allHyperVNodes+=$nodes
                                    }else{
                                        write-warning "$cluster is skipped as program is unable to gather node names"
                                    }
                                }catch{                                    
                                    write-warning $_
                                }
                                remove-pssession $newSession
                            }else{
                                write-warning $newSession
                                write-warning "$env:computername cannot connect to $clustername as required to gather Hyper-V node names."
                            }
                        }
                    }else{
                        write-warning "$env:computername cannot connect to $dc as required to gather domain objects data."
                    }
                }
                return $allHyperVNodes
            }catch{
                Write-Error $_
                return $false
                }
        }

        try{
            #$null=includeRSAT;
            #$rsatClusteringPowershell=get-WindowsFeature RSAT-Clustering-PowerShell
            #if(!$rsatClusteringPowershell.Installed){Add-WindowsFeature RSAT-Clustering-PowerShell}                                    
            $hyperVHosts=listAllHyperVNodes $domainObjects
            $hyperVHostNames=$hyperVHosts|sort -property Cluster
            return $hyperVHostNames
        }catch{
            Write-Error $_
            return $false
            }
    }
    function sortArrayStringAsNumbers([string[]]$names){
        $hashTable=@{}
        $maxLength=($names | Measure-Object -Maximum -Property Length).Maximum
        foreach ($name in $names){
            #[int]$x=.{[void]($name -match '(?:.(\d+))+$');$matches[1]}
            #$x=.{[void]($name -match '(?:.(\d+)+)$');@($name.substring(0,$name.length-$matches[1].length),$matches[1])}
            $originalName=$name
            $x=.{
                [void]($name -match '(?:.(\d+)+)\w{0,}$');
                if($matches){
                    [int]$trailingNonDigits=([regex]::match($name,'\D+$').value).length
                    if($trailingNonDigits){
                        $name=$name.substring(0,$name.length-$trailingNonDigits)
                    }
                    return ($name.substring(0,$name.length-$matches[1].length))+$matches[1].PadLeft($maxLength,'0');
                }else{
                    return $name+''.PadLeft($maxLength,'0');
                }}
            $hashTable.Add($originalName,$x)
            }
        $sorted=foreach($item in $hashTable.GetEnumerator() | Sort Value){$item.Name}
        return $sorted
    }

    #write-host "Obtaining cluster names and associated hosts..."
    $hyperVHostsInForest=getHyperVHosts $domainObjects
    $sortedArray=@()
    $clusters=$hyperVHostsInForest|Group-Object -Property Cluster
    foreach($cluster in $clusters){
        $clusterName=$cluster.Name
        write-host $clusterName       
        $sortedHostnames=sortArrayStringAsNumbers $cluster.Group.Name
        $sortedHostnames|%{$sortedArray+=New-Object -TypeName psobject -Property @{hostname=$_; cluster=$clusterName}}
    }
    return $sortedArray
}
function getQuickStats($computername=$env:computername){    
    $ErrorActionPreference='stop'
    try{
        # write-host "Checking $env:computername`:"
        # Server Model
        $biosRegKey="REGISTRY::HKEY_LOCAL_MACHINE\Hardware\Description\System\Bios"
        $bios=Get-ItemProperty $biosRegKey
        $model=$bios.SystemProductName

        # RAM (IN GB)
        write-host " system memory..." -NoNewline
        $ramInfo=get-wmiobject win32_physicalmemory|Select-Object *
        $ramInfo=get-CimInstance win32_physicalmemory|Select-Object *
        $ramGb=.{$sum=0;$ramInfo.Capacity|%{ $sum += $_};return $sum/1GB}

        # RAM Module size
        #$ramModuleSize=($ramInfo.Capacity|select -unique)|%{$_/1GB}
        $ramGroups=$ramInfo.Capacity|Group|select Count,@{name='ramSize';e={($_.Group|select -Unique)/1GB}}
        $ramModuleSize=$ramGroups|%{"($($_.Count)) $($_.ramSize)"}

        # Ram Speed (Mhz)
        # $ramSpeed=$ramInfo.Speed|select -unique
        $ramSpeedGroups=$ramInfo.Speed|Group|select Count,@{name='ramSize';e={$_.Group|select -Unique}}
        $ramSpeed=$ramSpeedGroups|%{"($($_.Count)) $($_.ramSize)"}

        # ramConfiguredClockSpeed
        #$ramConfiguredSpeed=$ramInfo.ConfiguredClockSpeed|select -unique
        $ramConfigSpeedGroups=$ramInfo.ConfiguredClockSpeed|Group|select Count,@{name='ramSize';e={$_.Group|select -Unique}}
        $ramConfiguredSpeed=$ramConfigSpeedGroups|%{"($($_.Count)) $($_.ramSize)"}
        
        # RAM in use        
        $osObject=get-wmiobject Win32_OperatingSystem
        $os=$osObject.Caption
        $ramPercentFree=$osObject.FreePhysicalMemory/$osObject.TotalVisibleMemorySize
        $ramUsed=[math]::round($ramGb-($ramPercentFree*$ramGb),2)    

        # Slots filled
        $ramSlotsFilled=($ramInfo|?{$_.Capacity}).count

        # Slots available
        $ramSlotsAvailable=$ramInfo.count

        # Cores
        write-host " system CPU..."
        $cpu=Get-CimInstance Win32_processor
        $cores=.{$total=0;$cpu.NumberOfCores|%{$total+=$_};return $total}

        # Proc count
        $sockets=$cpu.count

        # Cpu Type
        $cpuType=$cpu.Name|select -unique

        # Clock speed
        $cpuClock=($cpu.MaxClockSpeed|Measure-Object -Average).Average
        $cpuBandwidth=$cpuClock*$cores
        $cpuUtilizationPercent=($cpu|Measure-Object -property LoadPercentage -Average).Average
        $cpuUtilization=($cpuUtilizationPercent/100)*$cpuBandwidth

        #$cpuUtilizationPercent=((Get-WmiObject -Class Win32_PerfFormattedData_PerfOS_Processor).PercentProcessorTime|measure-object -Average).Average  
        # $cpuUtilizationPercent=.{#$rawValues=wmic cpu get loadpercentage
        #     $rawValues=$cpu.LoadPercentage
        #     $selectNumbers=$rawValues|?{$_ -match '\d+'}
        #     ($selectNumbers|Measure-Object -Average).Average
        # }

        # $cpuUtilizationPercent=(get-counter -Counter "\Processor(*)\% Processor Time" -SampleInterval 1 -MaxSamples 2 |`
        #     select -ExpandProperty countersamples | select -ExpandProperty cookedvalue | Measure-Object -Average).average
        
        # $cpuUtilization=((Get-process|Select-Object CPU).CPU|Measure-Object -sum).Sum
        # $cpuUtilizationPercent=($cpuUtilization/$cpuBandwidth)*100

        # $cpuProperties=@(
        #     @{Name="processName"; Expression = {$_.name}},
        #     @{Name="cpuPercent"; Expression = {$_.PercentProcessorTime}},    
        #     @{Name="MemoryGb"; Expression = {[Math]::Round(($_.workingSetPrivate / 1GB),2)}}
        # )
        # $totalCpuUsage=(Get-WmiObject -class Win32_PerfFormattedData_PerfProc_Process | 
        #     Select-Object $cpuProperties)|?{$_.'processName' -eq '_Total'}

        # Other methods not being used
        #$cs = Get-WmiObject -class Win32_ComputerSystem
        #$Sockets=$cs.numberofprocessors
        #$Cores=$cs.numberoflogicalprocessors
        
        # write-host " Hyper-V Guest VMs..." -NoNewline
        # $guestVms = Get-VM|Where-Object {$_.State -eq 'Running'}
        # $vmsCount = $guestVms.count
    }catch{
        write-warning $_
        continue
    }  
    return [PSCustomObject][ordered]@{
        node=$computername        
        model=$model
        cpuType=$cpuType
        sockets=$sockets
        cores=$cores
        cpuBandwidth=$cpuBandwidth
        cpuUtilization=[math]::round($cpuUtilization,2)
        cpuUtilizationPercent=[math]::round($cpuUtilizationPercent,2)
        ramGb=$ramGb
        ramUsedGb=[math]::round($ramUsed,2)
        ramUsedPercent=[math]::round(($ramUsed/$ramGb)*100,2)
        ramModuleSizeGb=$ramModuleSize
        ramSlotsAvailable=$ramSlotsAvailable
        ramSlotsFilled=$ramSlotsFilled
        ramSpeed=$ramSpeed
        ramConfigSpeed=$ramConfiguredSpeed
        #vmsCount=$vmsCount
        os=$os
    }    
}
function checkSystem($computername=$env:computername,$credential,$retries=3){ 
    do{
        $session=try{
            if($credential){
                    New-PSSession $computername -credential $credential
            }else{
                    New-PSSession $computername
            }
        }catch{
            $retries--       
            write-warning "There's a problem connecting to $computername ... Retrying $retries more times..."            
            $null
        }        
    }while(!$session -or $retries -gt 0)
    if($session.State -eq 'Opened'){        
        $quickStats=invoke-command -session $session -scriptblock{
                param($getQuickStats)
                [scriptblock]::create($getQuickStats).invoke()
            } -Args ${function:getQuickStats}
        Remove-PSSession $session
    }else{
        Write-Warning "$computername is not Reachable from $env:computername via WinRM"
        $quickStats=$false        
    }
    if($quickStats){
        return $quickStats|Select-Object -Property * -ExcludeProperty PSComputerName,RunspaceId,PSShowComputerName
    }else{
        return [PSCustomObject]@{
            computerName = $computername+' Unreachable'
        }
    }
}
function generateEmailContent{
    param(
        $arrayObjects,
        $selectFields='node,model,cpuType,sockets,cores,cpuBandwidth,cpuUtilizationPercent,ramGb,ramUsedPercent',
        $reportName,
        $summary,
        $css="
        <style>
        .h1 {
            font-size: 18px;
            height: 40px;
            padding-top: 80px;
            margin: auto;
            text-align: center;
        }
        .h5 {
            font-size: 22px;
            text-align: center;
        }
        .th {text-align: center;}
        .table {
            padding:7px;
            border:#4e95f4 1px solid;
            background-color: white;
            margin-left: auto;
            margin-right: auto;
            width: 100%
            }
        .colgroup {}
        .th { background: #0046c3; color: #fff; padding: 5px 10px; }
        .td { font-size: 11px; padding: 5px 20px; color: #000;
            width: 1px;
            white-space: pre;
            }
        .tr { background: #b8d1f3;}
        .tr:nth-child(even) {
            background: #dae5f4;
            width: 1%;
            white-space: nowrap
        }
        .tr:nth-child(odd) {
            background: #b8d1f3;
            width: 1%;
            white-space: nowrap
        }
        </style>
        "
        )
    $filteredArray=invoke-expression "`$arrayObjects|select-object $selectFields"
    $report=$filteredArray|ConvertTo-Html -Fragment|Out-String
    $reportHtml=$report -replace '\<(?<item>\w+)\>', '<${item} class=''${item}''>'
    $emailContent='<html><head>'+$css+"</head><body><h1>$reportName</h1>`n<h5>$summary</h5>"+$reportHtml+'</body></html>'
    return $emailContent
}

function generateHtmlFragment($arrayObjects,$selectFields,$reportName,$summary){
    $filteredArray=invoke-expression "`$arrayObjects|select-object $selectFields"
    $htmlFragment=$filteredArray|ConvertTo-Html -Fragment|Out-String
    $customHtml=$htmlFragment -replace '\<(?<item>\w+)\>', '<${item} class=''${item}''>'
    $customHtmlFragment="<h1>$reportName</h1>`n<h5>$summary</h5>"+$customHtml
    return $customHtmlFragment    
}

function packageHtmlEmailContent{
    param(
        $htmlBody,
        $footer,
        $css="
        <style>
        .h1 {
            font-size: 18px;
            height: 40px;
            padding-top: 80px;
            margin: auto;
            text-align: center;
        }
        .h5 {
            font-size: 22px;
            text-align: center;
        }
        .th {text-align: center;}
        .table {
            padding:7px;
            border:#4e95f4 1px solid;
            background-color: white;
            margin-left: auto;
            margin-right: auto;
            width: 100%
            }
        .colgroup {}
        .th { background: #0046c3; color: #fff; padding: 5px 10px; }
        .td { font-size: 11px; padding: 5px 20px; color: #000;
            width: 1px;
            white-space: pre;
            }
        .tr { background: #b8d1f3;}
        .tr:nth-child(even) {
            background: #dae5f4;
            width: 1%;
            white-space: nowrap
        }
        .tr:nth-child(odd) {
            background: #b8d1f3;
            width: 1%;
            white-space: nowrap
        }
        </style>
        "
        )
    $emailContent='<html><head>'+$css+"</head><body>$htmlBody"+$footer+'</body></html>'
    return $emailContent         
}
function emailReport{
    param(
        $emailFrom,
        $emailTo,
        $subject,
        $emailContent,
        $smtpRelayServer
    )
    Send-MailMessage -From $emailFrom `
    -To $emailTo `
    -Subject $subject `
    -Body $emailContent `
    -BodyAsHtml `
    -SmtpServer $smtpRelayServer    
}

function updateGoogleSheets{
    param(
        $spreadsheetID,
        $sheetName=$env:USERDNSDOMAIN,
        $reportCsv,
        $certPath,
        $certPassword='notasecret',
        $iss,
        $scope="https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.file"
    )
    # Set security protocol to TLS 1.2 to avoid TLS errors
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
    Import-Module UMN-Google

    # obtain token if necessary
    $accessToken = Get-GOAuthTokenService -scope $scope -certPath $certPath -certPswd $certPassword -iss $iss
    # Create new sheet, if not already exists
    try{Add-GSheetSheet -accessToken $accessToken -sheetName $sheetName -spreadSheetID $spreadsheetID}catch{}

    # Upload CSV data to Google Sheets with Set-GSheetData
    $inputCsv = Import-Csv $reportCsv
    $header = $inputCsv[0].psobject.properties.name|select -first 26 # Google default 26 columns max
    $import = new-Object System.Collections.ArrayList
    $import.Add($header) | Out-Null
    $inputCsv | ForEach-Object {
    $row=@(foreach($label in $header){$_."$($label)"})
    $import.Add($row) | Out-Null
    }
    $import.Add(@()) | Out-Null
    $import.Add(@( $summary -split '\r?\n'|%{$_.Trim('<br>$')}|out-string )) | Out-Null
    $columnsCount=$header.count
    $columnLetter=if($columnsCount -le 25){
            [char](65+$columnsCount)
        }else{
            'Z'
        }
    $range="A1:$columnLetter$($import.Count)"
    try{
        Set-GSheetData -accessToken $accessToken `
            -rangeA1 $range `
            -sheetName $sheetName `
            -spreadSheetID $spreadsheetID `
            -values $import #-Debug -Verbose            
        return $true
    }catch{
        write-warning $_
        return $false
    }
}

$hyperVClusters=getAllHyperVClusters $domainObjects
$clusterGroups=$hyperVClusters|Group-Object -Property cluster
$emailFragments=''
foreach($group in $clusterGroups){
    $clusterName=$group.Name
    $domain=[regex]::matches($clusterName,'\.(.*)').captures.groups[1].value
    $username=($domainObjects|?{$_.domain.toupper() -eq $domain.toupper()}).username
    $password=($domainObjects|?{$_.domain.toupper() -eq $domain.toupper()}).password
    $encryptedPassword=ConvertTo-securestring $password -AsPlainText -Force
    $credential=New-Object -TypeName System.Management.Automation.PSCredential -Args $username,$encryptedPassword
    write-host $clusterName
    $timeStamp=(get-date).tostring()
    $results=@()
    $hyperVHosts=$group.Group.hostname
    $hostCount=$hyperVHosts.Count
    for ($i=0;$i -lt $hostCount;$i++){
        $hostname="$($hyperVHosts[$i]).$domain"
        write-host "Scanning $($i+1) of $hostCount`: $($hyperVHosts[$i])..."        
        $retries=3        
        do{
            $result=checkSystem $hostname $credential
            $retries--
            if(!$result){
                $nslookupTime=measure-command {nslookup $hostname}
                write-warning "It has taken $([math]::round($nslookupTime.totalseconds,2)) seconds for $env:computername to lookup $hostname"
            }
        }until ($result -or $retries -eq 0)
        if($result){$results+=$result}
    }
    $totalRam=.{$total=0;$results.ramGb|%{$total+=$_};$total}
    $usedRam=.{$total=0;$results.ramUsedGb|%{$total+=$_};$total}
    $usedRamPercent=[math]::round($usedRam/$totalRam*100,2)
    $availableRam=[math]::round($totalRam-$usedRam,2)
    $totalCpuBandwidth=.{$total=0;$results.cpuBandwidth|%{$total+=$_};$total}
    $usedCpuBandwidth=.{$total=0;$results.cpuUtilization|%{$total+=$_};$total}
    $usedCpuPercent=[math]::round($usedCpuBandwidth/$totalCpuBandwidth*100,2)
    $summary="Data Timestamp: $timeStamp<br>
Hosts Count: $($hyperVHosts.count)<br>
Total CPU Bandwidth: $([math]::round($totalCpuBandwidth/1000)) Ghz<br>
Used CPU Percent: $usedCpuPercent %<br>
Total RAM: $totalRam GB<br>
Used RAM: $usedRam GB<br>
Used RAM Percent: $usedRamPercent %<br>
Available RAM: $availableRam GB<br>"
    $reportName="Hyper-V Capacity Report for Cluster $clustername"
    $emailFragment=generateHtmlFragment $results $selectFields $reportName $summary
    $emailFragments+=$emailFragment
    $reportCsvPath=join-path $workingDirectory "$clustername.csv"
    $null=$results|export-csv $reportCsvPath -NoTypeInformation   
    updateGoogleSheets $spreadsheetID $clusterName $reportCsvPath $certPath $certPassword $iss $scope
    }

$today=(get-date).DayOfWeek
if ($today -eq $emailDay){
    $footer="<br>GoogleSheets URL: $spreadsheetUrl<br>"
    $emailContent=packageHtmlEmailContent $emailFragments $footer
    emailReport $emailFrom $emailTo $subject $emailContent $smtpRelayServer
}

Previous Version:

# hyperVCapacityReport.ps1
# Version: 0.1

# Report parameters
$workingDirectory='C:\scripts\hyperVReports'
$selectFields='node,model,os,cpuType,sockets,cores,ramGb,ramUsedPercent,vmsCount'
$domainObjects=@(
    @{domain='kimconnect.com';dc='dc2.kimconnect.com';username='testAdmin';password='password'}    
    )

# Update this Google Sheets
$spreadsheetID='abcdefghijklmnopqstvwzyz1234567890'
$spreadsheetUrl='https://docs.google.com/spreadsheets/d/'+$spreadsheetID

# Google API Authozation
$scope = "https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.file"
$certPath = 'C:\scripts\googleSheets\googleApiCert12345.p12'
$iss = '[email protected]'
$certPassword = 'notasecret'

# Email parameters
$emailFrom='[email protected]'
$emailTo='[email protected]'
$subject='Hyper-V Hosts Capacity Report'
$smtpRelayServer='smtprelay.kimconnect.com'
$emailDay='Monday'

function getAllHyperVClusters($domainObjects){
    function getHyperVHosts($domainObjects){
        function includeRSAT{
            $ErrorActionPreference='stop'
            [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
            #$rsatWindows7x32='https://download.microsoft.com/download/4/F/7/4F71806A-1C56-4EF2-9B4F-9870C4CFD2EE/Windows6.1-KB958830-x86-RefreshPkg.msu'
            $rsatWindows7x64='https://download.microsoft.com/download/4/F/7/4F71806A-1C56-4EF2-9B4F-9870C4CFD2EE/Windows6.1-KB958830-x64-RefreshPkg.msu'
            $rsatWindows81='https://download.microsoft.com/download/1/8/E/18EA4843-C596-4542-9236-DE46F780806E/Windows8.1-KB2693643-x64.msu'
            $rsat1709 = "https://download.microsoft.com/download/1/D/8/1D8B5022-5477-4B9A-8104-6A71FF9D98AB/WindowsTH-RSAT_WS_1709-x64.msu"
            $rsat1803 = "https://download.microsoft.com/download/1/D/8/1D8B5022-5477-4B9A-8104-6A71FF9D98AB/WindowsTH-RSAT_WS_1803-x64.msu"
            $rsatWs2016 = "https://download.microsoft.com/download/1/D/8/1D8B5022-5477-4B9A-8104-6A71FF9D98AB/WindowsTH-RSAT_WS2016-x64.msu"
    
            # This command does not work on Windows 2012R2
            #$releaseId=(Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name ReleaseId).ReleaseId
            #Get-ItemProperty : Property ReleaseId does not exist at path HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows
            #NT\CurrentVersion.
            #At line:1 char:2
            #+ (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Na ...
            #+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
            #    + CategoryInfo          : InvalidArgument: (ReleaseId:String) [Get-ItemProperty], PSArgumentException
            #    + FullyQualifiedErrorId : System.Management.Automation.PSArgumentException,Microsoft.PowerShell.Commands.GetItemPropertyCommand
    
            $releaseId=(Get-Item "HKLM:SOFTWARE\Microsoft\Windows NT\CurrentVersion").GetValue('ReleaseID')
            $osVersion=[System.Environment]::OSVersion.Version
            [double]$osVersionMajorMinor="$($osVersion.Major).$($osVersion.Minor)" 
            $osName=(Get-WmiObject Win32_OperatingSystem).Name
            #$osType=switch ((Get-CimInstance -ClassName Win32_OperatingSystem).ProductType){
            #    1 {'client'}
            #    2 {'domaincontroller'}
            #    3 {'memberserver'}
            #    }
    
            $windowsVersion=(Get-CimInstance Win32_OperatingSystem).Version
    
            switch ($releaseId){
                1607{write-host 'Windows Server 2016 Release 1607 detected';$link=$rsatWs2016;break}
                1709{write-host 'Windows Server 2016 Release 1709 detected';$link=$rsat1709;break}
                1803{write-host 'Windows Server 2016 Release 1803 detected';$link=$rsat1803}
            }
        
            switch ($osVersionMajorMinor){
                {$_ -eq 6.0}{write-host 'Windows Server 2008 or Windows Vista detected';$link=$rsat1709;break}
                {$_ -eq 6.1}{write-host 'Windows Server 2008 R2 or Windows 7 detected';$link=$rsatWindows7x64;break}
                {$_ -eq 6.2}{write-host 'Windows Server 2012 or Windows 8.1 detected';$link=$rsatWindows81;break}
                {$_ -eq 6.3}{write-host 'Windows Server 2012 R2 detected';$link=$rsatWindows81}
            }
    
            if (!(Get-Module -ListAvailable -Name ActiveDirectory -EA SilentlyContinue)){
                Write-host "Prerequisite checks: module ActiveDirectory NOT currently available on this system. Please wait while the program adds that plugin..."
                try{
                    # If OS is Windows Server, then install RSAT using a different method
                    if ($osName -match "^Microsoft Windows Server") {
                        # This sequence has confirmed to be valid on Windows Server 2008 R2 and above
                        Write-Verbose "Importing Windows Feature: RSAT-AD-PowerShell"
                        Import-Module ServerManager
                        Add-WindowsFeature RSAT-AD-PowerShell
                        }
                    else{
                        Write-Verbose "This sequence targets Windows Client versions"
                        $destinationFile= ($ENV:USERPROFILE) + "\Downloads\" + (split-path $link -leaf)
                        Write-Host "Downloading RSAT from $link..."
                        Start-BitsTransfer -Source $link -Destination $destinationFile
                        $fileCheck=Get-AuthenticodeSignature $destinationFile
                        if($fileCheck.status -ne "valid") {write-host "$destinationFile is not valid. Please try again...";break}
                        $wusaCommand = $destinationFile + " /quiet"
                        Write-host "Installing RSAT - please wait..."
                        Start-Process -FilePath "C:\Windows\System32\wusa.exe" -ArgumentList $wusaCommand -Wait
                        }
                    return $true
                    }
                catch{
                    write-warning "$($error[0].Exception)"
                    return $false
                    }
            }else{
                Write-host "Prerequisite checks: module ActiveDirectory IS currently available on this system." -ForegroundColor Green
                return $true
                }
        }
        function listAllHyperVNodes($domainObjects){
            try{
                #if(!$domains){$domains=(Get-ADForest).Name|%{(Get-ADForest -Identity $_).Name}}
                $allHyperVNodes=@()
                foreach ($domainObject in $domainObjects){
                    #[string]$dc=(get-addomaincontroller -DomainName "$domain" -Discover -NextClosestSite).HostName
                    $domain=$domainObject.domain
                    $username=$domainObject.username
                    $password=$domainObject.password
                    $encryptedPassword=ConvertTo-securestring $password -AsPlainText -Force
                    $credential=New-Object -TypeName System.Management.Automation.PSCredential -Args $username,$encryptedPassword
                    $dc=if(!($domainObject.dc)){
                        $domainNode=.{try{get-addomain -Server $domain}catch{$false}}
                        if (!$domainNode){return $null}
                        if($domainNode.ReplicaDirectoryServers[1]){
                                $domainNode.ReplicaDirectoryServers[1]
                            }else{
                                $domainNode.PDCEmulator
                            }
                    }else{
                        $domainObject.dc
                    }
                    $session=.{
                        try{
                            new-pssession -computername $dc -credential $credential -EA Stop
                            write-host "Connected to $dc..."
                        }catch{
                            write-warning $_
                            return $false
                        }
                    }
                    if($session){
                        write-host "Collecting all Hyper-V Clusters in $domain. This may take a while, depending on cluster sizes."
                        $allClusters=.{              
                            $clusters=invoke-command -session $session -scriptblock{
                                $rsatClusteringPowershell=get-WindowsFeature RSAT-Clustering-PowerShell
                                if(!$rsatClusteringPowershell.Installed){Add-WindowsFeature RSAT-Clustering-PowerShell}
                                (get-cluster -domain $env:USERDNSDOMAIN).Name
                            }
                            return $clusters
                        }                
                        foreach ($cluster in $allClusters){
                            write-host "Checking $cluster"
                            try{
                                $nodes=invoke-command -computername "$cluster.$domain" -credential $credential -scriptblock{
                                    param($clustername)
                                    #$rsatClusteringPowershell=get-WindowsFeature RSAT-Clustering-PowerShell
                                    #if(!$rsatClusteringPowershell.Installed){Add-WindowsFeature RSAT-Clustering-PowerShell}
                                    $x=Get-ClusterNode -Cluster $clustername -ea SilentlyContinue
                                    if($x){
                                        $x|Where-Object{$_.State -eq 'Up'}|Select-Object Name,@{name='Cluster';e={"$clustername.$env:USERDNSDOMAIN"}}
                                    }else{
                                        $false
                                    }
                                } -Args $cluster -EA Stop|select Name,Cluster
                                if($nodes){$allHyperVNodes+=$nodes} 
                            }catch{
                                write-warning "$cluster is skipped..."
                            }                   
                        }
                        Remove-PSSession $session
                    }else{
                        write-warning "$env:computername cannot connect to $dc..."
                    }
                }
                return $allHyperVNodes
            }catch{
                Write-Error $_
                return $false
                }
        }

        try{
            #$null=includeRSAT;
            #$rsatClusteringPowershell=get-WindowsFeature RSAT-Clustering-PowerShell
            #if(!$rsatClusteringPowershell.Installed){Add-WindowsFeature RSAT-Clustering-PowerShell}                                    
            $hyperVHosts=listAllHyperVNodes $domainObjects
            $hyperVHostNames=$hyperVHosts|sort -property Cluster
            return $hyperVHostNames
        }catch{
            Write-Error $_
            return $false
            }
    }
    function sortArrayStringAsNumbers([string[]]$names){
        $hashTable=@{}
        $maxLength=($names | Measure-Object -Maximum -Property Length).Maximum
        foreach ($name in $names){
            #[int]$x=.{[void]($name -match '(?:.(\d+))+$');$matches[1]}
            #$x=.{[void]($name -match '(?:.(\d+)+)$');@($name.substring(0,$name.length-$matches[1].length),$matches[1])}
            $originalName=$name
            $x=.{
                [void]($name -match '(?:.(\d+)+)\w{0,}$');
                if($matches){
                    [int]$trailingNonDigits=([regex]::match($name,'\D+$').value).length
                    if($trailingNonDigits){
                        $name=$name.substring(0,$name.length-$trailingNonDigits)
                    }
                    return ($name.substring(0,$name.length-$matches[1].length))+$matches[1].PadLeft($maxLength,'0');
                }else{
                    return $name+''.PadLeft($maxLength,'0');
                }}
            $hashTable.Add($originalName,$x)
            }
        $sorted=foreach($item in $hashTable.GetEnumerator() | Sort Value){$item.Name}
        return $sorted
    }

    #write-host "Obtaining cluster names and associated hosts..."
    $hyperVHostsInForest=getHyperVHosts $domainObjects
    $sortedArray=@()
    $clusters=$hyperVHostsInForest|Group-Object -Property Cluster
    foreach($cluster in $clusters){
        $clusterName=$cluster.Name
        write-host $clusterName       
        $sortedHostnames=sortArrayStringAsNumbers $cluster.Group.Name
        $sortedHostnames|%{$sortedArray+=New-Object -TypeName psobject -Property @{hostname=$_; cluster=$clusterName}}
    }
    return $sortedArray
}
function getQuickStats($computername=$env:computername){    
    try{
        # Server Model
        $biosRegKey="REGISTRY::HKEY_LOCAL_MACHINE\Hardware\Description\System\Bios"
        $bios=Get-ItemProperty $biosRegKey
        $model=$bios.SystemProductName

        # RAM (IN GB)
        $ramInfo=get-wmiobject win32_physicalmemory|Select-Object *
        $ramInfo=get-CimInstance win32_physicalmemory|Select-Object *
        $ramGb=.{$sum=0;$ramInfo.Capacity|%{ $sum += $_};return $sum/1GB}

        # RAM Module size
        #$ramModuleSize=($ramInfo.Capacity|select -unique)|%{$_/1GB}
        $ramGroups=$ramInfo.Capacity|Group|select Count,@{name='ramSize';e={($_.Group|select -Unique)/1GB}}
        $ramModuleSize=$ramGroups|%{"($($_.Count)) $($_.ramSize)"}

        # Ram Speed (Mhz)
        # $ramSpeed=$ramInfo.Speed|select -unique
        $ramSpeedGroups=$ramInfo.Speed|Group|select Count,@{name='ramSize';e={$_.Group|select -Unique}}
        $ramSpeed=$ramSpeedGroups|%{"($($_.Count)) $($_.ramSize)"}

        # ramConfiguredClockSpeed
        #$ramConfiguredSpeed=$ramInfo.ConfiguredClockSpeed|select -unique
        $ramConfigSpeedGroups=$ramInfo.ConfiguredClockSpeed|Group|select Count,@{name='ramSize';e={$_.Group|select -Unique}}
        $ramConfiguredSpeed=$ramConfigSpeedGroups|%{"($($_.Count)) $($_.ramSize)"}
        
        # RAM in use
        $osObject=get-wmiobject Win32_OperatingSystem
        $os=$osObject.Caption
        $ramPercentFree=$osObject.FreePhysicalMemory/$osObject.TotalVisibleMemorySize
        $ramUsed=[math]::round($ramGb-($ramPercentFree*$ramGb),2)    

        # Slots filled
        $ramSlotsFilled=($ramInfo|?{$_.Capacity}).count

        # Slots available
        $ramSlotsAvailable=$ramInfo.count
        # Cores
        $cpu=Get-CimInstance Win32_processor
        $cores=.{$total=0;$cpu.NumberOfCores|%{$total+=$_};return $total}

        # Proc count
        $sockets=$cpu.count

        # Cpu Type
        $cpuType=$cpu.Name|select -unique

        # Clock speed
        $cpuClock=($cpu.MaxClockSpeed|Measure-Object -Average).Average
        $cpuBandwidth=$cpuClock*$cores
        $cpuUtilizationPercent=($cpu|Measure-Object -property LoadPercentage -Average).Average
        $cpuUtilization=($cpuUtilizationPercent/100)*$cpuBandwidth

        #$cpuUtilizationPercent=((Get-WmiObject -Class Win32_PerfFormattedData_PerfOS_Processor).PercentProcessorTime|measure-object -Average).Average  
        # $cpuUtilizationPercent=.{#$rawValues=wmic cpu get loadpercentage
        #     $rawValues=$cpu.LoadPercentage
        #     $selectNumbers=$rawValues|?{$_ -match '\d+'}
        #     ($selectNumbers|Measure-Object -Average).Average
        # }

        # $cpuUtilizationPercent=(get-counter -Counter "\Processor(*)\% Processor Time" -SampleInterval 1 -MaxSamples 2 |`
        #     select -ExpandProperty countersamples | select -ExpandProperty cookedvalue | Measure-Object -Average).average
        
        # $cpuUtilization=((Get-process|Select-Object CPU).CPU|Measure-Object -sum).Sum
        # $cpuUtilizationPercent=($cpuUtilization/$cpuBandwidth)*100

        # $cpuProperties=@(
        #     @{Name="processName"; Expression = {$_.name}},
        #     @{Name="cpuPercent"; Expression = {$_.PercentProcessorTime}},    
        #     @{Name="MemoryGb"; Expression = {[Math]::Round(($_.workingSetPrivate / 1GB),2)}}
        # )
        # $totalCpuUsage=(Get-WmiObject -class Win32_PerfFormattedData_PerfProc_Process | 
        #     Select-Object $cpuProperties)|?{$_.'processName' -eq '_Total'}

        # Other methods not being used
        #$cs = Get-WmiObject -class Win32_ComputerSystem
        #$Sockets=$cs.numberofprocessors
        #$Cores=$cs.numberoflogicalprocessors

        $guestVms = Get-VM|Where-Object {$_.State -eq 'Running'}
        $vmsCount = $guestVms.count
    }catch{}  
    return [PSCustomObject][ordered]@{
        node=$computername        
        model=$model
        cpuType=$cpuType
        sockets=$sockets
        cores=$cores
        cpuBandwidth=$cpuBandwidth
        cpuUtilization=[math]::round($cpuUtilization,2)
        cpuUtilizationPercent=[math]::round($cpuUtilizationPercent,2)
        ramGb=$ramGb
        ramUsedGb=[math]::round($ramUsed,2)
        ramUsedPercent=[math]::round(($ramUsed/$ramGb)*100,2)
        ramModuleSizeGb=$ramModuleSize
        ramSlotsAvailable=$ramSlotsAvailable
        ramSlotsFilled=$ramSlotsFilled
        ramSpeed=$ramSpeed
        ramConfigSpeed=$ramConfiguredSpeed
        vmsCount=$vmsCount
        os=$os
    }    
}
function checkSystem($computername=$env:computername,$credential){ 
    $session=if($credential){
            New-PSSession $computername -credential $credential
        }else{
            New-PSSession $computername
        }
    if($session.State -eq 'Opened'){        
        $quickStats=invoke-command -session $session -scriptblock{
                param($getQuickStats)
                [scriptblock]::create($getQuickStats).invoke()
            } -Args ${function:getQuickStats}
        Remove-PSSession $session
    }else{
        Write-Warning "$computername is not Reachable from $env:computername via WinRM"
        $quickStats=$false        
    }
    if($quickStats){
        return $quickStats|Select-Object -Property * -ExcludeProperty PSComputerName,RunspaceId,PSShowComputerName
    }else{
        return [PSCustomObject]@{
            computerName = $computername+' Unreachable'
        }
    }
}
function generateEmailContent{
    param(
        $arrayObjects,
        $selectFields='node,model,cpuType,sockets,cores,cpuBandwidth,cpuUtilizationPercent,ramGb,ramUsedPercent',
        $reportName,
        $summary,
        $css="
        <style>
        .h1 {
            font-size: 18px;
            height: 40px;
            padding-top: 80px;
            margin: auto;
            text-align: center;
        }
        .h5 {
            font-size: 22px;
            text-align: center;
        }
        .th {text-align: center;}
        .table {
            padding:7px;
            border:#4e95f4 1px solid;
            background-color: white;
            margin-left: auto;
            margin-right: auto;
            width: 100%
            }
        .colgroup {}
        .th { background: #0046c3; color: #fff; padding: 5px 10px; }
        .td { font-size: 11px; padding: 5px 20px; color: #000;
            width: 1px;
            white-space: pre;
            }
        .tr { background: #b8d1f3;}
        .tr:nth-child(even) {
            background: #dae5f4;
            width: 1%;
            white-space: nowrap
        }
        .tr:nth-child(odd) {
            background: #b8d1f3;
            width: 1%;
            white-space: nowrap
        }
        </style>
        "
        )
    $filteredArray=invoke-expression "`$arrayObjects|select-object $selectFields"
    $report=$filteredArray|ConvertTo-Html -Fragment|Out-String
    $reportHtml=$report -replace '\<(?<item>\w+)\>', '<${item} class=''${item}''>'
    $emailContent='<html><head>'+$css+"</head><body><h1>$reportName</h1>`n<h5>$summary</h5>"+$reportHtml+'</body></html>'
    return $emailContent
}

function generateHtmlFragment($arrayObjects,$selectFields,$reportName,$summary){
    $filteredArray=invoke-expression "`$arrayObjects|select-object $selectFields"
    $htmlFragment=$filteredArray|ConvertTo-Html -Fragment|Out-String
    $customHtml=$htmlFragment -replace '\<(?<item>\w+)\>', '<${item} class=''${item}''>'
    $customHtmlFragment="<h1>$reportName</h1>`n<h5>$summary</h5>"+$customHtml
    return $customHtmlFragment    
}

function packageHtmlEmailContent{
    param(
        $htmlBody,
        $footer,
        $css="
        <style>
        .h1 {
            font-size: 18px;
            height: 40px;
            padding-top: 80px;
            margin: auto;
            text-align: center;
        }
        .h5 {
            font-size: 22px;
            text-align: center;
        }
        .th {text-align: center;}
        .table {
            padding:7px;
            border:#4e95f4 1px solid;
            background-color: white;
            margin-left: auto;
            margin-right: auto;
            width: 100%
            }
        .colgroup {}
        .th { background: #0046c3; color: #fff; padding: 5px 10px; }
        .td { font-size: 11px; padding: 5px 20px; color: #000;
            width: 1px;
            white-space: pre;
            }
        .tr { background: #b8d1f3;}
        .tr:nth-child(even) {
            background: #dae5f4;
            width: 1%;
            white-space: nowrap
        }
        .tr:nth-child(odd) {
            background: #b8d1f3;
            width: 1%;
            white-space: nowrap
        }
        </style>
        "
        )
    $emailContent='<html><head>'+$css+"</head><body>$htmlBody"+$footer+'</body></html>'
    return $emailContent         
}
function emailReport{
    param(
        $emailFrom,
        $emailTo,
        $subject,
        $emailContent,
        $smtpRelayServer
    )
    Send-MailMessage -From $emailFrom `
    -To $emailTo `
    -Subject $subject `
    -Body $emailContent `
    -BodyAsHtml `
    -SmtpServer $smtpRelayServer    
}

function updateGoogleSheets{
    param(
        $spreadsheetID,
        $sheetName=$env:USERDNSDOMAIN,
        $reportCsv,
        $certPath,
        $certPassword='notasecret',
        $iss,
        $scope="https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.file"
    )
    # Set security protocol to TLS 1.2 to avoid TLS errors
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
    Import-Module UMN-Google

    # obtain token if necessary
    $accessToken = Get-GOAuthTokenService -scope $scope -certPath $certPath -certPswd $certPassword -iss $iss
    # Create new sheet, if not already exists
    try{Add-GSheetSheet -accessToken $accessToken -sheetName $sheetName -spreadSheetID $spreadsheetID}catch{}

    # Upload CSV data to Google Sheets with Set-GSheetData
    $inputCsv = Import-Csv $reportCsv
    $header = $inputCsv[0].psobject.properties.name|select -first 26 # Google default 26 columns max
    $import = new-Object System.Collections.ArrayList
    $import.Add($header) | Out-Null
    $inputCsv | ForEach-Object {
    $row=@(foreach($label in $header){$_."$($label)"})
    $import.Add($row) | Out-Null
    }
    $import.Add(@()) | Out-Null
    $import.Add(@( $summary -split '\r?\n'|%{$_.Trim('<br>$')}|out-string )) | Out-Null
    $columnsCount=$header.count
    $columnLetter=if($columnsCount -le 25){
            [char](65+$columnsCount)
        }else{
            'Z'
        }
    $range="A1:$columnLetter$($import.Count)"
    try{
        $null=Clear-GSheetSheet -spreadSheetID $spreadsheetID -SheetName $sheetName -accessToken $accessToken
        $null=Set-GSheetData -accessToken $accessToken `
            -rangeA1 $range `
            -sheetName $sheetName `
            -spreadSheetID $spreadsheetID `
            -values $import #-Debug -Verbose
        write-host "Google Sheet $sheetName range $range has been updated."          
        return $true
    }catch{
        write-warning $_
        return $false
    }
}

$hyperVClusters=getAllHyperVClusters $domainObjects
$clusterGroups=$hyperVClusters|Group-Object -Property cluster
$emailFragments=''
foreach($group in $clusterGroups){
    $clusterName=$group.Name
    $domain=[regex]::matches($clusterName,'\.(.*)').captures.groups[1].value
    $username=($domainObjects|?{$_.domain.toupper() -eq $domain.toupper()}).username
    $password=($domainObjects|?{$_.domain.toupper() -eq $domain.toupper()}).password
    $encryptedPassword=ConvertTo-securestring $password -AsPlainText -Force
    $credential=New-Object -TypeName System.Management.Automation.PSCredential -Args $username,$encryptedPassword
    write-host $clusterName
    $timeStamp=(get-date).tostring()
    $results=@()
    $hyperVHosts=$group.Group.hostname
    $hostCount=$hyperVHosts.Count
    for ($i=0;$i -lt $hostCount;$i++){
        write-host "Scanning $($i+1) of $hostCount`: $($hyperVHosts[$i])..."
        $results+=checkSystem "$($hyperVHosts[$i]).$domain" $credential
    }
    $totalRam=.{$total=0;$results.ramGb|%{$total+=$_};$total}
    $usedRam=.{$total=0;$results.ramUsedGb|%{$total+=$_};$total}
    $usedRamPercent=[math]::round($usedRam/$totalRam*100,2)
    $availableRam=[math]::round($totalRam-$usedRam,2)
    $totalCpuBandwidth=.{$total=0;$results.cpuBandwidth|%{$total+=$_};$total}
    $usedCpuBandwidth=.{$total=0;$results.cpuUtilization|%{$total+=$_};$total}
    $usedCpuPercent=[math]::round($usedCpuBandwidth/$totalCpuBandwidth*100,2)
    $summary="Data Timestamp: $timeStamp<br>
Hosts Count: $($hyperVHosts.count)<br>
Total CPU Bandwidth: $([math]::round($totalCpuBandwidth/1000)) Ghz<br>
Used CPU Percent: $usedCpuPercent %<br>
Total RAM: $totalRam GB<br>
Used RAM: $usedRam GB<br>
Used RAM Percent: $usedRamPercent %<br>
Available RAM: $availableRam GB<br>"
    $reportName="Hyper-V Capacity Report for Cluster $clustername"
    $emailFragment=generateHtmlFragment $results $selectFields $reportName $summary
    $emailFragments+=$emailFragment
    $reportCsvPath=join-path $workingDirectory "$clustername.csv"
    $null=$results|export-csv $reportCsvPath -NoTypeInformation   
    updateGoogleSheets $spreadsheetID $clusterName $reportCsvPath $certPath $certPassword $iss $scope
    }

$today=(get-date).DayOfWeek
if ($today -eq $emailDay){
    $footer="<br>GoogleSheets URL: $spreadsheetUrl<br>"
    $emailContent=packageHtmlEmailContent $emailFragments $footer
    emailReport $emailFrom $emailTo $subject $emailContent $smtpRelayServer
}

Leave a Reply

Your email address will not be published. Required fields are marked *

Related Post

How to increase the database size limit on Exchange 2003 SP2

Connect to the Server that is running Exchange 2003 SP2 Click Start > Run >…

Active Directory Domain Controller Certificates Installation Guide

Overview This article deals with Domain Controllers, rather than member servers. Thus, Active Directory or…

Manually Dealing with Windows Updates

This unabridged note contains some usable copy/paste lines to work with Windows updates. There's a…