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