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