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