Skip to content

Instantly share code, notes, and snippets.

@davidlu1001
Last active July 30, 2024 06:12
Show Gist options
  • Select an option

  • Save davidlu1001/75b7a4b1adf1385b2e8c05f733073b5c to your computer and use it in GitHub Desktop.

Select an option

Save davidlu1001/75b7a4b1adf1385b2e8c05f733073b5c to your computer and use it in GitHub Desktop.
Operation scripts for copy files and run commands on remote servers
<#
.SYNOPSIS
Executes commands or copies files on remote computers with enhanced features.
.DESCRIPTION
This script provides functionality to execute commands or copy files on multiple remote computers simultaneously.
It supports both running commands/scripts and copying files/directories, with added features like retry logic,
enhanced error handling, and more detailed logging.
.PARAMETER ComputerName
Specifies the target computers. Can be used with or without ConfigFile.
.PARAMETER Command
Specifies the command to run on remote computers.
.PARAMETER ScriptPath
Specifies the path to a script file to run on remote computers. Supports both absolute and relative paths.
.PARAMETER ArgumentList
Specifies arguments to pass to the script specified by ScriptPath.
.PARAMETER Source
Specifies the source path(s) for file/directory copying. Supports both absolute and relative paths, wildcards, and regular expressions.
.PARAMETER Destination
Specifies the destination path for file/directory copying on remote computers.
.PARAMETER ConfigFile
Specifies a file containing a list of target computers. Supports both absolute and relative paths.
Default is "config.txt" in the script directory.
.PARAMETER DryRun
If specified, shows what would happen without making any changes.
.PARAMETER OutputFile
Specifies a file to save the results.
.PARAMETER Timeout
Specifies the timeout in seconds for each operation. Default is 300 seconds.
.PARAMETER ThrottleLimit
Specifies the maximum number of concurrent operations. Default is 10.
.PARAMETER RetryCount
Specifies the number of retry attempts for failed operations. Default is 3.
.PARAMETER RetryDelay
Specifies the delay in seconds between retry attempts. Default is 5 seconds.
.PARAMETER Verbose
If specified, provides detailed logging information.
.EXAMPLE
.\ops.ps1 -ComputerName Server1, Server2 -Command "Get-Service | Where-Object {`$_.Status -eq 'Running'}" -Verbose
.EXAMPLE
.\ops.ps1 -ScriptPath .\MyScript.ps1 -ArgumentList @{Param1 = 'Value1'; Param2 = 'Value2'} -RetryCount 5 -RetryDelay 10
.EXAMPLE
.\ops.ps1 -ConfigFile .\servers.txt -Source C:\Scripts\*, D:\Data -Destination C:\RemoteData -OutputFile results.csv -DryRun
.NOTES
Requires PowerShell 5.0 or later.
Ensure you have the necessary permissions on the remote computers.
#>
[CmdletBinding(DefaultParameterSetName='Run')]
param (
[Parameter(ParameterSetName='Run')]
[Parameter(ParameterSetName='Copy')]
[string[]]$ComputerName,
[Parameter(ParameterSetName='Run')]
[ValidateNotNullOrEmpty()]
[string]$Command,
[Parameter(ParameterSetName='Run')]
[ValidateNotNullOrEmpty()]
[string]$ScriptPath,
[Parameter(ParameterSetName='Run')]
[hashtable]$ArgumentList,
[Parameter(ParameterSetName='Copy', Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string[]]$Source,
[Parameter(ParameterSetName='Copy', Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]$Destination,
[Parameter(ParameterSetName='Run')]
[Parameter(ParameterSetName='Copy')]
[string]$ConfigFile = "config.txt",
[Parameter(ParameterSetName='Run')]
[Parameter(ParameterSetName='Copy')]
[switch]$DryRun,
[Parameter(ParameterSetName='Run')]
[Parameter(ParameterSetName='Copy')]
[ValidateNotNullOrEmpty()]
[string]$OutputFile,
[Parameter(ParameterSetName='Run')]
[Parameter(ParameterSetName='Copy')]
[ValidateRange(1, 3600)]
[int]$Timeout = 300,
[Parameter(ParameterSetName='Run')]
[Parameter(ParameterSetName='Copy')]
[ValidateRange(1, 100)]
[int]$ThrottleLimit = 10,
[Parameter(ParameterSetName='Run')]
[Parameter(ParameterSetName='Copy')]
[ValidateRange(0, 10)]
[int]$RetryCount = 3,
[Parameter(ParameterSetName='Run')]
[Parameter(ParameterSetName='Copy')]
[ValidateRange(1, 60)]
[int]$RetryDelay = 5
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Write-Log {
param(
[string]$Message,
[ValidateSet('Information', 'Warning', 'Error')]
[string]$Severity = 'Information'
)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logMessage = "[$timestamp] [$Severity] $Message"
Write-Verbose $logMessage
if ($Severity -eq 'Warning') {
Write-Warning $Message
} elseif ($Severity -eq 'Error') {
Write-Error $Message
}
}
function Resolve-Path2 {
param([string]$Path)
try {
$resolvedPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)
if (-not [System.IO.Path]::IsPathRooted($resolvedPath)) {
$resolvedPath = Join-Path -Path $PSScriptRoot -ChildPath $resolvedPath
}
return $resolvedPath
} catch {
throw "Unable to resolve path '$Path': $_"
}
}
function Get-RemoteHosts {
[CmdletBinding()]
param (
[string]$ConfigFile,
[string[]]$ComputerName
)
if ($ComputerName -and $ComputerName.Count -gt 0) {
return $ComputerName
}
if (-not [string]::IsNullOrEmpty($ConfigFile)) {
$resolvedPath = Resolve-Path2 $ConfigFile
if (Test-Path $resolvedPath) {
$hosts = Get-Content $resolvedPath | Where-Object { $_ -notmatch '^\s*$' -and $_ -notmatch '^\s*#' }
if ($hosts.Count -eq 0) {
throw "No valid hosts found in config file: $resolvedPath"
}
return $hosts
} else {
throw "Config file not found: $resolvedPath"
}
}
throw "No remote hosts specified. Use either -ConfigFile or -ComputerName"
}
function Invoke-RemoteOperation {
[CmdletBinding()]
param (
[string[]]$ComputerName,
[hashtable]$Params,
[int]$ThrottleLimit,
[int]$Timeout,
[int]$RetryCount,
[int]$RetryDelay
)
# If ScriptPath is specified, read the script content
if ($Params.ContainsKey('ScriptPath') -and $Params.ScriptPath) {
if (Test-Path $Params.ScriptPath) {
$Params.ScriptContent = Get-Content -Path $Params.ScriptPath -Raw
} else {
throw "Local script file not found: $($Params.ScriptPath)"
}
}
$scriptBlock = {
param($Params, $RetryCount, $RetryDelay)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Write-Log {
param([string]$Message)
Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] $Message"
}
function Invoke-WithRetry {
param(
[ScriptBlock]$ScriptBlock,
[int]$MaxAttempts,
[int]$DelaySeconds
)
$attempt = 1
$success = $false
while (-not $success -and $attempt -le $MaxAttempts) {
try {
Write-Log "Attempt $attempt of $MaxAttempts"
$result = & $ScriptBlock
$success = $true
return $result
}
catch {
if ($attempt -eq $MaxAttempts) {
throw
}
Write-Log "Attempt $attempt failed: $_"
Start-Sleep -Seconds $DelaySeconds
$attempt++
}
}
}
try {
Write-Log "Starting operation on $env:COMPUTERNAME"
switch ($Params.ParameterSet) {
'Run' {
if ($Params.ContainsKey('Command') -and $Params.Command) {
Write-Log "Executing command: $($Params.Command)"
Invoke-WithRetry -ScriptBlock {
$result = Invoke-Expression -Command $Params.Command
if ($null -ne $result) { $result } else { "Command executed successfully" }
} -MaxAttempts $RetryCount -DelaySeconds $RetryDelay
} elseif ($Params.ContainsKey('ScriptContent') -and $Params.ScriptContent) {
$tempScriptPath = [System.IO.Path]::GetTempFileName() + ".ps1"
Write-Log "Creating temporary script file: $tempScriptPath"
Set-Content -Path $tempScriptPath -Value $Params.ScriptContent
try {
Write-Log "Executing temporary script file"
Invoke-WithRetry -ScriptBlock {
if ($Params.ContainsKey('ArgumentList') -and $Params.ArgumentList -is [hashtable] -and $Params.ArgumentList.Count -gt 0) {
$argList = @()
foreach ($key in $Params.ArgumentList.Keys) {
$argList += "-$key"
$argList += $Params.ArgumentList[$key]
}
Write-Log "Executing script with arguments: $($argList -join ' ')"
$result = & $tempScriptPath @argList
} else {
Write-Log "Executing script without arguments"
$result = & $tempScriptPath
}
if ($null -ne $result) { $result } else { "Script executed successfully" }
} -MaxAttempts $RetryCount -DelaySeconds $RetryDelay
}
finally {
Write-Log "Removing temporary script file"
Remove-Item -Path $tempScriptPath -Force -ErrorAction SilentlyContinue
}
}
}
'Copy' {
foreach ($src in $Params.Source) {
$dest = Join-Path -Path $Params.Destination -ChildPath (Split-Path -Path $src -Leaf)
Write-Log "Copying from $src to $dest"
Invoke-WithRetry -ScriptBlock {
$copyParams = @{
Path = $src
Destination = $dest
Force = $true
Recurse = $true
ErrorAction = 'Stop'
}
Copy-Item @copyParams
"File/Directory copied successfully: $src -> $dest"
} -MaxAttempts $RetryCount -DelaySeconds $RetryDelay
}
}
}
Write-Log "Operation completed successfully on $env:COMPUTERNAME"
}
catch {
Write-Log "Error occurred: $_"
throw "Error on $env:COMPUTERNAME: $_"
}
}
$invokeCommandParams = @{
ScriptBlock = $scriptBlock
ComputerName = $ComputerName
ArgumentList = @($Params, $RetryCount, $RetryDelay)
ErrorAction = 'Stop'
ThrottleLimit = $ThrottleLimit
}
$job = Invoke-Command @invokeCommandParams -AsJob
if (-not (Wait-Job -Job $job -Timeout $Timeout)) {
Stop-Job -Job $job
Remove-Job -Job $job -Force
throw "Operation timed out after $Timeout seconds"
}
$results = Receive-Job -Job $job -ErrorAction Stop
Remove-Job -Job $job
$results | ForEach-Object {
if ($_ -is [System.Management.Automation.ErrorRecord]) {
[PSCustomObject]@{
ComputerName = $_.TargetObject
Status = "Error"
Output = $_.Exception.Message
}
} else {
[PSCustomObject]@{
ComputerName = $_.PSComputerName
Status = "Success"
Output = $_
}
}
}
}
try {
Write-Log "Script started"
# Validate input parameters
if (-not $Command -and -not $ScriptPath -and -not $Source) {
throw "You must specify either a Command, ScriptPath, or Source."
}
if ($Command -and $ScriptPath) {
throw "You cannot specify both Command and ScriptPath."
}
$remoteHosts = Get-RemoteHosts -ConfigFile $ConfigFile -ComputerName $ComputerName
Write-Log "Remote hosts obtained: $($remoteHosts -join ', ')"
$params = @{}
foreach ($key in $PSBoundParameters.Keys) {
if ($key -notin @('ThrottleLimit', 'Timeout', 'DryRun', 'OutputFile', 'ConfigFile', 'RetryCount', 'RetryDelay')) {
$params[$key] = $PSBoundParameters[$key]
}
}
$params.ParameterSet = $PSCmdlet.ParameterSetName
if ($params.ParameterSet -eq 'Copy') {
$params.Source = $params.Source | ForEach-Object {
$fullPath = Resolve-Path2 $_
if (-not (Test-Path $fullPath)) {
throw "Cannot find path '$_' because it does not exist."
}
$fullPath
}
$params.Destination = Resolve-Path2 $params.Destination
}
if ($params.ContainsKey('ScriptPath')) {
$params.ScriptPath = Resolve-Path2 $params.ScriptPath
if (-not (Test-Path $params.ScriptPath)) {
throw "Script file not found: $($params.ScriptPath)"
}
}
# Ensure ArgumentList is a hashtable if provided
if ($params.ContainsKey('ArgumentList')) {
if ($params.ArgumentList -isnot [hashtable]) {
throw "ArgumentList must be a hashtable."
}
} else {
# If ArgumentList is not provided, add an empty hashtable
$params.ArgumentList = @{}
}
if ($DryRun) {
Write-Host "Dry run mode. The following operations would be performed:"
foreach ($computer in $remoteHosts) {
if ($params.ParameterSet -eq 'Run') {
if ($Command) {
Write-Host "On $computer, would execute command: $Command"
} elseif ($ScriptPath) {
Write-Host "On $computer, would execute script: $ScriptPath"
if ($ArgumentList -and $ArgumentList.Count -gt 0) {
Write-Host "With arguments: $($ArgumentList | ConvertTo-Json -Compress)"
}
}
} elseif ($params.ParameterSet -eq 'Copy') {
Write-Host "Would copy files/directories from $($params.Source -join ', ') to $($params.Destination) on $computer"
}
}
} else {
Write-Log "Invoking remote operation"
$progress = 0
$totalHosts = $remoteHosts.Count
$results = Invoke-RemoteOperation -ComputerName $remoteHosts -Params $params -ThrottleLimit $ThrottleLimit -Timeout $Timeout -RetryCount $RetryCount -RetryDelay $RetryDelay | ForEach-Object {
$progress++
Write-Progress -Activity "Executing remote operations" -Status "Processing $($_.ComputerName)" -PercentComplete (($progress / $totalHosts) * 100)
$_
}
Write-Progress -Activity "Executing remote operations" -Completed
if ($null -ne $results -and $results.Count -gt 0) {
$results | Format-Table -AutoSize
if ($PSBoundParameters.ContainsKey('OutputFile')) {
$outputPath = Resolve-Path2 $OutputFile
$results | Export-Csv -Path $outputPath -NoTypeInformation
Write-Host "Results exported to $outputPath"
}
} else {
Write-Warning "No results obtained."
}
}
}
catch {
Write-Log -Message "An error occurred: $_" -Severity 'Error'
exit 1
}
finally {
Get-Job | Where-Object { $_.State -eq 'Running' } | Stop-Job
Get-Job | Remove-Job -Force -ErrorAction SilentlyContinue
Write-Log "Script finished"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment