github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/boot/modeenv.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 boot 21 22 import ( 23 "bytes" 24 "encoding/json" 25 "fmt" 26 "io" 27 "os" 28 "path/filepath" 29 "reflect" 30 "sort" 31 "strings" 32 33 "github.com/mvo5/goconfigparser" 34 35 "github.com/snapcore/snapd/dirs" 36 "github.com/snapcore/snapd/osutil" 37 ) 38 39 type bootAssetsMap map[string][]string 40 41 // bootCommandLines is a list of kernel command lines. The command lines are 42 // marshalled as JSON as a comma can be present in the module parameters. 43 type bootCommandLines []string 44 45 // Modeenv is a file on UC20 that provides additional information 46 // about the current mode (run,recover,install) 47 type Modeenv struct { 48 Mode string `key:"mode"` 49 RecoverySystem string `key:"recovery_system"` 50 // CurrentRecoverySystems is a list of labels corresponding to recovery 51 // systems that have been tested or are in the process of being tried, 52 // thus only the run key is resealed for these systems. 53 CurrentRecoverySystems []string `key:"current_recovery_systems"` 54 // GoodRecoverySystems is a list of labels corresponding to recovery 55 // systems that were tested and are prepared to use for recovering. 56 // The fallback keys are resealed for these systems. 57 GoodRecoverySystems []string `key:"good_recovery_systems"` 58 Base string `key:"base"` 59 TryBase string `key:"try_base"` 60 BaseStatus string `key:"base_status"` 61 CurrentKernels []string `key:"current_kernels"` 62 Model string `key:"model"` 63 BrandID string `key:"model,secondary"` 64 Grade string `key:"grade"` 65 // CurrentTrustedBootAssets is a map of a run bootloader's asset names to 66 // a list of hashes of the asset contents. Typically the first entry in 67 // the list is a hash of an asset the system currently boots with (or is 68 // expected to have booted with). The second entry, if present, is the 69 // hash of an entry added when an update of the asset was being applied 70 // and will become the sole entry after a successful boot. 71 CurrentTrustedBootAssets bootAssetsMap `key:"current_trusted_boot_assets"` 72 // CurrentTrustedRecoveryBootAssetsMap is a map of a recovery bootloader's 73 // asset names to a list of hashes of the asset contents. Used similarly 74 // to CurrentTrustedBootAssets. 75 CurrentTrustedRecoveryBootAssets bootAssetsMap `key:"current_trusted_recovery_boot_assets"` 76 // CurrentKernelCommandLines is a list of the expected kernel command 77 // lines when booting into run mode. It will typically only be one 78 // element for normal operations, but may contain two elements during 79 // update scenarios. 80 CurrentKernelCommandLines bootCommandLines `key:"current_kernel_command_lines"` 81 // TODO:UC20 add a per recovery system list of kernel command lines 82 83 // read is set to true when a modenv was read successfully 84 read bool 85 86 // originRootdir is set to the root whence the modeenv was 87 // read from, and where it will be written back to 88 originRootdir string 89 90 // extrakeys is all the keys in the modeenv we read from the file but don't 91 // understand, we keep track of this so that if we read a new modeenv with 92 // extra keys and need to rewrite it, we will write those new keys as well 93 extrakeys map[string]string 94 } 95 96 var modeenvKnownKeys = make(map[string]bool) 97 98 func init() { 99 st := reflect.TypeOf(Modeenv{}) 100 num := st.NumField() 101 for i := 0; i < num; i++ { 102 f := st.Field(i) 103 if f.PkgPath != "" { 104 // unexported 105 continue 106 } 107 key := f.Tag.Get("key") 108 if key == "" { 109 panic(fmt.Sprintf("modeenv %s field has no key tag", f.Name)) 110 } 111 const secondaryModifier = ",secondary" 112 if strings.HasSuffix(key, secondaryModifier) { 113 // secondary field in a group fields 114 // corresponding to one file key 115 key := key[:len(key)-len(secondaryModifier)] 116 if !modeenvKnownKeys[key] { 117 panic(fmt.Sprintf("modeenv %s field marked as secondary for not yet defined key %q", f.Name, key)) 118 } 119 continue 120 } 121 if modeenvKnownKeys[key] { 122 panic(fmt.Sprintf("modeenv key %q repeated on %s", key, f.Name)) 123 } 124 modeenvKnownKeys[key] = true 125 } 126 } 127 128 func modeenvFile(rootdir string) string { 129 if rootdir == "" { 130 rootdir = dirs.GlobalRootDir 131 } 132 return dirs.SnapModeenvFileUnder(rootdir) 133 } 134 135 // ReadModeenv attempts to read the modeenv file at 136 // <rootdir>/var/iib/snapd/modeenv. 137 func ReadModeenv(rootdir string) (*Modeenv, error) { 138 modeenvPath := modeenvFile(rootdir) 139 cfg := goconfigparser.New() 140 cfg.AllowNoSectionHeader = true 141 if err := cfg.ReadFile(modeenvPath); err != nil { 142 return nil, err 143 } 144 145 // TODO:UC20: should we check these errors and try to do something? 146 m := Modeenv{ 147 read: true, 148 originRootdir: rootdir, 149 extrakeys: make(map[string]string), 150 } 151 unmarshalModeenvValueFromCfg(cfg, "recovery_system", &m.RecoverySystem) 152 unmarshalModeenvValueFromCfg(cfg, "current_recovery_systems", &m.CurrentRecoverySystems) 153 unmarshalModeenvValueFromCfg(cfg, "good_recovery_systems", &m.GoodRecoverySystems) 154 unmarshalModeenvValueFromCfg(cfg, "mode", &m.Mode) 155 if m.Mode == "" { 156 return nil, fmt.Errorf("internal error: mode is unset") 157 } 158 unmarshalModeenvValueFromCfg(cfg, "base", &m.Base) 159 unmarshalModeenvValueFromCfg(cfg, "base_status", &m.BaseStatus) 160 unmarshalModeenvValueFromCfg(cfg, "try_base", &m.TryBase) 161 162 // current_kernels is a comma-delimited list in a string 163 unmarshalModeenvValueFromCfg(cfg, "current_kernels", &m.CurrentKernels) 164 var bm modeenvModel 165 unmarshalModeenvValueFromCfg(cfg, "model", &bm) 166 m.BrandID = bm.brandID 167 m.Model = bm.model 168 // expect the caller to validate the grade 169 unmarshalModeenvValueFromCfg(cfg, "grade", &m.Grade) 170 unmarshalModeenvValueFromCfg(cfg, "current_trusted_boot_assets", &m.CurrentTrustedBootAssets) 171 unmarshalModeenvValueFromCfg(cfg, "current_trusted_recovery_boot_assets", &m.CurrentTrustedRecoveryBootAssets) 172 unmarshalModeenvValueFromCfg(cfg, "current_kernel_command_lines", &m.CurrentKernelCommandLines) 173 174 // save all the rest of the keys we don't understand 175 keys, err := cfg.Options("") 176 if err != nil { 177 return nil, err 178 } 179 for _, k := range keys { 180 if !modeenvKnownKeys[k] { 181 val, err := cfg.Get("", k) 182 if err != nil { 183 return nil, err 184 } 185 m.extrakeys[k] = val 186 } 187 } 188 189 return &m, nil 190 } 191 192 // deepEqual compares two modeenvs to ensure they are textually the same. It 193 // does not consider whether the modeenvs were read from disk or created purely 194 // in memory. It also does not sort or otherwise mutate any sub-objects, 195 // performing simple strict verification of sub-objects. 196 func (m *Modeenv) deepEqual(m2 *Modeenv) bool { 197 b, err := json.Marshal(m) 198 if err != nil { 199 return false 200 } 201 b2, err := json.Marshal(m2) 202 if err != nil { 203 return false 204 } 205 return bytes.Equal(b, b2) 206 } 207 208 // Copy will make a deep copy of a Modeenv. 209 func (m *Modeenv) Copy() (*Modeenv, error) { 210 // to avoid hard-coding all fields here and manually copying everything, we 211 // take the easy way out and serialize to json then re-import into a 212 // empty Modeenv 213 b, err := json.Marshal(m) 214 if err != nil { 215 return nil, err 216 } 217 m2 := &Modeenv{} 218 err = json.Unmarshal(b, m2) 219 if err != nil { 220 return nil, err 221 } 222 223 // manually copy the unexported fields as they won't be in the JSON 224 m2.read = m.read 225 m2.originRootdir = m.originRootdir 226 return m2, nil 227 } 228 229 // Write outputs the modeenv to the file where it was read, only valid on 230 // modeenv that has been read. 231 func (m *Modeenv) Write() error { 232 if m.read { 233 return m.WriteTo(m.originRootdir) 234 } 235 return fmt.Errorf("internal error: must use WriteTo with modeenv not read from disk") 236 } 237 238 // WriteTo outputs the modeenv to the file at <rootdir>/var/lib/snapd/modeenv. 239 func (m *Modeenv) WriteTo(rootdir string) error { 240 modeenvPath := modeenvFile(rootdir) 241 242 if err := os.MkdirAll(filepath.Dir(modeenvPath), 0755); err != nil { 243 return err 244 } 245 buf := bytes.NewBuffer(nil) 246 if m.Mode == "" { 247 return fmt.Errorf("internal error: mode is unset") 248 } 249 marshalModeenvEntryTo(buf, "mode", m.Mode) 250 marshalModeenvEntryTo(buf, "recovery_system", m.RecoverySystem) 251 marshalModeenvEntryTo(buf, "current_recovery_systems", m.CurrentRecoverySystems) 252 marshalModeenvEntryTo(buf, "good_recovery_systems", m.GoodRecoverySystems) 253 marshalModeenvEntryTo(buf, "base", m.Base) 254 marshalModeenvEntryTo(buf, "try_base", m.TryBase) 255 marshalModeenvEntryTo(buf, "base_status", m.BaseStatus) 256 marshalModeenvEntryTo(buf, "current_kernels", strings.Join(m.CurrentKernels, ",")) 257 if m.Model != "" || m.Grade != "" { 258 if m.Model == "" { 259 return fmt.Errorf("internal error: model is unset") 260 } 261 if m.BrandID == "" { 262 return fmt.Errorf("internal error: brand is unset") 263 } 264 marshalModeenvEntryTo(buf, "model", &modeenvModel{brandID: m.BrandID, model: m.Model}) 265 } 266 marshalModeenvEntryTo(buf, "grade", m.Grade) 267 marshalModeenvEntryTo(buf, "current_trusted_boot_assets", m.CurrentTrustedBootAssets) 268 marshalModeenvEntryTo(buf, "current_trusted_recovery_boot_assets", m.CurrentTrustedRecoveryBootAssets) 269 marshalModeenvEntryTo(buf, "current_kernel_command_lines", m.CurrentKernelCommandLines) 270 271 // write all the extra keys at the end 272 // sort them for test convenience 273 extraKeys := make([]string, 0, len(m.extrakeys)) 274 for k := range m.extrakeys { 275 extraKeys = append(extraKeys, k) 276 } 277 sort.Strings(extraKeys) 278 for _, k := range extraKeys { 279 marshalModeenvEntryTo(buf, k, m.extrakeys[k]) 280 } 281 282 if err := osutil.AtomicWriteFile(modeenvPath, buf.Bytes(), 0644, 0); err != nil { 283 return err 284 } 285 return nil 286 } 287 288 type modeenvValueMarshaller interface { 289 MarshalModeenvValue() (string, error) 290 } 291 292 type modeenvValueUnmarshaller interface { 293 UnmarshalModeenvValue(value string) error 294 } 295 296 // marshalModeenvEntryTo marshals to out what as value for an entry 297 // with the given key. If what is empty this is a no-op. 298 func marshalModeenvEntryTo(out io.Writer, key string, what interface{}) error { 299 var asString string 300 switch v := what.(type) { 301 case string: 302 if v == "" { 303 return nil 304 } 305 asString = v 306 case []string: 307 if len(v) == 0 { 308 return nil 309 } 310 asString = asModeenvStringList(v) 311 default: 312 if vm, ok := what.(modeenvValueMarshaller); ok { 313 marshalled, err := vm.MarshalModeenvValue() 314 if err != nil { 315 return fmt.Errorf("cannot marshal value for key %q: %v", key, err) 316 } 317 asString = marshalled 318 } else if jm, ok := what.(json.Marshaler); ok { 319 marshalled, err := jm.MarshalJSON() 320 if err != nil { 321 return fmt.Errorf("cannot marshal value for key %q as JSON: %v", key, err) 322 } 323 asString = string(marshalled) 324 if asString == "null" { 325 // no need to keep nulls in the modeenv 326 return nil 327 } 328 } else { 329 return fmt.Errorf("internal error: cannot marshal unsupported type %T value %v for key %q", what, what, key) 330 } 331 } 332 _, err := fmt.Fprintf(out, "%s=%s\n", key, asString) 333 return err 334 } 335 336 // unmarshalModeenvValueFromCfg unmarshals the value of the entry with 337 // th given key to dest. If there's no such entry dest might be left 338 // empty. 339 func unmarshalModeenvValueFromCfg(cfg *goconfigparser.ConfigParser, key string, dest interface{}) error { 340 if dest == nil { 341 return fmt.Errorf("internal error: cannot unmarshal to nil") 342 } 343 kv, _ := cfg.Get("", key) 344 345 switch v := dest.(type) { 346 case *string: 347 *v = kv 348 case *[]string: 349 *v = splitModeenvStringList(kv) 350 default: 351 if vm, ok := v.(modeenvValueUnmarshaller); ok { 352 if err := vm.UnmarshalModeenvValue(kv); err != nil { 353 return fmt.Errorf("cannot unmarshal modeenv value %q to %T: %v", kv, dest, err) 354 } 355 return nil 356 } else if jm, ok := v.(json.Unmarshaler); ok { 357 if len(kv) == 0 { 358 // leave jm empty 359 return nil 360 } 361 if err := jm.UnmarshalJSON([]byte(kv)); err != nil { 362 return fmt.Errorf("cannot unmarshal modeenv value %q as JSON to %T: %v", kv, dest, err) 363 } 364 return nil 365 } 366 return fmt.Errorf("internal error: cannot unmarshal value %q for unsupported type %T", kv, dest) 367 } 368 return nil 369 } 370 371 func splitModeenvStringList(v string) []string { 372 if v == "" { 373 return nil 374 } 375 split := strings.Split(v, ",") 376 // drop empty strings 377 nonEmpty := make([]string, 0, len(split)) 378 for _, one := range split { 379 if one != "" { 380 nonEmpty = append(nonEmpty, one) 381 } 382 } 383 if len(nonEmpty) == 0 { 384 return nil 385 } 386 return nonEmpty 387 } 388 389 func asModeenvStringList(v []string) string { 390 return strings.Join(v, ",") 391 } 392 393 type modeenvModel struct { 394 brandID, model string 395 } 396 397 func (m *modeenvModel) MarshalModeenvValue() (string, error) { 398 return fmt.Sprintf("%s/%s", m.brandID, m.model), nil 399 } 400 401 func (m *modeenvModel) UnmarshalModeenvValue(brandSlashModel string) error { 402 if bsmSplit := strings.SplitN(brandSlashModel, "/", 2); len(bsmSplit) == 2 { 403 if bsmSplit[0] != "" && bsmSplit[1] != "" { 404 m.brandID = bsmSplit[0] 405 m.model = bsmSplit[1] 406 } 407 } 408 return nil 409 } 410 411 func (b bootAssetsMap) MarshalJSON() ([]byte, error) { 412 asMap := map[string][]string(b) 413 return json.Marshal(asMap) 414 } 415 416 func (b *bootAssetsMap) UnmarshalJSON(data []byte) error { 417 var asMap map[string][]string 418 if err := json.Unmarshal(data, &asMap); err != nil { 419 return err 420 } 421 *b = bootAssetsMap(asMap) 422 return nil 423 } 424 425 func (s bootCommandLines) MarshalJSON() ([]byte, error) { 426 return json.Marshal([]string(s)) 427 } 428 429 func (s *bootCommandLines) UnmarshalJSON(data []byte) error { 430 var asList []string 431 if err := json.Unmarshal(data, &asList); err != nil { 432 return err 433 } 434 *s = bootCommandLines(asList) 435 return nil 436 }