github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/boot/cmdline.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 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  	"bufio"
    24  	"bytes"
    25  	"errors"
    26  	"fmt"
    27  	"io/ioutil"
    28  	"strings"
    29  
    30  	"github.com/snapcore/snapd/asserts"
    31  	"github.com/snapcore/snapd/bootloader"
    32  	"github.com/snapcore/snapd/logger"
    33  	"github.com/snapcore/snapd/strutil"
    34  )
    35  
    36  const (
    37  	// ModeRun indicates the regular operating system mode of the device.
    38  	ModeRun = "run"
    39  	// ModeInstall is a mode in which a new system is installed on the
    40  	// device.
    41  	ModeInstall = "install"
    42  	// ModeRecover is a mode in which the device boots into the recovery
    43  	// system.
    44  	ModeRecover = "recover"
    45  )
    46  
    47  var (
    48  	// the kernel commandline - can be overridden in tests
    49  	procCmdline = "/proc/cmdline"
    50  
    51  	validModes = []string{ModeInstall, ModeRecover, ModeRun}
    52  )
    53  
    54  func whichModeAndRecoverySystem(cmdline []byte) (mode string, sysLabel string, err error) {
    55  	scanner := bufio.NewScanner(bytes.NewBuffer(cmdline))
    56  	scanner.Split(bufio.ScanWords)
    57  
    58  	for scanner.Scan() {
    59  		w := scanner.Text()
    60  		if strings.HasPrefix(w, "snapd_recovery_mode=") {
    61  			if mode != "" {
    62  				return "", "", fmt.Errorf("cannot specify mode more than once")
    63  			}
    64  			mode = strings.SplitN(w, "=", 2)[1]
    65  			if mode == "" {
    66  				mode = ModeInstall
    67  			}
    68  			if !strutil.ListContains(validModes, mode) {
    69  				return "", "", fmt.Errorf("cannot use unknown mode %q", mode)
    70  			}
    71  		}
    72  		if strings.HasPrefix(w, "snapd_recovery_system=") {
    73  			if sysLabel != "" {
    74  				return "", "", fmt.Errorf("cannot specify recovery system label more than once")
    75  			}
    76  			sysLabel = strings.SplitN(w, "=", 2)[1]
    77  		}
    78  	}
    79  	if err := scanner.Err(); err != nil {
    80  		return "", "", err
    81  	}
    82  	switch {
    83  	case mode == "" && sysLabel == "":
    84  		return "", "", fmt.Errorf("cannot detect mode nor recovery system to use")
    85  	case mode == ModeInstall && sysLabel == "":
    86  		return "", "", fmt.Errorf("cannot specify install mode without system label")
    87  	case mode == ModeRun && sysLabel != "":
    88  		// XXX: should we silently ignore the label? at least log for now
    89  		logger.Noticef(`ignoring recovery system label %q in "run" mode`, sysLabel)
    90  		sysLabel = ""
    91  	}
    92  	return mode, sysLabel, nil
    93  }
    94  
    95  // ModeAndRecoverySystemFromKernelCommandLine returns the current system mode
    96  // and the recovery system label as passed in the kernel command line by the
    97  // bootloader.
    98  func ModeAndRecoverySystemFromKernelCommandLine() (mode, sysLabel string, err error) {
    99  	cmdline, err := ioutil.ReadFile(procCmdline)
   100  	if err != nil {
   101  		return "", "", err
   102  	}
   103  	return whichModeAndRecoverySystem(cmdline)
   104  }
   105  
   106  // MockProcCmdline overrides the path to /proc/cmdline. For use in tests.
   107  func MockProcCmdline(newPath string) (restore func()) {
   108  	oldProcCmdline := procCmdline
   109  	procCmdline = newPath
   110  	return func() {
   111  		procCmdline = oldProcCmdline
   112  	}
   113  }
   114  
   115  var errBootConfigNotManaged = errors.New("boot config is not managed")
   116  
   117  func getBootloaderManagingItsAssets(where string, opts *bootloader.Options) (bootloader.TrustedAssetsBootloader, error) {
   118  	bl, err := bootloader.Find(where, opts)
   119  	if err != nil {
   120  		return nil, fmt.Errorf("internal error: cannot find trusted assets bootloader under %q: %v", where, err)
   121  	}
   122  	mbl, ok := bl.(bootloader.TrustedAssetsBootloader)
   123  	if !ok {
   124  		// the bootloader cannot manage its scripts
   125  		return nil, errBootConfigNotManaged
   126  	}
   127  	return mbl, nil
   128  }
   129  
   130  const (
   131  	currentEdition = iota
   132  	candidateEdition
   133  )
   134  
   135  func composeCommandLine(model *asserts.Model, currentOrCandidate int, mode, system string) (string, error) {
   136  	if model.Grade() == asserts.ModelGradeUnset {
   137  		return "", nil
   138  	}
   139  	if mode != ModeRun && mode != ModeRecover {
   140  		return "", fmt.Errorf("internal error: unsupported command line mode %q", mode)
   141  	}
   142  	// get the run mode bootloader under the native run partition layout
   143  	opts := &bootloader.Options{
   144  		Role:        bootloader.RoleRunMode,
   145  		NoSlashBoot: true,
   146  	}
   147  	bootloaderRootDir := InitramfsUbuntuBootDir
   148  	modeArg := "snapd_recovery_mode=run"
   149  	systemArg := ""
   150  	if mode == ModeRecover {
   151  		// dealing with recovery system bootloader
   152  		opts.Role = bootloader.RoleRecovery
   153  		bootloaderRootDir = InitramfsUbuntuSeedDir
   154  		// recovery mode & system command line arguments
   155  		modeArg = "snapd_recovery_mode=recover"
   156  		systemArg = fmt.Sprintf("snapd_recovery_system=%v", system)
   157  	}
   158  	mbl, err := getBootloaderManagingItsAssets(bootloaderRootDir, opts)
   159  	if err != nil {
   160  		if err == errBootConfigNotManaged {
   161  			return "", nil
   162  		}
   163  		return "", err
   164  	}
   165  	// TODO:UC20: fetch extra args from gadget
   166  	extraArgs := ""
   167  	if currentOrCandidate == currentEdition {
   168  		return mbl.CommandLine(modeArg, systemArg, extraArgs)
   169  	} else {
   170  		return mbl.CandidateCommandLine(modeArg, systemArg, extraArgs)
   171  	}
   172  }
   173  
   174  // ComposeRecoveryCommandLine composes the kernel command line used when booting
   175  // a given system in recover mode.
   176  func ComposeRecoveryCommandLine(model *asserts.Model, system string) (string, error) {
   177  	return composeCommandLine(model, currentEdition, ModeRecover, system)
   178  }
   179  
   180  // ComposeCommandLine composes the kernel command line used when booting the
   181  // system in run mode.
   182  func ComposeCommandLine(model *asserts.Model) (string, error) {
   183  	return composeCommandLine(model, currentEdition, ModeRun, "")
   184  }
   185  
   186  // TODO:UC20: add helper to compose candidate command line for a recovery system
   187  
   188  // ComposeCandidateCommandLine composes the kernel command line used when
   189  // booting the system in run mode with the current built-in edition of managed
   190  // boot assets.
   191  func ComposeCandidateCommandLine(model *asserts.Model) (string, error) {
   192  	return composeCommandLine(model, candidateEdition, ModeRun, "")
   193  }