github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/gadget/install/content_test.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 install_test
    21  
    22  import (
    23  	"errors"
    24  	"io/ioutil"
    25  	"os"
    26  	"path/filepath"
    27  
    28  	. "gopkg.in/check.v1"
    29  
    30  	"github.com/snapcore/snapd/boot"
    31  	"github.com/snapcore/snapd/dirs"
    32  	"github.com/snapcore/snapd/gadget"
    33  	"github.com/snapcore/snapd/gadget/install"
    34  	"github.com/snapcore/snapd/testutil"
    35  )
    36  
    37  type contentTestSuite struct {
    38  	testutil.BaseTest
    39  
    40  	dir string
    41  
    42  	gadgetRoot string
    43  
    44  	mockMountPoint   string
    45  	mockMountCalls   []struct{ source, target, fstype string }
    46  	mockUnmountCalls []string
    47  
    48  	mockMountErr error
    49  }
    50  
    51  var _ = Suite(&contentTestSuite{})
    52  
    53  func (s *contentTestSuite) SetUpTest(c *C) {
    54  	s.BaseTest.SetUpTest(c)
    55  
    56  	s.dir = c.MkDir()
    57  
    58  	s.mockMountErr = nil
    59  	s.mockMountCalls = nil
    60  	s.mockUnmountCalls = nil
    61  
    62  	s.gadgetRoot = c.MkDir()
    63  	err := makeMockGadget(s.gadgetRoot, gadgetContent)
    64  	c.Assert(err, IsNil)
    65  
    66  	s.mockMountPoint = c.MkDir()
    67  	restore := install.MockContentMountpoint(s.mockMountPoint)
    68  	s.AddCleanup(restore)
    69  
    70  	restore = install.MockSysMount(func(source, target, fstype string, flags uintptr, data string) error {
    71  		s.mockMountCalls = append(s.mockMountCalls, struct{ source, target, fstype string }{source, target, fstype})
    72  		return s.mockMountErr
    73  	})
    74  	s.AddCleanup(restore)
    75  
    76  	restore = install.MockSysUnmount(func(target string, flags int) error {
    77  		s.mockUnmountCalls = append(s.mockUnmountCalls, target)
    78  		return nil
    79  	})
    80  	s.AddCleanup(restore)
    81  }
    82  
    83  var mockOnDiskStructureBiosBoot = gadget.OnDiskStructure{
    84  	Node: "/dev/node1",
    85  	LaidOutStructure: gadget.LaidOutStructure{
    86  		VolumeStructure: &gadget.VolumeStructure{
    87  			Name: "BIOS Boot",
    88  			Size: 1 * 1024 * 1024,
    89  			Type: "DA,21686148-6449-6E6F-744E-656564454649",
    90  			Content: []gadget.VolumeContent{
    91  				{
    92  					Image: "pc-core.img",
    93  				},
    94  			},
    95  		},
    96  		StartOffset: 0,
    97  		Index:       1,
    98  	},
    99  }
   100  
   101  var mockOnDiskStructureSystemSeed = gadget.OnDiskStructure{
   102  	Node: "/dev/node2",
   103  	LaidOutStructure: gadget.LaidOutStructure{
   104  		VolumeStructure: &gadget.VolumeStructure{
   105  			Name:       "Recovery",
   106  			Size:       1258291200,
   107  			Type:       "EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
   108  			Role:       "system-seed",
   109  			Label:      "ubuntu-seed",
   110  			Filesystem: "vfat",
   111  			Content: []gadget.VolumeContent{
   112  				{
   113  					Source: "grubx64.efi",
   114  					Target: "EFI/boot/grubx64.efi",
   115  				},
   116  			},
   117  		},
   118  		StartOffset: 2097152,
   119  		Index:       2,
   120  	},
   121  }
   122  
   123  func makeMockGadget(gadgetRoot, gadgetContent string) error {
   124  	if err := os.MkdirAll(filepath.Join(gadgetRoot, "meta"), 0755); err != nil {
   125  		return err
   126  	}
   127  	if err := ioutil.WriteFile(filepath.Join(gadgetRoot, "meta", "gadget.yaml"), []byte(gadgetContent), 0644); err != nil {
   128  		return err
   129  	}
   130  	if err := ioutil.WriteFile(filepath.Join(gadgetRoot, "pc-boot.img"), []byte("pc-boot.img content"), 0644); err != nil {
   131  		return err
   132  	}
   133  	if err := ioutil.WriteFile(filepath.Join(gadgetRoot, "pc-core.img"), []byte("pc-core.img content"), 0644); err != nil {
   134  		return err
   135  	}
   136  	if err := ioutil.WriteFile(filepath.Join(gadgetRoot, "grubx64.efi"), []byte("grubx64.efi content"), 0644); err != nil {
   137  		return err
   138  	}
   139  
   140  	return nil
   141  }
   142  
   143  const gadgetContent = `volumes:
   144    pc:
   145      bootloader: grub
   146      structure:
   147        - name: mbr
   148          type: mbr
   149          size: 440
   150          content:
   151            - image: pc-boot.img
   152        - name: BIOS Boot
   153          type: DA,21686148-6449-6E6F-744E-656564454649
   154          size: 1M
   155          offset: 1M
   156          offset-write: mbr+92
   157          content:
   158            - image: pc-core.img
   159        - name: Recovery
   160          role: system-seed
   161          filesystem: vfat
   162          # UEFI will boot the ESP partition by default first
   163          type: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B
   164          size: 1200M
   165          content:
   166            - source: grubx64.efi
   167              target: EFI/boot/grubx64.efi
   168        - name: Writable
   169          role: system-data
   170          filesystem: ext4
   171          type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4
   172          size: 1200M
   173  `
   174  
   175  type mockContentChange struct {
   176  	path   string
   177  	change *gadget.ContentChange
   178  }
   179  
   180  type mockWriteObserver struct {
   181  	content        map[string][]*mockContentChange
   182  	observeErr     error
   183  	expectedStruct *gadget.LaidOutStructure
   184  	c              *C
   185  }
   186  
   187  func (m *mockWriteObserver) Observe(op gadget.ContentOperation, sourceStruct *gadget.LaidOutStructure,
   188  	targetRootDir, relativeTargetPath string, data *gadget.ContentChange) (gadget.ContentChangeAction, error) {
   189  	if m.content == nil {
   190  		m.content = make(map[string][]*mockContentChange)
   191  	}
   192  	m.content[targetRootDir] = append(m.content[targetRootDir],
   193  		&mockContentChange{path: relativeTargetPath, change: data})
   194  	m.c.Assert(sourceStruct, NotNil)
   195  	m.c.Check(sourceStruct, DeepEquals, m.expectedStruct)
   196  	return gadget.ChangeApply, m.observeErr
   197  }
   198  
   199  func (s *contentTestSuite) TestWriteFilesystemContent(c *C) {
   200  	for _, tc := range []struct {
   201  		mountErr   error
   202  		unmountErr error
   203  		observeErr error
   204  		err        string
   205  	}{
   206  		{
   207  			mountErr:   nil,
   208  			unmountErr: nil,
   209  			err:        "",
   210  		}, {
   211  			mountErr:   errors.New("mount error"),
   212  			unmountErr: nil,
   213  			err:        "cannot mount filesystem .*: mount error",
   214  		}, {
   215  			mountErr:   nil,
   216  			unmountErr: errors.New("unmount error"),
   217  			err:        "unmount error",
   218  		}, {
   219  			observeErr: errors.New("observe error"),
   220  			err:        "cannot create filesystem image: cannot write filesystem content of source:grubx64.efi: cannot observe file write: observe error",
   221  		},
   222  	} {
   223  		mockMountpoint := c.MkDir()
   224  
   225  		restore := install.MockContentMountpoint(mockMountpoint)
   226  		defer restore()
   227  
   228  		restore = install.MockSysMount(func(source, target, fstype string, flags uintptr, data string) error {
   229  			return tc.mountErr
   230  		})
   231  		defer restore()
   232  
   233  		restore = install.MockSysUnmount(func(target string, flags int) error {
   234  			return tc.unmountErr
   235  		})
   236  		defer restore()
   237  
   238  		// copy existing mock
   239  		m := mockOnDiskStructureSystemSeed
   240  		m.LaidOutContent = []gadget.LaidOutContent{
   241  			{
   242  				VolumeContent: &gadget.VolumeContent{
   243  					Source: "grubx64.efi",
   244  					Target: "EFI/boot/grubx64.efi",
   245  				},
   246  			},
   247  		}
   248  		obs := &mockWriteObserver{
   249  			c:              c,
   250  			observeErr:     tc.observeErr,
   251  			expectedStruct: &m.LaidOutStructure,
   252  		}
   253  		err := install.WriteContent(&m, s.gadgetRoot, obs)
   254  		if tc.err == "" {
   255  			c.Assert(err, IsNil)
   256  		} else {
   257  			c.Assert(err, ErrorMatches, tc.err)
   258  		}
   259  
   260  		if err == nil {
   261  			// the target file system is mounted on a directory named after the structure index
   262  			content, err := ioutil.ReadFile(filepath.Join(mockMountpoint, "2", "EFI/boot/grubx64.efi"))
   263  			c.Assert(err, IsNil)
   264  			c.Check(string(content), Equals, "grubx64.efi content")
   265  			c.Assert(obs.content, DeepEquals, map[string][]*mockContentChange{
   266  				filepath.Join(mockMountpoint, "2"): {
   267  					{
   268  						path:   "EFI/boot/grubx64.efi",
   269  						change: &gadget.ContentChange{After: filepath.Join(s.gadgetRoot, "grubx64.efi")},
   270  					},
   271  				},
   272  			})
   273  		}
   274  	}
   275  }
   276  
   277  func (s *contentTestSuite) TestWriteRawContent(c *C) {
   278  	mockNode := filepath.Join(s.dir, "mock-node")
   279  	err := ioutil.WriteFile(mockNode, nil, 0644)
   280  	c.Assert(err, IsNil)
   281  
   282  	// copy existing mock
   283  	m := mockOnDiskStructureBiosBoot
   284  	m.Node = mockNode
   285  	m.LaidOutContent = []gadget.LaidOutContent{
   286  		{
   287  			VolumeContent: &gadget.VolumeContent{
   288  				Image: "pc-core.img",
   289  			},
   290  			StartOffset: 2,
   291  			Size:        gadget.Size(len("pc-core.img content")),
   292  		},
   293  	}
   294  
   295  	err = install.WriteContent(&m, s.gadgetRoot, nil)
   296  	c.Assert(err, IsNil)
   297  
   298  	content, err := ioutil.ReadFile(m.Node)
   299  	c.Assert(err, IsNil)
   300  	// note the 2 zero byte start offset
   301  	c.Check(string(content), Equals, "\x00\x00pc-core.img content")
   302  }
   303  
   304  func (s *contentTestSuite) TestMountFilesystem(c *C) {
   305  	dirs.SetRootDir(c.MkDir())
   306  	defer dirs.SetRootDir("")
   307  
   308  	// mounting will only happen for devices with a label
   309  	mockOnDiskStructureBiosBoot.Label = "bios-boot"
   310  	defer func() { mockOnDiskStructureBiosBoot.Label = "" }()
   311  
   312  	err := install.MountFilesystem(&mockOnDiskStructureBiosBoot, boot.InitramfsRunMntDir)
   313  	c.Assert(err, ErrorMatches, "cannot mount a partition with no filesystem")
   314  
   315  	// mount a filesystem...
   316  	err = install.MountFilesystem(&mockOnDiskStructureSystemSeed, boot.InitramfsRunMntDir)
   317  	c.Assert(err, IsNil)
   318  
   319  	// ...and check if it was mounted at the right mount point
   320  	c.Check(s.mockMountCalls, HasLen, 1)
   321  	c.Check(s.mockMountCalls, DeepEquals, []struct{ source, target, fstype string }{
   322  		{"/dev/node2", boot.InitramfsUbuntuSeedDir, "vfat"},
   323  	})
   324  
   325  	// now try to mount a filesystem with no label
   326  	mockOnDiskStructureSystemSeed.Label = ""
   327  	defer func() { mockOnDiskStructureSystemSeed.Label = "ubuntu-seed" }()
   328  
   329  	err = install.MountFilesystem(&mockOnDiskStructureSystemSeed, boot.InitramfsRunMntDir)
   330  	c.Assert(err, ErrorMatches, "cannot mount a filesystem with no label")
   331  }