feat(abcip): final review I1 — node-level Drivers:AbCip:DefaultTransport
The old AdminUI cannot author the per-driver transport key (#576), so the 'pin before upgrading' recipe had no working path. A node-level appsetting (env Drivers__AbCip__DefaultTransport) now supplies the default for AbCip drivers with no key: explicit key > node default > compiled Native. Bound and validated unconditionally (bad value fails host start); the driver factory and the Test Connect probe both read it so they agree. AbCip.md upgrade recipe rewritten around the key; Configuration.md documents it. Also documents first-wins for AllowPacking/probe and host-alias throttles (final review M6) and moves the status-bit note out of AbCip.md (M5). Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+2
-2
@@ -35,11 +35,11 @@
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" for="abcipTransport">Transport</label>
|
||||
<InputSelect @bind-Value="_form.Transport" @bind-Value:after="EmitAsync" class="form-select form-select-sm" id="abcipTransport">
|
||||
<option value="">Default (Native)</option>
|
||||
<option value="">Default (node setting; Native unless pinned)</option>
|
||||
<option value="@AbCipTransport.Native">Native (managed)</option>
|
||||
<option value="@AbCipTransport.Libplctag">libplctag (fallback)</option>
|
||||
</InputSelect>
|
||||
<div class="form-text">Which protocol stack talks to the controller. Default omits the setting so the driver picks; takes effect on the next deploy.</div>
|
||||
<div class="form-text">Which protocol stack talks to the controller. Default omits the setting so the node-level <code>Drivers:AbCip:DefaultTransport</code> picks (Native unless an operator pinned it); takes effect on the next deploy.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
using ZB.MOM.WW.Configuration;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Node-level defaults for the AbCip driver (final review I1), bound from <c>Drivers:AbCip</c>. Lets an
|
||||
/// operator pin the transport an AbCip driver with NO <c>transport</c> key runs on — at upgrade time,
|
||||
/// without editing (or being able to edit, #576) every driver config. An explicit per-driver
|
||||
/// <c>transport</c> key still wins. Overridable by environment variable
|
||||
/// <c>Drivers__AbCip__DefaultTransport</c>.
|
||||
/// </summary>
|
||||
public sealed class AbCipDriverDefaultsOptions
|
||||
{
|
||||
/// <summary>The configuration section path.</summary>
|
||||
public const string SectionName = "Drivers:AbCip";
|
||||
|
||||
/// <summary>
|
||||
/// <c>Native</c> (the default) or <c>Libplctag</c>, case-insensitive. Blank/absent = <c>Native</c>.
|
||||
/// Any other value fails host start (<see cref="AbCipDriverDefaultsOptionsValidator"/>).
|
||||
/// </summary>
|
||||
public string? DefaultTransport { get; set; } = nameof(AbCipTransport.Native);
|
||||
|
||||
/// <summary>
|
||||
/// The parsed default: blank resolves to the compiled <see cref="AbCipTransportResolver.Default"/>.
|
||||
/// Throws on an unparseable value — unreachable at runtime because the validator fails host start first.
|
||||
/// </summary>
|
||||
/// <returns>The node-level default transport.</returns>
|
||||
public AbCipTransport ResolveDefaultTransport()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(DefaultTransport)) return AbCipTransportResolver.Default;
|
||||
return AbCipTransportResolver.TryParse(DefaultTransport, out var t)
|
||||
? t
|
||||
: throw new InvalidOperationException(AbCipDriverDefaultsOptionsValidator.Message(DefaultTransport));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Fail-fast startup validator for <see cref="AbCipDriverDefaultsOptions"/>.</summary>
|
||||
public sealed class AbCipDriverDefaultsOptionsValidator : OptionsValidatorBase<AbCipDriverDefaultsOptions>
|
||||
{
|
||||
/// <summary>The operator-facing failure text for a bad value.</summary>
|
||||
/// <param name="value">The rejected value.</param>
|
||||
/// <returns>The message.</returns>
|
||||
internal static string Message(string? value) =>
|
||||
$"Drivers:AbCip:DefaultTransport is '{value}' — expected one of " +
|
||||
$"{string.Join(", ", Enum.GetNames<AbCipTransport>())} (case-insensitive). It selects the transport an " +
|
||||
"AbCip driver with no \"transport\" key runs on; set it via appsettings or the environment variable " +
|
||||
"Drivers__AbCip__DefaultTransport.";
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Validate(ValidationBuilder builder, AbCipDriverDefaultsOptions options)
|
||||
{
|
||||
builder.RequireThat(
|
||||
string.IsNullOrWhiteSpace(options.DefaultTransport)
|
||||
|| AbCipTransportResolver.TryParse(options.DefaultTransport, out _),
|
||||
Message(options.DefaultTransport));
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Resilience;
|
||||
using ZB.MOM.WW.Secrets.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Host.Configuration;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.Redundancy;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Drivers;
|
||||
@@ -78,7 +80,8 @@ public static class DriverFactoryBootstrap
|
||||
// whole mechanism exists to prevent, and it would fail as a plant outage rather than as a
|
||||
// startup error.
|
||||
var primaryRoleSource = sp.GetRequiredService<IDriverPrimaryRoleSource>();
|
||||
Register(registry, loggerFactory, secretResolver, primaryRoleSource, scriptRoot);
|
||||
Register(registry, loggerFactory, secretResolver, primaryRoleSource, ResolveAbCipDefaultTransport(sp),
|
||||
scriptRoot);
|
||||
return registry;
|
||||
});
|
||||
services.AddSingleton<IDriverFactory>(sp =>
|
||||
@@ -140,7 +143,12 @@ public static class DriverFactoryBootstrap
|
||||
public static IServiceCollection AddOtOpcUaDriverProbes(this IServiceCollection services)
|
||||
{
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, ModbusProbe>());
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, AbCipProbe>());
|
||||
// Factory-built so the probe resolves an absent "transport" key to the SAME node default the driver
|
||||
// factory uses (Drivers:AbCip:DefaultTransport, final review I1) — Test Connect probes the stack the
|
||||
// driver would run. The generic factory overload keeps AbCipProbe as the implementation type, which
|
||||
// TryAddEnumerable de-dups on.
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, AbCipProbe>(
|
||||
sp => new AbCipProbe(tagFactory: null, defaultTransport: ResolveAbCipDefaultTransport(sp))));
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, AbLegacyProbe>());
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, S7Probe>());
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, TwinCATProbe>());
|
||||
@@ -155,6 +163,16 @@ public static class DriverFactoryBootstrap
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The node-level AbCip default transport (<c>Drivers:AbCip:DefaultTransport</c>, bound + validated in
|
||||
/// Program.cs). A container without the options bound (test graphs) gets the compiled default.
|
||||
/// </summary>
|
||||
/// <param name="sp">The service provider.</param>
|
||||
/// <returns>The transport an AbCip driver with no <c>transport</c> key runs on.</returns>
|
||||
internal static Driver.AbCip.AbCipTransport ResolveAbCipDefaultTransport(IServiceProvider sp) =>
|
||||
sp.GetService<IOptions<AbCipDriverDefaultsOptions>>()?.Value.ResolveDefaultTransport()
|
||||
?? Driver.AbCip.AbCipTransportResolver.Default;
|
||||
|
||||
/// <summary>
|
||||
/// Invoke every cross-platform driver's <c>Register</c> extension. New driver assemblies
|
||||
/// get added here — one line per type. ShouldStub() in <c>DriverInstanceActor</c> still
|
||||
@@ -166,9 +184,10 @@ public static class DriverFactoryBootstrap
|
||||
ILoggerFactory? loggerFactory,
|
||||
ISecretResolver secretResolver,
|
||||
IDriverPrimaryRoleSource primaryRoleSource,
|
||||
Driver.AbCip.AbCipTransport abCipDefaultTransport,
|
||||
ZB.MOM.WW.OtOpcUa.Core.Scripting.ScriptRootLogger? scriptRoot = null)
|
||||
{
|
||||
Driver.AbCip.AbCipDriverFactoryExtensions.Register(registry, loggerFactory);
|
||||
Driver.AbCip.AbCipDriverFactoryExtensions.Register(registry, loggerFactory, abCipDefaultTransport);
|
||||
Driver.AbLegacy.AbLegacyDriverFactoryExtensions.Register(registry, loggerFactory);
|
||||
Driver.Calculation.CalculationDriverFactoryExtensions.Register(registry, loggerFactory, scriptRoot);
|
||||
Driver.FOCAS.FocasDriverFactoryExtensions.Register(registry);
|
||||
|
||||
@@ -175,6 +175,13 @@ builder.Services.AddOtOpcUaCluster(builder.Configuration);
|
||||
// the startup check (AddOtOpcUaAuth binds LdapOptions but does not attach a validator).
|
||||
builder.Services.AddValidatedOptions<LdapOptions, LdapOptionsValidator>(builder.Configuration, LdapOptions.SectionName);
|
||||
|
||||
// Node-level AbCip default transport (final review I1): the transport an AbCip driver with NO "transport"
|
||||
// key runs on. Unconditional because BOTH consumers need it — the driver factory (driver role) and the
|
||||
// Test Connect probe (admin role, which must probe the stack the driver would run). A bad value fails host
|
||||
// start here rather than silently falling back.
|
||||
builder.Services.AddValidatedOptions<AbCipDriverDefaultsOptions, AbCipDriverDefaultsOptionsValidator>(
|
||||
builder.Configuration, AbCipDriverDefaultsOptions.SectionName);
|
||||
|
||||
if (hasDriver)
|
||||
{
|
||||
builder.Services.AddOtOpcUaRuntime();
|
||||
|
||||
Reference in New Issue
Block a user