feat: add self-enrolling Windows Agent package
This commit is contained in:
1 parent
b9b5e5e36f
commit
abf5654a25
20 files changed
+1834
-42
No files matched your search
@@ -0,0 +1,323 @@
|
||||
#Requires -RunAsAdministrator
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$Target = "C:\MinerOpsAgent"
|
||||
$ConfigPath = Join-Path $Target "agent-config.json"
|
||||
$PendingConfigPath = Join-Path $Target "agent-config.json.pending"
|
||||
$RecoveryConfigPath = Join-Path $Target "agent-config.recovery.json"
|
||||
$EnrollmentPath = Join-Path $PSScriptRoot "enrollment.json"
|
||||
$Installer = Join-Path $PSScriptRoot "install.ps1"
|
||||
|
||||
$RequiredFiles = @(
|
||||
"install.ps1",
|
||||
"MinerOpsAgentService.ps1",
|
||||
"MinerOpsAgentTray.ps1",
|
||||
"MinerOpsAgentGUI.ps1",
|
||||
"MinerOpsApi3EnrichmentPoller.ps1",
|
||||
"MinerOpsPoolPoller.ps1",
|
||||
"MinerOpsPoolPollerTask.ps1",
|
||||
"EnrichOneMiner.ps1",
|
||||
"MinerOpsControl.ps1",
|
||||
"VERSION",
|
||||
"enrollment.json"
|
||||
)
|
||||
|
||||
function Protect-MinerOpsCredentialFile {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string] $Path
|
||||
)
|
||||
|
||||
$acl = New-Object System.Security.AccessControl.FileSecurity
|
||||
$acl.SetAccessRuleProtection($true, $false)
|
||||
|
||||
$identities = @(
|
||||
(New-Object System.Security.Principal.SecurityIdentifier("S-1-5-18")),
|
||||
(New-Object System.Security.Principal.SecurityIdentifier("S-1-5-32-544")),
|
||||
[System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
||||
)
|
||||
|
||||
foreach ($identity in $identities) {
|
||||
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
|
||||
$identity,
|
||||
[System.Security.AccessControl.FileSystemRights]::FullControl,
|
||||
[System.Security.AccessControl.InheritanceFlags]::None,
|
||||
[System.Security.AccessControl.PropagationFlags]::None,
|
||||
[System.Security.AccessControl.AccessControlType]::Allow
|
||||
)
|
||||
|
||||
$acl.AddAccessRule($rule)
|
||||
}
|
||||
|
||||
Set-Acl -LiteralPath $Path -AclObject $acl
|
||||
}
|
||||
|
||||
function ConvertFrom-MinerOpsSecureString {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[Security.SecureString] $SecureValue
|
||||
)
|
||||
|
||||
$pointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR(
|
||||
$SecureValue
|
||||
)
|
||||
|
||||
try {
|
||||
return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($pointer)
|
||||
}
|
||||
finally {
|
||||
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($pointer)
|
||||
}
|
||||
}
|
||||
|
||||
# Validate the complete package before modifying the Agent host or consuming
|
||||
# the single-use enrollment token.
|
||||
foreach ($name in $RequiredFiles) {
|
||||
$source = Join-Path $PSScriptRoot $name
|
||||
|
||||
if (!(Test-Path -LiteralPath $source -PathType Leaf)) {
|
||||
throw "Enrollment package is missing $name"
|
||||
}
|
||||
}
|
||||
|
||||
if (Test-Path -LiteralPath $ConfigPath) {
|
||||
throw @"
|
||||
C:\MinerOpsAgent\agent-config.json already exists.
|
||||
This enrollment bootstrapper is for a new Agent identity and will not overwrite
|
||||
an existing identity. Use the normal MinerOps upgrade procedure instead.
|
||||
"@
|
||||
}
|
||||
|
||||
foreach ($recoveryPath in @($PendingConfigPath, $RecoveryConfigPath)) {
|
||||
if (Test-Path -LiteralPath $recoveryPath) {
|
||||
throw @"
|
||||
A previous enrollment recovery file exists at:
|
||||
$recoveryPath
|
||||
|
||||
Do not create or redeem another enrollment until that recovery state has been
|
||||
reviewed.
|
||||
"@
|
||||
}
|
||||
}
|
||||
|
||||
$enrollment = Get-Content -LiteralPath $EnrollmentPath -Raw |
|
||||
ConvertFrom-Json
|
||||
|
||||
$HubUrl = ([string] $enrollment.hub_url).TrimEnd("/")
|
||||
$EnrollmentToken = ([string] $enrollment.token).Trim()
|
||||
|
||||
$parsedHub = $null
|
||||
|
||||
if (
|
||||
-not [Uri]::TryCreate(
|
||||
$HubUrl,
|
||||
[UriKind]::Absolute,
|
||||
[ref] $parsedHub
|
||||
) -or
|
||||
$parsedHub.Scheme -notin @("http", "https")
|
||||
) {
|
||||
throw "enrollment.json contains an invalid MinerOps Hub URL."
|
||||
}
|
||||
|
||||
if (
|
||||
$EnrollmentToken.Length -lt 32 -or
|
||||
$EnrollmentToken.Length -gt 256
|
||||
) {
|
||||
throw "enrollment.json contains an invalid enrollment token."
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "MinerOps Windows Agent enrollment"
|
||||
Write-Host "Hub: $HubUrl"
|
||||
Write-Host ""
|
||||
Write-Host "API3 credentials stay only on this Windows Agent."
|
||||
Write-Host "Leave the account blank if API3 features are not being configured yet."
|
||||
Write-Host ""
|
||||
|
||||
$Api3Account = (Read-Host "WhatsMiner API3 account").Trim()
|
||||
$Api3PasswordPlain = ""
|
||||
|
||||
if ($Api3Account) {
|
||||
$Api3PasswordSecure = Read-Host `
|
||||
"WhatsMiner API3 password" `
|
||||
-AsSecureString
|
||||
|
||||
$Api3PasswordPlain = ConvertFrom-MinerOpsSecureString `
|
||||
-SecureValue $Api3PasswordSecure
|
||||
}
|
||||
|
||||
$Redeemed = $false
|
||||
$ConfigPersisted = $false
|
||||
|
||||
try {
|
||||
New-Item -ItemType Directory -Force -Path $Target | Out-Null
|
||||
|
||||
# Create protected empty files BEFORE token redemption. If persistence is
|
||||
# not possible, fail before consuming the single-use token.
|
||||
[IO.File]::WriteAllText($PendingConfigPath, "")
|
||||
Protect-MinerOpsCredentialFile -Path $PendingConfigPath
|
||||
|
||||
[IO.File]::WriteAllText($RecoveryConfigPath, "")
|
||||
Protect-MinerOpsCredentialFile -Path $RecoveryConfigPath
|
||||
|
||||
$requestBody = @{
|
||||
token = $EnrollmentToken
|
||||
} | ConvertTo-Json -Compress
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Redeeming one-time MinerOps enrollment..."
|
||||
|
||||
$response = Invoke-RestMethod `
|
||||
-Method Post `
|
||||
-Uri "$HubUrl/api/agent/enroll" `
|
||||
-ContentType "application/json" `
|
||||
-Body $requestBody `
|
||||
-TimeoutSec 30
|
||||
|
||||
$Redeemed = $true
|
||||
|
||||
if (-not $response.data) {
|
||||
throw "MinerOps enrollment returned no Agent data."
|
||||
}
|
||||
|
||||
$agent = $response.data
|
||||
|
||||
foreach ($required in @(
|
||||
"agent_id",
|
||||
"agent_secret",
|
||||
"site_id",
|
||||
"site_name",
|
||||
"agent_zone_id",
|
||||
"agent_zone_name"
|
||||
)) {
|
||||
if (-not ([string] $agent.$required).Trim()) {
|
||||
throw "MinerOps enrollment response is missing $required."
|
||||
}
|
||||
}
|
||||
|
||||
$ranges = @()
|
||||
|
||||
foreach ($cidr in @($agent.ip_ranges)) {
|
||||
$network = ([string] $cidr).Trim()
|
||||
|
||||
if (-not $network) {
|
||||
continue
|
||||
}
|
||||
|
||||
$ranges += [pscustomobject]@{
|
||||
name = "$($agent.agent_zone_name) - $network"
|
||||
cidr = $network
|
||||
enabled = $true
|
||||
api_port = 4028
|
||||
api3_port = 4433
|
||||
}
|
||||
}
|
||||
|
||||
$config = [ordered]@{
|
||||
hub_url = $HubUrl
|
||||
agent_id = [string] $agent.agent_id
|
||||
site_id = [string] $agent.site_id
|
||||
agent_zone_id = [string] $agent.agent_zone_id
|
||||
site_name = [string] $agent.site_name
|
||||
agent_secret = [string] $agent.agent_secret
|
||||
poll_interval_seconds = 120
|
||||
ip_ranges = @($ranges)
|
||||
api3_account = $Api3Account
|
||||
api3_password = $Api3PasswordPlain
|
||||
api3_port = 4433
|
||||
}
|
||||
|
||||
$configJson = $config | ConvertTo-Json -Depth 20
|
||||
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
||||
|
||||
# Write a protected recovery copy first. If anything after redemption
|
||||
# fails, the permanent Agent credential is not lost with the consumed token.
|
||||
[IO.File]::WriteAllText(
|
||||
$RecoveryConfigPath,
|
||||
$configJson + [Environment]::NewLine,
|
||||
$utf8NoBom
|
||||
)
|
||||
|
||||
[IO.File]::WriteAllText(
|
||||
$PendingConfigPath,
|
||||
$configJson + [Environment]::NewLine,
|
||||
$utf8NoBom
|
||||
)
|
||||
|
||||
Move-Item `
|
||||
-LiteralPath $PendingConfigPath `
|
||||
-Destination $ConfigPath
|
||||
|
||||
$ConfigPersisted = $true
|
||||
|
||||
Remove-Item -LiteralPath $RecoveryConfigPath -Force
|
||||
|
||||
# The extracted bootstrap token is no longer needed once permanent
|
||||
# credentials have been persisted locally.
|
||||
Remove-Item -LiteralPath $EnrollmentPath -Force
|
||||
|
||||
$EnrollmentToken = $null
|
||||
$requestBody = $null
|
||||
$Api3PasswordPlain = $null
|
||||
|
||||
Write-Host "Permanent Agent identity saved locally."
|
||||
Write-Host "Installing MinerOps Agent runtime..."
|
||||
|
||||
& $Installer
|
||||
|
||||
$mainTask = Get-ScheduledTask `
|
||||
-TaskName "MinerOpsAgent" `
|
||||
-ErrorAction Stop
|
||||
|
||||
if ([string] $mainTask.State -ne "Running") {
|
||||
Start-ScheduledTask -TaskName "MinerOpsAgent" -ErrorAction Stop
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "MinerOps Agent enrollment completed."
|
||||
Write-Host "Agent ID: $($agent.agent_id)"
|
||||
Write-Host "Site: $($agent.site_name)"
|
||||
Write-Host "Agent Zone: $($agent.agent_zone_name)"
|
||||
Write-Host ""
|
||||
Write-Host "Delete the downloaded ZIP and extracted setup folder after verification."
|
||||
}
|
||||
catch {
|
||||
if (-not $Redeemed) {
|
||||
foreach ($temporaryPath in @(
|
||||
$PendingConfigPath,
|
||||
$RecoveryConfigPath
|
||||
)) {
|
||||
if (Test-Path -LiteralPath $temporaryPath) {
|
||||
Remove-Item `
|
||||
-LiteralPath $temporaryPath `
|
||||
-Force `
|
||||
-ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
elseif ($ConfigPersisted) {
|
||||
Write-Warning @"
|
||||
The enrollment token was consumed and the permanent Agent identity WAS saved
|
||||
to C:\MinerOpsAgent\agent-config.json.
|
||||
|
||||
Do not create a replacement enrollment. Correct the installation problem and
|
||||
rerun install.ps1 from this package.
|
||||
"@
|
||||
}
|
||||
else {
|
||||
Write-Warning @"
|
||||
The enrollment token was consumed.
|
||||
|
||||
A protected credential recovery file may exist at:
|
||||
C:\MinerOpsAgent\agent-config.recovery.json
|
||||
|
||||
Do not create or redeem another enrollment until that recovery file has been
|
||||
reviewed.
|
||||
"@
|
||||
}
|
||||
|
||||
throw
|
||||
}
|
||||
Reference in new issue
Block a user