PsiPS.AzureAutomation.psm1
#Requires -Modules @{ ModuleName = 'AzureRM.Automation'; ModuleVersion = '6.1.1' }; $script:ModuleRoot = $PSScriptRoot; $script:ModuleName = $MyInvocation.MyCommand.Name.Replace('.psm1', ''); $script:FeatureFlags = $MyInvocation.MyCommand.Module.PrivateData.FeatureFlags; $script:RequiredModules = $MyInvocation.MyCommand.Module.RequiredModules; $script:CurrentUserPrincipal = [Security.Principal.WindowsPrincipal]::new([Security.Principal.WindowsIdentity]::GetCurrent()); $script:IsUserAdmin = $CurrentUserPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator); $script:TabWidth = 4; $script:TabString = ' ' * $script:TabWidth; # Make sure to double up on any ` (backtick) characters that exist in the ASCII art. $script:ModuleASCIIArt = @" $script:TabString _____ _ _____ _____ $script:TabString| __ \ (_) __ \ / ____| $script:TabString| |__) |__ _| |__) | (___ $script:TabString| ___/ __| | ___/ \___ \ _ _ _ $script:TabString| | /\\__ \ | | ____) | /\ | | | | (_) $script:TabString|_|/ \___/_|_|_ |_____/___ / \ _ _| |_ ___ _ __ ___ __ _| |_ _ ___ _ __ $script:TabString / /\ \ |_ / | | | '__/ _ \ / /\ \| | | | __/ _ \| '_ `` _ \ / _`` | __| |/ _ \| '_ \ $script:TabString / ____ \ / /| |_| | | | __/ / ____ \ |_| | || (_) | | | | | | (_| | |_| | (_) | | | | $script:TabString/_/ \_\/___|\__,_|_| \___| /_/ \_\__,_|\__\___/|_| |_| |_|\__,_|\__|_|\___/|_| |_| "@; function Format-TimeSpan { [CmdletBinding()] [OutputType([string])] param ( [Parameter(Mandatory)] [TimeSpan] $Elapsed ); $StringBuilder = [Text.StringBuilder]::new(); if ($Elapsed.Hours -gt 0) { $StringBuilder.Append("$($Elapsed.Hours.ToString().PadLeft(2, '0'))h ") | Out-Null; } if ($Elapsed.Minutes -gt 0) { $StringBuilder.Append("$($Elapsed.Minutes.ToString().PadLeft(2, '0'))m ") | Out-Null; } if ($Elapsed.Seconds -gt 0 -and $Elapsed.Milliseconds -gt 0) { $StringBuilder.Append("$($Elapsed.Seconds.ToString().PadLeft(2, '0')).") | Out-Null; $StringBuilder.Append("$($Elapsed.Milliseconds.ToString().PadRight(4, '0'))s") | Out-Null; } elseif ($Elapsed.Seconds -eq 0 -and $Elapsed.Milliseconds -gt 0) { $StringBuilder.Append("00.$($Elapsed.Milliseconds.ToString().PadRight(4, '0'))s") | Out-Null; } elseif ($Elapsed.Seconds -gt 0 -and $Elapsed.Milliseconds -eq 0) { $StringBuilder.Append("$($Elapsed.Seconds.ToString().PadLeft(2, '0'))s") | Out-Null; } return $StringBuilder.ToString().Trim(); } function Get-ModuleInformation { [CmdletBinding()] param ( [Parameter()] [switch] $IncludeFunctions ); process { Write-Host $script:ModuleASCIIArt; $ModuleInfo = Get-Module -Name $script:ModuleName ` | Select-Object -Property Name, Version, Author, CompanyName, Description, ExportedFunctions; Write-Host "$($script:TabString)Name: $($ModuleInfo.Name) v$($ModuleInfo.Version)"; Write-Host "$($script:TabString)Author: $($ModuleInfo.Author), $($ModuleInfo.CompanyName)"; Write-Host "$($script:TabString)Description: $($ModuleInfo.Description)`n"; if ($IncludeFunctions.IsPresent) { $ExportedFunctions = ($ModuleInfo.ExportedFunctions.Keys | Sort-Object) -join "`n$($script:TabString * 2)"; Write-Host "$($script:TabString)Functions:`n$($script:TabString * 2)$ExportedFunctions"; } } } function Get-RunbookJobHistory { [CmdletBinding(DefaultParameterSetName = 'All')] [OutputType([Collections.Generic.List[Microsoft.Azure.Commands.Automation.Model.Job]], ParameterSetName = 'All')] [OutputType([Collections.Generic.List[Microsoft.Azure.Commands.Automation.Model.Job]], ParameterSetName = 'ByName')] [OutputType([Microsoft.Azure.Commands.Automation.Model.Job], ParameterSetName = 'ById')] param ( [Parameter(Mandatory, ParameterSetName = 'All')] [Parameter(Mandatory, ParameterSetName = 'ByName')] [Parameter(Mandatory, ParameterSetName = 'ById')] [string] $ResourceGroupName, [Parameter(Mandatory, ParameterSetName = 'All')] [Parameter(Mandatory, ParameterSetName = 'ByName')] [Parameter(Mandatory, ParameterSetName = 'ById')] [string] $AutomationAccountName, [Parameter(Mandatory, ParameterSetName = 'ByName')] [string] $RunbookName, [Parameter(Mandatory, ParameterSetName = 'ById')] [string] $JobId, [Parameter(ParameterSetName = 'All')] [Parameter(ParameterSetName = 'ByName')] [ValidateSet('Activating', 'Completed', 'Failed', 'Queued', 'Resuming', 'Running', 'Starting', 'Stopped', 'Stopping', 'Suspended', 'Suspending')] [string] $Status, [Parameter(ParameterSetName = 'All')] [Parameter(ParameterSetName = 'ByName')] [ValidateScript({ $ThirtyDaysAgo = [DateTimeOffset]::new((Get-Date).AddDays(-30)); if ($ThirtyDaysAgo -gt $_) { throw ("StartDateTime must be greater than, or equal to, 30 days ago.`n" + "Received: $($_.ToString()); Expected >=: $($ThirtyDaysAgo.ToString())"); return $false; } return $true; })] [DateTimeOffset] $StartDateTime, [Parameter(ParameterSetName = 'All')] [Parameter(ParameterSetName = 'ByName')] [ValidateScript({ $Now = [DateTimeOffset]::new((Get-Date)); if ($Now -lt $_) { throw ("EndDateTime must be less than, or equal to, now.`n" + "Received: $($_.ToString()); Expected >=: $($Now.ToString())"); return $false; } return $true; })] [DateTimeOffset] $EndDateTime, [Parameter(ParameterSetName = 'All')] [Parameter(ParameterSetName = 'ByName')] [switch] $FullDetails, [Parameter(ParameterSetName = 'All')] [Parameter(ParameterSetName = 'ByName')] [switch] $ShowProgress ); $RunbookJobs = [Collections.Generic.List[Microsoft.Azure.Commands.Automation.Model.Job]]::new(); $BaseParameterKeys = @('ResourceGroupName', 'AutomationAccountName'); $GetAzureRmAutomationJobParameters = @{ ResourceGroupName = $ResourceGroupName; AutomationAccountName = $AutomationAccountName; }; switch ($PSCmdlet.ParameterSetName) { 'ById' ` { $GetAzureRmAutomationJobParameters.Id = $JobId; $RunbookJobs = Get-AzureRmAutomationJob @GetAzureRmAutomationJobParameters; } 'ByName' ` { $GetAzureRmAutomationJobParameters.RunbookName = $RunbookName; if (-not [String]::IsNullOrEmpty($Status)) { $GetAzureRmAutomationJobParameters.Status = $Status; } if ($null -ne $StartDateTime) { $GetAzureRmAutomationJobParameters.StartTime = $StartDateTime; } if ($null -ne $EndDateTime) { $GetAzureRmAutomationJobParameters.EndTime = $EndDateTime; } $RunbookJobs = Get-AzureRmAutomationJob @GetAzureRmAutomationJobParameters; } Default ` { if (-not [String]::IsNullOrEmpty($Status)) { $GetAzureRmAutomationJobParameters.Status = $Status; } if ($null -ne $StartDateTime) { $GetAzureRmAutomationJobParameters.StartTime = $StartDateTime; } if ($null -ne $EndDateTime) { $GetAzureRmAutomationJobParameters.EndTime = $EndDateTime; } $RunbookJobs = Get-AzureRmAutomationJob @GetAzureRmAutomationJobParameters; } } if ($FullDetails.IsPresent) { if ($ShowProgress.IsPresent) { $WriteProgressParameters = @{ Activity = "Getting Runbook Job History Details..."; StatusMessage = { "Querying '$($RunbookJobs[$RunbookJobIndex].RunbookName)' (Id: $($RunbookJobs[$RunbookJobIndex].JobId.Guid))"; }; PercentComplete = { [Math]::Round((100 * ($RunbookJobIndex / $RunbookJobs.Count)), 2); }; }; } for ($RunbookJobIndex = 0; $RunbookJobIndex -lt $RunbookJobs.Count; $RunbookJobIndex++) { if ($ShowProgress.IsPresent) { Write-Progress ` -Activity $WriteProgressParameters.Activity ` -Status (& $WriteProgressParameters.StatusMessage) ` -PercentComplete (& $WriteProgressParameters.PercentComplete); } # TODO: Recurse this function instead of calling the base azurerm.automation cmdlet, again $GetAzureRmAutomationJobParameters.Keys.Where({ $BaseParameterKeys -notcontains $_ }) ` | ForEach-Object { $GetAzureRmAutomationJobParameters.Remove($_) }; $GetAzureRmAutomationJobParameters.Id = $RunbookJobs[$RunbookJobIndex].JobId; $RunbookJobs[$RunbookJobIndex] = Get-AzureRmAutomationJob @GetAzureRmAutomationJobParameters; } if ($ShowProgress.IsPresent) { Write-Progress ` -Activity $WriteProgressParameters.Activity ` -PercentComplete 100 -Completed; } } $RunbookJobs | ForEach-Object -Process ` { if ($null -ne $_.EndTime -and $null -ne $_.StartTime) { Add-Member -InputObject $_ ` -MemberType NoteProperty ` -Name 'RunTime' ` -Value ($_.EndTime - $_.StartTime); } }; return [Collections.Generic.List[Microsoft.Azure.Commands.Automation.Model.Job]] $RunbookJobs; } function Get-RunbookJobOutput { [CmdletBinding()] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $ResourceGroupName, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $AutomationAccountName, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $JobId, [Parameter()] [switch] $IncludeVerboseStreams, [Parameter()] [switch] $ShowProgress ); $RequestedStreams = @('Error', 'Output', 'Warning'); #region Parameter Splatting(s) $GetJobStreamParameters = @{ ResourceGroupName = $ResourceGroupName; AutomationAccountName = $AutomationAccountName; Id = $JobId; }; if ($ShowProgress.IsPresent) { $WriteProgressParameters = @{ Id = 1; Activity = 'Getting Job Output'; } } #endregion Parameter Splatting(s) $Stopwatch = [Diagnostics.Stopwatch]::StartNew(); $JobStreams = [Collections.Generic.List[Microsoft.Azure.Commands.Automation.Model.JobStream]]::new(); $JobStreamRecords = [Collections.Generic.List[Microsoft.Azure.Commands.Automation.Model.JobStreamRecord]]::new(); if ($IncludeVerboseStreams.IsPresent) { $RequestedStreams += 'Verbose'; $StatusMessage = 'Fetching all job streams...'; Write-Verbose $StatusMessage; if ($ShowProgress.IsPresent) { Write-Progress @WriteProgressParameters -Status $StatusMessage ` -CurrentOperation "Time Elapsed: $(Format-TimeSpan $Stopwatch.Elapsed)"; } $JobStreams_Temp = Get-AzureRmAutomationJobOutput ` @GetJobStreamParameters ` -Stream 'Any'; if ($null -ne $JobStreams_Temp -and $JobStreams_Temp.Count -gt 0) { $JobStreams.AddRange(([Microsoft.Azure.Commands.Automation.Model.JobStream[]] $JobStreams_Temp)); } } else { $TotalRequestedStreamCount = $RequestedStreams.Count; if ($ShowProgress.IsPresent) { $RequestedStreamsString = $RequestedStreams -Join ', '; $RequestedStreamsString = $RequestedStreamsString.Insert( $RequestedStreamsString.LastIndexOf(','), ' and'); $StatusMessage = "Fetching $RequestedStreamsString job streams..."; } for ($StreamIndex = 0; $StreamIndex -lt $TotalRequestedStreamCount; $StreamIndex++) { $OperationMessage = "Fetching $($RequestedStreams[$StreamIndex]) job streams..."; Write-Verbose $OperationMessage; if ($ShowProgress.IsPresent) { Write-Progress @WriteProgressParameters -Status $StatusMessage ` -CurrentOperation ("Time Elapsed: $(Format-TimeSpan $Stopwatch.Elapsed); " + "$($StreamIndex + 1) of ${TotalRequestedStreamCount}: $OperationMessage") ` -PercentComplete ([Math]::Round((($StreamIndex / $TotalRequestedStreamCount) * 100), 2)); } $JobStreams_Temp = Get-AzureRmAutomationJobOutput ` @GetJobStreamParameters -Stream $RequestedStreams[$StreamIndex]; if ($null -ne $JobStreams_Temp -and $JobStreams_Temp.Count -gt 0) { $JobStreams.AddRange(([Microsoft.Azure.Commands.Automation.Model.JobStream[]] $JobStreams_Temp)); } } } Remove-Variable 'JobStreams_Temp' -Scope 'Local' -Force -ErrorAction:SilentlyContinue; [GC]::Collect(); $TotalJobStreamCount = $JobStreams.Count; $StatusMessage = "Fetching $TotalJobStreamCount job stream records..."; Write-Verbose $StatusMessage; $GetJobStreamRecordParameters = $GetJobStreamParameters.Clone(); $GetJobStreamRecordParameters.JobId = $GetJobStreamRecordParameters.Id; $CurrentOperationMessage = [String]::Empty; $MaxTotalJobStreamCountStringLength = $TotalJobStreamCount.ToString().Length; for ($JobStreamIndex = 0; $JobStreamIndex -lt $TotalJobStreamCount; $JobStreamIndex++) { $GetJobStreamRecordParameters.Id = $JobStreams[$JobStreamIndex].StreamRecordId; $CurrentOperationMessage = ( "Time Elapsed: $(Format-TimeSpan $Stopwatch.Elapsed); " + "$(($JobStreamIndex + 1).ToString().PadLeft($MaxTotalJobStreamCountStringLength, '0')) " + "of ${TotalJobStreamCount}: Fetching job stream $($JobStreams[$JobStreamIndex].Type.ToLower()) " + "record Id: $($GetJobStreamRecordParameters.Id)" ); Write-Verbose $CurrentOperationMessage; if ($ShowProgress.IsPresent) { Write-Progress @WriteProgressParameters -ParentId ($WriteProgressParameters.Id + 1) ` -Status $StatusMessage -CurrentOperation $CurrentOperationMessage ` -PercentComplete ([Math]::Round((($JobStreamIndex / $TotalJobStreamCount) * 100), 2)); } $JobStreamRecords_Temp = Get-AzureRmAutomationJobOutputRecord ` @GetJobStreamRecordParameters; if ($null -ne $JobStreamRecords_Temp) { $JobStreamRecords.Add($JobStreamRecords_Temp); } Remove-Variable 'JobStreamRecords_Temp' -Scope 'Local' -Force -ErrorAction:SilentlyContinue; [GC]::Collect(); } if ($ShowProgress.IsPresent) { # Write-Progress @WriteProgressParameters ` # -ParentId ($WriteProgressParameters.Id + 1) ` # -PercentComplete 100 -Completed; Write-Progress @WriteProgressParameters ` -PercentComplete 100 -Completed; } $RequestedJobStreamRecords = @{ }; foreach ($RequestedStream in $RequestedStreams) { $RequestedJobStreamRecords.$RequestedStream = $JobStreamRecords.Where({ $_.Type -eq $RequestedStream }); } Remove-Variable 'JobStreams' -Scope 'Local' -Force -ErrorAction:SilentlyContinue; Remove-Variable 'JobStreamRecords' -Scope 'Local' -Force -ErrorAction:SilentlyContinue; [GC]::Collect(); return (New-Object -TypeName 'PSCustomObject' -Property $RequestedJobStreamRecords); } function Start-RunbookJob { [CmdletBinding()] [OutputType([Microsoft.Azure.Commands.Automation.Model.Job])] param ( [Parameter(Mandatory)] [string] $ResourceGroupName, [Parameter(Mandatory)] [string] $AutomationAccountName, [Parameter(Mandatory)] [string] $RunbookName, [Parameter()] [string] $HybridWorkerName, [Parameter()] [Hashtable] $RunbookArguments, [Parameter()] [int] $PollingInterval = 5, [Parameter()] [switch] $ShowProgress ); #region Parameter Splatting(s) $GenericParameters = @{ ResourceGroupName = $ResourceGroupName; AutomationAccountName = $AutomationAccountName; }; $GetRunbookJobHistoryParameters = $GenericParameters.Clone(); $GetRunbookJobHistoryParameters.RunbookName = $RunbookName; $StartRunbookJobParameters = $GenericParameters.Clone(); $StartRunbookJobParameters.Name = $RunbookName; if (-not [String]::IsNullOrEmpty($HybridWorkerName)) { $StartRunbookJobParameters.RunOn = $HybridWorkerName; } if ($null -ne $RunbookArguments) { $StartRunbookJobParameters.Parameters = $RunbookArguments; } #endregion Parameter Splatting(s) $JobHistory = Get-RunbookJobHistory ` @GetRunbookJobHistoryParameters -Status 'Completed'; if ($null -ne $JobHistory) { $AverageRunTime = ($JobHistory.RunTime ` | Measure-Object -Average -Property TotalSeconds).Average; Write-Verbose ("Average RunTime for '$RunbookName'" + "is $([Math]::Round($AverageRunTime, 2))"); } if ($ShowProgress.IsPresent) { $RemainingProgress = 100; $IndefiniteDenominator = 1.1; $WriteProgressParameters = @{ Activity = "Executing Runbook: $RunbookName"; StatusMessage = { ("JobId: $($RunbookJob.JobId); " + "Time Elapsed: $(Format-TimeSpan $Stopwatch.Elapsed)") }; PercentComplete = { (100 - $RemainingProgress) }; }; } $Stopwatch = [Diagnostics.Stopwatch]::StartNew(); $StartDateTime = [DateTimeOffset]::new((Get-Date).AddMinutes(-1)); $RunbookJob = Start-AzureRmAutomationRunbook @StartRunbookJobParameters; do { Start-Sleep -Seconds $PollingInterval; if ($null -eq $RunbookJob.JobId) { $RetryCount = 1; $MaxRetryCount = 5 do { Write-Warning ("Failed to query for the current" + "Job. Retrying ($RetryIndex of $MaxRetryCount)."); $RunbookJob = Get-RunbookJobHistory ` @GetRunbookJobHistoryParameters ` -StartDateTime $StartDateTime ` | Sort-Object CreationTime -Descending ` | Select-Object -First 1; $RetryCount++; } while ( $null -ne $RunbookJob -or $RetryCount -le $MaxRetryCount ); if ($null -eq $RunbookJob) { Write-Error ("An error has occurred, while" + "retrieving the runbook job from Azure."); return $null; } } if ($ShowProgress.IsPresent) { Write-Progress ` -Activity $WriteProgressParameters.Activity ` -Status (& $WriteProgressParameters.StatusMessage) ` -PercentComplete (& $WriteProgressParameters.PercentComplete); $RemainingProgress /= $IndefiniteDenominator; } $RunbookJob = Get-RunbookJobHistory @GenericParameters ` -JobId $RunbookJob.JobId; } while (@('Suspended', 'Completed', 'Failed', 'Stopped') -notcontains $RunbookJob.Status) $Stopwatch.Stop(); if ($ShowProgress.IsPresent) { Write-Progress ` -Activity $WriteProgressParameters.Activity ` -PercentComplete 100 -Completed; } return $RunbookJob; } function Publish-Runbook { [CmdletBinding( SupportsShouldProcess, ConfirmImpact = 'Medium' )] [OutputType([Microsoft.Azure.Commands.Automation.Model.Runbook])] param ( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $ResourceGroupName, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $AutomationAccountName, [Parameter(Mandatory)] [ValidateScript({ if (-not ($_ | Test-Path)) { throw 'File does not exist'; return $false; } elseif ($_ -notmatch "(\.ps1)") { throw 'If passing a file to the Path argument, it must be a powershell script (ps1).'; return $false; } else { return $true; } })] [IO.FileInfo] $Path, [Parameter()] [string] $Name, [Parameter()] [string] $Description, [Parameter()] [ValidateSet('PowerShell', 'PowerShellWorkflow')] [string] $Type = 'PowerShell', [Parameter()] [switch] $LogVerbose, [Parameter()] [switch] $LogProgress, [Parameter()] [switch] $AsDraft, [Parameter()] [HashTable] $Tags, [Parameter()] [switch] $Force ); begin { if (-not $PSBoundParameters.ContainsKey('Confirm')) { $ConfirmPreference = $PSCmdlet.SessionState.PSVariable.GetValue('ConfirmPreference'); } if (-not $PSBoundParameters.ContainsKey('WhatIf')) { $WhatIfPreference = $PSCmdlet.SessionState.PSVariable.GetValue('WhatIfPreference'); } } process { $ImportAzureRmAutomationRunbookParameters = @{ ResourceGroupName = $ResourceGroupName; AutomationAccountName = $AutomationAccountName; Path = $Path.FullName; Name = $Name; Type = $Type; LogVerbose = $LogVerbose.IsPresent; LogProgress = $LogProgress.IsPresent; Published = (-not $AsDraft.IsPresent); Force = $Force.IsPresent; }; if ([String]::IsNullOrEmpty($Name)) { $ImportAzureRmAutomationRunbookParameters.Name = $Path.BaseName; } if (-not [String]::IsNullOrEmpty($Description)) { $ImportAzureRmAutomationRunbookParameters.Description = $Description; } if ($null -ne $Tags) { $ImportAzureRmAutomationRunbookParameters.Tags = $Tags.Clone(); } return ( Import-AzureRmAutomationRunbook @ImportAzureRmAutomationRunbookParameters ` -WhatIf:(-not ($Force -or $PSCmdlet.ShouldProcess("ShouldProcess?"))) ); } } |