github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/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 "bytes" 24 "encoding/json" 25 "fmt" 26 "os/exec" 27 "strconv" 28 "strings" 29 30 "github.com/snapcore/snapd/logger" 31 "github.com/snapcore/snapd/osutil" 32 "github.com/snapcore/snapd/strutil" 33 ) 34 35 const ( 36 ubuntuBootLabel = "ubuntu-boot" 37 ubuntuSeedLabel = "ubuntu-seed" 38 ubuntuDataLabel = "ubuntu-data" 39 40 sectorSize Size = 512 41 42 createdPartitionAttr = "59" 43 ) 44 45 var createdPartitionGUID = []string{ 46 "0FC63DAF-8483-4772-8E79-3D69D8477DE4", // Linux filesystem data 47 "0657FD6D-A4AB-43C4-84E5-0933C84B4F4F", // Linux swap partition 48 } 49 50 // creationSupported returns whether we support and expect to create partitions 51 // of the given type, it also means we are ready to remove them for re-installation 52 // or retried installation if they are appropriately marked with createdPartitionAttr. 53 func creationSupported(ptype string) bool { 54 return strutil.ListContains(createdPartitionGUID, strings.ToUpper(ptype)) 55 } 56 57 // sfdiskDeviceDump represents the sfdisk --dump JSON output format. 58 type sfdiskDeviceDump struct { 59 PartitionTable sfdiskPartitionTable `json:"partitiontable"` 60 } 61 62 type sfdiskPartitionTable struct { 63 Label string `json:"label"` 64 ID string `json:"id"` 65 Device string `json:"device"` 66 Unit string `json:"unit"` 67 FirstLBA uint64 `json:"firstlba"` 68 LastLBA uint64 `json:"lastlba"` 69 Partitions []sfdiskPartition `json:"partitions"` 70 } 71 72 type sfdiskPartition struct { 73 Node string `json:"node"` 74 Start uint64 `json:"start"` 75 Size uint64 `json:"size"` 76 // List of GPT partition attributes in <attr>[ <attr>] format, numeric attributes 77 // are listed as GUID:<bit>[,<bit>]. Note that the even though the sfdisk(8) manpage 78 // says --part-attrs takes a space or comma separated list, the output from 79 // --json/--dump uses a different format. 80 Attrs string `json:"attrs"` 81 Type string `json:"type"` 82 UUID string `json:"uuid"` 83 Name string `json:"name"` 84 } 85 86 func isCreatedDuringInstall(p *sfdiskPartition, fs *lsblkBlockDevice, sfdiskLabel string) bool { 87 switch sfdiskLabel { 88 case "gpt": 89 // the created partitions use specific GPT GUID types and set a 90 // specific bit in partition attributes 91 if !creationSupported(p.Type) { 92 return false 93 } 94 for _, a := range strings.Fields(p.Attrs) { 95 if !strings.HasPrefix(a, "GUID:") { 96 continue 97 } 98 attrs := strings.Split(a[5:], ",") 99 if strutil.ListContains(attrs, createdPartitionAttr) { 100 return true 101 } 102 } 103 case "dos": 104 // we have no similar type/bit attribute setting for MBR, on top 105 // of that MBR does not support partition names, fall back to 106 // reasonable assumption that only partitions carrying 107 // ubuntu-boot and ubuntu-data labels are created during 108 // install, everything else was part of factory image 109 110 // TODO:UC20 consider using gadget layout information to build a 111 // mapping of partition start offset to label/name 112 createdDuringInstall := []string{ubuntuBootLabel, ubuntuDataLabel} 113 return strutil.ListContains(createdDuringInstall, fs.Label) 114 } 115 return false 116 } 117 118 // TODO: consider looking into merging LaidOutVolume/Structure OnDiskVolume/Structure 119 120 // OnDiskStructure represents a gadget structure laid on a block device. 121 type OnDiskStructure struct { 122 LaidOutStructure 123 124 // Node identifies the device node of the block device. 125 Node string 126 // CreatedDuringInstall is true when the structure has properties indicating 127 // it was created based on the gadget description during installation. 128 CreatedDuringInstall bool 129 } 130 131 // OnDiskVolume holds information about the disk device including its partitioning 132 // schema, the partition table, and the structure layout it contains. 133 type OnDiskVolume struct { 134 Structure []OnDiskStructure 135 ID string 136 Device string 137 Schema string 138 // size in bytes 139 Size Size 140 // sector size in bytes 141 SectorSize Size 142 partitionTable *sfdiskPartitionTable 143 } 144 145 // OnDiskVolumeFromDevice obtains the partitioning and filesystem information from 146 // the block device. 147 func OnDiskVolumeFromDevice(device string) (*OnDiskVolume, error) { 148 output, err := exec.Command("sfdisk", "--json", "-d", device).Output() 149 if err != nil { 150 return nil, osutil.OutputErr(output, err) 151 } 152 153 var dump sfdiskDeviceDump 154 if err := json.Unmarshal(output, &dump); err != nil { 155 return nil, fmt.Errorf("cannot parse sfdisk output: %v", err) 156 } 157 158 dl, err := onDiskVolumeFromPartitionTable(dump.PartitionTable) 159 if err != nil { 160 return nil, err 161 } 162 dl.Device = device 163 164 return dl, nil 165 } 166 167 func fromSfdiskPartitionType(st string, sfdiskLabel string) (string, error) { 168 switch sfdiskLabel { 169 case "dos": 170 // sometimes sfdisk reports what is "0C" in gadget.yaml as "c", 171 // normalize the values 172 v, err := strconv.ParseUint(st, 16, 8) 173 if err != nil { 174 return "", fmt.Errorf("cannot convert MBR partition type %q", st) 175 } 176 return fmt.Sprintf("%02X", v), nil 177 case "gpt": 178 return st, nil 179 default: 180 return "", fmt.Errorf("unsupported partitioning schema %q", sfdiskLabel) 181 } 182 } 183 184 func blockDeviceSizeInSectors(devpath string) (Size, error) { 185 // the size is reported in 512-byte sectors 186 // XXX: consider using /sys/block/<dev>/size directly 187 out, err := exec.Command("blockdev", "--getsz", devpath).CombinedOutput() 188 if err != nil { 189 return 0, osutil.OutputErr(out, err) 190 } 191 nospace := strings.TrimSpace(string(out)) 192 sz, err := strconv.Atoi(nospace) 193 if err != nil { 194 return 0, fmt.Errorf("cannot parse device size %q: %v", nospace, err) 195 } 196 return Size(sz), nil 197 } 198 199 // onDiskVolumeFromPartitionTable takes an sfdisk dump partition table and returns 200 // the partitioning information as an on-disk volume. 201 func onDiskVolumeFromPartitionTable(ptable sfdiskPartitionTable) (*OnDiskVolume, error) { 202 if ptable.Unit != "sectors" { 203 return nil, fmt.Errorf("cannot position partitions: unknown unit %q", ptable.Unit) 204 } 205 206 structure := make([]VolumeStructure, len(ptable.Partitions)) 207 ds := make([]OnDiskStructure, len(ptable.Partitions)) 208 209 for i, p := range ptable.Partitions { 210 info, err := filesystemInfo(p.Node) 211 if err != nil { 212 return nil, fmt.Errorf("cannot obtain filesystem information: %v", err) 213 } 214 switch { 215 case len(info.BlockDevices) == 0: 216 continue 217 case len(info.BlockDevices) > 1: 218 return nil, fmt.Errorf("unexpected number of blockdevices for node %q: %v", p.Node, info.BlockDevices) 219 } 220 bd := info.BlockDevices[0] 221 222 vsType, err := fromSfdiskPartitionType(p.Type, ptable.Label) 223 if err != nil { 224 return nil, fmt.Errorf("cannot convert sfdisk partition type %q: %v", p.Type, err) 225 } 226 227 structure[i] = VolumeStructure{ 228 Name: p.Name, 229 Size: Size(p.Size) * sectorSize, 230 Label: bd.Label, 231 Type: vsType, 232 Filesystem: bd.FSType, 233 } 234 235 ds[i] = OnDiskStructure{ 236 LaidOutStructure: LaidOutStructure{ 237 VolumeStructure: &structure[i], 238 StartOffset: Size(p.Start) * sectorSize, 239 Index: i + 1, 240 }, 241 Node: p.Node, 242 CreatedDuringInstall: isCreatedDuringInstall(&p, &bd, ptable.Label), 243 } 244 } 245 246 var numSectors Size 247 if ptable.LastLBA != 0 { 248 // sfdisk reports the last usable LBA for GPT disks only 249 numSectors = Size(ptable.LastLBA + 1) 250 } else { 251 // sfdisk does not report any information about the size of a 252 // MBR partitioned disk, find out the size of the device by 253 // other means 254 sz, err := blockDeviceSizeInSectors(ptable.Device) 255 if err != nil { 256 return nil, fmt.Errorf("cannot obtain the size of device %q: %v", ptable.Device, err) 257 } 258 numSectors = sz 259 } 260 261 dl := &OnDiskVolume{ 262 Structure: ds, 263 ID: ptable.ID, 264 Device: ptable.Device, 265 Schema: ptable.Label, 266 Size: numSectors * sectorSize, 267 SectorSize: sectorSize, 268 partitionTable: &ptable, 269 } 270 271 return dl, nil 272 } 273 274 func deviceName(name string, index int) string { 275 if len(name) > 0 { 276 last := name[len(name)-1] 277 if last >= '0' && last <= '9' { 278 return fmt.Sprintf("%sp%d", name, index) 279 } 280 } 281 return fmt.Sprintf("%s%d", name, index) 282 } 283 284 // BuildPartitionList builds a list of partitions based on the current 285 // device contents and gadget structure list, in sfdisk dump format, and 286 // returns a partitioning description suitable for sfdisk input and a 287 // list of the partitions to be created. 288 func BuildPartitionList(dl *OnDiskVolume, pv *LaidOutVolume) (sfdiskInput *bytes.Buffer, toBeCreated []OnDiskStructure) { 289 ptable := dl.partitionTable 290 291 // Keep track what partitions we already have on disk 292 seen := map[uint64]bool{} 293 for _, p := range ptable.Partitions { 294 seen[p.Start] = true 295 } 296 297 // Check if the last partition has a system-data role 298 canExpandData := false 299 if n := len(pv.LaidOutStructure); n > 0 { 300 last := pv.LaidOutStructure[n-1] 301 if last.VolumeStructure.Role == SystemData { 302 canExpandData = true 303 } 304 } 305 306 // The partition index 307 pIndex := 0 308 309 // Write new partition data in named-fields format 310 buf := &bytes.Buffer{} 311 for _, p := range pv.LaidOutStructure { 312 if !p.IsPartition() { 313 continue 314 } 315 316 pIndex++ 317 s := p.VolumeStructure 318 319 // Skip partitions that are already in the volume 320 start := p.StartOffset / sectorSize 321 if seen[uint64(start)] { 322 continue 323 } 324 325 // Only allow the creation of partitions with known GUIDs 326 // TODO:UC20: also provide a mechanism for MBR (RPi) 327 ptype := partitionType(ptable.Label, p.Type) 328 if ptable.Label == "gpt" && !creationSupported(ptype) { 329 logger.Noticef("cannot create partition with unsupported type %s", ptype) 330 continue 331 } 332 333 // Check if the data partition should be expanded 334 size := s.Size 335 if s.Role == SystemData && canExpandData && p.StartOffset+s.Size < dl.Size { 336 size = dl.Size - p.StartOffset 337 } 338 339 // Can we use the index here? Get the largest existing partition number and 340 // build from there could be safer if the disk partitions are not consecutive 341 // (can this actually happen in our images?) 342 node := deviceName(ptable.Device, pIndex) 343 fmt.Fprintf(buf, "%s : start=%12d, size=%12d, type=%s, name=%q, attrs=\"GUID:%s\"\n", node, 344 p.StartOffset/sectorSize, size/sectorSize, ptype, s.Name, createdPartitionAttr) 345 346 // Set expected labels based on role 347 switch s.Role { 348 case SystemBoot: 349 s.Label = ubuntuBootLabel 350 case SystemSeed: 351 s.Label = ubuntuSeedLabel 352 case SystemData: 353 s.Label = ubuntuDataLabel 354 } 355 356 toBeCreated = append(toBeCreated, OnDiskStructure{ 357 LaidOutStructure: p, 358 Node: node, 359 CreatedDuringInstall: true, 360 }) 361 } 362 363 return buf, toBeCreated 364 } 365 366 // UpdatePartitionList re-reads the partitioning data from the device and 367 // updates the partition list in the specified volume. 368 func UpdatePartitionList(dl *OnDiskVolume) error { 369 layout, err := OnDiskVolumeFromDevice(dl.Device) 370 if err != nil { 371 return fmt.Errorf("cannot read disk layout: %v", err) 372 } 373 if dl.ID != layout.ID { 374 return fmt.Errorf("partition table IDs don't match") 375 } 376 377 dl.Structure = layout.Structure 378 dl.partitionTable = layout.partitionTable 379 380 return nil 381 } 382 383 // CreatedDuringInstall returns a list of partitions created during the 384 // install process. 385 func CreatedDuringInstall(layout *OnDiskVolume) (created []string) { 386 created = make([]string, 0, len(layout.Structure)) 387 for _, s := range layout.Structure { 388 if s.CreatedDuringInstall { 389 created = append(created, s.Node) 390 } 391 } 392 return created 393 } 394 395 func partitionType(label, ptype string) string { 396 t := strings.Split(ptype, ",") 397 if len(t) < 1 { 398 return "" 399 } 400 if len(t) == 1 { 401 return t[0] 402 } 403 if label == "gpt" { 404 return t[1] 405 } 406 return t[0] 407 } 408 409 // lsblkFilesystemInfo represents the lsblk --fs JSON output format. 410 type lsblkFilesystemInfo struct { 411 BlockDevices []lsblkBlockDevice `json:"blockdevices"` 412 } 413 414 type lsblkBlockDevice struct { 415 Name string `json:"name"` 416 FSType string `json:"fstype"` 417 Label string `json:"label"` 418 UUID string `json:"uuid"` 419 Mountpoint string `json:"mountpoint"` 420 } 421 422 func filesystemInfo(node string) (*lsblkFilesystemInfo, error) { 423 output, err := exec.Command("lsblk", "--fs", "--json", node).CombinedOutput() 424 if err != nil { 425 return nil, osutil.OutputErr(output, err) 426 } 427 428 var info lsblkFilesystemInfo 429 if err := json.Unmarshal(output, &info); err != nil { 430 return nil, fmt.Errorf("cannot parse lsblk output: %v", err) 431 } 432 433 return &info, nil 434 }