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