gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/overlord/devicestate/handlers_install.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2021 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package devicestate 21 22 import ( 23 "compress/gzip" 24 "fmt" 25 "os" 26 "os/exec" 27 "path/filepath" 28 29 "gopkg.in/tomb.v2" 30 31 "github.com/snapcore/snapd/asserts" 32 "github.com/snapcore/snapd/boot" 33 "github.com/snapcore/snapd/gadget" 34 "github.com/snapcore/snapd/gadget/install" 35 "github.com/snapcore/snapd/logger" 36 "github.com/snapcore/snapd/osutil" 37 "github.com/snapcore/snapd/overlord/snapstate" 38 "github.com/snapcore/snapd/overlord/state" 39 "github.com/snapcore/snapd/randutil" 40 "github.com/snapcore/snapd/secboot" 41 "github.com/snapcore/snapd/sysconfig" 42 "github.com/snapcore/snapd/timings" 43 ) 44 45 var ( 46 bootMakeRunnable = boot.MakeRunnableSystem 47 bootEnsureNextBootToRunMode = boot.EnsureNextBootToRunMode 48 installRun = install.Run 49 50 sysconfigConfigureTargetSystem = sysconfig.ConfigureTargetSystem 51 ) 52 53 func setSysconfigCloudOptions(opts *sysconfig.Options, gadgetDir string, model *asserts.Model) { 54 ubuntuSeedCloudCfg := filepath.Join(boot.InitramfsUbuntuSeedDir, "data/etc/cloud/cloud.cfg.d") 55 56 grade := model.Grade() 57 58 // we always set the cloud-init src directory if it exists, it is 59 // automatically ignored by sysconfig in the case it shouldn't be used 60 if osutil.IsDirectory(ubuntuSeedCloudCfg) { 61 opts.CloudInitSrcDir = ubuntuSeedCloudCfg 62 } 63 64 switch { 65 // if the gadget has a cloud.conf file, always use that regardless of grade 66 case sysconfig.HasGadgetCloudConf(gadgetDir): 67 opts.AllowCloudInit = true 68 69 // next thing is if are in secured grade and didn't have gadget config, we 70 // disable cloud-init always, clouds should have their own config via 71 // gadgets for grade secured 72 case grade == asserts.ModelSecured: 73 opts.AllowCloudInit = false 74 75 // all other cases we allow cloud-init to run, either through config that is 76 // available at runtime via a CI-DATA USB drive, or via config on 77 // ubuntu-seed if that is allowed by the model grade, etc. 78 default: 79 opts.AllowCloudInit = true 80 } 81 } 82 83 func writeModel(model *asserts.Model, where string) error { 84 f, err := os.OpenFile(where, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644) 85 if err != nil { 86 return err 87 } 88 defer f.Close() 89 return asserts.NewEncoder(f).Encode(model) 90 } 91 92 func writeLogs(rootdir string) error { 93 // XXX: would be great to use native journal format but it's tied 94 // to machine-id, we could journal -o export but there 95 // is no systemd-journal-remote on core{,18,20} 96 // 97 // XXX: or only log if persistent journal is enabled? 98 logPath := filepath.Join(rootdir, "var/log/install-mode.log.gz") 99 if err := os.MkdirAll(filepath.Dir(logPath), 0755); err != nil { 100 return err 101 } 102 103 f, err := os.Create(logPath) 104 if err != nil { 105 return err 106 } 107 defer f.Close() 108 109 gz := gzip.NewWriter(f) 110 defer gz.Close() 111 112 cmd := exec.Command("journalctl", "-b", "0") 113 cmd.Stdout = gz 114 if err := cmd.Run(); err != nil { 115 return fmt.Errorf("cannot collect journal output: %v", err) 116 } 117 if err := gz.Flush(); err != nil { 118 return fmt.Errorf("cannot flush compressed log output: %v", err) 119 } 120 121 return nil 122 } 123 124 func writeTimings(st *state.State, rootdir string) error { 125 logPath := filepath.Join(rootdir, "var/log/install-timings.txt.gz") 126 if err := os.MkdirAll(filepath.Dir(logPath), 0755); err != nil { 127 return err 128 } 129 130 f, err := os.Create(logPath) 131 if err != nil { 132 return err 133 } 134 defer f.Close() 135 136 gz := gzip.NewWriter(f) 137 defer gz.Close() 138 139 var chgIDs []string 140 for _, chg := range st.Changes() { 141 if chg.Kind() == "seed" || chg.Kind() == "install-system" { 142 // this is captured via "--ensure=seed" and 143 // "--ensure=install-system" below 144 continue 145 } 146 chgIDs = append(chgIDs, chg.ID()) 147 } 148 149 // state must be unlocked for "snap changes/debug timings" to work 150 st.Unlock() 151 defer st.Lock() 152 153 // XXX: ugly, ugly, but using the internal timings requires 154 // some refactor as a lot of the required bits are not 155 // exported right now 156 // first all changes 157 fmt.Fprintf(gz, "---- Output of: snap changes\n") 158 cmd := exec.Command("snap", "changes") 159 cmd.Stdout = gz 160 if err := cmd.Run(); err != nil { 161 return fmt.Errorf("cannot collect timings output: %v", err) 162 } 163 fmt.Fprintf(gz, "\n") 164 // then the seeding 165 fmt.Fprintf(gz, "---- Output of snap debug timings --ensure=seed\n") 166 cmd = exec.Command("snap", "debug", "timings", "--ensure=seed") 167 cmd.Stdout = gz 168 if err := cmd.Run(); err != nil { 169 return fmt.Errorf("cannot collect timings output: %v", err) 170 } 171 fmt.Fprintf(gz, "\n") 172 // then the install 173 fmt.Fprintf(gz, "---- Output of snap debug timings --ensure=install-system\n") 174 cmd = exec.Command("snap", "debug", "timings", "--ensure=install-system") 175 cmd.Stdout = gz 176 if err := cmd.Run(); err != nil { 177 return fmt.Errorf("cannot collect timings output: %v", err) 178 } 179 // then the other changes (if there are any) 180 for _, chgID := range chgIDs { 181 fmt.Fprintf(gz, "---- Output of snap debug timings %s\n", chgID) 182 cmd = exec.Command("snap", "debug", "timings", chgID) 183 cmd.Stdout = gz 184 if err := cmd.Run(); err != nil { 185 return fmt.Errorf("cannot collect timings output: %v", err) 186 } 187 fmt.Fprintf(gz, "\n") 188 } 189 190 if err := gz.Flush(); err != nil { 191 return fmt.Errorf("cannot flush timings output: %v", err) 192 } 193 194 return nil 195 } 196 197 func (m *DeviceManager) doSetupRunSystem(t *state.Task, _ *tomb.Tomb) error { 198 st := t.State() 199 st.Lock() 200 defer st.Unlock() 201 202 perfTimings := state.TimingsForTask(t) 203 defer perfTimings.Save(st) 204 205 // get gadget dir 206 deviceCtx, err := DeviceCtx(st, t, nil) 207 if err != nil { 208 return fmt.Errorf("cannot get device context: %v", err) 209 } 210 gadgetInfo, err := snapstate.GadgetInfo(st, deviceCtx) 211 if err != nil { 212 return fmt.Errorf("cannot get gadget info: %v", err) 213 } 214 gadgetDir := gadgetInfo.MountDir() 215 216 kernelInfo, err := snapstate.KernelInfo(st, deviceCtx) 217 if err != nil { 218 return fmt.Errorf("cannot get kernel info: %v", err) 219 } 220 kernelDir := kernelInfo.MountDir() 221 222 modeEnv, err := maybeReadModeenv() 223 if err != nil { 224 return err 225 } 226 if modeEnv == nil { 227 return fmt.Errorf("missing modeenv, cannot proceed") 228 } 229 230 // bootstrap 231 bopts := install.Options{ 232 Mount: true, 233 } 234 useEncryption, err := m.checkEncryption(st, deviceCtx) 235 if err != nil { 236 return err 237 } 238 bopts.Encrypt = useEncryption 239 240 model := deviceCtx.Model() 241 242 // make sure that gadget is usable for the set up we want to use it in 243 validationConstraints := gadget.ValidationConstraints{ 244 EncryptedData: useEncryption, 245 } 246 var ginfo *gadget.Info 247 timings.Run(perfTimings, "read-info-and-validate", "Read and validate gagdet info", func(timings.Measurer) { 248 ginfo, err = gadget.ReadInfoAndValidate(gadgetDir, model, &validationConstraints) 249 }) 250 if err != nil { 251 return fmt.Errorf("cannot use gadget: %v", err) 252 } 253 if err := gadget.ValidateContent(ginfo, gadgetDir, kernelDir); err != nil { 254 return fmt.Errorf("cannot use gadget: %v", err) 255 } 256 257 var trustedInstallObserver *boot.TrustedAssetsInstallObserver 258 // get a nice nil interface by default 259 var installObserver gadget.ContentObserver 260 trustedInstallObserver, err = boot.TrustedAssetsInstallObserverForModel(model, gadgetDir, useEncryption) 261 if err != nil && err != boot.ErrObserverNotApplicable { 262 return fmt.Errorf("cannot setup asset install observer: %v", err) 263 } 264 if err == nil { 265 installObserver = trustedInstallObserver 266 if !useEncryption { 267 // there will be no key sealing, so past the 268 // installation pass no other methods need to be called 269 trustedInstallObserver = nil 270 } 271 } 272 273 var installedSystem *install.InstalledSystemSideData 274 // run the create partition code 275 logger.Noticef("create and deploy partitions") 276 timings.Run(perfTimings, "install-run", "Install the run system", func(tm timings.Measurer) { 277 st.Unlock() 278 defer st.Lock() 279 installedSystem, err = installRun(model, gadgetDir, kernelDir, "", bopts, installObserver, tm) 280 }) 281 if err != nil { 282 return fmt.Errorf("cannot install system: %v", err) 283 } 284 285 if trustedInstallObserver != nil { 286 // sanity check 287 if installedSystem.KeysForRoles == nil || installedSystem.KeysForRoles[gadget.SystemData] == nil || installedSystem.KeysForRoles[gadget.SystemSave] == nil { 288 return fmt.Errorf("internal error: system encryption keys are unset") 289 } 290 dataKeySet := installedSystem.KeysForRoles[gadget.SystemData] 291 saveKeySet := installedSystem.KeysForRoles[gadget.SystemSave] 292 293 // make note of the encryption keys 294 trustedInstallObserver.ChosenEncryptionKeys(dataKeySet.Key, saveKeySet.Key) 295 296 // keep track of recovery assets 297 if err := trustedInstallObserver.ObserveExistingTrustedRecoveryAssets(boot.InitramfsUbuntuSeedDir); err != nil { 298 return fmt.Errorf("cannot observe existing trusted recovery assets: err") 299 } 300 if err := saveKeys(installedSystem.KeysForRoles); err != nil { 301 return err 302 } 303 // write markers containing a secret to pair data and save 304 if err := writeMarkers(); err != nil { 305 return err 306 } 307 } 308 309 // keep track of the model we installed 310 err = os.MkdirAll(filepath.Join(boot.InitramfsUbuntuBootDir, "device"), 0755) 311 if err != nil { 312 return fmt.Errorf("cannot store the model: %v", err) 313 } 314 err = writeModel(model, filepath.Join(boot.InitramfsUbuntuBootDir, "device/model")) 315 if err != nil { 316 return fmt.Errorf("cannot store the model: %v", err) 317 } 318 319 // configure the run system 320 opts := &sysconfig.Options{TargetRootDir: boot.InstallHostWritableDir, GadgetDir: gadgetDir} 321 // configure cloud init 322 setSysconfigCloudOptions(opts, gadgetDir, model) 323 timings.Run(perfTimings, "sysconfig-configure-target-system", "Configure target system", func(timings.Measurer) { 324 err = sysconfigConfigureTargetSystem(model, opts) 325 }) 326 if err != nil { 327 return err 328 } 329 330 // make it bootable 331 logger.Noticef("make system runnable") 332 bootBaseInfo, err := snapstate.BootBaseInfo(st, deviceCtx) 333 if err != nil { 334 return fmt.Errorf("cannot get boot base info: %v", err) 335 } 336 recoverySystemDir := filepath.Join("/systems", modeEnv.RecoverySystem) 337 bootWith := &boot.BootableSet{ 338 Base: bootBaseInfo, 339 BasePath: bootBaseInfo.MountFile(), 340 Kernel: kernelInfo, 341 KernelPath: kernelInfo.MountFile(), 342 RecoverySystemDir: recoverySystemDir, 343 UnpackedGadgetDir: gadgetDir, 344 } 345 timings.Run(perfTimings, "boot-make-runnable", "Make target system runnable", func(timings.Measurer) { 346 err = bootMakeRunnable(deviceCtx.Model(), bootWith, trustedInstallObserver) 347 }) 348 if err != nil { 349 return fmt.Errorf("cannot make system runnable: %v", err) 350 } 351 352 // TODO: FIXME: this should go away after we have time to design a proper 353 // solution 354 // TODO: only run on specific models? 355 356 // on some specific devices, we need to create these directories in 357 // _writable_defaults in order to allow the install-device hook to install 358 // some files there, this eventually will go away when we introduce a proper 359 // mechanism not using system-files to install files onto the root 360 // filesystem from the install-device hook 361 if err := fixupWritableDefaultDirs(boot.InstallHostWritableDir); err != nil { 362 return err 363 } 364 365 return nil 366 } 367 368 func fixupWritableDefaultDirs(systemDataDir string) error { 369 // the _writable_default directory is used to put files in place on 370 // ubuntu-data from install mode, so we abuse it here for a specific device 371 // to let that device install files with system-files and the install-device 372 // hook 373 374 // eventually this will be a proper, supported, designed mechanism instead 375 // of just this hack, but this hack is just creating the directories, since 376 // the system-files interface only allows creating the file, not creating 377 // the directories leading up to that file, and since the file is deeply 378 // nested we would effectively have to give all permission to the device 379 // to create any file on ubuntu-data which we don't want to do, so we keep 380 // this restriction to let the device create one specific file, and then 381 // we behind the scenes just create the directories for the device 382 383 for _, subDirToCreate := range []string{"/etc/udev/rules.d", "/etc/modprobe.d", "/etc/modules-load.d/"} { 384 dirToCreate := sysconfig.WritableDefaultsDir(systemDataDir, subDirToCreate) 385 386 if err := os.MkdirAll(dirToCreate, 0755); err != nil { 387 return err 388 } 389 } 390 391 return nil 392 } 393 394 // writeMarkers writes markers containing the same secret to pair data and save. 395 func writeMarkers() error { 396 // ensure directory for markers exists 397 if err := os.MkdirAll(boot.InstallHostFDEDataDir, 0755); err != nil { 398 return err 399 } 400 if err := os.MkdirAll(boot.InstallHostFDESaveDir, 0755); err != nil { 401 return err 402 } 403 404 // generate a secret random marker 405 markerSecret, err := randutil.CryptoTokenBytes(32) 406 if err != nil { 407 return fmt.Errorf("cannot create ubuntu-data/save marker secret: %v", err) 408 } 409 410 dataMarker := filepath.Join(boot.InstallHostFDEDataDir, "marker") 411 if err := osutil.AtomicWriteFile(dataMarker, markerSecret, 0600, 0); err != nil { 412 return err 413 } 414 415 saveMarker := filepath.Join(boot.InstallHostFDESaveDir, "marker") 416 if err := osutil.AtomicWriteFile(saveMarker, markerSecret, 0600, 0); err != nil { 417 return err 418 } 419 420 return nil 421 } 422 423 func saveKeys(keysForRoles map[string]*install.EncryptionKeySet) error { 424 dataKeySet := keysForRoles[gadget.SystemData] 425 426 // ensure directory for keys exists 427 if err := os.MkdirAll(boot.InstallHostFDEDataDir, 0755); err != nil { 428 return err 429 } 430 431 // Write the recovery key 432 recoveryKeyFile := filepath.Join(boot.InstallHostFDEDataDir, "recovery.key") 433 if err := dataKeySet.RecoveryKey.Save(recoveryKeyFile); err != nil { 434 return fmt.Errorf("cannot store recovery key: %v", err) 435 } 436 437 saveKeySet := keysForRoles[gadget.SystemSave] 438 if saveKeySet == nil { 439 // no system-save support 440 return nil 441 } 442 443 saveKey := filepath.Join(boot.InstallHostFDEDataDir, "ubuntu-save.key") 444 reinstallSaveKey := filepath.Join(boot.InstallHostFDEDataDir, "reinstall.key") 445 446 if err := saveKeySet.Key.Save(saveKey); err != nil { 447 return fmt.Errorf("cannot store system save key: %v", err) 448 } 449 if err := saveKeySet.RecoveryKey.Save(reinstallSaveKey); err != nil { 450 return fmt.Errorf("cannot store reinstall key: %v", err) 451 } 452 return nil 453 } 454 455 var secbootCheckTPMKeySealingSupported = secboot.CheckTPMKeySealingSupported 456 457 // checkEncryption verifies whether encryption should be used based on the 458 // model grade and the availability of a TPM device or a fde-setup hook 459 // in the kernel. 460 func (m *DeviceManager) checkEncryption(st *state.State, deviceCtx snapstate.DeviceContext) (res bool, err error) { 461 model := deviceCtx.Model() 462 secured := model.Grade() == asserts.ModelSecured 463 dangerous := model.Grade() == asserts.ModelDangerous 464 encrypted := model.StorageSafety() == asserts.StorageSafetyEncrypted 465 466 // check if we should disable encryption non-secured devices 467 // TODO:UC20: this is not the final mechanism to bypass encryption 468 if dangerous && osutil.FileExists(filepath.Join(boot.InitramfsUbuntuSeedDir, ".force-unencrypted")) { 469 return false, nil 470 } 471 472 // check if the model prefers to be unencrypted 473 // TODO: provide way to select via install chooser menu 474 // if the install is unencrypted or encrypted 475 if model.StorageSafety() == asserts.StorageSafetyPreferUnencrypted { 476 logger.Noticef(`installing system unencrypted to comply with prefer-unencrypted storage-safety model option`) 477 return false, nil 478 } 479 480 // check if encryption is available 481 var ( 482 hasFDESetupHook bool 483 checkEncryptionErr error 484 ) 485 if kernelInfo, err := snapstate.KernelInfo(st, deviceCtx); err == nil { 486 if hasFDESetupHook = hasFDESetupHookInKernel(kernelInfo); hasFDESetupHook { 487 checkEncryptionErr = m.checkFDEFeatures(st) 488 } 489 } 490 // Note that having a fde-setup hook will disable the build-in 491 // secboot encryption 492 if !hasFDESetupHook { 493 checkEncryptionErr = secbootCheckTPMKeySealingSupported() 494 } 495 496 // check if encryption is required 497 if checkEncryptionErr != nil { 498 if secured { 499 return false, fmt.Errorf("cannot encrypt device storage as mandated by model grade secured: %v", checkEncryptionErr) 500 } 501 if encrypted { 502 return false, fmt.Errorf("cannot encrypt device storage as mandated by encrypted storage-safety model option: %v", checkEncryptionErr) 503 } 504 505 if hasFDESetupHook { 506 logger.Noticef("not encrypting device storage as querying kernel fde-setup hook did not succeed: %v", checkEncryptionErr) 507 } else { 508 logger.Noticef("not encrypting device storage as checking TPM gave: %v", checkEncryptionErr) 509 } 510 511 // not required, go without 512 return false, nil 513 } 514 515 // encrypt 516 return true, nil 517 } 518 519 // RebootOptions can be attached to restart-system-to-run-mode tasks to control 520 // their restart behavior. 521 type RebootOptions struct { 522 Op string `json:"op,omitempty"` 523 } 524 525 const ( 526 RebootHaltOp = "halt" 527 RebootPoweroffOp = "poweroff" 528 ) 529 530 func (m *DeviceManager) doRestartSystemToRunMode(t *state.Task, _ *tomb.Tomb) error { 531 st := t.State() 532 st.Lock() 533 defer st.Unlock() 534 535 perfTimings := state.TimingsForTask(t) 536 defer perfTimings.Save(st) 537 538 modeEnv, err := maybeReadModeenv() 539 if err != nil { 540 return err 541 } 542 543 if modeEnv == nil { 544 return fmt.Errorf("missing modeenv, cannot proceed") 545 } 546 547 // ensure the next boot goes into run mode 548 if err := bootEnsureNextBootToRunMode(modeEnv.RecoverySystem); err != nil { 549 return err 550 } 551 552 var rebootOpts RebootOptions 553 err = t.Get("reboot", &rebootOpts) 554 if err != nil && err != state.ErrNoState { 555 return err 556 } 557 558 // write timing information 559 if err := writeTimings(st, boot.InstallHostWritableDir); err != nil { 560 logger.Noticef("cannot write timings: %v", err) 561 } 562 // store install-mode log into ubuntu-data partition 563 if err := writeLogs(boot.InstallHostWritableDir); err != nil { 564 logger.Noticef("cannot write installation log: %v", err) 565 } 566 567 // request by default a restart as the last action after a 568 // successful install or what install-device requested via 569 // snapctl reboot 570 rst := state.RestartSystemNow 571 what := "restart" 572 switch rebootOpts.Op { 573 case RebootHaltOp: 574 what = "halt" 575 rst = state.RestartSystemHaltNow 576 case RebootPoweroffOp: 577 what = "poweroff" 578 rst = state.RestartSystemPoweroffNow 579 } 580 logger.Noticef("request immediate system %s", what) 581 st.RequestRestart(rst) 582 583 return nil 584 }