github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/gadget/device_linux.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2019 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  	"os"
    26  	"path/filepath"
    27  	"strings"
    28  
    29  	"github.com/snapcore/snapd/dirs"
    30  	"github.com/snapcore/snapd/osutil"
    31  	"github.com/snapcore/snapd/osutil/disks"
    32  )
    33  
    34  var ErrDeviceNotFound = errors.New("device not found")
    35  var ErrMountNotFound = errors.New("mount point not found")
    36  var ErrNoFilesystemDefined = errors.New("no filesystem defined")
    37  
    38  var evalSymlinks = filepath.EvalSymlinks
    39  
    40  // FindDeviceForStructure attempts to find an existing block device matching
    41  // given volume structure, by inspecting its name and, optionally, the
    42  // filesystem label. Assumes that the host's udev has set up device symlinks
    43  // correctly.
    44  func FindDeviceForStructure(ps *LaidOutStructure) (string, error) {
    45  	var candidates []string
    46  
    47  	if ps.Name != "" {
    48  		byPartlabel := filepath.Join(dirs.GlobalRootDir, "/dev/disk/by-partlabel/", disks.BlkIDEncodeLabel(ps.Name))
    49  		candidates = append(candidates, byPartlabel)
    50  	}
    51  	if ps.HasFilesystem() {
    52  		fsLabel := ps.EffectiveFilesystemLabel()
    53  		if fsLabel == "" && ps.Name != "" {
    54  			// when image is built and the structure has no
    55  			// filesystem label, the structure name will be used by
    56  			// default as the label
    57  			fsLabel = ps.Name
    58  		}
    59  		if fsLabel != "" {
    60  			byFsLabel := filepath.Join(dirs.GlobalRootDir, "/dev/disk/by-label/", disks.BlkIDEncodeLabel(fsLabel))
    61  			candidates = append(candidates, byFsLabel)
    62  		}
    63  	}
    64  
    65  	var found string
    66  	var match string
    67  	for _, candidate := range candidates {
    68  		if !osutil.FileExists(candidate) {
    69  			continue
    70  		}
    71  		if !osutil.IsSymlink(candidate) {
    72  			// /dev/disk/by-label/* and /dev/disk/by-partlabel/* are
    73  			// expected to be symlink
    74  			return "", fmt.Errorf("candidate %v is not a symlink", candidate)
    75  		}
    76  		target, err := evalSymlinks(candidate)
    77  		if err != nil {
    78  			return "", fmt.Errorf("cannot read device link: %v", err)
    79  		}
    80  		if found != "" && target != found {
    81  			// partition and filesystem label links point to
    82  			// different devices
    83  			return "", fmt.Errorf("conflicting device match, %q points to %q, previous match %q points to %q",
    84  				candidate, target, match, found)
    85  		}
    86  		found = target
    87  		match = candidate
    88  	}
    89  
    90  	if found == "" {
    91  		return "", ErrDeviceNotFound
    92  	}
    93  
    94  	return found, nil
    95  }
    96  
    97  // findDeviceForStructureWithFallback attempts to find an existing block device
    98  // partition containing given non-filesystem volume structure, by inspecting the
    99  // structure's name.
   100  //
   101  // Should there be no match, attempts to find the block device corresponding to
   102  // the volume enclosing the structure under the following conditions:
   103  // - the structure has no filesystem
   104  // - and the structure is of type: bare (no partition table entry)
   105  // - or the structure has no name, but a partition table entry (hence no label
   106  //   by which we could find it)
   107  //
   108  // The fallback mechanism uses the fact that Core devices always have a mount at
   109  // /writable. The system is booted from the parent of the device mounted at
   110  // /writable.
   111  //
   112  // Returns the device name and an offset at which the structure content starts
   113  // within the device or an error.
   114  func findDeviceForStructureWithFallback(ps *LaidOutStructure) (dev string, offs Size, err error) {
   115  	if ps.HasFilesystem() {
   116  		return "", 0, fmt.Errorf("internal error: cannot use with filesystem structures")
   117  	}
   118  
   119  	dev, err = FindDeviceForStructure(ps)
   120  	if err == nil {
   121  		// found exact device representing this structure, thus the
   122  		// structure starts at 0 offset within the device
   123  		return dev, 0, nil
   124  	}
   125  	if err != ErrDeviceNotFound {
   126  		// error out on other errors
   127  		return "", 0, err
   128  	}
   129  	if err == ErrDeviceNotFound && ps.IsPartition() && ps.Name != "" {
   130  		// structures with partition table entry and a name must have
   131  		// been located already
   132  		return "", 0, err
   133  	}
   134  
   135  	// we're left with structures that have no partition table entry, or
   136  	// have a partition but no name that could be used to find them
   137  
   138  	dev, err = findParentDeviceWithWritableFallback()
   139  	if err != nil {
   140  		return "", 0, err
   141  	}
   142  	// start offset is calculated as an absolute position within the volume
   143  	return dev, ps.StartOffset, nil
   144  }
   145  
   146  // findMountPointForStructure locates a mount point of a device that matches
   147  // given structure. The structure must have a filesystem defined, otherwise an
   148  // error is raised.
   149  func findMountPointForStructure(ps *LaidOutStructure) (string, error) {
   150  	if !ps.HasFilesystem() {
   151  		return "", ErrNoFilesystemDefined
   152  	}
   153  
   154  	devpath, err := FindDeviceForStructure(ps)
   155  	if err != nil {
   156  		return "", err
   157  	}
   158  
   159  	var mountPoint string
   160  	mountInfo, err := osutil.LoadMountInfo()
   161  	if err != nil {
   162  		return "", fmt.Errorf("cannot read mount info: %v", err)
   163  	}
   164  	for _, entry := range mountInfo {
   165  		if entry.Root != "/" {
   166  			// only interested at the location where root of the
   167  			// structure filesystem is mounted
   168  			continue
   169  		}
   170  		if entry.MountSource == devpath && entry.FsType == ps.Filesystem {
   171  			mountPoint = entry.MountDir
   172  			break
   173  		}
   174  	}
   175  
   176  	if mountPoint == "" {
   177  		return "", ErrMountNotFound
   178  	}
   179  
   180  	return mountPoint, nil
   181  }
   182  
   183  func isWritableMount(entry *osutil.MountInfoEntry) bool {
   184  	// example mountinfo entry:
   185  	// 26 27 8:3 / /writable rw,relatime shared:7 - ext4 /dev/sda3 rw,data=ordered
   186  	return entry.Root == "/" && entry.MountDir == "/writable" && entry.FsType == "ext4"
   187  }
   188  
   189  func findDeviceForWritable() (device string, err error) {
   190  	mountInfo, err := osutil.LoadMountInfo()
   191  	if err != nil {
   192  		return "", fmt.Errorf("cannot read mount info: %v", err)
   193  	}
   194  	for _, entry := range mountInfo {
   195  		if isWritableMount(entry) {
   196  			return entry.MountSource, nil
   197  		}
   198  	}
   199  	return "", ErrDeviceNotFound
   200  }
   201  
   202  func findParentDeviceWithWritableFallback() (string, error) {
   203  	partitionWritable, err := findDeviceForWritable()
   204  	if err != nil {
   205  		return "", err
   206  	}
   207  	return ParentDiskFromMountSource(partitionWritable)
   208  }
   209  
   210  // ParentDiskFromMountSource will find the parent disk device for the given
   211  // partition. E.g. /dev/nvmen0n1p5 -> /dev/nvme0n1.
   212  //
   213  // When the mount source is a symlink, it is resolved to the actual device that
   214  // is mounted. Should the device be one created by device mapper, it is followed
   215  // up to the actual underlying block device. As an example, this is how devices
   216  // are followed with a /writable mounted from an encrypted volume:
   217  //
   218  // /dev/mapper/ubuntu-data-<uuid> (a symlink)
   219  //   ⤷ /dev/dm-0 (set up by device mapper)
   220  //       ⤷ /dev/hda4 (actual partition with the content)
   221  //          ⤷ /dev/hda (returned by this function)
   222  //
   223  func ParentDiskFromMountSource(mountSource string) (string, error) {
   224  	// mount source can be a symlink
   225  	st, err := os.Lstat(mountSource)
   226  	if err != nil {
   227  		return "", err
   228  	}
   229  	if mode := st.Mode(); mode&os.ModeSymlink != 0 {
   230  		// resolve to actual device
   231  		target, err := filepath.EvalSymlinks(mountSource)
   232  		if err != nil {
   233  			return "", fmt.Errorf("cannot resolve mount source symlink %v: %v", mountSource, err)
   234  		}
   235  		mountSource = target
   236  	}
   237  	// /dev/sda3 -> sda3
   238  	devname := filepath.Base(mountSource)
   239  
   240  	if strings.HasPrefix(devname, "dm-") {
   241  		// looks like a device set up by device mapper
   242  		resolved, err := resolveParentOfDeviceMapperDevice(devname)
   243  		if err != nil {
   244  			return "", fmt.Errorf("cannot resolve device mapper device %v: %v", devname, err)
   245  		}
   246  		devname = resolved
   247  	}
   248  
   249  	// do not bother with investigating major/minor devices (inconsistent
   250  	// across block device types) or mangling strings, but look at sys
   251  	// hierarchy for block devices instead:
   252  	// /sys/block/sda               - main SCSI device
   253  	// /sys/block/sda/sda1          - partition 1
   254  	// /sys/block/sda/sda<n>        - partition n
   255  	// /sys/block/nvme0n1           - main NVME device
   256  	// /sys/block/nvme0n1/nvme0n1p1 - partition 1
   257  	matches, err := filepath.Glob(filepath.Join(dirs.GlobalRootDir, "/sys/block/*/", devname))
   258  	if err != nil {
   259  		return "", fmt.Errorf("cannot glob /sys/block/ entries: %v", err)
   260  	}
   261  	if len(matches) != 1 {
   262  		return "", fmt.Errorf("unexpected number of matches (%v) for /sys/block/*/%s", len(matches), devname)
   263  	}
   264  
   265  	// at this point we have /sys/block/sda/sda3
   266  	// /sys/block/sda/sda3 -> /dev/sda
   267  	mainDev := filepath.Join(dirs.GlobalRootDir, "/dev/", filepath.Base(filepath.Dir(matches[0])))
   268  
   269  	if !osutil.FileExists(mainDev) {
   270  		return "", fmt.Errorf("device %v does not exist", mainDev)
   271  	}
   272  	return mainDev, nil
   273  }
   274  
   275  func resolveParentOfDeviceMapperDevice(devname string) (string, error) {
   276  	// devices set up by device mapper have /dev/block/dm-*/slaves directory
   277  	// which lists the devices that are upper in the chain, follow that to
   278  	// find the first device that is non-dm one
   279  	dmSlavesLevel := 0
   280  	const maxDmSlavesLevel = 5
   281  	for strings.HasPrefix(devname, "dm-") {
   282  		// /sys/block/dm-*/slaves/ lists a device that this dm device is part of
   283  		slavesGlob := filepath.Join(dirs.GlobalRootDir, "/sys/block", devname, "slaves/*")
   284  		slaves, err := filepath.Glob(slavesGlob)
   285  		if err != nil {
   286  			return "", fmt.Errorf("cannot glob slaves of dm device %v: %v", devname, err)
   287  		}
   288  		if len(slaves) != 1 {
   289  			return "", fmt.Errorf("unexpected number of dm device %v slaves: %v", devname, len(slaves))
   290  		}
   291  		devname = filepath.Base(slaves[0])
   292  
   293  		// if we're this deep in resolving dm devices, things are clearly getting out of hand
   294  		dmSlavesLevel++
   295  		if dmSlavesLevel >= maxDmSlavesLevel {
   296  			return "", fmt.Errorf("too many levels")
   297  		}
   298  
   299  	}
   300  	return devname, nil
   301  }