Base implementation of smart deploy for servers, order screen, subscription service

This commit is contained in:
Marcel Baumgartner 2023-04-03 21:23:27 +02:00
parent a41a929e7f
commit 2298bab71e
9 changed files with 287 additions and 9 deletions

View file

@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
using Moonlight.App.Database.Entities;
namespace Moonlight.App.Models.Forms;
public class ServerOrderDataModel
{
[Required(ErrorMessage = "You need to enter a name")]
[MaxLength(32, ErrorMessage = "The name cannot be longer that 32 characters")]
public string Name { get; set; } = "";
[Required(ErrorMessage = "You need to specify a server image")]
public Image Image { get; set; }
}

View file

@ -5,6 +5,12 @@ public class SubscriptionLimit
public string Identifier { get; set; } = "";
public int Amount { get; set; }
public List<LimitOption> Options { get; set; } = new();
public string? ReadValue(string key)
{
var d = Options.FirstOrDefault(x => string.Equals(x.Key, key, StringComparison.InvariantCultureIgnoreCase));
return d?.Value;
}
public class LimitOption
{

View file

@ -0,0 +1,64 @@
using Moonlight.App.Database.Entities;
using Moonlight.App.Repositories;
namespace Moonlight.App.Services;
public class SmartDeployService
{
private readonly NodeRepository NodeRepository;
private readonly NodeService NodeService;
public SmartDeployService(NodeRepository nodeRepository, NodeService nodeService)
{
NodeRepository = nodeRepository;
NodeService = nodeService;
}
public async Task<Node?> GetNode()
{
var data = new Dictionary<Node, double>();
foreach (var node in NodeRepository.Get().ToArray())
{
var u = await GetUsageScore(node);
if(u != 0)
data.Add(node, u);
}
if (!data.Any())
return null;
return data.MaxBy(x => x.Value).Key;
}
private async Task<double> GetUsageScore(Node node)
{
var score = 0;
try
{
var cpuStats = await NodeService.GetCpuStats(node);
var memoryStats = await NodeService.GetMemoryStats(node);
var diskStats = await NodeService.GetDiskStats(node);
var cpuWeight = 0.5; // Weight of CPU usage in the final score
var memoryWeight = 0.3; // Weight of memory usage in the final score
var diskSpaceWeight = 0.2; // Weight of free disk space in the final score
var cpuScore = (1 - cpuStats.Usage) * cpuWeight; // CPU score is based on the inverse of CPU usage
var memoryScore = (1 - (memoryStats.Used / 1024)) * memoryWeight; // Memory score is based on the percentage of free memory
var diskSpaceScore = (double) diskStats.FreeBytes / 1000000000 * diskSpaceWeight; // Disk space score is based on the amount of free disk space in GB
var finalScore = cpuScore + memoryScore + diskSpaceScore;
return finalScore;
}
catch (Exception e)
{
// ignored
}
return score;
}
}

View file

@ -82,16 +82,19 @@ public class SubscriptionService
{
var configSection = ConfigService.GetSection("Moonlight").GetSection("Subscriptions");
var defaultLimits = configSection.GetValue<SubscriptionLimit[]>("defaultLimits");
var defaultLimits = configSection.GetValue<SubscriptionLimit[]>("DefaultLimits");
var subscription = await GetCurrent();
if (subscription == null)
{
var foundDefault = defaultLimits.FirstOrDefault(x => x.Identifier == identifier);
if (defaultLimits != null)
{
var foundDefault = defaultLimits.FirstOrDefault(x => x.Identifier == identifier);
if (foundDefault != null)
return foundDefault;
if (foundDefault != null)
return foundDefault;
}
return new()
{
@ -109,11 +112,14 @@ public class SubscriptionService
if (foundLimit != null)
return foundLimit;
var foundDefault = defaultLimits.FirstOrDefault(x => x.Identifier == identifier);
if (foundDefault != null)
return foundDefault;
if (defaultLimits != null)
{
var foundDefault = defaultLimits.FirstOrDefault(x => x.Identifier == identifier);
if (foundDefault != null)
return foundDefault;
}
return new()
{

View file

@ -91,6 +91,7 @@ namespace Moonlight
builder.Services.AddScoped<NotificationAdminService>();
builder.Services.AddScoped<NotificationClientService>();
builder.Services.AddScoped<ModalService>();
builder.Services.AddScoped<SmartDeployService>();
builder.Services.AddScoped<GoogleOAuth2Service>();
builder.Services.AddScoped<DiscordOAuth2Service>();

View file

@ -153,7 +153,8 @@
await JsRuntime.InvokeVoidAsync("document.body.removeAttribute", "data-kt-app-page-loading");
await JsRuntime.InvokeVoidAsync("KTMenu.createInstances");
await JsRuntime.InvokeVoidAsync("KTDrawer.createInstances");
await JsRuntime.InvokeVoidAsync("createSnow");
//await JsRuntime.InvokeVoidAsync("createSnow");
await SessionService.Register();

View file

@ -0,0 +1,174 @@
@page "/servers/new"
@using Moonlight.App.Services
@using Moonlight.App.Database.Entities
@using Moonlight.App.Models.Forms
@using Moonlight.App.Models.Misc
@using Moonlight.App.Repositories
@inject SubscriptionService SubscriptionService
@inject ImageRepository ImageRepository
@inject SmartTranslateService SmartTranslateService
@inject SmartDeployService SmartDeployService
<LazyLoader Load="Load">
@if (DeployNode == null)
{
<div class="d-flex justify-content-center flex-center">
<div class="card">
<img src="/assets/media/svg/nodata.svg" class="card-img-top w-25 mx-auto pt-5" alt="Not found image"/>
<div class="card-body text-center">
<h4 class="card-title">
<TL>No node found</TL>
</h4>
<p class="card-text">
<TL>No node found to deploy to found</TL>
</p>
</div>
</div>
</div>
}
else
{
<div class="d-flex flex-column flex-lg-row">
<div class="w-100 flex-lg-row-auto w-lg-300px mb-7 me-7 me-lg-10" data-select2-id="select2-data-131-dr2d">
<div class="card card-flush py-4" data-select2-id="select2-data-130-ru5y">
<div class="card-header">
<div class="card-title">
<h2>
<TL>Server details</TL>
</h2>
</div>
</div>
<div class="card-body pt-0">
<div class="d-flex flex-column gap-10">
<div class="fv-row">
<label class="form-label">Node</label>
<div class="fw-bold fs-3">@(DeployNode.Name)</div>
</div>
@if (Model.Image != null)
{
var limit = Images[Model.Image];
<div class="fv-row">
<label class="form-label"><TL>Image</TL></label>
<div class="fw-bold fs-3">@(Model.Image.Name)</div>
</div>
<div class="fv-row">
<label class="form-label"><TL>CPU</TL></label>
<div class="fw-bold fs-3">
@{
var cpu = limit.ReadValue("cpu");
if (cpu == null)
cpu = "N/A";
else
cpu = (int.Parse(cpu) / 100).ToString();
}
@(cpu) <TL>Cores</TL>
</div>
</div>
<div class="fv-row">
<label class="form-label"><TL>Memory</TL></label>
<div class="fw-bold fs-3">@(limit.ReadValue("memory")) MB</div>
</div>
<div class="fv-row">
<label class="form-label"><TL>Disk</TL></label>
<div class="fw-bold fs-3">@(limit.ReadValue("disk")) MB</div>
</div>
}
</div>
</div>
</div>
</div>
<div class="d-flex flex-column flex-lg-row-fluid gap-7 gap-lg-10">
<div class="card card-flush py-4">
<div class="card-header">
<div class="card-title">
<h2>
<TL>Configure your server</TL>
</h2>
</div>
</div>
<div class="card-body pt-0">
<SmartForm Model="Model" OnValidSubmit="OnValidSubmit">
<label class="form-label">
<TL>Name</TL>
</label>
<div class="input-group mb-5">
<InputText @bind-Value="Model.Name" class="form-control"></InputText>
</div>
@if (Images.Any())
{
<label class="form-label">
<TL>Image</TL>
</label>
<SmartSelect TField="Image"
@bind-Value="Model.Image"
Items="Images.Keys.ToArray()"
DisplayField="@(x => x.Name)">
</SmartSelect>
<button type="submit" class="mt-5 float-end btn btn-primary">
<TL>Create</TL>
</button>
}
else
{
<div class="alert alert-warning d-flex align-items-center p-5 mb-10">
<span>
<TL>You reached the maximum amount of servers for every image of your subscription</TL>: @(Subscription == null ? SmartTranslateService.Translate("Default") : Subscription.Name)
</span>
</div>
}
</SmartForm>
</div>
</div>
</div>
</div>
}
</LazyLoader>
@code
{
private Node? DeployNode;
private Subscription? Subscription;
private Dictionary<Image, SubscriptionLimit> Images = new();
private ServerOrderDataModel Model = new();
private async Task Load(LazyLoader lazyLoader)
{
// Reset state
Images.Clear();
Model = new();
await lazyLoader.SetText(SmartTranslateService.Translate("Loading your subscription"));
Subscription = await SubscriptionService.GetCurrent();
await lazyLoader.SetText(SmartTranslateService.Translate("Searching for deploy node"));
DeployNode = await SmartDeployService.GetNode();
await lazyLoader.SetText(SmartTranslateService.Translate("Searching for available images"));
var images = ImageRepository.Get().ToArray();
foreach (var image in images)
{
var limit = await SubscriptionService.GetLimit("image." + image.Id);
if (limit.Amount > 0)
{
Images.Add(image, limit);
}
}
}
private async Task OnValidSubmit()
{
}
}

View file

@ -463,3 +463,15 @@ Options;Options
Amount;Amount
Do you really want to delete it?;Do you really want to delete it?
Save subscription;Save subscription
Loading your subscription;Loading your subscription
Searching for deploy node;Searching for deploy node
Searching for available images;Searching for available images
Server details;Server details
Configure your server;Configure your server
Default;Default
No images available;No images available
You reached the maximum amount of servers for every image of your subscription;You reached the maximum amount of servers for every image of your subscription
No node found;No node found
No node found to deploy to found;No node found to deploy to found
You need to specify a server image;You need to specify a server image
CPU;CPU