Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- The one-shot `SPSUpdate-RebootConfirm` boot task (automatic reboot) now runs as **`NT AUTHORITY\SYSTEM`** instead of the farm InstallAccount decrypted from `secrets.psd1`. This removes the stored-credential dependency that could abort the reboot at task registration (`0x8007052E` when the secret did not match the account password) and decouples the reboot from password rotation. `Add-SPSScheduledTask` gains a `-RunAsSystem` switch (mutually exclusive with `-ExecuteAsCredential`). Because a `SYSTEM` task reaches a UNC status store as the computer account, the share must grant the farm computer accounts (e.g. `Domain Computers`) Modify — add them to `New-SPSDashboardSite.ps1 -WriteAccounts`; `-Action ConfirmReboot` logs a clear, actionable warning (and retries on the next boot) when the grant is missing. ([#64](https://github.com/luigilink/SPSUpdate/issues/64))

### Added

- **SharePoint 2016 / 2019 compatibility restored on the `4.x` line.** This maintenance line keeps the full feature set of the `5.x` (Subscription Edition) line but re-introduces support for SharePoint Server 2016 and 2019: the SharePoint cmdlets are loaded through the legacy `Microsoft.SharePoint.PowerShell` snap-in on 2016/2019 (and the `SharePointServer` module on Subscription Edition), both locally and in remote sessions, and `Get-SPSLocalVersionInfo` is version-aware again (`2016`/`2019`/`SE`) so the correct product-name string is matched in the registry. SharePoint 2016, 2019 and Subscription Edition all share the same `16.0` hive and `OSearch16` search service, so no other paths change. Version detection is automatic at runtime (no configuration change). As an added safety check on this multi-version line, `Start-SPSProductUpdate` now compares the update package's product line (2016/2019/SE, derived from the CU build) with the installed SharePoint line and fails fast — before stopping any service or launching the installer — when they do not match. ([#38](https://github.com/luigilink/SPSUpdate/issues/38))

- A deferred automatic reboot that was already performed **out-of-band** (a manual restart or a Windows Update reboot) is now reconciled instead of restarting the server again: when a `.pending` reboot request predates the server's last boot time, `Invoke-SPSAutomaticReboot` marks the Reboot phase `Done` ("Reboot already completed out-of-band"), clears the pending marker and skips the restart. A fresh reboot required by an install performed in the same run is unaffected. ([#68](https://github.com/luigilink/SPSUpdate/issues/68))
- `-Action ProductUpdate` now refreshes the installed server's live **Patch Status** on the Binaries Installation card after the cumulative update is installed, so the column reflects the real post-install state (typically `Upgrade Required` until the Configuration Wizard runs) instead of keeping the pre-install value (`No Action Required`) until the next `Default` run. It reuses the same master-side patch-status read as the `Default` baseline (no remoting) and is status-only — the Content Databases and Configuration Wizard cards still evolve only during a `Default` run. A failed read preserves the last-known-good value. ([#59](https://github.com/luigilink/SPSUpdate/issues/59))
- The near real-time dashboard now refreshes the live farm status at key transitions (the pre-patch baseline, after the Configuration Wizard completes, and the final render) instead of only at the pre-patch baseline. The master re-reads each server's patch status and re-publishes the content-database inventory with fresh upgrade status, so a successful campaign no longer shows a stale `Upgrade Required` Patch Status or a lingering pre-patch inconsistency banner once the farm is back to `No Action Required`. The Content Databases card also shows a database that SPSUpdate has already upgraded (State `Done`) as `No update pending` even before the next refresh. Server patch status and role are shown with short readable labels (for example `Upgrade Required`, `Application with Search`) via the new public helpers `ConvertTo-SPSPatchStatusLabel` and `ConvertTo-SPSRoleLabel`, instead of the raw enum names. ([#51](https://github.com/luigilink/SPSUpdate/issues/51))
- A standalone, idempotent helper `New-SPSDashboardSite.ps1` (in `src/`) that provisions the dashboard hosting target on an IIS/pull server — folder, SMB share (Modify for the write accounts), NTFS permissions, a static-file `web.config`, and either a dedicated IIS site or a sub-application under an existing site. It has no dependency on the `SPSUpdate.Common` module, so it can be copied on its own to the IIS server. A new wiki page, *Hosting the dashboard on IIS*, documents the validated setup (and the manual equivalent). ([#49](https://github.com/luigilink/SPSUpdate/issues/49))
- `Test-SPSUpdateReadiness.ps1` now checks the optional `Dashboard.OutputPath`: it verifies the folder exists and is writable, and warns when it is a master-local (drive-letter) path rather than a shared UNC path that worker runs can also reach. ([#49](https://github.com/luigilink/SPSUpdate/issues/49))
Expand Down
6 changes: 6 additions & 0 deletions src/Config/CONTOSO-PROD.example.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@
# exit code (never by Windows pending-reboot registry markers, which stay stuck on
# production farms). WARNING: reboots are per-server; on a farm you remain responsible
# for not rebooting the sole Distributed Cache host or the last available WFE at once.
# NOTE: the boot task that confirms the reboot runs as NT AUTHORITY\SYSTEM, so each
# server writes its reboot completion to the status store as its computer account. When
# StatusStorePath / Dashboard.OutputPath is a UNC share, grant the farm computer accounts
# (e.g. 'CONTOSO\Domain Computers') Modify on the share and NTFS - see the "Hosting the
# dashboard on IIS" wiki page. Without that grant the reboot still happens but its
# completion is not recorded until the grant is added.
Reboot = @{
# Enable : allow SPSUpdate to reboot a server automatically after a CU install.
# Possible values : $true | $false. Default if omitted: $false
Expand Down
30 changes: 24 additions & 6 deletions src/Modules/SPSUpdate.Common/Public/Add-SPSScheduledTask.ps1
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
function Add-SPSScheduledTask {
[CmdletBinding(DefaultParameterSetName = 'Credential')]
param
(
[Parameter(Mandatory = $true)]
[Parameter(Mandatory = $true, ParameterSetName = 'Credential')]
[System.Management.Automation.PSCredential]
$ExecuteAsCredential, # Credentials for Task Schedule

[Parameter(ParameterSetName = 'System')]
[switch]
$RunAsSystem, # Register the task to run as NT AUTHORITY\SYSTEM (no stored credential)

[Parameter(Mandatory = $true)]
[System.String]
$ActionArguments, # Arguments for the task action
Expand All @@ -28,8 +33,11 @@

# Initialize variables
$TaskCmd = 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' # Path to PowerShell executable
$UserName = $ExecuteAsCredential.UserName
$Password = $ExecuteAsCredential.GetNetworkCredential().Password
# A credential is only read for the password-based registration path (not for SYSTEM).
if (-not $RunAsSystem) {
$UserName = $ExecuteAsCredential.UserName
$Password = $ExecuteAsCredential.GetNetworkCredential().Password
}

# Connect to the local TaskScheduler Service
$TaskSvc = New-Object -ComObject ('Schedule.service')
Expand Down Expand Up @@ -60,6 +68,11 @@
$TaskSchd.RegistrationInfo.Description = "$($Description)" # Task description
$TaskSchd.RegistrationInfo.Author = $TaskAuthor # Task author
$TaskSchd.Principal.RunLevel = 1 # Task run level (1 = Highest)
# SYSTEM uses the well-known SID with the service-account logon type (5), so no password.
if ($RunAsSystem) {
$TaskSchd.Principal.UserId = 'S-1-5-18'
$TaskSchd.Principal.LogonType = 5
}

# Task Schedule - Modify Settings Section
$TaskSettings = $TaskSchd.Settings
Expand Down Expand Up @@ -88,9 +101,14 @@
}

try {
# Register/update the task (6 = create or update). Cast to [void] so the
# returned RegisteredTask COM object is not dumped into the transcript.
[void]$TaskFolder.RegisterTaskDefinition($Name, $TaskSchd, 6, $TaskUser, $TaskUserPwd, 1)
# Register/update (6). SYSTEM registers with the service-account logon type (5); the
# credential path uses password logon (1). [void] keeps the COM object out of the transcript.
if ($RunAsSystem) {
[void]$TaskFolder.RegisterTaskDefinition($Name, $TaskSchd, 6, 'S-1-5-18', $null, 5)
}
else {
[void]$TaskFolder.RegisterTaskDefinition($Name, $TaskSchd, 6, $TaskUser, $TaskUserPwd, 1)
}
Write-Output "Successfully added or updated '$Name' script in Task Scheduler Service"
}
catch {
Expand Down
51 changes: 38 additions & 13 deletions src/SPSUpdate.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,24 @@ function Invoke-SPSAutomaticReboot {
return
}

# Reconcile a deferred reboot already performed out-of-band (pending predates last boot), but
# not when this run itself needs a reboot (fresh -RebootRequired, or Force on a successful install).
if ($hasPending -and -not $RebootRequired -and -not ($rebootCfg.Force -and $InstallPerformed)) {
$pendingStamp = $null
try { $pendingStamp = [datetimeoffset]::Parse((Get-Content -Path $rebootPendingMarker -Raw).Trim(), [cultureinfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::RoundtripKind).UtcDateTime } catch { $pendingStamp = $null }
$lastBoot = $null
try { $lastBoot = (Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop).LastBootUpTime.ToUniversalTime() } catch { $lastBoot = $null }
if ($null -ne $pendingStamp -and $null -ne $lastBoot -and $lastBoot -gt $pendingStamp) {
Write-Output "Deferred reboot for $thisServer already satisfied out-of-band (last boot $lastBoot > request $pendingStamp); reconciling."
if ($null -ne $rebootDoneMarker) { Set-Content -Path $rebootDoneMarker -Value (Get-Date -Format o) -Force -ErrorAction SilentlyContinue }
Remove-Item -Path $rebootPendingMarker -Force -ErrorAction SilentlyContinue
Write-SPSStatus -Scope 'Reboot' -Phase 'Reboot' -Server $thisServer -State 'Done' -Detail 'Reboot already completed out-of-band'
Add-SPSUpdateEvent -Message "Deferred reboot on $thisServer reconciled: the server was already rebooted out-of-band." -Source 'Restart-SPSServer' -EntryType 'Information' -EventID 3010
Write-SPSDashboard
return
}
}

# Decide whether a reboot is warranted. Force reboots even on a successful no-reboot
# install (exit 0), but only when an install actually ran this time, so an already-patched
# server (null / exit 17025) is never rebooted just because a new campaign started.
Expand Down Expand Up @@ -687,16 +705,10 @@ function Invoke-SPSAutomaticReboot {
return
}

# Register the one-shot boot-triggered confirmation task (runs as the InstallAccount so it
# can write to the status store share). This is REQUIRED: without it nothing transitions
# the Reboot phase from Running to Done, so a failure to register aborts the reboot and is
# surfaced as Failed rather than leaving the dashboard stuck on Running forever.
# Register the one-shot boot task as SYSTEM (no stored credential). REQUIRED: without it the
# Reboot phase never reaches Done, so a registration failure aborts the reboot (surfaced Failed).
$confirmRegistered = $false
try {
$rebootCredential = Get-SPSSecret -CredentialKey $envCfg.CredentialKey -ConfigPath $pathConfigFolder
if ($null -eq $rebootCredential) {
throw 'the InstallAccount secret could not be loaded to register the reboot-confirm task.'
}
$confirmArguments = "-ExecutionPolicy Bypass -File `"$($fullScriptPath)`" -ConfigFile `"$($resolvedConfigFile)`" -Action ConfirmReboot"
$existingConfirm = Get-ScheduledTask -TaskName $script:TaskNameRebootConfirm -TaskPath "\$script:TaskPath\" -ErrorAction SilentlyContinue
if ($null -ne $existingConfirm) {
Expand All @@ -705,7 +717,7 @@ function Invoke-SPSAutomaticReboot {
Add-SPSScheduledTask -Name $script:TaskNameRebootConfirm `
-Description 'One-shot SPSUpdate task that confirms the automatic reboot completed and self-deletes.' `
-ActionArguments $confirmArguments `
-ExecuteAsCredential $rebootCredential `
-RunAsSystem `
-TaskPath $script:TaskPath `
-BootTrigger
# Verify the task is actually registered before we commit to rebooting.
Expand Down Expand Up @@ -1045,7 +1057,8 @@ Exception: $_
$rebootDoneMarker = Get-SPSRebootMarkerPath -Kind 'done'
$campaignReachable = Test-Path -Path $statusCampaignPath -ErrorAction SilentlyContinue
if (-not $campaignReachable) {
Write-Warning -Message "Status store campaign folder is unreachable on $thisServer; leaving the confirmation task to retry on the next boot."
Write-Warning -Message ("Status store campaign folder is unreachable on $thisServer; leaving the confirmation task to retry on the next boot. " +
"This boot task runs as SYSTEM, so it reaches a UNC status store as the computer account ($($env:COMPUTERNAME)`$) - make sure the share and NTFS grant the farm machine accounts (for example 'Domain Computers') write access. See the wiki.")
}
elseif ($null -eq $rebootDoneMarker -or -not (Test-Path -Path $rebootDoneMarker)) {
Write-Warning -Message "No reboot guard marker for the current campaign on $thisServer; this reboot-confirmation task is stale and will be removed without recording completion."
Expand Down Expand Up @@ -1073,8 +1086,9 @@ Exception: $_
Add-SPSUpdateEvent -Message "Automatic reboot completed on $thisServer - server is back online." -Source 'Restart-SPSServer' -EntryType 'Information' -EventID 3010
}
else {
Write-Warning -Message "Could not persist Reboot=Done for $thisServer after several attempts; leaving the confirmation task in place to retry on the next boot."
Add-SPSUpdateEvent -Message "Could not persist the reboot completion status for $thisServer; the confirmation task will retry on the next boot." -Source 'Restart-SPSServer' -EntryType 'Warning'
Write-Warning -Message ("Could not persist Reboot=Done for $thisServer after several attempts; leaving the confirmation task in place to retry on the next boot. " +
"This boot task runs as SYSTEM, so it writes to a UNC status store as the computer account ($($env:COMPUTERNAME)`$) - make sure the share and NTFS grant the farm machine accounts (for example 'Domain Computers') Modify, not just read. See the wiki.")
Add-SPSUpdateEvent -Message "Could not persist the reboot completion status for $thisServer; the confirmation task will retry on the next boot. The SYSTEM boot task writes as the computer account - grant the farm machine accounts Modify on the status store share and NTFS." -Source 'Restart-SPSServer' -EntryType 'Warning'
}
}
}
Expand Down Expand Up @@ -1422,7 +1436,18 @@ Shutdown Services: $($envCfg.Binaries.ShutdownServices)
else {
$puInstalledBuild = ''
try { $puInstalledBuild = (Get-SPSInstalledProductVersion).FileVersion.ToString() } catch { $puInstalledBuild = '' }
Write-SPSStatus -Scope 'ProductUpdate' -Phase 'ProductUpdate' -State 'Done' -Detail 'All updates processed' -Build $puInstalledBuild
# Refresh this server's live Patch Status only after a real install, so the card
# shows the post-install state (no-op runs keep the last-known-good value).
$puPatchStatus = ''
if ($installPerformed) {
try { $puPatchStatus = ConvertTo-SPSPatchStatusLabel -Status "$(Get-SPSServersPatchStatus -Server $thisServer)" } catch { $puPatchStatus = '' }
}
if ([string]::IsNullOrWhiteSpace($puPatchStatus)) {
Write-SPSStatus -Scope 'ProductUpdate' -Phase 'ProductUpdate' -State 'Done' -Detail 'All updates processed' -Build $puInstalledBuild
}
else {
Write-SPSStatus -Scope 'ProductUpdate' -Phase 'ProductUpdate' -State 'Done' -Detail 'All updates processed' -Build $puInstalledBuild -PatchStatus $puPatchStatus
}
}
Write-SPSDashboard
# Optional automatic reboot (opt-in). No-op unless Reboot.Enable is set. When a
Expand Down
Loading
Loading