mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 04:32:32 +00:00
Agent: use remote-config exclusivly
Function: deliver new default config
This commit is contained in:
@@ -7,23 +7,16 @@
|
||||
It runs VERY EARLY in the enrollment process (first Intune action) and:
|
||||
1. Creates a unique session ID for this enrollment
|
||||
2. Downloads the monitoring agent binaries
|
||||
3. Configures the agent (filesystem only - NO registry)
|
||||
3. Installs agent binaries (agent uses built-in backend URL or CLI override)
|
||||
4. Registers agent as Scheduled Task (runs on computer startup)
|
||||
5. Agent self-destructs when enrollment completes
|
||||
|
||||
.PARAMETER ApiBaseUrl
|
||||
The base URL of the backend API (default: http://localhost:7071 for local testing)
|
||||
Production example: https://autopilot-api.azurewebsites.net
|
||||
|
||||
.PARAMETER AgentDownloadUrl
|
||||
URL to download the agent binaries from (ZIP file)
|
||||
|
||||
.EXAMPLE
|
||||
.\Install-AutopilotMonitor.ps1
|
||||
(Uses default local API for testing)
|
||||
|
||||
.EXAMPLE
|
||||
.\Install-AutopilotMonitor.ps1 -ApiBaseUrl "https://autopilot-api.azurewebsites.net"
|
||||
(Uses built-in backend URL from the agent)
|
||||
|
||||
.NOTES
|
||||
- Agent is temporary and auto-removes after enrollment
|
||||
@@ -34,21 +27,16 @@
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
#[string]$ApiBaseUrl = "http://localhost:7071",
|
||||
[string]$ApiBaseUrl = "https://autopilotmonitor-func.azurewebsites.net",
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$AgentDownloadUrl = "https://autopilotmonitor.blob.core.windows.net/agent/AutopilotMonitor-Agent.zip",
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[int]$MaxOsAgeHours = 5
|
||||
[int]$MaxOsAgeMinutes = 60
|
||||
)
|
||||
|
||||
# Configuration - Everything in ProgramData for easy cleanup
|
||||
$AgentBasePath = "$env:ProgramData\AutopilotMonitor"
|
||||
$AgentBinPath = "$AgentBasePath\Agent"
|
||||
$AgentConfigPath = "$AgentBasePath\Config"
|
||||
$AgentSpoolPath = "$AgentBasePath\Spool"
|
||||
$AgentLogsPath = "$AgentBasePath\Logs"
|
||||
$TaskName = "AutopilotMonitor-Agent"
|
||||
@@ -57,7 +45,6 @@ $LogFile = "$AgentLogsPath\Bootstrap.log"
|
||||
# Ensure directories exist
|
||||
New-Item -Path $AgentBasePath -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null
|
||||
New-Item -Path $AgentBinPath -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null
|
||||
New-Item -Path $AgentConfigPath -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null
|
||||
New-Item -Path $AgentSpoolPath -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null
|
||||
New-Item -Path $AgentLogsPath -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null
|
||||
|
||||
@@ -94,18 +81,17 @@ function Get-FileMd5Base64 {
|
||||
|
||||
try {
|
||||
Write-Log "===== Autopilot Monitor Bootstrap Started ====="
|
||||
Write-Log "API Base URL: $ApiBaseUrl"
|
||||
|
||||
# ── Pre-flight: Skip installation on already-enrolled devices ──
|
||||
|
||||
# Check 1: OS install date must be within threshold (fresh device)
|
||||
$osInstallDate = (Get-CimInstance Win32_OperatingSystem).InstallDate
|
||||
$osAge = (Get-Date) - $osInstallDate
|
||||
$osAgeHoursRounded = "{0:0.0}" -f $osAge.TotalHours
|
||||
Write-Log "OS install date: $osInstallDate (age: ${osAgeHoursRounded}h)"
|
||||
$osAgeMinutesRounded = [int][Math]::Round($osAge.TotalMinutes)
|
||||
Write-Log "OS install date: $osInstallDate (age: ${osAgeMinutesRounded}m)"
|
||||
|
||||
if ($osAge.TotalHours -gt $MaxOsAgeHours) {
|
||||
Write-Log "SKIP: OS was installed ${osAgeHoursRounded}h ago (threshold: ${MaxOsAgeHours}h). Device is not freshly provisioned."
|
||||
if ($osAge.TotalMinutes -gt $MaxOsAgeMinutes) {
|
||||
Write-Log "SKIP: OS was installed ${osAgeMinutesRounded}m ago (threshold: ${MaxOsAgeMinutes}m). Device is not freshly provisioned."
|
||||
exit 0
|
||||
}
|
||||
|
||||
@@ -149,22 +135,6 @@ try {
|
||||
|
||||
Write-Log "Pre-flight checks passed - device is freshly provisioned and enrollment in progress"
|
||||
|
||||
# Create agent configuration (JSON - NO REGISTRY!)
|
||||
Write-Log "Creating agent configuration..."
|
||||
$agentConfig = @{
|
||||
apiBaseUrl = $ApiBaseUrl
|
||||
uploadIntervalSeconds = 30
|
||||
cleanupOnExit = $false
|
||||
selfDestructOnComplete = $false
|
||||
keepLogFile = $true
|
||||
imeMatchLogPath = "C:\ProgramData\AutopilotMonitor\Logs\ime_pattern_matches.log"
|
||||
}
|
||||
|
||||
$agentConfigJson = $agentConfig | ConvertTo-Json -Depth 10
|
||||
$agentConfigFile = Join-Path $AgentConfigPath "agent-config.json"
|
||||
Set-Content -Path $agentConfigFile -Value $agentConfigJson -Force
|
||||
Write-Log "Configuration saved to $agentConfigFile"
|
||||
|
||||
# Download and extract agent binaries
|
||||
$agentExePath = Join-Path $AgentBinPath "AutopilotMonitor.Agent.exe"
|
||||
|
||||
@@ -291,7 +261,6 @@ try {
|
||||
Write-Log "===== Bootstrap Completed Successfully ====="
|
||||
Write-Log "Agent Path: $AgentBasePath"
|
||||
Write-Log "Scheduled Task: $TaskName"
|
||||
Write-Log "API Base URL: $ApiBaseUrl"
|
||||
Write-Log "Agent will monitor enrollment and auto-remove when complete"
|
||||
Write-Log "Bootstrap log: $LogFile"
|
||||
Write-Log ""
|
||||
@@ -299,7 +268,7 @@ try {
|
||||
Write-Log " - Generate unique session ID on startup"
|
||||
Write-Log " - Survive reboots during enrollment"
|
||||
Write-Log " - Monitor enrollment phases in real-time"
|
||||
Write-Log " - Upload events to $ApiBaseUrl"
|
||||
Write-Log " - Upload events to built-in backend URL (or CLI override /--backend-api)"
|
||||
Write-Log " - Self-destruct when enrollment completes"
|
||||
Write-Log " - Clean removal: delete C:\ProgramData\AutopilotMonitor folder"
|
||||
|
||||
|
||||
@@ -58,11 +58,10 @@ namespace AutopilotMonitor.Agent.Core.Configuration
|
||||
_logger.Info("Fetching remote configuration from backend...");
|
||||
var config = await _apiClient.GetAgentConfigAsync(_tenantId);
|
||||
|
||||
if (config != null && config.Success)
|
||||
if (config != null)
|
||||
{
|
||||
_logger.Info($"Remote config fetched: ConfigVersion={config.ConfigVersion}, " +
|
||||
$"GatherRules={config.GatherRules?.Count ?? 0}, " +
|
||||
$"RefreshInterval={config.RefreshIntervalSeconds}s");
|
||||
$"GatherRules={config.GatherRules?.Count ?? 0}");
|
||||
|
||||
LogCollectorSettings(config.Collectors);
|
||||
SetConfig(config);
|
||||
@@ -70,7 +69,7 @@ namespace AutopilotMonitor.Agent.Core.Configuration
|
||||
return config;
|
||||
}
|
||||
|
||||
_logger.Warning($"Remote config fetch returned unsuccessful: {config?.Message}");
|
||||
_logger.Warning("Remote config fetch returned empty response");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -148,11 +147,15 @@ namespace AutopilotMonitor.Agent.Core.Configuration
|
||||
{
|
||||
return new AgentConfigResponse
|
||||
{
|
||||
Success = true,
|
||||
Message = "Default configuration (offline fallback)",
|
||||
Collectors = CollectorConfiguration.CreateDefault(),
|
||||
ConfigVersion = 0,
|
||||
RefreshIntervalSeconds = 300
|
||||
UploadIntervalSeconds = 30,
|
||||
CleanupOnExit = false,
|
||||
SelfDestructOnComplete = false,
|
||||
KeepLogFile = true,
|
||||
ImeMatchLogPath = @"C:\ProgramData\AutopilotMonitor\Logs\ime_pattern_matches.log",
|
||||
Collectors = CollectorConfiguration.CreateDefault(),
|
||||
GatherRules = new System.Collections.Generic.List<GatherRule>(),
|
||||
ImeLogPatterns = new System.Collections.Generic.List<ImeLogPattern>()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -215,6 +215,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
|
||||
{
|
||||
_remoteConfigService = new RemoteConfigService(_apiClient, _configuration.TenantId, _logger);
|
||||
_remoteConfigService.FetchConfigAsync().Wait(TimeSpan.FromSeconds(15));
|
||||
ApplyRuntimeSettingsFromRemoteConfig();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -222,6 +223,26 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyRuntimeSettingsFromRemoteConfig()
|
||||
{
|
||||
var config = _remoteConfigService?.CurrentConfig;
|
||||
if (config == null) return;
|
||||
|
||||
_configuration.UploadIntervalSeconds = config.UploadIntervalSeconds;
|
||||
_configuration.CleanupOnExit = config.CleanupOnExit;
|
||||
_configuration.SelfDestructOnComplete = config.SelfDestructOnComplete;
|
||||
_configuration.KeepLogFile = config.KeepLogFile;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(config.ImeMatchLogPath))
|
||||
{
|
||||
_configuration.ImeMatchLogPath = config.ImeMatchLogPath;
|
||||
}
|
||||
|
||||
_logger.Info("Applied runtime settings from remote config");
|
||||
_logger.Info($" uploadIntervalSeconds={_configuration.UploadIntervalSeconds}, cleanupOnExit={_configuration.CleanupOnExit}, selfDestructOnComplete={_configuration.SelfDestructOnComplete}, keepLogFile={_configuration.KeepLogFile}");
|
||||
_logger.Info($" imeMatchLogPath={_configuration.ImeMatchLogPath ?? "(none)"}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts optional collectors based on remote configuration
|
||||
/// </summary>
|
||||
|
||||
@@ -8,7 +8,6 @@ using AutopilotMonitor.Agent.Core.Configuration;
|
||||
using AutopilotMonitor.Agent.Core.Logging;
|
||||
using AutopilotMonitor.Agent.Core.Monitoring.Core;
|
||||
using Microsoft.Win32;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace AutopilotMonitor.Agent
|
||||
{
|
||||
@@ -289,6 +288,7 @@ namespace AutopilotMonitor.Agent
|
||||
|
||||
string certThumbprint = null;
|
||||
string tenantIdOverride = null;
|
||||
string apiBaseUrlOverride = null;
|
||||
string imeLogPathOverride = null;
|
||||
string imeMatchLogPath = null;
|
||||
string simulationLogDir = null;
|
||||
@@ -304,6 +304,13 @@ namespace AutopilotMonitor.Agent
|
||||
if (tenantIdIndex >= 0 && tenantIdIndex + 1 < args.Length)
|
||||
tenantIdOverride = args[tenantIdIndex + 1];
|
||||
|
||||
// Local backend URL override for debugging/testing.
|
||||
// Supported aliases:
|
||||
// --ApiUrl https://...
|
||||
// --api-url https://...
|
||||
// --backend-api https://...
|
||||
apiBaseUrlOverride = GetArgValue(args, "--ApiUrl", "--api-url", "--backend-api");
|
||||
|
||||
var imeLogPathIndex = Array.IndexOf(args, "--ime-log-path");
|
||||
if (imeLogPathIndex >= 0 && imeLogPathIndex + 1 < args.Length)
|
||||
imeLogPathOverride = args[imeLogPathIndex + 1];
|
||||
@@ -325,56 +332,17 @@ namespace AutopilotMonitor.Agent
|
||||
// Defaults
|
||||
string apiBaseUrl = "https://autopilotmonitor-func.azurewebsites.net";
|
||||
int uploadIntervalSeconds = 30;
|
||||
bool cleanupOnExit = true;
|
||||
bool selfDestructOnComplete = true;
|
||||
bool cleanupOnExit = false;
|
||||
bool selfDestructOnComplete = false;
|
||||
bool rebootOnCompleteConfig = false;
|
||||
bool enableGeoLocationConfig = true;
|
||||
bool useClientCertAuthConfig = true;
|
||||
|
||||
// Load configuration JSON written by PowerShell bootstrap script
|
||||
var configFilePath = Environment.ExpandEnvironmentVariables(@"%ProgramData%\AutopilotMonitor\Config\agent-config.json");
|
||||
if (File.Exists(configFilePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
var configJson = File.ReadAllText(configFilePath);
|
||||
var configDict = JsonConvert.DeserializeObject<Dictionary<string, object>>(configJson);
|
||||
|
||||
if (configDict != null)
|
||||
{
|
||||
if (configDict.ContainsKey("apiBaseUrl") && configDict["apiBaseUrl"] != null)
|
||||
apiBaseUrl = configDict["apiBaseUrl"].ToString();
|
||||
|
||||
if (configDict.ContainsKey("uploadIntervalSeconds") && configDict["uploadIntervalSeconds"] != null)
|
||||
uploadIntervalSeconds = Convert.ToInt32(configDict["uploadIntervalSeconds"]);
|
||||
|
||||
if (configDict.ContainsKey("cleanupOnExit") && configDict["cleanupOnExit"] != null)
|
||||
cleanupOnExit = Convert.ToBoolean(configDict["cleanupOnExit"]);
|
||||
|
||||
if (configDict.ContainsKey("selfDestructOnComplete") && configDict["selfDestructOnComplete"] != null)
|
||||
selfDestructOnComplete = Convert.ToBoolean(configDict["selfDestructOnComplete"]);
|
||||
|
||||
if (configDict.ContainsKey("rebootOnComplete") && configDict["rebootOnComplete"] != null)
|
||||
rebootOnCompleteConfig = Convert.ToBoolean(configDict["rebootOnComplete"]);
|
||||
|
||||
if (configDict.ContainsKey("enableGeoLocation") && configDict["enableGeoLocation"] != null)
|
||||
enableGeoLocationConfig = Convert.ToBoolean(configDict["enableGeoLocation"]);
|
||||
|
||||
if (configDict.ContainsKey("useClientCertAuth") && configDict["useClientCertAuth"] != null)
|
||||
useClientCertAuthConfig = Convert.ToBoolean(configDict["useClientCertAuth"]);
|
||||
|
||||
if (configDict.ContainsKey("keepLogFile") && configDict["keepLogFile"] != null)
|
||||
keepLogFile = keepLogFile || Convert.ToBoolean(configDict["keepLogFile"]);
|
||||
|
||||
if (configDict.ContainsKey("imeMatchLogPath") && configDict["imeMatchLogPath"] != null)
|
||||
imeMatchLogPath = configDict["imeMatchLogPath"].ToString();
|
||||
}
|
||||
}
|
||||
catch { /* use defaults */ }
|
||||
}
|
||||
|
||||
// Environment variable overrides config file
|
||||
// Environment variable overrides built-in default
|
||||
apiBaseUrl = Environment.GetEnvironmentVariable("AUTOPILOT_MONITOR_API") ?? apiBaseUrl;
|
||||
// CLI override wins over environment variable/default
|
||||
if (!string.IsNullOrWhiteSpace(apiBaseUrlOverride))
|
||||
apiBaseUrl = apiBaseUrlOverride;
|
||||
|
||||
if (noCleanup)
|
||||
{
|
||||
@@ -429,6 +397,25 @@ namespace AutopilotMonitor.Agent
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetArgValue(string[] args, params string[] names)
|
||||
{
|
||||
if (args == null || names == null || names.Length == 0)
|
||||
return null;
|
||||
|
||||
for (int i = 0; i < args.Length - 1; i++)
|
||||
{
|
||||
foreach (var name in names)
|
||||
{
|
||||
if (string.Equals(args[i], name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return args[i + 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if another AutopilotMonitor.Agent.exe process is already running.
|
||||
/// </summary>
|
||||
|
||||
@@ -1 +1 @@
|
||||
5
|
||||
9
|
||||
|
||||
@@ -45,10 +45,9 @@ namespace AutopilotMonitor.Functions.Functions
|
||||
if (string.IsNullOrEmpty(tenantId))
|
||||
{
|
||||
var badRequest = req.CreateResponse(HttpStatusCode.BadRequest);
|
||||
await badRequest.WriteAsJsonAsync(new AgentConfigResponse
|
||||
await badRequest.WriteAsJsonAsync(new
|
||||
{
|
||||
Success = false,
|
||||
Message = "tenantId query parameter is required"
|
||||
error = "tenantId query parameter is required"
|
||||
});
|
||||
return badRequest;
|
||||
}
|
||||
@@ -87,13 +86,15 @@ namespace AutopilotMonitor.Functions.Functions
|
||||
var response = req.CreateResponse(HttpStatusCode.OK);
|
||||
await response.WriteAsJsonAsync(new AgentConfigResponse
|
||||
{
|
||||
Success = true,
|
||||
Message = "Configuration retrieved successfully",
|
||||
ConfigVersion = 2,
|
||||
UploadIntervalSeconds = 30,
|
||||
CleanupOnExit = false,
|
||||
SelfDestructOnComplete = false,
|
||||
KeepLogFile = true,
|
||||
ImeMatchLogPath = @"C:\ProgramData\AutopilotMonitor\Logs\ime_pattern_matches.log",
|
||||
Collectors = collectors,
|
||||
GatherRules = gatherRules,
|
||||
ImeLogPatterns = imeLogPatterns,
|
||||
ConfigVersion = 1,
|
||||
RefreshIntervalSeconds = 300
|
||||
ImeLogPatterns = imeLogPatterns
|
||||
});
|
||||
|
||||
return response;
|
||||
@@ -102,10 +103,9 @@ namespace AutopilotMonitor.Functions.Functions
|
||||
{
|
||||
_logger.LogError(ex, "Error getting agent config");
|
||||
var errorResp = req.CreateResponse(HttpStatusCode.InternalServerError);
|
||||
await errorResp.WriteAsJsonAsync(new AgentConfigResponse
|
||||
await errorResp.WriteAsJsonAsync(new
|
||||
{
|
||||
Success = false,
|
||||
Message = "Internal server error"
|
||||
error = "Internal server error"
|
||||
});
|
||||
return errorResp;
|
||||
}
|
||||
|
||||
@@ -8,8 +8,37 @@ namespace AutopilotMonitor.Shared.Models
|
||||
/// </summary>
|
||||
public class AgentConfigResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string Message { get; set; }
|
||||
/// <summary>
|
||||
/// Semantic config version from backend.
|
||||
/// Used for debugging and future schema evolution.
|
||||
/// </summary>
|
||||
public int ConfigVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Event upload debounce interval in seconds.
|
||||
/// </summary>
|
||||
public int UploadIntervalSeconds { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to cleanup files on exit.
|
||||
/// </summary>
|
||||
public bool CleanupOnExit { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to self-destruct after enrollment completion.
|
||||
/// </summary>
|
||||
public bool SelfDestructOnComplete { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Preserve logs during cleanup/self-destruct.
|
||||
/// </summary>
|
||||
public bool KeepLogFile { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Optional path to IME pattern match log file.
|
||||
/// </summary>
|
||||
public string ImeMatchLogPath { get; set; } = @"C:\ProgramData\AutopilotMonitor\Logs\ime_pattern_matches.log";
|
||||
|
||||
public CollectorConfiguration Collectors { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -22,14 +51,6 @@ namespace AutopilotMonitor.Shared.Models
|
||||
/// Delivered from backend so patterns can be updated without agent rebuild.
|
||||
/// </summary>
|
||||
public List<ImeLogPattern> ImeLogPatterns { get; set; } = new List<ImeLogPattern>();
|
||||
|
||||
public int ConfigVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How often the agent should re-fetch config (seconds)
|
||||
/// Default: 300 (5 minutes)
|
||||
/// </summary>
|
||||
public int RefreshIntervalSeconds { get; set; } = 300;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user