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