How To Recover SQLSERVER Service from Being Unable to Start

# startSqlService.ps1

# special provision to deal with SQL Server not starting up due to various reasons
$sqlServerService='mssqlserver'
$sqlWaitSeconds=120
$desiredState='Running'
$sqlWaitTimespan=[timespan]::fromseconds($sqlWaitSeconds)
if($sqlServerService -in $originalStoppedServices.Name){            	
    try{
    $sqlService=get-service $sqlServerService -EA Ignore
    if($sqlService.Status -ne $desiredState -and $null -ne $sqlService){
        $stopped=try{$sqlService.waitforstatus('Stopped',$sqlWaitTimespan);$true}catch{$false}
        if($stopped){
            $attempt=net start $sqlServerService /T902 2>&1
            $running=(get-service $sqlServerService).Status -eq 'Running'
            $running=if(!$running){try{$sqlService.waitforstatus('Running',$sqlWaitTimespan);$true}catch{$false}}else{$running}
            if($attempt -match 'started' -and $running){
                write-host "$sqlServerService has been started with trace flag /T902" -foregroundcolor Green
            }else{
                write-host "$sqlServerService has NOT been started with trace flag /T902" -foregroundcolor Red
            }
        }else{
            write-warning "$sqlServerService has current state is $((get-service $sqlServerService).Status)"
        }                 
    }                
    }catch{
    write-warning $_                               
    }            
}

How to Search for Installed Application by Name in PowerShell

One can search using the known Vendor or application Name as illustrated below:

[testwindows]: PS C:\Users\user1\Documents> Get-WmiObject -Class Win32_Product | ?{$_.Name -like '*exporter*'}


IdentifyingNumber : {EDD0CDE3-4519-4C1A-9FB4-C8C067615698}
Name              : windows_exporter
Vendor            : prometheus-community
Version           : 0.20.0
Caption           : windows_exporter

[btestwindows]: PS C:\Users\user1\Documents> Get-WmiObject -Class Win32_Product | ?{$_.Vendor -like '*prometheus*'}


IdentifyingNumber : {EDD0CDE3-4519-4C1A-9FB4-C8C067615698}
Name              : windows_exporter
Vendor            : prometheus-community
Version           : 0.20.0
Caption           : windows_exporter

PowerShell: Create Registry Keys within Windows Localhost

# createRegKey.ps1

$regKeys=@(
	@{
        hive='REGISTRY::HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Google\Chrome';
        name='ChromeCleanupEnabled';
        value=0
    }
	@{
        hive='REGISTRY::HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Google\Chrome';
        name='ChromeCleanupReportingEnabled';
        value=0
        }
)
 
function createRegKey{
    param(
        $regHive,
        $keyName,
        $value
    )
    try{
        $keyValue=(Get-ItemProperty -Path $regHive -Name $keyName -ErrorAction Stop).$keyName
        if($keyValue -ne $value){
            New-ItemProperty -Path $regHive -Name $keyName -Value $value -Type String -Force
        }else{
            write-host "$regHive $keyName already has a value of $value"
        }
    }catch [System.Management.Automation.ItemNotFoundException],[System.Management.Automation.PSArgumentException] {
        New-Item -Path $regHive -Force
        New-ItemProperty -Path $regHive -Name $keyName -Value $value -Force
        write-host "$regHive $keyName value has been set to $value"
    }catch {
        write-warning $_
        New-ItemProperty -Path $regHive -Name $keyName -Value $value -Type String -Force
        write-host "$regHive $keyName value has been set to $value"
    }
}

$regKeys|%{createRegKey $_.hive $_.name $_.value}

Windows: How to Map Network Drive for All Users of a Local Machine

# Mapping file share using legacy commands (more effective than newer PowerShell equivalents in certain scenarios)
$textContent=@'
net use m: /delete
net use m: \\FILESERVER\SHARENAME
'@
Set-Content "$env:ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp\mountShares.bat" $textContent

Use DISM To Install Windows Features

# dismInstallFeatures.ps1

$featuresToInstall = ('FileAndStorage-Services','Storage-Services','Web-Server','Web-WebServer','Web-Common-Http','Web-Default-Doc','Web-Dir-Browsing','Web-Http-Errors',
'Web-Static-Content','Web-Http-Redirect','Web-Health','Web-Http-Logging','Web-Log-Libraries','Web-ODBC-Logging','Web-Request-Monitor',
'Web-Http-Tracing','Web-Performance','Web-Stat-Compression','Web-Dyn-Compression','Web-Security','Web-Filtering','Web-Basic-Auth',
'Web-Client-Auth','Web-Digest-Auth','Web-Cert-Auth','Web-IP-Security','Web-Url-Auth','Web-Windows-Auth','Web-App-Dev','Web-Net-Ext',
'Web-Net-Ext45','Web-ASP','Web-Asp-Net','Web-Asp-Net45','Web-CGI','Web-ISAPI-Ext','Web-ISAPI-Filter','Web-Includes','Web-WebSockets',
'Web-Ftp-Server','Web-Ftp-Service','Web-Mgmt-Tools','Web-Mgmt-Console','Web-Mgmt-Compat','Web-Metabase','Web-Lgcy-Mgmt-Console','Web-Lgcy-Scripting',
'Web-WMI','Web-Scripting-Tools','Web-Mgmt-Service','NET-Framework-Features','NET-Framework-Core','NET-Framework-45-Features','NET-Framework-45-Core',
'NET-Framework-45-ASPNET','NET-WCF-Services45','NET-WCF-HTTP-Activation45','NET-WCF-TCP-PortSharing45','RSAT','RSAT-Feature-Tools','RSAT-SMTP',
'RSAT-SNMP','FS-SMB1','SMTP-Server','SNMP-Service','User-Interfaces-Infra','Server-Gui-Mgmt-Infra','Server-Gui-Shell','PowerShellRoot','PowerShell',
'PowerShell-V2','PowerShell-ISE','WAS','WAS-Process-Model','WAS-Config-APIs','WoW64-Support')

# Remove any feature that is not available to prevent dism failures. MSMQ-Container for example is only available 
# on Windows 7 but not on Windows Server 2008 R2. 
$availableFeatures = dism /online /Get-Features
$featuresToRemove = @()
$featuresToInstall | % { if (-not ($availableFeatures | Select-String ('{0}$' -f $_) -Quiet)) { $featuresToRemove += $_} }
$featuresToInstall = Compare-Object -ReferenceObject $featuresToInstall -DifferenceObject $featuresToRemove | select -ExpandProperty InputObject

$dismParameter = @('/online', '/Enable-Feature', ($featuresToInstall | % { '/FeatureName:{0}' -f $_ }), '/NoRestart', '/all')
$output = dism @dismParameter

# throw an error if dism wasn't successful
if ($global:LastExitCode -ne 0){
    throw 'Error while installing Windows Features. {0}' -f ($output | Select-String '\.log$')
}
# dismCheckWindowsFeature.ps1

$featuresToCheck = ('RDS-RD-Server')
$availableFeatures = dism /online /Get-Features
$featuresToRemove = @()
$featuresToInstall | % { if (-not ($availableFeatures | Select-String ('{0}$' -f $_) -Quiet)) { $featuresToRemove += $_} }
$featuresToInstall = Compare-Object -ReferenceObject $featuresToInstall -DifferenceObject $featuresToRemove | select -ExpandProperty InputObject

$dismParameter = @('/online', '/Get-Features', ($featuresToCheck | % { '/FeatureName:{0}' -f $_ }), '/NoRestart', '/all')

$output = dism @dismParameter

# throw an error if dism wasn't successful
if ($global:LastExitCode -ne 0)
{
    throw 'Error while installing Windows Features. {0}' -f ($output | Select-String '\.log$')
}

dism /online /get-Features 'RDS-RD-Server'

Upgrade Virtual Hardware Version in VMM

The following script will upgrade all guest virtual machines from a lower version to the desired version:

# upgradevHardwareVersion.ps1

# User inputs
$desiredVersion='9.0'
$excludeKeywords=@(
  'lax',
  'aws'
)
$vmmServer=$env:computername

function updateVmVersion($vmNames,$desiredVersion='9.0',$vmmServer=$env:computername){
    $results=@()
    foreach($vmName in $vmNames){
        $version=.{            
            try{
                $vm=get-scvirtualmachine $vmName -vmmServer $vmmServer
                $previousVersion=$vm.Version
                if($vm.VirtualMachineState -eq 'Running' -and [version]$previousVersion -lt [version]$desiredVersion){
                    $null=stop-scvirtualmachine -vm $vm
                    $null=Update-SCVMVersion -vm $vm
                    $vm=start-scvirtualmachine -vm $vm
                    if($vm.status -eq 'Running'){
                        write-host "$vmName has been updated from version $previousVersion to $($vm.Version) and is now back online"
                    }else{
                        write-host "$vmName has been updated from version $previousVersion to $($vm.Version) but it's now offline"
                        return $previousVersion
                    }
                }elseif($vm.VirtualMachineState -eq 'PowerOff' -and [version]$previousVersion -lt [version]$desiredVersion){
                    $null=Update-SCVMVersion -vm $vm
                }
                return $vm.Version
            }catch{
                write-warning $_
                return $previousVersion
            }
        }
        $results+=@{$vmName=$version}
    }
    return $results
}

# Get the report
$vmsWithLowerVersions=get-scvirtualmachine|?{[version]$_.version -lt [version]$desiredVersion}
write-host "Processing list:`r`n$($vmsWithLowerVersions|select name,version|out-string)"
$filteredList=$vmsWithLowerVersions|?{$name=$_.Name;$null -eq $($excludeKeywords|?{$name -like "*$_*"})}
write-host "Filtered list:`r`n$($filteredList|select name,version|out-string)"
$inversedList=$vmsWithLowerVersions|?{$name=$_.Name;$null -ne $($excludeKeywords|?{$name -like "*$_*"})}
write-host "Inverted list:`r`n$($inversedList|select name,version|out-string)"

# Upgrade all VMs with lower versions
updateVmVersion $vmsWithLowerVersions.Name $desiredVersion $vmmServer

Upgrade a selected list of VMs

$vmsToUpgrade=@"
eqwebmdadfs01
test
test1
"@
$vmmServer='lax-vmm01-kimconnect.com'
$desiredVersion='9.0'

function updateVmVersion($vmNames,$desiredVersion='9.0',$vmmServer=$env:computername){
    $results=@()
    foreach($vmName in $vmNames){
        $version=.{            
            try{
                $vm=get-scvirtualmachine $vmName -vmmServer $vmmServer
                $previousVersion=$vm.Version
                if($vm.VirtualMachineState -eq 'Running' -and [version]$previousVersion -lt [version]$desiredVersion){
                    $null=stop-scvirtualmachine -vm $vm
                    $null=Update-SCVMVersion -vm $vm
                    $vm=start-scvirtualmachine -vm $vm
                    if($vm.status -eq 'Running'){
                        write-host "$vmName has been updated from version $previousVersion to $($vm.Version) and is now back online"
                    }else{
                        write-host "$vmName has been updated from version $previousVersion to $($vm.Version) but it's now offline"
                        return $previousVersion
                    }
                }elseif($vm.VirtualMachineState -eq 'PowerOff' -and [version]$previousVersion -lt [version]$desiredVersion){
                    $null=Update-SCVMVersion -vm $vm
                }
                return $vm.Version
            }catch{
                write-warning $_
                return $previousVersion
            }
        }
        $results+=@{$vmName=$version}
    }
    return $results
}

$vmnames=@($vmsToUpgrade -split "`n")|%{$_.Trim()}
write-warning "Please confirm that we're ready to have these virtual machines rebooted:`r`n$vmnames"
pause
updateVmVersion $vmnames $desiredVersion $vmmServer

Simple version – upgrading only VMs that are offline

$vmsToUpgrade=@"
TEST
TEST1
"@
$vmmServer='lax-vmm01-kimconnect.com'

$vmnames=@($vmsToUpgrade -split "`n")|%{$_.Trim()}
foreach($vmName in $vmNames){
    $vm=get-scvirtualmachine $vmName -vmmServer $vmmServer
    if($vm.VirtualMachineState -eq 'PowerOff'){
        try{
            $oldVersion=$vm.Version
            $null=Update-SCVMVersion -vm $vm
            $newVersion=$vm.Version
            write-host "$vmName virtual hardward has been upgraded from version $oldVersion to $newVersion"
            pause
        }catch{
            write-warning $_
        }

    }
}

PowerShell: Get Available RAM Slots

# getRamSlotsAvailable.ps1

$computername=$env:computername

function getRamSlotsAvailable{
    param($computername=$env:computername)
    write-host "Computer name: $computerName"
    $slots = Get-WmiObject -Class "win32_PhysicalMemoryArray" -namespace "root\CIMV2" -computerName $computerName
    $ramModules = Get-WmiObject -Class "win32_PhysicalMemory" -namespace "root\CIMV2" -computerName $computerName
    $ramSum=0
    $ramModules.Capacity|%{$ramSum+=$_}
    $slotsSum=0
    $slots.MemoryDevices|%{$slotsSum+=$_}
    write-host "Total slots: $slotsSum"
    write-host "Total RAM: $([math]::round($ramSum/1GB,2)) GB"
    Foreach ($module In $ramModules) {
        write-host "Memory Installed: $($module.DeviceLocator)"
        write-host "Memory Size: $($module.Capacity/1GB) GB"
    }
    return $slotsSum
}

getRamSlotsAvailable $computername

# Sample output
# PS C:\Windows\system32> getRamSlotsAvailable $computername
# Computer name: TEST-SERVER
# Total slots: 24
# Total RAM: 256 GB
# Memory Installed: DIMM_A1
# Memory Size: 16 GB
# Memory Installed: DIMM_A2
# Memory Size: 16 GB
# Memory Installed: DIMM_A3
# Memory Size: 16 GB
# Memory Installed: DIMM_A4
# Memory Size: 16 GB
# Memory Installed: DIMM_A5
# Memory Size: 16 GB
# Memory Installed: DIMM_A6
# Memory Size: 16 GB
# Memory Installed: DIMM_A7
# Memory Size: 16 GB
# Memory Installed: DIMM_A8
# Memory Size: 16 GB
# Memory Installed: DIMM_B1
# Memory Size: 16 GB
# Memory Installed: DIMM_B2
# Memory Size: 16 GB
# Memory Installed: DIMM_B3
# Memory Size: 16 GB
# Memory Installed: DIMM_B4
# Memory Size: 16 GB
# Memory Installed: DIMM_B5
# Memory Size: 16 GB
# Memory Installed: DIMM_B6
# Memory Size: 16 GB
# Memory Installed: DIMM_B7
# Memory Size: 16 GB
# Memory Installed: DIMM_B8
# Memory Size: 16 GB

Use PowerShell to Get GeoLocation of a Computer’s Public IP

Method 1:

Invoke-RestMethod -Uri ('http://ipinfo.io/'+(Invoke-WebRequest -uri "http://ifconfig.me/ip").Content)

Method 2:

function getIPGeolocation($ipAddress) {

$request = Invoke-RestMethod -Method Get -Uri "http://ip-api.com/json/$ipAddress"

[PSCustomObject]@{
IP = $request.query
City = $request.city
Country = $request.country
Isp = $request.isp
}
}

$thisPublicIP=(Invoke-WebRequest -uri "http://ifconfig.me/ip").Content
getIPGeolocation $thisPublicIP

How To Add or Remove a Path in Windows Environmental Paths

There are 2 functions to Add and Remove, at your convenience:

# addEnvironmentalPath.ps1

$pathToAdd='C:\Scripts'
$pathToRemove='C:\Scripts'

function addEnvironmentalPath($pathToAdd){
    $registryEnvironment='Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment'
    $pathToAdd=if($pathToAdd -match '\\$'){$pathToAdd -replace '.$'}else{$pathToAdd}
    try{
        $originalPaths=(Get-ItemProperty -Path $registryEnvironment -Name PATH).path
        $pathsArray=$originalPaths -split ';'|?{$_.trim() -ne ''}|%{if($_ -match '\\$'){$_ -replace '.$'}else{$_}}|Sort-Object -Unique
        if($pathToAdd -in $pathsArray){
            write-host "$pathToAdd is already included in the environmental paths: '$originalPaths'"
            return $true
        }else{
            $newPathsArray=$pathsArray+$pathToAdd
            $newPaths=$newPathsArray -join ';'
            Set-ItemProperty -Path $registryEnvironment -Name PATH -Value $newPaths
            $newRegistryEnvironment=(Get-ItemProperty -Path $registryEnvironment -Name PATH).Path
            write-host "Environmental paths have been changed:`r`nFrom: $originalPaths`r`nTo: $newPaths"
            return $true
        }
    }catch{
        write-warning $_
        return $false
    }
}

function removeEnvironmentalPath($pathToRemove){
    $registryEnvironment='Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment'
    $pathToRemove=if($pathToRemove -match '\\$'){$pathToRemove -replace '.$'}else{$pathToRemove}
    try{        
        $originalPaths=(Get-ItemProperty -Path $registryEnvironment -Name PATH).path
        $pathsArray=$originalPaths -split ';'|?{$_.trim() -ne ''}|%{if($_ -match '\\$'){$_ -replace '.$'}else{$_}}|Sort-Object -Unique
        if($pathToRemove -notin $pathsArray){
            write-host "$pathToRemove does not exist in the environmental paths: '$originalPaths'"
            return $true
        }else{
            $itemIndex=[array]::indexof($pathsArray,$pathToRemove)
            [System.Collections.ArrayList]$newPathsArrayList=$pathsArray
            $newPathsArrayList.RemoveAt($itemIndex)
            $newPaths=$newPathsArrayList -join ';'
            Set-ItemProperty -Path $registryEnvironment -Name PATH -Value $newPaths
            $newRegistryEnvironment=(Get-ItemProperty -Path $registryEnvironment -Name PATH).Path
            write-host "Environmental paths have been changed:`r`nFrom: $originalPaths`r`nTo: $newPaths"
            return $true            
        }
    }catch{
        write-warning $_
        return $false
    }
}

addEnvironmentalPath $pathToAdd
removeEnvironmentalPath $pathToRemove

PowerShell: Find Windows RDS Roles and Their Licensing Servers

# Get TS Licensing Servers (Enterprise or AD Registered)
$termLicenseServers=Get-ADGroupMember -Identity "Terminal Server License Servers"
$termLicenseServers|Select-Object -Property @{label='computername';expression={[regex]::match($_.DistinguishedName,'([\w\d-]+)(?=,OU=)').Value}}

# Get TS Connection Points with Licensing Role Installed (may or may not be registered on the domain)
$termServers=Get-ADObject -Filter {objectClass -eq 'serviceConnectionPoint' -and Name -eq 'TermServLicensing'}
$termServers|Select-Object -Property @{label='computername';expression={[regex]::match($_.DistinguishedName,'([\w\d-]+)(?=,OU=)').Value}}

# Check localhost for TS Licensing Servers
$tsSettings=gwmi -namespace "Root/CIMV2/TerminalServices" Win32_TerminalServiceSetting 
$tsSettings.GetSpecifiedLicenseServerList().SpecifiedLSList

# Find RDS Servers and their Licensing Configs
$servers=Get-ADComputer -Filter {OperatingSystem -Like '*Windows Server*' -and Enabled -eq $true}
$results=@()
$noResults=@()
$initCount=0
$serversCount=$servers.Count
write-host "There are $serversCount servers to check..."
foreach($server in $servers.DNSHostName){
	$initCount++
	write-host "$initCount of $serversCount`: $server"
	$rdsRole=try{Get-WindowsFeature RDS-RD-Server -ComputerName $server -EA Ignore}catch{$null}	
	if($rdsRole){
		$rdsRoleInstalled=$rdsRole.InstallState -eq 'Installed'
		if($rdsRoleInstalled){
		$rdsLicenseServer=try{		
			$tsSettings=gwmi -namespace "Root/CIMV2/TerminalServices" Win32_TerminalServiceSetting -computername $server
			$tsSettings.GetSpecifiedLicenseServerList().SpecifiedLSList
		}catch{
			'not configured'
		}
		$result=@{
			computername=$server
			rdsLicenseServer=$rdsLicenseServer
			licensingType=$tsSettings.LicensingName
		}
		write-host "$server has RDS-RD-Server role installed with licensing server(s) $rdsLicenseServer"
		$results+=$result
		}else{	
			write-host "$server doesn't have RDS-RD-Server role installed"
		}
	}else{
		write-warning "$env:computername cannot connect to $server"
		$noResults+=@{
			computername=$server
			connectionProblem=$True			
		}
	}
	#pause
}

$results|%{write-host "$($_.computername)`: $($_.rdsLicenseServer) | $($_.LicensingType)"}
$noResults|%{write-host "$($_.computername)"}

PowerShell: Install App Using MSI on Remote Computers Via WinRM

# installMSiRemoteComputers.ps1
# version 0.0.1

$computernames='REMOTEPC001','REMOTEPC002'
$thisMsiFile='C:\Temp\something.msi'
$appName='testapp'
$desiredVersion='1.0'
$maxWaitSeconds=120

function installMsiOnRemoteComputers($computernames,$msiFile,$appName,$desiredVersion,$maxWaitSeconds){
    function installMsiOnRemoteComputer{
        param(
            $computernames=$env:computername,
            $msiFile,
            $destinationLocalTempFolder='C:\Temp',
            $testFileName='testfile.txt'
        )
        
        function translateLocalPathToSmbPath($computername,$localPath,$testFileName){
            $adminDriveLetter=[regex]::match($localPath,'^([\w\W])\:').captures.groups[1].value
            $partialPath=[regex]::match($localPath,'^([\w\W])\:(.*)').captures.groups[2].value
            $testPath=join-path "\\$computername\$adminDriveLetter`$" "$partialPath"
            if(!(test-path $testPath)){
                try{
                    New-Item -Path $testPath -ItemType "directory" -force
                }catch{
                    write-warning "Unable to create $testPath"
                    return $false
                }
            }
            try{
                $null=New-Item -Path $testPath -Name $testFileName -ItemType "file" -force
                Remove-Item "$testPath\$testFileName" -force
                return $testPath
            }catch{
                write-warning "Unable to read or write to $testPath"
                return $false
            }        
        }
    
        $results=[hashtable]@{}
        $msiLocalFilePath=join-path $destinationLocalTempFolder $(split-path $msiFile -leaf)
        foreach ($computername in $computernames){
            $translatedDestination=translateLocalPathToSmbPath $computername $destinationLocalTempFolder $testFileName
            if($translatedDestination){
                copy-item $msiFile $translatedDestination
            }else{
                write-warning "Unable to copy $msiFile to $translatedDestination"
                $results+=[hashtable]@{$computername=$false}
                continue
            }        
            $psSession=new-psSession $computername
            if($psSession.State -eq 'Opened'){
                $result=invoke-command -session $pssession -scriptblock{
                    param($filePath)
                    $file=gi $filePath
                    $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
                    )
                    try{
                        [diagnostics.process]::start("msiexec.exe", $MSIArguments).WaitForExit()
                        write-host "MSIEXEC has been called for file $filePath on $env:computername"
                        return $true
                    }catch{
                        write-warning $_
                        return $false
                    }             
                } -Args $msiLocalFilePath
    
                # Note Error:
                # Resolved by not using -wait switch and calling [diagnostics.process] instead of Start-Process "msiexec.exe"
                # Although, this error would still being thrown with the [diagnostics.process] result of success
                # Processing data for a remote command failed with the following error message: The I/O operation has been aborted
                # because of either a thread exit or an application request. For more information, see the about_Remote_Troubleshooting
                # Help topic.
                #     + CategoryInfo          : OperationStopped: (:String) [], PSRemotingTransportException
                #     + FullyQualifiedErrorId : JobFailure
                #     + PSComputerName        : 
    
                $results+=[hashtable]@{$computername=$result}
                remove-psSession $psSession
            }else{
                write-warning "Unable to connect to $computername"
                $results+=[hashtable]@{$computername="Unable to connect to $computername"}
            }
        }
        return $results
    }
    $results=[hashtable]@{}
    foreach($computername in $computernames){
        try{
            installMsiOnRemoteComputer $computername $msiFile
            write-host "Now waiting up to $maxWaitSeconds seconds before checking on the install result"
            $timer=[System.Diagnostics.Stopwatch]::StartNew()
            do{                        
                start-sleep -seconds 5
                $appVersionPassed=invoke-command -computername $computername {
                    param($appName,$desiredVersion)
                    $matchedApp=Get-WmiObject -Class Win32_Product -Filter "Name='$appName'"
                    if($matchedApp.Version -eq $desiredVersion){
                        return $true
                    }else{
                        return $false
                    }
                } -Args $appName,$desiredVersion
                if($appVersionPassed){
                    write-host "$appName $desiredVersion installed on $computername successfully"
                    $results+=[hashtable]@{$computername='$appName $desiredVersion installed'}                            
                }
                $exitCondition=$timer.elapsed.totalseconds -ge $maxWaitSeconds
            }until($appVersionPassed -or $exitCondition)
            if(!$appVersionPassed){                        
                    write-host "$appName $desiredVersion has NOT been installed successfully on $computername"
                    $results+=[hashtable]@{$computername="$appName $desiredVersion NOT installed"}
            }
            $timer.stop()
        }catch{
            write-warning $_
            $results+=[hashtable]@{$computername="$_"}
            exit 1   
        }
    }
    return $results
}

installMsiOnRemoteComputers $computernames $thisMsiFile $appName $desiredVersion $maxWaitSeconds

PowerShell: Move Guest VM to Different Cluster in Hyper-V

# Assumptions:
# - Guest VM files are stored at a single location
# - The service account or admin user has access to source and destination
# - Source and destination storage paths are joined to the same domain

$vmName='TESTWINDOWS'
$sourceStorage='C:\ClusterStorage\Blob1\TESTWINDOWS'
$destinationStorage='\\LAX-SMB1\VMS\TESTWINDOWS'

# step 1: Stop VM at Source and copy its files to Destination Storage
$vm=try{get-vm $vmName}catch{$null}
if($null -ne $vm){
    stop-vm $vmName -force -Confirm:$false
    # Remove-VM -VM $vm -DeletePermanently:$false -Confirm:$false # Unregister VM
    robocopy $sourceStorage $destinationStorage /E /R:0 /NP
}else{
    write-warning "$vmName is not found."
}

# Step 2: Register VM at destination
$parentPath='\\LAX-SMB1\VMS\TESTWINDOWS'
$vmPath=join-path $parentPath '\Virtual Machines\'
$vmcxFile=(gci $vmpath|?{$_.extension -like '.vmcx'}).FullName
Import-VM -Path $vmcxFile -copy $parentPath -GenerateNewId

# Step 3: Make adjustments

# While disconnected from network, run this command within the Windows VM
C:\Windows\System32\Sysprep\sysprep.exe /generalize
# Connect VM to the correct subnet / vlan
# Join to domain if necessary
# Verify that all disks are attached

# Step 4: Add vm into cluster
$vmName='TESTWINDOWS'
$targetCluster=(get-cluster).Name
 
function addVmToCluster{
    param($vmName,$targetCluster)
    try{
        if(!$targetCluster){
            $targetCluster=(get-cluster -ea SilentlyContinue).Name
            }
        if($targetCluster){
            Add-ClusterVirtualMachineRole -Cluster $targetCluster -VirtualMachine $vmName -EA Stop
            return $true
            }
        else{
            write-host "No clusters defined."
            return $false
            }
        }
    catch{
        write-warning "$($error[0])"
        return $false
        }
}
 
addVmToCluster $vmName $targetCluster

PowerShell: How To Install VMM Agent on Hyper-V Nodes

# Update these variables to match your system
$appName='Microsoft System Center Virtual Machine Manager Agent (x64)'
$desiredVersion='10.22.1287.0'
$msiFile='X:\System Center Virtual Machine Manager\amd64\Setup\msi\Agent\vmmAgent.msi'
$maxWaitSeconds=120
$computersList = @'
    HYPERV001
    HYPERV002
'@

function installMsiOnRemoteComputer{
    param(
        $computernames=$env:computername,
        $msiFile,
        $destinationLocalTempFolder='C:\Temp',
        $testFileName='testfile.txt'
    )
    
    function translateLocalPathToSmbPath($computername,$localPath,$testFileName){
        $adminDriveLetter=[regex]::match($localPath,'^([\w\W])\:').captures.groups[1].value
        $partialPath=[regex]::match($localPath,'^([\w\W])\:(.*)').captures.groups[2].value
        $testPath=join-path "\\$computername\$adminDriveLetter`$" "$partialPath"
        if(!(test-path $testPath)){
            try{
                New-Item -Path $testPath -ItemType "directory" -force
            }catch{
                write-warning "Unable to create $testPath"
                return $false
            }
        }
        try{
            $null=New-Item -Path $testPath -Name $testFileName -ItemType "file" -force
            Remove-Item "$testPath\$testFileName" -force
            return $testPath
        }catch{
            write-warning "Unable to read or write to $testPath"
            return $false
        }        
    }

    $results=[hashtable]@{}
    $msiLocalFilePath=join-path $destinationLocalTempFolder $(split-path $msiFile -leaf)
    foreach ($computername in $computernames){
        $translatedDestination=translateLocalPathToSmbPath $computername $destinationLocalTempFolder $testFileName
        if($translatedDestination){
            copy-item $msiFile $translatedDestination
        }else{
            write-warning "Unable to copy $msiFile to $translatedDestination"
            $results+=[hashtable]@{$computername=$false}
            continue
        }        
        $psSession=new-psSession $computername
        if($psSession.State -eq 'Opened'){
            $result=invoke-command -session $pssession -scriptblock{
                param($filePath)
                $file=gi $filePath
                $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
                )
                try{
                    [diagnostics.process]::start("msiexec.exe", $MSIArguments).WaitForExit()
                    write-host "MSIEXEC has been called for file $filePath on $env:computername"
                    return $true
                }catch{
                    write-warning $_
                    return $false
                }             
            } -Args $msiLocalFilePath

            # Note Error:
            # Resolved by not using -wait switch and calling [diagnostics.process] instead of Start-Process "msiexec.exe"
            # Although, this error would still being thrown with the [diagnostics.process] result of success
            # Processing data for a remote command failed with the following error message: The I/O operation has been aborted
            # because of either a thread exit or an application request. For more information, see the about_Remote_Troubleshooting
            # Help topic.
            #     + CategoryInfo          : OperationStopped: (:String) [], PSRemotingTransportException
            #     + FullyQualifiedErrorId : JobFailure
            #     + PSComputerName        : 

            $results+=[hashtable]@{$computername=$result}
            remove-psSession $psSession
        }else{
            write-warning "Unable to connect to $computername"
            $results+=[hashtable]@{$computername="Unable to connect to $computername"}
        }
    }
    return $results
}

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
}

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=.{Clear-Variable matches
            [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
}

function main{
    $computerNames=sortArrayStringAsNumbers(@($computersList -split "`n" -replace "\..*$")|%{$_.tostring().trim()})
    $results=[hashtable]@{}
    foreach($node in $computernames){
        write-host "Processing $node ..."
        $appVersionPassed=invoke-command -computername $node {
            param($appName,$desiredVersion)
            $matchedApp=Get-WmiObject -Class Win32_Product -Filter "Name='$appName'"
            if($matchedApp.Version -eq $desiredVersion){
                return $true
            }else{
                return $false
            }
        } -Args $appName,$desiredVersion
        
        if(!$appVersionPassed){
            $vcredist140Installed=invoke-command -computername $node {        
                $null=choco install vcredist140 -y
                $chocoApps=choco list -l
                if($chocoApps -match 'vcredist140'){
                    return $true
                }else{
                    return $false
                }
            }
            if($vcredist140Installed){
                $appRemoved=removeAppwizProgram $node $appName
                if(($appRemoved|out-string) -match "True"){
                    try{
                        installMsiOnRemoteComputer $node $msiFile
                    }catch{
                        write-warning $_                    
                    }
                    write-host "Now waiting up to $maxWaitSeconds seconds before checking on the install result"
                    $timer=[System.Diagnostics.Stopwatch]::StartNew()
                    do{                        
                        start-sleep -seconds 5
                        $appVersionPassed=invoke-command -computername $node {
                            param($appName,$desiredVersion)
                            $matchedApp=Get-WmiObject -Class Win32_Product -Filter "Name='$appName'"
                            if($matchedApp.Version -eq $desiredVersion){
                                return $true
                            }else{
                                return $false
                            }
                        } -Args $appName,$desiredVersion
                        if($appVersionPassed){
                            write-host "$appName $desiredVersion installed on $node successfully"
                            $results+=[hashtable]@{$node='$appName $desiredVersion installed'}                            
                        }
                        $exitCondition=$timer.elapsed.totalseconds -ge $maxWaitSeconds 
                    }until($appVersionPassed -or $exitCondition)
                    if(!$appVersionPassed){                        
                            write-host "$appName $desiredVersion has NOT been installed successfully on $node"
                            $results+=[hashtable]@{$node='$appName $desiredVersion NOT installed'}
                    }
                    $timer.stop()
                }else{
                    write-warning "Unable to uninstall outdated app on $node"
                    $results+=[hashtable]@{$node='Unable to uninstall outdated app'}
                }
            }else{
                $results+=[hashtable]@{$node='Unable to install vcredist140'}
            }
        }else{
            write-host "$appName is already at $desiredVersion"
            $results+=[hashtable]@{$node='$appName is already at $desiredVersion'}
        }
    }
    return sortArrayStringAsNumbers($results.GetEnumerator()|select Name,Value)
}

main

PowerShell: How to Convert Multi-Line Texts Into an Array of Strings

Significance: we often run into situations where a list of items (such as usernames, computernames, etc.) that are needed to be actioned within a PowerShell script. Instead of manually creating double quotes around each string, we could quickly paste them into a text file or a text variable signified with the @ symbol as demonstrated below

Example 1:

# Paste a list of names here
# Important: there could spaces on each line
$usersList = @'
username1
username2
username3
'@

$userNames = @($usersList -split "`n")|%{$_.Trim()}

Example 2:

# Paste a list of names here
# Important: there must be no spaces nor extra characters between each name
$computersList = @'
computername1
computername2
computername3
'@

$computerNames = @($computersList -split "`n" -replace "\..*$")

PowerShell: Remove Virtual Machine Snapshots in VMM

Base Cmdlets:

$vmmServer='SOMETHINGHERE'
$vmName='VMNAMEHERE'
$snapshots=Get-SCVMCheckpoint -vmmserver $vmmServer -vm $(get-scvirtualmachine $vmName)
$snapshots|%{Remove-SCVMCheckpoint -VMCheckpoint $_}

Automation:

# removeVmSnapshotsInVmm.ps1

$vmmServers=@(
    'vmm01.kimconnect.com',
    'vmm02.kimconnect.com'
)
$daysThreshold=30
$maxConfirmationsCount=3           

function removeVmSnapshotsInVmm{
    param(
        $vmmServers=$env:computername,
        $daysThreshold=30,
        $maxConfirmationsCount=3
    )
    foreach($vmmServer in $vmmServers){
        $session=new-pssession -computername $vmmServer -credential (get-credential -message "Admin Credential to $vmmServer")
        if($session.State -eq 'Opened'){
            invoke-command -session $session{
                param($daysThreshold,$maxConfirmationsCount)
            
                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;
                }
                
                $snapshotsToRemove=Get-SCVMCheckpoint -vmmserver localhost|?{$_.AddedTime -lt (get-date).adddays(-$daysThreshold)}
                foreach($snapshot in $snapshotsToRemove){
                    $snapshotName=$snapshot.Name
                    $vm=$snapshot.VM
                    $vmState=$vm.Status
                    $confirmed=if($maxConfirmationsCount-- -gt 1){confirmation "Delete snapshot $snapshotName ?"}else{$true}
                    if($confirmed){
                        $ready=if($vmState -eq 'Running'){
                            $true
                        }else{
                            try{                                
                                    Repair-SCVirtualMachine -VM $vm -Dismiss                                
                                    $true
                                }catch{
                                    write-warning $_
                                    $false
                                }
                        }
                        if($ready){Remove-SCVMCheckpoint -VMCheckpoint $snapshot}                        
                    }else{
                        write-host "Snapshot $snapshotName NOT removed."
                        break
                    }
                }
            } -Args $daysThreshold,$maxConfirmationsCount
            Remove-PSSession $session
        }else{
            write-warning "Unable to connect to VMM Server $vmmServer."
        }
    }
}

removeVmSnapshotsInVmm $vmmServers $daysThreshold $maxConfirmationsCount

PowerShell: Add Local Group Members Onto a Server List

Sometimes, the GUI method of accomplishing tasks is too arduous and error prone. Thus, these quick cmdlets would get the job done in matter of seconds, assuming that WinRM and firewall allows the jump host to reach its targets.

# addLocalGroupMemberOnServerList.ps1

$computernames=@(
	'testserver1',
    'testserver2'
)

$localAdmins=@(
    'domain\backupadmin'
)

$remoteDesktopUsers=@(
    'domain\testuser'
)

invoke-command -computername $computernames -scriptblock{
		param($localAdmins,$remoteDesktopUsers)
		$localAdmins=$localAdmins.ToArray() # converting ArrayList datatype to Array [of objects]
		$remoteDesktopUsers=$remoteDesktopUsers.ToArray()
		try{
			$remoteDesktopUsers|%{add-localgroupmember -group 'Remote Desktop Users' -Member $_}
			$localAdmins|%{add-localgroupmember -group 'Administrators' -Member $_}
			return $true
		}catch{
			write-warning $_
			return $false
		}
	} -Args (,$localAdmins),(,$remoteDesktopUsers)

# Use legacy cmdlets to preemt this error
# Failed to compare two elements in the array.
#     + CategoryInfo          : NotSpecified: (:) [Get-LocalGroupMember], InvalidOperationException
#     + FullyQualifiedErrorId : An unspecified error occurred.,Microsoft.PowerShell.Commands.GetLocalGroupMemberCommand
#     + PSComputerName        : testserver.kimconnect.com
invoke-command -computername $computernames {net localgroup 'Administrators'; net localgroup 'Remote Desktop Users'}

Virtual Machine Manager Error ID 23351 FirstBootDevice Invalid

When moving, importing, exporting Generation 2 template in VMM, the following error occurs when trying to deploy :

The input string "
" for the FirstBootDevice parameter cannot be parsed and may be invalid.
Make sure the string uses one of the following formats: "SCSI,BusId(integer),LunId(integer)" or "NIC,SlotId(integer)"
ID: 23351

Here’s a quick Fix:

Set-SCVMTemplate -Name 'TEMPLATENAME' -FirstBootDevice 'SCSI,0,0'

Better yet, it’s advisable to include the cmdlet above into the importing function of VM templates. A Try-Catch clause with error 23351 keyword could be coded to handle these exceptions during VM provisioning automations.

PowerShell: Quickly Start Windows Services on Remote Computers

$computernames=@(
	"COMPUTER1",
	"COMPUTER2"
)
$serviceName='windows_exporter'

get-service $serviceName -computername $computernames|start-Service
get-service $serviceName -computername $computernames|select MachineName,Name,Status
Sample output:

MachineName    Name              Status
-----------    ----              ------
COMPUTER1      windows_exporter Running
COMPUTER2      windows_exporter Running

PowerShell: Search Windows Event Logs

# searchWindowsEventsLog.ps1

$computername=$env:computername
$logType='Security'
$eventId=4732
$daysBack=365
$limit=9999
$messageLike="*Remote Desktop Users*"

function searchWindowsEvents{
    param(
        $computername=$env:computername
        $logType='Security'
        $eventId=4732
        $daysBack=365
        $limit=9999
        $messageLike="*Remote Desktop Users*"    
    )    

    $filter=@{
        LogName=$logType
        ID=$eventId
        StartTime=[datetime]::Now.AddDays(-$daysBack)
    }
    
    $events=Get-WinEvent -FilterHashTable $filter -ComputerName $computername -EA Ignore|select -first $limit
    $events|?{$_.Message -like $messageLike}
}

searchWindowsEvents $computername $logType $eventId $daysBack $limit $messageLike

PowerShell: Increase CPU Count or Memory of VMs via Virtual Machine Manager

# IncreaseCpuandRamViaVMM.ps1

# User Input Variables
$vmNames=@(
    'TESTWINDOWS',
    'TESTWINDOWS2'
)
$vmmServer=$env:computername
$setCpuCount=8
$setDynamicMemory=$false
$dynamicMemoryMinimumGB='2GB'
$dynamicMemoryMaximumGB='16GB'

# Get all VMs beloging to specific cloud(s) - this is to dynamically configure all VMs within a virtual cloud
# $vms=Get-SCVirtualMachine -VMMServer $vmmServer|?{$_.Cloud.Name -in $cloudNames}
$dynamicMemoryMinimumMB=$dynamicMemoryMinimumGB/1MB
$dynamicMemoryMaximumMB=$dynamicMemoryMaximumGB/1MB
$results=@()
foreach ($vm in $vms){
    $vmName=$vm.Name
    $previousCpuCount=$vm.CPUCount
    write-host "Upgrading $vmName..."
    if($vm.Status -eq 'Running'){
        Stop-SCVirtualMachine -VM $vm -Shutdown
    }    
    if($setCpuCount -and !$setDynamicMemory){
        if($vm.CPUCount -lt $setCpuCount){
            Set-SCVirtualMachine -VM $vm -RunAsSystem -CPUCount $setCpuCount -CPUExpectedUtilizationPercent 20
        }else{
            write-host "$($vm.Name) CPU Count is $previousCpuCount, which is already equal or higher than $setCpuCount"
        }
        $results+=@{
            vmName=$vmName;
            previousCpuCount=$previousCpuCount;
            currentCpuCount=$setCpuCount
            }
    }elseif($setCpuCount -and $setDynamicMemory){
        Set-SCVirtualMachine -VM $vm -RunAsSystem `
        -CPUCount $setCpuCount -CPUExpectedUtilizationPercent 20 `
        -DynamicMemoryEnabled $setDynamicMemory -DynamicMemoryBufferPercentage 20 -MemoryWeight 5000 `
        -MemoryMB $dynamicMemoryMinimumMB -DynamicMemoryMinimumMB $dynamicMemoryMinimumMB -DynamicMemoryMaximumMB $dynamicMemoryMaximumMB        
        $results+=@{
            vmName=$vmName;
            previousCpuCount=$previousCpuCount;
            currentCpuCount=$setCpuCount;
            StartupMemory=$dynamicMemoryMinimumMB;
            DynamicMemoryMinimumMB=$dynamicMemoryMinimumMB;
            DynamicMemoryMaximumMB=$dynamicMemoryMaximumMB
            }
    }elseif(!$setCpuCount -and $setDynamicMemory){
        Set-SCVirtualMachine -VM $vm -RunAsSystem `
                -DynamicMemoryEnabled $setDynamicMemory -DynamicMemoryBufferPercentage 20 -MemoryWeight 5000 `
                -MemoryMB $dynamicMemoryMinimumMB -DynamicMemoryMinimumMB $dynamicMemoryMinimumMB -DynamicMemoryMaximumMB $dynamicMemoryMaximumMB        
                $results+=@{
                    vmName=$vmName;
                    StartupMemory=$dynamicMemoryMinimumMB;
                    DynamicMemoryMinimumMB=$dynamicMemoryMinimumMB;
                    DynamicMemoryMaximumMB=$dynamicMemoryMaximumMB
                    }
    }else{    
        write-warning "User must input the `$setDynamicMemory and/or `$setCpuCount variables"
        break
    }
    if($vm.Status -eq 'PowerOff'){
        Start-SCVirtualMachine -VM $vm
    }    
}
write-host $($results|out-string).trim()