github.com/stackdocker/rkt@v0.10.1-0.20151109095037-1aa827478248/stage1/init/kvm/mount.go (about)

     1  // Copyright 2015 The rkt Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // mount.go provides functions for creating mount units for managing
    16  // inner(kind=empty) and external(kind=host) volumes.
    17  // note: used only for kvm flavor (lkvm based)
    18  //
    19  // Idea.
    20  // For example when we have two volumes:
    21  // 1) --volume=hostdata,kind=host,source=/host/some_data_to_share
    22  // 2) --volume=temporary,kind=empty
    23  // then in stage1/rootfs rkt creates two folders (in rootfs of guest)
    24  // 1) /mnt/hostdata - which is mounted through 9p host thanks to
    25  //					lkvm --9p=/host/some_data_to_share,hostdata flag shared to quest
    26  // 2) /mnt/temporary - is created as empty directory in guest
    27  //
    28  // both of them are then bind mounted to /opt/stage2/<application/<mountPoint.path>
    29  // for every application, that has mountPoints specified in ACI json
    30  // - host mounting is realized by podToSystemdHostMountUnits (for whole pod),
    31  //   which creates mount.units (9p) required and ordered before all applications
    32  //   service units
    33  // - bind mounting is realized by appToSystemdMountUnits (for each app),
    34  //   which creates mount.units (bind) required and ordered before particular application
    35  // note: systemd mount units require /usr/bin/mount
    36  package kvm
    37  
    38  import (
    39  	"fmt"
    40  	"io/ioutil"
    41  	"log"
    42  	"os"
    43  	"path/filepath"
    44  	"strings"
    45  
    46  	"github.com/coreos/rkt/Godeps/_workspace/src/github.com/appc/spec/schema"
    47  	"github.com/coreos/rkt/Godeps/_workspace/src/github.com/appc/spec/schema/types"
    48  	"github.com/coreos/rkt/Godeps/_workspace/src/github.com/coreos/go-systemd/unit"
    49  	"github.com/coreos/rkt/common"
    50  	initcommon "github.com/coreos/rkt/stage1/init/common"
    51  )
    52  
    53  const (
    54  	// location within stage1 rootfs where shared volumes will be put
    55  	// (or empty directories for kind=empty)
    56  	stage1MntDir = "/mnt/"
    57  )
    58  
    59  // serviceUnitName returns a systemd service unit name for the given app name.
    60  // note: it was shamefully copy-pasted from stage1/init/path.go
    61  // TODO: extract common functions from path.go
    62  func serviceUnitName(appName types.ACName) string {
    63  	return appName.String() + ".service"
    64  }
    65  
    66  // installNewMountUnit creates and installs a new mount unit in the default
    67  // systemd location (/usr/lib/systemd/system) inside the pod stage1 filesystem.
    68  // root is pod's absolute stage1 path (from Pod.Root).
    69  // beforeAndrequiredBy creates a systemd unit dependency (can be space separated
    70  // for multi).
    71  // It returns the name of the generated unit.
    72  func installNewMountUnit(root, what, where, fsType, options, beforeAndrequiredBy, unitsDir string) (string, error) {
    73  	opts := []*unit.UnitOption{
    74  		unit.NewUnitOption("Unit", "Description", fmt.Sprintf("Mount unit for %s", where)),
    75  		unit.NewUnitOption("Unit", "DefaultDependencies", "false"),
    76  		unit.NewUnitOption("Unit", "Before", beforeAndrequiredBy),
    77  		unit.NewUnitOption("Mount", "What", what),
    78  		unit.NewUnitOption("Mount", "Where", where),
    79  		unit.NewUnitOption("Mount", "Type", fsType),
    80  		unit.NewUnitOption("Mount", "Options", options),
    81  		unit.NewUnitOption("Install", "RequiredBy", beforeAndrequiredBy),
    82  	}
    83  
    84  	unitsPath := filepath.Join(root, unitsDir)
    85  	unitName := unit.UnitNamePathEscape(where + ".mount")
    86  
    87  	if err := writeUnit(opts, filepath.Join(unitsPath, unitName)); err != nil {
    88  		return "", err
    89  	}
    90  	log.Printf("mount unit created: %q in %q (what=%q, where=%q)", unitName, unitsPath, what, where)
    91  
    92  	return unitName, nil
    93  }
    94  
    95  func writeUnit(opts []*unit.UnitOption, unitPath string) error {
    96  	unitBytes, err := ioutil.ReadAll(unit.Serialize(opts))
    97  	if err != nil {
    98  		return fmt.Errorf("failed to serialize mount unit file to bytes %q: %v", unitPath, err)
    99  	}
   100  
   101  	err = ioutil.WriteFile(unitPath, unitBytes, 0644)
   102  	if err != nil {
   103  		return fmt.Errorf("failed to create mount unit file %q: %v", unitPath, err)
   104  	}
   105  
   106  	return nil
   107  }
   108  
   109  // PodToSystemdHostMountUnits create host shared remote file system
   110  // mounts (using e.g. 9p) according to https://www.kernel.org/doc/Documentation/filesystems/9p.txt.
   111  // Additionally it creates required directories in stage1MntDir and then prepares
   112  // bind mount unit for each app.
   113  // "root" parameter is stage1 root filesystem path.
   114  // appNames are used to create before/required dependency between mount unit and
   115  // app service units.
   116  func PodToSystemdHostMountUnits(root string, volumes []types.Volume, appNames []types.ACName, unitsDir string) error {
   117  	// pod volumes need to mount p9 qemu mount_tags
   118  	for _, vol := range volumes {
   119  		// only host shared volumes
   120  
   121  		name := vol.Name.String() // acts as a mount tag 9p
   122  		// /var/lib/.../pod/run/rootfs/mnt/{volumeName}
   123  		mountPoint := filepath.Join(root, stage1MntDir, name)
   124  
   125  		// for kind "empty" that will be shared among applications
   126  		log.Printf("creating an empty volume folder for sharing: %q", mountPoint)
   127  		err := os.MkdirAll(mountPoint, 0700)
   128  		if err != nil {
   129  			return err
   130  		}
   131  
   132  		// serviceNames for ordering and requirements dependency for apps
   133  		var serviceNames []string
   134  		for _, appName := range appNames {
   135  			serviceNames = append(serviceNames, serviceUnitName(appName))
   136  		}
   137  
   138  		// for host kind we create a mount unit to mount host shared folder
   139  		if vol.Kind == "host" {
   140  			_, err = installNewMountUnit(root,
   141  				name, // what (source) in 9p it is a channel tag which equals to volume.Name/mountPoint.name
   142  				filepath.Join(stage1MntDir, name), // where - destination
   143  				"9p",                            // fsType
   144  				"trans=virtio",                  // 9p specific options
   145  				strings.Join(serviceNames, " "), // space separated list of services for unit dependency
   146  				unitsDir,
   147  			)
   148  			if err != nil {
   149  				return err
   150  			}
   151  		}
   152  	}
   153  
   154  	return nil
   155  }
   156  
   157  // AppToSystemdMountUnits prepare bind mount unit for empty or host kind mounting
   158  // between stage1 rootfs and chrooted filesystem for application
   159  func AppToSystemdMountUnits(root string, appName types.ACName, volumes []types.Volume, ra *schema.RuntimeApp, unitsDir string) error {
   160  	app := ra.App
   161  
   162  	vols := make(map[types.ACName]types.Volume)
   163  	for _, v := range volumes {
   164  		vols[v.Name] = v
   165  	}
   166  
   167  	mounts, err := initcommon.GenerateMounts(ra, vols)
   168  	if err != nil {
   169  		return err
   170  	}
   171  
   172  	for _, m := range mounts {
   173  		vol := vols[m.Volume]
   174  
   175  		// source relative to stage1 rootfs to relative pod root
   176  		whatPath := filepath.Join(stage1MntDir, vol.Name.String())
   177  		whatFullPath := filepath.Join(root, whatPath)
   178  
   179  		// destination relative to stage1 rootfs and relative to pod root
   180  		wherePath := filepath.Join(common.RelAppRootfsPath(appName), m.Path)
   181  		whereFullPath := filepath.Join(root, wherePath)
   182  
   183  		// assertion to make sure that "what" exists (created earlier by PodToSystemdHostMountUnits)
   184  		log.Printf("checking required source path: %q", whatFullPath)
   185  		if _, err := os.Stat(whatFullPath); os.IsNotExist(err) {
   186  			return fmt.Errorf("bug: missing source for volume %v", vol.Name)
   187  		}
   188  
   189  		// optionally prepare app directory
   190  		log.Printf("optionally preparing destination path: %q", whereFullPath)
   191  		err := os.MkdirAll(whereFullPath, 0700)
   192  		if err != nil {
   193  			return fmt.Errorf("failed to prepare dir for mount %v: %v", m.Volume, err)
   194  		}
   195  
   196  		// install new mount unit for bind mount /mnt/volumeName -> /opt/stage2/{app-id}/rootfs/{{mountPoint.Path}}
   197  		mu, err := installNewMountUnit(
   198  			root,      // where put a mount unit
   199  			whatPath,  // what - stage1 rootfs /mnt/VolumeName
   200  			wherePath, // where - inside chroot app filesystem
   201  			"bind",    // fstype
   202  			"bind",    // options
   203  			serviceUnitName(appName),
   204  			unitsDir,
   205  		)
   206  		if err != nil {
   207  			return fmt.Errorf("cannot install new mount unit for app %q: %v", appName.String(), err)
   208  		}
   209  
   210  		// TODO(iaguis) when we update util-linux to 2.27, this code can go
   211  		// away and we can bind-mount RO with one unit file.
   212  		// http://ftp.kernel.org/pub/linux/utils/util-linux/v2.27/v2.27-ReleaseNotes
   213  		if initcommon.IsMountReadOnly(vol, app.MountPoints) {
   214  			opts := []*unit.UnitOption{
   215  				unit.NewUnitOption("Unit", "Description", fmt.Sprintf("Remount read-only unit for %s", wherePath)),
   216  				unit.NewUnitOption("Unit", "DefaultDependencies", "false"),
   217  				unit.NewUnitOption("Unit", "After", mu),
   218  				unit.NewUnitOption("Unit", "Wants", mu),
   219  				unit.NewUnitOption("Service", "ExecStart", fmt.Sprintf("/usr/bin/mount -o remount,ro %s", wherePath)),
   220  				unit.NewUnitOption("Install", "RequiredBy", mu),
   221  			}
   222  
   223  			remountUnitPath := filepath.Join(root, unitsDir, unit.UnitNamePathEscape(wherePath+"-remount.service"))
   224  			if err := writeUnit(opts, remountUnitPath); err != nil {
   225  				return err
   226  			}
   227  		}
   228  	}
   229  	return nil
   230  }
   231  
   232  // VolumesToKvmDiskArgs prepares argument list to be passed to lkvm to configure
   233  // shared volumes (only for "host" kind).
   234  // Example return is ["--9p,src/folder,9ptag"].
   235  func VolumesToKvmDiskArgs(volumes []types.Volume) []string {
   236  	var args []string
   237  
   238  	for _, vol := range volumes {
   239  		mountTag := vol.Name.String() // tag/channel name for virtio
   240  		if vol.Kind == "host" {
   241  			// eg. --9p=/home/jon/srcdir,tag
   242  			arg := "--9p=" + vol.Source + "," + mountTag
   243  			log.Printf("stage1: --disk argument: %#v\n", arg)
   244  			args = append(args, arg)
   245  		}
   246  	}
   247  
   248  	return args
   249  }