<# Aytechnet monitoring agent (aymond) - Datto RMM component. Installs the agent, or updates it when a newer release is published, or removes it. Safe to run again and again on a whole fleet: a machine already up to date is left alone. Variables (Datto exposes them to the script as environment variables): AytechnetParcKey SITE variable, one per customer: the parc key AYT-XXXX-XXXX-XXXX from the Aytechnet account. Required to install, never printed. AytechnetAction component variable: Install (default: install or update) | Remove AytechnetSHA256 optional: the expected SHA-256 of aymond.exe, pinned by hand from the installation page. Default: read from /downloads/SHA256SUMS. AytechnetUDF optional: a Datto user-defined field number (1-30) that receives "aymond " at the next audit. Exit code 0 = done (or nothing to do), 1 = failed (the reason is the last line). Messages are ASCII on purpose: Datto shows the script output as is, and accented characters would not survive every code page. Requires: Windows Server 2016 or later / Windows 10 or later, 64-bit, PowerShell 5.1, run as SYSTEM (Datto's default), outbound HTTPS to aytechnet.fr (download) and watch.aytechnet.net (reports). #> $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' # Invoke-WebRequest is many times slower with its progress bar $Base = 'https://aytechnet.fr/downloads' $ServiceName = 'aymond' # Program Files of the 64-bit system, even if the Datto agent runs this # script in a 32-bit PowerShell (where $env:ProgramFiles is the x86 one). $ProgramFiles64 = if ($env:ProgramW6432) { $env:ProgramW6432 } else { $env:ProgramFiles } $InstallDir = Join-Path $ProgramFiles64 'Aytechnet' $InstallExe = Join-Path $InstallDir 'aymond.exe' $IdentityDir = Join-Path $env:ProgramData 'Aytechnet\aymond' # Windows Server 2016's PowerShell 5.1 still offers TLS 1.0 first. [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 function Fail([string]$Message) { throw "FAILED: $Message" } # Runs aymond.exe; its error output must not turn into a PowerShell # exception (PowerShell 5.1 does that to native stderr under 'Stop'). function Invoke-Aymond([string]$Exe, [string[]]$Arguments) { $saved = $ErrorActionPreference $ErrorActionPreference = 'Continue' try { $out = & $Exe @Arguments 2>&1 | ForEach-Object { "$_" } return @{ Code = $LASTEXITCODE; Output = (($out | Where-Object { $_ }) -join ' | ') } } finally { $ErrorActionPreference = $saved } } # The executable the installed service runs, from its command line # ("C:\Program Files\Aytechnet\aymond.exe" -key ... or unquoted). function Get-InstalledExe($Service) { $path = $Service.PathName.Trim() if ($path.StartsWith('"')) { return $path.Substring(1, $path.IndexOf('"', 1) - 1) } return ($path -split ' ', 2)[0] } function Write-UDF([string]$Value) { $slot = "$env:AytechnetUDF".Trim() if (-not $slot) { return } if ($slot -notmatch '^([1-9]|[12][0-9]|30)$') { Write-Output "UDF '$slot' ignored (expected 1 to 30)"; return } New-Item -Path 'HKLM:\SOFTWARE\CentraStage' -Force | Out-Null Set-ItemProperty -Path 'HKLM:\SOFTWARE\CentraStage' -Name "Custom$slot" -Value $Value } function Wait-Running { for ($i = 0; $i -lt 20; $i++) { $s = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue if ($s -and $s.Status -eq 'Running') { return } Start-Sleep -Seconds 1 } Fail "service $ServiceName is not running after 20 s (Windows event log, source aymond)" } function Remove-Agent { $svc = Get-CimInstance Win32_Service -Filter "Name='$ServiceName'" if (-not $svc) { Write-Output 'aymond is not installed, nothing to remove' return } $exe = Get-InstalledExe $svc $r = Invoke-Aymond $exe @('remove') if ($r.Code -ne 0) { Fail "aymond remove (exit $($r.Code)): $($r.Output)" } for ($i = 0; $i -lt 15 -and (Get-Process -Name aymond -ErrorAction SilentlyContinue); $i++) { Start-Sleep -Seconds 1 } # The binary only, then its folder if nothing else is left in it: the # service may have been installed by hand anywhere, never delete a # folder recursively on the strength of a service's command line. Remove-Item -Path $exe -Force -ErrorAction SilentlyContinue $dir = Split-Path $exe if ((Test-Path $dir) -and -not (Get-ChildItem -Path $dir -Force)) { Remove-Item -Path $dir -Force -ErrorAction SilentlyContinue } Remove-Item -Path $IdentityDir -Recurse -Force -ErrorAction SilentlyContinue Write-UDF '' Write-Output 'aymond removed (service, program, machine identity). Press "Forget" on this machine in the Aytechnet account to delete its data.' } function Install-Agent([string]$WorkDir) { $version = ((Invoke-WebRequest -Uri "$Base/VERSION" -UseBasicParsing).Content | Out-String).Trim() $expected = "$env:AytechnetSHA256".Trim() if (-not $expected) { $sums = (Invoke-WebRequest -Uri "$Base/SHA256SUMS" -UseBasicParsing).Content | Out-String foreach ($line in ($sums -split "`n")) { $f = $line.Trim() -split '\s+' if ($f.Count -eq 2 -and $f[1].TrimStart('*') -eq 'aymond.exe') { $expected = $f[0] } } } if ($expected -notmatch '^[0-9a-fA-F]{64}$') { Fail 'no valid SHA-256 for aymond.exe (SHA256SUMS unreadable, or AytechnetSHA256 malformed)' } $downloaded = Join-Path $WorkDir 'aymond.exe' Invoke-WebRequest -Uri "$Base/aymond.exe" -OutFile $downloaded -UseBasicParsing $hash = (Get-FileHash -Path $downloaded -Algorithm SHA256).Hash if ($hash -ne $expected.ToUpperInvariant()) { Fail "checksum mismatch for aymond.exe: got $hash, expected $($expected.ToUpperInvariant()) (release in progress? run again in a few minutes)" } $svc = Get-CimInstance Win32_Service -Filter "Name='$ServiceName'" if (-not $svc) { $key = "$env:AytechnetParcKey".Trim().ToUpperInvariant() if ($key -notmatch '^AYT-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$') { Fail 'site variable AytechnetParcKey missing or malformed (expected AYT-XXXX-XXXX-XXXX)' } New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null Copy-Item -Path $downloaded -Destination $InstallExe -Force $r = Invoke-Aymond $InstallExe @('install', '-key', $key) if ($r.Code -ne 0) { Fail "aymond install (exit $($r.Code)): $($r.Output)" } Wait-Running Write-UDF "aymond $version" Write-Output "aymond $version installed to $InstallExe, service running (LocalSystem). The machine appears in the Aytechnet account within two minutes." return } $installed = Get-InstalledExe $svc if ((Test-Path $installed) -and ((Get-FileHash -Path $installed -Algorithm SHA256).Hash -eq $hash)) { if ((Get-Service -Name $ServiceName).Status -ne 'Running') { Start-Service -Name $ServiceName Wait-Running Write-UDF "aymond $version" Write-Output "aymond $version already installed; the service was stopped and has been started" return } Write-UDF "aymond $version" Write-Output "aymond $version already installed and running, nothing to do" return } # Newer release: the downloaded binary replaces the installed one itself # (stop, copy, start), keeping the machine's identity and settings. $r = Invoke-Aymond $downloaded @('update') if ($r.Code -ne 0) { Fail "aymond update (exit $($r.Code)): $($r.Output)" } Wait-Running Write-UDF "aymond $version" Write-Output "aymond updated to $version ($installed), service running" } $exitCode = 0 $work = Join-Path $env:TEMP ('aymond-datto-' + [guid]::NewGuid().ToString('N')) try { if (-not [Environment]::Is64BitOperatingSystem) { Fail 'aymond requires 64-bit Windows' } switch ("$env:AytechnetAction".Trim().ToLowerInvariant()) { { $_ -in '', 'install' } { New-Item -ItemType Directory -Path $work -Force | Out-Null Install-Agent $work } 'remove' { Remove-Agent } default { Fail "unknown AytechnetAction '$env:AytechnetAction' (Install or Remove)" } } } catch { Write-Output $_.Exception.Message $exitCode = 1 } finally { Remove-Item -Path $work -Recurse -Force -ErrorAction SilentlyContinue } exit $exitCode