Merge pull request #3 from Moonlight-Panel/DDosDetection

Ddos detection
This commit is contained in:
Marcel Baumgartner 2023-03-20 17:44:07 +01:00 committed by GitHub
commit 940dc6a75b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 2004 additions and 217 deletions

View file

@ -41,6 +41,7 @@ public class DataContext : DbContext
public DbSet<NotificationAction> NotificationActions { get; set; }
public DbSet<AaPanel> AaPanels { get; set; }
public DbSet<Website> Websites { get; set; }
public DbSet<DdosAttack> DdosAttacks { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{

View file

@ -0,0 +1,11 @@
namespace Moonlight.App.Database.Entities;
public class DdosAttack
{
public int Id { get; set; }
public bool Ongoing { get; set; }
public long Data { get; set; }
public string Ip { get; set; } = "";
public Node Node { get; set; } = null!;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,53 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class AddedDdosAttacks : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "DdosAttacks",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Ongoing = table.Column<bool>(type: "tinyint(1)", nullable: false),
Data = table.Column<long>(type: "bigint", nullable: false),
Ip = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
NodeId = table.Column<int>(type: "int", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_DdosAttacks", x => x.Id);
table.ForeignKey(
name: "FK_DdosAttacks_Nodes_NodeId",
column: x => x.NodeId,
principalTable: "Nodes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_DdosAttacks_NodeId",
table: "DdosAttacks",
column: "NodeId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DdosAttacks");
}
}
}

View file

@ -70,6 +70,35 @@ namespace Moonlight.App.Database.Migrations
b.ToTable("Databases");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.DdosAttack", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<long>("Data")
.HasColumnType("bigint");
b.Property<string>("Ip")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("NodeId")
.HasColumnType("int");
b.Property<bool>("Ongoing")
.HasColumnType("tinyint(1)");
b.HasKey("Id");
b.HasIndex("NodeId");
b.ToTable("DdosAttacks");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.DockerImage", b =>
{
b.Property<int>("Id")
@ -819,6 +848,17 @@ namespace Moonlight.App.Database.Migrations
b.Navigation("Owner");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.DdosAttack", b =>
{
b.HasOne("Moonlight.App.Database.Entities.Node", "Node")
.WithMany()
.HasForeignKey("NodeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Node");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.DockerImage", b =>
{
b.HasOne("Moonlight.App.Database.Entities.Image", null)

View file

@ -0,0 +1,53 @@
using Moonlight.App.Database.Entities;
using Moonlight.App.Exceptions;
using Newtonsoft.Json;
using RestSharp;
namespace Moonlight.App.Helpers;
public class DaemonApiHelper
{
private readonly RestClient Client;
public DaemonApiHelper()
{
Client = new();
}
private string GetApiUrl(Node node)
{
if(node.Ssl)
return $"https://{node.Fqdn}:{node.MoonlightDaemonPort}/";
else
return $"http://{node.Fqdn}:{node.MoonlightDaemonPort}/";
//return $"https://{node.Fqdn}:{node.HttpPort}/";
}
public async Task<T> Get<T>(Node node, string resource)
{
RestRequest request = new(GetApiUrl(node) + resource);
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Accept", "application/json");
request.AddHeader("Authorization", node.Token);
var response = await Client.GetAsync(request);
if (!response.IsSuccessful)
{
if (response.StatusCode != 0)
{
throw new WingsException(
$"An error occured: ({response.StatusCode}) {response.Content}",
(int)response.StatusCode
);
}
else
{
throw new Exception($"An internal error occured: {response.ErrorMessage}");
}
}
return JsonConvert.DeserializeObject<T>(response.Content!)!;
}
}

View file

@ -88,4 +88,25 @@ public static class Formatter
return $"{i2s(e.Day)}.{i2s(e.Month)}.{e.Year}";
}
public static string FormatSize(double bytes)
{
var i = Math.Abs(bytes) / 1024D;
if (i < 1)
{
return bytes + " B";
}
else if (i / 1024D < 1)
{
return i.Round(2) + " KB";
}
else if (i / (1024D * 1024D) < 1)
{
return (i / 1024D).Round(2) + " MB";
}
else
{
return (i / (1024D * 1024D)).Round(2) + " GB";
}
}
}

View file

@ -0,0 +1,54 @@
using Logging.Net;
using Microsoft.AspNetCore.Mvc;
using Moonlight.App.Database.Entities;
using Moonlight.App.Http.Requests.Daemon;
using Moonlight.App.Repositories;
using Moonlight.App.Services;
namespace Moonlight.App.Http.Controllers.Api.Remote;
[ApiController]
[Route("api/remote/ddos")]
public class DdosController : Controller
{
private readonly NodeRepository NodeRepository;
private readonly MessageService MessageService;
private readonly DdosAttackRepository DdosAttackRepository;
public DdosController(NodeRepository nodeRepository, MessageService messageService, DdosAttackRepository ddosAttackRepository)
{
NodeRepository = nodeRepository;
MessageService = messageService;
DdosAttackRepository = ddosAttackRepository;
}
[HttpPost("update")]
public async Task<ActionResult> Update([FromBody] DdosStatus ddosStatus)
{
var tokenData = Request.Headers.Authorization.ToString().Replace("Bearer ", "");
var id = tokenData.Split(".")[0];
var token = tokenData.Split(".")[1];
var node = NodeRepository.Get().FirstOrDefault(x => x.TokenId == id);
if (node == null)
return NotFound();
if (token != node.Token)
return Unauthorized();
var ddosAttack = new DdosAttack()
{
Ongoing = ddosStatus.Ongoing,
Data = ddosStatus.Data,
Ip = ddosStatus.Ip,
Node = node
};
ddosAttack = DdosAttackRepository.Add(ddosAttack);
await MessageService.Emit("node.ddos", ddosAttack);
return Ok();
}
}

View file

@ -0,0 +1,8 @@
namespace Moonlight.App.Http.Requests.Daemon;
public class DdosStatus
{
public bool Ongoing { get; set; }
public long Data { get; set; }
public string Ip { get; set; } = "";
}

View file

@ -0,0 +1,15 @@
namespace Moonlight.App.Models.Daemon.Resources;
public class ContainerStats
{
public List<Container> Containers { get; set; } = new();
public class Container
{
public Guid 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,8 @@
namespace Moonlight.App.Models.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,9 @@
namespace Moonlight.App.Models.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,15 @@
namespace Moonlight.App.Models.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

@ -1,6 +0,0 @@
namespace Moonlight.App.Models.Node;
public class CpuStats
{
public double CpuUsage { get; set; }
}

View file

@ -1,6 +0,0 @@
namespace Moonlight.App.Models.Node;
public class DiskStats
{
public long FreeBytes { get; set; }
}

View file

@ -1,17 +0,0 @@
namespace Moonlight.App.Models.Node;
public class DockerStats
{
public ContainerStats[] Containers { get; set; }
public int NodeId { get; set; }
}
public class ContainerStats
{
public Guid Name { get; set; }
public long Memory { get; set; }
public double Cpu { get; set; }
public long NetworkIn { get; set; }
public long NetworkOut { get; set; }
public int NodeId { get; set; }
}

View file

@ -1,8 +0,0 @@
namespace Moonlight.App.Models.Node;
public class MemoryStats
{
public long Free { get; set; }
public long Used { get; set; }
public long Total { get; set; }
}

View file

@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore;
using Moonlight.App.Database;
using Moonlight.App.Database.Entities;
namespace Moonlight.App.Repositories;
public class DdosAttackRepository
{
private readonly DataContext DataContext;
public DdosAttackRepository(DataContext dataContext)
{
DataContext = dataContext;
}
public DbSet<DdosAttack> Get()
{
return DataContext.DdosAttacks;
}
public DdosAttack Add(DdosAttack ddosAttack)
{
var x = DataContext.DdosAttacks.Add(ddosAttack);
DataContext.SaveChanges();
return x.Entity;
}
public void Update(DdosAttack ddosAttack)
{
DataContext.DdosAttacks.Update(ddosAttack);
DataContext.SaveChanges();
}
public void Delete(DdosAttack ddosAttack)
{
DataContext.DdosAttacks.Remove(ddosAttack);
DataContext.SaveChanges();
}
}

View file

@ -1,5 +1,6 @@
using Moonlight.App.Database.Entities;
using Moonlight.App.Helpers;
using Moonlight.App.Models.Daemon.Resources;
using Moonlight.App.Models.Wings.Resources;
using Moonlight.App.Repositories;
@ -7,17 +8,37 @@ namespace Moonlight.App.Services;
public class NodeService
{
private readonly NodeRepository NodeRepository;
private readonly WingsApiHelper WingsApiHelper;
private readonly DaemonApiHelper DaemonApiHelper;
public NodeService(NodeRepository nodeRepository, WingsApiHelper wingsApiHelper)
public NodeService(WingsApiHelper wingsApiHelper, DaemonApiHelper daemonApiHelper)
{
NodeRepository = nodeRepository;
WingsApiHelper = wingsApiHelper;
DaemonApiHelper = daemonApiHelper;
}
public async Task<SystemStatus> GetStatus(Node node)
{
return await WingsApiHelper.Get<SystemStatus>(node, "api/system");
}
public async Task<CpuStats> GetCpuStats(Node node)
{
return await DaemonApiHelper.Get<CpuStats>(node, "stats/cpu");
}
public async Task<MemoryStats> GetMemoryStats(Node node)
{
return await DaemonApiHelper.Get<MemoryStats>(node, "stats/memory");
}
public async Task<DiskStats> GetDiskStats(Node node)
{
return await DaemonApiHelper.Get<DiskStats>(node, "stats/disk");
}
public async Task<ContainerStats> GetContainerStats(Node node)
{
return await DaemonApiHelper.Get<ContainerStats>(node, "stats/container");
}
}

View file

@ -62,7 +62,7 @@
<ItemGroup>
<Folder Include="App\Http\Middleware" />
<Folder Include="App\Models\AuditLogData" />
<Folder Include="App\Models\Daemon\Requests" />
<Folder Include="App\Models\Google\Resources" />
<Folder Include="App\Services\DiscordBot\Modules" />
<Folder Include="resources\lang" />

View file

@ -64,6 +64,7 @@ namespace Moonlight
builder.Services.AddScoped<NotificationRepository>();
builder.Services.AddScoped<AaPanelRepository>();
builder.Services.AddScoped<WebsiteRepository>();
builder.Services.AddScoped<DdosAttackRepository>();
builder.Services.AddScoped<AuditLogEntryRepository>();
builder.Services.AddScoped<ErrorLogEntryRepository>();
@ -118,6 +119,7 @@ namespace Moonlight
builder.Services.AddScoped<WingsConsoleHelper>();
builder.Services.AddSingleton<PaperApiHelper>();
builder.Services.AddSingleton<HostSystemHelper>();
builder.Services.AddScoped<DaemonApiHelper>();
// Background services
builder.Services.AddSingleton<DiscordBotService>();

View file

@ -0,0 +1,22 @@
<div class="card mb-5 mb-xl-10">
<div class="card-body pt-0 pb-0">
<ul class="nav nav-stretch nav-line-tabs nav-line-tabs-2x border-transparent fs-5 fw-bold">
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 0 ? "active" : "")" href="/admin/nodes">
<TL>Nodes</TL>
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 1 ? "active" : "")" href="/admin/nodes/ddos">
<TL>DDos</TL>
</a>
</li>
</ul>
</div>
</div>
@code
{
[Parameter]
public int Index { get; set; } = 0;
}

View file

@ -3,12 +3,12 @@
<ul class="nav nav-stretch nav-line-tabs nav-line-tabs-2x border-transparent fs-5 fw-bold">
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 0 ? "active" : "")" href="/admin/users">
Users
<TL>Users</TL>
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 1 ? "active" : "")" href="/admin/users/sessions">
Sessions
<TL>Sessions</TL>
</a>
</li>
</ul>

View file

@ -0,0 +1,100 @@
@page "/admin/nodes/ddos"
@using Moonlight.Shared.Components.Navigations
@using Moonlight.App.Repositories
@using BlazorTable
@using Microsoft.EntityFrameworkCore
@using Moonlight.App.Database.Entities
@using Moonlight.App.Helpers
@using Moonlight.App.Services
@implements IDisposable
@inject DdosAttackRepository DdosAttackRepository
@inject SmartTranslateService SmartTranslateService
@inject MessageService MessageService
<OnlyAdmin>
<AdminNodesNavigation Index="1"/>
<LazyLoader @ref="LazyLoader" Load="Load">
<div class="card">
<div class="card-body pt-0">
<div class="table-responsive">
<Table TableItem="DdosAttack" Items="DdosAttacks" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
<Column TableItem="DdosAttack" Title="@(SmartTranslateService.Translate("Id"))" Field="@(x => x.Id)" Sortable="true" Filterable="true"/>
<Column TableItem="DdosAttack" Title="@(SmartTranslateService.Translate("Status"))" Field="@(x => x.Ongoing)" Sortable="true" Filterable="true">
<Template>
@if (context.Ongoing)
{
<TL>DDos attack started</TL>
}
else
{
<TL>DDos attack stopped</TL>
}
</Template>
</Column>
<Column TableItem="DdosAttack" Title="@(SmartTranslateService.Translate("Node"))" Field="@(x => x.Node)" Sortable="false" Filterable="false">
<Template>
<a href="/admin/nodes/view/@(context.Id)">
@(context.Node.Name)
</a>
</Template>
</Column>
<Column TableItem="DdosAttack" Title="Ip" Field="@(x => x.Ip)" Sortable="true" Filterable="true"/>
<Column TableItem="DdosAttack" Title="@(SmartTranslateService.Translate("Status"))" Field="@(x => x.Ongoing)" Sortable="true" Filterable="true">
<Template>
@if (context.Ongoing)
{
@(context.Data)
<TL> packets</TL>
}
else
{
@(context.Data)
<span> MB</span>
}
</Template>
</Column>
<Column TableItem="DdosAttack" Title="@(SmartTranslateService.Translate("Date"))" Field="@(x => x.Ongoing)" Sortable="true" Filterable="true">
<Template>
@(Formatter.FormatDate(context.CreatedAt))
</Template>
</Column>
<Pager ShowPageNumber="true" ShowTotalCount="true"/>
</Table>
</div>
</div>
</div>
</LazyLoader>
</OnlyAdmin>
@code
{
private DdosAttack[] DdosAttacks;
private LazyLoader LazyLoader;
protected override Task OnAfterRenderAsync(bool firstRender)
{
MessageService.Subscribe<Ddos, DdosAttack>("node.ddos", this, async attack => { Task.Run(async () => await LazyLoader.Reload()); });
return Task.CompletedTask;
}
private Task Load(LazyLoader arg)
{
DdosAttacks = DdosAttackRepository
.Get()
.Include(x => x.Node)
.OrderByDescending(x => x.CreatedAt)
.ToArray();
return Task.CompletedTask;
}
public void Dispose()
{
MessageService.Unsubscribe("node.ddos", this);
}
}

View file

@ -1,12 +1,12 @@
@page "/admin/nodes"
@using Moonlight.App.Repositories
@using Moonlight.App.Database.Entities
@using Moonlight.App.Helpers
@using Moonlight.App.Models.Node
@using Moonlight.App.Models.Wings.Resources
@using Moonlight.Shared.Components.Navigations
@using Moonlight.App.Services
@using Moonlight.App.Services.Interop
@using Logging.Net
@using BlazorTable
@inject NodeRepository NodeRepository
@inject AlertService AlertService
@ -14,170 +14,87 @@
@inject SmartTranslateService SmartTranslateService
<OnlyAdmin>
<div class="row mb-5">
<div class="card card-body">
<a class="btn btn-primary" href="/admin/nodes/new">
<TL>Add a new node</TL>
</a>
</div>
</div>
<div class="row">
<LazyLoader @ref="LazyLoader" Load="Load">
@if (!Nodes.Any())
{
<div class="card card-body">
<div class="alert alert-info">
<TL>No nodes found. Start with adding a new node</TL>
</div>
<AdminNodesNavigation Index="0"/>
<LazyLoader @ref="LazyLoader" Load="Load">
<div class="card">
<div class="card-header border-0 pt-5">
<h3 class="card-title align-items-start flex-column">
<span class="card-label fw-bold fs-3 mb-1">
<TL>Nodes</TL>
</span>
</h3>
<div class="card-toolbar">
<a href="/admin/nodes/new" class="btn btn-sm btn-light-success">
<i class="bx bx-layer-plus"></i>
<TL>New node</TL>
</a>
</div>
}
else
{
foreach (var node in Nodes)
</div>
<div class="card-body pt-0">
@if (Nodes.Any())
{
<div class="col-xl-6 mb-xl-10">
<div class="card card-flush h-xl-100">
<div class="card-header pt-5">
<h4 class="card-title d-flex align-items-start flex-column">
<span class="card-label fw-bold text-gray-800">@(node.Name) - (ID: @(node.Id))</span>
</h4>
</div>
<div class="card-body pt-6">
<div class="d-flex flex-stack">
<div class="d-flex align-items-center me-3">
<div class="flex-grow-1">
<span class="text-gray-800 fs-5 fw-bold lh-0">
<TL>Status</TL>
</span>
</div>
</div>
<div class="d-flex align-items-center w-100 mw-125px">
<span class="text-white-400 fw-semibold">
@{
var ss = StatusCache.ContainsKey(node) ? StatusCache[node] : null;
}
<div class="table-responsive">
<Table TableItem="Node" Items="Nodes" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
<Column TableItem="Node" Title="@(SmartTranslateService.Translate("Id"))" Field="@(x => x.Id)" Sortable="true" Filterable="true"/>
<Column TableItem="Node" Title="@(SmartTranslateService.Translate("Status"))" Field="@(x => x.Id)" Sortable="true" Filterable="true">
<Template>
@{
var ss = StatusCache.ContainsKey(context) ? StatusCache[context] : null;
}
@if (ss == null)
{
<span class="text-danger">Offline</span>
}
else
{
<span class="text-success">Online (@(ss.Version))</span>
}
</span>
</div>
</div>
<div class="separator separator-dashed my-3"></div>
<div class="d-flex flex-stack">
<div class="d-flex align-items-center me-3">
<div class="flex-grow-1">
<span class="text-gray-800 fs-5 fw-bold lh-0">
<TL>CPU Usage</TL>
</span>
<span class="text-gray-400 fw-semibold d-block fs-6">
<TL>In %</TL>
</span>
</div>
</div>
<div class="d-flex align-items-center w-100 mw-125px">
<span class="text-white-400 fw-semibold">
@{
var cpu = CpuCache.ContainsKey(node) ? CpuCache[node] : null;
}
@if (cpu == null)
{
<span>Loading</span>
}
else
{
<span>@(cpu.CpuUsage)%</span>
}
</span>
</div>
</div>
<div class="separator separator-dashed my-3"></div>
<div class="d-flex flex-stack">
<div class="d-flex align-items-center me-3">
<div class="flex-grow-1">
<span class="text-gray-800 fs-5 fw-bold lh-0">
<TL>Memory</TL>
</span>
<span class="text-gray-400 fw-semibold d-block fs-6">
<TL>Used / Available memory</TL>
</span>
</div>
</div>
<div class="d-flex align-items-center w-100 mw-125px">
<span class="text-white-400 fw-semibold">
@{
var memory = MemoryCache.ContainsKey(node) ? MemoryCache[node] : null;
}
@if (memory == null)
{
<span>Loading</span>
}
else
{
<span>@(Formatter.FormatSize(memory.Total - memory.Free)) / @(Formatter.FormatSize(memory.Total))</span>
}
</span>
</div>
</div>
<div class="separator separator-dashed my-3"></div>
<div class="d-flex flex-stack">
<div class="d-flex align-items-center me-3">
<div class="flex-grow-1">
<span class="text-gray-800 fs-5 fw-bold lh-0">
<TL>Storage</TL>
</span>
<span class="text-gray-400 fw-semibold d-block fs-6">
<TL>Available storage</TL>
</span>
</div>
</div>
<div class="d-flex align-items-center w-100 mw-125px">
<span class="text-white-400 fw-semibold">
@{
var disk = DiskCache.ContainsKey(node) ? DiskCache[node] : null;
}
@if (disk == null)
{
<span>Loading</span>
}
else
{
<span>@(Formatter.FormatSize(disk.FreeBytes))</span>
}
</span>
</div>
</div>
<div class="separator my-5"></div>
<div class="d-flex flex-stack">
<div class="align-items-start">
<a class="btn btn-primary" href="/admin/nodes/edit/@(node.Id)">
<TL>Edit</TL>
</a>
<a class="btn btn-success" href="/admin/nodes/setup/@(node.Id)">
<TL>Setup</TL>
</a>
<WButton Text="@(SmartTranslateService.Translate("Delete"))"
WorkingText="@(SmartTranslateService.Translate("Deleting"))"
CssClasses="btn-danger"
OnClick="() => Delete(node)">
</WButton>
</div>
</div>
</div>
</div>
@if (ss == null)
{
<span class="text-danger">Offline</span>
}
else
{
<span class="text-success">Online (@(ss.Version))</span>
}
</Template>
</Column>
<Column TableItem="Node" Title="@(SmartTranslateService.Translate("Name"))" Field="@(x => x.Name)" Sortable="true" Filterable="true">
<Template>
<a href="/admin/nodes/view/@(context.Id)">@(context.Name)</a>
</Template>
</Column>
<Column TableItem="Node" Title="@(SmartTranslateService.Translate("Fqdn"))" Field="@(x => x.Fqdn)" Sortable="true" Filterable="true"/>
<Column TableItem="Node" Title="" Field="@(x => x.Id)" Sortable="false" Filterable="false">
<Template>
<a href="/admin/nodes/edit/@(context.Id)">
@(SmartTranslateService.Translate("Edit"))
</a>
</Template>
</Column>
<Column TableItem="Node" Title="" Field="@(x => x.Id)" Sortable="false" Filterable="false">
<Template>
<a href="/admin/nodes/setup/@(context.Id)">
@(SmartTranslateService.Translate("Setup"))
</a>
</Template>
</Column>
<Column TableItem="Node" Title="" Field="@(x => x.Id)" Sortable="false" Filterable="false">
<Template>
<WButton Text="@(SmartTranslateService.Translate("Delete"))"
WorkingText="@(SmartTranslateService.Translate("Deleting"))"
CssClasses="btn-sm btn-danger"
OnClick="() => Delete(context)">
</WButton>
</Template>
</Column>
<Pager ShowPageNumber="true" ShowTotalCount="true"/>
</Table>
</div>
}
}
</LazyLoader>
</div>
else
{
<div class="alert alert-info">
<TL>No nodes found</TL>
</div>
}
</div>
</div>
</LazyLoader>
</OnlyAdmin>
@code
@ -186,17 +103,10 @@
private LazyLoader LazyLoader;
private Dictionary<Node, CpuStats> CpuCache = new();
private Dictionary<Node, MemoryStats> MemoryCache = new();
private Dictionary<Node, DiskStats> DiskCache = new();
private Dictionary<Node, SystemStatus?> StatusCache = new();
private Task Load(LazyLoader lazyLoader)
{
CpuCache.Clear();
MemoryCache.Clear();
DiskCache.Clear();
lock (StatusCache)
{
StatusCache.Clear();
@ -222,11 +132,6 @@
catch (Exception e)
{
Logger.Debug(e.Message);
lock (StatusCache)
{
StatusCache.Add(node, null);
}
}
await InvokeAsync(StateHasChanged);

View file

@ -0,0 +1,357 @@
@page "/admin/nodes/view/{id:int}"
@using Moonlight.App.Repositories
@using Moonlight.App.Database.Entities
@using Moonlight.App.Helpers
@using Moonlight.App.Models.Daemon.Resources
@using Moonlight.App.Models.Wings.Resources
@using Moonlight.App.Services
@inject NodeRepository NodeRepository
@inject NodeService NodeService
<OnlyAdmin>
<LazyLoader Load="Load">
@if (Node == null)
{
<div class="alert alert-warning">
<TL>No node with this id found</TL>
</div>
}
else
{
<div class="d-flex flex-center">
<div class="row">
<div class="card">
<div class="card-header">
<h3 class="card-title">
<span class="fw-bold fs-3">
@(Node.Name) <TL>details</TL>
</span>
</h3>
</div>
<div class="card-body">
<div class="row g-3 g-lg-6">
<div class="col">
<div class="bg-gray-100 bg-opacity-70 rounded-2 px-6 py-5">
<div class="symbol symbol-30px me-5 mb-8">
<span class="symbol-label">
<i class="text-primary bx bx-lg bx-chip"></i>
</span>
</div>
<div class="m-0">
<span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1">
@if (CpuStats == null)
{
<span class="text-muted">
<TL>Loading</TL>
</span>
}
else
{
<span>
@(CpuStats.Usage)% <TL>of</TL> @(CpuStats.Cores) <TL>Cores used</TL>
</span>
}
</span>
<span class="fw-semibold fs-6">
@if (CpuStats == null)
{
<span class="text-muted">
<TL>Loading</TL>
</span>
}
else
{
<span>@(CpuStats.Model)</span>
}
</span>
</div>
</div>
</div>
<div class="col">
<div class="bg-gray-100 bg-opacity-70 rounded-2 px-6 py-5">
<div class="symbol symbol-30px me-5 mb-8">
<span class="symbol-label">
<i class="text-primary bx bx-lg bx-microchip"></i>
</span>
</div>
<div class="m-0">
<span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1">
@if (MemoryStats == null)
{
<span class="text-muted">
<TL>Loading</TL>
</span>
}
else
{
<span>
@(Formatter.FormatSize(MemoryStats.Used * 1024D * 1024D)) <TL>of</TL> @(Formatter.FormatSize(MemoryStats.Total * 1024D * 1024D)) <TL>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>
}
}
</span>
</div>
</div>
</div>
<div class="col">
<div class="bg-gray-100 bg-opacity-70 rounded-2 px-6 py-5">
<div class="symbol symbol-30px me-5 mb-8">
<span class="symbol-label">
<i class="text-primary bx bx-lg bx-microchip"></i>
</span>
</div>
<div class="m-0">
<span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1">
@if (DiskStats == null)
{
<span class="text-muted">
<TL>Loading</TL>
</span>
}
else
{
<span>
@(Formatter.FormatSize(DiskStats.TotalSize - DiskStats.FreeBytes)) <TL>of</TL> @(Formatter.FormatSize(DiskStats.TotalSize)) <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>
}
</span>
</div>
</div>
</div>
</div>
<div class="mt-3 row g-3 g-lg-6">
<div class="col">
<div class="bg-gray-100 bg-opacity-70 rounded-2 px-6 py-5">
<div class="symbol symbol-30px me-5 mb-8">
<span class="symbol-label">
<i class="text-primary bx bx-lg bx-purchase-tag"></i>
</span>
</div>
<div class="m-0">
<span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1">
@if (SystemStatus == null)
{
<span class="text-muted">
<TL>Loading</TL>
</span>
}
else
{
<span class="text-success">
<TL>Online</TL>
</span>
}
</span>
<span class="fw-semibold fs-6">
@if (SystemStatus == null)
{
<span class="text-muted">
<TL>Loading</TL>
</span>
}
else
{
<span>@(SystemStatus.Version)</span>
}
</span>
</div>
</div>
</div>
<div class="col">
<div class="bg-gray-100 bg-opacity-70 rounded-2 px-6 py-5">
<div class="symbol symbol-30px me-5 mb-8">
<span class="symbol-label">
<i class="text-primary bx bx-lg bx-fingerprint"></i>
</span>
</div>
<div class="m-0">
<span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1">
@if (SystemStatus == null)
{
<span class="text-muted">
<TL>Loading</TL>
</span>
}
else
{
<span>
@(SystemStatus.KernelVersion) - @(SystemStatus.Architecture)
</span>
}
</span>
<span class="fw-semibold fs-6">
@if (SystemStatus == null)
{
<span class="text-muted">
<TL>Loading</TL>
</span>
}
else
{
<TL>Host system information</TL>
}
</span>
</div>
</div>
</div>
<div class="col">
<div class="bg-gray-100 bg-opacity-70 rounded-2 px-6 py-5">
<div class="symbol symbol-30px me-5 mb-8">
<span class="symbol-label">
<i class="text-primary bx bx-lg bxl-docker"></i>
</span>
</div>
<div class="m-0">
<span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1">
@if (ContainerStats == null)
{
<span class="text-muted">
<TL>Loading</TL>
</span>
}
else
{
<span>
<TL>@(ContainerStats.Containers.Count)</TL>
</span>
}
</span>
<span class="fw-semibold fs-6">
@if (ContainerStats == null)
{
<span class="text-muted">
<TL>Loading</TL>
</span>
}
else
{
<TL>Docker containers running</TL>
}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="mt-5 card card-body">
<div class="d-flex justify-content-end">
<a href="/admin/nodes" class="btn btn-primary">
<TL>Cancel</TL>
</a>
</div>
</div>
</div>
</div>
}
</LazyLoader>
</OnlyAdmin>
@code
{
[Parameter]
public int Id { get; set; }
private Node? Node;
private CpuStats CpuStats;
private MemoryStats MemoryStats;
private DiskStats DiskStats;
private SystemStatus SystemStatus;
private ContainerStats ContainerStats;
private async Task Load(LazyLoader arg)
{
Node = NodeRepository
.Get()
.FirstOrDefault(x => x.Id == Id);
if (Node != null)
{
Task.Run(async () =>
{
try
{
SystemStatus = await NodeService.GetStatus(Node);
await InvokeAsync(StateHasChanged);
CpuStats = await NodeService.GetCpuStats(Node);
await InvokeAsync(StateHasChanged);
MemoryStats = await NodeService.GetMemoryStats(Node);
await InvokeAsync(StateHasChanged);
DiskStats = await NodeService.GetDiskStats(Node);
await InvokeAsync(StateHasChanged);
ContainerStats = await NodeService.GetContainerStats(Node);
await InvokeAsync(StateHasChanged);
}
catch (Exception e)
{
// ignored
}
});
}
}
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

@ -29,7 +29,11 @@
<div class="table-responsive">
<Table TableItem="Server" Items="Servers" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
<Column TableItem="Server" Title="@(SmartTranslateService.Translate("Id"))" Field="@(x => x.Id)" Sortable="true" Filterable="true"/>
<Column TableItem="Server" Title="@(SmartTranslateService.Translate("Name"))" Field="@(x => x.Name)" Sortable="true" Filterable="true"/>
<Column TableItem="Server" Title="@(SmartTranslateService.Translate("Name"))" Field="@(x => x.Name)" Sortable="true" Filterable="true">
<Template>
<a href="/server/@(context.Uuid)">@(context.Name)</a>
</Template>
</Column>
<Column TableItem="Server" Title="@(SmartTranslateService.Translate("Cores"))" Field="@(x => x.Cpu)" Sortable="true" Filterable="true"/>
<Column TableItem="Server" Title="@(SmartTranslateService.Translate("Memory"))" Field="@(x => x.Memory)" Sortable="true" Filterable="true"/>
<Column TableItem="Server" Title="@(SmartTranslateService.Translate("Disk"))" Field="@(x => x.Disk)" Sortable="true" Filterable="true"/>

View file

@ -354,3 +354,23 @@ Allocations;Allocations
No variables found;No variables found
Successfully added image;Successfully added image
Password change for;Password change for
of;of
New node;New node
Fqdn;Fqdn
Cores used;Cores used
used;used
5.15.90.1-microsoft-standard-WSL2 - amd64;5.15.90.1-microsoft-standard-WSL2 - amd64
Host system information;Host system information
0;0
Docker containers running;Docker containers running
details;details
1;1
2;2
DDos;DDos
No ddos attacks found;No ddos attacks found
Node;Node
Date;Date
DDos attack started;DDos attack started
packets;packets
DDos attack stopped;DDos attack stopped
packets; packets