Skip to content

Commit 235e7e1

Browse files
authored
o/devicestate: Copy install-mode system hostname to run-mode target (#17214)
This adds support to set the hostname for the run-mode target install, by copying it from install-mode. The install-mode hostname can be set from the install-device hook of a gadget snap, assuming it has the hostname-control interface plug, using hostnamectl. If no static hostname is set in install-mode, the hostname won't be copied.
1 parent 5f9a899 commit 235e7e1

8 files changed

Lines changed: 246 additions & 0 deletions

File tree

overlord/devicestate/devicestate_install_mode_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,13 @@ func (s *deviceMgrInstallModeSuite) TestInstallExpTasks(c *C) {
825825
})
826826
defer restore()
827827

828+
var copyHostnameRootDir string
829+
restore = devicestate.MockCopyInstallModeHostname(func(rootdir string) error {
830+
copyHostnameRootDir = rootdir
831+
return nil
832+
})
833+
defer restore()
834+
828835
err := os.WriteFile(filepath.Join(dirs.GlobalRootDir, "/var/lib/snapd/modeenv"),
829836
[]byte("mode=install\n"), 0644)
830837
c.Assert(err, IsNil)
@@ -872,6 +879,8 @@ func (s *deviceMgrInstallModeSuite) TestInstallExpTasks(c *C) {
872879

873880
// we did request a restart through restartSystemToRunModeTask
874881
c.Check(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystemNow})
882+
c.Check(s.SystemctlDaemonReloadCalls, Equals, 0)
883+
c.Check(copyHostnameRootDir, Equals, filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-data/system-data"))
875884
}
876885

877886
func (s *deviceMgrInstallModeSuite) TestInstallExpTasksWithKMods(c *C) {
@@ -1432,6 +1441,56 @@ func (s *deviceMgrInstallModeSuite) TestInstallWithInstallDeviceHookExpTasks(c *
14321441
c.Assert(s.SystemctlDaemonReloadCalls, Equals, 1)
14331442
}
14341443

1444+
func (s *deviceMgrInstallModeSuite) TestInstallWithInstallDeviceHookCopiesHostname(c *C) {
1445+
restore := release.MockOnClassic(false)
1446+
defer restore()
1447+
1448+
restore = devicestate.MockInstallRun(func(mod gadget.Model, gadgetRoot string, kernelSnapInfo *install.KernelSnapInfo, device string, options install.Options, _ gadget.ContentObserver, _ timings.Measurer) (*install.InstalledSystemSideData, error) {
1449+
return nil, nil
1450+
})
1451+
defer restore()
1452+
1453+
restore = hookstate.MockRunHook(func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) {
1454+
hostnamePath := filepath.Join(dirs.GlobalRootDir, "etc/hostname")
1455+
c.Assert(os.MkdirAll(filepath.Dir(hostnamePath), 0755), IsNil)
1456+
c.Assert(os.WriteFile(hostnamePath, []byte("device-hostname\n"), 0644), IsNil)
1457+
return nil, nil
1458+
})
1459+
defer restore()
1460+
1461+
err := os.WriteFile(filepath.Join(dirs.GlobalRootDir, "/var/lib/snapd/modeenv"),
1462+
[]byte("mode=install\n"), 0644)
1463+
c.Assert(err, IsNil)
1464+
1465+
seedCopyFn := func(seedDir string, opts seed.CopyOptions, tm timings.Measurer) error {
1466+
return fmt.Errorf("unexpected copy call")
1467+
}
1468+
seedOpts := mockSystemSeedWithLabelOpts{
1469+
isClassic: false,
1470+
hasSystemSeed: true,
1471+
hasPartial: false,
1472+
types: []snap.Type{snap.TypeKernel},
1473+
}
1474+
s.mockSystemSeedWithLabel(c, "1234", seedCopyFn, seedOpts)
1475+
1476+
s.state.Lock()
1477+
s.makeMockInstallModel(c, "dangerous")
1478+
s.makeMockInstalledPcKernelAndGadget(c, "install-device-hook-content", "", core20SnapID)
1479+
devicestate.SetSystemMode(s.mgr, "install")
1480+
s.state.Unlock()
1481+
1482+
s.settle(c)
1483+
1484+
s.state.Lock()
1485+
defer s.state.Unlock()
1486+
1487+
installSystem := s.findInstallSystem()
1488+
c.Assert(installSystem.Err(), IsNil)
1489+
1490+
c.Check(filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-data/system-data/_writable_defaults/etc/writable/hostname"),
1491+
testutil.FileEquals, "device-hostname\n")
1492+
}
1493+
14351494
func (s *deviceMgrInstallModeSuite) testInstallWithInstallDeviceHookSnapctlReboot(c *C, arg string, rst restart.RestartType) {
14361495
restore := release.MockOnClassic(false)
14371496
defer restore()

overlord/devicestate/export_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,10 @@ func MockInstallLogicPrepareRunSystemData(f func(mod *asserts.Model, gadgetDir s
423423
return r
424424
}
425425

426+
func MockCopyInstallModeHostname(f func(rootdir string) error) (restore func()) {
427+
return testutil.Mock(&copyInstallModeHostname, f)
428+
}
429+
426430
func MockInstallRun(f func(model gadget.Model, gadgetRoot string, kernelSnapInfo *install.KernelSnapInfo, device string, options install.Options, observer gadget.ContentObserver, perfTimings timings.Measurer) (*install.InstalledSystemSideData, error)) (restore func()) {
427431
old := installRun
428432
installRun = f

overlord/devicestate/handlers_install.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"os"
3131
"os/exec"
3232
"path/filepath"
33+
"strings"
3334

3435
_ "golang.org/x/crypto/sha3"
3536
"gopkg.in/tomb.v2"
@@ -59,6 +60,7 @@ import (
5960
"github.com/snapcore/snapd/snap"
6061
"github.com/snapcore/snapd/snap/snapfile"
6162
"github.com/snapcore/snapd/snapdtool"
63+
"github.com/snapcore/snapd/sysconfig"
6264
"github.com/snapcore/snapd/systemd"
6365
"github.com/snapcore/snapd/timings"
6466
)
@@ -86,6 +88,7 @@ var (
8688
fdestateGenerateRecoveryKey = fdestate.GenerateRecoveryKey
8789

8890
installLogicPrepareRunSystemData = installLogic.PrepareRunSystemData
91+
copyInstallModeHostname = copyInstallModeHostnameImpl
8992
)
9093

9194
func writeLogs(rootdir string, fromMode string) error {
@@ -202,6 +205,32 @@ func writeTimings(st *state.State, rootdir, fromMode string) error {
202205
return nil
203206
}
204207

208+
func copyInstallModeHostnameImpl(rootdir string) error {
209+
hostnamePath := filepath.Join(dirs.GlobalRootDir, "etc/hostname")
210+
hostnameBytes, err := os.ReadFile(hostnamePath)
211+
if err != nil {
212+
if os.IsNotExist(err) {
213+
return nil
214+
}
215+
return fmt.Errorf("cannot read install-mode hostname: %v", err)
216+
}
217+
218+
hostname := strings.TrimSpace(string(hostnameBytes))
219+
if hostname == "" {
220+
return nil
221+
}
222+
223+
targetHostnamePath := sysconfig.WritableDefaultsDir(rootdir, "etc/writable/hostname")
224+
if err := os.MkdirAll(filepath.Dir(targetHostnamePath), 0755); err != nil {
225+
return err
226+
}
227+
if err := osutil.AtomicWriteFile(targetHostnamePath, []byte(hostname+"\n"), 0644, 0); err != nil {
228+
return fmt.Errorf("cannot write install-mode hostname: %v", err)
229+
}
230+
231+
return nil
232+
}
233+
205234
func (m *DeviceManager) doSetupUbuntuSave(t *state.Task, _ *tomb.Tomb) error {
206235
st := t.State()
207236
st.Lock()
@@ -418,6 +447,10 @@ func (m *DeviceManager) doRestartSystemToRunMode(t *state.Task, _ *tomb.Tomb) er
418447
}
419448
}
420449

450+
if err := copyInstallModeHostname(boot.InstallHostWritableDir(model)); err != nil {
451+
return err
452+
}
453+
421454
// ensure the next boot goes into run mode
422455
if err := bootEnsureNextBootToRunMode(modeEnv.RecoverySystem); err != nil {
423456
return err
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
defaults:
2+
system:
3+
refresh:
4+
hold: "@HOLD-TIME@"
5+
journal:
6+
persistent: true
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/bin/sh
2+
3+
set -eu
4+
5+
hostnamectl set-hostname install-device-hostname
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"plugs": {
3+
"hostname-control": {
4+
"allow-installation": "true",
5+
"allow-auto-connection": "true"
6+
}
7+
},
8+
"format": "1"
9+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
hooks:
2+
install-device:
3+
plugs:
4+
- hostname-control
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
summary: Verify install-device hostname is copied to run mode
2+
3+
details: |
4+
Check that a hostname set by the gadget install-device hook during install
5+
mode is copied to the target run-mode system before the device reboots.
6+
On UC26 this also verifies that the presence of the install-device hook
7+
prevents the single-boot initramfs install path and that the hook runs.
8+
9+
backends: [-garden]
10+
systems: [ubuntu-24.04-64, ubuntu-26.04-64]
11+
12+
environment:
13+
NESTED_BUILD_SNAPD_FROM_CURRENT: true
14+
15+
NESTED_ENABLE_TPM: false
16+
NESTED_ENABLE_SECURE_BOOT: false
17+
NESTED_SIGN_SNAPS_FAKESTORE: true
18+
NESTED_CUSTOM_MODEL: $TESTSLIB/assertions/developer1-{VERSION}-dangerous.model
19+
NESTED_FAKESTORE_BLOB_DIR: $(pwd)/fake-store-blobdir
20+
NESTED_UBUNTU_IMAGE_SNAPPY_FORCE_SAS_URL: http://localhost:11028
21+
NESTED_USE_CLOUD_INIT: false
22+
23+
prepare: |
24+
# shellcheck source=tests/lib/nested.sh
25+
. "$TESTSLIB/nested.sh"
26+
27+
"$TESTSTOOLS"/store-state setup-fake-store "$NESTED_FAKESTORE_BLOB_DIR"
28+
"$TESTSTOOLS"/store-state teardown-staging-store
29+
30+
echo "Expose the needed assertions through the fakestore"
31+
cp "$TESTSLIB"/assertions/testrootorg-store.account-key "$NESTED_FAKESTORE_BLOB_DIR/asserts"
32+
cp "$TESTSLIB"/assertions/developer1.account "$NESTED_FAKESTORE_BLOB_DIR/asserts"
33+
cp "$TESTSLIB"/assertions/developer1.account-key "$NESTED_FAKESTORE_BLOB_DIR/asserts"
34+
35+
VERSION="$(tests.nested show version)"
36+
37+
echo "Generate a system-user assertion for the dangerous model"
38+
gojq --arg model "testkeys-snapd-dangerous-core-${VERSION}-amd64" \
39+
".models += [\$model] | .models |= unique" \
40+
"$TESTSLIB/assertions/developer1-${VERSION}-auto-import.json" > auto-import.json
41+
"$TESTSLIB/gendeveloper1assert/main.sh" auto-import.json auto-import.assert
42+
export NESTED_CUSTOM_AUTO_IMPORT_ASSERTION="$PWD/auto-import.assert"
43+
44+
echo "Grab and prepare the gadget snap"
45+
snap download --basename=pc --channel="$VERSION/edge" pc
46+
unsquashfs -d pc-gadget pc.snap
47+
48+
echo "Add the install-device hook"
49+
mkdir -p pc-gadget/meta/hooks
50+
cp install-device pc-gadget/meta/hooks/install-device
51+
chmod +x pc-gadget/meta/hooks/install-device
52+
53+
echo "Disable serial registration for this test"
54+
test -f pc-gadget/meta/hooks/prepare-device
55+
sed -i '0,/^set -eu$/s//set -eu\n\nsnapctl set device-service.access=offline/' pc-gadget/meta/hooks/prepare-device
56+
grep -q '^snapctl set device-service.access=offline$' pc-gadget/meta/hooks/prepare-device
57+
chmod +x pc-gadget/meta/hooks/prepare-device
58+
59+
echo "Add the install-device hook plug to snap.yaml"
60+
gojq -s --yaml-input --yaml-output '.[0] * .[1]' snap-yaml-extras.yaml pc-gadget/meta/snap.yaml > snap.yaml.tmp
61+
cp -v snap.yaml.tmp pc-gadget/meta/snap.yaml
62+
63+
echo "Hold refreshes and make the journal persistent"
64+
sed defaults.yaml -e "s/@HOLD-TIME@/$(date --date='next week' +%Y-%m-%dT%H:%M:%S%:z)/" >> pc-gadget/meta/gadget.yaml
65+
66+
snap pack pc-gadget/ "$(tests.nested get extra-snaps-path)"
67+
rm -rf pc-gadget/
68+
69+
NESTED_FAKESTORE_SNAP_DECL_PC_GADGET="pc-snap-decl-extras.json"
70+
export NESTED_FAKESTORE_SNAP_DECL_PC_GADGET
71+
72+
tests.nested build-image core
73+
unset NESTED_FAKESTORE_SNAP_DECL_PC_GADGET
74+
75+
tests.nested create-vm core
76+
77+
restore: |
78+
"$TESTSTOOLS"/store-state teardown-fake-store "$NESTED_FAKESTORE_BLOB_DIR"
79+
tests.cleanup restore
80+
81+
debug: |
82+
# shellcheck source=tests/lib/nested.sh
83+
. "$TESTSLIB/nested.sh"
84+
85+
cat "$NESTED_LOGS_DIR/ubuntu-image.log" || true
86+
journalctl -u nested-vm --no-pager || true
87+
nested_print_serial_log || true
88+
remote.exec "sudo snap changes" || true
89+
remote.exec "sudo snap changes | awk '/Initialize device/ { print \$1 }' | xargs -r -n1 sudo snap change" || true
90+
remote.exec "cat /proc/cmdline" || true
91+
remote.exec "cat /var/lib/snapd/modeenv" || true
92+
remote.exec "cat /etc/hostname" || true
93+
remote.exec "hostnamectl status --static" || true
94+
remote.exec "snap get pc device-service.access" || true
95+
remote.exec "snap connections pc" || true
96+
remote.exec "sudo journalctl -b --no-pager" || true
97+
remote.exec "zcat /var/log/install-mode.log.gz" || true
98+
99+
execute: |
100+
# shellcheck source=tests/lib/nested.sh
101+
. "$TESTSLIB/nested.sh"
102+
103+
echo "Wait for device initialisation to be done"
104+
INITIALIZE_DEVICE_CHANGE="$(remote.exec snap changes | awk '/Initialize device/ { print $1; exit }')"
105+
test -n "$INITIALIZE_DEVICE_CHANGE"
106+
retry -n 200 --wait 1 --env "INITIALIZE_DEVICE_CHANGE=$INITIALIZE_DEVICE_CHANGE" sh -c \
107+
"remote.exec snap changes | MATCH \"^${INITIALIZE_DEVICE_CHANGE}\\s+(Done|Error).*Initialize device\""
108+
remote.exec "sudo snap change $INITIALIZE_DEVICE_CHANGE" || true
109+
remote.exec snap changes | MATCH "^${INITIALIZE_DEVICE_CHANGE}\\s+Done.*Initialize device"
110+
111+
echo "Check the system booted into run mode"
112+
remote.exec "cat /proc/cmdline" | MATCH "snapd_recovery_mode=run"
113+
remote.exec "cat /var/lib/snapd/modeenv" | MATCH "mode=run"
114+
115+
echo "Check that install-device ran"
116+
remote.exec "zcat /var/log/install-mode.log.gz" | MATCH "Run install-device hook"
117+
118+
echo "Check the gadget hook has hostname-control connected"
119+
remote.exec "snap connections pc" | MATCH 'hostname-control\s+pc:hostname-control\s+:hostname-control'
120+
121+
echo "Check that serial registration was disabled for the test"
122+
remote.exec "snap get pc device-service.access" | MATCH "^offline$"
123+
124+
echo "Check that the install-device hostname reached run mode"
125+
remote.exec "cat /etc/hostname" | MATCH "^install-device-hostname$"
126+
remote.exec "hostnamectl status --static" | MATCH "^install-device-hostname$"

0 commit comments

Comments
 (0)