Files
OTSSignsOrchestrator/OTSSignsOrchestrator.Server/Jobs/ByoiCertExpiryJob.cs
Matt Batchelder c6d46098dd feat: Implement provisioning pipelines for subscription management
- Add ReactivatePipeline to handle subscription reactivation, including scaling Docker services, health verification, status updates, audit logging, and broadcasting status changes.
- Introduce RotateCredentialsPipeline for OAuth2 credential rotation, managing the deletion of old apps, creation of new ones, credential storage, access verification, and audit logging.
- Create StepRunner to manage job step execution, including lifecycle management and progress broadcasting via SignalR.
- Implement SuspendPipeline for subscription suspension, scaling down services, updating statuses, logging audits, and broadcasting changes.
- Add UpdateScreenLimitPipeline to update Xibo CMS screen limits and record snapshots.
- Introduce XiboFeatureManifests for hardcoded feature ACLs per role.
- Add docker-compose.dev.yml for local development with PostgreSQL setup.
2026-03-18 10:27:26 -04:00

107 lines
3.7 KiB
C#

using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using Quartz;
using OTSSignsOrchestrator.Server.Data;
using OTSSignsOrchestrator.Server.Data.Entities;
using OTSSignsOrchestrator.Server.Hubs;
namespace OTSSignsOrchestrator.Server.Jobs;
/// <summary>
/// Quartz job that runs daily to check BYOI certificate expiry dates across all enabled
/// ByoiConfig entries. Alerts at 60, 30, and 7 day thresholds via FleetHub and logs to AuditLog.
///
/// Severity escalation:
/// - &gt; 7 days remaining → "Warning"
/// - ≤ 7 days remaining → "Critical"
/// </summary>
// IMMUTABLE AuditLog — this job only appends, never updates or deletes audit records.
[DisallowConcurrentExecution]
public sealed class ByoiCertExpiryJob : IJob
{
/// <summary>Alert thresholds in days. Alerts fire when remaining days ≤ threshold.</summary>
internal static readonly int[] AlertThresholdDays = [60, 30, 7];
/// <summary>Days at or below which severity escalates to "Critical".</summary>
internal const int CriticalThresholdDays = 7;
private readonly IServiceProvider _services;
private readonly ILogger<ByoiCertExpiryJob> _logger;
public ByoiCertExpiryJob(
IServiceProvider services,
ILogger<ByoiCertExpiryJob> logger)
{
_services = services;
_logger = logger;
}
public async Task Execute(IJobExecutionContext context)
{
await using var scope = _services.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<OrchestratorDbContext>();
var hub = scope.ServiceProvider.GetRequiredService<IHubContext<FleetHub, IFleetClient>>();
var configs = await db.ByoiConfigs
.AsNoTracking()
.Include(b => b.Instance)
.ThenInclude(i => i.Customer)
.Where(b => b.Enabled)
.ToListAsync(context.CancellationToken);
foreach (var config in configs)
{
var daysRemaining = (config.CertExpiry - DateTime.UtcNow).TotalDays;
var abbrev = config.Instance.Customer.Abbreviation;
if (!ShouldAlert(daysRemaining))
continue;
var severity = GetSeverity(daysRemaining);
var daysInt = (int)Math.Floor(daysRemaining);
var message = daysRemaining <= 0
? $"BYOI cert for {abbrev} has EXPIRED."
: $"BYOI cert for {abbrev} expires in {daysInt} days.";
_logger.LogWarning("BYOI cert expiry alert: {Severity} — {Message}", severity, message);
await hub.Clients.All.SendAlertRaised(severity, message);
// Append-only audit log
db.AuditLogs.Add(new AuditLog
{
Id = Guid.NewGuid(),
InstanceId = config.InstanceId,
Actor = "ByoiCertExpiryJob",
Action = "CertExpiryAlert",
Target = config.Slug,
Outcome = severity,
Detail = message,
OccurredAt = DateTime.UtcNow,
});
}
await db.SaveChangesAsync(context.CancellationToken);
}
/// <summary>
/// Determines whether an alert should fire based on remaining days.
/// Alerts at ≤ 60, ≤ 30, ≤ 7 days (or already expired).
/// </summary>
internal static bool ShouldAlert(double daysRemaining)
{
foreach (var threshold in AlertThresholdDays)
{
if (daysRemaining <= threshold)
return true;
}
return false;
}
/// <summary>
/// Returns "Critical" when ≤ 7 days remain, otherwise "Warning".
/// </summary>
internal static string GetSeverity(double daysRemaining) =>
daysRemaining <= CriticalThresholdDays ? "Critical" : "Warning";
}