github.com/freetocompute/snapd@v0.0.0-20210618182524-2fb355d72fd9/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  	// layout still compatible with a larger disk sector size
   123  	mockDeviceLayout.SectorSize = 4096
   124  	err = install.EnsureLayoutCompatibility(gadgetLayout, &mockDeviceLayout)
   125  	c.Assert(err, IsNil)
   126  
   127  	// layout not compatible with a sector size that's not a factor of the
   128  	// structure sizes
   129  	mockDeviceLayout.SectorSize = 513
   130  	err = install.EnsureLayoutCompatibility(gadgetLayout, &mockDeviceLayout)
   131  	c.Assert(err, ErrorMatches, `gadget volume structure #1 \(\"BIOS Boot\"\) size is not a multiple of disk sector size 513`)
   132  
   133  	// rest for the rest of the test
   134  	mockDeviceLayout.SectorSize = 512
   135  
   136  	// missing structure (that's ok)
   137  	gadgetLayoutWithExtras := layoutFromYaml(c, mockGadgetYaml+mockExtraStructure, nil)
   138  	err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &mockDeviceLayout)
   139  	c.Assert(err, IsNil)
   140  
   141  	deviceLayoutWithExtras := mockDeviceLayout
   142  	deviceLayoutWithExtras.Structure = append(deviceLayoutWithExtras.Structure,
   143  		gadget.OnDiskStructure{
   144  			LaidOutStructure: gadget.LaidOutStructure{
   145  				VolumeStructure: &gadget.VolumeStructure{
   146  					Name:  "Extra partition",
   147  					Size:  10 * quantity.SizeMiB,
   148  					Label: "extra",
   149  				},
   150  				StartOffset: 2 * quantity.OffsetMiB,
   151  			},
   152  			Node: "/dev/node3",
   153  		},
   154  	)
   155  	// extra structure (should fail)
   156  	err = install.EnsureLayoutCompatibility(gadgetLayout, &deviceLayoutWithExtras)
   157  	c.Assert(err, ErrorMatches, `cannot find disk partition /dev/node3.* in gadget`)
   158  
   159  	// layout is not compatible if the device is too small
   160  	smallDeviceLayout := mockDeviceLayout
   161  	smallDeviceLayout.Size = 100 * quantity.SizeMiB
   162  	// sanity check
   163  	c.Check(gadgetLayoutWithExtras.Size > smallDeviceLayout.Size, Equals, true)
   164  	err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &smallDeviceLayout)
   165  	c.Assert(err, ErrorMatches, `device /dev/node \(100 MiB\) is too small to fit the requested layout \(1\.17 GiB\)`)
   166  
   167  }
   168  
   169  func (s *installSuite) TestMBRLayoutCompatibility(c *C) {
   170  	const mockMBRGadgetYaml = `volumes:
   171    pc:
   172      schema: mbr
   173      bootloader: grub
   174      structure:
   175        - name: mbr
   176          type: mbr
   177          size: 440
   178        - name: BIOS Boot
   179          type: DA,21686148-6449-6E6F-744E-656564454649
   180          size: 1M
   181          offset: 1M
   182          offset-write: mbr+92
   183  `
   184  	var mockMBRDeviceLayout = gadget.OnDiskVolume{
   185  		Structure: []gadget.OnDiskStructure{
   186  			{
   187  				LaidOutStructure: gadget.LaidOutStructure{
   188  					VolumeStructure: &gadget.VolumeStructure{
   189  						// partition names have no
   190  						// meaning in MBR schema
   191  						Name: "other",
   192  						Size: 440,
   193  					},
   194  					StartOffset: 0,
   195  				},
   196  				Node: "/dev/node1",
   197  			},
   198  			{
   199  				LaidOutStructure: gadget.LaidOutStructure{
   200  					VolumeStructure: &gadget.VolumeStructure{
   201  						// partition names have no
   202  						// meaning in MBR schema
   203  						Name: "different BIOS Boot",
   204  						Size: 1 * quantity.SizeMiB,
   205  					},
   206  					StartOffset: 1 * quantity.OffsetMiB,
   207  				},
   208  				Node: "/dev/node2",
   209  			},
   210  		},
   211  		ID:         "anything",
   212  		Device:     "/dev/node",
   213  		Schema:     "dos",
   214  		Size:       2 * quantity.SizeGiB,
   215  		SectorSize: 512,
   216  	}
   217  	gadgetLayout := layoutFromYaml(c, mockMBRGadgetYaml, nil)
   218  	err := install.EnsureLayoutCompatibility(gadgetLayout, &mockMBRDeviceLayout)
   219  	c.Assert(err, IsNil)
   220  	// structure is missing from disk
   221  	gadgetLayoutWithExtras := layoutFromYaml(c, mockMBRGadgetYaml+mockExtraStructure, nil)
   222  	err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &mockMBRDeviceLayout)
   223  	c.Assert(err, IsNil)
   224  	// add it now
   225  	deviceLayoutWithExtras := mockMBRDeviceLayout
   226  	deviceLayoutWithExtras.Structure = append(deviceLayoutWithExtras.Structure,
   227  		gadget.OnDiskStructure{
   228  			LaidOutStructure: gadget.LaidOutStructure{
   229  				VolumeStructure: &gadget.VolumeStructure{
   230  					// name is ignored with MBR schema
   231  					Name:       "Extra partition",
   232  					Size:       1200 * quantity.SizeMiB,
   233  					Label:      "extra",
   234  					Filesystem: "ext4",
   235  					Type:       "83",
   236  				},
   237  				StartOffset: 2 * quantity.OffsetMiB,
   238  			},
   239  			Node: "/dev/node3",
   240  		},
   241  	)
   242  	err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayoutWithExtras)
   243  	c.Assert(err, IsNil)
   244  
   245  	// test with a larger sector size that is still an even multiple of the
   246  	// structure sizes in the gadget
   247  	mockMBRDeviceLayout.SectorSize = 4096
   248  	err = install.EnsureLayoutCompatibility(gadgetLayout, &mockMBRDeviceLayout)
   249  	c.Assert(err, IsNil)
   250  
   251  	// but with a sector size that is not an even multiple of the structure size
   252  	// then we have an error
   253  	mockMBRDeviceLayout.SectorSize = 513
   254  	err = install.EnsureLayoutCompatibility(gadgetLayout, &mockMBRDeviceLayout)
   255  	c.Assert(err, ErrorMatches, `gadget volume structure #1 \(\"BIOS Boot\"\) size is not a multiple of disk sector size 513`)
   256  
   257  	// add another structure that's not part of the gadget
   258  	deviceLayoutWithExtras.Structure = append(deviceLayoutWithExtras.Structure,
   259  		gadget.OnDiskStructure{
   260  			LaidOutStructure: gadget.LaidOutStructure{
   261  				VolumeStructure: &gadget.VolumeStructure{
   262  					// name is ignored with MBR schema
   263  					Name: "Extra extra partition",
   264  					Size: 1 * quantity.SizeMiB,
   265  				},
   266  				StartOffset: 1202 * quantity.OffsetMiB,
   267  			},
   268  			Node: "/dev/node4",
   269  		},
   270  	)
   271  	err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayoutWithExtras)
   272  	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\)\)`)
   273  }
   274  
   275  func (s *installSuite) TestLayoutCompatibilityWithCreatedPartitions(c *C) {
   276  	gadgetLayoutWithExtras := layoutFromYaml(c, mockGadgetYaml+mockExtraStructure, nil)
   277  	deviceLayout := mockDeviceLayout
   278  
   279  	// device matches gadget except for the filesystem type
   280  	deviceLayout.Structure = append(deviceLayout.Structure,
   281  		gadget.OnDiskStructure{
   282  			LaidOutStructure: gadget.LaidOutStructure{
   283  				VolumeStructure: &gadget.VolumeStructure{
   284  					Name:       "Writable",
   285  					Size:       1200 * quantity.SizeMiB,
   286  					Label:      "writable",
   287  					Filesystem: "something_else",
   288  				},
   289  				StartOffset: 2 * quantity.OffsetMiB,
   290  			},
   291  			Node: "/dev/node3",
   292  		},
   293  	)
   294  	err := install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayout)
   295  	c.Assert(err, IsNil)
   296  
   297  	// we are going to manipulate last structure, which has system-data role
   298  	c.Assert(gadgetLayoutWithExtras.Structure[len(deviceLayout.Structure)-1].Role, Equals, gadget.SystemData)
   299  
   300  	// change the role for the laid out volume to not be a partition role that
   301  	// is created at install time (note that the duplicated seed role here is
   302  	// technically incorrect, you can't have duplicated roles, but this
   303  	// demonstrates that a structure that otherwise fits the bill but isn't a
   304  	// role that is created during install will fail the filesystem match check)
   305  	gadgetLayoutWithExtras.Structure[len(deviceLayout.Structure)-1].Role = gadget.SystemSeed
   306  
   307  	// now we fail to find the /dev/node3 structure from the gadget on disk
   308  	err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayout)
   309  	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`)
   310  
   311  	// undo the role change
   312  	gadgetLayoutWithExtras.Structure[len(deviceLayout.Structure)-1].Role = gadget.SystemData
   313  
   314  	// change the gadget size to be bigger than the on disk size
   315  	gadgetLayoutWithExtras.Structure[len(deviceLayout.Structure)-1].Size = 10000000 * quantity.SizeMiB
   316  
   317  	// now we fail to find the /dev/node3 structure from the gadget on disk because the gadget says it must be bigger
   318  	err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayout)
   319  	c.Assert(err, ErrorMatches, `cannot find disk partition /dev/node3 \(starting at 2097152\) in gadget: on disk size is smaller than gadget size`)
   320  
   321  	// change the gadget size to be smaller than the on disk size and the role to be one that is not expanded
   322  	gadgetLayoutWithExtras.Structure[len(deviceLayout.Structure)-1].Size = 1 * quantity.SizeMiB
   323  	gadgetLayoutWithExtras.Structure[len(deviceLayout.Structure)-1].Role = gadget.SystemBoot
   324  
   325  	// now we fail because the gadget says it should be smaller and it can't be expanded
   326  	err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayout)
   327  	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\)`)
   328  
   329  	// but a smaller partition on disk for SystemData role is okay
   330  	gadgetLayoutWithExtras.Structure[len(deviceLayout.Structure)-1].Role = gadget.SystemData
   331  	err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayout)
   332  	c.Assert(err, IsNil)
   333  }
   334  
   335  func (s *installSuite) TestSchemaCompatibility(c *C) {
   336  	gadgetLayout := layoutFromYaml(c, mockGadgetYaml, nil)
   337  	deviceLayout := mockDeviceLayout
   338  
   339  	error_msg := "disk partitioning.* doesn't match gadget.*"
   340  
   341  	for i, tc := range []struct {
   342  		gs string
   343  		ds string
   344  		e  string
   345  	}{
   346  		{"", "dos", error_msg},
   347  		{"", "gpt", ""},
   348  		{"", "xxx", error_msg},
   349  		{"mbr", "dos", ""},
   350  		{"mbr", "gpt", error_msg},
   351  		{"mbr", "xxx", error_msg},
   352  		{"gpt", "dos", error_msg},
   353  		{"gpt", "gpt", ""},
   354  		{"gpt", "xxx", error_msg},
   355  		// XXX: "mbr,gpt" is currently unsupported
   356  		{"mbr,gpt", "dos", error_msg},
   357  		{"mbr,gpt", "gpt", error_msg},
   358  		{"mbr,gpt", "xxx", error_msg},
   359  	} {
   360  		c.Logf("%d: %q %q\n", i, tc.gs, tc.ds)
   361  		gadgetLayout.Volume.Schema = tc.gs
   362  		deviceLayout.Schema = tc.ds
   363  		err := install.EnsureLayoutCompatibility(gadgetLayout, &deviceLayout)
   364  		if tc.e == "" {
   365  			c.Assert(err, IsNil)
   366  		} else {
   367  			c.Assert(err, ErrorMatches, tc.e)
   368  		}
   369  	}
   370  	c.Logf("-----")
   371  }
   372  
   373  func (s *installSuite) TestIDCompatibility(c *C) {
   374  	gadgetLayout := layoutFromYaml(c, mockGadgetYaml, nil)
   375  	deviceLayout := mockDeviceLayout
   376  
   377  	error_msg := "disk ID.* doesn't match gadget volume ID.*"
   378  
   379  	for i, tc := range []struct {
   380  		gid string
   381  		did string
   382  		e   string
   383  	}{
   384  		{"", "", ""},
   385  		{"", "123", ""},
   386  		{"123", "345", error_msg},
   387  		{"123", "123", ""},
   388  	} {
   389  		c.Logf("%d: %q %q\n", i, tc.gid, tc.did)
   390  		gadgetLayout.Volume.ID = tc.gid
   391  		deviceLayout.ID = tc.did
   392  		err := install.EnsureLayoutCompatibility(gadgetLayout, &deviceLayout)
   393  		if tc.e == "" {
   394  			c.Assert(err, IsNil)
   395  		} else {
   396  			c.Assert(err, ErrorMatches, tc.e)
   397  		}
   398  	}
   399  	c.Logf("-----")
   400  }
   401  
   402  func layoutFromYaml(c *C, gadgetYaml string, model gadget.Model) *gadget.LaidOutVolume {
   403  	gadgetRoot := filepath.Join(c.MkDir(), "gadget")
   404  	err := os.MkdirAll(filepath.Join(gadgetRoot, "meta"), 0755)
   405  	c.Assert(err, IsNil)
   406  	err = ioutil.WriteFile(filepath.Join(gadgetRoot, "meta", "gadget.yaml"), []byte(gadgetYaml), 0644)
   407  	c.Assert(err, IsNil)
   408  	pv, err := mustLayOutVolumeFromGadget(c, gadgetRoot, "", model)
   409  	c.Assert(err, IsNil)
   410  	return pv
   411  }
   412  
   413  const mockUC20GadgetYaml = `volumes:
   414    pc:
   415      bootloader: grub
   416      structure:
   417        - name: mbr
   418          type: mbr
   419          size: 440
   420        - name: BIOS Boot
   421          type: DA,21686148-6449-6E6F-744E-656564454649
   422          size: 1M
   423          offset: 1M
   424          offset-write: mbr+92
   425        - name: ubuntu-seed
   426          role: system-seed
   427          filesystem: vfat
   428          # UEFI will boot the ESP partition by default first
   429          type: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B
   430          size: 1200M
   431        - name: ubuntu-boot
   432          role: system-boot
   433          filesystem: ext4
   434          type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4
   435          size: 1200M
   436        - name: ubuntu-data
   437          role: system-data
   438          filesystem: ext4
   439          type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4
   440          size: 750M
   441  `
   442  
   443  func (s *installSuite) setupMockSysfs(c *C) {
   444  	err := os.MkdirAll(filepath.Join(s.dir, "/dev/disk/by-partlabel"), 0755)
   445  	c.Assert(err, IsNil)
   446  
   447  	err = ioutil.WriteFile(filepath.Join(s.dir, "/dev/fakedevice0p1"), nil, 0644)
   448  	c.Assert(err, IsNil)
   449  	err = os.Symlink("../../fakedevice0p1", filepath.Join(s.dir, "/dev/disk/by-partlabel/ubuntu-seed"))
   450  	c.Assert(err, IsNil)
   451  
   452  	// make parent device
   453  	err = ioutil.WriteFile(filepath.Join(s.dir, "/dev/fakedevice0"), nil, 0644)
   454  	c.Assert(err, IsNil)
   455  	// and fake /sys/block structure
   456  	err = os.MkdirAll(filepath.Join(s.dir, "/sys/block/fakedevice0/fakedevice0p1"), 0755)
   457  	c.Assert(err, IsNil)
   458  }
   459  
   460  func (s *installSuite) TestDeviceFromRoleHappy(c *C) {
   461  	s.setupMockSysfs(c)
   462  	lv := layoutFromYaml(c, mockUC20GadgetYaml, uc20Mod)
   463  
   464  	device, err := install.DeviceFromRole(lv, gadget.SystemSeed)
   465  	c.Assert(err, IsNil)
   466  	c.Check(device, Matches, ".*/dev/fakedevice0")
   467  }
   468  
   469  func (s *installSuite) TestDeviceFromRoleErrorNoMatchingSysfs(c *C) {
   470  	// note no sysfs mocking
   471  	lv := layoutFromYaml(c, mockUC20GadgetYaml, uc20Mod)
   472  
   473  	_, err := install.DeviceFromRole(lv, gadget.SystemSeed)
   474  	c.Assert(err, ErrorMatches, `cannot find device for role "system-seed": device not found`)
   475  }
   476  
   477  func (s *installSuite) TestDeviceFromRoleErrorNoRole(c *C) {
   478  	s.setupMockSysfs(c)
   479  	lv := layoutFromYaml(c, mockGadgetYaml, nil)
   480  
   481  	_, err := install.DeviceFromRole(lv, gadget.SystemSeed)
   482  	c.Assert(err, ErrorMatches, "cannot find role system-seed in gadget")
   483  }