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