// Copyright Epic Games, Inc. All Rights Reserved. using System.ComponentModel.DataAnnotations; using EpicGames.Core; using EpicGames.Horde; using EpicGames.Horde.Utilities; using HordeCommon.Rpc; using HordeServer.Utilities; using Microsoft.Extensions.Configuration; namespace HordeAgent { /// /// Defines the operation mode of the agent /// Duplicated to prevent depending on Protobuf structures (weak name reference when deserializing JSON) /// public enum AgentMode { /// Dedicated, /// Workstation, } /// /// Describes a network share to mount /// public class MountNetworkShare { /// /// Where the share should be mounted on the local machine. Must be a drive letter for Windows. /// public string? MountPoint { get; set; } /// /// Path to the remote resource /// public string? RemotePath { get; set; } } /// /// Information about a server to use /// public class ServerProfile { /// /// Name of this server profile /// public string? Name { get; set; } /// /// Name of the environment (currently just used for tracing) /// [Required] public string Environment { get; set; } = "prod"; /// /// Url of the server /// [Required] public Uri Url { get; set; } = null!; /// /// Bearer token to use to initiate the connection /// public string? Token { get; set; } /// /// Whether to authenticate interactively in a desktop environment (for example, when agent is running on a user's workstation) /// public bool UseInteractiveAuth { get; set; } = false; /// /// Thumbprint of a certificate to trust. Allows using self-signed certs for the server. /// public string? Thumbprint { get; set; } /// /// Thumbprints of certificates to trust. Allows using self-signed certs for the server. /// public List Thumbprints { get; } = new List(); /// /// Returns auth token if interactive auth is disabled. Otherwise, null is returned. /// public string? GetAuthToken() { return UseInteractiveAuth ? null : Token; } /// /// Checks whether the given certificate thumbprint should be trusted /// /// The cert thumbprint /// True if the cert should be trusted public bool IsTrustedCertificate(string certificateThumbprint) { if (Thumbprint != null && Thumbprint.Equals(certificateThumbprint, StringComparison.OrdinalIgnoreCase)) { return true; } if (Thumbprints.Any(x => x.Equals(certificateThumbprint, StringComparison.OrdinalIgnoreCase))) { return true; } return false; } } /// /// Global settings for the agent /// [ConfigDoc("Agent.json (Agent)", "[Horde](../../README.md) > [Deployment](../Deployment.md) > [Agent](Agent.md)", "Deployment/AgentSettings.md", Introduction = "All Horde-specific settings are stored in a root object called `Horde`. Other .NET functionality may be configured using properties in the root of this file.")] public class AgentSettings { /// /// Name of the section containing these settings /// public const string SectionName = "Horde"; /// /// Known servers to connect to /// public Dictionary ServerProfiles { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); /// /// The default server, unless overridden from the command line /// public string? Server { get; set; } /// /// Name of agent to report as when connecting to server. /// By default, the computer's hostname will be used. /// public string? Name { get; set; } /// /// Mode of operation for the agent /// - For trusted agents in controlled environments (e.g., build farms). /// These agents handle all lease types and run exclusively Horde workloads. /// /// - For low-trust workstations, uses interactive authentication (human logs in). /// These agents yield to non-Horde workloads and only support compute leases for remote execution. /// public AgentMode? Mode { get; set; } = AgentMode.Dedicated; /// /// Whether the server is running in 'installed' mode. In this mode, on Windows, the default data directory will use the common /// application data folder (C:\ProgramData\Epic\Horde), and configuration data will be read from here and the registry. /// This setting is overridden to false for local builds from appsettings.Local.json. /// public bool Installed { get; set; } = true; /// /// Whether agent should register as being ephemeral. /// Doing so will not persist any long-lived data on the server and /// once disconnected it's assumed to have been deleted permanently. /// Ideal for short-lived agents, such as spot instances on AWS EC2. /// public bool Ephemeral { get; set; } = false; /// /// Working directory for leases and jobs (i.e where files from Perforce will be checked out) /// public DirectoryReference WorkingDir { get; set; } = DirectoryReference.Combine(AgentApp.DataDir, "Sandbox"); /// /// Directory where agent and lease logs are written /// public DirectoryReference LogsDir { get; set; } = AgentApp.DataDir; /// /// Whether to mount the specified list of network shares /// public bool ShareMountingEnabled { get; set; } = true; /// /// List of network shares to mount /// public List Shares { get; } = new List(); /// /// Path to Wine executable. If null, execution under Wine is disabled /// public string? WineExecutablePath { get; set; } /// /// Path to container engine executable, such as /usr/bin/podman. If null, execution of compute workloads inside a container is disabled /// public string? ContainerEngineExecutablePath { get; set; } /// /// Whether to write step output to the logging device /// public bool WriteStepOutputToLogger { get; set; } /// /// Queries information about the current agent through the AWS EC2 interface /// public bool EnableAwsEc2Support { get; set; } = false; /// /// Option to use a local storage client rather than connecting through the server. Primarily for convenience when debugging / iterating locally. /// public bool UseLocalStorageClient { get; set; } /// /// Incoming IP for listening for compute work. If not set, it will be automatically resolved. /// public string? ComputeIp { get; set; } = null; /// /// Incoming port for listening for compute work. Needs to be tied with a lease. Set port to 0 to disable incoming compute requests. /// public int ComputePort { get; set; } = 7000; /// /// Options for OpenTelemetry /// public OpenTelemetrySettings OpenTelemetry { get; set; } = new (); /// /// Whether to send telemetry back to Horde server /// public bool EnableTelemetry { get; set; } = false; /// /// How often to report telemetry events to server in milliseconds /// public int TelemetryReportInterval { get; set; } = 30 * 1000; /// /// Maximum size of the bundle cache, in megabytes. /// public long BundleCacheSize { get; set; } = 1024; /// /// Maximum number of logical CPU cores workloads should use /// Currently this is only provided as a hint and requires leases to respect this value as it's set via an env variable (UE_HORDE_CPU_COUNT). /// public int? CpuCount { get; set; } = null; /// /// CPU core multiplier applied to CPU core count setting /// For example, 32 CPU cores and a multiplier of 0.5 results in max 16 CPU usage. /// /// public double CpuMultiplier { get; set; } = 1.0; /// /// Key/value properties in addition to those set internally by the agent /// public Dictionary Properties { get; } = new(); /// /// Listen addresses for the built-in HTTP admin/management server. Disabled when empty. /// If activated, it's recommended to bind only to localhost for security reasons. /// Example: localhost:7008 to listen on localhost, port 7008 /// public string[] AdminEndpoints { get; set; } = []; /// /// Listen addresses for the built-in HTTP health check server. Disabled when empty. /// If activated, it's recommended to bind only to localhost for security reasons. /// Example: *:7009 to listen on all interfaces/IPs, port 7009 /// If all interfaces are bound with *, make sure to run process as administrator. /// public string[] HealthCheckEndpoints { get; set; } = []; /// /// Gets the current server settings /// /// The current server settings public ServerProfile GetServerProfile(string name) { ServerProfile? serverProfile; if (!ServerProfiles.TryGetValue(name, out serverProfile)) { serverProfile = ServerProfiles.Values.FirstOrDefault(x => name.Equals(x.Name, StringComparison.OrdinalIgnoreCase)); if (serverProfile == null) { if (ServerProfiles.Count == 0) { throw new Exception("No server profiles are defined (missing configuration?)"); } else { throw new Exception($"Unknown server profile name '{name}' (valid profiles: {GetServerProfileNames()})"); } } } return serverProfile; } string GetServerProfileNames() { HashSet names = new HashSet(StringComparer.OrdinalIgnoreCase); foreach ((string key, ServerProfile profile) in ServerProfiles) { if (!String.IsNullOrEmpty(profile.Name) && Int32.TryParse(key, out _)) { names.Add(profile.Name); } else { names.Add(key); } } return String.Join("/", names); } /// /// Gets the current server settings /// /// The current server settings public ServerProfile GetCurrentServerProfile() { if (Server == null) { Uri? defaultServerUrl = Installed ? HordeOptions.GetDefaultServerUrl() : null; ServerProfile defaultServerProfile = new ServerProfile(); defaultServerProfile.Name = "Default"; defaultServerProfile.Environment = "Development"; defaultServerProfile.Url = defaultServerUrl ?? new Uri("http://localhost:5000"); return defaultServerProfile; } return GetServerProfile(Server); } /// /// Path to file used for signaling impending termination and shutdown of the agent /// /// Path to file which may or may not exist public FileReference GetTerminationSignalFile() { return FileReference.Combine(WorkingDir, ".horde-termination-signal"); } internal string GetAgentName() { return Name ?? Environment.MachineName; } internal Mode GetMode() { return Mode switch { AgentMode.Dedicated => HordeCommon.Rpc.Mode.Dedicated, AgentMode.Workstation => HordeCommon.Rpc.Mode.Workstation, _ => throw new Exception($"Unknown agent mode: {Mode}") }; } } /// /// Extension methods for retrieving config settings /// public static class AgentSettingsExtensions { /// /// Gets the configuration section for the active server profile /// /// /// public static IConfigurationSection GetCurrentServerProfile(this IConfigurationSection configSection) { string? profileName = configSection[nameof(AgentSettings.Server)]; if (profileName == null) { throw new Exception("Server is not set"); } return configSection.GetSection(nameof(AgentSettings.ServerProfiles)).GetChildren().First(x => x[nameof(ServerProfile.Name)] == profileName); } } }