github.com/canonical/ubuntu-image@v0.0.0-20240430122802-2202fe98b290/internal/statemachine/mount_helper.go (about)

     1  package statemachine
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"os/exec"
     7  	"path/filepath"
     8  	"strings"
     9  )
    10  
    11  type mountPoint struct {
    12  	src      string
    13  	path     string
    14  	basePath string // basePath + relpath = path
    15  	relpath  string
    16  	typ      string
    17  	opts     []string
    18  	bind     bool
    19  }
    20  
    21  // getMountCmd returns mount/umount commands to mount the given mountpoint
    22  // If the mountpoint does not exist, it will be created.
    23  func (m *mountPoint) getMountCmd() (mountCmds, umountCmds []*exec.Cmd, err error) {
    24  	if m.bind && len(m.typ) > 0 {
    25  		return nil, nil, fmt.Errorf("invalid mount arguments. Cannot use --bind and -t at the same time.")
    26  	}
    27  
    28  	targetPath := filepath.Join(m.basePath, m.relpath)
    29  	mountCmd := execCommand("mount")
    30  
    31  	if len(m.typ) > 0 {
    32  		mountCmd.Args = append(mountCmd.Args, "-t", m.typ)
    33  	}
    34  
    35  	if m.bind {
    36  		mountCmd.Args = append(mountCmd.Args, "--bind")
    37  	}
    38  
    39  	mountCmd.Args = append(mountCmd.Args, m.src)
    40  	if len(m.opts) > 0 {
    41  		mountCmd.Args = append(mountCmd.Args, "-o", strings.Join(m.opts, ","))
    42  	}
    43  	mountCmd.Args = append(mountCmd.Args, targetPath)
    44  
    45  	if _, err := os.Stat(targetPath); err != nil {
    46  		err := osMkdirAll(targetPath, 0755)
    47  		if err != nil && !os.IsExist(err) {
    48  			return nil, nil, fmt.Errorf("Error creating mountpoint \"%s\": \"%s\"", targetPath, err.Error())
    49  		}
    50  	}
    51  
    52  	umountCmds = getUnmountCmd(targetPath)
    53  
    54  	return []*exec.Cmd{mountCmd}, umountCmds, nil
    55  }
    56  
    57  // getUnmountCmd generates unmount commands from a path
    58  func getUnmountCmd(targetPath string) []*exec.Cmd {
    59  	return []*exec.Cmd{
    60  		execCommand("mount", "--make-rprivate", targetPath),
    61  		execCommand("umount", "--recursive", targetPath),
    62  	}
    63  }
    64  
    65  // teardownMount executed teardown commands after making sure every mountpoints matching the given path
    66  // are listed and will be properly unmounted
    67  func teardownMount(path string, mountPoints []*mountPoint, teardownCmds []*exec.Cmd, err error, debug bool) error {
    68  	addedUmountCmds, errAddedUmount := umountAddedMountPointsCmds(path, mountPoints)
    69  	if errAddedUmount != nil {
    70  		err = fmt.Errorf("%s\n%s", err, errAddedUmount)
    71  	}
    72  	teardownCmds = append(addedUmountCmds, teardownCmds...)
    73  
    74  	return execTeardownCmds(teardownCmds, debug, err)
    75  }
    76  
    77  // umountAddedMountPointsCmds generates umount commands for newly added mountpoints
    78  func umountAddedMountPointsCmds(path string, mountPoints []*mountPoint) (umountCmds []*exec.Cmd, err error) {
    79  	currentMountPoints, err := listMounts(path)
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  	newMountPoints := diffMountPoints(mountPoints, currentMountPoints)
    84  	if len(newMountPoints) > 0 {
    85  		for _, m := range newMountPoints {
    86  			umountCmds = append(umountCmds, getUnmountCmd(m.path)...)
    87  		}
    88  	}
    89  
    90  	return umountCmds, nil
    91  }
    92  
    93  // diffMountPoints compares 2 lists of mountpoint and returns the added ones
    94  func diffMountPoints(olds []*mountPoint, currents []*mountPoint) (added []*mountPoint) {
    95  	for _, m := range currents {
    96  		found := false
    97  		for _, o := range olds {
    98  			if equalMountPoints(*m, *o) {
    99  				found = true
   100  			}
   101  		}
   102  		if !found {
   103  			added = append(added, m)
   104  		}
   105  	}
   106  
   107  	return added
   108  }
   109  
   110  // equalMountPoints compare 2 mountpoints. Since mountPoints go object can be either
   111  // created by hand or parsed from /proc/self/mount, we need to compare strictly on the fields
   112  // useful to identify a unique mountpoint from the point of view of the OS.
   113  func equalMountPoints(a, b mountPoint) bool {
   114  	if len(a.path) == 0 {
   115  		a.path = filepath.Join(a.basePath, a.relpath)
   116  	}
   117  	if len(b.path) == 0 {
   118  		b.path = filepath.Join(b.basePath, b.relpath)
   119  	}
   120  
   121  	return a.src == b.src && a.path == b.path && a.typ == b.typ
   122  }
   123  
   124  // listMounts returns mountpoints matching the given path from /proc/self/mounts
   125  func listMounts(path string) ([]*mountPoint, error) {
   126  	procMounts := "/proc/self/mounts"
   127  	f, err := osReadFile(procMounts)
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  
   132  	return parseMounts(string(f), path)
   133  }
   134  
   135  // parseMounts list existing mounts and submounts in the current path
   136  // The returned splice is already inverted so unmount can be called on it
   137  // without further modification.
   138  func parseMounts(procMount string, path string) ([]*mountPoint, error) {
   139  	mountPoints := []*mountPoint{}
   140  	mountLines := strings.Split(procMount, "\n")
   141  
   142  	for _, line := range mountLines {
   143  		if line == "" {
   144  			continue
   145  		}
   146  
   147  		fields := strings.Fields(line)
   148  		mountPath := fields[1]
   149  
   150  		if len(path) != 0 && !strings.HasPrefix(mountPath, path) {
   151  			continue
   152  		}
   153  
   154  		m := &mountPoint{
   155  			src:  fields[0],
   156  			path: mountPath,
   157  			typ:  fields[2],
   158  			opts: strings.Split(fields[3], ","),
   159  		}
   160  		mountPoints = append([]*mountPoint{m}, mountPoints...)
   161  	}
   162  
   163  	return mountPoints, nil
   164  }