fix(dcl): verify-endpoint probe logs through a real logger so first-time OPC UA app-cert creation is visible (#121)
The site's DataConnectionManagerActor handed VerifyEndpointAsync a NullLogger. A secured probe is usually what first creates a node's application certificate, so the creation Warning carrying the thumbprint every server must trust was silently discarded (found on the rig live gate). The actor takes an optional probe logger; the Host supplies one. Test pins creation logged once (Warning, thumbprint), not on reuse.
This commit is contained in:
@@ -33,6 +33,7 @@ public class DataConnectionManagerActor : ReceiveActor
|
|||||||
// UA connections. Needed by the verify-endpoint probe (VerifyEndpointCommand), which
|
// UA connections. Needed by the verify-endpoint probe (VerifyEndpointCommand), which
|
||||||
// builds an ApplicationConfiguration directly rather than through a connection actor.
|
// builds an ApplicationConfiguration directly rather than through a connection actor.
|
||||||
private readonly OpcUaGlobalOptions _opcUaGlobalOptions;
|
private readonly OpcUaGlobalOptions _opcUaGlobalOptions;
|
||||||
|
private readonly Microsoft.Extensions.Logging.ILogger _probeLogger;
|
||||||
private readonly Dictionary<string, IActorRef> _connectionActors = new();
|
private readonly Dictionary<string, IActorRef> _connectionActors = new();
|
||||||
|
|
||||||
// Children this manager has STOPPED (RemoveConnectionCommand) but whose Terminated has not
|
// Children this manager has STOPPED (RemoveConnectionCommand) but whose Terminated has not
|
||||||
@@ -56,13 +57,20 @@ public class DataConnectionManagerActor : ReceiveActor
|
|||||||
/// verify-endpoint probe; null falls back to defaults (mirrors
|
/// verify-endpoint probe; null falls back to defaults (mirrors
|
||||||
/// <see cref="DataConnectionFactory"/>'s default-options constructor).
|
/// <see cref="DataConnectionFactory"/>'s default-options constructor).
|
||||||
/// </param>
|
/// </param>
|
||||||
|
/// <param name="probeLogger">
|
||||||
|
/// Logger for the verify-endpoint probe; null discards. Gitea #121: a secured probe is
|
||||||
|
/// usually what first creates the node's OPC UA application certificate, and that
|
||||||
|
/// creation's Warning (with the thumbprint every server must trust) must reach the node log.
|
||||||
|
/// </param>
|
||||||
public DataConnectionManagerActor(
|
public DataConnectionManagerActor(
|
||||||
IDataConnectionFactory factory,
|
IDataConnectionFactory factory,
|
||||||
DataConnectionOptions options,
|
DataConnectionOptions options,
|
||||||
ISiteHealthCollector healthCollector,
|
ISiteHealthCollector healthCollector,
|
||||||
ISiteEventLogger? siteEventLogger = null,
|
ISiteEventLogger? siteEventLogger = null,
|
||||||
OpcUaGlobalOptions? opcUaGlobalOptions = null)
|
OpcUaGlobalOptions? opcUaGlobalOptions = null,
|
||||||
|
Microsoft.Extensions.Logging.ILogger? probeLogger = null)
|
||||||
{
|
{
|
||||||
|
_probeLogger = probeLogger ?? NullLogger.Instance;
|
||||||
_factory = factory;
|
_factory = factory;
|
||||||
_options = options;
|
_options = options;
|
||||||
_healthCollector = healthCollector;
|
_healthCollector = healthCollector;
|
||||||
@@ -403,9 +411,8 @@ public class DataConnectionManagerActor : ReceiveActor
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var probeLogger = NullLogger.Instance;
|
|
||||||
RealOpcUaClient
|
RealOpcUaClient
|
||||||
.VerifyEndpointAsync(config, _opcUaGlobalOptions, probeLogger, TimeSpan.FromSeconds(6), CancellationToken.None)
|
.VerifyEndpointAsync(config, _opcUaGlobalOptions, _probeLogger, TimeSpan.FromSeconds(6), CancellationToken.None)
|
||||||
.PipeTo(Sender);
|
.PipeTo(Sender);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -872,7 +872,9 @@ akka {{
|
|||||||
?? new ZB.MOM.WW.ScadaBridge.DataConnectionLayer.OpcUaGlobalOptions();
|
?? new ZB.MOM.WW.ScadaBridge.DataConnectionLayer.OpcUaGlobalOptions();
|
||||||
dclManager = _actorSystem!.ActorOf(
|
dclManager = _actorSystem!.ActorOf(
|
||||||
Props.Create(() => new ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Actors.DataConnectionManagerActor(
|
Props.Create(() => new ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Actors.DataConnectionManagerActor(
|
||||||
dclFactory, dclOptions, healthCollector, siteEventLogger, opcUaGlobalOptions)),
|
dclFactory, dclOptions, healthCollector, siteEventLogger, opcUaGlobalOptions,
|
||||||
|
_serviceProvider.GetRequiredService<ILoggerFactory>()
|
||||||
|
.CreateLogger("ZB.MOM.WW.ScadaBridge.DataConnectionLayer.VerifyEndpoint"))),
|
||||||
"dcl-manager");
|
"dcl-manager");
|
||||||
_logger.LogInformation("Data Connection Layer manager actor created");
|
_logger.LogInformation("Data Connection Layer manager actor created");
|
||||||
}
|
}
|
||||||
|
|||||||
+37
@@ -1,4 +1,5 @@
|
|||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.DataConnections;
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.DataConnections;
|
||||||
@@ -179,6 +180,42 @@ public sealed class RealOpcUaSecureConnectTests : IDisposable
|
|||||||
Assert.Contains("80130000", result.Error); // BadSecurityChecksFailed, not BadConfigurationError
|
Assert.Contains("80130000", result.Error); // BadSecurityChecksFailed, not BadConfigurationError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The probe is usually what first creates a node's identity, so creation must reach the
|
||||||
|
/// supplied logger as a Warning carrying the thumbprint servers must trust — once, not on reuse.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task Verify_FirstSecuredProbe_LogsCertificateCreationOnce()
|
||||||
|
{
|
||||||
|
await using var server = await SecureOpcUaTestServer.StartAsync();
|
||||||
|
var log = new CapturingLogger();
|
||||||
|
|
||||||
|
for (var i = 0; i < 2; i++)
|
||||||
|
await RealOpcUaClient.VerifyEndpointAsync(
|
||||||
|
ProbeConfig(server.Url, OpcUaSecurityMode.SignAndEncrypt), ClientOptions(), log,
|
||||||
|
TimeSpan.FromSeconds(30), CancellationToken.None);
|
||||||
|
|
||||||
|
var der = Assert.Single(Directory.GetFiles(OwnCertsDir, "*.der"));
|
||||||
|
using var cert = X509CertificateLoader.LoadCertificate(await File.ReadAllBytesAsync(der));
|
||||||
|
var created = Assert.Single(log.Entries, e => e.Level == LogLevel.Warning && e.Message.Contains("CREATED"));
|
||||||
|
Assert.Contains(cert.Thumbprint, created.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class CapturingLogger : ILogger
|
||||||
|
{
|
||||||
|
public List<(LogLevel Level, string Message)> Entries { get; } = new();
|
||||||
|
|
||||||
|
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
|
||||||
|
|
||||||
|
public bool IsEnabled(LogLevel logLevel) => true;
|
||||||
|
|
||||||
|
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
|
||||||
|
Func<TState, Exception?, string> formatter)
|
||||||
|
{
|
||||||
|
lock (Entries) Entries.Add((logLevel, formatter(state, exception)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>None is unchanged: it connects without reading or creating the own store.</summary>
|
/// <summary>None is unchanged: it connects without reading or creating the own store.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Connect_None_DoesNotTouchTheOwnStore()
|
public async Task Connect_None_DoesNotTouchTheOwnStore()
|
||||||
|
|||||||
Reference in New Issue
Block a user