feat(lmxproxy): phase 1 — v2 protocol types and domain model
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Serilog;
|
||||
|
||||
namespace ZB.MOM.WW.LmxProxy.Host.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates LmxProxy configuration settings on startup.
|
||||
/// </summary>
|
||||
public static class ConfigurationValidator
|
||||
{
|
||||
private static readonly ILogger Logger = Log.ForContext(typeof(ConfigurationValidator));
|
||||
|
||||
/// <summary>
|
||||
/// Validates the provided configuration and returns a list of validation errors.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration to validate.</param>
|
||||
/// <returns>A list of validation error messages. Empty if configuration is valid.</returns>
|
||||
public static List<string> Validate(LmxProxyConfiguration configuration)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
if (configuration == null)
|
||||
{
|
||||
errors.Add("Configuration is null");
|
||||
return errors;
|
||||
}
|
||||
|
||||
// Validate gRPC port
|
||||
if (configuration.GrpcPort <= 0 || configuration.GrpcPort > 65535)
|
||||
{
|
||||
errors.Add($"Invalid gRPC port: {configuration.GrpcPort}. Must be between 1 and 65535.");
|
||||
}
|
||||
|
||||
// Validate API key configuration file
|
||||
if (string.IsNullOrWhiteSpace(configuration.ApiKeyConfigFile))
|
||||
{
|
||||
errors.Add("API key configuration file path is not specified.");
|
||||
}
|
||||
|
||||
// Validate Connection settings
|
||||
if (configuration.Connection != null)
|
||||
{
|
||||
ValidateConnectionConfiguration(configuration.Connection, errors);
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.Add("Connection configuration is missing.");
|
||||
}
|
||||
|
||||
// Validate Subscription settings
|
||||
if (configuration.Subscription != null)
|
||||
{
|
||||
ValidateSubscriptionConfiguration(configuration.Subscription, errors);
|
||||
}
|
||||
|
||||
// Validate Service Recovery settings
|
||||
if (configuration.ServiceRecovery != null)
|
||||
{
|
||||
ValidateServiceRecoveryConfiguration(configuration.ServiceRecovery, errors);
|
||||
}
|
||||
|
||||
// Validate TLS settings
|
||||
if (configuration.Tls != null)
|
||||
{
|
||||
if (!configuration.Tls.Validate())
|
||||
{
|
||||
errors.Add("TLS configuration validation failed. Check the logs for details.");
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
private static void ValidateConnectionConfiguration(ConnectionConfiguration config, List<string> errors)
|
||||
{
|
||||
if (config.MonitorIntervalSeconds <= 0)
|
||||
{
|
||||
errors.Add(
|
||||
$"Invalid monitor interval: {config.MonitorIntervalSeconds} seconds. Must be greater than 0.");
|
||||
}
|
||||
|
||||
if (config.ConnectionTimeoutSeconds <= 0)
|
||||
{
|
||||
errors.Add(
|
||||
$"Invalid connection timeout: {config.ConnectionTimeoutSeconds} seconds. Must be greater than 0.");
|
||||
}
|
||||
|
||||
if (config.ReadTimeoutSeconds <= 0)
|
||||
{
|
||||
errors.Add($"Invalid read timeout: {config.ReadTimeoutSeconds} seconds. Must be greater than 0.");
|
||||
}
|
||||
|
||||
if (config.WriteTimeoutSeconds <= 0)
|
||||
{
|
||||
errors.Add($"Invalid write timeout: {config.WriteTimeoutSeconds} seconds. Must be greater than 0.");
|
||||
}
|
||||
|
||||
if (config.MaxConcurrentOperations.HasValue && config.MaxConcurrentOperations.Value <= 0)
|
||||
{
|
||||
errors.Add(
|
||||
$"Invalid max concurrent operations: {config.MaxConcurrentOperations}. Must be greater than 0.");
|
||||
}
|
||||
|
||||
// Validate node and galaxy names if provided
|
||||
if (!string.IsNullOrWhiteSpace(config.NodeName) && config.NodeName?.Length > 255)
|
||||
{
|
||||
errors.Add($"Node name is too long: {config.NodeName.Length} characters. Maximum is 255.");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(config.GalaxyName) && config.GalaxyName?.Length > 255)
|
||||
{
|
||||
errors.Add($"Galaxy name is too long: {config.GalaxyName.Length} characters. Maximum is 255.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateSubscriptionConfiguration(SubscriptionConfiguration config, List<string> errors)
|
||||
{
|
||||
if (config.ChannelCapacity <= 0)
|
||||
{
|
||||
errors.Add($"Invalid channel capacity: {config.ChannelCapacity}. Must be greater than 0.");
|
||||
}
|
||||
|
||||
if (config.ChannelCapacity > 100000)
|
||||
{
|
||||
errors.Add($"Channel capacity too large: {config.ChannelCapacity}. Maximum recommended is 100000.");
|
||||
}
|
||||
|
||||
string[] validChannelModes = { "DropOldest", "DropNewest", "Wait" };
|
||||
if (!validChannelModes.Contains(config.ChannelFullMode))
|
||||
{
|
||||
errors.Add(
|
||||
$"Invalid channel full mode: {config.ChannelFullMode}. Valid values are: {string.Join(", ", validChannelModes)}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateServiceRecoveryConfiguration(ServiceRecoveryConfiguration config,
|
||||
List<string> errors)
|
||||
{
|
||||
if (config.FirstFailureDelayMinutes < 0)
|
||||
{
|
||||
errors.Add(
|
||||
$"Invalid first failure delay: {config.FirstFailureDelayMinutes} minutes. Must be 0 or greater.");
|
||||
}
|
||||
|
||||
if (config.SecondFailureDelayMinutes < 0)
|
||||
{
|
||||
errors.Add(
|
||||
$"Invalid second failure delay: {config.SecondFailureDelayMinutes} minutes. Must be 0 or greater.");
|
||||
}
|
||||
|
||||
if (config.SubsequentFailureDelayMinutes < 0)
|
||||
{
|
||||
errors.Add(
|
||||
$"Invalid subsequent failure delay: {config.SubsequentFailureDelayMinutes} minutes. Must be 0 or greater.");
|
||||
}
|
||||
|
||||
if (config.ResetPeriodDays <= 0)
|
||||
{
|
||||
errors.Add($"Invalid reset period: {config.ResetPeriodDays} days. Must be greater than 0.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs validation results and returns whether the configuration is valid.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration to validate.</param>
|
||||
/// <returns>True if configuration is valid; otherwise, false.</returns>
|
||||
public static bool ValidateAndLog(LmxProxyConfiguration configuration)
|
||||
{
|
||||
List<string> errors = Validate(configuration);
|
||||
|
||||
if (errors.Any())
|
||||
{
|
||||
Logger.Error("Configuration validation failed with {ErrorCount} errors:", errors.Count);
|
||||
foreach (string? error in errors)
|
||||
{
|
||||
Logger.Error(" - {ValidationError}", error);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger.Information("Configuration validation successful");
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an exception if the configuration is invalid.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration to validate.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown when configuration is invalid.</exception>
|
||||
public static void ValidateOrThrow(LmxProxyConfiguration configuration)
|
||||
{
|
||||
List<string> errors = Validate(configuration);
|
||||
|
||||
if (errors.Any())
|
||||
{
|
||||
string message = $"Configuration validation failed with {errors.Count} error(s):\n" +
|
||||
string.Join("\n", errors.Select(e => $" - {e}"));
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
namespace ZB.MOM.WW.LmxProxy.Host.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration settings for LmxProxy service
|
||||
/// </summary>
|
||||
public class LmxProxyConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// gRPC server port
|
||||
/// </summary>
|
||||
public int GrpcPort { get; set; } = 50051;
|
||||
|
||||
/// <summary>
|
||||
/// Subscription management settings
|
||||
/// </summary>
|
||||
public SubscriptionConfiguration Subscription { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Windows service recovery settings
|
||||
/// </summary>
|
||||
public ServiceRecoveryConfiguration ServiceRecovery { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// API key configuration file path
|
||||
/// </summary>
|
||||
public string ApiKeyConfigFile { get; set; } = "apikeys.json";
|
||||
|
||||
/// <summary>
|
||||
/// MxAccess connection settings
|
||||
/// </summary>
|
||||
public ConnectionConfiguration Connection { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// TLS/SSL configuration for secure gRPC communication
|
||||
/// </summary>
|
||||
public TlsConfiguration Tls { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Web server configuration for status display
|
||||
/// </summary>
|
||||
public WebServerConfiguration WebServer { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for MxAccess connection monitoring and reconnection
|
||||
/// </summary>
|
||||
public class ConnectionConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Interval in seconds between connection health checks
|
||||
/// </summary>
|
||||
public int MonitorIntervalSeconds { get; set; } = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Timeout in seconds for initial connection attempts
|
||||
/// </summary>
|
||||
public int ConnectionTimeoutSeconds { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to automatically reconnect when connection is lost
|
||||
/// </summary>
|
||||
public bool AutoReconnect { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Timeout in seconds for read operations
|
||||
/// </summary>
|
||||
public int ReadTimeoutSeconds { get; set; } = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Timeout in seconds for write operations
|
||||
/// </summary>
|
||||
public int WriteTimeoutSeconds { get; set; } = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of concurrent read/write operations allowed
|
||||
/// </summary>
|
||||
public int? MaxConcurrentOperations { get; set; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Name of the node to connect to (optional)
|
||||
/// </summary>
|
||||
public string? NodeName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the galaxy to connect to (optional)
|
||||
/// </summary>
|
||||
public string? GalaxyName { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for web server that displays status information
|
||||
/// </summary>
|
||||
public class WebServerConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the web server is enabled
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Port number for the web server
|
||||
/// </summary>
|
||||
public int Port { get; set; } = 8080;
|
||||
|
||||
/// <summary>
|
||||
/// Prefix URL for the web server (default: http://+:{Port}/)
|
||||
/// </summary>
|
||||
public string? Prefix { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace ZB.MOM.WW.LmxProxy.Host.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration for Windows service recovery
|
||||
/// </summary>
|
||||
public class ServiceRecoveryConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Minutes to wait before restart on first failure
|
||||
/// </summary>
|
||||
public int FirstFailureDelayMinutes { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Minutes to wait before restart on second failure
|
||||
/// </summary>
|
||||
public int SecondFailureDelayMinutes { get; set; } = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Minutes to wait before restart on subsequent failures
|
||||
/// </summary>
|
||||
public int SubsequentFailureDelayMinutes { get; set; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Days before resetting the failure count
|
||||
/// </summary>
|
||||
public int ResetPeriodDays { get; set; } = 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace ZB.MOM.WW.LmxProxy.Host.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration for subscription management
|
||||
/// </summary>
|
||||
public class SubscriptionConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Buffer size for each client's channel (number of messages)
|
||||
/// </summary>
|
||||
public int ChannelCapacity { get; set; } = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// Strategy when channel buffer is full: "DropOldest", "DropNewest", or "Wait"
|
||||
/// </summary>
|
||||
public string ChannelFullMode { get; set; } = "DropOldest";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using System.IO;
|
||||
using Serilog;
|
||||
|
||||
namespace ZB.MOM.WW.LmxProxy.Host.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration for TLS/SSL settings for secure gRPC communication
|
||||
/// </summary>
|
||||
public class TlsConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets whether TLS is enabled for gRPC communication
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path to the server certificate file (.pem or .crt)
|
||||
/// </summary>
|
||||
public string ServerCertificatePath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path to the server private key file (.key)
|
||||
/// </summary>
|
||||
public string ServerKeyPath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path to the certificate authority file for client certificate validation (optional)
|
||||
/// </summary>
|
||||
public string? ClientCaCertificatePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to require client certificates for mutual TLS
|
||||
/// </summary>
|
||||
public bool RequireClientCertificate { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to check certificate revocation
|
||||
/// </summary>
|
||||
public bool CheckCertificateRevocation { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Validates the TLS configuration
|
||||
/// </summary>
|
||||
/// <returns>True if configuration is valid, false otherwise</returns>
|
||||
public bool Validate()
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
return true; // No validation needed if TLS is disabled
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(ServerCertificatePath))
|
||||
{
|
||||
Log.Error("TLS is enabled but ServerCertificatePath is not configured");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(ServerKeyPath))
|
||||
{
|
||||
Log.Error("TLS is enabled but ServerKeyPath is not configured");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!File.Exists(ServerCertificatePath))
|
||||
{
|
||||
Log.Warning("Server certificate file not found: {Path} - will be auto-generated on startup",
|
||||
ServerCertificatePath);
|
||||
}
|
||||
|
||||
if (!File.Exists(ServerKeyPath))
|
||||
{
|
||||
Log.Warning("Server key file not found: {Path} - will be auto-generated on startup", ServerKeyPath);
|
||||
}
|
||||
|
||||
if (RequireClientCertificate && string.IsNullOrWhiteSpace(ClientCaCertificatePath))
|
||||
{
|
||||
Log.Error("Client certificate is required but ClientCaCertificatePath is not configured");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(ClientCaCertificatePath) && !File.Exists(ClientCaCertificatePath))
|
||||
{
|
||||
Log.Warning("Client CA certificate file not found: {Path} - will be auto-generated on startup",
|
||||
ClientCaCertificatePath);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user