Last active
September 14, 2024 04:17
-
-
Save AfroThundr3007730/5c1d9213b2fb51a23c951da95aa7dd30 to your computer and use it in GitHub Desktop.
Collection of helpful bash and powershell functions (most are in my other gists).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
#Functions pulled from other sources | |
# Source: https://gist.github.com/flungo/428c374c040de1d0a30fd4a593d39040 | |
lsiommu() { | |
for d in $(find /sys/kernel/iommu_groups/ -type l | sort -n -k5 -t/); do | |
n=${d#*/iommu_groups/*} | |
n=${n%%/*} | |
printf 'IOMMU Group %s ' "$n" | |
lspci -nns "${d##*/}" | |
done | |
} | |
# Source: https://gist.github.com/r15ch13/ba2d738985fce8990a4e9f32d07c6ada | |
lsiommu2() { | |
shopt -s nullglob | |
lastgroup="" | |
for g in $(find /sys/kernel/iommu_groups/* -maxdepth 0 -type d | sort -V); do | |
for d in "$g"/devices/*; do | |
if [ "${g##*/}" != "$lastgroup" ]; then | |
echo -en "Group ${g##*/}:\t" | |
else | |
echo -en "\t\t" | |
fi | |
lastgroup=${g##*/} | |
lspci -nms "${d##*/}" | awk -F'"' '{printf "[%s:%s]", $4, $6}' | |
if [[ -e "$d"/reset ]]; then echo -en " [R] "; else echo -en " "; fi | |
lspci -mms "${d##*/}" | awk -F'"' '{printf "%s %-40s %s\n", $1, $2, $6}' | |
for u in "$d"/usb*/; do | |
bus=$(cat "${u}/busnum" 2 &>/dev/null) | |
lsusb -s "$bus": | awk '{ | |
gsub(/:/, "", $4); printf "%s|%s %s %s %s|", $6, $1, $2, $3, $4; | |
for(i = 7; i <= NF; i++){printf "%s ", $i}; printf "\n" | |
}' | awk -F'|' '{printf "USB:\t\t[%s]\t\t %-40s %s\n", $1, $2, $3}' | |
done | |
done | |
done | |
shopt -u nullglob | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# Last updated 20240913 by AfroThundr | |
# SPDX-License-Identifier: GPL-3.0-or-later | |
#set -euo pipefail | |
#shopt -s extdebug | |
#trap 's=$?; echo >&2 "$0: Error on line $LINENO: $BASH_COMMAND"; exit $s' ERR | |
alias gen-challenge="dd if=/dev/urandom count=16 bs=1 status=none | hexdump -e '8/1 \"%02X \" \"\\n\"'" | |
alias glances='nice -n 19 ionice -c 3 glances' | |
alias gpg='gpg -v --expert' | |
alias gpg-minexport='gpg --export-options export-clean,export-minimal,no-export-attributes' | |
alias htop='nice -n 19 ionice -c 3 htop' | |
alias myip='echo "My public IP is: $(curl -4 https://icanhazip.com 2>/dev/null)"' | |
alias myip6='echo "My public IP is: $(curl -6 https://icanhazip.com 2>/dev/null)"' | |
alias pwgen='pwgen -1sy' | |
alias pwsh='pwsh -NoLogo' | |
alias sudo='sudo ' | |
alias ujournalctl='journalctl --user' | |
alias usystemctl='systemctl --user' | |
alias wine='flatpak run org.winehq.Wine' | |
alias winetricks='flatpak run --command=winetricks org.winehq.Wine' | |
alias xfreerdp='xfreerdp +glyph-cache +toggle-fullscreen /dynamic-resolution ' | |
itunes_upc() { | |
curl -s https://itunes.apple.com/lookup?upc="$1" | jq | |
} | |
split_music_compilation() { | |
[[ -f $1 ]] || { | |
printf 'Audio file not found: %s' "$1" | |
return 1 | |
} | |
[[ -f $2 ]] || { | |
printf 'Cuesheet file not found: %s' "$2" | |
return 1 | |
} | |
local source=$1 cuesheet=$2 count=0 pstamp='0:00' stamp title album | |
while read -r line; do | |
[[ $line =~ ([0-9]+:[0-9]+:[0-9]+).-.(.*).\[(.*)\] ]] && { | |
stamp=${BASH_REMATCH[1]} | |
title=${BASH_REMATCH[2]} | |
album=${BASH_REMATCH[3]} | |
[[ count -gt 0 ]] && nice -n 19 ionice -c 3 ffmpeg -i "$source" -ss "$pstamp" \ | |
-t $(($(date -d "$stamp" +%s) - $(date -d "$pstamp" +%s))) -c copy -v 0 \ | |
"$(printf '%02d - %s [%s].%s' "$count" "$ptitle" "$palbum" "${source##*.}")" & | |
((count++)) | |
pstamp=$stamp | |
ptitle=$title | |
palbum=$album | |
} | |
done <"$cuesheet" | |
nice -n 19 ionice -c 3 ffmpeg -i "$source" -ss "$pstamp" -c copy -v 0 \ | |
"$(printf '%02d - %s [%s].%s' "$count" "$title" "$album" "${source##*.}")" | |
return 0 | |
} | |
getpower() { | |
printf "%.1f W Battery\n" "$( | |
bc -l <<<"$(</sys/class/power_supply/BAT0/current_now) * \ | |
$(</sys/class/power_supply/BAT0/voltage_now) / 10^12" | |
)" | |
printf "%.1f W Charging\n" "$( | |
bc -l <<<"$(</sys/class/power_supply/BAT0/charge_now) * \ | |
$(</sys/class/power_supply/BAT0/voltage_now) / 10^12" | |
)" | |
} | |
genpw() { | |
local length=20 count=5 | |
while [[ -n $1 ]]; do | |
[[ $1 =~ -l|--length ]] && { | |
length=$2 | |
shift 2 | |
} | |
[[ $1 =~ -c|--count ]] && { | |
count=$2 | |
shift 2 | |
} | |
[[ $1 =~ -q|--quiet ]] && { | |
local quiet=true | |
shift | |
} | |
done | |
[[ -n $quiet ]] || | |
printf 'Generating %d passwords of length %d\n\n' "$count" "$length" | |
printf '%b\n' "$( | |
tr -dc '[:graph:]' </dev/urandom | fold -w "$length" | head -n "$count" | |
)" | |
[[ -n $quiet ]] || | |
printf '\nEffective entropy of each: %.2f\n' \ | |
"$(bc -l <<<"l(94^$length)/l(2)")" | |
} | |
get_stream_stats() { | |
awk 'BEGIN { | |
match(ARGV[1], /([^-\/]+)-([0-9-]+)/, meta) | |
printf("Channel: %s Date: %s\n", meta[1], meta[2]) | |
} /[Cc]heer/ { | |
for (i = 1; i <= NF; i++) { | |
if ($i ~ /^[Cc]heer$/) { | |
j = i + 1 | |
token = $i $j | |
} else { token = $i } | |
match(token, /[Cc]heer([0-9]+)/, bit) | |
bits += bit[1] | |
} | |
} /subscribed/ { | |
match($0, /subscribed at Tier ([1-3])|subscribed with (Prime)/, tier) | |
subs[tier[1]]++ | |
} /gifting/ { | |
match($0, /is gifting ([0-9]+) Tier ([1-3]) Subs to/, gift) | |
gifts[gift[2]] += gift[1] | |
} $2 ~ /streamelements:/ && /just tipped/ { | |
match($0, /just tipped \$([0-9\.]+)/, tip) | |
tips++ | |
tipi += (tip[1] - .3) * .97 | |
} $2 ~ /soundalerts:/ && /play/ { | |
match($0, /played .+ for ([0-9]+) Bits|used ([0-9]+) Bits to play/, abit) | |
alerts++ | |
abits += abit[1] + abit[2] | |
} END { | |
printf("\nStream statistics:\n") | |
printf("%13s %\47-5d\t%14s %\47-8d\n", | |
"Prime Subs:", subs[""], "Cheered Bits:", bits) | |
for (i = 1; i <= 3; i++) { | |
printf("%13s %\47-5d\t%14s %\47-5d\n", | |
"Tier " i " Subs:", subs[i], "Tier " i " Gifted:", gifts[i]) | |
} | |
printf("%13s %\47-5d\t%14s %\47-5d\n", | |
"SE Tips:", tips, "Sound Alerts:", alerts) | |
biti = bits / 100 | |
subi = (subs[""] * 5 + subs[1] * 5 + subs[2] * 10 + subs[3] * 25) * .5 | |
gifti = (gifts[1] * 5 + gifts[2] * 10 + gifts[3] * 25) * .5 | |
abiti = abits / 100 * .8 | |
printf("\nEstimated stream income:\n") | |
printf("%15s\t$%\47 10.2f\n", "Bit Cheers:", biti) | |
printf("%15s\t$%\47 10.2f\n", "Subscriptions:", subi) | |
printf("%15s\t$%\47 10.2f\n", "Gifted Subs:", gifti) | |
printf("%15s\t$%\47 10.2f\n", "SE Tips:", tipi) | |
printf("%15s\t$%\47 10.2f\n", "Sound Alerts:", abiti) | |
printf("%15s\t$%\47 10.2f\n", | |
"Total Income:", biti + subi + gifti + tipi + abiti) | |
printf("\nUsing rates:\n") | |
printf("\tBits: %.2f, Subs: %.2f, Tips: %.2f, Alerts: %.2f\n", | |
1.0, 0.5, 0.97, 0.8) | |
}' "$1" | |
} | |
streamlink_vod() { | |
opts=( | |
--player-passthrough http | |
--stream-segment-timeout 1 | |
--stream-segment-threads 2 | |
) | |
[[ -n $2 ]] && opts+=(--hls-start-offset "$2") | |
~/.local/bin/streamlink "${opts[@]}" "$1" best | |
} | |
streamlink_save() { | |
file=${1##*/} | |
file=${file%%\?*} | |
streamlink -o "${2:-$file}".m2t "$1" best | |
} | |
streamlink_audio() { | |
file=${1##*/} | |
file=${file%%\?*} | |
streamlink --stdout "$1" best | ffmpeg -i pipe:0 "$file".aac | |
} | |
kill_ffcp() { | |
ps auxk -%mem | awk '/firefox/ && /contentproc/ && $4 >= 1 {print $2}' | | |
head -n "${1:-1}" | xargs -l kill -9 | |
} | |
# shellcheck source=contrib.sh | |
. "$(dirname "${BASH_SOURCE[0]}")"/contrib.sh |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Last updated 20240913 by AfroThundr | |
# SPDX-License-Identifier: GPL-3.0-or-later | |
Set-StrictMode -Version Latest | |
#region Internal Variables | |
$DefaultPSProfileContent = @' | |
# Note: This profile stub is automatically overwritten, make changes instead in profile_local.ps1 | |
# Always use strict mode | |
Set-StrictMode -Version Latest | |
# Profile and module auto-update | |
function _update { | |
$remote = '\\file-share\Scripts\modules\HelperFunctions.psm1' | |
$local = $env:PSModulePath.split(';')[0] + '\HelperFunctions\HelperFunctions.psm1' | |
# Update the module if there are changes | |
if (([IO.Fileinfo]$remote).Exists) { | |
Try { | |
if (!(Get-Command -Name Update-InternalModule -ErrorAction SilentlyContinue)) { | |
New-Module -Name HelperFunctionsTemp -ScriptBlock ( | |
[scriptblock]::Create([IO.File]::ReadAllText($remote)) | |
) | Import-Module | |
} | |
$updated = Update-InternalModule -Remote $remote -Local $local | |
Remove-Module -Name HelperFunctionsTemp -ErrorAction Ignore | |
} Catch { Write-Output 'Failed to update module HelperFunctions' } | |
} | |
# Import the module and update this profile stub | |
Try { | |
Import-Module -Name HelperFunctions -Force | |
if ($updated) { Initialize-PSProfileAutoUpdate -Overwrite } | |
} Catch { Write-Output 'Unable to import module HelperFunctions' } | |
# Import the local profile (if it exists) | |
$localProfile = $PROFILE.CurrentUserAllHosts.ForEach{ $_.Insert($_.LastIndexOf('.'), '_local') } | |
if (([IO.FIleInfo][string]$localProfile).Exists) { . $localProfile } | |
} | |
_update | |
'@ | |
#endregion | |
#region External Functions | |
#region Introspection | |
function Get-StaticProperties { | |
<# .SYNOPSIS | |
Enumerates the static properties of a class #> | |
Param( | |
# The class to enumerate | |
[Parameter(Mandatory)] | |
[type]$Class | |
) | |
return (Get-Member -InputObject $Class -Static -MemberType Property).ForEach{ | |
@{ $_.Name = $Class::($_.Name) } | |
} | |
} | |
function Get-StaticMethods { | |
<# .SYNOPSIS | |
Enumerates the static methods of a class #> | |
Param( | |
# The class to enumerate | |
[Parameter(Mandatory)] | |
[type]$Class | |
) | |
return (Get-Member -InputObject $Class -Static -MemberType Method).ForEach{ | |
[PSCustomObject]@{ Name = $_.Name; OverloadDefinitions = $class::($_.Name) } | |
} | |
} | |
function Get-TypeAccelerators { | |
<# .SYNOPSIS | |
Enumerate the available type accelerators #> | |
Param( | |
# Filter by type FullName | |
[string]$Filter | |
) | |
return [PSObject].Assembly.GetType( | |
'System.Management.Automation.TypeAccelerators' | |
)::get.GetEnumerator().Where{ $_.Value.Fullname -match $Filter } | |
} | |
function Get-ExportedTypes { | |
<# .SYNOPSIS | |
Get a list of exported types matching the provided filter #> | |
Param( | |
# Filter to match against | |
[string]$Filter, | |
# Match against the type FullName | |
[switch]$FullName | |
) | |
return [AppDomain]::CurrentDomain.GetAssemblies().GetExportedTypes().Where{ | |
$_.IsPublic -and !$_.IsGenericType | |
}.Where{ | |
if ($FullName) { $_.FullName -match $Filter } else { $_.Name -match $Filter } | |
}.FullName | Sort-Object | |
} | |
#endregion | |
#region Security | |
function Set-ScriptSignature { | |
<# .SYNOPSIS | |
Wrapper function to sign and timestamp a script file #> | |
[Alias('SignScript')] | |
Param( | |
# The script file to sign | |
[Parameter(Mandatory)] | |
[IO.FileInfo]$ScriptFile | |
) | |
Set-AuthenticodeSignature -FilePath $ScriptFile -Certificate ( | |
Get-ChildItem -Path Cert:\CurrentUser\My -CodeSigningCert | |
)[0] -TimestampServer 'http://timestamp.digicert.com' #DevSkim: ignore DS137138 | |
} | |
function New-SecurePassword { | |
<# .SYNOPSIS | |
Generates high entropy passwords of configurable length #> | |
[Alias('genpw')] | |
Param( | |
# Length of passwords to genereate | |
[int]$Length = 20, | |
# How many passwords to generate | |
[int]$Count = 5, | |
# Quiet mode, only emit passwords | |
[switch]$Quiet | |
) | |
if (!$Quiet) { | |
Write-Output ("Generating {0} passwords of length {1}`n" -f $Count, $Length) | |
} | |
foreach ($_ in (1..$Count)) { | |
$b = [byte[]]::new(1) | |
$gen = [Security.Cryptography.RandomNumberGenerator]::Create() | |
[string]::Concat((1..$Length | & { process { | |
do { $gen.GetBytes($b) } until ($b -ge 33 -and $b -le 126); [char[]]$b | |
} })) | |
} | |
if (!$Quiet) { | |
Write-Output ("`nEffective entropy of each: {0:n}" -f | |
([math]::log([math]::pow(94, $Length)) / [math]::log(2))) | |
} | |
} | |
if ($PSVersionTable.PSVersion.Major -ge 7) { | |
function New-SecurePassword { | |
<# .SYNOPSIS | |
Generates high entropy passwords of configurable length #> | |
[Alias('genpw')] | |
Param( | |
# Length of passwords to genereate | |
[int]$Length = 20, | |
# How many passwords to generate | |
[int]$Count = 5, | |
# Quiet mode, only emit passwords | |
[switch]$Quiet | |
) | |
if (!$Quiet) { | |
Write-Output ("Generating {0} passwords of length {1}`n" -f $Count, $Length) | |
} | |
foreach ($_ in (1..$Count)) { | |
[string]::Concat([char[]](Get-Random -Min 33 -Max 126 -Count $Length)) | |
} | |
if (!$Quiet) { | |
Write-Output ("`nEffective entropy of each: {0:n}" -f | |
[math]::log2([math]::pow(94, $Length))) | |
} | |
} | |
} | |
function New-SecureKey { | |
<# .SYNOPSIS | |
Generates high entropy keys of configurable size #> | |
[Alias('genkey')] | |
Param( | |
# Length of keys to genereate | |
[int]$Bytes = 16, | |
# How many keys to generate | |
[int]$Count = 1, | |
# Quiet mode, only emit keys | |
[switch]$Quiet | |
) | |
if (!$Quiet) { | |
Write-Output ("Generating {0} keys of size {1}`n" -f $Count, $Bytes) | |
} | |
foreach ($_ in (1..$Count)) { | |
$buff = [byte[]]::new($Bytes) | |
[Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($buff) | |
[string]::Concat(($buff | & { process { $_.ToString('x2') } })) | |
} | |
if (!$Quiet) { | |
Write-Output ("`nEntropy of each: {0}" -f ($Bytes * 8)) | |
} | |
} | |
if ($PSVersionTable.PSVersion.Major -ge 7) { | |
function New-SecureKey { | |
<# .SYNOPSIS | |
Generates high entropy keys of configurable size #> | |
[Alias('genkey')] | |
Param( | |
# Length of keys to genereate | |
[int]$Bytes = 16, | |
# How many keys to generate | |
[int]$Count = 1, | |
# Quiet mode, only emit keys | |
[switch]$Quiet | |
) | |
if (!$Quiet) { | |
Write-Output ("Generating {0} keys of size {1}`n" -f $Count, $Bytes) | |
} | |
foreach ($_ in (1..$Count)) { | |
[String]::Concat( | |
([Security.Cryptography.RandomNumberGenerator]::GetBytes($Bytes) | | |
& { process { $_.ToString('x2') } })) | |
} | |
if (!$Quiet) { | |
Write-Output ("`nEntropy of each: {0}" -f ($Bytes * 8)) | |
} | |
} | |
} | |
function Get-SSLServerCertificate { | |
<# .SYNOPSIS | |
Retrieves the X509 certificate by connecting to a SSL enabled server #> | |
[Alias('s_client')] | |
Param( | |
# Hostname or IP address to connect to | |
[Parameter(Mandatory)] | |
[String]$Hostname, | |
# Port to connect to | |
[Parameter()] | |
[Int]$Port = 443, | |
# Save cert to file | |
[Parameter()] | |
[switch]$Save, | |
# Don't output base64 | |
[Parameter()] | |
[switch]$AsBase64 | |
) | |
$sslStream = [Net.Security.SslStream]::new( | |
[Net.Sockets.TcpClient]::new($Hostname, $Port).GetStream(), $false, { $true }) | |
$sslStream.AuthenticateAsClient(( | |
& { if ($env:COMPUTERNAME) { $env:COMPUTERNAME } else { $env:HOSTNAME } })) | |
if ($Save) { | |
[IO.File]::WriteAllBytes( | |
[IO.Path]::Combine($pwd.Path, ( | |
($sslStream.RemoteCertificate.Subject.replace('*', '_') -split ',|=')[1] + '.cer')), | |
$sslStream.RemoteCertificate.GetRawCertData()) | |
} | |
if ($AsBase64) { $sslStream.RemoteCertificate.ExportCertificatePem() } | |
else { $sslStream.RemoteCertificate } | |
$sslStream.Dispose() | |
} | |
function Get-CertificateKeyData { | |
<# .SYNOPSIS | |
Retrieves the private key for a certificate object #> | |
Param( | |
# The certificate to retrieve the private key from | |
[Parameter(Mandatory, ValueFromPipeline)] | |
[X509Certificate]$Certificate | |
) | |
switch -Regex ($Certificate.SignatureAlgorithm.FriendlyName) { | |
'^.+RSA$' { | |
return [Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey( | |
$Certificate).ExportRSAPrivateKeyPem() | |
} | |
'.+ECDSA$' { | |
return [Security.Cryptography.X509Certificates.ECDSACertificateExtensions]::GetECDSAPrivateKey( | |
$Certificate).ExportECPrivateKeyPem() | |
} | |
'.+DSA$' { | |
return [Security.Cryptography.X509Certificates.DSACertificateExtensions]::GetDSAPrivateKey( | |
$Certificate).ExportPkcs8PrivateKeyPem() | |
} | |
default { Write-Error ('Unsupported certificate key type: {0}' -f $_) } | |
} | |
} | |
#endregion Security | |
#region Utilities | |
function Get-StringPermutations { | |
<# .SYNOPSIS | |
Calculates the permutations of an input string #> | |
[Alias('permutate')] | |
Param( | |
# String to calculate permutations from | |
[Parameter(Mandatory)] | |
[String]$String, | |
# Return only unique permutations | |
[Parameter()] | |
[Switch]$Unique | |
) | |
function _Permutate($String, $Stub = '') { | |
if ($String.length -eq 2) { | |
return ($Stub + $String[0] + $String[1]), ($Stub + $String[1] + $String[0]) | |
} | |
for ($i = 0; $i -lt $String.Length; $i++) { | |
_Permutate -String $String.remove($i, 1) -Stub ($Stub + $String[$i]) | |
} | |
} | |
if ($String.length -eq 1) { return $String } | |
if (!$Unique) { _Permutate -String $String } else { | |
[Collections.Generic.HashSet[string]]::new([string[]](_Permutate -String $String)) | |
} | |
} | |
function Get-Factorial { | |
<# .SYNOPSIS | |
Get factorial of a number #> | |
[Alias('factorial', 'x!')] | |
Param( | |
# Number to compute factorial for | |
[Parameter(Mandatory, ValueFromPipeline)] | |
[double]$Value | |
) | |
if ($Value -ge 1) { | |
return $Value * (Get-Factorial -Value ($Value - 1)) | |
} elseif ($Value -eq 0) { | |
return 1 | |
} else { | |
Throw 'NaN' | |
} | |
} | |
function Get-GreatestCommonDenominator { | |
<# .SYNOPSIS | |
Find the greatest common denominator of two integers #> | |
[Alias('Get-GCD', 'gcd')] | |
param( | |
[Parameter(Mandatory)] | |
[double]$ValueA, | |
[Parameter(Mandatory)] | |
[double]$ValueB | |
) | |
Write-Verbose ('a: {0:d}, b: {1:d}' -f $ValueA, $ValueB) | |
if ($ValueB -eq 0) { | |
return $ValueA | |
} else { | |
return Get-GreatestCommonDenominator -ValueA $ValueB -ValueB ($ValueA % $ValueB) | |
} | |
} | |
function Get-Base64String { | |
<# .SYNOPSIS | |
Calculates the base64 of a string or a file #> | |
[Alias('base64')] | |
Param( | |
# Type of object (file or string) | |
[Parameter()] | |
[ValidateSet('File', 'String')] | |
[String]$Type = 'String', | |
# What to get the base64 from | |
[Parameter(Position = 0, ValueFromPipeline)] | |
[String]$InputObject | |
) | |
if ($Type -ieq 'File') { | |
return [Convert]::ToBase64String([IO.File]::ReadAllBytes($InputObject)) | |
} elseif ($Type -ieq 'String') { | |
return [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($InputObject)) | |
} else { | |
Throw 'Invalid Type Specified' | |
} | |
} | |
function Get-StringHash { | |
<# .SYNOPSIS | |
Calculates the hash of a string using the specified algorithm #> | |
[Alias('hash')] | |
Param ( | |
# The string to get a hash for | |
[Parameter(Position = 0, ValueFromPipeline)] | |
[String]$InputString, | |
# The hashing algorithm to use | |
[Parameter(Position = 1)] | |
[String]$Algorithm = 'SHA256' | |
) | |
return [string]::Concat( | |
([Security.Cryptography.HashAlgorithm]::Create($Algorithm).ComputeHash( | |
[Text.Encoding]::UTF8.GetBytes($InputString)) | & { process { $_.ToString('x2') } })) | |
} | |
function Get-CanonicalFilePath { | |
<# .SYNOPSIS | |
Canonicalize a given file path #> | |
[Alias('realpath')] | |
Param( | |
# Path to be canonicalized | |
[Parameter(Mandatory)] | |
[string]$FilePath | |
) | |
if (![IO.Path]::IsPathFullyQualified($FilePath)) { | |
[IO.Path]::GetFullPath($FilePath, $PWD) | |
} else { $FilePath } | |
} | |
function Split-BinaryFile { | |
<# .SYNOPSIS | |
Split a binary file into chunks of specified size #> | |
[Alias('bsplit')] | |
Param( | |
# Name of input file to be split | |
[Parameter(Mandatory, ValueFromPipeline)] | |
[string]$InputFile, | |
# Size in bytes to split the file into | |
[Parameter()] | |
[ValidateRange(4KB, 1PB)] | |
[UInt64]$SplitSize = 1MB, | |
# Prefix to use for output files | |
[Parameter()] | |
[string]$OutPrefix = $InputFile | |
) | |
try { | |
$in = [IO.File]::OpenRead((Get-CanonicalFilePath -FilePath $InputFile)) | |
$pad = [Math]::Max(3, [Math]::Ceiling([Math]::Log10( | |
[Math]::Ceiling($in.Length / $SplitSize)))) | |
while ($in.Position -ne $in.Length) { | |
try { | |
$out = [IO.File]::OpenWrite( | |
(Get-CanonicalFilePath -FilePath $OutPrefix) + '.' + ( | |
[Math]::Floor($in.Position / $SplitSize) + 1 | |
).ToString('0' * $pad)) | |
$buffer = [Byte[]]::new([Math]::Min(1MB, $SplitSize)) | |
while ($out.Position -eq 0 -or ( | |
$out.Position -ne $SplitSize -and | |
$in.Position -ne $in.Length)) { | |
$out.Write($buffer, 0, $in.Read($buffer, 0, [Math]::Min( | |
$buffer.Length, $SplitSize - $out.Position))) | |
} | |
} finally { $out.Dispose() } | |
} | |
} finally { $in.Dispose() } | |
} | |
function Join-BinaryFile { | |
<# .SYNOPSIS | |
Join (concatenate) multiple binary files #> | |
[Alias('bjoin')] | |
Param( | |
# List of files to be combined | |
[Parameter(Mandatory, ValueFromPipeline)] | |
[string[]]$FileList, | |
# Name of combined output file | |
[Parameter()] | |
[string]$OutFile = [Regex]::Replace($FileList[0], '\.[0-9]+$', '') | |
) | |
try { | |
$out = [IO.File]::OpenWrite((Get-CanonicalFilePath -FilePath $OutFile)) | |
$FileList.ForEach{ | |
try { | |
$in = [IO.File]::OpenRead((Get-CanonicalFilePath -FilePath $_)) | |
$in.CopyTo($out) | |
} finally { $in.Dispose() } | |
} | |
} finally { $out.Dispose() } | |
} | |
function Split-TextFile { | |
<# .SYNOPSIS | |
Split a text file based on line count #> | |
[Alias('tsplit')] | |
Param( | |
# The input file to be split | |
[Parameter(Mandatory)] | |
[string]$InputFile, | |
# The number of lines to split on | |
[Parameter()] | |
[ValidateRange(10, 1000000)] | |
[int]$SplitLines = 10000, | |
# Prefix to use for output files | |
[Parameter()] | |
[string]$OutPrefix = $InputFile, | |
# Copy the file header to each file | |
[Parameter()] | |
[switch]$CopyHeader | |
) | |
try { | |
$in = [IO.File]::OpenText((Get-CanonicalFilePath -FilePath $InputFile)) | |
$pad = [Math]::Max(3, [Math]::Ceiling([Math]::Log10([Math]::Ceiling( | |
[Linq.Enumerable]::Count( | |
[IO.File]::ReadLines($in.BaseStream.Name) | |
) / $SplitLines)))) | |
if ($CopyHeader) { $header = $in.ReadLine() } | |
[UInt32]$lineCount = 0 | |
while (!$in.EndOfStream) { | |
try { | |
$out = [IO.File]::CreateText( | |
(Get-CanonicalFilePath -FilePath $OutPrefix).ForEach{ | |
[IO.Path]::Combine([IO.Path]::GetDirectoryName($_), | |
[IO.Path]::GetFileNameWithoutExtension($_)) + '_' + | |
([Math]::Floor($lineCount / $SplitLines) + 1 | |
).ToString('0' * $pad) + [IO.Path]::GetExtension($_) }) | |
if ($CopyHeader) { $out.WriteLine($header) } | |
[UInt32]$chunkCount = 0 | |
while ($lineCount -eq 0 -or $chunkCount -eq 0 -or ( | |
$lineCount % $SplitLines -ne 0 -and | |
!$in.EndOfStream)) { | |
$out.WriteLine($in.ReadLine()) | |
$lineCount++ && $chunkCount++ | |
} | |
} finally { $out.Dispose() } | |
} | |
} finally { $in.Dispose() } | |
} | |
function Join-TextFile { | |
<# .SYNOPSIS | |
Merge multiple text files into one #> | |
[Alias('tjoin')] | |
Param( | |
# List of files to be combined | |
[Parameter(Mandatory)] | |
[string[]]$FileList, | |
# Name of combined output file | |
[Parameter()] | |
[string]$OutFile = [Regex]::Replace($FileList[0], | |
'_[0-9]+(\.[^\.]+)?$', '$1'), | |
# Strip header from files during merge | |
[Parameter()] | |
[switch]$StripHeader | |
) | |
try { | |
$out = [IO.File]::CreateText((Get-CanonicalFilePath -FilePath $OutFile)) | |
$FileList.ForEach{ | |
try { | |
$in = [IO.File]::OpenText((Get-CanonicalFilePath -FilePath $_)) | |
if ($StripHeader) { | |
if ($_ -notmatch $FileList[0]) { [void]$in.ReadLine() } | |
} | |
while (!$in.EndOfStream) { $out.WriteLine($in.ReadLine()) } | |
} finally { $in.Dispose() } | |
} | |
} finally { $out.Dispose() } | |
} | |
function Measure-CommandBatch { | |
<# .SYNOPSIS | |
Wrapper for Measure-Command to get batch execution stats #> | |
[Alias('BatchMeasure')] | |
Param( | |
# Command to measure | |
[Parameter(Mandatory, ValueFromPipeline)] | |
[ScriptBlock]$Expression, | |
# Number of executions to measure | |
[Parameter()] | |
[int]$Count = 5, | |
# Specify a property to measure | |
[Parameter()] | |
[String]$Property = 'TotalMilliseconds' | |
) | |
if ($PSVersionTable.PSVersion.Major -ge 6) { | |
return (1..$Count) | & { process { Measure-Command -Expression $Expression } } | | |
Measure-Object -AllStats -Property $Property | |
} else { | |
return (1..$Count) | & { process { Measure-Command -Expression $Expression } } | | |
Measure-Object -Sum -Average -Maximum -Minimum -Property $Property | |
} | |
} | |
function Hide-ConsoleWindow { | |
<# .SYNOPSIS | |
Hides a console window completely #> | |
Param() | |
Add-Type -Name User32 -Namespace Win32 -MemberDefinition ` | |
'[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);' | |
$Host.UI.RawUI.WindowTitle = [Guid]::NewGuid() | |
[Win32.User32]::ShowWindowAsync( | |
[Diagnostics.Process]::GetProcesses().Where{ | |
$_.MainWindowTitle -eq $Host.UI.RawUI.WindowTitle | |
}.MainWindowHandle, 0) | |
} | |
function Set-ProcessPriorityClass { | |
<# .SYNOPSIS | |
Sets the PriorityClass of a process #> | |
[Alias('renice')] | |
Param( | |
# The process name to search for | |
[Parameter(Mandatory, ParameterSetName = 'single')] | |
[string[]]$ProcessName, | |
# The process regex to search for | |
[Parameter(Mandatory, ParameterSetName = 'regex')] | |
[string]$ProcessRegex, | |
# The process glob to search for | |
[Parameter(Mandatory, ParameterSetName = 'glob')] | |
[string]$ProcessGlob, | |
# The process object to use | |
[Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'pipe')] | |
[Diagnostics.Process[]]$Process, | |
# The PriorityClass to set on the process | |
[Parameter(Position = 1)] | |
[Diagnostics.ProcessPriorityClass]$PriorityClass = 'BelowNormal' | |
) | |
(& { | |
switch ($PSCmdlet.ParameterSetName) { | |
'single' { | |
[Diagnostics.Process]::GetProcesses().Where{ | |
$_.Name -in $ProcessName } | |
} | |
'regex' { | |
[Diagnostics.Process]::GetProcesses().Where{ | |
$_.Name -match $ProcessRegex } | |
} | |
'glob' { | |
[Diagnostics.Process]::GetProcesses().Where{ | |
$_.Name -like $ProcessGlob } | |
} | |
'pipe' { $Process } | |
} | |
}).ForEach{ | |
if ($_.get_PriorityClass -ne $PriorityClass) { | |
$_.set_PriorityClass($PriorityClass) | |
} | |
} | |
} | |
#endregion | |
#region Directory Services | |
function Get-SMSManagementPoint { | |
<# .SYNOPSIS | |
Finds SCCM management point based on site code #> | |
Param( | |
# The site code to search | |
[Parameter(Mandatory)] | |
[string]$SiteCode | |
) | |
return [adsisearcher]::new( | |
[adsi]('LDAP://CN=Partitions,' + ( | |
[adsi]'LDAP://RootDSE').get('configurationNamingContext') | |
), | |
'(&(objectCategory=crossref)(netbiosname=*))' | |
).findAll().Properties.ncname.forEach{ | |
Try { | |
[adsisearcher]::new( | |
[adsi]('LDAP://CN=System Management,CN=System,' + $_), | |
"(&(ObjectClass=mSSMSManagementPoint)(Name=SMS-MP*-$SiteCode-*))" | |
).findAll().Properties.mssmsmpname | |
} Catch {} | |
} | |
} | |
function Resolve-DnsRecordAllDCs { | |
<# .SYNOPSIS | |
Resolves a DNS record on all active DCs in a domain #> | |
[Alias('nslookup_all')] | |
Param( | |
# Name to resolve | |
[Parameter(Mandatory, ValueFromPipeline)] | |
[string]$Name, | |
# Type of record | |
[Parameter(ValueFromPipelineByPropertyName)] | |
[string]$Type = 'A', | |
# Batch size for parallel query | |
[Parameter()] | |
[int]$ThrottleLimit = 10 | |
) | |
Resolve-DnsName -Type 'SRV' -Name _ldap._tcp.dc._msdcs.$env:USERDNSDOMAIN | | |
Where-Object { $_.Type -eq 'SRV' } | ForEach-Object -ThrottleLimit $ThrottleLimit -Parallel { | |
if (Test-Connection -TargetName $_.NameTarget -Count 1 -TimeoutSeconds 1 -Quiet) { | |
Resolve-DnsName -Type $Using:Type -Name $Using:Name -Server $_.NameTarget ` | |
-DnsOnly -QuickTimeout -ErrorAction SilentlyContinue | |
} | |
} | Where-Object { $_.QueryType -eq $Type } # | Select-Object -Unique | |
} | |
function Get-ADComputerReport { | |
<# .SYNOPSIS | |
Generate a CSV report on AD computer objects. #> | |
Param( | |
# Site code to search | |
[Parameter(Mandatory)] | |
[string]$Site, | |
# Output file | |
[Parameter()] | |
[IO.FileInfo]$File = '.\computers.csv' | |
) | |
Get-ADComputer -Filter ('Name -like "*{0}*"' -f $Site) -Property * | Sort-Object | | |
Select-Object -Property Name, | |
@{ N = 'DNSHostName'; E = { if ($_.DNSHostName) { $_.DNSHostname } else { 'Unknown' } } }, | |
@{ N = 'IPv4Address'; E = { if ($_.IPv4Address) { $_.IPv4Address } else { 'Unknown' } } }, | |
Enabled, | |
@{ N = 'Modified'; E = { if ($_.Modified) { $_.Modified } else { 'Unknown' } } }, | |
@{ N = 'PasswordLastSet'; E = { if ($_.PasswordLastSet) { $_.PasswordLastSet } else { 'Unknown' } } }, | |
@{ N = 'LastLogonDate'; E = { if ($_.LastLogonDate) { $_.LastLogonDate } else { 'Unknown' } } }, | |
@{ N = 'OSVersion'; E = { if ($_.OperatingSystemVersion) { | |
switch -wildcard ($_.OperatingSystemVersion) { | |
'*14393*' { $_ + ' [1607]' } '*17763*' { $_ + ' [1809]' } | |
'*18363*' { $_ + ' [1909]' } '*19042*' { $_ + ' [20H2]' } | |
'*19044*' { $_ + ' [21H2]' } '*19045*' { $_ + ' [22H2]' } | |
'*22000*' { $_ + ' [21H2]' } '*22621*' { $_ + ' [22H2]' } | |
'*22631*' { $_ + ' [23H2]' } default { $_ + ' [Unknown]' } | |
} | |
} else { 'Unknown' } } | |
}, | |
Description, | |
@{ N = 'MemberOf'; E = { ($_.MemberOf.ForEach{ ($_ -split '=|,')[1] }) -join ', ' } } | | |
Export-Csv -Path $File | |
} | |
#endregion | |
#region Miscellaneous | |
function Write-ScriptEvent { | |
<# .SYNOPSIS | |
Wrapper to write PowerShell transcripts to the event log #> | |
Param( | |
# Transcript file to read from | |
[Parameter(Mandatory)] | |
[string]$LogFile, | |
# Event source to apply | |
[Parameter()] | |
[string]$Source = 'PSScript Transcript', | |
# Event ID to apply | |
[Parameter()] | |
[int]$EventID = 1000 | |
) | |
$split = [regex]::Escape('**********************') | |
$lastRun = ([IO.File]::ReadAllText($LogFile) -split ($split))[-3] | |
if ($lastRun.Length -gt 32600) { | |
$lastRun = $lastRun.Substring(0, 32600) + "`n`n****OUTPUT TRUNCATED****" | |
} | |
if (!([System.Diagnostics.EventLog]::SourceExists($Source))) { | |
New-EventLog -LogName 'Application' -Source $Source | |
} | |
Write-EventLog -LogName 'Application' -Source $Source ` | |
-EntryType Information -EventId $EventID -Message $lastRun | |
} | |
function Edit-VMDKHeader { | |
<# .SYNOPSIS | |
Edit the metadata header of a VMDK file #> | |
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] | |
Param( | |
# The VMDK file to edit | |
[Parameter(Mandatory)] | |
[ValidateNotNullOrEmpty()] | |
[IO.FileInfo]$VMDKFile | |
) | |
if (!$VMDKFile.Exists) { throw 'The file specified does not exist.' } | |
$buffer = [byte[]]::new(1024) | |
$tmpFile = [IO.Path]::Combine([IO.Path]::GetTempPath(), [IO.Path]::GetTempFileName()) | |
$vmdkStream = [IO.File]::Open($VMDKFile, [IO.FileMode]::Open, [IO.FileAccess]::ReadWrite) | |
$vmdkStream.Position = 512 | |
[void]$vmdkStream.Read($buffer, 0, 1024) | |
Write-Output 'Dumping VMDK header metadata to:' $tmpFile | |
$tmpStream = [IO.File]::OpenWrite($tmpFile) | |
$tmpStream.Write($buffer, 0, 1024) | |
$tmpStream.Dispose() | |
Write-Output 'Make necessary changes, then save and close the editor.' | |
[Diagnostics.Process]::Start('notepad.exe', $tmpFile).WaitForExit() | |
$tmpStream = [IO.File]::OpenRead($tmpFile) | |
[void]$tmpStream.Read($buffer, 0, 1024) | |
$tmpStream.Dispose() | |
([IO.FileInfo]$tmpFile).Delete() | |
if ($PSCmdlet.ShouldProcess($VMDKFile, 'Overwrite VMDK header metadata')) { | |
Write-Output 'Overwriting VMDK header metadata.' | |
$vmdkStream.Position = 512 | |
$vmdkStream.Write($buffer, 0, 1024) | |
} | |
$vmdkStream.Dispose() | |
} | |
function Invoke-GithubFileRequest { | |
<# .SYNOPSIS | |
Retrieves a file from a Github repository. #> | |
[Alias('gh_curl')] | |
Param( | |
# URL of file to download | |
[Parameter(Mandatory)] | |
[Uri]$URL, | |
# Output file path | |
[Parameter()] | |
[IO.FileInfo]$FilePath = $URL.Segments[-1] | |
) | |
function _validate { | |
Param([Uri]$link) | |
switch ($link.Host) { | |
'github.com' { | |
if ($link.Segments.Count -lt 6 -or $link.Segments[3] -ne 'blob/') { break } | |
return [Uri]('https://api.github.com/repos/{1}/{2}/contents/{5}?ref={4}' -f | |
$link.LocalPath.Split('/', 6)) | |
} | |
'raw.githubusercontent.com' { | |
if ($link.Segments.Count -lt 5) { break } | |
return [Uri]('https://api.github.com/repos/{1}/{2}/contents/{4}?ref={3}' -f | |
$link.LocalPath.Split('/', 5)) | |
} | |
'api.github.com' { | |
if ($link.LocalPath -notmatch 'git/blobs|contents') { break } | |
return $link | |
} | |
} | |
Write-Output 'The provided URL is not a valid Github file URL:' $link.OriginalString | |
break 2 | |
} | |
[IO.File]::WriteAllBytes($FilePath, [Convert]::FromBase64String( | |
(Invoke-RestMethod -Uri (_validate $URL)).Content)) | |
} | |
function Get-GHRepoList { | |
<# .SYNOPSIS | |
Gets a list of repositories of a GitHub user. #> | |
Param( | |
# User to enumerate | |
[Parameter(Mandatory)] | |
[string]$User, | |
# Page size to request | |
[Parameter()] | |
[int]$Size = 100 | |
) | |
$page = 0 | |
$count = (Invoke-RestMethod -Uri ('https://api.github.com/users/{0:s}' -f $User)).public_repos | |
do { | |
$page++ | |
(Invoke-RestMethod -Uri ('https://api.github.com/users/{0:s}/repos?page={1:d}&per_page={2:d}' -f $User, $page, $Size)).html_url | |
[Threading.Thread]::Sleep(1000) | |
} while ($page * $size -lt $count) | |
} | |
function Update-LocalNugetRepository { | |
<# .SYNOPSIS | |
Update a local Nuget repo from a remote repo #> | |
Param( | |
# Remote repository feed URL | |
[Uri]$FeedURLBase = 'https://aka.ms/sme-extension-feed', | |
# Local repository directory | |
[IO.DirectoryInfo]$Destination = [IO.Path]::Join([Environment]::GetFolderPath('MyDocuments'), 'NuGetLocal'), | |
# Overwrite local file if it exists | |
[switch]$Overwrite, | |
# Number of times to retry a download | |
[int]$Retries = 2 | |
) | |
function _DownloadEntries { | |
Param([Uri]$feedURL) | |
[xml]$feed = [Net.WebClient]::new().DownloadString($feedUrl) | |
$entries = $feed.feed.entry.where{ $_.properties.IsLatestVersion.'#text' -eq 'true' } | |
[int]$progress = 0 | |
foreach ($entry in $entries) { | |
$fileName = $entry.properties.id + '.' + $entry.properties.version + '.nupkg' | |
[IO.FileInfo]$filePath = [IO.Path]::Join($Destination, $fileName) | |
[int]$pagePercent = (++$progress) / $entries.Count * 100 | |
if (!$Overwrite -and $filePath.Exists) { | |
Write-Progress -Activity ('{0} already downloaded' -f $fileName) ` | |
-Status ('{0}% of current page complete' -f $pagePercent) ` | |
-PercentComplete $pagePercent | |
continue | |
} | |
Write-Progress -Activity ('Downloading {0}' -f $fileName) ` | |
-Status ('{0}% of current page complete' -f $pagePercent) ` | |
-PercentComplete $pagePercent | |
[int]$tries = 0 | |
while ($tries -le $Retries) { | |
try { | |
$tries++ | |
[Net.WebClient]::new().DownloadFile($entry.content.src, $filePath) | |
break | |
} catch [System.Net.WebException] { | |
Write-Host ("Problem downloading URL: {0}`tTry: {1}`n`tException: {2}" -f | |
$entry.content.src, $tries, $_.Exception.Message) | |
} | |
} | |
} | |
$nextLink = ([object[]]$feed.feed.link).Where{ $_.rel.startsWith('next') } | |
if ($nextLink) { return [Uri]$nextLink.href } | |
} | |
$downloadURL = ([xml][Net.WebClient]::new().DownloadString($FeedURLBase)).service.GetAttribute('xml:base') + '/Packages' | |
if (!$Destination.Exists) { [void]$Destination.Create() } | |
while ($downloadURL) { $downloadURL = _DownloadEntries -feedURL $downloadURL } | |
} | |
if ($PSVersionTable.PSVersion.Major -ge 7) { | |
function Install-PythonRelease { | |
<# .SYNOPSIS | |
Install the Python language interpreter #> | |
param( | |
# Install for all users (requires admin) | |
[Switch]$AllUsers, | |
# Use beta build instead of latest stable | |
[Switch]$Beta, | |
# Use version X.Y.Z instead of latest stable | |
[Version]$Version | |
) | |
if ($AllUsers -and !( | |
[Security.Principal.WindowsIdentity]::GetCurrent().groups -contains 'S-1-5-32-544')) { | |
throw 'The AllUsers switch requires administrator privileges.' | |
} | |
[Uri]$baseURI = 'https://www.python.org/ftp/python/' | |
if (!$Version) { | |
[Version[]]$versionList = ( | |
Invoke-WebRequest -NoProxy -Uri $baseURI | |
).links.href.where{ $_ -match '\d+\.\d+\.\d+' }.trim('/') | |
[Array]::sort($versionList) | |
$Version = $versionList[$($Beta ? -1 : -2)] | |
} | |
[IO.DirectoryInfo]$installDir = $AllUsers ? | |
[IO.Path]::Join( | |
[Environment]::GetFolderPath('ProgramFiles'), 'Python', | |
('Python {0:d}.{1:d}' -f $Version.Major, $Version.Minor)) : | |
[IO.Path]::Join( | |
[Environment]::GetFolderPath('LocalApplicationData'), 'Programs', 'Python', | |
('Python{0:d}{1:d}' -f $Version.Major, $Version.Minor)) | |
if ($installDir.Exists) { | |
throw 'Selected python release already exists at {0:s}' -f $installDir | |
} | |
[Uri]$download = $Beta ? | |
'{0:s}{1:s}/{2:s}' -f $baseURI, $Version, ( | |
Invoke-WebRequest -NoProxy -Uri ('{0:s}{1:s}' -f $baseURI, $Version) | |
).links.href.where{ $_ -match 'amd64\.exe$' }[-1] : | |
'{0:s}{1:s}/python-{1:s}-amd64.exe' -f $baseURI, $Version | |
$setup = [IO.Path]::Join([IO.Path]::GetTempPath(), $download.Segments[-1]) | |
Invoke-WebRequest -NoProxy -Uri $download -OutFile $setup | |
if (!([IO.FileInfo]$setup).Exists) { | |
throw 'Setup file {0:s} not downloaded' -f $setup | |
} | |
& $setup /quiet InstallAllUsers=$($AllUsers ? 1 : 0) PrependPath=1 Include_launcher=0 Shortcuts=0 | |
([IO.FileInfo]$setup).Delete() | |
} | |
} | |
function Install-GitSCMRelease { | |
<# .SYNOPSIS | |
Installs the Git SCM #> | |
param() | |
[Uri]$gitRelease = 'https://api.github.com/repos/git-for-windows/git/releases/latest' | |
[Uri]$installer = ( | |
Invoke-RestMethod -NoProxy -Uri $gitRelease | |
).assets.browser_download_url.where{ $_ -match '64-bit.7z.exe' }[0] | |
[IO.DirectoryInfo]$installDir = [IO.Path]::Join( | |
[Environment]::GetFolderPath('LocalApplicationData'), 'Programs', 'PortableGit' | |
) | |
if ($installDir.Exists) { | |
throw 'Git already exists at {0:s}' -f $installDir | |
} | |
$setup = [IO.Path]::Join([IO.Path]::GetTempPath(), $installer.Segments[-1]) | |
Invoke-WebRequest -NoProxy -Uri $installer -OutFile $setup | |
if (!([IO.FileInfo]$setup).Exists) { | |
throw 'Setup file {0:s} not downloaded' -f $setup | |
} | |
& $setup -o $installDir -y | |
[Environment]::SetEnvironmentVariable('Path', | |
[Environment]::GetEnvironmentVariable('Path', 'User').Insert( | |
0, [IO.Path]::Join($installDir, 'bin') + ';'), 'User') | |
([IO.FileInfo]$setup).Delete() | |
} | |
function Get-UserSessionsFromSubnet { | |
<# .SYNOPSIS | |
Queries computers in a subnet for active logon sessions #> | |
Param( | |
# DHCP scope to query for leases | |
[Parameter(Mandatory)] | |
[IPAddress]$ScopeId, | |
# DHCP server to query | |
[Parameter()] | |
[string]$Server, | |
# NETBIOS domain name | |
[Parameter()] | |
[string]$Domain = $env:USERDOMAIN | |
) | |
(Get-DhcpServerv4Lease -ComputerName $Server -ScopeId $ScopeId).HostName.ForEach{ | |
Try { | |
[PSCustomObject]@{ | |
Computer = $_ | |
Sessions = [Collections.Generic.Hashset[String]]( | |
Get-CimInstance -ClassName Win32_LoggedOnUser -ComputerName $_ | |
).Antecedent.Where{ $_.Domain -eq $Domain }.Name.Where{ $_ -notmatch $env:USERNAME } | |
} | |
} Catch {} } | Format-Table Name, Value | |
} | |
function Get-QUserEntries { | |
<# .SYNOPSIS | |
Get the quser logged on user table #> | |
Param( | |
# Computer to query | |
[string]$Computer = $env:COMPUTERNAME | |
) | |
((query user /server:$Computer | Select-Object -Skip 1) -replace '\s{2,}', ',').ForEach{ | |
line = if ($_.Split(',').Count -eq 5) { | |
$_.Insert($_.IndexOf(','), ',') | |
} else { $_ } | |
$user, $session, $id, $state, $idleTime, $logonTime = $line.split(',') | |
$idleTime = if ($idleTime -match '\+') { | |
$idleTime.replace('+', '.') | |
} elseif ($idleTime -match '\d') { | |
'0:' + $idleTime | |
} else { 0 } | |
[PSCustomObject]@{ | |
PSTypeName = 'QuserObject' | |
UserName = $user.replace('>', '').trim() | |
Session = $session | |
ID = [int]$id | |
State = $state | |
IdleTime = [timespan]$idleTime | |
LogonTime = [datetime]$logonTime | |
IsCurrent = $user.StartsWith('>') | |
} | |
} | |
} | |
function Stop-InactiveUserSession { | |
<# .SYNOPSIS | |
Removes inactive user sessions on a computer #> | |
Praram( | |
# Idle threshold in days | |
[int]$IdleDays = 2 | |
) | |
(Get-QUserEntries).Where{ | |
$_.State -match 'Disc' -and $_.IdleTime.Days -ge $idleDays }.ForEach{ | |
Write-Output 'Logging off inactive user' $_.UserName | |
logoff $_.ID | |
} | |
} | |
#endregion | |
#region PowerCLI | |
# Get completion percentage for batch VM clone tasks | |
function Get-VMDeployProgress { | |
[Alias('deployProgress')] | |
Param( | |
# vCenter user to filter for | |
[string]$VCUser = $global:DefaultVIServers[0].User.Split('\')[1] | |
) | |
do { | |
$Tasks = (Get-Task).Where{ $_.Name -match 'clone' ` | |
-and $_.State -match 'running' ` | |
-and $_.ExtensionData.Info.Reason.UserName -match $VCUser } | |
if ($null -eq $Original) { $Original = $Tasks.Count } | |
$Running = $Percent = 0 | |
$Tasks.ForEach{ | |
$Percent += $_.PercentComplete | |
if ($_.state -match 'Running') { $Running++ } | |
} | |
$Total = (($Original - $Running) * 100 + $Percent) / $Original | |
Write-Progress -Activity 'Deploying VMs' -PercentComplete $total ` | |
-Status ('VMs still deploying: {0} of {1}, {2:n}% complete...' ` | |
-f $running, $original, $total) | |
[Threading.Thread]::Sleep(1000) | |
} | |
while ($Running -ne 0) | |
} | |
function Get-RunningTasks { | |
<# .SYNOPSIS | |
Get a running list of active VITasks #> | |
Param() | |
while ((Get-Task).Where{ $_.state -eq 'running' }.count -gt 0) { | |
(Get-Task).Where{ $_.state -eq 'running' } | | |
Sort-Object name, percentcomplete | | |
Format-Table Name, State, PercentComplete, StartTime, | |
@{ L = 'Target'; E = { $_.ExtensionData.Info.EntityName } }, | |
@{ L = 'Initiator'; E = { $_.ExtensionData.Info.Reason.UserName } } | |
[Threading.Thread]::Sleep(10000) | |
} | |
} | |
function Connect-VIServerAuto { | |
<# .SYNOPSIS | |
Automated PowerCLI connections to vCenter Server #> | |
Param( | |
# VCenter server URL to connect to | |
[string]$server = 'vcenter', | |
# vCenter admin security group | |
[string]$group = $env:USERDOMAIN + '\vCenter_Admins' | |
) | |
if ([System.Security.Principal.WindowsIdentity]::GetCurrent().Groups.Translate( | |
[System.Security.Principal.NTAccount]) -contains $group) { | |
[void](Connect-VIServer (Resolve-DnsName $server).Name) | |
} else { | |
[IO.FileInfo]$filePath = [IO.Path]::Join($env:APPDATA, | |
(Get-StringHash "$env:USERNAME,$env:COMPUTERNAME")) | |
if (!$filePath.Exists) { | |
$cred = Get-Credential -Message 'Enter credentials for VMWare Admin:' | |
$cred.UserName, ($cred.Password | ConvertFrom-SecureString) -join ',' | | |
Out-File $filePath | |
} | |
if (!$cred) { | |
$user, $pass = (Get-Content $filePath).Split(',') | |
$cred = [System.Management.Automation.PSCredential]::new( | |
$user, (ConvertTo-SecureString $pass) | |
) | |
} | |
[void](Connect-VIServer (Resolve-DnsName $server).Name -Credential $cred) | |
$filePath, $user, $pass, $cred = $null | |
} | |
} | |
function Get-ESXiSerials { | |
<# .SYNOPSIS | |
Get the serial number and service tags for ESXi hosts #> | |
Param( | |
# Full name of host or a regex | |
[string]$HostSpec | |
) | |
Get-View -ViewType HostSystem -Property Name, Hardware.SystemInfo ` | |
-Filter @{ 'Name' = $(if ($HostSpec) { $HostSpec } else { '' }) } | | |
Sort-Object -Property Name | Select-Object Name, | |
@{ | |
Name = 'Serials' | |
Expression = { $_.Hardware.SystemInfo | Select-Object ` | |
@{ | |
Name = 'AssetTag' | |
Expression = { $_.OtherIdentifyingInfo[0].IdentifierValue } | |
}, | |
@{ | |
Name = 'ServiceTag' | |
Expression = { $_.OtherIdentifyingInfo[1].IdentifierValue } | |
}, | |
@{ | |
Name = 'EnclosureSerialNumberTag' | |
Expression = { $_.OtherIdentifyingInfo[2].IdentifierValue } | |
}, | |
@{ | |
Name = 'SerialNumberTag' | |
Expression = { $_.OtherIdentifyingInfo[3].IdentifierValue } | |
} | |
} | |
} | Select-Object Name -ExpandProperty Serials | Format-Table | |
} | |
#endregion | |
#region Module Setup | |
function Update-InternalModule { | |
Param( | |
# Remote module path | |
[Parameter(Mandatory)] | |
[IO.FIleInfo]$Remote, | |
# Local module path | |
[Parameter(Mandatory)] | |
[IO.FileInfo]$Local, | |
# Don't error if unable to update | |
[Parameter()] | |
[switch]$Quiet | |
) | |
if (!$Remote.Exists -and !$Quiet) { | |
Throw 'Unable to check for updates to module, ensure the file is reachable: ' + $Remote.FullName | |
} | |
if ($Remote.Exists -and !$Local.Exists -or (Get-FileHash $Remote).Hash -ne (Get-FileHash $Local).Hash) { | |
[void][IO.Directory]::CreateDirectory($Local.Directory) | |
[void][IO.File]::Copy($Remote.FullName, $Local.FullName, 1) | |
return $true | |
} | |
return $false | |
} | |
function Initialize-PSProfileAutoUpdate { | |
<# .SYNOPSIS | |
Creates the user's PowerShell profile and enables module auto updating #> | |
[Alias('init_profile')] | |
Param( | |
# Overwrite the file without asking | |
[switch]$Overwrite, | |
# The path to the user's PowerShell profile | |
[IO.FileInfo]$ProfilePath = $PROFILE.CurrentUserAllHosts | |
) | |
if ($ProfilePath.Exists -and !$Overwrite) { | |
Write-Output 'File exists, use -Overwrite to nuke it' | |
return | |
} | |
[void][IO.Directory]::CreateDirectory($ProfilePath.Directory) | |
[void][IO.File]::WriteAllText($ProfilePath.FullName, $script:DefaultPSProfileContent) | |
} | |
#endregion | |
#endregion | |
Export-ModuleMember -Function * -Alias * |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
See also:
utils.common.sh