Started implementing schedules feature

This commit is contained in:
Marcel Baumgartner 2024-02-14 10:20:04 +01:00
parent d1f73e6d78
commit a0ca9af5c9
13 changed files with 1382 additions and 1 deletions

View file

@ -2,6 +2,7 @@
using MoonCore.Helpers;
using Moonlight.Features.Advertisement.Configuration;
using Moonlight.Features.FileManager.Configuration;
using Moonlight.Features.Servers.Configuration;
using Moonlight.Features.StoreSystem.Configuration;
using Moonlight.Features.Theming.Configuration;
using Newtonsoft.Json;
@ -27,6 +28,8 @@ public class ConfigV1
[JsonProperty("WebServer")] public WebServerData WebServer { get; set; } = new();
[JsonProperty("Servers")] public ServersData Servers { get; set; } = new();
public class WebServerData
{
[JsonProperty("HttpUploadLimit")]

View file

@ -51,6 +51,7 @@ public class DataContext : DbContext
public DbSet<ServerVariable> ServerVariables { get; set; }
public DbSet<ServerDockerImage> ServerDockerImages { get; set; }
public DbSet<ServerImageVariable> ServerImageVariables { get; set; }
public DbSet<ServerSchedule> ServerSchedules { get; set; }
public DataContext(ConfigService<ConfigV1> configService)
{

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,51 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.Core.Database.Migrations
{
/// <inheritdoc />
public partial class AddedServerSchedules : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ServerSchedules",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", nullable: false),
Cron = table.Column<string>(type: "TEXT", nullable: false),
ActionType = table.Column<int>(type: "INTEGER", nullable: false),
ActionData = table.Column<string>(type: "TEXT", nullable: false),
LastRunAt = table.Column<DateTime>(type: "TEXT", nullable: false),
WasLastRunAutomatic = table.Column<bool>(type: "INTEGER", nullable: false),
ServerId = table.Column<int>(type: "INTEGER", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ServerSchedules", x => x.Id);
table.ForeignKey(
name: "FK_ServerSchedules_Servers_ServerId",
column: x => x.ServerId,
principalTable: "Servers",
principalColumn: "Id");
});
migrationBuilder.CreateIndex(
name: "IX_ServerSchedules_ServerId",
table: "ServerSchedules",
column: "ServerId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ServerSchedules");
}
}
}

View file

@ -398,6 +398,43 @@ namespace Moonlight.Core.Database.Migrations
b.ToTable("ServerNodes");
});
modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerSchedule", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ActionData")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("ActionType")
.HasColumnType("INTEGER");
b.Property<string>("Cron")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("LastRunAt")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int?>("ServerId")
.HasColumnType("INTEGER");
b.Property<bool>("WasLastRunAutomatic")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ServerId");
b.ToTable("ServerSchedules");
});
modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerVariable", b =>
{
b.Property<int>("Id")
@ -871,6 +908,13 @@ namespace Moonlight.Core.Database.Migrations
.HasForeignKey("ServerImageId");
});
modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerSchedule", b =>
{
b.HasOne("Moonlight.Features.Servers.Entities.Server", null)
.WithMany("Schedules")
.HasForeignKey("ServerId");
});
modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerVariable", b =>
{
b.HasOne("Moonlight.Features.Servers.Entities.Server", null)
@ -1010,6 +1054,8 @@ namespace Moonlight.Core.Database.Migrations
{
b.Navigation("Allocations");
b.Navigation("Schedules");
b.Navigation("Variables");
});

View file

@ -17,6 +17,7 @@ public class ServerServiceDefinition : ServiceDefinition
await context.AddPage<Console>("Console", "/", "bx bx-sm bxs-terminal");
await context.AddPage<Files>("Files", "/files", "bx bx-sm bxs-folder");
await context.AddPage<Network>("Network", "/network", "bx bx-sm bx-cloud");
await context.AddPage<Schedules>("Schedules", "/schedules", "bx bx-sm bx-timer");
await context.AddPage<Variables>("Variables", "/variables", "bx bx-sm bx-slider");
await context.AddPage<Reset>("Reset", "/reset", "bx bx-sm bx-revision");
}

View file

@ -0,0 +1,125 @@
using Cronos;
using Microsoft.EntityFrameworkCore;
using MoonCore.Abstractions;
using MoonCore.Helpers;
using MoonCore.Services;
using Moonlight.Core.Configuration;
using Moonlight.Features.Servers.Entities;
using Moonlight.Features.Servers.Entities.Enums;
using Moonlight.Features.Servers.Services;
using BackgroundService = MoonCore.Abstractions.BackgroundService;
namespace Moonlight.Features.Servers.BackgroundServices;
public class ScheduleRunnerService : BackgroundService
{
private readonly IServiceProvider ServiceProvider;
private readonly ConfigService<ConfigV1> ConfigService;
public ScheduleRunnerService(IServiceProvider serviceProvider, ConfigService<ConfigV1> configService)
{
ServiceProvider = serviceProvider;
ConfigService = configService;
}
public override async Task Run()
{
var config = ConfigService.Get().Servers.Schedules;
if(config.Disable)
Logger.Info("Server schedules are disabled");
while (!Cancellation.IsCancellationRequested)
{
Logger.Debug("Performing scheduler run");
// Resolve services from di
using var scope = ServiceProvider.CreateScope();
var serverRepo = scope.ServiceProvider.GetRequiredService<Repository<Server>>();
var scheduleRepo = scope.ServiceProvider.GetRequiredService<Repository<ServerSchedule>>();
var serverService = scope.ServiceProvider.GetRequiredService<ServerService>();
// Load required data
var servers = serverRepo
.Get()
.Include(x => x.Schedules)
.Where(x => x.Schedules.Any())
.ToArray();
Logger.Debug($"Found {servers.Length} servers with schedules");
foreach (var server in servers)
{
foreach (var schedule in server.Schedules)
{
if (!CronExpression.TryParse(schedule.Cron, CronFormat.Standard, out CronExpression cronExpression))
{
Logger.Warn($"Unable to parse cron expression for schedule '{schedule.Name}' for server '{server.Id}'");
continue;
}
var nextRun = cronExpression.GetNextOccurrence(schedule.LastRunAt, TimeZoneInfo.Utc);
if (nextRun == null)
{
Logger.Warn($"Unable to determine next run time for schedule '{schedule.Name}' for server '{server.Id}'");
continue;
}
// Check if the next run is in the past
if (DateTime.UtcNow > nextRun)
{
var diff = DateTime.UtcNow - nextRun;
if(diff.Value.TotalMinutes > 0)
Logger.Warn($"Missed executing schedule '{schedule.Name}' for server '{server.Id}'. Was moonlight offline for a while?");
else
Logger.Warn($"Missed executing schedule '{schedule.Name}' for server '{server.Id}'. The miss difference ({diff.Value.TotalSeconds}s) indicate a miss configuration. Increase your time drift or lower your check delay to fix the error");
schedule.LastRunAt = DateTime.UtcNow;
schedule.WasLastRunAutomatic = false;
scheduleRepo.Update(schedule);
continue;
}
// Check if the next run is too far in the future
if ((nextRun - DateTime.UtcNow).Value.TotalSeconds > config.TimeDrift)
continue;
// Its time to execute the schedule :D
await RunSchedule(server, schedule, scope.ServiceProvider);
schedule.LastRunAt = DateTime.UtcNow;
schedule.WasLastRunAutomatic = true;
scheduleRepo.Update(schedule);
}
}
await Task.Delay(config.CheckDelay, Cancellation.Token);
}
}
public async Task RunSchedule(Server server, ServerSchedule schedule, IServiceProvider? provider = null)
{
IServiceProvider serviceProvider;
if (provider != null)
serviceProvider = provider;
else
serviceProvider = ServiceProvider.CreateScope().ServiceProvider;
switch (schedule.ActionType)
{
case ScheduleActionType.Power:
//TODO: Implement actual action here
break;
default:
Logger.Warn($"Schedule action type {schedule.ActionType} has not been implemented yet");
break;
}
}
}

View file

@ -0,0 +1,30 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
namespace Moonlight.Features.Servers.Configuration;
public class ServersData
{
[JsonProperty("Schedules")] public SchedulesData Schedules { get; set; } = new();
public class SchedulesData
{
[JsonProperty("Disable")]
[Description("This flag stops the schedule execution system from starting. Changing this requires a restart")]
public bool Disable { get; set; } = false;
[JsonProperty("SchedulePerServer")]
[Description("This specifies the schedules a user can create for a server")]
public int SchedulePerServer { get; set; } = 10;
[JsonProperty("CheckDelay")]
[Description("The delay in seconds between every schedule run")]
public int CheckDelay { get; set; } = 10;
[JsonProperty("TimeDrift")]
[Description(
"The amount of seconds the planned execution time is allowed to vary to the current time and still be executed")]
public int TimeDrift { get; set; } = 30;
}
}

View file

@ -0,0 +1,8 @@
namespace Moonlight.Features.Servers.Entities.Enums;
public enum ScheduleActionType
{
Command = 0,
Power = 1,
Backup = 2
}

View file

@ -19,4 +19,6 @@ public class Server
public ServerNode Node { get; set; }
public ServerAllocation MainAllocation { get; set; }
public List<ServerAllocation> Allocations { get; set; } = new();
public List<ServerSchedule> Schedules { get; set; } = new();
}

View file

@ -0,0 +1,16 @@
using Moonlight.Features.Servers.Entities.Enums;
namespace Moonlight.Features.Servers.Entities;
public class ServerSchedule
{
public int Id { get; set; }
public string Name { get; set; }
public string Cron { get; set; }
public ScheduleActionType ActionType { get; set; }
public string ActionData { get; set; } = "";
public DateTime LastRunAt { get; set; } = DateTime.Now;
public bool WasLastRunAutomatic { get; set; } = false;
}

View file

@ -0,0 +1,9 @@
@using Moonlight.Features.Servers.Entities
@code
{
[CascadingParameter]
public Server Server { get; set; }
}

View file

@ -43,7 +43,6 @@
<Folder Include="Features\FileManager\Models\Forms\" />
<Folder Include="Features\FileManager\UI\Views\" />
<Folder Include="Features\Servers\Api\Resources\" />
<Folder Include="Features\Servers\Configuration\" />
<Folder Include="Features\Servers\Http\Resources\" />
<Folder Include="Features\StoreSystem\Helpers\" />
<Folder Include="Features\Ticketing\Models\Abstractions\" />
@ -52,6 +51,7 @@
<ItemGroup>
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
<PackageReference Include="BlazorTable" Version="1.17.0" />
<PackageReference Include="Cronos" Version="0.8.3" />
<PackageReference Include="FluentFTP" Version="49.0.2" />
<PackageReference Include="HtmlSanitizer" Version="8.0.746" />
<PackageReference Include="JWT" Version="10.1.1" />