Started implementing schedules feature
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
using MoonCore.Helpers;
|
using MoonCore.Helpers;
|
||||||
using Moonlight.Features.Advertisement.Configuration;
|
using Moonlight.Features.Advertisement.Configuration;
|
||||||
using Moonlight.Features.FileManager.Configuration;
|
using Moonlight.Features.FileManager.Configuration;
|
||||||
|
using Moonlight.Features.Servers.Configuration;
|
||||||
using Moonlight.Features.StoreSystem.Configuration;
|
using Moonlight.Features.StoreSystem.Configuration;
|
||||||
using Moonlight.Features.Theming.Configuration;
|
using Moonlight.Features.Theming.Configuration;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
@@ -27,6 +28,8 @@ public class ConfigV1
|
|||||||
|
|
||||||
[JsonProperty("WebServer")] public WebServerData WebServer { get; set; } = new();
|
[JsonProperty("WebServer")] public WebServerData WebServer { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonProperty("Servers")] public ServersData Servers { get; set; } = new();
|
||||||
|
|
||||||
public class WebServerData
|
public class WebServerData
|
||||||
{
|
{
|
||||||
[JsonProperty("HttpUploadLimit")]
|
[JsonProperty("HttpUploadLimit")]
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ public class DataContext : DbContext
|
|||||||
public DbSet<ServerVariable> ServerVariables { get; set; }
|
public DbSet<ServerVariable> ServerVariables { get; set; }
|
||||||
public DbSet<ServerDockerImage> ServerDockerImages { get; set; }
|
public DbSet<ServerDockerImage> ServerDockerImages { get; set; }
|
||||||
public DbSet<ServerImageVariable> ServerImageVariables { get; set; }
|
public DbSet<ServerImageVariable> ServerImageVariables { get; set; }
|
||||||
|
public DbSet<ServerSchedule> ServerSchedules { get; set; }
|
||||||
|
|
||||||
public DataContext(ConfigService<ConfigV1> configService)
|
public DataContext(ConfigService<ConfigV1> configService)
|
||||||
{
|
{
|
||||||
|
|||||||
1089
Moonlight/Core/Database/Migrations/20240214091019_AddedServerSchedules.Designer.cs
generated
Normal file
1089
Moonlight/Core/Database/Migrations/20240214091019_AddedServerSchedules.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -398,6 +398,43 @@ namespace Moonlight.Core.Database.Migrations
|
|||||||
b.ToTable("ServerNodes");
|
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 =>
|
modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerVariable", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -871,6 +908,13 @@ namespace Moonlight.Core.Database.Migrations
|
|||||||
.HasForeignKey("ServerImageId");
|
.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 =>
|
modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerVariable", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Moonlight.Features.Servers.Entities.Server", null)
|
b.HasOne("Moonlight.Features.Servers.Entities.Server", null)
|
||||||
@@ -1010,6 +1054,8 @@ namespace Moonlight.Core.Database.Migrations
|
|||||||
{
|
{
|
||||||
b.Navigation("Allocations");
|
b.Navigation("Allocations");
|
||||||
|
|
||||||
|
b.Navigation("Schedules");
|
||||||
|
|
||||||
b.Navigation("Variables");
|
b.Navigation("Variables");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ public class ServerServiceDefinition : ServiceDefinition
|
|||||||
await context.AddPage<Console>("Console", "/", "bx bx-sm bxs-terminal");
|
await context.AddPage<Console>("Console", "/", "bx bx-sm bxs-terminal");
|
||||||
await context.AddPage<Files>("Files", "/files", "bx bx-sm bxs-folder");
|
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<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<Variables>("Variables", "/variables", "bx bx-sm bx-slider");
|
||||||
await context.AddPage<Reset>("Reset", "/reset", "bx bx-sm bx-revision");
|
await context.AddPage<Reset>("Reset", "/reset", "bx bx-sm bx-revision");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
Moonlight/Features/Servers/Configuration/ServersData.cs
Normal file
30
Moonlight/Features/Servers/Configuration/ServersData.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Moonlight.Features.Servers.Entities.Enums;
|
||||||
|
|
||||||
|
public enum ScheduleActionType
|
||||||
|
{
|
||||||
|
Command = 0,
|
||||||
|
Power = 1,
|
||||||
|
Backup = 2
|
||||||
|
}
|
||||||
@@ -19,4 +19,6 @@ public class Server
|
|||||||
public ServerNode Node { get; set; }
|
public ServerNode Node { get; set; }
|
||||||
public ServerAllocation MainAllocation { get; set; }
|
public ServerAllocation MainAllocation { get; set; }
|
||||||
public List<ServerAllocation> Allocations { get; set; } = new();
|
public List<ServerAllocation> Allocations { get; set; } = new();
|
||||||
|
|
||||||
|
public List<ServerSchedule> Schedules { get; set; } = new();
|
||||||
}
|
}
|
||||||
16
Moonlight/Features/Servers/Entities/ServerSchedule.cs
Normal file
16
Moonlight/Features/Servers/Entities/ServerSchedule.cs
Normal 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;
|
||||||
|
}
|
||||||
9
Moonlight/Features/Servers/UI/UserViews/Schedules.razor
Normal file
9
Moonlight/Features/Servers/UI/UserViews/Schedules.razor
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
@using Moonlight.Features.Servers.Entities
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[CascadingParameter]
|
||||||
|
public Server Server { get; set; }
|
||||||
|
}
|
||||||
@@ -43,7 +43,6 @@
|
|||||||
<Folder Include="Features\FileManager\Models\Forms\" />
|
<Folder Include="Features\FileManager\Models\Forms\" />
|
||||||
<Folder Include="Features\FileManager\UI\Views\" />
|
<Folder Include="Features\FileManager\UI\Views\" />
|
||||||
<Folder Include="Features\Servers\Api\Resources\" />
|
<Folder Include="Features\Servers\Api\Resources\" />
|
||||||
<Folder Include="Features\Servers\Configuration\" />
|
|
||||||
<Folder Include="Features\Servers\Http\Resources\" />
|
<Folder Include="Features\Servers\Http\Resources\" />
|
||||||
<Folder Include="Features\StoreSystem\Helpers\" />
|
<Folder Include="Features\StoreSystem\Helpers\" />
|
||||||
<Folder Include="Features\Ticketing\Models\Abstractions\" />
|
<Folder Include="Features\Ticketing\Models\Abstractions\" />
|
||||||
@@ -52,6 +51,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
|
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
|
||||||
<PackageReference Include="BlazorTable" Version="1.17.0" />
|
<PackageReference Include="BlazorTable" Version="1.17.0" />
|
||||||
|
<PackageReference Include="Cronos" Version="0.8.3" />
|
||||||
<PackageReference Include="FluentFTP" Version="49.0.2" />
|
<PackageReference Include="FluentFTP" Version="49.0.2" />
|
||||||
<PackageReference Include="HtmlSanitizer" Version="8.0.746" />
|
<PackageReference Include="HtmlSanitizer" Version="8.0.746" />
|
||||||
<PackageReference Include="JWT" Version="10.1.1" />
|
<PackageReference Include="JWT" Version="10.1.1" />
|
||||||
|
|||||||
Reference in New Issue
Block a user