mirror of
https://github.com/token2/snapd.git
synced 2026-03-13 11:15:47 -07:00
* many: ensure-dir mount entries from personal-files write attrs * many: review improvements * strutil: make pathiter current path slash trimming use existing method * osutil, strutil: more review fixes * i, i/apparmor, i/builtin, osutil: improve unit test coverage * i, i/apparmor, i/builtin, i/mount: review improvements * strutil: improve comment * interfaces/apparmor: allow snap-update-ns to open home directory * tests: revert interfaces-personal-files changes to simplify merge * interfaces/builtin: improve plug connect error message Co-authored-by: Miguel Pires <miguelpires94@gmail.com> * interfaces/builtin: fixed ut --------- Co-authored-by: Miguel Pires <miguelpires94@gmail.com>
385 lines
11 KiB
Go
385 lines
11 KiB
Go
// -*- Mode: Go; indent-tabs-mode: t -*-
|
|
|
|
/*
|
|
* Copyright (C) 2017 Canonical Ltd
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 3 as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
package osutil
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// MountEntry describes an /etc/fstab-like mount entry.
|
|
//
|
|
// Fields are named after names in struct returned by getmntent(3).
|
|
//
|
|
// struct mntent {
|
|
// char *mnt_fsname; /* name of mounted filesystem */
|
|
// char *mnt_dir; /* filesystem path prefix */
|
|
// char *mnt_type; /* mount type (see Mntent.h) */
|
|
// char *mnt_opts; /* mount options (see Mntent.h) */
|
|
// int mnt_freq; /* dump frequency in days */
|
|
// int mnt_passno; /* pass number on parallel fsck */
|
|
// };
|
|
type MountEntry struct {
|
|
Name string
|
|
Dir string
|
|
Type string
|
|
Options []string
|
|
|
|
DumpFrequency int
|
|
CheckPassNumber int
|
|
}
|
|
|
|
func equalStrings(a, b []string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := 0; i < len(a); i++ {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Equal checks if one entry is equal to another
|
|
func (e *MountEntry) Equal(o *MountEntry) bool {
|
|
return (e.Name == o.Name && e.Dir == o.Dir && e.Type == o.Type &&
|
|
equalStrings(e.Options, o.Options) && e.DumpFrequency == o.DumpFrequency &&
|
|
e.CheckPassNumber == o.CheckPassNumber)
|
|
}
|
|
|
|
// escape replaces whitespace characters so that getmntent can parse it correctly.
|
|
var escape = strings.NewReplacer(
|
|
" ", `\040`,
|
|
"\t", `\011`,
|
|
"\n", `\012`,
|
|
"\\", `\134`,
|
|
).Replace
|
|
|
|
// unescape replaces escape sequences used by setmnt with whitespace characters.
|
|
var unescape = strings.NewReplacer(
|
|
`\040`, " ",
|
|
`\011`, "\t",
|
|
`\012`, "\n",
|
|
`\134`, "\\",
|
|
).Replace
|
|
|
|
// Escape returns the given path with space, tab, newline and forward slash escaped.
|
|
func Escape(path string) string {
|
|
return escape(path)
|
|
}
|
|
|
|
// Unescape returns the given path with space, tab, newline and forward slash unescaped.
|
|
func Unescape(path string) string {
|
|
return unescape(path)
|
|
}
|
|
|
|
// ReplaceMountEntryOption replaces the first mount entry option that has
|
|
// the same prefix as the given option with the given option
|
|
func ReplaceMountEntryOption(entry *MountEntry, option string) {
|
|
if entry == nil {
|
|
return
|
|
}
|
|
kv := strings.SplitN(option, "=", 2)
|
|
if len(kv) < 2 {
|
|
return
|
|
}
|
|
if kv[1] == "" {
|
|
return
|
|
}
|
|
prefix := kv[0] + "="
|
|
for i, opt := range entry.Options {
|
|
if strings.HasPrefix(opt, prefix) {
|
|
entry.Options[i] = option
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (e MountEntry) String() string {
|
|
// Name represents name of the device in a mount entry.
|
|
name := "none"
|
|
if e.Name != "" {
|
|
name = escape(e.Name)
|
|
}
|
|
// Dir represents mount directory in a mount entry.
|
|
dir := "none"
|
|
if e.Dir != "" {
|
|
dir = escape(e.Dir)
|
|
}
|
|
// Type represents file system type in a mount entry.
|
|
fsType := "none"
|
|
if e.Type != "" {
|
|
fsType = escape(e.Type)
|
|
}
|
|
// Options represents mount options in a mount entry.
|
|
options := "defaults"
|
|
if len(e.Options) != 0 {
|
|
options = escape(strings.Join(e.Options, ","))
|
|
}
|
|
return fmt.Sprintf("%s %s %s %s %d %d",
|
|
name, dir, fsType, options, e.DumpFrequency, e.CheckPassNumber)
|
|
}
|
|
|
|
// OptStr returns the value part of a key=value mount option.
|
|
// The name of the option must not contain the trailing "=" character.
|
|
func (e *MountEntry) OptStr(name string) (string, bool) {
|
|
prefix := name + "="
|
|
for _, opt := range e.Options {
|
|
if strings.HasPrefix(opt, prefix) {
|
|
kv := strings.SplitN(opt, "=", 2)
|
|
return kv[1], true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// OptBool returns true if a given mount option is present.
|
|
func (e *MountEntry) OptBool(name string) bool {
|
|
for _, opt := range e.Options {
|
|
if opt == name {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
var (
|
|
validModeRe = regexp.MustCompile("^0[0-7]{3}$")
|
|
validUserGroupRe = regexp.MustCompile("(^[0-9]+$)")
|
|
)
|
|
|
|
// XSnapdMode returns the file mode associated with x-snapd.mode mount option.
|
|
// If the mode is not specified explicitly then a default mode of 0755 is assumed.
|
|
func (e *MountEntry) XSnapdMode() (os.FileMode, error) {
|
|
if opt, ok := e.OptStr("x-snapd.mode"); ok {
|
|
if !validModeRe.MatchString(opt) {
|
|
return 0, fmt.Errorf("cannot parse octal file mode from %q", opt)
|
|
}
|
|
var mode os.FileMode
|
|
n, err := fmt.Sscanf(opt, "%o", &mode)
|
|
if err != nil || n != 1 {
|
|
return 0, fmt.Errorf("cannot parse octal file mode from %q", opt)
|
|
}
|
|
return mode, nil
|
|
}
|
|
return 0755, nil
|
|
}
|
|
|
|
// XSnapdUID returns the user associated with x-snapd-user mount option. If
|
|
// the mode is not specified explicitly then a default "root" use is
|
|
// returned.
|
|
func (e *MountEntry) XSnapdUID() (uid uint64, err error) {
|
|
if opt, ok := e.OptStr("x-snapd.uid"); ok {
|
|
if !validUserGroupRe.MatchString(opt) {
|
|
return math.MaxUint64, fmt.Errorf("cannot parse user name %q", opt)
|
|
}
|
|
// Try to parse a numeric ID first.
|
|
if n, err := fmt.Sscanf(opt, "%d", &uid); n == 1 && err == nil {
|
|
return uid, nil
|
|
}
|
|
return uid, nil
|
|
}
|
|
return 0, nil
|
|
}
|
|
|
|
// XSnapdGID returns the user associated with x-snapd-user mount option. If
|
|
// the mode is not specified explicitly then a default "root" use is
|
|
// returned.
|
|
func (e *MountEntry) XSnapdGID() (gid uint64, err error) {
|
|
if opt, ok := e.OptStr("x-snapd.gid"); ok {
|
|
if !validUserGroupRe.MatchString(opt) {
|
|
return math.MaxUint64, fmt.Errorf("cannot parse group name %q", opt)
|
|
}
|
|
// Try to parse a numeric ID first.
|
|
if n, err := fmt.Sscanf(opt, "%d", &gid); n == 1 && err == nil {
|
|
return gid, nil
|
|
}
|
|
return gid, nil
|
|
}
|
|
return 0, nil
|
|
}
|
|
|
|
// XSnapdEntryID returns the identifier of a given mount entry.
|
|
//
|
|
// Identifiers are kept in the x-snapd.id mount option. The value is a string
|
|
// that identifies a mount entry and is stable across invocations of snapd. In
|
|
// absence of that identifier the entry mount point is returned.
|
|
func (e *MountEntry) XSnapdEntryID() string {
|
|
if val, ok := e.OptStr("x-snapd.id"); ok {
|
|
return val
|
|
}
|
|
return e.Dir
|
|
}
|
|
|
|
// XSnapdNeededBy the identifier of an entry which needs this entry to function.
|
|
//
|
|
// The "needed by" identifiers are kept in the x-snapd.needed-by mount option.
|
|
// The value is a string that identifies another mount entry which, in order to
|
|
// be feasible, has spawned one or more additional support entries. Each such
|
|
// entry contains the needed-by attribute.
|
|
func (e *MountEntry) XSnapdNeededBy() string {
|
|
val, _ := e.OptStr("x-snapd.needed-by")
|
|
return val
|
|
}
|
|
|
|
// XSnapdOrigin returns the origin of a given mount entry.
|
|
//
|
|
// Currently only "layout" entries are identified with a unique origin string.
|
|
func (e *MountEntry) XSnapdOrigin() string {
|
|
val, _ := e.OptStr("x-snapd.origin")
|
|
return val
|
|
}
|
|
|
|
// XSnapdSynthetic returns true of a given mount entry is synthetic.
|
|
//
|
|
// Synthetic mount entries are created by snap-update-ns itself, separately
|
|
// from what snapd instructed. Such entries are needed to make other things
|
|
// possible. They are identified by having the "x-snapd.synthetic" mount
|
|
// option.
|
|
func (e *MountEntry) XSnapdSynthetic() bool {
|
|
return e.OptBool("x-snapd.synthetic")
|
|
}
|
|
|
|
// XSnapdKind returns the kind of a given mount entry.
|
|
//
|
|
// There are four kinds of mount entries today: one for directories, one for
|
|
// files, one for symlinks and one for ensuring directories exist. The values are
|
|
// "", "file", "symlink" and "ensure-dir respectively.
|
|
//
|
|
// Directories use the empty string (in fact they don't need the option at
|
|
// all) as this was the default and is retained for backwards compatibility.
|
|
//
|
|
// An "ensure-dir" mount does not result in actual mounting. It is an instruction
|
|
// to create missing directories within a target directory path.
|
|
func (e *MountEntry) XSnapdKind() string {
|
|
val, _ := e.OptStr("x-snapd.kind")
|
|
return val
|
|
}
|
|
|
|
// XSnapdDetach returns true if a mount entry should be detached rather than unmounted.
|
|
//
|
|
// Whenever we create a recursive bind mount we don't want to just unmount it
|
|
// as it may have replicated additional mount entries. For simplicity and
|
|
// race-free behavior we just detach such mount entries and let the kernel do
|
|
// the rest.
|
|
func (e *MountEntry) XSnapdDetach() bool {
|
|
return e.OptBool("x-snapd.detach")
|
|
}
|
|
|
|
// XSnapdSymlink returns the target for a symlink mount entry.
|
|
//
|
|
// For non-symlinks an empty string is returned.
|
|
func (e *MountEntry) XSnapdSymlink() string {
|
|
val, _ := e.OptStr("x-snapd.symlink")
|
|
return val
|
|
}
|
|
|
|
// XSnapdIgnoreMissing returns true if a mount entry should be ignored
|
|
// if the source or target are missing.
|
|
//
|
|
// By default, snap-update-ns will try to create missing source and
|
|
// target paths when processing a mount entry. In some cases, this
|
|
// behaviour is not desired and it would be better to ignore the mount
|
|
// entry when the source or target are missing.
|
|
func (e *MountEntry) XSnapdIgnoreMissing() bool {
|
|
return e.OptBool("x-snapd.ignore-missing")
|
|
}
|
|
|
|
// XSnapdMustExistDir returns the path that must exist as prequisite
|
|
// to a mount operation.
|
|
func (e *MountEntry) XSnapdMustExistDir() string {
|
|
val, _ := e.OptStr("x-snapd.must-exist-dir")
|
|
return val
|
|
}
|
|
|
|
// XSnapdNeededBy returns the string "x-snapd.needed-by=..." with the given path appended.
|
|
func XSnapdNeededBy(path string) string {
|
|
return fmt.Sprintf("x-snapd.needed-by=%s", path)
|
|
}
|
|
|
|
// XSnapdSynthetic returns the string "x-snapd.synthetic".
|
|
func XSnapdSynthetic() string {
|
|
return "x-snapd.synthetic"
|
|
}
|
|
|
|
// XSnapdDetach returns the string "x-snapd.detach".
|
|
func XSnapdDetach() string {
|
|
return "x-snapd.detach"
|
|
}
|
|
|
|
// XSnapdKindSymlink returns the string "x-snapd.kind=symlink".
|
|
func XSnapdKindSymlink() string {
|
|
return "x-snapd.kind=symlink"
|
|
}
|
|
|
|
// XSnapdKindFile returns the string "x-snapd.kind=file".
|
|
func XSnapdKindFile() string {
|
|
return "x-snapd.kind=file"
|
|
}
|
|
|
|
// XSnapdKindEnsureDir returns the string "x-snapd.kind=ensure-dir".
|
|
func XSnapdKindEnsureDir() string {
|
|
return "x-snapd.kind=ensure-dir"
|
|
}
|
|
|
|
// XSnapdOriginLayout returns the string "x-snapd.origin=layout"
|
|
func XSnapdOriginLayout() string {
|
|
return "x-snapd.origin=layout"
|
|
}
|
|
|
|
// XSnapdOriginOvername returns the string "x-snapd.origin=overname"
|
|
func XSnapdOriginOvername() string {
|
|
return "x-snapd.origin=overname"
|
|
}
|
|
|
|
// XSnapdUser returns the string "x-snapd.user=%d".
|
|
func XSnapdUser(uid uint32) string {
|
|
return fmt.Sprintf("x-snapd.user=%d", uid)
|
|
}
|
|
|
|
// XSnapdGroup returns the string "x-snapd.group=%d".
|
|
func XSnapdGroup(gid uint32) string {
|
|
return fmt.Sprintf("x-snapd.group=%d", gid)
|
|
}
|
|
|
|
// XSnapdMode returns the string "x-snapd.mode=%#o".
|
|
func XSnapdMode(mode uint32) string {
|
|
return fmt.Sprintf("x-snapd.mode=%#o", mode)
|
|
}
|
|
|
|
// XSnapdSymlink returns the string "x-snapd.symlink=%s".
|
|
func XSnapdSymlink(oldname string) string {
|
|
return fmt.Sprintf("x-snapd.symlink=%s", oldname)
|
|
}
|
|
|
|
// XSnapdMustExistDir returns the string "x-snapd.must-exist-dir=%s".
|
|
func XSnapdMustExistDir(path string) string {
|
|
return fmt.Sprintf("x-snapd.must-exist-dir=%s", path)
|
|
}
|
|
|
|
// XSnapdIgnoreMissing returns the string "x-snapd.ignore-missing".
|
|
func XSnapdIgnoreMissing() string {
|
|
return "x-snapd.ignore-missing"
|
|
}
|