|
<#PSScriptInfo |
|
.VERSION 1.3.1 |
|
.GUID 8f3c9c3c-4b9d-4c1e-9c4a-2f1f0e8b7a11 |
|
.AUTHOR Michel Sijmons (ZyntriOps) |
|
.COMPANYNAME ZyntriOps |
|
.COPYRIGHT Copyright (c) 2026 Michel Sijmons - https://mickert.dev |
|
.LICENSEURI http://www.apache.org/licenses/LICENSE-2.0 |
|
.PROJECTURI https://zyntriops.com/tools/smartmontools-installer |
|
.ICONURI https://zyntriops.com/tools/smartmontools-installer/icon.png |
|
.TAGS smartmontools;installer;zyntriops;monitoring;storage;automation |
|
.RELEASENOTES |
|
- Added GitHub API asset discovery (future-proof for 7.5.1+, 7.6, etc.) |
|
- Added uninstall /S enforcement |
|
- Improved logging, cleanup, fail-fast behavior, and installer output capture |
|
- Added JSON output for automation systems |
|
- Added downgrade support with automatic uninstall |
|
- Added MOTW removal and MD5 validation |
|
#> |
|
|
|
<# |
|
.SYNOPSIS |
|
Installs, updates, downgrades, or uninstalls smartmontools on Windows systems. |
|
|
|
.DESCRIPTION |
|
ZyntriOps-SmartmontoolsInstaller.ps1 |
|
This script installs, updates, downgrades, or uninstalls smartmontools on Windows systems. |
|
It retrieves release metadata directly from the GitHub API, discovers the correct |
|
installer asset for the requested version, downloads the installer and its MD5 |
|
checksum, validates file integrity, removes Mark-of-the-Web (MOTW), and performs |
|
a fully silent installation. |
|
|
|
If a specific version is requested via -TargetVersion, the script will install that version. |
|
If the requested version is lower than the installed version, the script will uninstall the |
|
current version before installing the target. If the requested version equals the installed |
|
version, no action is taken unless -Force is specified. |
|
|
|
When -Uninstall is specified, the script will only uninstall smartmontools (if installed). |
|
If -RestartServiceName is provided, the specified service will be restarted after install, |
|
downgrade, or uninstall. This is useful for services that probe for smartmontools on startup. |
|
|
|
When -Cleanup is used, temporary files (installer, checksum, stdout/stderr logs) |
|
are removed after a successful operation. Cleanup is skipped on failure to |
|
preserve artifacts for debugging and auditing. |
|
|
|
Existing installations are detected through registry keys, where.exe, and default installation |
|
paths. |
|
|
|
The script supports JSON output for RMM/CI/CD systems and writes operational logs |
|
to ProgramData with timestamped entries. |
|
|
|
This script is licensed under the Apache License, Version 2.0. |
|
See the LICENSE section in this script. |
|
|
|
DISCLAIMER: |
|
This script is provided "as-is" without any warranties or guarantees. |
|
Use at your own risk. The author assumes no liability for any damages, |
|
data loss, misconfiguration, or operational impact resulting from the |
|
use of this script. Always test in a controlled environment before |
|
deploying to production. |
|
|
|
(c) 2026 Michel Sijmons - https://mickert.dev |
|
|
|
.PARAMETER TargetVersion |
|
Optional. Installs a specific smartmontools version instead of the latest. |
|
If the target version is lower than the installed version, the script will |
|
uninstall the current version before installing the requested version. |
|
If the target version equals the installed version, no action is taken |
|
unless -Force is specified. |
|
|
|
.PARAMETER Uninstall |
|
Uninstalls smartmontools if it is installed. No installation or update |
|
will be performed when this switch is used. If smartmontools is not |
|
installed, the script exits gracefully. If -RestartServiceName is |
|
specified, the service will be restarted after uninstall (or attempted |
|
uninstall). |
|
|
|
.PARAMETER RestartServiceName |
|
Optional. Restarts the specified Windows service after installation, |
|
downgrade, or uninstallation completes. This is useful for services |
|
that probe for smartmontools on startup or require a refresh after |
|
changes. |
|
|
|
.PARAMETER Force |
|
Forces installation even if the installed version appears up-to-date or |
|
matches the requested TargetVersion. |
|
|
|
.PARAMETER Json |
|
Outputs structured JSON for RMM/CI/CD pipelines. |
|
|
|
.PARAMETER Cleanup |
|
Removes downloaded temporary files (installer, checksum, and captured |
|
stdout/stderr logs) from $env:TEMP after a successful installation, |
|
update, downgrade, or uninstall. This option is disabled by default |
|
to preserve artifacts for debugging and auditing. |
|
|
|
.PARAMETER Verbose |
|
Enables detailed logging output. This script uses Write-Verbose extensively |
|
to provide timestamped operational logs. Use -Verbose to display them. |
|
|
|
.PARAMETER WhatIf |
|
Shows what actions would be performed without making any changes. Applies to |
|
installation, downgrade, uninstall, cleanup, and service restart operations. |
|
|
|
.PARAMETER Confirm |
|
Prompts for confirmation before executing actions. Applies to installation, |
|
downgrade, uninstall, cleanup, and service restart operations. |
|
|
|
.PARAMETER Debug |
|
Enables debug output. Useful for troubleshooting script behavior. |
|
|
|
.EXAMPLE |
|
PS> .\ZyntriOps-SmartmontoolsInstaller.ps1 -Verbose |
|
|
|
.EXAMPLE |
|
PS> .\ZyntriOps-SmartmontoolsInstaller.ps1 -TargetVersion 7.4 |
|
|
|
.EXAMPLE |
|
PS> .\ZyntriOps-SmartmontoolsInstaller.ps1 -Uninstall -RestartServiceName "DiskMonitor" |
|
|
|
.EXAMPLE |
|
PS> .\ZyntriOps-SmartmontoolsInstaller.ps1 -Json | ConvertFrom-Json |
|
|
|
.NOTES |
|
Author : Michel Sijmons (ZyntriOps) - https://mickert.dev |
|
License : Apache License 2.0 |
|
Version : 1.3.1 |
|
|
|
This script must be run with administrative privileges. |
|
Internet access is required to retrieve release metadata and download installers. |
|
Temporary files are stored in $env:TEMP and removed only when -Cleanup is used. |
|
Logs are written to: $env:ProgramData\ZyntriOps\Logs\Installer\SmartmontoolsInstaller.log |
|
|
|
.LINK |
|
https://gists.mickert.dev/c502202ca7dd1a56e14b48e53faa411c#file-zyntriops-smartmontoolsinstaller-ps1 |
|
|
|
#> |
|
|
|
# ===================================================================== |
|
# NOTICE |
|
# ===================================================================== |
|
# This script is part of the ZyntriOps Toolkit. |
|
# Developed by Michel Sijmons. |
|
# Copyright (c) 2026 Michel Sijmons and licensed under the |
|
# Apache License, Version 2.0. See License section below. |
|
# |
|
# For more information, visit: https://zyntriops.com |
|
|
|
# ===================================================================== |
|
# DISCLAIMER & LICENSE |
|
# ===================================================================== |
|
# Licensed under the Apache License, Version 2.0 (the "License"); |
|
# you may not use this file except in compliance with the License. |
|
# You may obtain a copy of the License at: |
|
# |
|
# http://www.apache.org/licenses/LICENSE-2.0 |
|
# |
|
# Unless required by applicable law or agreed to in writing, software |
|
# distributed under the License is distributed on an "AS IS" BASIS, |
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
# See the License for the specific language governing permissions and |
|
# limitations under the License. |
|
# |
|
# DISCLAIMER: |
|
# This script is provided "as-is" without any warranties or guarantees. |
|
# Use at your own risk. The author assumes no liability for any damages, |
|
# data loss, misconfiguration, or operational impact resulting from the |
|
# use of this script. Always test in a controlled environment before |
|
# deploying to production. |
|
|
|
# ===================================================================== |
|
# COPYRIGHT |
|
# ===================================================================== |
|
# Copyright (c) 2026 Michel Sijmons |
|
# All rights reserved. |
|
|
|
[CmdletBinding(SupportsShouldProcess = $true)] |
|
param( |
|
[string]$TargetVersion, |
|
[string]$RestartServiceName, |
|
[switch]$Force, |
|
[switch]$Json, |
|
[switch]$Uninstall, |
|
[switch]$Cleanup |
|
) |
|
|
|
# ===================================================================== |
|
# GLOBALS |
|
# ===================================================================== |
|
$script:ModuleName = "ZyntriOps.SmartmontoolsInstaller" |
|
$script:ScriptVersion = "1.3.1" |
|
$script:VerboseLoggingEnabled = $false |
|
$script:LogFile = $null |
|
|
|
# Initialize log file |
|
try { |
|
$logRoot = Join-Path $env:ProgramData "ZyntriOps\Logs\Installer" |
|
if (-not (Test-Path $logRoot)) { |
|
New-Item -ItemType Directory -Path $logRoot -Force | Out-Null |
|
} |
|
$script:LogFile = Join-Path $logRoot "SmartmontoolsInstaller.log" |
|
} |
|
catch { |
|
$script:LogFile = $null |
|
} |
|
|
|
if ($PSBoundParameters.ContainsKey('Verbose')) { |
|
$script:VerboseLoggingEnabled = $true |
|
} |
|
|
|
# ===================================================================== |
|
# LOGGING |
|
# ===================================================================== |
|
function Write-ZOLog { |
|
param( |
|
[Parameter(Mandatory = $true)][string]$Message, |
|
[ValidateSet("INFO","WARN","ERROR","DEBUG")][string]$Level = "INFO" |
|
) |
|
|
|
$timestamp = Get-Date -Format o |
|
$line = "[$timestamp] [$Level] $Message" |
|
|
|
if ($script:LogFile) { |
|
try { Add-Content -LiteralPath $script:LogFile -Value $line } catch {} |
|
} |
|
|
|
switch ($Level) { |
|
"ERROR" { |
|
Write-Error $line |
|
Write-Verbose $line |
|
} |
|
"DEBUG" { |
|
if ($script:VerboseLoggingEnabled) { Write-Verbose $line } |
|
} |
|
default { |
|
Write-Verbose $line |
|
} |
|
} |
|
} |
|
|
|
# ===================================================================== |
|
# NETWORK |
|
# ===================================================================== |
|
function Invoke-ZOWebRequest { |
|
[CmdletBinding()] |
|
[OutputType([bool])] |
|
param( |
|
[string]$Url, |
|
[string]$OutFile |
|
) |
|
|
|
Write-ZOLog "Downloading: $Url" "INFO" |
|
|
|
try { |
|
Invoke-WebRequest -Uri $Url -OutFile $OutFile -UseBasicParsing -TimeoutSec 60 |
|
return $true |
|
} |
|
catch { |
|
Write-ZOLog "Download failed: $($_.Exception.Message)" "ERROR" |
|
return $false |
|
} |
|
} |
|
|
|
# ===================================================================== |
|
# DETECTION |
|
# ===================================================================== |
|
function Get-ZOSmartmontoolsInstallInfo { |
|
[CmdletBinding()] |
|
[OutputType([PSCustomObject])] |
|
param() |
|
|
|
$paths = @( |
|
"HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\smartmontools", |
|
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\smartmontools" |
|
) |
|
|
|
foreach ($p in $paths) { |
|
if (Test-Path $p) { |
|
$key = Get-ItemProperty $p |
|
return [PSCustomObject]@{ |
|
DisplayVersion = $key.DisplayVersion |
|
InstallLocation = $key.InstallLocation |
|
UninstallString = $key.UninstallString |
|
} |
|
} |
|
} |
|
|
|
return $null |
|
} |
|
|
|
function Get-ZOSmartctlPath { |
|
[CmdletBinding()] |
|
[OutputType([string])] |
|
param() |
|
|
|
$info = Get-ZOSmartmontoolsInstallInfo |
|
if ($info -and $info.InstallLocation) { |
|
$candidate = Join-Path $info.InstallLocation "bin\smartctl.exe" |
|
if (Test-Path $candidate) { return $candidate } |
|
} |
|
|
|
$where = & where.exe smartctl 2>$null |
|
if ($where -and (Test-Path $where)) { return $where } |
|
|
|
$default = "C:\Program Files\smartmontools\bin\smartctl.exe" |
|
if (Test-Path $default) { return $default } |
|
|
|
return $null |
|
} |
|
|
|
function Get-ZOSmartmontoolsInstalledVersion { |
|
[CmdletBinding()] |
|
[OutputType([string])] |
|
param() |
|
|
|
$smartctl = Get-ZOSmartctlPath |
|
if (-not $smartctl) { return $null } |
|
|
|
try { |
|
$output = & $smartctl --version 2>$null |
|
if (-not $output) { return $null } |
|
|
|
$line = $output | Select-Object -First 1 |
|
if ($line -match "smartctl\s+([\d\.]+)") { |
|
return $Matches[1] |
|
} |
|
} |
|
catch { |
|
Write-ZOLog "Failed to query smartctl version: $($_.Exception.Message)" "WARN" |
|
} |
|
|
|
return $null |
|
} |
|
|
|
# ===================================================================== |
|
# UNINSTALL (with /S enforcement) |
|
# ===================================================================== |
|
function Uninstall-ZOSmartmontools { |
|
[CmdletBinding(SupportsShouldProcess = $true)] |
|
[OutputType([bool])] |
|
param() |
|
|
|
$info = Get-ZOSmartmontoolsInstallInfo |
|
if (-not $info -or -not $info.UninstallString) { |
|
Write-ZOLog "No uninstall information found; skipping uninstall." "WARN" |
|
return $true |
|
} |
|
|
|
$uninstallCmd = $info.UninstallString.Trim() |
|
|
|
# Ensure /S is present |
|
if ($uninstallCmd -notmatch "/S\b") { |
|
Write-ZOLog "Adding /S to uninstall parameters." "INFO" |
|
$uninstallCmd = "$uninstallCmd /S" |
|
} |
|
|
|
Write-ZOLog "Final uninstall command: $uninstallCmd" "DEBUG" |
|
|
|
if ($PSCmdlet.ShouldProcess("smartmontools", "Uninstall")) { |
|
try { |
|
$arguments = "/c `"$uninstallCmd`"" |
|
$proc = Start-Process -FilePath "cmd.exe" -ArgumentList $arguments -Wait -PassThru |
|
|
|
if ($proc.ExitCode -ne 0) { |
|
Write-ZOLog "Uninstall returned exit code $($proc.ExitCode)" "ERROR" |
|
return $false |
|
} |
|
|
|
Write-ZOLog "Uninstall completed successfully." "INFO" |
|
return $true |
|
} |
|
catch { |
|
Write-ZOLog "Uninstall failed: $($_.Exception.Message)" "ERROR" |
|
return $false |
|
} |
|
} |
|
|
|
return $true |
|
} |
|
|
|
# ===================================================================== |
|
# GITHUB RELEASE DISCOVERY (future-proof) |
|
# ===================================================================== |
|
function Get-ZOSmartmontoolsDownloadUrls { |
|
[CmdletBinding()] |
|
[OutputType([PSCustomObject])] |
|
param( |
|
[Parameter(Mandatory = $true)] |
|
[string]$Version |
|
) |
|
|
|
# Convert version to tag format: 7.5.1 → RELEASE_7_5_1 |
|
$tag = "RELEASE_" + ($Version -replace "\.", "_") |
|
|
|
$api = "https://api.github.com/repos/smartmontools/smartmontools/releases/tags/$tag" |
|
Write-ZOLog "Querying GitHub for release tag $tag" "INFO" |
|
|
|
try { |
|
$release = Invoke-RestMethod -Uri $api -Headers @{ "User-Agent" = "ZyntriOps" } |
|
} |
|
catch { |
|
throw "Failed to retrieve release metadata for ${Version}: $($_.Exception.Message)" |
|
} |
|
|
|
# Find installer and MD5 assets |
|
$installer = $release.assets | Where-Object { $_.name -match "win32-setup\.exe$" } |
|
$md5 = $release.assets | Where-Object { $_.name -match "win32-setup\.exe\.md5$" } |
|
|
|
if (-not $installer) { throw "Could not locate installer asset for version $Version" } |
|
if (-not $md5) { throw "Could not locate MD5 asset for version $Version" } |
|
|
|
return [PSCustomObject]@{ |
|
Installer = $installer.browser_download_url |
|
Md5 = $md5.browser_download_url |
|
} |
|
} |
|
|
|
# ===================================================================== |
|
# SERVICE RESTART |
|
# ===================================================================== |
|
function Restart-ZOService { |
|
[CmdletBinding(SupportsShouldProcess = $true)] |
|
[OutputType([bool])] |
|
param([string]$Name) |
|
|
|
Write-ZOLog "Requested restart of service '$Name'." "INFO" |
|
|
|
try { |
|
Get-Service -Name $Name -ErrorAction Stop | Out-Null |
|
} |
|
catch { |
|
Write-ZOLog "Service '$Name' not found: $($_.Exception.Message)" "ERROR" |
|
return $false |
|
} |
|
|
|
if ($PSCmdlet.ShouldProcess("Service $Name", "Restart")) { |
|
try { |
|
Restart-Service -Name $Name -Force -ErrorAction Stop |
|
Write-ZOLog "Service '$Name' restarted successfully." "INFO" |
|
return $true |
|
} |
|
catch { |
|
Write-ZOLog "Failed to restart service '$Name': $($_.Exception.Message)" "ERROR" |
|
return $false |
|
} |
|
} |
|
|
|
return $false |
|
} |
|
|
|
# ===================================================================== |
|
# CLEANUP |
|
# ===================================================================== |
|
function Remove-ZOTempFile { |
|
[CmdletBinding(SupportsShouldProcess = $true)] |
|
[OutputType([bool])] |
|
param([string]$Path) |
|
|
|
if (-not (Test-Path $Path)) { |
|
Write-ZOLog "Temporary file not found: $Path" "WARN" |
|
return $true |
|
} |
|
|
|
if ($PSCmdlet.ShouldProcess($Path, "Remove temporary file")) { |
|
try { |
|
Remove-Item -Path $Path -Force -ErrorAction Stop |
|
Write-ZOLog "Removed temporary file: $Path" "INFO" |
|
return $true |
|
} |
|
catch { |
|
Write-ZOLog "Failed to remove temporary file '$Path': $($_.Exception.Message)" "ERROR" |
|
return $false |
|
} |
|
} |
|
|
|
return $true |
|
} |
|
|
|
# ===================================================================== |
|
# MAIN LOGIC — INITIALIZATION |
|
# ===================================================================== |
|
Write-ZOLog "Starting $script:ModuleName v$script:ScriptVersion" "INFO" |
|
|
|
$installedRaw = Get-ZOSmartmontoolsInstalledVersion |
|
$installedVer = $null |
|
if ($installedRaw) { |
|
try { $installedVer = [version]$installedRaw } catch {} |
|
} |
|
|
|
# Determine latest version |
|
try { |
|
$latestApi = "https://api.github.com/repos/smartmontools/smartmontools/releases/latest" |
|
$latestJson = Invoke-RestMethod -Uri $latestApi -Headers @{ "User-Agent" = "ZyntriOps" } |
|
$latestRaw = ($latestJson.tag_name -replace "RELEASE_", "") -replace "_", "." |
|
$latestVer = [version]$latestRaw |
|
} |
|
catch { |
|
Write-ZOLog "GitHub API unavailable, falling back to 7.5" "WARN" |
|
$latestRaw = "7.5" |
|
$latestVer = [version]"7.5" |
|
} |
|
|
|
Write-ZOLog "Installed version: $installedRaw" "INFO" |
|
Write-ZOLog "Latest version : $latestRaw" "INFO" |
|
|
|
$installerPath = Join-Path $env:TEMP "smartmontools-setup.exe" |
|
$md5Path = Join-Path $env:TEMP "smartmontools-setup.exe.md5" |
|
$stdoutPath = Join-Path $env:TEMP "smartmontools-stdout.log" |
|
$stderrPath = Join-Path $env:TEMP "smartmontools-stderr.log" |
|
|
|
$cleanupOK = $false |
|
$serviceRestarted = $false |
|
$rebootRequired = $false |
|
$installerExitCode = 0 |
|
|
|
# ===================================================================== |
|
# UNINSTALL MODE |
|
# ===================================================================== |
|
if ($Uninstall) { |
|
Write-ZOLog "Uninstall mode requested." "INFO" |
|
|
|
$info = Get-ZOSmartmontoolsInstallInfo |
|
|
|
if (-not $info) { |
|
Write-ZOLog "smartmontools is not installed. Nothing to uninstall." "WARN" |
|
|
|
if ($RestartServiceName) { |
|
$serviceRestarted = Restart-ZOService -Name $RestartServiceName |
|
if (-not $serviceRestarted) { |
|
if ($Json) { |
|
[PSCustomObject]@{ |
|
Status = "NotInstalled" |
|
Action = "Uninstall" |
|
ServiceRestarted = $RestartServiceName |
|
ServiceRestartOK = $serviceRestarted |
|
CleanupRequested = $Cleanup.IsPresent |
|
CleanupOK = $cleanupOK |
|
} | ConvertTo-Json -Depth 5 |
|
} |
|
exit 1 |
|
} |
|
} |
|
|
|
if ($Cleanup) { |
|
$cleanupOK = ( |
|
(Remove-ZOTempFile -Path $installerPath) -and |
|
(Remove-ZOTempFile -Path $md5Path) -and |
|
(Remove-ZOTempFile -Path $stdoutPath) -and |
|
(Remove-ZOTempFile -Path $stderrPath) |
|
) |
|
} |
|
|
|
if ($Json) { |
|
[PSCustomObject]@{ |
|
Status = "NotInstalled" |
|
Action = "Uninstall" |
|
ServiceRestarted = $RestartServiceName |
|
ServiceRestartOK = $serviceRestarted |
|
CleanupRequested = $Cleanup.IsPresent |
|
CleanupOK = $cleanupOK |
|
} | ConvertTo-Json -Depth 5 |
|
} |
|
exit 0 |
|
} |
|
|
|
$uninstallOK = Uninstall-ZOSmartmontools |
|
if (-not $uninstallOK) { |
|
if ($Json) { |
|
[PSCustomObject]@{ |
|
Status = "UninstallFailed" |
|
OldVersion = $installedRaw |
|
ServiceRestarted = $RestartServiceName |
|
ServiceRestartOK = $false |
|
CleanupRequested = $Cleanup.IsPresent |
|
CleanupOK = $cleanupOK |
|
} | ConvertTo-Json -Depth 5 |
|
} |
|
exit 1 |
|
} |
|
|
|
if ($RestartServiceName) { |
|
$serviceRestarted = Restart-ZOService -Name $RestartServiceName |
|
if (-not $serviceRestarted) { |
|
if ($Json) { |
|
[PSCustomObject]@{ |
|
Status = "Uninstalled" |
|
OldVersion = $installedRaw |
|
ServiceRestarted = $RestartServiceName |
|
ServiceRestartOK = $serviceRestarted |
|
CleanupRequested = $Cleanup.IsPresent |
|
CleanupOK = $cleanupOK |
|
} | ConvertTo-Json -Depth 5 |
|
} |
|
exit 1 |
|
} |
|
} |
|
|
|
if ($Cleanup) { |
|
$cleanupOK = ( |
|
(Remove-ZOTempFile -Path $installerPath) -and |
|
(Remove-ZOTempFile -Path $md5Path) -and |
|
(Remove-ZOTempFile -Path $stdoutPath) -and |
|
(Remove-ZOTempFile -Path $stderrPath) |
|
) |
|
} |
|
|
|
if ($Json) { |
|
[PSCustomObject]@{ |
|
Status = "Uninstalled" |
|
OldVersion = $installedRaw |
|
ServiceRestarted = $RestartServiceName |
|
ServiceRestartOK = $serviceRestarted |
|
CleanupRequested = $Cleanup.IsPresent |
|
CleanupOK = $cleanupOK |
|
} | ConvertTo-Json -Depth 5 |
|
} |
|
exit 0 |
|
} |
|
|
|
# ===================================================================== |
|
# INSTALL / UPDATE / DOWNGRADE |
|
# ===================================================================== |
|
|
|
# Determine desired version |
|
if ($TargetVersion) { |
|
try { |
|
$desiredVer = [version]$TargetVersion |
|
$desiredRaw = $TargetVersion |
|
} |
|
catch { |
|
Write-ZOLog "TargetVersion '$TargetVersion' is not a valid version." "ERROR" |
|
if ($Json) { |
|
[PSCustomObject]@{ |
|
Status = "InvalidTargetVersion" |
|
TargetVersion = $TargetVersion |
|
} | ConvertTo-Json -Depth 5 |
|
} |
|
exit 1 |
|
} |
|
Write-ZOLog "Target version : $desiredRaw" "INFO" |
|
} |
|
else { |
|
$desiredVer = $latestVer |
|
$desiredRaw = $latestRaw |
|
Write-ZOLog "No TargetVersion specified; using latest: $desiredRaw" "INFO" |
|
} |
|
|
|
# No action needed |
|
if ($installedVer -and $desiredVer -eq $installedVer -and -not $Force) { |
|
Write-ZOLog "Installed version already matches desired version ($desiredRaw). No action required." "INFO" |
|
if ($Json) { |
|
[PSCustomObject]@{ |
|
Status = "UpToDate" |
|
InstalledVersion = $installedRaw |
|
DesiredVersion = $desiredRaw |
|
LatestVersion = $latestRaw |
|
CleanupRequested = $Cleanup.IsPresent |
|
CleanupOK = $cleanupOK |
|
} | ConvertTo-Json -Depth 5 |
|
} |
|
exit 0 |
|
} |
|
|
|
# Downgrade detection |
|
$downgrade = $false |
|
if ($installedVer -and $desiredVer -lt $installedVer) { |
|
$downgrade = $true |
|
Write-ZOLog "Requested version ($desiredRaw) is lower than installed version ($installedRaw). Downgrade will uninstall first." "WARN" |
|
} |
|
|
|
# Resolve download URLs via GitHub API |
|
try { |
|
$urls = Get-ZOSmartmontoolsDownloadUrls -Version $desiredRaw |
|
} |
|
catch { |
|
Write-ZOLog "Failed to resolve download URLs for version '$desiredRaw': $($_.Exception.Message)" "ERROR" |
|
if ($Json) { |
|
[PSCustomObject]@{ |
|
Status = "UrlResolutionFailed" |
|
DesiredVersion = $desiredRaw |
|
LatestVersion = $latestRaw |
|
} | ConvertTo-Json -Depth 5 |
|
} |
|
exit 1 |
|
} |
|
|
|
# Downgrade uninstall |
|
if ($downgrade) { |
|
$uninstallOK = Uninstall-ZOSmartmontools |
|
if (-not $uninstallOK) { |
|
if ($Json) { |
|
[PSCustomObject]@{ |
|
Status = "DowngradeUninstallFailed" |
|
OldVersion = $installedRaw |
|
DesiredVersion = $desiredRaw |
|
LatestVersion = $latestRaw |
|
CleanupRequested = $Cleanup.IsPresent |
|
CleanupOK = $cleanupOK |
|
} | ConvertTo-Json -Depth 5 |
|
} |
|
exit 1 |
|
} |
|
} |
|
|
|
# Download installer |
|
if (-not (Invoke-ZOWebRequest -Url $urls.Installer -OutFile $installerPath)) { |
|
if ($Json) { |
|
[PSCustomObject]@{ |
|
Status = "InstallerDownloadFailed" |
|
DesiredVersion = $desiredRaw |
|
LatestVersion = $latestRaw |
|
CleanupRequested = $Cleanup.IsPresent |
|
CleanupOK = $cleanupOK |
|
} | ConvertTo-Json -Depth 5 |
|
} |
|
exit 1 |
|
} |
|
|
|
# Download MD5 |
|
if (-not (Invoke-ZOWebRequest -Url $urls.Md5 -OutFile $md5Path)) { |
|
if ($Json) { |
|
[PSCustomObject]@{ |
|
Status = "Md5DownloadFailed" |
|
DesiredVersion = $desiredRaw |
|
LatestVersion = $latestRaw |
|
CleanupRequested = $Cleanup.IsPresent |
|
CleanupOK = $cleanupOK |
|
} | ConvertTo-Json -Depth 5 |
|
} |
|
exit 1 |
|
} |
|
|
|
# Remove MOTW |
|
try { |
|
Unblock-File -Path $installerPath |
|
Write-ZOLog "Removed Mark-of-the-Web from installer." "INFO" |
|
} |
|
catch { |
|
Write-ZOLog "Failed to remove MOTW: $($_.Exception.Message)" "WARN" |
|
} |
|
|
|
# MD5 validation |
|
try { |
|
$expected = (Get-Content $md5Path).Split(" ")[0].Trim() |
|
$actual = (Get-FileHash -Path $installerPath -Algorithm MD5).Hash.ToLower() |
|
|
|
if ($expected -ne $actual) { |
|
Write-ZOLog "MD5 checksum mismatch. Expected: $expected, Actual: $actual. Aborting." "ERROR" |
|
if ($Json) { |
|
[PSCustomObject]@{ |
|
Status = "Md5Mismatch" |
|
DesiredVersion = $desiredRaw |
|
LatestVersion = $latestRaw |
|
CleanupRequested = $Cleanup.IsPresent |
|
CleanupOK = $cleanupOK |
|
} | ConvertTo-Json -Depth 5 |
|
} |
|
exit 1 |
|
} |
|
|
|
Write-ZOLog "MD5 checksum validated successfully." "INFO" |
|
} |
|
catch { |
|
Write-ZOLog "MD5 validation failed: $($_.Exception.Message)" "ERROR" |
|
if ($Json) { |
|
[PSCustomObject]@{ |
|
Status = "Md5ValidationFailed" |
|
DesiredVersion = $desiredRaw |
|
LatestVersion = $latestRaw |
|
CleanupRequested = $Cleanup.IsPresent |
|
CleanupOK = $cleanupOK |
|
} | ConvertTo-Json -Depth 5 |
|
} |
|
exit 1 |
|
} |
|
|
|
# Run installer |
|
if ($PSCmdlet.ShouldProcess("smartmontools $desiredRaw", "Install/Update")) { |
|
Write-ZOLog "Running silent installer for version $desiredRaw..." "INFO" |
|
|
|
try { |
|
$proc = Start-Process -FilePath $installerPath ` |
|
-ArgumentList "/S" ` |
|
-Wait ` |
|
-PassThru ` |
|
-RedirectStandardOutput $stdoutPath ` |
|
-RedirectStandardError $stderrPath |
|
|
|
$installerExitCode = $proc.ExitCode |
|
|
|
if (Test-Path $stdoutPath) { |
|
Get-Content $stdoutPath | ForEach-Object { Write-ZOLog $_ "INFO" } |
|
} |
|
if (Test-Path $stderrPath) { |
|
Get-Content $stderrPath | ForEach-Object { Write-ZOLog $_ "ERROR" } |
|
} |
|
|
|
if ($installerExitCode -ne 0 -and $installerExitCode -ne 3010) { |
|
Write-ZOLog "Installer returned exit code $installerExitCode" "ERROR" |
|
if ($Json) { |
|
[PSCustomObject]@{ |
|
Status = "InstallFailed" |
|
OldVersion = $installedRaw |
|
DesiredVersion = $desiredRaw |
|
LatestVersion = $latestRaw |
|
InstallerExitCode = $installerExitCode |
|
RebootRequired = $false |
|
CleanupRequested = $Cleanup.IsPresent |
|
CleanupOK = $cleanupOK |
|
} | ConvertTo-Json -Depth 5 |
|
} |
|
exit $installerExitCode |
|
} |
|
|
|
if ($installerExitCode -eq 3010) { |
|
Write-ZOLog "Installer indicates reboot required (3010)." "WARN" |
|
$rebootRequired = $true |
|
} |
|
|
|
Write-ZOLog "Installation completed successfully." "INFO" |
|
} |
|
catch { |
|
Write-ZOLog "Installer execution failed: $($_.Exception.Message)" "ERROR" |
|
if ($Json) { |
|
[PSCustomObject]@{ |
|
Status = "InstallerExecutionFailed" |
|
OldVersion = $installedRaw |
|
DesiredVersion = $desiredRaw |
|
LatestVersion = $latestRaw |
|
CleanupRequested = $Cleanup.IsPresent |
|
CleanupOK = $cleanupOK |
|
} | ConvertTo-Json -Depth 5 |
|
} |
|
exit 1 |
|
} |
|
} |
|
|
|
# Query new version |
|
$newRaw = Get-ZOSmartmontoolsInstalledVersion |
|
Write-ZOLog "New installed version: $newRaw" "INFO" |
|
|
|
# Restart service if requested |
|
if ($RestartServiceName) { |
|
$serviceRestarted = Restart-ZOService -Name $RestartServiceName |
|
if (-not $serviceRestarted) { |
|
if ($Json) { |
|
[PSCustomObject]@{ |
|
Status = "Installed" |
|
OldVersion = $installedRaw |
|
NewVersion = $newRaw |
|
DesiredVersion = $desiredRaw |
|
LatestVersion = $latestRaw |
|
Downgrade = $downgrade |
|
ServiceRestarted = $RestartServiceName |
|
ServiceRestartOK = $serviceRestarted |
|
InstallerExitCode = $installerExitCode |
|
RebootRequired = $rebootRequired |
|
CleanupRequested = $Cleanup.IsPresent |
|
CleanupOK = $cleanupOK |
|
} | ConvertTo-Json -Depth 5 |
|
} |
|
exit 1 |
|
} |
|
} |
|
|
|
# Cleanup |
|
if ($Cleanup) { |
|
$cleanupOK = ( |
|
(Remove-ZOTempFile -Path $installerPath) -and |
|
(Remove-ZOTempFile -Path $md5Path) -and |
|
(Remove-ZOTempFile -Path $stdoutPath) -and |
|
(Remove-ZOTempFile -Path $stderrPath) |
|
) |
|
} |
|
|
|
# JSON output |
|
if ($Json) { |
|
[PSCustomObject]@{ |
|
Status = "Installed" |
|
OldVersion = $installedRaw |
|
NewVersion = $newRaw |
|
DesiredVersion = $desiredRaw |
|
LatestVersion = $latestRaw |
|
Downgrade = $downgrade |
|
ServiceRestarted = $RestartServiceName |
|
ServiceRestartOK = $serviceRestarted |
|
InstallerExitCode = $installerExitCode |
|
RebootRequired = $rebootRequired |
|
CleanupRequested = $Cleanup.IsPresent |
|
CleanupOK = $cleanupOK |
|
} | ConvertTo-Json -Depth 5 |
|
} |
|
|
|
# Exit code propagation |
|
if ($installerExitCode -ne 0 -and $installerExitCode -ne 3010) { |
|
exit $installerExitCode |
|
} |
|
|
|
exit 0 |
|
|