Last active
July 30, 2024 06:12
-
-
Save davidlu1001/75b7a4b1adf1385b2e8c05f733073b5c to your computer and use it in GitHub Desktop.
Operation scripts for copy files and run commands on remote servers
This file contains hidden or 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
| <# | |
| .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