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