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 }