Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Update /etc/machine-id after restoring a VM snapshot
VM instance templates are applied by the LXD agent, which is a systemd
service and therefore can't modify the machine ID. When /etc/machine-id
is missing, systemd creates one based on the SMBIOS UUID. For us, this
means launchOrRebuildFromImage works fine.

However, launchOrRebuildFromSnapshot doesn't. Since VMs only support
SFTP while running, it's not easy to just delete /etc/machine-id after
taking the snapshot. Instead we can tell systemd to always use the UUID
via a kernel parameter. Since VM images use GRUB as a bootloader, the
kernel command line needs to be configured through GRUB. We accomplish
this with a custom GRUB script and drop-in config file.
  • Loading branch information
jonathan-conder committed Aug 31, 2026
commit f00efaf83dbe98a93aa80c5d25e379b89e391c49
23 changes: 23 additions & 0 deletions internal/workshop/lxd/lxd_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -1455,6 +1455,24 @@ write_files:

[Install]
WantedBy=multi-user.target
{{- if .HasGRUB}}
- path: /etc/grub.d/70_workshop
permissions: '0755'
content: |
#!/bin/sh
exec tail --lines=+4 "$0"

# Extract SMBIOS UUID and store it in a GRUB variable. We use it to set
# the systemd.machine_id kernel parameter to the LXD UUID, which forces
# systemd to use it. By default it prefers reading the machine ID from
# /etc/machine-id, which may be stale when restoring from a snapshot.
insmod smbios
smbios --type 1 --get-uuid 8 --set workshop_machine_id
export workshop_machine_id
- path: /etc/default/grub.d/70-workshop.cfg
content: |
GRUB_CMDLINE_LINUX="${GRUB_CMDLINE_LINUX:+$GRUB_CMDLINE_LINUX }"'systemd.machine_id=${workshop_machine_id}'
{{- end}}
runcmd:
# Project directory is required for 'workshop exec'.
- install --directory --mode=755 /project /usr/local/bin /usr/local/lib/workshop {{shquote .WorkshopStateDir}}
Expand All @@ -1473,6 +1491,9 @@ runcmd:
# sets $XDG_RUNTIME_DIR and more. Interfaces such as desktop rely on both of these to be present.
# This does not introduce any additional modification beyond what a login session would normally create.
- loginctl enable-linger workshop
{{- if .HasGRUB}}
- update-grub
{{- end}}
`[1:]

var cloudConfig strings.Builder
Expand All @@ -1485,10 +1506,12 @@ runcmd:
}
dot := struct {
FsFreezePath string
HasGRUB bool
WorkshopCtlPath string
WorkshopStateDir string
}{
FsFreezePath: fsFreezePath,
HasGRUB: file.Confinement == workshop.ConfinementVirtualMachine,
WorkshopCtlPath: filepath.Join(dirs.WorkshopGuestBinDir, filepath.Base(dirs.WorkshopCtlPath)),
WorkshopStateDir: dirs.WorkshopStateDir,
}
Expand Down
2 changes: 1 addition & 1 deletion internal/workshop/lxd/lxd_backend_snapshots.go
Original file line number Diff line number Diff line change
Expand Up @@ -955,7 +955,7 @@ func (s *Backend) snapshotClients(ctx context.Context) (lxd.InstanceServer, lxd.
// replay some of the install-sdk and setup-base tasks. These can be handled in
// the same way as in-progress launches and refreshes.
func (s *Backend) FormatRevision() sdk.Revision {
return sdk.R(12)
return sdk.R(13)
}

func (s *Backend) HashSnapshot(snapshot workshop.Snapshot) (string, error) {
Expand Down
4 changes: 3 additions & 1 deletion internal/workshop/lxd/lxd_backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ func (f *LxdBeTests) TestDefaultContainerConfig(c *check.C) {

// Validate
c.Assert(err, check.IsNil)
c.Assert(cfg["cloud-init.user-data"], check.Not(testutil.Contains), "GRUB_CMDLINE_LINUX")
c.Assert(cfg["raw.idmap"], check.Equals, "uid 1001 1000\ngid 1001 1000")
c.Assert(cfg["raw.lxc"], check.Equals, "lxc.mount.entry = tmpfs tmp tmpfs defaults")
c.Assert(cfg["security.nesting"], check.Equals, "true")
Expand Down Expand Up @@ -149,6 +150,7 @@ func (f *LxdBeTests) TestDefaultVMConfig(c *check.C) {

// Validate
c.Assert(err, check.IsNil)
c.Assert(cfg["cloud-init.user-data"], testutil.Contains, "GRUB_CMDLINE_LINUX")
c.Assert(cfg["raw.idmap"], check.Equals, "uid 1002 1000\ngid 1002 1000")
_, ok := cfg["raw.lxc"]
c.Assert(ok, check.Equals, false)
Expand All @@ -161,7 +163,7 @@ func (f *LxdBeTests) TestDefaultVMConfig(c *check.C) {

// Check hash here so it's easier to update snapshot-format.yaml.
digest := sha3.Sum384([]byte(cfg["cloud-init.user-data"]))
c.Check(hex.EncodeToString(digest[:]), check.Equals, "7e2a89d65435671a015502795a945615ebf4ddb261d24274937a916e0ef27923723867ca07e5a6b6359dbc7cdba3faaf")
c.Check(hex.EncodeToString(digest[:]), check.Equals, "b2197fbcdb2a08fbb712f3f2a525dac06c50e025793b652ea7f4bc5d8456775cc69bcbc73a12d91e07d18baa5cba0c12")
// Check for syntax errors (e.g. whitespace).
var config map[string]any
err = yaml.Unmarshal([]byte(cfg["cloud-init.user-data"]), &config)
Expand Down
14 changes: 2 additions & 12 deletions internal/workshop/lxd/tests/integration/snapshot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -431,12 +431,7 @@ func (s *snapshotSuite) snapshotDiff(c *check.C, base string, confinement worksh
c.Check(originFiles.sshKey, check.Not(check.Equals), siblingFiles.sshKey)

c.Check(originFiles.hostname, check.Not(check.Equals), cloneFiles.hostname)
if confinement == workshop.ConfinementContainer {
c.Check(originFiles.machineID, check.Not(check.Equals), cloneFiles.machineID)
} else {
// TODO: fix /etc/machine-id in VMs.
c.Check(originFiles.machineID, check.Equals, cloneFiles.machineID)
}
c.Check(originFiles.machineID, check.Not(check.Equals), cloneFiles.machineID)
c.Check(originFiles.networkCfg, check.Not(check.Equals), cloneFiles.networkCfg)
c.Check(originFiles.sshKey, check.Not(check.Equals), cloneFiles.sshKey)

Expand Down Expand Up @@ -468,12 +463,7 @@ func (s *snapshotSuite) snapshotDiff(c *check.C, base string, confinement worksh
restoredFiles := s.extractUniqueFiles(c, "test", "/mnt/origin")

c.Check(restoredFiles.hostname, check.Equals, originFiles.hostname)
if confinement == workshop.ConfinementContainer || i == 0 {
c.Check(restoredFiles.machineID, check.Equals, originFiles.machineID)
} else {
// TODO: fix /etc/machine-id in VMs.
c.Check(restoredFiles.machineID, check.Equals, cloneFiles.machineID)
}
c.Check(restoredFiles.machineID, check.Equals, originFiles.machineID)
c.Check(restoredFiles.networkCfg, check.Equals, originFiles.networkCfg)
c.Check(restoredFiles.sshKey, check.Equals, originFiles.sshKey)

Expand Down