feat: implement Phase 6 — Software Deployment

Backend:
- SoftwarePackage model (Name, Version, OsType, PackageManager, PackageName, InstallerUrl, Checksum, SilentArgs)
- RmmDbContext: SoftwarePackages DbSet + unique index on (Name, Version, OsType)
- SoftwarePackagesController: full CRUD with OsType filter
- DeployController: POST /api/v1/deploy creates InstallSoftware/UninstallSoftware TaskItem
- EF Migration: AddSoftwarePackages (20260319130448)

Go Agent:
- internal/deployer/deployer.go: Install() and Uninstall() with:
  - Chocolatey (Windows), apt/dnf (Linux), auto-detect
  - Direct installer fallback: HTTP download + SHA256 verify + silent install
  - Supports .msi, .exe (Windows) and .deb, .rpm (Linux)
- main.go: COMMAND_TYPE_INSTALL_SOFTWARE and COMMAND_TYPE_UNINSTALL_SOFTWARE routed to deployer

Frontend:
- SoftwarePage: Katalog tab (CRUD, OS filter, smart package manager select) + Deploy tab
- api/types.ts: SoftwarePackage, PackageManager, DeployRequest/Response types
- api/client.ts: softwarePackagesApi and deployApi
- App.tsx: Software nav item with Package icon

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-03-19 14:06:40 +01:00
parent eb114f68e2
commit 84629dfbcf
13 changed files with 1491 additions and 2 deletions

View File

@@ -13,6 +13,7 @@ public class RmmDbContext : DbContext
public DbSet<Ticket> Tickets => Set<Ticket>();
public DbSet<AlertRule> AlertRules => Set<AlertRule>();
public DbSet<Alert> Alerts => Set<Alert>();
public DbSet<SoftwarePackage> SoftwarePackages => Set<SoftwarePackage>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@@ -58,5 +59,11 @@ public class RmmDbContext : DbContext
e.HasOne(a => a.Rule).WithMany(r => r.Alerts).HasForeignKey(a => a.RuleId);
e.HasOne(a => a.Agent).WithMany(a => a.Alerts).HasForeignKey(a => a.AgentId);
});
modelBuilder.Entity<SoftwarePackage>(e =>
{
e.HasKey(p => p.Id);
e.HasIndex(p => new { p.Name, p.Version, p.OsType }).IsUnique();
});
}
}

View File

@@ -0,0 +1,368 @@
// <auto-generated />
using System;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using NexusRMM.Infrastructure.Data;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace NexusRMM.Infrastructure.Migrations
{
[DbContext(typeof(RmmDbContext))]
[Migration("20260319130448_AddSoftwarePackages")]
partial class AddSoftwarePackages
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("NexusRMM.Core.Models.Agent", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AgentVersion")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("EnrolledAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Hostname")
.IsRequired()
.HasColumnType("text");
b.Property<string>("IpAddress")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("LastSeen")
.HasColumnType("timestamp with time zone");
b.Property<string>("MacAddress")
.IsRequired()
.HasColumnType("text");
b.Property<string>("MeshAgentId")
.HasColumnType("text");
b.Property<int>("OsType")
.HasColumnType("integer");
b.Property<string>("OsVersion")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Status")
.HasColumnType("integer");
b.PrimitiveCollection<string>("Tags")
.IsRequired()
.HasColumnType("jsonb");
b.HasKey("Id");
b.HasIndex("Hostname");
b.HasIndex("MacAddress");
b.ToTable("Agents");
});
modelBuilder.Entity("NexusRMM.Core.Models.AgentMetric", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<Guid>("AgentId")
.HasColumnType("uuid");
b.Property<JsonElement>("Metrics")
.HasColumnType("jsonb");
b.Property<DateTime>("Timestamp")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("AgentId");
b.HasIndex("Timestamp");
b.ToTable("AgentMetrics");
});
modelBuilder.Entity("NexusRMM.Core.Models.Alert", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<bool>("Acknowledged")
.HasColumnType("boolean");
b.Property<Guid>("AgentId")
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("text");
b.Property<int>("RuleId")
.HasColumnType("integer");
b.Property<int>("Severity")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("AgentId");
b.HasIndex("CreatedAt");
b.HasIndex("RuleId");
b.ToTable("Alerts");
});
modelBuilder.Entity("NexusRMM.Core.Models.AlertRule", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<string>("MetricPath")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Operator")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Severity")
.HasColumnType("integer");
b.Property<double>("Threshold")
.HasColumnType("double precision");
b.HasKey("Id");
b.ToTable("AlertRules");
});
modelBuilder.Entity("NexusRMM.Core.Models.SoftwarePackage", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Checksum")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("InstallerUrl")
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<int>("OsType")
.HasColumnType("integer");
b.Property<string>("PackageManager")
.IsRequired()
.HasColumnType("text");
b.Property<string>("PackageName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SilentArgs")
.HasColumnType("text");
b.Property<string>("Version")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Name", "Version", "OsType")
.IsUnique();
b.ToTable("SoftwarePackages");
});
modelBuilder.Entity("NexusRMM.Core.Models.TaskItem", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("AgentId")
.HasColumnType("uuid");
b.Property<DateTime?>("CompletedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<JsonElement?>("Payload")
.HasColumnType("jsonb");
b.Property<JsonElement?>("Result")
.HasColumnType("jsonb");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<int>("Type")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("AgentId");
b.ToTable("Tasks");
});
modelBuilder.Entity("NexusRMM.Core.Models.Ticket", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<Guid?>("AgentId")
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Priority")
.HasColumnType("integer");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("AgentId");
b.ToTable("Tickets");
});
modelBuilder.Entity("NexusRMM.Core.Models.AgentMetric", b =>
{
b.HasOne("NexusRMM.Core.Models.Agent", "Agent")
.WithMany("Metrics")
.HasForeignKey("AgentId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Agent");
});
modelBuilder.Entity("NexusRMM.Core.Models.Alert", b =>
{
b.HasOne("NexusRMM.Core.Models.Agent", "Agent")
.WithMany("Alerts")
.HasForeignKey("AgentId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("NexusRMM.Core.Models.AlertRule", "Rule")
.WithMany("Alerts")
.HasForeignKey("RuleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Agent");
b.Navigation("Rule");
});
modelBuilder.Entity("NexusRMM.Core.Models.TaskItem", b =>
{
b.HasOne("NexusRMM.Core.Models.Agent", "Agent")
.WithMany("Tasks")
.HasForeignKey("AgentId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Agent");
});
modelBuilder.Entity("NexusRMM.Core.Models.Ticket", b =>
{
b.HasOne("NexusRMM.Core.Models.Agent", "Agent")
.WithMany("Tickets")
.HasForeignKey("AgentId");
b.Navigation("Agent");
});
modelBuilder.Entity("NexusRMM.Core.Models.Agent", b =>
{
b.Navigation("Alerts");
b.Navigation("Metrics");
b.Navigation("Tasks");
b.Navigation("Tickets");
});
modelBuilder.Entity("NexusRMM.Core.Models.AlertRule", b =>
{
b.Navigation("Alerts");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,50 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace NexusRMM.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddSoftwarePackages : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "SoftwarePackages",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(type: "text", nullable: false),
Version = table.Column<string>(type: "text", nullable: false),
OsType = table.Column<int>(type: "integer", nullable: false),
PackageManager = table.Column<string>(type: "text", nullable: false),
PackageName = table.Column<string>(type: "text", nullable: false),
InstallerUrl = table.Column<string>(type: "text", nullable: true),
Checksum = table.Column<string>(type: "text", nullable: true),
SilentArgs = table.Column<string>(type: "text", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SoftwarePackages", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_SoftwarePackages_Name_Version_OsType",
table: "SoftwarePackages",
columns: new[] { "Name", "Version", "OsType" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "SoftwarePackages");
}
}
}

View File

@@ -175,6 +175,53 @@ namespace NexusRMM.Infrastructure.Migrations
b.ToTable("AlertRules");
});
modelBuilder.Entity("NexusRMM.Core.Models.SoftwarePackage", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Checksum")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("InstallerUrl")
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<int>("OsType")
.HasColumnType("integer");
b.Property<string>("PackageManager")
.IsRequired()
.HasColumnType("text");
b.Property<string>("PackageName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SilentArgs")
.HasColumnType("text");
b.Property<string>("Version")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Name", "Version", "OsType")
.IsUnique();
b.ToTable("SoftwarePackages");
});
modelBuilder.Entity("NexusRMM.Core.Models.TaskItem", b =>
{
b.Property<Guid>("Id")