PowerShell: Find Locking PID of a File

$filePath="C:\Program Files\Google\Chrome\Application\chrome.exe"

function findPidOfFile($filepath){    
    try{
        if (!(Get-Command handle.exe -ErrorAction SilentlyContinue)) {
            if (!(Get-Command choco.exe -ErrorAction SilentlyContinue)) {
            [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
            Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))    
            }
            choco install sysinternals -y --ignore-checksums
        }
        $handles=handle.exe
        $matchedLines=$handles|?{$_ -like "*$filepath*"}
        $lockingPids=@();
        $lastKnownPid="";
        if($null -ne $matchedLines){
            foreach ($line in $matchedLines) {
            $lastKnownPid=.{
                [void]($line -match "pid:\s(.*)\s");
                if ($matches[1]){return $matches[1]}
                }
            if ($line -like "*$filepath*") {
                return $lastKnownPid;
                }
            }
        }else{
            write-host "$filepath does NOT currently have a locking pid"
        }
    }catch{
        write-warning $_
    }
}

findPidOfFile $filePath

PowerShell: Automatically Log Off Idling Remote Desktop Sessions

$computernames=@(
  'LAX-RDSNODE01'
)
$idleDaysThreshold=3
$forcedLogoff=$true

# Obtain credentials being passed by Jenkins
$runasUser=$env:runasUser
$runasPassword=$env:runasPassword
$encryptedPassword=ConvertTo-SecureString $runasPassword -AsPlainText -Force
$runasCredentials=New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $runasUser,$encryptedPassword

# Email relay parameters
$emailFrom='reports@kimconnect.com'
$emailTo='sysadmins@kimconnect.com','sysAdmins@dragoncoin.com'
$subject='Hyper-V Hosts Capacity Report'
$smtpRelayServer='relay02.dragoncoin.com'


function getSessionsInfo([string[]]$computernames=$env:computername,$runasCredentials){
  $results=@()
  function checkPorts($servers,$ports){
    function includePortQry{
        if (!(Get-Command portqry.exe -ErrorAction SilentlyContinue)){
            if (!(Get-Command choco.exe -ErrorAction SilentlyContinue)) {
            Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))}
            choco install portqry -y
            if (Get-Command portqry.exe -ErrorAction SilentlyContinue){return $true}else{return $false}
        }else{
            return $true
        }
    }
    $portQryExists=includePortQry
    if(!$portQryExists){
      write-warning "Unable to proceed without portqry"
      return $false
    }
    $results=@()
    foreach ($remoteComputer in $servers){
      #$ip=[System.Net.Dns]::GetHostAddresses($remoteComputer).IPAddressToString|select -first 1
      write-host "Now scanning $remoteComputer"
      $result=@()
      foreach ($item in $ports){
          $port=$item.port
          $protocol=$item.protocol        
          $reachable=if($port -ne 135){                
                  $command={
                      param($remoteComputer,$protocol,$port)
                      $command="portqry -n $remoteComputer -p $protocol -e $port|find ': LISTENING'"
                      invoke-expression $command
                  }
                  $jobId=(Start-Job $command -Args $remoteComputer,$protocol,$port).Id
                  $startCount=0;$waitSeconds=5
                  do{
                      $jobStatus=(get-job -id $jobId).State
                      if($jobStatus -eq 'Completed'){
                          $jobResult=receive-job $jobId
                      }else{
                          if($startCount++ -eq $waitSeconds){
                              $jobStatus='Completed'
                              $null=remove-job -id $jobId -force
                              $jobResult=$false
                          }else{
                              sleep 1
                          }                        
                      }
                  }until($jobStatus -eq 'Completed')
                  [bool]($jobResult)
              }else{
                  [bool](portqry -n $remoteComputer -p $protocol -e $port|find 'Total endpoints found: ')
              }       
          write-host "$port/$protocol : $reachable"
          $result+=[PSCustomObject]@{
              computername=$remoteComputer
              port=$port
              protocol=$protocol
              reachable=$reachable
          }
      }
      #$resultString=$results.GetEnumerator()|sort-object {[int]$_.Name}|out-string
      $results+=$result
      $resultString=$result|sort-object -property port|out-string
      write-host $resultString
    }
    return $results
  }
  function getDisconnectedSessionInfo($line,$computer){
           # convert multiple spaces into single space and split pieces into array
          $thisLine = $line.Trim() -Replace '\s+',' ' -Split '\s'
          $properties = @{
              UserName = $thisLine[0]
              ComputerName = $computer
              }
          $properties.SessionName = $null
          $properties.Id = $thisLine[1]
          $properties.State = $thisLine[2]                        
          $properties.IdleMinutes=.{[string]$x=$thisLine[3]
                          switch -regex ($x){
                              '\.' {0;break}                   
                              '\+' {$dayMinutes=.{[void]($x -match '^(\d+)\+');[int]($matches[1])*1440}
                                      $hourMinutes=.{[void]($x -match '\+(.*)$');([TimeSpan]::Parse($matches[1])).totalMinutes}
                                      $dayMinutes+$hourMinutes
                                      break;
                                   }               
                              '\:' {try{
                                      ([TimeSpan]::Parse($x)).totalMinutes
                                      }catch{
                                          "Invalid value: $x"
                                          }
                                      break
                                      }
                              default {$x}
                          }                                  
                      }
          $properties.LogonTime = $thisLine[4..6] -join ' '
          $result=New-Object -TypeName PSCustomObject -Property $properties
          return $result
  }

  function getConnectedSessionInfo($line,$computer){
          $thisLine = $line.Trim() -Replace '\s+',' ' -Split '\s'
          $properties = @{
              UserName = $thisLine[0]
              ComputerName = $computer
              }
          $properties.SessionName = $thisLine[1]
          $properties.Id = $thisLine[2]
          $properties.State = $thisLine[3]
          $properties.IdleMinutes=.{$x=$thisLine[4]
                          switch -regex ($x){
                              '\.' {0;break}                   
                              '\+' {$dayMinutes=.{[void]($x -match '^(\d+)\+');[int]($matches[1])*1440}
                                      $hourMinutes=.{[void]($x -match '\+(.*)$');([TimeSpan]::Parse($matches[1])).totalMinutes}
                                      $dayMinutes+$hourMinutes
                                      break;
                                   }               
                              '\:' {try{
                                      ([TimeSpan]::Parse($x)).totalMinutes
                                      }catch{
                                          "Invalid value: $x"
                                          }
                                      break
                                      }
                              default {$x}
                          }                                 
                      }
          $properties.LogonTime = $thisLine[5..($thisLine.GetUpperBound(0))] -join ' '
          $result=New-Object -TypeName PSCustomObject -Property $properties
          return $result
  }

  foreach ($computer in $computernames){
    if((checkPorts $computer @{protocol='tcp';port=135}).reachable){
      try {
        # Perusing legacy commandlets as there are no PowerShell equivalents at this time
        $sessions=if($runasCredentials){
            invoke-command -computername $computer -credential $runasCredentials {quser /server:$computer 2>&1 | Select-Object -Skip 1}
        }else{
            quser /server:$computer 2>&1 | Select-Object -Skip 1
        }
        ForEach ($session in $sessions) {               
            $result=$null
            $thatLine = $session.Trim() -Replace '\s+',' ' -Split '\s'
            if ($thatLine[2] -eq 'Disc'){
                $result=getDisconnectedSessionInfo $thatLine $computer
            }elseif(!$onlyDisconnected){
                $result=getConnectedSessionInfo $thatLine $computer
            }
            if($result){$results+=$result}
        }
      }catch{
          $results+=New-Object -TypeName PSCustomObject -Property @{
              ComputerName=$computer
              Error=$_.Exception.Message
          } | Select-Object -Property UserName,ComputerName,SessionName,Id,State,IdleMinutes,LogonTime,Error|sort -Property UserName
      }
    }else{
      $results+=New-Object -TypeName PSCustomObject -Property @{
        ComputerName=$computer
        Error=$_.Exception.Message
      } | Select-Object -Property UserName,ComputerName,SessionName,Id,State,IdleMinutes,LogonTime,Error|sort -Property UserName
    }
  }
  return $results
}

#getSessionsInfo $computernames $runasCredentials

function logOffRdpSession{
  param(
    $serverName=$env:computername,
    $username,
    $idleMinutes,
    $runasCredentials
  )
  $username=if($username -match '\\'){[regex]::match($username,'\\(.*)$').captures.groups.value[1]
    }else{
      $username.tostring()
    }
  $sessions=if(!$runasCredentials){
      qwinsta /server:$serverName
  }else{
      invoke-command -computername $servername -credential $runasCredentials -scriptblock {qwinsta /server:$env:computername}
  }
  $sessionId=.{
      $sessionMatch=$sessions|?{$_ -match $username}
      if($sessionMatch){
          $array=$sessionMatch -replace '(^\s+|\s+$)','' -replace '\s+',' ' -split ' '
          return $array|?{$_.tostring() -match '\d+'}
      }else{
          return $null  
      }
  }
  if($sessionId){
    if(!$runasCredentials){
        $sessionId|%{rwinsta $_ /SERVER:$serverName}
        $sessions=qwinsta /server:$serverName
        $newSessionId=.{
            $sessionMatch=$sessions|?{$_ -match $username}
            if($sessionMatch){
                $array=$sessionMatch -replace '(^\s+|\s+$)','' -replace '\s+',' ' -split ' '
                return $array[2]
            }else{
                return $null  
            }
        }
    }else{
        invoke-command -computername $servername -credential $runasCredentials -scriptblock{
            param($sessionId,$username)
            $sessionId|%{rwinsta $_ /SERVER:$env:computername}
            $sessions=qwinsta /server:$env:computername
            $newSessionId=.{
                $sessionMatch=$sessions|?{$_ -match $username}
                if($sessionMatch){
                    $array=$sessionMatch -replace '(^\s+|\s+$)','' -replace '\s+',' ' -split ' '
                    return $array[2]
                }else{
                    return $null  
                }
            }
        } -Args $sessionId,$username
    }
    if(!$newSessionId){
      write-host "$username RDP session ID $sessionId on $serverName has been forcefully disconnected due to its idling $idleMinutes minutes."
      return $true
    }else{
      write-warning "$username RDP session ID $sessionId still exists on $serverName"
      return $false
    }      
  }else{
    write-host "$username doesn't have an RDP session on $serverName"
    return $true
  }
}

# logOffRdpSession $computerName $username $runasCredentials

$allSessions=getSessionsInfo $computernames $runasCredentials
$targetSessions=$allSessions|?{[Int]$_.IdleMinutes -ge $idleDaysThreshold*1440}
#$logoffSessions=@()
if($forcedLogoff){
  foreach($session in $targetSessions){
    $result=logOffRdpSession $session.ComputerName $session.UserName $session.IdleMinutes $runasCredentials
    if($result){
      $targetSessions=$targetSessions|?{$_ -ne $session}
      #$logoffSessions+=$sessions
    }
  }
}
$sessionsToEmail=$targetSessions # future development: add a routine to check whether current report is identical to previous

if($null -ne $sessionsToEmail){
  $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
  }
  pre code {
    background-color: #eee;
    border: 1px solid #999;
    display: block;
    padding: 20px;
  }
  </style>
  "
  $howToLogOffSessionCode="<pre><code>function logOffRdpSession"+${function:logOffRdpSession}+"}`r`n`r`nlogOffRdpSession SERVERNAME USERNAME</code></pre>"
  $sessionsReformatted=$sessionsToEmail|select ComputerName,UserName,@{n='sessionId';e={$_.id}},IdleMinutes,State,LogonTime
  $currentReport=$sessionsReformatted|ConvertTo-Html -Fragment|Out-String
  $currentReportHtml=$currentReport -replace '\<(?<item>\w+)\>','<${item} class=''${item}''>'
  $howToLogOffSessionCode=$howToLogOffSessionCode -replace '\<(?<item>\w+)\>','<${item} class=''${item}''>'
  $emailContent='<html><head>'+$css+"</head><body><h5 class='h5'>$subject</h5>"+$currentReportHtml+"<br><br><h1 class='h1'>Function to forcefully log off a session</h1>"+$howToLogOffSessionCode+'</body></html>'
  Send-MailMessage -From $emailFrom `
  -To $emailTo `
  -Subject $subject `
  -Body $emailContent `
  -BodyAsHtml `
  -SmtpServer $smtpRelayServer 
}else{
  write-host "No idle sessions to notify Admins."
}

PowerShell: How to Reset Windows Update Service

# resetWindowsUpdateService
# This is a legacy method of reseting Windows Update
# Since most enterprises are having antiviruses nowadays, I've included an example of how to disable Palo Alto XDR Traps are a required to reset WuApp

$trapsAdminPassword='PASSWORDHERE'
$trapsBin='C:\Program Files\Palo Alto Networks\Traps'

function resetWindowsUpdateService{
	try{
		net stop wuauserv
		net stop cryptSvc
		net stop bits
		net stop msiserver
		mkdir C:\emptyDirectory
		md -Force C:\emptyDirectory
		Remove-Item "C:\emptyDirectory\*" -force -recurse -ErrorAction Continue
		robocopy C:\emptyDirectory C:\Windows\SoftwareDistribution /mir /R:0 /W:0 /NP
		robocopy C:\emptyDirectory C:\Windows\System32\catroot2 /mir /R:0 /W:0 /NP
		net start wuauserv
		net start cryptSvc
		net start bits
		net start msiserver
		return $true
	}catch{
		write-warning $_
	}
}

function stopXdr{
	param(
		$trapsAdminPassword,
		$trapsBin='C:\Program Files\Palo Alto Networks\Traps'
	)
	echo $trapsAdminPassword | & "$trapsBin\cytool.exe" runtime stop
}

function startXdr{
	param(
		$trapsAdminPassword,
		$trapsBin='C:\Program Files\Palo Alto Networks\Traps'
	)
	echo $trapsAdminPassword | & "$trapsBin\cytool.exe" runtime start
}

stopXdr $trapsAdminPassword
resetWindowsUpdateService
startXdr $trapsAdminPassword

PowerShell: Maintaining Processes on Remote Servers

# maintainProcess.ps1
# version 0.0.1

$computernames=@(
    'server1',
    'server2'
)
$processName='cmd'
$processPath='C:\WINDOWS\system32\cmd.exe'
$minutesToDefineCrashed=1 # this marker is only valid if the process is marked as unresponsive by the system
$runMinutes=1
$credentials=$null
$maxMinutesPerJob=10
$verbose=$true

function maintainProcess{
    param(
        $targetMachines=$env:computername,
        $processname='cmd',
        $processPath='C:\Windows\system32\cmd.exe',
        $minutesToDefineCrashed=1,
        $runMinutes=2,
        $credentials
        )
    $actionOnCrash={param($processName,$processPath) 
        stop-process -name $processName -force -ea Ignore
        if($processPath){$null=start-process -FilePath $processPath}else{$null=start-process -Name $processName}
    }    
    
    function restartProcess($actionOnCrash,$processname,$processPath,$psSession){
        invoke-command -session $psSession -scriptblock $actionOnCrash -Arg $processname,$processPath
        $processIsRunning=$null -ne (invoke-command -session $psSession -scriptblock {param($processname)get-process -name $processName -EA SilentlyContinue} -Args $processname)
        if($processIsRunning){
            write-host "$processName has successfully restarted on $targetMachine"
            return $true
        }else{
            write-host "$processName has NOT successfully restarted on $targetMachine"
            return $false
        }
    }
    
    $results=[hashtable]@{}
    foreach($targetMachine in $targetMachines){
        $overallClock=[System.Diagnostics.Stopwatch]::StartNew()
        $psSession=if($credentials){
            New-PSSession $targetMachine -Credential $credentials
        }else{
            New-PSSession $targetMachine
            }     
        if($psSession.State -eq 'Opened'){      
            $previousCpuConsumption=0
            write-host "Checking CPU consumption of $processName on $targetMachine"
            do{
                $iterationClock=[System.Diagnostics.Stopwatch]::StartNew()          
                $process=Invoke-Command -Session $psSession {param($processname) Get-Process $processname -EA SilentlyContinue} -Args $processName
                if($null -eq $process){
                    write-warning "$processname is NOT running on $targetMachine"
                    $result=restartProcess $actionOnCrash $processname $processPath $psSession                    
                }else{
                    $responding=$process.Responding
                    $currentCpuConsumption=$process.CPU
                    write-host $currentCpuConsumption
                    $cpuConsumptionChanged=$currentCpuConsumption -ne $previousCpuConsumption
                    $noActivities=$iterationClock.elapsed.totalminutes -ge $minutesToDefineCrashed
                    if($cpuConsumptionChanged){
                        $null=$iterationClock.reset
                        $previousCpuConsumption=$currentCpuConsumption
                        $result=$true
                    }elseif(!$responding -and $noActivities){
                        write-warning "$processName has CRASHED on $targetMachine as defined its RESPONDING flag equal False and there are no activities."
                        $result=restartProcess $actionOnCrash $processname $processPath $psSession
                    }
                    sleep -Seconds 10
                }
            } until ($overallClock.elapsed.totalminutes -ge $runMinutes)   
            Remove-PSSession $psSession
            $minutesElapsed=[math]::round($overallClock.Elapsed.TotalMinutes,2)
            write-host "Runtime of $minutesElapsed minutes has elapsed for $targetMachine"
            $results+=[hashtable]@{$targetMachine=$result}
        }else{
            write-warning "Unable to open a WinRM session to $targetMachine.`r`nPlease monitor it's progress manually."
            $results+=[hashtable]@{$targetMachine=$null}
        }
    }
    return $results
}

# maintainProcess $targetMachines $processname $processPath $minutesToDefineCrashed $runMinutes $credential
function maintainProcessParallel{
    param(
        $computerNames=$env:computername,
        $processname='cmd',
        $processPath='C:\Windows\system32\cmd.exe',
        $minutesToDefineCrashed=1, # this marker is only valid if the process is marked as unresponsive by the system
        $runMinutes=2,
        $credentials,
        $maxMinutesPerJob=10,
        $verbose=$true
    )

    $timer=[System.Diagnostics.Stopwatch]::StartNew()
    $jobResults=@()
    $lineBreak=60
    $dotCount=0
    $minute=0
    $processorsCount=(Get-CimInstance Win32_ComputerSystem).NumberOfLogicalProcessors
    $cpuLoad=(Get-WmiObject win32_processor|Measure-Object -property LoadPercentage -Average).Average
    $maxSimultaneousJobs=if($cpuLoad -gt 90){$processorsCount}else{($processorsCount*2)-1} # dynamically limiting concurrent jobs basing on available CPU cores
    write-host "CPU load detected as: $cpuLoad`%`r`nSetting concurrent jobs max count to be $maxSimultaneousJobs"
    $jobtimer = @{}
    foreach($computerName in $computerNames){
        $thisIterationCompleted=$false
        do {
            $jobsCount=(Get-Job -State 'Running').Count
            if ($jobsCount -lt $maxSimultaneousJobs){            
                if($verbose){write-host "Initiating job for $computerName"}
                $job=Start-Job -name $computerName -ScriptBlock {
                    param($maintainProcess,$computername,$processname,$processPath,$minutesToDefineCrashed,$runMinutes,$credentials)
                    [scriptblock]::create($maintainProcess).invoke($computername,$processname,$processPath,$minutesToDefineCrashed,$runMinutes,$credential)
                } -Args ${function:maintainProcess},$computername,$processname,$processPath,$minutesToDefineCrashed,$runMinutes,$credentials
                $jobtimer[$job.Id]=[System.Diagnostics.Stopwatch]::startnew()
                $thisIterationCompleted=$true
            }else{
                if($verbose){
                    if($dotCount++ -lt $lineBreak){
                        write-host '.' -NoNewline
                    }else{
                        $minute++
                        write-host "`r`n$minute`t:" -ForegroundColor Yellow -NoNewline
                        $dotCount=0
                        }
                }
                sleep -seconds 1
            }
            $expiredJobs=$jobtimer.GetEnumerator()|?{$_.value.elapsed.totalminutes -ge $maxMinutesPerJob}
            if($expiredJobs){
                $expiredJobs.Name|%{stop-job -name $_ -EA Ignore}
                $expiredJobs.Name|%{$jobTimer.Remove($_)}
            }            
        }until ($thisIterationCompleted)
    }
    $totalJobsCount=(get-job).count
    $processedCount=0
    while($processedCount -lt $totalJobsCount){
        $completedJobs=get-job|?{$_.State -eq 'Completed'}
        $stoppedJobs=get-job|?{$_.State -eq 'Stopped'}
        $expiredJobs=$jobtimer.GetEnumerator()|?{$_.value.elapsed.totalminutes -ge $maxMinutesPerJob}
        if($expiredJobs){
            $expiredJobs.Name|%{stop-job -name $_ -EA Ignore}
            $expiredJobs.Name|%{$jobTimer.Remove($_)}
        }
        if($completedJobs){
            foreach ($job in $completedJobs){
                $computer=$job.Name
                if($verbose){
                    write-host "`r`n===================================================`r`n$computer job COMPLETED with these messages:`r`n===================================================`r`n"
                }
                $jobResult=receive-job -id $job.id
                $jobResults+=,$jobResult
                remove-job -id $job.id -force
                $processedCount++
            }
        }
        if($stoppedJobs){
            foreach ($job in $stoppedJobs){
                $computer=$job.Name
                if($verbose){
                    write-host "`r`n===================================================`r`n$computer job STOPPED with these messages:`r`n===================================================`r`n" -ForegroundColor Red
                }
                $jobResult=receive-job -id $job.id
                # $jobResults+=,$jobResult
                $timeZoneName=[System.TimeZoneInfo]::Local.StandardName
                $abbreviatedZoneName=if($timeZoneName -match ' '){[regex]::replace($timeZoneName,'([A-Z])\w+\s*', '$1')}else{$timeZoneName}
                $timeStampFormat="yyyy-MM-dd HH:mm:ss $abbreviatedZoneName"
                $timeStamp=[System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId([datetime]::UtcNow,$timeZoneName).ToString($timeStampFormat)
                $jobResult=[pscustomobject]@{
                    timeStamp=$timeStamp
                    computerName=$computer
                    stoppedServices='serverTimeout'
                    fixedAutomatically=$False
                    }
                $jobResults+=,$jobResult
                remove-job -id $job.id -force
                $processedCount++
            }
        }

        # Safeguard against stuck jobs
        if($timer.elapsed.totalminutes -ge $maxMinutesPerJob){
            get-job|Remove-Job -Force
            write-warning "There were some errors in this iteration. Shell was aborted to mitigate potential persistency issues."
            exit
        }
    }    
    $minutesElapsed=[math]::round($timer.Elapsed.TotalMinutes,2)
    $timer.stop()
    write-host "$($computerNames.count) computers were processed in $minutesElapsed minutes."
    return $jobResults #|select -property * -excludeproperty RunspaceId
}

maintainProcessParallel $computerNames `
    $processname `
    $processPath `
    $minutesToDefineCrashed `
    $runMinutes `
    $credentials `
    $maxMinutesPerJob `
    $verbose

PowerShell: Installing a Program from Its Zip Archive

# installProgramFromExeZipArchive.ps1

# User inputs
$computernames=@(
	'SERVER001',
	'SERVER002'
)
$fileURL="https://dl.dell.com/FOLDER08543783M/1/DellEMC-iDRACTools-Web-WINX64-10.3.0.0-4945.exe"
$expectedExecutable='racadm.exe'
$expectedInstallPath='C:\Program Files\Dell\SysMgt\iDRACTools\racadm'
$stageFolder='C:\Temp\'

function installProgramFromExeZipArchive{
	param(
		$computernames=$env:computername,
		$fileURL="https://dl.dell.com/FOLDER07549599M/1/DellEMC-iDRACTools-Web-WINX64-10.2.0.0-4583_A00.exe",
		$expectedExecutable='racadm.exe',
		$expectedInstallPath='C:\program files\Dell\SysMgt\iDRACTools\racadm',
		$stageFolder='C:\Temp\'
	)
	$results=[hashtable]@{}
	foreach($computerName in $computernames){
		$alreadyInstalled=invoke-command -computername $computername -scriptblock{param($expectedExecutable);try{get-command $expectedExecutable -EA Ignore}catch{$false}} -Args $expectedExecutable
		if($alreadyInstalled){
			write-host "$computername has already installed $expectedExecutable"
			$results+=[hashtable]@{$computername=$true}
		}else{
			# Autogen variables
			$fileName=[regex]::match($fileURL,'[^/\\&\?]+\.\w{3,4}(?=([\?&].*$|$))').value
			$translatedVolume=[regex]::match($stageFolder,'^(\w)\:').captures.groups[1].value+'$'
			$translatedFoldername=[regex]::match($stageFolder,'\:(.*)$').captures.groups[1].value
			$remoteSmbPath=join-path $('\\'+$computerName+"\$translatedVolume") $translatedFoldername
			try{
				# Download the file directly onto target server's staging folder
				Import-Module BitsTransfer
				if(!(test-path $remoteSmbPath)){$null=mkdir $remoteSmbPath}
				# Start-BitsTransfer -Source $fileURL -Destination $output
				[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
				Start-BitsTransfer -Source $fileURL -Destination $remoteSmbPath
				$psSession=new-pssession $computerName
				if($psSession.State -eq 'Opened'){
					$result=invoke-command -Session $psSession -ScriptBlock{
						param($stageFolder,$filename,$expectedExecutable,$expectedInstallPath)
						# write-host "$env:computername : $stageFolder,$filename,$expectedExecutable,$expectedInstallPath"
						# pause					
						$commandAvailable=try{get-command $expectedExecutable -EA Ignore}catch{$false}
						$toInstall=if(!$commandAvailable){
							if(!($env:path -like "*$expectedInstallPath*")){$env:path+=";$expectedInstallPath"}						
							$null=RefreshEnv
							$commandNowAvailable=try{get-command $expectedExecutable -EA Ignore}catch{$false}
							if($commandNowAvailable){$false}else{$true}
						}else{
							write-host "Command $expectedExecutable is available on $env:computername"
							$true
						}
						if ($toInstall){
							$exeExists=try{ls $expectedInstallPath -EA Ignore|?{$_.Name -like $expectedExecutable}}catch{$false}
							if($exeExists){
								$env:path+=";$expectedInstallPath"
								$null=RefreshEnv
								$commandNowAvailable=try{get-command $expectedExecutable -EA Ignore}catch{$false}
								if($commandNowAvailable){return $true}else{return $false}
							}else{
								$expectedFileLocation=join-path $stageFolder $filename
								$zipFilename=$([System.IO.Path]::GetFileNameWithoutExtension($expectedFileLocation))+'.zip'
								$newZipFile=join-path $stageFolder $zipFilename
								#[System.IO.Path]::GetExtension($expectedFileLocation)
								Rename-Item -Path $expectedFileLocation -NewName $newZipFile -EA Ignore
								# $null=Expand-Archive -Path $newZipFile -DestinationPath $stageFolder
								# alternative method: backward compatible to older systems
								Add-Type -AssemblyName System.IO.Compression.FileSystem
								$null=[System.IO.Compression.ZipFile]::ExtractToDirectory($newZipFile, $stageFolder)
								$msiFile=(ls $stageFolder|?{$_.FullName -match '\.msi$'}|sort -property LastWriteTime|select -first 1).FullName
								Start-Process $msiFile -ArgumentList "/quiet" -wait
								# this command doesn't work: msiexec /i $msiFile /quiet /qn /norestart
								$environmentalPathExists=$env:path -like "*$expectedInstallPath*"
								if(!$environmentalPathExists){
									$env:path+=";C:\Program Files\Dell\SysMgt\iDRACTools\racadm"
									$null=RefreshEnv
								}
								$commandNowAvailable=try{get-command $expectedExecutable -EA Ignore}catch{$false}
								if($commandNowAvailable){return $true}else{return $false}
							}
						}					
					} -Args $stageFolder,$fileName,$expectedExecutable,$expectedInstallPath
					Remove-PSSession $psSession
					write-host "$computername result: $result"
					$results+=[hashtable]@{$computername=$result}
				}else{
					write-warning "Unable to connect to $computername via WinRM"
					$results+=[hashtable]@{$computername=$null}
				}
			}catch{
				write-warning $_
				$results+=[hashtable]@{$computername=$null}
			}
		}
	}
	return $results
}
installProgramFromExeZipArchive

How To Remove A Program on Windows Using PowerShell

# removeAppwizProgram.ps1
# Version 0.02

$computernames=@(
    'SERVER0001',
'SERVER0002'
)
$appName='Dell EMC OpenManage Systems Management Software (64-Bit)'
function removeAppwizProgram($computernames=$env:computername,$appName='Firefox'){
    $results=[hashtable]@{}
    foreach($computer in $computernames){
        $session=new-pssession $computer
        if($session.State -eq 'Opened'){
            $result=invoke-command -session $session -scriptblock{
                param($appName)
                write-host "Checking $env:computername..."
                try{
                    # Method 1: try using the Uninstall method of the application packager
                    $app=Get-WmiObject -Class Win32_Product -Filter "Name='$appName'"
                    if($app.Name -eq $appName){
                        write-host "Uninstalling $app"
                        # pause
                        $null=$app.Uninstall()
                        $appStillExists=Get-WmiObject -Class Win32_Product -Filter "Name='$appName'"
                        if($appStillExists){
                            write-host "'$appName' still exists"
                            # Method 2: Using Registry
                            $uninstallStringRegPaths='HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall','HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
                            $uninstallStrings=Get-ChildItem -Path $uninstallStringRegPaths
                            $uninstallString=($uninstallStrings|Get-ItemProperty|Where-Object {$_.DisplayName -match $appName}).UninstallString
                            if($uninstallString.count -eq 1){
                                $appCode=[regex]::match($uninstallString,'\{(.*)\}').Value
                                $uninstallCommand="& msiexec.exe /x $appCode /quiet /norestart"
                                write-host "Invoking uninstall Command: $uninstallCommand"
                                Invoke-Expression $uninstallCommand
                                $appStillExists=Get-WmiObject -Class Win32_Product -Filter "Name='$appName'"
                                if($appStillExists){
                                    write-warning "Uninstall has been unsuccessful at removing $appName"
                                    return $false
                                }else{
                                    return $true
                                }
                            }else{
                                write-warning "Please check this/these uninstall string(s):`r`n$uninstallString"
                                return $false
                            }
                        }else{
                            write-host "'$appName' has been removed"
                            return $true
                        }
                    }else{
                        write-host "No matches for $appName"
                        return $true
                    }                   
                }catch{
                    write-warning $_
                    return $false
                }
            } -Args $appName
            $results+=@{$computer=$result}
            remove-pssession $session
        }else{
            write-warning "Unable to connect to $computer"
            $results+=@{$computer=$null}
        }        
    }
    return $results
}

removeAppwizProgram $computernames $appName

Using Command Line to Configure iDrac Settings

Configure iDrac Settings
#Download and install RacAdm 10.2 :
$url='https://dl.dell.com/FOLDER07549599M/1/DellEMC-iDRACTools-Web-WINX64-10.2.0.0-4583_A00.exe'
$tempDir='C:\Temp'
mkdir $tempDir; cd $tempDir
wget $url
write-host "Please install RacAdm from $temp"
# Run this command after RacAdm is installed
$env:path += ";C:\Program Files\Dell\SysMgt\iDRACTools\racadm"; refreshenv
# Setup iDrac with standardized default settings
$adminUid=2
$adminPassword='insertPasswordHere'
racadm set iDRAC.Users.$adminUid.UserName admin
racadm set iDRAC.Users.$adminUid.Password $adminPassword
racadm set iDRAC.NIC.DNSRacName ($env:computername).tolower()
racadm set iDRAC.NIC.DNSRegister 0
racadm set iDRAC.IPv4.DHCPEnable 1
racadm set iDRAC.IPv4.DNSFromDHCP 1
racadm set iDRAC.NIC.DNSDomainFromDHCP 1
racadm set iDRAC.NIC.VLanEnable

PowerShell: Gather Information About Windows Shutdown Reasons

Copy and Paste this to See Result(s):

$computername=$env:computername
$limitEventsCount=40000
$daysSearchLimit=30

function getWindowsShutdownReason{
    param(
        $computername=$env:computername,
        $limitEventsCount=10000,
        $daysSearchLimit=7
    )
    try{
        $events = Get-WinEvent -ComputerName $computername -FilterHashtable @{
            Logname = 'system'
            Id = '1074', '6008'
            StartTime = (Get-Date).AddDays(-$daysSearchLimit)
        } -MaxEvents $limitEventsCount -ErrorAction Stop
        # There are 2 types of shutdown codes (1074 = user initiated; 6008 = abrupt shutdowns)
        foreach ($event in $events) {
            if ($event.Id -eq 1074) {
                [PSCustomObject]@{
                    TimeStamp    = $event.TimeCreated
                    ComputerName = $computername
                    UserName     = $event.Properties.value[6]
                    ShutdownType = $event.Properties.value[4]
                }
            }
            if ($event.Id -eq 6008) {
                [PSCustomObject]@{
                    TimeStamp    = $event.TimeCreated
                    ComputerName = $computername
                    UserName     = $null
                    ShutdownType = 'unexpected shutdown'
                }
            }
        }
    }catch{
        write-warning $_
    }    
}

getWindowsShutdownReason $computername $limitEventsCount $daysSearchLimit

Clean Windows C Volume

Have you ever ran into a sluggish performance on a Windows machine that is being shared by many [remote] users? Chances are that each user has lots of junk files that would be saved in certain important volumes, such as C:\ drive. Thus, I’ve put together these spaghetti lines to automate maintenance of these machines.

# cleanCVolume.ps1
# Version 0.02
# This little program performs routine cleanup of 7

## Variables Declaration ####
$directoriesToPurge=@(
    "$env:SystemDrive\Windows\Prefetch\", # $prefetchData
    "$env:SystemDrive\Windows\Logs\CBS\", # $cbs
    "$env:SystemDrive\swtools\", # $swtools
    "$env:SystemDrive\drivers\", # $drivers
    "$env:SystemDrive\swsetup\", # $swsetup
    #"$env:SystemDrive\Windows\SoftwareDistribution",
    "$env:SystemDrive\Windows\SoftwareDistribution\Download\", #$softwareDistribution
    'C:\windows\Temp',
    'C:\Temp',
    'C:\ProgramData\Microsoft\Windows\WER\ReportArchive',
    'C:\ProgramData\Microsoft\Windows\WER\ReportQueue',
    'C:\ServiceProfiles\LocalService\AppData\Local\Temp'
)
$maxConfirmations=3
$startComponentCleanup=$false

function cleanWindows{
    param(
        $directoriesToPurge=@(
            "$env:SystemDrive\Windows\Prefetch\", # $prefetchData
            "$env:SystemDrive\Windows\Logs\CBS\", # $cbs
            "$env:SystemDrive\swtools\", # $swtools
            "$env:SystemDrive\drivers\", # $drivers
            "$env:SystemDrive\swsetup\", # $swsetup
            "$env:SystemDrive\Windows\SoftwareDistribution\Download\", #$softwareDistribution
            'C:\windows\Temp',
            'C:\Temp',
            'C:\ProgramData\Microsoft\Windows\WER\ReportArchive',
            'C:\ProgramData\Microsoft\Windows\WER\ReportQueue',
            'C:\ServiceProfiles\LocalService\AppData\Local\Temp'
        ),
        $maxConfirmations=3,
        $startComponentCleanup=$false
    ) 

    function purgeDirectory($paths='c:\temp',$maxConfirmations=3){     
        $unsafeLocations=@(
            'C:\',
            'C:\Program Files',
            'C:\Program Files (x86)',
            'C:\ProgramData',
            'C:\ProgramData\Microsoft',
            'C:\ProgramData\Microsoft\Windows',
            'C:\Users',
            'C:\Users\Public',
            'C:\Users\Default',
            'C:\Windows\addins',
            'C:\Windows\ADFS',
            'C:\Windows\appcompat',
            'C:\Windows\apppatch',
            'C:\Windows\AppReadiness',
            'C:\Windows\assembly',
            'C:\Windows\AutoKMS',
            'C:\Windows\bcastdvr',
            'C:\Windows\Boot',
            'C:\Windows\Branding',
            'C:\Windows\CbsTemp',
            'C:\Windows\Cluster',
            'C:\Windows\Containers',
            'C:\Windows\CSC',
            'C:\Windows\Cursors',
            'C:\Windows\debug',
            'C:\Windows\diagnostics',
            'C:\Windows\DiagTrack',
            'C:\Windows\DigitalLocker',
            'C:\Windows\Downloaded Program Files',
            'C:\Windows\en-US',
            'C:\Windows\Fonts',
            'C:\Windows\GameBarPresenceWriter',
            'C:\Windows\Globalization',
            'C:\Windows\Help',
            'C:\Windows\IdentityCRL',
            'C:\Windows\IME',
            'C:\Windows\ImmersiveControlPanel',
            'C:\Windows\INF',
            'C:\Windows\InputMethod',
            'C:\Windows\L2Schemas',
            'C:\Windows\LiveKernelReports',
            'C:\Windows\Logs',
            'C:\Windows\Media',
            'C:\Windows\Microsoft.NET',
            'C:\Windows\Migration',
            'C:\Windows\Minidump',
            'C:\Windows\ModemLogs',
            'C:\Windows\OCR',
            'C:\Windows\Offline Web Pages',
            'C:\Windows\Panther',
            'C:\Windows\Performance',
            'C:\Windows\PLA',
            'C:\Windows\PolicyDefinitions',
            'C:\Windows\PrintDialog',
            'C:\Windows\Provisioning',
            'C:\Windows\Registration',
            'C:\Windows\RemotePackages',
            'C:\Windows\rescache',
            'C:\Windows\Resources',
            'C:\Windows\SchCache',
            'C:\Windows\schemas',
            'C:\Windows\security',
            'C:\Windows\ServiceProfiles',
            'C:\Windows\ServiceState',
            'C:\Windows\servicing',
            'C:\Windows\Setup',
            'C:\Windows\ShellComponents',
            'C:\Windows\ShellExperiences',
            'C:\Windows\SHELLNEW',
            'C:\Windows\SKB',
            'C:\Windows\Speech',
            'C:\Windows\Speech_OneCore',
            'C:\Windows\System',
            'C:\Windows\System32',
            'C:\Windows\SystemApps',
            'C:\Windows\SystemResources',
            'C:\Windows\SystemTemp',
            'C:\Windows\SysWOW64',
            'C:\Windows\TAPI',
            'C:\Windows\Tasks',
            'C:\Windows\TextInput',
            'C:\Windows\tracing',
            'C:\Windows\twain_32',
            'C:\Windows\Vss',
            'C:\Windows\WaaS',
            'C:\Windows\Web',
            'C:\Windows\WinSxS'
        )
        
        $results=[hashtable]@{}
        $emptyDirectory="C:\emptyDirectory"
        foreach($path in $paths){
            if($path -in $unsafeLocations){
                write-warning "'$path' is an unsafe location to purge! This iteration shall be skipped"
                $results+=[hashtable]@{$path='Not purged'}
                #continue
            }else{
                # write-host "Granting Administrators full access to $path..."
                # try{
                #     # $null=takeown /F $path /A /R /D Y
                #     $null=icacls $path /Grant Administrators:F /inheritance:e /T
                # }catch{
                #     write-warning $_
                #     continue
                # }                
                $null=md -Force $emptyDirectory
                Remove-Item "$emptyDirectory`\*" -force -recurse -ErrorAction Continue
                if(--$maxConfirmations -ge 0){
                    $confirmed=confirmation "This will delete all files and folders from $path"
                    if ($confirmed){
                        write-host "Now purging ALL files and folders inside $path..." -foregroundcolor yellow
                        $null=robocopy $emptyDirectory $path /mir /R:0 /W:0 /NP
                        $results+=[hashtable]@{$path='Purged'}
                        # rmdir -Force $path
                    }else{
                        write-host "Purging has been cancelled for $path and any/all subsequent paths"
                        $results+=[hashtable]@{$path='NOT purged'}
                        break
                    }
                }else{
                    write-host "Now purging ALL files and folders inside $path..." -foregroundcolor yellow
                    $null=robocopy $emptyDirectory $path /mir /R:0 /W:0 /NP
                    $results+=[hashtable]@{$path='Purged'}
                }
            }            
        }
        return $results
    }
    
    function getUserProfiles{
        $userTempDirectories=@()
        $profiles=(get-childitem 'C:\users' -Directory -EA Ignore).Name
        if($profiles){
            Foreach($profile in $profiles){            
                $tempPath = "C:\Users\$profile\AppData\Local\Temp"
                $downloadPath = "C:\Users\$profile\Downloads"
                $userTempDirectories+=$tempPath
                $userTempDirectories+=$downloadPath
                # purgeDirectory $tempPath
                # Write-host "$tempPath cleared."
                # purgeDirectory $downloadPath
                # Write-host "$downloadPath cleared."
            }
        }else{
            write-warning "There are no user profiles detected."
        }
        return $userTempDirectories
    }
    
    function confirmation($content,$testValue="I confirm",$maxAttempts=3){
        $confirmed=$false;
        $attempts=0;        
        $content|write-host
        write-host "Please review this content for accuracy.`r`n"
        while ($attempts -le $maxAttempts){
            if($attempts++ -ge $maxAttempts){
                write-host "A maximum number of attempts have reached. No confirmations received!`r`n"
                break;
                }
            $userInput = Read-Host -Prompt "Please type in this value => $testValue <= to confirm. Input CANCEL to skip this item.";
            if ($userInput.ToLower() -eq $testValue.ToLower()){
                $confirmed=$true;
                write-host "Confirmed!`r`n";
                break;                
            }elseif($userInput -like 'cancel'){
                write-host 'Cancel command received.'
                $confirmed=$false
                break
            }else{
                cls;
                $content|write-host
                write-host "Attempt number $attempts of $maxAttempts`: $userInput does not match $testValue. Try again or Input CANCEL to skip this item`r`n"
                }
            }
        return $confirmed;
    }

    $directoriesToPurge+=$(getUserProfiles)
    purgeDirectory $directoriesToPurge $maxConfirmations    
    if($startComponentCleanup){
        write-host "Now clearing all superseded versions of every component in the component store...."
        write-host "This also removes any backup components needed for uninstallation of service packs already installed"
        Dism.exe /online /Cleanup-Image /startcomponentcleanup /resetbase
    }
}

cleanWindows $directoriesToPurge $maxConfirmations $startComponentCleanup
# cleanCVolume.ps1
# Version 0.01

## Variables Declaration ####
$directoriesToPurge=@(
    "$env:SystemDrive\Windows\Prefetch\", # $prefetchData
    "$env:SystemDrive\Windows\Logs\CBS\", # $cbs
    "$env:SystemDrive\swtools\", # $swtools
    "$env:SystemDrive\drivers\", # $drivers
    "$env:SystemDrive\swsetup\", # $swsetup
    "$env:SystemDrive\Windows\SoftwareDistribution\Download\", #$softwareDistribution
    'C:\windows\Temp',
    'C:\Temp',
    'C:\ProgramData\Microsoft\Windows\WER\ReportArchive',
    'C:\ProgramData\Microsoft\Windows\WER\ReportQueue',
    'C:\ServiceProfiles\LocalService\AppData\Local\Temp'
)


function purgeDirectory($path='c:\temp'){     
    function confirmation($content,$testValue="I confirm",$maxAttempts=3){
            $confirmed=$false;
            $attempts=0;        
            $content|write-host
            write-host "Please review this content for accuracy.`r`n"
            while ($attempts -le $maxAttempts){
                if($attempts++ -ge $maxAttempts){
                    write-host "A maximum number of attempts have reached. No confirmations received!`r`n"
                    break;
                    }
                $userInput = Read-Host -Prompt "Please type in this value => $testValue <= to confirm. Input CANCEL to skip this item.";
                if ($userInput.ToLower() -eq $testValue.ToLower()){
                    $confirmed=$true;
                    write-host "Confirmed!`r`n";
                    break;                
                }elseif($userInput -like 'cancel'){
                    write-host 'Cancel command received.'
                    $confirmed=$false
                    break
                }else{
                    cls;
                    $content|write-host
                    write-host "Attempt number $attempts of $maxAttempts`: $userInput does not match $testValue. Try again or Input CANCEL to skip this item`r`n"
                    }
                }
            return $confirmed;
        }
    write-host "Granting Administrators full access to $path..."
    try{
        # $null=takeown /F $path /A /R /D Y
        $null=icacls $path /Grant Administrators:F /inheritance:e /T
    }catch{
        write-warning $_
        continue
    }
    $emptyDirectory="C:\emptyDirectory"
    md -Force $emptyDirectory
    Remove-Item "$emptyDirectory`\*" -force -recurse -ErrorAction Continue
    $confirmed=confirmation "This will delete all files and folders from $path"
    if ($confirmed){
        write-host "Now purging ALL files and folders inside $path..." -foregroundcolor yellow
        $null=robocopy $emptyDirectory $path /mir /R:0 /W:0 /NP
        # rmdir -Force $path
    }else{
        write-host "Purging has been cancelled for $path"
        }
}
function cleanUserProfiles{
    $profiles=(get-childitem c:\users -Directory -EA Ignore).Name
    if($profiles){
        Foreach($profile in $profiles){            
            $tempPath = "C:\Users\$profile\AppData\Local\Temp"
            $downloadPath = "C:\Users\$profile\Downloads"
            purgeDirectory $tempPath
            Write-host "$tempPath cleared."
            purgeDirectory $downloadPath
            Write-host "$downloadPath cleared."
        }
    }
}

function cleanWindows{
    foreach($directoryToPurge in $directoriesToPurge){
        purgeDirectory $directoryToPurge
    }
    write-host "Clearing all superseded versions of every component in the component store...."
    # This also removes any backup components needed for uninstallation of service packs already installed
    Dism.exe /online /Cleanup-Image /startcomponentcleanup /resetbase
}

cleanUserProfiles
cleanWindows

PowerShell: Change Google Chrome Cache Directory

Lately, I’ve been running a buck load of chrome tabs that utilize all available I/O disk speed that my default C:\ volume could keep up. Thus, Chrome often lags to the point of being annoying or unusable. Here are two methods to move the Default Chrome directory toward a different volume to improve Chrome’s performance:

# Method 1: Move the whole Chrome Default profile's storage directory
$profilePath="C:\Users\$env:username\AppData\Local\Google\Chrome\User Data\Default"
$newPath='D:\chrome\Default'

robocopy "$profilePath" "$newPath" /mir /xj /copyall
ren $profilePath "$profilePath`_old"
& mklink $profilePath $newPath /j
cmd /c mklink /d $profilePath $newPath

# Method 2: Move only Chrome Cache
$newCacheDirectory="D:\chromeCache\$env:username"
function changeChromeCacheDirectory{
    param(
      $newCacheDirectory="D:\chromeCache\$env:username",
      $defaultCacheDirectory="C:\Users\$env:username\AppData\Local\Google\Chrome\User Data\Default\Cache"
    )
    try{
        Stop-Process -Name chrome
        mkdir $newCacheDirectory
        rmdir $defaultCacheDirectory -force -Recurse
        New-Item -ItemType Junction -Path $defaultCacheDirectory -Target $newCacheDirectory
        write-host "Chrome cache directory has been changed`r`nFrom: $defaultCacheDirectory`r`nTo: $newCacheDirectory" -ForegroundColor Green
        return $true
    }catch{
        write-warning $_
        return $false
    }
}

changeChromeCacheDirectory $newCacheDirectory

The Process of Adding a New Hyper-V Server Into a Cluster and the Associated Virtual Machine Manager (VMM)

These are the steps:

  1. Install Windows
    Windows 2019 Data Center Edition is the standard as of May 20, 2022
    Certain hardware may require slipstreamed ISO’s to load RAID drivers so that the installation wizard would recognize the underlying hardware
    Install Windows with GUI. Although, headless Windows would still work, our Admins prefer to have GUI, boo
  2. Setup Network connectivity
    Obtain a static IP for the server
    Assign an IP for new server using
    Setup Interface with static IP, Gateway, and DNS
  3. Ensure that machine is accessible to VMM on these ports
    Ports required by SCVMM:
    TCP/22
    TCP/80
    TCP/135
    TCP/139
    TCP/445
    TCP/443
    TCP/623
    TCP/1433
    TCP/5985
    TCP/5986
    TCP/8530
    TCP/8531
    TCP/8100
    TCP/8101
    TCP/8102
    TCP/49152 to TCP/65535
    Ports required by Prometheus Windows Exporter:
    TCP/9182
  4. Enable RDP
    This feature would automatically be installed when a machine joins the domain. However, we’re ensuring that the hardware and OS would not go into a recovery loop due to driver issues. Hence, we would be patching Windows and attempting enable its Hyper-V features as a precursor to joining the machine to to domain, only if the machine is stable after these functions are added.
  5. Disable Startup repair
    Run this command: bcdedit /set {current} recoveryenabled no
  6. Update Windows
    There’s a PowerShell method to updating Windows directly from Microsoft
    Alternative, the equivalent GUI method would suffice
  7. Install Hyper-V Features: Install-WindowsFeature -Name Hyper-V -IncludeManagementTools -Restart
    Be advised that Windows will reboot after this command
    If there were any driver conflicts, Windows would go into a recovery loop. Hopefully, that doesn’t happen to the machine you’re preparing
  8. Join Domain
  9. Run these PowerShell Command as Administrator
    # Install Chocolatey
    if (!(Get-Command choco.exe -ErrorAction SilentlyContinue)) {
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
    Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))}

    # Defining $ENV:ChocotaleyInstall so that it would be called by refreshenv
    $ENV:ChocolateyInstall = Convert-Path "$((Get-Command choco).Path)\..\.."
    Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1"
    Update-SessionEnvironment
    # Add Private Repo
    $privateRepoName='kimconnectrepo'
    $privateRepoUrl='https://choco.kimconnect.com/chocolatey'
    $priority=1
    $privateRepo="'$privateRepoName' -s '$privateRepoUrl' --priority=$priority"
    choco source add -n=$privateRepo

  10. Uninstall Windows Defender
    Run this command: Remove-WindowsFeature Windows-Defender
  11. Install Enterprise Antivirus
  12. Install Prometheus Windows Exporter
    Use Chocolatey to install Prometheus Windows_Exporter
    choco install prometheus-windows-exporter.install -y --ignore-checksums
    $null=& sc.exe failure $serviceName reset= 30 actions= restart/100000/restart/100000/""/300000
    Set-Service -Name $serviceName -StartupType 'Automatic'
    Enabling Collectors for a Hyper-V Server
    $serviceName='Windows_Exporter'
    $enabledCollectors='os,cpu,cs,logical_disk,net,tcp,hyperv,service,textfile'
    $switches=" --log.format logger:eventlog?name=$serviceName --collectors.enabled $enabledCollectors"
    function modifyService{
    param(
    $serviceName,
    $switches
    )
    $wmiService=Get-WmiObject win32_service| ?{$_.Name -like "*$serviceName*"}
    $exePath=[regex]::match($wmiService.PathName,'\"(.*)\"').groups[1]
    $binarySwitches='\"' + $exePath + '\"' + $switches
    sc.exe config $serviceName binpath= $binarySwitches
    sc.exe qc windows_exporter
    restart-service $serviceName
    }
    # Local execution
    modifyService $serviceName $switches
  13. Install Failover Clustering Features
    # After restart, install Failover clustering features
    Install-WindowsFeature Failover-Clustering -IncludeManagementTools; Install-WindowsFeature RSAT-Clustering-MGMT,RSAT-Clustering-PowerShell,Multipath-IO
  14. Setup NIC Teaming (if appropriate)
    # Check Teaming setup
    Get-NetLbfoTeam

    # Initialize Teaming, if required
    $teamName='Team1'
    $vlanId=100
    Add-NetLbfoTeamNIC -Team $teamName -VlanID $vlanId

    # Set Teaming mode, if required
    $teamName='Team1'
    $teamMode='LACP'
    $lbAlgorithm='Dynamic'
    Set-NetLbfoTeam -Name $teamName -TeamingMode $teamMode -LoadBalancingAlgorithm $lbAlgorithm

    # Add Team members, if required
    $teamName='Team1'
    $nicTeamMembers='NIC1','NIC2'
    $nicTeamMembers|%{Add-NetLbfoTeamMember -Name $_ -Team $teamName}

    # Check Teaming setup, if required
    Get-NetLbfoTeam

    # Check NIC names
    Get-NetAdapter

  15. Enable WinRM: Enable-PSRemoting -Force
  16. Enable CredSSP: Enable-WSManCredSSP -Role Server -Force
  17. Create New Virtual Switch(es)
    $switchName='External-Connection' # change this label to reflect the actual vSwitch name to be added
    $adapterName='TEAM1' # change this value to reflect the correct interface
    $vlanId=101 # change this to the correct VLAN
    function addVirtualSwitch($switchName,$adapterName,$vlanId){
    New-VMSwitch -name $switchName -NetAdapterName $adapterName -AllowManagementOS $true
    if($vlanId){
    Get-VMNetworkAdapter -SwitchName $switchName -ManagementOS|Set-VMNetworkAdapterVlan -Access -VlanId $vlanId
    }
    }
    addVirtualSwitch $switchName $adapterName $vlanId
  18. Include appropriate Admins
    # Example for HQ
    $admins=@(
    "$env:USERDOMAIN\$($env:computername)$",
    "$env:USERDOMAIN\service-hv",
    "$env:USERDOMAIN\service-vmm"
    )
    Add-localgroupmember -group Administrators -member $admins
  19. Join Node to Cluster
    # Example
    $clusterName='hyperv-cluster'
    Add-ClusterNode -Name $env:computername -Cluster $clusterName
  20. Install VMM Agent
    The easy method:
    # Install VMM Agent
    $vmmServer='hq-vmm01' # change this value to reflect the correct VMM node
    $version='10.19.2591.0' # change to value to reflect the latest expected version
    $agentMsiFile="\\$vmmServer\C$\Program Files\Microsoft System Center\Virtual Machine Manager\agents\amd64\$version\vmmAgent.msi"

    # Installing VMM Agent using its MSI File
    $file=gi $agentMsiFile
    $DataStamp = get-date -Format yyyyMMddTHHmmss
    $logFile ="C:\" + '{0}-{1}.log' -f $file.name,$DataStamp
    $MSIArguments = @(
    "/i"
    ('"{0}"' -f $file.fullname)
    "/qn"
    "/norestart"
    "/L*v"
    $logFile
    )
    Start-Process "msiexec.exe" -ArgumentList $MSIArguments -Wait -NoNewWindow
  21. Add This New Hyper-V Node to VMM
    Access VMM as Administrator > Navigate to Fabric > select the appropriate cluster > right-click the newly introduced node > Associate with Cluster > Done

PowerShell: Check a List of Windows Computers for Online & Offline Statuses

# checkOnlineComputers.ps1

# Set a list of computers in a text file
$computerListFile='C:\Temp\computerlist.txt'

# Read the computer list and run SMB tests against each one
$content=get-content $computerListFile
$results=[hashtable]@{}
foreach ($line in $content){
  $smbReach=test-path "\\$($line.trim())\C$\Windows"
  write-host "$line`: $smbReach"
  #pause
  $results+=@{$line=$smbReach}
  }

# Display the results
$results.GetEnumerator() | ?{!$_.value} | %{
    $message = '{0}: {1}' -f $_.key, $_.value
    Write-Output $message
}  

PowerShell: Add Local User as an Administrator on All Servers in Domain


# addLocalAccountOnAllServers.ps1
# Feature: using only legacy commands for maximum compatibility

# Set variables
$newUsername='backupAdmin'
$newUserPass='VERYCOMPLEXPASSWORD'
$newUserFullName="Local System Admin"
$newUserDesc="Standardized local admin user"
$newUserGroup="Administrators"

function addLocalAccount{
    param(
        $servers=$env:computername,
        $newUsername='backupAdmin',
        $newUserPass='COMPLEXPASSWORDHERE',
        $newUserFullName="Systems Admin",
        $newUserDesc="Standardized local admin user",
        $newUserGroup="Administrators"
    )
    $results=@()
    $psSessionOptions=New-PSSessionOption -SkipCNCheck -OpenTimeOut 60
    foreach ($server in $servers){
        $pssession=new-pssession $server -SessionOption $psSessionOptions -EA Ignore
        $progress=if($pssession.State -eq 'Opened'){
                Invoke-command -session $pssession -ScriptBlock {
                    param($newUsername,$newUserPass,$newUserFullName,$newUserDesc,$newUserGroup)                            
                    # Check whether username exists and proceed accordingly
                    $usernameExists=$(net user $newUsername)[0] -match $newUsername
                    try{
                        if(!$usernameExists){
                            # Using legacy commands for maximum compatibility
                            $null=NET USER $newUsername $newUserPass /fullname:"$newUserFullName" /comment:"$newUserDesc" /Active:Yes /ADD /Y
                            write-host "$newUserName has been created on $env:computername successfully"
                        }else{
                            # if user exists, ensure that its password is matching the intended value
                            $null=invoke-expression "net user $newUsername $newUserPass" 2>&1
                            write-host "$newUserName exists on $env:computername and its password has been reset"
                        }
                        $isMembershipValid=$(net localgroup $newUserGroup) -match $newUsername
                        if(!$isMembershipValid){
                            $null=invoke-expression "NET LOCALGROUP $newUserGroup $newUsername /ADD /Y" 2>&1
                            write-host "$newUserName has been added to group $newUserGroup on $env:computername successfully"
                        }else{
                            write-host "$newUserName is already a member of group $newUserGroup on $env:computername"
                        }
                        $null=Net user $newUsername /active:yes
                    }catch{
                        write-warning $_
                        return $false
                    }                    
                    # Validation
                    $userEnabled=$(net user $newUsername)[5] -match 'Yes'
                    return $userEnabled
                  
                    # These lines only work in PowerShell 5.1+; hence, they are skipped
                    # New-LocalUser $newUsername -Password $newUserPass -FullName $newUserFullName -Description $newUserDesc
                    # Add-LocalGroupMember -Group $newUserGroup -Member $newUsername
                } -Args $newUsername,$newUserPass,$newUserFullName,$newUserDesc,$newUserGroup
                remove-pssession $pssession
            }else{
                write-warning "$env:computername is unable to connect to $server via WinRM"
                $null
            }
        $result=[pscustomobject]@{
            'computername'=$server
            'localUserExists'=$progress
            }
        write-host $result
        $results+=$result
    }
    return $results
}

# Get all servers, excluding domain controllers
$memberServers=Get-ADComputer -Filter 'operatingsystem -like "*server*" -and enabled -eq "true" -and primarygroupid -ne "516"' -Properties Name,Operatingsystem,OperatingSystemVersion,IPv4Address | Sort-Object -Property Operatingsystem | Select-Object -Property Name,Operatingsystem,OperatingSystemVersion,IPv4Address
$servers=$memberServers.Name

$results=addLocalAccount $servers $newUsername $newUserPass $newUserFullName $newUserDesc $newUserGroup

write-host $results

How To Modify a Windows Service

Here’s the freebie code:

# Version 0.02

$serviceName='Windows_Exporter'
$newExePath=$false
$newSwitches=" --log.format logger:eventlog?name=$serviceName --collectors.enabled os,cpu,cs,logical_disk,net,tcp,service,textfile"

function modifyService{
    param(
        $serviceName='windows_exporter',
        $newExePath=$false,
        $newSwitches=$false
    )
    $wmiService=Get-WmiObject win32_service| ?{$_.Name -like "*$serviceName*"}
    $pathToExecute=$wmiService.PathName
    $originalExePath=[regex]::match($pathToExecute,'\"(.*)\"').groups[1].value
    write-host "Original Path To Execute: $pathToExecute"
    $charIndexOfSwitches=try{$pathToExecute.indexof(' -')}catch{}
    $originalSwitches=if($charIndexOfSwitches -ne -1){
        $pathToExecute.Substring($pathToExecute.indexof(' -'))
        }else{$null}
    $switches=if($newSwitches){$newSwitches}else{$originalSwitches}
    $exePath=if((test-path $newExePath) -and $newExePath -ne $originalExePath){$newExePath}else{$originalExePath}
    $binarySwitches='\"' + $exePath + '\"' + $switches
    write-host "New path to execute: $binarySwitches"
    $output=sc.exe config $serviceName binpath= $binarySwitches
    write-host $output
    $output=sc.exe qc $serviceName
    $output|write-host
    restart-service $serviceName
}

modifyService $serviceName $newExePath $newSwitches

This is an example of how to modify a service named Windows_Exporter:

# Enabling Collectors for a Hyper-V Server
$serviceName='Windows_Exporter'
$enabledCollectors='os,cpu,cs,logical_disk,net,tcp,hyperv,service,textfile'
$switches=" --log.format logger:eventlog?name=$serviceName --collectors.enabled $enabledCollectors"
$resets=30
$restartWaitMs=100000
$maxWaitSeconds=120

function modifyService{
	param(
		$serviceName,
		$switches,
		$resets=30,
		$restartWaitMs=100000,
		$maxWaitSeconds=120
	)	
	$wmiService=Get-WmiObject win32_service| ?{$_.Name -like "*$serviceName*"}
	$exePath=[regex]::match($wmiService.PathName,'\"(.*)\"').groups[1]
	$binarySwitches='\"' + $exePath + '\"' + $switches
	sc.exe config $serviceName binpath= $binarySwitches
	# Set auto start and restart upon failures
	$null=& sc.exe failure $serviceName reset= $resets actions= restart/$restartWaitMs/restart/$restartWaitMs/""/$($restartWaitMs*3)
	Set-Service -Name $serviceName -StartupType 'Automatic'
	sc.exe qc windows_exporter
	restart-service $serviceName
}

# Local execution
modifyService $serviceName $switches
# Remote execution
$computerNames=@(
	'HyperV001',
	'HyperV002',
	'HyperV003',
	'HyperV004'
)
foreach($computer in $computernames){
	write-host "Executing function on $computer..."
	invoke-command -computername $computer -scriptblock {
			param($modifyService,$servicename,$switches)
			[ScriptBlock]::Create($modifyService).Invoke($servicename,$switches)
		} -Args ${function:modifyService},$servicename,$switches
}

How To Modify Collectors of Windows Exporter

Update: here’s a generalized version of this function that can be applied to other services

The follow cmdlets assumes that Prometheus Windows Exporter has already been installed. This is how to modify an existing instance:

# Example: Enabling Collectors for a Hyper-V Server
$serviceName='Windows_Exporter'
$enabledCollectors='os,cpu,cs,logical_disk,net,tcp,hyperv,service,textfile'
$wmiService=Get-WmiObject win32_service| ?{$_.Name -like "*$serviceName*"}
$exePath=[regex]::match($wmiService.PathName,'\"(.*)\"').groups[1]
$binaryPath = '\"' + $exePath + '\"' + " --log.format logger:eventlog?name=windows_exporter --collectors.enabled $enabledCollectors"
sc.exe config $serviceName binpath= $binaryPath
sc.exe qc windows_exporter
restart-service $serviceName

How To Create a Windows Scheduled Task to Call a Program or Script

Example on How To Call a Program:
  1. Set Action = Start a Program
  2. Set Program/Script = C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe (must specify the full path of PowerShell in Windows)
  3. Set Arguments = -Command "& 'C:\Windows\system32\ping.exe' -n 1 google.com"
  4. Set Start-in = C:\Windows\system32 (or where ever the executable resides)
  5. If program would require Administrator context, don’t forget to set task to run with elevated permissions

Example on How To Call a PowerShell Script:
  1. Set Action = Start a Program
  2. Set Program/Script = C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe (must specify the full path of PowerShell in Windows)
  3. Set Arguments = -Command "& 'C:\Scripts\deleteLogsOlderThanXDays.ps1'"
  4. Set Start-in = C:\Scripts (or where ever the script resides)
  5. If program would require Administrator context, don’t forget to set task to run with elevated permissions

PowerShell: Delete Files Older Than 30 Days

Have you ever run into C:\ volumes reaching critical thresholds because certain applications or users filling up system drive, causing Windoze to run out of disk space and perform sluggishly? Well, you’re in luck:

# deleteLogsOlderThanXDays.ps1
# Version 0.0.1

# User defined variables
$logDirectories='C:\Temp'
$deleteLogsOlderThanDays=30
$testOnly=$false
$logFile='C:\purgedFiles.txt'
$renameLogGreaterThanMb=5

# Execute deletion
# Safety check
$hardStopLocations=@(
	'C:\Windows',
	'C:\ProgramData',
	'C:\Users',
	'C:\Program Files (x86)',
	'C:\Program Files'
	)
$ErrorActionPreference='ignore'
$today=Get-Date
$deletionMarker=$today.AddDays(-$deleteLogsOlderThanDays)
$deletedFiles="`r`nJob started $($today.DateTime.ToString())`r`n--------------------------------------------`r`n"
foreach($directory in $logDirectories){
	$isSafe=.{
		$parent=if(test-path $directory){
				try{
					(get-item $directory -EA Ignore).parent.FullName
				}catch{
                    $null
                }
			}else{
				write-warning "$directory cannot be parsed."
				return $null
			}
		if($parent -eq $null){
				write-warning "$directory is invalid"
				return $false
		}elseif($parent -in $hardStopLocations){
			write-warning "$directory is not a safe location to manipulate files."
			return $false
		}else{
			return $true
		}
	}
	if($isSafe){
		if($testOnly){
			Get-ChildItem $directory -Recurse -EA Ignore|?{ $_.LastWriteTime -lt $deletionMarker }|%{write-host "Marked for deletion: $_"}
		}else{			
			$itemsToDelete=Get-ChildItem $directory -Recurse -EA Ignore|?{$_.LastWriteTime -lt $deletionMarker}
			if($itemsToDelete){
				foreach ($item in $itemsToDelete){
					try{
						Remove-Item $item.fullname -ea stop
						write-host "Deleted: $item"
						$deletedFiles+="Deleted: $($item.Fullname) with lastWriteTime of $($item.LastWriteTime.toString())`r`n"
					}catch{
						write-warning $_
					}
				}		
			}else{
                $comment="There are no item(s) to delete in $directory`r`n"
				write-host $comment
                $deletedFiles+=$comment
			}
		}
	}
}
$deletedFiles+="--------------------------------------------`r`nJob ended $((get-date).DateTime.ToString())"
if(test-path $logFile){
    $logFileInfo=get-item $logFile
    if($logFileInfo.length/1MB -gt $renameLogGreaterThanMb){
        $timeZoneName=[System.TimeZoneInfo]::Local.StandardName
        $timeStampFileName=[System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId([datetime]::UtcNow,$timeZoneName).ToString('yyyy-MM-dd_HH-mm-ss')+'_'+[regex]::replace($timeZoneName,'([A-Z])\w+\s*', '$1')
        $abbreviatedZoneName=if($timeZoneName -match ' '){[regex]::replace($timeZoneName,'([A-Z])\w+\s*', '$1')}else{$timeZoneName}
        $timeStampFormat="yyyy-MM-dd_HH-mm-ss_$abbreviatedZoneName"
        $timeStampFileName=[System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId([datetime]::UtcNow,$timeZoneName).ToString($timeStampFormat)
        $newName=join-path $logFileInfo.DirectoryName $($logFileInfo.BaseName+"_$timeStampFileName"+$logFileInfo.Extension)
        rename-item $logFile $newName
    }
}
Add-Content $logFile $deletedFiles
write-host "Log has been written to $logFile"