github.com/chipaca/snappy@v0.0.0-20210104084008-1f06296fe8ad/gadget/gadget.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2019-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 gadget 21 22 import ( 23 "errors" 24 "fmt" 25 "io/ioutil" 26 "os" 27 "path/filepath" 28 "regexp" 29 "sort" 30 "strings" 31 32 "gopkg.in/yaml.v2" 33 34 "github.com/snapcore/snapd/asserts" 35 "github.com/snapcore/snapd/gadget/edition" 36 "github.com/snapcore/snapd/gadget/quantity" 37 "github.com/snapcore/snapd/metautil" 38 "github.com/snapcore/snapd/snap" 39 "github.com/snapcore/snapd/snap/naming" 40 "github.com/snapcore/snapd/strutil" 41 ) 42 43 const ( 44 // schemaMBR identifies a Master Boot Record partitioning schema, or an 45 // MBR like role 46 schemaMBR = "mbr" 47 // schemaGPT identifies a GUID Partition Table partitioning schema 48 schemaGPT = "gpt" 49 50 SystemBoot = "system-boot" 51 SystemData = "system-data" 52 SystemSeed = "system-seed" 53 SystemSave = "system-save" 54 55 // extracted kernels for all uc systems 56 bootImage = "system-boot-image" 57 58 // extracted kernels for recovery kernels for uc20 specifically 59 seedBootImage = "system-seed-image" 60 61 // bootloader specific partition which stores bootloader environment vars 62 // for purposes of booting normal run mode on uc20 and all modes on 63 // uc16 and uc18 64 bootSelect = "system-boot-select" 65 66 // bootloader specific partition which stores bootloader environment vars 67 // for purposes of booting recovery systems on uc20, i.e. recover or install 68 seedBootSelect = "system-seed-select" 69 70 // implicitSystemDataLabel is the implicit filesystem label of structure 71 // of system-data role 72 implicitSystemDataLabel = "writable" 73 74 // UC20 filesystem labels for roles 75 ubuntuBootLabel = "ubuntu-boot" 76 ubuntuSeedLabel = "ubuntu-seed" 77 ubuntuDataLabel = "ubuntu-data" 78 ubuntuSaveLabel = "ubuntu-save" 79 80 // only supported for legacy reasons 81 legacyBootImage = "bootimg" 82 legacyBootSelect = "bootselect" 83 ) 84 85 var ( 86 validVolumeName = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9-]+$") 87 validTypeID = regexp.MustCompile("^[0-9A-F]{2}$") 88 validGUUID = regexp.MustCompile("^(?i)[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$") 89 ) 90 91 type Info struct { 92 Volumes map[string]*Volume `yaml:"volumes,omitempty"` 93 94 // Default configuration for snaps (snap-id => key => value). 95 Defaults map[string]map[string]interface{} `yaml:"defaults,omitempty"` 96 97 Connections []Connection `yaml:"connections"` 98 } 99 100 // Volume defines the structure and content for the image to be written into a 101 // block device. 102 type Volume struct { 103 // Schema describes the schema used for the volume 104 Schema string `yaml:"schema"` 105 // Bootloader names the bootloader used by the volume 106 Bootloader string `yaml:"bootloader"` 107 // ID is a 2-hex digit disk ID or GPT GUID 108 ID string `yaml:"id"` 109 // Structure describes the structures that are part of the volume 110 Structure []VolumeStructure `yaml:"structure"` 111 } 112 113 // VolumeStructure describes a single structure inside a volume. A structure can 114 // represent a partition, Master Boot Record, or any other contiguous range 115 // within the volume. 116 type VolumeStructure struct { 117 // Name, when non empty, provides the name of the structure 118 Name string `yaml:"name"` 119 // Label provides the filesystem label 120 Label string `yaml:"filesystem-label"` 121 // Offset defines a starting offset of the structure 122 Offset *quantity.Offset `yaml:"offset"` 123 // OffsetWrite describes a 32-bit address, within the volume, at which 124 // the offset of current structure will be written. The position may be 125 // specified as a byte offset relative to the start of a named structure 126 OffsetWrite *RelativeOffset `yaml:"offset-write"` 127 // Size of the structure 128 Size quantity.Size `yaml:"size"` 129 // Type of the structure, which can be 2-hex digit MBR partition, 130 // 36-char GUID partition, comma separated <mbr>,<guid> for hybrid 131 // partitioning schemes, or 'bare' when the structure is not considered 132 // a partition. 133 // 134 // For backwards compatibility type 'mbr' is also accepted, and the 135 // structure is treated as if it is of role 'mbr'. 136 Type string `yaml:"type"` 137 // Role describes the role of given structure, can be one of 138 // 'mbr', 'system-data', 'system-boot', 'system-boot-image', 139 // 'system-boot-select' or 'system-recovery-select'. Structures of type 'mbr', must have a 140 // size of 446 bytes and must start at 0 offset. 141 Role string `yaml:"role"` 142 // ID is the GPT partition ID 143 ID string `yaml:"id"` 144 // Filesystem used for the partition, 'vfat', 'ext4' or 'none' for 145 // structures of type 'bare' 146 Filesystem string `yaml:"filesystem"` 147 // Content of the structure 148 Content []VolumeContent `yaml:"content"` 149 Update VolumeUpdate `yaml:"update"` 150 } 151 152 // HasFilesystem returns true if the structure is using a filesystem. 153 func (vs *VolumeStructure) HasFilesystem() bool { 154 return vs.Filesystem != "none" && vs.Filesystem != "" 155 } 156 157 // IsPartition returns true when the structure describes a partition in a block 158 // device. 159 func (vs *VolumeStructure) IsPartition() bool { 160 return vs.Type != "bare" && vs.Role != schemaMBR 161 } 162 163 // VolumeContent defines the contents of the structure. The content can be 164 // either files within a filesystem described by the structure or raw images 165 // written into the area of a bare structure. 166 type VolumeContent struct { 167 // UnresovedSource is the data of the partition relative to 168 // the gadget base directory 169 UnresolvedSource string `yaml:"source"` 170 // Target is the location of the data inside the root filesystem 171 Target string `yaml:"target"` 172 173 // Image names the image, relative to gadget base directory, to be used 174 // for a 'bare' type structure 175 Image string `yaml:"image"` 176 // Offset the image is written at 177 Offset *quantity.Offset `yaml:"offset"` 178 // OffsetWrite describes a 32-bit address, within the volume, at which 179 // the offset of current image will be written. The position may be 180 // specified as a byte offset relative to the start of a named structure 181 OffsetWrite *RelativeOffset `yaml:"offset-write"` 182 // Size of the image, when empty size is calculated by looking at the 183 // image 184 Size quantity.Size `yaml:"size"` 185 186 Unpack bool `yaml:"unpack"` 187 } 188 189 func (vc VolumeContent) ResolvedSource() string { 190 // TODO: implement resolved sources 191 return vc.UnresolvedSource 192 } 193 194 func (vc VolumeContent) String() string { 195 if vc.Image != "" { 196 return fmt.Sprintf("image:%s", vc.Image) 197 } 198 return fmt.Sprintf("source:%s", vc.UnresolvedSource) 199 } 200 201 type VolumeUpdate struct { 202 Edition edition.Number `yaml:"edition"` 203 Preserve []string `yaml:"preserve"` 204 } 205 206 // GadgetConnect describes an interface connection requested by the gadget 207 // between seeded snaps. The syntax is of a mapping like: 208 // 209 // plug: (<plug-snap-id>|system):plug 210 // [slot: (<slot-snap-id>|system):slot] 211 // 212 // "system" indicates a system plug or slot. 213 // Fully omitting the slot part indicates a system slot with the same name 214 // as the plug. 215 type Connection struct { 216 Plug ConnectionPlug `yaml:"plug"` 217 Slot ConnectionSlot `yaml:"slot"` 218 } 219 220 type ConnectionPlug struct { 221 SnapID string 222 Plug string 223 } 224 225 func (gcplug *ConnectionPlug) Empty() bool { 226 return gcplug.SnapID == "" && gcplug.Plug == "" 227 } 228 229 func (gcplug *ConnectionPlug) UnmarshalYAML(unmarshal func(interface{}) error) error { 230 var s string 231 if err := unmarshal(&s); err != nil { 232 return err 233 } 234 snapID, name, err := parseSnapIDColonName(s) 235 if err != nil { 236 return fmt.Errorf("in gadget connection plug: %v", err) 237 } 238 gcplug.SnapID = snapID 239 gcplug.Plug = name 240 return nil 241 } 242 243 type ConnectionSlot struct { 244 SnapID string 245 Slot string 246 } 247 248 func (gcslot *ConnectionSlot) Empty() bool { 249 return gcslot.SnapID == "" && gcslot.Slot == "" 250 } 251 252 func (gcslot *ConnectionSlot) UnmarshalYAML(unmarshal func(interface{}) error) error { 253 var s string 254 if err := unmarshal(&s); err != nil { 255 return err 256 } 257 snapID, name, err := parseSnapIDColonName(s) 258 if err != nil { 259 return fmt.Errorf("in gadget connection slot: %v", err) 260 } 261 gcslot.SnapID = snapID 262 gcslot.Slot = name 263 return nil 264 } 265 266 func parseSnapIDColonName(s string) (snapID, name string, err error) { 267 parts := strings.Split(s, ":") 268 if len(parts) == 2 { 269 snapID = parts[0] 270 name = parts[1] 271 } 272 if snapID == "" || name == "" { 273 return "", "", fmt.Errorf(`expected "(<snap-id>|system):name" not %q`, s) 274 } 275 return snapID, name, nil 276 } 277 278 func systemOrSnapID(s string) bool { 279 if s != "system" && naming.ValidateSnapID(s) != nil { 280 return false 281 } 282 return true 283 } 284 285 // Model carries information about the model that is relevant to gadget. 286 // Note *asserts.Model implements this, and that's the expected use case. 287 type Model interface { 288 Classic() bool 289 Grade() asserts.ModelGrade 290 } 291 292 func classicOrUnconstrained(m Model) bool { 293 return m == nil || m.Classic() 294 } 295 296 func wantsSystemSeed(m Model) bool { 297 return m != nil && m.Grade() != asserts.ModelGradeUnset 298 } 299 300 // InfoFromGadgetYaml parses the provided gadget metadata. 301 // If model is nil only self-consistency checks are performed. 302 // If model is not nil implied values for filesystem labels will be set 303 // as well, based whether the model is for classic, UC16/18 or UC20. 304 // UC gadget metadata is expected to have volumes definitions. 305 func InfoFromGadgetYaml(gadgetYaml []byte, model Model) (*Info, error) { 306 var gi Info 307 308 if err := yaml.Unmarshal(gadgetYaml, &gi); err != nil { 309 return nil, fmt.Errorf("cannot parse gadget metadata: %v", err) 310 } 311 312 for k, v := range gi.Defaults { 313 if !systemOrSnapID(k) { 314 return nil, fmt.Errorf(`default stanza not keyed by "system" or snap-id: %s`, k) 315 } 316 dflt, err := metautil.NormalizeValue(v) 317 if err != nil { 318 return nil, fmt.Errorf("default value %q of %q: %v", v, k, err) 319 } 320 gi.Defaults[k] = dflt.(map[string]interface{}) 321 } 322 323 for i, gconn := range gi.Connections { 324 if gconn.Plug.Empty() { 325 return nil, errors.New("gadget connection plug cannot be empty") 326 } 327 if gconn.Slot.Empty() { 328 gi.Connections[i].Slot.SnapID = "system" 329 gi.Connections[i].Slot.Slot = gconn.Plug.Plug 330 } 331 } 332 333 if len(gi.Volumes) == 0 && classicOrUnconstrained(model) { 334 // volumes can be left out on classic 335 // can still specify defaults though 336 return &gi, nil 337 } 338 339 // basic validation 340 var bootloadersFound int 341 for name, v := range gi.Volumes { 342 if err := validateVolume(name, v, model); err != nil { 343 return nil, fmt.Errorf("invalid volume %q: %v", name, err) 344 } 345 346 switch v.Bootloader { 347 case "": 348 // pass 349 case "grub", "u-boot", "android-boot", "lk": 350 bootloadersFound += 1 351 default: 352 return nil, errors.New("bootloader must be one of grub, u-boot, android-boot or lk") 353 } 354 } 355 switch { 356 case bootloadersFound == 0: 357 return nil, errors.New("bootloader not declared in any volume") 358 case bootloadersFound > 1: 359 return nil, fmt.Errorf("too many (%d) bootloaders declared", bootloadersFound) 360 } 361 362 for name, v := range gi.Volumes { 363 if err := setImplicitForVolume(name, v, model); err != nil { 364 return nil, fmt.Errorf("invalid volume %q: %v", name, err) 365 } 366 } 367 368 /*// XXX non-basic validation, should be done optionally/separately 369 if err := ruleValidateVolumes(gi.Volumes, model); err != nil { 370 return nil, err 371 }*/ 372 373 return &gi, nil 374 } 375 376 type volRuleset int 377 378 const ( 379 volRulesetUnknown volRuleset = iota 380 volRuleset16 381 volRuleset20 382 ) 383 384 func whichVolRuleset(model Model) volRuleset { 385 if model == nil { 386 return volRulesetUnknown 387 } 388 if model.Grade() != asserts.ModelGradeUnset { 389 return volRuleset20 390 } 391 return volRuleset16 392 } 393 394 func setImplicitForVolume(name string, vol *Volume, model Model) error { 395 rs := whichVolRuleset(model) 396 if vol.Schema == "" { 397 // default for schema is gpt 398 vol.Schema = schemaGPT 399 } 400 for i := range vol.Structure { 401 if err := setImplicitForVolumeStructure(&vol.Structure[i], rs); err != nil { 402 return err 403 } 404 } 405 return nil 406 } 407 408 func setImplicitForVolumeStructure(vs *VolumeStructure, rs volRuleset) error { 409 if vs.Role == "" && vs.Type == schemaMBR { 410 vs.Role = schemaMBR 411 return nil 412 } 413 if rs == volRuleset16 && vs.Role == "" && vs.Label == SystemBoot { 414 // legacy behavior, for gadgets that only specify a filesystem-label, eg. pc 415 vs.Role = SystemBoot 416 return nil 417 } 418 if vs.Label == "" { 419 switch { 420 case rs == volRuleset16 && vs.Role == SystemData: 421 vs.Label = implicitSystemDataLabel 422 case rs == volRuleset20 && vs.Role == SystemData: 423 vs.Label = ubuntuDataLabel 424 case rs == volRuleset20 && vs.Role == SystemSeed: 425 vs.Label = ubuntuSeedLabel 426 case rs == volRuleset20 && vs.Role == SystemBoot: 427 vs.Label = ubuntuBootLabel 428 case rs == volRuleset20 && vs.Role == SystemSave: 429 vs.Label = ubuntuSaveLabel 430 } 431 } 432 return nil 433 } 434 435 func readInfo(f func(string) ([]byte, error), gadgetYamlFn string, model Model) (*Info, error) { 436 gmeta, err := f(gadgetYamlFn) 437 if classicOrUnconstrained(model) && os.IsNotExist(err) { 438 // gadget.yaml is optional for classic gadgets 439 return &Info{}, nil 440 } 441 if err != nil { 442 return nil, err 443 } 444 445 return InfoFromGadgetYaml(gmeta, model) 446 } 447 448 // ReadInfo reads the gadget specific metadata from meta/gadget.yaml in the snap 449 // root directory. 450 // See ReadInfoAndValidate for a variant that does role-usage consistency 451 // validation like Validate. 452 func ReadInfo(gadgetSnapRootDir string, model Model) (*Info, error) { 453 gadgetYamlFn := filepath.Join(gadgetSnapRootDir, "meta", "gadget.yaml") 454 ginfo, err := readInfo(ioutil.ReadFile, gadgetYamlFn, model) 455 if err != nil { 456 return nil, err 457 } 458 return ginfo, nil 459 } 460 461 // ReadInfoAndValidate reads the gadget specific metadata from 462 // meta/gadget.yaml in the snap root directory. 463 // It also performs role-usage consistency validation as Validate does 464 // using the given constraints. See ReadInfo for a variant that does not. 465 // See also ValidateContent for further validating the content itself 466 // instead of the metadata. 467 func ReadInfoAndValidate(gadgetSnapRootDir string, model Model, validationConstraints *ValidationConstraints) (*Info, error) { 468 ginfo, err := ReadInfo(gadgetSnapRootDir, model) 469 if err != nil { 470 return nil, err 471 } 472 if err := Validate(ginfo, model, validationConstraints); err != nil { 473 return nil, err 474 } 475 return ginfo, err 476 } 477 478 // ReadInfoFromSnapFile reads the gadget specific metadata from 479 // meta/gadget.yaml in the given snap container. 480 // It also performs role-usage consistency validation as Validate does. 481 // See ReadInfoFromSnapFileNoValidate for a variant that does not. 482 func ReadInfoFromSnapFile(snapf snap.Container, model Model) (*Info, error) { 483 ginfo, err := ReadInfoFromSnapFileNoValidate(snapf, model) 484 if err != nil { 485 return nil, err 486 } 487 if err := Validate(ginfo, model, nil); err != nil { 488 return nil, err 489 } 490 return ginfo, nil 491 } 492 493 // ReadInfoFromSnapFileNoValidate reads the gadget specific metadata from 494 // meta/gadget.yaml in the given snap container. 495 // See ReadInfoFromSnapFile for a variant that does role-usage consistency 496 // validation like Validate as well. 497 func ReadInfoFromSnapFileNoValidate(snapf snap.Container, model Model) (*Info, error) { 498 gadgetYamlFn := "meta/gadget.yaml" 499 ginfo, err := readInfo(snapf.ReadFile, gadgetYamlFn, model) 500 if err != nil { 501 return nil, err 502 } 503 return ginfo, nil 504 } 505 506 func fmtIndexAndName(idx int, name string) string { 507 if name != "" { 508 return fmt.Sprintf("#%v (%q)", idx, name) 509 } 510 return fmt.Sprintf("#%v", idx) 511 } 512 513 func validateVolume(name string, vol *Volume, model Model) error { 514 if !validVolumeName.MatchString(name) { 515 return errors.New("invalid name") 516 } 517 if vol.Schema != "" && vol.Schema != schemaGPT && vol.Schema != schemaMBR { 518 return fmt.Errorf("invalid schema %q", vol.Schema) 519 } 520 521 // named structures, for cross-referencing relative offset-write names 522 knownStructures := make(map[string]*LaidOutStructure, len(vol.Structure)) 523 // for uniqueness of filesystem labels 524 knownFsLabels := make(map[string]bool, len(vol.Structure)) 525 // for validating structure overlap 526 structures := make([]LaidOutStructure, len(vol.Structure)) 527 528 previousEnd := quantity.Offset(0) 529 // TODO: should we also validate that if there is a system-recovery-select 530 // role there should also be at least 2 system-recovery-image roles and 531 // same for system-boot-select and at least 2 system-boot-image roles? 532 for idx, s := range vol.Structure { 533 if err := validateVolumeStructure(&s, vol); err != nil { 534 return fmt.Errorf("invalid structure %v: %v", fmtIndexAndName(idx, s.Name), err) 535 } 536 var start quantity.Offset 537 if s.Offset != nil { 538 start = *s.Offset 539 } else { 540 start = previousEnd 541 } 542 end := start + quantity.Offset(s.Size) 543 ps := LaidOutStructure{ 544 VolumeStructure: &vol.Structure[idx], 545 StartOffset: start, 546 Index: idx, 547 } 548 structures[idx] = ps 549 if s.Name != "" { 550 if _, ok := knownStructures[s.Name]; ok { 551 return fmt.Errorf("structure name %q is not unique", s.Name) 552 } 553 // keep track of named structures 554 knownStructures[s.Name] = &ps 555 } 556 if s.Label != "" { 557 // XXX what about implicit labels 558 if seen := knownFsLabels[s.Label]; seen { 559 return fmt.Errorf("filesystem label %q is not unique", s.Label) 560 } 561 knownFsLabels[s.Label] = true 562 } 563 564 previousEnd = end 565 } 566 567 // sort by starting offset 568 sort.Sort(byStartOffset(structures)) 569 570 return validateCrossVolumeStructure(structures, knownStructures) 571 } 572 573 // isMBR returns whether the structure is the MBR and can be used before setImplicitForVolume 574 func isMBR(vs *VolumeStructure) bool { 575 if vs.Role == schemaMBR { 576 return true 577 } 578 if vs.Role == "" && vs.Type == schemaMBR { 579 return true 580 } 581 return false 582 } 583 584 func validateCrossVolumeStructure(structures []LaidOutStructure, knownStructures map[string]*LaidOutStructure) error { 585 previousEnd := quantity.Offset(0) 586 // cross structure validation: 587 // - relative offsets that reference other structures by name 588 // - laid out structure overlap 589 // use structures laid out within the volume 590 for pidx, ps := range structures { 591 if isMBR(ps.VolumeStructure) { 592 if ps.StartOffset != 0 { 593 return fmt.Errorf(`structure %v has "mbr" role and must start at offset 0`, ps) 594 } 595 } 596 if ps.OffsetWrite != nil && ps.OffsetWrite.RelativeTo != "" { 597 // offset-write using a named structure 598 other := knownStructures[ps.OffsetWrite.RelativeTo] 599 if other == nil { 600 return fmt.Errorf("structure %v refers to an unknown structure %q", 601 ps, ps.OffsetWrite.RelativeTo) 602 } 603 } 604 605 if ps.StartOffset < previousEnd { 606 previous := structures[pidx-1] 607 return fmt.Errorf("structure %v overlaps with the preceding structure %v", ps, previous) 608 } 609 previousEnd = ps.StartOffset + quantity.Offset(ps.Size) 610 611 if ps.HasFilesystem() { 612 // content relative offset only possible if it's a bare structure 613 continue 614 } 615 for cidx, c := range ps.Content { 616 if c.OffsetWrite == nil || c.OffsetWrite.RelativeTo == "" { 617 continue 618 } 619 relativeToStructure := knownStructures[c.OffsetWrite.RelativeTo] 620 if relativeToStructure == nil { 621 return fmt.Errorf("structure %v, content %v refers to an unknown structure %q", 622 ps, fmtIndexAndName(cidx, c.Image), c.OffsetWrite.RelativeTo) 623 } 624 } 625 } 626 return nil 627 } 628 629 func validateVolumeStructure(vs *VolumeStructure, vol *Volume) error { 630 if vs.Size == 0 { 631 return errors.New("missing size") 632 } 633 if err := validateStructureType(vs.Type, vol); err != nil { 634 return fmt.Errorf("invalid type %q: %v", vs.Type, err) 635 } 636 if err := validateRole(vs, vol); err != nil { 637 var what string 638 if vs.Role != "" { 639 what = fmt.Sprintf("role %q", vs.Role) 640 } else { 641 what = fmt.Sprintf("implicit role %q", vs.Type) 642 } 643 return fmt.Errorf("invalid %s: %v", what, err) 644 } 645 if vs.Filesystem != "" && !strutil.ListContains([]string{"ext4", "vfat", "none"}, vs.Filesystem) { 646 return fmt.Errorf("invalid filesystem %q", vs.Filesystem) 647 } 648 649 var contentChecker func(*VolumeContent) error 650 651 if !vs.HasFilesystem() { 652 contentChecker = validateBareContent 653 } else { 654 contentChecker = validateFilesystemContent 655 } 656 for i, c := range vs.Content { 657 if err := contentChecker(&c); err != nil { 658 return fmt.Errorf("invalid content #%v: %v", i, err) 659 } 660 } 661 662 if err := validateStructureUpdate(&vs.Update, vs); err != nil { 663 return err 664 } 665 666 // TODO: validate structure size against sector-size; ubuntu-image uses 667 // a tmp file to find out the default sector size of the device the tmp 668 // file is created on 669 return nil 670 } 671 672 func validateStructureType(s string, vol *Volume) error { 673 // Type can be one of: 674 // - "mbr" (backwards compatible) 675 // - "bare" 676 // - [0-9A-Z]{2} - MBR type 677 // - GPT UUID 678 // - hybrid ID 679 // 680 // Hybrid ID is 2 hex digits of MBR type, followed by 36 GUUID 681 // example: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B 682 683 schema := vol.Schema 684 if schema == "" { 685 schema = schemaGPT 686 } 687 688 if s == "" { 689 return errors.New(`type is not specified`) 690 } 691 692 if s == "bare" { 693 // unknonwn blob 694 return nil 695 } 696 697 if s == schemaMBR { 698 // backward compatibility for type: mbr 699 return nil 700 } 701 702 var isGPT, isMBR bool 703 704 idx := strings.IndexRune(s, ',') 705 if idx == -1 { 706 // just ID 707 switch { 708 case validTypeID.MatchString(s): 709 isMBR = true 710 case validGUUID.MatchString(s): 711 isGPT = true 712 default: 713 return fmt.Errorf("invalid format") 714 } 715 } else { 716 // hybrid ID 717 code := s[:idx] 718 guid := s[idx+1:] 719 if len(code) != 2 || len(guid) != 36 || !validTypeID.MatchString(code) || !validGUUID.MatchString(guid) { 720 return fmt.Errorf("invalid format of hybrid type") 721 } 722 } 723 724 if schema != schemaGPT && isGPT { 725 // type: <uuid> is only valid for GPT volumes 726 return fmt.Errorf("GUID structure type with non-GPT schema %q", vol.Schema) 727 } 728 if schema != schemaMBR && isMBR { 729 return fmt.Errorf("MBR structure type with non-MBR schema %q", vol.Schema) 730 } 731 732 return nil 733 } 734 735 func validateRole(vs *VolumeStructure, vol *Volume) error { 736 if vs.Type == "bare" { 737 if vs.Role != "" && vs.Role != schemaMBR { 738 return fmt.Errorf("conflicting type: %q", vs.Type) 739 } 740 } 741 vsRole := vs.Role 742 if vs.Type == schemaMBR { 743 if vsRole != "" && vsRole != schemaMBR { 744 return fmt.Errorf(`conflicting legacy type: "mbr"`) 745 } 746 // backward compatibility 747 vsRole = schemaMBR 748 } 749 750 switch vsRole { 751 case SystemData, SystemSeed, SystemSave: 752 // roles have cross dependencies, consistency checks are done at 753 // the volume level 754 case schemaMBR: 755 if vs.Size > SizeMBR { 756 return errors.New("mbr structures cannot be larger than 446 bytes") 757 } 758 if vs.Offset != nil && *vs.Offset != 0 { 759 return errors.New("mbr structure must start at offset 0") 760 } 761 if vs.ID != "" { 762 return errors.New("mbr structure must not specify partition ID") 763 } 764 if vs.Filesystem != "" && vs.Filesystem != "none" { 765 return errors.New("mbr structures must not specify a file system") 766 } 767 case SystemBoot, bootImage, bootSelect, seedBootSelect, seedBootImage, "": 768 // noop 769 case legacyBootImage, legacyBootSelect: 770 // noop 771 // legacy role names were added in 2.42 can be removed 772 // on snapd epoch bump 773 default: 774 return fmt.Errorf("unsupported role") 775 } 776 return nil 777 } 778 779 func validateBareContent(vc *VolumeContent) error { 780 if vc.UnresolvedSource != "" || vc.Target != "" { 781 return fmt.Errorf("cannot use non-image content for bare file system") 782 } 783 if vc.Image == "" { 784 return fmt.Errorf("missing image file name") 785 } 786 return nil 787 } 788 789 func validateFilesystemContent(vc *VolumeContent) error { 790 if vc.Image != "" || vc.Offset != nil || vc.OffsetWrite != nil || vc.Size != 0 { 791 return fmt.Errorf("cannot use image content for non-bare file system") 792 } 793 if vc.UnresolvedSource == "" || vc.Target == "" { 794 return fmt.Errorf("missing source or target") 795 } 796 return nil 797 } 798 799 func validateStructureUpdate(up *VolumeUpdate, vs *VolumeStructure) error { 800 if !vs.HasFilesystem() && len(vs.Update.Preserve) > 0 { 801 return errors.New("preserving files during update is not supported for non-filesystem structures") 802 } 803 804 names := make(map[string]bool, len(vs.Update.Preserve)) 805 for _, n := range vs.Update.Preserve { 806 if names[n] { 807 return fmt.Errorf(`duplicate "preserve" entry %q`, n) 808 } 809 names[n] = true 810 } 811 return nil 812 } 813 814 const ( 815 // SizeMBR is the maximum byte size of a structure of role 'mbr' 816 SizeMBR = quantity.Size(446) 817 // SizeLBA48Pointer is the byte size of a pointer value written at the 818 // location described by 'offset-write' 819 SizeLBA48Pointer = quantity.Size(4) 820 ) 821 822 // RelativeOffset describes an offset where structure data is written at. 823 // The position can be specified as byte-offset relative to the start of another 824 // named structure. 825 type RelativeOffset struct { 826 // RelativeTo names the structure relative to which the location of the 827 // address write will be calculated. 828 RelativeTo string 829 // Offset is a 32-bit value 830 Offset quantity.Offset 831 } 832 833 func (r *RelativeOffset) String() string { 834 if r == nil { 835 return "unspecified" 836 } 837 if r.RelativeTo != "" { 838 return fmt.Sprintf("%s+%d", r.RelativeTo, r.Offset) 839 } 840 return fmt.Sprintf("%d", r.Offset) 841 } 842 843 // parseRelativeOffset parses a string describing an offset that can be 844 // expressed relative to a named structure, with the format: [<name>+]<offset>. 845 func parseRelativeOffset(grs string) (*RelativeOffset, error) { 846 toWhat := "" 847 offsSpec := grs 848 if idx := strings.IndexRune(grs, '+'); idx != -1 { 849 toWhat, offsSpec = grs[:idx], grs[idx+1:] 850 if toWhat == "" { 851 return nil, errors.New("missing volume name") 852 } 853 } 854 if offsSpec == "" { 855 return nil, errors.New("missing offset") 856 } 857 858 offset, err := quantity.ParseOffset(offsSpec) 859 if err != nil { 860 return nil, fmt.Errorf("cannot parse offset %q: %v", offsSpec, err) 861 } 862 if offset > 4*1024*quantity.OffsetMiB { 863 return nil, fmt.Errorf("offset above 4G limit") 864 } 865 866 return &RelativeOffset{ 867 RelativeTo: toWhat, 868 Offset: offset, 869 }, nil 870 } 871 872 func (s *RelativeOffset) UnmarshalYAML(unmarshal func(interface{}) error) error { 873 var grs string 874 if err := unmarshal(&grs); err != nil { 875 return errors.New(`cannot unmarshal gadget relative offset`) 876 } 877 878 ro, err := parseRelativeOffset(grs) 879 if err != nil { 880 return fmt.Errorf("cannot parse relative offset %q: %v", grs, err) 881 } 882 *s = *ro 883 return nil 884 } 885 886 // IsCompatible checks whether the current and an update are compatible. Returns 887 // nil or an error describing the incompatibility. 888 func IsCompatible(current, new *Info) error { 889 // XXX: the only compatibility we have now is making sure that the new 890 // layout can be used on an existing volume 891 if len(new.Volumes) > 1 { 892 return fmt.Errorf("gadgets with multiple volumes are unsupported") 893 } 894 895 // XXX: the code below errors out with more than 1 volume in the current 896 // gadget, we allow this scenario in update but better bail out here and 897 // have users fix their gadgets 898 currentVol, newVol, err := resolveVolume(current, new) 899 if err != nil { 900 return err 901 } 902 903 if currentVol.Schema == "" || newVol.Schema == "" { 904 return fmt.Errorf("internal error: unset volume schemas: old: %q new: %q", currentVol.Schema, newVol.Schema) 905 } 906 907 // layout both volumes partially, without going deep into the layout of 908 // structure content, we only want to make sure that structures are 909 // comapatible 910 pCurrent, err := LayoutVolumePartially(currentVol, defaultConstraints) 911 if err != nil { 912 return fmt.Errorf("cannot lay out the current volume: %v", err) 913 } 914 pNew, err := LayoutVolumePartially(newVol, defaultConstraints) 915 if err != nil { 916 return fmt.Errorf("cannot lay out the new volume: %v", err) 917 } 918 if err := isLayoutCompatible(pCurrent, pNew); err != nil { 919 return fmt.Errorf("incompatible layout change: %v", err) 920 } 921 return nil 922 } 923 924 // LaidOutVolumeFromGadget takes a gadget rootdir and lays out the 925 // partitions as specified. 926 func LaidOutVolumeFromGadget(gadgetRoot string, model Model) (*LaidOutVolume, error) { 927 info, err := ReadInfo(gadgetRoot, model) 928 if err != nil { 929 return nil, err 930 } 931 // Limit ourselves to just one volume for now. 932 if len(info.Volumes) != 1 { 933 return nil, fmt.Errorf("cannot position multiple volumes yet") 934 } 935 936 constraints := LayoutConstraints{ 937 NonMBRStartOffset: 1 * quantity.OffsetMiB, 938 SectorSize: 512, 939 } 940 941 for _, vol := range info.Volumes { 942 pvol, err := LayoutVolume(gadgetRoot, vol, constraints) 943 if err != nil { 944 return nil, err 945 } 946 // we know info.Volumes map has size 1 so we can return here 947 return pvol, nil 948 } 949 return nil, fmt.Errorf("internal error in PositionedVolumeFromGadget: this line cannot be reached") 950 } 951 952 func flatten(path string, cfg interface{}, out map[string]interface{}) { 953 if cfgMap, ok := cfg.(map[string]interface{}); ok { 954 for k, v := range cfgMap { 955 p := k 956 if path != "" { 957 p = path + "." + k 958 } 959 flatten(p, v, out) 960 } 961 } else { 962 out[path] = cfg 963 } 964 } 965 966 // SystemDefaults returns default system configuration from gadget defaults. 967 func SystemDefaults(gadgetDefaults map[string]map[string]interface{}) map[string]interface{} { 968 for _, systemSnap := range []string{"system", naming.WellKnownSnapID("core")} { 969 if defaults, ok := gadgetDefaults[systemSnap]; ok { 970 coreDefaults := map[string]interface{}{} 971 flatten("", defaults, coreDefaults) 972 return coreDefaults 973 } 974 } 975 return nil 976 }