Merge pull request #149 from Moonlight-Panel/main

Update AddHealthChecks with latest commits
This commit is contained in:
Marcel Baumgartner 2023-06-09 14:21:28 +02:00 committed by GitHub
commit bd8ba11410
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 242 additions and 171 deletions

View file

@ -1,5 +1,4 @@
using Moonlight.App.Database.Entities;
using Moonlight.App.Exceptions;
using Newtonsoft.Json;
using RestSharp;
@ -14,26 +13,13 @@ public class DaemonApiHelper
Client = new();
}
private string GetApiUrl(Node node)
{
/* SSL not implemented in moonlight daemon
if(node.Ssl)
return $"https://{node.Fqdn}:{node.MoonlightDaemonPort}/";
else
return $"http://{node.Fqdn}:{node.MoonlightDaemonPort}/";*/
return $"http://{node.Fqdn}:{node.MoonlightDaemonPort}/";
}
public async Task<T> Get<T>(Node node, string resource)
{
RestRequest request = new(GetApiUrl(node) + resource);
var request = await CreateRequest(node, resource);
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Accept", "application/json");
request.AddHeader("Authorization", node.Token);
var response = await Client.GetAsync(request);
request.Method = Method.Get;
var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful)
{
@ -52,4 +38,69 @@ public class DaemonApiHelper
return JsonConvert.DeserializeObject<T>(response.Content!)!;
}
public async Task Post(Node node, string resource, object body)
{
var request = await CreateRequest(node, resource);
request.Method = Method.Post;
request.AddParameter("text/plain", JsonConvert.SerializeObject(body), ParameterType.RequestBody);
var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful)
{
if (response.StatusCode != 0)
{
throw new DaemonException(
$"An error occured: ({response.StatusCode}) {response.Content}",
(int)response.StatusCode
);
}
else
{
throw new Exception($"An internal error occured: {response.ErrorMessage}");
}
}
}
public async Task Delete(Node node, string resource, object body)
{
var request = await CreateRequest(node, resource);
request.Method = Method.Delete;
request.AddParameter("text/plain", JsonConvert.SerializeObject(body), ParameterType.RequestBody);
var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful)
{
if (response.StatusCode != 0)
{
throw new DaemonException(
$"An error occured: ({response.StatusCode}) {response.Content}",
(int)response.StatusCode
);
}
else
{
throw new Exception($"An internal error occured: {response.ErrorMessage}");
}
}
}
private Task<RestRequest> CreateRequest(Node node, string resource)
{
var url = $"http://{node.Fqdn}:{node.MoonlightDaemonPort}/";
RestRequest request = new(url + resource);
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Accept", "application/json");
request.AddHeader("Authorization", node.Token);
return Task.FromResult(request);
}
}

View file

@ -0,0 +1,8 @@
namespace Moonlight.App.ApiClients.Daemon.Requests;
public class Mount
{
public string Server { get; set; } = "";
public string ServerPath { get; set; } = "";
public string Path { get; set; } = "";
}

View file

@ -0,0 +1,6 @@
namespace Moonlight.App.ApiClients.Daemon.Requests;
public class Unmount
{
public string Path { get; set; } = "";
}

View file

@ -0,0 +1,10 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class Container
{
public string Name { get; set; } = "";
public long Memory { get; set; }
public double Cpu { get; set; }
public long NetworkIn { get; set; }
public long NetworkOut { get; set; }
}

View file

@ -1,15 +0,0 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class ContainerStats
{
public List<Container> Containers { get; set; } = new();
public class Container
{
public string Name { get; set; }
public long Memory { get; set; }
public double Cpu { get; set; }
public long NetworkIn { get; set; }
public long NetworkOut { get; set; }
}
}

View file

@ -0,0 +1,7 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class CpuMetrics
{
public string CpuModel { get; set; } = "";
public double CpuUsage { get; set; }
}

View file

@ -1,8 +0,0 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class CpuStats
{
public double Usage { get; set; }
public int Cores { get; set; }
public string Model { get; set; } = "";
}

View file

@ -0,0 +1,7 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class DiskMetrics
{
public long Used { get; set; }
public long Total { get; set; }
}

View file

@ -1,9 +0,0 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class DiskStats
{
public long FreeBytes { get; set; }
public string DriveFormat { get; set; }
public string Name { get; set; }
public long TotalSize { get; set; }
}

View file

@ -0,0 +1,6 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class DockerMetrics
{
public Container[] Containers { get; set; } = Array.Empty<Container>();
}

View file

@ -0,0 +1,7 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class MemoryMetrics
{
public long Used { get; set; }
public long Total { get; set; }
}

View file

@ -1,15 +0,0 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class MemoryStats
{
public List<MemoryStick> Sticks { get; set; } = new();
public double Free { get; set; }
public double Used { get; set; }
public double Total { get; set; }
public class MemoryStick
{
public int Size { get; set; }
public string Type { get; set; } = "";
}
}

View file

@ -0,0 +1,7 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class SystemMetrics
{
public string OsName { get; set; } = "";
public long Uptime { get; set; }
}

View file

@ -116,4 +116,12 @@ public static class Formatter
return (i / (1024D * 1024D)).Round(2) + " GB";
}
}
public static double BytesToGb(long bytes)
{
const double gbMultiplier = 1024 * 1024 * 1024; // 1 GB = 1024 MB * 1024 KB * 1024 B
double gigabytes = (double)bytes / gbMultiplier;
return gigabytes;
}
}

View file

@ -6,5 +6,5 @@ namespace Moonlight.App.Models.Misc;
public class RunningServer
{
public Server Server { get; set; }
public ContainerStats.Container Container { get; set; }
public Container Container { get; set; }
}

View file

@ -5,6 +5,7 @@ using Moonlight.App.ApiClients.Daemon.Resources;
using Moonlight.App.ApiClients.Wings;
using Moonlight.App.Database.Entities;
using Moonlight.App.Events;
using Moonlight.App.Helpers;
using Moonlight.App.Repositories;
using Moonlight.App.Repositories.Servers;
using Newtonsoft.Json;
@ -81,12 +82,12 @@ public class CleanupService
{
try
{
var cpuStats = await nodeService.GetCpuStats(node);
var memoryStats = await nodeService.GetMemoryStats(node);
var cpuMetrics = await nodeService.GetCpuMetrics(node);
var memoryMetrics = await nodeService.GetMemoryMetrics(node);
if (cpuStats.Usage > maxCpu || memoryStats.Free < minMemory)
if (cpuMetrics.CpuUsage > maxCpu || (Formatter.BytesToGb(memoryMetrics.Total) - (Formatter.BytesToGb(memoryMetrics.Used))) < minMemory)
{
var containerStats = await nodeService.GetContainerStats(node);
var dockerMetrics = await nodeService.GetDockerMetrics(node);
var serverRepository = scope.ServiceProvider.GetRequiredService<ServerRepository>();
var imageRepository = scope.ServiceProvider.GetRequiredService<ImageRepository>();
@ -101,9 +102,9 @@ public class CleanupService
)
.ToArray();
var containerMappedToServers = new Dictionary<ContainerStats.Container, Server>();
var containerMappedToServers = new Dictionary<Container, Server>();
foreach (var container in containerStats.Containers)
foreach (var container in dockerMetrics.Containers)
{
if (Guid.TryParse(container.Name, out Guid uuid))
{

View file

@ -1,4 +1,5 @@
using Moonlight.App.ApiClients.Daemon;
using Moonlight.App.ApiClients.Daemon.Requests;
using Moonlight.App.ApiClients.Daemon.Resources;
using Moonlight.App.ApiClients.Wings;
using Moonlight.App.ApiClients.Wings.Resources;
@ -24,35 +25,56 @@ public class NodeService
return await WingsApiHelper.Get<SystemStatus>(node, "api/system");
}
public async Task<CpuStats> GetCpuStats(Node node)
public async Task<CpuMetrics> GetCpuMetrics(Node node)
{
return await DaemonApiHelper.Get<CpuStats>(node, "stats/cpu");
return await DaemonApiHelper.Get<CpuMetrics>(node, "metrics/cpu");
}
public async Task<MemoryStats> GetMemoryStats(Node node)
public async Task<MemoryMetrics> GetMemoryMetrics(Node node)
{
return await DaemonApiHelper.Get<MemoryStats>(node, "stats/memory");
return await DaemonApiHelper.Get<MemoryMetrics>(node, "metrics/memory");
}
public async Task<DiskStats> GetDiskStats(Node node)
public async Task<DiskMetrics> GetDiskMetrics(Node node)
{
return await DaemonApiHelper.Get<DiskStats>(node, "stats/disk");
return await DaemonApiHelper.Get<DiskMetrics>(node, "metrics/disk");
}
public async Task<ContainerStats> GetContainerStats(Node node)
public async Task<SystemMetrics> GetSystemMetrics(Node node)
{
return await DaemonApiHelper.Get<ContainerStats>(node, "stats/container");
return await DaemonApiHelper.Get<SystemMetrics>(node, "metrics/system");
}
public async Task<DockerMetrics> GetDockerMetrics(Node node)
{
return await DaemonApiHelper.Get<DockerMetrics>(node, "metrics/docker");
}
public async Task Mount(Node node, string server, string serverPath, string path)
{
await DaemonApiHelper.Post(node, "mount", new Mount()
{
Server = server,
ServerPath = serverPath,
Path = path
});
}
public async Task Unmount(Node node, string path)
{
await DaemonApiHelper.Delete(node, "mount", new Unmount()
{
Path = path
});
}
public async Task<bool> IsHostUp(Node node)
{
try
{
//TODO: Implement status caching
var data = await GetStatus(node);
await GetSystemMetrics(node);
if (data != null)
return true;
return true;
}
catch (Exception)
{

View file

@ -9,21 +9,36 @@ public class SmartDeployService
private readonly Repository<CloudPanel> CloudPanelRepository;
private readonly WebSpaceService WebSpaceService;
private readonly NodeService NodeService;
private readonly ConfigService ConfigService;
public SmartDeployService(
NodeRepository nodeRepository,
NodeService nodeService,
WebSpaceService webSpaceService,
Repository<CloudPanel> cloudPanelRepository)
Repository<CloudPanel> cloudPanelRepository,
ConfigService configService)
{
NodeRepository = nodeRepository;
NodeService = nodeService;
WebSpaceService = webSpaceService;
CloudPanelRepository = cloudPanelRepository;
ConfigService = configService;
}
public async Task<Node?> GetNode()
{
var config = ConfigService
.GetSection("Moonlight")
.GetSection("SmartDeploy")
.GetSection("Server");
if (config.GetValue<bool>("EnableOverride"))
{
var nodeId = config.GetValue<int>("OverrideNode");
return NodeRepository.Get().FirstOrDefault(x => x.Id == nodeId);
}
var data = new Dictionary<Node, double>();
foreach (var node in NodeRepository.Get().ToArray())
@ -59,17 +74,17 @@ public class SmartDeployService
try
{
var cpuStats = await NodeService.GetCpuStats(node);
var memoryStats = await NodeService.GetMemoryStats(node);
var diskStats = await NodeService.GetDiskStats(node);
var cpuMetrics = await NodeService.GetCpuMetrics(node);
var memoryMetrics = await NodeService.GetMemoryMetrics(node);
var diskMetrics = await NodeService.GetDiskMetrics(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 cpuScore = (1 - cpuMetrics.CpuUsage) * cpuWeight; // CPU score is based on the inverse of CPU usage
var memoryScore = (1 - (memoryMetrics.Used / 1024)) * memoryWeight; // Memory score is based on the percentage of free memory
var diskSpaceScore = (double) (diskMetrics.Total - diskMetrics.Used) / 1000000000 * diskSpaceWeight; // Disk space score is based on the amount of free disk space in GB
var finalScore = cpuScore + memoryScore + diskSpaceScore;

View file

@ -72,7 +72,6 @@
<ItemGroup>
<Folder Include="App\ApiClients\CloudPanel\Resources\" />
<Folder Include="App\ApiClients\Daemon\Requests\" />
<Folder Include="App\Http\Middleware" />
<Folder Include="storage\backups\" />
</ItemGroup>

View file

@ -5,9 +5,11 @@
@using Moonlight.App.Repositories
@using Moonlight.App.Repositories.Servers
@using Logging.Net
@using Moonlight.App.ApiClients.Wings
@using Moonlight.App.Database.Entities
@inject ServerRepository ServerRepository
@inject ServerService ServerService
@inject SmartTranslateService TranslationService
<div class="col">
@ -28,7 +30,8 @@
OnClick="Save"
Text="@(TranslationService.Translate("Change"))"
WorkingText="@(TranslationService.Translate("Changing"))"
CssClasses="btn-primary"></WButton>
CssClasses="btn-primary">
</WButton>
</td>
</tr>
</table>
@ -55,9 +58,23 @@
private async Task Save()
{
CurrentServer.Variables.First(x => x.Key == "J2S").Value = Value ? "1" : "0";
ServerRepository.Update(CurrentServer);
var details = await ServerService.GetDetails(CurrentServer);
// For better user experience, we start the j2s server right away when the user enables j2s
if (details.State == "offline")
{
await ServerService.SetPowerState(CurrentServer, PowerSignal.Start);
}
// For better user experience, we kill the j2s server right away when the user disables j2s and the server is starting
if (details.State == "starting")
{
await ServerService.SetPowerState(CurrentServer, PowerSignal.Kill);
}
await Loader.Reload();
}
}

View file

@ -41,7 +41,7 @@ else
</div>
<div class="m-0">
<span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1">
@if (CpuStats == null)
@if (CpuMetrics == null)
{
<span class="text-muted">
<TL>Loading</TL>
@ -50,12 +50,12 @@ else
else
{
<span>
@(CpuStats.Usage)% <TL>of</TL> @(CpuStats.Cores) <TL>Cores used</TL>
@(CpuMetrics.CpuUsage)% <TL>of CPU used</TL>
</span>
}
</span>
<span class="fw-semibold fs-6">
@if (CpuStats == null)
@if (CpuMetrics == null)
{
<span class="text-muted">
<TL>Loading</TL>
@ -63,7 +63,7 @@ else
}
else
{
<span>@(CpuStats.Model)</span>
<span>@(CpuMetrics.CpuModel)</span>
}
</span>
</div>
@ -78,7 +78,7 @@ else
</div>
<div class="m-0">
<span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1">
@if (MemoryStats == null)
@if (MemoryMetrics == null)
{
<span class="text-muted">
<TL>Loading</TL>
@ -87,32 +87,12 @@ else
else
{
<span>
@(Formatter.FormatSize(MemoryStats.Used * 1024D * 1024D)) <TL>of</TL> @(Formatter.FormatSize(MemoryStats.Total * 1024D * 1024D)) <TL>used</TL>
@(Formatter.FormatSize(MemoryMetrics.Used)) <TL>of</TL> @(Formatter.FormatSize(MemoryMetrics.Total)) <TL>memory used</TL>
</span>
}
</span>
<span class="fw-semibold fs-6">
@if (MemoryStats == null)
{
<span class="text-muted">
<TL>Loading</TL>
</span>
}
else
{
if (MemoryStats.Sticks.Any())
{
foreach (var stick in SortMemorySticks(MemoryStats.Sticks))
{
<span>@(stick)</span>
<br/>
}
}
else
{
<span>No memory sticks detected</span>
}
}
@*IDK what to put here*@
</span>
</div>
</div>
@ -126,7 +106,7 @@ else
</div>
<div class="m-0">
<span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1">
@if (DiskStats == null)
@if (DiskMetrics == null)
{
<span class="text-muted">
<TL>Loading</TL>
@ -135,21 +115,12 @@ else
else
{
<span>
@(Formatter.FormatSize(DiskStats.TotalSize - DiskStats.FreeBytes)) <TL>of</TL> @(Formatter.FormatSize(DiskStats.TotalSize)) <TL>used</TL>
@(Formatter.FormatSize(DiskMetrics.Used)) <TL>of</TL> @(Formatter.FormatSize(DiskMetrics.Total)) <TL>used</TL>
</span>
}
</span>
<span class="fw-semibold fs-6">
@if (DiskStats == null)
{
<span class="text-muted">
<TL>Loading</TL>
</span>
}
else
{
<span>@(DiskStats.Name) - @(DiskStats.DriveFormat)</span>
}
@*IDK what to put here*@
</span>
</div>
</div>
@ -239,7 +210,7 @@ else
</div>
<div class="m-0">
<span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1">
@if (ContainerStats == null)
@if (DockerMetrics == null)
{
<span class="text-muted">
<TL>Loading</TL>
@ -248,12 +219,12 @@ else
else
{
<span>
<TL>@(ContainerStats.Containers.Count)</TL>
<TL>@(DockerMetrics.Containers.Length)</TL>
</span>
}
</span>
<span class="fw-semibold fs-6">
@if (ContainerStats == null)
@if (DockerMetrics == null)
{
<span class="text-muted">
<TL>Loading</TL>
@ -290,11 +261,11 @@ else
private Node? Node;
private CpuStats CpuStats;
private MemoryStats MemoryStats;
private DiskStats DiskStats;
private CpuMetrics CpuMetrics;
private MemoryMetrics MemoryMetrics;
private DiskMetrics DiskMetrics;
private DockerMetrics DockerMetrics;
private SystemStatus SystemStatus;
private ContainerStats ContainerStats;
private async Task Load(LazyLoader arg)
{
@ -311,16 +282,16 @@ else
SystemStatus = await NodeService.GetStatus(Node);
await InvokeAsync(StateHasChanged);
CpuStats = await NodeService.GetCpuStats(Node);
CpuMetrics = await NodeService.GetCpuMetrics(Node);
await InvokeAsync(StateHasChanged);
MemoryStats = await NodeService.GetMemoryStats(Node);
MemoryMetrics = await NodeService.GetMemoryMetrics(Node);
await InvokeAsync(StateHasChanged);
DiskStats = await NodeService.GetDiskStats(Node);
DiskMetrics = await NodeService.GetDiskMetrics(Node);
await InvokeAsync(StateHasChanged);
ContainerStats = await NodeService.GetContainerStats(Node);
DockerMetrics = await NodeService.GetDockerMetrics(Node);
await InvokeAsync(StateHasChanged);
}
catch (Exception e)
@ -330,28 +301,4 @@ else
});
}
}
private List<string> SortMemorySticks(List<MemoryStats.MemoryStick> sticks)
{
// Thank you ChatGPT <3
var groupedMemory = sticks.GroupBy(memory => new { memory.Type, memory.Size })
.Select(group => new
{
Type = group.Key.Type,
Size = group.Key.Size,
Count = group.Count()
});
var sortedMemory = groupedMemory.OrderBy(memory => memory.Type)
.ThenBy(memory => memory.Size);
List<string> sortedList = sortedMemory.Select(memory =>
{
string sizeString = $"{memory.Size}GB";
return $"{memory.Count}x {memory.Type} {sizeString}";
}).ToList();
return sortedList;
}
}

View file

@ -132,9 +132,9 @@
try
{
var containerStats = await NodeService.GetContainerStats(node);
var dockerMetrics = await NodeService.GetDockerMetrics(node);
foreach (var container in containerStats.Containers)
foreach (var container in dockerMetrics.Containers)
{
if (Guid.TryParse(container.Name, out Guid uuid))
{