github.com/chipaca/snappy@v0.0.0-20210104084008-1f06296fe8ad/gadget/install/install_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  // +build !nosecboot
     3  
     4  /*
     5   * Copyright (C) 2019-2020 Canonical Ltd
     6   *
     7   * This program is free software: you can redistribute it and/or modify
     8   * it under the terms of the GNU General Public License version 3 as
     9   * published by the Free Software Foundation.
    10   *
    11   * This program is distributed in the hope that it will be useful,
    12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14   * GNU General Public License for more details.
    15   *
    16   * You should have received a copy of the GNU General Public License
    17   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18   *
    19   */
    20  
    21  package install_test
    22  
    23  import (
    24  	"io/ioutil"
    25  	"os"
    26  	"path/filepath"
    27  
    28  	. "gopkg.in/check.v1"
    29  
    30  	"github.com/snapcore/snapd/dirs"
    31  	"github.com/snapcore/snapd/gadget"
    32  	"github.com/snapcore/snapd/gadget/install"
    33  	"github.com/snapcore/snapd/gadget/quantity"
    34  	"github.com/snapcore/snapd/testutil"
    35  )
    36  
    37  type installSuite struct {
    38  	testutil.BaseTest
    39  
    40  	dir string
    41  }
    42  
    43  var _ = Suite(&installSuite{})
    44  
    45  // XXX: write a very high level integration like test here that
    46  // mocks the world (sfdisk,lsblk,mkfs,...)? probably silly as
    47  // each part inside bootstrap is tested and we have a spread test
    48  
    49  func (s *installSuite) SetUpTest(c *C) {
    50  	s.BaseTest.SetUpTest(c)
    51  
    52  	s.dir = c.MkDir()
    53  	dirs.SetRootDir(s.dir)
    54  	s.AddCleanup(func() { dirs.SetRootDir("/") })
    55  }
    56  
    57  func (s *installSuite) TestInstallRunError(c *C) {
    58  	sys, err := install.Run(nil, "", "", install.Options{}, nil)
    59  	c.Assert(err, ErrorMatches, "cannot use empty gadget root directory")
    60  	c.Check(sys, IsNil)
    61  }
    62  
    63  const mockGadgetYaml = `volumes:
    64    pc:
    65      bootloader: grub
    66      structure:
    67        - name: mbr
    68          type: mbr
    69          size: 440
    70        - name: BIOS Boot
    71          type: DA,21686148-6449-6E6F-744E-656564454649
    72          size: 1M
    73          offset: 1M
    74          offset-write: mbr+92
    75  `
    76  
    77  const mockExtraStructure = `
    78        - name: Writable
    79          role: system-data
    80          filesystem-label: writable
    81          filesystem: ext4
    82          type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4
    83          size: 1200M
    84  `
    85  
    86  var mockDeviceLayout = gadget.OnDiskVolume{
    87  	Structure: []gadget.OnDiskStructure{
    88  		{
    89  			LaidOutStructure: gadget.LaidOutStructure{
    90  				VolumeStructure: &gadget.VolumeStructure{
    91  					Name: "mbr",
    92  					Size: 440,
    93  				},
    94  				StartOffset: 0,
    95  			},
    96  			Node: "/dev/node1",
    97  		},
    98  		{
    99  			LaidOutStructure: gadget.LaidOutStructure{
   100  				VolumeStructure: &gadget.VolumeStructure{
   101  					Name: "BIOS Boot",
   102  					Size: 1 * quantity.SizeMiB,
   103  				},
   104  				StartOffset: 1 * quantity.OffsetMiB,
   105  			},
   106  			Node: "/dev/node2",
   107  		},
   108  	},
   109  	ID:         "anything",
   110  	Device:     "/dev/node",
   111  	Schema:     "gpt",
   112  	Size:       2 * quantity.SizeGiB,
   113  	SectorSize: 512,
   114  }
   115  
   116  func (s *installSuite) TestLayoutCompatibility(c *C) {
   117  	// same contents (the locally created structure should be ignored)
   118  	gadgetLayout := layoutFromYaml(c, mockGadgetYaml, nil)
   119  	err := install.EnsureLayoutCompatibility(gadgetLayout, &mockDeviceLayout)
   120  	c.Assert(err, IsNil)
   121  
   122  	// missing structure (that's ok)
   123  	gadgetLayoutWithExtras := layoutFromYaml(c, mockGadgetYaml+mockExtraStructure, nil)
   124  	err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &mockDeviceLayout)
   125  	c.Assert(err, IsNil)
   126  
   127  	deviceLayoutWithExtras := mockDeviceLayout
   128  	deviceLayoutWithExtras.Structure = append(deviceLayoutWithExtras.Structure,
   129  		gadget.OnDiskStructure{
   130  			LaidOutStructure: gadget.LaidOutStructure{
   131  				VolumeStructure: &gadget.VolumeStructure{
   132  					Name:  "Extra partition",
   133  					Size:  10 * quantity.SizeMiB,
   134  					Label: "extra",
   135  				},
   136  				StartOffset: 2 * quantity.OffsetMiB,
   137  			},
   138  			Node: "/dev/node3",
   139  		},
   140  	)
   141  	// extra structure (should fail)
   142  	err = install.EnsureLayoutCompatibility(gadgetLayout, &deviceLayoutWithExtras)
   143  	c.Assert(err, ErrorMatches, `cannot find disk partition /dev/node3.* in gadget`)
   144  
   145  	// layout is not compatible if the device is too small
   146  	smallDeviceLayout := mockDeviceLayout
   147  	smallDeviceLayout.Size = 100 * quantity.SizeMiB
   148  	// sanity check
   149  	c.Check(gadgetLayoutWithExtras.Size > smallDeviceLayout.Size, Equals, true)
   150  	err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &smallDeviceLayout)
   151  	c.Assert(err, ErrorMatches, `device /dev/node \(100 MiB\) is too small to fit the requested layout \(1\.17 GiB\)`)
   152  }
   153  
   154  func (s *installSuite) TestMBRLayoutCompatibility(c *C) {
   155  	const mockMBRGadgetYaml = `volumes:
   156    pc:
   157      schema: mbr
   158      bootloader: grub
   159      structure:
   160        - name: mbr
   161          type: mbr
   162          size: 440
   163        - name: BIOS Boot
   164          type: DA,21686148-6449-6E6F-744E-656564454649
   165          size: 1M
   166          offset: 1M
   167          offset-write: mbr+92
   168  `
   169  	var mockMBRDeviceLayout = gadget.OnDiskVolume{
   170  		Structure: []gadget.OnDiskStructure{
   171  			{
   172  				LaidOutStructure: gadget.LaidOutStructure{
   173  					VolumeStructure: &gadget.VolumeStructure{
   174  						// partition names have no
   175  						// meaning in MBR schema
   176  						Name: "other",
   177  						Size: 440,
   178  					},
   179  					StartOffset: 0,
   180  				},
   181  				Node: "/dev/node1",
   182  			},
   183  			{
   184  				LaidOutStructure: gadget.LaidOutStructure{
   185  					VolumeStructure: &gadget.VolumeStructure{
   186  						// partition names have no
   187  						// meaning in MBR schema
   188  						Name: "different BIOS Boot",
   189  						Size: 1 * quantity.SizeMiB,
   190  					},
   191  					StartOffset: 1 * quantity.OffsetMiB,
   192  				},
   193  				Node: "/dev/node2",
   194  			},
   195  		},
   196  		ID:         "anything",
   197  		Device:     "/dev/node",
   198  		Schema:     "dos",
   199  		Size:       2 * quantity.SizeGiB,
   200  		SectorSize: 512,
   201  	}
   202  	gadgetLayout := layoutFromYaml(c, mockMBRGadgetYaml, nil)
   203  	err := install.EnsureLayoutCompatibility(gadgetLayout, &mockMBRDeviceLayout)
   204  	c.Assert(err, IsNil)
   205  	// structure is missing from disk
   206  	gadgetLayoutWithExtras := layoutFromYaml(c, mockMBRGadgetYaml+mockExtraStructure, nil)
   207  	err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &mockMBRDeviceLayout)
   208  	c.Assert(err, IsNil)
   209  	// add it now
   210  	deviceLayoutWithExtras := mockMBRDeviceLayout
   211  	deviceLayoutWithExtras.Structure = append(deviceLayoutWithExtras.Structure,
   212  		gadget.OnDiskStructure{
   213  			LaidOutStructure: gadget.LaidOutStructure{
   214  				VolumeStructure: &gadget.VolumeStructure{
   215  					// name is ignored with MBR schema
   216  					Name:       "Extra partition",
   217  					Size:       1200 * quantity.SizeMiB,
   218  					Label:      "extra",
   219  					Filesystem: "ext4",
   220  					Type:       "83",
   221  				},
   222  				StartOffset: 2 * quantity.OffsetMiB,
   223  			},
   224  			Node: "/dev/node3",
   225  		},
   226  	)
   227  	err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayoutWithExtras)
   228  	c.Assert(err, IsNil)
   229  	// add another structure that's not part of the gadget
   230  	deviceLayoutWithExtras.Structure = append(deviceLayoutWithExtras.Structure,
   231  		gadget.OnDiskStructure{
   232  			LaidOutStructure: gadget.LaidOutStructure{
   233  				VolumeStructure: &gadget.VolumeStructure{
   234  					// name is ignored with MBR schema
   235  					Name: "Extra extra partition",
   236  					Size: 1 * quantity.SizeMiB,
   237  				},
   238  				StartOffset: 1202 * quantity.OffsetMiB,
   239  			},
   240  			Node: "/dev/node4",
   241  		},
   242  	)
   243  	err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayoutWithExtras)
   244  	c.Assert(err, ErrorMatches, `cannot find disk partition /dev/node4 \(starting at 1260388352\) in gadget: start offsets do not match \(disk: 1260388352 \(1.17 GiB\) and gadget: 2097152 \(2 MiB\)\)`)
   245  }
   246  
   247  func (s *installSuite) TestLayoutCompatibilityWithCreatedPartitions(c *C) {
   248  	gadgetLayoutWithExtras := layoutFromYaml(c, mockGadgetYaml+mockExtraStructure, nil)
   249  	deviceLayout := mockDeviceLayout
   250  
   251  	// device matches gadget except for the filesystem type
   252  	deviceLayout.Structure = append(deviceLayout.Structure,
   253  		gadget.OnDiskStructure{
   254  			LaidOutStructure: gadget.LaidOutStructure{
   255  				VolumeStructure: &gadget.VolumeStructure{
   256  					Name:       "Writable",
   257  					Size:       1200 * quantity.SizeMiB,
   258  					Label:      "writable",
   259  					Filesystem: "something_else",
   260  				},
   261  				StartOffset: 2 * quantity.OffsetMiB,
   262  			},
   263  			Node: "/dev/node3",
   264  		},
   265  	)
   266  	err := install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayout)
   267  	c.Assert(err, IsNil)
   268  
   269  	// we are going to manipulate last structure, which has system-data role
   270  	c.Assert(gadgetLayoutWithExtras.Structure[len(deviceLayout.Structure)-1].Role, Equals, gadget.SystemData)
   271  
   272  	// change the role for the laid out volume to not be a partition role that
   273  	// is created at install time (note that the duplicated seed role here is
   274  	// technically incorrect, you can't have duplicated roles, but this
   275  	// demonstrates that a structure that otherwise fits the bill but isn't a
   276  	// role that is created during install will fail the filesystem match check)
   277  	gadgetLayoutWithExtras.Structure[len(deviceLayout.Structure)-1].Role = gadget.SystemSeed
   278  
   279  	// now we fail to find the /dev/node3 structure from the gadget on disk
   280  	err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayout)
   281  	c.Assert(err, ErrorMatches, `cannot find disk partition /dev/node3 \(starting at 2097152\) in gadget: filesystems do not match and the partition is not creatable at install`)
   282  
   283  	// undo the role change
   284  	gadgetLayoutWithExtras.Structure[len(deviceLayout.Structure)-1].Role = gadget.SystemData
   285  
   286  	// change the gadget size to be bigger than the on disk size
   287  	gadgetLayoutWithExtras.Structure[len(deviceLayout.Structure)-1].Size = 10000000 * quantity.SizeMiB
   288  
   289  	// now we fail to find the /dev/node3 structure from the gadget on disk because the gadget says it must be bigger
   290  	err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayout)
   291  	c.Assert(err, ErrorMatches, `cannot find disk partition /dev/node3 \(starting at 2097152\) in gadget: on disk size is smaller than gadget size`)
   292  
   293  	// change the gadget size to be smaller than the on disk size and the role to be one that is not expanded
   294  	gadgetLayoutWithExtras.Structure[len(deviceLayout.Structure)-1].Size = 1 * quantity.SizeMiB
   295  	gadgetLayoutWithExtras.Structure[len(deviceLayout.Structure)-1].Role = gadget.SystemBoot
   296  
   297  	// now we fail because the gadget says it should be smaller and it can't be expanded
   298  	err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayout)
   299  	c.Assert(err, ErrorMatches, `cannot find disk partition /dev/node3 \(starting at 2097152\) in gadget: on disk size is larger than gadget size \(and the role should not be expanded\)`)
   300  
   301  	// but a smaller partition on disk for SystemData role is okay
   302  	gadgetLayoutWithExtras.Structure[len(deviceLayout.Structure)-1].Role = gadget.SystemData
   303  	err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayout)
   304  	c.Assert(err, IsNil)
   305  }
   306  
   307  func (s *installSuite) TestSchemaCompatibility(c *C) {
   308  	gadgetLayout := layoutFromYaml(c, mockGadgetYaml, nil)
   309  	deviceLayout := mockDeviceLayout
   310  
   311  	error_msg := "disk partitioning.* doesn't match gadget.*"
   312  
   313  	for i, tc := range []struct {
   314  		gs string
   315  		ds string
   316  		e  string
   317  	}{
   318  		{"", "dos", error_msg},
   319  		{"", "gpt", ""},
   320  		{"", "xxx", error_msg},
   321  		{"mbr", "dos", ""},
   322  		{"mbr", "gpt", error_msg},
   323  		{"mbr", "xxx", error_msg},
   324  		{"gpt", "dos", error_msg},
   325  		{"gpt", "gpt", ""},
   326  		{"gpt", "xxx", error_msg},
   327  		// XXX: "mbr,gpt" is currently unsupported
   328  		{"mbr,gpt", "dos", error_msg},
   329  		{"mbr,gpt", "gpt", error_msg},
   330  		{"mbr,gpt", "xxx", error_msg},
   331  	} {
   332  		c.Logf("%d: %q %q\n", i, tc.gs, tc.ds)
   333  		gadgetLayout.Volume.Schema = tc.gs
   334  		deviceLayout.Schema = tc.ds
   335  		err := install.EnsureLayoutCompatibility(gadgetLayout, &deviceLayout)
   336  		if tc.e == "" {
   337  			c.Assert(err, IsNil)
   338  		} else {
   339  			c.Assert(err, ErrorMatches, tc.e)
   340  		}
   341  	}
   342  	c.Logf("-----")
   343  }
   344  
   345  func (s *installSuite) TestIDCompatibility(c *C) {
   346  	gadgetLayout := layoutFromYaml(c, mockGadgetYaml, nil)
   347  	deviceLayout := mockDeviceLayout
   348  
   349  	error_msg := "disk ID.* doesn't match gadget volume ID.*"
   350  
   351  	for i, tc := range []struct {
   352  		gid string
   353  		did string
   354  		e   string
   355  	}{
   356  		{"", "", ""},
   357  		{"", "123", ""},
   358  		{"123", "345", error_msg},
   359  		{"123", "123", ""},
   360  	} {
   361  		c.Logf("%d: %q %q\n", i, tc.gid, tc.did)
   362  		gadgetLayout.Volume.ID = tc.gid
   363  		deviceLayout.ID = tc.did
   364  		err := install.EnsureLayoutCompatibility(gadgetLayout, &deviceLayout)
   365  		if tc.e == "" {
   366  			c.Assert(err, IsNil)
   367  		} else {
   368  			c.Assert(err, ErrorMatches, tc.e)
   369  		}
   370  	}
   371  	c.Logf("-----")
   372  }
   373  
   374  func layoutFromYaml(c *C, gadgetYaml string, model gadget.Model) *gadget.LaidOutVolume {
   375  	gadgetRoot := filepath.Join(c.MkDir(), "gadget")
   376  	err := os.MkdirAll(filepath.Join(gadgetRoot, "meta"), 0755)
   377  	c.Assert(err, IsNil)
   378  	err = ioutil.WriteFile(filepath.Join(gadgetRoot, "meta", "gadget.yaml"), []byte(gadgetYaml), 0644)
   379  	c.Assert(err, IsNil)
   380  	pv, err := gadget.LaidOutVolumeFromGadget(gadgetRoot, model)
   381  	c.Assert(err, IsNil)
   382  	return pv
   383  }
   384  
   385  const mockUC20GadgetYaml = `volumes:
   386    pc:
   387      bootloader: grub
   388      structure:
   389        - name: mbr
   390          type: mbr
   391          size: 440
   392        - name: BIOS Boot
   393          type: DA,21686148-6449-6E6F-744E-656564454649
   394          size: 1M
   395          offset: 1M
   396          offset-write: mbr+92
   397        - name: ubuntu-seed
   398          role: system-seed
   399          filesystem: vfat
   400          # UEFI will boot the ESP partition by default first
   401          type: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B
   402          size: 1200M
   403        - name: ubuntu-data
   404          role: system-data
   405          filesystem: ext4
   406          type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4
   407          size: 750M
   408  `
   409  
   410  func (s *installSuite) setupMockSysfs(c *C) {
   411  	err := os.MkdirAll(filepath.Join(s.dir, "/dev/disk/by-partlabel"), 0755)
   412  	c.Assert(err, IsNil)
   413  
   414  	err = ioutil.WriteFile(filepath.Join(s.dir, "/dev/fakedevice0p1"), nil, 0644)
   415  	c.Assert(err, IsNil)
   416  	err = os.Symlink("../../fakedevice0p1", filepath.Join(s.dir, "/dev/disk/by-partlabel/ubuntu-seed"))
   417  	c.Assert(err, IsNil)
   418  
   419  	// make parent device
   420  	err = ioutil.WriteFile(filepath.Join(s.dir, "/dev/fakedevice0"), nil, 0644)
   421  	c.Assert(err, IsNil)
   422  	// and fake /sys/block structure
   423  	err = os.MkdirAll(filepath.Join(s.dir, "/sys/block/fakedevice0/fakedevice0p1"), 0755)
   424  	c.Assert(err, IsNil)
   425  }
   426  
   427  func (s *installSuite) TestDeviceFromRoleHappy(c *C) {
   428  	s.setupMockSysfs(c)
   429  	lv := layoutFromYaml(c, mockUC20GadgetYaml, uc20mod)
   430  
   431  	device, err := install.DeviceFromRole(lv, gadget.SystemSeed)
   432  	c.Assert(err, IsNil)
   433  	c.Check(device, Matches, ".*/dev/fakedevice0")
   434  }
   435  
   436  func (s *installSuite) TestDeviceFromRoleErrorNoMatchingSysfs(c *C) {
   437  	// note no sysfs mocking
   438  	lv := layoutFromYaml(c, mockUC20GadgetYaml, uc20mod)
   439  
   440  	_, err := install.DeviceFromRole(lv, gadget.SystemSeed)
   441  	c.Assert(err, ErrorMatches, `cannot find device for role "system-seed": device not found`)
   442  }
   443  
   444  func (s *installSuite) TestDeviceFromRoleErrorNoRole(c *C) {
   445  	s.setupMockSysfs(c)
   446  	lv := layoutFromYaml(c, mockGadgetYaml, nil)
   447  
   448  	_, err := install.DeviceFromRole(lv, gadget.SystemSeed)
   449  	c.Assert(err, ErrorMatches, "cannot find role system-seed in gadget")
   450  }