From be31c0a54b4cf715d9aa9fad2a0fb1dd6e112626 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Graber?= Date: Thu, 16 Jan 2025 16:17:12 -0500 Subject: [PATCH] incus-osd: Add support for sysupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stéphane Graber --- incus-osd/cmd/incus-osd/main.go | 95 +++++++++++++++++++++---- incus-osd/internal/keyring/keys.go | 2 +- incus-osd/internal/systemd/paths.go | 6 ++ incus-osd/internal/systemd/systemctl.go | 12 ++++ incus-osd/internal/systemd/sysupdate.go | 62 ++++++++++++++++ 5 files changed, 162 insertions(+), 15 deletions(-) create mode 100644 incus-osd/internal/systemd/paths.go create mode 100644 incus-osd/internal/systemd/sysupdate.go diff --git a/incus-osd/cmd/incus-osd/main.go b/incus-osd/cmd/incus-osd/main.go index c98246c..b11805b 100644 --- a/incus-osd/cmd/incus-osd/main.go +++ b/incus-osd/cmd/incus-osd/main.go @@ -19,16 +19,16 @@ import ( ) var ( - ghOrganization = "lxc" - ghRepository = "incus-os" - osExtensions = []string{"debug.raw.gz", "incus.raw.gz"} - osExtensionsPath = "/var/lib/extensions" + ghOrganization = "lxc" + ghRepository = "incus-os" + + incusExtensions = []string{"debug.raw.gz", "incus.raw.gz"} ) func main() { err := run() if err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) + _, _ = fmt.Fprintf(os.Stderr, "Error: %v\n", err) } } @@ -39,6 +39,12 @@ func run() error { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) slog.SetDefault(logger) + // Get current release. + release, err := systemd.GetCurrentRelease(ctx) + if err != nil { + return err + } + // Check kernel keyring. slog.Info("Getting trusted system keys") keys, err := keyring.GetKeys(ctx, keyring.PlatformKeyring) @@ -61,34 +67,95 @@ func run() error { slog.Info("Platform keyring entry", "name", key.Description, "key", key.Fingerprint) } - slog.Info("Starting up", "mode", mode, "app", "incus") + slog.Info("Starting up", "mode", mode, "app", "incus", "release", release) - // Fetch the system extensions. + // Fetch the Github release. gh := github.NewClient(nil) - release, _, err := gh.Repositories.GetLatestRelease(ctx, ghOrganization, ghRepository) + ghRelease, _, err := gh.Repositories.GetLatestRelease(ctx, ghOrganization, ghRepository) if err != nil { return err } - slog.Info(fmt.Sprintf("Found latest %s/%s release", ghOrganization, ghRepository), "tag", release.GetTagName()) + slog.Info(fmt.Sprintf("Found latest %s/%s release", ghOrganization, ghRepository), "tag", ghRelease.GetTagName()) + + assets, _, err := gh.Repositories.ListReleaseAssets(ctx, ghOrganization, ghRepository, ghRelease.GetID(), nil) + if err != nil { + return err + } - assets, _, err := gh.Repositories.ListReleaseAssets(ctx, ghOrganization, ghRepository, release.GetID(), nil) + // Download OS updates. + err = os.MkdirAll(systemd.SystemUpdatesPath, 0o700) if err != nil { return err } - err = os.MkdirAll(osExtensionsPath, 0700) + if release != ghRelease.GetName() { + for _, asset := range assets { + // Skip system extensions. + if !strings.HasPrefix(asset.GetName(), "IncusOS_") { + continue + } + + fields := strings.SplitN(asset.GetName(), ".", 2) + if len(fields) != 2 { + continue + } + + // Skip the full image. + if fields[1] == "raw.gz" { + continue + } + + slog.Info("Downloading OS update", "file", asset.GetName(), "url", asset.GetBrowserDownloadURL()) + + rc, _, err := gh.Repositories.DownloadReleaseAsset(ctx, ghOrganization, ghRepository, asset.GetID(), http.DefaultClient) + if err != nil { + return err + } + + defer rc.Close() + + body, err := gzip.NewReader(rc) + if err != nil { + return err + } + + defer body.Close() + + fd, err := os.Create(filepath.Join(systemd.SystemUpdatesPath, strings.TrimSuffix(asset.GetName(), ".gz"))) + if err != nil { + return err + } + + defer fd.Close() + + _, err = io.Copy(fd, body) + if err != nil { + return err + } + } + + err = systemd.ApplySystemUpdate(ctx, ghRelease.GetName(), true) + if err != nil { + return err + } + + return nil + } + + // Download system extensions. + err = os.MkdirAll(systemd.SystemExtensionsPath, 0o700) if err != nil { return err } for _, asset := range assets { - if !slices.Contains(osExtensions, asset.GetName()) { + if !slices.Contains(incusExtensions, asset.GetName()) { continue } - slog.Info("Downloading OS extension", "file", asset.GetName(), "url", asset.GetBrowserDownloadURL()) + slog.Info("Downloading system extension", "file", asset.GetName(), "url", asset.GetBrowserDownloadURL()) rc, _, err := gh.Repositories.DownloadReleaseAsset(ctx, ghOrganization, ghRepository, asset.GetID(), http.DefaultClient) if err != nil { @@ -104,7 +171,7 @@ func run() error { defer body.Close() - fd, err := os.Create(filepath.Join(osExtensionsPath, strings.TrimSuffix(asset.GetName(), ".gz"))) + fd, err := os.Create(filepath.Join(systemd.SystemExtensionsPath, strings.TrimSuffix(asset.GetName(), ".gz"))) if err != nil { return err } diff --git a/incus-osd/internal/keyring/keys.go b/incus-osd/internal/keyring/keys.go index 97c7aaa..4752fc1 100644 --- a/incus-osd/internal/keyring/keys.go +++ b/incus-osd/internal/keyring/keys.go @@ -18,7 +18,7 @@ type Key struct { } // GetKeys returns a list of keys in the requested keyring. -func GetKeys(ctx context.Context, keyring string) ([]Key, error) { +func GetKeys(_ context.Context, keyring string) ([]Key, error) { keys := []Key{} // Read the key list. diff --git a/incus-osd/internal/systemd/paths.go b/incus-osd/internal/systemd/paths.go new file mode 100644 index 0000000..e454ab8 --- /dev/null +++ b/incus-osd/internal/systemd/paths.go @@ -0,0 +1,6 @@ +package systemd + +var ( + SystemExtensionsPath = "/var/lib/extensions" + SystemUpdatesPath = "/var/lib/updates" +) diff --git a/incus-osd/internal/systemd/systemctl.go b/incus-osd/internal/systemd/systemctl.go index c25c723..70ddf83 100644 --- a/incus-osd/internal/systemd/systemctl.go +++ b/incus-osd/internal/systemd/systemctl.go @@ -6,6 +6,18 @@ import ( "github.com/lxc/incus/v6/shared/subprocess" ) +func StartUnit(ctx context.Context, units ...string) error { + args := []string{"start"} + args = append(args, units...) + + _, err := subprocess.RunCommandContext(ctx, "systemctl", args...) + if err != nil { + return err + } + + return nil +} + func EnableUnit(ctx context.Context, now bool, units ...string) error { args := []string{"enable"} diff --git a/incus-osd/internal/systemd/sysupdate.go b/incus-osd/internal/systemd/sysupdate.go new file mode 100644 index 0000000..d858333 --- /dev/null +++ b/incus-osd/internal/systemd/sysupdate.go @@ -0,0 +1,62 @@ +package systemd + +import ( + "bufio" + "context" + "errors" + "os" + "strings" + + "github.com/lxc/incus/v6/shared/subprocess" +) + +var ErrReleaseNotFound = errors.New("couldn't determine current OS release") + +// GetCurrentRelease returns the current IMAGE_VERSION from the os-release file. +func GetCurrentRelease(_ context.Context) (string, error) { + // Open the os-release file. + fd, err := os.Open("/lib/os-release") + if err != nil { + return "", err + } + + defer fd.Close() + + // Prepare reader. + fdScan := bufio.NewScanner(fd) + for fdScan.Scan() { + line := fdScan.Text() + fields := strings.SplitN(line, "=", 2) + + if len(fields) != 2 { + continue + } + + if fields[0] == "IMAGE_VERSION" { + return strings.Trim(fields[1], "\""), nil + } + } + + return "", ErrReleaseNotFound +} + +func ApplySystemUpdate(ctx context.Context, version string, reboot bool) error { + // WORKAROUND: Start the boot.mount unit so /boot autofs is active before we create a new mount namespace. + err := StartUnit(ctx, "boot.mount") + if err != nil { + return err + } + + // WORKAROUND: Needed until systemd-sysupdate can be run with system extensions applied. + cmd := "mount /dev/mapper/usr /usr && /usr/lib/systemd/systemd-sysupdate update " + version + if reboot { + cmd += "&& /usr/lib/systemd/systemd-sysupdate reboot" + } + + _, err = subprocess.RunCommandContext(ctx, "unshare", "-m", "--", "sh", "-c", cmd) + if err != nil { + return err + } + + return nil +}