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  }