github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/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 // TODO:UC20: on grade signed, allow files from ubuntu-seed, but do 57 // filtering on the resultant cloud config 58 shouldUseUbuntuSeed := model.Grade() == asserts.ModelDangerous && osutil.IsDirectory(ubuntuSeedCloudCfg) 59 60 switch { 61 // if the gadget has a cloud.conf file, always use that regardless of grade 62 case sysconfig.HasGadgetCloudConf(gadgetDir): 63 // this is implicitly handled by ConfigureTargetSystem when it 64 // configures cloud-init, so we just need to allow cloud-init for the 65 // gadget config to be used, but we also should check to see if 66 // ubuntu-seed config should be allowed as well 67 opts.AllowCloudInit = true 68 if shouldUseUbuntuSeed { 69 opts.CloudInitSrcDir = ubuntuSeedCloudCfg 70 } 71 72 // next thing is if are in secured grade and didn't have gadget config, we 73 // disable cloud-init always, clouds should have their own config via 74 // gadgets for grade secured 75 case model.Grade() == asserts.ModelSecured: 76 opts.AllowCloudInit = false 77 78 // next if we are grade dangerous, then we also install cloud configuration 79 // from ubuntu-seed if it exists 80 case shouldUseUbuntuSeed: 81 opts.AllowCloudInit = true 82 opts.CloudInitSrcDir = ubuntuSeedCloudCfg 83 84 // note that if none of the conditions were true, it means we are on grade 85 // dangerous or signed, and cloud-init is still allowed to run without 86 // additional configuration on first-boot, so that NoCloud CIDATA can be 87 // provided for example 88 default: 89 opts.AllowCloudInit = true 90 } 91 } 92 93 func writeModel(model *asserts.Model, where string) error { 94 f, err := os.OpenFile(where, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644) 95 if err != nil { 96 return err 97 } 98 defer f.Close() 99 return asserts.NewEncoder(f).Encode(model) 100 } 101 102 func writeLogs(rootdir string) error { 103 // XXX: would be great to use native journal format but it's tied 104 // to machine-id, we could journal -o export but there 105 // is no systemd-journal-remote on core{,18,20} 106 // 107 // XXX: or only log if persistent journal is enabled? 108 logPath := filepath.Join(rootdir, "var/log/install-mode.log.gz") 109 if err := os.MkdirAll(filepath.Dir(logPath), 0755); err != nil { 110 return err 111 } 112 113 f, err := os.Create(logPath) 114 if err != nil { 115 return err 116 } 117 defer f.Close() 118 119 gz := gzip.NewWriter(f) 120 defer gz.Close() 121 122 cmd := exec.Command("journalctl", "-b", "0") 123 cmd.Stdout = gz 124 if err := cmd.Run(); err != nil { 125 return fmt.Errorf("cannot collect journal output: %v", err) 126 } 127 if err := gz.Flush(); err != nil { 128 return fmt.Errorf("cannot flush compressed log output: %v", err) 129 } 130 131 return nil 132 } 133 134 func writeTimings(st *state.State, rootdir string) error { 135 logPath := filepath.Join(rootdir, "var/log/install-timings.txt.gz") 136 if err := os.MkdirAll(filepath.Dir(logPath), 0755); err != nil { 137 return err 138 } 139 140 f, err := os.Create(logPath) 141 if err != nil { 142 return err 143 } 144 defer f.Close() 145 146 gz := gzip.NewWriter(f) 147 defer gz.Close() 148 149 var chgIDs []string 150 for _, chg := range st.Changes() { 151 if chg.Kind() == "seed" { 152 // this is captured via "--ensure=seed" below 153 continue 154 } 155 chgIDs = append(chgIDs, chg.ID()) 156 } 157 158 // state must be unlocked for "snap changes/debug timings" to work 159 st.Unlock() 160 defer st.Lock() 161 162 // XXX: ugly, ugly, but using the internal timings requires 163 // some refactor as a lot of the required bits are not 164 // exported right now 165 // first all changes 166 fmt.Fprintf(gz, "---- Output of: snap changes\n") 167 cmd := exec.Command("snap", "changes") 168 cmd.Stdout = gz 169 if err := cmd.Run(); err != nil { 170 return fmt.Errorf("cannot collect timings output: %v", err) 171 } 172 fmt.Fprintf(gz, "\n") 173 // then the seeding 174 fmt.Fprintf(gz, "---- Output of snap debug timings --ensure=seed\n") 175 cmd = exec.Command("snap", "debug", "timings", "--ensure=seed") 176 cmd.Stdout = gz 177 if err := cmd.Run(); err != nil { 178 return fmt.Errorf("cannot collect timings output: %v", err) 179 } 180 fmt.Fprintf(gz, "\n") 181 // then the changes 182 for _, chgID := range chgIDs { 183 fmt.Fprintf(gz, "---- Output of snap debug timings %s\n", chgID) 184 cmd = exec.Command("snap", "debug", "timings", chgID) 185 cmd.Stdout = gz 186 if err := cmd.Run(); err != nil { 187 return fmt.Errorf("cannot collect timings output: %v", err) 188 } 189 fmt.Fprintf(gz, "\n") 190 } 191 192 if err := gz.Flush(); err != nil { 193 return fmt.Errorf("cannot flush timings output: %v", err) 194 } 195 196 return nil 197 } 198 199 func (m *DeviceManager) doSetupRunSystem(t *state.Task, _ *tomb.Tomb) error { 200 st := t.State() 201 st.Lock() 202 defer st.Unlock() 203 204 perfTimings := state.TimingsForTask(t) 205 defer perfTimings.Save(st) 206 207 // get gadget dir 208 deviceCtx, err := DeviceCtx(st, t, nil) 209 if err != nil { 210 return fmt.Errorf("cannot get device context: %v", err) 211 } 212 gadgetInfo, err := snapstate.GadgetInfo(st, deviceCtx) 213 if err != nil { 214 return fmt.Errorf("cannot get gadget info: %v", err) 215 } 216 gadgetDir := gadgetInfo.MountDir() 217 218 kernelInfo, err := snapstate.KernelInfo(st, deviceCtx) 219 if err != nil { 220 return fmt.Errorf("cannot get kernel info: %v", err) 221 } 222 kernelDir := kernelInfo.MountDir() 223 224 modeEnv, err := maybeReadModeenv() 225 if err != nil { 226 return err 227 } 228 if modeEnv == nil { 229 return fmt.Errorf("missing modeenv, cannot proceed") 230 } 231 232 // bootstrap 233 bopts := install.Options{ 234 Mount: true, 235 } 236 useEncryption, err := m.checkEncryption(st, deviceCtx) 237 if err != nil { 238 return err 239 } 240 bopts.Encrypt = useEncryption 241 242 model := deviceCtx.Model() 243 244 // make sure that gadget is usable for the set up we want to use it in 245 validationConstraints := gadget.ValidationConstraints{ 246 EncryptedData: useEncryption, 247 } 248 var ginfo *gadget.Info 249 timings.Run(perfTimings, "read-info-and-validate", "Read and validate gagdet info", func(timings.Measurer) { 250 ginfo, err = gadget.ReadInfoAndValidate(gadgetDir, model, &validationConstraints) 251 }) 252 if err != nil { 253 return fmt.Errorf("cannot use gadget: %v", err) 254 } 255 if err := gadget.ValidateContent(ginfo, gadgetDir, kernelDir); err != nil { 256 return fmt.Errorf("cannot use gadget: %v", err) 257 } 258 259 var trustedInstallObserver *boot.TrustedAssetsInstallObserver 260 // get a nice nil interface by default 261 var installObserver gadget.ContentObserver 262 trustedInstallObserver, err = boot.TrustedAssetsInstallObserverForModel(model, gadgetDir, useEncryption) 263 if err != nil && err != boot.ErrObserverNotApplicable { 264 return fmt.Errorf("cannot setup asset install observer: %v", err) 265 } 266 if err == nil { 267 installObserver = trustedInstallObserver 268 if !useEncryption { 269 // there will be no key sealing, so past the 270 // installation pass no other methods need to be called 271 trustedInstallObserver = nil 272 } 273 } 274 275 var installedSystem *install.InstalledSystemSideData 276 // run the create partition code 277 logger.Noticef("create and deploy partitions") 278 timings.Run(perfTimings, "install-run", "Install the run system", func(tm timings.Measurer) { 279 st.Unlock() 280 defer st.Lock() 281 installedSystem, err = installRun(model, gadgetDir, kernelDir, "", bopts, installObserver, tm) 282 }) 283 if err != nil { 284 return fmt.Errorf("cannot install system: %v", err) 285 } 286 287 if trustedInstallObserver != nil { 288 // sanity check 289 if installedSystem.KeysForRoles == nil || installedSystem.KeysForRoles[gadget.SystemData] == nil || installedSystem.KeysForRoles[gadget.SystemSave] == nil { 290 return fmt.Errorf("internal error: system encryption keys are unset") 291 } 292 dataKeySet := installedSystem.KeysForRoles[gadget.SystemData] 293 saveKeySet := installedSystem.KeysForRoles[gadget.SystemSave] 294 295 // make note of the encryption keys 296 trustedInstallObserver.ChosenEncryptionKeys(dataKeySet.Key, saveKeySet.Key) 297 298 // keep track of recovery assets 299 if err := trustedInstallObserver.ObserveExistingTrustedRecoveryAssets(boot.InitramfsUbuntuSeedDir); err != nil { 300 return fmt.Errorf("cannot observe existing trusted recovery assets: err") 301 } 302 if err := saveKeys(installedSystem.KeysForRoles); err != nil { 303 return err 304 } 305 // write markers containing a secret to pair data and save 306 if err := writeMarkers(); err != nil { 307 return err 308 } 309 } 310 311 // keep track of the model we installed 312 err = os.MkdirAll(filepath.Join(boot.InitramfsUbuntuBootDir, "device"), 0755) 313 if err != nil { 314 return fmt.Errorf("cannot store the model: %v", err) 315 } 316 err = writeModel(model, filepath.Join(boot.InitramfsUbuntuBootDir, "device/model")) 317 if err != nil { 318 return fmt.Errorf("cannot store the model: %v", err) 319 } 320 321 // configure the run system 322 opts := &sysconfig.Options{TargetRootDir: boot.InstallHostWritableDir, GadgetDir: gadgetDir} 323 // configure cloud init 324 setSysconfigCloudOptions(opts, gadgetDir, model) 325 timings.Run(perfTimings, "sysconfig-configure-target-system", "Configure target system", func(timings.Measurer) { 326 err = sysconfigConfigureTargetSystem(model, opts) 327 }) 328 if err != nil { 329 return err 330 } 331 332 // make it bootable 333 logger.Noticef("make system runnable") 334 bootBaseInfo, err := snapstate.BootBaseInfo(st, deviceCtx) 335 if err != nil { 336 return fmt.Errorf("cannot get boot base info: %v", err) 337 } 338 recoverySystemDir := filepath.Join("/systems", modeEnv.RecoverySystem) 339 bootWith := &boot.BootableSet{ 340 Base: bootBaseInfo, 341 BasePath: bootBaseInfo.MountFile(), 342 Kernel: kernelInfo, 343 KernelPath: kernelInfo.MountFile(), 344 RecoverySystemDir: recoverySystemDir, 345 UnpackedGadgetDir: gadgetDir, 346 } 347 timings.Run(perfTimings, "boot-make-runnable", "Make target system runnable", func(timings.Measurer) { 348 err = bootMakeRunnable(deviceCtx.Model(), bootWith, trustedInstallObserver) 349 }) 350 if err != nil { 351 return fmt.Errorf("cannot make system runnable: %v", err) 352 } 353 354 return nil 355 } 356 357 // writeMarkers writes markers containing the same secret to pair data and save. 358 func writeMarkers() error { 359 // ensure directory for markers exists 360 if err := os.MkdirAll(boot.InstallHostFDEDataDir, 0755); err != nil { 361 return err 362 } 363 if err := os.MkdirAll(boot.InstallHostFDESaveDir, 0755); err != nil { 364 return err 365 } 366 367 // generate a secret random marker 368 markerSecret, err := randutil.CryptoTokenBytes(32) 369 if err != nil { 370 return fmt.Errorf("cannot create ubuntu-data/save marker secret: %v", err) 371 } 372 373 dataMarker := filepath.Join(boot.InstallHostFDEDataDir, "marker") 374 if err := osutil.AtomicWriteFile(dataMarker, markerSecret, 0600, 0); err != nil { 375 return err 376 } 377 378 saveMarker := filepath.Join(boot.InstallHostFDESaveDir, "marker") 379 if err := osutil.AtomicWriteFile(saveMarker, markerSecret, 0600, 0); err != nil { 380 return err 381 } 382 383 return nil 384 } 385 386 func saveKeys(keysForRoles map[string]*install.EncryptionKeySet) error { 387 dataKeySet := keysForRoles[gadget.SystemData] 388 389 // ensure directory for keys exists 390 if err := os.MkdirAll(boot.InstallHostFDEDataDir, 0755); err != nil { 391 return err 392 } 393 394 // Write the recovery key 395 recoveryKeyFile := filepath.Join(boot.InstallHostFDEDataDir, "recovery.key") 396 if err := dataKeySet.RecoveryKey.Save(recoveryKeyFile); err != nil { 397 return fmt.Errorf("cannot store recovery key: %v", err) 398 } 399 400 saveKeySet := keysForRoles[gadget.SystemSave] 401 if saveKeySet == nil { 402 // no system-save support 403 return nil 404 } 405 406 saveKey := filepath.Join(boot.InstallHostFDEDataDir, "ubuntu-save.key") 407 reinstallSaveKey := filepath.Join(boot.InstallHostFDEDataDir, "reinstall.key") 408 409 if err := saveKeySet.Key.Save(saveKey); err != nil { 410 return fmt.Errorf("cannot store system save key: %v", err) 411 } 412 if err := saveKeySet.RecoveryKey.Save(reinstallSaveKey); err != nil { 413 return fmt.Errorf("cannot store reinstall key: %v", err) 414 } 415 return nil 416 } 417 418 var secbootCheckTPMKeySealingSupported = secboot.CheckTPMKeySealingSupported 419 420 // checkEncryption verifies whether encryption should be used based on the 421 // model grade and the availability of a TPM device or a fde-setup hook 422 // in the kernel. 423 func (m *DeviceManager) checkEncryption(st *state.State, deviceCtx snapstate.DeviceContext) (res bool, err error) { 424 model := deviceCtx.Model() 425 secured := model.Grade() == asserts.ModelSecured 426 dangerous := model.Grade() == asserts.ModelDangerous 427 encrypted := model.StorageSafety() == asserts.StorageSafetyEncrypted 428 429 // check if we should disable encryption non-secured devices 430 // TODO:UC20: this is not the final mechanism to bypass encryption 431 if dangerous && osutil.FileExists(filepath.Join(boot.InitramfsUbuntuSeedDir, ".force-unencrypted")) { 432 return false, nil 433 } 434 435 // check if the model prefers to be unencrypted 436 // TODO: provide way to select via install chooser menu 437 // if the install is unencrypted or encrypted 438 if model.StorageSafety() == asserts.StorageSafetyPreferUnencrypted { 439 logger.Noticef(`installing system unencrypted to comply with prefer-unencrypted storage-safety model option`) 440 return false, nil 441 } 442 443 // check if encryption is available 444 var ( 445 hasFDESetupHook bool 446 checkEncryptionErr error 447 ) 448 if kernelInfo, err := snapstate.KernelInfo(st, deviceCtx); err == nil { 449 if hasFDESetupHook = hasFDESetupHookInKernel(kernelInfo); hasFDESetupHook { 450 checkEncryptionErr = m.checkFDEFeatures(st) 451 } 452 } 453 // Note that having a fde-setup hook will disable the build-in 454 // secboot encryption 455 if !hasFDESetupHook { 456 checkEncryptionErr = secbootCheckTPMKeySealingSupported() 457 } 458 459 // check if encryption is required 460 if checkEncryptionErr != nil { 461 if secured { 462 return false, fmt.Errorf("cannot encrypt device storage as mandated by model grade secured: %v", checkEncryptionErr) 463 } 464 if encrypted { 465 return false, fmt.Errorf("cannot encrypt device storage as mandated by encrypted storage-safety model option: %v", checkEncryptionErr) 466 } 467 468 if hasFDESetupHook { 469 logger.Noticef("not encrypting device storage as querying kernel fde-setup hook did not succeed: %v", checkEncryptionErr) 470 } else { 471 logger.Noticef("not encrypting device storage as checking TPM gave: %v", checkEncryptionErr) 472 } 473 474 // not required, go without 475 return false, nil 476 } 477 478 // encrypt 479 return true, nil 480 } 481 482 // RebootOptions can be attached to restart-system-to-run-mode tasks to control 483 // their restart behavior. 484 type RebootOptions struct { 485 Op string `json:"op,omitempty"` 486 } 487 488 const ( 489 RebootHaltOp = "halt" 490 RebootPoweroffOp = "poweroff" 491 ) 492 493 func (m *DeviceManager) doRestartSystemToRunMode(t *state.Task, _ *tomb.Tomb) error { 494 st := t.State() 495 st.Lock() 496 defer st.Unlock() 497 498 perfTimings := state.TimingsForTask(t) 499 defer perfTimings.Save(st) 500 501 modeEnv, err := maybeReadModeenv() 502 if err != nil { 503 return err 504 } 505 506 if modeEnv == nil { 507 return fmt.Errorf("missing modeenv, cannot proceed") 508 } 509 510 // ensure the next boot goes into run mode 511 if err := bootEnsureNextBootToRunMode(modeEnv.RecoverySystem); err != nil { 512 return err 513 } 514 515 var rebootOpts RebootOptions 516 err = t.Get("reboot", &rebootOpts) 517 if err != nil && err != state.ErrNoState { 518 return err 519 } 520 521 // write timing information 522 if err := writeTimings(st, boot.InstallHostWritableDir); err != nil { 523 logger.Noticef("cannot write timings: %v", err) 524 } 525 // store install-mode log into ubuntu-data partition 526 if err := writeLogs(boot.InstallHostWritableDir); err != nil { 527 logger.Noticef("cannot write installation log: %v", err) 528 } 529 530 // request by default a restart as the last action after a 531 // successful install or what install-device requested via 532 // snapctl reboot 533 rst := state.RestartSystemNow 534 what := "restart" 535 switch rebootOpts.Op { 536 case RebootHaltOp: 537 what = "halt" 538 rst = state.RestartSystemHaltNow 539 case RebootPoweroffOp: 540 what = "poweroff" 541 rst = state.RestartSystemPoweroffNow 542 } 543 logger.Noticef("request immediate system %s", what) 544 st.RequestRestart(rst) 545 546 return nil 547 }