# install.ps1 - Systemframe workstation monitoring (Telegraf) installer. # # Installs Telegraf, the collectors and their configuration, registers the service and # starts collecting. Everything comes from https://setup-workstation.systemframe.com.br and is checked against the # SHA256 values in manifest.json. Re-running it upgrades in place. # # irm https://setup-workstation.systemframe.com.br/ | iex install / upgrade # irm https://setup-workstation.systemframe.com.br/uninstall | iex remove # & ([scriptblock]::Create((irm https://setup-workstation.systemframe.com.br))) -Customer same, as a parameter # # The customer comes from -Customer, else from the URL path (nginx writes it into this # file when it is served at /), else from $env:SF_CUSTOMER, else a prompt. # # Labels sent with every metric: customer (from -Customer), display_name (hostname, upper case), # machine_id (short id from the Windows MachineGuid, stable across renames), host_role=workstation. # Requires an elevated PowerShell 5.1+ (64-bit) on Windows 10/11 or Server 2016+. param( [string]$Customer, [switch]$Uninstall, [string]$BaseUrl = 'https://setup-workstation.systemframe.com.br' ) # Filled in by nginx for / and /uninstall; left as is when served at /. $SfUrlCustomer = '__URL_CUSTOMER__' $SfUrlMode = '__URL_MODE__' if (-not $Customer -and $SfUrlCustomer -notlike '__*') { $Customer = $SfUrlCustomer } if (-not $Customer) { $Customer = $env:SF_CUSTOMER } if ($SfUrlMode -eq 'uninstall') { $Uninstall = $true } $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } catch { } $SfRoot = 'C:\Monitoring\Telegraf' $SfService = 'telegraf' $SfPrimeTask = 'SystemframeTelegrafPrime' $SfExe = Join-Path $SfRoot 'telegraf.exe' $SfConf = Join-Path $SfRoot 'telegraf.conf' $SfConfDir = Join-Path $SfRoot 'telegraf.d' $BaseUrl = $BaseUrl.TrimEnd('/') function Write-SfStep([string]$Message) { Write-Host "[sf-workstation] $Message" } # Native tools write warnings/logs to stderr; under ErrorActionPreference=Stop PS 5.1 turns # redirected stderr into a terminating NativeCommandError. Run them with Continue, discard # the output and return the exit code. function Invoke-SfNative([string]$FilePath, [string[]]$ArgumentList) { $previous = $ErrorActionPreference $ErrorActionPreference = 'Continue' try { & $FilePath @ArgumentList 2>&1 | Out-Null; return $LASTEXITCODE } finally { $ErrorActionPreference = $previous } } function Stop-SfInstall([string]$Message) { Write-Host "[sf-workstation] ERRO: $Message" -ForegroundColor Red throw $Message } # Short machine id in the shape of a Windows default computer name: 'WIN-' + 11 chars of # Crockford base32 (no I/L/O/U) from SHA-256 of the MachineGuid. Stable across renames and # reinstalls; clones that share a MachineGuid share the id (the hostname tells them apart). # The Ansible role (pre_install_checks.yml) computes the same value. function Get-SfMachineId { $guid = ([string](Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Cryptography' -Name MachineGuid).MachineGuid).Trim().ToLowerInvariant() $sha = [System.Security.Cryptography.SHA256]::Create() $bytes = $sha.ComputeHash([System.Text.Encoding]::ASCII.GetBytes($guid)) $alphabet = '0123456789ABCDEFGHJKMNPQRSTVWXYZ' $id = New-Object System.Text.StringBuilder $acc = 0; $bits = 0 foreach ($b in $bytes) { $acc = (($acc -shl 8) -bor $b) -band 0xFFFF; $bits += 8 while ($bits -ge 5 -and $id.Length -lt 11) { $bits -= 5; [void]$id.Append($alphabet[($acc -shr $bits) -band 31]) } if ($id.Length -ge 11) { break } } return 'WIN-' + $id.ToString() } function Assert-SfPrerequisites { $principal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Stop-SfInstall 'execute o PowerShell como Administrador.' } if ($PSVersionTable.PSVersion -lt [version]'5.1') { Stop-SfInstall "PowerShell $($PSVersionTable.PSVersion) nao suportado (minimo 5.1)." } if (-not [Environment]::Is64BitOperatingSystem -or -not [Environment]::Is64BitProcess) { Stop-SfInstall 'use o PowerShell 64-bit num Windows 64-bit.' } } function Get-SfVerifiedFile([string[]]$Urls, [string]$Destination, [string]$Sha256) { $lastError = $null foreach ($url in $Urls) { try { Invoke-WebRequest -Uri $url -OutFile $Destination -UseBasicParsing -TimeoutSec 900 $hash = (Get-FileHash -Path $Destination -Algorithm SHA256).Hash.ToLowerInvariant() if ($hash -eq $Sha256.ToLowerInvariant()) { return } $lastError = "SHA256 divergente em $url" } catch { $lastError = "$url : $($_.Exception.Message)" } Remove-Item -Path $Destination -Force -ErrorAction SilentlyContinue } Stop-SfInstall "falha ao baixar $(Split-Path $Destination -Leaf): $lastError" } function Remove-SfService { $svc = Get-Service -Name $SfService -ErrorAction SilentlyContinue if ($svc) { Stop-Service -Name $SfService -Force -ErrorAction SilentlyContinue if (Test-Path $SfExe) { $null = Invoke-SfNative $SfExe @('--service-name', $SfService, 'service', 'uninstall') } if (Get-Service -Name $SfService -ErrorAction SilentlyContinue) { $null = Invoke-SfNative 'sc.exe' @('delete', $SfService) } Start-Sleep -Seconds 2 } # "telegraf service install" refuses to run while an old event log source exists. Remove-Item -Path "HKLM:\SYSTEM\CurrentControlSet\Services\EventLog\Application\$SfService" -Recurse -Force -ErrorAction SilentlyContinue } function Format-SfToml([string]$Value) { return ($Value -replace '\\', '\\' -replace '"', '\"') } function New-SfTelegrafConf($Manifest, [string]$CustomerName, [string]$DisplayName, [string]$MachineId) { $lines = New-Object 'System.Collections.Generic.List[string]' $lines.Add('# Telegraf configuration - Systemframe workstation monitoring.') $lines.Add("# Written by install.ps1 $($Manifest.version) from $BaseUrl; re-run the installer to change it.") $lines.Add('# Inputs live in telegraf.d\*.conf (one file per collector).') $lines.Add('') $lines.Add('[global_tags]') $lines.Add(" customer = `"$(Format-SfToml $CustomerName)`"") $lines.Add(" display_name = `"$(Format-SfToml $DisplayName)`"") $lines.Add(" instance = `"$(Format-SfToml $DisplayName)`"") $lines.Add(" machine_id = `"$(Format-SfToml $MachineId)`"") $lines.Add(' host_role = "workstation"') $lines.Add(' collector_name = "telegraf"') $lines.Add(" deploy_version = `"$(Format-SfToml ([string]$Manifest.version))`"") $lines.Add('') $lines.Add('[agent]') foreach ($p in $Manifest.agent.PSObject.Properties) { if ($p.Value -is [bool]) { $lines.Add(" $($p.Name) = $(([string]$p.Value).ToLowerInvariant())") } elseif ($p.Value -is [int] -or $p.Value -is [long]) { $lines.Add(" $($p.Name) = $($p.Value)") } else { $lines.Add(" $($p.Name) = `"$(Format-SfToml ([string]$p.Value))`"") } } $lines.Add(" logfile = '$SfRoot\telegraf.log'") $lines.Add('') $lines.Add('[[inputs.internal]]') $lines.Add(' interval = "60s"') $lines.Add(' collect_memstats = false') $lines.Add(' name_override = "telegraf_internal"') $lines.Add('') $lines.Add('[[inputs.file]]') $lines.Add(" files = ['$SfRoot\custom_thresholds.txt']") $lines.Add(' data_format = "influx"') foreach ($url in @($Manifest.outputs)) { $lines.Add('') $lines.Add('[[outputs.http]]') $lines.Add(" url = `"$(Format-SfToml ([string]$url))`"") $lines.Add(' method = "POST"') $lines.Add(' data_format = "prometheusremotewrite"') $lines.Add(' [outputs.http.headers]') $lines.Add(' Content-Type = "application/x-protobuf"') $lines.Add(' Content-Encoding = "snappy"') $lines.Add(' X-Prometheus-Remote-Write-Version = "0.1.0"') } return ($lines -join "`r`n") + "`r`n" } function Install-SfWorkstation([string]$CustomerName) { Assert-SfPrerequisites if ([string]::IsNullOrWhiteSpace($CustomerName) -and [Environment]::UserInteractive) { $CustomerName = Read-Host '[sf-workstation] Customer (ex.: granado)' } $CustomerName = ([string]$CustomerName).Trim().ToLowerInvariant() if ($CustomerName -notmatch '^[a-z0-9][a-z0-9_-]{1,48}$') { Stop-SfInstall "informe -Customer com letras minusculas, numeros, '-' ou '_' (recebido: '$CustomerName')." } $machineId = Get-SfMachineId $displayName = $env:COMPUTERNAME.ToUpperInvariant() Write-SfStep "maquina $displayName (machine_id $machineId), customer $CustomerName" Write-SfStep "lendo $BaseUrl/manifest.json" $manifest = Invoke-RestMethod -Uri "$BaseUrl/manifest.json" -UseBasicParsing -TimeoutSec 60 Write-SfStep "versao $($manifest.version), Telegraf $($manifest.telegraf.version)" $work = Join-Path $env:TEMP ('sf-workstation-' + [guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Path $work -Force | Out-Null try { # 1. collectors and inputs (downloaded and verified before touching the running install) foreach ($f in @($manifest.files)) { $dest = Join-Path $work ($f.path -replace '/', '__') Get-SfVerifiedFile -Urls @("$BaseUrl/$($f.url)") -Destination $dest -Sha256 $f.sha256 } # 2. Telegraf binary (mirror first, upstream as fallback) $exeOk = (Test-Path $SfExe) -and ((Get-FileHash -Path $SfExe -Algorithm SHA256).Hash.ToLowerInvariant() -eq ([string]$manifest.telegraf.exe_sha256).ToLowerInvariant()) if (-not $exeOk) { Write-SfStep "baixando Telegraf $($manifest.telegraf.version)" $zip = Join-Path $work 'telegraf.zip' Get-SfVerifiedFile -Urls @("$BaseUrl/$($manifest.telegraf.mirror)", [string]$manifest.telegraf.upstream) -Destination $zip -Sha256 $manifest.telegraf.zip_sha256 Expand-Archive -Path $zip -DestinationPath (Join-Path $work 'telegraf') -Force $newExe = Get-ChildItem -Path (Join-Path $work 'telegraf') -Recurse -Filter 'telegraf.exe' | Select-Object -First 1 if (-not $newExe) { Stop-SfInstall 'telegraf.exe nao encontrado no pacote.' } } # 3. stop the service, put files in place Write-SfStep 'parando o servico anterior (se existir)' Stop-Service -Name $SfService -Force -ErrorAction SilentlyContinue foreach ($d in @($SfRoot, (Join-Path $SfRoot 'Scripts'), $SfConfDir, (Join-Path $SfRoot 'State'))) { New-Item -ItemType Directory -Path $d -Force | Out-Null } if (-not $exeOk) { Copy-Item -Path $newExe.FullName -Destination $SfExe -Force } $keep = @{} foreach ($f in @($manifest.files)) { $target = Join-Path $SfRoot ($f.path -replace '/', '\') Copy-Item -Path (Join-Path $work ($f.path -replace '/', '__')) -Destination $target -Force $keep[$target.ToLowerInvariant()] = $true } # inputs of collectors removed in a newer version foreach ($old in (Get-ChildItem -Path $SfConfDir -Filter '*.conf')) { if (-not $keep.ContainsKey($old.FullName.ToLowerInvariant())) { Remove-Item -Path $old.FullName -Force } } Set-Content -Path $SfConf -Value (New-SfTelegrafConf $manifest $CustomerName $displayName $machineId) -Encoding ASCII $epoch = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() $t = $manifest.thresholds Set-Content -Path (Join-Path $SfRoot 'custom_thresholds.txt') -Encoding ASCII -Value @( "custom_threshold,metric=cpu value=$($t.cpu)", "custom_threshold,metric=disk value=$($t.disk)", "custom_threshold,metric=mem value=$($t.mem)", '', "last_deployed_on value=$epoch") # 4. Security log auditing used by the event collectors (GUIDs: language independent) foreach ($a in @($manifest.audit)) { $setArgs = @('/set', "/subcategory:$($a.guid)") if ($a.success) { $setArgs += '/success:enable' } if ($a.failure) { $setArgs += '/failure:enable' } $rc = Invoke-SfNative 'auditpol.exe' $setArgs if ($rc -ne 0) { Write-SfStep "aviso: auditpol falhou para $($a.name) ($rc)" } } # 5. service (re-registered when it does not load telegraf.d) $svc = Get-CimInstance -ClassName Win32_Service -Filter "Name='$SfService'" -ErrorAction SilentlyContinue if (-not $svc -or ([string]$svc.PathName) -notmatch 'telegraf\.d') { Write-SfStep 'registrando o servico telegraf' Remove-SfService $null = Invoke-SfNative $SfExe @('--config', $SfConf, '--config-directory', $SfConfDir, '--service-name', $SfService, 'service', 'install') if (-not (Get-Service -Name $SfService -ErrorAction SilentlyContinue)) { Stop-SfInstall 'telegraf service install falhou.' } } Set-Service -Name $SfService -StartupType Automatic Start-Service -Name $SfService Write-SfStep ("servico telegraf: " + (Get-Service -Name $SfService).Status) # 6. first inventory now (one collector at a time, Idle priority, in background). # One-shot SYSTEM task instead of a child process: it survives the console closing # and remote sessions (WinRM/RMM kill their children on exit). prime.ps1 deletes it. $prime = Join-Path $SfRoot 'Scripts\prime.ps1' if (Test-Path $prime) { # no inner quotes: PS 5.1 does not escape them for native args ($SfRoot has no spaces) $tr = 'powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File ' + $prime # /ST in the past only warns; the task is started right away with /Run $null = Invoke-SfNative 'schtasks.exe' @('/Create', '/TN', $SfPrimeTask, '/TR', $tr, '/SC', 'ONCE', '/ST', '00:00', '/RU', 'SYSTEM', '/RL', 'HIGHEST', '/F') if ((Invoke-SfNative 'schtasks.exe' @('/Run', '/TN', $SfPrimeTask)) -eq 0) { Write-SfStep 'primeira coleta de inventario iniciada em segundo plano (alguns minutos)' } else { Write-SfStep 'aviso: primeira coleta nao iniciou; os coletores rodam no proximo intervalo' } } [pscustomobject]@{ version = $manifest.version; customer = $CustomerName; display_name = $displayName; machine_id = $machineId installed_at = (Get-Date).ToUniversalTime().ToString('o'); base_url = $BaseUrl } | ConvertTo-Json | Set-Content -Path (Join-Path $SfRoot 'State\install.json') -Encoding ASCII Write-SfStep "pronto. No Grafana: Customer=$CustomerName, Host=$displayName." } finally { Remove-Item -Path $work -Recurse -Force -ErrorAction SilentlyContinue } } function Uninstall-SfWorkstation { Assert-SfPrerequisites Write-SfStep 'removendo o servico e os arquivos' Remove-SfService $null = Invoke-SfNative 'schtasks.exe' @('/Delete', '/TN', $SfPrimeTask, '/F') Get-CimInstance Win32_Process -Filter "Name='powershell.exe'" -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -match 'Monitoring\\Telegraf\\Scripts|Monitoring/Telegraf/Scripts' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } Remove-Item -Path $SfRoot -Recurse -Force -ErrorAction SilentlyContinue Write-SfStep 'desinstalado.' } if ($Uninstall) { Uninstall-SfWorkstation } else { Install-SfWorkstation $Customer }