github.com/stulluk/snapd@v0.0.0-20210611110309-f6d5d5bd24b0/gadget/ondisk.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 gadget 21 22 import ( 23 "encoding/json" 24 "fmt" 25 "os/exec" 26 "strconv" 27 "strings" 28 29 "github.com/snapcore/snapd/gadget/quantity" 30 "github.com/snapcore/snapd/osutil" 31 ) 32 33 // sfdiskDeviceDump represents the sfdisk --dump JSON output format. 34 type sfdiskDeviceDump struct { 35 PartitionTable sfdiskPartitionTable `json:"partitiontable"` 36 } 37 38 type sfdiskPartitionTable struct { 39 Label string `json:"label"` 40 ID string `json:"id"` 41 Device string `json:"device"` 42 Unit string `json:"unit"` 43 FirstLBA uint64 `json:"firstlba"` 44 LastLBA uint64 `json:"lastlba"` 45 Partitions []sfdiskPartition `json:"partitions"` 46 } 47 48 type sfdiskPartition struct { 49 Node string `json:"node"` 50 Start uint64 `json:"start"` 51 Size uint64 `json:"size"` 52 // List of GPT partition attributes in <attr>[ <attr>] format, numeric attributes 53 // are listed as GUID:<bit>[,<bit>]. Note that the even though the sfdisk(8) manpage 54 // says --part-attrs takes a space or comma separated list, the output from 55 // --json/--dump uses a different format. 56 Attrs string `json:"attrs"` 57 Type string `json:"type"` 58 UUID string `json:"uuid"` 59 Name string `json:"name"` 60 } 61 62 // TODO: consider looking into merging LaidOutVolume/Structure OnDiskVolume/Structure 63 64 // OnDiskStructure represents a gadget structure laid on a block device. 65 type OnDiskStructure struct { 66 LaidOutStructure 67 68 // Node identifies the device node of the block device. 69 Node string 70 71 // Size of the on disk structure, which is at least equal to the 72 // LaidOutStructure.Size but may be bigger if the partition was 73 // expanded. 74 Size quantity.Size 75 } 76 77 // OnDiskVolume holds information about the disk device including its partitioning 78 // schema, the partition table, and the structure layout it contains. 79 type OnDiskVolume struct { 80 Structure []OnDiskStructure 81 ID string 82 Device string 83 Schema string 84 // size in bytes 85 Size quantity.Size 86 // sector size in bytes 87 SectorSize quantity.Size 88 } 89 90 // OnDiskVolumeFromDevice obtains the partitioning and filesystem information from 91 // the block device. 92 func OnDiskVolumeFromDevice(device string) (*OnDiskVolume, error) { 93 output, err := exec.Command("sfdisk", "--json", device).Output() 94 if err != nil { 95 return nil, osutil.OutputErr(output, err) 96 } 97 98 var dump sfdiskDeviceDump 99 if err := json.Unmarshal(output, &dump); err != nil { 100 return nil, fmt.Errorf("cannot parse sfdisk output: %v", err) 101 } 102 103 dl, err := onDiskVolumeFromPartitionTable(dump.PartitionTable) 104 if err != nil { 105 return nil, err 106 } 107 dl.Device = device 108 109 return dl, nil 110 } 111 112 func fromSfdiskPartitionType(st string, sfdiskLabel string) (string, error) { 113 switch sfdiskLabel { 114 case "dos": 115 // sometimes sfdisk reports what is "0C" in gadget.yaml as "c", 116 // normalize the values 117 v, err := strconv.ParseUint(st, 16, 8) 118 if err != nil { 119 return "", fmt.Errorf("cannot convert MBR partition type %q", st) 120 } 121 return fmt.Sprintf("%02X", v), nil 122 case "gpt": 123 return st, nil 124 default: 125 return "", fmt.Errorf("unsupported partitioning schema %q", sfdiskLabel) 126 } 127 } 128 129 func blockdevSizeCmd(cmd, devpath string) (quantity.Size, error) { 130 out, err := exec.Command("blockdev", cmd, devpath).CombinedOutput() 131 if err != nil { 132 return 0, osutil.OutputErr(out, err) 133 } 134 nospace := strings.TrimSpace(string(out)) 135 sz, err := strconv.Atoi(nospace) 136 if err != nil { 137 return 0, fmt.Errorf("cannot parse blockdev %s result size %q: %v", cmd, nospace, err) 138 } 139 return quantity.Size(sz), nil 140 } 141 142 func blockDeviceSizeInSectors(devpath string) (quantity.Size, error) { 143 // the size is always reported in 512-byte sectors, even if the device does 144 // not have a physical sector size of 512 145 // XXX: consider using /sys/block/<dev>/size directly 146 return blockdevSizeCmd("--getsz", devpath) 147 } 148 149 func blockDeviceSectorSize(devpath string) (quantity.Size, error) { 150 // the size is reported in raw bytes 151 sz, err := blockdevSizeCmd("--getss", devpath) 152 if err != nil { 153 return 0, err 154 } 155 156 // ensure that the sector size is a multiple of 512, since we rely on that 157 // when we calculate the size in sectors, as blockdev --getsz always returns 158 // the size in 512-byte sectors 159 if sz%512 != 0 { 160 return 0, fmt.Errorf("cannot calculate structure size: sector size (%s) is not a multiple of 512", sz.String()) 161 } 162 if sz == 0 { 163 // extra paranoia 164 return 0, fmt.Errorf("internal error: sector size returned as 0") 165 } 166 return sz, nil 167 } 168 169 // onDiskVolumeFromPartitionTable takes an sfdisk dump partition table and returns 170 // the partitioning information as an on-disk volume. 171 func onDiskVolumeFromPartitionTable(ptable sfdiskPartitionTable) (*OnDiskVolume, error) { 172 if ptable.Unit != "sectors" { 173 return nil, fmt.Errorf("cannot position partitions: unknown unit %q", ptable.Unit) 174 } 175 176 structure := make([]VolumeStructure, len(ptable.Partitions)) 177 ds := make([]OnDiskStructure, len(ptable.Partitions)) 178 179 sectorSize, err := blockDeviceSectorSize(ptable.Device) 180 if err != nil { 181 return nil, err 182 } 183 184 for i, p := range ptable.Partitions { 185 info, err := filesystemInfo(p.Node) 186 if err != nil { 187 return nil, fmt.Errorf("cannot obtain filesystem information: %v", err) 188 } 189 switch { 190 case len(info.BlockDevices) == 0: 191 continue 192 case len(info.BlockDevices) > 1: 193 return nil, fmt.Errorf("unexpected number of blockdevices for node %q: %v", p.Node, info.BlockDevices) 194 } 195 bd := info.BlockDevices[0] 196 197 vsType, err := fromSfdiskPartitionType(p.Type, ptable.Label) 198 if err != nil { 199 return nil, fmt.Errorf("cannot convert sfdisk partition type %q: %v", p.Type, err) 200 } 201 202 structure[i] = VolumeStructure{ 203 Name: p.Name, 204 Size: quantity.Size(p.Size) * sectorSize, 205 Label: bd.Label, 206 Type: vsType, 207 Filesystem: bd.FSType, 208 } 209 210 ds[i] = OnDiskStructure{ 211 LaidOutStructure: LaidOutStructure{ 212 VolumeStructure: &structure[i], 213 StartOffset: quantity.Offset(p.Start) * quantity.Offset(sectorSize), 214 Index: i + 1, 215 }, 216 Node: p.Node, 217 } 218 } 219 220 var numSectors quantity.Size 221 if ptable.LastLBA != 0 { 222 // sfdisk reports the last usable LBA for GPT disks only 223 numSectors = quantity.Size(ptable.LastLBA + 1) 224 } else { 225 // sfdisk does not report any information about the size of a 226 // MBR partitioned disk, find out the size of the device by 227 // other means 228 sz, err := blockDeviceSizeInSectors(ptable.Device) 229 if err != nil { 230 return nil, fmt.Errorf("cannot obtain the size of device %q: %v", ptable.Device, err) 231 } 232 233 // since blockdev always reports the size in 512-byte sectors, if for 234 // some reason we are on a disk that does not 512-byte sectors, we will 235 // get confused, so in this case, multiply the number of 512-byte 236 // sectors by 512, then divide by the actual sector size to get the 237 // number of sectors 238 239 // this will never have a divisor, since we verified that sector size is 240 // a multiple of 512 above 241 numSectors = sz * 512 / sectorSize 242 } 243 244 dl := &OnDiskVolume{ 245 Structure: ds, 246 ID: ptable.ID, 247 Device: ptable.Device, 248 Schema: ptable.Label, 249 Size: numSectors * sectorSize, 250 SectorSize: sectorSize, 251 } 252 253 return dl, nil 254 } 255 256 // UpdatePartitionList re-reads the partitioning data from the device and 257 // updates the volume structures in the specified volume. 258 func UpdatePartitionList(dl *OnDiskVolume) error { 259 layout, err := OnDiskVolumeFromDevice(dl.Device) 260 if err != nil { 261 return fmt.Errorf("cannot read disk layout: %v", err) 262 } 263 if dl.ID != layout.ID { 264 return fmt.Errorf("partition table IDs don't match") 265 } 266 267 dl.Structure = layout.Structure 268 return nil 269 } 270 271 // lsblkFilesystemInfo represents the lsblk --fs JSON output format. 272 type lsblkFilesystemInfo struct { 273 BlockDevices []lsblkBlockDevice `json:"blockdevices"` 274 } 275 276 type lsblkBlockDevice struct { 277 Name string `json:"name"` 278 FSType string `json:"fstype"` 279 Label string `json:"label"` 280 UUID string `json:"uuid"` 281 Mountpoint string `json:"mountpoint"` 282 } 283 284 func filesystemInfo(node string) (*lsblkFilesystemInfo, error) { 285 output, err := exec.Command("lsblk", "--fs", "--json", node).CombinedOutput() 286 if err != nil { 287 return nil, osutil.OutputErr(output, err) 288 } 289 290 var info lsblkFilesystemInfo 291 if err := json.Unmarshal(output, &info); err != nil { 292 return nil, fmt.Errorf("cannot parse lsblk output: %v", err) 293 } 294 295 return &info, nil 296 }