From a0ca9af5c9db3b6abff413c3133fef0e59319c9c Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Wed, 14 Feb 2024 10:20:04 +0100 Subject: [PATCH] Started implementing schedules feature --- Moonlight/Core/Configuration/ConfigV1.cs | 3 + Moonlight/Core/Database/DataContext.cs | 1 + ...214091019_AddedServerSchedules.Designer.cs | 1089 +++++++++++++++++ .../20240214091019_AddedServerSchedules.cs | 51 + .../Migrations/DataContextModelSnapshot.cs | 46 + .../Actions/ServerServiceDefinition.cs | 1 + .../ScheduleRunnerService.cs | 125 ++ .../Servers/Configuration/ServersData.cs | 30 + .../Entities/Enums/ScheduleActionType.cs | 8 + Moonlight/Features/Servers/Entities/Server.cs | 2 + .../Servers/Entities/ServerSchedule.cs | 16 + .../Servers/UI/UserViews/Schedules.razor | 9 + Moonlight/Moonlight.csproj | 2 +- 13 files changed, 1382 insertions(+), 1 deletion(-) create mode 100644 Moonlight/Core/Database/Migrations/20240214091019_AddedServerSchedules.Designer.cs create mode 100644 Moonlight/Core/Database/Migrations/20240214091019_AddedServerSchedules.cs create mode 100644 Moonlight/Features/Servers/BackgroundServices/ScheduleRunnerService.cs create mode 100644 Moonlight/Features/Servers/Configuration/ServersData.cs create mode 100644 Moonlight/Features/Servers/Entities/Enums/ScheduleActionType.cs create mode 100644 Moonlight/Features/Servers/Entities/ServerSchedule.cs create mode 100644 Moonlight/Features/Servers/UI/UserViews/Schedules.razor diff --git a/Moonlight/Core/Configuration/ConfigV1.cs b/Moonlight/Core/Configuration/ConfigV1.cs index bdc5685..51a378f 100644 --- a/Moonlight/Core/Configuration/ConfigV1.cs +++ b/Moonlight/Core/Configuration/ConfigV1.cs @@ -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; @@ -26,6 +27,8 @@ public class ConfigV1 [JsonProperty("FileManager")] public FileManagerData FileManager { get; set; } = new(); [JsonProperty("WebServer")] public WebServerData WebServer { get; set; } = new(); + + [JsonProperty("Servers")] public ServersData Servers { get; set; } = new(); public class WebServerData { diff --git a/Moonlight/Core/Database/DataContext.cs b/Moonlight/Core/Database/DataContext.cs index d8f8fe1..53a354a 100644 --- a/Moonlight/Core/Database/DataContext.cs +++ b/Moonlight/Core/Database/DataContext.cs @@ -51,6 +51,7 @@ public class DataContext : DbContext public DbSet ServerVariables { get; set; } public DbSet ServerDockerImages { get; set; } public DbSet ServerImageVariables { get; set; } + public DbSet ServerSchedules { get; set; } public DataContext(ConfigService configService) { diff --git a/Moonlight/Core/Database/Migrations/20240214091019_AddedServerSchedules.Designer.cs b/Moonlight/Core/Database/Migrations/20240214091019_AddedServerSchedules.Designer.cs new file mode 100644 index 0000000..632b3cf --- /dev/null +++ b/Moonlight/Core/Database/Migrations/20240214091019_AddedServerSchedules.Designer.cs @@ -0,0 +1,1089 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Moonlight.Core.Database; + +#nullable disable + +namespace Moonlight.Core.Database.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20240214091019_AddedServerSchedules")] + partial class AddedServerSchedules + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.2"); + + modelBuilder.Entity("Moonlight.Core.Database.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Avatar") + .HasColumnType("TEXT"); + + b.Property("Balance") + .HasColumnType("REAL"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Flags") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("INTEGER"); + + b.Property("TokenValidTimestamp") + .HasColumnType("TEXT"); + + b.Property("TotpKey") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Moonlight.Features.Community.Entities.Post", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.ToTable("Posts"); + }); + + modelBuilder.Entity("Moonlight.Features.Community.Entities.PostComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("PostId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("PostId"); + + b.ToTable("PostComments"); + }); + + modelBuilder.Entity("Moonlight.Features.Community.Entities.PostLike", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("PostId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PostId"); + + b.HasIndex("UserId"); + + b.ToTable("PostLikes"); + }); + + modelBuilder.Entity("Moonlight.Features.Community.Entities.WordFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Filter") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("WordFilters"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.Server", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Cpu") + .HasColumnType("INTEGER"); + + b.Property("Disk") + .HasColumnType("INTEGER"); + + b.Property("DockerImageIndex") + .HasColumnType("INTEGER"); + + b.Property("ImageId") + .HasColumnType("INTEGER"); + + b.Property("MainAllocationId") + .HasColumnType("INTEGER"); + + b.Property("Memory") + .HasColumnType("INTEGER"); + + b.Property("NodeId") + .HasColumnType("INTEGER"); + + b.Property("OverrideStartupCommand") + .HasColumnType("TEXT"); + + b.Property("ServiceId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.HasIndex("MainAllocationId"); + + b.HasIndex("NodeId"); + + b.HasIndex("ServiceId"); + + b.ToTable("Servers"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerAllocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("ServerNodeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("ServerNodeId"); + + b.ToTable("ServerAllocations"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerDockerImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AutoPull") + .HasColumnType("INTEGER"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ServerImageId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ServerImageId"); + + b.ToTable("ServerDockerImages"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllocationsNeeded") + .HasColumnType("INTEGER"); + + b.Property("AllowUserToChangeDockerImage") + .HasColumnType("INTEGER"); + + b.Property("Author") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DefaultDockerImageIndex") + .HasColumnType("INTEGER"); + + b.Property("DonateUrl") + .HasColumnType("TEXT"); + + b.Property("InstallDockerImage") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("InstallScript") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("InstallShell") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OnlineDetection") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ParseConfigurations") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartupCommand") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StopCommand") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdateUrl") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ServerImages"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerImageVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowUserToEdit") + .HasColumnType("INTEGER"); + + b.Property("AllowUserToView") + .HasColumnType("INTEGER"); + + b.Property("DefaultValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ServerImageId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ServerImageId"); + + b.ToTable("ServerImageVariables"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerNode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Fqdn") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FtpPort") + .HasColumnType("INTEGER"); + + b.Property("HttpPort") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Token") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UseSsl") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ServerNodes"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ActionData") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ActionType") + .HasColumnType("INTEGER"); + + b.Property("Cron") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastRunAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("WasLastRunAutomatic") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.ToTable("ServerSchedules"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.ToTable("ServerVariables"); + }); + + modelBuilder.Entity("Moonlight.Features.ServiceManagement.Entities.Service", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConfigJsonOverride") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Nickname") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("INTEGER"); + + b.Property("ProductId") + .HasColumnType("INTEGER"); + + b.Property("RenewAt") + .HasColumnType("TEXT"); + + b.Property("Suspended") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("ProductId"); + + b.ToTable("Services"); + }); + + modelBuilder.Entity("Moonlight.Features.ServiceManagement.Entities.ServiceShare", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ServiceId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ServiceId"); + + b.HasIndex("UserId"); + + b.ToTable("ServiceShares"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.Coupon", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Percent") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.CouponUse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CouponId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CouponId"); + + b.HasIndex("UserId"); + + b.ToTable("CouponUses"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.GiftCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.ToTable("GiftCodes"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.GiftCodeUse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("GiftCodeId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GiftCodeId"); + + b.HasIndex("UserId"); + + b.ToTable("GiftCodeUses"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("ConfigJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Duration") + .HasColumnType("INTEGER"); + + b.Property("MaxPerUser") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("REAL"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Stock") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.Transaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("REAL"); + + b.Property("Text") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Transaction"); + }); + + modelBuilder.Entity("Moonlight.Features.Theming.Entities.Theme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CssUrl") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DonateUrl") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("JsUrl") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Themes"); + }); + + modelBuilder.Entity("Moonlight.Features.Ticketing.Entities.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatorId") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Open") + .HasColumnType("INTEGER"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("ServiceId") + .HasColumnType("INTEGER"); + + b.Property("Tries") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatorId"); + + b.HasIndex("ServiceId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("Moonlight.Features.Ticketing.Entities.TicketMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attachment") + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("IsSupport") + .HasColumnType("INTEGER"); + + b.Property("SenderId") + .HasColumnType("INTEGER"); + + b.Property("TicketId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SenderId"); + + b.HasIndex("TicketId"); + + b.ToTable("TicketMessages"); + }); + + modelBuilder.Entity("Moonlight.Features.Community.Entities.Post", b => + { + b.HasOne("Moonlight.Core.Database.Entities.User", "Author") + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("Moonlight.Features.Community.Entities.PostComment", b => + { + b.HasOne("Moonlight.Core.Database.Entities.User", "Author") + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Features.Community.Entities.Post", null) + .WithMany("Comments") + .HasForeignKey("PostId"); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("Moonlight.Features.Community.Entities.PostLike", b => + { + b.HasOne("Moonlight.Features.Community.Entities.Post", null) + .WithMany("Likes") + .HasForeignKey("PostId"); + + b.HasOne("Moonlight.Core.Database.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.Server", b => + { + b.HasOne("Moonlight.Features.Servers.Entities.ServerImage", "Image") + .WithMany() + .HasForeignKey("ImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Features.Servers.Entities.ServerAllocation", "MainAllocation") + .WithMany() + .HasForeignKey("MainAllocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Features.Servers.Entities.ServerNode", "Node") + .WithMany() + .HasForeignKey("NodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Features.ServiceManagement.Entities.Service", "Service") + .WithMany() + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Image"); + + b.Navigation("MainAllocation"); + + b.Navigation("Node"); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerAllocation", b => + { + b.HasOne("Moonlight.Features.Servers.Entities.Server", null) + .WithMany("Allocations") + .HasForeignKey("ServerId"); + + b.HasOne("Moonlight.Features.Servers.Entities.ServerNode", null) + .WithMany("Allocations") + .HasForeignKey("ServerNodeId"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerDockerImage", b => + { + b.HasOne("Moonlight.Features.Servers.Entities.ServerImage", null) + .WithMany("DockerImages") + .HasForeignKey("ServerImageId"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerImageVariable", b => + { + b.HasOne("Moonlight.Features.Servers.Entities.ServerImage", null) + .WithMany("Variables") + .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) + .WithMany("Variables") + .HasForeignKey("ServerId"); + }); + + modelBuilder.Entity("Moonlight.Features.ServiceManagement.Entities.Service", b => + { + b.HasOne("Moonlight.Core.Database.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Features.StoreSystem.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("Moonlight.Features.ServiceManagement.Entities.ServiceShare", b => + { + b.HasOne("Moonlight.Features.ServiceManagement.Entities.Service", null) + .WithMany("Shares") + .HasForeignKey("ServiceId"); + + b.HasOne("Moonlight.Core.Database.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.CouponUse", b => + { + b.HasOne("Moonlight.Features.StoreSystem.Entities.Coupon", "Coupon") + .WithMany() + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Core.Database.Entities.User", null) + .WithMany("CouponUses") + .HasForeignKey("UserId"); + + b.Navigation("Coupon"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.GiftCodeUse", b => + { + b.HasOne("Moonlight.Features.StoreSystem.Entities.GiftCode", "GiftCode") + .WithMany() + .HasForeignKey("GiftCodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Core.Database.Entities.User", null) + .WithMany("GiftCodeUses") + .HasForeignKey("UserId"); + + b.Navigation("GiftCode"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.Product", b => + { + b.HasOne("Moonlight.Features.StoreSystem.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.Transaction", b => + { + b.HasOne("Moonlight.Core.Database.Entities.User", null) + .WithMany("Transactions") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Moonlight.Features.Ticketing.Entities.Ticket", b => + { + b.HasOne("Moonlight.Core.Database.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Features.ServiceManagement.Entities.Service", "Service") + .WithMany() + .HasForeignKey("ServiceId"); + + b.Navigation("Creator"); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("Moonlight.Features.Ticketing.Entities.TicketMessage", b => + { + b.HasOne("Moonlight.Core.Database.Entities.User", "Sender") + .WithMany() + .HasForeignKey("SenderId"); + + b.HasOne("Moonlight.Features.Ticketing.Entities.Ticket", null) + .WithMany("Messages") + .HasForeignKey("TicketId"); + + b.Navigation("Sender"); + }); + + modelBuilder.Entity("Moonlight.Core.Database.Entities.User", b => + { + b.Navigation("CouponUses"); + + b.Navigation("GiftCodeUses"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Moonlight.Features.Community.Entities.Post", b => + { + b.Navigation("Comments"); + + b.Navigation("Likes"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.Server", b => + { + b.Navigation("Allocations"); + + b.Navigation("Schedules"); + + b.Navigation("Variables"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerImage", b => + { + b.Navigation("DockerImages"); + + b.Navigation("Variables"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerNode", b => + { + b.Navigation("Allocations"); + }); + + modelBuilder.Entity("Moonlight.Features.ServiceManagement.Entities.Service", b => + { + b.Navigation("Shares"); + }); + + modelBuilder.Entity("Moonlight.Features.Ticketing.Entities.Ticket", b => + { + b.Navigation("Messages"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Moonlight/Core/Database/Migrations/20240214091019_AddedServerSchedules.cs b/Moonlight/Core/Database/Migrations/20240214091019_AddedServerSchedules.cs new file mode 100644 index 0000000..3c39ea0 --- /dev/null +++ b/Moonlight/Core/Database/Migrations/20240214091019_AddedServerSchedules.cs @@ -0,0 +1,51 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Moonlight.Core.Database.Migrations +{ + /// + public partial class AddedServerSchedules : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ServerSchedules", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", nullable: false), + Cron = table.Column(type: "TEXT", nullable: false), + ActionType = table.Column(type: "INTEGER", nullable: false), + ActionData = table.Column(type: "TEXT", nullable: false), + LastRunAt = table.Column(type: "TEXT", nullable: false), + WasLastRunAutomatic = table.Column(type: "INTEGER", nullable: false), + ServerId = table.Column(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"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ServerSchedules"); + } + } +} diff --git a/Moonlight/Core/Database/Migrations/DataContextModelSnapshot.cs b/Moonlight/Core/Database/Migrations/DataContextModelSnapshot.cs index 2a2d6ee..df1c5be 100644 --- a/Moonlight/Core/Database/Migrations/DataContextModelSnapshot.cs +++ b/Moonlight/Core/Database/Migrations/DataContextModelSnapshot.cs @@ -398,6 +398,43 @@ namespace Moonlight.Core.Database.Migrations b.ToTable("ServerNodes"); }); + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ActionData") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ActionType") + .HasColumnType("INTEGER"); + + b.Property("Cron") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastRunAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("WasLastRunAutomatic") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.ToTable("ServerSchedules"); + }); + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerVariable", b => { b.Property("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"); }); diff --git a/Moonlight/Features/Servers/Actions/ServerServiceDefinition.cs b/Moonlight/Features/Servers/Actions/ServerServiceDefinition.cs index 5868b7f..630d9b0 100644 --- a/Moonlight/Features/Servers/Actions/ServerServiceDefinition.cs +++ b/Moonlight/Features/Servers/Actions/ServerServiceDefinition.cs @@ -17,6 +17,7 @@ public class ServerServiceDefinition : ServiceDefinition await context.AddPage("Console", "/", "bx bx-sm bxs-terminal"); await context.AddPage("Files", "/files", "bx bx-sm bxs-folder"); await context.AddPage("Network", "/network", "bx bx-sm bx-cloud"); + await context.AddPage("Schedules", "/schedules", "bx bx-sm bx-timer"); await context.AddPage("Variables", "/variables", "bx bx-sm bx-slider"); await context.AddPage("Reset", "/reset", "bx bx-sm bx-revision"); } diff --git a/Moonlight/Features/Servers/BackgroundServices/ScheduleRunnerService.cs b/Moonlight/Features/Servers/BackgroundServices/ScheduleRunnerService.cs new file mode 100644 index 0000000..7be1ec7 --- /dev/null +++ b/Moonlight/Features/Servers/BackgroundServices/ScheduleRunnerService.cs @@ -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 ConfigService; + + public ScheduleRunnerService(IServiceProvider serviceProvider, ConfigService 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>(); + var scheduleRepo = scope.ServiceProvider.GetRequiredService>(); + var serverService = scope.ServiceProvider.GetRequiredService(); + + // 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; + } + } +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/Configuration/ServersData.cs b/Moonlight/Features/Servers/Configuration/ServersData.cs new file mode 100644 index 0000000..cfda890 --- /dev/null +++ b/Moonlight/Features/Servers/Configuration/ServersData.cs @@ -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; + } +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/Entities/Enums/ScheduleActionType.cs b/Moonlight/Features/Servers/Entities/Enums/ScheduleActionType.cs new file mode 100644 index 0000000..c32a0a8 --- /dev/null +++ b/Moonlight/Features/Servers/Entities/Enums/ScheduleActionType.cs @@ -0,0 +1,8 @@ +namespace Moonlight.Features.Servers.Entities.Enums; + +public enum ScheduleActionType +{ + Command = 0, + Power = 1, + Backup = 2 +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/Entities/Server.cs b/Moonlight/Features/Servers/Entities/Server.cs index c96454e..78e0351 100644 --- a/Moonlight/Features/Servers/Entities/Server.cs +++ b/Moonlight/Features/Servers/Entities/Server.cs @@ -19,4 +19,6 @@ public class Server public ServerNode Node { get; set; } public ServerAllocation MainAllocation { get; set; } public List Allocations { get; set; } = new(); + + public List Schedules { get; set; } = new(); } \ No newline at end of file diff --git a/Moonlight/Features/Servers/Entities/ServerSchedule.cs b/Moonlight/Features/Servers/Entities/ServerSchedule.cs new file mode 100644 index 0000000..44c29e3 --- /dev/null +++ b/Moonlight/Features/Servers/Entities/ServerSchedule.cs @@ -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; +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/UI/UserViews/Schedules.razor b/Moonlight/Features/Servers/UI/UserViews/Schedules.razor new file mode 100644 index 0000000..09911ba --- /dev/null +++ b/Moonlight/Features/Servers/UI/UserViews/Schedules.razor @@ -0,0 +1,9 @@ +@using Moonlight.Features.Servers.Entities + + + +@code +{ + [CascadingParameter] + public Server Server { get; set; } +} diff --git a/Moonlight/Moonlight.csproj b/Moonlight/Moonlight.csproj index ac48f0b..617e76d 100644 --- a/Moonlight/Moonlight.csproj +++ b/Moonlight/Moonlight.csproj @@ -43,7 +43,6 @@ - @@ -52,6 +51,7 @@ +