Separates ApplicationUri from namespace identity so each instance in a redundant pair has a unique server URI while sharing the same Galaxy namespace. Exposes RedundancySupport, ServerUriArray, and dynamic ServiceLevel through the standard OPC UA server object. ServiceLevel is computed from role (Primary/Secondary) and runtime health (MXAccess and DB connectivity). Adds CLI redundancy command, second deployed service instance, and 31 new tests including paired-server integration. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
292 lines
13 KiB
Markdown
292 lines
13 KiB
Markdown
# Configuration
|
|
|
|
## Overview
|
|
|
|
The service loads configuration from `appsettings.json` at startup using the Microsoft.Extensions.Configuration stack. `AppConfiguration` is the root holder class that aggregates typed sections: `OpcUa`, `MxAccess`, `GalaxyRepository`, `Dashboard`, `Historian`, `Authentication`, and `Security`. Each section binds to a dedicated POCO class with sensible defaults, so the service runs with zero configuration on a standard deployment.
|
|
|
|
## Config Binding Pattern
|
|
|
|
The production constructor in `OpcUaService` builds the configuration pipeline and binds each JSON section to its typed class:
|
|
|
|
```csharp
|
|
var configuration = new ConfigurationBuilder()
|
|
.AddJsonFile("appsettings.json", optional: false)
|
|
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production"}.json", optional: true)
|
|
.AddEnvironmentVariables()
|
|
.Build();
|
|
|
|
_config = new AppConfiguration();
|
|
configuration.GetSection("OpcUa").Bind(_config.OpcUa);
|
|
configuration.GetSection("MxAccess").Bind(_config.MxAccess);
|
|
configuration.GetSection("GalaxyRepository").Bind(_config.GalaxyRepository);
|
|
configuration.GetSection("Dashboard").Bind(_config.Dashboard);
|
|
configuration.GetSection("Historian").Bind(_config.Historian);
|
|
configuration.GetSection("Authentication").Bind(_config.Authentication);
|
|
configuration.GetSection("Security").Bind(_config.Security);
|
|
```
|
|
|
|
This pattern uses `IConfiguration.GetSection().Bind()` rather than `IOptions<T>` because the service targets .NET Framework 4.8, where the full dependency injection container is not used.
|
|
|
|
## Environment-Specific Overrides
|
|
|
|
The configuration pipeline supports three layers of override, applied in order:
|
|
|
|
1. `appsettings.json` -- base configuration (required)
|
|
2. `appsettings.{DOTNET_ENVIRONMENT}.json` -- environment-specific overlay (optional)
|
|
3. Environment variables -- highest priority, useful for deployment automation
|
|
|
|
Set the `DOTNET_ENVIRONMENT` variable to load a named overlay file. For example, setting `DOTNET_ENVIRONMENT=Staging` loads `appsettings.Staging.json` if it exists.
|
|
|
|
Environment variables follow the standard `Section__Property` naming convention. For example, `OpcUa__Port=5840` overrides the OPC UA port.
|
|
|
|
## Configuration Sections
|
|
|
|
### OpcUa
|
|
|
|
Controls the OPC UA server endpoint and session limits. Defined in `OpcUaConfiguration`.
|
|
|
|
| Property | Type | Default | Description |
|
|
|----------|------|---------|-------------|
|
|
| `BindAddress` | `string` | `"0.0.0.0"` | IP address or hostname the server binds to. Use `0.0.0.0` for all interfaces, `localhost` for local-only, or a specific IP |
|
|
| `Port` | `int` | `4840` | TCP port the OPC UA server listens on |
|
|
| `EndpointPath` | `string` | `"/LmxOpcUa"` | Path appended to the host URI |
|
|
| `ServerName` | `string` | `"LmxOpcUa"` | Server name presented to OPC UA clients |
|
|
| `GalaxyName` | `string` | `"ZB"` | Galaxy name used as the OPC UA namespace |
|
|
| `MaxSessions` | `int` | `100` | Maximum simultaneous OPC UA sessions |
|
|
| `SessionTimeoutMinutes` | `int` | `30` | Idle session timeout in minutes |
|
|
| `AlarmTrackingEnabled` | `bool` | `false` | Enables `AlarmConditionState` nodes for alarm attributes |
|
|
| `ApplicationUri` | `string?` | `null` | Explicit application URI for this server instance. Required when redundancy is enabled. Defaults to `urn:{GalaxyName}:LmxOpcUa` when null |
|
|
|
|
### MxAccess
|
|
|
|
Controls the MXAccess runtime connection used for live tag reads and writes. Defined in `MxAccessConfiguration`.
|
|
|
|
| Property | Type | Default | Description |
|
|
|----------|------|---------|-------------|
|
|
| `ClientName` | `string` | `"LmxOpcUa"` | Client name registered with MXAccess |
|
|
| `NodeName` | `string?` | `null` | Optional Galaxy node name to target |
|
|
| `GalaxyName` | `string?` | `null` | Optional Galaxy name for MXAccess reference resolution |
|
|
| `ReadTimeoutSeconds` | `int` | `5` | Maximum wait for a live tag read |
|
|
| `WriteTimeoutSeconds` | `int` | `5` | Maximum wait for a write acknowledgment |
|
|
| `MaxConcurrentOperations` | `int` | `10` | Cap on concurrent MXAccess operations |
|
|
| `MonitorIntervalSeconds` | `int` | `5` | Connectivity monitor probe interval |
|
|
| `AutoReconnect` | `bool` | `true` | Automatically re-establish dropped MXAccess sessions |
|
|
| `ProbeTag` | `string?` | `null` | Optional tag used to verify the runtime returns fresh data |
|
|
| `ProbeStaleThresholdSeconds` | `int` | `60` | Seconds a probe value may remain unchanged before the connection is considered stale |
|
|
|
|
### GalaxyRepository
|
|
|
|
Controls the Galaxy repository database connection used to build the OPC UA address space. Defined in `GalaxyRepositoryConfiguration`.
|
|
|
|
| Property | Type | Default | Description |
|
|
|----------|------|---------|-------------|
|
|
| `ConnectionString` | `string` | `"Server=localhost;Database=ZB;Integrated Security=true;"` | SQL Server connection string for the Galaxy database |
|
|
| `ChangeDetectionIntervalSeconds` | `int` | `30` | How often the service polls for Galaxy deploy changes |
|
|
| `CommandTimeoutSeconds` | `int` | `30` | SQL command timeout for repository queries |
|
|
| `ExtendedAttributes` | `bool` | `false` | Load extended Galaxy attribute metadata into the OPC UA model |
|
|
|
|
### Dashboard
|
|
|
|
Controls the embedded HTTP status dashboard. Defined in `DashboardConfiguration`.
|
|
|
|
| Property | Type | Default | Description |
|
|
|----------|------|---------|-------------|
|
|
| `Enabled` | `bool` | `true` | Whether the status dashboard is hosted |
|
|
| `Port` | `int` | `8081` | HTTP port for the dashboard endpoint |
|
|
| `RefreshIntervalSeconds` | `int` | `10` | HTML auto-refresh interval in seconds |
|
|
|
|
### Historian
|
|
|
|
Controls the Wonderware Historian connection for OPC UA historical data access. Defined in `HistorianConfiguration`.
|
|
|
|
| Property | Type | Default | Description |
|
|
|----------|------|---------|-------------|
|
|
| `Enabled` | `bool` | `false` | Enables OPC UA historical data access |
|
|
| `ConnectionString` | `string` | `"Server=localhost;Database=Runtime;Integrated Security=true;"` | Connection string for the Historian Runtime database |
|
|
| `CommandTimeoutSeconds` | `int` | `30` | SQL command timeout for historian queries |
|
|
| `MaxValuesPerRead` | `int` | `10000` | Maximum values returned per `HistoryRead` request |
|
|
|
|
### Authentication
|
|
|
|
Controls user authentication and write authorization for the OPC UA server. Defined in `AuthenticationConfiguration`.
|
|
|
|
| Property | Type | Default | Description |
|
|
|----------|------|---------|-------------|
|
|
| `AllowAnonymous` | `bool` | `true` | Accepts anonymous client connections when `true` |
|
|
| `AnonymousCanWrite` | `bool` | `true` | Permits anonymous users to write when `true` |
|
|
| `Users` | `List<UserCredential>` | `[]` | List of username/password credentials for `UserName` token authentication |
|
|
|
|
Each entry in the `Users` list has two properties: `Username` (string) and `Password` (string).
|
|
|
|
The defaults preserve the existing behavior: anonymous clients can connect, read, and write with no credentials required. To restrict writes to authenticated users, set `AnonymousCanWrite` to `false` and add entries to the `Users` list.
|
|
|
|
Example configuration:
|
|
|
|
```json
|
|
"Authentication": {
|
|
"AllowAnonymous": true,
|
|
"AnonymousCanWrite": false,
|
|
"Users": [
|
|
{ "Username": "operator", "Password": "op123" },
|
|
{ "Username": "engineer", "Password": "eng456" }
|
|
]
|
|
}
|
|
```
|
|
|
|
### Security
|
|
|
|
Controls OPC UA transport security profiles and certificate handling. Defined in `SecurityProfileConfiguration`. See [Security Guide](security.md) for detailed usage.
|
|
|
|
| Property | Type | Default | Description |
|
|
|----------|------|---------|-------------|
|
|
| `Profiles` | `List<string>` | `["None"]` | Security profiles to expose. Valid: `None`, `Basic256Sha256-Sign`, `Basic256Sha256-SignAndEncrypt` |
|
|
| `AutoAcceptClientCertificates` | `bool` | `true` | Auto-accept untrusted client certificates. Set to `false` in production |
|
|
| `RejectSHA1Certificates` | `bool` | `true` | Reject client certificates signed with SHA-1 |
|
|
| `MinimumCertificateKeySize` | `int` | `2048` | Minimum RSA key size for client certificates |
|
|
| `PkiRootPath` | `string?` | `null` | Override for PKI root directory. Defaults to `%LOCALAPPDATA%\OPC Foundation\pki` |
|
|
| `CertificateSubject` | `string?` | `null` | Override for server certificate subject. Defaults to `CN={ServerName}, O=ZB MOM, DC=localhost` |
|
|
|
|
Example — production deployment with encrypted transport:
|
|
|
|
```json
|
|
"Security": {
|
|
"Profiles": ["Basic256Sha256-SignAndEncrypt"],
|
|
"AutoAcceptClientCertificates": false,
|
|
"RejectSHA1Certificates": true,
|
|
"MinimumCertificateKeySize": 2048
|
|
}
|
|
```
|
|
|
|
### Redundancy
|
|
|
|
Controls non-transparent OPC UA redundancy. Defined in `RedundancyConfiguration`. See [Redundancy Guide](Redundancy.md) for detailed usage.
|
|
|
|
| Property | Type | Default | Description |
|
|
|----------|------|---------|-------------|
|
|
| `Enabled` | `bool` | `false` | Enables redundancy mode and ServiceLevel computation |
|
|
| `Mode` | `string` | `"Warm"` | Redundancy mode: `Warm` or `Hot` |
|
|
| `Role` | `string` | `"Primary"` | Instance role: `Primary` (higher ServiceLevel) or `Secondary` |
|
|
| `ServerUris` | `List<string>` | `[]` | ApplicationUri values for all servers in the redundant set |
|
|
| `ServiceLevelBase` | `int` | `200` | Base ServiceLevel when healthy (1-255). Secondary receives base - 50 |
|
|
|
|
Example — two-instance redundant pair (Primary):
|
|
|
|
```json
|
|
"Redundancy": {
|
|
"Enabled": true,
|
|
"Mode": "Warm",
|
|
"Role": "Primary",
|
|
"ServerUris": ["urn:localhost:LmxOpcUa:instance1", "urn:localhost:LmxOpcUa:instance2"],
|
|
"ServiceLevelBase": 200
|
|
}
|
|
```
|
|
|
|
## Feature Flags
|
|
|
|
Three boolean properties act as feature flags that control optional subsystems:
|
|
|
|
- **`OpcUa.AlarmTrackingEnabled`** -- When `true`, the node manager creates `AlarmConditionState` nodes for alarm attributes and monitors `InAlarm` transitions. Disabled by default because alarm tracking adds per-attribute overhead.
|
|
- **`Historian.Enabled`** -- When `true`, the service creates a `HistorianDataSource` connected to the Wonderware Historian Runtime database and registers it with the OPC UA server host. Disabled by default because not all deployments have a Historian instance.
|
|
- **`GalaxyRepository.ExtendedAttributes`** -- When `true`, the repository loads additional Galaxy attribute metadata beyond the core set needed for the address space. Disabled by default to minimize startup query time.
|
|
|
|
## Configuration Validation
|
|
|
|
`ConfigurationValidator.ValidateAndLog()` runs at the start of `OpcUaService.Start()`. It logs every resolved configuration value at `Information` level and validates required constraints:
|
|
|
|
- `OpcUa.Port` must be between 1 and 65535
|
|
- `OpcUa.GalaxyName` must not be empty
|
|
- `MxAccess.ClientName` must not be empty
|
|
- `GalaxyRepository.ConnectionString` must not be empty
|
|
- `Security.MinimumCertificateKeySize` must be at least 2048
|
|
- Unknown security profile names are logged as warnings
|
|
- `AutoAcceptClientCertificates = true` emits a warning
|
|
- Only-`None` profile configuration emits a warning
|
|
- `OpcUa.ApplicationUri` must be set when `Redundancy.Enabled = true`
|
|
- `Redundancy.ServiceLevelBase` must be between 1 and 255
|
|
- `Redundancy.ServerUris` should contain at least 2 entries when enabled
|
|
- Local `ApplicationUri` should appear in `Redundancy.ServerUris`
|
|
|
|
If validation fails, the service throws `InvalidOperationException` and does not start.
|
|
|
|
## Test Constructor Pattern
|
|
|
|
`OpcUaService` provides an `internal` constructor that accepts pre-built dependencies instead of loading `appsettings.json`:
|
|
|
|
```csharp
|
|
internal OpcUaService(
|
|
AppConfiguration config,
|
|
IMxProxy? mxProxy,
|
|
IGalaxyRepository? galaxyRepository,
|
|
IMxAccessClient? mxAccessClientOverride = null,
|
|
bool hasMxAccessClientOverride = false)
|
|
```
|
|
|
|
Integration tests use this constructor to inject substitute implementations of `IMxProxy`, `IGalaxyRepository`, and `IMxAccessClient`, bypassing the STA thread, COM interop, and SQL Server dependencies. The `hasMxAccessClientOverride` flag tells the service to use the injected `IMxAccessClient` directly instead of creating one from the `IMxProxy` on the STA thread.
|
|
|
|
## Example appsettings.json
|
|
|
|
```json
|
|
{
|
|
"OpcUa": {
|
|
"BindAddress": "0.0.0.0",
|
|
"Port": 4840,
|
|
"EndpointPath": "/LmxOpcUa",
|
|
"ServerName": "LmxOpcUa",
|
|
"GalaxyName": "ZB",
|
|
"MaxSessions": 100,
|
|
"SessionTimeoutMinutes": 30,
|
|
"AlarmTrackingEnabled": false,
|
|
"ApplicationUri": null
|
|
},
|
|
"MxAccess": {
|
|
"ClientName": "LmxOpcUa",
|
|
"NodeName": null,
|
|
"GalaxyName": null,
|
|
"ReadTimeoutSeconds": 5,
|
|
"WriteTimeoutSeconds": 5,
|
|
"MaxConcurrentOperations": 10,
|
|
"MonitorIntervalSeconds": 5,
|
|
"AutoReconnect": true,
|
|
"ProbeTag": null,
|
|
"ProbeStaleThresholdSeconds": 60
|
|
},
|
|
"GalaxyRepository": {
|
|
"ConnectionString": "Server=localhost;Database=ZB;Integrated Security=true;",
|
|
"ChangeDetectionIntervalSeconds": 30,
|
|
"CommandTimeoutSeconds": 30,
|
|
"ExtendedAttributes": false
|
|
},
|
|
"Dashboard": {
|
|
"Enabled": true,
|
|
"Port": 8081,
|
|
"RefreshIntervalSeconds": 10
|
|
},
|
|
"Historian": {
|
|
"Enabled": false,
|
|
"ConnectionString": "Server=localhost;Database=Runtime;Integrated Security=true;",
|
|
"CommandTimeoutSeconds": 30,
|
|
"MaxValuesPerRead": 10000
|
|
},
|
|
"Authentication": {
|
|
"AllowAnonymous": true,
|
|
"AnonymousCanWrite": true,
|
|
"Users": []
|
|
},
|
|
"Security": {
|
|
"Profiles": ["None"],
|
|
"AutoAcceptClientCertificates": true,
|
|
"RejectSHA1Certificates": true,
|
|
"MinimumCertificateKeySize": 2048,
|
|
"PkiRootPath": null,
|
|
"CertificateSubject": null
|
|
},
|
|
"Redundancy": {
|
|
"Enabled": false,
|
|
"Mode": "Warm",
|
|
"Role": "Primary",
|
|
"ServerUris": [],
|
|
"ServiceLevelBase": 200
|
|
}
|
|
}
|
|
```
|