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  }