feat: implement gRPC AgentService, Program.cs with Kestrel HTTP/2 config
This commit is contained in:
43
Backend/src/NexusRMM.Api/Controllers/AgentsController.cs
Normal file
43
Backend/src/NexusRMM.Api/Controllers/AgentsController.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using NexusRMM.Infrastructure.Data;
|
||||||
|
|
||||||
|
namespace NexusRMM.Api.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/v1/[controller]")]
|
||||||
|
public class AgentsController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly RmmDbContext _db;
|
||||||
|
public AgentsController(RmmDbContext db) => _db = db;
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetAll()
|
||||||
|
{
|
||||||
|
var agents = await _db.Agents
|
||||||
|
.OrderBy(a => a.Hostname)
|
||||||
|
.Select(a => new { a.Id, a.Hostname, a.OsType, a.OsVersion, a.IpAddress, a.Status, a.AgentVersion, a.LastSeen, a.Tags })
|
||||||
|
.ToListAsync();
|
||||||
|
return Ok(agents);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id:guid}")]
|
||||||
|
public async Task<IActionResult> GetById(Guid id)
|
||||||
|
{
|
||||||
|
var agent = await _db.Agents.FindAsync(id);
|
||||||
|
return agent is null ? NotFound() : Ok(agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id:guid}/metrics")]
|
||||||
|
public async Task<IActionResult> GetMetrics(Guid id, [FromQuery] int hours = 24)
|
||||||
|
{
|
||||||
|
var since = DateTime.UtcNow.AddHours(-hours);
|
||||||
|
var metrics = await _db.AgentMetrics
|
||||||
|
.Where(m => m.AgentId == id && m.Timestamp >= since)
|
||||||
|
.OrderByDescending(m => m.Timestamp)
|
||||||
|
.Take(1000)
|
||||||
|
.Select(m => new { m.Timestamp, m.Metrics })
|
||||||
|
.ToListAsync();
|
||||||
|
return Ok(metrics);
|
||||||
|
}
|
||||||
|
}
|
||||||
51
Backend/src/NexusRMM.Api/Controllers/TasksController.cs
Normal file
51
Backend/src/NexusRMM.Api/Controllers/TasksController.cs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using NexusRMM.Core.Models;
|
||||||
|
using NexusRMM.Infrastructure.Data;
|
||||||
|
|
||||||
|
namespace NexusRMM.Api.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/v1/[controller]")]
|
||||||
|
public class TasksController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly RmmDbContext _db;
|
||||||
|
public TasksController(RmmDbContext db) => _db = db;
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> Create([FromBody] CreateTaskRequest request)
|
||||||
|
{
|
||||||
|
var task = new TaskItem
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
AgentId = request.AgentId,
|
||||||
|
Type = request.Type,
|
||||||
|
Payload = request.Payload is not null ? JsonSerializer.SerializeToElement(request.Payload) : null,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
_db.Tasks.Add(task);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
return CreatedAtAction(nameof(GetById), new { id = task.Id }, task);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id:guid}")]
|
||||||
|
public async Task<IActionResult> GetById(Guid id)
|
||||||
|
{
|
||||||
|
var task = await _db.Tasks.FindAsync(id);
|
||||||
|
return task is null ? NotFound() : Ok(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("agent/{agentId:guid}")]
|
||||||
|
public async Task<IActionResult> GetByAgent(Guid agentId)
|
||||||
|
{
|
||||||
|
var tasks = await _db.Tasks
|
||||||
|
.Where(t => t.AgentId == agentId)
|
||||||
|
.OrderByDescending(t => t.CreatedAt)
|
||||||
|
.Take(50)
|
||||||
|
.ToListAsync();
|
||||||
|
return Ok(tasks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record CreateTaskRequest(Guid AgentId, TaskType Type, object? Payload);
|
||||||
64
Backend/src/NexusRMM.Api/Controllers/TicketsController.cs
Normal file
64
Backend/src/NexusRMM.Api/Controllers/TicketsController.cs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using NexusRMM.Core.Models;
|
||||||
|
using NexusRMM.Infrastructure.Data;
|
||||||
|
|
||||||
|
namespace NexusRMM.Api.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/v1/[controller]")]
|
||||||
|
public class TicketsController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly RmmDbContext _db;
|
||||||
|
public TicketsController(RmmDbContext db) => _db = db;
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetAll([FromQuery] TicketStatus? status = null)
|
||||||
|
{
|
||||||
|
var query = _db.Tickets.AsQueryable();
|
||||||
|
if (status.HasValue) query = query.Where(t => t.Status == status.Value);
|
||||||
|
var tickets = await query.OrderByDescending(t => t.CreatedAt).Take(100).ToListAsync();
|
||||||
|
return Ok(tickets);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id:int}")]
|
||||||
|
public async Task<IActionResult> GetById(int id)
|
||||||
|
{
|
||||||
|
var ticket = await _db.Tickets.Include(t => t.Agent).FirstOrDefaultAsync(t => t.Id == id);
|
||||||
|
return ticket is null ? NotFound() : Ok(ticket);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> Create([FromBody] CreateTicketRequest request)
|
||||||
|
{
|
||||||
|
var ticket = new Ticket
|
||||||
|
{
|
||||||
|
Title = request.Title,
|
||||||
|
Description = request.Description,
|
||||||
|
Priority = request.Priority,
|
||||||
|
AgentId = request.AgentId,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
UpdatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
_db.Tickets.Add(ticket);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
return CreatedAtAction(nameof(GetById), new { id = ticket.Id }, ticket);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id:int}")]
|
||||||
|
public async Task<IActionResult> Update(int id, [FromBody] UpdateTicketRequest request)
|
||||||
|
{
|
||||||
|
var ticket = await _db.Tickets.FindAsync(id);
|
||||||
|
if (ticket is null) return NotFound();
|
||||||
|
if (request.Status.HasValue) ticket.Status = request.Status.Value;
|
||||||
|
if (request.Priority.HasValue) ticket.Priority = request.Priority.Value;
|
||||||
|
if (request.Title is not null) ticket.Title = request.Title;
|
||||||
|
if (request.Description is not null) ticket.Description = request.Description;
|
||||||
|
ticket.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
return Ok(ticket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record CreateTicketRequest(string Title, string Description, TicketPriority Priority, Guid? AgentId);
|
||||||
|
public record UpdateTicketRequest(string? Title, string? Description, TicketStatus? Status, TicketPriority? Priority);
|
||||||
117
Backend/src/NexusRMM.Api/GrpcServices/AgentGrpcService.cs
Normal file
117
Backend/src/NexusRMM.Api/GrpcServices/AgentGrpcService.cs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Grpc.Core;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using NexusRMM.Core.Models;
|
||||||
|
using NexusRMM.Infrastructure.Data;
|
||||||
|
using NexusRMM.Protos;
|
||||||
|
using AgentModel = NexusRMM.Core.Models.Agent;
|
||||||
|
using CoreTaskStatus = NexusRMM.Core.Models.TaskStatus;
|
||||||
|
|
||||||
|
namespace NexusRMM.Api.GrpcServices;
|
||||||
|
|
||||||
|
public class AgentGrpcService : AgentService.AgentServiceBase
|
||||||
|
{
|
||||||
|
private readonly RmmDbContext _db;
|
||||||
|
private readonly ILogger<AgentGrpcService> _logger;
|
||||||
|
|
||||||
|
public AgentGrpcService(RmmDbContext db, ILogger<AgentGrpcService> logger)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<EnrollResponse> Enroll(EnrollRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var agent = new AgentModel
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Hostname = request.Hostname,
|
||||||
|
OsType = request.OsType.ToLower() == "windows" ? OsType.Windows : OsType.Linux,
|
||||||
|
OsVersion = request.OsVersion,
|
||||||
|
IpAddress = request.IpAddress,
|
||||||
|
MacAddress = request.MacAddress,
|
||||||
|
AgentVersion = request.AgentVersion,
|
||||||
|
Status = AgentStatus.Online,
|
||||||
|
LastSeen = DateTime.UtcNow,
|
||||||
|
EnrolledAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
_db.Agents.Add(agent);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
_logger.LogInformation("Agent enrolled: {AgentId} ({Hostname})", agent.Id, agent.Hostname);
|
||||||
|
|
||||||
|
return new EnrollResponse
|
||||||
|
{
|
||||||
|
AgentId = agent.Id.ToString(),
|
||||||
|
HeartbeatInterval = 60
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<HeartbeatResponse> Heartbeat(HeartbeatRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var agentId = Guid.Parse(request.AgentId);
|
||||||
|
var agent = await _db.Agents.FindAsync(agentId)
|
||||||
|
?? throw new RpcException(new Status(StatusCode.NotFound, "Agent not found"));
|
||||||
|
|
||||||
|
agent.LastSeen = DateTime.UtcNow;
|
||||||
|
agent.Status = AgentStatus.Online;
|
||||||
|
|
||||||
|
_db.AgentMetrics.Add(new AgentMetric
|
||||||
|
{
|
||||||
|
AgentId = agentId,
|
||||||
|
Timestamp = DateTime.UtcNow,
|
||||||
|
Metrics = JsonSerializer.SerializeToElement(request.Metrics)
|
||||||
|
});
|
||||||
|
|
||||||
|
var pendingTasks = await _db.Tasks
|
||||||
|
.Where(t => t.AgentId == agentId && t.Status == CoreTaskStatus.Pending)
|
||||||
|
.OrderBy(t => t.CreatedAt)
|
||||||
|
.Take(10)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var response = new HeartbeatResponse();
|
||||||
|
foreach (var task in pendingTasks)
|
||||||
|
{
|
||||||
|
task.Status = CoreTaskStatus.InProgress;
|
||||||
|
response.PendingCommands.Add(new AgentCommand
|
||||||
|
{
|
||||||
|
CommandId = task.Id.ToString(),
|
||||||
|
Type = MapTaskType(task.Type),
|
||||||
|
Payload = task.Payload?.GetRawText() ?? "{}"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<CommandResultResponse> ReportCommandResult(CommandResult request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var taskId = Guid.Parse(request.CommandId);
|
||||||
|
var task = await _db.Tasks.FindAsync(taskId)
|
||||||
|
?? throw new RpcException(new Status(StatusCode.NotFound, "Task not found"));
|
||||||
|
|
||||||
|
task.Status = request.Success ? CoreTaskStatus.Completed : CoreTaskStatus.Failed;
|
||||||
|
task.CompletedAt = DateTime.UtcNow;
|
||||||
|
task.Result = JsonSerializer.SerializeToElement(new
|
||||||
|
{
|
||||||
|
request.ExitCode,
|
||||||
|
request.Stdout,
|
||||||
|
request.Stderr,
|
||||||
|
request.Success
|
||||||
|
});
|
||||||
|
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
return new CommandResultResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CommandType MapTaskType(NexusRMM.Core.Models.TaskType type) => type switch
|
||||||
|
{
|
||||||
|
NexusRMM.Core.Models.TaskType.Shell => CommandType.Shell,
|
||||||
|
NexusRMM.Core.Models.TaskType.InstallSoftware => CommandType.InstallSoftware,
|
||||||
|
NexusRMM.Core.Models.TaskType.UninstallSoftware => CommandType.UninstallSoftware,
|
||||||
|
NexusRMM.Core.Models.TaskType.UpdateAgent => CommandType.UpdateAgent,
|
||||||
|
NexusRMM.Core.Models.TaskType.NetworkScan => CommandType.NetworkScan,
|
||||||
|
_ => CommandType.Unspecified
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -17,6 +17,9 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Grpc.AspNetCore" Version="2.76.0" />
|
<PackageReference Include="Grpc.AspNetCore" Version="2.76.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.9" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.9" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.5" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.5" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,20 +1,47 @@
|
|||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using NexusRMM.Api.GrpcServices;
|
||||||
|
using NexusRMM.Infrastructure.Data;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// Add services to the container.
|
// Kestrel: Port 5000 für REST+SignalR (HTTP/1.1+2), Port 5001 für gRPC (HTTP/2)
|
||||||
|
builder.WebHost.ConfigureKestrel(options =>
|
||||||
|
{
|
||||||
|
options.ListenAnyIP(5000, o => o.Protocols = HttpProtocols.Http1AndHttp2);
|
||||||
|
options.ListenAnyIP(5001, o => o.Protocols = HttpProtocols.Http2);
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddDbContext<RmmDbContext>(options =>
|
||||||
|
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
|
||||||
|
|
||||||
|
builder.Services.AddGrpc();
|
||||||
|
builder.Services.AddSignalR();
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
builder.Services.AddSwaggerGen();
|
||||||
|
|
||||||
|
builder.Services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddDefaultPolicy(policy =>
|
||||||
|
{
|
||||||
|
policy.WithOrigins(builder.Configuration.GetSection("Cors:Origins").Get<string[]>() ?? ["http://localhost:5173"])
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowAnyMethod()
|
||||||
|
.AllowCredentials();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
app.UseCors();
|
||||||
|
app.MapGrpcService<AgentGrpcService>();
|
||||||
app.UseAuthorization();
|
|
||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
{
|
{
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"DefaultConnection": "Host=localhost;Port=5432;Database=nexusrmm;Username=nexusrmm;Password=nexusrmm_dev"
|
||||||
|
},
|
||||||
|
"Cors": {
|
||||||
|
"Origins": ["http://localhost:5173"]
|
||||||
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
|
|||||||
Reference in New Issue
Block a user