package particle_os import ( "fmt" "os" "os/exec" "path/filepath" "strings" "github.com/sirupsen/logrus" ) // PackageManager handles package operations in the extracted container type PackageManager struct { logger *logrus.Logger rootfsDir string workDir string } // NewPackageManager creates a new package manager func NewPackageManager(rootfsDir, workDir string, logLevel logrus.Level) *PackageManager { logger := logrus.New() logger.SetLevel(logLevel) return &PackageManager{ logger: logger, rootfsDir: rootfsDir, workDir: workDir, } } // writeFileWithSudo writes a file to the rootfs using sudo to handle permission issues func (pm *PackageManager) writeFileWithSudo(path string, content []byte, mode os.FileMode) error { // Create a temporary file in the work directory tempFile := filepath.Join(pm.workDir, "temp_"+filepath.Base(path)) if err := os.WriteFile(tempFile, content, mode); err != nil { return fmt.Errorf("failed to create temp file: %w", err) } defer os.Remove(tempFile) // Clean up temp file // Use sudo to copy the file to the target location cmd := exec.Command("sudo", "cp", tempFile, path) if err := cmd.Run(); err != nil { return fmt.Errorf("failed to copy file with sudo: %w", err) } // Set the correct permissions chmodCmd := exec.Command("sudo", "chmod", fmt.Sprintf("%o", mode), path) if err := chmodCmd.Run(); err != nil { pm.logger.Warnf("Failed to set permissions on %s: %v", path, err) } return nil } // removeFileWithSudo removes a file from the rootfs using sudo func (pm *PackageManager) removeFileWithSudo(path string) error { cmd := exec.Command("sudo", "rm", "-f", path) if err := cmd.Run(); err != nil { return fmt.Errorf("failed to remove file with sudo: %w", err) } return nil } // createSymlinkWithSudo creates a symlink in the rootfs using sudo func (pm *PackageManager) createSymlinkWithSudo(target, linkPath string) error { // Remove existing symlink if it exists pm.removeFileWithSudo(linkPath) // Create symlink with sudo cmd := exec.Command("sudo", "ln", "-s", target, linkPath) if err := cmd.Run(); err != nil { return fmt.Errorf("failed to create symlink with sudo: %w", err) } return nil } // InstallPackages installs packages using apt func (pm *PackageManager) InstallPackages(packages []string, update, clean bool) error { pm.logger.Infof("Installing packages: %v", packages) if len(packages) == 0 { pm.logger.Info("No packages to install") return nil } // Update package lists if requested if update { if err := pm.updatePackageLists(); err != nil { return fmt.Errorf("failed to update package lists: %w", err) } } // Install packages if err := pm.installPackages(packages); err != nil { return fmt.Errorf("failed to install packages: %w", err) } // Clean package cache if requested if clean { if err := pm.cleanPackageCache(); err != nil { return fmt.Errorf("failed to clean package cache: %w", err) } } pm.logger.Info("Package installation completed successfully") return nil } // updatePackageLists updates the package lists func (pm *PackageManager) updatePackageLists() error { pm.logger.Info("Updating package lists") // Create a chroot environment for apt with sudo cmd := exec.Command("sudo", "/usr/sbin/chroot", pm.rootfsDir, "apt-get", "update") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("apt-get update failed: %w", err) } pm.logger.Info("Package lists updated successfully") return nil } // installPackages installs the specified packages func (pm *PackageManager) installPackages(packages []string) error { pm.logger.Infof("Installing %d packages", len(packages)) // Prepare apt-get install command args := []string{"apt-get", "install", "-y", "--no-install-recommends"} args = append(args, packages...) // Execute in chroot with sudo chrootArgs := append([]string{"/usr/sbin/chroot", pm.rootfsDir}, args...) cmd := exec.Command("sudo", chrootArgs...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("apt-get install failed: %w", err) } pm.logger.Infof("Successfully installed %d packages", len(packages)) return nil } // cleanPackageCache cleans the package cache func (pm *PackageManager) cleanPackageCache() error { pm.logger.Info("Cleaning package cache") cmd := exec.Command("sudo", "/usr/sbin/chroot", pm.rootfsDir, "apt-get", "clean") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("apt-get clean failed: %w", err) } pm.logger.Info("Package cache cleaned successfully") return nil } // ConfigureSources configures package sources with optional apt-cacher-ng support func (pm *PackageManager) ConfigureSources(mirror string, components []string, additionalSources []string) error { pm.logger.Infof("Configuring package sources: %s", mirror) // Create sources.list.d directory sourcesDir := filepath.Join(pm.rootfsDir, "etc/apt/sources.list.d") if err := os.MkdirAll(sourcesDir, 0755); err != nil { return fmt.Errorf("failed to create sources directory: %w", err) } // Check for apt-cacher-ng configuration (optional) aptCacheURL := pm.detectAptCacherNG() if aptCacheURL != "" { pm.logger.Infof("apt-cacher-ng detected at: %s (optional enhancement)", aptCacheURL) mirror = pm.convertToAptCacherNG(mirror, aptCacheURL) } else { pm.logger.Info("apt-cacher-ng not detected, using direct repository URLs") } // Create main sources.list mainSources := filepath.Join(pm.rootfsDir, "etc/apt/sources.list") mainContent := fmt.Sprintf("deb %s trixie %s\n", mirror, strings.Join(components, " ")) if err := os.WriteFile(mainSources, []byte(mainContent), 0644); err != nil { return fmt.Errorf("failed to write main sources.list: %w", err) } // Add additional sources with optional apt-cacher-ng support for i, source := range additionalSources { // Convert additional sources to use apt-cacher-ng if available if aptCacheURL != "" { source = pm.convertToAptCacherNG(source, aptCacheURL) } sourceFile := filepath.Join(sourcesDir, fmt.Sprintf("additional-%d.list", i)) if err := os.WriteFile(sourceFile, []byte(source+"\n"), 0644); err != nil { return fmt.Errorf("failed to write additional source %d: %w", i, err) } } pm.logger.Info("Package sources configured successfully") return nil } // detectAptCacherNG detects if apt-cacher-ng is available and returns the URL (optional) func (pm *PackageManager) detectAptCacherNG() string { // Check environment variables first (CI/CD friendly) if envURL := os.Getenv("APT_CACHER_NG_URL"); envURL != "" { return envURL } // Check common apt-cacher-ng URLs (optional enhancement) commonURLs := []string{ "http://192.168.1.101:3142", // Your specific setup "http://localhost:3142", // Local development "http://apt-cacher-ng:3142", // Docker container "http://192.168.1.100:3142", // Common local network } for _, url := range commonURLs { if pm.isAptCacherNGAvailable(url) { return url } } return "" // No apt-cacher-ng found, which is perfectly fine } // isAptCacherNGAvailable checks if apt-cacher-ng is responding at the given URL (optional) func (pm *PackageManager) isAptCacherNGAvailable(url string) bool { // Simple HTTP check - we could make this more sophisticated cmd := exec.Command("curl", "-s", "--connect-timeout", "5", "--max-time", "10", url) if err := cmd.Run(); err != nil { return false } return true } // convertToAptCacherNG converts a standard Debian mirror URL to use apt-cacher-ng (optional enhancement) func (pm *PackageManager) convertToAptCacherNG(originalURL, cacheURL string) string { // Handle different URL patterns if strings.HasPrefix(originalURL, "https://") { // Convert https://deb.debian.org/debian to http://cache:3142/HTTPS///deb.debian.org/debian url := strings.TrimPrefix(originalURL, "https://") return fmt.Sprintf("%s/HTTPS///%s", cacheURL, url) } else if strings.HasPrefix(originalURL, "http://") { // Convert http://mirror to http://cache:3142/mirror url := strings.TrimPrefix(originalURL, "http://") return fmt.Sprintf("%s/%s", cacheURL, url) } else if strings.HasPrefix(originalURL, "deb ") { // Handle deb lines parts := strings.Fields(originalURL) if len(parts) >= 2 { url := parts[1] if strings.HasPrefix(url, "https://") { url = strings.TrimPrefix(url, "https://") parts[1] = fmt.Sprintf("%s/HTTPS///%s", cacheURL, url) } else if strings.HasPrefix(url, "http://") { url = strings.TrimPrefix(url, "http://") parts[1] = fmt.Sprintf("%s/%s", cacheURL, url) } return strings.Join(parts, " ") } } // Return original if we can't parse it return originalURL } // InstallDebootstrap installs debootstrap if not present func (pm *PackageManager) InstallDebootstrap() error { pm.logger.Info("Checking debootstrap installation") // Check if debootstrap is already installed in the host system if _, err := exec.LookPath("debootstrap"); err == nil { pm.logger.Info("Debootstrap already available in host system") return nil } // Also check common locations commonPaths := []string{"/usr/sbin/debootstrap", "/usr/bin/debootstrap", "/bin/debootstrap"} for _, path := range commonPaths { if _, err := os.Stat(path); err == nil { pm.logger.Infof("Debootstrap found at: %s", path) return nil } } pm.logger.Info("Debootstrap not found in host system") pm.logger.Info("Please install debootstrap manually: sudo apt-get install debootstrap") // For now, return an error to inform the user return fmt.Errorf("debootstrap not available in host system - please install manually") } // CreateDebootstrap installs a base system using debootstrap func (pm *PackageManager) CreateDebootstrap(suite, target, arch, variant string, components []string) error { pm.logger.Infof("Creating debootstrap system: %s/%s (%s)", suite, arch, variant) // Ensure debootstrap is installed if err := pm.InstallDebootstrap(); err != nil { return fmt.Errorf("debootstrap not available: %w", err) } // Find debootstrap path debootstrapPath := "/usr/sbin/debootstrap" if _, err := os.Stat(debootstrapPath); err != nil { // Try alternative paths altPaths := []string{"/usr/bin/debootstrap", "/bin/debootstrap"} for _, path := range altPaths { if _, err := os.Stat(path); err == nil { debootstrapPath = path break } } } // Prepare debootstrap command args := []string{} if variant != "" { args = append(args, "--variant", variant) } if len(components) > 0 { args = append(args, "--components", strings.Join(components, ",")) } args = append(args, suite, target, "https://deb.debian.org/debian") // Execute debootstrap with full path cmd := exec.Command(debootstrapPath, args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("debootstrap failed: %w", err) } pm.logger.Info("Debootstrap system created successfully") return nil } // ConfigureLocale configures system locale func (pm *PackageManager) ConfigureLocale(language, defaultLocale string, additionalLocales []string) error { pm.logger.Infof("Configuring locale: %s (default: %s)", language, defaultLocale) // Generate locales locales := []string{language} locales = append(locales, additionalLocales...) // Create locale.gen content localeGenContent := "" for _, locale := range locales { localeGenContent += locale + " UTF-8\n" } localeGenPath := filepath.Join(pm.rootfsDir, "etc/locale.gen") if err := pm.writeFileWithSudo(localeGenPath, []byte(localeGenContent), 0644); err != nil { return fmt.Errorf("failed to write locale.gen: %w", err) } // Generate locales in chroot with sudo cmd := exec.Command("sudo", "/usr/sbin/chroot", pm.rootfsDir, "locale-gen") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("locale-gen failed: %w", err) } // Set default locale defaultLocalePath := filepath.Join(pm.rootfsDir, "etc/default/locale") defaultContent := fmt.Sprintf("LANG=%s\n", defaultLocale) if err := pm.writeFileWithSudo(defaultLocalePath, []byte(defaultContent), 0644); err != nil { return fmt.Errorf("failed to write default locale: %w", err) } pm.logger.Info("Locale configuration completed successfully") return nil } // ConfigureTimezone configures system timezone func (pm *PackageManager) ConfigureTimezone(timezone string) error { pm.logger.Infof("Configuring timezone: %s", timezone) // Create timezone file timezonePath := filepath.Join(pm.rootfsDir, "etc/timezone") if err := pm.writeFileWithSudo(timezonePath, []byte(timezone+"\n"), 0644); err != nil { return fmt.Errorf("failed to write timezone: %w", err) } // Create localtime symlink zoneInfoPath := filepath.Join(pm.rootfsDir, "usr/share/zoneinfo", timezone) localtimePath := filepath.Join(pm.rootfsDir, "etc/localtime") // Remove existing localtime if it exists pm.removeFileWithSudo(localtimePath) // Create symlink to zoneinfo if err := pm.createSymlinkWithSudo(zoneInfoPath, localtimePath); err != nil { return fmt.Errorf("failed to create localtime symlink: %w", err) } pm.logger.Info("Timezone configuration completed successfully") return nil } // CreateUser creates a user account func (pm *PackageManager) CreateUser(username, password, shell string, groups []string, uid, gid int, home, comment string) error { pm.logger.Infof("Creating user: %s (UID: %d, GID: %d)", username, uid, gid) // Create user using chroot with sudo useraddArgs := []string{"useradd", "--create-home", "--shell", shell, "--uid", fmt.Sprintf("%d", uid), "--gid", fmt.Sprintf("%d", gid), "--comment", comment, username} chrootArgs := append([]string{"/usr/sbin/chroot", pm.rootfsDir}, useraddArgs...) cmd := exec.Command("sudo", chrootArgs...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("useradd failed: %w", err) } // Add user to groups if len(groups) > 0 { usermodArgs := []string{"usermod", "--append", "--groups", strings.Join(groups, ","), username} chrootArgs := append([]string{"/usr/sbin/chroot", pm.rootfsDir}, usermodArgs...) cmd := exec.Command("sudo", chrootArgs...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("usermod failed: %w", err) } } // Set password if provided if password != "" { chpasswdCmd := fmt.Sprintf("echo '%s:%s' | chpasswd", username, password) cmd := exec.Command("sudo", "/usr/sbin/chroot", pm.rootfsDir, "bash", "-c", chpasswdCmd) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("password setting failed: %w", err) } } pm.logger.Infof("User %s created successfully", username) return nil } // InstallEssentialPackages installs essential system packages func (pm *PackageManager) InstallEssentialPackages() error { pm.logger.Info("Installing essential system packages") essentialPackages := []string{ "systemd", "systemd-sysv", "systemd-resolved", "dbus", "udev", "init", "bash", "coreutils", "util-linux", "procps", "grep", "sed", "gawk", "tar", "gzip", "bzip2", "xz-utils", "ca-certificates", "wget", "curl", } return pm.InstallPackages(essentialPackages, false, false) }