github.com/anonymouse64/snapd@v0.0.0-20210824153203-04c4c42d842d/gadget/install/partition.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 install 21 22 import ( 23 "bytes" 24 "fmt" 25 "os/exec" 26 "strconv" 27 "strings" 28 "time" 29 30 "github.com/snapcore/snapd/gadget" 31 "github.com/snapcore/snapd/gadget/quantity" 32 "github.com/snapcore/snapd/logger" 33 "github.com/snapcore/snapd/osutil" 34 "github.com/snapcore/snapd/strutil" 35 ) 36 37 var ( 38 ensureNodesExist = ensureNodesExistImpl 39 ) 40 41 var createdPartitionGUID = []string{ 42 "0FC63DAF-8483-4772-8E79-3D69D8477DE4", // Linux filesystem data 43 "0657FD6D-A4AB-43C4-84E5-0933C84B4F4F", // Linux swap partition 44 "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7", // Windows Basic Data Partition 45 } 46 47 // creationSupported returns whether we support and expect to create partitions 48 // of the given type, it also means we are ready to remove them for re-installation 49 // or retried installation if they are appropriately marked with createdPartitionAttr. 50 func creationSupported(ptype string) bool { 51 return strutil.ListContains(createdPartitionGUID, strings.ToUpper(ptype)) 52 } 53 54 // createMissingPartitions creates the partitions listed in the laid out volume 55 // pv that are missing from the existing device layout, returning a list of 56 // structures that have been created. 57 func createMissingPartitions(dl *gadget.OnDiskVolume, pv *gadget.LaidOutVolume) ([]gadget.OnDiskStructure, error) { 58 buf, created := buildPartitionList(dl, pv) 59 if len(created) == 0 { 60 return created, nil 61 } 62 63 logger.Debugf("create partitions on %s: %s", dl.Device, buf.String()) 64 65 // Write the partition table. By default sfdisk will try to re-read the 66 // partition table with the BLKRRPART ioctl but will fail because the 67 // kernel side rescan removes and adds partitions and we have partitions 68 // mounted (so it fails on removal). Use --no-reread to skip this attempt. 69 cmd := exec.Command("sfdisk", "--append", "--no-reread", dl.Device) 70 cmd.Stdin = buf 71 if output, err := cmd.CombinedOutput(); err != nil { 72 return created, osutil.OutputErr(output, err) 73 } 74 75 // Re-read the partition table 76 if err := reloadPartitionTable(dl.Device); err != nil { 77 return nil, err 78 } 79 80 // Make sure the devices for the partitions we created are available 81 if err := ensureNodesExist(created, 5*time.Second); err != nil { 82 return nil, fmt.Errorf("partition not available: %v", err) 83 } 84 85 return created, nil 86 } 87 88 // buildPartitionList builds a list of partitions based on the current 89 // device contents and gadget structure list, in sfdisk dump format, and 90 // returns a partitioning description suitable for sfdisk input and a 91 // list of the partitions to be created. 92 func buildPartitionList(dl *gadget.OnDiskVolume, pv *gadget.LaidOutVolume) (sfdiskInput *bytes.Buffer, toBeCreated []gadget.OnDiskStructure) { 93 sectorSize := dl.SectorSize 94 95 // Keep track what partitions we already have on disk 96 seen := map[quantity.Offset]bool{} 97 for _, s := range dl.Structure { 98 start := s.StartOffset / quantity.Offset(sectorSize) 99 seen[start] = true 100 } 101 102 // Check if the last partition has a system-data role 103 canExpandData := false 104 if n := len(pv.LaidOutStructure); n > 0 { 105 last := pv.LaidOutStructure[n-1] 106 if last.VolumeStructure.Role == gadget.SystemData { 107 canExpandData = true 108 } 109 } 110 111 // The partition index 112 pIndex := 0 113 114 // Write new partition data in named-fields format 115 buf := &bytes.Buffer{} 116 for _, p := range pv.LaidOutStructure { 117 if !p.IsPartition() { 118 continue 119 } 120 121 pIndex++ 122 s := p.VolumeStructure 123 124 // Skip partitions that are already in the volume 125 start := p.StartOffset / quantity.Offset(sectorSize) 126 if seen[start] { 127 continue 128 } 129 130 // Only allow the creation of partitions with known GUIDs 131 // TODO:UC20: also provide a mechanism for MBR (RPi) 132 ptype := partitionType(dl.Schema, p.Type) 133 if dl.Schema == "gpt" && !creationSupported(ptype) { 134 logger.Noticef("cannot create partition with unsupported type %s", ptype) 135 continue 136 } 137 138 // Check if the data partition should be expanded 139 size := s.Size 140 if s.Role == gadget.SystemData && canExpandData && quantity.Size(p.StartOffset)+s.Size < dl.Size { 141 size = dl.Size - quantity.Size(p.StartOffset) 142 } 143 144 // Can we use the index here? Get the largest existing partition number and 145 // build from there could be safer if the disk partitions are not consecutive 146 // (can this actually happen in our images?) 147 node := deviceName(dl.Device, pIndex) 148 fmt.Fprintf(buf, "%s : start=%12d, size=%12d, type=%s, name=%q\n", node, 149 p.StartOffset/quantity.Offset(sectorSize), size/sectorSize, ptype, s.Name) 150 151 toBeCreated = append(toBeCreated, gadget.OnDiskStructure{ 152 LaidOutStructure: p, 153 Node: node, 154 Size: size, 155 }) 156 } 157 158 return buf, toBeCreated 159 } 160 161 func partitionType(label, ptype string) string { 162 t := strings.Split(ptype, ",") 163 if len(t) < 1 { 164 return "" 165 } 166 if len(t) == 1 { 167 return t[0] 168 } 169 if label == "gpt" { 170 return t[1] 171 } 172 return t[0] 173 } 174 175 func deviceName(name string, index int) string { 176 if len(name) > 0 { 177 last := name[len(name)-1] 178 if last >= '0' && last <= '9' { 179 return fmt.Sprintf("%sp%d", name, index) 180 } 181 } 182 return fmt.Sprintf("%s%d", name, index) 183 } 184 185 // removeCreatedPartitions removes partitions added during a previous install. 186 func removeCreatedPartitions(lv *gadget.LaidOutVolume, dl *gadget.OnDiskVolume) error { 187 indexes := make([]string, 0, len(dl.Structure)) 188 for i, s := range dl.Structure { 189 if wasCreatedDuringInstall(lv, s) { 190 logger.Noticef("partition %s was created during previous install", s.Node) 191 indexes = append(indexes, strconv.Itoa(i+1)) 192 } 193 } 194 if len(indexes) == 0 { 195 return nil 196 } 197 198 // Delete disk partitions 199 logger.Debugf("delete disk partitions %v", indexes) 200 cmd := exec.Command("sfdisk", append([]string{"--no-reread", "--delete", dl.Device}, indexes...)...) 201 if output, err := cmd.CombinedOutput(); err != nil { 202 return osutil.OutputErr(output, err) 203 } 204 205 // Reload the partition table 206 if err := reloadPartitionTable(dl.Device); err != nil { 207 return err 208 } 209 210 // Re-read the partition table from the device to update our partition list 211 if err := gadget.UpdatePartitionList(dl); err != nil { 212 return err 213 } 214 215 // Ensure all created partitions were removed 216 if remaining := createdDuringInstall(lv, dl); len(remaining) > 0 { 217 return fmt.Errorf("cannot remove partitions: %s", strings.Join(remaining, ", ")) 218 } 219 220 return nil 221 } 222 223 // ensureNodeExists makes sure the device nodes for all device structures are 224 // available and notified to udev, within a specified amount of time. 225 func ensureNodesExistImpl(dss []gadget.OnDiskStructure, timeout time.Duration) error { 226 t0 := time.Now() 227 for _, ds := range dss { 228 found := false 229 for time.Since(t0) < timeout { 230 if osutil.FileExists(ds.Node) { 231 found = true 232 break 233 } 234 time.Sleep(100 * time.Millisecond) 235 } 236 if found { 237 if err := udevTrigger(ds.Node); err != nil { 238 return err 239 } 240 } else { 241 return fmt.Errorf("device %s not available", ds.Node) 242 } 243 } 244 return nil 245 } 246 247 // reloadPartitionTable instructs the kernel to re-read the partition 248 // table of a given block device. 249 func reloadPartitionTable(device string) error { 250 // Re-read the partition table using the BLKPG ioctl, which doesn't 251 // remove existing partitions, only appends new partitions with the right 252 // size and offset. As long as we provide consistent partitioning from 253 // userspace we're safe. 254 output, err := exec.Command("partx", "-u", device).CombinedOutput() 255 if err != nil { 256 return osutil.OutputErr(output, err) 257 } 258 return nil 259 } 260 261 // udevTrigger triggers udev for the specified device and waits until 262 // all events in the udev queue are handled. 263 func udevTrigger(device string) error { 264 if output, err := exec.Command("udevadm", "trigger", "--settle", device).CombinedOutput(); err != nil { 265 return osutil.OutputErr(output, err) 266 } 267 return nil 268 } 269 270 // wasCreatedDuringInstall returns if the OnDiskStructure was created during 271 // install by referencing the gadget volume. A structure is only considered to 272 // be created during install if it is a role that is created during install and 273 // the start offsets match. We specifically don't look at anything on the 274 // structure such as filesystem information since this may be incomplete due to 275 // a failed installation, or due to the partial layout that is created by some 276 // ARM tools (i.e. ptool and fastboot) when flashing images to internal MMC. 277 func wasCreatedDuringInstall(lv *gadget.LaidOutVolume, s gadget.OnDiskStructure) bool { 278 // for a structure to have been created during install, it must be one of 279 // the system-boot, system-data, or system-save roles from the gadget, and 280 // as such the on disk structure must exist in the exact same location as 281 // the role from the gadget, so only return true if the provided structure 282 // has the exact same StartOffset as one of those roles 283 for _, gs := range lv.LaidOutStructure { 284 // TODO: how to handle ubuntu-save here? maybe a higher level function 285 // should decide whether to delete it or not? 286 switch gs.Role { 287 case gadget.SystemSave, gadget.SystemData, gadget.SystemBoot: 288 // then it was created during install or is to be created during 289 // install, see if the offset matches the provided on disk structure 290 // has 291 if s.StartOffset == gs.StartOffset { 292 return true 293 } 294 } 295 } 296 297 return false 298 } 299 300 // createdDuringInstall returns a list of partitions created during the 301 // install process. 302 func createdDuringInstall(lv *gadget.LaidOutVolume, layout *gadget.OnDiskVolume) (created []string) { 303 created = make([]string, 0, len(layout.Structure)) 304 for _, s := range layout.Structure { 305 if wasCreatedDuringInstall(lv, s) { 306 created = append(created, s.Node) 307 } 308 } 309 return created 310 }