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