#
.SYNOPSIS
Download, verify, configure, install and start one Mugnsoft component as a
Windows service, in one command. Run it from an elevated PowerShell.
.EXAMPLE
.\mugnsoft-install.ps1 webserver -License .\license_MNS.dat
.EXAMPLE
.\mugnsoft-install.ps1 monitor -Webserver 10.0.0.5:8050 -SharedInternal 3f9c...e1 -Location Paris
.PARAMETER Component
webserver, monitor, integrator, sentinel or webserver-front.
.PARAMETER Version
Release to install (default 4.2.0).
.PARAMETER Dir
Parent folder (default C:\Mugnsoft); the component goes in
\.
.PARAMETER Package
Install this package (.zip) instead of downloading it.
.PARAMETER NoVerify
Skip the GPG signature check (not recommended).
.PARAMETER License
webserver and integrator: the license_MNS.dat received from Mugnsoft.
.PARAMETER ApiPort
webserver: API port (default 8050).
.PARAMETER WebPort
webserver: web UI port (default 9090).
.PARAMETER Webserver
monitor, integrator, sentinel: the Webserver API address, host:port (e.g. 10.0.0.5:8050).
.PARAMETER SharedInternal
monitor, integrator, sentinel: the "_sharedInternal" value of the Webserver's webserver.json.
.PARAMETER Name
Component name (default -).
.PARAMETER Port
Component API port (default: the package's).
.PARAMETER Location
Location shown on the Webserver (monitor, sentinel).
.PARAMETER Advertise
monitor: IP address the Webserver must use to reach this probe (default: auto-detected).
.PARAMETER WebserverUrl
integrator: Webserver web UI URL used in notifications (default https://:9090).
.PARAMETER FrontPki
webserver-front: the front\ folder produced by mugnsoft-front-pki.sh.
.PARAMETER NoFirewall
Do not add the Windows Firewall rule opening the component's port(s).
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, Position = 0)]
[ValidateSet("webserver", "monitor", "integrator", "sentinel", "webserver-front")]
[string]$Component,
[string]$Version = "4.2.0",
[string]$Dir = "C:\Mugnsoft",
[string]$Package = "",
[switch]$NoVerify,
[string]$License = "",
[string]$ApiPort = "",
[string]$WebPort = "",
[string]$Webserver = "",
[string]$SharedInternal = "",
[string]$Name = "",
[string]$Port = "",
[string]$Location = "",
[string]$Advertise = "",
[string]$WebserverUrl = "",
[string]$FrontPki = "",
[switch]$NoFirewall
)
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue" # Invoke-WebRequest is 10x faster without the progress bar
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$BaseUrl = "https://mugnsoft.com/bin"
function Fail($msg) { Write-Host "ERROR: $msg" -ForegroundColor Red; exit 1 }
function Info($msg) { Write-Host "==> $msg" -ForegroundColor Cyan }
# Run a native program and capture stdout+stderr. Windows PowerShell turns every
# stderr line into a terminating error under ErrorActionPreference=Stop, so relax
# it for the call only.
function Native([string]$exe, [string[]]$a) {
$prev = $ErrorActionPreference; $ErrorActionPreference = "Continue"
try { $o = & $exe @a 2>&1 | ForEach-Object { "$_" }; $code = $LASTEXITCODE } finally { $ErrorActionPreference = $prev }
return @{ Out = ($o -join "`n"); Code = $code }
}
# component -> package folder and binary
$pkg = @{ "webserver" = "webserver"; "webserver-front" = "webserver-front"; "monitor" = "monitor";
"integrator" = "integrator"; "sentinel" = "discovery_agent" }[$Component]
$bin = "$pkg.exe"; $conf = "$pkg.json"
$target = Join-Path $Dir $pkg
# ---- checks, before anything is written -------------------------------------
$admin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $admin) { Fail "run this script from an elevated PowerShell (Run as administrator)" }
foreach ($p in @(@("-ApiPort", $ApiPort), @("-WebPort", $WebPort), @("-Port", $Port))) {
if ($p[1] -and -not ($p[1] -match '^\d+$' -and [int]$p[1] -ge 1 -and [int]$p[1] -le 65535)) { Fail "$($p[0]) must be a port number, got '$($p[1])'" }
}
foreach ($p in @(@("-Location", $Location), @("-Advertise", $Advertise), @("-WebserverUrl", $WebserverUrl))) {
if ($p[1] -notmatch '^[A-Za-z0-9._:/@+=, -]*$') { Fail "$($p[0]) contains characters that are not allowed: '$($p[1])'" }
}
if ($Component -in @("webserver", "integrator")) {
if (-not $License) { Fail "-License is required for the $Component" }
if (-not (Test-Path $License -PathType Leaf)) { Fail "license file not found: $License" }
}
if ($Component -in @("monitor", "integrator", "sentinel")) {
if (-not $Webserver) { Fail "-Webserver is required (the Webserver API address)" }
if ($Webserver -notmatch '^[A-Za-z0-9.-]+:\d+$') { Fail "-Webserver must look like host:port, e.g. 10.0.0.5:8050" }
if (-not $SharedInternal) { Fail "-SharedInternal is required: copy `"_sharedInternal`" from the Webserver's webserver.json" }
if ($SharedInternal -notmatch '^[A-Za-z0-9._~!#%^*+=-]+$') { Fail "-SharedInternal holds unexpected characters" }
if (-not $Name) { $Name = "$Component-$($env:COMPUTERNAME.ToLower())" }
if ($Name -notmatch '^[A-Za-z0-9-]+$') { Fail "-Name may hold letters, digits and hyphens only: '$Name'" }
}
if ($Component -eq "webserver-front") {
if (-not $FrontPki) { Fail "-FrontPki is required: the front\ folder made by mugnsoft-front-pki.sh" }
if (-not ((Test-Path "$FrontPki\config\ssl\certificates\webserver.pem") -and (Test-Path "$FrontPki\webserver-front.json"))) {
Fail "$FrontPki does not look like the front\ folder of mugnsoft-front-pki.sh" }
}
if (Test-Path $target) { Fail "$target already exists: this script installs, it does not upgrade (see the Upgrade section of the docs)" }
$work = Join-Path $env:TEMP ("mugnsoft-install-" + [guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory $work | Out-Null
try {
# ---- 1. get the package -----------------------------------------------------
$file = "mugnsoft-$pkg-$Version.windows-amd64.zip"
if (-not $Package) {
Info "Downloading $file"
try { Invoke-WebRequest "$BaseUrl/$Version/$file" -OutFile "$work\$file" -UseBasicParsing } catch { Fail "download failed: $BaseUrl/$Version/$file" }
if (-not $NoVerify) { try { Invoke-WebRequest "$BaseUrl/$Version/$file.sig" -OutFile "$work\$file.sig" -UseBasicParsing } catch { Fail "signature download failed" } }
$Package = "$work\$file"
} else {
if (-not (Test-Path $Package -PathType Leaf)) { Fail "package not found: $Package" }
$Package = (Resolve-Path $Package).Path
if (-not $NoVerify -and -not (Test-Path "$Package.sig")) { Fail "no $Package.sig next to the package: download it too, or pass -NoVerify" }
}
# ---- 2. verify its signature (gpg from PATH, Git for Windows or Gpg4win) ------
if (-not $NoVerify) {
$gpg = (Get-Command gpg -ErrorAction SilentlyContinue).Source
$git = (Get-Command git -ErrorAction SilentlyContinue).Source # Git for Windows ships gpg in usr\bin
$fromGit = if ($git) { Join-Path (Split-Path (Split-Path $git)) "usr\bin\gpg.exe" } else { "" }
foreach ($c in @($fromGit, "$env:ProgramFiles\Git\usr\bin\gpg.exe", "${env:ProgramFiles(x86)}\GnuPG\bin\gpg.exe", "$env:ProgramFiles\GnuPG\bin\gpg.exe")) {
if (-not $gpg -and $c -and (Test-Path $c)) { $gpg = $c }
}
if (-not $gpg) { Fail "gpg not found: install Git for Windows or Gpg4win to verify the package (or pass -NoVerify)" }
Info "Verifying the GPG signature"
try { Invoke-WebRequest "$BaseUrl/Mugnsoft_public.key" -OutFile "$work\Mugnsoft_public.key" -UseBasicParsing } catch { Fail "could not download Mugnsoft_public.key" }
# a throw-away keyring, given as a RELATIVE --homedir: Git's (MSYS) gpg misreads
# an absolute Windows path there, Gpg4win accepts both
Push-Location $work
New-Item -ItemType Directory gnupg | Out-Null
Native $gpg @("--homedir", "gnupg", "--batch", "--quiet", "--import", "$work\Mugnsoft_public.key") | Out-Null
$sig = if (Test-Path "$Package.sig") { "$Package.sig" } else { "$work\$file.sig" }
$ok = (Native $gpg @("--homedir", "gnupg", "--batch", "--verify", $sig, $Package)).Code -eq 0
Pop-Location
if (-not $ok) { Fail "SIGNATURE CHECK FAILED for $Package : do not install it" }
Info "Signature OK"
} else {
Write-Host "WARNING: signature not verified (-NoVerify)" -ForegroundColor Yellow
}
# ---- 3. extract ---------------------------------------------------------------
Info "Extracting to $target"
New-Item -ItemType Directory -Force $Dir | Out-Null
Expand-Archive $Package -DestinationPath "$work\x"
if (-not (Test-Path "$work\x\$pkg")) { Fail "unexpected package layout: no $pkg\ folder inside" }
Move-Item "$work\x\$pkg" $target
Set-Location $target
if (-not (Test-Path $conf)) { Fail "$conf missing from the package" }
# set "key": "value" in a flat JSON file, adding the key when it is absent;
# written back as UTF-8 WITHOUT BOM (a BOM makes the JSON unreadable to the component)
$script:text = [IO.File]::ReadAllText("$target\$conf")
function SetKey($key, $val) {
$re = '("' + [regex]::Escape($key) + '"\s*:\s*)"[^"]*"'
if ($script:text -match $re) { $script:text = [regex]::Replace($script:text, $re, { param($m) $m.Groups[1].Value + '"' + $val + '"' }) }
else { $script:text = ([regex]'\{').Replace($script:text, "{`r`n `"$key`": `"$val`",", 1) }
}
# ---- 4. configure ---------------------------------------------------------------
Info "Configuring $conf"
$ports = @()
switch ($Component) {
"webserver" {
Copy-Item $License "$target\license_MNS.dat"
if ($ApiPort) { SetKey "PortAPI" $ApiPort }
if ($WebPort) { SetKey "PortWEB" $WebPort }
}
"webserver-front" { Copy-Item -Recurse -Force "$FrontPki\*" $target; $script:text = [IO.File]::ReadAllText("$target\$conf") }
default {
SetKey "_sharedInternal" $SharedInternal
SetKey "webserver" $Webserver
SetKey "name" $Name
SetKey "description" "Mugnsoft $Component on $env:COMPUTERNAME"
if ($Port) { SetKey "port" $Port }
if ($Component -ne "integrator") { SetKey "location" $Location }
if ($Component -eq "monitor" -and $Advertise) { SetKey "ip" $Advertise }
if ($Component -eq "integrator") {
Copy-Item $License "$target\license_MNS.dat"
SetKey "os" "windows"
if (-not $WebserverUrl) { $WebserverUrl = "https://" + $Webserver.Split(":")[0] + ":9090" }
SetKey "webserverUrl" $WebserverUrl
}
}
}
[IO.File]::WriteAllText("$target\$conf", $script:text, (New-Object Text.UTF8Encoding $false))
$cfg = $script:text | ConvertFrom-Json
if ($Component -in @("webserver", "webserver-front")) { $ports = @($cfg.PortAPI, $cfg.PortWEB) } else { $ports = @($cfg.port) }
# ---- 5. open the port(s) in Windows Firewall --------------------------------------
if (-not $NoFirewall) {
$rule = "Mugnsoft $Component"
if (-not (Get-NetFirewallRule -DisplayName $rule -ErrorAction SilentlyContinue)) {
New-NetFirewallRule -DisplayName $rule -Direction Inbound -Protocol TCP -LocalPort $ports -Action Allow | Out-Null
Info "Firewall: inbound TCP $($ports -join ', ') allowed (rule '$rule')"
}
}
# ---- 6. install the service (components register with the Webserver here) ------
Info "Installing the service"
$out = (Native "$target\$bin" @("install")).Out
$out -split "`r?`n" | Where-Object { $_ -and $_ -notmatch '^time=' } | ForEach-Object { Write-Host " $_" }
# "Service '' installed." - not "Service '' not installed."
if ($out -notmatch "Service '[^']+' installed\.") { Fail "the service was not installed - read the messages above, fix, then run: cd $target; .\$bin install" }
# ---- 7. start it ----------------------------------------------------------------
Info "Starting the service"
(Native "$target\$bin" @("start")).Out -split "`r?`n" | Where-Object { $_ -and $_ -notmatch '^time=' } | ForEach-Object { Write-Host " $_" }
# install generates the webserver's _sharedInternal: read the config again
$cfg = [IO.File]::ReadAllText("$target\$conf") | ConvertFrom-Json
Write-Host ""
switch ($Component) {
"webserver" {
Write-Host "Webserver installed in $target"
Write-Host " Web UI: https://:$($cfg.PortWEB)/ (admin / admin, change it at first login)"
Write-Host " API: :$($cfg.PortAPI) -> the -Webserver value for the components"
Write-Host " _sharedInternal (pass it to every component as -SharedInternal):"
Write-Host " $($cfg._sharedInternal)"
}
"webserver-front" { Write-Host "Webserver-Front installed in $target - now configure ADMIN (see the Webserver-Front install page)." }
default {
Write-Host "$Component '$Name' installed in $target and registered with $Webserver."
Write-Host "Last step: on the Webserver, open Settings (gear) > Components, right-click '$Name' and choose enable server."
}
}
} finally {
Set-Location $env:TEMP
Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue
}