Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 31 additions & 28 deletions cmd/quarry-sysupdate/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ package sysupdate

import (
"context"
"errors"
"fmt"
"log/slog"
"slices"

"github.com/urfave/cli/v3"

"go.amutable.dev/quarry/internal/hostnamed"
"go.amutable.dev/quarry/internal/third_party/assert"
"go.amutable.dev/quarry/internal/tufclient"
"go.amutable.dev/quarry/internal/xsysupdate"
Expand All @@ -24,14 +25,6 @@ var updateCommand = withExtensionDirFlag(withQuarryUserFlag(withQuarryProxyFlag(
&xsysupdate.TransferFileExtension{
OverrideSourcePathURL: ctxQuarryProxyURL(ctx),
},
&xsysupdate.TagsExtension{
// TODO: There is no ownership model for machine tags yet, so
// as a temporary workaround we take full control of the acp.*
// and amutable.* tag namespaces. These tags always exactly
// match the set shipped by the configured repositories, and
// tags outside them are never touched.
AllowedNamespaces: []string{"acp.", "amutable."},
},
}
ctx, err := exts.DoInit(ctx)
if err != nil {
Expand Down Expand Up @@ -106,37 +99,47 @@ var updateCommand = withExtensionDirFlag(withQuarryUserFlag(withQuarryProxyFlag(
// the extension application, clean up the successfully installed
// components, and force-downgrade to the old versions.

components, err := listComponents(ctx)
if err != nil {
return fmt.Errorf("get list of sysupdate components: %w", err)
}
slog.Info("Spawning 'systemd-sysupdate update'...",
"components", components)
var errs []error
for _, component := range components {
if err := sysupdate(ctx, component); err != nil {
slog.Error("sysupdate for component failed.",
"err", err.Error(), "component", component)
// FIXME: If we hit an error, continue on because we don't have
// a decent revert story at the moment.
errs = append(errs, err)
}
}
if err := errors.Join(errs...); err != nil {
return fmt.Errorf("sysupdate failed: %w", err)
if err := sysupdateAll(ctx); err != nil {
return err
}
slog.Info("'systemd-sysupdate update' completed.")

if err := exts.Close(); err != nil {
return fmt.Errorf("post-update extension cleanup: %w", err)
}

// A merged confext may ship tags.d(5) files, which systemd applies as
// part of the refresh. Features gated on those tags were evaluated
// with the old tags above, so they need another update pass.
tagsBefore, err := hostnamed.CurrentTags(ctx, "")
if err != nil {
return fmt.Errorf("fetch machine tags before refresh: %w", err)
}

// Do online-reload of sysexts and confexts.
slog.Info("Trigger online-reload of new components.")
if err := sysupdateRefresh(ctx); err != nil {
return fmt.Errorf("online-reload components: %w", err)
}

tagsAfter, err := hostnamed.CurrentTags(ctx, "")
if err != nil {
return fmt.Errorf("fetch machine tags after refresh: %w", err)
}
if slices.Equal(tagsBefore, tagsAfter) {
return nil
}
slog.Info("Machine tags changed by refresh, running a second update pass.",
"before", tagsBefore, "after", tagsAfter)
if err := enableSuggested(ctx); err != nil {
return err
}
if err := sysupdateAll(ctx); err != nil {
return err
}
if err := sysupdateRefresh(ctx); err != nil {
return fmt.Errorf("online-reload components after second pass: %w", err)
}

return nil
},
})))
34 changes: 34 additions & 0 deletions cmd/quarry-sysupdate/utils_sysupdate.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,40 @@ func sysupdate(ctx context.Context, component string) error {
return systemdcmd.Call(ctx, "sysupdate", args...)
}

// sysupdateAll runs "systemd-sysupdate update" for every component.
func sysupdateAll(ctx context.Context) error {
components, err := listComponents(ctx)
if err != nil {
return fmt.Errorf("get list of sysupdate components: %w", err)
}
slog.Info("Spawning 'systemd-sysupdate update'...",
"components", components)
var errs []error
for _, component := range components {
if err := sysupdate(ctx, component); err != nil {
slog.Error("sysupdate for component failed.",
"err", err.Error(), "component", component)
// FIXME: If we hit an error, continue on because we don't have
// a decent revert story at the moment.
errs = append(errs, err)
}
}
if err := errors.Join(errs...); err != nil {
return fmt.Errorf("sysupdate failed: %w", err)
}
slog.Info("'systemd-sysupdate update' completed.")
return nil
}

// enableSuggested enables the components and features whose SuggestOn*
// conditions now hold, e.g. after the machine tags changed.
func enableSuggested(ctx context.Context) error {
if err := systemdcmd.Call(ctx, "systemctl", "start", "systemd-sysupdate-auto-enable.service"); err != nil {
return fmt.Errorf("failed to trigger systemd-sysupdate-auto-enable service: %w", err)
}
return nil
}

func sysupdateRefresh(ctx context.Context) error {
errs := []error{
// systemd-confext refresh
Expand Down
6 changes: 6 additions & 0 deletions contrib/systemd/quarry-sysupdate.service
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
Description=Quarry-Based Automatic System Update
Wants=network-online.target
After=network-online.target
# Features gated on machine tags (SuggestOnMachineTag=) become enabled only
# when `systemd-sysupdate enable-feature --feature-suggested` runs, which is
# what systemd-sysupdate-auto-enable.service does. Run it ahead of every
# update: a tag that arrives with this run's confext is picked up by the next.
Wants=systemd-sysupdate-auto-enable.service
After=systemd-sysupdate-auto-enable.service

# For containers we assume that the manager will handle updates. And we likely
# can't even access our backing block device anyway.
Expand Down
4 changes: 2 additions & 2 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -392,8 +392,8 @@ directly, but for those who are interested:
local HTTP server as a source in transfer files.

* `quarry sysupdate update` wraps `systemd-sysupdate` and does some pre-update
and post-update steps (such as installing tags and instantiating transfer
files from the TUF repository).
and post-update steps (such as instantiating transfer files from the TUF
repository).

In most cases, you should avoid interacting with these tools -- these are
intended to be used by a `systemd-sysupdate`-managed operating system directly.
Expand Down
Loading