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>
|
||||
<PackageReference Include="Grpc.AspNetCore" Version="2.76.0" />
|
||||
<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>
|
||||
|
||||
</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);
|
||||
|
||||
// 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.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();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseCors();
|
||||
app.MapGrpcService<AgentGrpcService>();
|
||||
app.MapControllers();
|
||||
|
||||
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": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
|
||||
Reference in New Issue
Block a user