From 4b515cbd44f988b1f0be5d84f5abeae0542b35ad Mon Sep 17 00:00:00 2001 From: gerblesh <101901964+gerblesh@users.noreply.github.com> Date: Fri, 29 Nov 2024 20:23:17 -0800 Subject: [PATCH] add rpm-ostree support --- README.md | 2 +- cmd/imageOutdated.go | 6 ++- cmd/update.go | 50 ++++++++++++---------- cmd/updateCheck.go | 6 ++- drv/bootc.go | 98 ++++++++++++++++++++++++++++++++++++++++++-- drv/brew.go | 1 - ublue-upd.service | 2 +- 7 files changed, 134 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 9b35001..d477d2c 100644 --- a/README.md +++ b/README.md @@ -62,4 +62,4 @@ $ journalctl -exu 'ublue-upd.service' # How do I build this? 1. `just build` will build this project and place the binary in `output/ublue-upd` -1. `sudo ./output/ublue-upd` will run a system update +1. `sudo ./output/ublue-upd` will run an update diff --git a/cmd/imageOutdated.go b/cmd/imageOutdated.go index b62610e..4539af1 100644 --- a/cmd/imageOutdated.go +++ b/cmd/imageOutdated.go @@ -8,7 +8,11 @@ import ( ) func ImageOutdated(cmd *cobra.Command, args []string) { - outdated, err := drv.IsImageOutdated() + systemDriver, err := drv.GetSystemUpdateDriver() + if err != nil { + log.Fatalf("Failed to get system update driver: %v", err) + } + outdated, err := systemDriver.ImageOutdated() if err != nil { log.Fatalf("Cannot determine if image is outdated: %v", err) } diff --git a/cmd/update.go b/cmd/update.go index f0621af..ee9c41a 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -18,7 +18,11 @@ type Failure struct { } func Update(cmd *cobra.Command, args []string) { - outdated, err := drv.IsImageOutdated() + systemDriver, err := drv.GetSystemUpdateDriver() + if err != nil { + log.Fatalf("Failed to get system update driver") + } + outdated, err := systemDriver.ImageOutdated() if err != nil { log.Fatalf("Unable to determine if image is outdated: %v", err) } @@ -48,17 +52,19 @@ func Update(cmd *cobra.Command, args []string) { log.Fatalf("Failed to list users") } - // Check if bootc update is available - updateAvailable, err := drv.CheckForImageUpdate() + // Check if system update is available + log.Printf("Checking for system updates (%s)", systemDriver.Name) + updateAvailable, err := systemDriver.UpdateAvailable() // ignore error on dry run if err != nil && !dryRun { log.Fatalf("Failed to check for image updates: %v", err) } - // don't update bootc if there's a dry run + log.Printf("System updates available: %t (%s)", updateAvailable, systemDriver.Name) + // don't update system if there's a dry run updateAvailable = updateAvailable && !dryRun - bootcUpdate := 0 + systemUpdate := 0 if updateAvailable { - bootcUpdate = 1 + systemUpdate = 1 } // Check if brew is installed @@ -68,16 +74,16 @@ func Update(cmd *cobra.Command, args []string) { brewUpdate = 1 } - totalUpdates := brewUpdate + bootcUpdate + 1 + len(users) + 1 + len(users) // Bootc + Brew + Flatpak (users + root) + Distrobox (users + root) - currentUpdate := 0 + totalSteps := brewUpdate + systemUpdate + 1 + len(users) + 1 + len(users) // system + Brew + Flatpak (users + root) + Distrobox (users + root) + currentStep := 0 failures := make(map[string]Failure) if updateAvailable { - currentUpdate++ - log.Printf("[%d/%d] Updating System (Bootc)", currentUpdate, totalUpdates) - out, err := drv.BootcUpdate() + currentStep++ + log.Printf("[%d/%d] Updating System (%s)", currentStep, totalSteps, systemDriver.Name) + out, err := systemDriver.Update() if err != nil { - failures["Bootc"] = Failure{ + failures[systemDriver.Name] = Failure{ err, string(out), } @@ -85,8 +91,8 @@ func Update(cmd *cobra.Command, args []string) { } if brewUpdate == 1 { - currentUpdate++ - log.Printf("[%d/%d] Updating CLI Apps (Brew)", currentUpdate, totalUpdates) + currentStep++ + log.Printf("[%d/%d] Updating CLI Apps (Brew)", currentStep, totalSteps) out, err := drv.BrewUpdate(brewUid) if err != nil { failures["Brew"] = Failure{ @@ -97,8 +103,8 @@ func Update(cmd *cobra.Command, args []string) { } // Run flatpak updates - currentUpdate++ - log.Printf("[%d/%d] Updating System Apps (Flatpak)", currentUpdate, totalUpdates) + currentStep++ + log.Printf("[%d/%d] Updating System Apps (Flatpak)", currentStep, totalSteps) flatpakCmd := exec.Command("/usr/bin/flatpak", "update", "-y") out, err := flatpakCmd.CombinedOutput() if err != nil { @@ -108,8 +114,8 @@ func Update(cmd *cobra.Command, args []string) { } } for _, user := range users { - currentUpdate++ - log.Printf("[%d/%d] Updating Apps for User: %s (Flatpak)", currentUpdate, totalUpdates, user.Name) + currentStep++ + log.Printf("[%d/%d] Updating Apps for User: %s (Flatpak)", currentStep, totalSteps, user.Name) out, err := lib.RunUID(user.UID, []string{"/usr/bin/flatpak", "update", "-y"}, nil) if err != nil { failures[fmt.Sprintf("Flatpak User: %s", user.Name)] = Failure{ @@ -120,8 +126,8 @@ func Update(cmd *cobra.Command, args []string) { } // Run distrobox updates - currentUpdate++ - log.Printf("[%d/%d] Updating System Distroboxes", currentUpdate, totalUpdates) + currentStep++ + log.Printf("[%d/%d] Updating System Distroboxes", currentStep, totalSteps) // distrobox doesn't support sudo, run with systemd-run out, err = lib.RunUID(0, []string{"/usr/bin/distrobox", "upgrade", "-a"}, nil) if err != nil { @@ -131,8 +137,8 @@ func Update(cmd *cobra.Command, args []string) { } } for _, user := range users { - currentUpdate++ - log.Printf("[%d/%d] Updating Distroboxes for User: %s", currentUpdate, totalUpdates, user.Name) + currentStep++ + log.Printf("[%d/%d] Updating Distroboxes for User: %s", currentStep, totalSteps, user.Name) out, err := lib.RunUID(user.UID, []string{"/usr/bin/distrobox", "upgrade", "-a"}, nil) if err != nil { failures[fmt.Sprintf("Distrobox User: %s", user.Name)] = Failure{ diff --git a/cmd/updateCheck.go b/cmd/updateCheck.go index c63c2e6..ba771f1 100644 --- a/cmd/updateCheck.go +++ b/cmd/updateCheck.go @@ -7,7 +7,11 @@ import ( ) func UpdateCheck(cmd *cobra.Command, args []string) { - update, err := drv.CheckForImageUpdate() + systemDriver, err := drv.GetSystemUpdateDriver() + if err != nil { + log.Fatalf("Failed to get system update driver: %v", err) + } + update, err := systemDriver.UpdateAvailable() if err != nil { log.Fatalf("Failed to check for updates: %v", err) } diff --git a/drv/bootc.go b/drv/bootc.go index 4f04bad..766d52f 100644 --- a/drv/bootc.go +++ b/drv/bootc.go @@ -7,17 +7,43 @@ import ( "time" ) +// implementation of bootc and rpm-ostree commands (rpm-ostree support will be removed in the future) + type bootcStatus struct { Status struct { Booted struct { - Image struct { + Incompatible bool `json:"incompatible"` + Image struct { Timestamp string `json:"timestamp"` } `json:"image"` } `json:"booted"` + Staged struct { + Incompatible bool `json:"incompatible"` + } } `json:"status"` } -func IsImageOutdated() (bool, error) { +type rpmOstreeStatus struct { + Deployments []struct { + Timestamp int64 `json:"timestamp"` + } `json:"deployments"` +} + +func BootcCompat() (bool, error) { + cmd := exec.Command("bootc", "status", "--format=json") + out, err := cmd.CombinedOutput() + if err != nil { + return false, nil + } + var status bootcStatus + err = json.Unmarshal(out, &status) + if err != nil { + return false, nil + } + return !(status.Status.Booted.Incompatible || status.Status.Staged.Incompatible), nil +} + +func IsBootcImageOutdated() (bool, error) { cmd := exec.Command("bootc", "status", "--format=json") out, err := cmd.CombinedOutput() if err != nil { @@ -32,7 +58,7 @@ func IsImageOutdated() (bool, error) { if err != nil { return false, nil } - oneMonthAgo := time.Now().AddDate(0, -1, 0) + oneMonthAgo := time.Now().UTC().AddDate(0, -1, 0) return timestamp.Before(oneMonthAgo), nil } @@ -46,7 +72,7 @@ func BootcUpdate() ([]byte, error) { return out, nil } -func CheckForImageUpdate() (bool, error) { +func CheckForBootcImageUpdate() (bool, error) { cmd := exec.Command("/usr/bin/bootc", "upgrade", "--check") out, err := cmd.CombinedOutput() if err != nil { @@ -54,3 +80,67 @@ func CheckForImageUpdate() (bool, error) { } return !strings.Contains(string(out), "No changes in:"), nil } + +func IsRpmOstreeImageOutdated() (bool, error) { + cmd := exec.Command("rpm-ostree", "status", "--json", "--booted") + out, err := cmd.CombinedOutput() + if err != nil { + return false, nil + } + var status rpmOstreeStatus + err = json.Unmarshal(out, &status) + if err != nil { + return false, nil + } + timestamp := time.Unix(status.Deployments[0].Timestamp, 0).UTC() + oneMonthAgo := time.Now().AddDate(0, -1, 0) + + return timestamp.Before(oneMonthAgo), nil +} + +func RpmOstreeUpdate() ([]byte, error) { + cmd := exec.Command("/usr/bin/rpm-ostree", "upgrade") + out, err := cmd.CombinedOutput() + if err != nil { + return out, err + } + return out, nil +} + +func CheckForRpmOstreeImageUpdate() (bool, error) { + cmd := exec.Command("/usr/bin/rpm-ostree", "upgrade", "--check") + out, err := cmd.CombinedOutput() + if err != nil { + return true, err + } + return strings.Contains(string(out), "AvailableUpdate"), nil +} + +// Generalize bootc and rpm-ostree drivers into this struct as system updaters +type SystemUpdateDriver struct { + ImageOutdated func() (bool, error) + Update func() ([]byte, error) + UpdateAvailable func() (bool, error) + Name string +} + +func GetSystemUpdateDriver() (SystemUpdateDriver, error) { + useBootc, err := BootcCompat() + if err != nil { + return SystemUpdateDriver{}, err + } + if useBootc { + return SystemUpdateDriver{ + IsBootcImageOutdated, + BootcUpdate, + CheckForBootcImageUpdate, + "Bootc", + }, nil + } + return SystemUpdateDriver{ + IsRpmOstreeImageOutdated, + RpmOstreeUpdate, + CheckForRpmOstreeImageUpdate, + "rpm-ostree", + }, nil +} diff --git a/drv/brew.go b/drv/brew.go index 371fb98..d4973ba 100644 --- a/drv/brew.go +++ b/drv/brew.go @@ -3,7 +3,6 @@ package drv import ( "fmt" "github.com/gerblesh/ublue-upd/lib" - "log" "os" "syscall" ) diff --git a/ublue-upd.service b/ublue-upd.service index 04bf208..42027a4 100644 --- a/ublue-upd.service +++ b/ublue-upd.service @@ -3,4 +3,4 @@ Description=Universal Blue Update Oneshot Service [Service] Type=oneshot -ExecStart=/usr/bin/ublue-update +ExecStart=/usr/bin/ublue-upd -c