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