Create-azUpdatePatchDeploymentList.ps1
<#PSScriptInfo
.VERSION 1.3 .GUID 848f7086-8a09-4f63-a7a9-f0c9b2d612b9 .AUTHOR jbritt@microsoft.com .COMPANYNAME Microsoft .COPYRIGHT Microsoft .TAGS .LICENSEURI .PROJECTURI https://aka.ms/JimBritt .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES July 30, 2021 1.3 Added additional logic to create a configuration but disable the schedule. Again, a Huge Thank you to Manjeet Bavage (Consultant at Microsoft) for leaning in and providing some additional testing and recommendations on improvements #> <# .SYNOPSIS A script to programmatically create an Azure Update Management Patch list object based on a pre-prod environment Pre-Prod --> PROD scenario Note This script currently leverages the Az cmdlets https://www.powershellgallery.com/packages/Create-AzUpdatePatchDeploymentList/ .DESCRIPTION This script meant to help you automate the creation of a production Azure Update Management Deployment Schedule based on KBIDs needed in a pre-prod environment. You take a source subscription / Azure Automation Account / Linked Log Analytics Workspace and query for existing needed updates according to your classification requirements as well as the target operating system (Windows or Linux). The Production configuration contains only the KBIDs of the source pre-prod environment that are required for the monthly patching. .PARAMETER SourceSubscriptionId The subscriptionID of the Azure Subscription where your reference Azure Update Configuration is located .PARAMETER TargetSubscriptionId The subscriptionID of the Azure Subscription where your target Azure Update Configuration is to be created .PARAMETER AAAcountName The Azure Automation Account Name to be referenced in the source subscription that supports Azure Update Management .PARAMETER AAResourceGroupName The Azure Automation ResourceGroupName in the source subscription that supports Azure Update Management .PARAMETER WSID The log Analytics Workspace ID (CustomerID) in the Source Subscription used to gather your needed KBIDs .PARAMETER queryScope Array of scopes to include in the scope for the query based update management object option Ex: -queryScope "/subscriptions/22e2445a-0984-4fa5-86a4-0280d76c4b2c/resourceGroups/resourceGroupName,/subscriptions/32e2445a-0984-4fa5-86a4-0280d76c4b2d/" Note: Defaults to target subscription if no value is provided on parameter .PARAMETER queryLocation What region(s) include in the scope for the query based update management object option Ex: "eastasia","southeastasia","centralus","eastus","eastus2","westus","northcentralus" .PARAMETER tags Tags levered for query based on tags in target subscription to deploy the patches to Ex: @{PatchWindow = "SaturdayMorning";ENVIRONMENT = "PROD"} .PARAMETER RebootOptions Parameter leveraged to determine reboot behavior for the updates (validated parameter set) .PARAMETER ApplicablePatchesQuery Log Analytics Search Query to scope reference set of machines to build a KBLIST from Note: Needs to be in one line instead of multiple! Example: 'Update | where OSType=="Linux" and Optional==false | where Classification has "Unclassified" or Classification has "Critical" or Classification has "Security" or Classification has "Others" | summarize arg_max(TimeGenerated, *) by Computer,SourceComputerId,UpdateID, ApprovalSource, KBID | summarize hint.strategy=partitioned arg_max(TimeGenerated, *) by KBID| where UpdateState=~"Needed" and Approved!=false| project KBID, Title' .PARAMETER KBLIST The list of approved (certified) KBIDs that you want to include in your target Azure Update Configuration Object Ex: "KB12345, KB21234" .PARAMETER SavedSearchID A saved search ID from Log Analytics that you can use to run a saved query (pulls the query details from the SavedSearchID) Example Script to leverage for aquiring the saved Search IDs: https://www.powershellgallery.com/packages/Invoke-AzOperationalInsightsQueryExport .PARAMETER DaysOfWeek What days of the week you want to execute the patch on. Defaults to every day Ex: "Monday,"Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday" .PARAMETER SoftwareUpdateSchedName Name of the Software Update Schedule in target subscription you will create .PARAMETER SoftwareUpdateScheduleDescr Description for the Software Update Schedule in target subscription .PARAMETER PreScript Prescript to run during the patch run (leveraged for the target Azure Update Configuration object) .PARAMETER PreScriptParams PreScript parameters to run during the patch run (leveraged for the target Azure Update Configuration object) Needs to be in the form of a hashtable Ex: @{Drive = "C";LOG = "Results.txt"} .PARAMETER PostScript Postscript to run during the patch run (leveraged for the target Azure Update Configuration object) .PARAMETER PostScriptParams PostScript parameters to run during the patch run (leveraged for the target Azure Update Configuration object) Needs to be in the form of a hashtable Ex: @{Drive = "C";LOG = "Results.txt"} .PARAMETER ClassificationList List of Classifications of updates to include Ex: "Critical,Security,UpdateRollup" .PARAMETER duration The number of minutes you want the update to run related to timespan (Default is 120 mins) .PARAMETER TargetOS Determines which OS is update configuration is meant for in the Target subscription.(Windows or Linux) .PARAMETER StartTime When should the Azure Update Management Schedule be intially created to start (needs to be at least 5 mins later than current run time of script) .PARAMETER ExpiryTime When should the schedule expire for the Azure Update Management configuration .PARAMETER Force When provided, this will allow the script to run silently without prompting All Parameters need to have proper values to succeed .PARAMETER ScheduleDisabled When provided, this will create the configuration but disable the schedule Can be re-enabled in the portal under schedules or via PowerShell .EXAMPLE .\Create-azUpdatePatchDeploymentList.ps1 -SoftwareUpdateScheduleName "Linux Update" ` -TargetOS Linux ` -StartTime '03/15/2020' ` -AAAcountName AzureAutoEast ` -AAResourceGroupName OI-Default-East-US ` -WSID b571a98c-6828-4045-bb5f-857543f2a9e3 ` -queryFilterOperator any ` -DaysOfWeek "Sunday","Monday" ` -duration (New-TimeSpan -Hours 4) ` -WeekInterval 3 Will use resource group and workspace name as your target workspace within specified subscription and will prompt for other details. This update schedule will only run on Sunday and Monday with a duration of 4 hours every 3 weeks starting on March 15, 20020 .EXAMPLE .\Create-azUpdatePatchDeploymentList.ps1 -SoftwareUpdateScheduleName "Linux Update" ` -TargetOS Linux ` -tags @{PatchWindow = "SaturdayMorning";ENV = "PROD"} ` -StartTime '03/15/2020' ` -AAAcountName AzureAutoEast ` -AAResourceGroupName OI-Default-East-US ` -WSID b571a98c-6828-4045-bb5f-857543f2a9e3 ` -queryFilterOperator any ` -DaysOfWeek "Sunday","Monday" ` -duration (New-TimeSpan -Hours 4) ` -WeekInterval 3 This example adds the option of specifying tags to add to your Azure Query for targeting .\Create-azUpdatePatchDeploymentList.ps1 -SoftwareUpdateScheduleName "Linux Update" ` -TargetOS Linux ` -tags @{PatchWindow = "SaturdayMorning";ENV = "PROD"} ` -StartTime '03/15/2020' ` -AAAcountName AzureAutoEast ` -AAResourceGroupName OI-Default-East-US ` -WSID b571a98c-6828-4045-bb5f-857543f2a9e3 ` -queryFilterOperator any ` -DaysOfWeek "Sunday","Monday" ` -duration (New-TimeSpan -Hours 4) ` -WeekInterval 3 ` -ScheduleDisabled This example adds the option of creating the configuration but disabling the schedule .EXAMPLE .\Create-AzUpdatePatchDeploymentList.ps1 -SoftwareUpdateScheduleName "Windows Update" ` -TargetOS Windows ` -StartTime '4/15/2020' ` -AAAcountName AzureAutoEast ` -AAResourceGroupName OI-Default-East-US ` -WSID b571a98c-6828-4045-bb5f-857543f2a9e3 ` -queryFilterOperator all ` -DaysOfWeek "Sunday","Monday" ` -duration (New-TimeSpan -Hours 4) ` -WeekInterval 3 ` -SourceSubscriptionID c627c0bd-814f-4671-9bda-c8476ccb6abc ` -TargetSubscriptionID c627c0bd-814f-4671-9bda-c8476ccb6abc ` -ExpiryTime 5/1/2020 ` -KBLIST "KB12345","KB34567" ` -queryLocation "eastasia","southeastasia","centralus","eastus","eastus2","westus" ` -ClassificationList "Unclassified" ` -force This example has all needed details to run silently (including force switch). Some details are assumed such as scope for the deployment coverage This example also shows an expiration date (default is no expiration) .EXAMPLE .\Create-AzUpdatePatchDeploymentList.ps1 -SoftwareUpdateScheduleName "Windows Update" ` -TargetOS Windows ` -StartTime '4/15/2020' ` -AAAcountName AzureAutoEast ` -AAResourceGroupName OI-Default-East-US ` -WSID b571a98c-6828-4045-bb5f-857543f2a9e3 ` -queryFilterOperator all ` -DaysOfWeek "Sunday","Monday" ` -duration (New-TimeSpan -Hours 4) ` -WeekInterval 3 ` -SourceSubscriptionID c627c0bd-814f-4671-9bda-c8476ccb6abc ` -TargetSubscriptionID c627c0bd-814f-4671-9bda-c8476ccb6abc ` -ExpiryTime 5/1/2020 ` -KBLIST "KB12345","KB34567" ` -queryLocation "eastasia","southeastasia","centralus","eastus","eastus2","westus" ` -ClassificationList "Unclassified" ` -PreScript "UpdateManagement-TurnOnVMs" ` -PostScript "UpdateManagement-TurnOffVMs" ` -force This example provides the additional option of a pre and post script from Azure Automation. This example assumes these runbooks have already been downloaded from the Script Center and installed in your Azure Automation Account See: https://gallery.technet.microsoft.com/scriptcenter/Update-Management-Turn-On-ffadfc26 and https://docs.microsoft.com/en-us/azure/automation/pre-post-scripts for more information. .NOTES AUTHOR: Jim Britt Senior Program Manager - Azure CXP API LASTEDIT: July 30, 2021 1.3 Added additional logic to create a configuration but disable the schedule. Again, a Huge Thank you to Manjeet Bavage (Consultant at Microsoft) for leaning in and providing some additional testing and recommendations on improvements July 7, 2021 1.2 Resolved some logic issues related to Linux Category Assignments A Huge Thank you to Manjeet Bavage (Consultant at Microsoft) for leaning in and providing some additional testing and updates to help resolve a breaking bug for Linux. Jun 17, 2020 v1.1 Updates to Description March 29, 2020 v1.0 Initial .LINK This script posted to and discussed at the following locations:PowerShell Gallery & GitHub https://aka.ms/JimBritt https://github.com/JimGBritt/Azure-Update-Management https://www.powershellgallery.com/packages/AUM-PatchDeployment-RunBook-Wrapper #> param ( # SubscriptionId of where your SOURCELog Analytics Workspace is to get saved SearchID or leveraging query param (optional) [Parameter(Mandatory=$false)] [guid]$SourceSubscriptionID, # Target Subscription where the Azure Automation Account is located for the update management configuration [Parameter(Mandatory=$false)] [guid]$TargetSubscriptionID, # Resource Group name for Azure Automation Account [Parameter(Mandatory=$false)] [string]$AAResourceGroupName, # Azure Automation Account Name for the target subscription / update management configuration [Parameter(Mandatory=$false)] [string]$AAAcountName, # List of Classifications of updates to include # Example values # -ClassificationList "Critical,Security,UpdateRollup" [Parameter(Mandatory=$false)] [array]$ClassificationList, # List of approved KBs (if not collected from reference workspace) ## NEED TO ADD LOGIC FOR THIS if provided # Example values # -KBLIST "KB12345, KB21234" [Parameter(Mandatory=$false)] [array]$KBLIST, # Array of scopes to include in the scope for the update management object # Example values # -queryScope "/subscriptions/22e2445a-0984-4fa5-86a4-0280d76c4b2c/resourceGroups/resourceGroupName,/subscriptions/32e2445a-0984-4fa5-86a4-0280d76c4b2d/" [Parameter(Mandatory=$false)] [array]$queryScope, # Regions to narrow in the scope for resources to update # example values # -querylocation "EastUS", "WestUS"" [Parameter(Mandatory=$false)] [array]$queryLocation, # Used for the Azure Query Logic to determine if all or any of the filters will evaluate as true if met [Parameter(Mandatory=$false)] [ValidateSet('Any','All')] [string[]] $queryFilterOperator= 'All', # How you want to reboot. note: reboot only conincides with undefined in Linux classification option. [Parameter(Mandatory=$false)] [ValidateSet('IfRequired','Never', 'Always', 'RebootOnly')] [string[]] $RebootOptions = 'IfRequired', # Hashtable for tag based query # Needs to be in the format of # example: -tags @{PatchWindow = "SaturdayMorning";ENV = "PROD"} [Parameter(Mandatory=$false)] [hashtable]$tags, # Workspace ID (optional) # This is the actual Workspace ID (client ID) of the Log Analytics workspace # If ommitted, you will be promted to select a workspace in the source subscription [Parameter(Mandatory=$false)] [string]$WSID, # Log Analytics SavedSearchID (use ad hoc query if preferred) # Example Script to leverage for aquiring the saved Search IDs: https://www.powershellgallery.com/packages/Invoke-AzOperationalInsightsQueryExport [Parameter(Mandatory=$false)] [string]$SavedSearchID, # Ad hoc query in lieu of SavedSearchID [Parameter(Mandatory=$false)] [string]$ApplicablePatchesQuery, # Defaults to 5 days from now but can be overridden from cmdline [Parameter(Mandatory=$false)] [System.DateTimeOffset]$StartTime= ((Get-Date) + (5d)), # When do you want to patch according to set schedule # Array set of days for example # -DaysOfWeek "Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday" [Parameter(Mandatory=$false)] [array]$DaysOfWeek=('Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'), # Defaults to every week for interval - can override from cmdline [Parameter(Mandatory=$false)] [string]$WeekInterval = 1, # How long will the script run # Defaults to 2 hours [Parameter(Mandatory=$false)] [System.TimeSpan]$duration = (New-TimeSpan -Hours 2), # Software Update Schedule Name default - use parameter to define [Parameter(Mandatory=$false)] [string]$SoftwareUpdateScheduleName = "SoftwareUpdateSchedule", # Description of Software Update schedule - defaults to "Software Update". Use parameter to override [Parameter(Mandatory=$false)] [string]$SoftwareUpdateScheduleDescr = "Software Update", # Which OS to target - Windows is default [Parameter(Mandatory=$false)] [ValidateSet('Windows', 'Linux')] [string[]] $TargetOS = 'Windows', # Pre and Post Scripts for Azure Update Management Configuration # Example -PreScript "UpdateManagement-TurnOnVMs" [Parameter(Mandatory=$false)] [string]$PreScript, # Hashtable for prescript parameters # Needs to be in the format of # example: -PreScriptParams @{Drive = "C";LOG = "Results.txt"} [Parameter(Mandatory=$false)] [hashtable]$PreScriptParams, # Example -PostScript "UpdateManagement-TurnOffVMs" [Parameter(Mandatory=$false)] [string]$PostScript, # Hashtable for prescript parameters # Needs to be in the format of # example: -PostScriptParams @{Drive = "C";LOG = "Results.txt"} [Parameter(Mandatory=$false)] [hashtable]$PostScriptParams, # Expiration of the Azure Update Management Schedule [Parameter(Mandatory=$false)] [System.DateTimeOffset]$ExpiryTime, [switch]$force = $false, # Create the Azure Update Management Schedule but disable it [Parameter(Mandatory=$false)] [switch]$ScheduleDisabled = $false ) function Add-IndexNumberToArray ( [Parameter(Mandatory=$True)] [array]$array ) { for($i=0; $i -lt $array.Count; $i++) { Add-Member -InputObject $array[$i] -Name "#" -Value ($i+1) -MemberType NoteProperty } $array } If($force) { Write-HOST "Force switch is $Force. Running silently if all parameters are provided" -ForegroundColor Magenta } write-host "We are working with the $TargetOS operating system" -ForegroundColor Yellow #Target OS Variable init # Note OS is used in LA query to indicate what OS to leave out of results for patch query $OSQueryString = $Null if($TargetOS -eq 'Windows') { $OSQueryString = "!=""Linux""" } if($TargetOS -eq 'Linux') { $OSQueryString = "==""Linux""" } # Login to Azure - if already logged in, use existing credentials. Write-Host "Authenticating to Azure..." -ForegroundColor Cyan try { $AzureLogin = Get-AzSubscription } catch { $null = Login-AzAccount $AzureLogin = Get-AzSubscription } # Select Source Azure Subscription for Log Analytics AUM query If($AzureLogin -and !($SourceSubscriptionID)) { [array]$SubscriptionArray = Add-IndexNumberToArray (Get-AzSubscription) [int]$SelectedSub = 0 # use the current subscription if there is only one subscription available if ($SubscriptionArray.Count -eq 1) { $SelectedSub = 1 } # Get SubscriptionID if one isn't provided while($SelectedSub -gt $SubscriptionArray.Count -or $SelectedSub -lt 1) { Write-host "Please select a SOURCE subscription from the list below for the refrence Update Management Details" -NoNewline $SubscriptionArray | Select-Object "#", Name, ID | Format-Table try { $SelectedSub = Read-Host "Please enter a selection from 1 to $($SubscriptionArray.count) for the SOURCE subscription for Query of Updates" } catch { Write-Warning -Message 'Invalid option, please try again.' } } if($($SubscriptionArray[$SelectedSub - 1].Name)) {$SubscriptionName = $($SubscriptionArray[$SelectedSub - 1].Name) } elseif($($SubscriptionArray[$SelectedSub - 1].SubscriptionName)) { $SubscriptionName = $($SubscriptionArray[$SelectedSub - 1].SubscriptionName) } write-verbose "You Selected Azure Subscription: $SubscriptionName as your SOURCE subscription" if($($SubscriptionArray[$SelectedSub - 1].SubscriptionID)) { [guid]$SourceSubscriptionID = $($SubscriptionArray[$SelectedSub - 1].SubscriptionID) } if($($SubscriptionArray[$SelectedSub - 1].ID)) { [guid]$SourceSubscriptionID = $($SubscriptionArray[$SelectedSub - 1].ID) } } Write-Host "Selecting the Source Azure Subscription: $($SourceSubscriptionID)..." -ForegroundColor Cyan $Null = Select-AzSubscription -SubscriptionId $SourceSubscriptionID # Use workspacename and resourcegroup if that is provided as parameters and validate it is a workspace that can be accessed if($WSID -and $SourceSubscriptionID) { try { $Workspaces = Get-AzOperationalInsightsWorkspace foreach($WS in $Workspaces) { if($WS.CustomerID -match $WSID) { $WorkspaceName = $WS.Name $WorkspaceRG = $WS.ResourceGroupName } } Write-Host "You Selected Workspace: " -nonewline -ForegroundColor Cyan Write-Host $WorkspaceName -ForegroundColor Yellow } catch { Write-Warning -Message 'No Workspace found' break } } # Build a list of workspaces to choose from. If workspace is in another subscription # provide the resourceID of that workspace as a parameter elseif(!($WSID)) { [array]$Workspaces=@() try { # Build an object to list all Log Analytics Workspaces $Workspaces = Add-IndexNumberToArray (Get-AzOperationalInsightsWorkspace) Write-Host "Generating a list of workspaces from Azure Subscription Selected..." -ForegroundColor Cyan [int]$SelectedWS = 0 if ($Workspaces.Count -eq 1) { $SelectedWS = 1 } # Get WS Resource ID if one isn't provided while($SelectedWS -gt $Workspaces.Count -or $SelectedWS -lt 1 -and $Null -ne $Workspaces) { Write-Host "Please select a workspace from the list below" $Workspaces| Select-Object "#", Name, Location, ResourceGroupName, ResourceId | Format-Table if($Workspaces.count -ne 0) { try { $SelectedWS = Read-Host "Please enter a selection from 1 to $($Workspaces.count)" } catch { Write-Warning -Message 'Invalid option, please try again.' } } } } catch { Write-Warning -Message 'No Workspace found - try specifying workspacename, resourcegroup and subscriptionID parameters' } If($Workspaces) { Write-Host "You Selected Workspace: " -nonewline -ForegroundColor Cyan Write-Host "$($Workspaces[$SelectedWS - 1].Name)" -ForegroundColor Yellow $WorkspaceName = $($Workspaces[$SelectedWS - 1].Name) $WSID = $($Workspaces[$SelectedWS - 1].CustomerId.Guid) $WorkspaceRG = $($Workspaces[$SelectedWS - 1].ResourceGroupName) } else { Throw "No OMS workspaces available in selected subscription $SubscriptionID" break } } # Establish a list of approved classifications available for the OS target If($TargetOS -eq 'Windows') { $Classifications = @('Unclassified','Critical','Security','UpdateRollup','FeaturePack','ServicePack','Definition','Tools','Updates') } If($TargetOS -eq 'Linux') { $Classifications = @('Unclassified','Critical','Security','Other') } # If classifications were not provided via parameter, let's prompt for them (unless we have a query already given via parameter) if(!($ClassificationList)) { $ClassificationsChosen =@() While(!($ClassificationsChosen)) { $CAnalysis=@() foreach($Classification in $Classifications) { $cObject = New-Object -TypeName PSObject -Property @{'Name' = $Classification} $CAnalysis += $CObject } # Build the menu and prompt for a selection $Canalysis = Add-IndexNumberToArray ($CAnalysis) Write-Host "The following Classifications are available for $TargetOS" $CAnalysis|Select-Object "#",Name|Format-Table [array]$ClassificationCategories =@() [array]$ClassificationsChosen = (Read-host "Please provide # of classification(s) to process (separated by a comma) Note: Unclassified Reboots Only!").ToUpper() if($ClassificationsChosen -and (($ClassificationsChosen[0] -in 1..($Classifications.count -1))-or ($ClassificationsChosen[0].contains(",")))) { # Trim spaces out $ClassificationsChosen = $ClassificationsChosen.replace(" ","") [array]$ClassificationsChosen = ($ClassificationsChosen -split ",") foreach($Class in $ClassificationsChosen) { $ClassificationCategories = $ClassificationCategories + $($CAnalysis[$Class-1].Name) } } else { $ClassificationsChosen = $Null } } } else { [array]$ClassificationCategories = $ClassificationList } If($ClassificationCategories) { Write-Host "You Chose the following patch classifications" -ForegroundColor Cyan write-host $ClassificationCategories -ForegroundColor Yellow foreach($Line in $ClassificationCategories) { if($Line -eq "Unclassified") { write-host "You've chosen Unclassified! RebootOnly will be set." -ForegroundColor Red $RebootOptions = 'RebootOnly' $ClassificationCategories = @("Unclassified") break } } } # Build a query for applicable patches if((!($ApplicablePatchesQuery) -and (!($KBLIST)-and (!($SavedSearchID))))) { # Building a custom string for query to support a variable set of classifications $ClassificationsQueryString = "| where " $Count = 1 # Establish classifcations query string appropriate for Target OS according to those selected # Only used if a query was not provided through $ApplicablePatchesQuery foreach($Cat in $ClassificationCategories) { if($Count -lt $ClassificationCategories.Count) { $ClassificationsQueryString += "Classification has ""$Cat"" or " } else { $ClassificationsQueryString += "Classification has ""$Cat""" } $Count++ } # Applicable Patches to Build against # This would include things such as classification, as well as Update State and OS $ApplicablePatchesQuery = 'Update | where OSType' + $OSQueryString + ' ' + $ClassificationsQueryString + ' | summarize arg_max(TimeGenerated, *) by Computer,SourceComputerId,UpdateID, ApprovalSource, Product | summarize hint.strategy=partitioned arg_max(TimeGenerated, *) by Product | where UpdateState=~"Needed" and Approved!=false | project Product, Title ' # fixing a bug in SDK improperly setting "other" to "others" $ApplicablePatchesQuery = $($ApplicablePatchesQuery -replace "Other", "Others") Write-Host "Leveraging the following query for applicable patches for target OS" write-host "$ApplicablePatchesQuery" -ForegroundColor Cyan } # If not KBList Provided and we aren't doing a RebootOnly - build the list if(!($KBLIST)-and !($RebootOptions -eq "RebootOnly")) { $KBLIST=@() # Use a query from a saved search in LA if one is provided if($SavedSearchID) { $SavedSearch = Get-AzOperationalInsightsSavedSearch -ResourceGroupName $WorkspaceRG -WorkspaceName $WorkspaceName -SavedSearchId $SavedSearchID $ApplicablePatchesQuery = $($SavedSearch.Properties.Query) Write-Host "Leveraging the following query for applicable patches for target OS" write-host "$ApplicablePatchesQuery" -ForegroundColor Cyan } # Search against Log Analytics to determine applicable patches to present as initial approved KBs for target Update Management Object $Value = Invoke-AzOperationalInsightsQuery -WorkspaceId $WSID -Query $ApplicablePatchesQuery foreach($Val in $Value.Results){$KBLIST += $Val.Product} write-host "Leveraging the following KBs for Target Update List" -ForegroundColor Cyan write-host $KBLIST -ForegroundColor Yellow } elseif($KBLIST-and !($RebootOptions -eq "RebootOnly")) { write-host "Leveraging the following KBs for Target Update List" -ForegroundColor Cyan write-host $KBLIST -ForegroundColor Yellow } # Target subscription for Update Management Azure Automation Account If($AzureLogin -and !($TargetSubscriptionID)) { if(!($SubscriptionArray)) { [array]$SubscriptionArray = Add-IndexNumberToArray (Get-AzSubscription) [int]$SelectedSub = 0 } elseif ($SubscriptionArray.Count -eq 1) # use the current subscription if there is only one subscription available { [int]$SelectedSub = 1 } else { [int]$SelectedSub = 0 # Get SubscriptionID if one isn't provided while($SelectedSub -gt $SubscriptionArray.Count -or $SelectedSub -lt 1) { Write-host "Please select a TARGET subscription from the list below for the Azure Update Configuration Package" -NoNewline $SubscriptionArray | Select-Object "#", Name, ID | Format-Table try { $SelectedSub = Read-Host "Please enter a selection from 1 to $($SubscriptionArray.count) for the TARGET subscription for Update Management Configuration" } catch { Write-Warning -Message 'Invalid option, please try again.' } } if($($SubscriptionArray[$SelectedSub - 1].Name)) { $SubscriptionName = $($SubscriptionArray[$SelectedSub - 1].Name) } elseif($($SubscriptionArray[$SelectedSub - 1].SubscriptionName)) { $SubscriptionName = $($SubscriptionArray[$SelectedSub - 1].SubscriptionName) } write-verbose "You Selected Azure Subscription: $SubscriptionName as your TARGET subscription" if($($SubscriptionArray[$SelectedSub - 1].SubscriptionID)) { [guid]$TargetSubscriptionID = $($SubscriptionArray[$SelectedSub - 1].SubscriptionID) } if($($SubscriptionArray[$SelectedSub - 1].ID)) { [guid]$TargetSubscriptionID = $($SubscriptionArray[$SelectedSub - 1].ID) } #$SubscriptionID = $SubscriptionID.Guid } } Write-Host "Selecting Target Azure Subscription: $($TargetSubscriptionID) ..." -ForegroundColor Cyan $Null = Select-AzSubscription -SubscriptionId $TargetSubscriptionID # Build a list of automation accounts to choose from.if Account and RG are not provided if(!($AAAcountName -and $AAResourceGroupName)) { [array]$AAAcounts=@() try { $AAAcounts = Add-IndexNumberToArray (Get-AzAutomationAccount) Write-Host "Generating a list of automation accounts from target Azure Subscription Selected..." -ForegroundColor Cyan [int]$SelectedAAA = 0 if ($AAAcounts.Count -eq 1) { $SelectedAAA = 1 } # Get AutomationAccount if one isn't provided while($SelectedAAA -gt $AAAcounts.Count -or $SelectedAAA -lt 1 -and $Null -ne $AAAcounts) { Write-Host "Please select an Automation Account from the list below" $AAAcounts| Select-Object "#", AutomationAccountName, Location, ResourceGroupName, SubscriptionIDId | Format-Table if($AAAcounts.count -ne 0) { try { $SelectedAAA = Read-Host "Please enter a selection from 1 to $($AAAcounts.count)" } catch { Write-Warning -Message 'Invalid option, please try again.' } } } } catch { Write-Warning -Message 'No Automation Account found - try specifying AAAcountName, AAResourceGroupName and TargetSubscriptionID parameters' } If($AAAcounts) { Write-Host "You Selected Automation Account: " -nonewline -ForegroundColor Cyan Write-Host "$($AAAcounts[$SelectedAAA - 1].AutomationAccountName)" -ForegroundColor Yellow $AAAcountName = $($AAAcounts[$SelectedAAA - 1].AutomationAccountName) $AAResourceGroupName = $($AAAcounts[$SelectedAAA - 1].ResourceGroupName) } else { Throw "No Automation Accounts available in selected subscription $TargetSubscriptionID" break } } # Use Automation Account Name and resourcegroup if that is provided as parameters and validate it is an automation account that can be accessed else { try { $AAAcounts = Get-AzAutomationAccount foreach($AAA in $AAAcounts) { if($AAA.AutomationAccountName -match $AAAcountName) { Write-Host "Automation Account $AAAcountName exists" -ForegroundColor Cyan } } Write-Host "Selecting Azure Automation Account: " -nonewline -ForegroundColor Cyan Write-Host $AAAcountName -ForegroundColor Yellow } catch { Write-Warning -Message 'No Azure Automation Accounts found' break } } # Use parameter to target more than one subscription / broader scope if(!($queryScope)) { $queryScope = @("/subscriptions/$($TargetSubscriptionID)") write-host "No target scope provided. Using target subscription as scope" -ForegroundColor Cyan } # If queryLocation is not provided, prompt for region(s) if(!($queryLocation)) { $RAnalysis=@() $queryLocation=@() $Regions = (Get-AzLocation).Location foreach($Region in $Regions) { $RObject = New-Object -TypeName PSObject -Property @{'Location' = $Region} $RAnalysis += $RObject } $Ranalysis = Add-IndexNumberToArray ($RAnalysis) Write-Host "The following Regions are available for $OSTarget" $RAnalysis|Select-Object "#",Location|Format-Table [array]$RegionsChosen = (Read-host "Please provide # of region(s) to process (separated by a comma) or type ALL").ToUpper() if($RegionsChosen[0] -eq "ALL") { foreach($Reg in $RAnalysis) { $queryLocation = $queryLocation + $($Reg.Location) } } else { # Trim spaces out $RegionsChosen = $RegionsChosen.replace(" ","") [array]$RegionsChosen = ($RegionsChosen -split ",") foreach($R in $RegionsChosen) { $queryLocation = $queryLocation + $($RAnalysis[$R-1].Location) } } Write-Host "You chose the following location(s)" -ForegroundColor Cyan foreach($Line in $queryLocation) { write-host $Line -ForegroundColor Yellow } write-host "" } elseif($queryLocation) { Write-Host "You've selected the following regions" -ForegroundColor Cyan write-host $queryLocation -ForegroundColor Yellow write-host "" } # Define the Azure Query for Scoping the Reference Machine set $azq = New-AzAutomationUpdateManagementAzureQuery -ResourceGroupName $AAResourceGroupName ` -AutomationAccountName $AAAcountName ` -Scope $queryScope ` -Location $queryLocation ` -Tag $tags ` -FilterOperator $queryFilterOperator # Validate customer wants to continue to create the target schedule and Azure Update Management Configuration Patch List # If Force used, will update without prompting if ($Force -OR $PSCmdlet.ShouldContinue("This operation will create an Azure Update Management Deployment Schedule called ""$($SoftwareUpdateScheduleName)"" in your selected target subscription. Continue?","Creating Target Schedule named ""$SoftwareUpdateScheduleName""") ) { # BUG Description doesn't currently populate # If there is an expiration date, use it in the creation of the schedule if($ExpiryTime) { $Schedule = New-AzAutomationSchedule -Name $SoftwareUpdateScheduleName -AutomationAccountName $AAAcountName ` -ResourceGroupName $AAResourceGroupName ` -StartTime $StartTime ` -Description $SoftwareUpdateScheduleDescr ` -DaysOfWeek $DaysOfWeek ` -WeekInterval $WeekInterval ` -ForUpdateConfiguration ` -ExpiryTime $ExpiryTime write-host "Creating / Updating the Target Azure Update Management Schedule ""$SoftwareUpdateScheduleName"" with expiration of $ExpiryTime" -ForegroundColor Cyan } # No expiration date provided - this is t he default behavior else { $Schedule = New-AzAutomationSchedule -Name $SoftwareUpdateScheduleName -AutomationAccountName $AAAcountName ` -ResourceGroupName $AAResourceGroupName ` -StartTime $StartTime ` -Description $SoftwareUpdateScheduleDescr ` -DaysOfWeek $DaysOfWeek ` -WeekInterval $WeekInterval ` -ForUpdateConfiguration write-host "Creating / Updating the Target Azure Update Management Schedule ""$SoftwareUpdateScheduleName"" with no expiration" -ForegroundColor Cyan } if($TargetOS -eq "Windows") { # Setup for Windows OS $Schedule = New-AzAutomationSoftwareUpdateConfiguration -ResourceGroupName $AAResourceGroupName ` -AutomationAccountName $AAAcountName ` -Schedule $Schedule ` -Windows ` -Duration $duration ` -IncludedUpdateClassification $ClassificationCategories ` -IncludedKbNumber $KBLIST ` -AzureQuery $azq ` -PreTaskRunbookName $PreScript ` -PreTaskRunbookParameter $PreScriptParams ` -PostTaskRunbookName $PostScript ` -PostTaskRunbookParameter $PostScriptParams ` -RebootSetting $RebootOptions write-host "Creating / Updating the Target Azure Update Management Deployment Schedule based on ""$SoftwareUpdateScheduleName"" for Windows" -ForegroundColor Cyan } elseif ($TargetOS -eq "Linux") { # Setup for Linux OS (note different parameters) $Schedule = New-AzAutomationSoftwareUpdateConfiguration -ResourceGroupName $AAResourceGroupName ` -AutomationAccountName $AAAcountName ` -Schedule $Schedule ` -Linux ` -Duration $duration ` -IncludedPackageClassification $ClassificationCategories ` -IncludedPackageNameMask $KBLIST ` -AzureQuery $azq ` -PreTaskRunbookName $PreScript ` -PreTaskRunbookParameter $PreScriptParams ` -PostTaskRunbookName $PostScript ` -PostTaskRunbookParameter $PostScriptParams ` -RebootSetting $RebootOptions write-host "Creating / Updating the Target Azure Update Management Deployment Schedule based on ""$SoftwareUpdateScheduleName"" for Linux" -ForegroundColor Cyan } if($ScheduleDisabled) { write-host "You opted to disable the schedule once creating this configuration!" -ForegroundColor Yellow try{ $AASchedule = Get-AzAutomationSchedule -ResourceGroupName $AAResourceGroupName -AutomationAccountName $AAAcountName | where-object {$_.Name -match "^$($Schedule.Name)" -and $_.Description -match "^$($Schedule.Description)" } } catch{} try{ $Null = Set-AzAutomationSchedule -AutomationAccountName $AAAcountName -Name $AASchedule.Name -IsEnabled $false -ResourceGroupName $AAResourceGroupName write-host "Successfully disabled schedule." -ForegroundColor Green } catch{} } Write-Host "Complete!" -ForegroundColor Green } else { Write-Host "You selected No - exiting" Write-Host "Complete" -ForegroundColor Cyan } |