github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/cmd/snap-bootstrap/initramfs_systemd_mount.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 main
    21  
    22  import (
    23  	"fmt"
    24  	"io/ioutil"
    25  	"os"
    26  	"os/exec"
    27  	"path/filepath"
    28  	"time"
    29  
    30  	"github.com/snapcore/snapd/dirs"
    31  	"github.com/snapcore/snapd/osutil"
    32  	"github.com/snapcore/snapd/systemd"
    33  )
    34  
    35  var (
    36  	timeNow = time.Now
    37  
    38  	// default 1:30, as that is how long systemd will wait for services by
    39  	// default so seems a sensible default
    40  	defaultMountUnitWaitTimeout = time.Minute + 30*time.Second
    41  
    42  	unitFileDependOverride = `[Unit]
    43  Requires=%[1]s
    44  After=%[1]s
    45  `
    46  
    47  	doSystemdMount = doSystemdMountImpl
    48  )
    49  
    50  // systemdMountOptions reflects the set of options for mounting something using
    51  // systemd-mount(1)
    52  type systemdMountOptions struct {
    53  	// Tmpfs indicates that "what" should be ignored and a new tmpfs should be
    54  	// mounted at the location.
    55  	Tmpfs bool
    56  	// Ephemeral indicates that the mount should not persist from the initramfs
    57  	// to after the pivot_root to normal userspace. The default value, false,
    58  	// means that the mount will persist across the transition, this is done by
    59  	// creating systemd unit overrides for various initrd targets in /run that
    60  	// systemd understands when it isolates to the initrd-cleanup.target when
    61  	// the pivot_root is performed.
    62  	Ephemeral bool
    63  	// NeedsFsck indicates that before returning to the caller, an fsck check
    64  	// should be performed on the thing being mounted.
    65  	NeedsFsck bool
    66  	// NoWait will not wait until the systemd unit is active and running, which
    67  	// is the default behavior.
    68  	NoWait bool
    69  }
    70  
    71  // doSystemdMount will mount "what" at "where" using systemd-mount(1) with
    72  // various options. Note that in some error cases, the mount unit may have
    73  // already been created and it will not be deleted here, if that is the case
    74  // callers should check manually if the unit needs to be removed on error
    75  // conditions.
    76  func doSystemdMountImpl(what, where string, opts *systemdMountOptions) error {
    77  	if opts == nil {
    78  		opts = &systemdMountOptions{}
    79  	}
    80  
    81  	// doesn't make sense to fsck a tmpfs
    82  	if opts.NeedsFsck && opts.Tmpfs {
    83  		return fmt.Errorf("cannot mount %q at %q: impossible to fsck a tmpfs", what, where)
    84  	}
    85  
    86  	whereEscaped := systemd.EscapeUnitNamePath(where)
    87  	unitName := whereEscaped + ".mount"
    88  
    89  	args := []string{what, where, "--no-pager", "--no-ask-password"}
    90  	if opts.Tmpfs {
    91  		args = append(args, "--type=tmpfs")
    92  	}
    93  
    94  	if opts.NeedsFsck {
    95  		// note that with the --fsck=yes argument, systemd will block starting
    96  		// the mount unit on a new systemd-fsck@<what> unit that will run the
    97  		// fsck, so we don't need to worry about waiting for that to finish in
    98  		// the case where we are supposed to wait (which is the default for this
    99  		// function)
   100  		args = append(args, "--fsck=yes")
   101  	} else {
   102  		// the default is to use fsck=yes, so if it doesn't need fsck we need to
   103  		// explicitly turn it off
   104  		args = append(args, "--fsck=no")
   105  	}
   106  
   107  	// Under all circumstances that we use systemd-mount here from
   108  	// snap-bootstrap, it is expected to be okay to block waiting for the unit
   109  	// to be started and become active, because snap-bootstrap is, by design,
   110  	// expected to run as late as possible in the initramfs, and so any
   111  	// dependencies there might be in systemd creating and starting these mount
   112  	// units should already be ready and so we will not block forever. If
   113  	// however there was something going on in systemd at the same time that the
   114  	// mount unit depended on, we could hit a deadlock blocking as systemd will
   115  	// not enqueue this job until it's dependencies are ready, and so if those
   116  	// things depend on this mount unit we are stuck. The solution to this
   117  	// situation is to make snap-bootstrap run as late as possible before
   118  	// mounting things.
   119  	// However, we leave in the option to not block if there is ever a reason
   120  	// we need to do so.
   121  	if opts.NoWait {
   122  		args = append(args, "--no-block")
   123  	}
   124  
   125  	// note that we do not currently parse any output from systemd-mount, but if
   126  	// we ever do, take special care surrounding the debug output that systemd
   127  	// outputs with the "debug" kernel command line present (or equivalently the
   128  	// SYSTEMD_LOG_LEVEL=debug env var) which will add lots of additional output
   129  	// to stderr from systemd commands
   130  	out, err := exec.Command("systemd-mount", args...).CombinedOutput()
   131  	if err != nil {
   132  		return osutil.OutputErr(out, err)
   133  	}
   134  
   135  	// if it should survive pivot_root() then we need to add overrides for this
   136  	// unit to /run/systemd units
   137  	if !opts.Ephemeral {
   138  		// to survive the pivot_root, we need to make the mount units depend on
   139  		// all of the various initrd special targets by adding runtime conf
   140  		// files there
   141  		// note we could do this statically in the initramfs main filesystem
   142  		// layout, but that means that changes to snap-bootstrap would block on
   143  		// waiting for those files to be added before things works here, this is
   144  		// a more flexible strategy that puts snap-bootstrap in control
   145  		overrideContent := []byte(fmt.Sprintf(unitFileDependOverride, unitName))
   146  		for _, initrdUnit := range []string{
   147  			"initrd.target",
   148  			"initrd-fs.target",
   149  			"initrd-switch-root.target",
   150  			"local-fs.target",
   151  		} {
   152  			targetDir := filepath.Join(dirs.GlobalRootDir, "/run/systemd/system", initrdUnit+".d")
   153  			err := os.MkdirAll(targetDir, 0755)
   154  			if err != nil {
   155  				return err
   156  			}
   157  
   158  			// add an override file for the initrd unit to depend on this mount
   159  			// unit so that when we isolate to the initrd unit, it does not get
   160  			// unmounted
   161  			fname := fmt.Sprintf("snap_bootstrap_%s.conf", whereEscaped)
   162  			err = ioutil.WriteFile(filepath.Join(targetDir, fname), overrideContent, 0644)
   163  			if err != nil {
   164  				return err
   165  			}
   166  		}
   167  	}
   168  
   169  	if !opts.NoWait {
   170  		// TODO: is this necessary, systemd-mount seems to only return when the
   171  		// unit is active and the mount is there, but perhaps we should be a bit
   172  		// paranoid here and wait anyways?
   173  		// see systemd-mount(1)
   174  
   175  		// wait for the mount to exist
   176  		start := timeNow()
   177  		var now time.Time
   178  		for now = timeNow(); now.Sub(start) < defaultMountUnitWaitTimeout; now = timeNow() {
   179  			mounted, err := osutilIsMounted(where)
   180  			if mounted {
   181  				break
   182  			}
   183  			if err != nil {
   184  				return err
   185  			}
   186  		}
   187  
   188  		if now.Sub(start) > defaultMountUnitWaitTimeout {
   189  			return fmt.Errorf("timed out after %s waiting for mount %s on %s", defaultMountUnitWaitTimeout, what, where)
   190  		}
   191  	}
   192  
   193  	return nil
   194  }