Skip to content

Instantly share code, notes, and snippets.

@madalinignisca
Created November 13, 2024 20:37
Show Gist options
  • Save madalinignisca/d81ee1c4a516e7306c8a40d2e43f2ca0 to your computer and use it in GitHub Desktop.
Save madalinignisca/d81ee1c4a516e7306c8a40d2e43f2ca0 to your computer and use it in GitHub Desktop.
Source code for setting up unattended upgrades with compiled binary
// 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