Private/ConvertFrom-AzLocalUpdateWindow.ps1
|
function ConvertFrom-AzLocalUpdateWindow { <# .SYNOPSIS Parses an UpdateWindow tag value into structured window objects. .DESCRIPTION Parses the compact maintenance window syntax used in the UpdateWindow Azure resource tag into structured objects suitable for schedule evaluation and display. Syntax: <days>_<HH:MM>-<HH:MM>[;<days>_<HH:MM>-<HH:MM>] Days: Mon,Tue,Wed,Thu,Fri,Sat,Sun (ranges with -), * or Daily for all days Times: 24-hour UTC. Overnight wraps supported (22:00-06:00 = wraps to next day). .PARAMETER WindowString The UpdateWindow tag value to parse. .OUTPUTS PSCustomObject[] with Days (DayOfWeek[]), StartTime (TimeSpan), EndTime (TimeSpan), Overnight (bool) .EXAMPLE ConvertFrom-AzLocalUpdateWindow -WindowString "Sat-Sun_02:00-06:00" .NOTES Time zone / DST behaviour: - Window times are compared against the current time of the host running the automation (Get-Date), NOT against the cluster's local time zone. Run your pipeline on a host configured for UTC (recommended for fleet automation) so that tag values map unambiguously to wall-clock intervals. - Daylight Saving Time (DST) transitions on the host where the automation runs can cause a window to appear to shift by +/-1 hour on the transition day. A 22:00-06:00 window evaluated on a host that "springs forward" will have one fewer hour of effective coverage that night, and "falls back" will have one extra hour. If strict wall-clock coverage matters, (a) use UTC on the automation host, and/or (b) set the window wide enough to absorb a 1-hour shift on transition days. - The parser does not interpret UTC offsets embedded in tag values; supply times in the host's effective time zone. #> [CmdletBinding()] [OutputType([PSCustomObject[]])] param( [Parameter(Mandatory = $true)] [AllowEmptyString()] [string]$WindowString ) if ([string]::IsNullOrWhiteSpace($WindowString)) { throw "UpdateWindow value cannot be empty." } # Azure tag values max 256 chars if ($WindowString.Length -gt 256) { throw "UpdateWindow value exceeds Azure tag limit of 256 characters (length: $($WindowString.Length))." } $windows = @() $segments = $WindowString -split ';' foreach ($segment in $segments) { $segment = $segment.Trim() if ([string]::IsNullOrWhiteSpace($segment)) { continue } # Parse <days>_<start>-<end> if ($segment -notmatch '^([^_]+)_(\d{2}:\d{2})-(\d{2}:\d{2})$') { throw "Invalid window segment syntax: '$segment'. Expected format: <days>_<HH:MM>-<HH:MM>" } $daysPart = $matches[1] $startStr = $matches[2] $endStr = $matches[3] # Parse time components (PS 5.1 compatible - avoid TryParse [ref] issues) $startTime = $null $endTime = $null try { $startTime = [TimeSpan]::Parse($startStr) } catch { throw "Invalid start time '$startStr' in segment '$segment'." } try { $endTime = [TimeSpan]::Parse($endStr) } catch { throw "Invalid end time '$endStr' in segment '$segment'." } # Parse days $resolvedDays = @() if ($daysPart -eq '*' -or $daysPart -ieq 'Daily') { $resolvedDays = @([DayOfWeek]::Monday, [DayOfWeek]::Tuesday, [DayOfWeek]::Wednesday, [DayOfWeek]::Thursday, [DayOfWeek]::Friday, [DayOfWeek]::Saturday, [DayOfWeek]::Sunday) } else { $daySpecs = $daysPart -split ',' foreach ($spec in $daySpecs) { $spec = $spec.Trim() if ($spec -match '^(\w{3})-(\w{3})$') { # Day range (e.g., Mon-Fri, Sat-Sun) $rangeStart = $matches[1] $rangeEnd = $matches[2] # Find indices in ordered day list $startIdx = -1; $endIdx = -1 for ($i = 0; $i -lt $script:DayAbbreviations.Count; $i++) { if ($script:DayAbbreviations[$i] -ieq $rangeStart) { $startIdx = $i } if ($script:DayAbbreviations[$i] -ieq $rangeEnd) { $endIdx = $i } } if ($startIdx -lt 0) { throw "Invalid day abbreviation '$rangeStart' in segment '$segment'. Valid: Mon,Tue,Wed,Thu,Fri,Sat,Sun" } if ($endIdx -lt 0) { throw "Invalid day abbreviation '$rangeEnd' in segment '$segment'. Valid: Mon,Tue,Wed,Thu,Fri,Sat,Sun" } # Handle wrap-around (e.g., Fri-Mon) if ($startIdx -le $endIdx) { for ($i = $startIdx; $i -le $endIdx; $i++) { $resolvedDays += $script:DayMap[$script:DayAbbreviations[$i]] } } else { # Wrap: Fri-Mon = Fri,Sat,Sun,Mon for ($i = $startIdx; $i -lt 7; $i++) { $resolvedDays += $script:DayMap[$script:DayAbbreviations[$i]] } for ($i = 0; $i -le $endIdx; $i++) { $resolvedDays += $script:DayMap[$script:DayAbbreviations[$i]] } } } elseif ($spec -match '^\w{3}$') { # Single day $matched = $false foreach ($abbr in $script:DayAbbreviations) { if ($abbr -ieq $spec) { $resolvedDays += $script:DayMap[$abbr] $matched = $true break } } if (-not $matched) { throw "Invalid day abbreviation '$spec' in segment '$segment'. Valid: Mon,Tue,Wed,Thu,Fri,Sat,Sun" } } else { throw "Invalid day specification '$spec' in segment '$segment'. Use 3-letter abbreviations (Mon-Sun), ranges (Mon-Fri), or * / Daily." } } } $overnight = ($endTime -le $startTime) $windows += [PSCustomObject]@{ Days = @($resolvedDays | Select-Object -Unique) StartTime = $startTime EndTime = $endTime Overnight = $overnight Raw = $segment } } if ($windows.Count -eq 0) { throw "No valid window segments found in '$WindowString'." } return $windows } |