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