diff --git a/Directory.Packages.props b/Directory.Packages.props index 8687080..35ccaa6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -54,18 +54,18 @@ - - - - - - + + + + + + - + - - + + diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ConfigEdit.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ConfigEdit.cs new file mode 100644 index 0000000..237fcb1 --- /dev/null +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ConfigEdit.cs @@ -0,0 +1,27 @@ +namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities; + +/// +/// Append-only audit row written by AdminOperationsActor on every mutating live-edit +/// operation. The ExecutionId optionally correlates a sequence of edits that ran inside one +/// admin transaction (e.g. an import batch that updates many Equipment rows). +/// +public sealed class ConfigEdit +{ + public Guid EditId { get; init; } = Guid.NewGuid(); + + public required string EntityType { get; init; } + + public Guid EntityId { get; init; } + + /// JSON payload of the column-name → new-value pairs touched by this edit. + public required string FieldsJson { get; init; } + + /// Optional correlation across edits inside a single admin operation. + public Guid? ExecutionId { get; init; } + + public required string EditedBy { get; init; } + + public DateTime EditedAtUtc { get; init; } = DateTime.UtcNow; + + public required string SourceNode { get; init; } +} diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Deployment.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Deployment.cs new file mode 100644 index 0000000..6e618a5 --- /dev/null +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Deployment.cs @@ -0,0 +1,30 @@ +using ZB.MOM.WW.OtOpcUa.Configuration.Enums; + +namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities; + +/// +/// Immutable snapshot of a config artifact dispatched to every driver-role node by the +/// ConfigPublishCoordinator. Replaces the v1 draft/publish +/// row; the ArtifactBlob carries the SnapshotAndFlatten() output produced by +/// AdminOperationsActor. +/// +public sealed class Deployment +{ + public Guid DeploymentId { get; init; } = Guid.NewGuid(); + + public required string RevisionHash { get; init; } + + public DeploymentStatus Status { get; set; } = DeploymentStatus.Dispatching; + + public required string CreatedBy { get; init; } + + public DateTime CreatedAtUtc { get; init; } = DateTime.UtcNow; + + public byte[] ArtifactBlob { get; init; } = Array.Empty(); + + public byte[] RowVersion { get; set; } = Array.Empty(); + + public string? FailureReason { get; set; } + + public DateTime? SealedAtUtc { get; set; } +} diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/NodeDeploymentState.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/NodeDeploymentState.cs new file mode 100644 index 0000000..df20587 --- /dev/null +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/NodeDeploymentState.cs @@ -0,0 +1,29 @@ +using ZB.MOM.WW.OtOpcUa.Configuration.Enums; + +namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities; + +/// +/// Per-(node, deployment) apply progress row owned by the DriverHostActor. Replaces the +/// v1 single-row-per-node model with a history +/// of every apply attempt so the ConfigPublishCoordinator can reconstruct in-flight state +/// after a failover. +/// +public sealed class NodeDeploymentState +{ + public required string NodeId { get; init; } + + public Guid DeploymentId { get; init; } + + public NodeDeploymentStatus Status { get; set; } = NodeDeploymentStatus.Applying; + + public DateTime StartedAtUtc { get; set; } = DateTime.UtcNow; + + public DateTime? AppliedAtUtc { get; set; } + + public string? FailureReason { get; set; } + + public byte[] RowVersion { get; set; } = Array.Empty(); + + public ClusterNode? Node { get; set; } + public Deployment? Deployment { get; set; } +} diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/DeploymentStatus.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/DeploymentStatus.cs new file mode 100644 index 0000000..e268048 --- /dev/null +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/DeploymentStatus.cs @@ -0,0 +1,15 @@ +namespace ZB.MOM.WW.OtOpcUa.Configuration.Enums; + +/// +/// Lifecycle of a deployment artifact dispatched by the v2 ConfigPublishCoordinator. +/// Replaces the v1 ConfigGeneration draft/publish lifecycle (decision tracked in the +/// v2 hosting-alignment design doc). +/// +public enum DeploymentStatus +{ + Dispatching = 0, + AwaitingApplyAcks = 1, + Sealed = 2, + PartiallyFailed = 3, + TimedOut = 4, +} diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/NodeDeploymentStatus.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/NodeDeploymentStatus.cs new file mode 100644 index 0000000..1ce5de6 --- /dev/null +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/NodeDeploymentStatus.cs @@ -0,0 +1,12 @@ +namespace ZB.MOM.WW.OtOpcUa.Configuration.Enums; + +/// +/// Per-node deployment apply state. Replaces the v1 NodeApplyStatus that was attached to +/// ClusterNodeGenerationState. +/// +public enum NodeDeploymentStatus +{ + Applying = 0, + Applied = 1, + Failed = 2, +} diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs index 9862de8..cba2bec 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs @@ -1,3 +1,4 @@ +using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using ZB.MOM.WW.OtOpcUa.Configuration.Entities; using ZB.MOM.WW.OtOpcUa.Configuration.Enums; @@ -9,7 +10,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration; /// any divergence is a defect caught by the SchemaComplianceTests introspection check. /// public sealed class OtOpcUaConfigDbContext(DbContextOptions options) - : DbContext(options) + : DbContext(options), IDataProtectionKeyContext { public DbSet ServerClusters => Set(); public DbSet ClusterNodes => Set(); @@ -37,6 +38,16 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions ScriptedAlarms => Set(); public DbSet ScriptedAlarmStates => Set(); + // v2 deploy-model tables (Phase 1 of the Akka + fused-hosting alignment). Replace the + // ConfigGeneration/ClusterNodeGenerationState pair when Task 14's migration runs. + public DbSet Deployments => Set(); + public DbSet NodeDeploymentStates => Set(); + public DbSet ConfigEdits => Set(); + + // ASP.NET DataProtection key ring storage (decision: keys persisted in ConfigDb so every + // admin-role node decrypts the same cookies without sharing a filesystem). + public DbSet DataProtectionKeys => Set(); + protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); @@ -64,6 +75,10 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions x.UpdatedAtUtc).HasColumnType("datetime2(3)").HasDefaultValueSql("SYSUTCDATETIME()"); }); } + + private static void ConfigureDeployment(ModelBuilder modelBuilder) + { + modelBuilder.Entity(e => + { + e.ToTable("Deployment"); + e.HasKey(x => x.DeploymentId); + e.Property(x => x.DeploymentId).HasDefaultValueSql("NEWSEQUENTIALID()"); + e.Property(x => x.RevisionHash).HasMaxLength(64).IsRequired(); + e.Property(x => x.Status).HasConversion(); + e.Property(x => x.CreatedBy).HasMaxLength(128).IsRequired(); + e.Property(x => x.CreatedAtUtc).HasColumnType("datetime2(3)").HasDefaultValueSql("SYSUTCDATETIME()"); + e.Property(x => x.ArtifactBlob).HasColumnType("varbinary(max)"); + e.Property(x => x.RowVersion).IsRowVersion(); + e.Property(x => x.FailureReason).HasMaxLength(2048); + e.Property(x => x.SealedAtUtc).HasColumnType("datetime2(3)"); + + e.HasIndex(x => x.Status).HasDatabaseName("IX_Deployment_Status"); + e.HasIndex(x => x.CreatedAtUtc).HasDatabaseName("IX_Deployment_CreatedAt"); + }); + } + + private static void ConfigureNodeDeploymentState(ModelBuilder modelBuilder) + { + modelBuilder.Entity(e => + { + e.ToTable("NodeDeploymentState"); + e.HasKey(x => new { x.NodeId, x.DeploymentId }); + e.Property(x => x.NodeId).HasMaxLength(64); + e.Property(x => x.Status).HasConversion(); + e.Property(x => x.StartedAtUtc).HasColumnType("datetime2(3)").HasDefaultValueSql("SYSUTCDATETIME()"); + e.Property(x => x.AppliedAtUtc).HasColumnType("datetime2(3)"); + e.Property(x => x.FailureReason).HasMaxLength(2048); + e.Property(x => x.RowVersion).IsRowVersion(); + + e.HasOne(x => x.Node).WithMany().HasForeignKey(x => x.NodeId).OnDelete(DeleteBehavior.Restrict); + e.HasOne(x => x.Deployment).WithMany().HasForeignKey(x => x.DeploymentId).OnDelete(DeleteBehavior.Cascade); + + e.HasIndex(x => x.DeploymentId).HasDatabaseName("IX_NodeDeploymentState_Deployment"); + e.HasIndex(x => x.Status).HasDatabaseName("IX_NodeDeploymentState_Status"); + }); + } + + private static void ConfigureConfigEdit(ModelBuilder modelBuilder) + { + modelBuilder.Entity(e => + { + e.ToTable("ConfigEdit", t => + { + t.HasCheckConstraint("CK_ConfigEdit_FieldsJson_IsJson", "ISJSON(FieldsJson) = 1"); + }); + e.HasKey(x => x.EditId); + e.Property(x => x.EditId).HasDefaultValueSql("NEWSEQUENTIALID()"); + e.Property(x => x.EntityType).HasMaxLength(64).IsRequired(); + e.Property(x => x.FieldsJson).HasColumnType("nvarchar(max)").IsRequired(); + e.Property(x => x.EditedBy).HasMaxLength(128).IsRequired(); + e.Property(x => x.EditedAtUtc).HasColumnType("datetime2(3)").HasDefaultValueSql("SYSUTCDATETIME()"); + e.Property(x => x.SourceNode).HasMaxLength(64).IsRequired(); + + // Replays of admin operations group rows by ExecutionId, then by time. + e.HasIndex(x => new { x.EntityType, x.EntityId }).HasDatabaseName("IX_ConfigEdit_Entity"); + e.HasIndex(x => x.ExecutionId).HasFilter("[ExecutionId] IS NOT NULL").HasDatabaseName("IX_ConfigEdit_Execution"); + e.HasIndex(x => x.EditedAtUtc).HasDatabaseName("IX_ConfigEdit_EditedAt"); + }); + } + + private static void ConfigureDataProtectionKey(ModelBuilder modelBuilder) + { + // ASP.NET DataProtection ships its own EF mapping; override only the table name so it + // matches the rest of the schema's PascalCase convention. + modelBuilder.Entity(e => + { + e.ToTable("DataProtectionKeys"); + }); + } } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/ZB.MOM.WW.OtOpcUa.Configuration.csproj b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/ZB.MOM.WW.OtOpcUa.Configuration.csproj index ad38f5e..db15f31 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/ZB.MOM.WW.OtOpcUa.Configuration.csproj +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/ZB.MOM.WW.OtOpcUa.Configuration.csproj @@ -18,6 +18,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive +