From 33b2c13072a08d8f0dc024d9d06377c9575090de Mon Sep 17 00:00:00 2001 From: Jeff Hicks Date: Wed, 31 Jul 2019 13:39:39 -0400 Subject: [PATCH] v3.1.0 --- PSRemoteOperations.psd1 | 7 +- README.md | 5 +- autocompleters.ps1 | 10 +- changelog.md | 10 + docs/Get-PSRemoteOperationResult.md | 6 +- docs/Invoke-PSRemoteOperation.md | 6 +- docs/New-PSRemoteOperation.md | 6 +- docs/Register-PSRemoteOperationWatcher.md | 6 +- docs/Wait-PSRemoteOperation.md | 137 +++++++++++ docs/about_PSRemoteOperations.md | 4 +- en-us/PSRemoteOperations-help.xml | 213 +++++++++++++++++- functions.ps1 | 204 ++++++++++++----- license.txt | 2 +- private.ps1 | 10 +- windows/PSRemoteOperations.psm1 | 2 +- .../PSRemoteOperations.Tests.ps1 | 33 +-- 16 files changed, 549 insertions(+), 112 deletions(-) create mode 100644 docs/Wait-PSRemoteOperation.md rename windows/{test => Tests}/PSRemoteOperations.Tests.ps1 (90%) diff --git a/PSRemoteOperations.psd1 b/PSRemoteOperations.psd1 index ead6741..9dbc023 100644 --- a/PSRemoteOperations.psd1 +++ b/PSRemoteOperations.psd1 @@ -8,7 +8,7 @@ RootModule = "" # Version number of this module. -ModuleVersion = '3.0.0' +ModuleVersion = '3.1.0' # Supported PSEditions CompatiblePSEditions = @("Desktop","Core") @@ -23,7 +23,7 @@ Author = 'Jeff Hicks' CompanyName = 'JDH Information Technology Solutions, Inc.' # Copyright statement for this module -Copyright = '(c) 2018 JDH Information Technology Solutions, Inc. All rights reserved.' +Copyright = '(c) 2018-2019 JDH Information Technology Solutions, Inc. All rights reserved.' # Description of the functionality provided by this module Description = 'A PowerShell module for executing commands remotely in a non-remoting environment.' @@ -70,7 +70,8 @@ else { } # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. -FunctionsToExport = @('New-PSRemoteOperation','Invoke-PSRemoteOperation','Get-PSRemoteOperationResult','Register-PSRemoteOperationWatcher') + FunctionsToExport = @('New-PSRemoteOperation', 'Invoke-PSRemoteOperation', 'Get-PSRemoteOperationResult', 'Register-PSRemoteOperationWatcher', +'Wait-PSRemoteOperation') # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. CmdletsToExport = '' diff --git a/README.md b/README.md index 9c1d8bb..0eebdc8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # PSRemoteOperations -This PowerShell module is designed to run commands on remote computers but without using PowerShell remoting. It takes advantage of cloud services like DropBox and OneDrive. The idea is that you create a file with instructions on a command to run. The file includes the target computer name. The remote computer is monitoring the folder and when a matching file is detected the operation is invoked. +This PowerShell module is designed to run commands on remote computers but _without_ using PowerShell remoting. It takes advantage of cloud services like Dropbox and OneDrive. The central concept is that you create a file with instructions about a command to run on a remote or target computer. The file includes the target computer name. The remote computer is monitoring a shared folder and when a matching file is detected the operation is invoked. The shared or common folder is managed by whatever cloud or other service of your choice. You can install the latest version from the PowerShell Gallery: @@ -16,9 +16,10 @@ Or check out the individual commands: + [Invoke-PSRemoteOperation](docs/Invoke-PSRemoteOperation.md) + [New-PSRemoteOperation](docs/New-PSRemoteOperation.md) + [Register-PSRemoteOperationWatcher](docs/Register-PSRemoteOperationWatcher.md) ++ [Wait-PSRemoteOperation](docs/Wait-PSRemoteOperation.md) ## Cross-Platform and PowerShell Core The long-term goal is to ensure that this module will work cross-platform and in PowerShell Core. Basic functionality should exist running this module on PowerShell Core, both in Windows and non-Windows environments. Support for CMS messages is limited to Windows platforms through the use of dynamic parameters. `Register-PSRemoteOperationWatcher` requires a Windows platform but should work under PowerShell Core. For non-Windows systems, you will have to come up with your own tooling for monitoring and execution using `Invoke-PSRemoteOperation`. - *last updated 22 October 2018* +Last updated 2019-07-31 17:39:11Z UTC diff --git a/autocompleters.ps1 b/autocompleters.ps1 index 1483724..74a49b3 100644 --- a/autocompleters.ps1 +++ b/autocompleters.ps1 @@ -2,15 +2,15 @@ Register-ArgumentCompleter -CommandName Get-PSRemoteOperationResult, New-PSRemoteOperation -ParameterName Computername -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) - $names = (get-childitem -Path $PSRemoteOpArchive | Split-Path -leaf).foreach( {$_.split("_")[0].toUpper()}) | Get-Unique + $names = (Get-ChildItem -Path $PSRemoteOpArchive | Split-Path -leaf).foreach( { $_.split("_")[0].toUpper() }) | Get-Unique if ($wordToComplete) { - $fill = $names | where-object {$_ -match "$wordToComplete" } + $fill = $names | Where-Object { $_ -match "$wordToComplete" } } else { $fill = $names } - $fill | ForEach-Object { + $fill | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } @@ -18,8 +18,8 @@ Register-ArgumentCompleter -CommandName Get-PSRemoteOperationResult, New-PSRemot Register-ArgumentCompleter -CommandName New-PSRemoteOperation -ParameterName To -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) - (get-childitem -Path Cert:\CurrentUser\my -DocumentEncryptionCert).Subject | - ForEach-Object { + (Get-ChildItem -Path Cert:\CurrentUser\my -DocumentEncryptionCert).Subject | + ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } \ No newline at end of file diff --git a/changelog.md b/changelog.md index ca8f321..c72e867 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,15 @@ # Change Log for PSRemoteOperations +## v3.1.0 + ++ Fixed bug using CMS messages with a dynamic parameter ++ Fixed pester tests to accommodate sub-modules ++ Modified code to use `[void]` in place of `Out-Null` ++ All `-Computername` parameters now support an alias of `-cn` ++ Added `Wait-PSRemoteOperation` (Issue #10) ++ Updated help documentation. Online links now point to markdown files in the Github repository. ++ Updated `README.md` + ## v3.0.0 + restructured module to support Core and Windows through nested modules. (Issue #9) diff --git a/docs/Get-PSRemoteOperationResult.md b/docs/Get-PSRemoteOperationResult.md index 0115198..b0994f5 100644 --- a/docs/Get-PSRemoteOperationResult.md +++ b/docs/Get-PSRemoteOperationResult.md @@ -1,7 +1,7 @@ --- external help file: PSRemoteOperations-help.xml Module Name: PSRemoteOperations -online version: +online version: https://github.com/jdhitsolutions/PSRemoteOperations/blob/master/docs/Get-PSRemoteOperationResult.md schema: 2.0.0 --- @@ -65,7 +65,7 @@ Enter a computername to filter on. ```yaml Type: String Parameter Sets: (All) -Aliases: +Aliases: cn Required: False Position: Named @@ -112,4 +112,4 @@ Learn more about PowerShell: http://jdhitsolutions.com/blog/essential-powershell [Invoke-PSRemoteOperation](./Invoke-PSRemoteOperation) -[New-PSRemoteOperation](./New-PSRemoteOperation) \ No newline at end of file +[New-PSRemoteOperation](./New-PSRemoteOperation) diff --git a/docs/Invoke-PSRemoteOperation.md b/docs/Invoke-PSRemoteOperation.md index 2356e7c..f9e8e29 100644 --- a/docs/Invoke-PSRemoteOperation.md +++ b/docs/Invoke-PSRemoteOperation.md @@ -1,7 +1,7 @@ --- external help file: PSRemoteOperations-help.xml Module Name: PSRemoteOperations -online version: +online version: https://github.com/jdhitsolutions/PSRemoteOperations/blob/master/docs/Invoke-PSRemoteOperation.md schema: 2.0.0 --- @@ -28,7 +28,7 @@ Normally, this command will be called by a remote operation watcher job or simil ### Example 1 ```powershell -PS C:\> $file = Get-Childitem $PSRemoteOpPath\*.psd1 | where-object {$_.name -match "^$($env:Computername)"} +PS C:\> $file = Get-ChildItem $PSRemoteOpPath\*.psd1 | Where-Object {$_.name -match "^$($env:Computername)"} PS C:\> Invoke-PSRemoteOperation $file ``` @@ -122,4 +122,4 @@ Learn more about PowerShell: http://jdhitsolutions.com/blog/essential-powershell [New-PSRemoteOperation](./New-PSRemoteOperation) -[Get-PSRemoteOperationResult](./Get-PSRemoteOperationResult]) \ No newline at end of file +[Get-PSRemoteOperationResult](./Get-PSRemoteOperationResult]) diff --git a/docs/New-PSRemoteOperation.md b/docs/New-PSRemoteOperation.md index e8b8223..4ef4046 100644 --- a/docs/New-PSRemoteOperation.md +++ b/docs/New-PSRemoteOperation.md @@ -1,7 +1,7 @@ --- external help file: PSRemoteOperations-help.xml Module Name: PSRemoteOperations -online version: +online version: https://github.com/jdhitsolutions/PSRemoteOperations/blob/master/docs/New-PSRemoteOperation.md schema: 2.0.0 --- @@ -118,7 +118,7 @@ Enter the name or names of the computer where this command will execute. ```yaml Type: String[] Parameter Sets: (All) -Aliases: CN +Aliases: cn Required: True Position: 0 @@ -279,4 +279,4 @@ Learn more about PowerShell: http://jdhitsolutions.com/blog/essential-powershell [Register-PSRemoteOperationWatcher](./Register-PSRemoteOperationWatcher) -[Protect-CmsMessage](http://go.microsoft.com/fwlink/?LinkId=821716) \ No newline at end of file +[Protect-CmsMessage](http://go.microsoft.com/fwlink/?LinkId=821716) diff --git a/docs/Register-PSRemoteOperationWatcher.md b/docs/Register-PSRemoteOperationWatcher.md index 3986391..ca4e47b 100644 --- a/docs/Register-PSRemoteOperationWatcher.md +++ b/docs/Register-PSRemoteOperationWatcher.md @@ -1,7 +1,7 @@ --- external help file: PSRemoteOperations-help.xml Module Name: PSRemoteOperations -online version: +online version: https://github.com/jdhitsolutions/PSRemoteOperations/blob/master/docs/Register-PSRemoteOperationWatcher.md schema: 2.0.0 --- @@ -47,7 +47,7 @@ Create a scheduled job called Watch. This job is using the user defined defaults PS C:\> Unregister-Scheduledjob watch ``` -Use the PowerShell scheduledjob cmdlets to remove the watcher job. +Use the PowerShell scheduled job cmdlets to remove the watcher job. ## PARAMETERS @@ -203,4 +203,4 @@ Learn more about PowerShell: http://jdhitsolutions.com/blog/essential-powershell [New-PSRemoteOperation](./New-PSRemoteOperation) -[Register-ScheduledJob](http://go.microsoft.com/fwlink/?LinkId=821702) \ No newline at end of file +[Register-ScheduledJob](http://go.microsoft.com/fwlink/?LinkId=821702) diff --git a/docs/Wait-PSRemoteOperation.md b/docs/Wait-PSRemoteOperation.md new file mode 100644 index 0000000..3e64660 --- /dev/null +++ b/docs/Wait-PSRemoteOperation.md @@ -0,0 +1,137 @@ +--- +external help file: PSRemoteOperations-help.xml +Module Name: PSRemoteOperations +online version: https://github.com/jdhitsolutions/PSRemoteOperations/blob/master/docs/Wait-PSRemoteOperation.md +schema: 2.0.0 +--- + +# Wait-PSRemoteOperation + +## SYNOPSIS + +Wait for a PSRemoteOperation to complete. + +## SYNTAX + +### folder (Default) + +```yaml +Wait-PSRemoteOperation [-Path ] [-Computername ] [-Timeout ] [] +``` + +### file + +```yaml +Wait-PSRemoteOperation [[-FilePath] ] [-Timeout ] [] +``` + +## DESCRIPTION + +Most of the time remote operations are intended to be run asynchronously in much the same way that you use Start-Job. But there my be situations where you want to wait for a remote operation to complete. This command will pause your PowerShell prompt until the job completes or a timeout value has been exceeded. + +This command does not write any results to the pipeline. + +## EXAMPLES + +### Example 1 + +```powershell +PS C:\> New-PSRemoteOperation -scriptblock { get-process | export-clixml c:\shared\proc.xml} -computername SRV1 -passthru | Wait-PSRemoteOperation +``` + +This example will create a new PSRemote operation which passes the resulting .psd1 file to Wait-PSRemoteOperation. + +### Example 2 + +```powershell +PS C:\> Wait-PSRemoteOperation -computername SRV2 -timeout 30 +``` + +Watch the $PSRemoteOpPath folder for a job targeted to SRV2 but timeout waiting after 30 seconds. + +## PARAMETERS + +### -Computername + +Wait for results from a specific computer + +```yaml +Type: String +Parameter Sets: folder +Aliases: cn + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -FilePath + +Specify the path to a PSRemoteOperation file. + +```yaml +Type: String +Parameter Sets: file +Aliases: fullname + +Required: False +Position: 0 +Default value: None +Accept pipeline input: True (ByPropertyName, ByValue) +Accept wildcard characters: False +``` + +### -Path + +This should be the value of $PSRemoteOpPath + +```yaml +Type: String +Parameter Sets: folder +Aliases: + +Required: False +Position: Named +Default value: $PSRemoteOpPath +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Timeout + +Specify a timeout value in seconds between 5 and 300. + +```yaml +Type: Int32 +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### System.String + +## OUTPUTS + +### None + +## NOTES + +Learn more about PowerShell: +http://jdhitsolutions.com/blog/essential-powershell-resources/ + +## RELATED LINKS + +[New-PSRemoteOperation](./New-PSRemoteOperation.md) diff --git a/docs/about_PSRemoteOperations.md b/docs/about_PSRemoteOperations.md index b6c52cc..2748bcc 100644 --- a/docs/about_PSRemoteOperations.md +++ b/docs/about_PSRemoteOperations.md @@ -173,8 +173,8 @@ GitHub repository at: ## SEE ALSO - Protect-CMSMessage +`Protect-CMSMessage` ## KEYWORDS - RemoteOperation \ No newline at end of file +`RemoteOperation` diff --git a/en-us/PSRemoteOperations-help.xml b/en-us/PSRemoteOperations-help.xml index a3e09d6..665d796 100644 --- a/en-us/PSRemoteOperations-help.xml +++ b/en-us/PSRemoteOperations-help.xml @@ -27,7 +27,7 @@ $PSRemoteOpArchive - + Computername Enter a computername to filter on. @@ -66,7 +66,7 @@ $PSRemoteOpArchive - + Computername Enter a computername to filter on. @@ -134,6 +134,10 @@ Error : + + Online Version: + https://github.com/jdhitsolutions/PSRemoteOperations/blob/master/docs/Get-PSRemoteOperationResult.md + about_PSRemoteOperations @@ -290,7 +294,7 @@ Error : -------------------------- Example 1 -------------------------- - PS C:\> $file = Get-Childitem $PSRemoteOpPath\*.psd1 | where-object {$_.name -match "^$($env:Computername)"} + PS C:\> $file = Get-ChildItem $PSRemoteOpPath\*.psd1 | Where-Object {$_.name -match "^$($env:Computername)"} PS C:\> Invoke-PSRemoteOperation $file Assuming there is only a single file that starts with the local computer name, get the file and then call Invoke-PSRemoteOperation to invoke the scriptblock. Upon completion the file will be deleted and an archive copy added to the $PSRemoteOpArchive path. @@ -298,6 +302,10 @@ PS C:\> Invoke-PSRemoteOperation $file + + Online Version: + https://github.com/jdhitsolutions/PSRemoteOperations/blob/master/docs/Invoke-PSRemoteOperation.md + about_PSRemoteOperations @@ -328,7 +336,7 @@ PS C:\> Invoke-PSRemoteOperation $file New-PSRemoteOperation - + Computername Enter the name or names of the computer where this command will execute. @@ -436,7 +444,7 @@ PS C:\> Invoke-PSRemoteOperation $file New-PSRemoteOperation - + Computername Enter the name or names of the computer where this command will execute. @@ -556,7 +564,7 @@ PS C:\> Invoke-PSRemoteOperation $file None - + Computername Enter the name or names of the computer where this command will execute. @@ -752,6 +760,10 @@ PS C:\> New-PSRemoteOperation -Computername SRV4 -Scriptblock $sb -ArgumentLi + + Online Version: + https://github.com/jdhitsolutions/PSRemoteOperations/blob/master/docs/New-PSRemoteOperation.md + about_PSRemoteOperations @@ -1018,11 +1030,15 @@ Id Name JobTriggers Command Enabled -------------------------- Example 2 -------------------------- PS C:\> Unregister-Scheduledjob watch - Use the PowerShell scheduledjob cmdlets to remove the watcher job. + Use the PowerShell scheduled job cmdlets to remove the watcher job. + + Online Version: + https://github.com/jdhitsolutions/PSRemoteOperations/blob/master/docs/Register-PSRemoteOperationWatcher.md + about_PSRemoteOperations @@ -1041,4 +1057,187 @@ Id Name JobTriggers Command Enabled + + + Wait-PSRemoteOperation + Wait + PSRemoteOperation + + Wait for a PSRemoteOperation to complete. + + + + Most of the time remote operations are intended to be run asynchronously in much the same way that you use Start-Job. But there my be situations where you want to wait for a remote operation to complete. This command will pause your PowerShell prompt until the job completes or a timeout value has been exceeded. + This command does not write any results to the pipeline. + + + + Wait-PSRemoteOperation + + Computername + + Wait for results from a specific computer + + String + + String + + + None + + + Path + + This should be the value of $PSRemoteOpPath + + String + + String + + + $PSRemoteOpPath + + + Timeout + + Specify a timeout value in seconds between 5 and 300. + + Int32 + + Int32 + + + None + + + + Wait-PSRemoteOperation + + FilePath + + Specify the path to a PSRemoteOperation file. + + String + + String + + + None + + + Timeout + + Specify a timeout value in seconds between 5 and 300. + + Int32 + + Int32 + + + None + + + + + + Computername + + Wait for results from a specific computer + + String + + String + + + None + + + FilePath + + Specify the path to a PSRemoteOperation file. + + String + + String + + + None + + + Path + + This should be the value of $PSRemoteOpPath + + String + + String + + + $PSRemoteOpPath + + + Timeout + + Specify a timeout value in seconds between 5 and 300. + + Int32 + + Int32 + + + None + + + + + + System.String + + + + + + + + + + None + + + + + + + + + Learn more about PowerShell: http://jdhitsolutions.com/blog/essential-powershell-resources/ + + + + + -------------------------- Example 1 -------------------------- + PS C:\> New-PSRemoteOperation -scriptblock { get-process | export-clixml c:\shared\proc.xml} -computername SRV1 -passthru | Wait-PSRemoteOperation + + This example will create a new PSRemote operation which passes the resulting .psd1 file to Wait-PSRemoteOperation. + + + + -------------------------- Example 2 -------------------------- + PS C:\> Wait-PSRemoteOperation -computername SRV2 -timeout 30 + + Watch the $PSRemoteOpPath folder for a job targeted to SRV2 but timeout waiting after 30 seconds. + + + + + + Online Version: + https://github.com/jdhitsolutions/PSRemoteOperations/blob/master/docs/Wait-PSRemoteOperation.md + + + New-PSRemoteOperation + + + + \ No newline at end of file diff --git a/functions.ps1 b/functions.ps1 index de2a3e5..7d45829 100644 --- a/functions.ps1 +++ b/functions.ps1 @@ -9,7 +9,7 @@ Function New-PSRemoteOperation { Position = 0, Mandatory, HelpMessage = "Enter the name of the computer where this command will execute.")] - [Alias("CN")] + [Alias("cn")] [ValidateNotNullorEmpty()] [string[]]$Computername, @@ -35,14 +35,14 @@ Function New-PSRemoteOperation { [Parameter(HelpMessage = "A script block of commands to run prior to executing your script or scriptblock.")] [scriptblock]$Initialization, - [ValidateScript({Test-Path -Path $_})] + [ValidateScript( { Test-Path -Path $_ })] [Parameter(HelpMessage = "The folder where the remote operation file will be created.")] [string]$Path = $PSRemoteOpPath, [switch]$Passthru ) DynamicParam { - if (Get-command protect-cmsmessage -ea silentlycontinue) { + if (Get-Command Protect-CmsMessage -ea silentlycontinue) { $attributes = New-Object System.Management.Automation.ParameterAttribute $attributes.HelpMessage = "Specify one or more CMS message recipients." @@ -61,10 +61,12 @@ Function New-PSRemoteOperation { } Process { - foreach ($Computer in $Computername) { - Write-Verbose "Creating a remote operations file for $($computer.toUpper())" - #define a here string for the psd1 content - $out = @" + Write-Verbose "Using these PSBoundparameters" + $PSBoundParameters | Out-String | Write-Verbose + foreach ($Computer in $Computername) { + Write-Verbose "Creating a remote operations file for $($computer.toUpper())" + #define a here string for the psd1 content + $out = @" @{ CreatedOn = '$(hostname)' CreatedBy = '$(whoami)' @@ -73,49 +75,49 @@ Computername = '$($Computer.ToUpper())' "@ - if ($ArgumentList) { + if ($ArgumentList) { - $opArgs = Convert-HashTableToCode $ArgumentList - $out += "ArgumentList = $opargs" - $out += "`n" - } - - if ($Scriptblock) { - $out += "Scriptblock = '$scriptblock'" - $out += "`n" - } - else { - $out += "Filepath = '$Scriptpath'" - $out += "`n" - } - - if ($Initialization) { - $out += "Initialization = '$Initialization'" - $out += "`n" - } - $out += "}" - - $out | Write-Verbose - - #make the filename all lower case - $fname = "$($Computer.ToUpper())_$(New-GUID).psd1" - $outFile = Join-path -Path $Path -ChildPath $fname #.toLower() - - Write-Verbose "Creating datafile $outfile" - if ($PSCmdlet.ShouldProcess($outFile, "Creating PSRemote Operations File")) { + $opArgs = Convert-HashTableToCode $ArgumentList + $out += "ArgumentList = $opargs" + $out += "`n" + } - if ($To) { - Write-Verbose "Creating a CMS file" - Protect-CmsMessage -To $to -Content $out -OutFile $outFile + if ($Scriptblock) { + $out += "Scriptblock = '$scriptblock'" + $out += "`n" } else { - $out | Out-File -FilePath $outFile -force -Encoding ascii + $out += "Filepath = '$Scriptpath'" + $out += "`n" } - if ($Passthru) { - Get-Item -path $outFile + + if ($Initialization) { + $out += "Initialization = '$Initialization'" + $out += "`n" } - } #if should process - } #foreach computer + $out += "}" + + $out | Write-Verbose + + #make the filename all lower case + $fname = "$($Computer.ToUpper())_$(New-GUID).psd1" + $outFile = Join-path -Path $Path -ChildPath $fname #.toLower() + + Write-Verbose "Creating datafile $outfile" + if ($PSCmdlet.ShouldProcess($outFile, "Creating PSRemote Operations File")) { + + if ($PSBoundParameters.ContainsKey("To")) { + Write-Verbose "Creating a CMS file" + Protect-CmsMessage -To $PSBoundParameters.Item("to") -Content $out -OutFile $outFile + } + else { + $out | Out-File -FilePath $outFile -force -Encoding ascii + } + if ($Passthru) { + Get-Item -path $outFile + } + } #if should process + } #foreach computer } #process End { Write-Verbose "Ending $($MyInvocation.MyCommand)" @@ -129,17 +131,19 @@ Function Invoke-PSRemoteOperation { [alias('iro')] Param( - [Parameter(Position = 0, Mandatory, + [Parameter( + Position = 0, + Mandatory, HelpMessage = "Enter the path of a remote operation .psd1 file", ValueFromPipelineByPropertyName )] [ValidatePattern("\.psd1$")] - [ValidateScript( {Test-Path -Path $_})] + [ValidateScript( { Test-Path -Path $_ })] [Alias("pspath")] [string]$Path, [Parameter(HelpMessage = "Enter the path for the archived .psd1 file")] - [ValidateScript( {Test-Path -Path $_})] + [ValidateScript( { Test-Path -Path $_ })] [string]$ArchivePath = $PSRemoteOpArchive ) @@ -195,7 +199,7 @@ Function Invoke-PSRemoteOperation { #arguments must be entered as a hashtable Write-Verbose "Adding Parameters" $actionParams = $in.ArgumentList - write-verbose ($actionParams | Out-String) + Write-Verbose ($actionParams | Out-String) } if ($PSCmdlet.ShouldProcess($cPath)) { @@ -205,13 +209,13 @@ Function Invoke-PSRemoteOperation { if ($in.Initialization) { Write-Verbose "Adding initialization" $init = [scriptblock]::Create($in.Initialization) - $psrunspace.AddScript($init) | Out-Null + [void]$psrunspace.AddScript($init) } Write-Verbose "Adding action" - $psrunspace.Addscript($action) | Out-Null + [void]$psrunspace.Addscript($action) if ($actionParams) { - $psrunspace.AddParameters($actionParams) | Out-Null + [void]$psrunspace.AddParameters($actionParams) } Write-Verbose ($psrunspace.Commands.commands | Out-String) @@ -233,7 +237,7 @@ Function Invoke-PSRemoteOperation { "@ #append the result data to the data file. - ($Raw | Select-Object -skip 1 | Select-Object -SkipLast 1 ).Foreach( {$resultData += "$_`n"}) + ($Raw | Select-Object -skip 1 | Select-Object -SkipLast 1 ).Foreach( { $resultData += "$_`n" }) $resultData += "Completed = '$completed'`n" #replace any variables in the errormessage with escaped literals @@ -275,10 +279,17 @@ Function Get-PSRemoteOperationResult { [alias('gro')] Param( - [Parameter(Position = 0, HelpMessage = "Enter a computername to filter on.")] + [Parameter( + Position = 0, + HelpMessage = "Enter a computername to filter on." + )] + [Alias("cn")] [string]$Computername, - [Parameter(Position = 1, HelpMessage = "Enter the path to the archive folder.")] - [ValidateScript( {Test-Path -path $_})] + [Parameter( + Position = 1, + HelpMessage = "Enter the path to the archive folder." + )] + [ValidateScript( { Test-Path -path $_ })] [Alias("path")] [string]$ArchivePath = $PSRemoteOpArchive, [Alias("Last")] @@ -295,7 +306,7 @@ Function Get-PSRemoteOperationResult { $filter = "*.psd1" } - Write-verbose "Filtering for $filter" + Write-Verbose "Filtering for $filter" $data = Get-ChildItem -Path $ArchivePath -filter $filter | Sort-Object -Property LastWriteTime -Descending if ($Newest -gt 0) { @@ -306,8 +317,8 @@ Function Get-PSRemoteOperationResult { Write-Verbose "Processing $($file.fullname)" #Test if file is CMS protected Try { - write-Verbose "Testing for CMS Message" - Get-CmsMessage -Path $file.Fullname -ErrorAction Stop | Out-Null + Write-Verbose "Testing for CMS Message" + $null = Get-CmsMessage -Path $file.Fullname -ErrorAction Stop $hash = Unprotect-CmsMessage -Path $file.fullname | Out-String | Convert-HashtableString } Catch { @@ -321,4 +332,79 @@ Function Get-PSRemoteOperationResult { } Write-Verbose "Ending $($myinvocation.MyCommand)" -} #end PSGet-RemoteOperationResult \ No newline at end of file +} #end PSGet-RemoteOperationResult + +Function Wait-PSRemoteOperation { + [cmdletbinding(DefaultParameterSetName = "folder")] + [Outputtype("None")] + Param( + [Parameter( + Position = 0, + ValueFromPipeline, + ValueFromPipelineByPropertyName, + HelpMessage = "Specify the path to a PSRemoteOperation file.", + ParameterSetName = "file" + )] + [alias("fullname")] + [ValidateNotNullOrEmpty()] + [string]$FilePath, + + [Parameter(ParameterSetName = "folder")] + [ValidateNotNullOrEmpty()] + [string]$Path = $PSRemoteOpPath, + + [Parameter( + ParameterSetName = "folder", + HelpMessage = "Wait for results from a specific computer" + )] + [ValidateNotNullOrEmpty()] + [alias("cn")] + [string]$Computername, + + [Parameter(HelpMessage = "Specify a timeout value in seconds between 5 and 300.")] + [ValidateRange(5, 300)] + [int32]$Timeout + ) + Begin { + Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)" + } #begin + + Process { + Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Using these PSBoundparameters" + $PSBoundParameters | Out-String | Write-Verbose + + Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Using parameter set $($pscmdlet.ParameterSetName)" + + if ($pscmdlet.ParameterSetName -eq 'file') { + $target = $FilePath + } + else { + if ($Computername) { + $target = "$PSRemoteOpPath\$($computername)_*.psd1" + } + else { + $target = "$PSremoteOpPath\*.psd1" + } + } + Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Watching $target" + if ($Timeout) { + Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Waiting $Timeout seconds" + } + $timer = 0 + do { + Start-Sleep -Seconds 1 + $timer++ + if ($timeout -AND ($timer -gt $Timeout)) { + Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Timeout exceeded" + Break + } + } while (Test-Path -path $Target ) + Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Total waiting time $timer seconds." + } #process + + End { + Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)" + + } #end + +} #close Wait-PSRemoteOperation \ No newline at end of file diff --git a/license.txt b/license.txt index 9598533..97ba7d2 100644 --- a/license.txt +++ b/license.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 JDH Information Technology Solutions, Inc. +Copyright (c) 2018-2019 JDH Information Technology Solutions, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/private.ps1 b/private.ps1 index 7246187..466b081 100644 --- a/private.ps1 +++ b/private.ps1 @@ -17,7 +17,7 @@ Function Convert-HashtableString { $tokens = $null $err = $null $ast = [System.Management.Automation.Language.Parser]::ParseInput($Text, [ref]$tokens, [ref]$err) - $data = $ast.find( {$args[0] -is [System.Management.Automation.Language.HashtableAst]}, $true) + $data = $ast.find( { $args[0] -is [System.Management.Automation.Language.HashtableAst] }, $true) if ($err) { Throw $err @@ -45,7 +45,6 @@ Function Convert-HashTableToCode { Begin { Write-Verbose "Starting $($myinvocation.mycommand)" - } Process { Write-Verbose "Processing a hashtable with $($hashtable.keys.count) keys" @@ -60,12 +59,12 @@ Function Convert-HashTableToCode { Write-Verbose "Testing type $($_.value.gettype().name) for $($_.key)" #determine if the value needs to be enclosed in quotes if ($_.value.gettype().name -match "Int|double") { - write-Verbose "..is an numeric" + Write-Verbose "..is an numeric" $value = $_.value } elseif ($_.value -is [array]) { #assuming all the members of the array are of the same type - write-Verbose "..is an array" + Write-Verbose "..is an array" #test if an array of numbers otherwise treat as strings if ($_.value[0].Gettype().name -match "int|double") { $value = $_.value -join ',' @@ -79,7 +78,7 @@ Function Convert-HashTableToCode { $value = "$($nested)" } else { - write-Verbose "..defaulting as a string" + Write-Verbose "..defaulting as a string" $value = "'$($_.value)'" } $tabcount = "`t" * $Indent @@ -90,7 +89,6 @@ Function Convert-HashTableToCode { $out += "$tabcount}`n" $out - } } #process diff --git a/windows/PSRemoteOperations.psm1 b/windows/PSRemoteOperations.psm1 index 559bf48..f9c33f1 100644 --- a/windows/PSRemoteOperations.psm1 +++ b/windows/PSRemoteOperations.psm1 @@ -1,5 +1,5 @@ -#region main code +#region main code for Windows platforms . $PSScriptRoot\..\functions.ps1 . $PSScriptRoot\..\private.ps1 diff --git a/windows/test/PSRemoteOperations.Tests.ps1 b/windows/Tests/PSRemoteOperations.Tests.ps1 similarity index 90% rename from windows/test/PSRemoteOperations.Tests.ps1 rename to windows/Tests/PSRemoteOperations.Tests.ps1 index 1f9cdba..9afccc4 100644 --- a/windows/test/PSRemoteOperations.Tests.ps1 +++ b/windows/Tests/PSRemoteOperations.Tests.ps1 @@ -2,20 +2,24 @@ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringwithPlainText', '')] Param() -$moduleName = (Get-Item -path $PSScriptRoot).Parent.Name + +$moduleName = (($MyInvocation.mycommand).name -split "\.")[0] $ModuleManifestName = "$modulename.psd1" -$ModuleManifestPath = "$PSScriptRoot\..\$ModuleManifestName" +$ModuleManifestPath = "$PSScriptRoot\..\..\$ModuleManifestName" +If (Get-Module $modulename) { + Remove-module $moduleName +} import-module $ModuleManifestPath -Force - Describe $ModuleName { $myModule = Test-ModuleManifest -Path $ModuleManifestPath + Context Manifest { It 'Passes Test-ModuleManifest' { $myModule | Should Not BeNullOrEmpty } - It "Should have a root module" { - $myModule.RootModule | Should Not BeNullOrEmpty + It "Should NOT have a root module" { + $myModule.RootModule | Should BeNullOrEmpty } It "Contains exported commands" { $myModule.ExportedCommands | Should Not BeNullOrEmpty @@ -44,7 +48,8 @@ Describe $ModuleName { Context Exports { $exported = Get-Command -Module $ModuleName -CommandType Function $names = 'New-PSRemoteOperation', 'Invoke-PSRemoteOperation', 'Get-PSRemoteOperationResult', - 'Register-PSRemoteOperationWatcher' + 'Register-PSRemoteOperationWatcher', + 'Wait-PSRemoteOperation' It "Should export $($names.count) functions" { $names.Count -eq $exported.count | Should be $True @@ -73,31 +78,31 @@ Describe $ModuleName { } Context Structure { It "Should have a Docs folder" { - Get-Item $PSScriptRoot\..\docs | Should Be $True + Get-Item $PSScriptRoot\..\..\docs | Should Be $True } foreach ($cmd in $myModule.ExportedFunctions.keys) { It "Should have a markdown help file for $cmd" { - "$PSScriptRoot\..\docs\$cmd.md" | Should Exist + "$PSScriptRoot\..\..\docs\$cmd.md" | Should Exist } } It "Should have an external help file" { - "$PSScriptRoot\..\en-us\*.xml" | Should Exist - "$PSScriptRoot\..\en-us\*.txt" | Should Exist + "$PSScriptRoot\..\..\en-us\*.xml" | Should Exist + "$PSScriptRoot\..\..\en-us\*.txt" | Should Exist } It "Should have an about file" { - "$PSScriptRoot\..\docs\about_$ModuleName.md"| Should Exist + "$PSScriptRoot\..\..\docs\about_$ModuleName.md"| Should Exist } It "Should have a license file" { - "$PSScriptRoot\..\license.*" | Should Exist + "$PSScriptRoot\..\..\license.*" | Should Exist } It "Should have a changelog file" { - "$PSScriptRoot\..\changelog*" | Should Exist + "$PSScriptRoot\..\..\changelog*" | Should Exist } It "Should have a README.md file" { - "$PSScriptRoot\..\README.md" | Should Exist + "$PSScriptRoot\..\..\README.md" | Should Exist } } } -Tag module