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  }