Update README with current features and screenshots
- Cross-platform support (Windows/Linux) with Windows Updates and Winget - Added dependency confirmation workflow and refresh token authentication - New screenshots: History, Live Operations, Windows Agent Details - Local CLI features with terminal output and cache system - Updated known limitations - Proxmox integration is broken - Organized docs to docs/ folder and updated .gitignore - Probably introduced a dozen bugs with Windows agents - stay tuned
This commit is contained in:
@@ -2,18 +2,23 @@ package installer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aggregator-project/aggregator-agent/internal/client"
|
||||
)
|
||||
|
||||
// DNFInstaller handles DNF package installations
|
||||
type DNFInstaller struct{}
|
||||
type DNFInstaller struct {
|
||||
executor *SecureCommandExecutor
|
||||
}
|
||||
|
||||
// NewDNFInstaller creates a new DNF installer
|
||||
func NewDNFInstaller() *DNFInstaller {
|
||||
return &DNFInstaller{}
|
||||
return &DNFInstaller{
|
||||
executor: NewSecureCommandExecutor(),
|
||||
}
|
||||
}
|
||||
|
||||
// IsAvailable checks if DNF is available on this system
|
||||
@@ -26,38 +31,36 @@ func (i *DNFInstaller) IsAvailable() bool {
|
||||
func (i *DNFInstaller) Install(packageName string) (*InstallResult, error) {
|
||||
startTime := time.Now()
|
||||
|
||||
// Refresh package cache first
|
||||
refreshCmd := exec.Command("sudo", "dnf", "refresh", "-y")
|
||||
if output, err := refreshCmd.CombinedOutput(); err != nil {
|
||||
return &InstallResult{
|
||||
Success: false,
|
||||
ErrorMessage: fmt.Sprintf("Failed to refresh DNF cache: %v\nStdout: %s", err, string(output)),
|
||||
DurationSeconds: int(time.Since(startTime).Seconds()),
|
||||
}, fmt.Errorf("dnf refresh failed: %w", err)
|
||||
// Refresh package cache first using secure executor
|
||||
refreshResult, err := i.executor.ExecuteCommand("dnf", []string{"makecache"})
|
||||
if err != nil {
|
||||
refreshResult.DurationSeconds = int(time.Since(startTime).Seconds())
|
||||
refreshResult.ErrorMessage = fmt.Sprintf("Failed to refresh DNF cache: %v", err)
|
||||
return refreshResult, fmt.Errorf("dnf refresh failed: %w", err)
|
||||
}
|
||||
|
||||
// Install package
|
||||
installCmd := exec.Command("sudo", "dnf", "install", "-y", packageName)
|
||||
output, err := installCmd.CombinedOutput()
|
||||
// Install package using secure executor
|
||||
installResult, err := i.executor.ExecuteCommand("dnf", []string{"install", "-y", packageName})
|
||||
duration := int(time.Since(startTime).Seconds())
|
||||
|
||||
if err != nil {
|
||||
return &InstallResult{
|
||||
Success: false,
|
||||
ErrorMessage: fmt.Sprintf("DNF install failed: %v", err),
|
||||
Stdout: string(output),
|
||||
Stderr: "",
|
||||
ExitCode: getExitCode(err),
|
||||
Stdout: installResult.Stdout,
|
||||
Stderr: installResult.Stderr,
|
||||
ExitCode: installResult.ExitCode,
|
||||
DurationSeconds: duration,
|
||||
}, err
|
||||
}
|
||||
|
||||
return &InstallResult{
|
||||
Success: true,
|
||||
Stdout: string(output),
|
||||
Stderr: "",
|
||||
ExitCode: 0,
|
||||
Stdout: installResult.Stdout,
|
||||
Stderr: installResult.Stderr,
|
||||
ExitCode: installResult.ExitCode,
|
||||
DurationSeconds: duration,
|
||||
PackagesInstalled: []string{packageName},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -72,39 +75,36 @@ func (i *DNFInstaller) InstallMultiple(packageNames []string) (*InstallResult, e
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
// Refresh package cache first
|
||||
refreshCmd := exec.Command("sudo", "dnf", "refresh", "-y")
|
||||
if output, err := refreshCmd.CombinedOutput(); err != nil {
|
||||
return &InstallResult{
|
||||
Success: false,
|
||||
ErrorMessage: fmt.Sprintf("Failed to refresh DNF cache: %v\nStdout: %s", err, string(output)),
|
||||
DurationSeconds: int(time.Since(startTime).Seconds()),
|
||||
}, fmt.Errorf("dnf refresh failed: %w", err)
|
||||
// Refresh package cache first using secure executor
|
||||
refreshResult, err := i.executor.ExecuteCommand("dnf", []string{"makecache"})
|
||||
if err != nil {
|
||||
refreshResult.DurationSeconds = int(time.Since(startTime).Seconds())
|
||||
refreshResult.ErrorMessage = fmt.Sprintf("Failed to refresh DNF cache: %v", err)
|
||||
return refreshResult, fmt.Errorf("dnf refresh failed: %w", err)
|
||||
}
|
||||
|
||||
// Install all packages in one command
|
||||
// Install all packages in one command using secure executor
|
||||
args := []string{"install", "-y"}
|
||||
args = append(args, packageNames...)
|
||||
installCmd := exec.Command("sudo", "dnf", args...)
|
||||
output, err := installCmd.CombinedOutput()
|
||||
installResult, err := i.executor.ExecuteCommand("dnf", args)
|
||||
duration := int(time.Since(startTime).Seconds())
|
||||
|
||||
if err != nil {
|
||||
return &InstallResult{
|
||||
Success: false,
|
||||
ErrorMessage: fmt.Sprintf("DNF install failed: %v", err),
|
||||
Stdout: string(output),
|
||||
Stderr: "",
|
||||
ExitCode: getExitCode(err),
|
||||
Stdout: installResult.Stdout,
|
||||
Stderr: installResult.Stderr,
|
||||
ExitCode: installResult.ExitCode,
|
||||
DurationSeconds: duration,
|
||||
}, err
|
||||
}
|
||||
|
||||
return &InstallResult{
|
||||
Success: true,
|
||||
Stdout: string(output),
|
||||
Stderr: "",
|
||||
ExitCode: 0,
|
||||
Stdout: installResult.Stdout,
|
||||
Stderr: installResult.Stderr,
|
||||
ExitCode: installResult.ExitCode,
|
||||
DurationSeconds: duration,
|
||||
PackagesInstalled: packageNames,
|
||||
}, nil
|
||||
@@ -114,42 +114,191 @@ func (i *DNFInstaller) InstallMultiple(packageNames []string) (*InstallResult, e
|
||||
func (i *DNFInstaller) Upgrade() (*InstallResult, error) {
|
||||
startTime := time.Now()
|
||||
|
||||
// Refresh package cache first
|
||||
refreshCmd := exec.Command("sudo", "dnf", "refresh", "-y")
|
||||
if output, err := refreshCmd.CombinedOutput(); err != nil {
|
||||
return &InstallResult{
|
||||
Success: false,
|
||||
ErrorMessage: fmt.Sprintf("Failed to refresh DNF cache: %v\nStdout: %s", err, string(output)),
|
||||
DurationSeconds: int(time.Since(startTime).Seconds()),
|
||||
}, fmt.Errorf("dnf refresh failed: %w", err)
|
||||
// Refresh package cache first using secure executor
|
||||
refreshResult, err := i.executor.ExecuteCommand("dnf", []string{"makecache"})
|
||||
if err != nil {
|
||||
refreshResult.DurationSeconds = int(time.Since(startTime).Seconds())
|
||||
refreshResult.ErrorMessage = fmt.Sprintf("Failed to refresh DNF cache: %v", err)
|
||||
return refreshResult, fmt.Errorf("dnf refresh failed: %w", err)
|
||||
}
|
||||
|
||||
// Upgrade all packages
|
||||
upgradeCmd := exec.Command("sudo", "dnf", "upgrade", "-y")
|
||||
output, err := upgradeCmd.CombinedOutput()
|
||||
// Upgrade all packages using secure executor
|
||||
upgradeResult, err := i.executor.ExecuteCommand("dnf", []string{"upgrade", "-y"})
|
||||
duration := int(time.Since(startTime).Seconds())
|
||||
|
||||
if err != nil {
|
||||
return &InstallResult{
|
||||
Success: false,
|
||||
ErrorMessage: fmt.Sprintf("DNF upgrade failed: %v", err),
|
||||
Stdout: string(output),
|
||||
Stderr: "",
|
||||
ExitCode: getExitCode(err),
|
||||
Stdout: upgradeResult.Stdout,
|
||||
Stderr: upgradeResult.Stderr,
|
||||
ExitCode: upgradeResult.ExitCode,
|
||||
DurationSeconds: duration,
|
||||
}, err
|
||||
}
|
||||
|
||||
return &InstallResult{
|
||||
Success: true,
|
||||
Stdout: string(output),
|
||||
Stderr: "",
|
||||
ExitCode: 0,
|
||||
Stdout: upgradeResult.Stdout,
|
||||
Stderr: upgradeResult.Stderr,
|
||||
ExitCode: upgradeResult.ExitCode,
|
||||
DurationSeconds: duration,
|
||||
Action: "upgrade",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DryRun performs a dry run installation to check dependencies
|
||||
func (i *DNFInstaller) DryRun(packageName string) (*InstallResult, error) {
|
||||
startTime := time.Now()
|
||||
|
||||
// Attempt to refresh package cache, but don't fail if it doesn't work
|
||||
// (dry run can still work with slightly stale cache)
|
||||
refreshResult, refreshErr := i.executor.ExecuteCommand("dnf", []string{"makecache"})
|
||||
if refreshErr != nil {
|
||||
// Log refresh attempt but don't fail the dry run
|
||||
log.Printf("Warning: DNF makecache failed (continuing with dry run): %v", refreshErr)
|
||||
}
|
||||
_ = refreshResult // Discard refresh result intentionally
|
||||
|
||||
// Perform dry run installation using secure executor
|
||||
installResult, err := i.executor.ExecuteCommand("dnf", []string{"install", "--assumeno", "--downloadonly", packageName})
|
||||
duration := int(time.Since(startTime).Seconds())
|
||||
|
||||
// Parse dependencies from the output
|
||||
dependencies := i.parseDependenciesFromDNFOutput(installResult.Stdout, packageName)
|
||||
|
||||
if err != nil {
|
||||
// DNF dry run may return non-zero exit code even for successful dependency resolution
|
||||
// so we check if we were able to parse dependencies
|
||||
if len(dependencies) > 0 {
|
||||
return &InstallResult{
|
||||
Success: true,
|
||||
Stdout: installResult.Stdout,
|
||||
Stderr: installResult.Stderr,
|
||||
ExitCode: installResult.ExitCode,
|
||||
DurationSeconds: duration,
|
||||
Dependencies: dependencies,
|
||||
IsDryRun: true,
|
||||
Action: "dry_run",
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &InstallResult{
|
||||
Success: false,
|
||||
ErrorMessage: fmt.Sprintf("DNF dry run failed: %v", err),
|
||||
Stdout: installResult.Stdout,
|
||||
Stderr: installResult.Stderr,
|
||||
ExitCode: installResult.ExitCode,
|
||||
DurationSeconds: duration,
|
||||
IsDryRun: true,
|
||||
Action: "dry_run",
|
||||
}, err
|
||||
}
|
||||
|
||||
return &InstallResult{
|
||||
Success: true,
|
||||
Stdout: installResult.Stdout,
|
||||
Stderr: installResult.Stderr,
|
||||
ExitCode: installResult.ExitCode,
|
||||
DurationSeconds: duration,
|
||||
Dependencies: dependencies,
|
||||
IsDryRun: true,
|
||||
Action: "dry_run",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseDependenciesFromDNFOutput extracts dependency package names from DNF dry run output
|
||||
func (i *DNFInstaller) parseDependenciesFromDNFOutput(output string, packageName string) []string {
|
||||
var dependencies []string
|
||||
|
||||
// Regex patterns to find dependencies in DNF output
|
||||
patterns := []*regexp.Regexp{
|
||||
// Match "Installing dependencies:" section
|
||||
regexp.MustCompile(`(?s)Installing dependencies:(.*?)(\n\n|\z|Transaction Summary:)`),
|
||||
// Match "Dependencies resolved." section and package list
|
||||
regexp.MustCompile(`(?s)Dependencies resolved\.(.*?)(\n\n|\z|Transaction Summary:)`),
|
||||
// Match package installation lines
|
||||
regexp.MustCompile(`^\s*([a-zA-Z0-9][a-zA-Z0-9+._-]*)\s+[a-zA-Z0-9:.]+(?:\s+[a-zA-Z]+)?$`),
|
||||
}
|
||||
|
||||
for _, pattern := range patterns {
|
||||
if strings.Contains(pattern.String(), "Installing dependencies:") {
|
||||
matches := pattern.FindStringSubmatch(output)
|
||||
if len(matches) > 1 {
|
||||
// Extract package names from the dependencies section
|
||||
lines := strings.Split(matches[1], "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" && !strings.Contains(line, "Dependencies") {
|
||||
pkg := i.extractPackageNameFromDNFLine(line)
|
||||
if pkg != "" {
|
||||
dependencies = append(dependencies, pkg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also look for transaction summary which lists all packages to be installed
|
||||
transactionPattern := regexp.MustCompile(`(?s)Transaction Summary:\s*\n\s*Install\s+(\d+) Packages?\s*\n((?:\s+\d+\s+[a-zA-Z0-9+._-]+\s+[a-zA-Z0-9:.]+.*\n?)*)`)
|
||||
matches := transactionPattern.FindStringSubmatch(output)
|
||||
if len(matches) > 2 {
|
||||
installLines := strings.Split(matches[2], "\n")
|
||||
for _, line := range installLines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" {
|
||||
pkg := i.extractPackageNameFromDNFLine(line)
|
||||
if pkg != "" && pkg != packageName {
|
||||
dependencies = append(dependencies, pkg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicates
|
||||
uniqueDeps := make([]string, 0)
|
||||
seen := make(map[string]bool)
|
||||
for _, dep := range dependencies {
|
||||
if dep != packageName && !seen[dep] {
|
||||
seen[dep] = true
|
||||
uniqueDeps = append(uniqueDeps, dep)
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueDeps
|
||||
}
|
||||
|
||||
// extractPackageNameFromDNFLine extracts package name from a DNF output line
|
||||
func (i *DNFInstaller) extractPackageNameFromDNFLine(line string) string {
|
||||
// Remove architecture info if present
|
||||
if idx := strings.LastIndex(line, "."); idx > 0 {
|
||||
archSuffix := line[idx:]
|
||||
if strings.Contains(archSuffix, ".x86_64") || strings.Contains(archSuffix, ".noarch") ||
|
||||
strings.Contains(archSuffix, ".i386") || strings.Contains(archSuffix, ".arm64") {
|
||||
line = line[:idx]
|
||||
}
|
||||
}
|
||||
|
||||
// Extract package name (typically at the start of the line)
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) > 0 {
|
||||
pkg := fields[0]
|
||||
// Remove version info if present
|
||||
if idx := strings.Index(pkg, "-"); idx > 0 {
|
||||
potentialName := pkg[:idx]
|
||||
// Check if this looks like a version (contains numbers)
|
||||
versionPart := pkg[idx+1:]
|
||||
if strings.Contains(versionPart, ".") || regexp.MustCompile(`\d`).MatchString(versionPart) {
|
||||
return potentialName
|
||||
}
|
||||
}
|
||||
return pkg
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetPackageType returns type of packages this installer handles
|
||||
func (i *DNFInstaller) GetPackageType() string {
|
||||
return "dnf"
|
||||
|
||||
Reference in New Issue
Block a user