Phase 0 — mechanical rename ZB.MOM.WW.LmxOpcUa.* → ZB.MOM.WW.OtOpcUa.*
Renames all 11 projects (5 src + 6 tests), the .slnx solution file, all source-file namespaces, all axaml namespace references, and all v1 documentation references in CLAUDE.md and docs/*.md (excluding docs/v2/ which is already in OtOpcUa form). Also updates the TopShelf service registration name from "LmxOpcUa" to "OtOpcUa" per Phase 0 Task 0.6.
Preserves runtime identifiers per Phase 0 Out-of-Scope rules to avoid breaking v1/v2 client trust during coexistence: OPC UA `ApplicationUri` defaults (`urn:{GalaxyName}:LmxOpcUa`), server `EndpointPath` (`/LmxOpcUa`), `ServerName` default (feeds cert subject CN), `MxAccessConfiguration.ClientName` default (defensive — stays "LmxOpcUa" for MxAccess audit-trail consistency), client OPC UA identifiers (`ApplicationName = "LmxOpcUaClient"`, `ApplicationUri = "urn:localhost:LmxOpcUaClient"`, cert directory `%LocalAppData%\LmxOpcUaClient\pki\`), and the `LmxOpcUaServer` class name (class rename out of Phase 0 scope per Task 0.5 sed pattern; happens in Phase 1 alongside `LmxNodeManager → GenericDriverNodeManager` Core extraction). 23 LmxOpcUa references retained, all enumerated and justified in `docs/v2/implementation/exit-gate-phase-0.md`.
Build clean: 0 errors, 30 warnings (lower than baseline 167). Tests at strict improvement over baseline: 821 passing / 1 failing vs baseline 820 / 2 (one flaky pre-existing failure passed this run; the other still fails — both pre-existing and unrelated to the rename). `Client.UI.Tests`, `Historian.Aveva.Tests`, `Client.Shared.Tests`, `IntegrationTests` all match baseline exactly. Exit gate compliance results recorded in `docs/v2/implementation/exit-gate-phase-0.md` with all 7 checks PASS or DEFERRED-to-PR-review (#7 service install verification needs Windows service permissions on the reviewer's box).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
325
src/ZB.MOM.WW.OtOpcUa.Host/OpcUa/OpcUaServerHost.cs
Normal file
325
src/ZB.MOM.WW.OtOpcUa.Host/OpcUa/OpcUaServerHost.cs
Normal file
@@ -0,0 +1,325 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Configuration;
|
||||
using Serilog;
|
||||
using ZB.MOM.WW.OtOpcUa.Host.Configuration;
|
||||
using ZB.MOM.WW.OtOpcUa.Host.Domain;
|
||||
using ZB.MOM.WW.OtOpcUa.Host.Historian;
|
||||
using ZB.MOM.WW.OtOpcUa.Host.Metrics;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.OpcUa
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages the OPC UA ApplicationInstance lifecycle. Programmatic config, no XML. (OPC-001, OPC-012, OPC-013)
|
||||
/// </summary>
|
||||
public class OpcUaServerHost : IDisposable
|
||||
{
|
||||
private static readonly ILogger Log = Serilog.Log.ForContext<OpcUaServerHost>();
|
||||
private readonly AlarmObjectFilter? _alarmObjectFilter;
|
||||
private readonly AuthenticationConfiguration _authConfig;
|
||||
private readonly IUserAuthenticationProvider? _authProvider;
|
||||
|
||||
private readonly OpcUaConfiguration _config;
|
||||
private readonly IHistorianDataSource? _historianDataSource;
|
||||
private readonly PerformanceMetrics _metrics;
|
||||
private readonly IMxAccessClient _mxAccessClient;
|
||||
private readonly RedundancyConfiguration _redundancyConfig;
|
||||
private readonly SecurityProfileConfiguration _securityConfig;
|
||||
private ApplicationInstance? _application;
|
||||
private LmxOpcUaServer? _server;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new host for the Galaxy-backed OPC UA server instance.
|
||||
/// </summary>
|
||||
/// <param name="config">The endpoint and session settings for the OPC UA host.</param>
|
||||
/// <param name="mxAccessClient">The runtime client used by the node manager for live reads, writes, and subscriptions.</param>
|
||||
/// <param name="metrics">The metrics collector shared with the node manager and runtime bridge.</param>
|
||||
/// <param name="historianDataSource">The optional historian adapter that enables OPC UA history read support.</param>
|
||||
public OpcUaServerHost(OpcUaConfiguration config, IMxAccessClient mxAccessClient, PerformanceMetrics metrics,
|
||||
IHistorianDataSource? historianDataSource = null,
|
||||
AuthenticationConfiguration? authConfig = null,
|
||||
IUserAuthenticationProvider? authProvider = null,
|
||||
SecurityProfileConfiguration? securityConfig = null,
|
||||
RedundancyConfiguration? redundancyConfig = null,
|
||||
AlarmObjectFilter? alarmObjectFilter = null,
|
||||
MxAccessConfiguration? mxAccessConfig = null,
|
||||
HistorianConfiguration? historianConfig = null)
|
||||
{
|
||||
_config = config;
|
||||
_mxAccessClient = mxAccessClient;
|
||||
_metrics = metrics;
|
||||
_historianDataSource = historianDataSource;
|
||||
_authConfig = authConfig ?? new AuthenticationConfiguration();
|
||||
_authProvider = authProvider;
|
||||
_securityConfig = securityConfig ?? new SecurityProfileConfiguration();
|
||||
_redundancyConfig = redundancyConfig ?? new RedundancyConfiguration();
|
||||
_alarmObjectFilter = alarmObjectFilter;
|
||||
_mxAccessConfig = mxAccessConfig ?? new MxAccessConfiguration();
|
||||
_historianConfig = historianConfig ?? new HistorianConfiguration();
|
||||
}
|
||||
|
||||
private readonly MxAccessConfiguration _mxAccessConfig;
|
||||
private readonly HistorianConfiguration _historianConfig;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the active node manager that holds the published Galaxy namespace.
|
||||
/// </summary>
|
||||
public LmxNodeManager? NodeManager => _server?.NodeManager;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of currently connected OPC UA client sessions.
|
||||
/// </summary>
|
||||
public int ActiveSessionCount => _server?.ActiveSessionCount ?? 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the OPC UA server has been started and not yet stopped.
|
||||
/// </summary>
|
||||
public bool IsRunning => _server != null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of opc.tcp base addresses the server is currently listening on.
|
||||
/// Returns an empty list when the server has not started.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> BaseAddresses
|
||||
{
|
||||
get
|
||||
{
|
||||
var addrs = _application?.ApplicationConfiguration?.ServerConfiguration?.BaseAddresses;
|
||||
return addrs != null ? addrs.ToList() : Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of active security policies advertised to clients (SecurityMode + PolicyUri).
|
||||
/// Returns an empty list when the server has not started.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ServerSecurityPolicy> SecurityPolicies
|
||||
{
|
||||
get
|
||||
{
|
||||
var policies = _application?.ApplicationConfiguration?.ServerConfiguration?.SecurityPolicies;
|
||||
return policies != null ? policies.ToList() : Array.Empty<ServerSecurityPolicy>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of user token policy names advertised to clients (Anonymous, UserName, Certificate).
|
||||
/// Returns an empty list when the server has not started.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> UserTokenPolicies
|
||||
{
|
||||
get
|
||||
{
|
||||
var policies = _application?.ApplicationConfiguration?.ServerConfiguration?.UserTokenPolicies;
|
||||
return policies != null ? policies.Select(p => p.TokenType.ToString()).ToList() : Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the host and releases server resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the OPC UA ServiceLevel based on current runtime health.
|
||||
/// </summary>
|
||||
public void UpdateServiceLevel(bool mxAccessConnected, bool dbConnected)
|
||||
{
|
||||
_server?.UpdateServiceLevel(mxAccessConnected, dbConnected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the OPC UA application instance, prepares certificates, and binds the Galaxy namespace to the configured
|
||||
/// endpoint.
|
||||
/// </summary>
|
||||
public async Task StartAsync()
|
||||
{
|
||||
var namespaceUri = $"urn:{_config.GalaxyName}:LmxOpcUa";
|
||||
var applicationUri = _config.ApplicationUri ?? namespaceUri;
|
||||
|
||||
// Resolve configured security profiles
|
||||
var securityPolicies = SecurityProfileResolver.Resolve(_securityConfig.Profiles);
|
||||
foreach (var sp in securityPolicies)
|
||||
Log.Information("Security profile active: {PolicyUri} / {Mode}", sp.SecurityPolicyUri, sp.SecurityMode);
|
||||
|
||||
// Build PKI paths
|
||||
var pkiRoot = _securityConfig.PkiRootPath ?? Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"OPC Foundation", "pki");
|
||||
var certSubject = _securityConfig.CertificateSubject ?? $"CN={_config.ServerName}, O=ZB MOM, DC=localhost";
|
||||
|
||||
var serverConfig = new ServerConfiguration
|
||||
{
|
||||
BaseAddresses = { $"opc.tcp://{_config.BindAddress}:{_config.Port}{_config.EndpointPath}" },
|
||||
MaxSessionCount = _config.MaxSessions,
|
||||
MaxSessionTimeout = _config.SessionTimeoutMinutes * 60 * 1000, // ms
|
||||
MinSessionTimeout = 10000,
|
||||
UserTokenPolicies = BuildUserTokenPolicies()
|
||||
};
|
||||
foreach (var policy in securityPolicies)
|
||||
serverConfig.SecurityPolicies.Add(policy);
|
||||
|
||||
var secConfig = new SecurityConfiguration
|
||||
{
|
||||
ApplicationCertificate = new CertificateIdentifier
|
||||
{
|
||||
StoreType = CertificateStoreType.Directory,
|
||||
StorePath = Path.Combine(pkiRoot, "own"),
|
||||
SubjectName = certSubject
|
||||
},
|
||||
TrustedIssuerCertificates = new CertificateTrustList
|
||||
{
|
||||
StoreType = CertificateStoreType.Directory,
|
||||
StorePath = Path.Combine(pkiRoot, "issuer")
|
||||
},
|
||||
TrustedPeerCertificates = new CertificateTrustList
|
||||
{
|
||||
StoreType = CertificateStoreType.Directory,
|
||||
StorePath = Path.Combine(pkiRoot, "trusted")
|
||||
},
|
||||
RejectedCertificateStore = new CertificateTrustList
|
||||
{
|
||||
StoreType = CertificateStoreType.Directory,
|
||||
StorePath = Path.Combine(pkiRoot, "rejected")
|
||||
},
|
||||
AutoAcceptUntrustedCertificates = _securityConfig.AutoAcceptClientCertificates,
|
||||
RejectSHA1SignedCertificates = _securityConfig.RejectSHA1Certificates,
|
||||
MinimumCertificateKeySize = (ushort)_securityConfig.MinimumCertificateKeySize
|
||||
};
|
||||
|
||||
var appConfig = new ApplicationConfiguration
|
||||
{
|
||||
ApplicationName = _config.ServerName,
|
||||
ApplicationUri = applicationUri,
|
||||
ApplicationType = ApplicationType.Server,
|
||||
ProductUri = namespaceUri,
|
||||
ServerConfiguration = serverConfig,
|
||||
SecurityConfiguration = secConfig,
|
||||
|
||||
TransportQuotas = new TransportQuotas
|
||||
{
|
||||
OperationTimeout = 120000,
|
||||
MaxStringLength = 4 * 1024 * 1024,
|
||||
MaxByteStringLength = 4 * 1024 * 1024,
|
||||
MaxArrayLength = 65535,
|
||||
MaxMessageSize = 4 * 1024 * 1024,
|
||||
MaxBufferSize = 65535,
|
||||
ChannelLifetime = 600000,
|
||||
SecurityTokenLifetime = 3600000
|
||||
},
|
||||
|
||||
TraceConfiguration = new TraceConfiguration
|
||||
{
|
||||
OutputFilePath = null,
|
||||
TraceMasks = 0
|
||||
}
|
||||
};
|
||||
|
||||
await appConfig.Validate(ApplicationType.Server);
|
||||
|
||||
// Hook certificate validation logging
|
||||
appConfig.CertificateValidator.CertificateValidation += OnCertificateValidation;
|
||||
|
||||
_application = new ApplicationInstance
|
||||
{
|
||||
ApplicationName = _config.ServerName,
|
||||
ApplicationType = ApplicationType.Server,
|
||||
ApplicationConfiguration = appConfig
|
||||
};
|
||||
|
||||
// Check/create application certificate
|
||||
var minKeySize = (ushort)_securityConfig.MinimumCertificateKeySize;
|
||||
var certLifetimeMonths = (ushort)_securityConfig.CertificateLifetimeMonths;
|
||||
var certOk = await _application.CheckApplicationInstanceCertificate(false, minKeySize, certLifetimeMonths);
|
||||
if (!certOk)
|
||||
{
|
||||
Log.Warning("Application certificate check failed, attempting to create...");
|
||||
certOk = await _application.CheckApplicationInstanceCertificate(false, minKeySize, certLifetimeMonths);
|
||||
}
|
||||
|
||||
_server = new LmxOpcUaServer(_config.GalaxyName, _mxAccessClient, _metrics, _historianDataSource,
|
||||
_config.AlarmTrackingEnabled, _authConfig, _authProvider, _redundancyConfig, applicationUri,
|
||||
_alarmObjectFilter,
|
||||
_mxAccessConfig.RuntimeStatusProbesEnabled,
|
||||
_mxAccessConfig.RuntimeStatusUnknownTimeoutSeconds,
|
||||
_mxAccessConfig.RequestTimeoutSeconds,
|
||||
_historianConfig.RequestTimeoutSeconds);
|
||||
await _application.Start(_server);
|
||||
|
||||
Log.Information(
|
||||
"OPC UA server started on opc.tcp://{BindAddress}:{Port}{EndpointPath} (applicationUri={ApplicationUri}, namespace={Namespace})",
|
||||
_config.BindAddress, _config.Port, _config.EndpointPath, applicationUri, namespaceUri);
|
||||
}
|
||||
|
||||
private void OnCertificateValidation(CertificateValidator sender, CertificateValidationEventArgs e)
|
||||
{
|
||||
var cert = e.Certificate;
|
||||
var subject = cert?.Subject ?? "Unknown";
|
||||
var thumbprint = cert?.Thumbprint ?? "N/A";
|
||||
|
||||
if (_securityConfig.AutoAcceptClientCertificates)
|
||||
{
|
||||
e.Accept = true;
|
||||
Log.Warning(
|
||||
"Client certificate auto-accepted: Subject={Subject}, Thumbprint={Thumbprint}, ValidTo={ValidTo}",
|
||||
subject, thumbprint, cert?.NotAfter.ToString("yyyy-MM-dd"));
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning(
|
||||
"Client certificate validation: Error={Error}, Subject={Subject}, Thumbprint={Thumbprint}, Accepted={Accepted}",
|
||||
e.Error?.StatusCode, subject, thumbprint, e.Accept);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the OPC UA application instance and releases its in-memory server objects.
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
try
|
||||
{
|
||||
_server?.Stop();
|
||||
Log.Information("OPC UA server stopped");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error stopping OPC UA server");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_server = null;
|
||||
_application = null;
|
||||
}
|
||||
}
|
||||
|
||||
private UserTokenPolicyCollection BuildUserTokenPolicies()
|
||||
{
|
||||
var policies = new UserTokenPolicyCollection();
|
||||
if (_authConfig.AllowAnonymous)
|
||||
policies.Add(new UserTokenPolicy(UserTokenType.Anonymous));
|
||||
if (_authConfig.Ldap.Enabled || _authProvider != null)
|
||||
policies.Add(new UserTokenPolicy(UserTokenType.UserName));
|
||||
|
||||
// X.509 certificate authentication is always available when security is configured
|
||||
if (_securityConfig.Profiles.Any(p =>
|
||||
!p.Equals("None", StringComparison.OrdinalIgnoreCase)))
|
||||
policies.Add(new UserTokenPolicy(UserTokenType.Certificate));
|
||||
|
||||
if (policies.Count == 0)
|
||||
{
|
||||
Log.Warning("No authentication methods configured — adding Anonymous as fallback");
|
||||
policies.Add(new UserTokenPolicy(UserTokenType.Anonymous));
|
||||
}
|
||||
|
||||
return policies;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user