# InventoryHQ installer for Windows 10/11. # # In PowerShell: # # irm https://ihq.monolith.ng/setup | iex # # To pass options, set them first: # # $env:IHQ_PORT = 8080; irm https://ihq.monolith.ng/setup | iex # # IHQ_PORT port for the console (default 80) # IHQ_HOSTNAME name for this machine's own address (default inventory-hq) # IHQ_OFFLINE=1 no cloud backups or remote updates # IHQ_CHANNEL stable (default) or beta # IHQ_BUNDLE path to a downloaded bundle, instead of fetching one # IHQ_OPEN_FIREWALL=1 let other devices on the network reach the console # IHQ_DRY_RUN=1 download and check everything, install nothing # # What it does: asks https://inventory-hq-api.monolith.ng for the newest release, downloads it, checks # its SHA-256, then hands over to the installer inside it, which sets up # Node.js, the background service, the update schedule and the database. Safe # to run again: it repairs an install and keeps its data. # # It needs administrator rights and asks for them (a Windows prompt) if the # window it runs in does not have them. param( [int]$Port = 0, [string]$Hostname = '', [switch]$Offline, [string]$Channel = '', [string]$Bundle = '', [switch]$OpenFirewall, [switch]$DryRun ) $ErrorActionPreference = 'Stop' # A progress bar slows Invoke-WebRequest down badly in Windows PowerShell 5.1. $ProgressPreference = 'SilentlyContinue' [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $FleetUrl = 'https://inventory-hq-api.monolith.ng' $SetupUrl = 'https://ihq.monolith.ng/setup' function Say([string]$text) { Write-Host "==> $text" -ForegroundColor Cyan } function Good([string]$text) { Write-Host " OK $text" -ForegroundColor Green } function Fail([string]$text) { Write-Host "ERROR $text" -ForegroundColor Red; throw $text } # Options: an explicit parameter wins, then the environment, then the default. if ($Port -eq 0) { if ($env:IHQ_PORT) { $Port = [int]$env:IHQ_PORT } else { $Port = 80 } } if (-not $Hostname) { if ($env:IHQ_HOSTNAME) { $Hostname = $env:IHQ_HOSTNAME } else { $Hostname = 'inventory-hq' } } if (-not $Channel) { if ($env:IHQ_CHANNEL) { $Channel = $env:IHQ_CHANNEL } else { $Channel = 'stable' } } if (-not $Bundle) { $Bundle = $env:IHQ_BUNDLE } if (-not $Offline) { $Offline = [bool]($env:IHQ_OFFLINE -eq '1') } if (-not $OpenFirewall) { $OpenFirewall = [bool]($env:IHQ_OPEN_FIREWALL -eq '1') } if (-not $DryRun) { $DryRun = [bool]($env:IHQ_DRY_RUN -eq '1') } if ($Channel -notin 'stable', 'beta') { Fail 'The channel must be stable or beta.' } if ($Port -lt 1 -or $Port -gt 65535) { Fail 'The port must be between 1 and 65535.' } if ($Hostname -notmatch '^[A-Za-z0-9][A-Za-z0-9.-]*$') { Fail 'That hostname has characters a hosts entry cannot hold.' } if ($env:OS -ne 'Windows_NT') { Fail 'This script is for Windows. On macOS or Linux run: curl -fsSL https://ihq.monolith.ng/setup | sh' } # ------------------------------------------------------------- elevation ---- $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $isAdmin = ([Security.Principal.WindowsPrincipal]$identity).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (-not $isAdmin -and -not $DryRun) { Say 'InventoryHQ needs administrator rights to install its service. A Windows prompt will ask.' # The script came through a pipe, so there is no file to relaunch: fetch the # same script again and run that elevated, carrying the options across. $copy = Join-Path $env:TEMP 'inventory-hq-setup.ps1' Invoke-WebRequest -UseBasicParsing -Uri "$SetupUrl`?os=windows" -OutFile $copy $relaunch = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', "`"$copy`"", '-Port', "$Port", '-Hostname', $Hostname, '-Channel', $Channel) if ($Offline) { $relaunch += '-Offline' } if ($OpenFirewall) { $relaunch += '-OpenFirewall' } if ($Bundle) { $relaunch += @('-Bundle', "`"$Bundle`"") } try { $proc = Start-Process -FilePath 'powershell.exe' -ArgumentList $relaunch -Verb RunAs -Wait -PassThru } catch { Fail 'Administrator rights were not given, so nothing was installed.' } finally { Remove-Item -Force $copy -ErrorAction SilentlyContinue } if ($proc.ExitCode -ne 0) { Fail "The installer stopped with code $($proc.ExitCode)." } return } Write-Host '' Write-Host 'InventoryHQ setup (Windows)' -ForegroundColor White Write-Host '' $Root = if ($env:INVENTORY_HQ_ROOT) { $env:INVENTORY_HQ_ROOT } else { 'C:\inventory-hq' } $Work = Join-Path $env:TEMP ("inventory-hq-" + [guid]::NewGuid().ToString('N').Substring(0, 8)) New-Item -ItemType Directory -Force -Path $Work | Out-Null try { # ----------------------------------------------------------- the bundle -- $zip = Join-Path $Work 'inventory-hq.zip' if ($Bundle) { if (-not (Test-Path $Bundle)) { Fail "No such file: $Bundle" } Say "Using the bundle you gave: $Bundle" Copy-Item $Bundle $zip $sha = (Get-FileHash -Algorithm SHA256 $zip).Hash.ToLower() $version = 'unknown' } else { Say "Asking $FleetUrl for the newest $Channel release" try { $reply = Invoke-RestMethod -UseBasicParsing -Uri "$FleetUrl/v1/public/releases/latest?component=local-client&channel=$Channel" } catch { Fail "Could not reach $FleetUrl. Check this machine's internet connection." } $release = $reply.data if (-not $release -or -not $release.version -or -not $release.checksum -or -not $release.downloadUrl) { Fail 'The release information was incomplete. Nothing has been changed.' } $version = [string]$release.version $sha = ([string]$release.checksum).ToLower() if ($version -notmatch '^[0-9A-Za-z.+-]+$') { Fail "The release has an unexpected version: $version" } if ($sha -notmatch '^[0-9a-f]{64}$') { Fail 'The release has an unexpected checksum.' } if (([string]$release.downloadUrl) -notmatch '^https?://') { Fail 'The release has an unexpected download address.' } Say "Newest release: $version" Say 'Downloading' Invoke-WebRequest -UseBasicParsing -Uri ([string]$release.downloadUrl) -OutFile $zip $actual = (Get-FileHash -Algorithm SHA256 $zip).Hash.ToLower() if ($actual -ne $sha) { Fail 'The download is damaged (checksum mismatch). Nothing has been changed. Try again.' } Good 'Download verified' } # The bundle carries its own installer; take only that out. Add-Type -AssemblyName System.IO.Compression.FileSystem $boot = Join-Path $Work 'bundle' New-Item -ItemType Directory -Force -Path $boot | Out-Null $archive = [System.IO.Compression.ZipFile]::OpenRead($zip) try { foreach ($entry in $archive.Entries) { if ($entry.FullName -like 'deploy/*' -and $entry.Name) { $target = Join-Path $boot ($entry.FullName -replace '/', '\') New-Item -ItemType Directory -Force -Path (Split-Path -Parent $target) | Out-Null [System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $target, $true) } } } finally { $archive.Dispose() } $installer = Join-Path $boot 'deploy\windows\setup.ps1' if (-not (Test-Path $installer)) { Fail 'This release has no Windows installer.' } if ($DryRun) { Good "Dry run: release $version is downloaded and verified, and its installer is present." Write-Host " It would be installed to $Root." return } # ------------------------------------------------------------ stage it -- # The installer applies whatever sits in staging\dist.zip. Copied under a # temporary name and renamed, so the update task never sees a half-written file. $staging = Join-Path $Root 'staging' New-Item -ItemType Directory -Force -Path $staging | Out-Null Copy-Item $zip (Join-Path $staging '.dist.zip.part') -Force Set-Content -Path (Join-Path $staging '.dist.zip.sha256.part') -Value "$sha dist.zip" -Encoding ASCII Move-Item -Force (Join-Path $staging '.dist.zip.sha256.part') (Join-Path $staging 'dist.zip.sha256') Move-Item -Force (Join-Path $staging '.dist.zip.part') (Join-Path $staging 'dist.zip') Say "Installing $version to $Root" $setupArgs = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $installer, '-Port', "$Port", '-Hostname', $Hostname) if ($Offline) { $setupArgs += '-Offline' } if ($OpenFirewall) { $setupArgs += '-OpenFirewall' } & powershell.exe @setupArgs if ($LASTEXITCODE -ne 0) { Fail "The installer stopped with code $LASTEXITCODE. See $Root\shared\logs\updater.log." } # ---------------------------------------------------------------- done -- $suffix = if ($Port -eq 80) { '' } else { ":$Port" } $consoleUrl = "http://localhost$suffix/" Write-Host '' Write-Host "InventoryHQ $version is installed." -ForegroundColor Green Write-Host " Open it here: $consoleUrl" Write-Host ' Next: create your admin account in the page that opens.' Write-Host '' if (-not $env:IHQ_NO_OPEN) { Start-Process $consoleUrl } } finally { Remove-Item -Recurse -Force $Work -ErrorAction SilentlyContinue }