Base implementation of smart deploy for servers, order screen, subscription service
This commit is contained in:
14
Moonlight/App/Models/Forms/ServerOrderDataModel.cs
Normal file
14
Moonlight/App/Models/Forms/ServerOrderDataModel.cs
Normal 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; }
|
||||||
|
}
|
||||||
@@ -5,6 +5,12 @@ public class SubscriptionLimit
|
|||||||
public string Identifier { get; set; } = "";
|
public string Identifier { get; set; } = "";
|
||||||
public int Amount { get; set; }
|
public int Amount { get; set; }
|
||||||
public List<LimitOption> Options { get; set; } = new();
|
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
|
public class LimitOption
|
||||||
{
|
{
|
||||||
|
|||||||
64
Moonlight/App/Services/SmartDeployService.cs
Normal file
64
Moonlight/App/Services/SmartDeployService.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -82,16 +82,19 @@ public class SubscriptionService
|
|||||||
{
|
{
|
||||||
var configSection = ConfigService.GetSection("Moonlight").GetSection("Subscriptions");
|
var configSection = ConfigService.GetSection("Moonlight").GetSection("Subscriptions");
|
||||||
|
|
||||||
var defaultLimits = configSection.GetValue<SubscriptionLimit[]>("defaultLimits");
|
var defaultLimits = configSection.GetValue<SubscriptionLimit[]>("DefaultLimits");
|
||||||
|
|
||||||
var subscription = await GetCurrent();
|
var subscription = await GetCurrent();
|
||||||
|
|
||||||
if (subscription == null)
|
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)
|
if (foundDefault != null)
|
||||||
return foundDefault;
|
return foundDefault;
|
||||||
|
}
|
||||||
|
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
@@ -109,11 +112,14 @@ public class SubscriptionService
|
|||||||
|
|
||||||
if (foundLimit != null)
|
if (foundLimit != null)
|
||||||
return foundLimit;
|
return foundLimit;
|
||||||
|
|
||||||
var foundDefault = defaultLimits.FirstOrDefault(x => x.Identifier == identifier);
|
|
||||||
|
|
||||||
if (foundDefault != null)
|
if (defaultLimits != null)
|
||||||
return foundDefault;
|
{
|
||||||
|
var foundDefault = defaultLimits.FirstOrDefault(x => x.Identifier == identifier);
|
||||||
|
|
||||||
|
if (foundDefault != null)
|
||||||
|
return foundDefault;
|
||||||
|
}
|
||||||
|
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ namespace Moonlight
|
|||||||
builder.Services.AddScoped<NotificationAdminService>();
|
builder.Services.AddScoped<NotificationAdminService>();
|
||||||
builder.Services.AddScoped<NotificationClientService>();
|
builder.Services.AddScoped<NotificationClientService>();
|
||||||
builder.Services.AddScoped<ModalService>();
|
builder.Services.AddScoped<ModalService>();
|
||||||
|
builder.Services.AddScoped<SmartDeployService>();
|
||||||
|
|
||||||
builder.Services.AddScoped<GoogleOAuth2Service>();
|
builder.Services.AddScoped<GoogleOAuth2Service>();
|
||||||
builder.Services.AddScoped<DiscordOAuth2Service>();
|
builder.Services.AddScoped<DiscordOAuth2Service>();
|
||||||
|
|||||||
@@ -153,7 +153,8 @@
|
|||||||
await JsRuntime.InvokeVoidAsync("document.body.removeAttribute", "data-kt-app-page-loading");
|
await JsRuntime.InvokeVoidAsync("document.body.removeAttribute", "data-kt-app-page-loading");
|
||||||
await JsRuntime.InvokeVoidAsync("KTMenu.createInstances");
|
await JsRuntime.InvokeVoidAsync("KTMenu.createInstances");
|
||||||
await JsRuntime.InvokeVoidAsync("KTDrawer.createInstances");
|
await JsRuntime.InvokeVoidAsync("KTDrawer.createInstances");
|
||||||
await JsRuntime.InvokeVoidAsync("createSnow");
|
|
||||||
|
//await JsRuntime.InvokeVoidAsync("createSnow");
|
||||||
|
|
||||||
await SessionService.Register();
|
await SessionService.Register();
|
||||||
|
|
||||||
|
|||||||
174
Moonlight/Shared/Views/Servers/New.razor
Normal file
174
Moonlight/Shared/Views/Servers/New.razor
Normal 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()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -463,3 +463,15 @@ Options;Options
|
|||||||
Amount;Amount
|
Amount;Amount
|
||||||
Do you really want to delete it?;Do you really want to delete it?
|
Do you really want to delete it?;Do you really want to delete it?
|
||||||
Save subscription;Save subscription
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user