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