Some checks failed
particle-os CI / Test particle-os (push) Failing after 1s
particle-os CI / Integration Test (push) Has been skipped
particle-os CI / Security & Quality (push) Failing after 1s
Test particle-os Basic Functionality / test-basic (push) Failing after 1s
particle-os CI / Build and Release (push) Has been skipped
490 lines
15 KiB
Go
490 lines
15 KiB
Go
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)
|
|
}
|