package deployer import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "runtime" "strings" "nexusrmm.local/agent/internal/executor" ) // Payload wird vom Backend als JSON im cmd.Payload-Feld gesendet. type Payload struct { PackageName string `json:"packageName"` PackageManager string `json:"packageManager"` // "choco", "apt", "dnf", "direct" InstallerUrl string `json:"installerUrl"` Checksum string `json:"checksum"` // SHA256 (hex, optional) SilentArgs string `json:"silentArgs"` DisplayName string `json:"displayName"` Version string `json:"version"` } // Install installiert ein Software-Paket auf dem aktuellen System. func Install(ctx context.Context, payloadJSON string) *executor.Result { payload, err := parsePayload(payloadJSON) if err != nil { return errorResult(fmt.Sprintf("Ungültiges Payload-JSON: %v", err)) } switch payload.PackageManager { case "choco": return installWithChoco(ctx, payload.PackageName) case "apt": return installWithApt(ctx, payload.PackageName) case "dnf": return installWithDnf(ctx, payload.PackageName) case "direct": return installDirect(ctx, *payload) default: // Auto-detect return installAutoDetect(ctx, *payload) } } // Uninstall deinstalliert ein Software-Paket. func Uninstall(ctx context.Context, payloadJSON string) *executor.Result { payload, err := parsePayload(payloadJSON) if err != nil { return errorResult(fmt.Sprintf("Ungültiges Payload-JSON: %v", err)) } switch payload.PackageManager { case "choco": return executor.Execute(ctx, fmt.Sprintf("choco uninstall %s -y", payload.PackageName), 600) case "apt": return executor.Execute(ctx, fmt.Sprintf("apt-get remove -y %s", payload.PackageName), 600) case "dnf": return executor.Execute(ctx, fmt.Sprintf("dnf remove -y %s", payload.PackageName), 600) default: return installAutoDetect(ctx, *payload) } } func installWithChoco(ctx context.Context, packageName string) *executor.Result { return executor.Execute(ctx, fmt.Sprintf("choco install %s -y --no-progress", packageName), 600) } func installWithApt(ctx context.Context, packageName string) *executor.Result { return executor.Execute(ctx, fmt.Sprintf("DEBIAN_FRONTEND=noninteractive apt-get install -y %s", packageName), 600) } func installWithDnf(ctx context.Context, packageName string) *executor.Result { return executor.Execute(ctx, fmt.Sprintf("dnf install -y %s", packageName), 600) } func installAutoDetect(ctx context.Context, payload Payload) *executor.Result { if runtime.GOOS == "windows" { // Versuche choco, dann winget als Fallback if isCommandAvailable("choco") { return installWithChoco(ctx, payload.PackageName) } if payload.InstallerUrl != "" { return installDirect(ctx, payload) } return errorResult("Kein unterstützter Paketmanager auf Windows verfügbar (choco nicht gefunden)") } // Linux if isCommandAvailable("apt-get") { return installWithApt(ctx, payload.PackageName) } if isCommandAvailable("dnf") { return installWithDnf(ctx, payload.PackageName) } if payload.InstallerUrl != "" { return installDirect(ctx, payload) } return errorResult("Kein unterstützter Paketmanager auf Linux verfügbar") } func installDirect(ctx context.Context, payload Payload) *executor.Result { if payload.InstallerUrl == "" { return errorResult("Direct-Install: Keine InstallerUrl angegeben") } // Temp-Datei herunterladen tmpFile, err := os.CreateTemp("", "nexusrmm-install-*") if err != nil { return errorResult(fmt.Sprintf("Temp-Datei konnte nicht erstellt werden: %v", err)) } tmpPath := tmpFile.Name() defer os.Remove(tmpPath) resp, err := http.Get(payload.InstallerUrl) if err != nil { tmpFile.Close() return errorResult(fmt.Sprintf("Download fehlgeschlagen: %v", err)) } defer resp.Body.Close() hasher := sha256.New() writer := io.MultiWriter(tmpFile, hasher) if _, err := io.Copy(writer, resp.Body); err != nil { tmpFile.Close() return errorResult(fmt.Sprintf("Download-Fehler beim Schreiben: %v", err)) } tmpFile.Close() // Checksum prüfen if payload.Checksum != "" { actualHash := hex.EncodeToString(hasher.Sum(nil)) expected := strings.ToLower(payload.Checksum) if actualHash != expected { return errorResult(fmt.Sprintf("Prüfsummen-Fehler: erwartet %s, erhalten %s", expected, actualHash)) } } // Installer ausführen ext := strings.ToLower(filepath.Ext(payload.InstallerUrl)) var installCmd string switch { case runtime.GOOS == "windows" && ext == ".msi": installCmd = fmt.Sprintf("msiexec /i \"%s\" /quiet /norestart %s", tmpPath, payload.SilentArgs) case runtime.GOOS == "windows" && (ext == ".exe"): installCmd = fmt.Sprintf("\"%s\" %s", tmpPath, payload.SilentArgs) case runtime.GOOS == "linux" && (ext == ".deb"): installCmd = fmt.Sprintf("dpkg -i \"%s\"", tmpPath) case runtime.GOOS == "linux" && (ext == ".rpm"): installCmd = fmt.Sprintf("rpm -i \"%s\"", tmpPath) default: installCmd = fmt.Sprintf("\"%s\" %s", tmpPath, payload.SilentArgs) } return executor.Execute(ctx, installCmd, 1200) } func isCommandAvailable(name string) bool { result := executor.Execute(context.Background(), "which "+name+" || where "+name, 5) return result.Success || result.ExitCode == 0 } func parsePayload(payloadJSON string) (*Payload, error) { var p Payload if err := json.Unmarshal([]byte(payloadJSON), &p); err != nil { return nil, err } return &p, nil } func errorResult(msg string) *executor.Result { return &executor.Result{ ExitCode: -1, Stderr: msg, Success: false, } }