Created
November 13, 2024 20:37
-
-
Save madalinignisca/d81ee1c4a516e7306c8a40d2e43f2ca0 to your computer and use it in GitHub Desktop.
Source code for setting up unattended upgrades with compiled binary
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// unattended_upgrades_setup.go | |
package main | |
import ( | |
"bufio" | |
"bytes" | |
"fmt" | |
"io" | |
"io/ioutil" | |
"log" | |
"os" | |
"os/exec" | |
"path/filepath" | |
"strings" | |
"time" | |
) | |
const ( | |
backupDirTemplate = "/etc/apt/apt.conf.d/backup_unattended_upgrades_%s" | |
autoUpgradesConfigPath = "/etc/apt/apt.conf.d/20auto-upgrades" | |
config50Path = "/etc/apt/apt.conf.d/50unattended-upgrades" | |
config10Path = "/etc/apt/apt.conf.d/10periodic" | |
rebootScriptPath = "/usr/local/bin/unattended-upgrades-reboot.sh" | |
rebootTimestampFile = "/var/log/last_unattended_reboot" | |
cronLogPath = "/var/log/unattended-upgrades-reboot.log" | |
cronJobSchedule = "0 3 * * *" | |
cronJobComment = "Unattended Upgrades Reboot Management" | |
rebootIntervalSeconds = 1209600 // 2 weeks | |
unattendedUpgradeService = "unattended-upgrades" | |
unattendedUpgradeServiceDesc = "Unattended Upgrades service" | |
) | |
func main() { | |
fmt.Println("Starting Unattended Upgrades setup...") | |
// 1. Privilege Check | |
if os.Geteuid() != 0 { | |
log.Fatal("This program must be run as root. Use sudo or switch to the root user.") | |
} | |
// 2. Install Required Packages | |
installPackages([]string{"unattended-upgrades", "update-notifier-common"}) | |
// 3. Configure Unattended Upgrades | |
backupConfigFiles() | |
configureAutoUpgrades() | |
configureUnattendedUpgrades() | |
configure10Period() | |
// 4. Enable and Start Unattended Upgrades Service | |
enableAndStartService(unattendedUpgradeService) | |
// 5. Create Reboot Management Script | |
createRebootScript() | |
// 6. Ensure Reboot Timestamp File Exists | |
ensureRebootTimestampFile() | |
// 7. Set Up Cron Job | |
setupCronJob() | |
// 8. Perform Dry Run | |
performDryRun() | |
// 9. Display Summary | |
displaySummary() | |
fmt.Println("Unattended Upgrades setup completed successfully.") | |
} | |
// installPackages installs the required packages using apt | |
func installPackages(packages []string) { | |
fmt.Println("Installing required packages: unattended-upgrades and update-notifier-common...") | |
cmd := exec.Command("apt", "update") | |
runCommand(cmd, "Updating package lists") | |
// Install packages | |
args := append([]string{"install", "-y"}, packages...) | |
cmd = exec.Command("apt", args...) | |
runCommand(cmd, "Installing packages: " + strings.Join(packages, ", ")) | |
} | |
// backupConfigFiles backs up existing configuration files if they exist | |
func backupConfigFiles() { | |
fmt.Println("Backing up existing configuration files if they exist...") | |
timestamp := time.Now().Format("2006-01-02_15-04-05") | |
backupDir := fmt.Sprintf(backupDirTemplate, timestamp) | |
if err := os.MkdirAll(backupDir, 0755); err != nil { | |
log.Fatalf("Failed to create backup directory %s: %v", backupDir, err) | |
} | |
configFiles := []string{config50Path, config10Path} | |
for _, file := range configFiles { | |
if _, err := os.Stat(file); err == nil { | |
dest := filepath.Join(backupDir, filepath.Base(file)) | |
if err := copyFile(file, dest); err != nil { | |
log.Fatalf("Failed to backup %s: %v", file, err) | |
} | |
fmt.Printf("Backup of %s created at %s\n", file, dest) | |
} | |
} | |
} | |
// configureAutoUpgrades sets up /etc/apt/apt.conf.d/20auto-upgrades | |
func configureAutoUpgrades() { | |
fmt.Printf("Configuring %s...\n", autoUpgradesConfigPath) | |
content := `APT::Periodic::Update-Package-Lists "1"; | |
APT::Periodic::Unattended-Upgrade "1"; | |
` | |
if err := ioutil.WriteFile(autoUpgradesConfigPath, []byte(content), 0644); err != nil { | |
log.Fatalf("Failed to write %s: %v", autoUpgradesConfigPath, err) | |
} | |
} | |
// configureUnattendedUpgrades sets up /etc/apt/apt.conf.d/50unattended-upgrades | |
func configureUnattendedUpgrades() { | |
fmt.Printf("Configuring %s...\n", config50Path) | |
content := `// 50unattended-upgrades | |
Unattended-Upgrade::Allowed-Origins { | |
"${distro_id}:${distro_codename}"; | |
"${distro_id}:${distro_codename}-updates"; | |
"${distro_id}:${distro_codename}-proposed"; | |
"${distro_id}:${distro_codename}-backports"; | |
"${distro_id}:${distro_codename}-security"; | |
}; | |
Unattended-Upgrade::Automatic-Reboot "true"; | |
Unattended-Upgrade::Automatic-Reboot-Time "02:00"; | |
Unattended-Upgrade::Automatic-Reboot-WithUsers "false"; | |
Unattended-Upgrade::Mail ""; | |
Unattended-Upgrade::MailOnlyOnError "false"; | |
Unattended-Upgrade::Remove-Unused-Dependencies "true"; | |
Unattended-Upgrade::InstallOnShutdown "false"; | |
Unattended-Upgrade::Automatic-Reboot-Successful "true"; | |
Unattended-Upgrade::DPkg::Options { | |
"--force-confdef"; | |
"--force-confold"; | |
}; | |
Unattended-Upgrade::Enable-Restore-Terminal "false"; | |
` | |
if err := ioutil.WriteFile(config50Path, []byte(content), 0644); err != nil { | |
log.Fatalf("Failed to write %s: %v", config50Path, err) | |
} | |
} | |
// configure10Period sets up /etc/apt/apt.conf.d/10periodic | |
func configure10Period() { | |
fmt.Printf("Configuring %s...\n", config10Path) | |
content := `// 10periodic | |
APT::Periodic::Update-Package-Lists "1"; | |
APT::Periodic::Unattended-Upgrade "1"; | |
APT::Periodic::Automatic-Reboot "1"; | |
APT::Periodic::Automatic-Reboot-Time "02:00"; | |
` | |
if err := ioutil.WriteFile(config10Path, []byte(content), 0644); err != nil { | |
log.Fatalf("Failed to write %s: %v", config10Path, err) | |
} | |
} | |
// enableAndStartService enables and starts a systemd service | |
func enableAndStartService(service string) { | |
fmt.Printf("Enabling and starting %s service...\n", service) | |
cmd := exec.Command("systemctl", "enable", service) | |
runCommand(cmd, fmt.Sprintf("Enabling %s service", service)) | |
cmd = exec.Command("systemctl", "start", service) | |
runCommand(cmd, fmt.Sprintf("Starting %s service", service)) | |
} | |
// createRebootScript creates the reboot management script | |
func createRebootScript() { | |
fmt.Printf("Creating reboot management script at %s...\n", rebootScriptPath) | |
scriptContent := `#!/bin/bash | |
# /usr/local/bin/unattended-upgrades-reboot.sh | |
# Variables | |
REBOOT_TIMESTAMP_FILE="/var/log/last_unattended_reboot" | |
REBOOT_INTERVAL_SECONDS=1209600 # 2 weeks | |
# Check if a reboot is required | |
if [ -f /var/run/reboot-required ]; then | |
CURRENT_TIMESTAMP=$(date +%s) | |
if [ -f "$REBOOT_TIMESTAMP_FILE" ]; then | |
LAST_REBOOT_TIMESTAMP=$(cat "$REBOOT_TIMESTAMP_FILE") | |
DIFF=$((CURRENT_TIMESTAMP - LAST_REBOOT_TIMESTAMP)) | |
if [ "$DIFF" -ge "$REBOOT_INTERVAL_SECONDS" ]; then | |
echo "Reboot required and interval elapsed. Rebooting now..." | |
echo "$CURRENT_TIMESTAMP" > "$REBOOT_TIMESTAMP_FILE" | |
/sbin/shutdown -r now | |
else | |
echo "Reboot required but within interval. Skipping reboot." | |
fi | |
else | |
# Timestamp file doesn't exist; create it and reboot | |
echo "$CURRENT_TIMESTAMP" > "$REBOOT_TIMESTAMP_FILE" | |
echo "Reboot required. Rebooting now..." | |
/sbin/shutdown -r now | |
fi | |
else | |
echo "No reboot required." | |
fi | |
` | |
if err := ioutil.WriteFile(rebootScriptPath, []byte(scriptContent), 0755); err != nil { | |
log.Fatalf("Failed to write reboot script: %v", err) | |
} | |
} | |
// ensureRebootTimestampFile ensures the reboot timestamp file exists | |
func ensureRebootTimestampFile() { | |
fmt.Printf("Ensuring reboot timestamp file exists at %s...\n", rebootTimestampFile) | |
file, err := os.OpenFile(rebootTimestampFile, os.O_RDONLY|os.O_CREATE, 0644) | |
if err != nil { | |
log.Fatalf("Failed to create or open %s: %v", rebootTimestampFile, err) | |
} | |
file.Close() | |
} | |
// setupCronJob sets up a cron job for the reboot management script | |
func setupCronJob() { | |
fmt.Println("Setting up cron job for reboot management script...") | |
cronJob := fmt.Sprintf("%s %s >> %s 2>&1", cronJobSchedule, rebootScriptPath, cronLogPath) | |
comment := fmt.Sprintf("# %s", cronJobComment) | |
// Read existing crontab | |
cmd := exec.Command("crontab", "-l") | |
output, err := cmd.Output() | |
if err != nil { | |
// If crontab doesn't exist, proceed with empty | |
if !strings.Contains(err.Error(), "no crontab for") { | |
log.Fatalf("Failed to read existing crontab: %v", err) | |
} | |
output = []byte{} | |
} | |
// Check if the cron job already exists | |
if bytes.Contains(output, []byte(rebootScriptPath)) { | |
fmt.Println("Cron job already exists. Skipping addition.") | |
return | |
} | |
// Append the new cron job | |
var buffer bytes.Buffer | |
buffer.Write(output) | |
if len(output) > 0 && !strings.HasSuffix(string(output), "\n") { | |
buffer.WriteString("\n") | |
} | |
buffer.WriteString(comment + "\n") | |
buffer.WriteString(cronJob + "\n") | |
// Write the updated crontab | |
cmd = exec.Command("crontab", "-") | |
cmd.Stdin = &buffer | |
if err := cmd.Run(); err != nil { | |
log.Fatalf("Failed to write crontab: %v", err) | |
} | |
fmt.Printf("Cron job added: %s\n", cronJob) | |
} | |
// performDryRun performs a dry run of unattended-upgrades | |
func performDryRun() { | |
fmt.Println("Performing a dry run of Unattended Upgrades to verify configuration...") | |
cmd := exec.Command("unattended-upgrade", "--dry-run", "--debug") | |
cmd.Stdout = os.Stdout | |
cmd.Stderr = os.Stderr | |
if err := cmd.Run(); err != nil { | |
log.Fatalf("Dry run failed: %v", err) | |
} | |
} | |
// displaySummary prints a summary of the actions performed | |
func displaySummary() { | |
fmt.Println("----------------------------------------") | |
fmt.Println("Summary of Unattended Upgrades Setup:") | |
fmt.Println("----------------------------------------") | |
fmt.Println("1. Packages installed: unattended-upgrades, update-notifier-common") | |
fmt.Println("2. Configuration files:") | |
fmt.Printf(" - %s\n", config50Path) | |
fmt.Printf(" - %s\n", config10Path) | |
fmt.Println(" - " + autoUpgradesConfigPath) | |
fmt.Printf("3. Reboot management script: %s\n", rebootScriptPath) | |
fmt.Printf("4. Reboot timestamp file: %s\n", rebootTimestampFile) | |
fmt.Printf("5. Cron job scheduled at 3:00 AM daily to manage reboots, logging to %s.\n", cronLogPath) | |
fmt.Printf("6. %s service enabled and started.\n", unattendedUpgradeServiceDesc) | |
fmt.Println("7. Configuration verified with a dry run.") | |
fmt.Println("----------------------------------------") | |
} | |
// runCommand executes a shell command and logs its output | |
func runCommand(cmd *exec.Cmd, description string) { | |
fmt.Printf("%s...\n", description) | |
stdout, err := cmd.StdoutPipe() | |
if err != nil { | |
log.Fatalf("Failed to get stdout pipe: %v", err) | |
} | |
stderr, err := cmd.StderrPipe() | |
if err != nil { | |
log.Fatalf("Failed to get stderr pipe: %v", err) | |
} | |
if err := cmd.Start(); err != nil { | |
log.Fatalf("Failed to start command: %v", err) | |
} | |
// Concurrently read stdout and stderr | |
go logOutput(stdout) | |
go logOutput(stderr) | |
if err := cmd.Wait(); err != nil { | |
log.Fatalf("Command failed: %v", err) | |
} | |
} | |
// logOutput reads from an io.Reader and logs the output | |
func logOutput(r io.Reader) { | |
scanner := bufio.NewScanner(r) | |
for scanner.Scan() { | |
log.Println(scanner.Text()) | |
} | |
if err := scanner.Err(); err != nil { | |
log.Printf("Error reading output: %v", err) | |
} | |
} | |
// copyFile copies a file from src to dst | |
func copyFile(src, dst string) error { | |
sourceFileStat, err := os.Stat(src) | |
if err != nil { | |
return err | |
} | |
if !sourceFileStat.Mode().IsRegular() { | |
return fmt.Errorf("%s is not a regular file", src) | |
} | |
source, err := os.Open(src) | |
if err != nil { | |
return err | |
} | |
defer source.Close() | |
dest, err := os.Create(dst) | |
if err != nil { | |
return err | |
} | |
defer dest.Close() | |
if _, err := io.Copy(dest, source); err != nil { | |
return err | |
} | |
return nil | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment