diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cb70f2..fe1b147 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/src/Config/CONTOSO-PROD.example.psd1 b/src/Config/CONTOSO-PROD.example.psd1 index 2da1b0b..34ca81c 100644 --- a/src/Config/CONTOSO-PROD.example.psd1 +++ b/src/Config/CONTOSO-PROD.example.psd1 @@ -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 diff --git a/src/Modules/SPSUpdate.Common/Public/Add-SPSScheduledTask.ps1 b/src/Modules/SPSUpdate.Common/Public/Add-SPSScheduledTask.ps1 index 6a05cc3..5e2237b 100644 --- a/src/Modules/SPSUpdate.Common/Public/Add-SPSScheduledTask.ps1 +++ b/src/Modules/SPSUpdate.Common/Public/Add-SPSScheduledTask.ps1 @@ -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 @@ -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') @@ -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 @@ -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 { diff --git a/src/SPSUpdate.ps1 b/src/SPSUpdate.ps1 index 8f1b82d..9a8840e 100644 --- a/src/SPSUpdate.ps1 +++ b/src/SPSUpdate.ps1 @@ -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. @@ -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) { @@ -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. @@ -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." @@ -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' } } } @@ -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 diff --git a/src/Test-SPSUpdateReadiness.ps1 b/src/Test-SPSUpdateReadiness.ps1 index 1e7ae3a..152d8c5 100644 --- a/src/Test-SPSUpdateReadiness.ps1 +++ b/src/Test-SPSUpdateReadiness.ps1 @@ -293,11 +293,8 @@ else { Add-CheckResult -Section 'Dashboard' -Name 'Dashboard.OutputPath writable (current user)' -Status 'FAIL' -Detail "Cannot write to $dashOutputPath : $($_.Exception.Message)" } - # Scheduled sequences and the boot-triggered ConfirmReboot render the dashboard AS the - # InstallAccount, so the current-user probe above is not enough. When OutputPath is a - # distinct folder from the status store (whose service-account write was already probed), - # probe it as the InstallAccount too, otherwise worker renders could fail while readiness - # passes. + # The sequence tasks render as the InstallAccount, so probe the dashboard folder as that + # account too. (ConfirmReboot runs as SYSTEM - that path is checked separately below.) $storeForCompare = '' if ($null -ne $cfg -and $cfg.Contains('StatusStorePath')) { $storeForCompare = ([string]$cfg.StatusStorePath).TrimEnd('\', '/') } $dashForCompare = $dashOutputPath.TrimEnd('\', '/') @@ -315,7 +312,7 @@ else { Add-CheckResult -Section 'Dashboard' -Name 'Dashboard.OutputPath writable (service account)' -Status 'SKIP' -Detail 'Folder not found; see the failure above' } elseif ($null -eq $dashSvcCred) { - Add-CheckResult -Section 'Dashboard' -Name 'Dashboard.OutputPath writable (service account)' -Status 'WARN' -Detail 'Could not load the InstallAccount to test; ensure it has Modify on the dashboard folder (scheduled sequences and ConfirmReboot render as that account)' + Add-CheckResult -Section 'Dashboard' -Name 'Dashboard.OutputPath writable (service account)' -Status 'WARN' -Detail 'Could not load the InstallAccount to test; ensure it has Modify on the dashboard folder (the scheduled sequences render as that account)' } else { $dashSvcProbe = Join-Path -Path $dashOutputPath -ChildPath (".spsupdate-readiness-svc-{0}.tmp" -f ([guid]::NewGuid().ToString('N'))) @@ -334,7 +331,7 @@ else { Add-CheckResult -Section 'Dashboard' -Name 'Dashboard.OutputPath writable (service account)' -Status 'PASS' -Detail "InstallAccount '$($dashSvcCred.UserName)' can write to the dashboard folder" } else { - Add-CheckResult -Section 'Dashboard' -Name 'Dashboard.OutputPath writable (service account)' -Status 'FAIL' -Detail "InstallAccount '$($dashSvcCred.UserName)' cannot write to $dashOutputPath. Grant it Modify; otherwise worker renders (sequences, ConfirmReboot) will not update the hosted dashboard." + Add-CheckResult -Section 'Dashboard' -Name 'Dashboard.OutputPath writable (service account)' -Status 'FAIL' -Detail "InstallAccount '$($dashSvcCred.UserName)' cannot write to $dashOutputPath. Grant it Modify; otherwise worker renders (sequences) will not update the hosted dashboard." } } catch { @@ -345,6 +342,23 @@ else { } } +# 5c. Reboot status store (machine accounts) - independent of Dashboard.OutputPath. +# Reboot enabled + UNC store: the SYSTEM boot task writes as the computer account, which we +# cannot probe here - surface an explicit WARN about the required machine-account grant. +$rebootEnabled = $false +try { $rebootEnabled = [bool]$cfg.Reboot.Enable } catch { $rebootEnabled = $false } +if ($rebootEnabled) { + $storeForReboot = '' + try { $storeForReboot = [string]$cfg.StatusStorePath } catch { $storeForReboot = '' } + if ([string]::IsNullOrWhiteSpace($storeForReboot) -and $null -ne $dashOutputPath) { $storeForReboot = $dashOutputPath } + if ($storeForReboot -like '\\*') { + Add-CheckResult -Section 'Dashboard' -Name 'Reboot status store (machine accounts)' -Status 'WARN' -Detail "Automatic reboot is enabled: the boot ConfirmReboot task runs as SYSTEM and writes to '$storeForReboot' as the computer account. Grant the farm machine accounts (e.g. 'Domain Computers') Modify on the share + NTFS (add them to New-SPSDashboardSite.ps1 -WriteAccounts). This cannot be auto-verified here." + } + elseif (-not [string]::IsNullOrWhiteSpace($storeForReboot)) { + Add-CheckResult -Section 'Dashboard' -Name 'Reboot status store (machine accounts)' -Status 'WARN' -Detail 'Automatic reboot is enabled but the status store is a local path; the boot ConfirmReboot task (SYSTEM) on other servers cannot reach it. Use a shared UNC status store and grant the farm machine accounts Modify.' + } +} + # 6. Network / CredSSP reachability Write-Section -Title 'Network' # Local WinRM / PS remoting prerequisite, checked before the per-server probes so a missing diff --git a/tests/Add-SPSScheduledTask.Tests.ps1 b/tests/Add-SPSScheduledTask.Tests.ps1 new file mode 100644 index 0000000..c11c17d --- /dev/null +++ b/tests/Add-SPSScheduledTask.Tests.ps1 @@ -0,0 +1,46 @@ +# Metadata/binding tests for Add-SPSScheduledTask parameter sets. +# Cross-platform by design: these assert the command's parameter-set contract only; they do not +# register a scheduled task (the COM registration is Windows-only). + +BeforeAll { + $repoRoot = Split-Path -Path $PSScriptRoot -Parent + $modulePath = Join-Path -Path $repoRoot -ChildPath 'src/Modules/SPSUpdate.Common/SPSUpdate.Common.psd1' + Import-Module -Name $modulePath -Force + $script:cmd = Get-Command Add-SPSScheduledTask +} + +AfterAll { + Remove-Module -Name SPSUpdate.Common -Force -ErrorAction SilentlyContinue +} + +Describe 'Add-SPSScheduledTask parameter sets' { + It 'places RunAsSystem and ExecuteAsCredential in distinct parameter sets' { + $runAsSystemSets = $script:cmd.Parameters['RunAsSystem'].ParameterSets.Keys + $credentialSets = $script:cmd.Parameters['ExecuteAsCredential'].ParameterSets.Keys + # No shared set between the two, so they cannot be supplied together. + (@($runAsSystemSets) | Where-Object { $credentialSets -contains $_ }) | Should -BeNullOrEmpty + } + + It 'makes ExecuteAsCredential mandatory in the Credential (default) set and absent from the System set' { + $credParam = $script:cmd.Parameters['ExecuteAsCredential'] + $credParam.ParameterSets.ContainsKey('Credential') | Should -BeTrue + $credParam.ParameterSets['Credential'].IsMandatory | Should -BeTrue + $credParam.ParameterSets.ContainsKey('System') | Should -BeFalse + } + + It 'exposes RunAsSystem only in the System set and as a switch (not mandatory)' { + $sysParam = $script:cmd.Parameters['RunAsSystem'] + $sysParam.ParameterType | Should -Be ([System.Management.Automation.SwitchParameter]) + $sysParam.ParameterSets.ContainsKey('System') | Should -BeTrue + $sysParam.ParameterSets['System'].IsMandatory | Should -BeFalse + $sysParam.ParameterSets.ContainsKey('Credential') | Should -BeFalse + } + + It 'rejects supplying RunAsSystem and ExecuteAsCredential together' { + $cred = [System.Management.Automation.PSCredential]::new('CONTOSO\svc', (New-Object System.Security.SecureString)) + { + Add-SPSScheduledTask -Name 'T' -ActionArguments '-x' -TaskPath 'SharePoint' ` + -RunAsSystem -ExecuteAsCredential $cred -ErrorAction Stop + } | Should -Throw -ExpectedMessage '*Parameter set cannot be resolved*' + } +} diff --git a/wiki/Hosting-the-dashboard-on-IIS.md b/wiki/Hosting-the-dashboard-on-IIS.md index 7b0a63a..ce4d1bd 100644 --- a/wiki/Hosting-the-dashboard-on-IIS.md +++ b/wiki/Hosting-the-dashboard-on-IIS.md @@ -115,12 +115,35 @@ HTTP 404.17): | SMB share | Modify (Change) | the account that runs SPSUpdate interactively, and/or the InstallAccount | | NTFS | Modify | same account(s) | | NTFS | Read | the IIS application-pool identity (`IIS_IUSRS` by default) | +| SMB share + NTFS | Modify | the farm **computer accounts** (e.g. `CONTOSO\Domain Computers`) — **only when the automatic reboot is enabled** (see below) | The four upgrade/mount sequence tasks run as the **InstallAccount**, so if it cannot write to the share the upgrade phase never appears on the dashboard. Run `Test-SPSUpdateReadiness.ps1` to verify both your account and the InstallAccount can write to the store, and that `Dashboard.OutputPath` exists, is writable, and is a shared UNC path. +### Automatic reboot: grant the farm computer accounts + +The one-shot `SPSUpdate-RebootConfirm` boot task (registered when the optional automatic reboot +runs) executes as **`NT AUTHORITY\SYSTEM`** so it needs no stored credential. On the network, a +`SYSTEM` task authenticates as the **computer account** (`DOMAIN\SERVER$`), so each rebooting +server must be able to write `Reboot=Done` to the UNC status store as its machine account. + +Grant the farm computer accounts **Modify** on both the SMB share and NTFS. The simplest option is +to add `Domain Computers` (or the specific server `$` accounts) to `-WriteAccounts` when provisioning +the share: + +```powershell +.\New-SPSDashboardSite.ps1 -Path 'C:\inetpub\PSDSCPullServer\SPSUpdate' -ShareName 'SPSUpdate$' ` + -WriteAccounts 'CONTOSO\svcspsfarm','CONTOSO\Domain Computers' ` + -ParentSite 'PSDSCPullServer' -AppAlias 'SPSUpdate' +``` + +If the share does not grant the machine accounts, the CU still installs and the server still +reboots, but `-Action ConfirmReboot` cannot persist the completion: it logs a clear warning and +retries on the next boot, and the dashboard stays on the `Reboot` running state until the grant is +added. This grant is only needed when the automatic reboot (`Reboot.Enable = $true`) is used. + ## Point SPSUpdate at it In your environment config (`Config\--.psd1`): diff --git a/wiki/Usage.md b/wiki/Usage.md index e3ca0a4..1e92407 100644 --- a/wiki/Usage.md +++ b/wiki/Usage.md @@ -208,8 +208,8 @@ When enabled, at the end of `-Action ProductUpdate` on a server: inside the optional `Reboot.Schedule` window, SPSUpdate marks the **Reboot** phase as *Running* on the dashboard ("Automatic Reboot launched, check the server in a few minutes") and logs a Windows Event Log entry (source `Restart-SPSServer`, ID 3010). -2. It registers a one-shot boot task (`SPSUpdate-RebootConfirm`, running as the - InstallAccount) and restarts the server. +2. It registers a one-shot boot task (`SPSUpdate-RebootConfirm`, running as + **`NT AUTHORITY\SYSTEM`** — no stored credential) and restarts the server. 3. When the server comes back, that task runs `-Action ConfirmReboot`, which marks the Reboot phase as *Done* ("Server back online after automatic reboot"), logs the completion and removes itself. @@ -219,6 +219,14 @@ pending-reboot registry markers (which commonly stay set on production farms), s happens at most once per patching campaign. Outside the schedule window the dashboard shows the reboot as *Pending*; with `Reboot.Enable = $false` no reboot is attempted. +> **Status store access for the reboot confirmation.** Because the boot task runs as +> `SYSTEM`, it writes its completion to a UNC status store as the **computer account** +> (`DOMAIN\SERVER$`). Grant the farm machine accounts (for example `Domain Computers`) +> **Modify** on the status store share **and** NTFS — add them to +> `New-SPSDashboardSite.ps1 -WriteAccounts`. Without that grant the reboot still happens, +> but `ConfirmReboot` cannot record it (it logs a clear warning and retries on the next +> boot). See [Hosting the dashboard on IIS](Hosting-the-dashboard-on-IIS). + Use `-WhatIf` on the `ProductUpdate` run for a full dry run: it skips the binary install (and therefore the reboot) and logs what it would do without making any change.