github.com/anonymouse64/snapd@v0.0.0-20210824153203-04c4c42d842d/gadget/install/install.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 // +build !nosecboot 3 4 /* 5 * Copyright (C) 2019-2020 Canonical Ltd 6 * 7 * This program is free software: you can redistribute it and/or modify 8 * it under the terms of the GNU General Public License version 3 as 9 * published by the Free Software Foundation. 10 * 11 * This program is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 * GNU General Public License for more details. 15 * 16 * You should have received a copy of the GNU General Public License 17 * along with this program. If not, see <http://www.gnu.org/licenses/>. 18 * 19 */ 20 21 package install 22 23 import ( 24 "fmt" 25 "os" 26 "path/filepath" 27 28 "github.com/snapcore/snapd/boot" 29 "github.com/snapcore/snapd/gadget" 30 "github.com/snapcore/snapd/logger" 31 "github.com/snapcore/snapd/secboot" 32 "github.com/snapcore/snapd/timings" 33 ) 34 35 const ( 36 ubuntuDataLabel = "ubuntu-data" 37 ubuntuSaveLabel = "ubuntu-save" 38 ) 39 40 func deviceFromRole(lv *gadget.LaidOutVolume, role string) (device string, err error) { 41 for _, vs := range lv.LaidOutStructure { 42 // XXX: this part of the finding maybe should be a 43 // method on gadget.*Volume 44 if vs.Role == role { 45 device, err = gadget.FindDeviceForStructure(&vs) 46 if err != nil { 47 return "", fmt.Errorf("cannot find device for role %q: %v", role, err) 48 } 49 return gadget.ParentDiskFromMountSource(device) 50 } 51 } 52 return "", fmt.Errorf("cannot find role %s in gadget", role) 53 } 54 55 func roleOrLabelOrName(part gadget.OnDiskStructure) string { 56 switch { 57 case part.Role != "": 58 return part.Role 59 case part.Label != "": 60 return part.Label 61 case part.Name != "": 62 return part.Name 63 default: 64 return "unknown" 65 } 66 } 67 68 // Run bootstraps the partitions of a device, by either creating 69 // missing ones or recreating installed ones. 70 func Run(model gadget.Model, gadgetRoot, kernelRoot, device string, options Options, observer gadget.ContentObserver, perfTimings timings.Measurer) (*InstalledSystemSideData, error) { 71 logger.Noticef("installing a new system") 72 logger.Noticef(" gadget data from: %v", gadgetRoot) 73 if options.Encrypt { 74 logger.Noticef(" encryption: on") 75 } 76 if gadgetRoot == "" { 77 return nil, fmt.Errorf("cannot use empty gadget root directory") 78 } 79 80 lv, err := gadget.LaidOutSystemVolumeFromGadget(gadgetRoot, kernelRoot, model) 81 if err != nil { 82 return nil, fmt.Errorf("cannot layout the volume: %v", err) 83 } 84 // TODO: resolve content paths from gadget here 85 86 // XXX: the only situation where auto-detect is not desired is 87 // in (spread) testing - consider to remove forcing a device 88 // 89 // auto-detect device if no device is forced 90 if device == "" { 91 device, err = deviceFromRole(lv, gadget.SystemSeed) 92 if err != nil { 93 return nil, fmt.Errorf("cannot find device to create partitions on: %v", err) 94 } 95 } 96 97 diskLayout, err := gadget.OnDiskVolumeFromDevice(device) 98 if err != nil { 99 return nil, fmt.Errorf("cannot read %v partitions: %v", device, err) 100 } 101 102 // check if the current partition table is compatible with the gadget, 103 // ignoring partitions added by the installer (will be removed later) 104 if err := ensureLayoutCompatibility(lv, diskLayout); err != nil { 105 return nil, fmt.Errorf("gadget and %v partition table not compatible: %v", device, err) 106 } 107 108 // remove partitions added during a previous install attempt 109 if err := removeCreatedPartitions(lv, diskLayout); err != nil { 110 return nil, fmt.Errorf("cannot remove partitions from previous install: %v", err) 111 } 112 // at this point we removed any existing partition, nuke any 113 // of the existing sealed key files placed outside of the 114 // encrypted partitions (LP: #1879338) 115 sealedKeyFiles, _ := filepath.Glob(filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "*.sealed-key")) 116 for _, keyFile := range sealedKeyFiles { 117 if err := os.Remove(keyFile); err != nil && !os.IsNotExist(err) { 118 return nil, fmt.Errorf("cannot cleanup obsolete key file: %v", keyFile) 119 } 120 } 121 122 var created []gadget.OnDiskStructure 123 timings.Run(perfTimings, "create-partitions", "Create partitions", func(timings.Measurer) { 124 created, err = createMissingPartitions(diskLayout, lv) 125 }) 126 if err != nil { 127 return nil, fmt.Errorf("cannot create the partitions: %v", err) 128 } 129 130 makeKeySet := func() (*EncryptionKeySet, error) { 131 key, err := secboot.NewEncryptionKey() 132 if err != nil { 133 return nil, fmt.Errorf("cannot create encryption key: %v", err) 134 } 135 136 rkey, err := secboot.NewRecoveryKey() 137 if err != nil { 138 return nil, fmt.Errorf("cannot create recovery key: %v", err) 139 } 140 return &EncryptionKeySet{ 141 Key: key, 142 RecoveryKey: rkey, 143 }, nil 144 } 145 roleNeedsEncryption := func(role string) bool { 146 return role == gadget.SystemData || role == gadget.SystemSave 147 } 148 var keysForRoles map[string]*EncryptionKeySet 149 150 for _, part := range created { 151 roleFmt := "" 152 if part.Role != "" { 153 roleFmt = fmt.Sprintf("role %v", part.Role) 154 } 155 logger.Noticef("created new partition %v for structure %v (size %v) %s", 156 part.Node, part, part.Size.IECString(), roleFmt) 157 if options.Encrypt && roleNeedsEncryption(part.Role) { 158 var keys *EncryptionKeySet 159 timings.Run(perfTimings, fmt.Sprintf("make-key-set[%s]", roleOrLabelOrName(part)), fmt.Sprintf("Create encryption key set for %s", roleOrLabelOrName(part)), func(timings.Measurer) { 160 keys, err = makeKeySet() 161 }) 162 if err != nil { 163 return nil, err 164 } 165 logger.Noticef("encrypting partition device %v", part.Node) 166 var dataPart *encryptedDevice 167 timings.Run(perfTimings, fmt.Sprintf("new-encrypted-device[%s]", roleOrLabelOrName(part)), fmt.Sprintf("Create encryption device for %s", roleOrLabelOrName(part)), func(timings.Measurer) { 168 dataPart, err = newEncryptedDevice(&part, keys.Key, part.Label) 169 }) 170 if err != nil { 171 return nil, err 172 } 173 174 timings.Run(perfTimings, fmt.Sprintf("add-recovery-key[%s]", roleOrLabelOrName(part)), fmt.Sprintf("Adding recovery key for %s", roleOrLabelOrName(part)), func(timings.Measurer) { 175 err = dataPart.AddRecoveryKey(keys.Key, keys.RecoveryKey) 176 }) 177 if err != nil { 178 return nil, err 179 } 180 181 // update the encrypted device node 182 part.Node = dataPart.Node 183 if keysForRoles == nil { 184 keysForRoles = map[string]*EncryptionKeySet{} 185 } 186 keysForRoles[part.Role] = keys 187 logger.Noticef("encrypted device %v", part.Node) 188 } 189 190 // use the diskLayout.SectorSize here instead of lv.SectorSize, we check 191 // that if there is a sector-size specified in the gadget that it 192 // matches what is on the disk, but sometimes there may not be a sector 193 // size specified in the gadget.yaml, but we will always have the sector 194 // size from the physical disk device 195 timings.Run(perfTimings, fmt.Sprintf("make-filesystem[%s]", roleOrLabelOrName(part)), fmt.Sprintf("Create filesystem for %s", part.Node), func(timings.Measurer) { 196 err = makeFilesystem(&part, diskLayout.SectorSize) 197 }) 198 if err != nil { 199 return nil, fmt.Errorf("cannot make filesystem for partition %s: %v", roleOrLabelOrName(part), err) 200 } 201 202 timings.Run(perfTimings, fmt.Sprintf("write-content[%s]", roleOrLabelOrName(part)), fmt.Sprintf("Write content for %s", roleOrLabelOrName(part)), func(timings.Measurer) { 203 err = writeContent(&part, gadgetRoot, observer) 204 }) 205 if err != nil { 206 return nil, err 207 } 208 209 if options.Mount && part.Label != "" && part.HasFilesystem() { 210 if err := mountFilesystem(&part, boot.InitramfsRunMntDir); err != nil { 211 return nil, err 212 } 213 } 214 } 215 216 return &InstalledSystemSideData{ 217 KeysForRoles: keysForRoles, 218 }, nil 219 } 220 221 // isCreatableAtInstall returns whether the gadget structure would be created at 222 // install - currently that is only ubuntu-save, ubuntu-data, and ubuntu-boot 223 func isCreatableAtInstall(gv *gadget.VolumeStructure) bool { 224 // a structure is creatable at install if it is one of the roles for 225 // system-save, system-data, or system-boot 226 switch gv.Role { 227 case gadget.SystemSave, gadget.SystemData, gadget.SystemBoot: 228 return true 229 default: 230 return false 231 } 232 } 233 234 func ensureLayoutCompatibility(gadgetLayout *gadget.LaidOutVolume, diskLayout *gadget.OnDiskVolume) error { 235 eq := func(ds gadget.OnDiskStructure, gs gadget.LaidOutStructure) (bool, string) { 236 dv := ds.VolumeStructure 237 gv := gs.VolumeStructure 238 nameMatch := gv.Name == dv.Name 239 if gadgetLayout.Schema == "mbr" { 240 // partitions have no names in MBR so bypass the name check 241 nameMatch = true 242 } 243 // Previous installation may have failed before filesystem creation or 244 // partition may be encrypted, so if the on disk offset matches the 245 // gadget offset, and the gadget structure is creatable during install, 246 // then they are equal 247 // otherwise, if they are not created during installation, the 248 // filesystem must be the same 249 check := nameMatch && ds.StartOffset == gs.StartOffset && (isCreatableAtInstall(gv) || dv.Filesystem == gv.Filesystem) 250 sizeMatches := dv.Size == gv.Size 251 if gv.Role == gadget.SystemData { 252 // system-data may have been expanded 253 sizeMatches = dv.Size >= gv.Size 254 } 255 if check && sizeMatches { 256 return true, "" 257 } 258 switch { 259 case !nameMatch: 260 // don't return a reason if the names don't match 261 return false, "" 262 case ds.StartOffset != gs.StartOffset: 263 return false, fmt.Sprintf("start offsets do not match (disk: %d (%s) and gadget: %d (%s))", ds.StartOffset, ds.StartOffset.IECString(), gs.StartOffset, gs.StartOffset.IECString()) 264 case !isCreatableAtInstall(gv) && dv.Filesystem != gv.Filesystem: 265 return false, "filesystems do not match and the partition is not creatable at install" 266 case dv.Size < gv.Size: 267 return false, "on disk size is smaller than gadget size" 268 case gv.Role != gadget.SystemData && dv.Size > gv.Size: 269 return false, "on disk size is larger than gadget size (and the role should not be expanded)" 270 default: 271 return false, "some other logic condition (should be impossible?)" 272 } 273 } 274 275 contains := func(haystack []gadget.LaidOutStructure, needle gadget.OnDiskStructure) (bool, string) { 276 reasonAbsent := "" 277 for _, h := range haystack { 278 matches, reasonNotMatches := eq(needle, h) 279 if matches { 280 return true, "" 281 } 282 // this has the effect of only returning the last non-empty reason 283 // string 284 if reasonNotMatches != "" { 285 reasonAbsent = reasonNotMatches 286 } 287 } 288 return false, reasonAbsent 289 } 290 291 // check size of volumes 292 if gadgetLayout.Size > diskLayout.Size { 293 return fmt.Errorf("device %v (%s) is too small to fit the requested layout (%s)", diskLayout.Device, 294 diskLayout.Size.IECString(), gadgetLayout.Size.IECString()) 295 } 296 297 // check that the sizes of all structures in the gadget are multiples of 298 // the disk sector size (unless the structure is the MBR) 299 for _, ls := range gadgetLayout.LaidOutStructure { 300 if !gadget.IsRoleMBR(ls) { 301 if ls.Size%diskLayout.SectorSize != 0 { 302 return fmt.Errorf("gadget volume structure %v size is not a multiple of disk sector size %v", 303 ls, diskLayout.SectorSize) 304 } 305 } 306 } 307 308 // Check if top level properties match 309 if !isCompatibleSchema(gadgetLayout.Volume.Schema, diskLayout.Schema) { 310 return fmt.Errorf("disk partitioning schema %q doesn't match gadget schema %q", diskLayout.Schema, gadgetLayout.Volume.Schema) 311 } 312 if gadgetLayout.Volume.ID != "" && gadgetLayout.Volume.ID != diskLayout.ID { 313 return fmt.Errorf("disk ID %q doesn't match gadget volume ID %q", diskLayout.ID, gadgetLayout.Volume.ID) 314 } 315 316 // Check if all existing device partitions are also in gadget 317 for _, ds := range diskLayout.Structure { 318 present, reasonAbsent := contains(gadgetLayout.LaidOutStructure, ds) 319 if !present { 320 if reasonAbsent != "" { 321 // use the right format so that it can be 322 // appended to the error message 323 reasonAbsent = fmt.Sprintf(": %s", reasonAbsent) 324 } 325 return fmt.Errorf("cannot find disk partition %s (starting at %d) in gadget%s", ds.Node, ds.StartOffset, reasonAbsent) 326 } 327 } 328 329 return nil 330 } 331 332 func isCompatibleSchema(gadgetSchema, diskSchema string) bool { 333 switch gadgetSchema { 334 // XXX: "mbr,gpt" is currently unsupported 335 case "", "gpt": 336 return diskSchema == "gpt" 337 case "mbr": 338 return diskSchema == "dos" 339 default: 340 return false 341 } 342 }