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  }