gitee.com/mysnapcore/mysnapd@v0.1.0/image/image_linux.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2014-2022 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 image 21 22 import ( 23 "fmt" 24 "io" 25 "io/ioutil" 26 "os" 27 "path/filepath" 28 "strings" 29 "syscall" 30 "time" 31 32 "gitee.com/mysnapcore/mysnapd/asserts" 33 "gitee.com/mysnapcore/mysnapd/asserts/sysdb" 34 "gitee.com/mysnapcore/mysnapd/boot" 35 "gitee.com/mysnapcore/mysnapd/dirs" 36 "gitee.com/mysnapcore/mysnapd/gadget" 37 "gitee.com/mysnapcore/mysnapd/store/tooling" 38 39 // to set sysconfig.ApplyFilesystemOnlyDefaults hook 40 "gitee.com/mysnapcore/mysnapd/image/preseed" 41 "gitee.com/mysnapcore/mysnapd/osutil" 42 _ "gitee.com/mysnapcore/mysnapd/overlord/configstate/configcore" 43 "gitee.com/mysnapcore/mysnapd/release" 44 "gitee.com/mysnapcore/mysnapd/seed/seedwriter" 45 "gitee.com/mysnapcore/mysnapd/snap" 46 "gitee.com/mysnapcore/mysnapd/snap/snapfile" 47 "gitee.com/mysnapcore/mysnapd/snap/squashfs" 48 "gitee.com/mysnapcore/mysnapd/strutil" 49 "gitee.com/mysnapcore/mysnapd/sysconfig" 50 ) 51 52 var ( 53 Stdout io.Writer = os.Stdout 54 Stderr io.Writer = os.Stderr 55 56 preseedCore20 = preseed.Core20 57 ) 58 59 func (custo *Customizations) validate(model *asserts.Model) error { 60 core20 := model.Grade() != asserts.ModelGradeUnset 61 var unsupported []string 62 unsupportedConsoleConfDisable := func() { 63 if custo.ConsoleConf == "disabled" { 64 unsupported = append(unsupported, "console-conf disable") 65 } 66 } 67 unsupportedBootFlags := func() { 68 if len(custo.BootFlags) != 0 { 69 unsupported = append(unsupported, fmt.Sprintf("boot flags (%s)", strings.Join(custo.BootFlags, " "))) 70 } 71 } 72 73 kind := "UC16/18" 74 switch { 75 case core20: 76 kind = "UC20+" 77 // TODO:UC20: consider supporting these with grade dangerous? 78 unsupportedConsoleConfDisable() 79 if custo.CloudInitUserData != "" { 80 unsupported = append(unsupported, "cloud-init user-data") 81 } 82 case model.Classic(): 83 kind = "classic" 84 unsupportedConsoleConfDisable() 85 unsupportedBootFlags() 86 default: 87 // UC16/18 88 unsupportedBootFlags() 89 } 90 if len(unsupported) != 0 { 91 return fmt.Errorf("cannot support with %s model requested customizations: %s", kind, strings.Join(unsupported, ", ")) 92 } 93 return nil 94 } 95 96 // classicHasSnaps returns whether the model or options specify any snaps for the classic case 97 func classicHasSnaps(model *asserts.Model, opts *Options) bool { 98 return model.Gadget() != "" || len(model.RequiredNoEssentialSnaps()) != 0 || len(opts.Snaps) != 0 99 } 100 101 var newToolingStoreFromModel = tooling.NewToolingStoreFromModel 102 103 func Prepare(opts *Options) error { 104 var model *asserts.Model 105 var err error 106 if opts.Classic && opts.ModelFile == "" { 107 // ubuntu-image has a use case for preseeding snaps in an arbitrary rootfs 108 // using its --filesystem flag. This rootfs may or may not already have 109 // snaps preseeded in it. In the case where the provided rootfs has no 110 // snaps seeded image.Prepare will be called with no model assertion, 111 // and we then use the GenericClassicModel. 112 model = sysdb.GenericClassicModel() 113 } else { 114 model, err = decodeModelAssertion(opts) 115 if err != nil { 116 return err 117 } 118 } 119 120 if model.Architecture() != "" && opts.Architecture != "" && model.Architecture() != opts.Architecture { 121 return fmt.Errorf("cannot override model architecture: %s", model.Architecture()) 122 } 123 124 if !opts.Classic { 125 if model.Classic() { 126 return fmt.Errorf("--classic mode is required to prepare the image for a classic model") 127 } 128 } else { 129 if !model.Classic() { 130 return fmt.Errorf("cannot prepare the image for a core model with --classic mode specified") 131 } 132 if model.Architecture() == "" && classicHasSnaps(model, opts) && opts.Architecture == "" { 133 return fmt.Errorf("cannot have snaps for a classic image without an architecture in the model or from --arch") 134 } 135 } 136 137 tsto, err := newToolingStoreFromModel(model, opts.Architecture) 138 if err != nil { 139 return err 140 } 141 tsto.Stdout = Stdout 142 143 // FIXME: limitation until we can pass series parametrized much more 144 if model.Series() != release.Series { 145 return fmt.Errorf("model with series %q != %q unsupported", model.Series(), release.Series) 146 } 147 148 if err := opts.Customizations.validate(model); err != nil { 149 return err 150 } 151 152 if err := setupSeed(tsto, model, opts); err != nil { 153 return err 154 } 155 156 if opts.Preseed { 157 // TODO: support UC22 158 if model.Classic() { 159 return fmt.Errorf("cannot preseed the image for a classic model") 160 } 161 if model.Base() != "core20" { 162 return fmt.Errorf("cannot preseed the image for a model other than core20") 163 } 164 return preseedCore20(opts.PrepareDir, opts.PreseedSignKey, opts.AppArmorKernelFeaturesDir) 165 } 166 167 return nil 168 } 169 170 // these are postponed, not implemented or abandoned, not finalized, 171 // don't let them sneak in into a used model assertion 172 var reserved = []string{"core", "os", "class", "allowed-modes"} 173 174 func decodeModelAssertion(opts *Options) (*asserts.Model, error) { 175 fn := opts.ModelFile 176 177 rawAssert, err := ioutil.ReadFile(fn) 178 if err != nil { 179 return nil, fmt.Errorf("cannot read model assertion: %s", err) 180 } 181 182 ass, err := asserts.Decode(rawAssert) 183 if err != nil { 184 return nil, fmt.Errorf("cannot decode model assertion %q: %s", fn, err) 185 } 186 modela, ok := ass.(*asserts.Model) 187 if !ok { 188 return nil, fmt.Errorf("assertion in %q is not a model assertion", fn) 189 } 190 191 for _, rsvd := range reserved { 192 if modela.Header(rsvd) != nil { 193 return nil, fmt.Errorf("model assertion cannot have reserved/unsupported header %q set", rsvd) 194 } 195 } 196 197 return modela, nil 198 } 199 200 func unpackSnap(gadgetFname, gadgetUnpackDir string) error { 201 // FIXME: jumping through layers here, we need to make 202 // unpack part of the container interface (again) 203 snap := squashfs.New(gadgetFname) 204 return snap.Unpack("*", gadgetUnpackDir) 205 } 206 207 func installCloudConfig(rootDir, gadgetDir string) error { 208 cloudConfig := filepath.Join(gadgetDir, "cloud.conf") 209 if !osutil.FileExists(cloudConfig) { 210 return nil 211 } 212 213 cloudDir := filepath.Join(rootDir, "/etc/cloud") 214 if err := os.MkdirAll(cloudDir, 0755); err != nil { 215 return err 216 } 217 dst := filepath.Join(cloudDir, "cloud.cfg") 218 return osutil.CopyFile(cloudConfig, dst, osutil.CopyFlagOverwrite) 219 } 220 221 func customizeImage(rootDir, defaultsDir string, custo *Customizations) error { 222 // customize with cloud-init user-data 223 if custo.CloudInitUserData != "" { 224 // See 225 // https://cloudinit.readthedocs.io/en/latest/topics/dir_layout.html 226 // https://cloudinit.readthedocs.io/en/latest/topics/datasources/nocloud.html 227 varCloudDir := filepath.Join(rootDir, "/var/lib/cloud/seed/nocloud-net") 228 if err := os.MkdirAll(varCloudDir, 0755); err != nil { 229 return err 230 } 231 if err := ioutil.WriteFile(filepath.Join(varCloudDir, "meta-data"), []byte("instance-id: nocloud-static\n"), 0644); err != nil { 232 return err 233 } 234 dst := filepath.Join(varCloudDir, "user-data") 235 if err := osutil.CopyFile(custo.CloudInitUserData, dst, osutil.CopyFlagOverwrite); err != nil { 236 return err 237 } 238 } 239 240 if custo.ConsoleConf == "disabled" { 241 // TODO: maybe share code with configcore somehow 242 consoleConfDisabled := filepath.Join(defaultsDir, "/var/lib/console-conf/complete") 243 if err := os.MkdirAll(filepath.Dir(consoleConfDisabled), 0755); err != nil { 244 return err 245 } 246 if err := ioutil.WriteFile(consoleConfDisabled, []byte("console-conf has been disabled by image customization\n"), 0644); err != nil { 247 return err 248 } 249 } 250 251 return nil 252 } 253 254 var trusted = sysdb.Trusted() 255 256 func MockTrusted(mockTrusted []asserts.Assertion) (restore func()) { 257 prevTrusted := trusted 258 trusted = mockTrusted 259 return func() { 260 trusted = prevTrusted 261 } 262 } 263 264 func makeLabel(now time.Time) string { 265 return now.UTC().Format("20060102") 266 } 267 268 var setupSeed = func(tsto *tooling.ToolingStore, model *asserts.Model, opts *Options) error { 269 if model.Classic() != opts.Classic { 270 return fmt.Errorf("internal error: classic model but classic mode not set") 271 } 272 273 core20 := model.Grade() != asserts.ModelGradeUnset 274 var rootDir string 275 var bootRootDir string 276 var seedDir string 277 var label string 278 if !core20 { 279 if opts.Classic { 280 // Classic, PrepareDir is the root dir itself 281 rootDir = opts.PrepareDir 282 } else { 283 // Core 16/18, writing for the writeable partition 284 rootDir = filepath.Join(opts.PrepareDir, "image") 285 bootRootDir = rootDir 286 } 287 seedDir = dirs.SnapSeedDirUnder(rootDir) 288 289 // validity check target 290 if osutil.FileExists(dirs.SnapStateFileUnder(rootDir)) { 291 return fmt.Errorf("cannot prepare seed over existing system or an already booted image, detected state file %s", dirs.SnapStateFileUnder(rootDir)) 292 } 293 if snaps, _ := filepath.Glob(filepath.Join(dirs.SnapBlobDirUnder(rootDir), "*.snap")); len(snaps) > 0 { 294 return fmt.Errorf("expected empty snap dir in rootdir, got: %v", snaps) 295 } 296 297 } else { 298 // Core 20, writing for the system-seed partition 299 seedDir = filepath.Join(opts.PrepareDir, "system-seed") 300 label = makeLabel(time.Now()) 301 bootRootDir = seedDir 302 303 // validity check target 304 if systems, _ := filepath.Glob(filepath.Join(seedDir, "systems", "*")); len(systems) > 0 { 305 return fmt.Errorf("expected empty systems dir in system-seed, got: %v", systems) 306 } 307 } 308 309 // TODO: developer database in home or use snapd (but need 310 // a bit more API there, potential issues when crossing stores/series) 311 db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ 312 Backstore: asserts.NewMemoryBackstore(), 313 Trusted: trusted, 314 }) 315 if err != nil { 316 return err 317 } 318 319 wOpts := &seedwriter.Options{ 320 SeedDir: seedDir, 321 Label: label, 322 DefaultChannel: opts.Channel, 323 324 TestSkipCopyUnverifiedModel: osutil.GetenvBool("UBUNTU_IMAGE_SKIP_COPY_UNVERIFIED_MODEL"), 325 } 326 327 w, err := seedwriter.New(model, wOpts) 328 if err != nil { 329 return err 330 } 331 332 optSnaps := make([]*seedwriter.OptionsSnap, 0, len(opts.Snaps)) 333 for _, snapName := range opts.Snaps { 334 var optSnap seedwriter.OptionsSnap 335 if strings.HasSuffix(snapName, ".snap") { 336 // local 337 optSnap.Path = snapName 338 } else { 339 optSnap.Name = snapName 340 } 341 optSnap.Channel = opts.SnapChannels[snapName] 342 optSnaps = append(optSnaps, &optSnap) 343 } 344 345 if err := w.SetOptionsSnaps(optSnaps); err != nil { 346 return err 347 } 348 349 var gadgetUnpackDir, kernelUnpackDir string 350 // create directory for later unpacking the gadget in 351 if !opts.Classic { 352 gadgetUnpackDir = filepath.Join(opts.PrepareDir, "gadget") 353 kernelUnpackDir = filepath.Join(opts.PrepareDir, "kernel") 354 for _, unpackDir := range []string{gadgetUnpackDir, kernelUnpackDir} { 355 if err := os.MkdirAll(unpackDir, 0755); err != nil { 356 return fmt.Errorf("cannot create unpack dir %q: %s", unpackDir, err) 357 } 358 } 359 } 360 361 newFetcher := func(save func(asserts.Assertion) error) asserts.Fetcher { 362 return tsto.AssertionFetcher(db, save) 363 } 364 f, err := w.Start(db, newFetcher) 365 if err != nil { 366 return err 367 } 368 369 if opts.Customizations.Validation == "" && !opts.Classic { 370 fmt.Fprintf(Stderr, "WARNING: proceeding to download snaps ignoring validations, this default will change in the future. For now use --validation=enforce for validations to be taken into account, pass instead --validation=ignore to preserve current behavior going forward\n") 371 } 372 if opts.Customizations.Validation == "" { 373 opts.Customizations.Validation = "ignore" 374 } 375 376 localSnaps, err := w.LocalSnaps() 377 if err != nil { 378 return err 379 } 380 381 var curSnaps []*tooling.CurrentSnap 382 for _, sn := range localSnaps { 383 si, aRefs, err := seedwriter.DeriveSideInfo(sn.Path, model, f, db) 384 if err != nil && !asserts.IsNotFound(err) { 385 return err 386 } 387 388 snapFile, err := snapfile.Open(sn.Path) 389 if err != nil { 390 return err 391 } 392 info, err := snap.ReadInfoFromSnapFile(snapFile, si) 393 if err != nil { 394 return err 395 } 396 397 if err := w.SetInfo(sn, info); err != nil { 398 return err 399 } 400 sn.ARefs = aRefs 401 402 if info.ID() != "" { 403 curSnaps = append(curSnaps, &tooling.CurrentSnap{ 404 SnapName: info.SnapName(), 405 SnapID: info.ID(), 406 Revision: info.Revision, 407 Epoch: info.Epoch, 408 }) 409 } 410 } 411 412 if err := w.InfoDerived(); err != nil { 413 return err 414 } 415 416 for { 417 toDownload, err := w.SnapsToDownload() 418 if err != nil { 419 return err 420 } 421 422 byName := make(map[string]*seedwriter.SeedSnap, len(toDownload)) 423 beforeDownload := func(info *snap.Info) (string, error) { 424 sn := byName[info.SnapName()] 425 if sn == nil { 426 return "", fmt.Errorf("internal error: downloading unexpected snap %q", info.SnapName()) 427 } 428 fmt.Fprintf(Stdout, "Fetching %s\n", sn.SnapName()) 429 if err := w.SetInfo(sn, info); err != nil { 430 return "", err 431 } 432 return sn.Path, nil 433 } 434 snapToDownloadOptions := make([]tooling.SnapToDownload, len(toDownload)) 435 for i, sn := range toDownload { 436 byName[sn.SnapName()] = sn 437 snapToDownloadOptions[i].Snap = sn 438 snapToDownloadOptions[i].Channel = sn.Channel 439 snapToDownloadOptions[i].CohortKey = opts.WideCohortKey 440 } 441 downloadedSnaps, err := tsto.DownloadMany(snapToDownloadOptions, curSnaps, tooling.DownloadManyOptions{ 442 BeforeDownloadFunc: beforeDownload, 443 EnforceValidation: opts.Customizations.Validation == "enforce", 444 }) 445 if err != nil { 446 return err 447 } 448 449 for _, sn := range toDownload { 450 dlsn := downloadedSnaps[sn.SnapName()] 451 452 if err := w.SetRedirectChannel(sn, dlsn.RedirectChannel); err != nil { 453 return err 454 } 455 456 // fetch snap assertions 457 prev := len(f.Refs()) 458 if _, err = FetchAndCheckSnapAssertions(dlsn.Path, dlsn.Info, model, f, db); err != nil { 459 return err 460 } 461 aRefs := f.Refs()[prev:] 462 sn.ARefs = aRefs 463 464 curSnaps = append(curSnaps, &tooling.CurrentSnap{ 465 SnapName: sn.Info.SnapName(), 466 SnapID: sn.Info.ID(), 467 Revision: sn.Info.Revision, 468 Epoch: sn.Info.Epoch, 469 Channel: sn.Channel, 470 }) 471 } 472 473 complete, err := w.Downloaded() 474 if err != nil { 475 return err 476 } 477 if complete { 478 break 479 } 480 } 481 482 for _, warn := range w.Warnings() { 483 fmt.Fprintf(Stderr, "WARNING: %s\n", warn) 484 } 485 486 unassertedSnaps, err := w.UnassertedSnaps() 487 if err != nil { 488 return err 489 } 490 if len(unassertedSnaps) > 0 { 491 locals := make([]string, len(unassertedSnaps)) 492 for i, sn := range unassertedSnaps { 493 locals[i] = sn.SnapName() 494 } 495 fmt.Fprintf(Stderr, "WARNING: %s installed from local snaps disconnected from a store cannot be refreshed subsequently!\n", strutil.Quoted(locals)) 496 } 497 498 copySnap := func(name, src, dst string) error { 499 fmt.Fprintf(Stdout, "Copying %q (%s)\n", src, name) 500 return osutil.CopyFile(src, dst, 0) 501 } 502 if err := w.SeedSnaps(copySnap); err != nil { 503 return err 504 } 505 506 if err := w.WriteMeta(); err != nil { 507 return err 508 } 509 510 // TODO: There will be classic UC20+ model based systems 511 // that will have a bootable ubuntu-seed partition. 512 // This will need to be handled here eventually too. 513 if opts.Classic { 514 var fpath string 515 if core20 { 516 fpath = filepath.Join(seedDir, "systems") 517 } else { 518 fpath = filepath.Join(seedDir, "seed.yaml") 519 } 520 // warn about ownership if not root:root 521 fi, err := os.Stat(fpath) 522 if err != nil { 523 return fmt.Errorf("cannot stat %q: %s", fpath, err) 524 } 525 if st, ok := fi.Sys().(*syscall.Stat_t); ok { 526 if st.Uid != 0 || st.Gid != 0 { 527 fmt.Fprintf(Stderr, "WARNING: ensure that the contents under %s are owned by root:root in the (final) image\n", seedDir) 528 } 529 } 530 // done already 531 return nil 532 } 533 534 bootSnaps, err := w.BootSnaps() 535 if err != nil { 536 return err 537 } 538 539 bootWith := &boot.BootableSet{ 540 UnpackedGadgetDir: gadgetUnpackDir, 541 Recovery: core20, 542 } 543 if label != "" { 544 bootWith.RecoverySystemDir = filepath.Join("/systems/", label) 545 bootWith.RecoverySystemLabel = label 546 } 547 548 // find the snap.Info/path for kernel/os/base/gadget so 549 // that boot.MakeBootable can DTRT 550 kernelFname := "" 551 for _, sn := range bootSnaps { 552 switch sn.Info.Type() { 553 case snap.TypeGadget: 554 bootWith.Gadget = sn.Info 555 bootWith.GadgetPath = sn.Path 556 case snap.TypeOS, snap.TypeBase: 557 bootWith.Base = sn.Info 558 bootWith.BasePath = sn.Path 559 case snap.TypeKernel: 560 bootWith.Kernel = sn.Info 561 bootWith.KernelPath = sn.Path 562 kernelFname = sn.Path 563 } 564 } 565 566 // unpacking the gadget for core models 567 if err := unpackSnap(bootWith.GadgetPath, gadgetUnpackDir); err != nil { 568 return err 569 } 570 if err := unpackSnap(kernelFname, kernelUnpackDir); err != nil { 571 return err 572 } 573 574 gadgetInfo, err := gadget.ReadInfoAndValidate(gadgetUnpackDir, model, nil) 575 if err != nil { 576 return err 577 } 578 // validate content against the kernel as well 579 if err := gadget.ValidateContent(gadgetInfo, gadgetUnpackDir, kernelUnpackDir); err != nil { 580 return err 581 } 582 583 // write resolved content to structure root 584 if err := writeResolvedContent(opts.PrepareDir, gadgetInfo, gadgetUnpackDir, kernelUnpackDir); err != nil { 585 return err 586 } 587 588 if err := boot.MakeBootableImage(model, bootRootDir, bootWith, opts.Customizations.BootFlags); err != nil { 589 return err 590 } 591 592 // early config & cloud-init config (done at install for Core 20) 593 if !core20 { 594 // and the cloud-init things 595 if err := installCloudConfig(rootDir, gadgetUnpackDir); err != nil { 596 return err 597 } 598 599 defaultsDir := sysconfig.WritableDefaultsDir(rootDir) 600 defaults := gadget.SystemDefaults(gadgetInfo.Defaults) 601 if len(defaults) > 0 { 602 if err := os.MkdirAll(sysconfig.WritableDefaultsDir(rootDir, "/etc"), 0755); err != nil { 603 return err 604 } 605 return sysconfig.ApplyFilesystemOnlyDefaults(model, defaultsDir, defaults) 606 } 607 608 customizeImage(rootDir, defaultsDir, &opts.Customizations) 609 } 610 611 return nil 612 }