diff -pruN 2.37.4-1/asserts/device_asserts.go 2.39.2+19.10ubuntu1/asserts/device_asserts.go
--- 2.37.4-1/asserts/device_asserts.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/asserts/device_asserts.go 2019-06-05 06:41:21.000000000 +0000
@@ -25,6 +25,7 @@ import (
"strings"
"time"
+ "github.com/snapcore/snapd/snap/naming"
"github.com/snapcore/snapd/strutil"
)
@@ -222,24 +223,8 @@ var (
classicModelOptional = []string{"architecture", "gadget"}
)
-var almostValidName = regexp.MustCompile("^[a-z0-9-]*[a-z][a-z0-9-]*$")
-
-// validateSnapName checks whether the name can be used as a snap name
-//
-// This function should be synchronized with the reference implementation
-// snap.ValidateName() in snap/validate.go
func validateSnapName(name string, headerName string) error {
- isValidName := func() bool {
- if !almostValidName.MatchString(name) {
- return false
- }
- if name[0] == '-' || name[len(name)-1] == '-' || strings.Contains(name, "--") {
- return false
- }
- return true
- }
-
- if len(name) > 40 || !isValidName() {
+ if err := naming.ValidateSnap(name); err != nil {
return fmt.Errorf("invalid snap name in %q header: %s", headerName, name)
}
return nil
diff -pruN 2.37.4-1/asserts/device_asserts_test.go 2.39.2+19.10ubuntu1/asserts/device_asserts_test.go
--- 2.37.4-1/asserts/device_asserts_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/asserts/device_asserts_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -201,7 +201,7 @@ func (mods modelSuite) TestDecodeValidSn
withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
validNames := []string{
- "a", "aa", "aaa", "aaaa",
+ "aa", "aaa", "aaaa",
"a-a", "aa-a", "a-aa", "a-b-c",
"a0", "a-0", "a-0a",
"01game", "1-or-2",
@@ -218,6 +218,8 @@ func (mods modelSuite) TestDecodeValidSn
invalidNames := []string{
// name cannot be empty, never reaches snap name validation
"",
+ // too short (min 2 chars)
+ "a",
// names cannot be too long
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"xxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx",
diff -pruN 2.37.4-1/asserts/snap_asserts.go 2.39.2+19.10ubuntu1/asserts/snap_asserts.go
--- 2.37.4-1/asserts/snap_asserts.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/asserts/snap_asserts.go 2019-06-05 06:41:21.000000000 +0000
@@ -23,13 +23,13 @@ import (
"bytes"
"crypto"
"fmt"
- "regexp"
"time"
_ "golang.org/x/crypto/sha3" // expected for digests
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap/naming"
)
// SnapDeclaration holds a snap-declaration assertion, declaring a
@@ -195,11 +195,6 @@ func snapDeclarationFormatAnalyze(header
return formatnum, nil
}
-var (
- validAlias = regexp.MustCompile("^[a-zA-Z0-9][-_.a-zA-Z0-9]*$")
- validAppName = regexp.MustCompile("^[a-zA-Z0-9](?:-?[a-zA-Z0-9])*$")
-)
-
func checkAliases(headers map[string]interface{}) (map[string]string, error) {
value, ok := headers["aliases"]
if !ok {
@@ -221,13 +216,13 @@ func checkAliases(headers map[string]int
}
what := fmt.Sprintf(`in "aliases" item %d`, i+1)
- name, err := checkStringMatchesWhat(aliasItem, "name", what, validAlias)
+ name, err := checkStringMatchesWhat(aliasItem, "name", what, naming.ValidAlias)
if err != nil {
return nil, err
}
what = fmt.Sprintf(`for alias %q`, name)
- target, err := checkStringMatchesWhat(aliasItem, "target", what, validAppName)
+ target, err := checkStringMatchesWhat(aliasItem, "target", what, naming.ValidApp)
if err != nil {
return nil, err
}
@@ -296,7 +291,7 @@ func assembleSnapDeclaration(assert asse
}
// XXX: depracated, will go away later
- autoAliases, err := checkStringListMatches(assert.headers, "auto-aliases", validAlias)
+ autoAliases, err := checkStringListMatches(assert.headers, "auto-aliases", naming.ValidAlias)
if err != nil {
return nil, err
}
diff -pruN 2.37.4-1/boot/kernel_os.go 2.39.2+19.10ubuntu1/boot/kernel_os.go
--- 2.37.4-1/boot/kernel_os.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/boot/kernel_os.go 2019-06-05 06:41:21.000000000 +0000
@@ -24,8 +24,8 @@ import (
"os"
"path/filepath"
+ "github.com/snapcore/snapd/bootloader"
"github.com/snapcore/snapd/logger"
- "github.com/snapcore/snapd/partition"
"github.com/snapcore/snapd/release"
"github.com/snapcore/snapd/snap"
)
@@ -33,14 +33,14 @@ import (
// RemoveKernelAssets removes the unpacked kernel/initrd for the given
// kernel snap.
func RemoveKernelAssets(s snap.PlaceInfo) error {
- bootloader, err := partition.FindBootloader()
+ loader, err := bootloader.Find()
if err != nil {
return fmt.Errorf("no not remove kernel assets: %s", err)
}
// remove the kernel blob
blobName := filepath.Base(s.MountFile())
- dstDir := filepath.Join(bootloader.Dir(), blobName)
+ dstDir := filepath.Join(loader.Dir(), blobName)
if err := os.RemoveAll(dstDir); err != nil {
return err
}
@@ -56,18 +56,24 @@ func ExtractKernelAssets(s *snap.Info, s
return fmt.Errorf("cannot extract kernel assets from snap type %q", s.Type)
}
- bootloader, err := partition.FindBootloader()
+ loader, err := bootloader.Find()
if err != nil {
return fmt.Errorf("cannot extract kernel assets: %s", err)
}
- if bootloader.Name() == "grub" {
+ // XXX: should we use "kernel.yaml" for this?
+ var forceKernelExtraction bool
+ if _, err := snapf.ReadFile("meta/force-kernel-extraction"); err == nil {
+ forceKernelExtraction = true
+ }
+
+ if !forceKernelExtraction && loader.Name() == "grub" {
return nil
}
// now do the kernel specific bits
blobName := filepath.Base(s.MountFile())
- dstDir := filepath.Join(bootloader.Dir(), blobName)
+ dstDir := filepath.Join(loader.Dir(), blobName)
if err := os.MkdirAll(dstDir, 0755); err != nil {
return err
}
@@ -104,7 +110,7 @@ func SetNextBoot(s *snap.Info) error {
return fmt.Errorf("cannot set next boot to snap %q with type %q", s.SnapName(), s.Type)
}
- bootloader, err := partition.FindBootloader()
+ bootloader, err := bootloader.Find()
if err != nil {
return fmt.Errorf("cannot set next boot: %s", err)
}
@@ -154,7 +160,7 @@ func ChangeRequiresReboot(s *snap.Info)
return false
}
- bootloader, err := partition.FindBootloader()
+ bootloader, err := bootloader.Find()
if err != nil {
logger.Noticef("cannot get boot settings: %s", err)
return false
@@ -187,7 +193,7 @@ func ChangeRequiresReboot(s *snap.Info)
// InUse checks if the given name/revision is used in the
// boot environment
func InUse(name string, rev snap.Revision) bool {
- bootloader, err := partition.FindBootloader()
+ bootloader, err := bootloader.Find()
if err != nil {
logger.Noticef("cannot get boot settings: %s", err)
return false
diff -pruN 2.37.4-1/boot/kernel_os_test.go 2.39.2+19.10ubuntu1/boot/kernel_os_test.go
--- 2.37.4-1/boot/kernel_os_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/boot/kernel_os_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -27,9 +27,9 @@ import (
"github.com/snapcore/snapd/boot"
"github.com/snapcore/snapd/boot/boottest"
+ "github.com/snapcore/snapd/bootloader"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/osutil"
- "github.com/snapcore/snapd/partition"
"github.com/snapcore/snapd/release"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/snaptest"
@@ -50,13 +50,13 @@ func (s *kernelOSSuite) SetUpTest(c *C)
s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}))
dirs.SetRootDir(c.MkDir())
s.bootloader = boottest.NewMockBootloader("mock", c.MkDir())
- partition.ForceBootloader(s.bootloader)
+ bootloader.Force(s.bootloader)
}
func (s *kernelOSSuite) TearDownTest(c *C) {
s.BaseTest.TearDownTest(c)
dirs.SetRootDir("")
- partition.ForceBootloader(nil)
+ bootloader.Force(nil)
}
const packageKernel = `
@@ -114,7 +114,7 @@ func (s *kernelOSSuite) TestExtractKerne
func (s *kernelOSSuite) TestExtractKernelAssetsNoUnpacksKernelForGrub(c *C) {
// pretend to be a grub system
mockGrub := boottest.NewMockBootloader("grub", c.MkDir())
- partition.ForceBootloader(mockGrub)
+ bootloader.Force(mockGrub)
files := [][]string{
{"kernel.img", "I'm a kernel"},
@@ -140,6 +140,46 @@ func (s *kernelOSSuite) TestExtractKerne
c.Assert(osutil.FileExists(kernimg), Equals, false)
}
+func (s *kernelOSSuite) TestExtractKernelForceWorks(c *C) {
+ // pretend to be a grub system
+ mockGrub := boottest.NewMockBootloader("grub", c.MkDir())
+ bootloader.Force(mockGrub)
+
+ files := [][]string{
+ {"kernel.img", "I'm a kernel"},
+ {"initrd.img", "...and I'm an initrd"},
+ {"meta/force-kernel-extraction", ""},
+ {"meta/kernel.yaml", "version: 4.2"},
+ }
+ si := &snap.SideInfo{
+ RealName: "ubuntu-kernel",
+ Revision: snap.R(42),
+ }
+ fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files)
+ snapf, err := snap.Open(fn)
+ c.Assert(err, IsNil)
+
+ info, err := snap.ReadInfoFromSnapFile(snapf, si)
+ c.Assert(err, IsNil)
+
+ err = boot.ExtractKernelAssets(info, snapf)
+ c.Assert(err, IsNil)
+
+ // kernel is extracted
+ kernimg := filepath.Join(mockGrub.Dir(), "ubuntu-kernel_42.snap", "kernel.img")
+ c.Assert(osutil.FileExists(kernimg), Equals, true)
+ // initrd
+ initrdimg := filepath.Join(mockGrub.Dir(), "ubuntu-kernel_42.snap", "initrd.img")
+ c.Assert(osutil.FileExists(initrdimg), Equals, true)
+
+ // ensure that removal of assets also works
+ err = boot.RemoveKernelAssets(info)
+ c.Assert(err, IsNil)
+ exists, _, err := osutil.DirExists(filepath.Dir(kernimg))
+ c.Assert(err, IsNil)
+ c.Check(exists, Equals, false)
+}
+
func (s *kernelOSSuite) TestExtractKernelAssetsError(c *C) {
info := &snap.Info{}
info.Type = snap.TypeApp
diff -pruN 2.37.4-1/bootloader/androidbootenv/androidbootenv.go 2.39.2+19.10ubuntu1/bootloader/androidbootenv/androidbootenv.go
--- 2.37.4-1/bootloader/androidbootenv/androidbootenv.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/bootloader/androidbootenv/androidbootenv.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,90 @@
+// -*- 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 .
+ *
+ */
+
+package androidbootenv
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/osutil"
+)
+
+type Env struct {
+ // Map with key-value strings
+ env map[string]string
+ // File for environment storage
+ path string
+}
+
+func NewEnv(path string) *Env {
+ return &Env{
+ env: make(map[string]string),
+ path: path,
+ }
+}
+
+func (a *Env) Get(name string) string {
+ return a.env[name]
+}
+
+func (a *Env) Set(key, value string) {
+ a.env[key] = value
+}
+
+func (a *Env) Load() error {
+ file, err := os.Open(a.path)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ scanner := bufio.NewScanner(file)
+ for scanner.Scan() {
+ l := strings.SplitN(scanner.Text(), "=", 2)
+ // be liberal in what you accept
+ if len(l) < 2 {
+ logger.Noticef("WARNING: bad value while parsing %v (line: %q)",
+ a.path, scanner.Text())
+ continue
+ }
+ a.env[l[0]] = l[1]
+ }
+ if err := scanner.Err(); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (a *Env) Save() error {
+ var w bytes.Buffer
+
+ for k, v := range a.env {
+ if _, err := fmt.Fprintf(&w, "%s=%s\n", k, v); err != nil {
+ return err
+ }
+ }
+
+ return osutil.AtomicWriteFile(a.path, w.Bytes(), 0644, 0)
+}
diff -pruN 2.37.4-1/bootloader/androidbootenv/androidbootenv_test.go 2.39.2+19.10ubuntu1/bootloader/androidbootenv/androidbootenv_test.go
--- 2.37.4-1/bootloader/androidbootenv/androidbootenv_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/bootloader/androidbootenv/androidbootenv_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,69 @@
+// -*- 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 .
+ *
+ */
+
+package androidbootenv_test
+
+import (
+ "path/filepath"
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/bootloader/androidbootenv"
+)
+
+// Hook up check.v1 into the "go test" runner
+func Test(t *testing.T) { TestingT(t) }
+
+type androidbootenvTestSuite struct {
+ envPath string
+ env *androidbootenv.Env
+}
+
+var _ = Suite(&androidbootenvTestSuite{})
+
+func (a *androidbootenvTestSuite) SetUpTest(c *C) {
+ a.envPath = filepath.Join(c.MkDir(), "androidbootenv")
+ a.env = androidbootenv.NewEnv(a.envPath)
+ c.Assert(a.env, NotNil)
+}
+
+func (a *androidbootenvTestSuite) TestSet(c *C) {
+ a.env.Set("key", "value")
+ c.Check(a.env.Get("key"), Equals, "value")
+}
+
+func (a *androidbootenvTestSuite) TestSaveAndLoad(c *C) {
+ a.env.Set("key1", "value1")
+ a.env.Set("key2", "")
+ a.env.Set("key3", "value3")
+
+ err := a.env.Save()
+ c.Assert(err, IsNil)
+
+ env2 := androidbootenv.NewEnv(a.envPath)
+ c.Check(env2, NotNil)
+
+ err = env2.Load()
+ c.Assert(err, IsNil)
+
+ c.Assert(env2.Get("key1"), Equals, "value1")
+ c.Assert(env2.Get("key2"), Equals, "")
+ c.Assert(env2.Get("key3"), Equals, "value3")
+}
diff -pruN 2.37.4-1/bootloader/androidboot.go 2.39.2+19.10ubuntu1/bootloader/androidboot.go
--- 2.37.4-1/bootloader/androidboot.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/bootloader/androidboot.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,77 @@
+// -*- 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 .
+ *
+ */
+
+package bootloader
+
+import (
+ "os"
+ "path/filepath"
+
+ "github.com/snapcore/snapd/bootloader/androidbootenv"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+)
+
+type androidboot struct{}
+
+// newAndroidboot creates a new Androidboot bootloader object
+func newAndroidBoot() Bootloader {
+ a := &androidboot{}
+ if !osutil.FileExists(a.ConfigFile()) {
+ return nil
+ }
+ return a
+}
+
+func (a *androidboot) Name() string {
+ return "androidboot"
+}
+
+func (a *androidboot) Dir() string {
+ return filepath.Join(dirs.GlobalRootDir, "/boot/androidboot")
+}
+
+func (a *androidboot) ConfigFile() string {
+ return filepath.Join(a.Dir(), "androidboot.env")
+}
+
+func (a *androidboot) GetBootVars(names ...string) (map[string]string, error) {
+ env := androidbootenv.NewEnv(a.ConfigFile())
+ if err := env.Load(); err != nil {
+ return nil, err
+ }
+
+ out := make(map[string]string, len(names))
+ for _, name := range names {
+ out[name] = env.Get(name)
+ }
+
+ return out, nil
+}
+
+func (a *androidboot) SetBootVars(values map[string]string) error {
+ env := androidbootenv.NewEnv(a.ConfigFile())
+ if err := env.Load(); err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ for k, v := range values {
+ env.Set(k, v)
+ }
+ return env.Save()
+}
diff -pruN 2.37.4-1/bootloader/androidboot_test.go 2.39.2+19.10ubuntu1/bootloader/androidboot_test.go
--- 2.37.4-1/bootloader/androidboot_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/bootloader/androidboot_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,65 @@
+// -*- 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 .
+ *
+ */
+
+package bootloader_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/bootloader"
+ "github.com/snapcore/snapd/dirs"
+)
+
+type androidBootTestSuite struct {
+}
+
+var _ = Suite(&androidBootTestSuite{})
+
+func (g *androidBootTestSuite) SetUpTest(c *C) {
+ dirs.SetRootDir(c.MkDir())
+
+ // the file needs to exist for androidboot object to be created
+ bootloader.MockAndroidBootFile(c, 0644)
+}
+
+func (g *androidBootTestSuite) TearDownTest(c *C) {
+ dirs.SetRootDir("")
+}
+
+func (s *androidBootTestSuite) TestNewAndroidbootNoAndroidbootReturnsNil(c *C) {
+ dirs.GlobalRootDir = "/something/not/there"
+ a := bootloader.NewAndroidBoot()
+ c.Assert(a, IsNil)
+}
+
+func (s *androidBootTestSuite) TestNewAndroidboot(c *C) {
+ a := bootloader.NewAndroidBoot()
+ c.Assert(a, NotNil)
+}
+
+func (s *androidBootTestSuite) TestSetGetBootVar(c *C) {
+ a := bootloader.NewAndroidBoot()
+ bootVars := map[string]string{"snap_mode": "try"}
+ a.SetBootVars(bootVars)
+
+ v, err := a.GetBootVars("snap_mode")
+ c.Assert(err, IsNil)
+ c.Check(v, HasLen, 1)
+ c.Check(v["snap_mode"], Equals, "try")
+}
diff -pruN 2.37.4-1/bootloader/bootloader.go 2.39.2+19.10ubuntu1/bootloader/bootloader.go
--- 2.37.4-1/bootloader/bootloader.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/bootloader/bootloader.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,166 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 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 .
+ *
+ */
+
+package bootloader
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/snapcore/snapd/osutil"
+)
+
+const (
+ // bootloader variable used to determine if boot was
+ // successful. Set to value of either modeTry (when
+ // attempting to boot a new rootfs) or modeSuccess (to denote
+ // that the boot of the new rootfs was successful).
+ bootmodeVar = "snap_mode"
+
+ // Initial and final values
+ modeTry = "try"
+ modeSuccess = ""
+)
+
+var (
+ // ErrBootloader is returned if the bootloader can not be determined
+ ErrBootloader = errors.New("cannot determine bootloader")
+)
+
+// Bootloader provides an interface to interact with the system
+// bootloader
+type Bootloader interface {
+ // Return the value of the specified bootloader variable
+ GetBootVars(names ...string) (map[string]string, error)
+
+ // Set the value of the specified bootloader variable
+ SetBootVars(values map[string]string) error
+
+ // Dir returns the bootloader directory
+ Dir() string
+
+ // Name returns the bootloader name
+ Name() string
+
+ // ConfigFile returns the name of the config file
+ ConfigFile() string
+}
+
+// InstallBootConfig installs the bootloader config from the gadget
+// snap dir into the right place.
+func InstallBootConfig(gadgetDir string) error {
+ for _, bl := range []Bootloader{&grub{}, &uboot{}, &androidboot{}} {
+ // the bootloader config file has to be root of the gadget snap
+ gadgetFile := filepath.Join(gadgetDir, bl.Name()+".conf")
+ if !osutil.FileExists(gadgetFile) {
+ continue
+ }
+
+ systemFile := bl.ConfigFile()
+ if err := os.MkdirAll(filepath.Dir(systemFile), 0755); err != nil {
+ return err
+ }
+ return osutil.CopyFile(gadgetFile, systemFile, osutil.CopyFlagOverwrite)
+ }
+
+ return fmt.Errorf("cannot find boot config in %q", gadgetDir)
+}
+
+var forcedBootloader Bootloader
+
+// Find returns the bootloader for the given system
+// or an error if no bootloader is found
+func Find() (Bootloader, error) {
+ if forcedBootloader != nil {
+ return forcedBootloader, nil
+ }
+
+ // try uboot
+ if uboot := newUboot(); uboot != nil {
+ return uboot, nil
+ }
+
+ // no, try grub
+ if grub := newGrub(); grub != nil {
+ return grub, nil
+ }
+
+ // no, try androidboot
+ if androidboot := newAndroidBoot(); androidboot != nil {
+ return androidboot, nil
+ }
+
+ // no, weeeee
+ return nil, ErrBootloader
+}
+
+// Force can be used to force setting a booloader to that Find will not use the
+// usual lookup process, use nil to reset to normal lookup.
+func Force(booloader Bootloader) {
+ forcedBootloader = booloader
+}
+
+// MarkBootSuccessful marks the current boot as successful. This means
+// that snappy will consider this combination of kernel/os a valid
+// target for rollback.
+//
+// The states that a boot goes through are the following:
+// - By default snap_mode is "" in which case the bootloader loads
+// two squashfs'es denoted by variables snap_core and snap_kernel.
+// - On a refresh of core/kernel snapd will set snap_mode=try and
+// will also set snap_try_{core,kernel} to the core/kernel that
+// will be tried next.
+// - On reboot the bootloader will inspect the snap_mode and if the
+// mode is set to "try" it will set "snap_mode=trying" and then
+// try to boot the snap_try_{core,kernel}".
+// - On a successful boot snapd resets snap_mode to "" and copies
+// snap_try_{core,kernel} to snap_{core,kernel}. The snap_try_*
+// values are cleared afterwards.
+// - On a failing boot the bootloader will see snap_mode=trying which
+// means snapd did not start successfully. In this case the bootloader
+// will set snap_mode="" and the system will boot with the known good
+// values from snap_{core,kernel}
+func MarkBootSuccessful(bootloader Bootloader) error {
+ m, err := bootloader.GetBootVars("snap_mode", "snap_try_core", "snap_try_kernel")
+ if err != nil {
+ return err
+ }
+
+ // snap_mode goes from "" -> "try" -> "trying" -> ""
+ // so if we are not in "trying" mode, nothing to do here
+ if m["snap_mode"] != "trying" {
+ return nil
+ }
+
+ // update the boot vars
+ for _, k := range []string{"kernel", "core"} {
+ tryBootVar := fmt.Sprintf("snap_try_%s", k)
+ bootVar := fmt.Sprintf("snap_%s", k)
+ // update the boot vars
+ if m[tryBootVar] != "" {
+ m[bootVar] = m[tryBootVar]
+ m[tryBootVar] = ""
+ }
+ }
+ m["snap_mode"] = modeSuccess
+
+ return bootloader.SetBootVars(m)
+}
diff -pruN 2.37.4-1/bootloader/bootloader_test.go 2.39.2+19.10ubuntu1/bootloader/bootloader_test.go
--- 2.37.4-1/bootloader/bootloader_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/bootloader/bootloader_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,159 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 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 .
+ *
+ */
+
+package bootloader
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+)
+
+// Hook up check.v1 into the "go test" runner
+func Test(t *testing.T) { TestingT(t) }
+
+// partition specific testsuite
+type PartitionTestSuite struct {
+}
+
+var _ = Suite(&PartitionTestSuite{})
+
+type mockBootloader struct {
+ bootVars map[string]string
+}
+
+func newMockBootloader() *mockBootloader {
+ return &mockBootloader{
+ bootVars: make(map[string]string),
+ }
+}
+func (b *mockBootloader) Name() string {
+ return "mocky"
+}
+func (b *mockBootloader) Dir() string {
+ return "/boot/mocky"
+}
+func (b *mockBootloader) GetBootVars(names ...string) (map[string]string, error) {
+ out := map[string]string{}
+ for _, name := range names {
+ out[name] = b.bootVars[name]
+ }
+
+ return out, nil
+}
+func (b *mockBootloader) SetBootVars(values map[string]string) error {
+ for k, v := range values {
+ b.bootVars[k] = v
+ }
+ return nil
+}
+func (b *mockBootloader) ConfigFile() string {
+ return "/boot/mocky/mocky.env"
+}
+
+func (s *PartitionTestSuite) SetUpTest(c *C) {
+ dirs.SetRootDir(c.MkDir())
+ err := os.MkdirAll((&grub{}).Dir(), 0755)
+ c.Assert(err, IsNil)
+ err = os.MkdirAll((&uboot{}).Dir(), 0755)
+ c.Assert(err, IsNil)
+}
+
+func (s *PartitionTestSuite) TestForceBootloader(c *C) {
+ b := newMockBootloader()
+ Force(b)
+ defer Force(nil)
+
+ got, err := Find()
+ c.Assert(err, IsNil)
+ c.Check(got, Equals, b)
+}
+
+func (s *PartitionTestSuite) TestMarkBootSuccessfulAllSnap(c *C) {
+ b := newMockBootloader()
+ b.bootVars["snap_mode"] = "trying"
+ b.bootVars["snap_try_core"] = "os1"
+ b.bootVars["snap_try_kernel"] = "k1"
+ err := MarkBootSuccessful(b)
+ c.Assert(err, IsNil)
+
+ expected := map[string]string{
+ // cleared
+ "snap_mode": "",
+ "snap_try_kernel": "",
+ "snap_try_core": "",
+ // updated
+ "snap_kernel": "k1",
+ "snap_core": "os1",
+ }
+ c.Assert(b.bootVars, DeepEquals, expected)
+
+ // do it again, verify its still valid
+ err = MarkBootSuccessful(b)
+ c.Assert(err, IsNil)
+ c.Assert(b.bootVars, DeepEquals, expected)
+}
+
+func (s *PartitionTestSuite) TestMarkBootSuccessfulKKernelUpdate(c *C) {
+ b := newMockBootloader()
+ b.bootVars["snap_mode"] = "trying"
+ b.bootVars["snap_core"] = "os1"
+ b.bootVars["snap_kernel"] = "k1"
+ b.bootVars["snap_try_core"] = ""
+ b.bootVars["snap_try_kernel"] = "k2"
+ err := MarkBootSuccessful(b)
+ c.Assert(err, IsNil)
+ c.Assert(b.bootVars, DeepEquals, map[string]string{
+ // cleared
+ "snap_mode": "",
+ "snap_try_kernel": "",
+ "snap_try_core": "",
+ // unchanged
+ "snap_core": "os1",
+ // updated
+ "snap_kernel": "k2",
+ })
+}
+
+func (s *PartitionTestSuite) TestInstallBootloaderConfigNoConfig(c *C) {
+ err := InstallBootConfig(c.MkDir())
+ c.Assert(err, ErrorMatches, `cannot find boot config in.*`)
+}
+
+func (s *PartitionTestSuite) TestInstallBootloaderConfig(c *C) {
+ for _, t := range []struct{ gadgetFile, systemFile string }{
+ {"grub.conf", "/boot/grub/grub.cfg"},
+ {"uboot.conf", "/boot/uboot/uboot.env"},
+ {"androidboot.conf", "/boot/androidboot/androidboot.env"},
+ } {
+ mockGadgetDir := c.MkDir()
+ err := ioutil.WriteFile(filepath.Join(mockGadgetDir, t.gadgetFile), nil, 0644)
+ c.Assert(err, IsNil)
+ err = InstallBootConfig(mockGadgetDir)
+ c.Assert(err, IsNil)
+ fn := filepath.Join(dirs.GlobalRootDir, t.systemFile)
+ c.Assert(osutil.FileExists(fn), Equals, true)
+ }
+}
diff -pruN 2.37.4-1/bootloader/export_test.go 2.39.2+19.10ubuntu1/bootloader/export_test.go
--- 2.37.4-1/bootloader/export_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/bootloader/export_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,40 @@
+// -*- 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 .
+ *
+ */
+
+package bootloader
+
+import (
+ "io/ioutil"
+ "os"
+
+ . "gopkg.in/check.v1"
+)
+
+// creates a new Androidboot bootloader object
+func NewAndroidBoot() Bootloader {
+ return newAndroidBoot()
+}
+
+func MockAndroidBootFile(c *C, mode os.FileMode) {
+ f := &androidboot{}
+ err := os.MkdirAll(f.Dir(), 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(f.ConfigFile(), nil, mode)
+ c.Assert(err, IsNil)
+}
diff -pruN 2.37.4-1/bootloader/grubenv/grubenv.go 2.39.2+19.10ubuntu1/bootloader/grubenv/grubenv.go
--- 2.37.4-1/bootloader/grubenv/grubenv.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/bootloader/grubenv/grubenv.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,117 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 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 .
+ *
+ */
+
+package grubenv
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "os"
+
+ "github.com/snapcore/snapd/strutil"
+)
+
+// FIXME: support for escaping (embedded \n in grubenv) missing
+type Env struct {
+ env map[string]string
+ ordering []string
+
+ path string
+}
+
+func NewEnv(path string) *Env {
+ return &Env{
+ env: make(map[string]string),
+ path: path,
+ }
+}
+
+func (g *Env) Get(name string) string {
+ return g.env[name]
+}
+
+func (g *Env) Set(key, value string) {
+ if !strutil.ListContains(g.ordering, key) {
+ g.ordering = append(g.ordering, key)
+ }
+
+ g.env[key] = value
+}
+
+func (g *Env) Load() error {
+ buf, err := ioutil.ReadFile(g.path)
+ if err != nil {
+ return err
+ }
+ if len(buf) != 1024 {
+ return fmt.Errorf("grubenv %q must be exactly 1024 byte, got %d", g.path, len(buf))
+ }
+ if !bytes.HasPrefix(buf, []byte("# GRUB Environment Block\n")) {
+ return fmt.Errorf("cannot find grubenv header in %q", g.path)
+ }
+ rawEnv := bytes.Split(buf, []byte("\n"))
+ for _, env := range rawEnv[1:] {
+ l := bytes.SplitN(env, []byte("="), 2)
+ // be liberal in what you accept
+ if len(l) < 2 {
+ continue
+ }
+ k := string(l[0])
+ v := string(l[1])
+ g.env[k] = v
+ g.ordering = append(g.ordering, k)
+ }
+
+ return nil
+}
+
+func (g *Env) Save() error {
+ w := bytes.NewBuffer(nil)
+ w.Grow(1024)
+
+ fmt.Fprintf(w, "# GRUB Environment Block\n")
+ for _, k := range g.ordering {
+ if _, err := fmt.Fprintf(w, "%s=%s\n", k, g.env[k]); err != nil {
+ return err
+ }
+ }
+ if w.Len() > 1024 {
+ return fmt.Errorf("cannot write grubenv %q: bigger than 1024 bytes (%d)", g.path, w.Len())
+ }
+ content := w.Bytes()[:w.Cap()]
+ for i := w.Len(); i < len(content); i++ {
+ content[i] = '#'
+ }
+
+ // write in place to avoid the file moving on disk
+ // (thats what grubenv is also doing)
+ f, err := os.Create(g.path)
+ if err != nil {
+ return err
+ }
+ if _, err := f.Write(content); err != nil {
+ return err
+ }
+ if err := f.Sync(); err != nil {
+ return err
+ }
+
+ return f.Close()
+}
diff -pruN 2.37.4-1/bootloader/grubenv/grubenv_test.go 2.39.2+19.10ubuntu1/bootloader/grubenv/grubenv_test.go
--- 2.37.4-1/bootloader/grubenv/grubenv_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/bootloader/grubenv/grubenv_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,92 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 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 .
+ *
+ */
+
+package grubenv_test
+
+import (
+ "fmt"
+ "path/filepath"
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/bootloader/grubenv"
+ "github.com/snapcore/snapd/testutil"
+)
+
+// Hook up check.v1 into the "go test" runner
+func Test(t *testing.T) { TestingT(t) }
+
+type grubenvTestSuite struct {
+ envPath string
+}
+
+var _ = Suite(&grubenvTestSuite{})
+
+func (g *grubenvTestSuite) SetUpTest(c *C) {
+ g.envPath = filepath.Join(c.MkDir(), "grubenv")
+}
+
+func (g *grubenvTestSuite) TestSet(c *C) {
+ env := grubenv.NewEnv(g.envPath)
+ c.Check(env, NotNil)
+
+ env.Set("key", "value")
+ c.Check(env.Get("key"), Equals, "value")
+}
+
+func (g *grubenvTestSuite) TestSave(c *C) {
+ env := grubenv.NewEnv(g.envPath)
+ c.Check(env, NotNil)
+
+ env.Set("key1", "value1")
+ env.Set("key2", "value2")
+ env.Set("key3", "value3")
+ env.Set("key4", "value4")
+ env.Set("key5", "value5")
+ env.Set("key6", "value6")
+ env.Set("key7", "value7")
+ // set "key1" again, ordering (position) does not change
+ env.Set("key1", "value1")
+
+ err := env.Save()
+ c.Assert(err, IsNil)
+
+ c.Assert(g.envPath, testutil.FileEquals, `# GRUB Environment Block
+key1=value1
+key2=value2
+key3=value3
+key4=value4
+key5=value5
+key6=value6
+key7=value7
+###################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################`)
+}
+
+func (g *grubenvTestSuite) TestSaveOverflow(c *C) {
+ env := grubenv.NewEnv(g.envPath)
+ c.Check(env, NotNil)
+
+ for i := 0; i < 101; i++ {
+ env.Set(fmt.Sprintf("key%d", i), "foo")
+ }
+
+ err := env.Save()
+ c.Assert(err, ErrorMatches, `cannot write grubenv .*: bigger than 1024 bytes \(1026\)`)
+}
diff -pruN 2.37.4-1/bootloader/grub.go 2.39.2+19.10ubuntu1/bootloader/grub.go
--- 2.37.4-1/bootloader/grub.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/bootloader/grub.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,83 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 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 .
+ *
+ */
+
+package bootloader
+
+import (
+ "os"
+ "path/filepath"
+
+ "github.com/snapcore/snapd/bootloader/grubenv"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+)
+
+type grub struct{}
+
+// newGrub create a new Grub bootloader object
+func newGrub() Bootloader {
+ g := &grub{}
+ if !osutil.FileExists(g.ConfigFile()) {
+ return nil
+ }
+
+ return g
+}
+
+func (g *grub) Name() string {
+ return "grub"
+}
+
+func (g *grub) Dir() string {
+ return filepath.Join(dirs.GlobalRootDir, "/boot/grub")
+}
+
+func (g *grub) ConfigFile() string {
+ return filepath.Join(g.Dir(), "grub.cfg")
+}
+
+func (g *grub) envFile() string {
+ return filepath.Join(g.Dir(), "grubenv")
+}
+
+func (g *grub) GetBootVars(names ...string) (map[string]string, error) {
+ out := make(map[string]string)
+
+ env := grubenv.NewEnv(g.envFile())
+ if err := env.Load(); err != nil {
+ return nil, err
+ }
+
+ for _, name := range names {
+ out[name] = env.Get(name)
+ }
+
+ return out, nil
+}
+
+func (g *grub) SetBootVars(values map[string]string) error {
+ env := grubenv.NewEnv(g.envFile())
+ if err := env.Load(); err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ for k, v := range values {
+ env.Set(k, v)
+ }
+ return env.Save()
+}
diff -pruN 2.37.4-1/bootloader/grub_test.go 2.39.2+19.10ubuntu1/bootloader/grub_test.go
--- 2.37.4-1/bootloader/grub_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/bootloader/grub_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,127 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 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 .
+ *
+ */
+
+package bootloader
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os/exec"
+ "path/filepath"
+
+ "github.com/mvo5/goconfigparser"
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+)
+
+// grubEditenvCmd finds the right grub{,2}-editenv command
+func grubEditenvCmd() string {
+ for _, exe := range []string{"grub2-editenv", "grub-editenv"} {
+ if osutil.ExecutableExists(exe) {
+ return exe
+ }
+ }
+ return ""
+}
+
+func grubEnvPath() string {
+ return filepath.Join(dirs.GlobalRootDir, "boot/grub/grubenv")
+}
+
+func grubEditenvSet(c *C, key, value string) {
+ if grubEditenvCmd() == "" {
+ c.Skip("grub{,2}-editenv is not available")
+ }
+
+ err := exec.Command(grubEditenvCmd(), grubEnvPath(), "set", fmt.Sprintf("%s=%s", key, value)).Run()
+ c.Assert(err, IsNil)
+}
+
+func grubEditenvGet(c *C, key string) string {
+ if grubEditenvCmd() == "" {
+ c.Skip("grub{,2}-editenv is not available")
+ }
+
+ output, err := exec.Command(grubEditenvCmd(), grubEnvPath(), "list").CombinedOutput()
+ c.Assert(err, IsNil)
+ cfg := goconfigparser.New()
+ cfg.AllowNoSectionHeader = true
+ err = cfg.ReadString(string(output))
+ c.Assert(err, IsNil)
+ v, err := cfg.Get("", key)
+ c.Assert(err, IsNil)
+ return v
+}
+
+func (s *PartitionTestSuite) makeFakeGrubEnv(c *C) {
+ g := &grub{}
+ err := ioutil.WriteFile(g.ConfigFile(), nil, 0644)
+ c.Assert(err, IsNil)
+ grubEditenvSet(c, "k", "v")
+}
+
+func (s *PartitionTestSuite) TestNewGrubNoGrubReturnsNil(c *C) {
+ dirs.GlobalRootDir = "/something/not/there"
+
+ g := newGrub()
+ c.Assert(g, IsNil)
+}
+
+func (s *PartitionTestSuite) TestNewGrub(c *C) {
+ s.makeFakeGrubEnv(c)
+
+ g := newGrub()
+ c.Assert(g, NotNil)
+ c.Assert(g, FitsTypeOf, &grub{})
+}
+
+func (s *PartitionTestSuite) TestGetBootloaderWithGrub(c *C) {
+ s.makeFakeGrubEnv(c)
+
+ bootloader, err := Find()
+ c.Assert(err, IsNil)
+ c.Assert(bootloader, FitsTypeOf, &grub{})
+}
+
+func (s *PartitionTestSuite) TestGetBootVer(c *C) {
+ s.makeFakeGrubEnv(c)
+ grubEditenvSet(c, bootmodeVar, "regular")
+
+ g := newGrub()
+ v, err := g.GetBootVars(bootmodeVar)
+ c.Assert(err, IsNil)
+ c.Check(v, HasLen, 1)
+ c.Check(v[bootmodeVar], Equals, "regular")
+}
+
+func (s *PartitionTestSuite) TestSetBootVer(c *C) {
+ s.makeFakeGrubEnv(c)
+
+ g := newGrub()
+ err := g.SetBootVars(map[string]string{
+ "k1": "v1",
+ "k2": "v2",
+ })
+ c.Assert(err, IsNil)
+
+ c.Check(grubEditenvGet(c, "k1"), Equals, "v1")
+ c.Check(grubEditenvGet(c, "k2"), Equals, "v2")
+}
diff -pruN 2.37.4-1/bootloader/ubootenv/env.go 2.39.2+19.10ubuntu1/bootloader/ubootenv/env.go
--- 2.37.4-1/bootloader/ubootenv/env.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/bootloader/ubootenv/env.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,294 @@
+// -*- 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 .
+ *
+ */
+
+package ubootenv
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/binary"
+ "fmt"
+ "hash/crc32"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+)
+
+// FIXME: add config option for that so that the user can select if
+// he/she wants env with or without flags
+var headerSize = 5
+
+// Env contains the data of the uboot environment
+type Env struct {
+ fname string
+ size int
+ data map[string]string
+}
+
+// little endian helpers
+func readUint32(data []byte) uint32 {
+ var ret uint32
+ buf := bytes.NewBuffer(data)
+ binary.Read(buf, binary.LittleEndian, &ret)
+ return ret
+}
+
+func writeUint32(u uint32) []byte {
+ buf := bytes.NewBuffer(nil)
+ binary.Write(buf, binary.LittleEndian, &u)
+ return buf.Bytes()
+}
+
+// Create a new empty uboot env file with the given size
+func Create(fname string, size int) (*Env, error) {
+ f, err := os.Create(fname)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ env := &Env{
+ fname: fname,
+ size: size,
+ data: make(map[string]string),
+ }
+
+ return env, nil
+}
+
+// OpenFlags instructs open how to alter its behavior.
+type OpenFlags int
+
+const (
+ // OpenBestEffort instructs OpenWithFlags to skip malformed data without returning an error.
+ OpenBestEffort OpenFlags = 1 << iota
+)
+
+// Open opens a existing uboot env file
+func Open(fname string) (*Env, error) {
+ return OpenWithFlags(fname, OpenFlags(0))
+}
+
+// OpenWithFlags opens a existing uboot env file, passing additional flags.
+func OpenWithFlags(fname string, flags OpenFlags) (*Env, error) {
+ f, err := os.Open(fname)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ contentWithHeader, err := ioutil.ReadAll(f)
+ if err != nil {
+ return nil, err
+ }
+ crc := readUint32(contentWithHeader)
+
+ payload := contentWithHeader[headerSize:]
+ actualCRC := crc32.ChecksumIEEE(payload)
+ if crc != actualCRC {
+ return nil, fmt.Errorf("cannot open %q: bad CRC %v != %v", fname, crc, actualCRC)
+ }
+
+ if eof := bytes.Index(payload, []byte{0, 0}); eof >= 0 {
+ payload = payload[:eof]
+ }
+
+ data, err := parseData(payload, flags)
+ if err != nil {
+ return nil, err
+ }
+
+ env := &Env{
+ fname: fname,
+ size: len(contentWithHeader),
+ data: data,
+ }
+
+ return env, nil
+}
+
+func parseData(data []byte, flags OpenFlags) (map[string]string, error) {
+ out := make(map[string]string)
+
+ for _, envStr := range bytes.Split(data, []byte{0}) {
+ if len(envStr) == 0 || envStr[0] == 0 || envStr[0] == 255 {
+ continue
+ }
+ l := strings.SplitN(string(envStr), "=", 2)
+ if len(l) != 2 || l[0] == "" {
+ if flags&OpenBestEffort == OpenBestEffort {
+ continue
+ }
+ return nil, fmt.Errorf("cannot parse line %q as key=value pair", envStr)
+ }
+ key := l[0]
+ value := l[1]
+ out[key] = value
+ }
+
+ return out, nil
+}
+
+func (env *Env) String() string {
+ out := ""
+
+ env.iterEnv(func(key, value string) {
+ out += fmt.Sprintf("%s=%s\n", key, value)
+ })
+
+ return out
+}
+
+func (env *Env) Size() int {
+ return env.size
+}
+
+// Get the value of the environment variable
+func (env *Env) Get(name string) string {
+ return env.data[name]
+}
+
+// Set an environment name to the given value, if the value is empty
+// the variable will be removed from the environment
+func (env *Env) Set(name, value string) {
+ if name == "" {
+ panic(fmt.Sprintf("Set() can not be called with empty key for value: %q", value))
+ }
+ if value == "" {
+ delete(env.data, name)
+ return
+ }
+ env.data[name] = value
+}
+
+// iterEnv calls the passed function f with key, value for environment
+// vars. The order is guaranteed (unlike just iterating over the map)
+func (env *Env) iterEnv(f func(key, value string)) {
+ keys := make([]string, 0, len(env.data))
+ for k := range env.data {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+
+ for _, k := range keys {
+ if k == "" {
+ panic("iterEnv iterating over a empty key")
+ }
+
+ f(k, env.data[k])
+ }
+}
+
+// Save will write out the environment data
+func (env *Env) Save() error {
+ w := bytes.NewBuffer(nil)
+ // will panic if the buffer can't grow, all writes to
+ // the buffer will be ok because we sized it correctly
+ w.Grow(env.size - headerSize)
+
+ // write the payload
+ env.iterEnv(func(key, value string) {
+ w.Write([]byte(fmt.Sprintf("%s=%s", key, value)))
+ w.Write([]byte{0})
+ })
+
+ // write double \0 to mark the end of the env
+ w.Write([]byte{0})
+
+ // no keys, so no previous \0 was written so we write one here
+ if len(env.data) == 0 {
+ w.Write([]byte{0})
+ }
+
+ // write ff into the remaining parts
+ writtenSoFar := w.Len()
+ for i := 0; i < env.size-headerSize-writtenSoFar; i++ {
+ w.Write([]byte{0xff})
+ }
+
+ // checksum
+ crc := crc32.ChecksumIEEE(w.Bytes())
+
+ // ensure dir sync
+ dir, err := os.Open(filepath.Dir(env.fname))
+ if err != nil {
+ return err
+ }
+ defer dir.Close()
+
+ // Note that we overwrite the existing file and do not do
+ // the usual write-rename. The rationale is that we want to
+ // minimize the amount of writes happening on a potential
+ // FAT partition where the env is loaded from. The file will
+ // always be of a fixed size so we know the writes will not
+ // fail because of ENOSPC.
+ //
+ // The size of the env file never changes so we do not
+ // truncate it.
+ //
+ // We also do not O_TRUNC to avoid reallocations on the FS
+ // to minimize risk of fs corruption.
+ f, err := os.OpenFile(env.fname, os.O_WRONLY, 0666)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ if _, err := f.Write(writeUint32(crc)); err != nil {
+ return err
+ }
+ // padding bytes (e.g. for redundant header)
+ pad := make([]byte, headerSize-binary.Size(crc))
+ if _, err := f.Write(pad); err != nil {
+ return err
+ }
+ if _, err := f.Write(w.Bytes()); err != nil {
+ return err
+ }
+
+ if err := f.Sync(); err != nil {
+ return err
+ }
+
+ return dir.Sync()
+}
+
+// Import is a helper that imports a given text file that contains
+// "key=value" paris into the uboot env. Lines starting with ^# are
+// ignored (like the input file on mkenvimage)
+func (env *Env) Import(r io.Reader) error {
+ scanner := bufio.NewScanner(r)
+ for scanner.Scan() {
+ line := scanner.Text()
+ if strings.HasPrefix(line, "#") || len(line) == 0 {
+ continue
+ }
+ l := strings.SplitN(line, "=", 2)
+ if len(l) == 1 {
+ return fmt.Errorf("Invalid line: %q", line)
+ }
+ env.data[l[0]] = l[1]
+
+ }
+
+ return scanner.Err()
+}
diff -pruN 2.37.4-1/bootloader/ubootenv/env_test.go 2.39.2+19.10ubuntu1/bootloader/ubootenv/env_test.go
--- 2.37.4-1/bootloader/ubootenv/env_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/bootloader/ubootenv/env_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,298 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016-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 .
+ *
+ */
+
+package ubootenv_test
+
+import (
+ "bytes"
+ "hash/crc32"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/bootloader/ubootenv"
+)
+
+// Hook up check.v1 into the "go test" runner
+func Test(t *testing.T) { TestingT(t) }
+
+type uenvTestSuite struct {
+ envFile string
+}
+
+var _ = Suite(&uenvTestSuite{})
+
+func (u *uenvTestSuite) SetUpTest(c *C) {
+ u.envFile = filepath.Join(c.MkDir(), "uboot.env")
+}
+
+func (u *uenvTestSuite) TestSetNoDuplicate(c *C) {
+ env, err := ubootenv.Create(u.envFile, 4096)
+ c.Assert(err, IsNil)
+ env.Set("foo", "bar")
+ env.Set("foo", "bar")
+ c.Assert(env.String(), Equals, "foo=bar\n")
+}
+
+func (u *uenvTestSuite) TestOpenEnv(c *C) {
+ env, err := ubootenv.Create(u.envFile, 4096)
+ c.Assert(err, IsNil)
+ env.Set("foo", "bar")
+ c.Assert(env.String(), Equals, "foo=bar\n")
+ err = env.Save()
+ c.Assert(err, IsNil)
+
+ env2, err := ubootenv.Open(u.envFile)
+ c.Assert(err, IsNil)
+ c.Assert(env2.String(), Equals, "foo=bar\n")
+}
+
+func (u *uenvTestSuite) TestOpenEnvBadCRC(c *C) {
+ corrupted := filepath.Join(c.MkDir(), "corrupted.env")
+
+ buf := make([]byte, 4096)
+ err := ioutil.WriteFile(corrupted, buf, 0644)
+ c.Assert(err, IsNil)
+
+ _, err = ubootenv.Open(corrupted)
+ c.Assert(err, ErrorMatches, `cannot open ".*": bad CRC 0 != .*`)
+}
+
+func (u *uenvTestSuite) TestGetSimple(c *C) {
+ env, err := ubootenv.Create(u.envFile, 4096)
+ c.Assert(err, IsNil)
+ env.Set("foo", "bar")
+ c.Assert(env.Get("foo"), Equals, "bar")
+}
+
+func (u *uenvTestSuite) TestGetNoSuchEntry(c *C) {
+ env, err := ubootenv.Create(u.envFile, 4096)
+ c.Assert(err, IsNil)
+ c.Assert(env.Get("no-such-entry"), Equals, "")
+}
+
+func (u *uenvTestSuite) TestImport(c *C) {
+ env, err := ubootenv.Create(u.envFile, 4096)
+ c.Assert(err, IsNil)
+
+ r := strings.NewReader("foo=bar\n#comment\n\nbaz=baz")
+ err = env.Import(r)
+ c.Assert(err, IsNil)
+ // order is alphabetic
+ c.Assert(env.String(), Equals, "baz=baz\nfoo=bar\n")
+}
+
+func (u *uenvTestSuite) TestImportHasError(c *C) {
+ env, err := ubootenv.Create(u.envFile, 4096)
+ c.Assert(err, IsNil)
+
+ r := strings.NewReader("foxy")
+ err = env.Import(r)
+ c.Assert(err, ErrorMatches, "Invalid line: \"foxy\"")
+}
+
+func (u *uenvTestSuite) TestSetEmptyUnsets(c *C) {
+ env, err := ubootenv.Create(u.envFile, 4096)
+ c.Assert(err, IsNil)
+
+ env.Set("foo", "bar")
+ c.Assert(env.String(), Equals, "foo=bar\n")
+ env.Set("foo", "")
+ c.Assert(env.String(), Equals, "")
+}
+
+func (u *uenvTestSuite) makeUbootEnvFromData(c *C, mockData []byte) {
+ w := bytes.NewBuffer(nil)
+ crc := crc32.ChecksumIEEE(mockData)
+ w.Write(ubootenv.WriteUint32(crc))
+ w.Write([]byte{0})
+ w.Write(mockData)
+
+ f, err := os.Create(u.envFile)
+ c.Assert(err, IsNil)
+ defer f.Close()
+ _, err = f.Write(w.Bytes())
+ c.Assert(err, IsNil)
+}
+
+// ensure that the data after \0\0 is discarded (except for crc)
+func (u *uenvTestSuite) TestReadStopsAfterDoubleNull(c *C) {
+ mockData := []byte{
+ // foo=bar
+ 0x66, 0x6f, 0x6f, 0x3d, 0x62, 0x61, 0x72,
+ // eof
+ 0x00, 0x00,
+ // junk after eof as written by fw_setenv sometimes
+ // =b
+ 0x3d, 62,
+ // empty
+ 0xff, 0xff,
+ }
+ u.makeUbootEnvFromData(c, mockData)
+
+ env, err := ubootenv.Open(u.envFile)
+ c.Assert(err, IsNil)
+ c.Assert(env.String(), Equals, "foo=bar\n")
+}
+
+// ensure that the malformed data is not causing us to panic.
+func (u *uenvTestSuite) TestErrorOnMalformedData(c *C) {
+ mockData := []byte{
+ // foo
+ 0x66, 0x6f, 0x6f,
+ // eof
+ 0x00, 0x00,
+ }
+ u.makeUbootEnvFromData(c, mockData)
+
+ env, err := ubootenv.Open(u.envFile)
+ c.Assert(err, ErrorMatches, `cannot parse line "foo" as key=value pair`)
+ c.Assert(env, IsNil)
+}
+
+// ensure that the malformed data is not causing us to panic.
+func (u *uenvTestSuite) TestOpenBestEffort(c *C) {
+ testCases := map[string][]byte{"noise": {
+ // key1=value1
+ 0x6b, 0x65, 0x79, 0x31, 0x3d, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x31, 0x00,
+ // foo
+ 0x66, 0x6f, 0x6f, 0x00,
+ // key2=value2
+ 0x6b, 0x65, 0x79, 0x32, 0x3d, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x32, 0x00,
+ // eof
+ 0x00, 0x00,
+ }, "no-eof": {
+ // key1=value1
+ 0x6b, 0x65, 0x79, 0x31, 0x3d, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x31, 0x00,
+ // key2=value2
+ 0x6b, 0x65, 0x79, 0x32, 0x3d, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x32, 0x00,
+ // NO EOF!
+ }, "noise-eof": {
+ // key1=value1
+ 0x6b, 0x65, 0x79, 0x31, 0x3d, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x31, 0x00,
+ // key2=value2
+ 0x6b, 0x65, 0x79, 0x32, 0x3d, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x32, 0x00,
+ // foo
+ 0x66, 0x6f, 0x6f, 0x00,
+ }}
+ for testName, mockData := range testCases {
+ u.makeUbootEnvFromData(c, mockData)
+
+ env, err := ubootenv.OpenWithFlags(u.envFile, ubootenv.OpenBestEffort)
+ c.Assert(err, IsNil, Commentf(testName))
+ c.Check(env.String(), Equals, "key1=value1\nkey2=value2\n", Commentf(testName))
+ }
+}
+
+func (u *uenvTestSuite) TestErrorOnMissingKeyInKeyValuePair(c *C) {
+ mockData := []byte{
+ // =foo
+ 0x3d, 0x66, 0x6f, 0x6f,
+ // eof
+ 0x00, 0x00,
+ }
+ u.makeUbootEnvFromData(c, mockData)
+
+ env, err := ubootenv.Open(u.envFile)
+ c.Assert(err, ErrorMatches, `cannot parse line "=foo" as key=value pair`)
+ c.Assert(env, IsNil)
+}
+
+func (u *uenvTestSuite) TestReadEmptyFile(c *C) {
+ mockData := []byte{
+ // eof
+ 0x00, 0x00,
+ // empty
+ 0xff, 0xff,
+ }
+ u.makeUbootEnvFromData(c, mockData)
+
+ env, err := ubootenv.Open(u.envFile)
+ c.Assert(err, IsNil)
+ c.Assert(env.String(), Equals, "")
+}
+
+func (u *uenvTestSuite) TestWritesEmptyFileWithDoubleNewline(c *C) {
+ env, err := ubootenv.Create(u.envFile, 12)
+ c.Assert(err, IsNil)
+ err = env.Save()
+ c.Assert(err, IsNil)
+
+ r, err := os.Open(u.envFile)
+ c.Assert(err, IsNil)
+ defer r.Close()
+ content, err := ioutil.ReadAll(r)
+ c.Assert(err, IsNil)
+ c.Assert(content, DeepEquals, []byte{
+ // crc
+ 0x11, 0x38, 0xb3, 0x89,
+ // redundant
+ 0x0,
+ // eof
+ 0x0, 0x0,
+ // footer
+ 0xff, 0xff, 0xff, 0xff, 0xff,
+ })
+
+ env, err = ubootenv.Open(u.envFile)
+ c.Assert(err, IsNil)
+ c.Assert(env.String(), Equals, "")
+}
+
+func (u *uenvTestSuite) TestWritesContentCorrectly(c *C) {
+ totalSize := 16
+
+ env, err := ubootenv.Create(u.envFile, totalSize)
+ c.Assert(err, IsNil)
+ env.Set("a", "b")
+ env.Set("c", "d")
+ err = env.Save()
+ c.Assert(err, IsNil)
+
+ r, err := os.Open(u.envFile)
+ c.Assert(err, IsNil)
+ defer r.Close()
+ content, err := ioutil.ReadAll(r)
+ c.Assert(err, IsNil)
+ c.Assert(content, DeepEquals, []byte{
+ // crc
+ 0xc7, 0xd9, 0x6b, 0xc5,
+ // redundant
+ 0x0,
+ // a=b
+ 0x61, 0x3d, 0x62,
+ // eol
+ 0x0,
+ // c=d
+ 0x63, 0x3d, 0x64,
+ // eof
+ 0x0, 0x0,
+ // footer
+ 0xff, 0xff,
+ })
+
+ env, err = ubootenv.Open(u.envFile)
+ c.Assert(err, IsNil)
+ c.Assert(env.String(), Equals, "a=b\nc=d\n")
+ c.Assert(env.Size(), Equals, totalSize)
+}
diff -pruN 2.37.4-1/bootloader/ubootenv/export_test.go 2.39.2+19.10ubuntu1/bootloader/ubootenv/export_test.go
--- 2.37.4-1/bootloader/ubootenv/export_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/bootloader/ubootenv/export_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,24 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016-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 .
+ *
+ */
+
+package ubootenv
+
+var (
+ WriteUint32 = writeUint32
+)
diff -pruN 2.37.4-1/bootloader/uboot.go 2.39.2+19.10ubuntu1/bootloader/uboot.go
--- 2.37.4-1/bootloader/uboot.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/bootloader/uboot.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,94 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 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 .
+ *
+ */
+
+package bootloader
+
+import (
+ "path/filepath"
+
+ "github.com/snapcore/snapd/bootloader/ubootenv"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+)
+
+type uboot struct{}
+
+// newUboot create a new Uboot bootloader object
+func newUboot() Bootloader {
+ u := &uboot{}
+ if !osutil.FileExists(u.envFile()) {
+ return nil
+ }
+
+ return u
+}
+
+func (u *uboot) Name() string {
+ return "uboot"
+}
+
+func (u *uboot) Dir() string {
+ return filepath.Join(dirs.GlobalRootDir, "/boot/uboot")
+}
+
+func (u *uboot) ConfigFile() string {
+ return u.envFile()
+}
+
+func (u *uboot) envFile() string {
+ return filepath.Join(u.Dir(), "uboot.env")
+}
+
+func (u *uboot) SetBootVars(values map[string]string) error {
+ env, err := ubootenv.OpenWithFlags(u.envFile(), ubootenv.OpenBestEffort)
+ if err != nil {
+ return err
+ }
+
+ dirty := false
+ for k, v := range values {
+ // already set to the right value, nothing to do
+ if env.Get(k) == v {
+ continue
+ }
+ env.Set(k, v)
+ dirty = true
+ }
+
+ if dirty {
+ return env.Save()
+ }
+
+ return nil
+}
+
+func (u *uboot) GetBootVars(names ...string) (map[string]string, error) {
+ out := map[string]string{}
+
+ env, err := ubootenv.OpenWithFlags(u.envFile(), ubootenv.OpenBestEffort)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, name := range names {
+ out[name] = env.Get(name)
+ }
+
+ return out, nil
+}
diff -pruN 2.37.4-1/bootloader/uboot_test.go 2.39.2+19.10ubuntu1/bootloader/uboot_test.go
--- 2.37.4-1/bootloader/uboot_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/bootloader/uboot_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,134 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 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 .
+ *
+ */
+
+package bootloader
+
+import (
+ "os"
+ "time"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/bootloader/ubootenv"
+)
+
+func (s *PartitionTestSuite) makeFakeUbootEnv(c *C) {
+ u := &uboot{}
+
+ // ensure that we have a valid uboot.env too
+ env, err := ubootenv.Create(u.envFile(), 4096)
+ c.Assert(err, IsNil)
+ err = env.Save()
+ c.Assert(err, IsNil)
+}
+
+func (s *PartitionTestSuite) TestNewUbootNoUbootReturnsNil(c *C) {
+ u := newUboot()
+ c.Assert(u, IsNil)
+}
+
+func (s *PartitionTestSuite) TestNewUboot(c *C) {
+ s.makeFakeUbootEnv(c)
+
+ u := newUboot()
+ c.Assert(u, NotNil)
+ c.Assert(u, FitsTypeOf, &uboot{})
+}
+
+func (s *PartitionTestSuite) TestUbootGetEnvVar(c *C) {
+ s.makeFakeUbootEnv(c)
+
+ u := newUboot()
+ c.Assert(u, NotNil)
+ err := u.SetBootVars(map[string]string{
+ "snap_mode": "",
+ "snap_core": "4",
+ })
+ c.Assert(err, IsNil)
+
+ m, err := u.GetBootVars("snap_mode", "snap_core")
+ c.Assert(err, IsNil)
+ c.Assert(m, DeepEquals, map[string]string{
+ "snap_mode": "",
+ "snap_core": "4",
+ })
+}
+
+func (s *PartitionTestSuite) TestGetBootloaderWithUboot(c *C) {
+ s.makeFakeUbootEnv(c)
+
+ bootloader, err := Find()
+ c.Assert(err, IsNil)
+ c.Assert(bootloader, FitsTypeOf, &uboot{})
+}
+
+func (s *PartitionTestSuite) TestUbootSetEnvNoUselessWrites(c *C) {
+ s.makeFakeUbootEnv(c)
+
+ envFile := (&uboot{}).envFile()
+ env, err := ubootenv.Create(envFile, 4096)
+ c.Assert(err, IsNil)
+ env.Set("snap_ab", "b")
+ env.Set("snap_mode", "")
+ err = env.Save()
+ c.Assert(err, IsNil)
+
+ st, err := os.Stat(envFile)
+ c.Assert(err, IsNil)
+ time.Sleep(100 * time.Millisecond)
+
+ u := newUboot()
+ c.Assert(u, NotNil)
+
+ // note that we set to the same var as above
+ err = u.SetBootVars(map[string]string{"snap_ab": "b"})
+ c.Assert(err, IsNil)
+
+ env, err = ubootenv.Open(envFile)
+ c.Assert(err, IsNil)
+ c.Assert(env.String(), Equals, "snap_ab=b\n")
+
+ st2, err := os.Stat(envFile)
+ c.Assert(err, IsNil)
+ c.Assert(st.ModTime(), Equals, st2.ModTime())
+}
+
+func (s *PartitionTestSuite) TestUbootSetBootVarFwEnv(c *C) {
+ s.makeFakeUbootEnv(c)
+
+ u := newUboot()
+ err := u.SetBootVars(map[string]string{"key": "value"})
+ c.Assert(err, IsNil)
+
+ content, err := u.GetBootVars("key")
+ c.Assert(err, IsNil)
+ c.Assert(content, DeepEquals, map[string]string{"key": "value"})
+}
+
+func (s *PartitionTestSuite) TestUbootGetBootVarFwEnv(c *C) {
+ s.makeFakeUbootEnv(c)
+
+ u := newUboot()
+ err := u.SetBootVars(map[string]string{"key2": "value2"})
+ c.Assert(err, IsNil)
+
+ content, err := u.GetBootVars("key2")
+ c.Assert(err, IsNil)
+ c.Assert(content, DeepEquals, map[string]string{"key2": "value2"})
+}
diff -pruN 2.37.4-1/.clang-format 2.39.2+19.10ubuntu1/.clang-format
--- 2.37.4-1/.clang-format 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/.clang-format 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,3 @@
+BasedOnStyle: Google
+IndentWidth: 4
+ColumnLimit: 120
diff -pruN 2.37.4-1/client/client.go 2.39.2+19.10ubuntu1/client/client.go
--- 2.37.4-1/client/client.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/client/client.go 2019-06-05 06:41:21.000000000 +0000
@@ -684,3 +684,12 @@ func (client *Client) Debug(action strin
_, err = client.doSync("POST", "/v2/debug", nil, nil, bytes.NewReader(body), result)
return err
}
+
+func (client *Client) DebugGet(aspect string, result interface{}, params map[string]string) error {
+ urlParams := url.Values{"aspect": []string{aspect}}
+ for k, v := range params {
+ urlParams.Set(k, v)
+ }
+ _, err := client.doSync("GET", "/v2/debug", urlParams, nil, nil, &result)
+ return err
+}
diff -pruN 2.37.4-1/client/client_test.go 2.39.2+19.10ubuntu1/client/client_test.go
--- 2.37.4-1/client/client_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/client/client_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -26,6 +26,7 @@ import (
"net"
"net/http"
"net/http/httptest"
+ "net/url"
"os"
"path/filepath"
"strings"
@@ -558,3 +559,16 @@ func (cs *clientSuite) TestDebugGeneric(
c.Assert(err, IsNil)
c.Check(string(data), DeepEquals, `{"action":"do-something","params":["param1","param2"]}`)
}
+
+func (cs *clientSuite) TestDebugGet(c *C) {
+ cs.rsp = `{"type": "sync", "result":["res1","res2"]}`
+
+ var result []string
+ err := cs.cli.DebugGet("do-something", &result, map[string]string{"foo": "bar"})
+ c.Check(err, IsNil)
+ c.Check(result, DeepEquals, []string{"res1", "res2"})
+ c.Check(cs.reqs, HasLen, 1)
+ c.Check(cs.reqs[0].Method, Equals, "GET")
+ c.Check(cs.reqs[0].URL.Path, Equals, "/v2/debug")
+ c.Check(cs.reqs[0].URL.Query(), DeepEquals, url.Values{"aspect": []string{"do-something"}, "foo": []string{"bar"}})
+}
diff -pruN 2.37.4-1/client/connections.go 2.39.2+19.10ubuntu1/client/connections.go
--- 2.37.4-1/client/connections.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/client/connections.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,81 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package client
+
+import (
+ "net/url"
+)
+
+// Connection describes a connection between a plug and a slot.
+type Connection struct {
+ Slot SlotRef `json:"slot"`
+ Plug PlugRef `json:"plug"`
+ Interface string `json:"interface"`
+ // Manual is set for connections that were established manually.
+ Manual bool `json:"manual"`
+ // Gadget is set for connections that were enabled by the gadget snap.
+ Gadget bool `json:"gadget"`
+ // SlotAttrs is the list of attributes of the slot side of the connection.
+ SlotAttrs map[string]interface{} `json:"slot-attrs,omitempty"`
+ // PlugAttrs is the list of attributes of the plug side of the connection.
+ PlugAttrs map[string]interface{} `json:"plug-attrs,omitempty"`
+}
+
+// Connections contains information about connections, as well as related plugs
+// and slots.
+type Connections struct {
+ // Established is the list of connections that are currently present.
+ Established []Connection `json:"established"`
+ // Undersired is a list of connections that are manually denied.
+ Undesired []Connection `json:"undesired"`
+ Plugs []Plug `json:"plugs"`
+ Slots []Slot `json:"slots"`
+}
+
+// ConnectionOptions contains criteria for selecting matching connections, plugs
+// and slots.
+type ConnectionOptions struct {
+ // Snap selects connections with the snap on one of the sides, as well
+ // as plugs and slots of a given snap.
+ Snap string
+ // Interface selects connections, plugs or slots using given interface.
+ Interface string
+ // All when true, selects established and undesired connections as well
+ // as all disconnected plugs and slots.
+ All bool
+}
+
+// Connections returns matching plugs, slots and their connections. Unless
+// specified by matching options, returns established connections.
+func (client *Client) Connections(opts *ConnectionOptions) (Connections, error) {
+ var conns Connections
+ query := url.Values{}
+ if opts != nil && opts.Snap != "" {
+ query.Set("snap", opts.Snap)
+ }
+ if opts != nil && opts.Interface != "" {
+ query.Set("interface", opts.Interface)
+ }
+ if opts != nil && opts.All {
+ query.Set("select", "all")
+ }
+ _, err := client.doSync("GET", "/v2/connections", query, nil, nil, &conns)
+ return conns, err
+}
diff -pruN 2.37.4-1/client/connections_test.go 2.39.2+19.10ubuntu1/client/connections_test.go
--- 2.37.4-1/client/connections_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/client/connections_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,270 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 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 .
+ *
+ */
+
+package client_test
+
+import (
+ "net/url"
+
+ "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/client"
+)
+
+func (cs *clientSuite) TestClientConnectionsCallsEndpoint(c *check.C) {
+ _, _ = cs.cli.Connections(nil)
+ c.Check(cs.req.Method, check.Equals, "GET")
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/connections")
+}
+
+func (cs *clientSuite) TestClientConnectionsDefault(c *check.C) {
+ cs.rsp = `{
+ "type": "sync",
+ "result": {
+ "established": [
+ {
+ "slot": {"snap": "keyboard-lights", "slot": "capslock-led"},
+ "plug": {"snap": "canonical-pi2", "plug": "pin-13"},
+ "interface": "bool-file",
+ "gadget": true
+ }
+ ],
+ "plugs": [
+ {
+ "snap": "canonical-pi2",
+ "plug": "pin-13",
+ "interface": "bool-file",
+ "label": "Pin 13",
+ "connections": [
+ {"snap": "keyboard-lights", "slot": "capslock-led"}
+ ]
+ }
+ ],
+ "slots": [
+ {
+ "snap": "keyboard-lights",
+ "slot": "capslock-led",
+ "interface": "bool-file",
+ "label": "Capslock indicator LED",
+ "connections": [
+ {"snap": "canonical-pi2", "plug": "pin-13"}
+ ]
+ }
+ ]
+ }
+ }`
+ conns, err := cs.cli.Connections(nil)
+ c.Assert(err, check.IsNil)
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/connections")
+ c.Check(conns, check.DeepEquals, client.Connections{
+ Established: []client.Connection{
+ {
+ Plug: client.PlugRef{Snap: "canonical-pi2", Name: "pin-13"},
+ Slot: client.SlotRef{Snap: "keyboard-lights", Name: "capslock-led"},
+ Interface: "bool-file",
+ Gadget: true,
+ },
+ },
+ Plugs: []client.Plug{
+ {
+ Snap: "canonical-pi2",
+ Name: "pin-13",
+ Interface: "bool-file",
+ Label: "Pin 13",
+ Connections: []client.SlotRef{
+ {
+ Snap: "keyboard-lights",
+ Name: "capslock-led",
+ },
+ },
+ },
+ },
+ Slots: []client.Slot{
+ {
+ Snap: "keyboard-lights",
+ Name: "capslock-led",
+ Interface: "bool-file",
+ Label: "Capslock indicator LED",
+ Connections: []client.PlugRef{
+ {
+ Snap: "canonical-pi2",
+ Name: "pin-13",
+ },
+ },
+ },
+ },
+ })
+}
+
+func (cs *clientSuite) TestClientConnectionsAll(c *check.C) {
+ cs.rsp = `{
+ "type": "sync",
+ "result": {
+ "established": [
+ {
+ "slot": {"snap": "keyboard-lights", "slot": "capslock-led"},
+ "plug": {"snap": "canonical-pi2", "plug": "pin-13"},
+ "interface": "bool-file",
+ "gadget": true
+ }
+ ],
+ "undesired": [
+ {
+ "slot": {"snap": "keyboard-lights", "slot": "numlock-led"},
+ "plug": {"snap": "canonical-pi2", "plug": "pin-14"},
+ "interface": "bool-file",
+ "gadget": true,
+ "manual": true
+ }
+ ],
+ "plugs": [
+ {
+ "snap": "canonical-pi2",
+ "plug": "pin-13",
+ "interface": "bool-file",
+ "label": "Pin 13",
+ "connections": [
+ {"snap": "keyboard-lights", "slot": "capslock-led"}
+ ]
+ },
+ {
+ "snap": "canonical-pi2",
+ "plug": "pin-14",
+ "interface": "bool-file",
+ "label": "Pin 14"
+ }
+ ],
+ "slots": [
+ {
+ "snap": "keyboard-lights",
+ "slot": "capslock-led",
+ "interface": "bool-file",
+ "label": "Capslock indicator LED",
+ "connections": [
+ {"snap": "canonical-pi2", "plug": "pin-13"}
+ ]
+ },
+ {
+ "snap": "keyboard-lights",
+ "slot": "numlock-led",
+ "interface": "bool-file",
+ "label": "Numlock LED"
+ }
+ ]
+ }
+ }`
+ conns, err := cs.cli.Connections(&client.ConnectionOptions{All: true})
+ c.Assert(err, check.IsNil)
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/connections")
+ c.Check(cs.req.URL.RawQuery, check.Equals, "select=all")
+ c.Check(conns, check.DeepEquals, client.Connections{
+ Established: []client.Connection{
+ {
+ Plug: client.PlugRef{Snap: "canonical-pi2", Name: "pin-13"},
+ Slot: client.SlotRef{Snap: "keyboard-lights", Name: "capslock-led"},
+ Interface: "bool-file",
+ Gadget: true,
+ },
+ },
+ Undesired: []client.Connection{
+ {
+ Plug: client.PlugRef{Snap: "canonical-pi2", Name: "pin-14"},
+ Slot: client.SlotRef{Snap: "keyboard-lights", Name: "numlock-led"},
+ Interface: "bool-file",
+ Gadget: true,
+ Manual: true,
+ },
+ },
+ Plugs: []client.Plug{
+ {
+ Snap: "canonical-pi2",
+ Name: "pin-13",
+ Interface: "bool-file",
+ Label: "Pin 13",
+ Connections: []client.SlotRef{
+ {
+ Snap: "keyboard-lights",
+ Name: "capslock-led",
+ },
+ },
+ },
+ {
+ Snap: "canonical-pi2",
+ Name: "pin-14",
+ Interface: "bool-file",
+ Label: "Pin 14",
+ },
+ },
+ Slots: []client.Slot{
+ {
+ Snap: "keyboard-lights",
+ Name: "capslock-led",
+ Interface: "bool-file",
+ Label: "Capslock indicator LED",
+ Connections: []client.PlugRef{
+ {
+ Snap: "canonical-pi2",
+ Name: "pin-13",
+ },
+ },
+ },
+ {
+ Snap: "keyboard-lights",
+ Name: "numlock-led",
+ Interface: "bool-file",
+ Label: "Numlock LED",
+ },
+ },
+ })
+}
+
+func (cs *clientSuite) TestClientConnectionsFilter(c *check.C) {
+ cs.rsp = `{
+ "type": "sync",
+ "result": {
+ "established": [],
+ "plugs": [],
+ "slots": []
+ }
+ }`
+
+ _, err := cs.cli.Connections(&client.ConnectionOptions{All: true})
+ c.Assert(err, check.IsNil)
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/connections")
+ c.Check(cs.req.URL.RawQuery, check.Equals, "select=all")
+
+ _, err = cs.cli.Connections(&client.ConnectionOptions{Snap: "foo"})
+ c.Assert(err, check.IsNil)
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/connections")
+ c.Check(cs.req.URL.RawQuery, check.Equals, "snap=foo")
+
+ _, err = cs.cli.Connections(&client.ConnectionOptions{Interface: "test"})
+ c.Assert(err, check.IsNil)
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/connections")
+ c.Check(cs.req.URL.RawQuery, check.Equals, "interface=test")
+
+ _, err = cs.cli.Connections(&client.ConnectionOptions{All: true, Snap: "foo", Interface: "test"})
+ c.Assert(err, check.IsNil)
+ query := cs.req.URL.Query()
+ c.Check(query, check.DeepEquals, url.Values{
+ "select": []string{"all"},
+ "interface": []string{"test"},
+ "snap": []string{"foo"},
+ })
+}
diff -pruN 2.37.4-1/client/interfaces.go 2.39.2+19.10ubuntu1/client/interfaces.go
--- 2.37.4-1/client/interfaces.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/client/interfaces.go 2019-06-05 06:41:21.000000000 +0000
@@ -60,12 +60,6 @@ type SlotRef struct {
Name string `json:"slot"`
}
-// Connections contains information about all plugs, slots and their connections
-type Connections struct {
- Plugs []Plug `json:"plugs"`
- Slots []Slot `json:"slots"`
-}
-
// Interface holds information about a given interface and its instances.
type Interface struct {
Name string `json:"name,omitempty"`
@@ -82,13 +76,6 @@ type InterfaceAction struct {
Slots []Slot `json:"slots,omitempty"`
}
-// Connections returns all plugs, slots and their connections.
-func (client *Client) Connections() (Connections, error) {
- var conns Connections
- _, err := client.doSync("GET", "/v2/interfaces", nil, nil, nil, &conns)
- return conns, err
-}
-
// InterfaceOptions represents opt-in elements include in responses.
type InterfaceOptions struct {
Names []string
diff -pruN 2.37.4-1/client/interfaces_test.go 2.39.2+19.10ubuntu1/client/interfaces_test.go
--- 2.37.4-1/client/interfaces_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/client/interfaces_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -156,74 +156,6 @@ func (cs *clientSuite) TestClientInterfa
})
}
-func (cs *clientSuite) TestClientConnectionsCallsEndpoint(c *check.C) {
- _, _ = cs.cli.Connections()
- c.Check(cs.req.Method, check.Equals, "GET")
- c.Check(cs.req.URL.Path, check.Equals, "/v2/interfaces")
-}
-
-func (cs *clientSuite) TestClientConnections(c *check.C) {
- cs.rsp = `{
- "type": "sync",
- "result": {
- "plugs": [
- {
- "snap": "canonical-pi2",
- "plug": "pin-13",
- "interface": "bool-file",
- "label": "Pin 13",
- "connections": [
- {"snap": "keyboard-lights", "slot": "capslock-led"}
- ]
- }
- ],
- "slots": [
- {
- "snap": "keyboard-lights",
- "slot": "capslock-led",
- "interface": "bool-file",
- "label": "Capslock indicator LED",
- "connections": [
- {"snap": "canonical-pi2", "plug": "pin-13"}
- ]
- }
- ]
- }
- }`
- conns, err := cs.cli.Connections()
- c.Assert(err, check.IsNil)
- c.Check(conns, check.DeepEquals, client.Connections{
- Plugs: []client.Plug{
- {
- Snap: "canonical-pi2",
- Name: "pin-13",
- Interface: "bool-file",
- Label: "Pin 13",
- Connections: []client.SlotRef{
- {
- Snap: "keyboard-lights",
- Name: "capslock-led",
- },
- },
- },
- },
- Slots: []client.Slot{
- {
- Snap: "keyboard-lights",
- Name: "capslock-led",
- Interface: "bool-file",
- Label: "Capslock indicator LED",
- Connections: []client.PlugRef{
- {
- Snap: "canonical-pi2",
- Name: "pin-13",
- },
- },
- },
- },
- })
-}
-
func (cs *clientSuite) TestClientConnectCallsEndpoint(c *check.C) {
cs.cli.Connect("producer", "plug", "consumer", "slot")
c.Check(cs.req.Method, check.Equals, "POST")
diff -pruN 2.37.4-1/client/model.go 2.39.2+19.10ubuntu1/client/model.go
--- 2.37.4-1/client/model.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/client/model.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,45 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package client
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+)
+
+type remodelData struct {
+ NewModel string `json:"new-model"`
+}
+
+// Remodel tries to remodel the system with the given assertion data
+func (client *Client) Remodel(b []byte) (changeID string, err error) {
+ data, err := json.Marshal(&remodelData{
+ NewModel: string(b),
+ })
+ if err != nil {
+ return "", fmt.Errorf("cannot marshal remodel data: %v", err)
+ }
+ headers := map[string]string{
+ "Content-Type": "application/json",
+ }
+
+ return client.doAsync("POST", "/v2/model", nil, headers, bytes.NewReader(data))
+}
diff -pruN 2.37.4-1/client/model_test.go 2.39.2+19.10ubuntu1/client/model_test.go
--- 2.37.4-1/client/model_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/client/model_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,55 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package client_test
+
+import (
+ "encoding/json"
+ "io/ioutil"
+
+ . "gopkg.in/check.v1"
+)
+
+func (cs *clientSuite) TestClientRemodelEndpoint(c *C) {
+ cs.cli.Remodel([]byte(`{"new-model": "some-model"}`))
+ c.Check(cs.req.Method, Equals, "POST")
+ c.Check(cs.req.URL.Path, Equals, "/v2/model")
+}
+
+func (cs *clientSuite) TestClientRemodel(c *C) {
+ cs.rsp = `{
+ "type": "async",
+ "status-code": 202,
+ "result": {},
+ "change": "d728"
+ }`
+ remodelJsonData := []byte(`{"new-model": "some-model"}`)
+ id, err := cs.cli.Remodel(remodelJsonData)
+ c.Assert(err, IsNil)
+ c.Check(id, Equals, "d728")
+ c.Assert(cs.req.Header.Get("Content-Type"), Equals, "application/json")
+
+ body, err := ioutil.ReadAll(cs.req.Body)
+ c.Assert(err, IsNil)
+ jsonBody := make(map[string]string)
+ err = json.Unmarshal(body, &jsonBody)
+ c.Assert(err, IsNil)
+ c.Check(jsonBody, HasLen, 1)
+ c.Check(jsonBody["new-model"], Equals, string(remodelJsonData))
+}
diff -pruN 2.37.4-1/client/packages.go 2.39.2+19.10ubuntu1/client/packages.go
--- 2.37.4-1/client/packages.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/client/packages.go 2019-06-05 06:41:21.000000000 +0000
@@ -117,12 +117,17 @@ type ResultInfo struct {
// - Private: return snaps that are private
// - Query: only return snaps that match the query string
type FindOptions struct {
- Refresh bool
- Private bool
- Prefix bool
- Query string
+ // Query is a term to search by or a prefix (if Prefix is true)
+ Query string
+ Prefix bool
+
+ CommonID string
+
Section string
+ Private bool
Scope string
+
+ Refresh bool
}
var ErrNoSnapsInstalled = errors.New("no snaps installed")
@@ -179,8 +184,14 @@ func (client *Client) Find(opts *FindOpt
if opts.Prefix {
q.Set("name", opts.Query+"*")
} else {
- q.Set("q", opts.Query)
+ if opts.CommonID != "" {
+ q.Set("common-id", opts.CommonID)
+ }
+ if opts.Query != "" {
+ q.Set("q", opts.Query)
+ }
}
+
switch {
case opts.Refresh && opts.Private:
return nil, nil, fmt.Errorf("cannot specify refresh and private together")
diff -pruN 2.37.4-1/client/packages_test.go 2.39.2+19.10ubuntu1/client/packages_test.go
--- 2.37.4-1/client/packages_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/client/packages_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -45,7 +45,7 @@ func (cs *clientSuite) TestClientFindRef
c.Check(cs.req.Method, check.Equals, "GET")
c.Check(cs.req.URL.Path, check.Equals, "/v2/find")
c.Check(cs.req.URL.Query(), check.DeepEquals, url.Values{
- "q": []string{""}, "select": []string{"refresh"},
+ "select": []string{"refresh"},
})
}
@@ -57,7 +57,7 @@ func (cs *clientSuite) TestClientFindRef
c.Check(cs.req.Method, check.Equals, "GET")
c.Check(cs.req.URL.Path, check.Equals, "/v2/find")
c.Check(cs.req.URL.Query(), check.DeepEquals, url.Values{
- "q": []string{""}, "section": []string{"mysection"}, "select": []string{"refresh"},
+ "section": []string{"mysection"}, "select": []string{"refresh"},
})
}
@@ -68,7 +68,7 @@ func (cs *clientSuite) TestClientFindWit
c.Check(cs.req.Method, check.Equals, "GET")
c.Check(cs.req.URL.Path, check.Equals, "/v2/find")
c.Check(cs.req.URL.Query(), check.DeepEquals, url.Values{
- "q": []string{""}, "section": []string{"mysection"},
+ "section": []string{"mysection"},
})
}
@@ -89,7 +89,7 @@ func (cs *clientSuite) TestClientFindWit
c.Check(cs.req.Method, check.Equals, "GET")
c.Check(cs.req.URL.Path, check.Equals, "/v2/find")
c.Check(cs.req.URL.Query(), check.DeepEquals, url.Values{
- "q": []string{""}, "scope": []string{"mouthwash"},
+ "scope": []string{"mouthwash"},
})
}
@@ -185,6 +185,12 @@ func (cs *clientSuite) TestClientFindPre
c.Check(cs.req.URL.RawQuery, check.Equals, "name=foo%2A") // 2A is `*`
}
+func (cs *clientSuite) TestClientFindCommonID(c *check.C) {
+ _, _, _ = cs.cli.Find(&client.FindOptions{CommonID: "org.kde.ktuberling.desktop"})
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/find")
+ c.Check(cs.req.URL.RawQuery, check.Equals, "common-id=org.kde.ktuberling.desktop")
+}
+
func (cs *clientSuite) TestClientFindOne(c *check.C) {
_, _, _ = cs.cli.FindOne("foo")
c.Check(cs.req.URL.Path, check.Equals, "/v2/find")
diff -pruN 2.37.4-1/client/snapshot.go 2.39.2+19.10ubuntu1/client/snapshot.go
--- 2.37.4-1/client/snapshot.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/client/snapshot.go 2019-06-05 06:41:21.000000000 +0000
@@ -72,6 +72,9 @@ type Snapshot struct {
Size int64 `json:"size,omitempty"`
// if the snapshot failed to open this will be the reason why
Broken string `json:"broken,omitempty"`
+
+ // set if the snapshot was created automatically on snap removal
+ Auto bool `json:"auto,omitempty"`
}
// IsValid checks whether the snapshot is missing information that
diff -pruN 2.37.4-1/cmd/appinfo.go 2.39.2+19.10ubuntu1/cmd/appinfo.go
--- 2.37.4-1/cmd/appinfo.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/appinfo.go 1970-01-01 00:00:00.000000000 +0000
@@ -1,133 +0,0 @@
-// -*- Mode: Go; indent-tabs-mode: t -*-
-
-/*
- * Copyright (C) 2018 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 .
- *
- */
-
-package cmd
-
-import (
- "fmt"
- "path/filepath"
- "strings"
-
- "github.com/snapcore/snapd/client"
- "github.com/snapcore/snapd/dirs"
- "github.com/snapcore/snapd/osutil"
- "github.com/snapcore/snapd/progress"
- "github.com/snapcore/snapd/snap"
- "github.com/snapcore/snapd/systemd"
-)
-
-func ClientAppInfoNotes(app *client.AppInfo) string {
- if !app.IsService() {
- return "-"
- }
-
- var notes = make([]string, 0, 2)
- var seenTimer, seenSocket bool
- for _, act := range app.Activators {
- switch act.Type {
- case "timer":
- seenTimer = true
- case "socket":
- seenSocket = true
- }
- }
- if seenTimer {
- notes = append(notes, "timer-activated")
- }
- if seenSocket {
- notes = append(notes, "socket-activated")
- }
- if len(notes) == 0 {
- return "-"
- }
- return strings.Join(notes, ",")
-}
-
-func ClientAppInfosFromSnapAppInfos(apps []*snap.AppInfo) ([]client.AppInfo, error) {
- // TODO: pass in an actual notifier here instead of null
- // (Status doesn't _need_ it, but benefits from it)
- sysd := systemd.New(dirs.GlobalRootDir, progress.Null)
-
- out := make([]client.AppInfo, 0, len(apps))
- for _, app := range apps {
- appInfo := client.AppInfo{
- Snap: app.Snap.InstanceName(),
- Name: app.Name,
- CommonID: app.CommonID,
- }
- if fn := app.DesktopFile(); osutil.FileExists(fn) {
- appInfo.DesktopFile = fn
- }
-
- appInfo.Daemon = app.Daemon
- if !app.IsService() || !app.Snap.IsActive() {
- out = append(out, appInfo)
- continue
- }
-
- // collect all services for a single call to systemctl
- serviceNames := make([]string, 0, 1+len(app.Sockets)+1)
- serviceNames = append(serviceNames, app.ServiceName())
-
- sockSvcFileToName := make(map[string]string, len(app.Sockets))
- for _, sock := range app.Sockets {
- sockUnit := filepath.Base(sock.File())
- sockSvcFileToName[sockUnit] = sock.Name
- serviceNames = append(serviceNames, sockUnit)
- }
- if app.Timer != nil {
- timerUnit := filepath.Base(app.Timer.File())
- serviceNames = append(serviceNames, timerUnit)
- }
-
- // sysd.Status() makes sure that we get only the units we asked
- // for and raises an error otherwise
- sts, err := sysd.Status(serviceNames...)
- if err != nil {
- return nil, fmt.Errorf("cannot get status of services of app %q: %v", app.Name, err)
- }
- if len(sts) != len(serviceNames) {
- return nil, fmt.Errorf("cannot get status of services of app %q: expected %v results, got %v", app.Name, len(serviceNames), len(sts))
- }
- for _, st := range sts {
- switch filepath.Ext(st.UnitName) {
- case ".service":
- appInfo.Enabled = st.Enabled
- appInfo.Active = st.Active
- case ".timer":
- appInfo.Activators = append(appInfo.Activators, client.AppActivator{
- Name: app.Name,
- Enabled: st.Enabled,
- Active: st.Active,
- Type: "timer",
- })
- case ".socket":
- appInfo.Activators = append(appInfo.Activators, client.AppActivator{
- Name: sockSvcFileToName[st.UnitName],
- Enabled: st.Enabled,
- Active: st.Active,
- Type: "socket",
- })
- }
- }
- out = append(out, appInfo)
- }
-
- return out, nil
-}
diff -pruN 2.37.4-1/cmd/appinfo_test.go 2.39.2+19.10ubuntu1/cmd/appinfo_test.go
--- 2.37.4-1/cmd/appinfo_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/appinfo_test.go 1970-01-01 00:00:00.000000000 +0000
@@ -1,71 +0,0 @@
-// -*- Mode: Go; indent-tabs-mode: t -*-
-
-/*
- * Copyright (C) 2018 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 .
- *
- */
-
-package cmd_test
-
-import (
- "gopkg.in/check.v1"
-
- "github.com/snapcore/snapd/client"
- "github.com/snapcore/snapd/cmd"
-)
-
-func (*cmdSuite) TestAppStatusNotes(c *check.C) {
- ai := client.AppInfo{}
- c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "-")
-
- ai = client.AppInfo{
- Daemon: "oneshot",
- }
- c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "-")
-
- ai = client.AppInfo{
- Daemon: "oneshot",
- Activators: []client.AppActivator{
- {Type: "timer"},
- },
- }
- c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "timer-activated")
-
- ai = client.AppInfo{
- Daemon: "oneshot",
- Activators: []client.AppActivator{
- {Type: "socket"},
- },
- }
- c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "socket-activated")
-
- // check that the output is stable regardless of the order of activators
- ai = client.AppInfo{
- Daemon: "oneshot",
- Activators: []client.AppActivator{
- {Type: "timer"},
- {Type: "socket"},
- },
- }
- c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "timer-activated,socket-activated")
- ai = client.AppInfo{
- Daemon: "oneshot",
- Activators: []client.AppActivator{
- {Type: "socket"},
- {Type: "timer"},
- },
- }
- c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "timer-activated,socket-activated")
-}
diff -pruN 2.37.4-1/cmd/autogen.sh 2.39.2+19.10ubuntu1/cmd/autogen.sh
--- 2.37.4-1/cmd/autogen.sh 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/autogen.sh 2019-06-05 06:41:21.000000000 +0000
@@ -33,13 +33,13 @@ case "$ID" in
extra_opts="--libexecdir=/usr/lib/snapd"
;;
ubuntu)
- extra_opts="--libexecdir=/usr/lib/snapd --enable-nvidia-multiarch --enable-static-libcap --enable-static-libapparmor --enable-static-libseccomp --with-host-arch-triplet=$(dpkg-architecture -qDEB_HOST_MULTIARCH)"
+ extra_opts="--libexecdir=/usr/lib/snapd --enable-nvidia-multiarch --enable-static-libcap --enable-static-libapparmor --with-host-arch-triplet=$(dpkg-architecture -qDEB_HOST_MULTIARCH)"
if [ "$(dpkg-architecture -qDEB_HOST_ARCH)" = "amd64" ]; then
extra_opts="$extra_opts --with-host-arch-32bit-triplet=$(dpkg-architecture -ai386 -qDEB_HOST_MULTIARCH)"
fi
;;
fedora|centos|rhel)
- extra_opts="--libexecdir=/usr/libexec/snapd --with-snap-mount-dir=/var/lib/snapd/snap --enable-merged-usr --disable-apparmor"
+ extra_opts="--libexecdir=/usr/libexec/snapd --with-snap-mount-dir=/var/lib/snapd/snap --enable-merged-usr --disable-apparmor --enable-selinux"
;;
opensuse|opensuse-tumbleweed)
extra_opts="--libexecdir=/usr/lib/snapd --enable-nvidia-biarch --with-32bit-libdir=/usr/lib --enable-merged-usr"
diff -pruN 2.37.4-1/cmd/cmd_linux.go 2.39.2+19.10ubuntu1/cmd/cmd_linux.go
--- 2.37.4-1/cmd/cmd_linux.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/cmd_linux.go 2019-06-05 06:41:21.000000000 +0000
@@ -114,14 +114,15 @@ func coreSupportsReExec(corePath string)
return true
}
+// TODO: move to cmd/cmdutil/
+//
// InternalToolPath returns the path of an internal snapd tool. The tool
-// *must* be located inside /usr/lib/snapd/.
+// *must* be located inside the same tree as the current binary.
//
// The return value is either the path of the tool in the current distribution
-// or in the core snap (or the ubuntu-core snap). This handles spiritual
-// "re-exec" where we run the tool from the core snap if the environment allows
-// us to do so.
-func InternalToolPath(tool string) string {
+// or in the core/snapd snap (or the ubuntu-core snap) if the current binary is
+// ran from that location.
+func InternalToolPath(tool string) (string, error) {
distroTool := filepath.Join(dirs.DistroLibExecDir, tool)
// find the internal path relative to the running snapd, this
@@ -129,23 +130,34 @@ func InternalToolPath(tool string) strin
// having a valid "current" symlink).
exe, err := osReadlink("/proc/self/exe")
if err != nil {
- logger.Noticef("cannot read /proc/self/exe: %v, using tool outside core", err)
- return distroTool
+ return "", err
}
- // ensure we never use this helper from anything but
- if !strings.HasSuffix(exe, "/snapd") && !strings.HasSuffix(exe, ".test") {
- log.Panicf("InternalToolPath can only be used from snapd, got: %s", exe)
- }
+ if !strings.HasPrefix(exe, dirs.DistroLibExecDir) {
+ // either running from mounted location or /usr/bin/snap*
- if !strings.HasPrefix(exe, dirs.SnapMountDir) {
- logger.Debugf("exe doesn't have snap mount dir prefix: %q vs %q", exe, dirs.SnapMountDir)
- return distroTool
+ // find the local prefix to the snap:
+ // /snap/snapd/123/usr/bin/snap -> /snap/snapd/123
+ // /snap/core/234/usr/lib/snapd/snapd -> /snap/core/234
+ idx := strings.LastIndex(exe, "/usr/")
+ if idx > 0 {
+ // only assume mounted location when path contains
+ // /usr/, but does not start with one
+ prefix := exe[:idx]
+ return filepath.Join(prefix, "/usr/lib/snapd", tool), nil
+ }
+ if idx == -1 {
+ // or perhaps some other random location, make sure the tool
+ // exists there and is an executable
+ maybeTool := filepath.Join(filepath.Dir(exe), tool)
+ if osutil.IsExecutable(maybeTool) {
+ return maybeTool, nil
+ }
+ }
}
- // if we are re-execed, then the tool is at the same location
- // as snapd
- return filepath.Join(filepath.Dir(exe), tool)
+ // fallback to distro tool
+ return distroTool, nil
}
// mustUnsetenv will unset the given environment key or panic if it
@@ -212,3 +224,12 @@ func ExecInSnapdOrCoreSnap() {
logger.Debugf("restarting into %q", full)
panic(syscallExec(full, os.Args, os.Environ()))
}
+
+// MockOsReadlink is for use in tests
+func MockOsReadlink(f func(string) (string, error)) func() {
+ realOsReadlink := osReadlink
+ osReadlink = f
+ return func() {
+ osReadlink = realOsReadlink
+ }
+}
diff -pruN 2.37.4-1/cmd/cmd_other.go 2.39.2+19.10ubuntu1/cmd/cmd_other.go
--- 2.37.4-1/cmd/cmd_other.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/cmd_other.go 2019-06-05 06:41:21.000000000 +0000
@@ -20,9 +20,21 @@
package cmd
+import (
+ "errors"
+)
+
// ExecInSnapdOrCoreSnap makes sure you're executing the binary that ships in
// the snapd/core snap.
// On this OS this is a stub.
func ExecInSnapdOrCoreSnap() {
return
}
+
+// InternalToolPath returns the path of an internal snapd tool. The tool
+// *must* be located inside the same tree as the current binary.
+//
+// On this OS this is a stub and always returns an error.
+func InternalToolPath(tool string) (string, error) {
+ return "", errors.New("unsupported on non-Linux systems")
+}
diff -pruN 2.37.4-1/cmd/cmd_test.go 2.39.2+19.10ubuntu1/cmd/cmd_test.go
--- 2.37.4-1/cmd/cmd_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/cmd_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -206,7 +206,9 @@ func (s *cmdSuite) TestInternalToolPathN
})
defer restore()
- c.Check(cmd.InternalToolPath("potato"), Equals, filepath.Join(dirs.DistroLibExecDir, "potato"))
+ path, err := cmd.InternalToolPath("potato")
+ c.Check(err, IsNil)
+ c.Check(path, Equals, filepath.Join(dirs.DistroLibExecDir, "potato"))
}
func (s *cmdSuite) TestInternalToolPathWithReexec(c *C) {
@@ -216,16 +218,126 @@ func (s *cmdSuite) TestInternalToolPathW
})
defer restore()
- c.Check(cmd.InternalToolPath("potato"), Equals, filepath.Join(dirs.SnapMountDir, "snapd/42/usr/lib/snapd/potato"))
+ path, err := cmd.InternalToolPath("potato")
+ c.Check(err, IsNil)
+ c.Check(path, Equals, filepath.Join(dirs.SnapMountDir, "snapd/42/usr/lib/snapd/potato"))
}
-func (s *cmdSuite) TestInternalToolPathFromIncorrectHelper(c *C) {
+func (s *cmdSuite) TestInternalToolPathWithOtherLocation(c *C) {
+ s.fakeInternalTool(c, s.snapdPath, "potato")
+ restore := cmd.MockOsReadlink(func(string) (string, error) {
+ return filepath.Join("/tmp/tmp.foo_1234/usr/lib/snapd/snapd"), nil
+ })
+ defer restore()
+
+ path, err := cmd.InternalToolPath("potato")
+ c.Check(err, IsNil)
+ c.Check(path, Equals, "/tmp/tmp.foo_1234/usr/lib/snapd/potato")
+}
+
+func (s *cmdSuite) TestInternalToolSnapPathWithOtherLocation(c *C) {
+ restore := cmd.MockOsReadlink(func(string) (string, error) {
+ return filepath.Join("/tmp/tmp.foo_1234/usr/bin/snap"), nil
+ })
+ defer restore()
+
+ path, err := cmd.InternalToolPath("potato")
+ c.Check(err, IsNil)
+ c.Check(path, Equals, "/tmp/tmp.foo_1234/usr/lib/snapd/potato")
+}
+
+func (s *cmdSuite) TestInternalToolPathWithOtherCrazyLocation(c *C) {
+ restore := cmd.MockOsReadlink(func(string) (string, error) {
+ return filepath.Join("/usr/foo/usr/tmp/tmp.foo_1234/usr/bin/snap"), nil
+ })
+ defer restore()
+
+ path, err := cmd.InternalToolPath("potato")
+ c.Check(err, IsNil)
+ c.Check(path, Equals, "/usr/foo/usr/tmp/tmp.foo_1234/usr/lib/snapd/potato")
+}
+
+func (s *cmdSuite) TestInternalToolPathWithDevLocationFallback(c *C) {
+ restore := cmd.MockOsReadlink(func(string) (string, error) {
+ return filepath.Join("/home/dev/snapd/snapd"), nil
+ })
+ defer restore()
+
+ path, err := cmd.InternalToolPath("potato")
+ c.Check(err, IsNil)
+ c.Check(path, Equals, filepath.Join(dirs.DistroLibExecDir, "potato"))
+}
+
+func (s *cmdSuite) TestInternalToolPathWithOtherDevLocationWhenExecutable(c *C) {
restore := cmd.MockOsReadlink(func(string) (string, error) {
- return "/usr/bin/potato", nil
+ return filepath.Join(dirs.GlobalRootDir, "/tmp/snapd"), nil
+ })
+ defer restore()
+
+ devTool := filepath.Join(dirs.GlobalRootDir, "/tmp/potato")
+ err := os.MkdirAll(filepath.Dir(devTool), 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(devTool, []byte(""), 0755)
+ c.Assert(err, IsNil)
+
+ path, err := cmd.InternalToolPath("potato")
+ c.Check(err, IsNil)
+ c.Check(path, Equals, filepath.Join(dirs.GlobalRootDir, "/tmp/potato"))
+}
+
+func (s *cmdSuite) TestInternalToolPathWithOtherDevLocationNonExecutable(c *C) {
+ restore := cmd.MockOsReadlink(func(string) (string, error) {
+ return filepath.Join(dirs.GlobalRootDir, "/tmp/snapd"), nil
+ })
+ defer restore()
+
+ devTool := filepath.Join(dirs.GlobalRootDir, "/tmp/non-executable-potato")
+ err := os.MkdirAll(filepath.Dir(devTool), 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(devTool, []byte(""), 0644)
+ c.Assert(err, IsNil)
+
+ path, err := cmd.InternalToolPath("non-executable-potato")
+ c.Check(err, IsNil)
+ c.Check(path, Equals, filepath.Join(dirs.DistroLibExecDir, "non-executable-potato"))
+}
+
+func (s *cmdSuite) TestInternalToolPathSnapdPathReexec(c *C) {
+ restore := cmd.MockOsReadlink(func(string) (string, error) {
+ return filepath.Join(dirs.SnapMountDir, "core/111/usr/bin/snap"), nil
+ })
+ defer restore()
+
+ p, err := cmd.InternalToolPath("snapd")
+ c.Assert(err, IsNil)
+ c.Check(p, Equals, filepath.Join(dirs.SnapMountDir, "/core/111/usr/lib/snapd/snapd"))
+}
+
+func (s *cmdSuite) TestInternalToolPathSnapdSnap(c *C) {
+ restore := cmd.MockOsReadlink(func(string) (string, error) {
+ return filepath.Join(dirs.SnapMountDir, "snapd/22/usr/bin/snap"), nil
+ })
+ defer restore()
+ p, err := cmd.InternalToolPath("snapd")
+ c.Assert(err, IsNil)
+ c.Check(p, Equals, filepath.Join(dirs.SnapMountDir, "/snapd/22/usr/lib/snapd/snapd"))
+}
+
+func (s *cmdSuite) TestInternalToolPathWithLibexecdirLocation(c *C) {
+ defer dirs.SetRootDir(s.fakeroot)
+ restore := release.MockReleaseInfo(&release.OS{ID: "fedora"})
+ defer restore()
+ // reload directory paths
+ dirs.SetRootDir("/")
+
+ restore = cmd.MockOsReadlink(func(string) (string, error) {
+ return filepath.Join("/usr/bin/snap"), nil
})
defer restore()
- c.Check(func() { cmd.InternalToolPath("potato") }, PanicMatches, "InternalToolPath can only be used from snapd, got: /usr/bin/potato")
+ path, err := cmd.InternalToolPath("potato")
+ c.Check(err, IsNil)
+ c.Check(path, Equals, filepath.Join("/usr/libexec/snapd/potato"))
}
func (s *cmdSuite) TestExecInSnapdOrCoreSnap(c *C) {
diff -pruN 2.37.4-1/cmd/cmdutil/cmdutil.go 2.39.2+19.10ubuntu1/cmd/cmdutil/cmdutil.go
--- 2.37.4-1/cmd/cmdutil/cmdutil.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/cmdutil/cmdutil.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,139 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+package cmdutil
+
+import (
+ "bufio"
+ "bytes"
+ "debug/elf"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+)
+
+func elfInterp(cmd string) (string, error) {
+ el, err := elf.Open(cmd)
+ if err != nil {
+ return "", err
+ }
+ defer el.Close()
+
+ for _, prog := range el.Progs {
+ if prog.Type == elf.PT_INTERP {
+ r := prog.Open()
+ interp, err := ioutil.ReadAll(r)
+ if err != nil {
+ return "", nil
+ }
+
+ return string(bytes.Trim(interp, "\x00")), nil
+ }
+ }
+
+ return "", fmt.Errorf("cannot find PT_INTERP header")
+}
+
+func parseLdSoConf(root string, confPath string) []string {
+ f, err := os.Open(filepath.Join(root, confPath))
+ if err != nil {
+ return nil
+ }
+ defer f.Close()
+
+ var out []string
+ scanner := bufio.NewScanner(f)
+ for scanner.Scan() {
+ line := scanner.Text()
+ switch {
+ case strings.HasPrefix(line, "#"):
+ // nothing
+ case strings.TrimSpace(line) == "":
+ // nothing
+ case strings.HasPrefix(line, "include "):
+ l := strings.SplitN(line, "include ", 2)
+ files, err := filepath.Glob(filepath.Join(root, l[1]))
+ if err != nil {
+ return nil
+ }
+ for _, f := range files {
+ out = append(out, parseLdSoConf(root, f[len(root):])...)
+ }
+ default:
+ out = append(out, filepath.Join(root, line))
+ }
+
+ }
+ if err := scanner.Err(); err != nil {
+ return nil
+ }
+
+ return out
+}
+
+// CommandFromSystemSnap runs a command from the snapd/core snap
+// using the proper interpreter and library paths.
+//
+// At the moment it can only run ELF files, expects a standard ld.so
+// interpreter, and can't handle RPATH.
+func CommandFromSystemSnap(name string, cmdArgs ...string) (*exec.Cmd, error) {
+ from := "snapd"
+ root := filepath.Join(dirs.SnapMountDir, "/snapd/current")
+ if !osutil.FileExists(root) {
+ from = "core"
+ root = filepath.Join(dirs.SnapMountDir, "/core/current")
+ }
+
+ cmdPath := filepath.Join(root, name)
+ interp, err := elfInterp(cmdPath)
+ if err != nil {
+ return nil, err
+ }
+ coreLdSo := filepath.Join(root, interp)
+ // we cannot use EvalSymlink here because we need to resolve
+ // relative and an absolute symlinks differently. A absolute
+ // symlink is relative to root of the snapd/core snap.
+ seen := map[string]bool{}
+ for osutil.IsSymlink(coreLdSo) {
+ link, err := os.Readlink(coreLdSo)
+ if err != nil {
+ return nil, err
+ }
+ if filepath.IsAbs(link) {
+ coreLdSo = filepath.Join(root, link)
+ } else {
+ coreLdSo = filepath.Join(filepath.Dir(coreLdSo), link)
+ }
+ if seen[coreLdSo] {
+ return nil, fmt.Errorf("cannot run command from %s: symlink cycle found", from)
+ }
+ seen[coreLdSo] = true
+ }
+
+ ldLibraryPathForCore := parseLdSoConf(root, "/etc/ld.so.conf")
+
+ ldSoArgs := []string{"--library-path", strings.Join(ldLibraryPathForCore, ":"), cmdPath}
+ allArgs := append(ldSoArgs, cmdArgs...)
+ return exec.Command(coreLdSo, allArgs...), nil
+}
diff -pruN 2.37.4-1/cmd/cmdutil/cmdutil_test.go 2.39.2+19.10ubuntu1/cmd/cmdutil/cmdutil_test.go
--- 2.37.4-1/cmd/cmdutil/cmdutil_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/cmdutil/cmdutil_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,118 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 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 .
+ *
+ */
+
+package cmdutil_test
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/cmd/cmdutil"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+)
+
+// Hook up check.v1 into the "go test" runner
+func Test(t *testing.T) { TestingT(t) }
+
+var truePath = osutil.LookPathDefault("true", "/bin/true")
+
+type cmdutilSuite struct{}
+
+var _ = Suite(&cmdutilSuite{})
+
+func (s *cmdutilSuite) SetUpTest(c *C) {
+ dirs.SetRootDir(c.MkDir())
+}
+
+func (s *cmdutilSuite) TearDownTest(c *C) {
+ dirs.SetRootDir("")
+}
+
+func (s *cmdutilSuite) makeMockLdSoConf(c *C, root string) {
+ ldSoConf := filepath.Join(root, "/etc/ld.so.conf")
+ ldSoConfD := ldSoConf + ".d"
+
+ err := os.MkdirAll(filepath.Dir(ldSoConf), 0755)
+ c.Assert(err, IsNil)
+ err = os.MkdirAll(ldSoConfD, 0755)
+ c.Assert(err, IsNil)
+
+ err = ioutil.WriteFile(ldSoConf, []byte("include /etc/ld.so.conf.d/*.conf"), 0644)
+ c.Assert(err, IsNil)
+
+ ldSoConf1 := filepath.Join(ldSoConfD, "x86_64-linux-gnu.conf")
+
+ err = ioutil.WriteFile(ldSoConf1, []byte(`
+# Multiarch support
+/lib/x86_64-linux-gnu
+/usr/lib/x86_64-linux-gnu`), 0644)
+ c.Assert(err, IsNil)
+}
+
+func (s *cmdutilSuite) TestCommandFromSystemSnap(c *C) {
+ for _, snap := range []string{"core", "snapd"} {
+
+ root := filepath.Join(dirs.SnapMountDir, snap, "current")
+ s.makeMockLdSoConf(c, root)
+
+ os.MkdirAll(filepath.Join(root, "/usr/bin"), 0755)
+ osutil.CopyFile(truePath, filepath.Join(root, "/usr/bin/xdelta3"), 0)
+ cmd, err := cmdutil.CommandFromSystemSnap("/usr/bin/xdelta3", "--some-xdelta-arg")
+ c.Assert(err, IsNil)
+
+ out, err := exec.Command("/bin/sh", "-c", fmt.Sprintf("readelf -l %s |grep interpreter:|cut -f2 -d:|cut -f1 -d]", truePath)).Output()
+ c.Assert(err, IsNil)
+ interp := strings.TrimSpace(string(out))
+
+ c.Check(cmd.Args, DeepEquals, []string{
+ filepath.Join(root, interp),
+ "--library-path",
+ fmt.Sprintf("%s/lib/x86_64-linux-gnu:%s/usr/lib/x86_64-linux-gnu", root, root),
+ filepath.Join(root, "/usr/bin/xdelta3"),
+ "--some-xdelta-arg",
+ })
+ }
+}
+
+func (s *cmdutilSuite) TestCommandFromCoreSymlinkCycle(c *C) {
+ root := filepath.Join(dirs.SnapMountDir, "/core/current")
+ s.makeMockLdSoConf(c, root)
+
+ os.MkdirAll(filepath.Join(root, "/usr/bin"), 0755)
+ osutil.CopyFile(truePath, filepath.Join(root, "/usr/bin/xdelta3"), 0)
+
+ out, err := exec.Command("/bin/sh", "-c", "readelf -l /bin/true |grep interpreter:|cut -f2 -d:|cut -f1 -d]").Output()
+ c.Assert(err, IsNil)
+ interp := strings.TrimSpace(string(out))
+
+ coreInterp := filepath.Join(root, interp)
+ c.Assert(os.MkdirAll(filepath.Dir(coreInterp), 0755), IsNil)
+ c.Assert(os.Symlink(filepath.Base(coreInterp), coreInterp), IsNil)
+
+ _, err = cmdutil.CommandFromSystemSnap("/usr/bin/xdelta3", "--some-xdelta-arg")
+ c.Assert(err, ErrorMatches, "cannot run command from core: symlink cycle found")
+}
diff -pruN 2.37.4-1/cmd/configure.ac 2.39.2+19.10ubuntu1/cmd/configure.ac
--- 2.37.4-1/cmd/configure.ac 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/configure.ac 2019-06-05 06:41:21.000000000 +0000
@@ -64,34 +64,26 @@ AC_ARG_ENABLE([apparmor],
esac], [enable_apparmor=yes])
AM_CONDITIONAL([APPARMOR], [test "x$enable_apparmor" = "xyes"])
-# Allow to build without seccomp support by calling:
-# ./configure --disable-seccomp
-# This is separate because seccomp support is generally very good and it
-# provides useful confinement for unsafe system calls.
-AC_ARG_ENABLE([seccomp],
- AS_HELP_STRING([--disable-seccomp], [Disable seccomp support]),
- [case "${enableval}" in
- yes) enable_seccomp=yes ;;
- no) enable_seccomp=no ;;
- *) AC_MSG_ERROR([bad value ${enableval} for --disable-seccomp])
- esac], [enable_seccomp=yes])
-AM_CONDITIONAL([SECCOMP], [test "x$enable_seccomp" = "xyes"])
+# Allow to build with SELinux support by calling:
+# ./configure --enable-selinux
+AC_ARG_ENABLE([selinux],
+ AS_HELP_STRING([--enable-selinux], [Enable SELinux support]),
+ [case "${enableval}" in
+ yes) enable_selinux=yes ;;
+ no) enable_selinux=no ;;
+ *) AC_MSG_ERROR([bad value ${enableval} for --enable-selinux])
+ esac], [enable_selinux=no])
+AM_CONDITIONAL([SELINUX], [test "x$enable_selinux" = "xyes"])
# Enable older tests only when confinement is enabled and we're building for PC
# The tests are of smaller value as we port more and more tests to spread.
-AM_CONDITIONAL([CONFINEMENT_TESTS], [test "x$enable_apparmor" = "xyes" && test "x$enable_seccomp" = "xyes" && ((test "x$host_cpu" = "xx86_64" && test "x$build_cpu" = "xx86_64") || (test "x$host_cpu" = "xi686" && test "x$build_cpu" = "xi686"))])
+AM_CONDITIONAL([CONFINEMENT_TESTS], [test "x$enable_apparmor" = "xyes" && ((test "x$host_cpu" = "xx86_64" && test "x$build_cpu" = "xx86_64") || (test "x$host_cpu" = "xi686" && test "x$build_cpu" = "xi686"))])
# Check for glib that we use for unit testing
AS_IF([test "x$with_unit_tests" = "xyes"], [
PKG_CHECK_MODULES([GLIB], [glib-2.0])
])
-# Check if seccomp userspace library is available
-AS_IF([test "x$enable_seccomp" = "xyes"], [
- PKG_CHECK_MODULES([SECCOMP], [libseccomp], [
- AC_DEFINE([HAVE_SECCOMP], [1], [Build with seccomp support])])
-])
-
# Check if apparmor userspace library is available.
AS_IF([test "x$enable_apparmor" = "xyes"], [
PKG_CHECK_MODULES([APPARMOR], [libapparmor], [
@@ -105,6 +97,12 @@ AS_IF([test "x$enable_apparmor" = "xyes"
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX])
])
+# Check if SELinux userspace library is available.
+AS_IF([test "x$enable_selinux" = "xyes"], [
+PKG_CHECK_MODULES([SELINUX], [libselinux], [
+AC_DEFINE([HAVE_SELINUX], [1], [Build with SELinux support])])
+])
+
# Check if udev and libudev are available.
# Those are now used unconditionally even if apparmor is disabled.
PKG_CHECK_MODULES([LIBUDEV], [libudev])
@@ -182,6 +180,7 @@ AC_PATH_PROG([HAVE_VALGRIND],[valgrind])
AM_CONDITIONAL([HAVE_VALGRIND], [test "x${HAVE_VALGRIND}" != "x"])
AS_IF([test "x$HAVE_VALGRIND" = "x"], [AC_MSG_WARN(["cannot find the valgrind tool, will not run unit tests through valgrind"])])
+# Allow linking selected libraries statically for reexec.
AC_ARG_ENABLE([static-libcap],
AS_HELP_STRING([--enable-static-libcap], [Link libcap statically]),
[case "${enableval}" in
@@ -200,14 +199,14 @@ AC_ARG_ENABLE([static-libapparmor],
esac], [enable_static_libapparmor=no])
AM_CONDITIONAL([STATIC_LIBAPPARMOR], [test "x$enable_static_libapparmor" = "xyes"])
-AC_ARG_ENABLE([static-libseccomp],
- AS_HELP_STRING([--enable-static-libseccomp], [Link libseccomp statically]),
- [case "${enableval}" in
- yes) enable_static_libseccomp=yes ;;
- no) enable_static_libseccomp=no ;;
- *) AC_MSG_ERROR([bad value ${enableval} for --enable-static-libseccomp])
- esac], [enable_static_libseccomp=no])
-AM_CONDITIONAL([STATIC_LIBSECCOMP], [test "x$enable_static_libseccomp" = "xyes"])
+AC_ARG_ENABLE([static-libselinux],
+AS_HELP_STRING([--enable-static-libselinux], [Link libselinux statically]),
+[case "${enableval}" in
+yes) enable_static_libselinux=yes ;;
+no) enable_static_libselinux=no ;;
+*) AC_MSG_ERROR([bad value ${enableval} for --enable-static-libselinux])
+esac], [enable_static_libselinux=no])
+AM_CONDITIONAL([STATIC_LIBSELINUX], [test "x$enable_static_libselinux" = "xyes"])
LIB32_DIR="${prefix}/lib32"
AC_ARG_WITH([32bit-libdir],
diff -pruN 2.37.4-1/cmd/export_test.go 2.39.2+19.10ubuntu1/cmd/export_test.go
--- 2.37.4-1/cmd/export_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/export_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -50,11 +50,3 @@ func MockSyscallExec(f func(argv0 string
syscallExec = oldSyscallExec
}
}
-
-func MockOsReadlink(f func(string) (string, error)) func() {
- realOsReadlink := osReadlink
- osReadlink = f
- return func() {
- osReadlink = realOsReadlink
- }
-}
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/cgroup-freezer-support.c 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/cgroup-freezer-support.c
--- 2.37.4-1/cmd/libsnap-confine-private/cgroup-freezer-support.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/cgroup-freezer-support.c 2019-06-05 06:41:21.000000000 +0000
@@ -1,3 +1,20 @@
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
// For AT_EMPTY_PATH and O_PATH
#define _GNU_SOURCE
@@ -11,6 +28,7 @@
#include
#include
+#include "cgroup-support.h"
#include "cleanup-funcs.h"
#include "string-utils.h"
#include "utils.h"
@@ -19,51 +37,9 @@ static const char *freezer_cgroup_dir =
void sc_cgroup_freezer_join(const char *snap_name, pid_t pid)
{
- // Format the name of the cgroup hierarchy.
char buf[PATH_MAX] = { 0 };
sc_must_snprintf(buf, sizeof buf, "snap.%s", snap_name);
-
- // Open the freezer cgroup directory.
- int cgroup_fd SC_CLEANUP(sc_cleanup_close) = -1;
- cgroup_fd = open(freezer_cgroup_dir,
- O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
- if (cgroup_fd < 0) {
- die("cannot open freezer cgroup (%s)", freezer_cgroup_dir);
- }
- // Create the freezer hierarchy for the given snap.
- if (mkdirat(cgroup_fd, buf, 0755) < 0 && errno != EEXIST) {
- die("cannot create freezer cgroup hierarchy for snap %s",
- snap_name);
- }
- // Open the hierarchy directory for the given snap.
- int hierarchy_fd SC_CLEANUP(sc_cleanup_close) = -1;
- hierarchy_fd = openat(cgroup_fd, buf,
- O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
- if (hierarchy_fd < 0) {
- die("cannot open freezer cgroup hierarchy for snap %s",
- snap_name);
- }
- // Since we may be running from a setuid but not setgid executable, ensure
- // that the group and owner of the hierarchy directory is root.root.
- if (fchownat(hierarchy_fd, "", 0, 0, AT_EMPTY_PATH) < 0) {
- die("cannot change owner of freezer cgroup hierarchy for snap %s to root.root", snap_name);
- }
- // Open the tasks file.
- int tasks_fd SC_CLEANUP(sc_cleanup_close) = -1;
- tasks_fd = openat(hierarchy_fd, "tasks",
- O_WRONLY | O_NOFOLLOW | O_CLOEXEC);
- if (tasks_fd < 0) {
- die("cannot open tasks file for freezer cgroup hierarchy for snap %s", snap_name);
- }
- // Write the process (task) number to the tasks file. Linux task IDs are
- // limited to 2^29 so a long int is enough to represent it.
- // See include/linux/threads.h in the kernel source tree for details.
- int n = sc_must_snprintf(buf, sizeof buf, "%ld", (long)pid);
- if (write(tasks_fd, buf, n) < n) {
- die("cannot move process %ld to freezer cgroup hierarchy for snap %s", (long)pid, snap_name);
- }
- debug("moved process %ld to freezer cgroup hierarchy for snap %s",
- (long)pid, snap_name);
+ sc_cgroup_create_and_join(freezer_cgroup_dir, buf, pid);
}
bool sc_cgroup_freezer_occupied(const char *snap_name)
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/cgroup-freezer-support.h 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/cgroup-freezer-support.h
--- 2.37.4-1/cmd/libsnap-confine-private/cgroup-freezer-support.h 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/cgroup-freezer-support.h 2019-06-05 06:41:21.000000000 +0000
@@ -1,3 +1,20 @@
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
#ifndef SC_CGROUP_FREEZER_SUPPORT_H
#define SC_CGROUP_FREEZER_SUPPORT_H
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/cgroup-pids-support.c 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/cgroup-pids-support.c
--- 2.37.4-1/cmd/libsnap-confine-private/cgroup-pids-support.c 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/cgroup-pids-support.c 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+#include "cgroup-pids-support.h"
+
+#include "cgroup-support.h"
+
+static const char *pids_cgroup_dir = "/sys/fs/cgroup/pids";
+
+void sc_cgroup_pids_join(const char *snap_security_tag, pid_t pid) {
+ sc_cgroup_create_and_join(pids_cgroup_dir, snap_security_tag, pid);
+}
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/cgroup-pids-support.h 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/cgroup-pids-support.h
--- 2.37.4-1/cmd/libsnap-confine-private/cgroup-pids-support.h 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/cgroup-pids-support.h 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+#ifndef SC_CGROUP_PIDS_SUPPORT_H
+#define SC_CGROUP_PIDS_SUPPORT_H
+
+#include
+
+/**
+ * Join the pid cgroup for the given snap application.
+ *
+ * This function adds the specified task to the pid cgroup specific to the
+ * given snap. The name of the cgroup is "snap.$snap_name.$app_name" for apps
+ * or "snap.$snap_name.hook.$hook_name" for hooks.
+ *
+ * The "tasks" file belonging to the cgroup contains the set of all the
+ * threads that originate from the given snap app or hook. Examining that
+ * file one can reliably determine if the set is empty or not.
+ *
+ * Similarly the "cgroup.procs" file belonging to the same directory contains
+ * the set of all the processes that originate from the given snap app or
+ * hook.
+ *
+ * For more details please review:
+ * https://www.kernel.org/doc/Documentation/cgroup-v1/pids.txt
+ **/
+void sc_cgroup_pids_join(const char *snap_security_tag, pid_t pid);
+
+#endif
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/cgroup-support.c 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/cgroup-support.c
--- 2.37.4-1/cmd/libsnap-confine-private/cgroup-support.c 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/cgroup-support.c 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+// For AT_EMPTY_PATH and O_PATH
+#define _GNU_SOURCE
+
+#include "cgroup-support.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "cleanup-funcs.h"
+#include "string-utils.h"
+#include "utils.h"
+
+void sc_cgroup_create_and_join(const char *parent, const char *name, pid_t pid) {
+ int parent_fd SC_CLEANUP(sc_cleanup_close) = -1;
+ parent_fd = open(parent, O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
+ if (parent_fd < 0) {
+ die("cannot open cgroup hierarchy %s", parent);
+ }
+ if (mkdirat(parent_fd, name, 0755) < 0 && errno != EEXIST) {
+ die("cannot create cgroup hierarchy %s/%s", parent, name);
+ }
+ int hierarchy_fd SC_CLEANUP(sc_cleanup_close) = -1;
+ hierarchy_fd = openat(parent_fd, name, O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
+ if (hierarchy_fd < 0) {
+ die("cannot open cgroup hierarchy %s/%s", parent, name);
+ }
+ // Since we may be running from a setuid but not setgid executable, ensure
+ // that the group and owner of the hierarchy directory is root.root.
+ if (fchownat(hierarchy_fd, "", 0, 0, AT_EMPTY_PATH) < 0) {
+ die("cannot change owner of cgroup hierarchy %s/%s to root.root", parent, name);
+ }
+ // Open the tasks file.
+ int tasks_fd SC_CLEANUP(sc_cleanup_close) = -1;
+ tasks_fd = openat(hierarchy_fd, "tasks", O_WRONLY | O_NOFOLLOW | O_CLOEXEC);
+ if (tasks_fd < 0) {
+ die("cannot open file %s/%s/tasks", parent, name);
+ }
+ // Write the process (task) number to the tasks file. Linux task IDs are
+ // limited to 2^29 so a long int is enough to represent it.
+ // See include/linux/threads.h in the kernel source tree for details.
+ char buf[22] = {0}; // 2^64 base10 + 2 for NUL and '-' for long
+ int n = sc_must_snprintf(buf, sizeof buf, "%ld", (long)pid);
+ if (write(tasks_fd, buf, n) < n) {
+ die("cannot move process %ld to cgroup hierarchy %s/%s", (long)pid, parent, name);
+ }
+ debug("moved process %ld to cgroup hierarchy %s/%s", (long)pid, parent, name);
+}
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/cgroup-support.h 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/cgroup-support.h
--- 2.37.4-1/cmd/libsnap-confine-private/cgroup-support.h 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/cgroup-support.h 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+#ifndef SC_CGROUP_SUPPORT_H
+#define SC_CGROUP_SUPPORT_H
+
+#include
+
+/**
+ * sc_cgroup_create_and_join joins, perhaps creating, a cgroup hierarchy.
+ *
+ * The code assumes that an existing hierarchy rooted at "parent". It follows
+ * up with a sub-hierarchy called "name", creating it if necessary. The created
+ * sub-hierarchy is made to belong to root.root and the specified process is
+ * moved there.
+ **/
+void sc_cgroup_create_and_join(const char *parent, const char *name, pid_t pid);
+
+#endif
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/classic.c 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/classic.c
--- 2.37.4-1/cmd/libsnap-confine-private/classic.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/classic.c 2019-06-05 06:41:21.000000000 +0000
@@ -56,8 +56,3 @@ sc_distro sc_classify_distro(void)
return SC_DISTRO_CLASSIC;
}
}
-
-bool sc_should_use_normal_mode(sc_distro distro, const char *base_snap_name)
-{
- return distro != SC_DISTRO_CORE16 || !sc_streq(base_snap_name, "core");
-}
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/classic.h 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/classic.h
--- 2.37.4-1/cmd/libsnap-confine-private/classic.h 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/classic.h 2019-06-05 06:41:21.000000000 +0000
@@ -30,6 +30,4 @@ typedef enum sc_distro {
sc_distro sc_classify_distro(void);
-bool sc_should_use_normal_mode(sc_distro distro, const char *base_snap_name);
-
#endif
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/classic-test.c 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/classic-test.c
--- 2.37.4-1/cmd/libsnap-confine-private/classic-test.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/classic-test.c 2019-06-05 06:41:21.000000000 +0000
@@ -176,19 +176,7 @@ static void test_is_on_custom_base(void)
g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CORE_OTHER);
}
-static void test_should_use_normal_mode(void)
-{
- g_assert_false(sc_should_use_normal_mode(SC_DISTRO_CORE16, "core"));
- g_assert_true(sc_should_use_normal_mode(SC_DISTRO_CORE_OTHER, "core"));
- g_assert_true(sc_should_use_normal_mode(SC_DISTRO_CLASSIC, "core"));
-
- g_assert_true(sc_should_use_normal_mode(SC_DISTRO_CORE16, "core18"));
- g_assert_true(sc_should_use_normal_mode
- (SC_DISTRO_CORE_OTHER, "core18"));
- g_assert_true(sc_should_use_normal_mode(SC_DISTRO_CLASSIC, "core18"));
-}
-
-static void __attribute__ ((constructor)) init(void)
+static void __attribute__((constructor)) init(void)
{
g_test_add_func("/classic/on-classic", test_is_on_classic);
g_test_add_func("/classic/on-classic-with-long-line",
@@ -199,6 +187,4 @@ static void __attribute__ ((constructor)
g_test_add_func("/classic/on-fedora-base", test_is_on_fedora_base);
g_test_add_func("/classic/on-fedora-ws", test_is_on_fedora_ws);
g_test_add_func("/classic/on-custom-base", test_is_on_custom_base);
- g_test_add_func("/classic/should-use-normal-mode",
- test_should_use_normal_mode);
}
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/cleanup-funcs.c 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/cleanup-funcs.c
--- 2.37.4-1/cmd/libsnap-confine-private/cleanup-funcs.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/cleanup-funcs.c 2019-06-05 06:41:21.000000000 +0000
@@ -22,8 +22,9 @@
void sc_cleanup_string(char **ptr)
{
- if (ptr != NULL) {
+ if (ptr != NULL && *ptr != NULL) {
free(*ptr);
+ *ptr = NULL;
}
}
@@ -31,6 +32,7 @@ void sc_cleanup_file(FILE ** ptr)
{
if (ptr != NULL && *ptr != NULL) {
fclose(*ptr);
+ *ptr = NULL;
}
}
@@ -38,6 +40,7 @@ void sc_cleanup_endmntent(FILE ** ptr)
{
if (ptr != NULL && *ptr != NULL) {
endmntent(*ptr);
+ *ptr = NULL;
}
}
@@ -45,6 +48,7 @@ void sc_cleanup_closedir(DIR ** ptr)
{
if (ptr != NULL && *ptr != NULL) {
closedir(*ptr);
+ *ptr = NULL;
}
}
@@ -52,5 +56,6 @@ void sc_cleanup_close(int *ptr)
{
if (ptr != NULL && *ptr != -1) {
close(*ptr);
+ *ptr = -1;
}
}
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/cleanup-funcs.h 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/cleanup-funcs.h
--- 2.37.4-1/cmd/libsnap-confine-private/cleanup-funcs.h 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/cleanup-funcs.h 2019-06-05 06:41:21.000000000 +0000
@@ -34,40 +34,45 @@
/**
* Free a dynamically allocated string.
*
- * This function is designed to be used with
- * __attribute__((cleanup(sc_cleanup_string))).
+ * This function is designed to be used with SC_CLEANUP() macro.
+ * The variable MUST be initialized for correct operation.
+ * The safe initialisation value is NULL.
**/
void sc_cleanup_string(char **ptr);
/**
* Close an open file.
*
- * This function is designed to be used with
- * __attribute__((cleanup(sc_cleanup_file))).
+ * This function is designed to be used with SC_CLEANUP() macro.
+ * The variable MUST be initialized for correct operation.
+ * The safe initialisation value is NULL.
**/
void sc_cleanup_file(FILE ** ptr);
/**
* Close an open file with endmntent(3)
*
- * This function is designed to be used with
- * __attribute__((cleanup(sc_cleanup_endmntent))).
+ * This function is designed to be used with SC_CLEANUP() macro.
+ * The variable MUST be initialized for correct operation.
+ * The safe initialisation value is NULL.
**/
void sc_cleanup_endmntent(FILE ** ptr);
/**
* Close an open directory with closedir(3)
*
- * This function is designed to be used with
- * __attribute__((cleanup(sc_cleanup_closedir))).
+ * This function is designed to be used with SC_CLEANUP() macro.
+ * The variable MUST be initialized for correct operation.
+ * The safe initialisation value is NULL.
**/
void sc_cleanup_closedir(DIR ** ptr);
/**
* Close an open file descriptor with close(2)
*
- * This function is designed to be used with
- * __attribute__((cleanup(sc_cleanup_close))).
+ * This function is designed to be used with SC_CLEANUP() macro.
+ * The variable MUST be initialized for correct operation.
+ * The safe initialisation value is -1.
**/
void sc_cleanup_close(int *ptr);
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/cleanup-funcs-test.c 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/cleanup-funcs-test.c
--- 2.37.4-1/cmd/libsnap-confine-private/cleanup-funcs-test.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/cleanup-funcs-test.c 2019-06-05 06:41:21.000000000 +0000
@@ -20,6 +20,8 @@
#include
+#include
+
static int called = 0;
static void cleanup_fn(int *ptr)
@@ -38,7 +40,94 @@ static void test_cleanup_sanity(void)
g_assert_cmpint(called, ==, 1);
}
-static void __attribute__ ((constructor)) init(void)
+static void test_cleanup_string(void)
+{
+ /* It is safe to use with a NULL pointer to a string. */
+ sc_cleanup_string(NULL);
+
+ /* It is safe to use with a NULL string. */
+ char *str = NULL;
+ sc_cleanup_string(&str);
+
+ /* It is safe to use with a non-NULL string. */
+ str = malloc(1);
+ g_assert_nonnull(str);
+ sc_cleanup_string(&str);
+ g_assert_null(str);
+}
+
+static void test_cleanup_file(void)
+{
+ /* It is safe to use with a NULL pointer to a FILE. */
+ sc_cleanup_file(NULL);
+
+ /* It is safe to use with a NULL FILE. */
+ FILE *f = NULL;
+ sc_cleanup_file(&f);
+
+ /* It is safe to use with a non-NULL FILE. */
+ f = fmemopen(NULL, 10, "rt");
+ g_assert_nonnull(f);
+ sc_cleanup_file(&f);
+ g_assert_null(f);
+}
+
+static void test_cleanup_endmntent(void)
+{
+ /* It is safe to use with a NULL pointer to a FILE. */
+ sc_cleanup_endmntent(NULL);
+
+ /* It is safe to use with a NULL FILE. */
+ FILE *f = NULL;
+ sc_cleanup_endmntent(&f);
+
+ /* It is safe to use with a non-NULL FILE. */
+ f = setmntent("/etc/fstab", "rt");
+ g_assert_nonnull(f);
+ sc_cleanup_endmntent(&f);
+ g_assert_null(f);
+}
+
+static void test_cleanup_closedir(void)
+{
+ /* It is safe to use with a NULL pointer to a DIR. */
+ sc_cleanup_closedir(NULL);
+
+ /* It is safe to use with a NULL DIR. */
+ DIR *d = NULL;
+ sc_cleanup_closedir(&d);
+
+ /* It is safe to use with a non-NULL DIR. */
+ d = opendir(".");
+ g_assert_nonnull(d);
+ sc_cleanup_closedir(&d);
+ g_assert_null(d);
+}
+
+static void test_cleanup_close(void)
+{
+ /* It is safe to use with a NULL pointer to an int. */
+ sc_cleanup_close(NULL);
+
+ /* It is safe to use with a -1 file descriptor. */
+ int fd = -1;
+ sc_cleanup_close(&fd);
+
+ /* It is safe to use with a non-invalid file descriptor. */
+ /* Timerfd is a simple to use and widely available object that can be
+ * created and closed without interacting with the filesystem. */
+ fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC);
+ g_assert_cmpint(fd, !=, -1);
+ sc_cleanup_close(&fd);
+ g_assert_cmpint(fd, ==, -1);
+}
+
+static void __attribute__((constructor)) init(void)
{
g_test_add_func("/cleanup/sanity", test_cleanup_sanity);
+ g_test_add_func("/cleanup/string", test_cleanup_string);
+ g_test_add_func("/cleanup/file", test_cleanup_file);
+ g_test_add_func("/cleanup/endmntent", test_cleanup_endmntent);
+ g_test_add_func("/cleanup/closedir", test_cleanup_closedir);
+ g_test_add_func("/cleanup/close", test_cleanup_close);
}
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/error.c 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/error.c
--- 2.37.4-1/cmd/libsnap-confine-private/error.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/error.c 2019-06-05 06:41:21.000000000 +0000
@@ -26,20 +26,10 @@
#include
#include
-struct sc_error {
- // Error domain defines a scope for particular error codes.
- const char *domain;
- // Code differentiates particular errors for the programmer.
- // The code may be zero if the particular meaning is not relevant.
- int code;
- // Message carries a formatted description of the problem.
- char *msg;
-};
-
-static struct sc_error *sc_error_initv(const char *domain, int code,
- const char *msgfmt, va_list ap)
+static sc_error *sc_error_initv(const char *domain, int code,
+ const char *msgfmt, va_list ap)
{
- struct sc_error *err = calloc(1, sizeof *err);
+ sc_error *err = calloc(1, sizeof *err);
if (err == NULL) {
die("cannot allocate memory for error object");
}
@@ -51,28 +41,25 @@ static struct sc_error *sc_error_initv(c
return err;
}
-struct sc_error *sc_error_init(const char *domain, int code, const char *msgfmt,
- ...)
+sc_error *sc_error_init(const char *domain, int code, const char *msgfmt, ...)
{
va_list ap;
va_start(ap, msgfmt);
- struct sc_error *err = sc_error_initv(domain, code, msgfmt, ap);
+ sc_error *err = sc_error_initv(domain, code, msgfmt, ap);
va_end(ap);
return err;
}
-struct sc_error *sc_error_init_from_errno(int errno_copy, const char *msgfmt,
- ...)
+sc_error *sc_error_init_from_errno(int errno_copy, const char *msgfmt, ...)
{
va_list ap;
va_start(ap, msgfmt);
- struct sc_error *err =
- sc_error_initv(SC_ERRNO_DOMAIN, errno_copy, msgfmt, ap);
+ sc_error *err = sc_error_initv(SC_ERRNO_DOMAIN, errno_copy, msgfmt, ap);
va_end(ap);
return err;
}
-const char *sc_error_domain(struct sc_error *err)
+const char *sc_error_domain(sc_error * err)
{
if (err == NULL) {
die("cannot obtain error domain from NULL error");
@@ -80,7 +67,7 @@ const char *sc_error_domain(struct sc_er
return err->domain;
}
-int sc_error_code(struct sc_error *err)
+int sc_error_code(sc_error * err)
{
if (err == NULL) {
die("cannot obtain error code from NULL error");
@@ -88,7 +75,7 @@ int sc_error_code(struct sc_error *err)
return err->code;
}
-const char *sc_error_msg(struct sc_error *err)
+const char *sc_error_msg(sc_error * err)
{
if (err == NULL) {
die("cannot obtain error message from NULL error");
@@ -96,7 +83,7 @@ const char *sc_error_msg(struct sc_error
return err->msg;
}
-void sc_error_free(struct sc_error *err)
+void sc_error_free(sc_error * err)
{
if (err != NULL) {
free(err->msg);
@@ -105,13 +92,13 @@ void sc_error_free(struct sc_error *err)
}
}
-void sc_cleanup_error(struct sc_error **ptr)
+void sc_cleanup_error(sc_error ** ptr)
{
sc_error_free(*ptr);
*ptr = NULL;
}
-void sc_die_on_error(struct sc_error *error)
+void sc_die_on_error(sc_error * error)
{
if (error != NULL) {
if (strcmp(sc_error_domain(error), SC_ERRNO_DOMAIN) == 0) {
@@ -125,7 +112,7 @@ void sc_die_on_error(struct sc_error *er
}
}
-void sc_error_forward(struct sc_error **recipient, struct sc_error *error)
+void sc_error_forward(sc_error ** recipient, sc_error * error)
{
if (recipient != NULL) {
*recipient = error;
@@ -134,7 +121,7 @@ void sc_error_forward(struct sc_error **
}
}
-bool sc_error_match(struct sc_error *error, const char *domain, int code)
+bool sc_error_match(sc_error * error, const char *domain, int code)
{
if (domain == NULL) {
die("cannot match error to a NULL domain");
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/error.h 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/error.h
--- 2.37.4-1/cmd/libsnap-confine-private/error.h 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/error.h 2019-06-05 06:41:21.000000000 +0000
@@ -43,9 +43,17 @@
**/
/**
- * Opaque error structure.
+ * Error structure.
**/
-struct sc_error;
+typedef struct sc_error {
+ // Error domain defines a scope for particular error codes.
+ const char *domain;
+ // Code differentiates particular errors for the programmer.
+ // The code may be zero if the particular meaning is not relevant.
+ int code;
+ // Message carries a formatted description of the problem.
+ char *msg;
+} sc_error;
/**
* Error domain for errors related to system errno.
@@ -62,10 +70,9 @@ struct sc_error;
*
* This function calls die() in case of memory allocation failure.
**/
-__attribute__ ((warn_unused_result,
- format(printf, 3, 4) SC_APPEND_RETURNS_NONNULL))
-struct sc_error *sc_error_init(const char *domain, int code, const char *msgfmt,
- ...);
+__attribute__((warn_unused_result,
+ format(printf, 3, 4) SC_APPEND_RETURNS_NONNULL))
+sc_error *sc_error_init(const char *domain, int code, const char *msgfmt, ...);
/**
* Initialize an errno-based error.
@@ -75,10 +82,10 @@ struct sc_error *sc_error_init(const cha
*
* This function calls die() in case of memory allocation failure.
**/
-__attribute__ ((warn_unused_result,
- format(printf, 2, 3) SC_APPEND_RETURNS_NONNULL))
-struct sc_error *sc_error_init_from_errno(int errno_copy, const char *msgfmt,
- ...);
+__attribute__((warn_unused_result,
+ format(printf, 2, 3) SC_APPEND_RETURNS_NONNULL))
+sc_error *sc_error_init_from_errno(int errno_copy, const char *msgfmt, ...);
+
/**
* Get the error domain out of an error object.
@@ -86,8 +93,8 @@ struct sc_error *sc_error_init_from_errn
* The error domain acts as a namespace for error codes.
* No change of ownership takes place.
**/
-__attribute__ ((warn_unused_result SC_APPEND_RETURNS_NONNULL))
-const char *sc_error_domain(struct sc_error *err);
+__attribute__((warn_unused_result SC_APPEND_RETURNS_NONNULL))
+const char *sc_error_domain(sc_error * err);
/**
* Get the error code out of an error object.
@@ -99,8 +106,8 @@ const char *sc_error_domain(struct sc_er
* can rely on programmatically. This can be used to return an error message
* without having to allocate a distinct code for each one.
**/
-__attribute__ ((warn_unused_result))
-int sc_error_code(struct sc_error *err);
+__attribute__((warn_unused_result))
+int sc_error_code(sc_error * err);
/**
* Get the error message out of an error object.
@@ -108,15 +115,15 @@ int sc_error_code(struct sc_error *err);
* The error message is bound to the life-cycle of the error object.
* No change of ownership takes place.
**/
-__attribute__ ((warn_unused_result SC_APPEND_RETURNS_NONNULL))
-const char *sc_error_msg(struct sc_error *err);
+__attribute__((warn_unused_result SC_APPEND_RETURNS_NONNULL))
+const char *sc_error_msg(sc_error * err);
/**
* Free an error object.
*
* The error object can be NULL.
**/
-void sc_error_free(struct sc_error *error);
+void sc_error_free(sc_error * error);
/**
* Cleanup an error with sc_error_free()
@@ -124,8 +131,8 @@ void sc_error_free(struct sc_error *erro
* This function is designed to be used with
* __attribute__((cleanup(sc_cleanup_error))).
**/
-__attribute__ ((nonnull))
-void sc_cleanup_error(struct sc_error **ptr);
+__attribute__((nonnull))
+void sc_cleanup_error(sc_error ** ptr);
/**
*
@@ -136,7 +143,7 @@ void sc_cleanup_error(struct sc_error **
* The error message is derived from the data in the error, using the special
* errno domain to provide additional information if that is available.
**/
-void sc_die_on_error(struct sc_error *error);
+void sc_die_on_error(sc_error * error);
/**
* Forward an error to the caller.
@@ -149,7 +156,7 @@ void sc_die_on_error(struct sc_error *er
**/
// NOTE: There's no nonnull(1) attribute as the recipient *can* be NULL. With
// the attribute in place GCC optimizes some things out and tests fail.
-void sc_error_forward(struct sc_error **recipient, struct sc_error *error);
+void sc_error_forward(sc_error ** recipient, sc_error * error);
/**
* Check if a given error matches the specified domain and code.
@@ -157,7 +164,7 @@ void sc_error_forward(struct sc_error **
* It is okay to match a NULL error, the function simply returns false in that
* case. The domain cannot be NULL though.
**/
-__attribute__ ((warn_unused_result))
-bool sc_error_match(struct sc_error *error, const char *domain, int code);
+__attribute__((warn_unused_result))
+bool sc_error_match(sc_error * error, const char *domain, int code);
#endif
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/error-test.c 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/error-test.c
--- 2.37.4-1/cmd/libsnap-confine-private/error-test.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/error-test.c 2019-06-05 06:41:21.000000000 +0000
@@ -227,7 +227,7 @@ static void test_sc_error_match__NULL_do
g_test_trap_assert_stderr("cannot match error to a NULL domain\n");
}
-static void __attribute__ ((constructor)) init(void)
+static void __attribute__((constructor)) init(void)
{
g_test_add_func("/error/sc_error_init", test_sc_error_init);
g_test_add_func("/error/sc_error_init_from_errno",
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/fault-injection.h 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/fault-injection.h
--- 2.37.4-1/cmd/libsnap-confine-private/fault-injection.h 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/fault-injection.h 2019-06-05 06:41:21.000000000 +0000
@@ -37,7 +37,7 @@ bool sc_faulty(const char *name, void *p
struct sc_fault_state;
-typedef bool(*sc_fault_fn) (struct sc_fault_state * state, void *ptr);
+typedef bool (*sc_fault_fn)(struct sc_fault_state * state, void *ptr);
struct sc_fault_state {
int ncalls;
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/fault-injection-test.c 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/fault-injection-test.c
--- 2.37.4-1/cmd/libsnap-confine-private/fault-injection-test.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/fault-injection-test.c 2019-06-05 06:41:21.000000000 +0000
@@ -57,7 +57,7 @@ static void test_fault_injection(void)
sc_reset_faults();
}
-static void __attribute__ ((constructor)) init(void)
+static void __attribute__((constructor)) init(void)
{
g_test_add_func("/fault-injection", test_fault_injection);
}
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/feature.c 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/feature.c
--- 2.37.4-1/cmd/libsnap-confine-private/feature.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/feature.c 2019-06-05 06:41:21.000000000 +0000
@@ -34,15 +34,20 @@ bool sc_feature_enabled(sc_feature_flag
{
const char *file_name;
switch (flag) {
- case SC_PER_USER_MOUNT_NAMESPACE:
+ case SC_FEATURE_PER_USER_MOUNT_NAMESPACE:
file_name = "per-user-mount-namespace";
break;
+ case SC_FEATURE_REFRESH_APP_AWARENESS:
+ file_name = "refresh-app-awareness";
+ break;
default:
die("unknown feature flag code %d", flag);
}
int dirfd SC_CLEANUP(sc_cleanup_close) = -1;
- dirfd = open(feature_flag_dir, O_CLOEXEC | O_DIRECTORY | O_NOFOLLOW | O_PATH);
+ dirfd =
+ open(feature_flag_dir,
+ O_CLOEXEC | O_DIRECTORY | O_NOFOLLOW | O_PATH);
if (dirfd < 0 && errno == ENOENT) {
return false;
}
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/feature.h 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/feature.h
--- 2.37.4-1/cmd/libsnap-confine-private/feature.h 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/feature.h 2019-06-05 06:41:21.000000000 +0000
@@ -21,7 +21,8 @@
#include
typedef enum sc_feature_flag {
- SC_PER_USER_MOUNT_NAMESPACE,
+ SC_FEATURE_PER_USER_MOUNT_NAMESPACE,
+ SC_FEATURE_REFRESH_APP_AWARENESS,
} sc_feature_flag;
/**
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/feature-test.c 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/feature-test.c
--- 2.37.4-1/cmd/libsnap-confine-private/feature-test.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/feature-test.c 2019-06-05 06:41:21.000000000 +0000
@@ -54,14 +54,14 @@ static void test_feature_enabled__missin
char subd[PATH_MAX];
sc_must_snprintf(subd, sizeof subd, "%s/absent", d);
sc_mock_feature_flag_dir(subd);
- g_assert(!sc_feature_enabled(SC_PER_USER_MOUNT_NAMESPACE));
+ g_assert(!sc_feature_enabled(SC_FEATURE_PER_USER_MOUNT_NAMESPACE));
}
static void test_feature_enabled__missing_file(void)
{
const char *d = sc_testdir();
sc_mock_feature_flag_dir(d);
- g_assert(!sc_feature_enabled(SC_PER_USER_MOUNT_NAMESPACE));
+ g_assert(!sc_feature_enabled(SC_FEATURE_PER_USER_MOUNT_NAMESPACE));
}
static void test_feature_enabled__present_file(void)
@@ -72,10 +72,10 @@ static void test_feature_enabled__presen
sc_must_snprintf(pname, sizeof pname, "%s/per-user-mount-namespace", d);
g_file_set_contents(pname, "", -1, NULL);
- g_assert(sc_feature_enabled(SC_PER_USER_MOUNT_NAMESPACE));
+ g_assert(sc_feature_enabled(SC_FEATURE_PER_USER_MOUNT_NAMESPACE));
}
-static void __attribute__ ((constructor)) init(void)
+static void __attribute__((constructor)) init(void)
{
g_test_add_func("/feature/missing_dir",
test_feature_enabled__missing_dir);
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/locking-test.c 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/locking-test.c
--- 2.37.4-1/cmd/libsnap-confine-private/locking-test.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/locking-test.c 2019-06-05 06:41:21.000000000 +0000
@@ -128,10 +128,11 @@ static void test_sc_enable_sanity_timeou
g_test_trap_subprocess(NULL, 1 * G_USEC_PER_SEC,
G_TEST_SUBPROCESS_INHERIT_STDERR);
g_test_trap_assert_failed();
- g_test_trap_assert_stderr ("sanity timeout expired: Interrupted system call\n");
+ g_test_trap_assert_stderr
+ ("sanity timeout expired: Interrupted system call\n");
}
-static void __attribute__ ((constructor)) init(void)
+static void __attribute__((constructor)) init(void)
{
g_test_add_func("/locking/sc_lock_unlock", test_sc_lock_unlock);
g_test_add_func("/locking/sc_enable_sanity_timeout",
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/mountinfo.c 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/mountinfo.c
--- 2.37.4-1/cmd/libsnap-confine-private/mountinfo.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/mountinfo.c 2019-06-05 06:41:21.000000000 +0000
@@ -17,6 +17,7 @@
#include "mountinfo.h"
#include
+#include
#include
#include
#include
@@ -43,35 +44,34 @@
* (10) mount source: filesystem specific information or "none"
* (11) super options: per super block options
**/
-static struct sc_mountinfo_entry *sc_parse_mountinfo_entry(const char *line)
- __attribute__ ((nonnull(1)));
+static sc_mountinfo_entry *sc_parse_mountinfo_entry(const char *line)
+ __attribute__((nonnull(1)));
/**
* Free a sc_mountinfo structure and all its entries.
**/
-static void sc_free_mountinfo(struct sc_mountinfo *info)
- __attribute__ ((nonnull(1)));
+static void sc_free_mountinfo(sc_mountinfo * info)
+ __attribute__((nonnull(1)));
/**
* Free a sc_mountinfo entry.
**/
-static void sc_free_mountinfo_entry(struct sc_mountinfo_entry *entry)
- __attribute__ ((nonnull(1)));
+static void sc_free_mountinfo_entry(sc_mountinfo_entry * entry)
+ __attribute__((nonnull(1)));
-struct sc_mountinfo_entry *sc_first_mountinfo_entry(struct sc_mountinfo *info)
+sc_mountinfo_entry *sc_first_mountinfo_entry(sc_mountinfo * info)
{
return info->first;
}
-struct sc_mountinfo_entry *sc_next_mountinfo_entry(struct sc_mountinfo_entry
- *entry)
+sc_mountinfo_entry *sc_next_mountinfo_entry(sc_mountinfo_entry * entry)
{
return entry->next;
}
-struct sc_mountinfo *sc_parse_mountinfo(const char *fname)
+sc_mountinfo *sc_parse_mountinfo(const char *fname)
{
- struct sc_mountinfo *info = calloc(1, sizeof *info);
+ sc_mountinfo *info = calloc(1, sizeof *info);
if (info == NULL) {
return NULL;
}
@@ -86,7 +86,7 @@ struct sc_mountinfo *sc_parse_mountinfo(
}
char *line SC_CLEANUP(sc_cleanup_string) = NULL;
size_t line_size = 0;
- struct sc_mountinfo_entry *entry, *last = NULL;
+ sc_mountinfo_entry *entry, *last = NULL;
for (;;) {
errno = 0;
if (getline(&line, &line_size, f) == -1) {
@@ -112,7 +112,7 @@ struct sc_mountinfo *sc_parse_mountinfo(
}
static void show_buffers(const char *line, int offset,
- struct sc_mountinfo_entry *entry)
+ sc_mountinfo_entry * entry)
{
#ifdef MOUNTINFO_DEBUG
fprintf(stderr, "Input buffer (first), with offset arrow\n");
@@ -127,7 +127,7 @@ static void show_buffers(const char *lin
fprintf(stderr, ">%s<\n", line);
fputc('>', stderr);
- for (int i = 0; i < strlen(line); ++i) {
+ for (size_t i = 0; i < strlen(line); ++i) {
int c = entry->line_buf[i];
fputc(c == 0 ? '@' : c == 1 ? '#' : c, stderr);
}
@@ -135,37 +135,98 @@ static void show_buffers(const char *lin
fputc('\n', stderr);
fputc('>', stderr);
- for (int i = 0; i < strlen(line); ++i)
+ for (size_t i = 0; i < strlen(line); ++i)
fputc('=', stderr);
fputc('<', stderr);
fputc('\n', stderr);
#endif // MOUNTINFO_DEBUG
}
-static char *parse_next_string_field(struct sc_mountinfo_entry *entry,
- const char *line, int *offset)
+static bool is_octal_digit(char c)
{
- int offset_delta = 0;
- char *field = &entry->line_buf[0] + *offset;
- if (line[*offset] == ' ') {
- // Special case for empty fields which cannot be parsed with %s.
- *field = '\0';
- *offset += 1;
- } else {
- int nscanned =
- sscanf(line + *offset, "%s%n", field, &offset_delta);
- if (nscanned != 1)
- return NULL;
- *offset += offset_delta;
- if (line[*offset] == ' ') {
- *offset += 1;
+ return c >= '0' && c <= '7';
+}
+
+static char *parse_next_string_field(sc_mountinfo_entry * entry,
+ const char *line, size_t * offset)
+{
+ const char *input = &line[*offset];
+ char *output = &entry->line_buf[*offset];
+ size_t input_idx = 0; // reading index
+ size_t output_idx = 0; // writing index
+
+ // Scan characters until we run out of memory to scan or we find a
+ // space. The kernel uses simple octal escape sequences for the
+ // following: space, tab, newline, backwards slash. Everything else is
+ // copied verbatim.
+ for (;;) {
+ int c = input[input_idx];
+ if (c == '\0') {
+ // The string is over before we see anything then
+ // return NULL. This is an indication of end-of-input
+ // to the caller.
+ if (output_idx == 0) {
+ return NULL;
+ }
+ // The scanned line is NUL terminated. This ensures that the
+ // terminator is copied to the output buffer.
+ output[output_idx] = '\0';
+ // NOTE: we must not advance the reading index since we
+ // reached the end of the buffer.
+ break;
+ } else if (c == ' ') {
+ // Fields are space delimited or end-of-string terminated.
+ // Represent either as the end-of-string marker, skip over it,
+ // and stop parsing by terminating the output, then
+ // breaking out of the loop but advancing the reading
+ // index which is needed for subsequent calls.
+ output[output_idx] = '\0';
+ input_idx++;
+ break;
+ } else if (c == '\\') {
+ // Three *more* octal digits required for the escape
+ // sequence. For reference see mangle_path() in
+ // fs/seq_file.c. Note that is_octal_digit returns
+ // false on the string terminator character NUL and the
+ // short-circuiting behavior of && makes this check
+ // correct even if '\\' is the last character of the
+ // string.
+ const char *s = &input[input_idx];
+ if (is_octal_digit(s[1]) && is_octal_digit(s[2])
+ && is_octal_digit(s[3])) {
+ // Unescape the octal value encoded in s[1],
+ // s[2] and s[3]. Because we are working with
+ // byte values there are no issues related to
+ // byte order.
+ output[output_idx++] =
+ ((s[1] - '0') << 6) |
+ ((s[2] - '0') << 3) | ((s[3] - '0'));
+ // Advance the reading index by the length of the escape
+ // sequence.
+ input_idx += 4;
+ } else {
+ // Partial escape sequence, copy verbatim and
+ // continue (since we don't use this).
+ output[output_idx++] = c;
+ input_idx++;
+ }
+ } else {
+ // All other characters are simply copied verbatim.
+ output[output_idx++] = c;
+ input_idx++;
}
}
+ *offset += input_idx;
+#ifdef MOUNTINFO_DEBUG
+ fprintf(stderr,
+ "\nscanned: >%s< (%zd bytes), input idx: %zd, output idx: %zd\n",
+ output, strlen(output), input_idx, output_idx);
+#endif
show_buffers(line, *offset, entry);
- return field;
+ return output;
}
-static struct sc_mountinfo_entry *sc_parse_mountinfo_entry(const char *line)
+static sc_mountinfo_entry *sc_parse_mountinfo_entry(const char *line)
{
// NOTE: the sc_mountinfo structure is allocated along with enough extra
// storage to hold the whole line we are parsing. This is used as backing
@@ -189,8 +250,7 @@ static struct sc_mountinfo_entry *sc_par
//
// If MOUNTINFO_DEBUG is defined then extra debugging is printed to stderr
// and this allows for visual analysis of what is going on.
- struct sc_mountinfo_entry *entry =
- calloc(1, sizeof *entry + strlen(line) + 1);
+ sc_mountinfo_entry *entry = calloc(1, sizeof *entry + strlen(line) + 1);
if (entry == NULL) {
return NULL;
}
@@ -199,14 +259,15 @@ static struct sc_mountinfo_entry *sc_par
// by show_buffers() below. This is "unaltered" memory.
memset(entry->line_buf, 1, strlen(line));
#endif // MOUNTINFO_DEBUG
- int nscanned;
- int offset_delta, offset = 0;
+ int nscanned, initial_offset = 0;
+ size_t offset = 0;
nscanned = sscanf(line, "%d %d %u:%u %n",
&entry->mount_id, &entry->parent_id,
- &entry->dev_major, &entry->dev_minor, &offset_delta);
+ &entry->dev_major, &entry->dev_minor,
+ &initial_offset);
if (nscanned != 4)
goto fail;
- offset += offset_delta;
+ offset += initial_offset;
show_buffers(line, offset, entry);
@@ -243,14 +304,13 @@ static struct sc_mountinfo_entry *sc_par
if ((entry->super_opts =
parse_next_string_field(entry, line, &offset)) == NULL)
goto fail;
- show_buffers(line, offset, entry);
return entry;
fail:
free(entry);
return NULL;
}
-void sc_cleanup_mountinfo(struct sc_mountinfo **ptr)
+void sc_cleanup_mountinfo(sc_mountinfo ** ptr)
{
if (*ptr != NULL) {
sc_free_mountinfo(*ptr);
@@ -258,9 +318,9 @@ void sc_cleanup_mountinfo(struct sc_moun
}
}
-static void sc_free_mountinfo(struct sc_mountinfo *info)
+static void sc_free_mountinfo(sc_mountinfo * info)
{
- struct sc_mountinfo_entry *entry, *next;
+ sc_mountinfo_entry *entry, *next;
for (entry = info->first; entry != NULL; entry = next) {
next = entry->next;
sc_free_mountinfo_entry(entry);
@@ -268,7 +328,7 @@ static void sc_free_mountinfo(struct sc_
free(info);
}
-static void sc_free_mountinfo_entry(struct sc_mountinfo_entry *entry)
+static void sc_free_mountinfo_entry(sc_mountinfo_entry * entry)
{
free(entry);
}
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/mountinfo.h 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/mountinfo.h
--- 2.37.4-1/cmd/libsnap-confine-private/mountinfo.h 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/mountinfo.h 2019-06-05 06:41:21.000000000 +0000
@@ -18,16 +18,9 @@
#define SNAP_CONFINE_MOUNTINFO_H
/**
- * Structure describing entire /proc/self/sc_mountinfo file
- **/
-struct sc_mountinfo {
- struct sc_mountinfo_entry *first;
-};
-
-/**
* Structure describing a single entry in /proc/self/sc_mountinfo
**/
-struct sc_mountinfo_entry {
+typedef struct sc_mountinfo_entry {
/**
* The mount identifier of a given mount entry.
**/
@@ -91,7 +84,14 @@ struct sc_mountinfo_entry {
// along with the structure itself and does not need to be freed
// separately.
char line_buf[0];
-};
+} sc_mountinfo_entry;
+
+/**
+ * Structure describing entire /proc/self/sc_mountinfo file
+ **/
+typedef struct sc_mountinfo {
+ sc_mountinfo_entry *first;
+} sc_mountinfo;
/**
* Parse a file in according to sc_mountinfo syntax.
@@ -100,7 +100,7 @@ struct sc_mountinfo_entry {
* implicitly parse /proc/self/sc_mountinfo, that is the mount information
* associated with the current process.
**/
-struct sc_mountinfo *sc_parse_mountinfo(const char *fname);
+sc_mountinfo *sc_parse_mountinfo(const char *fname);
/**
* Free a sc_mountinfo structure.
@@ -108,8 +108,8 @@ struct sc_mountinfo *sc_parse_mountinfo(
* This function is designed to be used with __attribute__((cleanup)) so it
* takes a pointer to the freed object (which is also a pointer).
**/
-void sc_cleanup_mountinfo(struct sc_mountinfo **ptr)
- __attribute__ ((nonnull(1)));
+void sc_cleanup_mountinfo(sc_mountinfo ** ptr)
+ __attribute__((nonnull(1)));
/**
* Get the first sc_mountinfo entry.
@@ -118,8 +118,8 @@ void sc_cleanup_mountinfo(struct sc_moun
* returned value is bound to the lifecycle of the whole sc_mountinfo structure
* and should not be freed explicitly.
**/
-struct sc_mountinfo_entry *sc_first_mountinfo_entry(struct sc_mountinfo *info)
- __attribute__ ((nonnull(1)));
+sc_mountinfo_entry *sc_first_mountinfo_entry(sc_mountinfo * info)
+ __attribute__((nonnull(1)));
/**
* Get the next sc_mountinfo entry.
@@ -128,8 +128,7 @@ struct sc_mountinfo_entry *sc_first_moun
* was the last entry. The returned value is bound to the lifecycle of the
* whole sc_mountinfo structure and should not be freed explicitly.
**/
-struct sc_mountinfo_entry *sc_next_mountinfo_entry(struct sc_mountinfo_entry
- *entry)
- __attribute__ ((nonnull(1)));
+sc_mountinfo_entry *sc_next_mountinfo_entry(sc_mountinfo_entry * entry)
+ __attribute__((nonnull(1)));
#endif
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/mountinfo-test.c 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/mountinfo-test.c
--- 2.37.4-1/cmd/libsnap-confine-private/mountinfo-test.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/mountinfo-test.c 2019-06-05 06:41:21.000000000 +0000
@@ -24,7 +24,7 @@ static void test_parse_mountinfo_entry__
{
const char *line =
"19 25 0:18 / /sys rw,nosuid,nodev,noexec,relatime shared:7 - sysfs sysfs rw";
- struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
+ sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
g_assert_nonnull(entry);
g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry);
g_assert_cmpint(entry->mount_id, ==, 19);
@@ -48,7 +48,7 @@ static void test_parse_mountinfo_entry__
{
const char *line =
"104 23 0:19 /snapd/ns /run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=99840k,mode=755";
- struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
+ sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
g_assert_nonnull(entry);
g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry);
g_assert_cmpint(entry->mount_id, ==, 104);
@@ -69,7 +69,7 @@ static void test_parse_mountinfo_entry__
{
const char *line =
"256 104 0:3 mnt:[4026532509] /run/snapd/ns/hello-world.mnt rw - nsfs nsfs rw";
- struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
+ sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
g_assert_nonnull(entry);
g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry);
g_assert_cmpint(entry->mount_id, ==, 256);
@@ -89,7 +89,7 @@ static void test_parse_mountinfo_entry__
static void test_parse_mountinfo_entry__garbage(void)
{
const char *line = "256 104 0:3";
- struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
+ sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
g_assert_null(entry);
}
@@ -97,7 +97,7 @@ static void test_parse_mountinfo_entry__
{
const char *line =
"1 2 3:4 root mount-dir mount-opts - fs-type mount-source super-opts";
- struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
+ sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
g_assert_nonnull(entry);
g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry);
g_assert_cmpint(entry->mount_id, ==, 1);
@@ -118,7 +118,7 @@ static void test_parse_mountinfo_entry__
{
const char *line =
"1 2 3:4 root mount-dir mount-opts tag:1 - fs-type mount-source super-opts";
- struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
+ sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
g_assert_nonnull(entry);
g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry);
g_assert_cmpint(entry->mount_id, ==, 1);
@@ -139,7 +139,7 @@ static void test_parse_mountinfo_entry__
{
const char *line =
"1 2 3:4 root mount-dir mount-opts tag:1 tag:2 tag:3 tag:4 - fs-type mount-source super-opts";
- struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
+ sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
g_assert_nonnull(entry);
g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry);
g_assert_cmpint(entry->mount_id, ==, 1);
@@ -160,7 +160,7 @@ static void test_parse_mountinfo_entry__
{
const char *line =
"304 301 0:45 / /snap/test-snapd-content-advanced-plug/x1 rw,relatime - tmpfs rw";
- struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
+ sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
g_assert_nonnull(entry);
g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry);
g_assert_cmpint(entry->mount_id, ==, 304);
@@ -178,7 +178,85 @@ static void test_parse_mountinfo_entry__
g_assert_null(entry->next);
}
-static void __attribute__ ((constructor)) init(void)
+static void test_parse_mountinfo_entry__octal_escaping(void)
+{
+ const char *line;
+ struct sc_mountinfo_entry *entry;
+
+ // The kernel escapes spaces as \040
+ line = "2 1 0:54 / /tmp rw - tmpfs tricky\\040path rw";
+ entry = sc_parse_mountinfo_entry(line);
+ g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry);
+ g_assert_nonnull(entry);
+ g_assert_cmpstr(entry->mount_source, ==, "tricky path");
+
+ // kernel escapes newlines as \012
+ line = "2 1 0:54 / /tmp rw - tmpfs tricky\\012path rw";
+ entry = sc_parse_mountinfo_entry(line);
+ g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry);
+ g_assert_nonnull(entry);
+ g_assert_cmpstr(entry->mount_source, ==, "tricky\npath");
+
+ // kernel escapes tabs as \011
+ line = "2 1 0:54 / /tmp rw - tmpfs tricky\\011path rw";
+ entry = sc_parse_mountinfo_entry(line);
+ g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry);
+ g_assert_nonnull(entry);
+ g_assert_cmpstr(entry->mount_source, ==, "tricky\tpath");
+
+ // kernel escapes forward slashes as \057
+ line = "2 1 0:54 / /tmp rw - tmpfs tricky\\057path rw";
+ entry = sc_parse_mountinfo_entry(line);
+ g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry);
+ g_assert_nonnull(entry);
+ g_assert_cmpstr(entry->mount_source, ==, "tricky/path");
+}
+
+static void test_parse_mountinfo_entry__broken_octal_escaping(void)
+{
+ // Invalid octal escape sequences are left intact.
+ const char *line =
+ "2074 27 0:54 / /tmp/strange-dir rw,relatime shared:1039 - tmpfs no\\888thing rw\\";
+ struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
+ g_assert_nonnull(entry);
+ g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry);
+ g_assert_cmpint(entry->mount_id, ==, 2074);
+ g_assert_cmpint(entry->parent_id, ==, 27);
+ g_assert_cmpint(entry->dev_major, ==, 0);
+ g_assert_cmpint(entry->dev_minor, ==, 54);
+ g_assert_cmpstr(entry->root, ==, "/");
+ g_assert_cmpstr(entry->mount_dir, ==, "/tmp/strange-dir");
+ g_assert_cmpstr(entry->mount_opts, ==, "rw,relatime");
+ g_assert_cmpstr(entry->optional_fields, ==, "shared:1039");
+ g_assert_cmpstr(entry->fs_type, ==, "tmpfs");
+ g_assert_cmpstr(entry->mount_source, ==, "no\\888thing");
+ g_assert_cmpstr(entry->super_opts, ==, "rw\\");
+ g_assert_null(entry->next);
+}
+
+static void test_parse_mountinfo_entry__unescaped_whitespace(void)
+{
+ // The kernel does not escape '\r'
+ const char *line =
+ "2074 27 0:54 / /tmp/strange\rdir rw,relatime shared:1039 - tmpfs tmpfs rw";
+ struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
+ g_assert_nonnull(entry);
+ g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry);
+ g_assert_cmpint(entry->mount_id, ==, 2074);
+ g_assert_cmpint(entry->parent_id, ==, 27);
+ g_assert_cmpint(entry->dev_major, ==, 0);
+ g_assert_cmpint(entry->dev_minor, ==, 54);
+ g_assert_cmpstr(entry->root, ==, "/");
+ g_assert_cmpstr(entry->mount_dir, ==, "/tmp/strange\rdir");
+ g_assert_cmpstr(entry->mount_opts, ==, "rw,relatime");
+ g_assert_cmpstr(entry->optional_fields, ==, "shared:1039");
+ g_assert_cmpstr(entry->fs_type, ==, "tmpfs");
+ g_assert_cmpstr(entry->mount_source, ==, "tmpfs");
+ g_assert_cmpstr(entry->super_opts, ==, "rw");
+ g_assert_null(entry->next);
+}
+
+static void __attribute__((constructor)) init(void)
{
g_test_add_func("/mountinfo/parse_mountinfo_entry/sysfs",
test_parse_mountinfo_entry__sysfs);
@@ -197,4 +275,11 @@ static void __attribute__ ((constructor)
g_test_add_func
("/mountinfo/parse_mountinfo_entry/empty_source",
test_parse_mountinfo_entry__empty_source);
+ g_test_add_func("/mountinfo/parse_mountinfo_entry/octal_escaping",
+ test_parse_mountinfo_entry__octal_escaping);
+ g_test_add_func
+ ("/mountinfo/parse_mountinfo_entry/broken_octal_escaping",
+ test_parse_mountinfo_entry__broken_octal_escaping);
+ g_test_add_func("/mountinfo/parse_mountinfo_entry/unescaped_whitespace",
+ test_parse_mountinfo_entry__unescaped_whitespace);
}
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/mount-opt-test.c 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/mount-opt-test.c
--- 2.37.4-1/cmd/libsnap-confine-private/mount-opt-test.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/mount-opt-test.c 2019-06-05 06:41:21.000000000 +0000
@@ -319,7 +319,7 @@ static void test_sc_do_optional_mount_fa
}
}
-static void __attribute__ ((constructor)) init(void)
+static void __attribute__((constructor)) init(void)
{
g_test_add_func("/mount/sc_mount_opt2str", test_sc_mount_opt2str);
g_test_add_func("/mount/sc_mount_cmd", test_sc_mount_cmd);
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/privs-test.c 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/privs-test.c
--- 2.37.4-1/cmd/libsnap-confine-private/privs-test.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/privs-test.c 2019-06-05 06:41:21.000000000 +0000
@@ -61,7 +61,7 @@ static void test_sc_privs_drop(void)
g_test_trap_assert_passed();
}
-static void __attribute__ ((constructor)) init(void)
+static void __attribute__((constructor)) init(void)
{
g_test_add_func("/privs/sc_privs_drop", test_sc_privs_drop);
}
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/secure-getenv.h 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/secure-getenv.h
--- 2.37.4-1/cmd/libsnap-confine-private/secure-getenv.h 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/secure-getenv.h 2019-06-05 06:41:21.000000000 +0000
@@ -30,7 +30,7 @@
* only used when glibc is not available.
**/
char *secure_getenv(const char *name)
- __attribute__ ((nonnull(1), warn_unused_result));
+ __attribute__((nonnull(1), warn_unused_result));
#endif // ! HAVE_SECURE_GETENV
#endif
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/snap.c 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/snap.c
--- 2.37.4-1/cmd/libsnap-confine-private/snap.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/snap.c 2019-06-05 06:41:21.000000000 +0000
@@ -99,11 +99,11 @@ static int skip_one_char(const char **p,
}
void sc_instance_name_validate(const char *instance_name,
- struct sc_error **errorp)
+ sc_error **errorp)
{
// NOTE: This function should be synchronized with the two other
// implementations: validate_instance_name and snap.ValidateInstanceName.
- struct sc_error *err = NULL;
+ sc_error *err = NULL;
// Ensure that name is not NULL
if (instance_name == NULL) {
@@ -143,11 +143,11 @@ void sc_instance_name_validate(const cha
}
void sc_instance_key_validate(const char *instance_key,
- struct sc_error **errorp)
+ sc_error **errorp)
{
// NOTE: see snap.ValidateInstanceName for reference of a valid instance key
// format
- struct sc_error *err = NULL;
+ sc_error *err = NULL;
// Ensure that name is not NULL
if (instance_key == NULL) {
@@ -186,11 +186,11 @@ void sc_instance_key_validate(const char
sc_error_forward(errorp, err);
}
-void sc_snap_name_validate(const char *snap_name, struct sc_error **errorp)
+void sc_snap_name_validate(const char *snap_name, sc_error **errorp)
{
// NOTE: This function should be synchronized with the two other
// implementations: validate_snap_name and snap.ValidateName.
- struct sc_error *err = NULL;
+ sc_error *err = NULL;
// Ensure that name is not NULL
if (snap_name == NULL) {
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/snap-test.c 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/snap-test.c
--- 2.37.4-1/cmd/libsnap-confine-private/snap-test.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/snap-test.c 2019-06-05 06:41:21.000000000 +0000
@@ -111,13 +111,13 @@ static void test_sc_is_hook_security_tag
static void test_sc_snap_or_instance_name_validate(gconstpointer data)
{
- typedef void (*validate_func_t) (const char *, struct sc_error **);
+ typedef void (*validate_func_t)(const char *, sc_error **);
validate_func_t validate = (validate_func_t) data;
bool is_instance =
(validate == sc_instance_name_validate) ? true : false;
- struct sc_error *err = NULL;
+ sc_error *err = NULL;
// Smoke test, a valid snap name
validate("hello-world", &err);
@@ -267,7 +267,7 @@ static void test_sc_snap_name_validate__
static void test_sc_instance_name_validate(void)
{
- struct sc_error *err = NULL;
+ sc_error *err = NULL;
sc_instance_name_validate("hello-world", &err);
g_assert_null(err);
@@ -530,7 +530,7 @@ static void test_sc_snap_split_instance_
g_assert_cmpstr(instance, ==, "");
}
-static void __attribute__ ((constructor)) init(void)
+static void __attribute__((constructor)) init(void)
{
g_test_add_func("/snap/verify_security_tag", test_verify_security_tag);
g_test_add_func("/snap/sc_is_hook_security_tag",
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/string-utils.c 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/string-utils.c
--- 2.37.4-1/cmd/libsnap-confine-private/string-utils.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/string-utils.c 2019-06-05 06:41:21.000000000 +0000
@@ -81,7 +81,7 @@ int sc_must_snprintf(char *str, size_t s
n = vsnprintf(str, size, format, va);
va_end(va);
- if (n < 0 || (size_t) n >= size)
+ if (n < 0 || (size_t)n >= size)
die("cannot format string: %s", str);
return n;
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/string-utils.h 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/string-utils.h
--- 2.37.4-1/cmd/libsnap-confine-private/string-utils.h 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/string-utils.h 2019-06-05 06:41:21.000000000 +0000
@@ -41,7 +41,7 @@ char *sc_strdup(const char *str);
*
* This version dies on any error condition.
**/
-__attribute__ ((format(printf, 3, 4)))
+__attribute__((format(printf, 3, 4)))
int sc_must_snprintf(char *str, size_t size, const char *format, ...);
/**
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/string-utils-test.c 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/string-utils-test.c
--- 2.37.4-1/cmd/libsnap-confine-private/string-utils-test.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/string-utils-test.c 2019-06-05 06:41:21.000000000 +0000
@@ -86,7 +86,8 @@ static void test_sc_string_append(void)
};
} data = {
.buf = {
- 'f', '\0', 0xFF, 0xFF},.canary1 = ~0,.canary2 = ~0,};
+ 'f', '\0', 0xFF, 0xFF},.canary1 = ~0,.canary2 = ~0,
+ };
// Sanity check, ensure that the layout of structures is as spelled above.
// (first canary1, then buf and finally canary2.
@@ -117,7 +118,8 @@ static void test_sc_string_append__empty
};
} data = {
.buf = {
- 'f', 'o', 'o', '\0'},.canary1 = ~0,.canary2 = ~0,};
+ 'f', 'o', 'o', '\0'},.canary1 = ~0,.canary2 = ~0,
+ };
// Sanity check, ensure that the layout of structures is as spelled above.
// (first canary1, then buf and finally canary2.
@@ -788,7 +790,7 @@ static void test_sc_strdup(void)
free(s);
}
-static void __attribute__ ((constructor)) init(void)
+static void __attribute__((constructor)) init(void)
{
g_test_add_func("/string-utils/sc_streq", test_sc_streq);
g_test_add_func("/string-utils/sc_endswith", test_sc_endswith);
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/test-utils-test.c 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/test-utils-test.c
--- 2.37.4-1/cmd/libsnap-confine-private/test-utils-test.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/test-utils-test.c 2019-06-05 06:41:21.000000000 +0000
@@ -39,7 +39,7 @@ static void test_rm_rf_tmp(void)
g_test_trap_assert_failed();
}
-static void __attribute__ ((constructor)) init(void)
+static void __attribute__((constructor)) init(void)
{
g_test_add_func("/test-utils/rm_rf_tmp", test_rm_rf_tmp);
}
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/tool.c 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/tool.c
--- 2.37.4-1/cmd/libsnap-confine-private/tool.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/tool.c 2019-06-05 06:41:21.000000000 +0000
@@ -105,7 +105,7 @@ void sc_call_snap_update_ns_as_user(int
snap_name);
const char *xdg_runtime_dir = getenv("XDG_RUNTIME_DIR");
- char xdg_runtime_dir_env[PATH_MAX+strlen("XDG_RUNTIME_DIR=")];
+ char xdg_runtime_dir_env[PATH_MAX + strlen("XDG_RUNTIME_DIR=")];
if (xdg_runtime_dir != NULL) {
sc_must_snprintf(xdg_runtime_dir_env,
sizeof(xdg_runtime_dir_env),
@@ -115,7 +115,7 @@ void sc_call_snap_update_ns_as_user(int
char *argv[] = {
"snap-update-ns",
/* This tells snap-update-ns we are calling from snap-confine and locking is in place */
- /* TODO: enable this in sync with snap-update-ns changes, "--from-snap-confine", */
+ "--from-snap-confine",
/* This tells snap-update-ns that we want to process the per-user profile */
"--user-mounts", snap_name_copy, NULL
};
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/utils.c 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/utils.c
--- 2.37.4-1/cmd/libsnap-confine-private/utils.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/utils.c 2019-06-05 06:41:21.000000000 +0000
@@ -74,7 +74,7 @@ static const struct sc_bool_name sc_bool
*
* If the text cannot be recognized, the default value is used.
**/
-static int parse_bool(const char *text, bool * value, bool default_value)
+static int parse_bool(const char *text, bool *value, bool default_value)
{
if (value == NULL) {
errno = EFAULT;
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/utils.h 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/utils.h
--- 2.37.4-1/cmd/libsnap-confine-private/utils.h 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/utils.h 2019-06-05 06:41:21.000000000 +0000
@@ -20,14 +20,14 @@
#include
#include
-__attribute__ ((noreturn))
- __attribute__ ((format(printf, 1, 2)))
+__attribute__((noreturn))
+ __attribute__((format(printf, 1, 2)))
void die(const char *fmt, ...);
-__attribute__ ((format(printf, 1, 2)))
+__attribute__((format(printf, 1, 2)))
bool error(const char *fmt, ...);
-__attribute__ ((format(printf, 1, 2)))
+__attribute__((format(printf, 1, 2)))
void debug(const char *fmt, ...);
/**
@@ -58,6 +58,6 @@ void write_string_to_file(const char *fi
*
* The function returns -1 in case of any error.
**/
-__attribute__ ((warn_unused_result))
+__attribute__((warn_unused_result))
int sc_nonfatal_mkpath(const char *const path, mode_t mode);
#endif
diff -pruN 2.37.4-1/cmd/libsnap-confine-private/utils-test.c 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/utils-test.c
--- 2.37.4-1/cmd/libsnap-confine-private/utils-test.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/libsnap-confine-private/utils-test.c 2019-06-05 06:41:21.000000000 +0000
@@ -191,7 +191,7 @@ static void test_sc_nonfatal_mkpath__abs
_test_sc_nonfatal_mkpath(dirname, subdirname);
}
-static void __attribute__ ((constructor)) init(void)
+static void __attribute__((constructor)) init(void)
{
g_test_add_func("/utils/parse_bool", test_parse_bool);
g_test_add_func("/utils/die", test_die);
diff -pruN 2.37.4-1/cmd/Makefile.am 2.39.2+19.10ubuntu1/cmd/Makefile.am
--- 2.37.4-1/cmd/Makefile.am 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/Makefile.am 2019-06-05 06:41:21.000000000 +0000
@@ -55,12 +55,22 @@ check-unit-tests:
endif
new_format = \
+ libsnap-confine-private/cgroup-pids-support.c \
+ libsnap-confine-private/cgroup-pids-support.h \
+ libsnap-confine-private/cgroup-support.c \
+ libsnap-confine-private/cgroup-support.h \
snap-confine/seccomp-support-ext.c \
snap-confine/seccomp-support-ext.h \
+ snap-confine/snap-confine-invocation.c \
+ snap-confine/snap-confine-invocation.h \
+ snap-confine/selinux-support.c \
+ snap-confine/selinux-support.h \
snap-discard-ns/snap-discard-ns.c
+
+# NOTE: clang-format is using project-wide .clang-format file.
.PHONY: fmt
fmt:: $(filter $(addprefix %,$(new_format)),$(foreach dir,$(subdirs),$(wildcard $(srcdir)/$(dir)/*.[ch])))
- clang-format -style='{BasedOnStyle: Google, IndentWidth: 4, ColumnLimit: 120}' -i $^
+ clang-format -i $^
fmt:: $(filter-out $(addprefix %,$(new_format)),$(foreach dir,$(subdirs),$(wildcard $(srcdir)/$(dir)/*.[ch])))
HOME=$(srcdir) indent $^
@@ -70,12 +80,13 @@ fmt:: $(filter-out $(addprefix %,$(new_f
.PHONY: hack
hack: snap-confine/snap-confine-debug snap-confine/snap-confine.apparmor snap-update-ns/snap-update-ns snap-seccomp/snap-seccomp snap-discard-ns/snap-discard-ns
sudo install -D -m 6755 snap-confine/snap-confine-debug $(DESTDIR)$(libexecdir)/snap-confine
- sudo install -m 644 snap-confine/snap-confine.apparmor $(DESTDIR)/etc/apparmor.d/$(patsubst .%,%,$(subst /,.,$(libexecdir))).snap-confine.real
+ if [ -d /etc/apparmor.d ]; then sudo install -m 644 snap-confine/snap-confine.apparmor $(DESTDIR)/etc/apparmor.d/$(patsubst .%,%,$(subst /,.,$(libexecdir))).snap-confine.real; fi
sudo install -d -m 755 $(DESTDIR)/var/lib/snapd/apparmor/snap-confine/
- sudo apparmor_parser -r snap-confine/snap-confine.apparmor
+ if [ "$$(command -v apparmor_parser)" != "" ]; then sudo apparmor_parser -r snap-confine/snap-confine.apparmor; fi
sudo install -m 755 snap-update-ns/snap-update-ns $(DESTDIR)$(libexecdir)/snap-update-ns
sudo install -m 755 snap-discard-ns/snap-discard-ns $(DESTDIR)$(libexecdir)/snap-discard-ns
sudo install -m 755 snap-seccomp/snap-seccomp $(DESTDIR)$(libexecdir)/snap-seccomp
+ if [ "$$(command -v restorecon)" != "" ]; then sudo restorecon -R -v $(DESTDIR)$(libexecdir)/; fi
# for the hack target also:
snap-update-ns/snap-update-ns: snap-update-ns/*.go snap-update-ns/*.[ch]
@@ -94,6 +105,10 @@ libsnap_confine_private_a_SOURCES = \
libsnap-confine-private/apparmor-support.h \
libsnap-confine-private/cgroup-freezer-support.c \
libsnap-confine-private/cgroup-freezer-support.h \
+ libsnap-confine-private/cgroup-pids-support.c \
+ libsnap-confine-private/cgroup-pids-support.h \
+ libsnap-confine-private/cgroup-support.c \
+ libsnap-confine-private/cgroup-support.h \
libsnap-confine-private/classic.c \
libsnap-confine-private/classic.h \
libsnap-confine-private/cleanup-funcs.c \
@@ -216,8 +231,14 @@ snap_confine_snap_confine_SOURCES = \
snap-confine/mount-support.h \
snap-confine/ns-support.c \
snap-confine/ns-support.h \
+ snap-confine/seccomp-support-ext.c \
+ snap-confine/seccomp-support-ext.h \
+ snap-confine/seccomp-support.c \
+ snap-confine/seccomp-support.h \
snap-confine/snap-confine-args.c \
snap-confine/snap-confine-args.h \
+ snap-confine/snap-confine-invocation.c \
+ snap-confine/snap-confine-invocation.h \
snap-confine/snap-confine.c \
snap-confine/udev-support.c \
snap-confine/udev-support.h \
@@ -255,20 +276,6 @@ snap-confine/snap-confine$(EXEEXT): LIBS
snap_confine_snap_confine_CFLAGS += $(SUID_CFLAGS)
snap_confine_snap_confine_LDFLAGS += $(SUID_LDFLAGS)
-if SECCOMP
-snap_confine_snap_confine_SOURCES += \
- snap-confine/seccomp-support-ext.c \
- snap-confine/seccomp-support-ext.h \
- snap-confine/seccomp-support.c \
- snap-confine/seccomp-support.h
-snap_confine_snap_confine_CFLAGS += $(SECCOMP_CFLAGS)
-if STATIC_LIBSECCOMP
-snap_confine_snap_confine_STATIC += $(shell $(PKG_CONFIG) --static --libs libseccomp)
-else
-snap_confine_snap_confine_extra_libs += $(SECCOMP_LIBS)
-endif # STATIC_LIBSECCOMP
-endif # SECCOMP
-
if APPARMOR
snap_confine_snap_confine_CFLAGS += $(APPARMOR_CFLAGS)
if STATIC_LIBAPPARMOR
@@ -278,6 +285,18 @@ snap_confine_snap_confine_extra_libs +=
endif # STATIC_LIBAPPARMOR
endif # APPARMOR
+if SELINUX
+snap_confine_snap_confine_SOURCES += \
+ snap-confine/selinux-support.c \
+ snap-confine/selinux-support.h
+snap_confine_snap_confine_CFLAGS += $(SELINUX_CFLAGS)
+if STATIC_LIBSELINUX
+snap_confine_snap_confine_STATIC += $(shell $(PKG_CONFIG) --static --libs libselinux)
+else
+snap_confine_snap_confine_extra_libs += $(SELINUX_LIBS)
+endif # STATIC_LIBSELINUX
+endif # SELINUX
+
# an extra build that has additional debugging enabled at compile time
noinst_PROGRAMS += snap-confine/snap-confine-debug
@@ -306,6 +325,8 @@ snap_confine_unit_tests_SOURCES = \
snap-confine/mount-support-test.c \
snap-confine/ns-support-test.c \
snap-confine/snap-confine-args-test.c \
+ snap-confine/snap-confine-invocation.c \
+ snap-confine/snap-confine-invocation.h \
snap-confine/snap-device-helper-test.c
snap_confine_unit_tests_CFLAGS = $(snap_confine_snap_confine_CFLAGS) $(GLIB_CFLAGS)
snap_confine_unit_tests_LDADD = $(snap_confine_snap_confine_LDADD) $(GLIB_LIBS)
@@ -340,9 +361,9 @@ if APPARMOR
endif
install -d -m 755 $(DESTDIR)/var/lib/snapd/apparmor/snap-confine/
-# NOTE: The 'void' directory *has to* be chmod 000
+# NOTE: The 'void' directory *has to* be chmod 111
install-data-local::
- install -d -m 000 $(DESTDIR)/var/lib/snapd/void
+ install -d -m 111 $(DESTDIR)/var/lib/snapd/void
install-exec-hook::
if CAPS_OVER_SETUID
@@ -367,6 +388,17 @@ snap-mgmt/$(am__dirstamp):
snap-mgmt/snap-mgmt: snap-mgmt/snap-mgmt.sh.in Makefile snap-mgmt/$(am__dirstamp)
sed -e 's,[@]SNAP_MOUNT_DIR[@],$(SNAP_MOUNT_DIR),' <$< >$@
+if SELINUX
+##
+## snap-mgmt-selinux
+##
+
+libexec_SCRIPTS += snap-mgmt/snap-mgmt-selinux
+CLEANFILES += snap-mgmt/$(am__dirstamp) snap-mgmt/snap-mgmt-selinux
+
+snap-mgmt/snap-mgmt-selinux: snap-mgmt/snap-mgmt-selinux.sh.in Makefile snap-mgmt/$(am__dirstamp)
+ sed -e 's,[@]SNAP_MOUNT_DIR[@],$(SNAP_MOUNT_DIR),' <$< >$@
+endif
##
## ubuntu-core-launcher
diff -pruN 2.37.4-1/cmd/snap/cmd_advise.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_advise.go
--- 2.37.4-1/cmd/snap/cmd_advise.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_advise.go 2019-06-05 06:41:21.000000000 +0000
@@ -64,7 +64,7 @@ func init() {
// TRANSLATORS: This should not start with a lowercase letter.
"command": i18n.G("Advise on snaps that provide the given command"),
// TRANSLATORS: This should not start with a lowercase letter.
- "from-apt": i18n.G("Advise will talk to apt via an apt hook"),
+ "from-apt": i18n.G("Run as an apt hook"),
// TRANSLATORS: This should not start with a lowercase letter.
"format": i18n.G("Use the given output format"),
}, []argDesc{
@@ -112,7 +112,6 @@ type jsonRPC struct {
Method string `json:"method"`
Params struct {
Command string `json:"command"`
- SearchTerms []string `json:"search-terms"`
UnknownPackages []string `json:"unknown-packages"`
}
}
diff -pruN 2.37.4-1/cmd/snap/cmd_connections.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_connections.go
--- 2.37.4-1/cmd/snap/cmd_connections.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_connections.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,214 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2018 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 .
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/i18n"
+)
+
+type cmdConnections struct {
+ clientMixin
+ All bool `long:"all"`
+ Positionals struct {
+ Snap installedSnapName
+ } `positional-args:"true"`
+}
+
+var shortConnectionsHelp = i18n.G("List interface connections")
+var longConnectionsHelp = i18n.G(`
+The connections command lists connections between plugs and slots
+in the system.
+
+Unless is provided, the listing is for connected plugs and
+slots for all snaps in the system. In this mode, pass --all to also
+list unconnected plugs and slots.
+
+$ snap connections
+
+Lists connected and unconnected plugs and slots for the specified
+snap.
+`)
+
+func init() {
+ addCommand("connections", shortConnectionsHelp, longConnectionsHelp, func() flags.Commander {
+ return &cmdConnections{}
+ }, map[string]string{
+ "all": i18n.G("Show connected and unconnected plugs and slots"),
+ }, []argDesc{{
+ // TRANSLATORS: This needs to be wrapped in <>s.
+ name: "",
+ // TRANSLATORS: This should not start with a lowercase letter.
+ desc: i18n.G("Constrain listing to a specific snap"),
+ }})
+}
+
+func isSystemSnap(snap string) bool {
+ return snap == "core" || snap == "snapd" || snap == "system"
+}
+
+func endpoint(snap, name string) string {
+ if isSystemSnap(snap) {
+ return ":" + name
+ }
+ return snap + ":" + name
+}
+
+type connection struct {
+ slot string
+ plug string
+ interfaceName string
+ interfaceDeterminant string
+ manual bool
+ gadget bool
+}
+
+func (cn connection) String() string {
+ opts := []string{}
+ if cn.manual {
+ opts = append(opts, "manual")
+ }
+ if cn.gadget {
+ opts = append(opts, "gadget")
+ }
+ if len(opts) == 0 {
+ return "-"
+ }
+ return strings.Join(opts, ",")
+}
+
+type byConnectionData []connection
+
+func (b byConnectionData) Len() int { return len(b) }
+func (b byConnectionData) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
+func (b byConnectionData) Less(i, j int) bool {
+ iCon, jCon := b[i], b[j]
+ if iCon.interfaceName != jCon.interfaceName {
+ return iCon.interfaceName < jCon.interfaceName
+ }
+ if iCon.plug != jCon.plug {
+ return iCon.plug < jCon.plug
+ }
+ return iCon.slot < jCon.slot
+}
+
+func interfaceDeterminant(conn *client.Connection) string {
+ var value string
+
+ switch conn.Interface {
+ case "content":
+ value, _ = conn.PlugAttrs["content"].(string)
+ if value == "" {
+ value, _ = conn.SlotAttrs["content"].(string)
+ }
+ }
+ if value == "" {
+ return ""
+ }
+ return fmt.Sprintf("[%v]", value)
+}
+
+func (x *cmdConnections) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ opts := client.ConnectionOptions{
+ All: x.All,
+ }
+ wanted := string(x.Positionals.Snap)
+ if wanted != "" {
+ if x.All {
+ // passing a snap name already implies --all, error out
+ // when it was passed explicitly
+ return fmt.Errorf(i18n.G("cannot use --all with snap name"))
+ }
+ // when asking for a single snap, include its disconnected plugs
+ // and slots
+ opts.Snap = wanted
+ opts.All = true
+ // print all slots
+ x.All = true
+ }
+
+ connections, err := x.client.Connections(&opts)
+ if err != nil {
+ return err
+ }
+ if len(connections.Plugs) == 0 && len(connections.Slots) == 0 {
+ return nil
+ }
+
+ annotatedConns := make([]connection, 0, len(connections.Established)+len(connections.Undesired))
+ for _, conn := range connections.Established {
+ annotatedConns = append(annotatedConns, connection{
+ plug: endpoint(conn.Plug.Snap, conn.Plug.Name),
+ slot: endpoint(conn.Slot.Snap, conn.Slot.Name),
+ manual: conn.Manual,
+ gadget: conn.Gadget,
+ interfaceName: conn.Interface,
+ interfaceDeterminant: interfaceDeterminant(&conn),
+ })
+ }
+
+ w := tabWriter()
+ fmt.Fprintln(w, i18n.G("Interface\tPlug\tSlot\tNotes"))
+
+ for _, plug := range connections.Plugs {
+ if len(plug.Connections) == 0 && x.All {
+ annotatedConns = append(annotatedConns, connection{
+ plug: endpoint(plug.Snap, plug.Name),
+ slot: "-",
+ interfaceName: plug.Interface,
+ })
+ }
+ }
+ for _, slot := range connections.Slots {
+ if !isSystemSnap(wanted) && isSystemSnap(slot.Snap) {
+ // displaying unconnected system snap slots is boring,
+ // unless explicitly asked to show them
+ continue
+ }
+ if len(slot.Connections) == 0 && x.All {
+ annotatedConns = append(annotatedConns, connection{
+ plug: "-",
+ slot: endpoint(slot.Snap, slot.Name),
+ interfaceName: slot.Interface,
+ })
+ }
+ }
+
+ sort.Sort(byConnectionData(annotatedConns))
+
+ for _, note := range annotatedConns {
+ fmt.Fprintf(w, "%s%s\t%s\t%s\t%s\n", note.interfaceName, note.interfaceDeterminant, note.plug, note.slot, note)
+ }
+
+ if len(annotatedConns) > 0 {
+ w.Flush()
+ }
+ return nil
+}
diff -pruN 2.37.4-1/cmd/snap/cmd_connections_test.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_connections_test.go
--- 2.37.4-1/cmd/snap/cmd_connections_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_connections_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,853 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2018 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 .
+ *
+ */
+
+package main_test
+
+import (
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/client"
+ . "github.com/snapcore/snapd/cmd/snap"
+)
+
+func (s *SnapSuite) TestConnectionsNoneConnected(c *C) {
+ result := client.Connections{}
+ query := url.Values{}
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
+ c.Check(r.URL.Query(), DeepEquals, query)
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": result,
+ })
+ })
+ _, err := Parser(Client()).ParseArgs([]string{"connections"})
+ c.Check(err, IsNil)
+ c.Assert(s.Stdout(), Equals, "")
+ c.Assert(s.Stderr(), Equals, "")
+
+ s.ResetStdStreams()
+
+ query = url.Values{
+ "select": []string{"all"},
+ }
+ _, err = Parser(Client()).ParseArgs([]string{"connections", "--all"})
+ c.Check(err, IsNil)
+ c.Assert(s.Stdout(), Equals, "")
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestConnectionsNotInstalled(c *C) {
+ query := url.Values{
+ "snap": []string{"foo"},
+ "select": []string{"all"},
+ }
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
+ c.Check(r.URL.Query(), DeepEquals, query)
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ fmt.Fprintln(w, `{"type": "error", "result": {"message": "not found", "value": "foo", "kind": "snap-not-found"}, "status-code": 404}`)
+ })
+ _, err := Parser(Client()).ParseArgs([]string{"connections", "foo"})
+ c.Check(err, ErrorMatches, `not found`)
+ c.Assert(s.Stdout(), Equals, "")
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestConnectionsNoneConnectedPlugs(c *C) {
+ query := url.Values{
+ "select": []string{"all"},
+ }
+ result := client.Connections{
+ Plugs: []client.Plug{
+ {
+ Snap: "keyboard-lights",
+ Name: "capslock-led",
+ Interface: "leds",
+ },
+ },
+ }
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
+ c.Check(r.URL.Query(), DeepEquals, query)
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": result,
+ })
+ })
+
+ rest, err := Parser(Client()).ParseArgs([]string{"connections", "--all"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ expectedStdout := "" +
+ "Interface Plug Slot Notes\n" +
+ "leds keyboard-lights:capslock-led - -\n"
+ c.Assert(s.Stdout(), Equals, expectedStdout)
+ c.Assert(s.Stderr(), Equals, "")
+
+ s.ResetStdStreams()
+
+ query = url.Values{
+ "select": []string{"all"},
+ "snap": []string{"keyboard-lights"},
+ }
+
+ rest, err = Parser(Client()).ParseArgs([]string{"connections", "keyboard-lights"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ expectedStdout = "" +
+ "Interface Plug Slot Notes\n" +
+ "leds keyboard-lights:capslock-led - -\n"
+ c.Assert(s.Stdout(), Equals, expectedStdout)
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestConnectionsNoneConnectedSlots(c *C) {
+ result := client.Connections{}
+ query := url.Values{}
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
+ c.Check(r.URL.Query(), DeepEquals, query)
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": result,
+ })
+ })
+ _, err := Parser(Client()).ParseArgs([]string{"connections"})
+ c.Check(err, IsNil)
+ c.Assert(s.Stdout(), Equals, "")
+ c.Assert(s.Stderr(), Equals, "")
+
+ s.ResetStdStreams()
+
+ query = url.Values{
+ "select": []string{"all"},
+ }
+ result = client.Connections{
+ Slots: []client.Slot{
+ {
+ Snap: "leds-provider",
+ Name: "capslock-led",
+ Interface: "leds",
+ },
+ },
+ }
+ rest, err := Parser(Client()).ParseArgs([]string{"connections", "--all"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ expectedStdout := "" +
+ "Interface Plug Slot Notes\n" +
+ "leds - leds-provider:capslock-led -\n"
+ c.Assert(s.Stdout(), Equals, expectedStdout)
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestConnectionsSomeConnected(c *C) {
+ result := client.Connections{
+ Established: []client.Connection{
+ {
+ Plug: client.PlugRef{Snap: "keyboard-lights", Name: "capslock"},
+ Slot: client.SlotRef{Snap: "leds-provider", Name: "capslock-led"},
+ Interface: "leds",
+ Gadget: true,
+ }, {
+ Plug: client.PlugRef{Snap: "keyboard-lights", Name: "numlock"},
+ Slot: client.SlotRef{Snap: "core", Name: "numlock-led"},
+ Interface: "leds",
+ Manual: true,
+ }, {
+ Plug: client.PlugRef{Snap: "keyboard-lights", Name: "scrollock"},
+ Slot: client.SlotRef{Snap: "core", Name: "scrollock-led"},
+ Interface: "leds",
+ },
+ },
+ Plugs: []client.Plug{
+ {
+ Snap: "keyboard-lights",
+ Name: "capslock",
+ Interface: "leds",
+ Connections: []client.SlotRef{{
+ Snap: "leds-provider",
+ Name: "capslock-led",
+ }},
+ }, {
+ Snap: "keyboard-lights",
+ Name: "numlock",
+ Interface: "leds",
+ Connections: []client.SlotRef{{
+ Snap: "core",
+ Name: "numlock-led",
+ }},
+ }, {
+ Snap: "keyboard-lights",
+ Name: "scrollock",
+ Interface: "leds",
+ Connections: []client.SlotRef{{
+ Snap: "core",
+ Name: "scrollock-led",
+ }},
+ },
+ },
+ Slots: []client.Slot{
+ {
+ Snap: "core",
+ Name: "numlock-led",
+ Interface: "leds",
+ Connections: []client.PlugRef{{
+ Snap: "keyuboard-lights",
+ Name: "numlock",
+ }},
+ }, {
+ Snap: "core",
+ Name: "scrollock-led",
+ Interface: "leds",
+ Connections: []client.PlugRef{{
+ Snap: "keyuboard-lights",
+ Name: "scrollock",
+ }},
+ }, {
+ Snap: "leds-provider",
+ Name: "capslock-led",
+ Interface: "leds",
+ Connections: []client.PlugRef{{
+ Snap: "keyuboard-lights",
+ Name: "capslock",
+ }},
+ },
+ },
+ }
+ query := url.Values{}
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
+ c.Check(r.URL.Query(), DeepEquals, query)
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": result,
+ })
+ })
+ rest, err := Parser(Client()).ParseArgs([]string{"connections"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ expectedStdout := "" +
+ "Interface Plug Slot Notes\n" +
+ "leds keyboard-lights:capslock leds-provider:capslock-led gadget\n" +
+ "leds keyboard-lights:numlock :numlock-led manual\n" +
+ "leds keyboard-lights:scrollock :scrollock-led -\n"
+ c.Assert(s.Stdout(), Equals, expectedStdout)
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestConnectionsSomeDisconnected(c *C) {
+ result := client.Connections{
+ Established: []client.Connection{
+ {
+ Plug: client.PlugRef{Snap: "keyboard-lights", Name: "scrollock"},
+ Slot: client.SlotRef{Snap: "core", Name: "scrollock-led"},
+ Interface: "leds",
+ }, {
+ Plug: client.PlugRef{Snap: "keyboard-lights", Name: "capslock"},
+ Slot: client.SlotRef{Snap: "leds-provider", Name: "capslock-led"},
+ Interface: "leds",
+ },
+ },
+ Undesired: []client.Connection{
+ {
+ Plug: client.PlugRef{Snap: "keyboard-lights", Name: "numlock"},
+ Slot: client.SlotRef{Snap: "core", Name: "numlock-led"},
+ Interface: "leds",
+ Manual: true,
+ },
+ },
+ Plugs: []client.Plug{
+ {
+ Snap: "keyboard-lights",
+ Name: "capslock",
+ Interface: "leds",
+ Connections: []client.SlotRef{{
+ Snap: "leds-provider",
+ Name: "capslock-led",
+ }},
+ }, {
+ Snap: "keyboard-lights",
+ Name: "numlock",
+ Interface: "leds",
+ }, {
+ Snap: "keyboard-lights",
+ Name: "scrollock",
+ Interface: "leds",
+ Connections: []client.SlotRef{{
+ Snap: "core",
+ Name: "scrollock-led",
+ }},
+ },
+ },
+ Slots: []client.Slot{
+ {
+ Snap: "core",
+ Name: "capslock-led",
+ Interface: "leds",
+ }, {
+ Snap: "core",
+ Name: "numlock-led",
+ Interface: "leds",
+ }, {
+ Snap: "core",
+ Name: "scrollock-led",
+ Interface: "leds",
+ Connections: []client.PlugRef{{
+ Snap: "keyuboard-lights",
+ Name: "scrollock",
+ }},
+ }, {
+ Snap: "leds-provider",
+ Name: "capslock-led",
+ Interface: "leds",
+ Connections: []client.PlugRef{{
+ Snap: "keyuboard-lights",
+ Name: "capslock",
+ }},
+ }, {
+ Snap: "leds-provider",
+ Name: "numlock-led",
+ Interface: "leds",
+ },
+ },
+ }
+ query := url.Values{
+ "select": []string{"all"},
+ }
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
+ c.Check(r.URL.Query(), DeepEquals, query)
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": result,
+ })
+ })
+
+ rest, err := Parser(Client()).ParseArgs([]string{"connections", "--all"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ expectedStdout := "" +
+ "Interface Plug Slot Notes\n" +
+ "leds - leds-provider:numlock-led -\n" +
+ "leds keyboard-lights:capslock leds-provider:capslock-led -\n" +
+ "leds keyboard-lights:numlock - -\n" +
+ "leds keyboard-lights:scrollock :scrollock-led -\n"
+ c.Assert(s.Stdout(), Equals, expectedStdout)
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestConnectionsOnlyDisconnected(c *C) {
+ result := client.Connections{
+ Undesired: []client.Connection{
+ {
+ Plug: client.PlugRef{Snap: "keyboard-lights", Name: "numlock"},
+ Slot: client.SlotRef{Snap: "leds-provider", Name: "numlock-led"},
+ Interface: "leds",
+ Manual: true,
+ },
+ },
+ Slots: []client.Slot{
+ {
+ Snap: "leds-provider",
+ Name: "capslock-led",
+ Interface: "leds",
+ }, {
+ Snap: "leds-provider",
+ Name: "numlock-led",
+ Interface: "leds",
+ },
+ },
+ }
+ query := url.Values{
+ "snap": []string{"leds-provider"},
+ "select": []string{"all"},
+ }
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
+ c.Check(r.URL.Query(), DeepEquals, query)
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": result,
+ })
+ })
+
+ rest, err := Parser(Client()).ParseArgs([]string{"connections", "leds-provider"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ expectedStdout := "" +
+ "Interface Plug Slot Notes\n" +
+ "leds - leds-provider:capslock-led -\n" +
+ "leds - leds-provider:numlock-led -\n"
+ c.Assert(s.Stdout(), Equals, expectedStdout)
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestConnectionsFiltering(c *C) {
+ result := client.Connections{}
+ query := url.Values{
+ "select": []string{"all"},
+ }
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
+ c.Check(r.URL.Query(), DeepEquals, query)
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": result,
+ })
+ })
+
+ query = url.Values{
+ "select": []string{"all"},
+ "snap": []string{"mouse-buttons"},
+ }
+ rest, err := Parser(Client()).ParseArgs([]string{"connections", "mouse-buttons"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+
+ rest, err = Parser(Client()).ParseArgs([]string{"connections", "mouse-buttons", "--all"})
+ c.Assert(err, ErrorMatches, "cannot use --all with snap name")
+ c.Assert(rest, DeepEquals, []string{"--all"})
+}
+
+func (s *SnapSuite) TestConnectionsSorting(c *C) {
+ result := client.Connections{
+ Established: []client.Connection{
+ {
+ Plug: client.PlugRef{Snap: "foo", Name: "plug"},
+ Slot: client.SlotRef{Snap: "a-content-provider", Name: "data"},
+ Interface: "content",
+ }, {
+ Plug: client.PlugRef{Snap: "foo", Name: "plug"},
+ Slot: client.SlotRef{Snap: "b-content-provider", Name: "data"},
+ Interface: "content",
+ }, {
+ Plug: client.PlugRef{Snap: "foo", Name: "desktop-plug"},
+ Slot: client.SlotRef{Snap: "core", Name: "desktop"},
+ Interface: "desktop",
+ }, {
+ Plug: client.PlugRef{Snap: "foo", Name: "x11-plug"},
+ Slot: client.SlotRef{Snap: "core", Name: "x11"},
+ Interface: "x11",
+ }, {
+ Plug: client.PlugRef{Snap: "foo", Name: "a-x11-plug"},
+ Slot: client.SlotRef{Snap: "core", Name: "x11"},
+ Interface: "x11",
+ }, {
+ Plug: client.PlugRef{Snap: "a-foo", Name: "plug"},
+ Slot: client.SlotRef{Snap: "a-content-provider", Name: "data"},
+ Interface: "content",
+ }, {
+ Plug: client.PlugRef{Snap: "keyboard-app", Name: "x11"},
+ Slot: client.SlotRef{Snap: "core", Name: "x11"},
+ Interface: "x11",
+ Manual: true,
+ },
+ },
+ Undesired: []client.Connection{
+ {
+ Plug: client.PlugRef{Snap: "foo", Name: "plug"},
+ Slot: client.SlotRef{Snap: "c-content-provider", Name: "data"},
+ Interface: "content",
+ Manual: true,
+ },
+ },
+ Plugs: []client.Plug{
+ {
+ Snap: "foo",
+ Name: "plug",
+ Interface: "content",
+ Connections: []client.SlotRef{{
+ Snap: "a-content-provider",
+ Name: "data",
+ }, {
+ Snap: "b-content-provider",
+ Name: "data",
+ }},
+ }, {
+ Snap: "foo",
+ Name: "desktop-plug",
+ Interface: "desktop",
+ Connections: []client.SlotRef{{
+ Snap: "core",
+ Name: "desktop",
+ }},
+ }, {
+ Snap: "foo",
+ Name: "x11-plug",
+ Interface: "x11",
+ Connections: []client.SlotRef{{
+ Snap: "core",
+ Name: "x11",
+ }},
+ }, {
+ Snap: "foo",
+ Name: "a-x11-plug",
+ Interface: "x11",
+ Connections: []client.SlotRef{{
+ Snap: "core",
+ Name: "x11",
+ }},
+ }, {
+ Snap: "a-foo",
+ Name: "plug",
+ Interface: "content",
+ Connections: []client.SlotRef{{
+ Snap: "a-content-provider",
+ Name: "data",
+ }},
+ }, {
+ Snap: "keyboard-app",
+ Name: "x11",
+ Interface: "x11",
+ Connections: []client.SlotRef{{
+ Snap: "core",
+ Name: "x11",
+ }},
+ }, {
+ Snap: "keyboard-lights",
+ Name: "numlock",
+ Interface: "leds",
+ },
+ },
+ Slots: []client.Slot{
+ {
+ Snap: "c-content-provider",
+ Name: "data",
+ Interface: "content",
+ }, {
+ Snap: "a-content-provider",
+ Name: "data",
+ Interface: "content",
+ Connections: []client.PlugRef{{
+ Snap: "foo",
+ Name: "plug",
+ }, {
+ Snap: "a-foo",
+ Name: "plug",
+ }},
+ }, {
+ Snap: "b-content-provider",
+ Name: "data",
+ Interface: "content",
+ Connections: []client.PlugRef{{
+ Snap: "foo",
+ Name: "plug",
+ }},
+ }, {
+ Snap: "core",
+ Name: "x11",
+ Interface: "x11",
+ Connections: []client.PlugRef{{
+ Snap: "foo",
+ Name: "a-x11-plug",
+ }, {
+ Snap: "foo",
+ Name: "x11-plug",
+ }, {
+ Snap: "keyboard-app",
+ Name: "x11",
+ }},
+ }, {
+ Snap: "leds-provider",
+ Name: "numlock-led",
+ Interface: "leds",
+ },
+ },
+ }
+ query := url.Values{
+ "select": []string{"all"},
+ }
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
+ c.Check(r.URL.Query(), DeepEquals, query)
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": result,
+ })
+ })
+
+ rest, err := Parser(Client()).ParseArgs([]string{"connections", "--all"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ expectedStdout := "" +
+ "Interface Plug Slot Notes\n" +
+ "content - c-content-provider:data -\n" +
+ "content a-foo:plug a-content-provider:data -\n" +
+ "content foo:plug a-content-provider:data -\n" +
+ "content foo:plug b-content-provider:data -\n" +
+ "desktop foo:desktop-plug :desktop -\n" +
+ "leds - leds-provider:numlock-led -\n" +
+ "leds keyboard-lights:numlock - -\n" +
+ "x11 foo:a-x11-plug :x11 -\n" +
+ "x11 foo:x11-plug :x11 -\n" +
+ "x11 keyboard-app:x11 :x11 manual\n"
+ c.Assert(s.Stdout(), Equals, expectedStdout)
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestConnectionsDefiningAttribute(c *C) {
+ result := client.Connections{
+ Established: []client.Connection{
+ {
+ Plug: client.PlugRef{Snap: "foo", Name: "a-plug"},
+ Slot: client.SlotRef{Snap: "a-content-provider", Name: "data"},
+ Interface: "content",
+ PlugAttrs: map[string]interface{}{
+ "content": "plug-some-data",
+ "target": "$SNAP/foo",
+ },
+ SlotAttrs: map[string]interface{}{
+ "content": "slot-some-data",
+ "source": map[string]interface{}{
+ "read": []string{"$SNAP/bar"},
+ },
+ },
+ }, {
+ Plug: client.PlugRef{Snap: "foo", Name: "b-plug"},
+ Slot: client.SlotRef{Snap: "b-content-provider", Name: "data"},
+ Interface: "content",
+ PlugAttrs: map[string]interface{}{
+ // no content attribute for plug, falls back to slot
+ "target": "$SNAP/foo",
+ },
+ SlotAttrs: map[string]interface{}{
+ "content": "slot-some-data",
+ "source": map[string]interface{}{
+ "read": []string{"$SNAP/bar"},
+ },
+ },
+ }, {
+ Plug: client.PlugRef{Snap: "foo", Name: "c-plug"},
+ Slot: client.SlotRef{Snap: "c-content-provider", Name: "data"},
+ Interface: "content",
+ PlugAttrs: map[string]interface{}{
+ // no content attribute for plug
+ "target": "$SNAP/foo",
+ },
+ SlotAttrs: map[string]interface{}{
+ // no content attribute for slot either
+ "source": map[string]interface{}{
+ "read": []string{"$SNAP/bar"},
+ },
+ },
+ }, {
+ Plug: client.PlugRef{Snap: "foo", Name: "d-plug"},
+ Slot: client.SlotRef{Snap: "d-content-provider", Name: "data"},
+ Interface: "content",
+ // no attributes at all
+ }, {
+ Plug: client.PlugRef{Snap: "foo", Name: "desktop-plug"},
+ Slot: client.SlotRef{Snap: "core", Name: "desktop"},
+ // desktop interface does not have any defining attributes
+ Interface: "desktop",
+ PlugAttrs: map[string]interface{}{
+ "this-is-ignored": "foo",
+ },
+ SlotAttrs: map[string]interface{}{
+ "this-is-ignored-too": "foo",
+ },
+ },
+ },
+ Plugs: []client.Plug{
+ {
+ Snap: "foo",
+ Name: "a-plug",
+ Interface: "content",
+ Connections: []client.SlotRef{{
+ Snap: "a-content-provider",
+ Name: "data",
+ }},
+ Attrs: map[string]interface{}{
+ "content": "plug-some-data",
+ "target": "$SNAP/foo",
+ },
+ }, {
+ Snap: "foo",
+ Name: "b-plug",
+ Interface: "content",
+ Connections: []client.SlotRef{{
+ Snap: "b-content-provider",
+ Name: "data",
+ }},
+ Attrs: map[string]interface{}{
+ // no content attribute for plug, falls back to slot
+ "target": "$SNAP/foo",
+ },
+ }, {
+ Snap: "foo",
+ Name: "c-plug",
+ Interface: "content",
+ Connections: []client.SlotRef{{
+ Snap: "c-content-provider",
+ Name: "data",
+ }},
+ Attrs: map[string]interface{}{
+ // no content attribute for plug
+ "target": "$SNAP/foo",
+ },
+ }, {
+ Snap: "foo",
+ Name: "d-plug",
+ Interface: "content",
+ Connections: []client.SlotRef{{
+ Snap: "d-content-provider",
+ Name: "data",
+ }},
+ }, {
+ Snap: "foo",
+ Name: "desktop-plug",
+ Interface: "desktop",
+ Connections: []client.SlotRef{{
+ Snap: "core",
+ Name: "desktop",
+ }},
+ },
+ },
+ Slots: []client.Slot{
+ {
+ Snap: "a-content-provider",
+ Name: "data",
+ Interface: "content",
+ Connections: []client.PlugRef{{
+ Snap: "foo",
+ Name: "a-plug",
+ }},
+ Attrs: map[string]interface{}{
+ "content": "slot-some-data",
+ "source": map[string]interface{}{
+ "read": []string{"$SNAP/bar"},
+ },
+ },
+ }, {
+ Snap: "b-content-provider",
+ Name: "data",
+ Interface: "content",
+ Connections: []client.PlugRef{{
+ Snap: "foo",
+ Name: "a-plug",
+ }},
+ Attrs: map[string]interface{}{
+ "content": "slot-some-data",
+ "source": map[string]interface{}{
+ "read": []string{"$SNAP/bar"},
+ },
+ },
+ }, {
+ Snap: "c-content-provider",
+ Name: "data",
+ Interface: "content",
+ Connections: []client.PlugRef{{
+ Snap: "foo",
+ Name: "a-plug",
+ }},
+ Attrs: map[string]interface{}{
+ "source": map[string]interface{}{
+ "read": []string{"$SNAP/bar"},
+ },
+ },
+ }, {
+ Snap: "a-content-provider",
+ Name: "data",
+ Interface: "content",
+ Connections: []client.PlugRef{{
+ Snap: "foo",
+ Name: "a-plug",
+ }},
+ }, {
+ Snap: "core",
+ Name: "desktop",
+ Interface: "desktop",
+ Connections: []client.PlugRef{{
+ Snap: "foo",
+ Name: "desktop-plug",
+ }},
+ },
+ },
+ }
+ query := url.Values{
+ "select": []string{"all"},
+ }
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
+ c.Check(r.URL.Query(), DeepEquals, query)
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": result,
+ })
+ })
+
+ rest, err := Parser(Client()).ParseArgs([]string{"connections", "--all"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ expectedStdout := "" +
+ "Interface Plug Slot Notes\n" +
+ "content[plug-some-data] foo:a-plug a-content-provider:data -\n" +
+ "content[slot-some-data] foo:b-plug b-content-provider:data -\n" +
+ "content foo:c-plug c-content-provider:data -\n" +
+ "content foo:d-plug d-content-provider:data -\n" +
+ "desktop foo:desktop-plug :desktop -\n"
+ c.Assert(s.Stdout(), Equals, expectedStdout)
+ c.Assert(s.Stderr(), Equals, "")
+}
diff -pruN 2.37.4-1/cmd/snap/cmd_connectivity_check.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_connectivity_check.go
--- 2.37.4-1/cmd/snap/cmd_connectivity_check.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_connectivity_check.go 2019-06-05 06:41:21.000000000 +0000
@@ -44,10 +44,9 @@ func (x *cmdConnectivityCheck) Execute(a
}
var status struct {
- Connectivity bool
- Unreachable []string
+ Unreachable []string
}
- if err := x.client.Debug("connectivity", nil, &status); err != nil {
+ if err := x.client.DebugGet("connectivity", &status, nil); err != nil {
return err
}
diff -pruN 2.37.4-1/cmd/snap/cmd_connectivity_check_test.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_connectivity_check_test.go
--- 2.37.4-1/cmd/snap/cmd_connectivity_check_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_connectivity_check_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -34,12 +34,12 @@ func (s *SnapSuite) TestConnectivityHapp
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
switch n {
case 0:
- c.Check(r.Method, check.Equals, "POST")
+ c.Check(r.Method, check.Equals, "GET")
c.Check(r.URL.Path, check.Equals, "/v2/debug")
- c.Check(r.URL.RawQuery, check.Equals, "")
+ c.Check(r.URL.RawQuery, check.Equals, "aspect=connectivity")
data, err := ioutil.ReadAll(r.Body)
c.Check(err, check.IsNil)
- c.Check(data, check.DeepEquals, []byte(`{"action":"connectivity"}`))
+ c.Check(data, check.HasLen, 0)
fmt.Fprintln(w, `{"type": "sync", "result": {}}`)
default:
c.Fatalf("expected to get 1 requests, now on %d", n+1)
@@ -61,12 +61,12 @@ func (s *SnapSuite) TestConnectivityUnha
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
switch n {
case 0:
- c.Check(r.Method, check.Equals, "POST")
+ c.Check(r.Method, check.Equals, "GET")
c.Check(r.URL.Path, check.Equals, "/v2/debug")
- c.Check(r.URL.RawQuery, check.Equals, "")
+ c.Check(r.URL.RawQuery, check.Equals, "aspect=connectivity")
data, err := ioutil.ReadAll(r.Body)
c.Check(err, check.IsNil)
- c.Check(data, check.DeepEquals, []byte(`{"action":"connectivity"}`))
+ c.Check(data, check.HasLen, 0)
fmt.Fprintln(w, `{"type": "sync", "result": {"connectivity":false,"unreachable":["foo.bar.com"]}}`)
default:
c.Fatalf("expected to get 1 requests, now on %d", n+1)
diff -pruN 2.37.4-1/cmd/snap/cmd_connect_test.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_connect_test.go
--- 2.37.4-1/cmd/snap/cmd_connect_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_connect_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -276,7 +276,7 @@ var fortestingConnectionList = client.Co
func (s *SnapSuite) TestConnectCompletion(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
- case "/v2/interfaces":
+ case "/v2/connections":
c.Assert(r.Method, Equals, "GET")
EncodeResponseBody(c, w, map[string]interface{}{
"type": "sync",
diff -pruN 2.37.4-1/cmd/snap/cmd_debug_model.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_debug_model.go
--- 2.37.4-1/cmd/snap/cmd_debug_model.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_debug_model.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,54 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+
+ "github.com/jessevdk/go-flags"
+)
+
+type cmdGetModel struct {
+ clientMixin
+}
+
+func init() {
+ cmd := addDebugCommand("model",
+ "(internal) obtain the active model assertion",
+ "(internal) obtain the active model assertion",
+ func() flags.Commander {
+ return &cmdGetModel{}
+ }, nil, nil)
+ cmd.hidden = true
+}
+
+func (x *cmdGetModel) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+ var resp struct {
+ Model string `json:"model"`
+ }
+ if err := x.client.DebugGet("model", &resp, nil); err != nil {
+ return err
+ }
+ fmt.Fprintf(Stdout, "%s\n", resp.Model)
+ return nil
+}
diff -pruN 2.37.4-1/cmd/snap/cmd_debug_model_test.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_debug_model_test.go
--- 2.37.4-1/cmd/snap/cmd_debug_model_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_debug_model_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,56 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main_test
+
+import (
+ "fmt"
+ "io/ioutil"
+ "net/http"
+
+ "gopkg.in/check.v1"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+func (s *SnapSuite) TestGetModel(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/debug")
+ c.Check(r.URL.RawQuery, check.Equals, "aspect=model")
+ data, err := ioutil.ReadAll(r.Body)
+ c.Check(err, check.IsNil)
+ c.Check(string(data), check.Equals, "")
+ fmt.Fprintln(w, `{"type": "sync", "result": {"model": "some-model-json"}}`)
+ default:
+ c.Fatalf("expected to get 1 requests, now on %d", n+1)
+ }
+
+ n++
+ })
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "model"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, "some-model-json\n")
+ c.Check(s.Stderr(), check.Equals, "")
+ c.Check(n, check.Equals, 1)
+}
diff -pruN 2.37.4-1/cmd/snap/cmd_debug_timings.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_debug_timings.go
--- 2.37.4-1/cmd/snap/cmd_debug_timings.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_debug_timings.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,142 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "io"
+ "strings"
+ "time"
+
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/i18n"
+)
+
+type cmdChangeTimings struct {
+ changeIDMixin
+ Verbose bool `long:"verbose"`
+}
+
+func init() {
+ addDebugCommand("timings",
+ i18n.G("Get the timings of the tasks of a change"),
+ i18n.G("The timings command displays details about the time each task runs."),
+ func() flags.Commander {
+ return &cmdChangeTimings{}
+ }, changeIDMixinOptDesc.also(map[string]string{
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "verbose": i18n.G("Show more information"),
+ }), changeIDMixinArgDesc)
+}
+
+type Timing struct {
+ Level int `json:"level,omitempty"`
+ Label string `json:"label,omitempty"`
+ Summary string `json:"summary,omitempty"`
+ Duration time.Duration `json:"duration,omitempty"`
+}
+
+func formatDuration(dur time.Duration) string {
+ return fmt.Sprintf("%dms", dur/time.Millisecond)
+}
+
+func printTiming(w io.Writer, t *Timing, verbose, doing bool) {
+ var doingTimeStr, undoingTimeStr string
+ if doing {
+ doingTimeStr = formatDuration(t.Duration)
+ undoingTimeStr = "-"
+ } else {
+ if doing {
+ doingTimeStr = "-"
+ undoingTimeStr = formatDuration(t.Duration)
+ }
+ }
+ if verbose {
+ fmt.Fprintf(w, "%s\t \t%11s\t%11s\t%s\t%s\n", strings.Repeat(" ", t.Level+1)+"^", doingTimeStr, undoingTimeStr, t.Label, strings.Repeat(" ", 2*(t.Level+1))+t.Summary)
+ } else {
+ fmt.Fprintf(w, "%s\t \t%11s\t%11s\t%s\n", strings.Repeat(" ", t.Level+1)+"^", doingTimeStr, undoingTimeStr, strings.Repeat(" ", 2*(t.Level+1))+t.Summary)
+ }
+}
+
+func (x *cmdChangeTimings) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+ chgid, err := x.GetChangeID()
+ if err != nil {
+ return err
+ }
+
+ // gather debug timings first
+ var timings map[string]struct {
+ DoingTime time.Duration `json:"doing-time,omitempty"`
+ UndoingTime time.Duration `json:"undoing-time,omitempty"`
+ DoingTimings []Timing `json:"doing-timings,omitempty"`
+ UndoingTimings []Timing `json:"undoing-timings,omitempty"`
+ }
+
+ if err := x.client.DebugGet("change-timings", &timings, map[string]string{"change-id": chgid}); err != nil {
+ return err
+ }
+
+ // now combine with the other data about the change
+ chg, err := x.client.Change(chgid)
+ if err != nil {
+ return err
+ }
+ w := tabWriter()
+ if x.Verbose {
+ fmt.Fprintf(w, "ID\tStatus\t%11s\t%11s\tLabel\tSummary\n", "Doing", "Undoing")
+ } else {
+ fmt.Fprintf(w, "ID\tStatus\t%11s\t%11s\tSummary\n", "Doing", "Undoing")
+ }
+ for _, t := range chg.Tasks {
+ doingTime := formatDuration(timings[t.ID].DoingTime)
+ if timings[t.ID].DoingTime == 0 {
+ doingTime = "-"
+ }
+ undoingTime := formatDuration(timings[t.ID].UndoingTime)
+ if timings[t.ID].UndoingTime == 0 {
+ undoingTime = "-"
+ }
+ summary := t.Summary
+ // Duration formats to 17m14.342s or 2.038s or 970ms, so with
+ // 11 chars we can go up to 59m59.999s
+ if x.Verbose {
+ fmt.Fprintf(w, "%s\t%s\t%11s\t%11s\t%s\t%s\n", t.ID, t.Status, doingTime, undoingTime, t.Kind, summary)
+ } else {
+ fmt.Fprintf(w, "%s\t%s\t%11s\t%11s\t%s\n", t.ID, t.Status, doingTime, undoingTime, summary)
+ }
+
+ for _, nested := range timings[t.ID].DoingTimings {
+ showDoing := true
+ printTiming(w, &nested, x.Verbose, showDoing)
+ }
+ for _, nested := range timings[t.ID].UndoingTimings {
+ showDoing := false
+ printTiming(w, &nested, x.Verbose, showDoing)
+ }
+ }
+ w.Flush()
+ fmt.Fprintln(Stdout)
+
+ return nil
+}
diff -pruN 2.37.4-1/cmd/snap/cmd_debug_validate_seed.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_debug_validate_seed.go
--- 2.37.4-1/cmd/snap/cmd_debug_validate_seed.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_debug_validate_seed.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,53 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main
+
+import (
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/snap"
+)
+
+type cmdValidateSeed struct {
+ Positionals struct {
+ SeedYamlPath string `positional-arg-name:""`
+ } `positional-args:"true"`
+}
+
+func init() {
+ cmd := addDebugCommand("validate-seed",
+ "(internal) validate seed.yaml",
+ "(internal) validate seed.yaml",
+ func() flags.Commander {
+ return &cmdValidateSeed{}
+ }, nil, nil)
+ cmd.hidden = true
+}
+
+func (x *cmdValidateSeed) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ if _, err := snap.ReadSeedYaml(x.Positionals.SeedYamlPath); err != nil {
+ return err
+ }
+ return nil
+}
diff -pruN 2.37.4-1/cmd/snap/cmd_debug_validate_seed_test.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_debug_validate_seed_test.go
--- 2.37.4-1/cmd/snap/cmd_debug_validate_seed_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_debug_validate_seed_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,68 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main_test
+
+import (
+ "io/ioutil"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+func (s *SnapSuite) TestDebugValidateSeedHappy(c *C) {
+ tmpf := filepath.Join(c.MkDir(), "seed.yaml")
+ err := ioutil.WriteFile(tmpf, []byte(`
+snaps:
+ -
+ name: core
+ channel: stable
+ file: core_6673.snap
+ -
+ name: gtk-common-themes
+ channel: stable/ubuntu-19.04
+ file: gtk-common-themes_1198.snap
+`), 0644)
+ c.Assert(err, IsNil)
+
+ _, err = snap.Parser(snap.Client()).ParseArgs([]string{"debug", "validate-seed", tmpf})
+ c.Assert(err, IsNil)
+}
+
+func (s *SnapSuite) TestDebugValidateSeedRegressionLp1825437(c *C) {
+ tmpf := filepath.Join(c.MkDir(), "seed.yaml")
+ err := ioutil.WriteFile(tmpf, []byte(`
+snaps:
+ -
+ name: core
+ channel: stable
+ file: core_6673.snap
+ -
+ -
+ name: gnome-foo
+ channel: stable/ubuntu-19.04
+ file: gtk-common-themes_1198.snap
+`), 0644)
+ c.Assert(err, IsNil)
+
+ _, err = snap.Parser(snap.Client()).ParseArgs([]string{"debug", "validate-seed", tmpf})
+ c.Assert(err, ErrorMatches, "cannot read seed yaml: empty element in seed")
+}
diff -pruN 2.37.4-1/cmd/snap/cmd_disconnect_test.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_disconnect_test.go
--- 2.37.4-1/cmd/snap/cmd_disconnect_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_disconnect_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -172,7 +172,7 @@ func (s *SnapSuite) TestDisconnectEveryt
func (s *SnapSuite) TestDisconnectCompletion(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
- case "/v2/interfaces":
+ case "/v2/connections":
c.Assert(r.Method, Equals, "GET")
EncodeResponseBody(c, w, map[string]interface{}{
"type": "sync",
diff -pruN 2.37.4-1/cmd/snap/cmd_find.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_find.go
--- 2.37.4-1/cmd/snap/cmd_find.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_find.go 2019-06-05 06:41:21.000000000 +0000
@@ -224,9 +224,9 @@ func (x *cmdFind) Execute(args []string)
}
opts := &client.FindOptions{
- Private: x.Private,
- Section: string(x.Section),
Query: x.Positional.Query,
+ Section: string(x.Section),
+ Private: x.Private,
}
if !x.Narrow {
diff -pruN 2.37.4-1/cmd/snap/cmd_get_base_declaration.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_get_base_declaration.go
--- 2.37.4-1/cmd/snap/cmd_get_base_declaration.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_get_base_declaration.go 2019-06-05 06:41:21.000000000 +0000
@@ -26,15 +26,24 @@ import (
)
type cmdGetBaseDeclaration struct {
+ get bool
clientMixin
}
func init() {
cmd := addDebugCommand("get-base-declaration",
+ "(internal) obtain the base declaration for all interfaces (deprecated)",
+ "(internal) obtain the base declaration for all interfaces (deprecated)",
+ func() flags.Commander {
+ return &cmdGetBaseDeclaration{}
+ }, nil, nil)
+ cmd.hidden = true
+
+ cmd = addDebugCommand("base-declaration",
"(internal) obtain the base declaration for all interfaces",
"(internal) obtain the base declaration for all interfaces",
func() flags.Commander {
- return &cmdGetBaseDeclaration{}
+ return &cmdGetBaseDeclaration{get: true}
}, nil, nil)
cmd.hidden = true
}
@@ -46,7 +55,13 @@ func (x *cmdGetBaseDeclaration) Execute(
var resp struct {
BaseDeclaration string `json:"base-declaration"`
}
- if err := x.client.Debug("get-base-declaration", nil, &resp); err != nil {
+ var err error
+ if x.get {
+ err = x.client.DebugGet("base-declaration", &resp, nil)
+ } else {
+ err = x.client.Debug("get-base-declaration", nil, &resp)
+ }
+ if err != nil {
return err
}
fmt.Fprintf(Stdout, "%s\n", resp.BaseDeclaration)
diff -pruN 2.37.4-1/cmd/snap/cmd_get_base_declaration_test.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_get_base_declaration_test.go
--- 2.37.4-1/cmd/snap/cmd_get_base_declaration_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_get_base_declaration_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -53,3 +53,28 @@ func (s *SnapSuite) TestGetBaseDeclarati
c.Check(s.Stdout(), check.Equals, "hello\n")
c.Check(s.Stderr(), check.Equals, "")
}
+
+func (s *SnapSuite) TestBaseDeclaration(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/debug")
+ c.Check(r.URL.RawQuery, check.Equals, "aspect=base-declaration")
+ data, err := ioutil.ReadAll(r.Body)
+ c.Check(err, check.IsNil)
+ c.Check(data, check.HasLen, 0)
+ fmt.Fprintln(w, `{"type": "sync", "result": {"base-declaration": "hello"}}`)
+ default:
+ c.Fatalf("expected to get 1 requests, now on %d", n+1)
+ }
+
+ n++
+ })
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "base-declaration"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, "hello\n")
+ c.Check(s.Stderr(), check.Equals, "")
+}
diff -pruN 2.37.4-1/cmd/snap/cmd_help.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_help.go
--- 2.37.4-1/cmd/snap/cmd_help.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_help.go 2019-06-05 06:41:21.000000000 +0000
@@ -22,6 +22,8 @@ package main
import (
"bytes"
"fmt"
+ "io"
+ "regexp"
"strings"
"unicode/utf8"
@@ -69,7 +71,7 @@ type cmdHelp struct {
Manpage bool `long:"man" hidden:"true"`
Positional struct {
// TODO: find a way to make Command tab-complete
- Sub string `positional-arg-name:""`
+ Subs []string `positional-arg-name:""`
} `positional-args:"yes"`
parser *flags.Parser
}
@@ -88,10 +90,11 @@ func (cmd *cmdHelp) setParser(parser *fl
cmd.parser = parser
}
-// manfixer is a hackish way to get the generated manpage into section 8
-// (go-flags doesn't have an option for this; I'll be proposing something
-// there soon, but still waiting on some other PRs to make it through)
+// manfixer is a hackish way to fix drawbacks in the generated manpage:
+// - no way to get it into section 8
+// - duplicated TP lines that break older groff (e.g. 14.04), lp:1814767
type manfixer struct {
+ bytes.Buffer
done bool
}
@@ -100,13 +103,20 @@ func (w *manfixer) Write(buf []byte) (in
w.done = true
if bytes.HasPrefix(buf, []byte(".TH snap 1 ")) {
// io.Writer.Write must not modify the buffer, even temporarily
- n, _ := Stdout.Write(buf[:9])
- Stdout.Write([]byte{'8'})
- m, err := Stdout.Write(buf[10:])
+ n, _ := w.Buffer.Write(buf[:9])
+ w.Buffer.Write([]byte{'8'})
+ m, err := w.Buffer.Write(buf[10:])
return n + m + 1, err
}
}
- return Stdout.Write(buf)
+ return w.Buffer.Write(buf)
+}
+
+var tpRegexp = regexp.MustCompile(`(?m)(?:^\.TP\n)+`)
+
+func (w *manfixer) flush() {
+ str := tpRegexp.ReplaceAllLiteralString(w.Buffer.String(), ".TP\n")
+ io.Copy(Stdout, strings.NewReader(str))
}
func (cmd cmdHelp) Execute(args []string) error {
@@ -116,27 +126,36 @@ func (cmd cmdHelp) Execute(args []string
if cmd.Manpage {
// you shouldn't try to to combine --man with --all nor a
// subcommand, but --man is hidden so no real need to check.
- cmd.parser.WriteManPage(&manfixer{})
+ out := &manfixer{}
+ cmd.parser.WriteManPage(out)
+ out.flush()
return nil
}
if cmd.All {
- if cmd.Positional.Sub != "" {
+ if len(cmd.Positional.Subs) > 0 {
return fmt.Errorf(i18n.G("help accepts a command, or '--all', but not both."))
}
printLongHelp(cmd.parser)
return nil
}
- if cmd.Positional.Sub != "" {
- subcmd := cmd.parser.Find(cmd.Positional.Sub)
+ var subcmd = cmd.parser.Command
+ for _, subname := range cmd.Positional.Subs {
+ subcmd = subcmd.Find(subname)
if subcmd == nil {
- return fmt.Errorf(i18n.G("Unknown command %q. Try 'snap help'."), cmd.Positional.Sub)
+ sug := "snap help"
+ if x := cmd.parser.Command.Active; x != nil && x.Name != "help" {
+ sug = "snap help " + x.Name
+ }
+ // TRANSLATORS: %q is the command the user entered; %s is 'snap help' or 'snap help '
+ return fmt.Errorf(i18n.G("unknown command %q, see '%s'."), subname, sug)
}
// this makes "snap help foo" work the same as "snap foo --help"
cmd.parser.Command.Active = subcmd
+ }
+ if subcmd != cmd.parser.Command {
return &flags.Error{Type: flags.ErrHelp}
}
-
return &flags.Error{Type: flags.ErrCommandRequired}
}
@@ -179,7 +198,7 @@ var helpCategories = []helpCategory{
}, {
Label: i18n.G("Permissions"),
Description: i18n.G("manage permissions"),
- Commands: []string{"interfaces", "interface", "connect", "disconnect"},
+ Commands: []string{"connections", "interface", "connect", "disconnect"},
}, {
Label: i18n.G("Snapshots"),
Description: i18n.G("archives of snap data"),
@@ -187,11 +206,11 @@ var helpCategories = []helpCategory{
}, {
Label: i18n.G("Other"),
Description: i18n.G("miscellanea"),
- Commands: []string{"version", "warnings", "okay"},
+ Commands: []string{"version", "warnings", "okay", "ack", "known"},
}, {
Label: i18n.G("Development"),
Description: i18n.G("developer-oriented features"),
- Commands: []string{"run", "pack", "try", "ack", "known", "download"},
+ Commands: []string{"run", "pack", "try", "download", "prepare-image"},
},
}
diff -pruN 2.37.4-1/cmd/snap/cmd_help_test.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_help_test.go
--- 2.37.4-1/cmd/snap/cmd_help_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_help_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -174,3 +174,33 @@ func (s *SnapSuite) TestManpageInSection
c.Check(s.Stdout(), check.Matches, `\.TH snap 8 (?s).*`)
}
+
+func (s *SnapSuite) TestManpageNoDoubleTP(c *check.C) {
+ origArgs := os.Args
+ defer func() { os.Args = origArgs }()
+ os.Args = []string{"snap", "help", "--man"}
+
+ err := snap.RunMain()
+ c.Assert(err, check.IsNil)
+
+ c.Check(s.Stdout(), check.Not(check.Matches), `(?s).*(?m-s)^\.TP\n\.TP$(?s-m).*`)
+
+}
+
+func (s *SnapSuite) TestBadSub(c *check.C) {
+ origArgs := os.Args
+ defer func() { os.Args = origArgs }()
+ os.Args = []string{"snap", "debug", "brotato"}
+
+ err := snap.RunMain()
+ c.Assert(err, check.ErrorMatches, `unknown command "brotato", see 'snap help debug'.`)
+}
+
+func (s *SnapSuite) TestWorseSub(c *check.C) {
+ origArgs := os.Args
+ defer func() { os.Args = origArgs }()
+ os.Args = []string{"snap", "-h", "debug", "brotato"}
+
+ err := snap.RunMain()
+ c.Assert(err, check.ErrorMatches, `unknown command "brotato", see 'snap help debug'.`)
+}
diff -pruN 2.37.4-1/cmd/snap/cmd_info.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_info.go
--- 2.37.4-1/cmd/snap/cmd_info.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_info.go 2019-06-05 06:41:21.000000000 +0000
@@ -23,6 +23,7 @@ import (
"fmt"
"io"
"path/filepath"
+ "strconv"
"strings"
"text/tabwriter"
"time"
@@ -118,7 +119,7 @@ func maybePrintBase(w io.Writer, base st
}
}
-func tryDirect(w io.Writer, path string, verbose bool) bool {
+func tryDirect(w io.Writer, path string, verbose bool, termWidth int) bool {
path = norm(path)
snapf, err := snap.Open(path)
@@ -141,7 +142,7 @@ func tryDirect(w io.Writer, path string,
}
fmt.Fprintf(w, "path:\t%q\n", path)
fmt.Fprintf(w, "name:\t%s\n", info.InstanceName())
- fmt.Fprintf(w, "summary:\t%s\n", formatSummary(info.Summary()))
+ printSummary(w, info.Summary(), termWidth)
var notes *Notes
if verbose {
@@ -195,9 +196,38 @@ func runesLastIndexSpace(text []rune) in
return -1
}
-// wrapLine wraps a line to fit into width, preserving the line's indent, and
+// wrapLine wraps a line, assumed to be part of a block-style yaml
+// string, to fit into termWidth, preserving the line's indent, and
// writes it out prepending padding to each line.
-func wrapLine(out io.Writer, text []rune, pad string, width int) error {
+func wrapLine(out io.Writer, text []rune, pad string, termWidth int) error {
+ // discard any trailing whitespace
+ text = runesTrimRightSpace(text)
+ // establish the indent of the whole block
+ idx := 0
+ for idx < len(text) && unicode.IsSpace(text[idx]) {
+ idx++
+ }
+ indent := pad + string(text[:idx])
+ text = text[idx:]
+ if len(indent) > termWidth/2 {
+ // If indent is too big there's not enough space for the actual
+ // text, in the pathological case the indent can even be bigger
+ // than the terminal which leads to lp:1828425.
+ // Rather than let that happen, give up.
+ indent = pad + " "
+ }
+ return wrapGeneric(out, text, indent, indent, termWidth)
+}
+
+// wrapFlow wraps the text using yaml's flow style, allowing indent
+// characters for the first line.
+func wrapFlow(out io.Writer, text []rune, indent string, termWidth int) error {
+ return wrapGeneric(out, text, indent, " ", termWidth)
+}
+
+// wrapGeneric wraps the given text to the given width, prefixing the
+// first line with indent and the remaining lines with indent2
+func wrapGeneric(out io.Writer, text []rune, indent, indent2 string, termWidth int) error {
// Note: this is _wrong_ for much of unicode (because the width of a rune on
// the terminal is anything between 0 and 2, not always 1 as this code
// assumes) but fixing that is Hard. Long story short, you can get close
@@ -210,16 +240,12 @@ func wrapLine(out io.Writer, text []rune
// This (and possibly printDescr below) should move to strutil once
// we're happy with it getting wider (heh heh) use.
- // discard any trailing whitespace
- text = runesTrimRightSpace(text)
+ indentWidth := utf8.RuneCountInString(indent)
+ delta := indentWidth - utf8.RuneCountInString(indent2)
+ width := termWidth - indentWidth
+
// establish the indent of the whole block
idx := 0
- for idx < len(text) && unicode.IsSpace(text[idx]) {
- idx++
- }
- indent := pad + string(text[:idx])
- text = text[idx:]
- width -= idx + utf8.RuneCountInString(pad)
var err error
for len(text) > width && err == nil {
// find a good place to chop the text
@@ -234,6 +260,9 @@ func wrapLine(out io.Writer, text []rune
idx++
}
text = text[idx:]
+ width += delta
+ indent = indent2
+ delta = 0
}
if err != nil {
return err
@@ -242,6 +271,21 @@ func wrapLine(out io.Writer, text []rune
return err
}
+func printSummary(w io.Writer, raw string, termWidth int) error {
+ // simplest way of checking to see if it needs quoting is to try
+ raw = strings.TrimSpace(raw)
+ type T struct {
+ S string
+ }
+ if len(raw) == 0 {
+ raw = `""`
+ } else if err := yaml.UnmarshalStrict([]byte("s: "+raw), &T{}); err != nil {
+ raw = strconv.Quote(raw)
+ }
+
+ return wrapFlow(w, []rune(raw), "summary:\t", termWidth)
+}
+
// printDescr formats a given string (typically a snap description)
// in a user friendly way.
//
@@ -249,11 +293,11 @@ func wrapLine(out io.Writer, text []rune
// - trim trailing whitespace
// - word wrap at "max" chars preserving line indent
// - keep \n intact and break there
-func printDescr(w io.Writer, descr string, max int) error {
+func printDescr(w io.Writer, descr string, termWidth int) error {
var err error
descr = strings.TrimRightFunc(descr, unicode.IsSpace)
for _, line := range strings.Split(descr, "\n") {
- err = wrapLine(w, []rune(line), " ", max)
+ err = wrapLine(w, []rune(line), " ", termWidth)
if err != nil {
break
}
@@ -321,21 +365,59 @@ func maybePrintServices(w io.Writer, sna
var channelRisks = []string{"stable", "candidate", "beta", "edge"}
-// displayChannels displays channels and tracks in the right order
-func (x *infoCmd) displayChannels(w io.Writer, chantpl string, esc *escapes, remote *client.Snap, revLen, sizeLen int) (maxRevLen, maxSizeLen int) {
- fmt.Fprintln(w, "channels:")
+type channelInfo struct {
+ indent, name, version, released, revision, size, notes string
+}
+
+type channelInfos struct {
+ channels []*channelInfo
+ maxRevLen, maxSizeLen int
+ releasedfmt, chantpl string
+ needsHeader bool
+ esc *escapes
+}
- releasedfmt := "2006-01-02"
- if x.AbsTime {
- releasedfmt = time.RFC3339
+func (chInfos *channelInfos) add(indent, name, version string, revision snap.Revision, released time.Time, size int64, notes *Notes) {
+ chInfo := &channelInfo{
+ indent: indent,
+ name: name,
+ version: version,
+ revision: fmt.Sprintf("(%s)", revision),
+ size: strutil.SizeToStr(size),
+ notes: notes.String(),
+ }
+ if !released.IsZero() {
+ chInfo.released = released.Format(chInfos.releasedfmt)
+ }
+ if len(chInfo.revision) > chInfos.maxRevLen {
+ chInfos.maxRevLen = len(chInfo.revision)
}
+ if len(chInfo.size) > chInfos.maxSizeLen {
+ chInfos.maxSizeLen = len(chInfo.size)
+ }
+ chInfos.channels = append(chInfos.channels, chInfo)
+}
+
+func (chInfos *channelInfos) addFromLocal(local *client.Snap) {
+ chInfos.add("", "installed", local.Version, local.Revision, time.Time{}, local.InstalledSize, NotesFromLocal(local))
+}
+
+func (chInfos *channelInfos) addOpenChannel(name, version string, revision snap.Revision, released time.Time, size int64, notes *Notes) {
+ chInfos.add(" ", name, version, revision, released, size, notes)
+}
- type chInfoT struct {
- name, version, released, revision, size, notes string
+func (chInfos *channelInfos) addClosedChannel(name string, trackHasOpenChannel bool) {
+ chInfo := &channelInfo{indent: " ", name: name}
+ if trackHasOpenChannel {
+ chInfo.version = chInfos.esc.uparrow
+ } else {
+ chInfo.version = chInfos.esc.dash
}
- var chInfos []*chInfoT
- maxRevLen, maxSizeLen = revLen, sizeLen
+ chInfos.channels = append(chInfos.channels, chInfo)
+}
+
+func (chInfos *channelInfos) addFromRemote(remote *client.Snap) {
// order by tracks
for _, tr := range remote.Tracks {
trackHasOpenChannel := false
@@ -345,44 +427,24 @@ func (x *infoCmd) displayChannels(w io.W
if tr == "latest" {
chName = risk
}
- chInfo := chInfoT{name: chName}
if ok {
- chInfo.version = ch.Version
- chInfo.revision = fmt.Sprintf("(%s)", ch.Revision)
- if len(chInfo.revision) > maxRevLen {
- maxRevLen = len(chInfo.revision)
- }
- chInfo.released = ch.ReleasedAt.Format(releasedfmt)
- chInfo.size = strutil.SizeToStr(ch.Size)
- if len(chInfo.size) > maxSizeLen {
- maxSizeLen = len(chInfo.size)
- }
- chInfo.notes = NotesFromChannelSnapInfo(ch).String()
+ chInfos.addOpenChannel(chName, ch.Version, ch.Revision, ch.ReleasedAt, ch.Size, NotesFromChannelSnapInfo(ch))
trackHasOpenChannel = true
} else {
- if trackHasOpenChannel {
- chInfo.version = esc.uparrow
- } else {
- chInfo.version = esc.dash
- }
+ chInfos.addClosedChannel(chName, trackHasOpenChannel)
}
- chInfos = append(chInfos, &chInfo)
}
}
-
- for _, chInfo := range chInfos {
- fmt.Fprintf(w, " "+chantpl, chInfo.name, chInfo.version, chInfo.released, maxRevLen, chInfo.revision, maxSizeLen, chInfo.size, chInfo.notes)
- }
-
- return maxRevLen, maxSizeLen
+ chInfos.needsHeader = len(chInfos.channels) > 0
}
-func formatSummary(raw string) string {
- s, err := yaml.Marshal(raw)
- if err != nil {
- return fmt.Sprintf("cannot marshal summary: %s", err)
+func (chInfos *channelInfos) dump(w io.Writer) {
+ if chInfos.needsHeader {
+ fmt.Fprintln(w, "channels:")
+ }
+ for _, c := range chInfos.channels {
+ fmt.Fprintf(w, chInfos.chantpl, c.indent, c.name, c.version, c.released, chInfos.maxRevLen, c.revision, chInfos.maxSizeLen, c.size, c.notes)
}
- return strings.TrimSpace(string(s))
}
func (x *infoCmd) Execute([]string) error {
@@ -407,7 +469,7 @@ func (x *infoCmd) Execute([]string) erro
continue
}
- if tryDirect(w, snapName, x.Verbose) {
+ if tryDirect(w, snapName, x.Verbose, termWidth) {
noneOK = false
continue
}
@@ -427,7 +489,7 @@ func (x *infoCmd) Execute([]string) erro
noneOK = false
fmt.Fprintf(w, "name:\t%s\n", both.Name)
- fmt.Fprintf(w, "summary:\t%s\n", formatSummary(both.Summary))
+ printSummary(w, both.Summary, termWidth)
fmt.Fprintf(w, "publisher:\t%s\n", longPublisher(esc, both.Publisher))
if both.Contact != "" {
fmt.Fprintf(w, "contact:\t%s\n", strings.TrimPrefix(both.Contact, "mailto:"))
@@ -449,7 +511,6 @@ func (x *infoCmd) Execute([]string) erro
fmt.Fprintf(w, " confinement:\t%s\n", both.Confinement)
}
- var notes *Notes
if local != nil {
if x.Verbose {
jailMode := local.Confinement == client.DevModeConfinement && !local.DevMode
@@ -464,8 +525,6 @@ func (x *infoCmd) Execute([]string) erro
}
fmt.Fprintf(w, " ignore-validation:\t%t\n", local.IgnoreValidation)
- } else {
- notes = NotesFromLocal(local)
}
}
// stops the notes etc trying to be aligned with channels
@@ -473,8 +532,6 @@ func (x *infoCmd) Execute([]string) erro
maybePrintType(w, both.Type)
maybePrintBase(w, both.Base, x.Verbose)
maybePrintID(w, both)
- var localRev, localSize string
- var revLen, sizeLen int
if local != nil {
if local.TrackingChannel != "" {
fmt.Fprintf(w, "tracking:\t%s\n", local.TrackingChannel)
@@ -482,24 +539,25 @@ func (x *infoCmd) Execute([]string) erro
if !local.InstallDate.IsZero() {
fmt.Fprintf(w, "refresh-date:\t%s\n", x.fmtTime(local.InstallDate))
}
- localRev = fmt.Sprintf("(%s)", local.Revision)
- revLen = len(localRev)
- localSize = strutil.SizeToStr(local.InstalledSize)
- sizeLen = len(localSize)
}
- chantpl := "%s:\t%s %s%*s %*s %s\n"
+ chInfos := channelInfos{
+ chantpl: "%s%s:\t%s %s%*s %*s %s\n",
+ releasedfmt: "2006-01-02",
+ esc: esc,
+ }
+ if x.AbsTime {
+ chInfos.releasedfmt = time.RFC3339
+ }
if remote != nil && remote.Channels != nil && remote.Tracks != nil {
- chantpl = "%s:\t%s\t%s\t%*s\t%*s\t%s\n"
-
w.Flush()
- revLen, sizeLen = x.displayChannels(w, chantpl, esc, remote, revLen, sizeLen)
+ chInfos.chantpl = "%s%s:\t%s\t%s\t%*s\t%*s\t%s\n"
+ chInfos.addFromRemote(remote)
}
if local != nil {
- fmt.Fprintf(w, chantpl,
- "installed", local.Version, "", revLen, localRev, sizeLen, localSize, notes)
+ chInfos.addFromLocal(local)
}
-
+ chInfos.dump(w)
}
w.Flush()
diff -pruN 2.37.4-1/cmd/snap/cmd_info_test.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_info_test.go
--- 2.37.4-1/cmd/snap/cmd_info_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_info_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -97,6 +97,45 @@ func (s *infoSuite) TestMaybePrintComman
}
}
+func (s *infoSuite) TestInfoPricedNarrowTerminal(c *check.C) {
+ defer snap.MockTermSize(func() (int, int) { return 44, 25 })()
+
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/find")
+ fmt.Fprintln(w, findPricedJSON)
+ case 1:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps/hello")
+ fmt.Fprintln(w, "{}")
+ default:
+ c.Fatalf("expected to get 1 requests, now on %d (%v)", n+1, r)
+ }
+
+ n++
+ })
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"info", "hello"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, `
+name: hello
+summary: GNU Hello, the "hello world"
+ snap
+publisher: Canonical*
+license: Proprietary
+price: 1.99GBP
+description: |
+ GNU hello prints a friendly greeting.
+ This is part of the snapcraft tour at
+ https://snapcraft.io/
+snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6
+`[1:])
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
func (s *infoSuite) TestInfoPriced(c *check.C) {
n := 0
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
@@ -550,3 +589,35 @@ func (infoSuite) TestDescr(c *check.C) {
c.Check(buf.String(), check.Equals, v, check.Commentf("%q", k))
}
}
+
+func (infoSuite) TestWrapCornerCase(c *check.C) {
+ // this particular corner case isn't currently reachable from
+ // printDescr nor printSummary, but best to have it covered
+ var buf bytes.Buffer
+ const s = "This is a paragraph indented with leading spaces that are encoded as multiple bytes. All hail EN SPACE."
+ snap.WrapFlow(&buf, []rune(s), "\u2002\u2002", 30)
+ c.Check(buf.String(), check.Equals, `
+ This is a paragraph indented
+ with leading spaces that are
+ encoded as multiple bytes.
+ All hail EN SPACE.
+`[1:])
+}
+
+func (infoSuite) TestBug1828425(c *check.C) {
+ const s = `This is a description
+ that has
+ lines
+ too deeply
+ indented.
+`
+ var buf bytes.Buffer
+ err := snap.PrintDescr(&buf, s, 30)
+ c.Assert(err, check.IsNil)
+ c.Check(buf.String(), check.Equals, ` This is a description
+ that has
+ lines
+ too deeply
+ indented.
+`)
+}
diff -pruN 2.37.4-1/cmd/snap/cmd_interfaces.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_interfaces.go
--- 2.37.4-1/cmd/snap/cmd_interfaces.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_interfaces.go 2019-06-05 06:41:21.000000000 +0000
@@ -22,6 +22,7 @@ package main
import (
"fmt"
+ "github.com/snapcore/snapd/client"
"github.com/snapcore/snapd/i18n"
"github.com/jessevdk/go-flags"
@@ -53,10 +54,13 @@ $ snap interfaces -i= [
Filters the complete output so only plugs and/or slots matching the provided
details are listed.
+
+NOTE this command is deprecated and has been replaced with the 'connections'
+ command.
`)
func init() {
- addCommand("interfaces", shortInterfacesHelp, longInterfacesHelp, func() flags.Commander {
+ cmd := addCommand("interfaces", shortInterfacesHelp, longInterfacesHelp, func() flags.Commander {
return &cmdInterfaces{}
}, map[string]string{
// TRANSLATORS: This should not start with a lowercase letter.
@@ -67,26 +71,35 @@ func init() {
// TRANSLATORS: This should not start with a lowercase letter.
desc: i18n.G("Constrain listing to a specific snap or snap:name"),
}})
+ cmd.hidden = true
}
+var interfacesDeprecationNotice = i18n.G("'snap interfaces' is deprecated; use 'snap connections'.")
+
func (x *cmdInterfaces) Execute(args []string) error {
if len(args) > 0 {
return ErrExtraArgs
}
- ifaces, err := x.client.Connections()
+ opts := client.ConnectionOptions{
+ All: true,
+ Snap: x.Positionals.Query.Snap,
+ }
+ ifaces, err := x.client.Connections(&opts)
if err != nil {
return err
}
if len(ifaces.Plugs) == 0 && len(ifaces.Slots) == 0 {
return fmt.Errorf(i18n.G("no interfaces found"))
}
+
+ defer fmt.Fprintln(Stderr, "\n"+fill(interfacesDeprecationNotice, 0))
+
w := tabWriter()
defer w.Flush()
fmt.Fprintln(w, i18n.G("Slot\tPlug"))
wantedSnap := x.Positionals.Query.Snap
-
for _, slot := range ifaces.Slots {
if wantedSnap != "" {
var ok bool
diff -pruN 2.37.4-1/cmd/snap/cmd_interfaces_test.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_interfaces_test.go
--- 2.37.4-1/cmd/snap/cmd_interfaces_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_interfaces_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -29,12 +29,13 @@ import (
"github.com/snapcore/snapd/client"
. "github.com/snapcore/snapd/cmd/snap"
+ "github.com/snapcore/snapd/testutil"
)
-func (s *SnapSuite) TestConnectionsZeroSlotsOnePlug(c *C) {
+func (s *SnapSuite) TestInterfacesZeroSlotsOnePlug(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, Equals, "GET")
- c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
body, err := ioutil.ReadAll(r.Body)
c.Check(err, IsNil)
c.Check(body, DeepEquals, []byte{})
@@ -57,13 +58,13 @@ func (s *SnapSuite) TestConnectionsZeroS
"Slot Plug\n" +
"- keyboard-lights:capslock-led\n"
c.Assert(s.Stdout(), Equals, expectedStdout)
- c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice)
}
-func (s *SnapSuite) TestConnectionsZeroPlugsOneSlot(c *C) {
+func (s *SnapSuite) TestInterfacesZeroPlugsOneSlot(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, Equals, "GET")
- c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
body, err := ioutil.ReadAll(r.Body)
c.Check(err, IsNil)
c.Check(body, DeepEquals, []byte{})
@@ -88,13 +89,13 @@ func (s *SnapSuite) TestConnectionsZeroP
"Slot Plug\n" +
"canonical-pi2:pin-13 -\n"
c.Assert(s.Stdout(), Equals, expectedStdout)
- c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice)
}
-func (s *SnapSuite) TestConnectionsOneSlotOnePlug(c *C) {
+func (s *SnapSuite) TestInterfacesOneSlotOnePlug(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, Equals, "GET")
- c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
body, err := ioutil.ReadAll(r.Body)
c.Check(err, IsNil)
c.Check(body, DeepEquals, []byte{})
@@ -139,7 +140,7 @@ func (s *SnapSuite) TestConnectionsOneSl
"Slot Plug\n" +
"canonical-pi2:pin-13 keyboard-lights:capslock-led\n"
c.Assert(s.Stdout(), Equals, expectedStdout)
- c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice)
s.SetUpTest(c)
// should be the same
@@ -147,7 +148,7 @@ func (s *SnapSuite) TestConnectionsOneSl
c.Assert(err, IsNil)
c.Assert(rest, DeepEquals, []string{})
c.Assert(s.Stdout(), Equals, expectedStdout)
- c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice)
s.SetUpTest(c)
// and the same again
@@ -155,13 +156,13 @@ func (s *SnapSuite) TestConnectionsOneSl
c.Assert(err, IsNil)
c.Assert(rest, DeepEquals, []string{})
c.Assert(s.Stdout(), Equals, expectedStdout)
- c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice)
}
-func (s *SnapSuite) TestConnectionsTwoPlugs(c *C) {
+func (s *SnapSuite) TestInterfacesTwoPlugs(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, Equals, "GET")
- c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
body, err := ioutil.ReadAll(r.Body)
c.Check(err, IsNil)
c.Check(body, DeepEquals, []byte{})
@@ -196,13 +197,13 @@ func (s *SnapSuite) TestConnectionsTwoPl
"Slot Plug\n" +
"canonical-pi2:pin-13 keyboard-lights:capslock-led,keyboard-lights:scrollock-led\n"
c.Assert(s.Stdout(), Equals, expectedStdout)
- c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice)
}
-func (s *SnapSuite) TestConnectionsPlugsWithCommonName(c *C) {
+func (s *SnapSuite) TestInterfacesPlugsWithCommonName(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, Equals, "GET")
- c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
body, err := ioutil.ReadAll(r.Body)
c.Check(err, IsNil)
c.Check(body, DeepEquals, []byte{})
@@ -263,13 +264,13 @@ func (s *SnapSuite) TestConnectionsPlugs
"Slot Plug\n" +
"canonical-pi2:network-listening paste-daemon,time-daemon\n"
c.Assert(s.Stdout(), Equals, expectedStdout)
- c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice)
}
-func (s *SnapSuite) TestConnectionsOsSnapSlots(c *C) {
+func (s *SnapSuite) TestInterfacesOsSnapSlots(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, Equals, "GET")
- c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
body, err := ioutil.ReadAll(r.Body)
c.Check(err, IsNil)
c.Check(body, DeepEquals, []byte{})
@@ -330,13 +331,13 @@ func (s *SnapSuite) TestConnectionsOsSna
"Slot Plug\n" +
":network-listening paste-daemon,time-daemon\n"
c.Assert(s.Stdout(), Equals, expectedStdout)
- c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice)
}
-func (s *SnapSuite) TestConnectionsTwoSlotsAndFiltering(c *C) {
+func (s *SnapSuite) TestInterfacesTwoSlotsAndFiltering(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, Equals, "GET")
- c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
body, err := ioutil.ReadAll(r.Body)
c.Check(err, IsNil)
c.Check(body, DeepEquals, []byte{})
@@ -379,13 +380,13 @@ func (s *SnapSuite) TestConnectionsTwoSl
"Slot Plug\n" +
"canonical-pi2:debug-console core\n"
c.Assert(s.Stdout(), Equals, expectedStdout)
- c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice)
}
-func (s *SnapSuite) TestConnectionsOfSpecificSnap(c *C) {
+func (s *SnapSuite) TestInterfacesOfSpecificSnap(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, Equals, "GET")
- c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
body, err := ioutil.ReadAll(r.Body)
c.Check(err, IsNil)
c.Check(body, DeepEquals, []byte{})
@@ -423,13 +424,13 @@ func (s *SnapSuite) TestConnectionsOfSpe
"wake-up-alarm:toggle -\n" +
"wake-up-alarm:snooze -\n"
c.Assert(s.Stdout(), Equals, expectedStdout)
- c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice)
}
-func (s *SnapSuite) TestConnectionsOfSystemNicknameSnap(c *C) {
+func (s *SnapSuite) TestInterfacesOfSystemNicknameSnap(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, Equals, "GET")
- c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
body, err := ioutil.ReadAll(r.Body)
c.Check(err, IsNil)
c.Check(body, DeepEquals, []byte{})
@@ -466,7 +467,7 @@ func (s *SnapSuite) TestConnectionsOfSys
"Slot Plug\n" +
":core-support core:core-support-plug\n"
c.Assert(s.Stdout(), Equals, expectedStdout)
- c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice)
s.ResetStdStreams()
@@ -478,13 +479,13 @@ func (s *SnapSuite) TestConnectionsOfSys
"Slot Plug\n" +
":core-support core:core-support-plug\n"
c.Assert(s.Stdout(), Equals, expectedStdoutSystem)
- c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice)
}
-func (s *SnapSuite) TestConnectionsOfSpecificSnapAndSlot(c *C) {
+func (s *SnapSuite) TestInterfacesOfSpecificSnapAndSlot(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, Equals, "GET")
- c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
body, err := ioutil.ReadAll(r.Body)
c.Check(err, IsNil)
c.Check(body, DeepEquals, []byte{})
@@ -521,13 +522,13 @@ func (s *SnapSuite) TestConnectionsOfSpe
"Slot Plug\n" +
"wake-up-alarm:snooze -\n"
c.Assert(s.Stdout(), Equals, expectedStdout)
- c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice)
}
-func (s *SnapSuite) TestConnectionsNothingAtAll(c *C) {
+func (s *SnapSuite) TestInterfacesNothingAtAll(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, Equals, "GET")
- c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
body, err := ioutil.ReadAll(r.Body)
c.Check(err, IsNil)
c.Check(body, DeepEquals, []byte{})
@@ -545,10 +546,10 @@ func (s *SnapSuite) TestConnectionsNothi
c.Assert(s.Stderr(), Equals, "")
}
-func (s *SnapSuite) TestConnectionsOfSpecificType(c *C) {
+func (s *SnapSuite) TestInterfacesOfSpecificType(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, Equals, "GET")
- c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
body, err := ioutil.ReadAll(r.Body)
c.Check(err, IsNil)
c.Check(body, DeepEquals, []byte{})
@@ -587,13 +588,13 @@ func (s *SnapSuite) TestConnectionsOfSpe
"wake-up-alarm:toggle -\n" +
"wake-up-alarm:snooze -\n"
c.Assert(s.Stdout(), Equals, expectedStdout)
- c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice)
}
-func (s *SnapSuite) TestConnectionsCompletion(c *C) {
+func (s *SnapSuite) TestInterfacesCompletion(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
- case "/v2/interfaces":
+ case "/v2/connections":
c.Assert(r.Method, Equals, "GET")
EncodeResponseBody(c, w, map[string]interface{}{
"type": "sync",
@@ -628,26 +629,26 @@ func (s *SnapSuite) TestConnectionsCompl
c.Assert(s.Stderr(), Equals, "")
}
-func (s *SnapSuite) TestConnectionsCoreNicknamedSystem(c *C) {
+func (s *SnapSuite) TestInterfacesCoreNicknamedSystem(c *C) {
s.checkConnectionsSystemCoreRemapping(c, "core", "system")
}
-func (s *SnapSuite) TestConnectionsSnapdNicknamedSystem(c *C) {
+func (s *SnapSuite) TestInterfacesSnapdNicknamedSystem(c *C) {
s.checkConnectionsSystemCoreRemapping(c, "snapd", "system")
}
-func (s *SnapSuite) TestConnectionsSnapdNicknamedCore(c *C) {
+func (s *SnapSuite) TestInterfacesSnapdNicknamedCore(c *C) {
s.checkConnectionsSystemCoreRemapping(c, "snapd", "core")
}
-func (s *SnapSuite) TestConnectionsCoreSnap(c *C) {
+func (s *SnapSuite) TestInterfacesCoreSnap(c *C) {
s.checkConnectionsSystemCoreRemapping(c, "core", "core")
}
func (s *SnapSuite) checkConnectionsSystemCoreRemapping(c *C, apiSnapName, cliSnapName string) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, Equals, "GET")
- c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
body, err := ioutil.ReadAll(r.Body)
c.Check(err, IsNil)
c.Check(body, DeepEquals, []byte{})
@@ -670,5 +671,5 @@ func (s *SnapSuite) checkConnectionsSyst
"Slot Plug\n" +
":network -\n"
c.Assert(s.Stdout(), Equals, expectedStdout)
- c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice)
}
diff -pruN 2.37.4-1/cmd/snap/cmd_known.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_known.go
--- 2.37.4-1/cmd/snap/cmd_known.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_known.go 2019-06-05 06:41:21.000000000 +0000
@@ -73,7 +73,7 @@ func downloadAssertion(typeName string,
var user *auth.UserState
// FIXME: set auth context
- var authContext auth.AuthContext
+ var storeCtx store.DeviceAndAuthContext
at := asserts.Type(typeName)
if at == nil {
@@ -84,7 +84,7 @@ func downloadAssertion(typeName string,
return nil, fmt.Errorf("cannot query remote assertion: %v", err)
}
- sto := storeNew(nil, authContext)
+ sto := storeNew(nil, storeCtx)
as, err := sto.Assertion(at, primaryKeys, user)
if err != nil {
return nil, err
diff -pruN 2.37.4-1/cmd/snap/cmd_known_test.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_known_test.go
--- 2.37.4-1/cmd/snap/cmd_known_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_known_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -28,7 +28,6 @@ import (
"github.com/jessevdk/go-flags"
"gopkg.in/check.v1"
- "github.com/snapcore/snapd/overlord/auth"
"github.com/snapcore/snapd/store"
snap "github.com/snapcore/snapd/cmd/snap"
@@ -53,13 +52,13 @@ AcLorsomethingthatlooksvaguelylikeasigna
func (s *SnapSuite) TestKnownRemote(c *check.C) {
var server *httptest.Server
- restorer := snap.MockStoreNew(func(cfg *store.Config, auth auth.AuthContext) *store.Store {
+ restorer := snap.MockStoreNew(func(cfg *store.Config, stoCtx store.DeviceAndAuthContext) *store.Store {
if cfg == nil {
cfg = store.DefaultConfig()
}
serverURL, _ := url.Parse(server.URL)
cfg.AssertionsBaseURL = serverURL
- return store.New(cfg, auth)
+ return store.New(cfg, stoCtx)
})
defer restorer()
diff -pruN 2.37.4-1/cmd/snap/cmd_prepare_image.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_prepare_image.go
--- 2.37.4-1/cmd/snap/cmd_prepare_image.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_prepare_image.go 2019-06-05 06:41:21.000000000 +0000
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2014-2016 Canonical Ltd
+ * Copyright (C) 2014-2019 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
@@ -21,6 +21,7 @@ package main
import (
"path/filepath"
+ "strings"
"github.com/jessevdk/go-flags"
@@ -29,27 +30,41 @@ import (
)
type cmdPrepareImage struct {
+ Classic bool `long:"classic"`
+ Architecture string `long:"arch"`
+
Positional struct {
ModelAssertionFn string
Rootdir string
} `positional-args:"yes" required:"yes"`
- ExtraSnaps []string `long:"extra-snaps"`
- Channel string `long:"channel" default:"stable"`
+ Channel string `long:"channel" default:"stable"`
+ // TODO: introduce SnapWithChannel?
+ Snaps []string `long:"snap" value-name:"[=]"`
+ ExtraSnaps []string `long:"extra-snaps" hidden:"yes"` // DEPRECATED
}
func init() {
- cmd := addCommand("prepare-image",
- i18n.G("Prepare a core device image"),
+ addCommand("prepare-image",
+ i18n.G("Prepare a device image"),
i18n.G(`
-The prepare-image command performs some of the steps necessary for creating
-core device images.
-`),
- func() flags.Commander {
- return &cmdPrepareImage{}
- }, map[string]string{
+The prepare-image command performs some of the steps necessary for
+creating device images.
+
+For core images it is not invoked directly but usually via
+ubuntu-image.
+
+For preparing classic images it supports a --classic mode`),
+ func() flags.Commander { return &cmdPrepareImage{} },
+ map[string]string{
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "classic": i18n.G("Enable classic mode to prepare a classic model image"),
// TRANSLATORS: This should not start with a lowercase letter.
- "extra-snaps": i18n.G("Extra snaps to be installed"),
+ "arch": i18n.G("Specify an architecture for snaps for --classic when the model does not"),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "snap": i18n.G("Include the given snap from the store or a local file and/or specify the channel to track for the given snap"),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "extra-snaps": i18n.G("Extra snaps to be installed (DEPRECATED)"),
// TRANSLATORS: This should not start with a lowercase letter.
"channel": i18n.G("The channel to use"),
}, []argDesc{
@@ -62,21 +77,47 @@ core device images.
// TRANSLATORS: This needs to begin with < and end with >
name: i18n.G(""),
// TRANSLATORS: This should not start with a lowercase letter.
- desc: i18n.G("The output directory"),
+ desc: i18n.G("The target directory"),
},
})
- cmd.hidden = true
}
+var imagePrepare = image.Prepare
+
func (x *cmdPrepareImage) Execute(args []string) error {
opts := &image.Options{
- ModelFile: x.Positional.ModelAssertionFn,
+ Snaps: x.ExtraSnaps,
+ ModelFile: x.Positional.ModelAssertionFn,
+ Channel: x.Channel,
+ Architecture: x.Architecture,
+ }
+
+ snaps := make([]string, 0, len(x.Snaps)+len(x.ExtraSnaps))
+ snapChannels := make(map[string]string)
+ for _, snapWChannel := range x.Snaps {
+ snapAndChannel := strings.SplitN(snapWChannel, "=", 2)
+ snaps = append(snaps, snapAndChannel[0])
+ if len(snapAndChannel) == 2 {
+ snapChannels[snapAndChannel[0]] = snapAndChannel[1]
+ }
+ }
+
+ snaps = append(snaps, x.ExtraSnaps...)
+
+ if len(snaps) != 0 {
+ opts.Snaps = snaps
+ }
+ if len(snapChannels) != 0 {
+ opts.SnapChannels = snapChannels
+ }
- RootDir: filepath.Join(x.Positional.Rootdir, "image"),
- GadgetUnpackDir: filepath.Join(x.Positional.Rootdir, "gadget"),
- Channel: x.Channel,
- Snaps: x.ExtraSnaps,
+ if x.Classic {
+ opts.Classic = true
+ opts.RootDir = x.Positional.Rootdir
+ } else {
+ opts.RootDir = filepath.Join(x.Positional.Rootdir, "image")
+ opts.GadgetUnpackDir = filepath.Join(x.Positional.Rootdir, "gadget")
}
- return image.Prepare(opts)
+ return imagePrepare(opts)
}
diff -pruN 2.37.4-1/cmd/snap/cmd_prepare_image_test.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_prepare_image_test.go
--- 2.37.4-1/cmd/snap/cmd_prepare_image_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_prepare_image_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,120 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+ "github.com/snapcore/snapd/image"
+)
+
+type SnapPrepareImageSuite struct {
+ BaseSnapSuite
+}
+
+var _ = Suite(&SnapPrepareImageSuite{})
+
+func (s *SnapPrepareImageSuite) TestPrepareImageCore(c *C) {
+ var opts *image.Options
+ prep := func(o *image.Options) error {
+ opts = o
+ return nil
+ }
+ r := snap.MockImagePrepare(prep)
+ defer r()
+
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"prepare-image", "model", "root-dir"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+
+ c.Check(opts, DeepEquals, &image.Options{
+ ModelFile: "model",
+ Channel: "stable",
+ RootDir: "root-dir/image",
+ GadgetUnpackDir: "root-dir/gadget",
+ })
+}
+
+func (s *SnapPrepareImageSuite) TestPrepareImageClassic(c *C) {
+ var opts *image.Options
+ prep := func(o *image.Options) error {
+ opts = o
+ return nil
+ }
+ r := snap.MockImagePrepare(prep)
+ defer r()
+
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"prepare-image", "--classic", "model", "root-dir"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+
+ c.Check(opts, DeepEquals, &image.Options{
+ Classic: true,
+ ModelFile: "model",
+ Channel: "stable",
+ RootDir: "root-dir",
+ })
+}
+
+func (s *SnapPrepareImageSuite) TestPrepareImageClassicArch(c *C) {
+ var opts *image.Options
+ prep := func(o *image.Options) error {
+ opts = o
+ return nil
+ }
+ r := snap.MockImagePrepare(prep)
+ defer r()
+
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"prepare-image", "--classic", "--arch", "i386", "model", "root-dir"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+
+ c.Check(opts, DeepEquals, &image.Options{
+ Classic: true,
+ Architecture: "i386",
+ ModelFile: "model",
+ Channel: "stable",
+ RootDir: "root-dir",
+ })
+}
+
+func (s *SnapPrepareImageSuite) TestPrepareImageExtraSnaps(c *C) {
+ var opts *image.Options
+ prep := func(o *image.Options) error {
+ opts = o
+ return nil
+ }
+ r := snap.MockImagePrepare(prep)
+ defer r()
+
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"prepare-image", "model", "root-dir", "--snap", "foo", "--snap", "bar=t/edge", "--snap", "local.snap", "--extra-snaps", "local2.snap", "--extra-snaps", "store-snap"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+
+ c.Check(opts, DeepEquals, &image.Options{
+ ModelFile: "model",
+ Channel: "stable",
+ RootDir: "root-dir/image",
+ GadgetUnpackDir: "root-dir/gadget",
+ Snaps: []string{"foo", "bar", "local.snap", "local2.snap", "store-snap"},
+ SnapChannels: map[string]string{"bar": "t/edge"},
+ })
+}
diff -pruN 2.37.4-1/cmd/snap/cmd_remodel.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_remodel.go
--- 2.37.4-1/cmd/snap/cmd_remodel.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_remodel.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,86 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "io/ioutil"
+
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/i18n"
+)
+
+var (
+ shortRemodelHelp = i18n.G("Remodel this device")
+ longRemodelHelp = i18n.G(`
+The remodel command changes the model assertion of the device, either to a new
+revision or a full new model.
+
+In the process it applies any implied changes to the device: new required
+snaps, new kernel or gadget etc.
+`)
+)
+
+type cmdRemodel struct {
+ waitMixin
+ RemodelOptions struct {
+ NewModelFile flags.Filename
+ } `positional-args:"true" required:"true"`
+}
+
+func init() {
+ cmd := addCommand("remodel",
+ shortRemodelHelp,
+ longRemodelHelp,
+ func() flags.Commander {
+ return &cmdRemodel{}
+ }, nil, []argDesc{{
+ // TRANSLATORS: This needs to begin with < and end with >
+ name: i18n.G(""),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ desc: i18n.G("New model file"),
+ }})
+ cmd.hidden = true
+}
+
+func (x *cmdRemodel) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+ newModelFile := x.RemodelOptions.NewModelFile
+ modelData, err := ioutil.ReadFile(string(newModelFile))
+ if err != nil {
+ return err
+ }
+ changeID, err := x.client.Remodel(modelData)
+ if err != nil {
+ return fmt.Errorf("cannot remodel: %v", err)
+ }
+
+ if _, err := x.wait(changeID); err != nil {
+ if err == noWait {
+ return nil
+ }
+ return err
+ }
+ fmt.Fprintf(Stdout, i18n.G("New model %s set"), newModelFile)
+ return nil
+}
diff -pruN 2.37.4-1/cmd/snap/cmd_run.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_run.go
--- 2.37.4-1/cmd/snap/cmd_run.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_run.go 2019-06-05 06:41:21.000000000 +0000
@@ -313,6 +313,14 @@ func createOrUpdateUserDataSymlink(info
}
func createUserDataDirs(info *snap.Info) error {
+ // Adjust umask so that the created directories have the permissions we
+ // expect and are unaffected by the initial umask. While go runtime creates
+ // threads at will behind the scenes, the setting of umask applies to the
+ // entire process so it doesn't need any special handling to lock the
+ // executing goroutine to a single thread.
+ oldUmask := syscall.Umask(0)
+ defer syscall.Umask(oldUmask)
+
usr, err := userCurrent()
if err != nil {
return fmt.Errorf(i18n.G("cannot get the current user: %v"), err)
@@ -448,13 +456,32 @@ func (x *cmdRun) snapRunTimer(snapApp, t
var osReadlink = os.Readlink
-func isReexeced() bool {
+// snapdHelperPath return the path of a helper like "snap-confine" or
+// "snap-exec" based on if snapd is re-execed or not
+func snapdHelperPath(toolName string) (string, error) {
exe, err := osReadlink("/proc/self/exe")
if err != nil {
- logger.Noticef("cannot read /proc/self/exe: %v", err)
- return false
+ return "", fmt.Errorf("cannot read /proc/self/exe: %v", err)
}
- return strings.HasPrefix(exe, dirs.SnapMountDir)
+ // no re-exec
+ if !strings.HasPrefix(exe, dirs.SnapMountDir) {
+ return filepath.Join(dirs.DistroLibExecDir, toolName), nil
+ }
+ // The logic below only works if the last two path components
+ // are /usr/bin
+ // FIXME: use a snap warning?
+ if !strings.HasSuffix(exe, "/usr/bin/"+filepath.Base(exe)) {
+ logger.Noticef("(internal error): unexpected exe input in snapdHelperPath: %v", exe)
+ return filepath.Join(dirs.DistroLibExecDir, toolName), nil
+ }
+ // snapBase will be "/snap/{core,snapd}/$rev/" because
+ // the snap binary is always at $root/usr/bin/snap
+ snapBase := filepath.Clean(filepath.Join(filepath.Dir(exe), "..", ".."))
+ // Run snap-confine from the core/snapd snap. The tools in
+ // core/snapd snap are statically linked, or mostly
+ // statically, with the exception of libraries such as libudev
+ // and libc.
+ return filepath.Join(snapBase, dirs.CoreLibExecDir, toolName), nil
}
func migrateXauthority(info *snap.Info) (string, error) {
@@ -841,24 +868,10 @@ func (x *cmdRun) runCmdUnderStrace(origC
}
func (x *cmdRun) runSnapConfine(info *snap.Info, securityTag, snapApp, hook string, args []string) error {
- snapConfine := filepath.Join(dirs.DistroLibExecDir, "snap-confine")
- // if we re-exec, we must run the snap-confine from the core/snapd snap
- // as well, if they get out of sync, havoc will happen
- if isReexeced() {
- // exe is something like /snap/{snapd,core}/123/usr/bin/snap
- exe, err := osReadlink("/proc/self/exe")
- if err != nil {
- return err
- }
- // snapBase will be "/snap/{core,snapd}/$rev/" because
- // the snap binary is always at $root/usr/bin/snap
- snapBase := filepath.Clean(filepath.Join(filepath.Dir(exe), "..", ".."))
- // Run snap-confine from the core/snapd snap. That
- // will work because snap-confine on the core/snapd snap is
- // mostly statically linked (except libudev and libc)
- snapConfine = filepath.Join(snapBase, dirs.CoreLibExecDir, "snap-confine")
+ snapConfine, err := snapdHelperPath("snap-confine")
+ if err != nil {
+ return err
}
-
if !osutil.FileExists(snapConfine) {
if hook != "" {
logger.Noticef("WARNING: skipping running hook %q of snap %q: missing snap-confine", hook, info.InstanceName())
@@ -895,14 +908,9 @@ func (x *cmdRun) runSnapConfine(info *sn
if info.NeedsClassic() {
// running with classic confinement, carefully pick snap-exec we
// are going to use
- if isReexeced() {
- // same rule as when choosing the location of snap-confine
- snapExecPath = filepath.Join(dirs.SnapMountDir, "core/current",
- dirs.CoreLibExecDir, "snap-exec")
- } else {
- // there is no mount namespace where 'core' is the
- // rootfs, hence we need to use distro's snap-exec
- snapExecPath = filepath.Join(dirs.DistroLibExecDir, "snap-exec")
+ snapExecPath, err = snapdHelperPath("snap-exec")
+ if err != nil {
+ return err
}
}
cmd = append(cmd, snapExecPath)
diff -pruN 2.37.4-1/cmd/snap/cmd_run_test.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_run_test.go
--- 2.37.4-1/cmd/snap/cmd_run_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_run_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -50,7 +50,25 @@ hooks:
configure:
`)
-func (s *SnapSuite) TestInvalidParameters(c *check.C) {
+type RunSuite struct {
+ fakeHome string
+ SnapSuite
+}
+
+var _ = check.Suite(&RunSuite{})
+
+func (s *RunSuite) SetUpTest(c *check.C) {
+ s.SnapSuite.SetUpTest(c)
+ s.fakeHome = c.MkDir()
+
+ u, err := user.Current()
+ c.Assert(err, check.IsNil)
+ s.AddCleanup(snaprun.MockUserCurrent(func() (*user.User, error) {
+ return &user.User{Uid: u.Uid, HomeDir: s.fakeHome}, nil
+ }))
+}
+
+func (s *RunSuite) TestInvalidParameters(c *check.C) {
invalidParameters := []string{"run", "--hook=configure", "--command=command-name", "--", "snap-name"}
_, err := snaprun.Parser(snaprun.Client()).ParseArgs(invalidParameters)
c.Check(err, check.ErrorMatches, ".*you can only use one of --hook, --command, and --timer.*")
@@ -76,7 +94,7 @@ func (s *SnapSuite) TestInvalidParameter
c.Check(err, check.ErrorMatches, ".*too many arguments for hook \"configure\": bar.*")
}
-func (s *SnapSuite) TestSnapRunWhenMissingConfine(c *check.C) {
+func (s *RunSuite) TestSnapRunWhenMissingConfine(c *check.C) {
_, r := logger.MockLogger()
defer r()
@@ -105,7 +123,7 @@ func (s *SnapSuite) TestSnapRunWhenMissi
c.Check(execs, check.IsNil)
}
-func (s *SnapSuite) TestSnapRunAppIntegration(c *check.C) {
+func (s *RunSuite) TestSnapRunAppIntegration(c *check.C) {
defer mockSnapConfine(dirs.DistroLibExecDir)()
// mock installed snap
@@ -138,7 +156,7 @@ func (s *SnapSuite) TestSnapRunAppIntegr
c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2")
}
-func (s *SnapSuite) TestSnapRunClassicAppIntegration(c *check.C) {
+func (s *RunSuite) TestSnapRunClassicAppIntegration(c *check.C) {
defer mockSnapConfine(dirs.DistroLibExecDir)()
// mock installed snap
@@ -172,7 +190,7 @@ func (s *SnapSuite) TestSnapRunClassicAp
}
-func (s *SnapSuite) TestSnapRunClassicAppIntegrationReexeced(c *check.C) {
+func (s *RunSuite) TestSnapRunClassicAppIntegrationReexecedFromCore(c *check.C) {
mountedCorePath := filepath.Join(dirs.SnapMountDir, "core/current")
mountedCoreLibExecPath := filepath.Join(mountedCorePath, dirs.CoreLibExecDir)
@@ -205,7 +223,40 @@ func (s *SnapSuite) TestSnapRunClassicAp
"snapname.app", "--arg1", "arg2"})
}
-func (s *SnapSuite) TestSnapRunAppWithCommandIntegration(c *check.C) {
+func (s *RunSuite) TestSnapRunClassicAppIntegrationReexecedFromSnapd(c *check.C) {
+ mountedSnapdPath := filepath.Join(dirs.SnapMountDir, "snapd/current")
+ mountedSnapdLibExecPath := filepath.Join(mountedSnapdPath, dirs.CoreLibExecDir)
+
+ defer mockSnapConfine(mountedSnapdLibExecPath)()
+
+ // mock installed snap
+ snaptest.MockSnapCurrent(c, string(mockYaml)+"confinement: classic\n", &snap.SideInfo{
+ Revision: snap.R("x2"),
+ })
+
+ restore := snaprun.MockOsReadlink(func(name string) (string, error) {
+ // pretend 'snap' is reexeced from 'core'
+ return filepath.Join(mountedSnapdPath, "usr/bin/snap"), nil
+ })
+ defer restore()
+
+ execArgs := []string{}
+ restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ execArgs = args
+ return nil
+ })
+ defer restorer()
+ rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app", "--arg1", "arg2"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"})
+ c.Check(execArgs, check.DeepEquals, []string{
+ filepath.Join(mountedSnapdLibExecPath, "snap-confine"), "--classic",
+ "snap.snapname.app",
+ filepath.Join(mountedSnapdLibExecPath, "snap-exec"),
+ "snapname.app", "--arg1", "arg2"})
+}
+
+func (s *RunSuite) TestSnapRunAppWithCommandIntegration(c *check.C) {
defer mockSnapConfine(dirs.DistroLibExecDir)()
// mock installed snap
@@ -237,48 +288,36 @@ func (s *SnapSuite) TestSnapRunAppWithCo
c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42")
}
-func (s *SnapSuite) TestSnapRunCreateDataDirs(c *check.C) {
+func (s *RunSuite) TestSnapRunCreateDataDirs(c *check.C) {
info, err := snap.InfoFromSnapYaml(mockYaml)
c.Assert(err, check.IsNil)
info.SideInfo.Revision = snap.R(42)
- fakeHome := c.MkDir()
- restorer := snaprun.MockUserCurrent(func() (*user.User, error) {
- return &user.User{HomeDir: fakeHome}, nil
- })
- defer restorer()
-
err = snaprun.CreateUserDataDirs(info)
c.Assert(err, check.IsNil)
- c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname/42")), check.Equals, true)
- c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname/common")), check.Equals, true)
+ c.Check(osutil.FileExists(filepath.Join(s.fakeHome, "/snap/snapname/42")), check.Equals, true)
+ c.Check(osutil.FileExists(filepath.Join(s.fakeHome, "/snap/snapname/common")), check.Equals, true)
}
-func (s *SnapSuite) TestParallelInstanceSnapRunCreateDataDirs(c *check.C) {
+func (s *RunSuite) TestParallelInstanceSnapRunCreateDataDirs(c *check.C) {
info, err := snap.InfoFromSnapYaml(mockYaml)
c.Assert(err, check.IsNil)
info.SideInfo.Revision = snap.R(42)
info.InstanceKey = "foo"
- fakeHome := c.MkDir()
- restorer := snaprun.MockUserCurrent(func() (*user.User, error) {
- return &user.User{HomeDir: fakeHome}, nil
- })
- defer restorer()
-
err = snaprun.CreateUserDataDirs(info)
c.Assert(err, check.IsNil)
- c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname_foo/42")), check.Equals, true)
- c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname_foo/common")), check.Equals, true)
+ c.Check(osutil.FileExists(filepath.Join(s.fakeHome, "/snap/snapname_foo/42")), check.Equals, true)
+ c.Check(osutil.FileExists(filepath.Join(s.fakeHome, "/snap/snapname_foo/common")), check.Equals, true)
// mount point for snap instance mapping has been created
- c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname")), check.Equals, true)
+ c.Check(osutil.FileExists(filepath.Join(s.fakeHome, "/snap/snapname")), check.Equals, true)
// and it's empty inside
- m, err := filepath.Glob(filepath.Join(fakeHome, "/snap/snapname/*"))
+ m, err := filepath.Glob(filepath.Join(s.fakeHome, "/snap/snapname/*"))
c.Assert(err, check.IsNil)
c.Assert(m, check.HasLen, 0)
}
-func (s *SnapSuite) TestSnapRunHookIntegration(c *check.C) {
+func (s *RunSuite) TestSnapRunHookIntegration(c *check.C) {
defer mockSnapConfine(dirs.DistroLibExecDir)()
// mock installed snap
@@ -310,7 +349,7 @@ func (s *SnapSuite) TestSnapRunHookInteg
c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42")
}
-func (s *SnapSuite) TestSnapRunHookUnsetRevisionIntegration(c *check.C) {
+func (s *RunSuite) TestSnapRunHookUnsetRevisionIntegration(c *check.C) {
defer mockSnapConfine(dirs.DistroLibExecDir)()
// mock installed snap
@@ -342,7 +381,7 @@ func (s *SnapSuite) TestSnapRunHookUnset
c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42")
}
-func (s *SnapSuite) TestSnapRunHookSpecificRevisionIntegration(c *check.C) {
+func (s *RunSuite) TestSnapRunHookSpecificRevisionIntegration(c *check.C) {
defer mockSnapConfine(dirs.DistroLibExecDir)()
// mock installed snap
@@ -378,7 +417,7 @@ func (s *SnapSuite) TestSnapRunHookSpeci
c.Check(execEnv, testutil.Contains, "SNAP_REVISION=41")
}
-func (s *SnapSuite) TestSnapRunHookMissingRevisionIntegration(c *check.C) {
+func (s *RunSuite) TestSnapRunHookMissingRevisionIntegration(c *check.C) {
// Only create revision 42
snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{
Revision: snap.R(42),
@@ -396,13 +435,13 @@ func (s *SnapSuite) TestSnapRunHookMissi
c.Check(err, check.ErrorMatches, "cannot find .*")
}
-func (s *SnapSuite) TestSnapRunHookInvalidRevisionIntegration(c *check.C) {
+func (s *RunSuite) TestSnapRunHookInvalidRevisionIntegration(c *check.C) {
_, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=configure", "-r=invalid", "--", "snapname"})
c.Assert(err, check.NotNil)
c.Check(err, check.ErrorMatches, "invalid snap revision: \"invalid\"")
}
-func (s *SnapSuite) TestSnapRunHookMissingHookIntegration(c *check.C) {
+func (s *RunSuite) TestSnapRunHookMissingHookIntegration(c *check.C) {
// Only create revision 42
snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{
Revision: snap.R(42),
@@ -421,22 +460,22 @@ func (s *SnapSuite) TestSnapRunHookMissi
c.Check(called, check.Equals, false)
}
-func (s *SnapSuite) TestSnapRunErorsForUnknownRunArg(c *check.C) {
+func (s *RunSuite) TestSnapRunErorsForUnknownRunArg(c *check.C) {
_, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--unknown", "--", "snapname.app", "--arg1", "arg2"})
c.Assert(err, check.ErrorMatches, "unknown flag `unknown'")
}
-func (s *SnapSuite) TestSnapRunErorsForMissingApp(c *check.C) {
+func (s *RunSuite) TestSnapRunErorsForMissingApp(c *check.C) {
_, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--command=shell"})
c.Assert(err, check.ErrorMatches, "need the application to run as argument")
}
-func (s *SnapSuite) TestSnapRunErorrForUnavailableApp(c *check.C) {
+func (s *RunSuite) TestSnapRunErorrForUnavailableApp(c *check.C) {
_, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "not-there"})
c.Assert(err, check.ErrorMatches, fmt.Sprintf("cannot find current revision for snap not-there: readlink %s/not-there/current: no such file or directory", dirs.SnapMountDir))
}
-func (s *SnapSuite) TestSnapRunSaneEnvironmentHandling(c *check.C) {
+func (s *RunSuite) TestSnapRunSaneEnvironmentHandling(c *check.C) {
defer mockSnapConfine(dirs.DistroLibExecDir)()
// mock installed snap
@@ -471,26 +510,39 @@ func (s *SnapSuite) TestSnapRunSaneEnvir
c.Check(execEnv, testutil.Contains, "SNAP_THE_WORLD=YES")
}
-func (s *SnapSuite) TestSnapRunIsReexeced(c *check.C) {
+func (s *RunSuite) TestSnapRunSnapdHelperPath(c *check.C) {
var osReadlinkResult string
restore := snaprun.MockOsReadlink(func(name string) (string, error) {
return osReadlinkResult, nil
})
defer restore()
+ tool := "snap-confine"
for _, t := range []struct {
readlink string
- expected bool
+ expected string
}{
- {filepath.Join(dirs.SnapMountDir, dirs.CoreLibExecDir, "snapd"), true},
- {filepath.Join(dirs.DistroLibExecDir, "snapd"), false},
+ {
+ filepath.Join(dirs.SnapMountDir, "core/current/usr/bin/snap"),
+ filepath.Join(dirs.SnapMountDir, "core/current", dirs.CoreLibExecDir, tool),
+ },
+ {
+ filepath.Join(dirs.SnapMountDir, "snapd/current/usr/bin/snap"),
+ filepath.Join(dirs.SnapMountDir, "snapd/current", dirs.CoreLibExecDir, tool),
+ },
+ {
+ filepath.Join("/usr/bin/snap"),
+ filepath.Join(dirs.DistroLibExecDir, tool),
+ },
} {
osReadlinkResult = t.readlink
- c.Check(snaprun.IsReexeced(), check.Equals, t.expected)
+ toolPath, err := snaprun.SnapdHelperPath(tool)
+ c.Assert(err, check.IsNil)
+ c.Check(toolPath, check.Equals, t.expected)
}
}
-func (s *SnapSuite) TestSnapRunAppIntegrationFromCore(c *check.C) {
+func (s *RunSuite) TestSnapRunAppIntegrationFromCore(c *check.C) {
defer mockSnapConfine(filepath.Join(dirs.SnapMountDir, "core", "111", dirs.CoreLibExecDir))()
// mock installed snap
@@ -529,7 +581,7 @@ func (s *SnapSuite) TestSnapRunAppIntegr
c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2")
}
-func (s *SnapSuite) TestSnapRunAppIntegrationFromSnapd(c *check.C) {
+func (s *RunSuite) TestSnapRunAppIntegrationFromSnapd(c *check.C) {
defer mockSnapConfine(filepath.Join(dirs.SnapMountDir, "snapd", "222", dirs.CoreLibExecDir))()
// mock installed snap
@@ -568,7 +620,7 @@ func (s *SnapSuite) TestSnapRunAppIntegr
c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2")
}
-func (s *SnapSuite) TestSnapRunXauthorityMigration(c *check.C) {
+func (s *RunSuite) TestSnapRunXauthorityMigration(c *check.C) {
defer mockSnapConfine(dirs.DistroLibExecDir)()
u, err := user.Current()
@@ -644,7 +696,7 @@ func mkCompArgs(compPoint string, argv .
return out
}
-func (s *SnapSuite) TestAntialiasHappy(c *check.C) {
+func (s *RunSuite) TestAntialiasHappy(c *check.C) {
c.Assert(os.MkdirAll(dirs.SnapBinariesDir, 0755), check.IsNil)
inArgs := mkCompArgs("10", "alias", "alias", "bo-alias")
@@ -672,7 +724,7 @@ func (s *SnapSuite) TestAntialiasHappy(c
})
}
-func (s *SnapSuite) TestAntialiasBailsIfUnhappy(c *check.C) {
+func (s *RunSuite) TestAntialiasBailsIfUnhappy(c *check.C) {
// alias exists but args are somehow wonky
c.Assert(os.MkdirAll(dirs.SnapBinariesDir, 0755), check.IsNil)
c.Assert(os.Symlink("an-app", filepath.Join(dirs.SnapBinariesDir, "alias")), check.IsNil)
@@ -701,7 +753,7 @@ func (s *SnapSuite) TestAntialiasBailsIf
}
}
-func (s *SnapSuite) TestSnapRunAppWithStraceIntegration(c *check.C) {
+func (s *RunSuite) TestSnapRunAppWithStraceIntegration(c *check.C) {
defer mockSnapConfine(dirs.DistroLibExecDir)()
// mock installed snap
@@ -781,7 +833,7 @@ and more
c.Check(s.Stderr(), check.Equals, fmt.Sprintf(expectedFullFmt, dirs.SnapMountDir))
}
-func (s *SnapSuite) TestSnapRunAppWithStraceOptions(c *check.C) {
+func (s *RunSuite) TestSnapRunAppWithStraceOptions(c *check.C) {
defer mockSnapConfine(dirs.DistroLibExecDir)()
// mock installed snap
@@ -822,7 +874,7 @@ func (s *SnapSuite) TestSnapRunAppWithSt
})
}
-func (s *SnapSuite) TestSnapRunShellIntegration(c *check.C) {
+func (s *RunSuite) TestSnapRunShellIntegration(c *check.C) {
defer mockSnapConfine(dirs.DistroLibExecDir)()
// mock installed snap
@@ -855,7 +907,7 @@ func (s *SnapSuite) TestSnapRunShellInte
c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2")
}
-func (s *SnapSuite) TestSnapRunAppTimer(c *check.C) {
+func (s *RunSuite) TestSnapRunAppTimer(c *check.C) {
defer mockSnapConfine(dirs.DistroLibExecDir)()
// mock installed snap
@@ -910,7 +962,7 @@ func (s *SnapSuite) TestSnapRunAppTimer(
"snapname.app", "--arg1", "arg2"})
}
-func (s *SnapSuite) TestRunCmdWithTraceExecUnhappy(c *check.C) {
+func (s *RunSuite) TestRunCmdWithTraceExecUnhappy(c *check.C) {
defer mockSnapConfine(dirs.DistroLibExecDir)()
// mock installed snap
@@ -933,7 +985,7 @@ func (s *SnapSuite) TestRunCmdWithTraceE
c.Check(s.Stderr(), check.Equals, "")
}
-func (s *SnapSuite) TestSnapRunRestoreSecurityContextHappy(c *check.C) {
+func (s *RunSuite) TestSnapRunRestoreSecurityContextHappy(c *check.C) {
logbuf, restorer := logger.MockLogger()
defer restorer()
@@ -944,12 +996,6 @@ func (s *SnapSuite) TestSnapRunRestoreSe
Revision: snap.R("x2"),
})
- fakeHome := c.MkDir()
- restorer = snaprun.MockUserCurrent(func() (*user.User, error) {
- return &user.User{HomeDir: fakeHome}, nil
- })
- defer restorer()
-
// redirect exec
execCalled := 0
restorer = snaprun.MockSyscallExec(func(_ string, args []string, envv []string) error {
@@ -964,7 +1010,7 @@ func (s *SnapSuite) TestSnapRunRestoreSe
enabled := false
verify := true
- snapUserDir := filepath.Join(fakeHome, dirs.UserHomeSnapDir)
+ snapUserDir := filepath.Join(s.fakeHome, dirs.UserHomeSnapDir)
restorer = snaprun.MockSELinuxVerifyPathContext(func(what string) (bool, error) {
c.Check(what, check.Equals, snapUserDir)
@@ -1020,7 +1066,7 @@ func (s *SnapSuite) TestSnapRunRestoreSe
c.Check(logbuf.String(), testutil.Contains, fmt.Sprintf("restoring default SELinux context of %s", snapUserDir))
}
-func (s *SnapSuite) TestSnapRunRestoreSecurityContextFail(c *check.C) {
+func (s *RunSuite) TestSnapRunRestoreSecurityContextFail(c *check.C) {
logbuf, restorer := logger.MockLogger()
defer restorer()
@@ -1031,12 +1077,6 @@ func (s *SnapSuite) TestSnapRunRestoreSe
Revision: snap.R("x2"),
})
- fakeHome := c.MkDir()
- restorer = snaprun.MockUserCurrent(func() (*user.User, error) {
- return &user.User{HomeDir: fakeHome}, nil
- })
- defer restorer()
-
// redirect exec
execCalled := 0
restorer = snaprun.MockSyscallExec(func(_ string, args []string, envv []string) error {
@@ -1052,7 +1092,7 @@ func (s *SnapSuite) TestSnapRunRestoreSe
verifyErr := errors.New("verify failed")
restoreErr := errors.New("restore failed")
- snapUserDir := filepath.Join(fakeHome, dirs.UserHomeSnapDir)
+ snapUserDir := filepath.Join(s.fakeHome, dirs.UserHomeSnapDir)
restorer = snaprun.MockSELinuxVerifyPathContext(func(what string) (bool, error) {
c.Check(what, check.Equals, snapUserDir)
diff -pruN 2.37.4-1/cmd/snap/cmd_services_test.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_services_test.go
--- 2.37.4-1/cmd/snap/cmd_services_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_services_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -23,6 +23,8 @@ import (
"encoding/json"
"fmt"
"net/http"
+ "sort"
+ "strings"
"time"
"gopkg.in/check.v1"
@@ -222,6 +224,51 @@ foo.zed enabled active -
c.Check(n, check.Equals, 1)
}
+func (s *appOpSuite) TestServiceCompletion(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.URL.Path, check.Equals, "/v2/apps")
+ c.Check(r.URL.Query(), check.HasLen, 1)
+ c.Check(r.URL.Query().Get("select"), check.Equals, "service")
+ c.Check(r.Method, check.Equals, "GET")
+ w.WriteHeader(200)
+ enc := json.NewEncoder(w)
+ enc.Encode(map[string]interface{}{
+ "type": "sync",
+ "result": []map[string]interface{}{
+ {"snap": "a-snap", "name": "foo", "daemon": "simple"},
+ {"snap": "a-snap", "name": "bar", "daemon": "simple"},
+ {"snap": "b-snap", "name": "baz", "daemon": "simple"},
+ },
+ "status": "OK",
+ "status-code": 200,
+ })
+
+ n++
+ })
+
+ var comp = func(s string) string {
+ comps := snap.ServiceName("").Complete(s)
+ as := make([]string, len(comps))
+ for i := range comps {
+ as[i] = comps[i].Item
+ }
+ sort.Strings(as)
+ return strings.Join(as, " ")
+ }
+
+ c.Check(comp(""), check.Equals, "a-snap a-snap.bar a-snap.foo b-snap.baz")
+ c.Check(comp("a"), check.Equals, "a-snap a-snap.bar a-snap.foo")
+ c.Check(comp("a-snap"), check.Equals, "a-snap a-snap.bar a-snap.foo")
+ c.Check(comp("a-snap."), check.Equals, "a-snap.bar a-snap.foo")
+ c.Check(comp("a-snap.b"), check.Equals, "a-snap.bar")
+ c.Check(comp("b"), check.Equals, "b-snap.baz")
+ c.Check(comp("c"), check.Equals, "")
+
+ // ensure that the fake server api was actually hit
+ c.Check(n, check.Equals, 7)
+}
+
func (s *appOpSuite) TestAppStatusNoServices(c *check.C) {
n := 0
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
diff -pruN 2.37.4-1/cmd/snap/cmd_snap_op.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_snap_op.go
--- 2.37.4-1/cmd/snap/cmd_snap_op.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_snap_op.go 2019-06-05 06:41:21.000000000 +0000
@@ -32,8 +32,10 @@ import (
"github.com/jessevdk/go-flags"
"github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/i18n"
"github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/snap"
)
var (
@@ -257,6 +259,42 @@ func (mx *channelMixin) setChannelFromCo
return nil
}
+// isSnapInPath checks whether the snap binaries dir (e.g. /snap/bin)
+// is in $PATH.
+//
+// TODO: consider symlinks
+func isSnapInPath() bool {
+ paths := filepath.SplitList(os.Getenv("PATH"))
+ for _, path := range paths {
+ if filepath.Clean(path) == dirs.SnapBinariesDir {
+ return true
+ }
+ }
+ return false
+}
+
+func isSameRisk(tracking, current string) (bool, error) {
+ if tracking == current {
+ return true, nil
+ }
+ var trackingRisk, currentRisk string
+ if tracking != "" {
+ traCh, err := snap.ParseChannel(tracking, "")
+ if err != nil {
+ return false, err
+ }
+ trackingRisk = traCh.Risk
+ }
+ if current != "" {
+ curCh, err := snap.ParseChannel(current, "")
+ if err != nil {
+ return false, err
+ }
+ currentRisk = curCh.Risk
+ }
+ return trackingRisk == currentRisk, nil
+}
+
// show what has been done
func showDone(cli *client.Client, names []string, op string, opts *client.SnapOptions, esc *escapes) error {
snaps, err := cli.List(names, nil)
@@ -264,6 +302,7 @@ func showDone(cli *client.Client, names
return err
}
+ needsPathWarning := !isSnapInPath()
for _, snap := range snaps {
channelStr := ""
if snap.Channel != "" && snap.Channel != "stable" {
@@ -271,12 +310,19 @@ func showDone(cli *client.Client, names
}
switch op {
case "install":
+ if needsPathWarning {
+ head := i18n.G("Warning:")
+ warn := fill(fmt.Sprintf(i18n.G("%s was not found in your $PATH. If you've not restarted your session since you installed snapd, try doing that. Please see https://forum.snapcraft.io/t/9469 for more details."), dirs.SnapBinariesDir), utf8.RuneCountInString(head)+1) // +1 for the space
+ fmt.Fprint(Stderr, esc.bold, head, esc.end, " ", warn, "\n\n")
+ needsPathWarning = false
+ }
+
if opts != nil && opts.Classic && snap.Confinement != client.ClassicConfinement {
// requested classic but the snap is not classic
head := i18n.G("Warning:")
// TRANSLATORS: the arg is a snap name (e.g. "some-snap")
warn := fill(fmt.Sprintf(i18n.G("flag --classic ignored for strictly confined snap %s"), snap.Name), utf8.RuneCountInString(head)+1) // +1 for the space
- fmt.Fprint(Stderr, head, " ", warn, "\n\n")
+ fmt.Fprint(Stderr, esc.bold, head, esc.end, " ", warn, "\n\n")
}
if snap.Publisher != nil {
@@ -301,8 +347,10 @@ func showDone(cli *client.Client, names
fmt.Fprintf(Stdout, "internal error: unknown op %q", op)
}
if snap.TrackingChannel != snap.Channel && snap.Channel != "" {
- // TRANSLATORS: first %s is a channel name, following %s is a snap name, last %s is a channel name again.
- fmt.Fprintf(Stdout, i18n.G("Channel %s for %s is closed; temporarily forwarding to %s.\n"), snap.TrackingChannel, snap.Name, snap.Channel)
+ if sameRisk, err := isSameRisk(snap.TrackingChannel, snap.Channel); err == nil && !sameRisk {
+ // TRANSLATORS: first %s is a channel name, following %s is a snap name, last %s is a channel name again.
+ fmt.Fprintf(Stdout, i18n.G("Channel %s for %s is closed; temporarily forwarding to %s.\n"), snap.TrackingChannel, snap.Name, snap.Channel)
+ }
}
}
diff -pruN 2.37.4-1/cmd/snap/cmd_snap_op_test.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_snap_op_test.go
--- 2.37.4-1/cmd/snap/cmd_snap_op_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_snap_op_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -44,13 +44,14 @@ import (
type snapOpTestServer struct {
c *check.C
- checker func(r *http.Request)
- n int
- total int
- channel string
- confinement string
- rebooting bool
- snap string
+ checker func(r *http.Request)
+ n int
+ total int
+ channel string
+ trackingChannel string
+ confinement string
+ rebooting bool
+ snap string
}
var _ = check.Suite(&SnapOpSuite{})
@@ -81,7 +82,7 @@ func (t *snapOpTestServer) handle(w http
case 3:
t.c.Check(r.Method, check.Equals, "GET")
t.c.Check(r.URL.Path, check.Equals, "/v2/snaps")
- fmt.Fprintf(w, `{"type": "sync", "result": [{"name": "%s", "status": "active", "version": "1.0", "developer": "bar", "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"}, "revision":42, "channel":"%s", "confinement": "%s"}]}\n`, t.snap, t.channel, t.confinement)
+ fmt.Fprintf(w, `{"type": "sync", "result": [{"name": "%s", "status": "active", "version": "1.0", "developer": "bar", "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"}, "revision":42, "channel":"%s", "tracking-channel": "%s", "confinement": "%s"}]}\n`, t.snap, t.channel, t.trackingChannel, t.confinement)
default:
t.c.Fatalf("expected to get %d requests, now on %d", t.total, t.n+1)
}
@@ -206,6 +207,28 @@ func (s *SnapOpSuite) TestInstall(c *che
c.Check(s.srv.n, check.Equals, s.srv.total)
}
+func (s *SnapOpSuite) TestInstallNoPATH(c *check.C) {
+ // PATH restored by test tear down
+ os.Setenv("PATH", "/bin:/usr/bin:/sbin:/usr/sbin")
+ s.srv.checker = func(r *http.Request) {
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo")
+ c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
+ "action": "install",
+ "channel": "candidate",
+ })
+ s.srv.channel = "candidate"
+ }
+
+ s.RedirectClientToTestServer(s.srv.handle)
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel", "candidate", "foo"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(candidate\) 1.0 from Bar installed`)
+ c.Check(s.Stderr(), testutil.MatchesWrapped, `Warning: \S+/bin was not found in your \$PATH.*`)
+ // ensure that the fake server api was actually hit
+ c.Check(s.srv.n, check.Equals, s.srv.total)
+}
+
func (s *SnapOpSuite) TestInstallFromTrack(c *check.C) {
s.srv.checker = func(r *http.Request) {
c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo")
@@ -232,16 +255,81 @@ func (s *SnapOpSuite) TestInstallFromBra
c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo")
c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
"action": "install",
- "channel": "3.4/hotfix-1",
+ "channel": "3.4/stable/hotfix-1",
+ })
+ s.srv.channel = "3.4/stable/hotfix-1"
+ }
+
+ s.RedirectClientToTestServer(s.srv.handle)
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel", "3.4/stable/hotfix-1", "foo"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(3.4/stable/hotfix-1\) 1.0 from Bar installed`)
+ c.Check(s.Stderr(), check.Equals, "")
+ // ensure that the fake server api was actually hit
+ c.Check(s.srv.n, check.Equals, s.srv.total)
+}
+
+func (s *SnapOpSuite) TestInstallSameRiskInTrack(c *check.C) {
+ s.srv.checker = func(r *http.Request) {
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo")
+ c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
+ "action": "install",
+ "channel": "latest/stable",
})
- s.srv.channel = "3.4/hotfix-1"
+ s.srv.channel = "stable"
+ s.srv.trackingChannel = "latest/stable"
}
s.RedirectClientToTestServer(s.srv.handle)
- rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel", "3.4/hotfix-1", "foo"})
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel", "latest/stable", "foo"})
c.Assert(err, check.IsNil)
c.Assert(rest, check.DeepEquals, []string{})
- c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(3.4/hotfix-1\) 1.0 from Bar installed`)
+ c.Check(s.Stdout(), check.Equals, "foo 1.0 from Bar installed\n")
+ c.Check(s.Stderr(), check.Equals, "")
+ // ensure that the fake server api was actually hit
+ c.Check(s.srv.n, check.Equals, s.srv.total)
+}
+
+func (s *SnapOpSuite) TestInstallSameRiskInDefaultTrack(c *check.C) {
+ s.srv.checker = func(r *http.Request) {
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo")
+ c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
+ "action": "install",
+ "channel": "stable",
+ })
+ s.srv.channel = "18/stable"
+ s.srv.trackingChannel = "18/stable"
+ }
+
+ s.RedirectClientToTestServer(s.srv.handle)
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--stable", "foo"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, "foo (18/stable) 1.0 from Bar installed\n")
+ c.Check(s.Stderr(), check.Equals, "")
+ // ensure that the fake server api was actually hit
+ c.Check(s.srv.n, check.Equals, s.srv.total)
+}
+
+func (s *SnapOpSuite) TestInstallRiskChannelClosed(c *check.C) {
+ s.srv.checker = func(r *http.Request) {
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo")
+ c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
+ "action": "install",
+ "channel": "edge",
+ })
+ s.srv.channel = "stable"
+ s.srv.trackingChannel = "edge"
+ }
+
+ s.RedirectClientToTestServer(s.srv.handle)
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel", "edge", "foo"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, `foo 1.0 from Bar installed
+Channel edge for foo is closed; temporarily forwarding to stable.
+`)
c.Check(s.Stderr(), check.Equals, "")
// ensure that the fake server api was actually hit
c.Check(s.srv.n, check.Equals, s.srv.total)
@@ -1090,6 +1178,23 @@ func (s *SnapOpSuite) TestRefreshOneSwit
c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(beta\) 1.0 from Bar refreshed`)
}
+func (s *SnapOpSuite) TestRefreshOneWithPinnedTrack(c *check.C) {
+ s.RedirectClientToTestServer(s.srv.handle)
+ s.srv.checker = func(r *http.Request) {
+ c.Check(r.Method, check.Equals, "POST")
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo")
+ c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
+ "action": "refresh",
+ "channel": "stable",
+ })
+ s.srv.channel = "18/stable"
+ s.srv.trackingChannel = "18/stable"
+ }
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--stable", "foo"})
+ c.Assert(err, check.IsNil)
+ c.Check(s.Stdout(), check.Equals, "foo (18/stable) 1.0 from Bar refreshed\n")
+}
+
func (s *SnapOpSuite) TestRefreshOneClassic(c *check.C) {
s.RedirectClientToTestServer(s.srv.handle)
s.srv.checker = func(r *http.Request) {
diff -pruN 2.37.4-1/cmd/snap/cmd_snapshot.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_snapshot.go
--- 2.37.4-1/cmd/snap/cmd_snapshot.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_snapshot.go 2019-06-05 06:41:21.000000000 +0000
@@ -21,6 +21,8 @@ package main
import (
"fmt"
+ "strconv"
+ "strings"
"github.com/jessevdk/go-flags"
@@ -30,7 +32,7 @@ import (
)
func fmtSize(size int64) string {
- return quantity.FormatAmount(uint64(size), -1)
+ return quantity.FormatAmount(uint64(size), -1) + "B"
}
var (
@@ -106,7 +108,14 @@ type savedCmd struct {
}
func (x *savedCmd) Execute([]string) error {
- setID := uint64(x.ID)
+ var setID uint64
+ var err error
+ if x.ID != "" {
+ setID, err = x.ID.ToUint()
+ if err != nil {
+ return err
+ }
+ }
snaps := installedSnapNames(x.Positional.Snaps)
list, err := x.client.SnapshotSets(setID, snaps)
if err != nil {
@@ -133,12 +142,20 @@ func (x *savedCmd) Execute([]string) err
i18n.G("Notes"))
for _, sg := range list {
for _, sh := range sg.Snapshots {
- note := "-"
+ notes := []string{}
+ if sh.Auto {
+ notes = append(notes, "auto")
+ }
if sh.Broken != "" {
- note = "broken: " + sh.Broken
+ notes = append(notes, "broken: "+sh.Broken)
}
- size := quantity.FormatAmount(uint64(sh.Size), -1) + "B"
- fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%s\t%s\n", sg.ID, sh.Snap, x.fmtDuration(sh.Time), sh.Version, sh.Revision, size, note)
+ note := "-"
+ if len(notes) > 0 {
+ note = strings.Join(notes, ", ")
+ }
+ size := fmtSize(sh.Size)
+ age := x.fmtDuration(sh.Time)
+ fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%s\t%s\n", sg.ID, sh.Snap, age, sh.Version, sh.Revision, size, note)
}
}
return nil
@@ -170,7 +187,7 @@ func (x *saveCmd) Execute([]string) erro
y := &savedCmd{
clientMixin: x.clientMixin,
durationMixin: x.durationMixin,
- ID: snapshotID(setID),
+ ID: snapshotID(strconv.FormatUint(setID, 10)),
}
return y.Execute(nil)
}
@@ -184,7 +201,10 @@ type forgetCmd struct {
}
func (x *forgetCmd) Execute([]string) error {
- setID := uint64(x.Positional.ID)
+ setID, err := x.Positional.ID.ToUint()
+ if err != nil {
+ return err
+ }
snaps := installedSnapNames(x.Positional.Snaps)
changeID, err := x.client.ForgetSnapshots(setID, snaps)
if err != nil {
@@ -200,9 +220,9 @@ func (x *forgetCmd) Execute([]string) er
if len(snaps) > 0 {
// TRANSLATORS: the %s is a comma-separated list of quoted snap names
- fmt.Fprintf(Stdout, i18n.NG("Snapshot #%d of snap %s forgotten.\n", "Snapshot #%d of snaps %s forgotten.\n", len(snaps)), x.Positional.ID, strutil.Quoted(snaps))
+ fmt.Fprintf(Stdout, i18n.NG("Snapshot #%s of snap %s forgotten.\n", "Snapshot #%s of snaps %s forgotten.\n", len(snaps)), x.Positional.ID, strutil.Quoted(snaps))
} else {
- fmt.Fprintf(Stdout, i18n.G("Snapshot #%d forgotten.\n"), x.Positional.ID)
+ fmt.Fprintf(Stdout, i18n.G("Snapshot #%s forgotten.\n"), x.Positional.ID)
}
return nil
}
@@ -217,7 +237,10 @@ type checkSnapshotCmd struct {
}
func (x *checkSnapshotCmd) Execute([]string) error {
- setID := uint64(x.Positional.ID)
+ setID, err := x.Positional.ID.ToUint()
+ if err != nil {
+ return err
+ }
snaps := installedSnapNames(x.Positional.Snaps)
users := strutil.CommaSeparatedList(x.Users)
changeID, err := x.client.CheckSnapshots(setID, snaps, users)
@@ -235,10 +258,10 @@ func (x *checkSnapshotCmd) Execute([]str
// TODO: also mention the home archives that were actually checked
if len(snaps) > 0 {
// TRANSLATORS: the %s is a comma-separated list of quoted snap names
- fmt.Fprintf(Stdout, i18n.G("Snapshot #%d of snaps %s verified successfully.\n"),
+ fmt.Fprintf(Stdout, i18n.G("Snapshot #%s of snaps %s verified successfully.\n"),
x.Positional.ID, strutil.Quoted(snaps))
} else {
- fmt.Fprintf(Stdout, i18n.G("Snapshot #%d verified successfully.\n"), x.Positional.ID)
+ fmt.Fprintf(Stdout, i18n.G("Snapshot #%s verified successfully.\n"), x.Positional.ID)
}
return nil
}
@@ -253,7 +276,10 @@ type restoreCmd struct {
}
func (x *restoreCmd) Execute([]string) error {
- setID := uint64(x.Positional.ID)
+ setID, err := x.Positional.ID.ToUint()
+ if err != nil {
+ return err
+ }
snaps := installedSnapNames(x.Positional.Snaps)
users := strutil.CommaSeparatedList(x.Users)
changeID, err := x.client.RestoreSnapshots(setID, snaps, users)
@@ -271,10 +297,10 @@ func (x *restoreCmd) Execute([]string) e
// TODO: also mention the home archives that were actually restored
if len(snaps) > 0 {
// TRANSLATORS: the %s is a comma-separated list of quoted snap names
- fmt.Fprintf(Stdout, i18n.G("Restored snapshot #%d of snaps %s.\n"),
+ fmt.Fprintf(Stdout, i18n.G("Restored snapshot #%s of snaps %s.\n"),
x.Positional.ID, strutil.Quoted(snaps))
} else {
- fmt.Fprintf(Stdout, i18n.G("Restored snapshot #%d.\n"), x.Positional.ID)
+ fmt.Fprintf(Stdout, i18n.G("Restored snapshot #%s.\n"), x.Positional.ID)
}
return nil
}
diff -pruN 2.37.4-1/cmd/snap/cmd_snapshot_test.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_snapshot_test.go
--- 2.37.4-1/cmd/snap/cmd_snapshot_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_snapshot_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,109 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main_test
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/cmd/snap"
+)
+
+var snapshotsTests = []getCmdArgs{{
+ args: "restore x",
+ error: "invalid argument for set id: expected a non-negative integer argument",
+}, {
+ args: "saved --id=x",
+ error: "invalid argument for set id: expected a non-negative integer argument",
+}, {
+ args: "saved --id=3",
+ stdout: "Set Snap Age Version Rev Size Notes\n3 htop .* 2 1168 1B auto\n",
+}, {
+ args: "saved",
+ stdout: "Set Snap Age Version Rev Size Notes\n1 htop .* 2 1168 1B -\n",
+}, {
+ args: "forget x",
+ error: "invalid argument for set id: expected a non-negative integer argument",
+}, {
+ args: "check-snapshot x",
+ error: "invalid argument for set id: expected a non-negative integer argument",
+}, {
+ args: "restore 1",
+ stdout: "Restored snapshot #1.\n",
+}, {
+ args: "forget 2",
+ stdout: "Snapshot #2 forgotten.\n",
+}, {
+ args: "forget 2 snap1 snap2",
+ stdout: "Snapshot #2 of snaps \"snap1\", \"snap2\" forgotten.\n",
+}, {
+ args: "check-snapshot 4",
+ stdout: "Snapshot #4 verified successfully.\n",
+}, {
+ args: "check-snapshot 4 snap1 snap2",
+ stdout: "Snapshot #4 of snaps \"snap1\", \"snap2\" verified successfully.\n",
+}}
+
+func (s *SnapSuite) TestSnapSnaphotsTest(c *C) {
+ s.mockSnapshotsServer(c)
+
+ restore := main.MockIsStdinTTY(true)
+ defer restore()
+
+ for _, test := range snapshotsTests {
+ s.stdout.Truncate(0)
+ s.stderr.Truncate(0)
+
+ c.Logf("Test: %s", test.args)
+
+ _, err := main.Parser(main.Client()).ParseArgs(strings.Fields(test.args))
+ if test.error != "" {
+ c.Check(err, ErrorMatches, test.error)
+ } else {
+ c.Check(err, IsNil)
+ c.Check(s.Stderr(), Equals, test.stderr)
+ c.Check(s.Stdout(), Matches, test.stdout)
+ }
+ }
+}
+
+func (s *SnapSuite) mockSnapshotsServer(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/snapshots":
+ if r.Method == "GET" {
+ if r.URL.Query().Get("set") == "3" {
+ fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":[{"id":3,"snapshots":[{"set":3,"time":"2019-03-18T16:15:20.48905909Z","snap":"htop","revision":"1168","snap-id":"Z","auto":true,"epoch":{"read":[0],"write":[0]},"summary":"","version":"2","sha3-384":{"archive.tgz":""},"size":1}]}]}`)
+ return
+ }
+ fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":[{"id":1,"snapshots":[{"set":1,"time":"2019-03-18T16:15:20.48905909Z","snap":"htop","revision":"1168","snap-id":"Z","epoch":{"read":[0],"write":[0]},"summary":"","version":"2","sha3-384":{"archive.tgz":""},"size":1}]}]}`)
+ } else {
+ fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "9"}`)
+ }
+ case "/v2/changes/9":
+ fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {}}}`)
+ default:
+ c.Errorf("unexpected path %q", r.URL.Path)
+ }
+ })
+}
diff -pruN 2.37.4-1/cmd/snap/cmd_wait.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_wait.go
--- 2.37.4-1/cmd/snap/cmd_wait.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_wait.go 2019-06-05 06:41:21.000000000 +0000
@@ -43,7 +43,7 @@ type cmdWait struct {
func init() {
addCommand("wait",
"Wait for configuration",
- "The wait command waits until a configration becomes true.",
+ "The wait command waits until a configuration becomes true.",
func() flags.Commander {
return &cmdWait{}
}, nil, []argDesc{
diff -pruN 2.37.4-1/cmd/snap/cmd_warnings.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_warnings.go
--- 2.37.4-1/cmd/snap/cmd_warnings.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_warnings.go 2019-06-05 06:41:21.000000000 +0000
@@ -38,6 +38,7 @@ import (
type cmdWarnings struct {
clientMixin
timeMixin
+ unicodeMixin
All bool `long:"all"`
Verbose bool `long:"verbose"`
}
@@ -64,7 +65,7 @@ sufficient time has passed.
`)
func init() {
- addCommand("warnings", shortWarningsHelp, longWarningsHelp, func() flags.Commander { return &cmdWarnings{} }, timeDescs.also(map[string]string{
+ addCommand("warnings", shortWarningsHelp, longWarningsHelp, func() flags.Commander { return &cmdWarnings{} }, timeDescs.also(unicodeDescs).also(map[string]string{
// TRANSLATORS: This should not start with a lowercase letter.
"all": i18n.G("Show all warnings"),
// TRANSLATORS: This should not start with a lowercase letter.
@@ -96,35 +97,36 @@ func (cmd *cmdWarnings) Execute(args []s
return err
}
+ termWidth, _ := termSize()
+ if termWidth > 100 {
+ // any wider than this and it gets hard to read
+ termWidth = 100
+ }
+
+ esc := cmd.getEscapes()
w := tabWriter()
- if cmd.Verbose {
- fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
- i18n.G("First occurrence"),
- i18n.G("Last occurrence"),
- i18n.G("Expires after"),
- i18n.G("Acknowledged"),
- i18n.G("Repeats after"),
- i18n.G("Warning"))
- for _, warning := range warnings {
- lastShown := "-"
+ for i, warning := range warnings {
+ if i > 0 {
+ fmt.Fprintln(w, "---")
+ }
+ if cmd.Verbose {
+ fmt.Fprintf(w, "first-occurrence:\t%s\n", cmd.fmtTime(warning.FirstAdded))
+ }
+ fmt.Fprintf(w, "last-occurrence:\t%s\n", cmd.fmtTime(warning.LastAdded))
+ if cmd.Verbose {
+ lastShown := esc.dash
if !warning.LastShown.IsZero() {
lastShown = cmd.fmtTime(warning.LastShown)
}
- fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
- cmd.fmtTime(warning.FirstAdded),
- cmd.fmtTime(warning.LastAdded),
- quantity.FormatDuration(warning.ExpireAfter.Seconds()),
- lastShown,
- quantity.FormatDuration(warning.RepeatAfter.Seconds()),
- warning.Message)
- }
- } else {
- fmt.Fprintf(w, "%s\t%s\n", i18n.G("Last occurrence"), i18n.G("Warning"))
- for _, warning := range warnings {
- fmt.Fprintf(w, "%s\t%s\n", cmd.fmtTime(warning.LastAdded), warning.Message)
+ fmt.Fprintf(w, "acknowledged:\t%s\n", lastShown)
+ // TODO: cmd.fmtDuration() using timeutil.HumanDuration
+ fmt.Fprintf(w, "repeats-after:\t%s\n", quantity.FormatDuration(warning.RepeatAfter.Seconds()))
+ fmt.Fprintf(w, "expires-after:\t%s\n", quantity.FormatDuration(warning.ExpireAfter.Seconds()))
}
+ fmt.Fprintln(w, "warning: |")
+ printDescr(w, warning.Message, termWidth)
+ w.Flush()
}
- w.Flush()
return nil
}
diff -pruN 2.37.4-1/cmd/snap/cmd_warnings_test.go 2.39.2+19.10ubuntu1/cmd/snap/cmd_warnings_test.go
--- 2.37.4-1/cmd/snap/cmd_warnings_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/cmd_warnings_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -102,28 +102,44 @@ func (s *warningSuite) TestNoFurtherWarn
func (s *warningSuite) TestWarnings(c *check.C) {
s.RedirectClientToTestServer(mkWarningsFakeHandler(c, twoWarnings))
- rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"warnings", "--abs-time"})
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"warnings", "--abs-time", "--unicode=never"})
c.Assert(err, check.IsNil)
c.Check(rest, check.HasLen, 0)
c.Check(s.Stderr(), check.Equals, "")
c.Check(s.Stdout(), check.Equals, `
-Last occurrence Warning
-2018-09-19T12:41:18Z hello world number one
-2018-09-19T12:44:19Z hello world number two
+last-occurrence: 2018-09-19T12:41:18Z
+warning: |
+ hello world number one
+---
+last-occurrence: 2018-09-19T12:44:19Z
+warning: |
+ hello world number two
`[1:])
}
func (s *warningSuite) TestVerboseWarnings(c *check.C) {
s.RedirectClientToTestServer(mkWarningsFakeHandler(c, twoWarnings))
- rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"warnings", "--abs-time", "--verbose"})
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"warnings", "--abs-time", "--verbose", "--unicode=never"})
c.Assert(err, check.IsNil)
c.Check(rest, check.HasLen, 0)
c.Check(s.Stderr(), check.Equals, "")
c.Check(s.Stdout(), check.Equals, `
-First occurrence Last occurrence Expires after Acknowledged Repeats after Warning
-2018-09-19T12:41:18Z 2018-09-19T12:41:18Z 28d0h - 1d00h hello world number one
-2018-09-19T12:44:19Z 2018-09-19T12:44:19Z 28d0h - 1d00h hello world number two
+first-occurrence: 2018-09-19T12:41:18Z
+last-occurrence: 2018-09-19T12:41:18Z
+acknowledged: --
+repeats-after: 1d00h
+expires-after: 28d0h
+warning: |
+ hello world number one
+---
+first-occurrence: 2018-09-19T12:44:19Z
+last-occurrence: 2018-09-19T12:44:19Z
+acknowledged: --
+repeats-after: 1d00h
+expires-after: 28d0h
+warning: |
+ hello world number two
`[1:])
}
diff -pruN 2.37.4-1/cmd/snap/color.go 2.39.2+19.10ubuntu1/cmd/snap/color.go
--- 2.37.4-1/cmd/snap/color.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/color.go 2019-06-05 06:41:21.000000000 +0000
@@ -30,14 +30,12 @@ import (
"github.com/snapcore/snapd/snap"
)
-type colorMixin struct {
- Color string `long:"color" default:"auto" choice:"auto" choice:"never" choice:"always"`
- Unicode string `long:"unicode" default:"auto" choice:"auto" choice:"never" choice:"always"` // do we want this hidden?
+type unicodeMixin struct {
+ Unicode string `long:"unicode" default:"auto" choice:"auto" choice:"never" choice:"always"`
}
-func (mx colorMixin) getEscapes() *escapes {
- esc := colorTable(mx.Color)
- if canUnicode(mx.Unicode) {
+func (ux unicodeMixin) addUnicodeChars(esc *escapes) {
+ if canUnicode(ux.Unicode) {
esc.dash = "–" // that's an en dash (so yaml is happy)
esc.uparrow = "↑"
esc.tick = "✓"
@@ -46,7 +44,22 @@ func (mx colorMixin) getEscapes() *escap
esc.uparrow = "^"
esc.tick = "*"
}
+}
+func (ux unicodeMixin) getEscapes() *escapes {
+ esc := &escapes{}
+ ux.addUnicodeChars(esc)
+ return esc
+}
+
+type colorMixin struct {
+ Color string `long:"color" default:"auto" choice:"auto" choice:"never" choice:"always"`
+ unicodeMixin
+}
+
+func (mx colorMixin) getEscapes() *escapes {
+ esc := colorTable(mx.Color)
+ mx.addUnicodeChars(&esc)
return &esc
}
@@ -103,13 +116,18 @@ func colorTable(mode string) escapes {
var colorDescs = mixinDescs{
// TRANSLATORS: This should not start with a lowercase letter.
- "color": i18n.G("Use a little bit of color to highlight some things."),
+ "color": i18n.G("Use a little bit of color to highlight some things."),
+ "unicode": unicodeDescs["unicode"],
+}
+
+var unicodeDescs = mixinDescs{
// TRANSLATORS: This should not start with a lowercase letter.
"unicode": i18n.G("Use a little bit of Unicode to improve legibility."),
}
type escapes struct {
green string
+ bold string
end string
tick, dash, uparrow string
@@ -118,11 +136,13 @@ type escapes struct {
var (
color = escapes{
green: "\033[32m",
+ bold: "\033[1m",
end: "\033[0m",
}
mono = escapes{
green: "\033[1m",
+ bold: "\033[1m",
end: "\033[0m",
}
diff -pruN 2.37.4-1/cmd/snap/complete.go 2.39.2+19.10ubuntu1/cmd/snap/complete.go
--- 2.37.4-1/cmd/snap/complete.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/complete.go 2019-06-05 06:41:21.000000000 +0000
@@ -31,6 +31,7 @@ import (
"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/client"
"github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/i18n"
"github.com/snapcore/snapd/snap"
)
@@ -105,8 +106,8 @@ func (s remoteSnapName) Complete(match s
return nil
}
snaps, _, err := mkClient().Find(&client.FindOptions{
- Prefix: true,
Query: match,
+ Prefix: true,
})
if err != nil {
return nil
@@ -296,7 +297,10 @@ func (spec *interfaceSpec) Complete(matc
parts := strings.SplitN(match, ":", 2)
// Ask snapd about available interfaces.
- ifaces, err := mkClient().Connections()
+ opts := client.ConnectionOptions{
+ All: true,
+ }
+ ifaces, err := mkClient().Connections(&opts)
if err != nil {
return nil
}
@@ -435,17 +439,25 @@ func (s serviceName) Complete(match stri
return nil
}
- snaps := map[string]bool{}
+ snaps := map[string]int{}
var ret []flags.Completion
for _, app := range apps {
if !app.IsService() {
continue
}
- if !snaps[app.Snap] {
- snaps[app.Snap] = true
- ret = append(ret, flags.Completion{Item: app.Snap})
+ name := snap.JoinSnapApp(app.Snap, app.Name)
+ if !strings.HasPrefix(name, match) {
+ continue
+ }
+ ret = append(ret, flags.Completion{Item: name})
+ if len(match) <= len(app.Snap) {
+ snaps[app.Snap]++
+ }
+ }
+ for snap, n := range snaps {
+ if n > 1 {
+ ret = append(ret, flags.Completion{Item: snap})
}
- ret = append(ret, flags.Completion{Item: app.Snap + "." + app.Name})
}
return ret
@@ -475,7 +487,7 @@ func (s aliasOrSnap) Complete(match stri
return ret
}
-type snapshotID uint64
+type snapshotID string
func (snapshotID) Complete(match string) []flags.Completion {
shots, err := mkClient().SnapshotSets(0, nil)
@@ -492,3 +504,11 @@ func (snapshotID) Complete(match string)
return ret
}
+
+func (s snapshotID) ToUint() (uint64, error) {
+ setID, err := strconv.ParseUint((string)(s), 10, 64)
+ if err != nil {
+ return 0, fmt.Errorf(i18n.G("invalid argument for set id: expected a non-negative integer argument"))
+ }
+ return setID, nil
+}
diff -pruN 2.37.4-1/cmd/snap/error.go 2.39.2+19.10ubuntu1/cmd/snap/error.go
--- 2.37.4-1/cmd/snap/error.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/error.go 2019-06-05 06:41:21.000000000 +0000
@@ -41,7 +41,9 @@ import (
var errorPrefix = i18n.G("error: %v\n")
-func termSize() (width, height int) {
+var termSize = termSizeImpl
+
+func termSizeImpl() (width, height int) {
if f, ok := Stdout.(*os.File); ok {
width, height, _ = terminal.GetSize(int(f.Fd()))
}
diff -pruN 2.37.4-1/cmd/snap/export_test.go 2.39.2+19.10ubuntu1/cmd/snap/export_test.go
--- 2.37.4-1/cmd/snap/export_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/export_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2016 Canonical Ltd
+ * Copyright (C) 2016-2019 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
@@ -26,7 +26,7 @@ import (
"github.com/jessevdk/go-flags"
"github.com/snapcore/snapd/client"
- "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/image"
"github.com/snapcore/snapd/selinux"
"github.com/snapcore/snapd/store"
)
@@ -40,7 +40,7 @@ var (
CreateUserDataDirs = createUserDataDirs
ResolveApp = resolveApp
- IsReexeced = isReexeced
+ SnapdHelperPath = snapdHelperPath
MaybePrintServices = maybePrintServices
MaybePrintCommands = maybePrintCommands
SortByPath = sortByPath
@@ -48,6 +48,7 @@ var (
Antialias = antialias
FormatChannel = fmtChannel
PrintDescr = printDescr
+ WrapFlow = wrapFlow
TrueishJSON = trueishJSON
CanUnicode = canUnicode
@@ -76,6 +77,8 @@ var (
LintDesc = lintDesc
FixupArg = fixupArg
+
+ InterfacesDeprecationNotice = interfacesDeprecationNotice
)
func MockPollTime(d time.Duration) (restore func()) {
@@ -110,7 +113,7 @@ func MockUserCurrent(f func() (*user.Use
}
}
-func MockStoreNew(f func(*store.Config, auth.AuthContext) *store.Store) (restore func()) {
+func MockStoreNew(f func(*store.Config, store.DeviceAndAuthContext) *store.Store) (restore func()) {
storeNewOrig := storeNew
storeNew = f
return func() {
@@ -211,7 +214,10 @@ func Wait(cli *client.Client, id string)
}
func ColorMixin(cmode, umode string) colorMixin {
- return colorMixin{Color: cmode, Unicode: umode}
+ return colorMixin{
+ Color: cmode,
+ unicodeMixin: unicodeMixin{Unicode: umode},
+ }
}
func CmdAdviseSnap() *cmdAdviseSnap {
@@ -241,3 +247,21 @@ func MockSELinuxRestoreContext(restoreco
selinuxRestoreContext = old
}
}
+
+func MockTermSize(newTermSize func() (int, int)) (restore func()) {
+ old := termSize
+ termSize = newTermSize
+ return func() {
+ termSize = old
+ }
+}
+
+func MockImagePrepare(newImagePrepare func(*image.Options) error) (restore func()) {
+ old := imagePrepare
+ imagePrepare = newImagePrepare
+ return func() {
+ imagePrepare = old
+ }
+}
+
+type ServiceName = serviceName
diff -pruN 2.37.4-1/cmd/snap/main.go 2.39.2+19.10ubuntu1/cmd/snap/main.go
--- 2.37.4-1/cmd/snap/main.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/main.go 2019-06-05 06:41:21.000000000 +0000
@@ -146,11 +146,7 @@ func lintDesc(cmdName, optName, desc, or
// decode the first rune instead of converting all of desc into []rune
r, _ := utf8.DecodeRuneInString(desc)
// note IsLower != !IsUpper for runes with no upper/lower.
- // Also note that login.u.c. is the only exception we're allowing for
- // now, but the list of exceptions could grow -- if it does, we might
- // want to change it to check for urlish things instead of just
- // login.u.c.
- if unicode.IsLower(r) && !strings.HasPrefix(desc, "login.ubuntu.com") {
+ if unicode.IsLower(r) && !strings.HasPrefix(desc, "login.ubuntu.com") && !strings.HasPrefix(desc, cmdName) {
noticef("description of %s's %q is lowercase: %q", cmdName, optName, desc)
}
}
@@ -478,7 +474,7 @@ var wrongDashes = string([]rune{
func run() error {
cli := mkClient()
parser := Parser(cli)
- _, err := parser.Parse()
+ xtra, err := parser.Parse()
if err != nil {
if e, ok := err.(*flags.Error); ok {
switch e.Type {
@@ -489,7 +485,16 @@ func run() error {
parser.WriteHelp(Stdout)
return nil
case flags.ErrUnknownCommand:
- return fmt.Errorf(i18n.G(`unknown command %q, see 'snap help'`), os.Args[1])
+ sub := os.Args[1]
+ sug := "snap help"
+ if len(xtra) > 0 {
+ sub = xtra[0]
+ if x := parser.Command.Active; x != nil && x.Name != "help" {
+ sug = "snap help " + x.Name
+ }
+ }
+ // TRANSLATORS: %q is the command the user entered; %s is 'snap help' or 'snap help '
+ return fmt.Errorf(i18n.G("unknown command %q, see '%s'."), sub, sug)
}
}
diff -pruN 2.37.4-1/cmd/snap/main_test.go 2.39.2+19.10ubuntu1/cmd/snap/main_test.go
--- 2.37.4-1/cmd/snap/main_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap/main_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -67,6 +67,12 @@ func (s *BaseSnapSuite) SetUpTest(c *C)
s.BaseTest.SetUpTest(c)
dirs.SetRootDir(c.MkDir())
+ path := os.Getenv("PATH")
+ s.AddCleanup(func() {
+ os.Setenv("PATH", path)
+ })
+ os.Setenv("PATH", path+":"+dirs.SnapBinariesDir)
+
s.stdin = bytes.NewBuffer(nil)
s.stdout = bytes.NewBuffer(nil)
s.stderr = bytes.NewBuffer(nil)
@@ -261,7 +267,7 @@ func (s *SnapSuite) TestUnknownCommand(c
defer restore()
err := snap.RunMain()
- c.Assert(err, ErrorMatches, `unknown command "unknowncmd", see 'snap help'`)
+ c.Assert(err, ErrorMatches, `unknown command "unknowncmd", see 'snap help'.`)
}
func (s *SnapSuite) TestResolveApp(c *C) {
@@ -364,6 +370,10 @@ func (s *SnapSuite) TestLintDesc(c *C) {
}
c.Check(fn, PanicMatches, `option on "command" has no name`)
log.Reset()
+
+ snap.LintDesc("snap-advise", "from-apt", "snap-advise will run as a hook", "")
+ c.Check(log.String(), HasLen, 0)
+ log.Reset()
}
func (s *SnapSuite) TestLintArg(c *C) {
diff -pruN 2.37.4-1/cmd/snap-confine/cookie-support-test.c 2.39.2+19.10ubuntu1/cmd/snap-confine/cookie-support-test.c
--- 2.37.4-1/cmd/snap-confine/cookie-support-test.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-confine/cookie-support-test.c 2019-06-05 06:41:21.000000000 +0000
@@ -93,7 +93,7 @@ static void test_cookie_get_from_snapd__
g_assert_null(cookie);
}
-static void __attribute__ ((constructor)) init(void)
+static void __attribute__((constructor)) init(void)
{
g_test_add_func("/snap-cookie/cookie_get_from_snapd/successful",
test_cookie_get_from_snapd__successful);
diff -pruN 2.37.4-1/cmd/snap-confine/mount-support.c 2.39.2+19.10ubuntu1/cmd/snap-confine/mount-support.c
--- 2.37.4-1/cmd/snap-confine/mount-support.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-confine/mount-support.c 2019-06-05 06:41:21.000000000 +0000
@@ -50,62 +50,74 @@
#define MAX_BUF 1000
-/*!
- * The void directory.
- *
- * Snap confine moves to that directory in case it cannot retain the current
- * working directory across the pivot_root call.
- **/
-#define SC_VOID_DIR "/var/lib/snapd/void"
-
// TODO: simplify this, after all it is just a tmpfs
// TODO: fold this into bootstrap
static void setup_private_mount(const char *snap_name)
{
- uid_t uid = getuid();
- gid_t gid = getgid();
- char tmpdir[MAX_BUF] = { 0 };
-
- // Create a 0700 base directory, this is the base dir that is
- // protected from other users.
- //
- // Under that basedir, we put a 1777 /tmp dir that is then bind
- // mounted for the applications to use
- sc_must_snprintf(tmpdir, sizeof(tmpdir), "/tmp/snap.%d_%s_XXXXXX", uid,
- snap_name);
- if (mkdtemp(tmpdir) == NULL) {
- die("cannot create temporary directory essential for private /tmp");
- }
- // now we create a 1777 /tmp inside our private dir
- mode_t old_mask = umask(0);
- char *d = sc_strdup(tmpdir);
- sc_must_snprintf(tmpdir, sizeof(tmpdir), "%s/tmp", d);
- free(d);
-
- if (mkdir(tmpdir, 01777) != 0) {
- die("cannot create temporary directory for private /tmp");
- }
- umask(old_mask);
-
- // chdir to '/' since the mount won't apply to the current directory
- char *pwd = get_current_dir_name();
- if (pwd == NULL)
- die("cannot get current working directory");
- if (chdir("/") != 0)
- die("cannot change directory to '/'");
-
- // MS_BIND is there from linux 2.4
- sc_do_mount(tmpdir, "/tmp", NULL, MS_BIND, NULL);
- // MS_PRIVATE needs linux > 2.6.11
+ // Create a 0700 base directory. This is the "base" directory that is
+ // protected from other users. This directory name is NOT randomly
+ // generated. This has several properties:
+ //
+ // Users can relate to the name and can find the temporary directory as
+ // visible from within the snap. If this directory was random it would be
+ // harder to find because there may be situations in which multiple
+ // directories related to the same snap name would exist.
+ //
+ // Snapd can partially manage the directory. Specifically on snap remove
+ // snapd could remove the directory and everything in it, potentially
+ // avoiding runaway disk use on a machine that either never reboots or uses
+ // persistent /tmp directory.
+ //
+ // Underneath the base directory there is a "tmp" sub-directory that has
+ // mode 1777 and behaves as a typical /tmp directory would. That directory
+ // is used as a bind-mounted /tmp directory.
+ //
+ // Because the directories are reused across invocations by distinct users
+ // and because the directories are trivially guessable, each invocation
+ // unconditionally chowns/chmods them to appropriate values.
+ char base_dir[MAX_BUF] = { 0 };
+ char tmp_dir[MAX_BUF] = { 0 };
+ int base_dir_fd SC_CLEANUP(sc_cleanup_close) = -1;
+ int tmp_dir_fd SC_CLEANUP(sc_cleanup_close) = -1;
+ sc_must_snprintf(base_dir, sizeof(base_dir), "/tmp/snap.%s", snap_name);
+ sc_must_snprintf(tmp_dir, sizeof(tmp_dir), "%s/tmp", base_dir);
+
+ // Create /tmp/snap.$SNAP_NAME/ 0700 root.root. Ignore EEXIST since we want
+ // to reuse and we will open with O_NOFOLLOW, below.
+ if (mkdir(base_dir, 0700) < 0 && errno != EEXIST) {
+ die("cannot create base directory %s", base_dir);
+ }
+ base_dir_fd = open(base_dir,
+ O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
+ if (base_dir_fd < 0) {
+ die("cannot open base directory %s", base_dir);
+ }
+ if (fchmod(base_dir_fd, 0700) < 0) {
+ die("cannot chmod base directory %s to 0700", base_dir);
+ }
+ if (fchown(base_dir_fd, 0, 0) < 0) {
+ die("cannot chown base directory %s to root.root", base_dir);
+ }
+ // Create /tmp/snap.$SNAP_NAME/tmp 01777 root.root Ignore EEXIST since we
+ // want to reuse and we will open with O_NOFOLLOW, below.
+ if (mkdirat(base_dir_fd, "tmp", 01777) < 0 && errno != EEXIST) {
+ die("cannot create private tmp directory %s/tmp", base_dir);
+ }
+ tmp_dir_fd = openat(base_dir_fd, "tmp",
+ O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
+ if (tmp_dir_fd < 0) {
+ die("cannot open private tmp directory %s/tmp", base_dir);
+ }
+ if (fchmod(tmp_dir_fd, 01777) < 0) {
+ die("cannot chmod private tmp directory %s/tmp to 01777",
+ base_dir);
+ }
+ if (fchown(tmp_dir_fd, 0, 0) < 0) {
+ die("cannot chown private tmp directory %s/tmp to root.root",
+ base_dir);
+ }
+ sc_do_mount(tmp_dir, "/tmp", NULL, MS_BIND, NULL);
sc_do_mount("none", "/tmp", NULL, MS_PRIVATE, NULL);
- // do the chown after the bind mount to avoid potential shenanigans
- if (chown("/tmp/", uid, gid) < 0) {
- die("cannot change ownership of /tmp");
- }
- // chdir to original directory
- if (chdir(pwd) != 0)
- die("cannot change current working directory to the original directory");
- free(pwd);
}
// TODO: fold this into bootstrap
@@ -298,20 +310,54 @@ static void sc_bootstrap_mount_namespace
};
for (const char **dirs = dirs_from_core; *dirs != NULL; dirs++) {
const char *dir = *dirs;
- struct stat buf;
- if (access(dir, F_OK) == 0) {
- sc_must_snprintf(src, sizeof src, "%s%s",
- config->rootfs_dir, dir);
- sc_must_snprintf(dst, sizeof dst, "%s%s",
- scratch_dir, dir);
- if (lstat(src, &buf) == 0
- && lstat(dst, &buf) == 0) {
- sc_do_mount(src, dst, NULL, MS_BIND,
- NULL);
- sc_do_mount("none", dst, NULL, MS_SLAVE,
- NULL);
+ if (access(dir, F_OK) != 0) {
+ continue;
+ }
+ struct stat dst_stat;
+ struct stat src_stat;
+ sc_must_snprintf(src, sizeof src, "%s%s",
+ config->rootfs_dir, dir);
+ sc_must_snprintf(dst, sizeof dst, "%s%s",
+ scratch_dir, dir);
+ if (lstat(src, &src_stat) != 0) {
+ if (errno == ENOENT) {
+ continue;
+ }
+ die("cannot stat %s from desired rootfs", src);
+ }
+ if (!S_ISREG(src_stat.st_mode)
+ && !S_ISDIR(src_stat.st_mode)) {
+ debug
+ ("entry %s from the desired rootfs is not a file or directory, skipping mount",
+ src);
+ continue;
+ }
+
+ if (lstat(dst, &dst_stat) != 0) {
+ if (errno == ENOENT) {
+ continue;
}
+ die("cannot stat %s from host", src);
}
+ if (!S_ISREG(dst_stat.st_mode)
+ && !S_ISDIR(dst_stat.st_mode)) {
+ debug
+ ("entry %s from the host is not a file or directory, skipping mount",
+ src);
+ continue;
+ }
+
+ if ((dst_stat.st_mode & S_IFMT) !=
+ (src_stat.st_mode & S_IFMT)) {
+ debug
+ ("entries %s and %s are of different types, skipping mount",
+ dst, src);
+ continue;
+ }
+ // both source and destination exist and are either files or
+ // directories
+ sc_do_mount(src, dst, NULL, MS_BIND, NULL);
+ sc_do_mount("none", dst, NULL, MS_SLAVE, NULL);
}
}
// The "core" base snap is special as it contains snapd and friends.
@@ -434,7 +480,7 @@ static void sc_bootstrap_mount_namespace
// This way we can remove the temporary directory we created and "clean up"
// after ourselves nicely.
sc_must_snprintf(dst, sizeof dst, "%s/%s", SC_HOSTFS_DIR, scratch_dir);
- sc_do_umount(dst, 0);
+ sc_do_umount(dst, UMOUNT_NOFOLLOW);
// Remove the scratch directory. Note that we are using the path that is
// based on the old root filesystem as after pivot_root we cannot guarantee
// what is present at the same location normally. (It is probably an empty
@@ -469,8 +515,8 @@ static void sc_bootstrap_mount_namespace
* @fulllen: full original path length.
* Returns a pointer to the next path segment, or NULL if done.
*/
-static char * __attribute__ ((used))
- get_nextpath(char *path, size_t * offsetp, size_t fulllen)
+static char * __attribute__((used))
+ get_nextpath(char *path, size_t *offsetp, size_t fulllen)
{
size_t offset = *offsetp;
@@ -489,7 +535,7 @@ static char * __attribute__ ((used))
/**
* Check that @subdir is a subdir of @dir.
**/
-static bool __attribute__ ((used))
+static bool __attribute__((used))
is_subdir(const char *subdir, const char *dir)
{
size_t dirlen = strlen(dir);
@@ -519,20 +565,13 @@ static bool __attribute__ ((used))
}
void sc_populate_mount_ns(struct sc_apparmor *apparmor, int snap_update_ns_fd,
- const char *base_snap_name, const char *snap_name)
+ const sc_invocation * inv)
{
- // Get the current working directory before we start fiddling with
- // mounts and possibly pivot_root. At the end of the whole process, we
- // will try to re-locate to the same directory (if possible).
- char *vanilla_cwd SC_CLEANUP(sc_cleanup_string) = NULL;
- vanilla_cwd = get_current_dir_name();
- if (vanilla_cwd == NULL) {
- die("cannot get the current working directory");
- }
// Classify the current distribution, as claimed by /etc/os-release.
sc_distro distro = sc_classify_distro();
+
// Check which mode we should run in, normal or legacy.
- if (sc_should_use_normal_mode(distro, base_snap_name)) {
+ if (inv->is_normal_mode) {
// In normal mode we use the base snap as / and set up several bind mounts.
const struct sc_mount mounts[] = {
{"/dev"}, // because it contains devices on host OS
@@ -562,34 +601,12 @@ void sc_populate_mount_ns(struct sc_appa
{"/var/lib/extrausers",.is_optional = true}, // access to UID/GID of extrausers (if available)
{},
};
- char rootfs_dir[PATH_MAX] = { 0 };
- sc_must_snprintf(rootfs_dir, sizeof rootfs_dir,
- "%s/%s/current/", SNAP_MOUNT_DIR,
- base_snap_name);
- if (access(rootfs_dir, F_OK) != 0) {
- if (sc_streq(base_snap_name, "core")) {
- // As a special fallback, allow the
- // base snap to degrade from "core" to
- // "ubuntu-core". This is needed for
- // the migration tests.
- base_snap_name = "ubuntu-core";
- sc_must_snprintf(rootfs_dir, sizeof rootfs_dir,
- "%s/%s/current/",
- SNAP_MOUNT_DIR,
- base_snap_name);
- if (access(rootfs_dir, F_OK) != 0) {
- die("cannot locate the core or legacy core snap (current symlink missing?)");
- }
- }
- if (access(rootfs_dir, F_OK) != 0)
- die("cannot locate the base snap: %s", base_snap_name);
- }
struct sc_mount_config normal_config = {
- .rootfs_dir = rootfs_dir,
+ .rootfs_dir = inv->rootfs_dir,
.mounts = mounts,
.distro = distro,
.normal_mode = true,
- .base_snap_name = base_snap_name,
+ .base_snap_name = inv->base_snap_name,
};
sc_bootstrap_mount_namespace(&normal_config);
} else {
@@ -605,45 +622,34 @@ void sc_populate_mount_ns(struct sc_appa
.mounts = mounts,
.distro = distro,
.normal_mode = false,
- .base_snap_name = base_snap_name,
+ .base_snap_name = inv->base_snap_name,
};
sc_bootstrap_mount_namespace(&legacy_config);
}
// set up private mounts
// TODO: rename this and fold it into bootstrap
- setup_private_mount(snap_name);
+ setup_private_mount(inv->snap_instance);
// set up private /dev/pts
// TODO: fold this into bootstrap
setup_private_pts();
// setup the security backend bind mounts
- sc_call_snap_update_ns(snap_update_ns_fd, snap_name, apparmor);
-
- // Try to re-locate back to vanilla working directory. This can fail
- // because that directory is no longer present.
- if (chdir(vanilla_cwd) != 0) {
- debug("cannot remain in %s, moving to the void directory",
- vanilla_cwd);
- if (chdir(SC_VOID_DIR) != 0) {
- die("cannot change directory to %s", SC_VOID_DIR);
- }
- debug("successfully moved to %s", SC_VOID_DIR);
- }
+ sc_call_snap_update_ns(snap_update_ns_fd, inv->snap_instance, apparmor);
}
static bool is_mounted_with_shared_option(const char *dir)
- __attribute__ ((nonnull(1)));
+ __attribute__((nonnull(1)));
static bool is_mounted_with_shared_option(const char *dir)
{
- struct sc_mountinfo *sm SC_CLEANUP(sc_cleanup_mountinfo) = NULL;
+ sc_mountinfo *sm SC_CLEANUP(sc_cleanup_mountinfo) = NULL;
sm = sc_parse_mountinfo(NULL);
if (sm == NULL) {
die("cannot parse /proc/self/mountinfo");
}
- struct sc_mountinfo_entry *entry = sc_first_mountinfo_entry(sm);
+ sc_mountinfo_entry *entry = sc_first_mountinfo_entry(sm);
while (entry != NULL) {
const char *mount_dir = entry->mount_dir;
if (sc_streq(mount_dir, dir)) {
@@ -675,9 +681,6 @@ void sc_ensure_shared_snap_mount(void)
static void sc_make_slave_mount_ns(void)
{
- if (unshare(CLONE_NEWNS) < 0) {
- die("can not unshare mount namespace");
- }
// In our new mount namespace, recursively change all mounts
// to slave mode, so we see changes from the parent namespace
// but don't propagate our own changes.
diff -pruN 2.37.4-1/cmd/snap-confine/mount-support.h 2.39.2+19.10ubuntu1/cmd/snap-confine/mount-support.h
--- 2.37.4-1/cmd/snap-confine/mount-support.h 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-confine/mount-support.h 2019-06-05 06:41:21.000000000 +0000
@@ -19,24 +19,7 @@
#define SNAP_MOUNT_SUPPORT_H
#include "../libsnap-confine-private/apparmor-support.h"
-
-/**
- * Return a file descriptor referencing the snap-update-ns utility
- *
- * By calling this prior to changing the mount namespace, it is
- * possible to execute the utility even if a different version is now
- * mounted at the expected location.
- **/
-int sc_open_snap_update_ns(void);
-
-/**
- * Return a file descriptor referencing the snap-discard-ns utility
- *
- * By calling this prior to changing the mount namespace, it is
- * possible to execute the utility even if a different version is now
- * mounted at the expected location.
- **/
-int sc_open_snap_discard_ns(void);
+#include "snap-confine-invocation.h"
/**
* Assuming a new mountspace, populate it accordingly.
@@ -46,12 +29,9 @@ int sc_open_snap_discard_ns(void);
* - creates private /tmp
* - creates private /dev/pts
* - processes mount profiles
- *
- * The function will also try to preserve the current working directory but if
- * this is impossible it will chdir to SC_VOID_DIR.
**/
void sc_populate_mount_ns(struct sc_apparmor *apparmor, int snap_update_ns_fd,
- const char *base_snap_name, const char *snap_name);
+ const sc_invocation * inv);
/**
* Ensure that / or /snap is mounted with the SHARED option.
diff -pruN 2.37.4-1/cmd/snap-confine/mount-support-nvidia.c 2.39.2+19.10ubuntu1/cmd/snap-confine/mount-support-nvidia.c
--- 2.37.4-1/cmd/snap-confine/mount-support-nvidia.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-confine/mount-support-nvidia.c 2019-06-05 06:41:21.000000000 +0000
@@ -142,7 +142,8 @@ static void sc_populate_libgl_with_hostf
{
size_t source_dir_len = strlen(source_dir);
glob_t glob_res SC_CLEANUP(globfree) = {
- .gl_pathv = NULL};
+ .gl_pathv = NULL
+ };
// Find all the entries matching the list of globs
for (size_t i = 0; i < glob_list_len; ++i) {
const char *glob_pattern = glob_list[i];
diff -pruN 2.37.4-1/cmd/snap-confine/mount-support-test.c 2.39.2+19.10ubuntu1/cmd/snap-confine/mount-support-test.c
--- 2.37.4-1/cmd/snap-confine/mount-support-test.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-confine/mount-support-test.c 2019-06-05 06:41:21.000000000 +0000
@@ -91,7 +91,7 @@ static void test_is_subdir(void)
g_assert_false(is_subdir("/", ""));
}
-static void __attribute__ ((constructor)) init(void)
+static void __attribute__((constructor)) init(void)
{
g_test_add_func("/mount/get_nextpath/typical",
test_get_nextpath__typical);
diff -pruN 2.37.4-1/cmd/snap-confine/ns-support.c 2.39.2+19.10ubuntu1/cmd/snap-confine/ns-support.c
--- 2.37.4-1/cmd/snap-confine/ns-support.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-confine/ns-support.c 2019-06-05 06:41:21.000000000 +0000
@@ -48,14 +48,6 @@
#include "../libsnap-confine-private/utils.h"
#include "user-support.h"
-/*!
- * The void directory.
- *
- * Snap confine moves to that directory in case it cannot retain the current
- * working directory across the pivot_root call.
- **/
-#define SC_VOID_DIR "/var/lib/snapd/void"
-
/**
* Directory where snap-confine keeps namespace files.
**/
@@ -133,7 +125,7 @@ void sc_initialize_mount_ns(void)
/* Read and analyze the mount table. We need to see whether /run/snapd/ns
* is a mount point with private event propagation. */
- struct sc_mountinfo *info SC_CLEANUP(sc_cleanup_mountinfo) = NULL;
+ sc_mountinfo *info SC_CLEANUP(sc_cleanup_mountinfo) = NULL;
info = sc_parse_mountinfo(NULL);
if (info == NULL) {
die("cannot parse /proc/self/mountinfo");
@@ -141,7 +133,7 @@ void sc_initialize_mount_ns(void)
bool is_mnt = false;
bool is_private = false;
- for (struct sc_mountinfo_entry * entry = sc_first_mountinfo_entry(info);
+ for (sc_mountinfo_entry * entry = sc_first_mountinfo_entry(info);
entry != NULL; entry = sc_next_mountinfo_entry(entry)) {
/* Find /run/snapd/ns */
if (!sc_streq(entry->mount_dir, sc_ns_dir)) {
@@ -239,13 +231,13 @@ static dev_t find_base_snap_device(const
sc_must_snprintf(base_squashfs_path,
sizeof base_squashfs_path, "%s/%s/%s",
SNAP_MOUNT_DIR, base_snap_name, base_snap_rev);
- struct sc_mountinfo *mi SC_CLEANUP(sc_cleanup_mountinfo) = NULL;
+ sc_mountinfo *mi SC_CLEANUP(sc_cleanup_mountinfo) = NULL;
mi = sc_parse_mountinfo(NULL);
if (mi == NULL) {
die("cannot parse mountinfo of the current process");
}
bool found = false;
- for (struct sc_mountinfo_entry * mie =
+ for (sc_mountinfo_entry * mie =
sc_first_mountinfo_entry(mi); mie != NULL;
mie = sc_next_mountinfo_entry(mie)) {
if (sc_streq(mie->mount_dir, base_squashfs_path)) {
@@ -272,8 +264,8 @@ static bool should_discard_current_ns(de
// The namespace may become "stale" when the rootfs is not the same
// device we found above. This will happen whenever the base snap is
// refreshed since the namespace was first created.
- struct sc_mountinfo_entry *mie;
- struct sc_mountinfo *mi SC_CLEANUP(sc_cleanup_mountinfo) = NULL;
+ sc_mountinfo_entry *mie;
+ sc_mountinfo *mi SC_CLEANUP(sc_cleanup_mountinfo) = NULL;
mi = sc_parse_mountinfo(NULL);
if (mi == NULL) {
@@ -306,33 +298,25 @@ enum sc_discard_vote {
// inspect the namespace and send information back via eventfd and then exit
// unconditionally.
static int sc_inspect_and_maybe_discard_stale_ns(int mnt_fd,
- const char *snap_name,
- const char *base_snap_name,
+ const sc_invocation * inv,
int snap_discard_ns_fd)
{
char base_snap_rev[PATH_MAX] = { 0 };
- char fname[PATH_MAX] = { 0 };
dev_t base_snap_dev;
int event_fd SC_CLEANUP(sc_cleanup_close) = -1;
// Read the revision of the base snap by looking at the current symlink.
- sc_must_snprintf(fname, sizeof fname, "%s/%s/current",
- SNAP_MOUNT_DIR, base_snap_name);
- if (readlink(fname, base_snap_rev, sizeof base_snap_rev) < 0) {
- die("cannot read current revision of snap %s", snap_name);
+ if (readlink(inv->rootfs_dir, base_snap_rev, sizeof base_snap_rev) < 0) {
+ die("cannot read current revision of snap %s",
+ inv->snap_instance);
}
if (base_snap_rev[sizeof base_snap_rev - 1] != '\0') {
die("cannot read current revision of snap %s: value too long",
- snap_name);
+ inv->snap_instance);
}
// Find the device that is backing the current revision of the base snap.
- base_snap_dev = find_base_snap_device(base_snap_name, base_snap_rev);
-
- // Check if we are running in normal mode with pivot root. Do this here
- // because once on the inside of the transformed mount namespace we can no
- // longer tell.
- bool is_normal_mode =
- sc_should_use_normal_mode(sc_classify_distro(), base_snap_name);
+ base_snap_dev =
+ find_base_snap_device(inv->base_snap_name, base_snap_rev);
// Store the PID of this process. This is done instead of calls to
// getppid() below because then we can reliably track the PID of the
@@ -388,8 +372,8 @@ static int sc_inspect_and_maybe_discard_
// systemd. This makes us end up in a situation where the outer base
// snap will never match the rootfs inside the mount namespace.
bool should_discard =
- is_normal_mode ? should_discard_current_ns(base_snap_dev) :
- false;
+ inv->is_normal_mode ?
+ should_discard_current_ns(base_snap_dev) : false;
// Send this back to the parent: 2 - discard, 1 - keep.
// Note that we cannot just use 0 and 1 because of the semantics of eventfd(2).
@@ -427,7 +411,7 @@ static int sc_inspect_and_maybe_discard_
return 0;
}
// The namespace is stale, let's check if we can discard it.
- if (sc_cgroup_freezer_occupied(snap_name)) {
+ if (sc_cgroup_freezer_occupied(inv->snap_instance)) {
// Some processes are still using the namespace so we cannot discard it
// as that would fracture the view that the set of processes inside
// have on what is mounted.
@@ -435,7 +419,7 @@ static int sc_inspect_and_maybe_discard_
return 0;
}
// The namespace is both stale and empty. We can discard it now.
- sc_call_snap_discard_ns(snap_discard_ns_fd, snap_name);
+ sc_call_snap_discard_ns(snap_discard_ns_fd, inv->snap_instance);
return EAGAIN;
}
@@ -447,8 +431,8 @@ static void helper_capture_ns(struct sc_
static void helper_capture_per_user_ns(struct sc_mount_ns *group, pid_t parent);
int sc_join_preserved_ns(struct sc_mount_ns *group, struct sc_apparmor
- *apparmor, const char *base_snap_name,
- const char *snap_name, int snap_discard_ns_fd)
+ *apparmor, const sc_invocation * inv,
+ int snap_discard_ns_fd)
{
// Open the mount namespace file.
char mnt_fname[PATH_MAX] = { 0 };
@@ -457,7 +441,10 @@ int sc_join_preserved_ns(struct sc_mount
// NOTE: There is no O_EXCL here because the file can be around but
// doesn't have to be a mounted namespace.
mnt_fd = openat(group->dir_fd, mnt_fname,
- O_CREAT | O_RDONLY | O_CLOEXEC | O_NOFOLLOW, 0600);
+ O_RDONLY | O_CLOEXEC | O_NOFOLLOW, 0600);
+ if (mnt_fd < 0 && errno == ENOENT) {
+ return ESRCH;
+ }
if (mnt_fd < 0) {
die("cannot open preserved mount namespace %s", group->name);
}
@@ -486,32 +473,15 @@ int sc_join_preserved_ns(struct sc_mount
// Inspect and perhaps discard the preserved mount namespace.
if (sc_inspect_and_maybe_discard_stale_ns
- (mnt_fd, snap_name, base_snap_name,
- snap_discard_ns_fd) == EAGAIN) {
+ (mnt_fd, inv, snap_discard_ns_fd) == EAGAIN) {
return ESRCH;
}
- // Remember the vanilla working directory so that we may attempt to restore it later.
- char *vanilla_cwd SC_CLEANUP(sc_cleanup_string) = NULL;
- vanilla_cwd = get_current_dir_name();
- if (vanilla_cwd == NULL) {
- die("cannot get the current working directory");
- }
// Move to the mount namespace of the snap we're trying to start.
if (setns(mnt_fd, CLONE_NEWNS) < 0) {
die("cannot join preserved mount namespace %s",
group->name);
}
debug("joined preserved mount namespace %s", group->name);
-
- // Try to re-locate back to vanilla working directory. This can fail
- // because that directory is no longer present.
- if (chdir(vanilla_cwd) != 0) {
- debug("cannot enter %s, moving to void", vanilla_cwd);
- if (chdir(SC_VOID_DIR) != 0) {
- die("cannot change directory to %s",
- SC_VOID_DIR);
- }
- }
return 0;
}
return ESRCH;
@@ -527,7 +497,10 @@ int sc_join_preserved_per_user_ns(struct
int mnt_fd SC_CLEANUP(sc_cleanup_close) = -1;
mnt_fd = openat(group->dir_fd, mnt_fname,
- O_CREAT | O_RDONLY | O_CLOEXEC | O_NOFOLLOW, 0600);
+ O_RDONLY | O_CLOEXEC | O_NOFOLLOW, 0600);
+ if (mnt_fd < 0 && errno == ENOENT) {
+ return ESRCH;
+ }
if (mnt_fd < 0) {
die("cannot open preserved mount namespace %s", group->name);
}
@@ -545,24 +518,11 @@ int sc_join_preserved_per_user_ns(struct
#endif
if (ns_statfs_buf.f_type == NSFS_MAGIC
|| ns_statfs_buf.f_type == PROC_SUPER_MAGIC) {
- // TODO: refactor the cwd workflow across all of snap-confine.
- char *vanilla_cwd SC_CLEANUP(sc_cleanup_string) = NULL;
- vanilla_cwd = get_current_dir_name();
- if (vanilla_cwd == NULL) {
- die("cannot get the current working directory");
- }
if (setns(mnt_fd, CLONE_NEWNS) < 0) {
die("cannot join preserved per-user mount namespace %s",
group->name);
}
debug("joined preserved mount namespace %s", group->name);
- if (chdir(vanilla_cwd) != 0) {
- debug("cannot enter %s, moving to void", vanilla_cwd);
- if (chdir(SC_VOID_DIR) != 0) {
- die("cannot change directory to %s",
- SC_VOID_DIR);
- }
- }
return 0;
}
return ESRCH;
@@ -710,6 +670,7 @@ static void helper_capture_ns(struct sc_
die("cannot create file %s", dst);
}
close(fd);
+
if (mount(src, dst, NULL, MS_BIND, NULL) < 0) {
die("cannot preserve mount namespace of process %d as %s",
(int)parent, dst);
@@ -727,6 +688,14 @@ static void helper_capture_per_user_ns(s
debug("capturing per-snap, per-user mount namespace");
sc_must_snprintf(src, sizeof src, "/proc/%d/ns/mnt", (int)parent);
sc_must_snprintf(dst, sizeof dst, "%s.%d.mnt", group->name, (int)uid);
+
+ /* Ensure the bind mount destination exists. */
+ int fd = open(dst, O_CREAT | O_CLOEXEC | O_NOFOLLOW | O_RDONLY, 0600);
+ if (fd < 0) {
+ die("cannot create file %s", dst);
+ }
+ close(fd);
+
if (mount(src, dst, NULL, MS_BIND, NULL) < 0) {
die("cannot preserve per-user mount namespace of process %d as %s", (int)parent, dst);
}
diff -pruN 2.37.4-1/cmd/snap-confine/ns-support.h 2.39.2+19.10ubuntu1/cmd/snap-confine/ns-support.h
--- 2.37.4-1/cmd/snap-confine/ns-support.h 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-confine/ns-support.h 2019-06-05 06:41:21.000000000 +0000
@@ -21,6 +21,7 @@
#include
#include "../libsnap-confine-private/apparmor-support.h"
+#include "snap-confine-invocation.h"
/**
* Re-associate the current process with the mount namespace of pid 1.
@@ -91,8 +92,8 @@ void sc_close_mount_ns(struct sc_mount_n
* function returns zero.
**/
int sc_join_preserved_ns(struct sc_mount_ns *group, struct sc_apparmor
- *apparmor, const char *base_snap_name,
- const char *snap_name, int snap_discard_ns_fd);
+ *apparmor, const sc_invocation * inv,
+ int snap_discard_ns_fd);
/**
* Join a preserved, per-user, mount namespace if one exists.
diff -pruN 2.37.4-1/cmd/snap-confine/ns-support-test.c 2.39.2+19.10ubuntu1/cmd/snap-confine/ns-support-test.c
--- 2.37.4-1/cmd/snap-confine/ns-support-test.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-confine/ns-support-test.c 2019-06-05 06:41:21.000000000 +0000
@@ -146,7 +146,7 @@ static void test_nsfs_fs_id(void)
g_assert_cmpint(buf.f_type, ==, NSFS_MAGIC);
}
-static void __attribute__ ((constructor)) init(void)
+static void __attribute__((constructor)) init(void)
{
g_test_add_func("/ns/sc_alloc_mount_ns", test_sc_alloc_mount_ns);
g_test_add_func("/ns/sc_open_mount_ns", test_sc_open_mount_ns);
diff -pruN 2.37.4-1/cmd/snap-confine/seccomp-support.h 2.39.2+19.10ubuntu1/cmd/snap-confine/seccomp-support.h
--- 2.37.4-1/cmd/snap-confine/seccomp-support.h 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-confine/seccomp-support.h 2019-06-05 06:41:21.000000000 +0000
@@ -17,7 +17,6 @@
#ifndef SNAP_CONFINE_SECCOMP_SUPPORT_H
#define SNAP_CONFINE_SECCOMP_SUPPORT_H
-#include
#include
/**
diff -pruN 2.37.4-1/cmd/snap-confine/selinux-support.c 2.39.2+19.10ubuntu1/cmd/snap-confine/selinux-support.c
--- 2.37.4-1/cmd/snap-confine/selinux-support.c 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-confine/selinux-support.c 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2018 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 .
+ *
+ */
+#include "selinux-support.h"
+#include "config.h"
+
+#include
+#include
+
+#include "../libsnap-confine-private/cleanup-funcs.h"
+#include "../libsnap-confine-private/string-utils.h"
+#include "../libsnap-confine-private/utils.h"
+
+static void sc_freecon(char **ctx) {
+ if (ctx != NULL && *ctx != NULL) {
+ freecon(*ctx);
+ *ctx = NULL;
+ }
+}
+
+static void sc_context_free(context_t *ctx) {
+ if (ctx != NULL && *ctx != NULL) {
+ context_free(*ctx);
+ *ctx = NULL;
+ }
+}
+
+/**
+ * Set security context for the snap.
+ *
+ * Sets up SELinux context transition to unconfined_service_t.
+ **/
+int sc_selinux_set_snap_execcon(void) {
+ if (is_selinux_enabled() < 1) {
+ debug("SELinux not enabled");
+ return 0;
+ }
+
+ char *ctx_str SC_CLEANUP(sc_freecon) = NULL;
+ if (getcon(&ctx_str) < 0) {
+ die("cannot obtain current SELinux process context");
+ }
+ debug("current SELinux process context: %s", ctx_str);
+
+ context_t ctx SC_CLEANUP(sc_context_free) = context_new(ctx_str);
+ if (ctx == NULL) {
+ die("cannot create SELinux context from context string %s", ctx_str);
+ }
+
+ /* freed by context_free(ctx) */
+ const char *ctx_type = context_type_get(ctx);
+
+ if (ctx_type == NULL) {
+ die("cannot obtain type from SELinux context string %s", ctx_str);
+ }
+
+ if (sc_streq(ctx_type, "snappy_confine_t")) {
+ /* We are running under a targeted policy which ended up transitioning
+ * to snappy_confine_t domain, at this point we are right before
+ * executing snap-exec. However we do not have a full SELinux support
+ * for services running in snaps, only the snapd bits and helpers are
+ * covered by the policy.
+ *
+ * At this point transition to the unconfined_service_t domain (allowed
+ * by snap_confine_t policy) upon the next exec() call.
+ */
+ if (context_type_set(ctx, "unconfined_service_t") != 0) {
+ die("cannot update SELinux context %s type to unconfined_service_t", ctx_str);
+ }
+
+ /* freed by context_free(ctx) */
+ char *new_ctx_str = context_str(ctx);
+ if (new_ctx_str == NULL) {
+ die("cannot obtain updated SELinux context string");
+ }
+ if (setexeccon(new_ctx_str) < 0) {
+ die("cannot set SELinux exec context to %s", new_ctx_str);
+ }
+ debug("SELinux context after next exec: %s", new_ctx_str);
+ }
+
+ return 0;
+}
diff -pruN 2.37.4-1/cmd/snap-confine/selinux-support.h 2.39.2+19.10ubuntu1/cmd/snap-confine/selinux-support.h
--- 2.37.4-1/cmd/snap-confine/selinux-support.h 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-confine/selinux-support.h 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2018 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 .
+ *
+ */
+#ifndef SNAP_CONFINE_SELINUX_SUPPORT_H
+#define SNAP_CONFINE_SELINUX_SUPPORT_H
+
+/**
+ * Set security context for the snap
+ *
+ * Sets up SELinux context transition to unconfined_service_t.
+ **/
+int sc_selinux_set_snap_execcon(void);
+
+#endif /* SNAP_CONFINE_SELINUX_SUPPORT_H */
diff -pruN 2.37.4-1/cmd/snap-confine/snap-confine.apparmor.in 2.39.2+19.10ubuntu1/cmd/snap-confine/snap-confine.apparmor.in
--- 2.37.4-1/cmd/snap-confine/snap-confine.apparmor.in 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-confine/snap-confine.apparmor.in 2019-06-05 06:41:21.000000000 +0000
@@ -67,6 +67,13 @@
/sys/fs/cgroup/freezer/snap.*/tasks w,
/sys/fs/cgroup/freezer/snap.*/cgroup.procs r,
+ # cgroup: pids
+ # allow creating per snap-security-tag hierarchy and adding snap command (task)
+ # invocations to the controller.
+ /sys/fs/cgroup/pids/ r,
+ /sys/fs/cgroup/pids/snap.*/ w,
+ /sys/fs/cgroup/pids/snap.*/tasks w,
+
# querying udev
/etc/udev/udev.conf r,
/sys/**/uevent r,
@@ -241,8 +248,8 @@
# set up snap-specific private /tmp dir
capability chown,
/tmp/ rw,
- /tmp/snap.*/ w,
- /tmp/snap.*/tmp/ w,
+ /tmp/snap.*/ rw,
+ /tmp/snap.*/tmp/ rw,
mount options=(rw private) -> /tmp/,
mount options=(rw bind) /tmp/snap.*/tmp/ -> /tmp/,
mount fstype=devpts options=(rw) devpts -> /dev/pts/,
@@ -343,8 +350,8 @@
@{HOMEDIRS}/.ecryptfs/*/.Private/ r,
@{HOMEDIRS}/.ecryptfs/*/.Private/** mrixwlk,
- # Allow snap-confine to move to the void
- /var/lib/snapd/void/ r,
+ # Allow snap-confine to move to the void, creating it if necessary.
+ /var/lib/snapd/void/ rw,
# Allow snap-confine to read snap contexts
/var/lib/snapd/context/snap.* r,
@@ -490,19 +497,20 @@
/var/lib/snapd/hostfs/usr/lib{,exec,64}/snapd/snap-update-ns r,
# ..snap-confine is, conceptually, re-executing and uses snap-update-ns
- # from the core snap. Note that the location of the core snap varies from
- # distribution to distribution. The variants here represent different
- # locations of snap mount directory across distributions.
- /{,var/lib/snapd/}snap/core/*/usr/lib/snapd/snap-update-ns r,
+ # from the core or snapd snaps. Note that the location of the actual snap
+ # varies from distribution to distribution. The variants here represent
+ # different locations of snap mount directory across distributions.
+ /{,var/lib/snapd/}snap/{core,snapd}/*/usr/lib/snapd/snap-update-ns r,
# ...snap-confine is, conceptually, re-executing and uses snap-update-ns
- # from the core snap but we are already inside the constructed mount
- # namespace. Here the apparmor kernel module re-constructs the path to
- # snap-update-ns using the "hostfs" mount entry rather than the more
- # "natural" /snap mount entry but we have no control over that. This is
- # reported as (LP: #1716339). The variants here represent different
- # locations of snap mount directory across distributions.
- /var/lib/snapd/hostfs/{,var/lib/snapd/}snap/core/*/usr/lib/snapd/snap-update-ns r,
+ # from the core snap or snapd snap, but we are already inside the
+ # constructed mount namespace. Here the apparmor kernel module
+ # re-constructs the path to snap-update-ns using the "hostfs" mount entry
+ # rather than the more "natural" /snap mount entry but we have no control
+ # over that. This is reported as (LP: #1716339). The variants here
+ # represent different locations of snap mount directory across
+ # distributions.
+ /var/lib/snapd/hostfs/{,var/lib/snapd/}snap/{core,snapd}/*/usr/lib/snapd/snap-update-ns r,
# Allow executing snap-discard-ns, just like the set for snap-update-ns
# above but with the key difference that snap-discard-ns does not
@@ -510,8 +518,8 @@
/usr/lib{,exec,64}/snapd/snap-discard-ns rix,
/var/lib/snapd/hostfs/usr/lib{,exec,64}/snapd/snap-discard-ns rix,
- /{,var/lib/snapd/}snap/core/*/usr/lib/snapd/snap-discard-ns rix,
- /var/lib/snapd/hostfs/{,var/lib/snapd/}snap/core/*/usr/lib/snapd/snap-discard-ns rix,
+ /{,var/lib/snapd/}snap/{core,snapd}/*/usr/lib/snapd/snap-discard-ns rix,
+ /var/lib/snapd/hostfs/{,var/lib/snapd/}snap/{core,snapd}/*/usr/lib/snapd/snap-discard-ns rix,
# Allow mounting /var/lib/jenkinks from the host into the snap.
mount options=(rw rbind) /var/lib/jenkins/ -> /tmp/snap.rootfs_*/var/lib/jenkins/,
diff -pruN 2.37.4-1/cmd/snap-confine/snap-confine-args.c 2.39.2+19.10ubuntu1/cmd/snap-confine/snap-confine-args.c
--- 2.37.4-1/cmd/snap-confine/snap-confine-args.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-confine/snap-confine-args.c 2019-06-05 06:41:21.000000000 +0000
@@ -37,10 +37,10 @@ struct sc_args {
};
struct sc_args *sc_nonfatal_parse_args(int *argcp, char ***argvp,
- struct sc_error **errorp)
+ sc_error **errorp)
{
struct sc_args *args = NULL;
- struct sc_error *err = NULL;
+ sc_error *err = NULL;
if (argcp == NULL || argvp == NULL) {
err = sc_error_init(SC_ARGS_DOMAIN, 0,
@@ -214,7 +214,7 @@ void sc_cleanup_args(struct sc_args **pt
*ptr = NULL;
}
-bool sc_args_is_version_query(struct sc_args *args)
+bool sc_args_is_version_query(const struct sc_args *args)
{
if (args == NULL) {
die("cannot obtain version query flag from NULL argument parser");
@@ -222,7 +222,7 @@ bool sc_args_is_version_query(struct sc_
return args->is_version_query;
}
-bool sc_args_is_classic_confinement(struct sc_args * args)
+bool sc_args_is_classic_confinement(const struct sc_args *args)
{
if (args == NULL) {
die("cannot obtain classic confinement flag from NULL argument parser");
@@ -230,7 +230,7 @@ bool sc_args_is_classic_confinement(stru
return args->is_classic_confinement;
}
-const char *sc_args_security_tag(struct sc_args *args)
+const char *sc_args_security_tag(const struct sc_args *args)
{
if (args == NULL) {
die("cannot obtain security tag from NULL argument parser");
@@ -238,7 +238,7 @@ const char *sc_args_security_tag(struct
return args->security_tag;
}
-const char *sc_args_executable(struct sc_args *args)
+const char *sc_args_executable(const struct sc_args *args)
{
if (args == NULL) {
die("cannot obtain executable from NULL argument parser");
@@ -246,7 +246,7 @@ const char *sc_args_executable(struct sc
return args->executable;
}
-const char *sc_args_base_snap(struct sc_args *args)
+const char *sc_args_base_snap(const struct sc_args *args)
{
if (args == NULL) {
die("cannot obtain base snap name from NULL argument parser");
diff -pruN 2.37.4-1/cmd/snap-confine/snap-confine-args.h 2.39.2+19.10ubuntu1/cmd/snap-confine/snap-confine-args.h
--- 2.37.4-1/cmd/snap-confine/snap-confine-args.h 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-confine/snap-confine-args.h 2019-06-05 06:41:21.000000000 +0000
@@ -64,9 +64,9 @@ struct sc_args;
* Both argc and argv are modified so the caller can look at the first unparsed
* argument at argc[0]. This is only done if argument parsing is successful.
**/
-__attribute__ ((warn_unused_result))
+__attribute__((warn_unused_result))
struct sc_args *sc_nonfatal_parse_args(int *argcp, char ***argvp,
- struct sc_error **errorp);
+ sc_error **errorp);
/**
* Free the object describing command-line arguments to snap-confine.
@@ -84,12 +84,12 @@ void sc_cleanup_args(struct sc_args **pt
/**
* Check if snap-confine was invoked with the --version switch.
**/
-bool sc_args_is_version_query(struct sc_args *args);
+bool sc_args_is_version_query(const struct sc_args *args);
/**
* Check if snap-confine was invoked with the --classic switch.
**/
-bool sc_args_is_classic_confinement(struct sc_args *args);
+bool sc_args_is_classic_confinement(const struct sc_args *args);
/**
* Get the security tag passed to snap-confine.
@@ -100,7 +100,7 @@ bool sc_args_is_classic_confinement(stru
* The return value must not be freed(). It is bound to the lifetime of
* the argument parser.
**/
-const char *sc_args_security_tag(struct sc_args *args);
+const char *sc_args_security_tag(const struct sc_args *args);
/**
* Get the executable name passed to snap-confine.
@@ -111,7 +111,7 @@ const char *sc_args_security_tag(struct
* The return value must not be freed(). It is bound to the lifetime of
* the argument parser.
**/
-const char *sc_args_executable(struct sc_args *args);
+const char *sc_args_executable(const struct sc_args *args);
/**
* Get the name of the base snap to use.
@@ -119,6 +119,6 @@ const char *sc_args_executable(struct sc
* The return value must not be freed(). It is bound to the lifetime of
* the argument parser.
**/
-const char *sc_args_base_snap(struct sc_args *args);
+const char *sc_args_base_snap(const struct sc_args *args);
#endif
diff -pruN 2.37.4-1/cmd/snap-confine/snap-confine-args-test.c 2.39.2+19.10ubuntu1/cmd/snap-confine/snap-confine-args-test.c
--- 2.37.4-1/cmd/snap-confine/snap-confine-args-test.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-confine/snap-confine-args-test.c 2019-06-05 06:41:21.000000000 +0000
@@ -27,7 +27,7 @@
* Create an argc + argv pair out of a NULL terminated argument list.
**/
static void
- __attribute__ ((sentinel)) test_argc_argv(int *argcp, char ***argvp, ...)
+ __attribute__((sentinel)) test_argc_argv(int *argcp, char ***argvp, ...)
{
int argc = 0;
char **argv = NULL;
@@ -77,7 +77,7 @@ static void test_test_argc_argv(void)
static void test_sc_nonfatal_parse_args__typical(void)
{
// Test that typical invocation of snap-confine is parsed correctly.
- struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
+ sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL;
int argc;
@@ -110,7 +110,7 @@ static void test_sc_nonfatal_parse_args_
static void test_sc_cleanup_args(void)
{
// Check that NULL argument parser can be cleaned up
- struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
+ sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
struct sc_args *args = NULL;
sc_cleanup_args(&args);
@@ -131,7 +131,7 @@ static void test_sc_cleanup_args(void)
static void test_sc_nonfatal_parse_args__typical_classic(void)
{
// Test that typical invocation of snap-confine is parsed correctly.
- struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
+ sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL;
int argc;
@@ -166,7 +166,7 @@ static void test_sc_nonfatal_parse_args_
// Test that typical legacy invocation of snap-confine via the
// ubuntu-core-launcher symlink, with duplicated security tag, is parsed
// correctly.
- struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
+ sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL;
int argc;
@@ -199,7 +199,7 @@ static void test_sc_nonfatal_parse_args_
static void test_sc_nonfatal_parse_args__version(void)
{
// Test that snap-confine --version is detected.
- struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
+ sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL;
int argc;
@@ -229,7 +229,7 @@ static void test_sc_nonfatal_parse_args_
static void test_sc_nonfatal_parse_args__evil_input(void)
{
// Check that calling without any arguments is reported as error.
- struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
+ sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL;
// NULL argcp/argvp attack
@@ -269,7 +269,7 @@ static void test_sc_nonfatal_parse_args_
static void test_sc_nonfatal_parse_args__nothing_to_parse(void)
{
// Check that calling without any arguments is reported as error.
- struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
+ sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL;
int argc;
@@ -288,7 +288,7 @@ static void test_sc_nonfatal_parse_args_
static void test_sc_nonfatal_parse_args__no_security_tag(void)
{
// Check that lack of security tag is reported as error.
- struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
+ sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL;
int argc;
@@ -310,7 +310,7 @@ static void test_sc_nonfatal_parse_args_
static void test_sc_nonfatal_parse_args__no_executable(void)
{
// Check that lack of security tag is reported as error.
- struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
+ sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL;
int argc;
@@ -332,7 +332,7 @@ static void test_sc_nonfatal_parse_args_
static void test_sc_nonfatal_parse_args__unknown_option(void)
{
// Check that unrecognized option switch is reported as error.
- struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
+ sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL;
int argc;
@@ -379,7 +379,7 @@ static void test_sc_nonfatal_parse_args_
static void test_sc_nonfatal_parse_args__base_snap(void)
{
// Check that --base specifies the name of the base snap.
- struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
+ sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL;
int argc;
@@ -407,7 +407,7 @@ static void test_sc_nonfatal_parse_args_
static void test_sc_nonfatal_parse_args__base_snap__missing_arg(void)
{
// Check that --base specifies the name of the base snap.
- struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
+ sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL;
int argc;
@@ -429,7 +429,7 @@ static void test_sc_nonfatal_parse_args_
static void test_sc_nonfatal_parse_args__base_snap__twice(void)
{
// Check that --base specifies the name of the base snap.
- struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
+ sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL;
int argc;
@@ -449,7 +449,7 @@ static void test_sc_nonfatal_parse_args_
g_assert_true(sc_error_match(err, SC_ARGS_DOMAIN, SC_ARGS_ERR_USAGE));
}
-static void __attribute__ ((constructor)) init(void)
+static void __attribute__((constructor)) init(void)
{
g_test_add_func("/args/test_argc_argv", test_test_argc_argv);
g_test_add_func("/args/sc_cleanup_args", test_sc_cleanup_args);
diff -pruN 2.37.4-1/cmd/snap-confine/snap-confine.c 2.39.2+19.10ubuntu1/cmd/snap-confine/snap-confine.c
--- 2.37.4-1/cmd/snap-confine/snap-confine.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-confine/snap-confine.c 2019-06-05 06:41:21.000000000 +0000
@@ -19,6 +19,7 @@
#endif
#include
+#include
#include
#include
#include
@@ -31,22 +32,27 @@
#include "../libsnap-confine-private/apparmor-support.h"
#include "../libsnap-confine-private/cgroup-freezer-support.h"
+#include "../libsnap-confine-private/cgroup-pids-support.h"
#include "../libsnap-confine-private/classic.h"
#include "../libsnap-confine-private/cleanup-funcs.h"
#include "../libsnap-confine-private/feature.h"
#include "../libsnap-confine-private/locking.h"
#include "../libsnap-confine-private/secure-getenv.h"
#include "../libsnap-confine-private/snap.h"
+#include "../libsnap-confine-private/string-utils.h"
+#include "../libsnap-confine-private/tool.h"
#include "../libsnap-confine-private/utils.h"
+#include "cookie-support.h"
#include "mount-support.h"
#include "ns-support.h"
+#include "seccomp-support.h"
+#include "snap-confine-args.h"
+#include "snap-confine-invocation.h"
#include "udev-support.h"
#include "user-support.h"
-#include "cookie-support.h"
-#include "snap-confine-args.h"
-#ifdef HAVE_SECCOMP
-#include "seccomp-support.h"
-#endif // ifdef HAVE_SECCOMP
+#ifdef HAVE_SELINUX
+#include "selinux-support.h"
+#endif
// sc_maybe_fixup_permissions fixes incorrect permissions
// inside the mount namespace for /var/lib. Before 1ccce4
@@ -74,7 +80,8 @@ static void sc_maybe_fixup_permissions(v
static void sc_maybe_fixup_udev(void)
{
glob_t glob_res SC_CLEANUP(globfree) = {
- .gl_pathv = NULL,.gl_pathc = 0,.gl_offs = 0,};
+ .gl_pathv = NULL,.gl_pathc = 0,.gl_offs = 0,
+ };
const char *glob_pattern = "/run/udev/tags/snap_*/*nvidia*";
int err = glob(glob_pattern, 0, NULL, &glob_res);
if (err == GLOB_NOMATCH) {
@@ -95,43 +102,229 @@ static void sc_maybe_fixup_udev(void)
}
}
+/**
+ * sc_preserved_process_state remembers clobbered state to restore.
+ *
+ * The umask is preserved and restored to ensure consistent permissions for
+ * runtime system. The value is preserved and restored perfectly.
+**/
+typedef struct sc_preserved_process_state {
+ mode_t orig_umask;
+ int orig_cwd_fd;
+ struct stat file_info_orig_cwd;
+} sc_preserved_process_state;
+
+/**
+ * sc_preserve_and_sanitize_process_state sanitizes process state.
+ *
+ * The following process state is sanitised:
+ * - the umask is set to 0
+ * - the current working directory is set to /
+ *
+ * The original values are stored to be restored later. Currently only the
+ * umask is altered. It is set to zero to make the ownership of created files
+ * and directories more predictable.
+**/
+static void sc_preserve_and_sanitize_process_state(sc_preserved_process_state *
+ proc_state)
+{
+ /* Reset umask to zero, storing the old value. */
+ proc_state->orig_umask = umask(0);
+ debug("umask reset, old umask was %#4o", proc_state->orig_umask);
+ /* Remember a file descriptor corresponding to the original working
+ * directory. This is an O_PATH file descriptor. The descriptor is
+ * used as explained below. */
+ proc_state->orig_cwd_fd =
+ openat(AT_FDCWD, ".",
+ O_PATH | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
+ if (proc_state->orig_cwd_fd < 0) {
+ die("cannot open path of the current working directory");
+ }
+ if (fstat(proc_state->orig_cwd_fd, &proc_state->file_info_orig_cwd) < 0) {
+ die("cannot stat path of the current working directory");
+ }
+ /* Move to the root directory. */
+ if (chdir("/") < 0) {
+ die("cannot move to /");
+ }
+}
+
+/**
+ * sc_restore_process_state restores values stored earlier.
+**/
+static void sc_restore_process_state(const sc_preserved_process_state *
+ proc_state)
+{
+ /* Restore original umask */
+ umask(proc_state->orig_umask);
+ debug("umask restored to %#4o", proc_state->orig_umask);
+
+ /* Restore original current working directory.
+ *
+ * This part is more involved for the following reasons. While we hold an
+ * O_PATH file descriptor that still points to the original working
+ * directory, that directory may not be representable in the target mount
+ * namespace. A quick example may be /custom that exists on the host but
+ * not in the base snap of the application.
+ *
+ * Also consider when the path of the original working directory now
+ * maps to a different inode we cannot use fchdir(2). One example of
+ * that is the /tmp directory, which exists in both the host mount
+ * namespace and the per-snap mount namespace but actually represents a
+ * different directory.
+ **/
+
+ /* Read the target of symlink at /proc/self/fd/ */
+ char fd_path[PATH_MAX];
+ char orig_cwd[PATH_MAX];
+ ssize_t nread;
+ /* If the original working directory cannot be used for whatever reason then
+ * move the process to a special void directory. */
+ const char *sc_void_dir = "/var/lib/snapd/void";
+ int void_dir_fd SC_CLEANUP(sc_cleanup_close) = -1;
+
+ sc_must_snprintf(fd_path, sizeof fd_path, "/proc/self/fd/%d",
+ proc_state->orig_cwd_fd);
+ nread = readlink(fd_path, orig_cwd, sizeof orig_cwd);
+ if (nread < 0) {
+ die("cannot read symbolic link target %s", fd_path);
+ }
+ if (nread == sizeof orig_cwd) {
+ die("cannot fit symbolic link target %s", fd_path);
+ }
+
+ /* Open path corresponding to the original working directory in the
+ * execution environment. This may normally fail if the path no longer
+ * exists here, this is not a fatal error. It may also fail if we don't
+ * have permissions to view that path, that is not a fatal error either. */
+ int inner_cwd_fd SC_CLEANUP(sc_cleanup_close) = -1;
+ inner_cwd_fd =
+ open(orig_cwd, O_PATH | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
+ if (inner_cwd_fd < 0) {
+ if (errno == EPERM || errno == EACCES || errno == ENOENT) {
+ debug
+ ("cannot open path of the original working directory %s",
+ orig_cwd);
+ goto the_void;
+ }
+ /* Any error other than the three above is unexpected. */
+ die("cannot open path of the original working directory %s",
+ orig_cwd);
+ }
+
+ /* The original working directory exists in the execution environment
+ * which lets us check if it points to the same inode as before. */
+ struct stat file_info_inner;
+ if (fstat(inner_cwd_fd, &file_info_inner) < 0) {
+ die("cannot stat path of working directory in the execution environment");
+ }
+
+ /* Note that we cannot use proc_state->orig_cwd_fd as that points to the
+ * directory but in another mount namespace and using that causes
+ * weird and undesired effects.
+ *
+ * By the time this code runs we are already running as the
+ * designated user so UNIX permissions are in effect. */
+ if (fchdir(inner_cwd_fd) < 0) {
+ if (errno == EPERM || errno == EACCES) {
+ debug("cannot access original working directory %s", orig_cwd);
+ goto the_void;
+ }
+ die("cannot restore original working directory via path");
+ }
+ /* The distinction below is only logged and not acted upon. Perhaps someday
+ * this will be somehow communicated to cooperating applications that can
+ * instruct the user and avoid potential confusion. This mostly applies to
+ * tools that are invoked from /tmp. */
+ if (proc_state->file_info_orig_cwd.st_dev ==
+ file_info_inner.st_dev
+ && proc_state->file_info_orig_cwd.st_ino ==
+ file_info_inner.st_ino) {
+ /* The path of the original working directory points to the same
+ * inode as before. */
+ debug("working directory restored to %s", orig_cwd);
+ } else {
+ /* The path of the original working directory points to a different
+ * inode inside inside the execution environment than the host
+ * environment. */
+ debug("working directory re-interpreted to %s", orig_cwd);
+ }
+ return;
+ the_void:
+ /* The void directory may be absent. On core18 system, and other
+ * systems using bootable base snap coupled with snapd snap, the
+ * /var/lib/snapd directory structure is not provided with packages but
+ * created on demand. */
+ void_dir_fd = open(sc_void_dir,
+ O_DIRECTORY | O_PATH | O_NOFOLLOW | O_CLOEXEC);
+ if (void_dir_fd < 0 && errno == ENOENT) {
+ if (mkdir(sc_void_dir, 0111) < 0) {
+ die("cannot create void directory: %s", sc_void_dir);
+ }
+ if (lchown(sc_void_dir, 0, 0) < 0) {
+ die("cannot change ownership of void directory %s",
+ sc_void_dir);
+ }
+ void_dir_fd = open(sc_void_dir,
+ O_DIRECTORY | O_PATH | O_NOFOLLOW |
+ O_CLOEXEC);
+ }
+ if (void_dir_fd < 0) {
+ die("cannot open the void directory %s", sc_void_dir);
+ }
+ if (fchdir(void_dir_fd) < 0) {
+ die("cannot move to void directory %s", sc_void_dir);
+ }
+ debug("the process has been placed in the special void directory");
+}
+
+/**
+ * sc_cleanup_preserved_process_state releases system resources.
+**/
+static void sc_cleanup_preserved_process_state(sc_preserved_process_state *
+ proc_state)
+{
+ sc_cleanup_close(&proc_state->orig_cwd_fd);
+}
+
+static void enter_classic_execution_environment(void);
+static void enter_non_classic_execution_environment(sc_invocation * inv,
+ struct sc_apparmor *aa,
+ uid_t real_uid,
+ gid_t real_gid,
+ gid_t saved_gid);
+
int main(int argc, char **argv)
{
// Use our super-defensive parser to figure out what we've been asked to do.
- struct sc_error *err = NULL;
+ sc_error *err = NULL;
struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL;
+ sc_preserved_process_state proc_state
+ SC_CLEANUP(sc_cleanup_preserved_process_state) = {
+ .orig_umask = 0,.orig_cwd_fd = -1};
args = sc_nonfatal_parse_args(&argc, &argv, &err);
sc_die_on_error(err);
+ // Remember certain properties of the process that are clobbered by
+ // snap-confine during execution. Those are restored just before calling
+ // execv.
+ sc_preserve_and_sanitize_process_state(&proc_state);
+
// We've been asked to print the version string so let's just do that.
if (sc_args_is_version_query(args)) {
printf("%s %s\n", PACKAGE, PACKAGE_VERSION);
return 0;
}
- const char *snap_instance = getenv("SNAP_INSTANCE_NAME");
- if (snap_instance == NULL) {
+ /* Collect all invocation parameters. This gives us authoritative
+ * information about what needs to be invoked and how. The data comes
+ * from either the environment or from command line arguments */
+ sc_invocation SC_CLEANUP(sc_cleanup_invocation) invocation;
+ const char *snap_instance_name_env = getenv("SNAP_INSTANCE_NAME");
+ if (snap_instance_name_env == NULL) {
die("SNAP_INSTANCE_NAME is not set");
}
- sc_instance_name_validate(snap_instance, NULL);
-
- // Collect and validate the security tag and a few other things passed on
- // command line.
- const char *security_tag = sc_args_security_tag(args);
- if (!verify_security_tag(security_tag, snap_instance)) {
- die("security tag %s not allowed", security_tag);
- }
- const char *executable = sc_args_executable(args);
- const char *base_snap_name = sc_args_base_snap(args) ? : "core";
- bool classic_confinement = sc_args_is_classic_confinement(args);
-
- sc_snap_name_validate(base_snap_name, NULL);
-
- debug("security tag: %s", security_tag);
- debug("executable: %s", executable);
- debug("confinement: %s",
- classic_confinement ? "classic" : "non-classic");
- debug("base snap: %s", base_snap_name);
+ sc_init_invocation(&invocation, args, snap_instance_name_env);
// Who are we?
uid_t real_uid, effective_uid, saved_uid;
@@ -161,9 +354,10 @@ int main(int argc, char **argv)
char *snap_context SC_CLEANUP(sc_cleanup_string) = NULL;
// Do no get snap context value if running a hook (we don't want to overwrite hook's SNAP_COOKIE)
- if (!sc_is_hook_security_tag(security_tag)) {
- struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
- snap_context = sc_cookie_get_from_snapd(snap_instance, &err);
+ if (!sc_is_hook_security_tag(invocation.security_tag)) {
+ sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
+ snap_context =
+ sc_cookie_get_from_snapd(invocation.snap_instance, &err);
if (err != NULL) {
error("%s\n", sc_error_msg(err));
}
@@ -184,175 +378,14 @@ int main(int argc, char **argv)
}
// TODO: check for similar situation and linux capabilities.
if (geteuid() == 0) {
- if (classic_confinement) {
- /* 'classic confinement' is designed to run without the sandbox
- * inside the shared namespace. Specifically:
- * - snap-confine skips using the snap-specific mount namespace
- * - snap-confine skips using device cgroups
- * - snapd sets up a lenient AppArmor profile for snap-confine to use
- * - snapd sets up a lenient seccomp profile for snap-confine to use
- */
- debug
- ("skipping sandbox setup, classic confinement in use");
+ if (invocation.classic_confinement) {
+ enter_classic_execution_environment();
} else {
- /* snap-confine uses privately-shared /run/snapd/ns to store
- * bind-mounted mount namespaces of each snap. In the case that
- * snap-confine is invoked from the mount namespace it typically
- * constructs, the said directory does not contain mount entries
- * for preserved namespaces as those are only visible in the main,
- * outer namespace.
- *
- * In order to operate in such an environment snap-confine must
- * first re-associate its own process with another namespace in
- * which the /run/snapd/ns directory is visible. The most obvious
- * candidate is pid one, which definitely doesn't run in a
- * snap-specific namespace, has a predictable PID and is long
- * lived.
- */
- sc_reassociate_with_pid1_mount_ns();
- // Do global initialization:
- int global_lock_fd = sc_lock_global();
- // ensure that "/" or "/snap" is mounted with the
- // "shared" option, see LP:#1668659
- debug("ensuring that snap mount directory is shared");
- sc_ensure_shared_snap_mount();
- debug("unsharing snap namespace directory");
- sc_initialize_mount_ns();
- sc_unlock(global_lock_fd);
-
- // Find and open snap-update-ns and snap-discard-ns from the same
- // path as where we (snap-confine) were called.
- int snap_update_ns_fd SC_CLEANUP(sc_cleanup_close) = -1;
- snap_update_ns_fd = sc_open_snap_update_ns();
- int snap_discard_ns_fd SC_CLEANUP(sc_cleanup_close) =
- -1;
- snap_discard_ns_fd = sc_open_snap_discard_ns();
-
- // Do per-snap initialization.
- int snap_lock_fd = sc_lock_snap(snap_instance);
- debug("initializing mount namespace: %s",
- snap_instance);
- struct sc_mount_ns *group = NULL;
- group = sc_open_mount_ns(snap_instance);
-
- /* Stale mount namespace discarded or no mount namespace to
- join. We need to construct a new mount namespace ourselves.
- To capture it we will need a helper process so make one. */
- sc_fork_helper(group, &apparmor);
- int retval = sc_join_preserved_ns(group, &apparmor,
- base_snap_name,
- snap_instance,
- snap_discard_ns_fd);
- if (retval == ESRCH) {
- /* Create and populate the mount namespace. This performs all
- of the bootstrapping mounts, pivots into the new root
- filesystem and applies the per-snap mount profile using
- snap-update-ns. */
- debug
- ("unsharing the mount namespace (per-snap)");
- if (unshare(CLONE_NEWNS) < 0) {
- die("cannot unshare the mount namespace");
- }
- sc_populate_mount_ns(&apparmor,
- snap_update_ns_fd,
- base_snap_name,
- snap_instance);
-
- /* Preserve the mount namespace. */
- sc_preserve_populated_mount_ns(group);
- }
-
- /* Older versions of snap-confine created incorrect 777 permissions
- for /var/lib and we need to fixup for systems that had their NS
- created with an old version. */
- sc_maybe_fixup_permissions();
- sc_maybe_fixup_udev();
-
- // Associate each snap process with a dedicated snap freezer
- // control group. This simplifies testing if any processes
- // belonging to a given snap are still alive.
- // See the documentation of the function for details.
-
- if (getegid() != 0 && saved_gid == 0) {
- // Temporarily raise egid so we can chown the freezer cgroup
- // under LXD.
- if (setegid(0) != 0) {
- die("cannot set effective group id to root");
- }
- }
- sc_cgroup_freezer_join(snap_instance, getpid());
- if (geteuid() == 0 && real_gid != 0) {
- if (setegid(real_gid) != 0) {
- die("cannot set effective group id to %d", real_gid);
- }
- }
-
- /* User mount profiles do not apply to non-root users. */
- if (real_uid != 0) {
- debug
- ("joining preserved per-user mount namespace");
- retval =
- sc_join_preserved_per_user_ns(group,
- snap_instance);
- if (retval == ESRCH) {
- debug
- ("unsharing the mount namespace (per-user)");
- if (unshare(CLONE_NEWNS) < 0) {
- die("cannot unshare the mount namespace");
- }
- sc_setup_user_mounts(&apparmor,
- snap_update_ns_fd,
- snap_instance);
- /* Preserve the mount per-user namespace. But only if the
- * experimental feature is enabled. This way if the feature is
- * disabled user mount namespaces will still exist but will be
- * entirely ephemeral. In addition the call
- * sc_join_preserved_user_ns() will never find a preserved
- * mount namespace and will always enter this code branch. */
- if (sc_feature_enabled
- (SC_PER_USER_MOUNT_NAMESPACE)) {
- sc_preserve_populated_per_user_mount_ns
- (group);
- } else {
- debug
- ("NOT preserving per-user mount namespace");
- }
- }
- }
-
- sc_unlock(snap_lock_fd);
-
- sc_close_mount_ns(group);
-
- // Reset path as we cannot rely on the path from the host OS to
- // make sense. The classic distribution may use any PATH that makes
- // sense but we cannot assume it makes sense for the core snap
- // layout. Note that the /usr/local directories are explicitly
- // left out as they are not part of the core snap.
- debug
- ("resetting PATH to values in sync with core snap");
- setenv("PATH",
- "/usr/local/sbin:"
- "/usr/local/bin:"
- "/usr/sbin:"
- "/usr/bin:"
- "/sbin:"
- "/bin:" "/usr/games:" "/usr/local/games", 1);
- // Ensure we set the various TMPDIRs to /tmp.
- // One of the parts of setting up the mount namespace is to create a private /tmp
- // directory (this is done in sc_populate_mount_ns() above). The host environment
- // may point to a directory not accessible by snaps so we need to reset it here.
- const char *tmpd[] = { "TMPDIR", "TEMPDIR", NULL };
- int i;
- for (i = 0; tmpd[i] != NULL; i++) {
- if (setenv(tmpd[i], "/tmp", 1) != 0) {
- die("cannot set environment variable '%s'", tmpd[i]);
- }
- }
- struct snappy_udev udev_s;
- if (snappy_udev_init(security_tag, &udev_s) == 0)
- setup_devices_cgroup(security_tag, &udev_s);
- snappy_udev_cleanup(&udev_s);
+ enter_non_classic_execution_environment(&invocation,
+ &apparmor,
+ real_uid,
+ real_gid,
+ saved_gid);
}
// The rest does not so temporarily drop privs back to calling
// user (we'll permanently drop after loading seccomp)
@@ -372,14 +405,16 @@ int main(int argc, char **argv)
setup_user_xdg_runtime_dir();
#endif
// https://wiki.ubuntu.com/SecurityTeam/Specifications/SnappyConfinement
- sc_maybe_aa_change_onexec(&apparmor, security_tag);
-#ifdef HAVE_SECCOMP
- if (sc_apply_seccomp_profile_for_security_tag(security_tag)) {
+ sc_maybe_aa_change_onexec(&apparmor, invocation.security_tag);
+#ifdef HAVE_SELINUX
+ // For classic and confined snaps
+ sc_selinux_set_snap_execcon();
+#endif
+ if (sc_apply_seccomp_profile_for_security_tag(invocation.security_tag)) {
/* If the process is not explicitly unconfined then load the global
* profile as well. */
sc_apply_global_seccomp_profile();
}
-#endif // ifdef HAVE_SECCOMP
if (snap_context != NULL) {
setenv("SNAP_COOKIE", snap_context, 1);
// for compatibility, if facing older snapd.
@@ -400,12 +435,210 @@ int main(int argc, char **argv)
die("permanently dropping privs did not work");
}
// and exec the new executable
- argv[0] = (char *)executable;
- debug("execv(%s, %s...)", executable, argv[0]);
+ argv[0] = (char *)invocation.executable;
+ debug("execv(%s, %s...)", invocation.executable, argv[0]);
for (int i = 1; i < argc; ++i) {
debug(" argv[%i] = %s", i, argv[i]);
}
- execv(executable, (char *const *)&argv[0]);
+ // Restore process state that was recorded earlier.
+ sc_restore_process_state(&proc_state);
+ execv(invocation.executable, (char *const *)&argv[0]);
perror("execv failed");
return 1;
}
+
+static void enter_classic_execution_environment(void)
+{
+ /* 'classic confinement' is designed to run without the sandbox inside the
+ * shared namespace. Specifically:
+ * - snap-confine skips using the snap-specific mount namespace
+ * - snap-confine skips using device cgroups
+ * - snapd sets up a lenient AppArmor profile for snap-confine to use
+ * - snapd sets up a lenient seccomp profile for snap-confine to use
+ */
+ debug("skipping sandbox setup, classic confinement in use");
+}
+
+static void enter_non_classic_execution_environment(sc_invocation * inv,
+ struct sc_apparmor *aa,
+ uid_t real_uid,
+ gid_t real_gid,
+ gid_t saved_gid)
+{
+ /* snap-confine uses privately-shared /run/snapd/ns to store bind-mounted
+ * mount namespaces of each snap. In the case that snap-confine is invoked
+ * from the mount namespace it typically constructs, the said directory
+ * does not contain mount entries for preserved namespaces as those are
+ * only visible in the main, outer namespace.
+ *
+ * In order to operate in such an environment snap-confine must first
+ * re-associate its own process with another namespace in which the
+ * /run/snapd/ns directory is visible. The most obvious candidate is pid
+ * one, which definitely doesn't run in a snap-specific namespace, has a
+ * predictable PID and is long lived.
+ */
+ sc_reassociate_with_pid1_mount_ns();
+ // Do global initialization:
+ int global_lock_fd = sc_lock_global();
+ // ensure that "/" or "/snap" is mounted with the
+ // "shared" option, see LP:#1668659
+ debug("ensuring that snap mount directory is shared");
+ sc_ensure_shared_snap_mount();
+ debug("unsharing snap namespace directory");
+ sc_initialize_mount_ns();
+ sc_unlock(global_lock_fd);
+
+ // Find and open snap-update-ns and snap-discard-ns from the same
+ // path as where we (snap-confine) were called.
+ int snap_update_ns_fd SC_CLEANUP(sc_cleanup_close) = -1;
+ snap_update_ns_fd = sc_open_snap_update_ns();
+ int snap_discard_ns_fd SC_CLEANUP(sc_cleanup_close) = -1;
+ snap_discard_ns_fd = sc_open_snap_discard_ns();
+
+ // Do per-snap initialization.
+ int snap_lock_fd = sc_lock_snap(inv->snap_instance);
+ debug("initializing mount namespace: %s", inv->snap_instance);
+ struct sc_mount_ns *group = NULL;
+ group = sc_open_mount_ns(inv->snap_instance);
+
+ // Init and check rootfs_dir, apply any fallback behaviors.
+ sc_check_rootfs_dir(inv);
+
+ /**
+ * is_normal_mode controls if we should pivot into the base snap.
+ *
+ * There are two modes of execution for snaps that are not using classic
+ * confinement: normal and legacy. The normal mode is where snap-confine
+ * sets up a rootfs and then pivots into it using pivot_root(2). The legacy
+ * mode is when snap-confine just unshares the initial mount namespace,
+ * makes some extra changes but largely runs with what was presented to it
+ * initially.
+ *
+ * Historically the ubuntu-core distribution used the now-legacy mode. This
+ * was sensible then since snaps already (kind of) have the right root
+ * file-system and just need some privacy and isolation features applied.
+ * With the introduction of snaps to classic distributions as well as the
+ * introduction of bases, where each snap can use a different root
+ * filesystem, this lost sensibility and thus became legacy.
+ *
+ * For compatibility with current installations of ubuntu-core
+ * distributions the legacy mode is used when: the distribution is
+ * SC_DISTRO_CORE16 or when the base snap name is not "core" or
+ * "ubuntu-core".
+ *
+ * The SC_DISTRO_CORE16 is applied to systems that boot with the "core",
+ * "ubuntu-core" or "core16" snap. Systems using the "core18" base snap do
+ * not qualify for that classification.
+ **/
+ sc_distro distro = sc_classify_distro();
+ inv->is_normal_mode = distro != SC_DISTRO_CORE16 ||
+ !sc_streq(inv->orig_base_snap_name, "core");
+
+ /* Stale mount namespace discarded or no mount namespace to
+ join. We need to construct a new mount namespace ourselves.
+ To capture it we will need a helper process so make one. */
+ sc_fork_helper(group, aa);
+ int retval = sc_join_preserved_ns(group, aa, inv, snap_discard_ns_fd);
+ if (retval == ESRCH) {
+ /* Create and populate the mount namespace. This performs all
+ of the bootstrapping mounts, pivots into the new root filesystem and
+ applies the per-snap mount profile using snap-update-ns. */
+ debug("unsharing the mount namespace (per-snap)");
+ if (unshare(CLONE_NEWNS) < 0) {
+ die("cannot unshare the mount namespace");
+ }
+ sc_populate_mount_ns(aa, snap_update_ns_fd, inv);
+
+ /* Preserve the mount namespace. */
+ sc_preserve_populated_mount_ns(group);
+ }
+
+ /* Older versions of snap-confine created incorrect 777 permissions
+ for /var/lib and we need to fixup for systems that had their NS created
+ with an old version. */
+ sc_maybe_fixup_permissions();
+ sc_maybe_fixup_udev();
+
+ /* User mount profiles do not apply to non-root users. */
+ if (real_uid != 0) {
+ debug("joining preserved per-user mount namespace");
+ retval =
+ sc_join_preserved_per_user_ns(group, inv->snap_instance);
+ if (retval == ESRCH) {
+ debug("unsharing the mount namespace (per-user)");
+ if (unshare(CLONE_NEWNS) < 0) {
+ die("cannot unshare the mount namespace");
+ }
+ sc_setup_user_mounts(aa, snap_update_ns_fd,
+ inv->snap_instance);
+ /* Preserve the mount per-user namespace. But only if the
+ * experimental feature is enabled. This way if the feature is
+ * disabled user mount namespaces will still exist but will be
+ * entirely ephemeral. In addition the call
+ * sc_join_preserved_user_ns() will never find a preserved mount
+ * namespace and will always enter this code branch. */
+ if (sc_feature_enabled
+ (SC_FEATURE_PER_USER_MOUNT_NAMESPACE)) {
+ sc_preserve_populated_per_user_mount_ns(group);
+ } else {
+ debug
+ ("NOT preserving per-user mount namespace");
+ }
+ }
+ }
+ // Associate each snap process with a dedicated snap freezer cgroup and
+ // snap pids cgroup. All snap processes belonging to one snap share the
+ // freezer cgroup. All snap processes belonging to one app or one hook
+ // share the pids cgroup.
+ //
+ // This simplifies testing if any processes belonging to a given snap are
+ // still alive as well as to properly account for each application and
+ // service.
+ if (getegid() != 0 && saved_gid == 0) {
+ // Temporarily raise egid so we can chown the freezer cgroup under LXD.
+ if (setegid(0) != 0) {
+ die("cannot set effective group id to root");
+ }
+ }
+ sc_cgroup_freezer_join(inv->snap_instance, getpid());
+ if (sc_feature_enabled(SC_FEATURE_REFRESH_APP_AWARENESS)) {
+ sc_cgroup_pids_join(inv->security_tag, getpid());
+ }
+ if (geteuid() == 0 && real_gid != 0) {
+ if (setegid(real_gid) != 0) {
+ die("cannot set effective group id to %d", real_gid);
+ }
+ }
+
+ sc_unlock(snap_lock_fd);
+
+ sc_close_mount_ns(group);
+
+ // Reset path as we cannot rely on the path from the host OS to make sense.
+ // The classic distribution may use any PATH that makes sense but we cannot
+ // assume it makes sense for the core snap layout. Note that the /usr/local
+ // directories are explicitly left out as they are not part of the core
+ // snap.
+ debug("resetting PATH to values in sync with core snap");
+ setenv("PATH",
+ "/usr/local/sbin:"
+ "/usr/local/bin:"
+ "/usr/sbin:"
+ "/usr/bin:"
+ "/sbin:" "/bin:" "/usr/games:" "/usr/local/games", 1);
+ // Ensure we set the various TMPDIRs to /tmp. One of the parts of setting
+ // up the mount namespace is to create a private /tmp directory (this is
+ // done in sc_populate_mount_ns() above). The host environment may point to
+ // a directory not accessible by snaps so we need to reset it here.
+ const char *tmpd[] = { "TMPDIR", "TEMPDIR", NULL };
+ int i;
+ for (i = 0; tmpd[i] != NULL; i++) {
+ if (setenv(tmpd[i], "/tmp", 1) != 0) {
+ die("cannot set environment variable '%s'", tmpd[i]);
+ }
+ }
+ struct snappy_udev udev_s;
+ if (snappy_udev_init(inv->security_tag, &udev_s) == 0)
+ setup_devices_cgroup(inv->security_tag, &udev_s);
+ snappy_udev_cleanup(&udev_s);
+}
diff -pruN 2.37.4-1/cmd/snap-confine/snap-confine-invocation.c 2.39.2+19.10ubuntu1/cmd/snap-confine/snap-confine-invocation.c
--- 2.37.4-1/cmd/snap-confine/snap-confine-invocation.c 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-confine/snap-confine-invocation.c 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+#include "snap-confine-invocation.h"
+
+#include
+#include
+#include
+
+#include "../libsnap-confine-private/cleanup-funcs.h"
+#include "../libsnap-confine-private/snap.h"
+#include "../libsnap-confine-private/string-utils.h"
+#include "../libsnap-confine-private/utils.h"
+
+void sc_init_invocation(sc_invocation *inv, const struct sc_args *args, const char *snap_instance) {
+ /* Snap instance name is conveyed via untrusted environment. It may be
+ * unset (typically when experimenting with snap-confine by hand). It
+ * must also be a valid snap instance name. */
+ if (snap_instance == NULL) {
+ die("cannot use NULL snap instance name");
+ }
+ sc_instance_name_validate(snap_instance, NULL);
+
+ /* The security tag is conveyed via untrusted command line. It must be
+ * in agreement with snap instance name and must be a valid security
+ * tag. */
+ const char *security_tag = sc_args_security_tag(args);
+ if (!verify_security_tag(security_tag, snap_instance)) {
+ die("security tag %s not allowed", security_tag);
+ }
+
+ /* The base snap name is conveyed via untrusted, optional, command line
+ * argument. It may be omitted where it implies the "core" snap is the
+ * base. */
+ const char *base_snap_name = sc_args_base_snap(args);
+ if (base_snap_name == NULL) {
+ base_snap_name = "core";
+ }
+ sc_snap_name_validate(base_snap_name, NULL);
+
+ /* The executable is conveyed via untrusted command line. It must be set
+ * but cannot be validated further than that at this time. It might be
+ * arguable to validate it to be snap-exec in one of the well-known
+ * locations or one of the special-cases like strace / gdb but this is
+ * not done at this time. */
+ const char *executable = sc_args_executable(args);
+ if (executable == NULL) {
+ die("cannot run with NULL executable");
+ }
+
+ /* Invocation helps to pass relevant data to various parts of snap-confine. */
+ memset(inv, 0, sizeof *inv);
+ inv->base_snap_name = sc_strdup(base_snap_name);
+ inv->orig_base_snap_name = sc_strdup(base_snap_name);
+ inv->executable = sc_strdup(executable);
+ inv->security_tag = sc_strdup(security_tag);
+ inv->snap_instance = sc_strdup(snap_instance);
+ inv->classic_confinement = sc_args_is_classic_confinement(args);
+
+ // construct rootfs_dir based on base_snap_name
+ char mount_point[PATH_MAX] = {0};
+ sc_must_snprintf(mount_point, sizeof mount_point, "%s/%s/current", SNAP_MOUNT_DIR, inv->base_snap_name);
+ inv->rootfs_dir = sc_strdup(mount_point);
+
+ debug("security tag: %s", inv->security_tag);
+ debug("executable: %s", inv->executable);
+ debug("confinement: %s", inv->classic_confinement ? "classic" : "non-classic");
+ debug("base snap: %s", inv->base_snap_name);
+}
+
+void sc_cleanup_invocation(sc_invocation *inv) {
+ if (inv != NULL) {
+ sc_cleanup_string(&inv->snap_instance);
+ sc_cleanup_string(&inv->base_snap_name);
+ sc_cleanup_string(&inv->orig_base_snap_name);
+ sc_cleanup_string(&inv->security_tag);
+ sc_cleanup_string(&inv->executable);
+ sc_cleanup_string(&inv->rootfs_dir);
+ }
+}
+
+void sc_check_rootfs_dir(sc_invocation *inv) {
+ if (access(inv->rootfs_dir, F_OK) == 0) {
+ return;
+ }
+
+ /* As a special fallback, allow the base snap to degrade from "core" to
+ * "ubuntu-core". This is needed for the migration from old
+ * ubuntu-core based systems to the new core.
+ */
+ if (sc_streq(inv->base_snap_name, "core")) {
+ char mount_point[PATH_MAX] = {0};
+
+ /* For "core" we can still use the ubuntu-core snap. This is helpful in
+ * the migration path when new snap-confine runs before snapd has
+ * finished obtaining the core snap. */
+ sc_must_snprintf(mount_point, sizeof mount_point, "%s/%s/current", SNAP_MOUNT_DIR, "ubuntu-core");
+ if (access(mount_point, F_OK) == 0) {
+ sc_cleanup_string(&inv->base_snap_name);
+ inv->base_snap_name = sc_strdup("ubuntu-core");
+ sc_cleanup_string(&inv->rootfs_dir);
+ inv->rootfs_dir = sc_strdup(mount_point);
+ debug("falling back to ubuntu-core instead of unavailable core snap");
+ return;
+ }
+ }
+
+ if (sc_streq(inv->base_snap_name, "core16")) {
+ char mount_point[PATH_MAX] = {0};
+
+ /* For "core16" we can still use the "core" snap. This is useful
+ * to help people transition to core16 bases without requiring
+ * twice the disk space.
+ */
+ sc_must_snprintf(mount_point, sizeof mount_point, "%s/%s/current", SNAP_MOUNT_DIR, "core");
+ if (access(mount_point, F_OK) == 0) {
+ sc_cleanup_string(&inv->base_snap_name);
+ inv->base_snap_name = sc_strdup("core");
+ sc_cleanup_string(&inv->rootfs_dir);
+ inv->rootfs_dir = sc_strdup(mount_point);
+ debug("falling back to core instead of unavailable core16 snap");
+ return;
+ }
+ }
+
+ die("cannot locate base snap %s", inv->base_snap_name);
+}
diff -pruN 2.37.4-1/cmd/snap-confine/snap-confine-invocation.h 2.39.2+19.10ubuntu1/cmd/snap-confine/snap-confine-invocation.h
--- 2.37.4-1/cmd/snap-confine/snap-confine-invocation.h 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-confine/snap-confine-invocation.h 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+#ifndef SC_SNAP_CONFINE_INVOCATION_H
+#define SC_SNAP_CONFINE_INVOCATION_H
+
+#include
+
+#include "snap-confine-args.h"
+
+/**
+ * sc_invocation contains information about how snap-confine was invoked.
+ *
+ * All of the pointer fields have the life-cycle bound to the main process.
+ **/
+typedef struct sc_invocation {
+ /* Things declared by the system. */
+ char *snap_instance;
+ char *orig_base_snap_name;
+ char *security_tag;
+ char *executable;
+ bool classic_confinement;
+ /* Things derived at runtime. */
+ char *base_snap_name;
+ char *rootfs_dir;
+ bool is_normal_mode;
+} sc_invocation;
+
+/**
+ * sc_init_invocation initializes the invocation object.
+ *
+ * Invocation is constructed based on command line arguments as well as
+ * environment value (SNAP_INSTANCE_NAME). All input is untrusted and is
+ * validated internally.
+ **/
+void sc_init_invocation(sc_invocation *inv, const struct sc_args *args, const char *snap_instance);
+
+/**
+ * sc_cleanup_invocation is a cleanup function for sc_invocation.
+ *
+ * Cleanup functions are automatically called by the compiler whenever a
+ * variable gets out of scope, like C++ destructors would.
+ *
+ * This function is designed to be used with SC_CLEANUP(sc_cleanup_invocation).
+ **/
+void sc_cleanup_invocation(sc_invocation *inv);
+
+/**
+ * sc_check_rootfs_dir checks the rootfs_dir and applies potential fall-backs.
+ *
+ * Checks that the rootfs_dir for the given base_snap exists and may apply
+ * the fallback logic below. Will die() if no base_snap can be found.
+ *
+ * When performing ubuntu-core to core migration, the snap "core" may not be
+ * mounted yet. In that mode when snapd instructs us to use "core" as the base
+ * snap name snap-confine may choose to transparently fallback to "ubuntu-core"
+ * it that is available instead.
+ *
+ * This check must be performed in the regular mount namespace (that is, that
+ * of the init process) because it relies on the value of compile-time-choice
+ * of SNAP_MOUNT_DIR.
+ **/
+void sc_check_rootfs_dir(sc_invocation *inv);
+
+#endif
diff -pruN 2.37.4-1/cmd/snap-confine/snap-device-helper-test.c 2.39.2+19.10ubuntu1/cmd/snap-confine/snap-device-helper-test.c
--- 2.37.4-1/cmd/snap-confine/snap-device-helper-test.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-confine/snap-device-helper-test.c 2019-06-05 06:41:21.000000000 +0000
@@ -98,10 +98,10 @@ static int run_sdh(gchar * action,
struct sdh_test_data {
char *action;
- // snap.foo.bar
+ // snap.foo.bar
char *app;
- // snap_foo_bar
- char *mangled_appname;
+ // snap_foo_bar
+ char *mangled_appname;
char *file_with_data;
char *file_with_no_data;
};
@@ -144,7 +144,9 @@ static void test_sdh_action(gconstpointe
g_assert_false(g_file_get_contents(without_data, &data, NULL, NULL));
- ret = run_sdh(td->action, td->mangled_appname, "/devices/foo/tty/ttyS0", "4:64");
+ ret =
+ run_sdh(td->action, td->mangled_appname, "/devices/foo/tty/ttyS0",
+ "4:64");
g_assert_cmpint(ret, ==, 0);
g_assert_true(g_file_get_contents(with_data, &data, NULL, NULL));
g_assert_cmpstr(data, ==, "c 4:64 rwm\n");
@@ -193,23 +195,31 @@ static void test_sdh_err(void)
static struct sdh_test_data add_data =
{ "add", "snap.foo.bar", "snap_foo_bar", "devices.allow", "devices.deny" };
static struct sdh_test_data change_data =
- { "change", "snap.foo.bar", "snap_foo_bar", "devices.allow", "devices.deny" };
+ { "change", "snap.foo.bar", "snap_foo_bar", "devices.allow",
+"devices.deny" };
static struct sdh_test_data remove_data =
- { "remove", "snap.foo.bar", "snap_foo_bar", "devices.deny", "devices.allow" };
+ { "remove", "snap.foo.bar", "snap_foo_bar", "devices.deny",
+"devices.allow" };
static struct sdh_test_data instance_add_data =
- { "add", "snap.foo_bar.baz", "snap_foo_bar_baz", "devices.allow", "devices.deny" };
+ { "add", "snap.foo_bar.baz", "snap_foo_bar_baz", "devices.allow",
+"devices.deny" };
static struct sdh_test_data instance_change_data =
- { "change", "snap.foo_bar.baz", "snap_foo_bar_baz", "devices.allow", "devices.deny" };
+ { "change", "snap.foo_bar.baz", "snap_foo_bar_baz", "devices.allow",
+"devices.deny" };
static struct sdh_test_data instance_remove_data =
- { "remove", "snap.foo_bar.baz", "snap_foo_bar_baz", "devices.deny", "devices.allow" };
+ { "remove", "snap.foo_bar.baz", "snap_foo_bar_baz", "devices.deny",
+"devices.allow" };
static struct sdh_test_data add_hook_data =
- { "add", "snap.foo.hook.configure", "snap_foo_hook_configure", "devices.allow", "devices.deny" };
+ { "add", "snap.foo.hook.configure", "snap_foo_hook_configure",
+"devices.allow", "devices.deny" };
static struct sdh_test_data instance_add_hook_data =
- { "add", "snap.foo_bar.hook.configure", "snap_foo_bar_hook_configure", "devices.allow", "devices.deny" };
+ { "add", "snap.foo_bar.hook.configure", "snap_foo_bar_hook_configure",
+"devices.allow", "devices.deny" };
static struct sdh_test_data instance_add_instance_name_is_hook_data =
- { "add", "snap.foo_hook.hook.configure", "snap_foo_hook_hook_configure", "devices.allow", "devices.deny" };
+ { "add", "snap.foo_hook.hook.configure", "snap_foo_hook_hook_configure",
+"devices.allow", "devices.deny" };
-static void __attribute__ ((constructor)) init(void)
+static void __attribute__((constructor)) init(void)
{
g_test_add_data_func("/snap-device-helper/add",
@@ -225,11 +235,12 @@ static void __attribute__ ((constructor)
&instance_change_data, test_sdh_action);
g_test_add_data_func("/snap-device-helper/parallel/remove",
&instance_remove_data, test_sdh_action);
- // hooks
+ // hooks
g_test_add_data_func("/snap-device-helper/hook/add",
&add_hook_data, test_sdh_action);
g_test_add_data_func("/snap-device-helper/hook/parallel/add",
&instance_add_hook_data, test_sdh_action);
g_test_add_data_func("/snap-device-helper/hook-name-hook/parallel/add",
- &instance_add_instance_name_is_hook_data, test_sdh_action);
+ &instance_add_instance_name_is_hook_data,
+ test_sdh_action);
}
diff -pruN 2.37.4-1/cmd/snap-confine/spread-tests/spread-prepare.sh 2.39.2+19.10ubuntu1/cmd/snap-confine/spread-tests/spread-prepare.sh
--- 2.37.4-1/cmd/snap-confine/spread-tests/spread-prepare.sh 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-confine/spread-tests/spread-prepare.sh 2019-06-05 06:41:21.000000000 +0000
@@ -56,7 +56,7 @@ build_debian_or_ubuntu_package() {
git clone -b "$distro_packaging_git_branch" "$distro_packaging_git" distro-packaging
# Install all the build dependencies declared by the package.
- apt-get install --quiet -y gdebi-core
+ eatmydata apt-get install --quiet -y gdebi-core
gdebi --quiet --apt-line ./distro-packaging/debian/control | xargs -r apt-get install --quiet -y
# Generate a new upstream tarball from the current state of the tree
diff -pruN 2.37.4-1/cmd/snap-confine/udev-support.c 2.39.2+19.10ubuntu1/cmd/snap-confine/udev-support.c
--- 2.37.4-1/cmd/snap-confine/udev-support.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-confine/udev-support.c 2019-06-05 06:41:21.000000000 +0000
@@ -108,7 +108,8 @@ void run_snappy_app_dev_add(struct snapp
dev_t devnum = udev_device_get_devnum(d);
udev_device_unref(d);
- _run_snappy_app_dev_add_majmin(udev_s, path, major(devnum), minor(devnum));
+ _run_snappy_app_dev_add_majmin(udev_s, path, major(devnum),
+ minor(devnum));
}
/*
diff -pruN 2.37.4-1/cmd/snapd/main.go 2.39.2+19.10ubuntu1/cmd/snapd/main.go
--- 2.37.4-1/cmd/snapd/main.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snapd/main.go 2019-06-05 06:41:21.000000000 +0000
@@ -64,7 +64,7 @@ func main() {
// data/systemd/snapd.service.in:SuccessExitStatus=
os.Exit(42)
}
- fmt.Fprintf(os.Stderr, "error: %v\n", err)
+ fmt.Fprintf(os.Stderr, "cannot run daemon: %v\n", err)
os.Exit(1)
}
}
diff -pruN 2.37.4-1/cmd/snapd/main_test.go 2.39.2+19.10ubuntu1/cmd/snapd/main_test.go
--- 2.37.4-1/cmd/snapd/main_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snapd/main_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -31,6 +31,8 @@ import (
"github.com/snapcore/snapd/client"
"github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/interfaces/apparmor"
+ "github.com/snapcore/snapd/interfaces/seccomp"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/testutil"
@@ -58,6 +60,12 @@ func (s *snapdSuite) SetUpTest(c *C) {
func (s *snapdSuite) TestSanityFailGoesIntoDegradedMode(c *C) {
logbuf, restore := logger.MockLogger()
defer restore()
+ restore = apparmor.MockIsHomeUsingNFS(func() (bool, error) { return false, nil })
+ defer restore()
+ restore = seccomp.MockSnapSeccompVersionInfo(func(s seccomp.Compiler) (string, error) {
+ return "abcdef 1.2.3 1234abcd", nil
+ })
+ defer restore()
sanityErr := fmt.Errorf("foo failed")
sanityCalled := make(chan bool)
diff -pruN 2.37.4-1/cmd/snapd-generator/main.c 2.39.2+19.10ubuntu1/cmd/snapd-generator/main.c
--- 2.37.4-1/cmd/snapd-generator/main.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snapd-generator/main.c 2019-06-05 06:41:21.000000000 +0000
@@ -25,10 +25,9 @@
#include "../libsnap-confine-private/mountinfo.h"
#include "../libsnap-confine-private/string-utils.h"
-static struct sc_mountinfo_entry *find_root_mountinfo(struct sc_mountinfo
- *mounts)
+static sc_mountinfo_entry *find_root_mountinfo(sc_mountinfo * mounts)
{
- struct sc_mountinfo_entry *cur, *root = NULL;
+ sc_mountinfo_entry *cur, *root = NULL;
for (cur = sc_first_mountinfo_entry(mounts); cur != NULL;
cur = sc_next_mountinfo_entry(cur)) {
// Look for the mount info entry for the root file-system.
@@ -52,14 +51,14 @@ int main(int argc, char **argv)
// const char *late_dir = argv[3];
// Load /proc/self/mountinfo so that we can inspect the root filesystem.
- struct sc_mountinfo *mounts SC_CLEANUP(sc_cleanup_mountinfo) = NULL;
+ sc_mountinfo *mounts SC_CLEANUP(sc_cleanup_mountinfo) = NULL;
mounts = sc_parse_mountinfo(NULL);
if (!mounts) {
fprintf(stderr, "cannot open or parse /proc/self/mountinfo\n");
return 1;
}
- struct sc_mountinfo_entry *root = find_root_mountinfo(mounts);
+ sc_mountinfo_entry *root = find_root_mountinfo(mounts);
if (!root) {
fprintf(stderr,
"cannot find mountinfo entry of the root filesystem\n");
diff -pruN 2.37.4-1/cmd/snap-discard-ns/snap-discard-ns.c 2.39.2+19.10ubuntu1/cmd/snap-discard-ns/snap-discard-ns.c
--- 2.37.4-1/cmd/snap-discard-ns/snap-discard-ns.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-discard-ns/snap-discard-ns.c 2019-06-05 06:41:21.000000000 +0000
@@ -59,7 +59,7 @@ int main(int argc, char** argv) {
snap_instance_name = argv[1];
}
- struct sc_error* err = NULL;
+ sc_error* err = NULL;
sc_instance_name_validate(snap_instance_name, &err);
sc_die_on_error(err);
@@ -101,7 +101,7 @@ int main(int argc, char** argv) {
*
* Applied mount profiles to unlink:
* - "snap.$SNAP_INSTANCE_NAME.fstab"
- * - "snap.$SNAP_INSTANCE_NAME.[0-9]+.fstab"
+ * - "snap.$SNAP_INSTANCE_NAME.[0-9]+.user-fstab"
*
* Use PATH_MAX as the size of each buffer since those can store any file
* name. */
@@ -110,7 +110,7 @@ int main(int argc, char** argv) {
char sys_mnt_pattern[PATH_MAX];
char usr_mnt_pattern[PATH_MAX];
sc_must_snprintf(sys_fstab_pattern, sizeof sys_fstab_pattern, "snap\\.%s\\.fstab", snap_instance_name);
- sc_must_snprintf(usr_fstab_pattern, sizeof usr_fstab_pattern, "snap\\.%s\\.*\\.fstab", snap_instance_name);
+ sc_must_snprintf(usr_fstab_pattern, sizeof usr_fstab_pattern, "snap\\.%s\\.*\\.user-fstab", snap_instance_name);
sc_must_snprintf(sys_mnt_pattern, sizeof sys_mnt_pattern, "%s\\.mnt", snap_instance_name);
sc_must_snprintf(usr_mnt_pattern, sizeof usr_mnt_pattern, "%s\\.*\\.mnt", snap_instance_name);
diff -pruN 2.37.4-1/cmd/snap-discard-ns/snap-discard-ns.rst 2.39.2+19.10ubuntu1/cmd/snap-discard-ns/snap-discard-ns.rst
--- 2.37.4-1/cmd/snap-discard-ns/snap-discard-ns.rst 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-discard-ns/snap-discard-ns.rst 2019-06-05 06:41:21.000000000 +0000
@@ -51,7 +51,7 @@ FILES
`snap-discard-ns`. The second form is for the per-user mount namespace.
`/run/snapd/ns/snap.$SNAP_INSTNACE_NAME.fstab`:
-`/run/snapd/ns/snap.$SNAP_INSTNACE_NAME.*.fstab`:
+`/run/snapd/ns/snap.$SNAP_INSTNACE_NAME.*.user-fstab`:
The current mount profile of a preserved mount namespace that is removed
by `snap-discard-ns`.
diff -pruN 2.37.4-1/cmd/snap-failure/cmd_snapd_test.go 2.39.2+19.10ubuntu1/cmd/snap-failure/cmd_snapd_test.go
--- 2.37.4-1/cmd/snap-failure/cmd_snapd_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-failure/cmd_snapd_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -33,6 +33,5 @@ func (r *failureSuite) TestRun(c *C) {
os.Args = []string{"snap-failure", "snapd"}
err := failure.Run()
c.Check(err, IsNil)
- c.Check(r.Stdout(), HasLen, 0)
c.Check(r.Stderr(), HasLen, 0)
}
diff -pruN 2.37.4-1/cmd/snap-failure/main.go 2.39.2+19.10ubuntu1/cmd/snap-failure/main.go
--- 2.37.4-1/cmd/snap-failure/main.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-failure/main.go 2019-06-05 06:41:21.000000000 +0000
@@ -31,7 +31,6 @@ import (
)
var (
- Stdout io.Writer = os.Stdout
Stderr io.Writer = os.Stderr
opts struct{}
diff -pruN 2.37.4-1/cmd/snap-failure/main_test.go 2.39.2+19.10ubuntu1/cmd/snap-failure/main_test.go
--- 2.37.4-1/cmd/snap-failure/main_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-failure/main_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -38,18 +38,12 @@ type failureSuite struct {
rootdir string
- stdout *bytes.Buffer
stderr *bytes.Buffer
}
func (r *failureSuite) SetUpTest(c *C) {
- r.stdout = bytes.NewBuffer(nil)
r.stderr = bytes.NewBuffer(nil)
- oldStdout := failure.Stdout
- r.AddCleanup(func() { failure.Stdout = oldStdout })
- failure.Stdout = r.stdout
-
oldStderr := failure.Stderr
r.AddCleanup(func() { failure.Stderr = oldStderr })
failure.Stderr = r.stderr
@@ -59,10 +53,6 @@ func (r *failureSuite) SetUpTest(c *C) {
r.AddCleanup(func() { dirs.SetRootDir("/") })
}
-func (r *failureSuite) Stdout() string {
- return r.stdout.String()
-}
-
func (r *failureSuite) Stderr() string {
return r.stderr.String()
}
diff -pruN 2.37.4-1/cmd/snapinfo.go 2.39.2+19.10ubuntu1/cmd/snapinfo.go
--- 2.37.4-1/cmd/snapinfo.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snapinfo.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,198 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2018 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 .
+ *
+ */
+
+package cmd
+
+import (
+ "fmt"
+ "path/filepath"
+ "sort"
+ "strings"
+
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/progress"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/systemd"
+)
+
+func ClientSnapFromSnapInfo(snapInfo *snap.Info) (*client.Snap, error) {
+ var publisher *snap.StoreAccount
+ if snapInfo.Publisher.Username != "" {
+ publisher = &snapInfo.Publisher
+ }
+
+ confinement := snapInfo.Confinement
+ if confinement == "" {
+ confinement = snap.StrictConfinement
+ }
+
+ snapapps := make([]*snap.AppInfo, 0, len(snapInfo.Apps))
+ for _, app := range snapInfo.Apps {
+ snapapps = append(snapapps, app)
+ }
+ sort.Sort(BySnapApp(snapapps))
+
+ apps, err := ClientAppInfosFromSnapAppInfos(snapapps)
+ result := &client.Snap{
+ Description: snapInfo.Description(),
+ Developer: snapInfo.Publisher.Username,
+ Publisher: publisher,
+ Icon: snapInfo.Media.IconURL(),
+ ID: snapInfo.SnapID,
+ InstallDate: snapInfo.InstallDate(),
+ Name: snapInfo.InstanceName(),
+ Revision: snapInfo.Revision,
+ Summary: snapInfo.Summary(),
+ Type: string(snapInfo.Type),
+ Base: snapInfo.Base,
+ Version: snapInfo.Version,
+ Channel: snapInfo.Channel,
+ Private: snapInfo.Private,
+ Confinement: string(confinement),
+ Apps: apps,
+ Broken: snapInfo.Broken,
+ Contact: snapInfo.Contact,
+ Title: snapInfo.Title(),
+ License: snapInfo.License,
+ Screenshots: snapInfo.Media.Screenshots(),
+ Media: snapInfo.Media,
+ Prices: snapInfo.Prices,
+ Channels: snapInfo.Channels,
+ Tracks: snapInfo.Tracks,
+ CommonIDs: snapInfo.CommonIDs,
+ }
+
+ return result, err
+}
+
+func ClientAppInfoNotes(app *client.AppInfo) string {
+ if !app.IsService() {
+ return "-"
+ }
+
+ var notes = make([]string, 0, 2)
+ var seenTimer, seenSocket bool
+ for _, act := range app.Activators {
+ switch act.Type {
+ case "timer":
+ seenTimer = true
+ case "socket":
+ seenSocket = true
+ }
+ }
+ if seenTimer {
+ notes = append(notes, "timer-activated")
+ }
+ if seenSocket {
+ notes = append(notes, "socket-activated")
+ }
+ if len(notes) == 0 {
+ return "-"
+ }
+ return strings.Join(notes, ",")
+}
+
+// BySnapApp sorts apps by (snap name, app name)
+type BySnapApp []*snap.AppInfo
+
+func (a BySnapApp) Len() int { return len(a) }
+func (a BySnapApp) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+func (a BySnapApp) Less(i, j int) bool {
+ iName := a[i].Snap.InstanceName()
+ jName := a[j].Snap.InstanceName()
+ if iName == jName {
+ return a[i].Name < a[j].Name
+ }
+ return iName < jName
+}
+
+func ClientAppInfosFromSnapAppInfos(apps []*snap.AppInfo) ([]client.AppInfo, error) {
+ // TODO: pass in an actual notifier here instead of null
+ // (Status doesn't _need_ it, but benefits from it)
+ sysd := systemd.New(dirs.GlobalRootDir, progress.Null)
+
+ out := make([]client.AppInfo, 0, len(apps))
+ for _, app := range apps {
+ appInfo := client.AppInfo{
+ Snap: app.Snap.InstanceName(),
+ Name: app.Name,
+ CommonID: app.CommonID,
+ }
+ if fn := app.DesktopFile(); osutil.FileExists(fn) {
+ appInfo.DesktopFile = fn
+ }
+
+ appInfo.Daemon = app.Daemon
+ if !app.IsService() || !app.Snap.IsActive() {
+ out = append(out, appInfo)
+ continue
+ }
+
+ // collect all services for a single call to systemctl
+ serviceNames := make([]string, 0, 1+len(app.Sockets)+1)
+ serviceNames = append(serviceNames, app.ServiceName())
+
+ sockSvcFileToName := make(map[string]string, len(app.Sockets))
+ for _, sock := range app.Sockets {
+ sockUnit := filepath.Base(sock.File())
+ sockSvcFileToName[sockUnit] = sock.Name
+ serviceNames = append(serviceNames, sockUnit)
+ }
+ if app.Timer != nil {
+ timerUnit := filepath.Base(app.Timer.File())
+ serviceNames = append(serviceNames, timerUnit)
+ }
+
+ // sysd.Status() makes sure that we get only the units we asked
+ // for and raises an error otherwise
+ sts, err := sysd.Status(serviceNames...)
+ if err != nil {
+ return nil, fmt.Errorf("cannot get status of services of app %q: %v", app.Name, err)
+ }
+ if len(sts) != len(serviceNames) {
+ return nil, fmt.Errorf("cannot get status of services of app %q: expected %v results, got %v", app.Name, len(serviceNames), len(sts))
+ }
+ for _, st := range sts {
+ switch filepath.Ext(st.UnitName) {
+ case ".service":
+ appInfo.Enabled = st.Enabled
+ appInfo.Active = st.Active
+ case ".timer":
+ appInfo.Activators = append(appInfo.Activators, client.AppActivator{
+ Name: app.Name,
+ Enabled: st.Enabled,
+ Active: st.Active,
+ Type: "timer",
+ })
+ case ".socket":
+ appInfo.Activators = append(appInfo.Activators, client.AppActivator{
+ Name: sockSvcFileToName[st.UnitName],
+ Enabled: st.Enabled,
+ Active: st.Active,
+ Type: "socket",
+ })
+ }
+ }
+ out = append(out, appInfo)
+ }
+
+ return out, nil
+}
diff -pruN 2.37.4-1/cmd/snapinfo_test.go 2.39.2+19.10ubuntu1/cmd/snapinfo_test.go
--- 2.37.4-1/cmd/snapinfo_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snapinfo_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,71 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2018 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 .
+ *
+ */
+
+package cmd_test
+
+import (
+ "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/cmd"
+)
+
+func (*cmdSuite) TestAppStatusNotes(c *check.C) {
+ ai := client.AppInfo{}
+ c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "-")
+
+ ai = client.AppInfo{
+ Daemon: "oneshot",
+ }
+ c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "-")
+
+ ai = client.AppInfo{
+ Daemon: "oneshot",
+ Activators: []client.AppActivator{
+ {Type: "timer"},
+ },
+ }
+ c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "timer-activated")
+
+ ai = client.AppInfo{
+ Daemon: "oneshot",
+ Activators: []client.AppActivator{
+ {Type: "socket"},
+ },
+ }
+ c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "socket-activated")
+
+ // check that the output is stable regardless of the order of activators
+ ai = client.AppInfo{
+ Daemon: "oneshot",
+ Activators: []client.AppActivator{
+ {Type: "timer"},
+ {Type: "socket"},
+ },
+ }
+ c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "timer-activated,socket-activated")
+ ai = client.AppInfo{
+ Daemon: "oneshot",
+ Activators: []client.AppActivator{
+ {Type: "socket"},
+ {Type: "timer"},
+ },
+ }
+ c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "timer-activated,socket-activated")
+}
diff -pruN 2.37.4-1/cmd/snaplock/lock.go 2.39.2+19.10ubuntu1/cmd/snaplock/lock.go
--- 2.37.4-1/cmd/snaplock/lock.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snaplock/lock.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,48 @@
+// -*- 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 .
+ *
+ */
+
+// Package snaplock offers per-snap locking also used by snap-confine.
+// The corresponding C code is in libsnap-confine-private/locking.c
+package snaplock
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+)
+
+// lockFileName returns the name of the lock file for the given snap.
+func lockFileName(snapName string) string {
+ return filepath.Join(dirs.SnapRunLockDir, fmt.Sprintf("%s.lock", snapName))
+}
+
+// OpenLock creates and opens a lock file associated with a particular snap.
+func OpenLock(snapName string) (*osutil.FileLock, error) {
+ if err := os.MkdirAll(dirs.SnapRunLockDir, 0700); err != nil {
+ return nil, fmt.Errorf("cannot create lock directory: %s", err)
+ }
+ flock, err := osutil.NewFileLock(lockFileName(snapName))
+ if err != nil {
+ return nil, err
+ }
+ return flock, nil
+}
diff -pruN 2.37.4-1/cmd/snaplock/lock_test.go 2.39.2+19.10ubuntu1/cmd/snaplock/lock_test.go
--- 2.37.4-1/cmd/snaplock/lock_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snaplock/lock_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,59 @@
+// -*- 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 .
+ *
+ */
+
+package snaplock_test
+
+import (
+ "os"
+ "strings"
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/cmd/snaplock"
+ "github.com/snapcore/snapd/dirs"
+)
+
+func Test(t *testing.T) {
+ TestingT(t)
+}
+
+type lockSuite struct{}
+
+var _ = Suite(&lockSuite{})
+
+func (s *lockSuite) SetUpTest(c *C) {
+ dirs.SetRootDir(c.MkDir())
+}
+
+func (s *lockSuite) TearDownTest(c *C) {
+ dirs.SetRootDir("")
+}
+
+func (s *lockSuite) TestOpenLock(c *C) {
+ lock, err := snaplock.OpenLock("name")
+ c.Assert(err, IsNil)
+ defer lock.Close()
+
+ _, err = os.Stat(lock.Path())
+ c.Assert(err, IsNil)
+
+ comment := Commentf("wrong prefix for %q, want %q", lock.Path(), dirs.SnapRunLockDir)
+ c.Check(strings.HasPrefix(lock.Path(), dirs.SnapRunLockDir), Equals, true, comment)
+}
diff -pruN 2.37.4-1/cmd/snap-mgmt/snap-mgmt-selinux.sh.in 2.39.2+19.10ubuntu1/cmd/snap-mgmt/snap-mgmt-selinux.sh.in
--- 2.37.4-1/cmd/snap-mgmt/snap-mgmt-selinux.sh.in 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-mgmt/snap-mgmt-selinux.sh.in 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,111 @@
+#!/bin/bash
+
+set -e
+
+SNAP_MOUNT_DIR="@SNAP_MOUNT_DIR@"
+
+show_help() {
+ exec cat <<'EOF'
+Usage: snap-mgmt-selinux.sh [OPTIONS]
+
+A helper script to manage SELinux contexts used by snapd
+
+Arguments:
+ --snap-mount-dir= Provide a path to be used as $SNAP_MOUNT_DIR
+ --patch-selinux-mount-context= Add SELinux context to mount units
+ --remove-selinux-mount-context= Remove SELinux context from mount units
+EOF
+}
+
+SNAP_UNIT_PREFIX="$(systemd-escape -p ${SNAP_MOUNT_DIR})"
+
+patch_selinux_mount_context() {
+ if ! command -v selinuxenabled > /dev/null; then
+ return
+ fi
+ if ! selinuxenabled; then
+ # The tools are there, but SELinux is not enabled
+ return
+ fi
+
+ selinux_mount_context="$1"
+ remove="$2"
+ if ! echo "$selinux_mount_context" | grep -qE '[a-zA-Z0-9_]+(:[a-zA-Z0-9_]+){2,3}'; then
+ echo "invalid mount context '$selinux_mount_context'"
+ exit 1
+ fi
+ context_opt="context=$selinux_mount_context"
+
+ mounts=$(systemctl list-unit-files --no-legend --full "$SNAP_UNIT_PREFIX-*.mount" | cut -f1 -d ' ' || true)
+ changed_mounts=
+ for unit in $mounts; do
+ # Ensure its really a snap mount unit or systemd unit
+ if ! grep -q 'What=/var/lib/snapd/snaps/' "/etc/systemd/system/$unit" && ! grep -q 'X-Snappy=yes' "/etc/systemd/system/$unit"; then
+ echo "Skipping non-snapd systemd unit $unit"
+ continue
+ fi
+
+ if [ "$remove" == "" ]; then
+ if grep -q "Options=.*,$context_opt" < "/etc/systemd/system/$unit"; then
+ # already patched
+ continue
+ fi
+
+ if ! sed -i -e "s#^\\(Options=nodev.*\\)#\\1,$context_opt#" "/etc/systemd/system/$unit"; then
+ echo "Cannot patch $unit"
+ fi
+
+ changed_mounts="$changed_mounts $unit"
+ elif [ "$remove" == "remove" ]; then
+ if ! grep -q "Options=.*,$context_opt" < "/etc/systemd/system/$unit"; then
+ # Not patched
+ continue
+ fi
+
+ if ! sed -i -e "s#^\\(Options=nodev.*\\),$context_opt\\(,.*\\)\\?#\\1\\2#" "/etc/systemd/system/$unit"; then
+ echo "Cannot patch $unit"
+ fi
+
+ changed_mounts="$changed_mounts $unit"
+ fi
+ done
+
+ if [ -z "$changed_mounts" ]; then
+ # Nothing changed, no need to reload
+ return
+ fi
+
+ systemctl daemon-reload
+
+ for unit in $changed_mounts; do
+ if ! systemctl try-restart "$unit" ; then
+ echo "Cannot restart $unit"
+ fi
+ done
+}
+
+while [ -n "$1" ]; do
+ case "$1" in
+ --help)
+ show_help
+ exit
+ ;;
+ --snap-mount-dir=*)
+ SNAP_MOUNT_DIR=${1#*=}
+ SNAP_UNIT_PREFIX=$(systemd-escape -p "$SNAP_MOUNT_DIR")
+ shift
+ ;;
+ --patch-selinux-mount-context=*)
+ patch_selinux_mount_context "${1#*=}"
+ shift
+ ;;
+ --remove-selinux-mount-context=*)
+ patch_selinux_mount_context "${1#*=}" remove
+ shift
+ ;;
+ *)
+ echo "Unknown command: $1"
+ exit 1
+ ;;
+ esac
+done
diff -pruN 2.37.4-1/cmd/snap-mgmt/snap-mgmt.sh.in 2.39.2+19.10ubuntu1/cmd/snap-mgmt/snap-mgmt.sh.in
--- 2.37.4-1/cmd/snap-mgmt/snap-mgmt.sh.in 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-mgmt/snap-mgmt.sh.in 2019-06-05 06:41:21.000000000 +0000
@@ -125,11 +125,7 @@ purge() {
rm -f "$mnt"
done
fi
- if [ "$(find /run/snapd/ns/ -name "*.fstab" | wc -l)" -gt 0 ]; then
- for fstab in /run/snapd/ns/*.fstab; do
- rm -f "$fstab"
- done
- fi
+ find /run/snapd/ns/ \( -name '*.fstab' -o -name '*.user-fstab' \) -delete
umount -l /run/snapd/ns/ || true
fi
@@ -157,7 +153,7 @@ purge() {
rm -f /var/lib/snapd/system-key
echo "Removing snapd catalog cache"
- rm -f /var/cache/snapd/*
+ rm -rf /var/cache/snapd/*
if test -d /etc/apparmor.d; then
# Remove auto-generated rules for snap-confine from the 'core' snap
diff -pruN 2.37.4-1/cmd/snap-seccomp/export_test.go 2.39.2+19.10ubuntu1/cmd/snap-seccomp/export_test.go
--- 2.37.4-1/cmd/snap-seccomp/export_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-seccomp/export_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2017 Canonical Ltd
+ * Copyright (C) 2019 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
@@ -22,6 +22,7 @@ package main
var (
Compile = compile
SeccompResolver = seccompResolver
+ VersionInfo = versionInfo
)
func MockArchUbuntuArchitecture(f func() string) (restore func()) {
@@ -47,3 +48,11 @@ func MockErrnoOnDenial(i int16) (retore
errnoOnDenial = origErrnoOnDenial
}
}
+
+func MockSeccompSyscalls(syscalls []string) (resture func()) {
+ old := seccompSyscalls
+ seccompSyscalls = syscalls
+ return func() {
+ seccompSyscalls = old
+ }
+}
diff -pruN 2.37.4-1/cmd/snap-seccomp/main.go 2.39.2+19.10ubuntu1/cmd/snap-seccomp/main.go
--- 2.37.4-1/cmd/snap-seccomp/main.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-seccomp/main.go 2019-06-05 06:41:21.000000000 +0000
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2017-2018 Canonical Ltd
+ * Copyright (C) 2017-2019 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
@@ -774,6 +774,8 @@ func main() {
err = compile(content, os.Args[3])
case "library-version":
err = showSeccompLibraryVersion()
+ case "version-info":
+ err = showVersionInfo()
default:
err = fmt.Errorf("unsupported argument %q", cmd)
}
diff -pruN 2.37.4-1/cmd/snap-seccomp/syscalls/syscalls.go 2.39.2+19.10ubuntu1/cmd/snap-seccomp/syscalls/syscalls.go
--- 2.37.4-1/cmd/snap-seccomp/syscalls/syscalls.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-seccomp/syscalls/syscalls.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,459 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package syscalls
+
+// Generated using arch-syscall-dump test tool from libseccomp tree, git
+// revision 584ca7a5e69d87a4c2c4e4c07ce8415fa59e1351.
+var SeccompSyscalls = []string{
+ "_llseek",
+ "_newselect",
+ "_sysctl",
+ "accept",
+ "accept4",
+ "access",
+ "acct",
+ "add_key",
+ "adjtimex",
+ "afs_syscall",
+ "alarm",
+ "arch_prctl",
+ "arm_fadvise64_64",
+ "arm_sync_file_range",
+ "bdflush",
+ "bind",
+ "bpf",
+ "break",
+ "breakpoint",
+ "brk",
+ "cachectl",
+ "cacheflush",
+ "capget",
+ "capset",
+ "chdir",
+ "chmod",
+ "chown",
+ "chown32",
+ "chroot",
+ "clock_adjtime",
+ "clock_getres",
+ "clock_gettime",
+ "clock_nanosleep",
+ "clock_settime",
+ "clone",
+ "close",
+ "connect",
+ "copy_file_range",
+ "creat",
+ "create_module",
+ "delete_module",
+ "dup",
+ "dup2",
+ "dup3",
+ "epoll_create",
+ "epoll_create1",
+ "epoll_ctl",
+ "epoll_ctl_old",
+ "epoll_pwait",
+ "epoll_wait",
+ "epoll_wait_old",
+ "eventfd",
+ "eventfd2",
+ "execve",
+ "execveat",
+ "exit",
+ "exit_group",
+ "faccessat",
+ "fadvise64",
+ "fadvise64_64",
+ "fallocate",
+ "fanotify_init",
+ "fanotify_mark",
+ "fchdir",
+ "fchmod",
+ "fchmodat",
+ "fchown",
+ "fchown32",
+ "fchownat",
+ "fcntl",
+ "fcntl64",
+ "fdatasync",
+ "fgetxattr",
+ "finit_module",
+ "flistxattr",
+ "flock",
+ "fork",
+ "fremovexattr",
+ "fsetxattr",
+ "fstat",
+ "fstat64",
+ "fstatat64",
+ "fstatfs",
+ "fstatfs64",
+ "fsync",
+ "ftime",
+ "ftruncate",
+ "ftruncate64",
+ "futex",
+ "futimesat",
+ "get_kernel_syms",
+ "get_mempolicy",
+ "get_robust_list",
+ "get_thread_area",
+ "get_tls",
+ "getcpu",
+ "getcwd",
+ "getdents",
+ "getdents64",
+ "getegid",
+ "getegid32",
+ "geteuid",
+ "geteuid32",
+ "getgid",
+ "getgid32",
+ "getgroups",
+ "getgroups32",
+ "getitimer",
+ "getpeername",
+ "getpgid",
+ "getpgrp",
+ "getpid",
+ "getpmsg",
+ "getppid",
+ "getpriority",
+ "getrandom",
+ "getresgid",
+ "getresgid32",
+ "getresuid",
+ "getresuid32",
+ "getrlimit",
+ "getrusage",
+ "getsid",
+ "getsockname",
+ "getsockopt",
+ "gettid",
+ "gettimeofday",
+ "getuid",
+ "getuid32",
+ "getxattr",
+ "gtty",
+ "idle",
+ "init_module",
+ "inotify_add_watch",
+ "inotify_init",
+ "inotify_init1",
+ "inotify_rm_watch",
+ "io_cancel",
+ "io_destroy",
+ "io_getevents",
+ "io_pgetevents",
+ "io_setup",
+ "io_submit",
+ "ioctl",
+ "ioperm",
+ "iopl",
+ "ioprio_get",
+ "ioprio_set",
+ "ipc",
+ "kcmp",
+ "kexec_file_load",
+ "kexec_load",
+ "keyctl",
+ "kill",
+ "lchown",
+ "lchown32",
+ "lgetxattr",
+ "link",
+ "linkat",
+ "listen",
+ "listxattr",
+ "llistxattr",
+ "lock",
+ "lookup_dcookie",
+ "lremovexattr",
+ "lseek",
+ "lsetxattr",
+ "lstat",
+ "lstat64",
+ "madvise",
+ "mbind",
+ "membarrier",
+ "memfd_create",
+ "migrate_pages",
+ "mincore",
+ "mkdir",
+ "mkdirat",
+ "mknod",
+ "mknodat",
+ "mlock",
+ "mlock2",
+ "mlockall",
+ "mmap",
+ "mmap2",
+ "modify_ldt",
+ "mount",
+ "move_pages",
+ "mprotect",
+ "mpx",
+ "mq_getsetattr",
+ "mq_notify",
+ "mq_open",
+ "mq_timedreceive",
+ "mq_timedsend",
+ "mq_unlink",
+ "mremap",
+ "msgctl",
+ "msgget",
+ "msgrcv",
+ "msgsnd",
+ "msync",
+ "multiplexer",
+ "munlock",
+ "munlockall",
+ "munmap",
+ "name_to_handle_at",
+ "nanosleep",
+ "newfstatat",
+ "nfsservctl",
+ "nice",
+ "oldfstat",
+ "oldlstat",
+ "oldolduname",
+ "oldstat",
+ "olduname",
+ "oldwait4",
+ "open",
+ "open_by_handle_at",
+ "openat",
+ "pause",
+ "pciconfig_iobase",
+ "pciconfig_read",
+ "pciconfig_write",
+ "perf_event_open",
+ "personality",
+ "pipe",
+ "pipe2",
+ "pivot_root",
+ "pkey_alloc",
+ "pkey_free",
+ "pkey_mprotect",
+ "poll",
+ "ppoll",
+ "prctl",
+ "pread64",
+ "preadv",
+ "preadv2",
+ "prlimit64",
+ "process_vm_readv",
+ "process_vm_writev",
+ "prof",
+ "profil",
+ "pselect6",
+ "ptrace",
+ "putpmsg",
+ "pwrite64",
+ "pwritev",
+ "pwritev2",
+ "query_module",
+ "quotactl",
+ "read",
+ "readahead",
+ "readdir",
+ "readlink",
+ "readlinkat",
+ "readv",
+ "reboot",
+ "recv",
+ "recvfrom",
+ "recvmmsg",
+ "recvmsg",
+ "remap_file_pages",
+ "removexattr",
+ "rename",
+ "renameat",
+ "renameat2",
+ "request_key",
+ "restart_syscall",
+ "rmdir",
+ "rseq",
+ "rt_sigaction",
+ "rt_sigpending",
+ "rt_sigprocmask",
+ "rt_sigqueueinfo",
+ "rt_sigreturn",
+ "rt_sigsuspend",
+ "rt_sigtimedwait",
+ "rt_tgsigqueueinfo",
+ "rtas",
+ "s390_guarded_storage",
+ "s390_pci_mmio_read",
+ "s390_pci_mmio_write",
+ "s390_runtime_instr",
+ "s390_sthyi",
+ "sched_get_priority_max",
+ "sched_get_priority_min",
+ "sched_getaffinity",
+ "sched_getattr",
+ "sched_getparam",
+ "sched_getscheduler",
+ "sched_rr_get_interval",
+ "sched_setaffinity",
+ "sched_setattr",
+ "sched_setparam",
+ "sched_setscheduler",
+ "sched_yield",
+ "seccomp",
+ "security",
+ "select",
+ "semctl",
+ "semget",
+ "semop",
+ "semtimedop",
+ "send",
+ "sendfile",
+ "sendfile64",
+ "sendmmsg",
+ "sendmsg",
+ "sendto",
+ "set_mempolicy",
+ "set_robust_list",
+ "set_thread_area",
+ "set_tid_address",
+ "set_tls",
+ "setdomainname",
+ "setfsgid",
+ "setfsgid32",
+ "setfsuid",
+ "setfsuid32",
+ "setgid",
+ "setgid32",
+ "setgroups",
+ "setgroups32",
+ "sethostname",
+ "setitimer",
+ "setns",
+ "setpgid",
+ "setpriority",
+ "setregid",
+ "setregid32",
+ "setresgid",
+ "setresgid32",
+ "setresuid",
+ "setresuid32",
+ "setreuid",
+ "setreuid32",
+ "setrlimit",
+ "setsid",
+ "setsockopt",
+ "settimeofday",
+ "setuid",
+ "setuid32",
+ "setxattr",
+ "sgetmask",
+ "shmat",
+ "shmctl",
+ "shmdt",
+ "shmget",
+ "shutdown",
+ "sigaction",
+ "sigaltstack",
+ "signal",
+ "signalfd",
+ "signalfd4",
+ "sigpending",
+ "sigprocmask",
+ "sigreturn",
+ "sigsuspend",
+ "socket",
+ "socketcall",
+ "socketpair",
+ "splice",
+ "spu_create",
+ "spu_run",
+ "ssetmask",
+ "stat",
+ "stat64",
+ "statfs",
+ "statfs64",
+ "statx",
+ "stime",
+ "stty",
+ "subpage_prot",
+ "swapcontext",
+ "swapoff",
+ "swapon",
+ "switch_endian",
+ "symlink",
+ "symlinkat",
+ "sync",
+ "sync_file_range",
+ "sync_file_range2",
+ "syncfs",
+ "sys_debug_setcontext",
+ "syscall",
+ "sysfs",
+ "sysinfo",
+ "syslog",
+ "sysmips",
+ "tee",
+ "tgkill",
+ "time",
+ "timer_create",
+ "timer_delete",
+ "timer_getoverrun",
+ "timer_gettime",
+ "timer_settime",
+ "timerfd",
+ "timerfd_create",
+ "timerfd_gettime",
+ "timerfd_settime",
+ "times",
+ "tkill",
+ "truncate",
+ "truncate64",
+ "tuxcall",
+ "ugetrlimit",
+ "ulimit",
+ "umask",
+ "umount",
+ "umount2",
+ "uname",
+ "unlink",
+ "unlinkat",
+ "unshare",
+ "uselib",
+ "userfaultfd",
+ "usr26",
+ "usr32",
+ "ustat",
+ "utime",
+ "utimensat",
+ "utimes",
+ "vfork",
+ "vhangup",
+ "vm86",
+ "vm86old",
+ "vmsplice",
+ "vserver",
+ "wait4",
+ "waitid",
+ "waitpid",
+ "write",
+ "writev",
+}
diff -pruN 2.37.4-1/cmd/snap-seccomp/versioninfo.go 2.39.2+19.10ubuntu1/cmd/snap-seccomp/versioninfo.go
--- 2.37.4-1/cmd/snap-seccomp/versioninfo.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-seccomp/versioninfo.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,67 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main
+
+import (
+ "crypto/sha256"
+ "fmt"
+ "os"
+
+ "github.com/mvo5/libseccomp-golang"
+
+ "github.com/snapcore/snapd/cmd/snap-seccomp/syscalls"
+ "github.com/snapcore/snapd/osutil"
+)
+
+var seccompSyscalls = syscalls.SeccompSyscalls
+
+func versionInfo() (string, error) {
+ myBuildID, err := osutil.MyBuildID()
+ if err != nil {
+ return "", fmt.Errorf("cannot get build-id of snap-seccomp: %v", err)
+ }
+ // Calculate the checksum of all syscall names supported by libseccomp
+ // library. We add that to the version info to cover the case when
+ // libseccomp version does not change, but the set of supported syscalls
+ // does due to distro patches.
+ sh := sha256.New()
+ newline := []byte("\n")
+ for _, syscallName := range seccompSyscalls {
+ if _, err := seccomp.GetSyscallFromName(syscallName); err != nil {
+ // syscall is unsupported by this version of libseccomp
+ continue
+ }
+ sh.Write([]byte(syscallName))
+ sh.Write(newline)
+ }
+
+ major, minor, micro := seccomp.GetLibraryVersion()
+
+ return fmt.Sprintf("%s %d.%d.%d %x", myBuildID, major, minor, micro, sh.Sum(nil)), nil
+}
+
+func showVersionInfo() error {
+ vi, err := versionInfo()
+ if err != nil {
+ return err
+ }
+ fmt.Fprintln(os.Stdout, vi)
+ return nil
+}
diff -pruN 2.37.4-1/cmd/snap-seccomp/versioninfo_test.go 2.39.2+19.10ubuntu1/cmd/snap-seccomp/versioninfo_test.go
--- 2.37.4-1/cmd/snap-seccomp/versioninfo_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-seccomp/versioninfo_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,73 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main_test
+
+import (
+ "fmt"
+ "strings"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/mvo5/libseccomp-golang"
+
+ main "github.com/snapcore/snapd/cmd/snap-seccomp"
+ "github.com/snapcore/snapd/osutil"
+)
+
+type versionInfoSuite struct{}
+
+var _ = Suite(&versionInfoSuite{})
+
+func (s *versionInfoSuite) TestVersionInfo(c *C) {
+ buildID, err := osutil.MyBuildID()
+ c.Assert(err, IsNil)
+
+ m, i, p := seccomp.GetLibraryVersion()
+ prefix := fmt.Sprintf("%s %d.%d.%d ", buildID, m, i, p)
+
+ defaultVi, err := main.VersionInfo()
+ c.Assert(err, IsNil)
+
+ // $ echo -n 'read\nwrite\n' | sha256sum
+ // 88b06efcea4b5946cebd4b0674b93744de328339de5d61b75db858119054ff93 -
+ readWriteHash := "88b06efcea4b5946cebd4b0674b93744de328339de5d61b75db858119054ff93"
+
+ c.Check(strings.HasPrefix(defaultVi, prefix), Equals, true)
+ c.Assert(len(defaultVi) > len(prefix), Equals, true)
+ hash := defaultVi[len(prefix):]
+ c.Check(len(hash), Equals, len(readWriteHash))
+ c.Check(hash, Not(Equals), readWriteHash)
+
+ restore := main.MockSeccompSyscalls([]string{"read", "write"})
+ defer restore()
+
+ vi, err := main.VersionInfo()
+ c.Assert(err, IsNil)
+ c.Check(vi, Equals, prefix+readWriteHash)
+
+ // pretend it's only 'read' now
+ readHash := "15fd60c6f5c6804626177d178f3dba849a41f4a1878b2e7e7e3ed38a194dc82b"
+ restore = main.MockSeccompSyscalls([]string{"read"})
+ defer restore()
+
+ vi, err = main.VersionInfo()
+ c.Assert(err, IsNil)
+ c.Check(vi, Equals, prefix+readHash)
+}
diff -pruN 2.37.4-1/cmd/snap-seccomp-blacklist/BE-bpf-script 2.39.2+19.10ubuntu1/cmd/snap-seccomp-blacklist/BE-bpf-script
--- 2.37.4-1/cmd/snap-seccomp-blacklist/BE-bpf-script 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-seccomp-blacklist/BE-bpf-script 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,3 @@
+load bpf-blob BE-blacklist.bpf
+disassemble
+dump
diff -pruN 2.37.4-1/cmd/snap-seccomp-blacklist/LE-bpf-script 2.39.2+19.10ubuntu1/cmd/snap-seccomp-blacklist/LE-bpf-script
--- 2.37.4-1/cmd/snap-seccomp-blacklist/LE-bpf-script 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-seccomp-blacklist/LE-bpf-script 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,3 @@
+load bpf-blob LE-blacklist.bpf
+disassemble
+dump
diff -pruN 2.37.4-1/cmd/snap-seccomp-blacklist/Makefile 2.39.2+19.10ubuntu1/cmd/snap-seccomp-blacklist/Makefile
--- 2.37.4-1/cmd/snap-seccomp-blacklist/Makefile 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-seccomp-blacklist/Makefile 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,40 @@
+.PHONY: all
+all: $(foreach v,LE BE,$v-blacklist.bpf) | analyze
+
+.PHONY: clean
+clean:
+ rm -f snap-seccomp-blacklist snap-seccomp-blacklist.o *.pfc *.bpf
+
+.PHONY: fmt
+fmt: snap-seccomp-blacklist.c
+ clang-format -style='{BasedOnStyle: Google, IndentWidth: 4, ColumnLimit: 120}' -i $^
+
+DESTDIR ?=
+
+.PHONY: install
+install:: blacklist.bpf | $(DESTDIR)/var/lib/snapd/seccomp/bpf
+ install -m 644 $^ $|/global.bin
+
+$(DESTDIR)/var/lib/snapd/seccomp/bpf:
+ install -m 755 -d $@
+
+$(foreach v,LE BE,$v-blacklist.pfc $v-blacklist.bpf): snap-seccomp-blacklist
+ ./$<
+
+.PHONY: analyze
+analyze: $(foreach v,LE BE,$v-blacklist.bpf $v-blacklist.pfc $v-bpf-script)
+ # Not everyone has bpf_dbg installed, not everyone has support for "load bpf-blob".
+ -bpf_dbg LE-bpf-script
+ cat LE-blacklist.pfc
+ -bpf_dbg BE-bpf-script
+ cat BE-blacklist.pfc
+
+snap-seccomp-blacklist: snap-seccomp-blacklist.o
+ $(CC) -o $@ $^ $(LDLIBS)
+
+%.o: %.c
+ $(CC) -o $@ -c $^ $(CFLAGS)
+
+CFLAGS += -Wall -Werror
+CFLAGS += $(shell pkg-config libseccomp --cflags)
+LDLIBS += $(shell pkg-config libseccomp --libs)
diff -pruN 2.37.4-1/cmd/snap-seccomp-blacklist/snap-seccomp-blacklist.c 2.39.2+19.10ubuntu1/cmd/snap-seccomp-blacklist/snap-seccomp-blacklist.c
--- 2.37.4-1/cmd/snap-seccomp-blacklist/snap-seccomp-blacklist.c 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-seccomp-blacklist/snap-seccomp-blacklist.c 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,222 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+__attribute__((format(printf, 1, 2))) static void showerr(const char *fmt, ...);
+
+static void showerr(const char *fmt, ...) {
+ va_list va;
+ va_start(va, fmt);
+ vfprintf(stderr, fmt, va);
+ fputc('\n', stderr);
+ va_end(va);
+}
+
+static int populate_filter(scmp_filter_ctx ctx, const uint32_t *arch_tags, size_t num_arch_tags) {
+ int sc_err;
+
+ /* If the native architecture is not one of the supported 64bit
+ * architectures listed in main in le_arch_tags and be_arch_tags, then
+ * remove it.
+ *
+ * Libseccomp automatically adds the native architecture to each new filter.
+ * If the native architecture is a 32bit-one then we will hit a bug in libseccomp
+ * and the generated BPF program is incorrect as described below. */
+ uint32_t native_arch = seccomp_arch_native();
+ bool remove_native_arch = true;
+ for (size_t i = 0; i < num_arch_tags; ++i) {
+ if (arch_tags[i] == native_arch) {
+ remove_native_arch = false;
+ break;
+ }
+ }
+ if (remove_native_arch) {
+ sc_err = seccomp_arch_remove(ctx, SCMP_ARCH_NATIVE);
+ if (sc_err < 0) {
+ showerr("cannot remove native architecture");
+ return sc_err;
+ }
+ }
+
+ /* Add 64-bit architectures supported by snapd into the seccomp filter.
+ *
+ * The documentation of seccomp_arch_add() is confusing. It says that after
+ * this call any new rules will be added to this architecture. This is
+ * correct. It doesn't, however, explain that the rules will be multiplied
+ * and re-written as explained below. */
+ for (size_t i = 0; i < num_arch_tags; ++i) {
+ uint32_t arch_tag = arch_tags[i];
+ sc_err = seccomp_arch_add(ctx, arch_tag);
+ if (sc_err < 0 && sc_err != -EEXIST) {
+ showerr("cannot add architecture %x", arch_tag);
+ return sc_err;
+ }
+ }
+
+ /* When the rule set doesn't match one of the architectures above then the
+ * resulting action should be a "allow" rather than "kill". We don't add
+ * any of the 32bit architectures since there is no need for any extra
+ * filtering there. */
+ sc_err = seccomp_attr_set(ctx, SCMP_FLTATR_ACT_BADARCH, SCMP_ACT_ALLOW);
+ if (sc_err < 0) {
+ showerr("cannot set action for unknown architectures");
+ return sc_err;
+ }
+
+ /* Resolve the name of "ioctl" on this architecture. We are not using the
+ * system call number as available through the appropriate linux-specific
+ * header. This allows us to use a system call number that is not defined
+ * for the current architecture. This does not matter here, in this
+ * specific program, however it is more generic. In addition this is more
+ * in sync with the snap-seccomp program, which does the same for every
+ * system call. */
+ int sys_ioctl_nr;
+ sys_ioctl_nr = seccomp_syscall_resolve_name("ioctl");
+ if (sys_ioctl_nr == __NR_SCMP_ERROR) {
+ showerr("cannot resolve ioctl system call number");
+ return -ESRCH;
+ }
+
+ /* All of the rules must be added for the native architecture (using native
+ * system call numbers). When the final program is generated the set of
+ * architectures added earlier will be used to determine the correct system
+ * call number for each architecture.
+ *
+ * In other words, arguments to scmp_rule_add() must always use native
+ * system call numbers. Translation for the correct architecture will be
+ * performed internally. This is not documented in libseccomp, but correct
+ * operation was confirmed using the pseudo-code program and the bpf_dbg
+ * tool from the kernel tools/bpf directory.
+ *
+ * NOTE: not using scmp_rule_add_exact as that was not doing anything
+ * at all (presumably due to having all the architectures defined). */
+
+ const struct scmp_arg_cmp no_tty_inject = {
+ /* We learned that existing programs make legitimate requests with all
+ * bits set in the more significant 32bit word of the 64 bit double
+ * word. While this kernel behavior remains suspect and presumably
+ * undesired it is unlikely to change for backwards compatibility
+ * reasons. As such we cannot block all requests with high-bits set.
+ *
+ * When faced with ioctl(fd, request); refuse to proceed when
+ * request&0xffffffff == TIOCSTI. This specific way to encode the
+ * filter has the following important properties:
+ *
+ * - it blocks ioctl(fd, TIOCSTI, ptr).
+ * - it also blocks ioctl(fd, (1UL<<32) | TIOCSTI, ptr).
+ * - it doesn't block ioctl(fd, (1UL<<32) | (request not equal to TIOCSTI), ptr); */
+ .arg = 1,
+ .op = SCMP_CMP_MASKED_EQ,
+ .datum_a = 0xffffffffUL,
+ .datum_b = TIOCSTI,
+ };
+ sc_err = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), sys_ioctl_nr, 1, no_tty_inject);
+
+ if (sc_err < 0) {
+ showerr("cannot add rule preventing the use high bits in ioctl");
+ return sc_err;
+ }
+ return 0;
+}
+
+typedef struct arch_set {
+ const char *name;
+ const uint32_t *arch_tags;
+ size_t num_arch_tags;
+} arch_set;
+
+int main(int argc, char **argv) {
+ const uint32_t le_arch_tags[] = {
+ SCMP_ARCH_X86_64,
+ SCMP_ARCH_AARCH64,
+ SCMP_ARCH_PPC64LE,
+ SCMP_ARCH_S390X,
+ };
+ const uint32_t be_arch_tags[] = {
+ SCMP_ARCH_S390X,
+ };
+ const arch_set arch_sets[] = {
+ {"LE", le_arch_tags, sizeof le_arch_tags / sizeof *le_arch_tags},
+ {"BE", be_arch_tags, sizeof be_arch_tags / sizeof *be_arch_tags},
+ };
+ int rc = -1;
+
+ for (size_t i = 0; i < sizeof arch_sets / sizeof *arch_sets; ++i) {
+ const arch_set *as = &arch_sets[i];
+ int sc_err;
+ int fd = -1;
+ int fname_len;
+ char fname[PATH_MAX];
+
+ scmp_filter_ctx ctx = NULL;
+ ctx = seccomp_init(SCMP_ACT_ALLOW);
+ if (ctx == NULL) {
+ showerr("cannot construct seccomp context");
+ return -rc;
+ }
+ sc_err = populate_filter(ctx, as->arch_tags, as->num_arch_tags);
+ if (sc_err < 0) {
+ seccomp_release(ctx);
+ return -rc;
+ }
+
+ /* Save pseudo-code program */
+ fname_len = snprintf(fname, sizeof fname, "%s-blacklist.pfc", as->name);
+ if (fname_len < 0 || fname_len >= sizeof fname) {
+ showerr("cannot format file name (%s)", as->name);
+ seccomp_release(ctx);
+ return -rc;
+ }
+ fd = open(fname, O_CREAT | O_TRUNC | O_WRONLY | O_NOFOLLOW, 0644);
+ if (fd < 0) {
+ showerr("cannot open file %s", fname);
+ seccomp_release(ctx);
+ return -rc;
+ }
+ sc_err = seccomp_export_pfc(ctx, fd);
+ if (sc_err < 0) {
+ showerr("cannot export PFC program %s", fname);
+ seccomp_release(ctx);
+ close(fd);
+ return -rc;
+ }
+
+ close(fd);
+
+ /* Save binary program. */
+ fname_len = snprintf(fname, sizeof fname, "%s-blacklist.bpf", as->name);
+ if (fname_len < 0 || fname_len >= sizeof fname) {
+ showerr("cannot format file name (%s)", as->name);
+ seccomp_release(ctx);
+ return -rc;
+ }
+ fd = open(fname, O_CREAT | O_TRUNC | O_WRONLY | O_NOFOLLOW, 0644);
+ if (fd < 0) {
+ showerr("cannot open file %s", fname);
+ seccomp_release(ctx);
+ return -rc;
+ }
+ sc_err = seccomp_export_bpf(ctx, fd);
+ if (sc_err < 0) {
+ showerr("cannot export BPF program %s", fname);
+ seccomp_release(ctx);
+ close(fd);
+ return -rc;
+ }
+
+ close(fd);
+ seccomp_release(ctx);
+ }
+ rc = 0;
+ return -rc;
+}
diff -pruN 2.37.4-1/cmd/snap-update-ns/bootstrap.c 2.39.2+19.10ubuntu1/cmd/snap-update-ns/bootstrap.c
--- 2.37.4-1/cmd/snap-update-ns/bootstrap.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-update-ns/bootstrap.c 2019-06-05 06:41:21.000000000 +0000
@@ -271,7 +271,11 @@ static int instance_key_validate(const c
// engine.
int i = 0;
for (i = 0; instance_key[i] != '\0'; i++) {
- if (islower(instance_key[i]) || isdigit(instance_key[i])) {
+ char c = instance_key[i];
+ /* NOTE: We are reimplementing islower() and isdigit()
+ * here. For context see
+ * https://github.com/golang/go/issues/29689 */
+ if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) {
continue;
}
bootstrap_msg =
@@ -329,7 +333,8 @@ int validate_instance_name(const char *i
}
// parse the -u argument, returns -1 on failure or 0 on success.
-static int parse_arg_u(int argc, char * const *argv, int *optind, unsigned long *uid_out)
+static int parse_arg_u(int argc, char *const *argv, int *optind,
+ unsigned long *uid_out)
{
if (*optind + 1 == argc || argv[*optind + 1] == NULL) {
bootstrap_msg = "-u requires an argument";
@@ -340,18 +345,24 @@ static int parse_arg_u(int argc, char *
errno = 0;
char *uid_text_end = NULL;
unsigned long parsed_uid = strtoul(uid_text, &uid_text_end, 10);
+ int saved_errno = errno;
+ char c = *uid_text;
if (
- /* Reject overflow in parsed representation */
- (parsed_uid == ULONG_MAX && errno != 0)
- /* Reject leading whitespace allowed by strtoul. */
- || (isspace(*uid_text))
- /* Reject empty string. */
- || (*uid_text == '\0')
- /* Reject partially parsed strings. */
- || (*uid_text != '\0' && uid_text_end != NULL
- && *uid_text_end != '\0')) {
+ /* Reject overflow in parsed representation */
+ (parsed_uid == ULONG_MAX && errno != 0)
+ /* Reject leading whitespace allowed by strtoul. */
+ /* NOTE: We are reimplementing isspace() here.
+ * For context see
+ * https://github.com/golang/go/issues/29689 */
+ || c == ' ' || c == '\t' || c == '\v' || c == '\r'
+ || c == '\n'
+ /* Reject empty string. */
+ || (*uid_text == '\0')
+ /* Reject partially parsed strings. */
+ || (*uid_text != '\0' && uid_text_end != NULL
+ && *uid_text_end != '\0')) {
bootstrap_msg = "cannot parse user id";
- bootstrap_errno = errno;
+ bootstrap_errno = saved_errno;
return -1;
}
if ((long)parsed_uid < 0) {
@@ -362,14 +373,15 @@ static int parse_arg_u(int argc, char *
if (uid_out != NULL) {
*uid_out = parsed_uid;
}
- *optind += 1; // Account for the argument to -u.
+ *optind += 1; // Account for the argument to -u.
return 0;
}
// process_arguments parses given a command line
// argc and argv are defined as for the main() function
void process_arguments(int argc, char *const *argv, const char **snap_name_out,
- bool * should_setns_out, bool * process_user_fstab, unsigned long * uid_out)
+ bool *should_setns_out, bool *process_user_fstab,
+ unsigned long *uid_out)
{
// Find the name of the called program. If it is ending with ".test" then do nothing.
// NOTE: This lets us use cgo/go to write tests without running the bulk
@@ -411,7 +423,7 @@ void process_arguments(int argc, char *c
// called from snap-confine.
should_setns = false;
} else if (!strcmp(arg, "-u")) {
- if (parse_arg_u(argc, argv, &i, uid_out)) {
+ if (parse_arg_u(argc, argv, &i, uid_out) < 0) {
return;
}
// Providing an user identifier implies we are performing an
diff -pruN 2.37.4-1/cmd/snap-update-ns/bootstrap.h 2.39.2+19.10ubuntu1/cmd/snap-update-ns/bootstrap.h
--- 2.37.4-1/cmd/snap-update-ns/bootstrap.h 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-update-ns/bootstrap.h 2019-06-05 06:41:21.000000000 +0000
@@ -28,7 +28,8 @@ extern const char *bootstrap_msg;
void bootstrap(int argc, char **argv, char **envp);
void process_arguments(int argc, char *const *argv, const char **snap_name_out,
- bool * should_setns_out, bool * process_user_fstab, unsigned long * uid_out);
+ bool *should_setns_out, bool *process_user_fstab,
+ unsigned long *uid_out);
int validate_instance_name(const char *instance_name);
#endif
diff -pruN 2.37.4-1/cmd/snap-update-ns/change.go 2.39.2+19.10ubuntu1/cmd/snap-update-ns/change.go
--- 2.37.4-1/cmd/snap-update-ns/change.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-update-ns/change.go 2019-06-05 06:41:21.000000000 +0000
@@ -289,13 +289,31 @@ func (c *Change) lowLevelPerform(as *Ass
// symlinks are handled in createInode directly, nothing to do here.
case "", "file":
flags, unparsed := osutil.MountOptsToCommonFlags(c.Entry.Options)
- // Use Secure.BindMount for bind mounts
+ // Split the mount flags from the event propagation changes.
+ // Those have to be applied separately.
+ const propagationMask = syscall.MS_SHARED | syscall.MS_SLAVE | syscall.MS_PRIVATE | syscall.MS_UNBINDABLE
+ maskedFlagsRecursive := flags & syscall.MS_REC
+ maskedFlagsPropagation := flags & propagationMask
+ maskedFlagsNotPropagationNotRecursive := flags & ^(propagationMask | syscall.MS_REC)
+
+ var flagsForMount uintptr
if flags&syscall.MS_BIND == syscall.MS_BIND {
- err = BindMount(c.Entry.Name, c.Entry.Dir, uint(flags))
+ // bind / rbind mount
+ flagsForMount = uintptr(maskedFlagsNotPropagationNotRecursive | maskedFlagsRecursive)
+ err = BindMount(c.Entry.Name, c.Entry.Dir, uint(flagsForMount))
} else {
- err = sysMount(c.Entry.Name, c.Entry.Dir, c.Entry.Type, uintptr(flags), strings.Join(unparsed, ","))
+ // normal mount, not bind / rbind, not propagation change
+ flagsForMount = uintptr(maskedFlagsNotPropagationNotRecursive)
+ err = sysMount(c.Entry.Name, c.Entry.Dir, c.Entry.Type, uintptr(flagsForMount), strings.Join(unparsed, ","))
+ }
+ logger.Debugf("mount %q %q %q %d %q (error: %v)", c.Entry.Name, c.Entry.Dir, c.Entry.Type, flagsForMount, strings.Join(unparsed, ","), err)
+ if err == nil && maskedFlagsPropagation != 0 {
+ // now change mount propagation (shared/rshared, private/rprivate,
+ // slave/rslave, unbindable/runbindable).
+ flagsForMount := uintptr(maskedFlagsPropagation | maskedFlagsRecursive)
+ err = sysMount("", c.Entry.Dir, "", flagsForMount, "")
+ logger.Debugf("mount %q %q %q %d %q (error: %v)", "", c.Entry.Dir, "", flagsForMount, "", err)
}
- logger.Debugf("mount %q %q %q %d %q (error: %v)", c.Entry.Name, c.Entry.Dir, c.Entry.Type, uintptr(flags), strings.Join(unparsed, ","), err)
if err == nil {
as.AddChange(c)
}
diff -pruN 2.37.4-1/cmd/snap-update-ns/change_test.go 2.39.2+19.10ubuntu1/cmd/snap-update-ns/change_test.go
--- 2.37.4-1/cmd/snap-update-ns/change_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-update-ns/change_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -478,6 +478,20 @@ func (s *changeSuite) TestPerformFilesys
})
}
+// Change.Perform wants to mount a filesystem with sharing changes.
+func (s *changeSuite) TestPerformFilesystemMountAndShareChanges(c *C) {
+ s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir)
+ chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type", Options: []string{"shared"}}}
+ synth, err := chg.Perform(s.as)
+ c.Assert(err, IsNil)
+ c.Assert(synth, HasLen, 0)
+ c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
+ {C: `lstat "/target"`, R: testutil.FileInfoDir},
+ {C: `mount "device" "/target" "type" 0 ""`},
+ {C: `mount "" "/target" "" MS_SHARED ""`},
+ })
+}
+
// Change.Perform wants to mount a filesystem but it fails.
func (s *changeSuite) TestPerformFilesystemMountWithError(c *C) {
s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir)
@@ -492,6 +506,20 @@ func (s *changeSuite) TestPerformFilesys
})
}
+// Change.Perform wants to mount a filesystem with sharing changes but mounting fails.
+func (s *changeSuite) TestPerformFilesystemMountAndShareWithError(c *C) {
+ s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir)
+ s.sys.InsertFault(`mount "device" "/target" "type" 0 ""`, errTesting)
+ chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type", Options: []string{"shared"}}}
+ synth, err := chg.Perform(s.as)
+ c.Assert(err, Equals, errTesting)
+ c.Assert(synth, HasLen, 0)
+ c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
+ {C: `lstat "/target"`, R: testutil.FileInfoDir},
+ {C: `mount "device" "/target" "type" 0 ""`, E: errTesting},
+ })
+}
+
// Change.Perform wants to mount a filesystem but the mount point isn't there.
func (s *changeSuite) TestPerformFilesystemMountWithoutMountPoint(c *C) {
defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted.
@@ -876,6 +904,34 @@ func (s *changeSuite) TestPerformDirecto
})
}
+// Change.Perform wants to bind mount a directory with sharing changes.
+func (s *changeSuite) TestPerformRecursiveDirectorySharedBindMount(c *C) {
+ s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoDir)
+ s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir)
+ s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{})
+ s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{})
+ chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"rshared", "rbind"}}}
+ synth, err := chg.Perform(s.as)
+ c.Assert(err, IsNil)
+ c.Assert(synth, HasLen, 0)
+ c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
+ {C: `lstat "/target"`, R: testutil.FileInfoDir},
+ {C: `lstat "/source"`, R: testutil.FileInfoDir},
+ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
+ {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4},
+ {C: `fstat 4 `, R: syscall.Stat_t{}},
+ {C: `close 3`},
+ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
+ {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5},
+ {C: `fstat 5 `, R: syscall.Stat_t{}},
+ {C: `close 3`},
+ {C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND|MS_REC ""`},
+ {C: `close 5`},
+ {C: `close 4`},
+ {C: `mount "" "/target" "" MS_REC|MS_SHARED ""`},
+ })
+}
+
// Change.Perform wants to bind mount a directory but it fails.
func (s *changeSuite) TestPerformDirectoryBindMountWithError(c *C) {
s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir)
diff -pruN 2.37.4-1/cmd/snap-update-ns/common.go 2.39.2+19.10ubuntu1/cmd/snap-update-ns/common.go
--- 2.37.4-1/cmd/snap-update-ns/common.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-update-ns/common.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,68 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+
+ "github.com/snapcore/snapd/osutil"
+)
+
+type CommonProfileUpdateContext struct {
+ // instanceName is the name of the snap instance to update.
+ instanceName string
+
+ currentProfilePath string
+ desiredProfilePath string
+}
+
+func (ctx *CommonProfileUpdateContext) Lock() (unlock func(), err error) {
+ return func() {}, nil
+}
+
+func (ctx *CommonProfileUpdateContext) Assumptions() *Assumptions {
+ return nil
+}
+
+// LoadDesiredProfile loads the desired mount profile.
+func (ctx *CommonProfileUpdateContext) LoadDesiredProfile() (*osutil.MountProfile, error) {
+ profile, err := osutil.LoadMountProfile(ctx.desiredProfilePath)
+ if err != nil {
+ return nil, fmt.Errorf("cannot load desired mount profile of snap %q: %s", ctx.instanceName, err)
+ }
+ return profile, nil
+}
+
+// LoadCurrentProfile loads the current mount profile.
+func (ctx *CommonProfileUpdateContext) LoadCurrentProfile() (*osutil.MountProfile, error) {
+ profile, err := osutil.LoadMountProfile(ctx.currentProfilePath)
+ if err != nil {
+ return nil, fmt.Errorf("cannot load current mount profile of snap %q: %s", ctx.instanceName, err)
+ }
+ return profile, nil
+}
+
+// SaveCurrentProfile saves the current mount profile.
+func (ctx *CommonProfileUpdateContext) SaveCurrentProfile(profile *osutil.MountProfile) error {
+ if err := profile.Save(ctx.currentProfilePath); err != nil {
+ return fmt.Errorf("cannot save current mount profile of snap %q: %s", ctx.instanceName, err)
+ }
+ return nil
+}
diff -pruN 2.37.4-1/cmd/snap-update-ns/common_test.go 2.39.2+19.10ubuntu1/cmd/snap-update-ns/common_test.go
--- 2.37.4-1/cmd/snap-update-ns/common_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-update-ns/common_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,116 @@
+// -*- 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 .
+ *
+ */
+
+package main_test
+
+import (
+ "bytes"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ update "github.com/snapcore/snapd/cmd/snap-update-ns"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type commonSuite struct {
+ dir string
+ ctx *update.CommonProfileUpdateContext
+}
+
+var _ = Suite(&commonSuite{})
+
+func (s *commonSuite) SetUpTest(c *C) {
+ s.dir = c.MkDir()
+ s.ctx = update.NewCommonProfileUpdateContext("foo",
+ filepath.Join(s.dir, "current.fstab"),
+ filepath.Join(s.dir, "desired.fstab"))
+}
+
+func (s *commonSuite) TestLoadDesiredProfile(c *C) {
+ ctx := s.ctx
+ text := "tmpfs /tmp tmpfs defaults 0 0\n"
+
+ // Ask the common profile update helper to read the desired profile.
+ profile, err := ctx.LoadCurrentProfile()
+ c.Assert(err, IsNil)
+
+ // A profile that is not present on disk just reads as a valid empty profile.
+ c.Check(profile.Entries, HasLen, 0)
+
+ // Write a desired user mount profile for snap "foo".
+ path := ctx.DesiredProfilePath()
+ c.Assert(os.MkdirAll(filepath.Dir(path), 0755), IsNil)
+ c.Assert(ioutil.WriteFile(path, []byte(text), 0644), IsNil)
+
+ // Ask the common profile update helper to read the desired profile.
+ profile, err = ctx.LoadDesiredProfile()
+ c.Assert(err, IsNil)
+ builder := &bytes.Buffer{}
+ profile.WriteTo(builder)
+
+ // The profile is returned unchanged.
+ c.Check(builder.String(), Equals, text)
+}
+
+func (s *commonSuite) TestLoadCurrentProfile(c *C) {
+ ctx := s.ctx
+ text := "tmpfs /tmp tmpfs defaults 0 0\n"
+
+ // Ask the common profile update helper to read the current profile.
+ profile, err := ctx.LoadCurrentProfile()
+ c.Assert(err, IsNil)
+
+ // A profile that is not present on disk just reads as a valid empty profile.
+ c.Check(profile.Entries, HasLen, 0)
+
+ // Write a current user mount profile for snap "foo".
+ path := ctx.CurrentProfilePath()
+ c.Assert(os.MkdirAll(filepath.Dir(path), 0755), IsNil)
+ c.Assert(ioutil.WriteFile(path, []byte(text), 0644), IsNil)
+
+ // Ask the common profile update helper to read the current profile.
+ profile, err = ctx.LoadCurrentProfile()
+ c.Assert(err, IsNil)
+ builder := &bytes.Buffer{}
+ profile.WriteTo(builder)
+
+ // The profile is returned unchanged.
+ c.Check(builder.String(), Equals, text)
+}
+
+func (s *commonSuite) TestSaveCurrentProfile(c *C) {
+ ctx := s.ctx
+ text := "tmpfs /tmp tmpfs defaults 0 0\n"
+
+ // Prepare a mount profile to be saved.
+ profile, err := osutil.LoadMountProfileText(text)
+ c.Assert(err, IsNil)
+
+ // Prepare the directory for saving the profile.
+ path := ctx.CurrentProfilePath()
+ c.Assert(os.MkdirAll(filepath.Dir(path), 0755), IsNil)
+
+ // Ask the common profile update to write the current profile.
+ c.Assert(ctx.SaveCurrentProfile(profile), IsNil)
+ c.Check(path, testutil.FileEquals, text)
+}
diff -pruN 2.37.4-1/cmd/snap-update-ns/debug.go 2.39.2+19.10ubuntu1/cmd/snap-update-ns/debug.go
--- 2.37.4-1/cmd/snap-update-ns/debug.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-update-ns/debug.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,47 @@
+// -*- 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 .
+ *
+ */
+
+package main
+
+import (
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/osutil"
+)
+
+func debugShowProfile(profile *osutil.MountProfile, header string) {
+ if len(profile.Entries) > 0 {
+ logger.Debugf("%s:", header)
+ for _, entry := range profile.Entries {
+ logger.Debugf("\t%s", entry)
+ }
+ } else {
+ logger.Debugf("%s: (none)", header)
+ }
+}
+
+func debugShowChanges(changes []*Change, header string) {
+ if len(changes) > 0 {
+ logger.Debugf("%s:", header)
+ for _, change := range changes {
+ logger.Debugf("\t%s", change)
+ }
+ } else {
+ logger.Debugf("%s: (none)", header)
+ }
+}
diff -pruN 2.37.4-1/cmd/snap-update-ns/export_test.go 2.39.2+19.10ubuntu1/cmd/snap-update-ns/export_test.go
--- 2.37.4-1/cmd/snap-update-ns/export_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-update-ns/export_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -40,8 +40,8 @@ var (
ExecWritableMimic = execWritableMimic
// main
- ComputeAndSaveChanges = computeAndSaveChanges
- ApplyUserFstab = applyUserFstab
+ ComputeAndSaveSystemChanges = computeAndSaveSystemChanges
+ ApplyUserFstab = applyUserFstab
// bootstrap
ClearBootstrapError = clearBootstrapError
@@ -49,6 +49,18 @@ var (
// trespassing
IsReadOnly = isReadOnly
IsPrivateTmpfsCreatedBySnapd = isPrivateTmpfsCreatedBySnapd
+
+ // system
+ DesiredSystemProfilePath = desiredSystemProfilePath
+ CurrentSystemProfilePath = currentSystemProfilePath
+
+ // user
+ DesiredUserProfilePath = desiredUserProfilePath
+
+ // xdg
+ XdgRuntimeDir = xdgRuntimeDir
+ ExpandPrefixVariable = expandPrefixVariable
+ ExpandXdgRuntimeDir = expandXdgRuntimeDir
)
// SystemCalls encapsulates various system interactions performed by this module.
@@ -146,6 +158,19 @@ func FreezerCgroupDir() string {
return freezerCgroupDir
}
+func MockFreezing(freeze, thaw func(snapName string) error) (restore func()) {
+ oldFreeze := freezeSnapProcesses
+ oldThaw := thawSnapProcesses
+
+ freezeSnapProcesses = freeze
+ thawSnapProcesses = thaw
+
+ return func() {
+ freezeSnapProcesses = oldFreeze
+ thawSnapProcesses = oldThaw
+ }
+}
+
func MockChangePerform(f func(chg *Change, as *Assumptions) ([]*Change, error)) func() {
origChangePerform := changePerform
changePerform = f
@@ -181,3 +206,19 @@ func (as *Assumptions) PastChanges() []*
func (as *Assumptions) CanWriteToDirectory(dirFd int, dirName string) (bool, error) {
return as.canWriteToDirectory(dirFd, dirName)
}
+
+func (ctx *CommonProfileUpdateContext) CurrentProfilePath() string {
+ return ctx.currentProfilePath
+}
+
+func (ctx *CommonProfileUpdateContext) DesiredProfilePath() string {
+ return ctx.desiredProfilePath
+}
+
+func NewCommonProfileUpdateContext(instanceName string, currentProfilePath, desiredProfilePath string) *CommonProfileUpdateContext {
+ return &CommonProfileUpdateContext{
+ instanceName: instanceName,
+ currentProfilePath: currentProfilePath,
+ desiredProfilePath: desiredProfilePath,
+ }
+}
diff -pruN 2.37.4-1/cmd/snap-update-ns/freezer.go 2.39.2+19.10ubuntu1/cmd/snap-update-ns/freezer.go
--- 2.37.4-1/cmd/snap-update-ns/freezer.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-update-ns/freezer.go 2019-06-05 06:41:21.000000000 +0000
@@ -30,10 +30,15 @@ import (
var freezerCgroupDir = "/sys/fs/cgroup/freezer"
-// freezeSnapProcesses freezes all the processes originating from the given snap.
+var (
+ freezeSnapProcesses = freezeSnapProcessesImpl
+ thawSnapProcesses = thawSnapProcessesImpl
+)
+
+// freezeSnapProcessesImpl freezes all the processes originating from the given snap.
// Processes are frozen regardless of which particular snap application they
// originate from.
-func freezeSnapProcesses(snapName string) error {
+func freezeSnapProcessesImpl(snapName string) error {
fname := filepath.Join(freezerCgroupDir, fmt.Sprintf("snap.%s", snapName), "freezer.state")
if err := ioutil.WriteFile(fname, []byte("FROZEN"), 0644); err != nil && os.IsNotExist(err) {
// When there's no freezer cgroup we don't have to freeze anything.
@@ -60,7 +65,7 @@ func freezeSnapProcesses(snapName string
return fmt.Errorf("cannot finish freezing processes of snap %q", snapName)
}
-func thawSnapProcesses(snapName string) error {
+func thawSnapProcessesImpl(snapName string) error {
fname := filepath.Join(freezerCgroupDir, fmt.Sprintf("snap.%s", snapName), "freezer.state")
if err := ioutil.WriteFile(fname, []byte("THAWED"), 0644); err != nil && os.IsNotExist(err) {
// When there's no freezer cgroup we don't have to thaw anything.
diff -pruN 2.37.4-1/cmd/snap-update-ns/main.go 2.39.2+19.10ubuntu1/cmd/snap-update-ns/main.go
--- 2.37.4-1/cmd/snap-update-ns/main.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-update-ns/main.go 2019-06-05 06:41:21.000000000 +0000
@@ -22,20 +22,16 @@ package main
import (
"fmt"
"os"
- "strings"
"github.com/jessevdk/go-flags"
- "github.com/snapcore/snapd/dirs"
- "github.com/snapcore/snapd/interfaces/mount"
"github.com/snapcore/snapd/logger"
- "github.com/snapcore/snapd/osutil"
- "github.com/snapcore/snapd/snap"
)
var opts struct {
FromSnapConfine bool `long:"from-snap-confine"`
UserMounts bool `long:"user-mounts"`
+ UserID int `short:"u"`
Positionals struct {
SnapName string `positional-arg-name:"SNAP_NAME" required:"yes"`
} `positional-args:"true"`
@@ -86,206 +82,5 @@ func run() error {
if opts.UserMounts {
return applyUserFstab(opts.Positionals.SnapName)
}
- return applyFstab(opts.Positionals.SnapName, opts.FromSnapConfine)
-}
-
-func applyFstab(instanceName string, fromSnapConfine bool) error {
- // Lock the mount namespace so that any concurrently attempted invocations
- // of snap-confine are synchronized and will see consistent state.
- lock, err := mount.OpenLock(instanceName)
- if err != nil {
- return fmt.Errorf("cannot open lock file for mount namespace of snap %q: %s", instanceName, err)
- }
- defer func() {
- logger.Debugf("unlocking mount namespace of snap %q", instanceName)
- lock.Close()
- }()
-
- logger.Debugf("locking mount namespace of snap %q", instanceName)
- if fromSnapConfine {
- // When --from-snap-confine is passed then we just ensure that the
- // namespace is locked. This is used by snap-confine to use
- // snap-update-ns to apply mount profiles.
- if err := lock.TryLock(); err != osutil.ErrAlreadyLocked {
- return fmt.Errorf("mount namespace of snap %q is not locked but --from-snap-confine was used", instanceName)
- }
- } else {
- if err := lock.Lock(); err != nil {
- return fmt.Errorf("cannot lock mount namespace of snap %q: %s", instanceName, err)
- }
- }
-
- // Freeze the mount namespace and unfreeze it later. This lets us perform
- // modifications without snap processes attempting to construct
- // symlinks or perform other malicious activity (such as attempting to
- // introduce a symlink that would cause us to mount something other
- // than what we expected).
- logger.Debugf("freezing processes of snap %q", instanceName)
- if err := freezeSnapProcesses(instanceName); err != nil {
- return err
- }
- defer func() {
- logger.Debugf("thawing processes of snap %q", instanceName)
- thawSnapProcesses(instanceName)
- }()
-
- // Allow creating directories related to this snap name.
- //
- // Note that we allow /var/snap instead of /var/snap/$SNAP_NAME because
- // content interface connections can readily create missing mount points on
- // both sides of the interface connection.
- //
- // We scope /snap/$SNAP_NAME because only one side of the connection can be
- // created, as snaps are read-only, the mimic construction will kick-in and
- // create the missing directory but this directory is only visible from the
- // snap that we are operating on (either plug or slot side, the point is,
- // the mount point is not universally visible).
- //
- // /snap/$SNAP_NAME needs to be there as the code that creates such mount
- // points must traverse writable host filesystem that contains /snap/*/ and
- // normally such access is off-limits. This approach allows /snap/foo
- // without allowing /snap/bin, for example.
- //
- // /snap/$SNAP_INSTANCE_NAME and /snap/$SNAP_NAME are added to allow
- // remapping for parallel installs only when the snap has an instance key
- //
- // TODO: Handle /home/*/snap/* when we do per-user mount namespaces and
- // allow defining layout items that refer to SNAP_USER_DATA and
- // SNAP_USER_COMMON.
- as := &Assumptions{}
- as.AddUnrestrictedPaths("/tmp", "/var/snap", "/snap/"+instanceName)
- if snapName := snap.InstanceSnap(instanceName); snapName != instanceName {
- as.AddUnrestrictedPaths("/snap/" + snapName)
- }
- return computeAndSaveChanges(instanceName, as)
-}
-
-func computeAndSaveChanges(snapName string, as *Assumptions) error {
- // Read the desired and current mount profiles. Note that missing files
- // count as empty profiles so that we can gracefully handle a mount
- // interface connection/disconnection.
- desiredProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapMountPolicyDir, snapName)
- desired, err := osutil.LoadMountProfile(desiredProfilePath)
- if err != nil {
- return fmt.Errorf("cannot load desired mount profile of snap %q: %s", snapName, err)
- }
- debugShowProfile(desired, "desired mount profile")
-
- currentProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapRunNsDir, snapName)
- currentBefore, err := osutil.LoadMountProfile(currentProfilePath)
- if err != nil {
- return fmt.Errorf("cannot load current mount profile of snap %q: %s", snapName, err)
- }
- debugShowProfile(currentBefore, "current mount profile (before applying changes)")
- // Synthesize mount changes that were applied before for the purpose of the tmpfs detector.
- for _, entry := range currentBefore.Entries {
- as.AddChange(&Change{Action: Mount, Entry: entry})
- }
-
- currentAfter, err := applyProfile(snapName, currentBefore, desired, as)
- if err != nil {
- return err
- }
-
- logger.Debugf("saving current mount profile of snap %q", snapName)
- if err := currentAfter.Save(currentProfilePath); err != nil {
- return fmt.Errorf("cannot save current mount profile of snap %q: %s", snapName, err)
- }
- return nil
-}
-
-func applyProfile(snapName string, currentBefore, desired *osutil.MountProfile, as *Assumptions) (*osutil.MountProfile, error) {
- // Compute the needed changes and perform each change if
- // needed, collecting those that we managed to perform or that
- // were performed already.
- changesNeeded := NeededChanges(currentBefore, desired)
- debugShowChanges(changesNeeded, "mount changes needed")
-
- logger.Debugf("performing mount changes:")
- var changesMade []*Change
- for _, change := range changesNeeded {
- logger.Debugf("\t * %s", change)
- synthesised, err := changePerform(change, as)
- changesMade = append(changesMade, synthesised...)
- if len(synthesised) > 0 {
- logger.Debugf("\tsynthesised additional mount changes:")
- for _, synth := range synthesised {
- logger.Debugf(" * \t\t%s", synth)
- }
- }
- if err != nil {
- // We may have done something even if Perform itself has
- // failed. We need to collect synthesized changes and
- // store them.
- origin := change.Entry.XSnapdOrigin()
- if origin == "layout" || origin == "overname" {
- return nil, err
- } else if err != ErrIgnoredMissingMount {
- logger.Noticef("cannot change mount namespace of snap %q according to change %s: %s", snapName, change, err)
- }
- continue
- }
-
- changesMade = append(changesMade, change)
- }
-
- // Compute the new current profile so that it contains only changes that were made
- // and save it back for next runs.
- var currentAfter osutil.MountProfile
- for _, change := range changesMade {
- if change.Action == Mount || change.Action == Keep {
- currentAfter.Entries = append(currentAfter.Entries, change.Entry)
- }
- }
- debugShowProfile(¤tAfter, "current mount profile (after applying changes)")
- return ¤tAfter, nil
-}
-
-func debugShowProfile(profile *osutil.MountProfile, header string) {
- if len(profile.Entries) > 0 {
- logger.Debugf("%s:", header)
- for _, entry := range profile.Entries {
- logger.Debugf("\t%s", entry)
- }
- } else {
- logger.Debugf("%s: (none)", header)
- }
-}
-
-func debugShowChanges(changes []*Change, header string) {
- if len(changes) > 0 {
- logger.Debugf("%s:", header)
- for _, change := range changes {
- logger.Debugf("\t%s", change)
- }
- } else {
- logger.Debugf("%s: (none)", header)
- }
-}
-
-func applyUserFstab(snapName string) error {
- desiredProfilePath := fmt.Sprintf("%s/snap.%s.user-fstab", dirs.SnapMountPolicyDir, snapName)
- desired, err := osutil.LoadMountProfile(desiredProfilePath)
- if err != nil {
- return fmt.Errorf("cannot load desired user mount profile of snap %q: %s", snapName, err)
- }
-
- // Replace XDG_RUNTIME_DIR in mount profile
- xdgRuntimeDir := fmt.Sprintf("%s/%d", dirs.XdgRuntimeDirBase, os.Getuid())
- for i := range desired.Entries {
- if strings.HasPrefix(desired.Entries[i].Name, "$XDG_RUNTIME_DIR/") {
- desired.Entries[i].Name = strings.Replace(desired.Entries[i].Name, "$XDG_RUNTIME_DIR", xdgRuntimeDir, 1)
- }
- if strings.HasPrefix(desired.Entries[i].Dir, "$XDG_RUNTIME_DIR/") {
- desired.Entries[i].Dir = strings.Replace(desired.Entries[i].Dir, "$XDG_RUNTIME_DIR", xdgRuntimeDir, 1)
- }
- }
-
- debugShowProfile(desired, "desired mount profile")
-
- // TODO: configure the secure helper and inform it about directories that
- // can be created without trespassing.
- as := &Assumptions{}
- _, err = applyProfile(snapName, &osutil.MountProfile{}, desired, as)
- return err
+ return applySystemFstab(opts.Positionals.SnapName, opts.FromSnapConfine)
}
diff -pruN 2.37.4-1/cmd/snap-update-ns/main_test.go 2.39.2+19.10ubuntu1/cmd/snap-update-ns/main_test.go
--- 2.37.4-1/cmd/snap-update-ns/main_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-update-ns/main_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -54,7 +54,7 @@ func (s *mainSuite) SetUpTest(c *C) {
s.log = buf
}
-func (s *mainSuite) TestComputeAndSaveChanges(c *C) {
+func (s *mainSuite) TestComputeAndSaveSystemChanges(c *C) {
dirs.SetRootDir(c.MkDir())
defer dirs.SetRootDir("/")
@@ -79,7 +79,8 @@ func (s *mainSuite) TestComputeAndSaveCh
err = ioutil.WriteFile(currentProfilePath, nil, 0644)
c.Assert(err, IsNil)
- err = update.ComputeAndSaveChanges(snapName, s.as)
+ ctx := update.NewSystemProfileUpdateContext(snapName)
+ err = update.ComputeAndSaveSystemChanges(ctx, snapName, s.as)
c.Assert(err, IsNil)
c.Check(currentProfilePath, testutil.FileEquals, `/var/lib/snapd/hostfs/usr/local/share/fonts /usr/local/share/fonts none bind,ro 0 0
@@ -146,7 +147,8 @@ func (s *mainSuite) TestAddingSyntheticC
})
defer restore()
- c.Assert(update.ComputeAndSaveChanges(snapName, s.as), IsNil)
+ ctx := update.NewSystemProfileUpdateContext(snapName)
+ c.Assert(update.ComputeAndSaveSystemChanges(ctx, snapName, s.as), IsNil)
c.Check(currentProfilePath, testutil.FileEquals,
`tmpfs /usr/share tmpfs x-snapd.synthetic,x-snapd.needed-by=/usr/share/mysnap 0 0
@@ -223,7 +225,8 @@ func (s *mainSuite) TestRemovingSyntheti
})
defer restore()
- c.Assert(update.ComputeAndSaveChanges(snapName, s.as), IsNil)
+ ctx := update.NewSystemProfileUpdateContext(snapName)
+ c.Assert(update.ComputeAndSaveSystemChanges(ctx, snapName, s.as), IsNil)
c.Check(currentProfilePath, testutil.FileEquals, "")
}
@@ -265,7 +268,8 @@ func (s *mainSuite) TestApplyingLayoutCh
defer restore()
// The error was not ignored, we bailed out.
- c.Assert(update.ComputeAndSaveChanges(snapName, s.as), ErrorMatches, "testing")
+ ctx := update.NewSystemProfileUpdateContext(snapName)
+ c.Assert(update.ComputeAndSaveSystemChanges(ctx, snapName, s.as), ErrorMatches, "testing")
c.Check(currentProfilePath, testutil.FileEquals, "")
}
@@ -307,7 +311,8 @@ func (s *mainSuite) TestApplyingParallel
defer restore()
// The error was not ignored, we bailed out.
- c.Assert(update.ComputeAndSaveChanges(snapName, nil), ErrorMatches, "testing")
+ ctx := update.NewSystemProfileUpdateContext(snapName)
+ c.Assert(update.ComputeAndSaveSystemChanges(ctx, snapName, nil), ErrorMatches, "testing")
c.Check(currentProfilePath, testutil.FileEquals, "")
}
@@ -350,7 +355,8 @@ func (s *mainSuite) TestApplyIgnoredMiss
defer restore()
// The error was ignored, and no mount was recorded in the profile
- c.Assert(update.ComputeAndSaveChanges(snapName, s.as), IsNil)
+ ctx := update.NewSystemProfileUpdateContext(snapName)
+ c.Assert(update.ComputeAndSaveSystemChanges(ctx, snapName, s.as), IsNil)
c.Check(s.log.String(), Equals, "")
c.Check(currentProfilePath, testutil.FileEquals, "")
}
diff -pruN 2.37.4-1/cmd/snap-update-ns/system.go 2.39.2+19.10ubuntu1/cmd/snap-update-ns/system.go
--- 2.37.4-1/cmd/snap-update-ns/system.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-update-ns/system.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,152 @@
+// -*- 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 .
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+
+ "github.com/snapcore/snapd/cmd/snaplock"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/snap"
+)
+
+// SystemProfileUpdateContext contains information about update to system-wide mount namespace.
+type SystemProfileUpdateContext struct {
+ CommonProfileUpdateContext
+}
+
+// NewSystemProfileUpdateContext returns encapsulated information for performing a per-user mount namespace update.
+func NewSystemProfileUpdateContext(instanceName string) *SystemProfileUpdateContext {
+ return &SystemProfileUpdateContext{CommonProfileUpdateContext: CommonProfileUpdateContext{
+ instanceName: instanceName,
+ currentProfilePath: currentSystemProfilePath(instanceName),
+ desiredProfilePath: desiredSystemProfilePath(instanceName),
+ }}
+}
+
+func applySystemFstab(instanceName string, fromSnapConfine bool) error {
+ upCtx := NewSystemProfileUpdateContext(instanceName)
+
+ // Lock the mount namespace so that any concurrently attempted invocations
+ // of snap-confine are synchronized and will see consistent state.
+ lock, err := snaplock.OpenLock(instanceName)
+ if err != nil {
+ return fmt.Errorf("cannot open lock file for mount namespace of snap %q: %s", instanceName, err)
+ }
+ defer func() {
+ logger.Debugf("unlocking mount namespace of snap %q", instanceName)
+ lock.Close()
+ }()
+
+ logger.Debugf("locking mount namespace of snap %q", instanceName)
+ if fromSnapConfine {
+ // When --from-snap-confine is passed then we just ensure that the
+ // namespace is locked. This is used by snap-confine to use
+ // snap-update-ns to apply mount profiles.
+ if err := lock.TryLock(); err != osutil.ErrAlreadyLocked {
+ return fmt.Errorf("mount namespace of snap %q is not locked but --from-snap-confine was used", instanceName)
+ }
+ } else {
+ if err := lock.Lock(); err != nil {
+ return fmt.Errorf("cannot lock mount namespace of snap %q: %s", instanceName, err)
+ }
+ }
+
+ // Freeze the mount namespace and unfreeze it later. This lets us perform
+ // modifications without snap processes attempting to construct
+ // symlinks or perform other malicious activity (such as attempting to
+ // introduce a symlink that would cause us to mount something other
+ // than what we expected).
+ logger.Debugf("freezing processes of snap %q", instanceName)
+ if err := freezeSnapProcesses(instanceName); err != nil {
+ return err
+ }
+ defer func() {
+ logger.Debugf("thawing processes of snap %q", instanceName)
+ thawSnapProcesses(instanceName)
+ }()
+
+ // Allow creating directories related to this snap name.
+ //
+ // Note that we allow /var/snap instead of /var/snap/$SNAP_NAME because
+ // content interface connections can readily create missing mount points on
+ // both sides of the interface connection.
+ //
+ // We scope /snap/$SNAP_NAME because only one side of the connection can be
+ // created, as snaps are read-only, the mimic construction will kick-in and
+ // create the missing directory but this directory is only visible from the
+ // snap that we are operating on (either plug or slot side, the point is,
+ // the mount point is not universally visible).
+ //
+ // /snap/$SNAP_NAME needs to be there as the code that creates such mount
+ // points must traverse writable host filesystem that contains /snap/*/ and
+ // normally such access is off-limits. This approach allows /snap/foo
+ // without allowing /snap/bin, for example.
+ //
+ // /snap/$SNAP_INSTANCE_NAME and /snap/$SNAP_NAME are added to allow
+ // remapping for parallel installs only when the snap has an instance key
+ as := &Assumptions{}
+ as.AddUnrestrictedPaths("/tmp", "/var/snap", "/snap/"+instanceName)
+ if snapName := snap.InstanceSnap(instanceName); snapName != instanceName {
+ as.AddUnrestrictedPaths("/snap/" + snapName)
+ }
+ return computeAndSaveSystemChanges(upCtx, instanceName, as)
+}
+
+func computeAndSaveSystemChanges(upCtx MountProfileUpdateContext, snapName string, as *Assumptions) error {
+ // Read the desired and current mount profiles. Note that missing files
+ // count as empty profiles so that we can gracefully handle a mount
+ // interface connection/disconnection.
+ desired, err := upCtx.LoadDesiredProfile()
+ if err != nil {
+ return err
+ }
+ debugShowProfile(desired, "desired mount profile")
+
+ currentBefore, err := upCtx.LoadCurrentProfile()
+ if err != nil {
+ return err
+ }
+ debugShowProfile(currentBefore, "current mount profile (before applying changes)")
+ // Synthesize mount changes that were applied before for the purpose of the tmpfs detector.
+ for _, entry := range currentBefore.Entries {
+ as.AddChange(&Change{Action: Mount, Entry: entry})
+ }
+
+ currentAfter, err := applyProfile(upCtx, snapName, currentBefore, desired, as)
+ if err != nil {
+ return err
+ }
+
+ logger.Debugf("saving current mount profile of snap %q", snapName)
+ return upCtx.SaveCurrentProfile(currentAfter)
+}
+
+// desiredSystemProfilePath returns the path of the fstab-like file with the desired, system-wide mount profile for a snap.
+func desiredSystemProfilePath(snapName string) string {
+ return fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapMountPolicyDir, snapName)
+}
+
+// currentSystemProfilePath returns the path of the fstab-like file with the applied, system-wide mount profile for a snap.
+func currentSystemProfilePath(snapName string) string {
+ return fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapRunNsDir, snapName)
+}
diff -pruN 2.37.4-1/cmd/snap-update-ns/system_test.go 2.39.2+19.10ubuntu1/cmd/snap-update-ns/system_test.go
--- 2.37.4-1/cmd/snap-update-ns/system_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-update-ns/system_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,38 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ update "github.com/snapcore/snapd/cmd/snap-update-ns"
+)
+
+type systemSuite struct{}
+
+var _ = Suite(&systemSuite{})
+
+func (s *systemSuite) TestDesiredSystemProfilePath(c *C) {
+ c.Check(update.DesiredSystemProfilePath("foo"), Equals, "/var/lib/snapd/mount/snap.foo.fstab")
+}
+
+func (s *systemSuite) TestCurrentSystemProfilePath(c *C) {
+ c.Check(update.CurrentSystemProfilePath("foo"), Equals, "/run/snapd/ns/snap.foo.fstab")
+}
diff -pruN 2.37.4-1/cmd/snap-update-ns/update.go 2.39.2+19.10ubuntu1/cmd/snap-update-ns/update.go
--- 2.37.4-1/cmd/snap-update-ns/update.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-update-ns/update.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,41 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main
+
+import (
+ "github.com/snapcore/snapd/osutil"
+)
+
+// MountProfileUpdateContext provides the context of a mount namespace update.
+// The context provides a way to synchronize the operation with other users of
+// the snap system, to load and save the mount profiles and to provide the file
+// system assumptions with which the mount namespace will be modified.
+type MountProfileUpdateContext interface {
+ // Lock obtains locks appropriate for the update.
+ Lock() (unlock func(), err error)
+ // Assumptions computes filesystem assumptions under which the update shall operate.
+ Assumptions() *Assumptions
+ // LoadDesiredProfile loads the mount profile that should be constructed.
+ LoadDesiredProfile() (*osutil.MountProfile, error)
+ // LoadCurrentProfile loads the mount profile that is currently applied.
+ LoadCurrentProfile() (*osutil.MountProfile, error)
+ // SaveCurrentProfile saves the mount profile that is currently applied.
+ SaveCurrentProfile(*osutil.MountProfile) error
+}
diff -pruN 2.37.4-1/cmd/snap-update-ns/update_test.go 2.39.2+19.10ubuntu1/cmd/snap-update-ns/update_test.go
--- 2.37.4-1/cmd/snap-update-ns/update_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-update-ns/update_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,65 @@
+// -*- 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 .
+ *
+ */
+
+package main_test
+
+import (
+ update "github.com/snapcore/snapd/cmd/snap-update-ns"
+ "github.com/snapcore/snapd/osutil"
+)
+
+// testProfileUpdateContext implements MountProfileUpdate and is suitable for testing.
+type testProfileUpdateContext struct {
+ loadCurrentProfile func() (*osutil.MountProfile, error)
+ loadDesiredProfile func() (*osutil.MountProfile, error)
+ saveCurrentProfile func(*osutil.MountProfile) error
+ assumptions func() *update.Assumptions
+}
+
+func (upCtx *testProfileUpdateContext) Lock() (unlock func(), err error) {
+ return func() {}, nil
+}
+
+func (upCtx *testProfileUpdateContext) Assumptions() *update.Assumptions {
+ if upCtx.assumptions != nil {
+ return upCtx.assumptions()
+ }
+ return &update.Assumptions{}
+}
+
+func (upCtx *testProfileUpdateContext) LoadCurrentProfile() (*osutil.MountProfile, error) {
+ if upCtx.loadCurrentProfile != nil {
+ return upCtx.loadCurrentProfile()
+ }
+ return &osutil.MountProfile{}, nil
+}
+
+func (upCtx *testProfileUpdateContext) LoadDesiredProfile() (*osutil.MountProfile, error) {
+ if upCtx.loadDesiredProfile != nil {
+ return upCtx.loadDesiredProfile()
+ }
+ return &osutil.MountProfile{}, nil
+}
+
+func (upCtx *testProfileUpdateContext) SaveCurrentProfile(profile *osutil.MountProfile) error {
+ if upCtx.saveCurrentProfile != nil {
+ return upCtx.saveCurrentProfile(profile)
+ }
+ return nil
+}
diff -pruN 2.37.4-1/cmd/snap-update-ns/user.go 2.39.2+19.10ubuntu1/cmd/snap-update-ns/user.go
--- 2.37.4-1/cmd/snap-update-ns/user.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-update-ns/user.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,59 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2017-2019 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 .
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+)
+
+// UserProfileUpdateContext contains information about update to per-user mount namespace.
+type UserProfileUpdateContext struct {
+ CommonProfileUpdateContext
+}
+
+func applyUserFstab(snapName string) error {
+ upCtx := &UserProfileUpdateContext{}
+ desiredProfilePath := desiredUserProfilePath(snapName)
+ desired, err := osutil.LoadMountProfile(desiredProfilePath)
+ if err != nil {
+ return fmt.Errorf("cannot load desired user mount profile of snap %q: %s", snapName, err)
+ }
+
+ expandXdgRuntimeDir(desired, os.Getuid())
+ debugShowProfile(desired, "desired mount profile")
+
+ // TODO: configure the secure helper and inform it about directories that
+ // can be created without trespassing.
+ as := &Assumptions{}
+ // TODO: Handle /home/*/snap/* when we do per-user mount namespaces and
+ // allow defining layout items that refer to SNAP_USER_DATA and
+ // SNAP_USER_COMMON.
+ _, err = applyProfile(upCtx, snapName, &osutil.MountProfile{}, desired, as)
+ return err
+}
+
+// desiredUserProfilePath returns the path of the fstab-like file with the desired, user-specific mount profile for a snap.
+func desiredUserProfilePath(snapName string) string {
+ return fmt.Sprintf("%s/snap.%s.user-fstab", dirs.SnapMountPolicyDir, snapName)
+}
diff -pruN 2.37.4-1/cmd/snap-update-ns/user_test.go 2.39.2+19.10ubuntu1/cmd/snap-update-ns/user_test.go
--- 2.37.4-1/cmd/snap-update-ns/user_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-update-ns/user_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,34 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ update "github.com/snapcore/snapd/cmd/snap-update-ns"
+)
+
+type userSuite struct{}
+
+var _ = Suite(&userSuite{})
+
+func (s *userSuite) TestDesiredUserProfilePath(c *C) {
+ c.Check(update.DesiredUserProfilePath("foo"), Equals, "/var/lib/snapd/mount/snap.foo.user-fstab")
+}
diff -pruN 2.37.4-1/cmd/snap-update-ns/utils.go 2.39.2+19.10ubuntu1/cmd/snap-update-ns/utils.go
--- 2.37.4-1/cmd/snap-update-ns/utils.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-update-ns/utils.go 2019-06-05 06:41:21.000000000 +0000
@@ -571,7 +571,7 @@ type FatalError struct {
func execWritableMimic(plan []*Change, as *Assumptions) ([]*Change, error) {
undoChanges := make([]*Change, 0, len(plan)-2)
for i, change := range plan {
- if _, err := changePerform(change, as); err != nil {
+ if _, err := change.Perform(as); err != nil {
// Drat, we failed! Let's undo everything according to our own undo
// plan, by following it in reverse order.
@@ -594,7 +594,7 @@ func execWritableMimic(plan []*Change, a
if recoveryUndoChange.Entry.OptBool("rbind") {
recoveryUndoChange.Entry.Options = append(recoveryUndoChange.Entry.Options, osutil.XSnapdDetach())
}
- if _, err2 := changePerform(recoveryUndoChange, as); err2 != nil {
+ if _, err2 := recoveryUndoChange.Perform(as); err2 != nil {
// Drat, we failed when trying to recover from an error.
// We cannot do anything at this stage.
return nil, &FatalError{error: fmt.Errorf("cannot undo change %q while recovering from earlier error %v: %v", recoveryUndoChange, err, err2)}
@@ -653,3 +653,50 @@ func createWritableMimic(dir, neededBy s
}
return changes, nil
}
+
+func applyProfile(up MountProfileUpdateContext, snapName string, currentBefore, desired *osutil.MountProfile, as *Assumptions) (*osutil.MountProfile, error) {
+ // Compute the needed changes and perform each change if
+ // needed, collecting those that we managed to perform or that
+ // were performed already.
+ changesNeeded := NeededChanges(currentBefore, desired)
+ debugShowChanges(changesNeeded, "mount changes needed")
+
+ logger.Debugf("performing mount changes:")
+ var changesMade []*Change
+ for _, change := range changesNeeded {
+ logger.Debugf("\t * %s", change)
+ synthesised, err := change.Perform(as)
+ changesMade = append(changesMade, synthesised...)
+ if len(synthesised) > 0 {
+ logger.Debugf("\tsynthesised additional mount changes:")
+ for _, synth := range synthesised {
+ logger.Debugf(" * \t\t%s", synth)
+ }
+ }
+ if err != nil {
+ // We may have done something even if Perform itself has
+ // failed. We need to collect synthesized changes and
+ // store them.
+ origin := change.Entry.XSnapdOrigin()
+ if origin == "layout" || origin == "overname" {
+ return nil, err
+ } else if err != ErrIgnoredMissingMount {
+ logger.Noticef("cannot change mount namespace of snap %q according to change %s: %s", snapName, change, err)
+ }
+ continue
+ }
+
+ changesMade = append(changesMade, change)
+ }
+
+ // Compute the new current profile so that it contains only changes that were made
+ // and save it back for next runs.
+ var currentAfter osutil.MountProfile
+ for _, change := range changesMade {
+ if change.Action == Mount || change.Action == Keep {
+ currentAfter.Entries = append(currentAfter.Entries, change.Entry)
+ }
+ }
+ debugShowProfile(¤tAfter, "current mount profile (after applying changes)")
+ return ¤tAfter, nil
+}
diff -pruN 2.37.4-1/cmd/snap-update-ns/xdg.go 2.39.2+19.10ubuntu1/cmd/snap-update-ns/xdg.go
--- 2.37.4-1/cmd/snap-update-ns/xdg.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-update-ns/xdg.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,56 @@
+// -*- 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 .
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+)
+
+// xdgRuntimeDir returns the path to XDG_RUNTIME_DIR for a given user ID.
+func xdgRuntimeDir(uid int) string {
+ return fmt.Sprintf("%s/%d", dirs.XdgRuntimeDirBase, uid)
+}
+
+// expandPrefixVariable expands variable at the beginning of a path-like string.
+func expandPrefixVariable(path, variable, value string) string {
+ if strings.HasPrefix(path, variable) {
+ if len(path) == len(variable) {
+ return value
+ }
+ if len(path) > len(variable) && path[len(variable)] == '/' {
+ return value + path[len(variable):]
+ }
+ }
+ return path
+}
+
+// expandXdgRuntimeDir expands the $XDG_RUNTIME_DIR variable in the given mount profile.
+func expandXdgRuntimeDir(profile *osutil.MountProfile, uid int) {
+ variable := "$XDG_RUNTIME_DIR"
+ value := xdgRuntimeDir(uid)
+ for i := range profile.Entries {
+ profile.Entries[i].Name = expandPrefixVariable(profile.Entries[i].Name, variable, value)
+ profile.Entries[i].Dir = expandPrefixVariable(profile.Entries[i].Dir, variable, value)
+ }
+}
diff -pruN 2.37.4-1/cmd/snap-update-ns/xdg_test.go 2.39.2+19.10ubuntu1/cmd/snap-update-ns/xdg_test.go
--- 2.37.4-1/cmd/snap-update-ns/xdg_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/snap-update-ns/xdg_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,56 @@
+// -*- 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 .
+ *
+ */
+
+package main_test
+
+import (
+ "bytes"
+ "strings"
+
+ . "gopkg.in/check.v1"
+
+ update "github.com/snapcore/snapd/cmd/snap-update-ns"
+ "github.com/snapcore/snapd/osutil"
+)
+
+type xdgSuite struct{}
+
+var _ = Suite(&xdgSuite{})
+
+func (s *xdgSuite) TestXdgRuntimeDir(c *C) {
+ c.Check(update.XdgRuntimeDir(1234), Equals, "/run/user/1234")
+}
+
+func (s *xdgSuite) TestExpandPrefixVariable(c *C) {
+ c.Check(update.ExpandPrefixVariable("$FOO", "$FOO", "/foo"), Equals, "/foo")
+ c.Check(update.ExpandPrefixVariable("$FOO/", "$FOO", "/foo"), Equals, "/foo/")
+ c.Check(update.ExpandPrefixVariable("$FOO/bar", "$FOO", "/foo"), Equals, "/foo/bar")
+ c.Check(update.ExpandPrefixVariable("$FOObar", "$FOO", "/foo"), Equals, "$FOObar")
+}
+
+func (s *xdgSuite) TestExpandXdgRuntimeDir(c *C) {
+ input := "$XDG_RUNTIME_DIR/doc/by-app/snap.foo $XDG_RUNTIME_DIR/doc none bind,rw 0 0\n"
+ output := "/run/user/1234/doc/by-app/snap.foo /run/user/1234/doc none bind,rw 0 0\n"
+ profile, err := osutil.ReadMountProfile(strings.NewReader(input))
+ c.Assert(err, IsNil)
+ update.ExpandXdgRuntimeDir(profile, 1234)
+ builder := &bytes.Buffer{}
+ profile.WriteTo(builder)
+ c.Check(builder.String(), Equals, output)
+}
diff -pruN 2.37.4-1/cmd/system-shutdown/system-shutdown-utils.c 2.39.2+19.10ubuntu1/cmd/system-shutdown/system-shutdown-utils.c
--- 2.37.4-1/cmd/system-shutdown/system-shutdown-utils.c 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/system-shutdown/system-shutdown-utils.c 2019-06-05 06:41:21.000000000 +0000
@@ -34,7 +34,7 @@
#include "../libsnap-confine-private/mountinfo.h"
#include "../libsnap-confine-private/string-utils.h"
-__attribute__ ((format(printf, 1, 2)))
+__attribute__((format(printf, 1, 2)))
void kmsg(const char *fmt, ...)
{
static FILE *kmsg = NULL;
@@ -53,7 +53,7 @@ void kmsg(const char *fmt, ...)
va_end(va);
}
-__attribute__ ((noreturn))
+__attribute__((noreturn))
void die(const char *msg)
{
if (errno == 0) {
@@ -111,13 +111,12 @@ bool umount_all(void)
bool had_writable = false;
for (int i = 0; i < 10 && did_umount; i++) {
- struct sc_mountinfo *mounts = sc_parse_mountinfo(NULL);
+ sc_mountinfo *mounts = sc_parse_mountinfo(NULL);
if (!mounts) {
// oh dear
die("unable to get mount info; giving up");
}
- struct sc_mountinfo_entry *cur =
- sc_first_mountinfo_entry(mounts);
+ sc_mountinfo_entry *cur = sc_first_mountinfo_entry(mounts);
had_writable = false;
did_umount = false;
diff -pruN 2.37.4-1/cmd/system-shutdown/system-shutdown-utils.h 2.39.2+19.10ubuntu1/cmd/system-shutdown/system-shutdown-utils.h
--- 2.37.4-1/cmd/system-shutdown/system-shutdown-utils.h 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/cmd/system-shutdown/system-shutdown-utils.h 2019-06-05 06:41:21.000000000 +0000
@@ -25,9 +25,9 @@
// no longer found writable.
bool umount_all(void);
-__attribute__ ((noreturn))
+__attribute__((noreturn))
void die(const char *msg);
-__attribute__ ((format(printf, 1, 2)))
+__attribute__((format(printf, 1, 2)))
void kmsg(const char *fmt, ...);
// Reads a possible argument for reboot syscall in /run/systemd/reboot-param,
diff -pruN 2.37.4-1/daemon/api_asserts.go 2.39.2+19.10ubuntu1/daemon/api_asserts.go
--- 2.37.4-1/daemon/api_asserts.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/daemon/api_asserts.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,126 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016-2019 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 .
+ *
+ */
+
+package daemon
+
+import (
+ "net/http"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/overlord/assertstate"
+ "github.com/snapcore/snapd/overlord/auth"
+)
+
+var (
+ // TODO: allow to post assertions for UserOK? they are verified anyway
+ assertsCmd = &Command{
+ Path: "/v2/assertions",
+ UserOK: true,
+ GET: getAssertTypeNames,
+ POST: doAssert,
+ }
+
+ assertsFindManyCmd = &Command{
+ Path: "/v2/assertions/{assertType}",
+ UserOK: true,
+ GET: assertsFindMany,
+ }
+)
+
+func getAssertTypeNames(c *Command, r *http.Request, user *auth.UserState) Response {
+ return SyncResponse(map[string][]string{
+ "types": asserts.TypeNames(),
+ }, nil)
+}
+
+func doAssert(c *Command, r *http.Request, user *auth.UserState) Response {
+ batch := assertstate.NewBatch()
+ _, err := batch.AddStream(r.Body)
+ if err != nil {
+ return BadRequest("cannot decode request body into assertions: %v", err)
+ }
+
+ state := c.d.overlord.State()
+ state.Lock()
+ defer state.Unlock()
+
+ if err := batch.Commit(state); err != nil {
+ return BadRequest("assert failed: %v", err)
+ }
+ // TODO: what more info do we want to return on success?
+ return &resp{
+ Type: ResponseTypeSync,
+ Status: 200,
+ }
+}
+
+func assertsFindMany(c *Command, r *http.Request, user *auth.UserState) Response {
+ assertTypeName := muxVars(r)["assertType"]
+ assertType := asserts.Type(assertTypeName)
+ if assertType == nil {
+ return BadRequest("invalid assert type: %q", assertTypeName)
+ }
+ jsonResult := false
+ headersOnly := false
+ headers := map[string]string{}
+ q := r.URL.Query()
+ for k := range q {
+ if k == "json" {
+ switch q.Get(k) {
+ case "false":
+ jsonResult = false
+ case "headers":
+ headersOnly = true
+ fallthrough
+ case "true":
+ jsonResult = true
+ default:
+ return BadRequest(`"json" query parameter when used must be set to "true" or "headers"`)
+ }
+ continue
+ }
+ headers[k] = q.Get(k)
+ }
+
+ state := c.d.overlord.State()
+ state.Lock()
+ db := assertstate.DB(state)
+ state.Unlock()
+
+ assertions, err := db.FindMany(assertType, headers)
+ if err != nil && !asserts.IsNotFound(err) {
+ return InternalError("searching assertions failed: %v", err)
+ }
+
+ if jsonResult {
+ assertsJSON := make([]struct {
+ Headers map[string]interface{} `json:"headers,omitempty"`
+ Body string `json:"body,omitempty"`
+ }, len(assertions))
+ for i := range assertions {
+ assertsJSON[i].Headers = assertions[i].Headers()
+ if !headersOnly {
+ assertsJSON[i].Body = string(assertions[i].Body())
+ }
+ }
+ return SyncResponse(assertsJSON, nil)
+ }
+
+ return AssertResponse(assertions, true)
+}
diff -pruN 2.37.4-1/daemon/api_asserts_test.go 2.39.2+19.10ubuntu1/daemon/api_asserts_test.go
--- 2.37.4-1/daemon/api_asserts_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/daemon/api_asserts_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,452 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016-2019 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 .
+ *
+ */
+
+package daemon_test
+
+import (
+ "bytes"
+ "encoding/json"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "sort"
+ "strconv"
+
+ "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/assertstest"
+ "github.com/snapcore/snapd/asserts/sysdb"
+ "github.com/snapcore/snapd/daemon"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/overlord"
+ "github.com/snapcore/snapd/overlord/assertstate"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type assertsSuite struct {
+ d *daemon.Daemon
+ o *overlord.Overlord
+
+ storeSigning *assertstest.StoreStack
+ trustedRestorer func()
+}
+
+var _ = check.Suite(&assertsSuite{})
+
+func (s *assertsSuite) SetUpTest(c *check.C) {
+ dirs.SetRootDir(c.MkDir())
+
+ s.o = overlord.Mock()
+ s.d = daemon.NewWithOverlord(s.o)
+
+ // adds an assertion db
+ s.storeSigning = assertstest.NewStoreStack("can0nical", nil)
+ s.trustedRestorer = sysdb.InjectTrusted(s.storeSigning.Trusted)
+
+ assertstate.Manager(s.o.State(), s.o.TaskRunner())
+}
+
+func (s *assertsSuite) TearDownTest(c *check.C) {
+ s.trustedRestorer()
+ s.o = nil
+ s.d = nil
+}
+
+func (s *assertsSuite) TestGetAsserts(c *check.C) {
+ resp := daemon.GetAssertTypeNames(daemon.AssertsCmd, nil, nil)
+ c.Check(resp.Status, check.Equals, 200)
+ c.Check(resp.Type, check.Equals, daemon.ResponseTypeSync)
+ c.Check(resp.Result, check.DeepEquals, map[string][]string{"types": asserts.TypeNames()})
+}
+
+func (s *assertsSuite) TestAssertOK(c *check.C) {
+ // add store key
+ st := s.o.State()
+ daemon.AssertAdd(st, s.storeSigning.StoreAccountKey(""))
+
+ acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "")
+ buf := bytes.NewBuffer(asserts.Encode(acct))
+ // Execute
+ req, err := http.NewRequest("POST", "/v2/assertions", buf)
+ c.Assert(err, check.IsNil)
+ rsp := daemon.DoAssert(daemon.AssertsCmd, req, nil)
+ // Verify (external)
+ c.Check(rsp.Type, check.Equals, daemon.ResponseTypeSync)
+ c.Check(rsp.Status, check.Equals, 200)
+ // Verify (internal)
+ st.Lock()
+ defer st.Unlock()
+ _, err = assertstate.DB(st).Find(asserts.AccountType, map[string]string{
+ "account-id": acct.AccountID(),
+ })
+ c.Check(err, check.IsNil)
+}
+
+func (s *assertsSuite) TestAssertStreamOK(c *check.C) {
+ st := s.o.State()
+
+ acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "")
+ buf := &bytes.Buffer{}
+ enc := asserts.NewEncoder(buf)
+ err := enc.Encode(acct)
+ c.Assert(err, check.IsNil)
+ err = enc.Encode(s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, check.IsNil)
+
+ // Execute
+ req, err := http.NewRequest("POST", "/v2/assertions", buf)
+ c.Assert(err, check.IsNil)
+ rsp := daemon.DoAssert(daemon.AssertsCmd, req, nil)
+ // Verify (external)
+ c.Check(rsp.Type, check.Equals, daemon.ResponseTypeSync)
+ c.Check(rsp.Status, check.Equals, 200)
+ // Verify (internal)
+ st.Lock()
+ defer st.Unlock()
+ _, err = assertstate.DB(st).Find(asserts.AccountType, map[string]string{
+ "account-id": acct.AccountID(),
+ })
+ c.Check(err, check.IsNil)
+}
+
+func (s *assertsSuite) TestAssertInvalid(c *check.C) {
+ // Setup
+ buf := bytes.NewBufferString("blargh")
+ req, err := http.NewRequest("POST", "/v2/assertions", buf)
+ c.Assert(err, check.IsNil)
+ rec := httptest.NewRecorder()
+ // Execute
+ daemon.AssertsCmd.POST(daemon.AssertsCmd, req, nil).ServeHTTP(rec, req)
+ // Verify (external)
+ c.Check(rec.Code, check.Equals, 400)
+ c.Check(rec.Body.String(), testutil.Contains,
+ "cannot decode request body into assertions")
+}
+
+func (s *assertsSuite) TestAssertError(c *check.C) {
+ // Setup
+ acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "")
+ buf := bytes.NewBuffer(asserts.Encode(acct))
+ req, err := http.NewRequest("POST", "/v2/assertions", buf)
+ c.Assert(err, check.IsNil)
+ rec := httptest.NewRecorder()
+ // Execute
+ daemon.AssertsCmd.POST(daemon.AssertsCmd, req, nil).ServeHTTP(rec, req)
+ // Verify (external)
+ c.Check(rec.Code, check.Equals, 400)
+ c.Check(rec.Body.String(), testutil.Contains, "assert failed")
+}
+
+func (s *assertsSuite) TestAssertsFindManyAll(c *check.C) {
+ // add store key
+ st := s.o.State()
+ daemon.AssertAdd(st, s.storeSigning.StoreAccountKey(""))
+
+ acct := assertstest.NewAccount(s.storeSigning, "developer1", map[string]interface{}{
+ "account-id": "developer1-id",
+ }, "")
+ daemon.AssertAdd(st, acct)
+
+ // Execute
+ req, err := http.NewRequest("POST", "/v2/assertions/account", nil)
+ c.Assert(err, check.IsNil)
+ defer daemon.MockMuxVars(func(*http.Request) map[string]string {
+ return map[string]string{"assertType": "account"}
+ })()
+
+ rec := httptest.NewRecorder()
+ daemon.AssertsFindManyCmd.GET(daemon.AssertsFindManyCmd, req, nil).ServeHTTP(rec, req)
+ // Verify
+ c.Check(rec.Code, check.Equals, 200, check.Commentf("body %q", rec.Body))
+ c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/x.ubuntu.assertion; bundle=y")
+ c.Check(rec.HeaderMap.Get("X-Ubuntu-Assertions-Count"), check.Equals, "4")
+ dec := asserts.NewDecoder(rec.Body)
+ a1, err := dec.Decode()
+ c.Assert(err, check.IsNil)
+ c.Check(a1.Type(), check.Equals, asserts.AccountType)
+
+ a2, err := dec.Decode()
+ c.Assert(err, check.IsNil)
+
+ a3, err := dec.Decode()
+ c.Assert(err, check.IsNil)
+
+ a4, err := dec.Decode()
+ c.Assert(err, check.IsNil)
+
+ _, err = dec.Decode()
+ c.Assert(err, check.Equals, io.EOF)
+
+ ids := []string{a1.(*asserts.Account).AccountID(), a2.(*asserts.Account).AccountID(), a3.(*asserts.Account).AccountID(), a4.(*asserts.Account).AccountID()}
+ sort.Strings(ids)
+ c.Check(ids, check.DeepEquals, []string{"can0nical", "canonical", "developer1-id", "generic"})
+}
+
+func (s *assertsSuite) TestAssertsFindManyFilter(c *check.C) {
+ st := s.o.State()
+ // add store key
+ daemon.AssertAdd(st, s.storeSigning.StoreAccountKey(""))
+
+ acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "")
+ daemon.AssertAdd(st, acct)
+
+ // Execute
+ req, err := http.NewRequest("POST", "/v2/assertions/account?username=developer1", nil)
+ c.Assert(err, check.IsNil)
+ defer daemon.MockMuxVars(func(*http.Request) map[string]string {
+ return map[string]string{"assertType": "account"}
+ })()
+
+ rec := httptest.NewRecorder()
+ daemon.AssertsFindManyCmd.GET(daemon.AssertsFindManyCmd, req, nil).ServeHTTP(rec, req)
+ // Verify
+ c.Check(rec.Code, check.Equals, 200, check.Commentf("body %q", rec.Body))
+ c.Check(rec.HeaderMap.Get("X-Ubuntu-Assertions-Count"), check.Equals, "1")
+ dec := asserts.NewDecoder(rec.Body)
+ a1, err := dec.Decode()
+ c.Assert(err, check.IsNil)
+ c.Check(a1.Type(), check.Equals, asserts.AccountType)
+ c.Check(a1.(*asserts.Account).Username(), check.Equals, "developer1")
+ c.Check(a1.(*asserts.Account).AccountID(), check.Equals, acct.AccountID())
+ _, err = dec.Decode()
+ c.Check(err, check.Equals, io.EOF)
+}
+
+func (s *assertsSuite) TestAssertsFindManyNoResults(c *check.C) {
+ // add store key
+ st := s.o.State()
+ daemon.AssertAdd(st, s.storeSigning.StoreAccountKey(""))
+
+ acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "")
+ daemon.AssertAdd(st, acct)
+
+ // Execute
+ req, err := http.NewRequest("POST", "/v2/assertions/account?username=xyzzyx", nil)
+ c.Assert(err, check.IsNil)
+ defer daemon.MockMuxVars(func(*http.Request) map[string]string {
+ return map[string]string{"assertType": "account"}
+ })()
+
+ rec := httptest.NewRecorder()
+ daemon.AssertsFindManyCmd.GET(daemon.AssertsFindManyCmd, req, nil).ServeHTTP(rec, req)
+ // Verify
+ c.Check(rec.Code, check.Equals, 200, check.Commentf("body %q", rec.Body))
+ c.Check(rec.HeaderMap.Get("X-Ubuntu-Assertions-Count"), check.Equals, "0")
+ dec := asserts.NewDecoder(rec.Body)
+ _, err = dec.Decode()
+ c.Check(err, check.Equals, io.EOF)
+}
+
+func (s *assertsSuite) TestAssertsInvalidType(c *check.C) {
+ // Execute
+ req, err := http.NewRequest("POST", "/v2/assertions/foo", nil)
+ c.Assert(err, check.IsNil)
+ defer daemon.MockMuxVars(func(*http.Request) map[string]string {
+ return map[string]string{"assertType": "foo"}
+ })()
+
+ rec := httptest.NewRecorder()
+ daemon.AssertsFindManyCmd.GET(daemon.AssertsFindManyCmd, req, nil).ServeHTTP(rec, req)
+ // Verify
+ c.Check(rec.Code, check.Equals, 400)
+ c.Check(rec.Body.String(), testutil.Contains, "invalid assert type")
+}
+
+func (s *assertsSuite) TestAssertsFindManyJSONFilter(c *check.C) {
+ st := s.o.State()
+ // add store key
+ daemon.AssertAdd(st, s.storeSigning.StoreAccountKey(""))
+
+ acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "")
+ daemon.AssertAdd(st, acct)
+
+ // Execute
+ req, err := http.NewRequest("POST", "/v2/assertions/account?json=true&username=developer1", nil)
+ c.Assert(err, check.IsNil)
+ defer daemon.MockMuxVars(func(*http.Request) map[string]string {
+ return map[string]string{"assertType": "account"}
+ })()
+
+ rec := httptest.NewRecorder()
+ daemon.AssertsFindManyCmd.GET(daemon.AssertsFindManyCmd, req, nil).ServeHTTP(rec, req)
+ // Verify
+ c.Check(rec.Code, check.Equals, 200, check.Commentf("body %q", rec.Body))
+ c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/json")
+
+ var body map[string]interface{}
+ err = json.Unmarshal(rec.Body.Bytes(), &body)
+ c.Assert(err, check.IsNil)
+ c.Check(body["result"], check.DeepEquals, []interface{}{
+ map[string]interface{}{
+ "headers": acct.Headers(),
+ },
+ })
+}
+
+func (s *assertsSuite) TestAssertsFindManyJSONNoResults(c *check.C) {
+ st := s.o.State()
+ // add store key
+ daemon.AssertAdd(st, s.storeSigning.StoreAccountKey(""))
+
+ acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "")
+ daemon.AssertAdd(st, acct)
+
+ // Execute
+ req, err := http.NewRequest("POST", "/v2/assertions/account?json=true&username=xyz", nil)
+ c.Assert(err, check.IsNil)
+ defer daemon.MockMuxVars(func(*http.Request) map[string]string {
+ return map[string]string{"assertType": "account"}
+ })()
+
+ rec := httptest.NewRecorder()
+ daemon.AssertsFindManyCmd.GET(daemon.AssertsFindManyCmd, req, nil).ServeHTTP(rec, req)
+ // Verify
+ c.Check(rec.Code, check.Equals, 200, check.Commentf("body %q", rec.Body))
+ c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/json")
+
+ var body map[string]interface{}
+ err = json.Unmarshal(rec.Body.Bytes(), &body)
+ c.Assert(err, check.IsNil)
+ c.Check(body["result"], check.DeepEquals, []interface{}{})
+}
+
+func (s *assertsSuite) TestAssertsFindManyJSONWithBody(c *check.C) {
+ st := s.o.State()
+ // add store key
+ daemon.AssertAdd(st, s.storeSigning.StoreAccountKey(""))
+
+ // Execute
+ req, err := http.NewRequest("POST", "/v2/assertions/account-key?json=true", nil)
+ c.Assert(err, check.IsNil)
+ defer daemon.MockMuxVars(func(*http.Request) map[string]string {
+ return map[string]string{"assertType": "account-key"}
+ })()
+
+ rec := httptest.NewRecorder()
+ daemon.AssertsFindManyCmd.GET(daemon.AssertsFindManyCmd, req, nil).ServeHTTP(rec, req)
+ // Verify
+ c.Check(rec.Code, check.Equals, 200, check.Commentf("body %q", rec.Body))
+ c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/json")
+
+ var got []string
+ var body map[string]interface{}
+ err = json.Unmarshal(rec.Body.Bytes(), &body)
+ c.Assert(err, check.IsNil)
+ for _, a := range body["result"].([]interface{}) {
+ h := a.(map[string]interface{})["headers"].(map[string]interface{})
+ got = append(got, h["account-id"].(string)+"/"+h["name"].(string))
+ // check body
+ l, err := strconv.Atoi(h["body-length"].(string))
+ c.Assert(err, check.IsNil)
+ c.Check(a.(map[string]interface{})["body"], check.HasLen, l)
+ }
+ sort.Strings(got)
+ c.Check(got, check.DeepEquals, []string{"can0nical/root", "can0nical/store", "canonical/root", "generic/models"})
+}
+
+func (s *assertsSuite) TestAssertsFindManyJSONHeadersOnly(c *check.C) {
+ st := s.o.State()
+ // add store key
+ daemon.AssertAdd(st, s.storeSigning.StoreAccountKey(""))
+
+ // Execute
+ req, err := http.NewRequest("POST", "/v2/assertions/account-key?json=headers&account-id=can0nical", nil)
+ c.Assert(err, check.IsNil)
+ defer daemon.MockMuxVars(func(*http.Request) map[string]string {
+ return map[string]string{"assertType": "account-key"}
+ })()
+
+ rec := httptest.NewRecorder()
+ daemon.AssertsFindManyCmd.GET(daemon.AssertsFindManyCmd, req, nil).ServeHTTP(rec, req)
+ // Verify
+ c.Check(rec.Code, check.Equals, 200, check.Commentf("body %q", rec.Body))
+ c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/json")
+
+ var got []string
+ var body map[string]interface{}
+ err = json.Unmarshal(rec.Body.Bytes(), &body)
+ c.Assert(err, check.IsNil)
+ for _, a := range body["result"].([]interface{}) {
+ h := a.(map[string]interface{})["headers"].(map[string]interface{})
+ got = append(got, h["account-id"].(string)+"/"+h["name"].(string))
+ // check body absent
+ _, ok := a.(map[string]interface{})["body"]
+ c.Assert(ok, check.Equals, false)
+ }
+ sort.Strings(got)
+ c.Check(got, check.DeepEquals, []string{"can0nical/root", "can0nical/store"})
+}
+
+func (s *assertsSuite) TestAssertsFindManyJSONInvalidParam(c *check.C) {
+ st := s.o.State()
+ // add store key
+ daemon.AssertAdd(st, s.storeSigning.StoreAccountKey(""))
+
+ // Execute
+ req, err := http.NewRequest("POST", "/v2/assertions/account-key?json=header&account-id=can0nical", nil)
+ c.Assert(err, check.IsNil)
+ defer daemon.MockMuxVars(func(*http.Request) map[string]string {
+ return map[string]string{"assertType": "account-key"}
+ })()
+
+ rec := httptest.NewRecorder()
+ daemon.AssertsFindManyCmd.GET(daemon.AssertsFindManyCmd, req, nil).ServeHTTP(rec, req)
+ // Verify
+ c.Check(rec.Code, check.Equals, 400, check.Commentf("body %q", rec.Body))
+ c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/json")
+
+ var rsp daemon.Resp
+ c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), check.IsNil)
+ c.Check(rsp.Status, check.Equals, 400)
+ c.Check(rsp.Type, check.Equals, daemon.ResponseTypeError)
+ c.Check(rsp.Result, check.DeepEquals, map[string]interface{}{
+ "message": `"json" query parameter when used must be set to "true" or "headers"`,
+ })
+}
+
+func (s *assertsSuite) TestAssertsFindManyJSONNopFilter(c *check.C) {
+ st := s.o.State()
+ // add store key
+ daemon.AssertAdd(st, s.storeSigning.StoreAccountKey(""))
+
+ acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "")
+ daemon.AssertAdd(st, acct)
+
+ // Execute
+ req, err := http.NewRequest("POST", "/v2/assertions/account?json=false&username=developer1", nil)
+ c.Assert(err, check.IsNil)
+ defer daemon.MockMuxVars(func(*http.Request) map[string]string {
+ return map[string]string{"assertType": "account"}
+ })()
+
+ rec := httptest.NewRecorder()
+ daemon.AssertsFindManyCmd.GET(daemon.AssertsFindManyCmd, req, nil).ServeHTTP(rec, req)
+ // Verify
+ c.Check(rec.Code, check.Equals, 200, check.Commentf("body %q", rec.Body))
+ c.Check(rec.HeaderMap.Get("X-Ubuntu-Assertions-Count"), check.Equals, "1")
+ dec := asserts.NewDecoder(rec.Body)
+ a1, err := dec.Decode()
+ c.Assert(err, check.IsNil)
+ c.Check(a1.Type(), check.Equals, asserts.AccountType)
+ c.Check(a1.(*asserts.Account).Username(), check.Equals, "developer1")
+ c.Check(a1.(*asserts.Account).AccountID(), check.Equals, acct.AccountID())
+ _, err = dec.Decode()
+ c.Check(err, check.Equals, io.EOF)
+}
diff -pruN 2.37.4-1/daemon/api_connections.go 2.39.2+19.10ubuntu1/daemon/api_connections.go
--- 2.37.4-1/daemon/api_connections.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/daemon/api_connections.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,275 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package daemon
+
+import (
+ "net/http"
+ "sort"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/overlord/ifacestate"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+)
+
+var connectionsCmd = &Command{
+ Path: "/v2/connections",
+ UserOK: true,
+ GET: getConnections,
+}
+
+type collectFilter struct {
+ snapName string
+ ifaceName string
+ connected bool
+}
+
+func (c *collectFilter) plugOrConnectedSlotMatches(plug *interfaces.PlugRef, connectedSlots []interfaces.SlotRef) bool {
+ for _, slot := range connectedSlots {
+ if c.slotOrConnectedPlugMatches(&slot, nil) {
+ return true
+ }
+ }
+ if c.snapName != "" && plug.Snap != c.snapName {
+ return false
+ }
+ return true
+}
+
+func (c *collectFilter) slotOrConnectedPlugMatches(slot *interfaces.SlotRef, connectedPlugs []interfaces.PlugRef) bool {
+ for _, plug := range connectedPlugs {
+ if c.plugOrConnectedSlotMatches(&plug, nil) {
+ return true
+ }
+ }
+ if c.snapName != "" && slot.Snap != c.snapName {
+ return false
+ }
+ return true
+}
+
+func (c *collectFilter) ifaceMatches(ifaceName string) bool {
+ if c.ifaceName != "" && c.ifaceName != ifaceName {
+ return false
+ }
+ return true
+}
+
+type bySlotRef []interfaces.SlotRef
+
+func (b bySlotRef) Len() int { return len(b) }
+func (b bySlotRef) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
+func (b bySlotRef) Less(i, j int) bool {
+ return b[i].SortsBefore(b[j])
+}
+
+type byPlugRef []interfaces.PlugRef
+
+func (b byPlugRef) Len() int { return len(b) }
+func (b byPlugRef) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
+func (b byPlugRef) Less(i, j int) bool {
+ return b[i].SortsBefore(b[j])
+}
+
+// mergeAttrs merges attributes from 2 disjoint sets of static and dynamic slot or
+// plug attributes into a single map.
+func mergeAttrs(one map[string]interface{}, other map[string]interface{}) map[string]interface{} {
+ merged := make(map[string]interface{}, len(one)+len(other))
+ for k, v := range one {
+ merged[k] = v
+ }
+ for k, v := range other {
+ merged[k] = v
+ }
+ return merged
+}
+
+func collectConnections(ifaceMgr *ifacestate.InterfaceManager, filter collectFilter) (*connectionsJSON, error) {
+ repo := ifaceMgr.Repository()
+ ifaces := repo.Interfaces()
+
+ var connsjson connectionsJSON
+ var connStates map[string]ifacestate.ConnectionState
+ plugConns := map[string][]interfaces.SlotRef{}
+ slotConns := map[string][]interfaces.PlugRef{}
+
+ var err error
+ connStates, err = ifaceMgr.ConnectionStates()
+ if err != nil {
+ return nil, err
+ }
+
+ connsjson.Established = make([]connectionJSON, 0, len(connStates))
+ connsjson.Plugs = make([]*plugJSON, 0, len(ifaces.Plugs))
+ connsjson.Slots = make([]*slotJSON, 0, len(ifaces.Slots))
+
+ for crefStr, cstate := range connStates {
+ if cstate.Undesired && filter.connected {
+ continue
+ }
+ if cstate.HotplugGone {
+ // XXX: hotplug connection - the device and slot are gone
+ continue
+ }
+
+ cref, err := interfaces.ParseConnRef(crefStr)
+ if err != nil {
+ return nil, err
+ }
+ if !filter.plugOrConnectedSlotMatches(&cref.PlugRef, nil) && !filter.slotOrConnectedPlugMatches(&cref.SlotRef, nil) {
+ continue
+ }
+ if !filter.ifaceMatches(cstate.Interface) {
+ continue
+ }
+ plugRef := interfaces.PlugRef{Snap: cref.PlugRef.Snap, Name: cref.PlugRef.Name}
+ slotRef := interfaces.SlotRef{Snap: cref.SlotRef.Snap, Name: cref.SlotRef.Name}
+ plugID := plugRef.String()
+ slotID := slotRef.String()
+
+ cj := connectionJSON{
+ Slot: slotRef,
+ Plug: plugRef,
+ Manual: cstate.Auto == false,
+ Gadget: cstate.ByGadget,
+ Interface: cstate.Interface,
+ PlugAttrs: mergeAttrs(cstate.StaticPlugAttrs, cstate.DynamicPlugAttrs),
+ SlotAttrs: mergeAttrs(cstate.StaticSlotAttrs, cstate.DynamicSlotAttrs),
+ }
+ if cstate.Undesired {
+ // explicitly disconnected are always manual
+ cj.Manual = true
+ connsjson.Undesired = append(connsjson.Undesired, cj)
+ } else {
+ plugConns[plugID] = append(plugConns[plugID], slotRef)
+ slotConns[slotID] = append(slotConns[slotID], plugRef)
+
+ connsjson.Established = append(connsjson.Established, cj)
+ }
+ }
+
+ for _, plug := range ifaces.Plugs {
+ plugRef := interfaces.PlugRef{Snap: plug.Snap.InstanceName(), Name: plug.Name}
+ connectedSlots, connected := plugConns[plugRef.String()]
+ if !connected && filter.connected {
+ continue
+ }
+ if !filter.ifaceMatches(plug.Interface) || !filter.plugOrConnectedSlotMatches(&plugRef, connectedSlots) {
+ continue
+ }
+ var apps []string
+ for _, app := range plug.Apps {
+ apps = append(apps, app.Name)
+ }
+ sort.Sort(bySlotRef(connectedSlots))
+ pj := &plugJSON{
+ Snap: plugRef.Snap,
+ Name: plugRef.Name,
+ Interface: plug.Interface,
+ Attrs: plug.Attrs,
+ Apps: apps,
+ Label: plug.Label,
+ Connections: connectedSlots,
+ }
+ connsjson.Plugs = append(connsjson.Plugs, pj)
+ }
+ for _, slot := range ifaces.Slots {
+ slotRef := interfaces.SlotRef{Snap: slot.Snap.InstanceName(), Name: slot.Name}
+ connectedPlugs, connected := slotConns[slotRef.String()]
+ if !connected && filter.connected {
+ continue
+ }
+ if !filter.ifaceMatches(slot.Interface) || !filter.slotOrConnectedPlugMatches(&slotRef, connectedPlugs) {
+ continue
+ }
+ var apps []string
+ for _, app := range slot.Apps {
+ apps = append(apps, app.Name)
+ }
+ sort.Sort(byPlugRef(connectedPlugs))
+ sj := &slotJSON{
+ Snap: slotRef.Snap,
+ Name: slotRef.Name,
+ Interface: slot.Interface,
+ Attrs: slot.Attrs,
+ Apps: apps,
+ Label: slot.Label,
+ Connections: connectedPlugs,
+ }
+ connsjson.Slots = append(connsjson.Slots, sj)
+ }
+ return &connsjson, nil
+}
+
+type byCrefConnJSON []connectionJSON
+
+func (b byCrefConnJSON) Len() int { return len(b) }
+func (b byCrefConnJSON) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
+func (b byCrefConnJSON) Less(i, j int) bool {
+ icj := b[i]
+ jcj := b[j]
+ iCref := interfaces.ConnRef{PlugRef: icj.Plug, SlotRef: icj.Slot}
+ jCref := interfaces.ConnRef{PlugRef: jcj.Plug, SlotRef: jcj.Slot}
+ sortsBefore := iCref.SortsBefore(&jCref)
+ return sortsBefore
+}
+
+func checkSnapInstalled(st *state.State, name string) error {
+ st.Lock()
+ defer st.Unlock()
+
+ var snapst snapstate.SnapState
+ return snapstate.Get(st, name, &snapst)
+}
+
+func getConnections(c *Command, r *http.Request, user *auth.UserState) Response {
+ query := r.URL.Query()
+ snapName := query.Get("snap")
+ ifaceName := query.Get("interface")
+ qselect := query.Get("select")
+ if qselect != "all" && qselect != "" {
+ return BadRequest("unsupported select qualifier")
+ }
+ onlyConnected := qselect == ""
+
+ snapName = ifacestate.RemapSnapFromRequest(snapName)
+ if snapName != "" {
+ if err := checkSnapInstalled(c.d.overlord.State(), snapName); err != nil {
+ if err == state.ErrNoState {
+ return SnapNotFound(snapName, err)
+ }
+ return InternalError("cannot access snap state: %v", err)
+ }
+ }
+
+ connsjson, err := collectConnections(c.d.overlord.InterfaceManager(), collectFilter{
+ snapName: snapName,
+ ifaceName: ifaceName,
+ connected: onlyConnected,
+ })
+ if err != nil {
+ return InternalError("collecting connection information failed: %v", err)
+ }
+ sort.Sort(byCrefConnJSON(connsjson.Established))
+ sort.Sort(byCrefConnJSON(connsjson.Undesired))
+
+ return SyncResponse(connsjson, nil)
+}
diff -pruN 2.37.4-1/daemon/api_connections_test.go 2.39.2+19.10ubuntu1/daemon/api_connections_test.go
--- 2.37.4-1/daemon/api_connections_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/daemon/api_connections_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,975 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package daemon
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+
+ "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/interfaces/ifacetest"
+)
+
+// Tests for GET /v2/connections
+
+func (s *apiSuite) testConnectionsConnected(c *check.C, query string, connsState map[string]interface{}, expected map[string]interface{}) {
+ c.Assert(s.d, check.NotNil, check.Commentf("call s.daemon() first"))
+
+ repo := s.d.overlord.InterfaceManager().Repository()
+ for crefStr, cstate := range connsState {
+ cref, err := interfaces.ParseConnRef(crefStr)
+ c.Assert(err, check.IsNil)
+ conn := cstate.(map[string]interface{})
+ if undesiredRaw, ok := conn["undesired"]; ok {
+ undesired, ok := undesiredRaw.(bool)
+ c.Assert(ok, check.Equals, true, check.Commentf("unexpected value for key 'undesired': %v", cstate))
+ if undesired {
+ // do not add connections that are undesired
+ continue
+ }
+ }
+ staticPlugAttrs, _ := conn["plug-static"].(map[string]interface{})
+ dynamicPlugAttrs, _ := conn["plug-dynamic"].(map[string]interface{})
+ staticSlotAttrs, _ := conn["slot-static"].(map[string]interface{})
+ dynamicSlotAttrs, _ := conn["slot-dynamic"].(map[string]interface{})
+ _, err = repo.Connect(cref, staticPlugAttrs, dynamicPlugAttrs, staticSlotAttrs, dynamicSlotAttrs, nil)
+ c.Assert(err, check.IsNil)
+ }
+
+ st := s.d.overlord.State()
+ st.Lock()
+ st.Set("conns", connsState)
+ st.Unlock()
+
+ s.testConnections(c, query, expected)
+}
+
+func (s *apiSuite) testConnections(c *check.C, query string, expected map[string]interface{}) {
+ req, err := http.NewRequest("GET", query, nil)
+ c.Assert(err, check.IsNil)
+ rec := httptest.NewRecorder()
+ connectionsCmd.GET(connectionsCmd, req, nil).ServeHTTP(rec, req)
+ c.Check(rec.Code, check.Equals, 200)
+ var body map[string]interface{}
+ err = json.Unmarshal(rec.Body.Bytes(), &body)
+ c.Check(err, check.IsNil)
+ c.Check(body, check.DeepEquals, expected)
+}
+
+func (s *apiSuite) TestConnectionsUnhappy(c *check.C) {
+ s.daemon(c)
+ req, err := http.NewRequest("GET", "/v2/connections?select=bad", nil)
+ c.Assert(err, check.IsNil)
+ rec := httptest.NewRecorder()
+ connectionsCmd.GET(connectionsCmd, req, nil).ServeHTTP(rec, req)
+ c.Check(rec.Code, check.Equals, 400)
+ var body map[string]interface{}
+ err = json.Unmarshal(rec.Body.Bytes(), &body)
+ c.Check(err, check.IsNil)
+ c.Check(body, check.DeepEquals, map[string]interface{}{
+ "result": map[string]interface{}{
+ "message": "unsupported select qualifier",
+ },
+ "status": "Bad Request",
+ "status-code": 400.0,
+ "type": "error",
+ })
+}
+
+func (s *apiSuite) TestConnectionsEmpty(c *check.C) {
+ s.daemon(c)
+ s.testConnections(c, "/v2/connections", map[string]interface{}{
+ "result": map[string]interface{}{
+ "established": []interface{}{},
+ "plugs": []interface{}{},
+ "slots": []interface{}{},
+ },
+ "status": "OK",
+ "status-code": 200.0,
+ "type": "sync",
+ })
+ s.testConnections(c, "/v2/connections?select=all", map[string]interface{}{
+ "result": map[string]interface{}{
+ "established": []interface{}{},
+ "plugs": []interface{}{},
+ "slots": []interface{}{},
+ },
+ "status": "OK",
+ "status-code": 200.0,
+ "type": "sync",
+ })
+}
+
+func (s *apiSuite) TestConnectionsNotFound(c *check.C) {
+ s.daemon(c)
+ req, err := http.NewRequest("GET", "/v2/connections?snap=not-found", nil)
+ c.Assert(err, check.IsNil)
+ rec := httptest.NewRecorder()
+ connectionsCmd.GET(connectionsCmd, req, nil).ServeHTTP(rec, req)
+ c.Check(rec.Code, check.Equals, 404)
+ var body map[string]interface{}
+ err = json.Unmarshal(rec.Body.Bytes(), &body)
+ c.Check(err, check.IsNil)
+ c.Check(body, check.DeepEquals, map[string]interface{}{
+ "result": map[string]interface{}{
+ "message": "no state entry for key",
+ "kind": "snap-not-found",
+ "value": "not-found",
+ },
+ "status": "Not Found",
+ "status-code": 404.0,
+ "type": "error",
+ })
+}
+
+func (s *apiSuite) TestConnectionsUnconnected(c *check.C) {
+ restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
+ defer restore()
+
+ s.daemon(c)
+
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, producerYaml)
+
+ s.testConnections(c, "/v2/connections?select=all", map[string]interface{}{
+ "result": map[string]interface{}{
+ "established": []interface{}{},
+ "plugs": []interface{}{
+ map[string]interface{}{
+ "snap": "consumer",
+ "plug": "plug",
+ "interface": "test",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app"},
+ "label": "label",
+ },
+ },
+ "slots": []interface{}{
+ map[string]interface{}{
+ "snap": "producer",
+ "slot": "slot",
+ "interface": "test",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app"},
+ "label": "label",
+ },
+ },
+ },
+ "status": "OK",
+ "status-code": 200.0,
+ "type": "sync",
+ })
+}
+
+func (s *apiSuite) TestConnectionsBySnapName(c *check.C) {
+ restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
+ defer restore()
+
+ s.daemon(c)
+
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, producerYaml)
+
+ s.testConnections(c, "/v2/connections?select=all&snap=producer", map[string]interface{}{
+ "result": map[string]interface{}{
+ "established": []interface{}{},
+ "slots": []interface{}{
+ map[string]interface{}{
+ "snap": "producer",
+ "slot": "slot",
+ "interface": "test",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app"},
+ "label": "label",
+ },
+ },
+ "plugs": []interface{}{},
+ },
+ "status": "OK",
+ "status-code": 200.0,
+ "type": "sync",
+ })
+
+ s.testConnections(c, "/v2/connections?select=all&snap=consumer", map[string]interface{}{
+ "result": map[string]interface{}{
+ "established": []interface{}{},
+ "plugs": []interface{}{
+ map[string]interface{}{
+ "snap": "consumer",
+ "plug": "plug",
+ "interface": "test",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app"},
+ "label": "label",
+ },
+ },
+ "slots": []interface{}{},
+ },
+ "status": "OK",
+ "status-code": 200.0,
+ "type": "sync",
+ })
+
+ s.testConnectionsConnected(c, "/v2/connections?snap=producer", map[string]interface{}{
+ "consumer:plug producer:slot": map[string]interface{}{
+ "interface": "test",
+ },
+ }, map[string]interface{}{
+ "result": map[string]interface{}{
+ "plugs": []interface{}{
+ map[string]interface{}{
+ "snap": "consumer",
+ "plug": "plug",
+ "interface": "test",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app"},
+ "label": "label",
+ "connections": []interface{}{
+ map[string]interface{}{"snap": "producer", "slot": "slot"},
+ },
+ },
+ },
+ "slots": []interface{}{
+ map[string]interface{}{
+ "snap": "producer",
+ "slot": "slot",
+ "interface": "test",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app"},
+ "label": "label",
+ "connections": []interface{}{
+ map[string]interface{}{"snap": "consumer", "plug": "plug"},
+ },
+ },
+ },
+ "established": []interface{}{
+ map[string]interface{}{
+ "plug": map[string]interface{}{"snap": "consumer", "plug": "plug"},
+ "slot": map[string]interface{}{"snap": "producer", "slot": "slot"},
+ "manual": true,
+ "interface": "test",
+ },
+ },
+ },
+ "status": "OK",
+ "status-code": 200.0,
+ "type": "sync",
+ })
+}
+
+func (s *apiSuite) TestConnectionsBySnapAlias(c *check.C) {
+ restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
+ defer restore()
+
+ s.daemon(c)
+
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, coreProducerYaml)
+
+ expectedUnconnected := map[string]interface{}{
+ "established": []interface{}{},
+ "slots": []interface{}{
+ map[string]interface{}{
+ "snap": "core",
+ "slot": "slot",
+ "interface": "test",
+ "attrs": map[string]interface{}{"key": "value"},
+ "label": "label",
+ },
+ },
+ "plugs": []interface{}{},
+ }
+ s.testConnections(c, "/v2/connections?select=all&snap=core", map[string]interface{}{
+ "result": expectedUnconnected,
+ "status": "OK",
+ "status-code": 200.0,
+ "type": "sync",
+ })
+ // try using a well know alias
+ s.testConnections(c, "/v2/connections?select=all&snap=system", map[string]interface{}{
+ "result": expectedUnconnected,
+ "status": "OK",
+ "status-code": 200.0,
+ "type": "sync",
+ })
+
+ expectedConnmected := map[string]interface{}{
+ "plugs": []interface{}{
+ map[string]interface{}{
+ "snap": "consumer",
+ "plug": "plug",
+ "interface": "test",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app"},
+ "label": "label",
+ "connections": []interface{}{
+ map[string]interface{}{"snap": "core", "slot": "slot"},
+ },
+ },
+ },
+ "slots": []interface{}{
+ map[string]interface{}{
+ "snap": "core",
+ "slot": "slot",
+ "interface": "test",
+ "attrs": map[string]interface{}{"key": "value"},
+ "label": "label",
+ "connections": []interface{}{
+ map[string]interface{}{"snap": "consumer", "plug": "plug"},
+ },
+ },
+ },
+ "established": []interface{}{
+ map[string]interface{}{
+ "plug": map[string]interface{}{"snap": "consumer", "plug": "plug"},
+ "slot": map[string]interface{}{"snap": "core", "slot": "slot"},
+ "manual": true,
+ "interface": "test",
+ },
+ },
+ }
+
+ s.testConnectionsConnected(c, "/v2/connections?snap=core", map[string]interface{}{
+ "consumer:plug core:slot": map[string]interface{}{
+ "interface": "test",
+ },
+ }, map[string]interface{}{
+ "result": expectedConnmected,
+ "status": "OK",
+ "status-code": 200.0,
+ "type": "sync",
+ })
+ // connection was already established
+ s.testConnections(c, "/v2/connections?snap=system", map[string]interface{}{
+ "result": expectedConnmected,
+ "status": "OK",
+ "status-code": 200.0,
+ "type": "sync",
+ })
+}
+
+func (s *apiSuite) TestConnectionsByIfaceName(c *check.C) {
+ restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
+ defer restore()
+ restore = builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "different"})
+ defer restore()
+
+ s.daemon(c)
+
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, producerYaml)
+ var differentProducerYaml = `
+name: different-producer
+version: 1
+apps:
+ app:
+slots:
+ slot:
+ interface: different
+ key: value
+ label: label
+`
+ var differentConsumerYaml = `
+name: different-consumer
+version: 1
+apps:
+ app:
+plugs:
+ plug:
+ interface: different
+ key: value
+ label: label
+`
+ s.mockSnap(c, differentProducerYaml)
+ s.mockSnap(c, differentConsumerYaml)
+
+ s.testConnections(c, "/v2/connections?select=all&interface=test", map[string]interface{}{
+ "result": map[string]interface{}{
+ "established": []interface{}{},
+ "plugs": []interface{}{
+ map[string]interface{}{
+ "snap": "consumer",
+ "plug": "plug",
+ "interface": "test",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app"},
+ "label": "label",
+ },
+ },
+ "slots": []interface{}{
+ map[string]interface{}{
+ "snap": "producer",
+ "slot": "slot",
+ "interface": "test",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app"},
+ "label": "label",
+ },
+ },
+ },
+ "status": "OK",
+ "status-code": 200.0,
+ "type": "sync",
+ })
+ s.testConnections(c, "/v2/connections?select=all&interface=different", map[string]interface{}{
+ "result": map[string]interface{}{
+ "established": []interface{}{},
+ "plugs": []interface{}{
+ map[string]interface{}{
+ "snap": "different-consumer",
+ "plug": "plug",
+ "interface": "different",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app"},
+ "label": "label",
+ },
+ },
+ "slots": []interface{}{
+ map[string]interface{}{
+ "snap": "different-producer",
+ "slot": "slot",
+ "interface": "different",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app"},
+ "label": "label",
+ },
+ },
+ },
+ "status": "OK",
+ "status-code": 200.0,
+ "type": "sync",
+ })
+
+ // modifies state internally
+ s.testConnectionsConnected(c, "/v2/connections?interfaces=test", map[string]interface{}{
+ "consumer:plug producer:slot": map[string]interface{}{
+ "interface": "test",
+ },
+ }, map[string]interface{}{
+ "result": map[string]interface{}{
+ "plugs": []interface{}{
+ map[string]interface{}{
+ "snap": "consumer",
+ "plug": "plug",
+ "interface": "test",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app"},
+ "label": "label",
+ "connections": []interface{}{
+ map[string]interface{}{"snap": "producer", "slot": "slot"},
+ },
+ },
+ },
+ "slots": []interface{}{
+ map[string]interface{}{
+ "snap": "producer",
+ "slot": "slot",
+ "interface": "test",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app"},
+ "label": "label",
+ "connections": []interface{}{
+ map[string]interface{}{"snap": "consumer", "plug": "plug"},
+ },
+ },
+ },
+ "established": []interface{}{
+ map[string]interface{}{
+ "plug": map[string]interface{}{"snap": "consumer", "plug": "plug"},
+ "slot": map[string]interface{}{"snap": "producer", "slot": "slot"},
+ "manual": true,
+ "interface": "test",
+ },
+ },
+ },
+ "status": "OK",
+ "status-code": 200.0,
+ "type": "sync",
+ })
+ // use state modified by previous cal
+ s.testConnections(c, "/v2/connections?interface=different", map[string]interface{}{
+ "result": map[string]interface{}{
+ "established": []interface{}{},
+ "slots": []interface{}{},
+ "plugs": []interface{}{},
+ },
+ "status": "OK",
+ "status-code": 200.0,
+ "type": "sync",
+ })
+}
+
+func (s *apiSuite) TestConnectionsDefaultManual(c *check.C) {
+ restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
+ defer restore()
+
+ s.daemon(c)
+
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, producerYaml)
+
+ s.testConnectionsConnected(c, "/v2/connections", map[string]interface{}{
+ "consumer:plug producer:slot": map[string]interface{}{
+ "interface": "test",
+ },
+ }, map[string]interface{}{
+ "result": map[string]interface{}{
+ "plugs": []interface{}{
+ map[string]interface{}{
+ "snap": "consumer",
+ "plug": "plug",
+ "interface": "test",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app"},
+ "label": "label",
+ "connections": []interface{}{
+ map[string]interface{}{"snap": "producer", "slot": "slot"},
+ },
+ },
+ },
+ "slots": []interface{}{
+ map[string]interface{}{
+ "snap": "producer",
+ "slot": "slot",
+ "interface": "test",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app"},
+ "label": "label",
+ "connections": []interface{}{
+ map[string]interface{}{"snap": "consumer", "plug": "plug"},
+ },
+ },
+ },
+ "established": []interface{}{
+ map[string]interface{}{
+ "plug": map[string]interface{}{"snap": "consumer", "plug": "plug"},
+ "slot": map[string]interface{}{"snap": "producer", "slot": "slot"},
+ "manual": true,
+ "interface": "test",
+ },
+ },
+ },
+ "status": "OK",
+ "status-code": 200.0,
+ "type": "sync",
+ })
+}
+
+func (s *apiSuite) TestConnectionsDefaultAuto(c *check.C) {
+ restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
+ defer restore()
+
+ s.daemon(c)
+
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, producerYaml)
+
+ s.testConnectionsConnected(c, "/v2/connections", map[string]interface{}{
+ "consumer:plug producer:slot": map[string]interface{}{
+ "interface": "test",
+ "auto": true,
+ "plug-static": map[string]interface{}{
+ "key": "value",
+ },
+ "plug-dynamic": map[string]interface{}{
+ "foo-plug-dynamic": "bar-dynamic",
+ },
+ "slot-static": map[string]interface{}{
+ "key": "value",
+ },
+ "slot-dynamic": map[string]interface{}{
+ "foo-slot-dynamic": "bar-dynamic",
+ },
+ },
+ }, map[string]interface{}{
+ "result": map[string]interface{}{
+ "plugs": []interface{}{
+ map[string]interface{}{
+ "snap": "consumer",
+ "plug": "plug",
+ "interface": "test",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app"},
+ "label": "label",
+ "connections": []interface{}{
+ map[string]interface{}{"snap": "producer", "slot": "slot"},
+ },
+ },
+ },
+ "slots": []interface{}{
+ map[string]interface{}{
+ "snap": "producer",
+ "slot": "slot",
+ "interface": "test",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app"},
+ "label": "label",
+ "connections": []interface{}{
+ map[string]interface{}{"snap": "consumer", "plug": "plug"},
+ },
+ },
+ },
+ "established": []interface{}{
+ map[string]interface{}{
+ "plug": map[string]interface{}{"snap": "consumer", "plug": "plug"},
+ "slot": map[string]interface{}{"snap": "producer", "slot": "slot"},
+ "interface": "test",
+ "plug-attrs": map[string]interface{}{
+ "key": "value",
+ "foo-plug-dynamic": "bar-dynamic",
+ },
+ "slot-attrs": map[string]interface{}{
+ "key": "value",
+ "foo-slot-dynamic": "bar-dynamic",
+ },
+ },
+ },
+ },
+ "status": "OK",
+ "status-code": 200.0,
+ "type": "sync",
+ })
+}
+
+func (s *apiSuite) TestConnectionsDefaultGadget(c *check.C) {
+ restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
+ defer restore()
+
+ s.daemon(c)
+
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, producerYaml)
+
+ s.testConnectionsConnected(c, "/v2/connections", map[string]interface{}{
+ "consumer:plug producer:slot": map[string]interface{}{
+ "interface": "test",
+ "by-gadget": true,
+ "auto": true,
+ },
+ }, map[string]interface{}{
+ "result": map[string]interface{}{
+ "plugs": []interface{}{
+ map[string]interface{}{
+ "snap": "consumer",
+ "plug": "plug",
+ "interface": "test",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app"},
+ "label": "label",
+ "connections": []interface{}{
+ map[string]interface{}{"snap": "producer", "slot": "slot"},
+ },
+ },
+ },
+ "slots": []interface{}{
+ map[string]interface{}{
+ "snap": "producer",
+ "slot": "slot",
+ "interface": "test",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app"},
+ "label": "label",
+ "connections": []interface{}{
+ map[string]interface{}{"snap": "consumer", "plug": "plug"},
+ },
+ },
+ },
+ "established": []interface{}{
+ map[string]interface{}{
+ "plug": map[string]interface{}{"snap": "consumer", "plug": "plug"},
+ "slot": map[string]interface{}{"snap": "producer", "slot": "slot"},
+ "gadget": true,
+ "interface": "test",
+ },
+ },
+ },
+ "status": "OK",
+ "status-code": 200.0,
+ "type": "sync",
+ })
+}
+
+func (s *apiSuite) TestConnectionsAll(c *check.C) {
+ restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
+ defer restore()
+
+ s.daemon(c)
+
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, producerYaml)
+
+ s.testConnectionsConnected(c, "/v2/connections?select=all", map[string]interface{}{
+ "consumer:plug producer:slot": map[string]interface{}{
+ "interface": "test",
+ "by-gadget": true,
+ "auto": true,
+ "undesired": true,
+ },
+ }, map[string]interface{}{
+ "result": map[string]interface{}{
+ "established": []interface{}{},
+ "plugs": []interface{}{
+ map[string]interface{}{
+ "snap": "consumer",
+ "plug": "plug",
+ "interface": "test",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app"},
+ "label": "label",
+ },
+ },
+ "slots": []interface{}{
+ map[string]interface{}{
+ "snap": "producer",
+ "slot": "slot",
+ "interface": "test",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app"},
+ "label": "label",
+ },
+ },
+ "undesired": []interface{}{
+ map[string]interface{}{
+ "plug": map[string]interface{}{"snap": "consumer", "plug": "plug"},
+ "slot": map[string]interface{}{"snap": "producer", "slot": "slot"},
+ "gadget": true,
+ "manual": true,
+ "interface": "test",
+ },
+ },
+ },
+ "status": "OK",
+ "status-code": 200.0,
+ "type": "sync",
+ })
+}
+
+func (s *apiSuite) TestConnectionsOnlyUndesired(c *check.C) {
+ restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
+ defer restore()
+
+ s.daemon(c)
+
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, producerYaml)
+
+ s.testConnectionsConnected(c, "/v2/connections", map[string]interface{}{
+ "consumer:plug producer:slot": map[string]interface{}{
+ "interface": "test",
+ "by-gadget": true,
+ "auto": true,
+ "undesired": true,
+ },
+ }, map[string]interface{}{
+ "result": map[string]interface{}{
+ "established": []interface{}{},
+ "plugs": []interface{}{},
+ "slots": []interface{}{},
+ },
+ "status": "OK",
+ "status-code": 200.0,
+ "type": "sync",
+ })
+}
+
+func (s *apiSuite) TestConnectionsHotplugGone(c *check.C) {
+ restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
+ defer restore()
+
+ s.daemon(c)
+
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, producerYaml)
+
+ s.testConnectionsConnected(c, "/v2/connections", map[string]interface{}{
+ "consumer:plug producer:slot": map[string]interface{}{
+ "interface": "test",
+ "hotplug-gone": true,
+ },
+ }, map[string]interface{}{
+ "result": map[string]interface{}{
+ "established": []interface{}{},
+ "plugs": []interface{}{},
+ "slots": []interface{}{},
+ },
+ "status": "OK",
+ "status-code": 200.0,
+ "type": "sync",
+ })
+}
+
+func (s *apiSuite) TestConnectionsSorted(c *check.C) {
+ restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
+ defer restore()
+
+ s.daemon(c)
+
+ var anotherConsumerYaml = `
+name: another-consumer-%s
+version: 1
+apps:
+ app:
+plugs:
+ plug:
+ interface: test
+ key: value
+ label: label
+`
+ var anotherProducerYaml = `
+name: another-producer
+version: 1
+apps:
+ app:
+slots:
+ slot:
+ interface: test
+ key: value
+ label: label
+`
+
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, fmt.Sprintf(anotherConsumerYaml, "def"))
+ s.mockSnap(c, fmt.Sprintf(anotherConsumerYaml, "abc"))
+
+ s.mockSnap(c, producerYaml)
+ s.mockSnap(c, anotherProducerYaml)
+
+ s.testConnectionsConnected(c, "/v2/connections", map[string]interface{}{
+ "consumer:plug producer:slot": map[string]interface{}{
+ "interface": "test",
+ "by-gadget": true,
+ "auto": true,
+ },
+ "another-consumer-def:plug producer:slot": map[string]interface{}{
+ "interface": "test",
+ "by-gadget": true,
+ "auto": true,
+ },
+ "another-consumer-abc:plug producer:slot": map[string]interface{}{
+ "interface": "test",
+ "by-gadget": true,
+ "auto": true,
+ },
+ "another-consumer-def:plug another-producer:slot": map[string]interface{}{
+ "interface": "test",
+ "by-gadget": true,
+ "auto": true,
+ },
+ }, map[string]interface{}{
+ "result": map[string]interface{}{
+ "plugs": []interface{}{
+ map[string]interface{}{
+ "snap": "another-consumer-abc",
+ "plug": "plug",
+ "interface": "test",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app"},
+ "label": "label",
+ "connections": []interface{}{
+ map[string]interface{}{"snap": "producer", "slot": "slot"},
+ },
+ },
+ map[string]interface{}{
+ "snap": "another-consumer-def",
+ "plug": "plug",
+ "interface": "test",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app"},
+ "label": "label",
+ "connections": []interface{}{
+ map[string]interface{}{"snap": "another-producer", "slot": "slot"},
+ map[string]interface{}{"snap": "producer", "slot": "slot"},
+ },
+ },
+ map[string]interface{}{
+ "snap": "consumer",
+ "plug": "plug",
+ "interface": "test",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app"},
+ "label": "label",
+ "connections": []interface{}{
+ map[string]interface{}{"snap": "producer", "slot": "slot"},
+ },
+ },
+ },
+ "slots": []interface{}{
+ map[string]interface{}{
+ "snap": "another-producer",
+ "slot": "slot",
+ "interface": "test",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app"},
+ "label": "label",
+ "connections": []interface{}{
+ map[string]interface{}{"snap": "another-consumer-def", "plug": "plug"},
+ },
+ },
+ map[string]interface{}{
+ "snap": "producer",
+ "slot": "slot",
+ "interface": "test",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app"},
+ "label": "label",
+ "connections": []interface{}{
+ map[string]interface{}{"snap": "another-consumer-abc", "plug": "plug"},
+ map[string]interface{}{"snap": "another-consumer-def", "plug": "plug"},
+ map[string]interface{}{"snap": "consumer", "plug": "plug"},
+ },
+ },
+ },
+ "established": []interface{}{
+ map[string]interface{}{
+ "plug": map[string]interface{}{"snap": "another-consumer-abc", "plug": "plug"},
+ "slot": map[string]interface{}{"snap": "producer", "slot": "slot"},
+ "interface": "test",
+ "gadget": true,
+ },
+ map[string]interface{}{
+ "plug": map[string]interface{}{"snap": "another-consumer-def", "plug": "plug"},
+ "slot": map[string]interface{}{"snap": "another-producer", "slot": "slot"},
+ "interface": "test",
+ "gadget": true,
+ },
+ map[string]interface{}{
+ "plug": map[string]interface{}{"snap": "another-consumer-def", "plug": "plug"},
+ "slot": map[string]interface{}{"snap": "producer", "slot": "slot"},
+ "interface": "test",
+ "gadget": true,
+ },
+ map[string]interface{}{
+ "plug": map[string]interface{}{"snap": "consumer", "plug": "plug"},
+ "slot": map[string]interface{}{"snap": "producer", "slot": "slot"},
+ "interface": "test",
+ "gadget": true,
+ },
+ },
+ },
+ "status": "OK",
+ "status-code": 200.0,
+ "type": "sync",
+ })
+}
diff -pruN 2.37.4-1/daemon/api_debug.go 2.39.2+19.10ubuntu1/daemon/api_debug.go
--- 2.37.4-1/daemon/api_debug.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/daemon/api_debug.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,191 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015-2019 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 .
+ *
+ */
+
+package daemon
+
+import (
+ "encoding/json"
+ "net/http"
+ "sort"
+ "time"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/overlord/assertstate"
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/overlord/devicestate"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/timings"
+)
+
+var debugCmd = &Command{
+ Path: "/v2/debug",
+ UserOK: true,
+ GET: getDebug,
+ POST: postDebug,
+}
+
+type debugAction struct {
+ Action string `json:"action"`
+ Message string `json:"message"`
+ Params struct {
+ ChgID string `json:"chg-id"`
+ } `json:"params"`
+}
+
+type ConnectivityStatus struct {
+ Connectivity bool `json:"connectivity"`
+ Unreachable []string `json:"unreachable,omitempty"`
+}
+
+func getBaseDeclaration(st *state.State) Response {
+ bd, err := assertstate.BaseDeclaration(st)
+ if err != nil {
+ return InternalError("cannot get base declaration: %s", err)
+ }
+ return SyncResponse(map[string]interface{}{
+ "base-declaration": string(asserts.Encode(bd)),
+ }, nil)
+}
+
+func checkConnectivity(st *state.State) Response {
+ theStore := snapstate.Store(st)
+ st.Unlock()
+ defer st.Lock()
+ checkResult, err := theStore.ConnectivityCheck()
+ if err != nil {
+ return InternalError("cannot run connectivity check: %v", err)
+ }
+ status := ConnectivityStatus{Connectivity: true}
+ for host, reachable := range checkResult {
+ if !reachable {
+ status.Connectivity = false
+ status.Unreachable = append(status.Unreachable, host)
+ }
+ }
+ sort.Strings(status.Unreachable)
+
+ return SyncResponse(status, nil)
+}
+
+type changeTimings struct {
+ DoingTime time.Duration `json:"doing-time,omitempty"`
+ UndoingTime time.Duration `json:"undoing-time,omitempty"`
+ DoingTimings []*timings.TimingJSON `json:"doing-timings,omitempty"`
+ UndoingTimings []*timings.TimingJSON `json:"undoing-timings,omitempty"`
+}
+
+func getChangeTimings(st *state.State, changeID string) Response {
+ chg := st.Change(changeID)
+ if chg == nil {
+ return BadRequest("cannot find change: %v", changeID)
+ }
+
+ doingTimingsByTask := make(map[string][]*timings.TimingJSON)
+ undoingTimingsByTask := make(map[string][]*timings.TimingJSON)
+
+ // collect "timings" for tasks of given change
+ stateTimings, err := timings.Get(st, -1, func(tags map[string]string) bool { return tags["change-id"] == changeID })
+ if err != nil {
+ return InternalError("cannot get timings of change %s: %v", changeID, err)
+ }
+ for _, tm := range stateTimings {
+ taskID := tm.Tags["task-id"]
+ if status, ok := tm.Tags["task-status"]; ok {
+ switch {
+ case status == state.DoingStatus.String():
+ doingTimingsByTask[taskID] = tm.NestedTimings
+ case status == state.UndoingStatus.String():
+ undoingTimingsByTask[taskID] = tm.NestedTimings
+ default:
+ return InternalError("unexpected task status %q for timing of task %s", status, taskID)
+ }
+ }
+ }
+
+ m := map[string]*changeTimings{}
+ for _, t := range chg.Tasks() {
+ m[t.ID()] = &changeTimings{
+ DoingTime: t.DoingTime(),
+ UndoingTime: t.UndoingTime(),
+ DoingTimings: doingTimingsByTask[t.ID()],
+ UndoingTimings: undoingTimingsByTask[t.ID()],
+ }
+ }
+ return SyncResponse(m, nil)
+}
+
+func getDebug(c *Command, r *http.Request, user *auth.UserState) Response {
+ query := r.URL.Query()
+ aspect := query.Get("aspect")
+ st := c.d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+ switch aspect {
+ case "base-declaration":
+ return getBaseDeclaration(st)
+ case "connectivity":
+ return checkConnectivity(st)
+ case "model":
+ model, err := devicestate.Model(st)
+ if err != nil {
+ return InternalError("cannot get model: %v", err)
+ }
+ return SyncResponse(map[string]interface{}{
+ "model": string(asserts.Encode(model)),
+ }, nil)
+ case "change-timings":
+ chgID := query.Get("change-id")
+ return getChangeTimings(st, chgID)
+ default:
+ return BadRequest("unknown debug aspect %q", aspect)
+ }
+}
+
+func postDebug(c *Command, r *http.Request, user *auth.UserState) Response {
+ var a debugAction
+ decoder := json.NewDecoder(r.Body)
+ if err := decoder.Decode(&a); err != nil {
+ return BadRequest("cannot decode request body into a debug action: %v", err)
+ }
+
+ st := c.d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+
+ switch a.Action {
+ case "add-warning":
+ st.Warnf("%v", a.Message)
+ return SyncResponse(true, nil)
+ case "unshow-warnings":
+ st.UnshowAllWarnings()
+ return SyncResponse(true, nil)
+ case "ensure-state-soon":
+ ensureStateSoon(st)
+ return SyncResponse(true, nil)
+ case "get-base-declaration":
+ return getBaseDeclaration(st)
+ case "can-manage-refreshes":
+ return SyncResponse(devicestate.CanManageRefreshes(st), nil)
+ case "connectivity":
+ return checkConnectivity(st)
+ default:
+ return BadRequest("unknown debug action: %v", a.Action)
+ }
+}
diff -pruN 2.37.4-1/daemon/api_debug_test.go 2.39.2+19.10ubuntu1/daemon/api_debug_test.go
--- 2.37.4-1/daemon/api_debug_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/daemon/api_debug_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,156 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2019 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 .
+ *
+ */
+
+package daemon
+
+import (
+ "bytes"
+ "net/http"
+
+ "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/testutil"
+)
+
+var _ = check.Suite(&postDebugSuite{})
+
+type postDebugSuite struct {
+ apiBaseSuite
+}
+
+func (s *postDebugSuite) TestPostDebugEnsureStateSoon(c *check.C) {
+ s.daemonWithOverlordMock(c)
+
+ soon := 0
+ ensureStateSoon = func(st *state.State) {
+ soon++
+ ensureStateSoonImpl(st)
+ }
+
+ buf := bytes.NewBufferString(`{"action": "ensure-state-soon"}`)
+ req, err := http.NewRequest("POST", "/v2/debug", buf)
+ c.Assert(err, check.IsNil)
+
+ rsp := postDebug(debugCmd, req, nil).(*resp)
+
+ c.Check(rsp.Type, check.Equals, ResponseTypeSync)
+ c.Check(rsp.Result, check.Equals, true)
+ c.Check(soon, check.Equals, 1)
+}
+
+func (s *postDebugSuite) TestPostDebugGetBaseDeclaration(c *check.C) {
+ _ = s.daemon(c)
+
+ buf := bytes.NewBufferString(`{"action": "get-base-declaration"}`)
+ req, err := http.NewRequest("POST", "/v2/debug", buf)
+ c.Assert(err, check.IsNil)
+
+ rsp := postDebug(debugCmd, req, nil).(*resp)
+
+ c.Check(rsp.Type, check.Equals, ResponseTypeSync)
+ c.Check(rsp.Result.(map[string]interface{})["base-declaration"],
+ testutil.Contains, "type: base-declaration")
+}
+
+func (s *postDebugSuite) testDebugConnectivityHappy(c *check.C, post bool) {
+ _ = s.daemon(c)
+
+ s.connectivityResult = map[string]bool{
+ "good.host.com": true,
+ "another.good.host.com": true,
+ }
+
+ var rsp *resp
+ if post {
+ buf := bytes.NewBufferString(`{"action": "connectivity"}`)
+ req, err := http.NewRequest("POST", "/v2/debug", buf)
+ c.Assert(err, check.IsNil)
+
+ rsp = postDebug(debugCmd, req, nil).(*resp)
+ } else {
+ req, err := http.NewRequest("POST", "/v2/debug?aspect=connectivity", nil)
+ c.Assert(err, check.IsNil)
+ rsp = getDebug(debugCmd, req, nil).(*resp)
+
+ }
+
+ c.Check(rsp.Type, check.Equals, ResponseTypeSync)
+ c.Check(rsp.Result, check.DeepEquals, ConnectivityStatus{
+ Connectivity: true,
+ Unreachable: []string(nil),
+ })
+}
+
+func (s *postDebugSuite) TestPostDebugConnectivityHappy(c *check.C) {
+ s.testDebugConnectivityHappy(c, true)
+}
+
+func (s *postDebugSuite) TestGetDebugConnectivityHappy(c *check.C) {
+ s.testDebugConnectivityHappy(c, false)
+}
+
+func (s *postDebugSuite) testDebugConnectivityUnhappy(c *check.C, post bool) {
+ _ = s.daemon(c)
+
+ s.connectivityResult = map[string]bool{
+ "good.host.com": true,
+ "bad.host.com": false,
+ }
+
+ var rsp *resp
+ if post {
+ buf := bytes.NewBufferString(`{"action": "connectivity"}`)
+ req, err := http.NewRequest("POST", "/v2/debug", buf)
+ c.Assert(err, check.IsNil)
+
+ rsp = postDebug(debugCmd, req, nil).(*resp)
+ } else {
+ req, err := http.NewRequest("GET", "/v2/debug?aspect=connectivity", nil)
+ c.Assert(err, check.IsNil)
+ rsp = getDebug(debugCmd, req, nil).(*resp)
+ }
+
+ c.Check(rsp.Type, check.Equals, ResponseTypeSync)
+ c.Check(rsp.Result, check.DeepEquals, ConnectivityStatus{
+ Connectivity: false,
+ Unreachable: []string{"bad.host.com"},
+ })
+}
+
+func (s *postDebugSuite) TestPostDebugConnectivityUnhappy(c *check.C) {
+ s.testDebugConnectivityUnhappy(c, true)
+}
+
+func (s *postDebugSuite) TestGetDebugConnectivityUnhappy(c *check.C) {
+ s.testDebugConnectivityUnhappy(c, false)
+}
+
+func (s *postDebugSuite) TestGetDebugBaseDeclaration(c *check.C) {
+ _ = s.daemon(c)
+
+ req, err := http.NewRequest("GET", "/v2/debug?aspect=base-declaration", nil)
+ c.Assert(err, check.IsNil)
+
+ rsp := getDebug(debugCmd, req, nil).(*resp)
+
+ c.Check(rsp.Type, check.Equals, ResponseTypeSync)
+ c.Check(rsp.Result.(map[string]interface{})["base-declaration"],
+ testutil.Contains, "type: base-declaration")
+}
diff -pruN 2.37.4-1/daemon/api.go 2.39.2+19.10ubuntu1/daemon/api.go
--- 2.37.4-1/daemon/api.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/daemon/api.go 2019-06-05 06:41:21.000000000 +0000
@@ -20,6 +20,7 @@
package daemon
import (
+ "context"
"encoding/json"
"errors"
"fmt"
@@ -41,7 +42,6 @@ import (
"github.com/gorilla/mux"
"github.com/jessevdk/go-flags"
- "golang.org/x/net/context"
"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/asserts/snapasserts"
@@ -58,7 +58,6 @@ import (
"github.com/snapcore/snapd/overlord/auth"
"github.com/snapcore/snapd/overlord/configstate"
"github.com/snapcore/snapd/overlord/configstate/config"
- "github.com/snapcore/snapd/overlord/configstate/proxyconf"
"github.com/snapcore/snapd/overlord/devicestate"
"github.com/snapcore/snapd/overlord/hookstate/ctlcmd"
"github.com/snapcore/snapd/overlord/ifacestate"
@@ -83,6 +82,7 @@ var api = []*Command{
findCmd,
snapsCmd,
snapCmd,
+ snapFileCmd,
snapConfCmd,
interfacesCmd,
assertsCmd,
@@ -101,6 +101,8 @@ var api = []*Command{
warningsCmd,
debugCmd,
snapshotCmd,
+ connectionsCmd,
+ modelCmd,
}
var (
@@ -183,20 +185,6 @@ var (
POST: changeInterfaces,
}
- // TODO: allow to post assertions for UserOK? they are verified anyway
- assertsCmd = &Command{
- Path: "/v2/assertions",
- UserOK: true,
- GET: getAssertTypeNames,
- POST: doAssert,
- }
-
- assertsFindManyCmd = &Command{
- Path: "/v2/assertions/{assertType}",
- UserOK: true,
- GET: assertsFindMany,
- }
-
stateChangeCmd = &Command{
Path: "/v2/changes/{id}",
UserOK: true,
@@ -211,11 +199,6 @@ var (
GET: getChanges,
}
- debugCmd = &Command{
- Path: "/v2/debug",
- POST: postDebug,
- }
-
createUserCmd = &Command{
Path: "/v2/create-user",
POST: postCreateUser,
@@ -635,16 +618,38 @@ func searchStore(c *Command, r *http.Req
}
query := r.URL.Query()
q := query.Get("q")
+ commonID := query.Get("common-id")
section := query.Get("section")
name := query.Get("name")
scope := query.Get("scope")
private := false
prefix := false
+ if sel := query.Get("select"); sel != "" {
+ switch sel {
+ case "refresh":
+ if commonID != "" {
+ return BadRequest("cannot use 'common-id' with 'select=refresh'")
+ }
+ if name != "" {
+ return BadRequest("cannot use 'name' with 'select=refresh'")
+ }
+ if q != "" {
+ return BadRequest("cannot use 'q' with 'select=refresh'")
+ }
+ return storeUpdates(c, r, user)
+ case "private":
+ private = true
+ }
+ }
+
if name != "" {
if q != "" {
return BadRequest("cannot use 'q' and 'name' together")
}
+ if commonID != "" {
+ return BadRequest("cannot use 'common-id' and 'name' together")
+ }
if name[len(name)-1] != '*' {
return findOne(c, r, user, name)
@@ -654,28 +659,18 @@ func searchStore(c *Command, r *http.Req
q = name[:len(name)-1]
}
- if sel := query.Get("select"); sel != "" {
- switch sel {
- case "refresh":
- if prefix {
- return BadRequest("cannot use 'name' with 'select=refresh'")
- }
- if q != "" {
- return BadRequest("cannot use 'q' with 'select=refresh'")
- }
- return storeUpdates(c, r, user)
- case "private":
- private = true
- }
+ if commonID != "" && q != "" {
+ return BadRequest("cannot use 'common-id' and 'q' together")
}
theStore := getStore(c)
found, err := theStore.Find(&store.Search{
- Query: q,
- Section: section,
- Private: private,
- Prefix: prefix,
- Scope: scope,
+ Query: q,
+ Prefix: prefix,
+ CommonID: commonID,
+ Section: section,
+ Private: private,
+ Scope: scope,
}, user)
switch err {
case nil:
@@ -707,6 +702,13 @@ func searchStore(c *Command, r *http.Req
Status: 400,
}, nil)
}
+ if e, ok := err.(*httputil.PerstistentNetworkError); ok {
+ return SyncResponse(&resp{
+ Type: ResponseTypeError,
+ Result: &errorResult{Message: e.Error(), Kind: errorKindDNSFailure},
+ Status: 400,
+ }, nil)
+ }
return InternalError("%v", err)
}
@@ -1387,6 +1389,9 @@ func snapsOp(c *Command, r *http.Request
return BadRequest("cannot decode request body into snap instruction: %v", err)
}
+ if err := verifySnapInstructions(&inst); err != nil {
+ return BadRequest("%v", err)
+ }
if inst.Channel != "" || !inst.Revision.Unset() || inst.DevMode || inst.JailMode {
return BadRequest("unsupported option provided for multi-snap operation")
}
@@ -1618,21 +1623,29 @@ func unsafeReadSnapInfoImpl(snapPath str
var unsafeReadSnapInfo = unsafeReadSnapInfoImpl
func iconGet(st *state.State, name string) Response {
- about, err := localSnapInfo(st, name)
+ st.Lock()
+ defer st.Unlock()
+
+ var snapst snapstate.SnapState
+ err := snapstate.Get(st, name, &snapst)
if err != nil {
- if err == errNoSnap {
+ if err == state.ErrNoState {
return SnapNotFound(name, err)
}
- return InternalError("%v", err)
+ return InternalError("cannot consult state: %v", err)
}
+ sideInfo := snapst.CurrentSideInfo()
+ if sideInfo == nil {
+ return NotFound("snap has no current revision")
+ }
+
+ icon := snapIcon(snap.MinimalPlaceInfo(name, sideInfo.Revision))
- path := filepath.Clean(snapIcon(about.info))
- if !strings.HasPrefix(path, dirs.SnapMountDir) {
- // XXX: how could this happen?
- return BadRequest("requested icon is not in snap path")
+ if icon == "" {
+ return NotFound("local snap has no icon")
}
- return FileResponse(path)
+ return FileResponse(icon)
}
func appIconGet(c *Command, r *http.Request, user *auth.UserState) Response {
@@ -1789,57 +1802,15 @@ func getInterfaces(c *Command, r *http.R
}
func getLegacyConnections(c *Command, r *http.Request, user *auth.UserState) Response {
- repo := c.d.overlord.InterfaceManager().Repository()
- ifaces := repo.Interfaces()
-
- var ifjson interfaceJSON
- plugConns := map[string][]interfaces.SlotRef{}
- slotConns := map[string][]interfaces.PlugRef{}
-
- for _, cref := range ifaces.Connections {
- plugRef := interfaces.PlugRef{Snap: cref.PlugRef.Snap, Name: cref.PlugRef.Name}
- slotRef := interfaces.SlotRef{Snap: cref.SlotRef.Snap, Name: cref.SlotRef.Name}
- plugID := plugRef.String()
- slotID := slotRef.String()
- plugConns[plugID] = append(plugConns[plugID], slotRef)
- slotConns[slotID] = append(slotConns[slotID], plugRef)
- }
-
- for _, plug := range ifaces.Plugs {
- var apps []string
- for _, app := range plug.Apps {
- apps = append(apps, app.Name)
- }
- plugRef := interfaces.PlugRef{Snap: plug.Snap.InstanceName(), Name: plug.Name}
- pj := &plugJSON{
- Snap: plugRef.Snap,
- Name: plugRef.Name,
- Interface: plug.Interface,
- Attrs: plug.Attrs,
- Apps: apps,
- Label: plug.Label,
- Connections: plugConns[plugRef.String()],
- }
- ifjson.Plugs = append(ifjson.Plugs, pj)
- }
- for _, slot := range ifaces.Slots {
- var apps []string
- for _, app := range slot.Apps {
- apps = append(apps, app.Name)
- }
- slotRef := interfaces.SlotRef{Snap: slot.Snap.InstanceName(), Name: slot.Name}
- sj := &slotJSON{
- Snap: slotRef.Snap,
- Name: slotRef.Name,
- Interface: slot.Interface,
- Attrs: slot.Attrs,
- Apps: apps,
- Label: slot.Label,
- Connections: slotConns[slotRef.String()],
- }
- ifjson.Slots = append(ifjson.Slots, sj)
+ connsjson, err := collectConnections(c.d.overlord.InterfaceManager(), collectFilter{})
+ if err != nil {
+ return InternalError("collecting connection information failed: %v", err)
+ }
+ legacyconnsjson := legacyConnectionsJSON{
+ Plugs: connsjson.Plugs,
+ Slots: connsjson.Slots,
}
- return SyncResponse(ifjson, nil)
+ return SyncResponse(legacyconnsjson, nil)
}
func snapNamesFromConns(conns []*interfaces.ConnRef) []string {
@@ -1922,7 +1893,8 @@ func changeInterfaces(c *Command, r *htt
}
for _, connRef := range conns {
var ts *state.TaskSet
- conn, err := repo.Connection(connRef)
+ var conn *interfaces.Connection
+ conn, err = repo.Connection(connRef)
if err != nil {
break
}
@@ -1946,59 +1918,6 @@ func changeInterfaces(c *Command, r *htt
return AsyncResponse(nil, &Meta{Change: change.ID()})
}
-func getAssertTypeNames(c *Command, r *http.Request, user *auth.UserState) Response {
- return SyncResponse(map[string][]string{
- "types": asserts.TypeNames(),
- }, nil)
-}
-
-func doAssert(c *Command, r *http.Request, user *auth.UserState) Response {
- batch := assertstate.NewBatch()
- _, err := batch.AddStream(r.Body)
- if err != nil {
- return BadRequest("cannot decode request body into assertions: %v", err)
- }
-
- state := c.d.overlord.State()
- state.Lock()
- defer state.Unlock()
-
- if err := batch.Commit(state); err != nil {
- return BadRequest("assert failed: %v", err)
- }
- // TODO: what more info do we want to return on success?
- return &resp{
- Type: ResponseTypeSync,
- Status: 200,
- }
-}
-
-func assertsFindMany(c *Command, r *http.Request, user *auth.UserState) Response {
- assertTypeName := muxVars(r)["assertType"]
- assertType := asserts.Type(assertTypeName)
- if assertType == nil {
- return BadRequest("invalid assert type: %q", assertTypeName)
- }
- headers := map[string]string{}
- q := r.URL.Query()
- for k := range q {
- headers[k] = q.Get(k)
- }
-
- state := c.d.overlord.State()
- state.Lock()
- db := assertstate.DB(state)
- state.Unlock()
-
- assertions, err := db.FindMany(assertType, headers)
- if asserts.IsNotFound(err) {
- return AssertResponse(nil, true)
- } else if err != nil {
- return InternalError("searching assertions failed: %v", err)
- }
- return AssertResponse(assertions, true)
-}
-
type changeInfo struct {
ID string `json:"id"`
Kind string `json:"kind"`
@@ -2196,15 +2115,6 @@ var (
osutilAddUser = osutil.AddUser
)
-func makeHttpClient(st *state.State) *http.Client {
- proxyConf := proxyconf.New(st)
- return httputil.NewHTTPClient(&httputil.ClientOptions{
- Timeout: 10 * time.Second,
- MayLogBody: true,
- Proxy: proxyConf.Conf,
- })
-}
-
func getUserDetailsFromStore(theStore snapstate.StoreService, email string) (string, *osutil.AddUserOptions, error) {
v, err := theStore.UserInfo(email)
if err != nil {
@@ -2500,70 +2410,6 @@ func convertBuyError(err error) Response
}
}
-type debugAction struct {
- Action string `json:"action"`
- Message string `json:"message"`
-}
-
-type ConnectivityStatus struct {
- Connectivity bool `json:"connectivity"`
- Unreachable []string `json:"unreachable,omitempty"`
-}
-
-func postDebug(c *Command, r *http.Request, user *auth.UserState) Response {
- var a debugAction
- decoder := json.NewDecoder(r.Body)
- if err := decoder.Decode(&a); err != nil {
- return BadRequest("cannot decode request body into a debug action: %v", err)
- }
-
- st := c.d.overlord.State()
- st.Lock()
- defer st.Unlock()
-
- switch a.Action {
- case "add-warning":
- st.Warnf("%v", a.Message)
- return SyncResponse(true, nil)
- case "unshow-warnings":
- st.UnshowAllWarnings()
- return SyncResponse(true, nil)
- case "ensure-state-soon":
- ensureStateSoon(st)
- return SyncResponse(true, nil)
- case "get-base-declaration":
- bd, err := assertstate.BaseDeclaration(st)
- if err != nil {
- return InternalError("cannot get base declaration: %s", err)
- }
- return SyncResponse(map[string]interface{}{
- "base-declaration": string(asserts.Encode(bd)),
- }, nil)
- case "can-manage-refreshes":
- return SyncResponse(devicestate.CanManageRefreshes(st), nil)
- case "connectivity":
- s := snapstate.Store(st)
- st.Unlock()
- checkResult, err := s.ConnectivityCheck()
- st.Lock()
- if err != nil {
- return InternalError("cannot run connectivity check: %v", err)
- }
- status := ConnectivityStatus{Connectivity: true}
- for host, reachable := range checkResult {
- if !reachable {
- status.Connectivity = false
- status.Unreachable = append(status.Unreachable, host)
- }
- }
- sort.Strings(status.Unreachable)
-
- return SyncResponse(status, nil)
- default:
- return BadRequest("unknown debug action: %v", a.Action)
- }
-}
-
func postBuy(c *Command, r *http.Request, user *auth.UserState) Response {
var opts client.BuyOptions
diff -pruN 2.37.4-1/daemon/api_json.go 2.39.2+19.10ubuntu1/daemon/api_json.go
--- 2.37.4-1/daemon/api_json.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/daemon/api_json.go 2019-06-05 06:41:21.000000000 +0000
@@ -62,3 +62,29 @@ type interfaceAction struct {
Plugs []plugJSON `json:"plugs,omitempty"`
Slots []slotJSON `json:"slots,omitempty"`
}
+
+// connectionsJSON aids in marshalling information about a single connection
+// into JSON
+type connectionJSON struct {
+ Slot interfaces.SlotRef `json:"slot"`
+ Plug interfaces.PlugRef `json:"plug"`
+ Interface string `json:"interface"`
+ Manual bool `json:"manual,omitempty"`
+ Gadget bool `json:"gadget,omitempty"`
+ SlotAttrs map[string]interface{} `json:"slot-attrs,omitempty"`
+ PlugAttrs map[string]interface{} `json:"plug-attrs,omitempty"`
+}
+
+// legacyConnectionsJSON aids in marshaling legacy connections into JSON.
+type legacyConnectionsJSON struct {
+ Plugs []*plugJSON `json:"plugs,omitempty"`
+ Slots []*slotJSON `json:"slots,omitempty"`
+}
+
+// connectionsJSON aids in marshaling connections into JSON.
+type connectionsJSON struct {
+ Established []connectionJSON `json:"established"`
+ Undesired []connectionJSON `json:"undesired,omitempty"`
+ Plugs []*plugJSON `json:"plugs"`
+ Slots []*slotJSON `json:"slots"`
+}
diff -pruN 2.37.4-1/daemon/api_model.go 2.39.2+19.10ubuntu1/daemon/api_model.go
--- 2.37.4-1/daemon/api_model.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/daemon/api_model.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,86 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package daemon
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/i18n"
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/overlord/devicestate"
+)
+
+var modelCmd = &Command{
+ Path: "/v2/model",
+ POST: postModel,
+ // TODO: provide GET here too once we decided on the details of the API
+}
+
+var devicestateRemodel = devicestate.Remodel
+
+type postModelData struct {
+ NewModel string `json:"new-model"`
+}
+
+func postModel(c *Command, r *http.Request, _ *auth.UserState) Response {
+ defer r.Body.Close()
+ var data postModelData
+ decoder := json.NewDecoder(r.Body)
+ if err := decoder.Decode(&data); err != nil {
+ return BadRequest("cannot decode request body into remodel operation: %v", err)
+ }
+ rawNewModel, err := asserts.Decode([]byte(data.NewModel))
+ if err != nil {
+ return BadRequest("cannot decode new model assertion: %v", err)
+ }
+ newModel, ok := rawNewModel.(*asserts.Model)
+ if !ok {
+ return BadRequest("new model is not a model assertion: %v", newModel.Type())
+ }
+
+ st := c.d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+
+ tss, err := devicestateRemodel(st, newModel)
+ if err != nil {
+ return BadRequest("cannot remodel device: %v", err)
+ }
+ model, err := devicestate.Model(st)
+ if err != nil {
+ return InternalError("cannot get model: %v", err)
+ }
+
+ var msg string
+ if model.BrandID() == newModel.BrandID() && model.Model() == newModel.Model() {
+ msg = fmt.Sprintf(i18n.G("Refresh model assertion from revision %v to %v"), model.Revision(), newModel.Revision())
+ } else {
+ msg = fmt.Sprintf(i18n.G("Remodel device to %v/%v (%v)"), newModel.BrandID(), newModel.Model(), newModel.Revision())
+ }
+ chg := newChange(st, "remodel", msg, tss, nil)
+
+ ensureStateSoon(st)
+
+ return AsyncResponse(nil, &Meta{Change: chg.ID()})
+
+}
diff -pruN 2.37.4-1/daemon/api_model_test.go 2.39.2+19.10ubuntu1/daemon/api_model_test.go
--- 2.37.4-1/daemon/api_model_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/daemon/api_model_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,104 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package daemon
+
+import (
+ "bytes"
+ "encoding/json"
+ "net/http"
+ "time"
+
+ "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/overlord/state"
+)
+
+func (s *apiSuite) TestPostRemodelUnhappy(c *check.C) {
+ data, err := json.Marshal(postModelData{NewModel: "invalid model"})
+ c.Check(err, check.IsNil)
+
+ req, err := http.NewRequest("POST", "/v2/model", bytes.NewBuffer(data))
+ c.Assert(err, check.IsNil)
+ rsp := postModel(appsCmd, req, nil).(*resp)
+ c.Check(rsp.Type, check.Equals, ResponseTypeError)
+ c.Assert(rsp.Status, check.Equals, 400)
+ c.Check(rsp.Result.(*errorResult).Message, check.Matches, "cannot decode new model assertion: .*")
+}
+
+func (s *apiSuite) testPostRemodel(c *check.C, newModel map[string]interface{}, expectedChgSummary string) {
+ d := s.daemonWithOverlordMock(c)
+ st := d.overlord.State()
+ st.Lock()
+ s.mockModel(c, st)
+ st.Unlock()
+
+ var devicestateRemodelGotModel *asserts.Model
+ devicestateRemodel = func(st *state.State, nm *asserts.Model) ([]*state.TaskSet, error) {
+ devicestateRemodelGotModel = nm
+ return nil, nil
+ }
+
+ // create a valid model assertion
+ mockModel, err := s.storeSigning.RootSigning.Sign(asserts.ModelType, newModel, nil, "")
+ c.Assert(err, check.IsNil)
+ mockModelEncoded := string(asserts.Encode(mockModel))
+ data, err := json.Marshal(postModelData{NewModel: mockModelEncoded})
+ c.Check(err, check.IsNil)
+
+ // set it and validate that this is what we was passed to
+ // devicestateRemodel
+ req, err := http.NewRequest("POST", "/v2/model", bytes.NewBuffer(data))
+ c.Assert(err, check.IsNil)
+ rsp := postModel(appsCmd, req, nil).(*resp)
+ c.Assert(rsp.Status, check.Equals, 202)
+ c.Check(devicestateRemodelGotModel, check.DeepEquals, mockModel)
+
+ st.Lock()
+ defer st.Unlock()
+ chg := st.Change(rsp.Change)
+ c.Check(chg.Summary(), check.Equals, expectedChgSummary)
+}
+
+func (s *apiSuite) TestPostRemodelDifferentBrandModel(c *check.C) {
+ newModel := map[string]interface{}{
+ "series": "16",
+ "authority-id": "my-brand",
+ "brand-id": "my-brand",
+ "model": "my-model",
+ "architecture": "amd64",
+ "gadget": "pc",
+ "kernel": "pc-kernel",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ expectedChgSummary := "Remodel device to my-brand/my-model (0)"
+ s.testPostRemodel(c, newModel, expectedChgSummary)
+}
+
+func (s *apiSuite) TestPostRemodelSameBrandModelDifferentRev(c *check.C) {
+ newModel := make(map[string]interface{})
+ for k, v := range makeMockModelHdrs() {
+ newModel[k] = v
+ }
+ newModel["revision"] = "2"
+
+ expectedChgSummary := "Refresh model assertion from revision 0 to 2"
+ s.testPostRemodel(c, newModel, expectedChgSummary)
+}
diff -pruN 2.37.4-1/daemon/api_snap_file.go 2.39.2+19.10ubuntu1/daemon/api_snap_file.go
--- 2.37.4-1/daemon/api_snap_file.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/daemon/api_snap_file.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,68 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package daemon
+
+import (
+ "net/http"
+
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+)
+
+var snapFileCmd = &Command{
+ Path: "/v2/snaps/{name}/file",
+ UserOK: true,
+ PolkitOK: "io.snapcraft.snapd.manage",
+ GET: getSnapFile,
+}
+
+func getSnapFile(c *Command, r *http.Request, user *auth.UserState) Response {
+ vars := muxVars(r)
+ name := vars["name"]
+
+ st := c.d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+
+ var snapst snapstate.SnapState
+ var info *snap.Info
+ err := snapstate.Get(st, name, &snapst)
+ if err == nil {
+ info, err = snapst.CurrentInfo()
+ }
+ switch err {
+ case nil:
+ // ok
+ case state.ErrNoState:
+ return SnapNotFound(name, err)
+ default:
+ return InternalError("cannot download file for snap %q: %v", name, err)
+ }
+ if !snapst.Active {
+ return BadRequest("cannot download file of inactive snap %q", name)
+ }
+ if snapst.TryMode {
+ return BadRequest("cannot download file for try-mode snap %q", name)
+ }
+
+ return FileResponse(info.MountFile())
+}
diff -pruN 2.37.4-1/daemon/api_snap_file_test.go 2.39.2+19.10ubuntu1/daemon/api_snap_file_test.go
--- 2.37.4-1/daemon/api_snap_file_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/daemon/api_snap_file_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,100 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package daemon_test
+
+import (
+ "net/http"
+ "path/filepath"
+
+ "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/daemon"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/overlord"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/snap"
+)
+
+var _ = check.Suite(&snapFileSuite{})
+
+type snapFileSuite struct{}
+
+func (s *snapFileSuite) SetUpTest(c *check.C) {
+ dirs.SetRootDir(c.MkDir())
+}
+
+func (s *snapFileSuite) TestGetFile(c *check.C) {
+ defer daemon.MockMuxVars(func(*http.Request) map[string]string {
+ return map[string]string{"name": "foo"}
+ })()
+
+ c.Check(daemon.SnapFileCmd.Path, check.Equals, "/v2/snaps/{name}/file")
+
+ o := overlord.Mock()
+ daemon.NewWithOverlord(o)
+ st := o.State()
+
+ type scenario struct {
+ status int
+ exists, active, try, wat bool
+ err string
+ }
+
+ req, err := http.NewRequest("GET", "/v2/snaps/foo/file", nil)
+ c.Assert(err, check.IsNil)
+
+ for i, scen := range []scenario{
+ {exists: true, active: true},
+ {exists: false, err: "no state entry for key"},
+ {exists: true, active: false, err: `cannot download file of inactive snap "foo"`},
+ {exists: true, active: true, try: true, err: `cannot download file for try-mode snap "foo"`},
+ {exists: true, wat: true, err: `cannot download file for snap "foo": internal error: .*`},
+ } {
+ var snapst snapstate.SnapState
+ if scen.wat {
+ st.Lock()
+ st.Set("snaps", 42)
+ st.Unlock()
+ } else {
+ if scen.exists {
+ sideInfo := &snap.SideInfo{Revision: snap.R(-1), RealName: "foo"}
+ snapst.Active = scen.active
+ snapst.Current = sideInfo.Revision
+ snapst.Sequence = append(snapst.Sequence, sideInfo)
+ if scen.try {
+ snapst.TryMode = true
+ }
+ }
+ st.Lock()
+ snapstate.Set(st, "foo", &snapst)
+ st.Unlock()
+ }
+
+ rsp := daemon.GetSnapFile(daemon.SnapFileCmd, req, nil)
+ if scen.err == "" {
+ c.Check(string(rsp.(daemon.FileResponse)), check.Equals, filepath.Join(dirs.SnapBlobDir, "foo_x1.snap"), check.Commentf("%d", i))
+ } else {
+ c.Assert(rsp, check.FitsTypeOf, &daemon.Resp{}, check.Commentf("%d", i))
+ result := rsp.(*daemon.Resp).Result
+ c.Assert(result, check.FitsTypeOf, &daemon.ErrorResult{}, check.Commentf("%d", i))
+ c.Check(result.(*daemon.ErrorResult).Message, check.Matches, scen.err, check.Commentf("%d", i))
+ }
+ }
+}
diff -pruN 2.37.4-1/daemon/api_snapshots.go 2.39.2+19.10ubuntu1/daemon/api_snapshots.go
--- 2.37.4-1/daemon/api_snapshots.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/daemon/api_snapshots.go 2019-06-05 06:41:21.000000000 +0000
@@ -20,14 +20,13 @@
package daemon
import (
+ "context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
- "golang.org/x/net/context"
-
"github.com/snapcore/snapd/client"
"github.com/snapcore/snapd/overlord/auth"
"github.com/snapcore/snapd/overlord/state"
diff -pruN 2.37.4-1/daemon/api_snapshots_test.go 2.39.2+19.10ubuntu1/daemon/api_snapshots_test.go
--- 2.37.4-1/daemon/api_snapshots_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/daemon/api_snapshots_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -20,16 +20,17 @@
package daemon_test
import (
+ "context"
"errors"
"fmt"
"net/http"
"strings"
- "golang.org/x/net/context"
"gopkg.in/check.v1"
"github.com/snapcore/snapd/client"
"github.com/snapcore/snapd/daemon"
+ "github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/overlord"
"github.com/snapcore/snapd/overlord/assertstate"
"github.com/snapcore/snapd/overlord/snapstate"
@@ -54,6 +55,7 @@ func (s *snapshotSuite) SetUpTest(c *che
st.Lock()
defer st.Unlock()
snapstate.ReplaceStore(st, storetest.Store{})
+ dirs.SetRootDir(c.MkDir())
}
func (s *snapshotSuite) TearDownTest(c *check.C) {
diff -pruN 2.37.4-1/daemon/api_test.go 2.39.2+19.10ubuntu1/daemon/api_test.go
--- 2.37.4-1/daemon/api_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/daemon/api_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -21,6 +21,7 @@ package daemon
import (
"bytes"
+ "context"
"crypto"
"encoding/json"
"errors"
@@ -42,7 +43,6 @@ import (
"time"
"golang.org/x/crypto/sha3"
- "golang.org/x/net/context"
"gopkg.in/check.v1"
"gopkg.in/tomb.v2"
@@ -52,6 +52,7 @@ import (
"github.com/snapcore/snapd/asserts/assertstest"
"github.com/snapcore/snapd/asserts/sysdb"
"github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/cmd"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/interfaces"
"github.com/snapcore/snapd/interfaces/builtin"
@@ -296,6 +297,8 @@ func (s *apiBaseSuite) SetUpTest(c *chec
snapstateTryPath = nil
snapstateUpdate = nil
snapstateUpdateMany = nil
+
+ devicestateRemodel = nil
}
func (s *apiBaseSuite) TearDownTest(c *check.C) {
@@ -319,23 +322,8 @@ func (s *apiBaseSuite) TearDownTest(c *c
snapstateUpdateMany = snapstate.UpdateMany
}
-func (s *apiBaseSuite) daemon(c *check.C) *Daemon {
- if s.d != nil {
- panic("called daemon() twice")
- }
- d, err := New()
- c.Assert(err, check.IsNil)
- d.addRoutes()
-
- st := d.overlord.State()
- st.Lock()
- defer st.Unlock()
- snapstate.ReplaceStore(st, s)
- // mark as already seeded
- st.Set("seeded", true)
- // registered
- // realistic model setup
- modelHdrs := map[string]interface{}{
+func makeMockModelHdrs() map[string]interface{} {
+ return map[string]interface{}{
"type": "model",
"authority-id": "can0nical",
"series": "16",
@@ -346,7 +334,11 @@ func (s *apiBaseSuite) daemon(c *check.C
"kernel": "kernel",
"timestamp": time.Now().Format(time.RFC3339),
}
- a, err := s.storeSigning.RootSigning.Sign(asserts.ModelType, modelHdrs, nil, "")
+}
+
+func (s *apiBaseSuite) mockModel(c *check.C, st *state.State) {
+ // realistic model setup
+ a, err := s.storeSigning.RootSigning.Sign(asserts.ModelType, makeMockModelHdrs(), nil, "")
c.Assert(err, check.IsNil)
model := a.(*asserts.Model)
@@ -360,6 +352,24 @@ func (s *apiBaseSuite) daemon(c *check.C
Model: "pc",
Serial: "serialserial",
})
+}
+
+func (s *apiBaseSuite) daemon(c *check.C) *Daemon {
+ if s.d != nil {
+ panic("called daemon() twice")
+ }
+ d, err := New()
+ c.Assert(err, check.IsNil)
+ d.addRoutes()
+
+ st := d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+ snapstate.ReplaceStore(st, s)
+ // mark as already seeded
+ st.Set("seeded", true)
+ // registered
+ s.mockModel(c, st)
// don't actually try to talk to the store on snapstate.Ensure
// needs doing after the call to devicestate.Manager (which
@@ -427,10 +437,6 @@ func (s *apiBaseSuite) waitTrivialChange
c.Assert(chg.IsReady(), check.Equals, true)
}
-func (s *apiBaseSuite) mkInstalled(c *check.C, name, developer, version string, revision snap.Revision, active bool, extraYaml string) *snap.Info {
- return s.mkInstalledInState(c, nil, name, developer, version, revision, active, extraYaml)
-}
-
func (s *apiBaseSuite) mkInstalledDesktopFile(c *check.C, name, content string) string {
df := filepath.Join(dirs.SnapDesktopFilesDir, name)
err := os.MkdirAll(filepath.Dir(df), 0755)
@@ -443,7 +449,14 @@ func (s *apiBaseSuite) mkInstalledDeskto
func (s *apiBaseSuite) mkInstalledInState(c *check.C, daemon *Daemon, instanceName, developer, version string, revision snap.Revision, active bool, extraYaml string) *snap.Info {
snapName, instanceKey := snap.SplitInstanceName(instanceName)
- snapID := snapName + "-id"
+ if revision.Local() && developer != "" {
+ panic("not supported")
+ }
+
+ var snapID string
+ if revision.Store() {
+ snapID = snapName + "-id"
+ }
// Collect arguments into a snap.SideInfo structure
sideInfo := &snap.SideInfo{
SnapID: snapID,
@@ -472,76 +485,77 @@ version: %s
c.Assert(os.MkdirAll(guidir, 0755), check.IsNil)
c.Check(ioutil.WriteFile(filepath.Join(guidir, "icon.svg"), []byte("yadda icon"), 0644), check.IsNil)
- if daemon != nil {
- st := daemon.overlord.State()
- st.Lock()
- defer st.Unlock()
+ if daemon == nil {
+ return snapInfo
+ }
+ st := daemon.overlord.State()
+ st.Lock()
+ defer st.Unlock()
- err := assertstate.Add(st, s.storeSigning.StoreAccountKey(""))
- if _, ok := err.(*asserts.RevisionError); !ok {
- c.Assert(err, check.IsNil)
- }
+ var snapst snapstate.SnapState
+ snapstate.Get(st, instanceName, &snapst)
+ snapst.Active = active
+ snapst.Sequence = append(snapst.Sequence, &snapInfo.SideInfo)
+ snapst.Current = snapInfo.SideInfo.Revision
+ snapst.Channel = "stable"
+ snapst.InstanceKey = instanceKey
- devAcct := assertstest.NewAccount(s.storeSigning, developer, map[string]interface{}{
- "account-id": developer + "-id",
- }, "")
- err = assertstate.Add(st, devAcct)
- if _, ok := err.(*asserts.RevisionError); !ok {
- c.Assert(err, check.IsNil)
- }
+ snapstate.Set(st, instanceName, &snapst)
- snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{
- "series": "16",
- "snap-id": snapID,
- "snap-name": snapName,
- "publisher-id": devAcct.AccountID(),
- "timestamp": time.Now().Format(time.RFC3339),
- }, nil, "")
- c.Assert(err, check.IsNil)
- err = assertstate.Add(st, snapDecl)
- if _, ok := err.(*asserts.RevisionError); !ok {
- c.Assert(err, check.IsNil)
- }
+ if developer == "" {
+ return snapInfo
+ }
- content, err := ioutil.ReadFile(snapInfo.MountFile())
- c.Assert(err, check.IsNil)
- h := sha3.Sum384(content)
- dgst, err := asserts.EncodeDigest(crypto.SHA3_384, h[:])
- c.Assert(err, check.IsNil)
- snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{
- "snap-sha3-384": string(dgst),
- "snap-size": "999",
- "snap-id": snapID,
- "snap-revision": fmt.Sprintf("%s", revision),
- "developer-id": devAcct.AccountID(),
- "timestamp": time.Now().Format(time.RFC3339),
- }, nil, "")
- c.Assert(err, check.IsNil)
- err = assertstate.Add(st, snapRev)
+ err := assertstate.Add(st, s.storeSigning.StoreAccountKey(""))
+ if _, ok := err.(*asserts.RevisionError); !ok {
c.Assert(err, check.IsNil)
+ }
- var snapst snapstate.SnapState
- snapstate.Get(st, instanceName, &snapst)
- snapst.Active = active
- snapst.Sequence = append(snapst.Sequence, &snapInfo.SideInfo)
- snapst.Current = snapInfo.SideInfo.Revision
- snapst.Channel = "stable"
- snapst.InstanceKey = instanceKey
+ devAcct := assertstest.NewAccount(s.storeSigning, developer, map[string]interface{}{
+ "account-id": developer + "-id",
+ }, "")
+ err = assertstate.Add(st, devAcct)
+ if _, ok := err.(*asserts.RevisionError); !ok {
+ c.Assert(err, check.IsNil)
+ }
+ snapInfo.Publisher = snap.StoreAccount{
+ ID: devAcct.AccountID(),
+ Username: devAcct.Username(),
+ DisplayName: devAcct.DisplayName(),
+ Validation: devAcct.Validation(),
+ }
- snapstate.Set(st, instanceName, &snapst)
+ snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{
+ "series": "16",
+ "snap-id": snapID,
+ "snap-name": snapName,
+ "publisher-id": devAcct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, check.IsNil)
+ err = assertstate.Add(st, snapDecl)
+ if _, ok := err.(*asserts.RevisionError); !ok {
+ c.Assert(err, check.IsNil)
}
- return snapInfo
-}
+ content, err := ioutil.ReadFile(snapInfo.MountFile())
+ c.Assert(err, check.IsNil)
+ h := sha3.Sum384(content)
+ dgst, err := asserts.EncodeDigest(crypto.SHA3_384, h[:])
+ c.Assert(err, check.IsNil)
+ snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{
+ "snap-sha3-384": string(dgst),
+ "snap-size": "999",
+ "snap-id": snapID,
+ "snap-revision": fmt.Sprintf("%s", revision),
+ "developer-id": devAcct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, check.IsNil)
+ err = assertstate.Add(st, snapRev)
+ c.Assert(err, check.IsNil)
-func (s *apiBaseSuite) mkGadget(c *check.C, store string) {
- yamlText := fmt.Sprintf(`name: test
-version: 1
-type: gadget
-gadget: {store: {id: %q}}
-`, store)
- snaptest.MockSnap(c, yamlText, &snap.SideInfo{Revision: snap.R(1)})
- c.Assert(os.Symlink("1", filepath.Join(dirs.SnapMountDir, "test", "current")), check.IsNil)
+ return snapInfo
}
type apiSuite struct {
@@ -625,8 +639,8 @@ UnitFileState=potatoes
Id=snap.foo.svc5.service
ActiveState=inactive
UnitFileState=static
-
-Id=snap.foo.svc5.timer
+`),
+ []byte(`Id=snap.foo.svc5.timer
ActiveState=active
UnitFileState=enabled
`),
@@ -634,8 +648,8 @@ UnitFileState=enabled
Id=snap.foo.svc6.service
ActiveState=inactive
UnitFileState=static
-
-Id=snap.foo.svc6.sock.socket
+`),
+ []byte(`Id=snap.foo.svc6.sock.socket
ActiveState=active
UnitFileState=enabled
`),
@@ -643,8 +657,8 @@ UnitFileState=enabled
Id=snap.foo.svc7.service
ActiveState=inactive
UnitFileState=static
-
-Id=snap.foo.svc7.other-sock.socket
+`),
+ []byte(`Id=snap.foo.svc7.other-sock.socket
ActiveState=inactive
UnitFileState=enabled
`),
@@ -770,10 +784,11 @@ UnitFileState=enabled
},
},
},
- Broken: "",
- Contact: "",
- License: "GPL-3.0",
- CommonIDs: []string{"org.foo.cmd"},
+ Broken: "",
+ Contact: "",
+ License: "GPL-3.0",
+ CommonIDs: []string{"org.foo.cmd"},
+ Screenshots: []snap.ScreenshotInfo{},
},
Meta: meta,
}
@@ -828,6 +843,114 @@ func (s *apiSuite) TestSnapInfoIgnoresRe
c.Check(rsp.Result, check.NotNil)
}
+func (s *apiSuite) TestMapLocalFields(c *check.C) {
+ media := snap.MediaInfos{
+ {
+ Type: "screenshot",
+ URL: "https://example.com/shot1.svg",
+ }, {
+ Type: "icon",
+ URL: "https://example.com/icon.png",
+ }, {
+ Type: "screenshot",
+ URL: "https://example.com/shot2.svg",
+ },
+ }
+
+ publisher := snap.StoreAccount{
+ ID: "some-dev-id",
+ Username: "some-dev",
+ DisplayName: "Some Developer",
+ Validation: "poor",
+ }
+ info := &snap.Info{
+ SideInfo: snap.SideInfo{
+ SnapID: "some-snap-id",
+ RealName: "some-snap",
+ EditedTitle: "A Title",
+ EditedSummary: "a summary",
+ EditedDescription: "the\nlong\ndescription",
+ Channel: "bleeding/edge",
+ Contact: "alice@example.com",
+ Revision: snap.R(7),
+ Private: true,
+ },
+ InstanceKey: "instance",
+ Type: "app",
+ Base: "the-base",
+ Version: "v1.0",
+ License: "MIT",
+ Broken: "very",
+ Confinement: "very strict",
+ CommonIDs: []string{"foo", "bar"},
+ Media: media,
+ DownloadInfo: snap.DownloadInfo{
+ Size: 42,
+ Sha3_384: "some-sum",
+ },
+ Publisher: publisher,
+ }
+
+ // make InstallDate work
+ c.Assert(os.MkdirAll(info.MountDir(), 0755), check.IsNil)
+ c.Assert(os.Symlink("7", filepath.Join(info.MountDir(), "..", "current")), check.IsNil)
+
+ info.Apps = map[string]*snap.AppInfo{
+ "foo": {Snap: info, Name: "foo", Command: "foo"},
+ "bar": {Snap: info, Name: "bar", Command: "bar"},
+ }
+ about := aboutSnap{
+ info: info,
+ snapst: &snapstate.SnapState{
+ Active: true,
+ Channel: "flaky/beta",
+ Current: snap.R(7),
+ Flags: snapstate.Flags{
+ IgnoreValidation: true,
+ DevMode: true,
+ JailMode: true,
+ },
+ },
+ }
+
+ expected := &client.Snap{
+ ID: "some-snap-id",
+ Name: "some-snap_instance",
+ Summary: "a summary",
+ Description: "the\nlong\ndescription",
+ Developer: "some-dev",
+ Publisher: &publisher,
+ Icon: "https://example.com/icon.png",
+ Type: "app",
+ Base: "the-base",
+ Version: "v1.0",
+ Revision: snap.R(7),
+ Channel: "bleeding/edge",
+ TrackingChannel: "flaky/beta",
+ InstallDate: info.InstallDate(),
+ InstalledSize: 42,
+ Status: "active",
+ Confinement: "very strict",
+ IgnoreValidation: true,
+ DevMode: true,
+ JailMode: true,
+ Private: true,
+ Broken: "very",
+ Contact: "alice@example.com",
+ Title: "A Title",
+ License: "MIT",
+ CommonIDs: []string{"foo", "bar"},
+ MountedFrom: filepath.Join(dirs.SnapBlobDir, "some-snap_instance_7.snap"),
+ Media: media,
+ Screenshots: media.Screenshots(),
+ Apps: []client.AppInfo{
+ {Snap: "some-snap_instance", Name: "bar"},
+ {Snap: "some-snap_instance", Name: "foo"},
+ },
+ }
+ c.Check(mapLocal(about), check.DeepEquals, expected)
+}
+
func (s *apiSuite) TestMapLocalOfTryResolvesSymlink(c *check.C) {
c.Assert(os.MkdirAll(dirs.SnapBlobDir, 0755), check.IsNil)
@@ -1523,6 +1646,34 @@ func (s *apiSuite) TestSnapsInfoOnlyLoca
c.Assert(snaps[0]["name"], check.Equals, "local")
}
+func (s *apiSuite) TestSnapsInfoAllMixedPublishers(c *check.C) {
+ d := s.daemon(c)
+
+ // the first 'local' is from a 'local' snap
+ s.mkInstalledInState(c, d, "local", "", "v1", snap.R(-1), false, "")
+ s.mkInstalledInState(c, d, "local", "foo", "v2", snap.R(1), false, "")
+ s.mkInstalledInState(c, d, "local", "foo", "v3", snap.R(2), true, "")
+
+ req, err := http.NewRequest("GET", "/v2/snaps?select=all", nil)
+ c.Assert(err, check.IsNil)
+ rsp := getSnapsInfo(snapsCmd, req, nil).(*resp)
+ c.Assert(rsp.Type, check.Equals, ResponseTypeSync)
+
+ snaps := snapList(rsp.Result)
+ c.Assert(snaps, check.HasLen, 3)
+
+ publisher := map[string]interface{}{
+ "id": "foo-id",
+ "username": "foo",
+ "display-name": "Foo",
+ "validation": "unproven",
+ }
+
+ c.Check(snaps[0]["publisher"], check.IsNil)
+ c.Check(snaps[1]["publisher"], check.DeepEquals, publisher)
+ c.Check(snaps[2]["publisher"], check.DeepEquals, publisher)
+}
+
func (s *apiSuite) TestSnapsInfoAll(c *check.C) {
d := s.daemon(c)
@@ -1765,6 +1916,33 @@ func (s *apiSuite) TestFindCommonID(c *c
c.Check(snaps[0]["common-ids"], check.DeepEquals, []interface{}{"org.foo"})
}
+func (s *apiSuite) TestFindByCommonID(c *check.C) {
+ s.daemon(c)
+
+ s.rsnaps = []*snap.Info{{
+ SideInfo: snap.SideInfo{
+ RealName: "store",
+ },
+ Publisher: snap.StoreAccount{
+ ID: "foo-id",
+ Username: "foo",
+ DisplayName: "Foo",
+ Validation: "unproven",
+ },
+ CommonIDs: []string{"org.foo"},
+ }}
+ s.mockSnap(c, "name: store\nversion: 1.0")
+
+ req, err := http.NewRequest("GET", "/v2/find?common-id=org.foo", nil)
+ c.Assert(err, check.IsNil)
+
+ rsp := searchStore(findCmd, req, nil).(*resp)
+
+ snaps := snapList(rsp.Result)
+ c.Assert(snaps, check.HasLen, 1)
+ c.Check(s.storeSearch, check.DeepEquals, store.Search{CommonID: "org.foo"})
+}
+
func (s *apiSuite) TestFindOne(c *check.C) {
s.daemon(c)
@@ -1824,14 +2002,37 @@ func (s *apiSuite) TestFindOneNotFound(c
c.Check(rsp.Status, check.Equals, 404)
}
-func (s *apiSuite) TestFindRefreshNotQ(c *check.C) {
- req, err := http.NewRequest("GET", "/v2/find?select=refresh&q=foo", nil)
- c.Assert(err, check.IsNil)
+func (s *apiSuite) TestFindRefreshNotOther(c *check.C) {
+ for _, other := range []string{"name", "q", "common-id"} {
+ req, err := http.NewRequest("GET", "/v2/find?select=refresh&"+other+"=foo*", nil)
+ c.Assert(err, check.IsNil)
- rsp := searchStore(findCmd, req, nil).(*resp)
- c.Check(rsp.Type, check.Equals, ResponseTypeError)
- c.Check(rsp.Status, check.Equals, 400)
- c.Check(rsp.Result.(*errorResult).Message, check.Matches, "cannot use 'q' with 'select=refresh'")
+ rsp := searchStore(findCmd, req, nil).(*resp)
+ c.Check(rsp.Type, check.Equals, ResponseTypeError)
+ c.Check(rsp.Status, check.Equals, 400)
+ c.Check(rsp.Result.(*errorResult).Message, check.Equals, "cannot use '"+other+"' with 'select=refresh'")
+ }
+}
+
+func (s *apiSuite) TestFindNotTogether(c *check.C) {
+ queries := map[string]string{"q": "foo", "name": "foo*", "common-id": "foo"}
+ for ki, vi := range queries {
+ for kj, vj := range queries {
+ if ki == kj {
+ continue
+ }
+
+ req, err := http.NewRequest("GET", fmt.Sprintf("/v2/find?%s=%s&%s=%s", ki, vi, kj, vj), nil)
+ c.Assert(err, check.IsNil)
+
+ rsp := searchStore(findCmd, req, nil).(*resp)
+ c.Check(rsp.Type, check.Equals, ResponseTypeError)
+ c.Check(rsp.Status, check.Equals, 400)
+ exp1 := "cannot use '" + ki + "' and '" + kj + "' together"
+ exp2 := "cannot use '" + kj + "' and '" + ki + "' together"
+ c.Check(rsp.Result.(*errorResult).Message, check.Matches, exp1+"|"+exp2)
+ }
+ }
}
func (s *apiSuite) TestFindBadQueryReturnsCorrectErrorKind(c *check.C) {
@@ -2203,7 +2404,7 @@ func (s *apiSuite) TestPostSnap(c *check
c.Check(soon, check.Equals, 1)
}
-func (s *apiSuite) TestPostSnapVerfySnapInstruction(c *check.C) {
+func (s *apiSuite) TestPostSnapVerifySnapInstruction(c *check.C) {
s.daemonWithOverlordMock(c)
buf := bytes.NewBufferString(`{"action": "install"}`)
@@ -2218,6 +2419,21 @@ func (s *apiSuite) TestPostSnapVerfySnap
c.Check(rsp.Result.(*errorResult).Message, testutil.Contains, `cannot install "ubuntu-core", please use "core" instead`)
}
+func (s *apiSuite) TestPostSnapVerifyMultiSnapInstruction(c *check.C) {
+ s.daemonWithOverlordMock(c)
+
+ buf := strings.NewReader(`{"action": "install","snaps":["ubuntu-core"]}`)
+ req, err := http.NewRequest("POST", "/v2/snaps", buf)
+ c.Assert(err, check.IsNil)
+ req.Header.Set("Content-Type", "application/json")
+
+ rsp := postSnaps(snapsCmd, req, nil).(*resp)
+
+ c.Check(rsp.Type, check.Equals, ResponseTypeError)
+ c.Check(rsp.Status, check.Equals, 400)
+ c.Check(rsp.Result.(*errorResult).Message, testutil.Contains, `cannot install "ubuntu-core", please use "core" instead`)
+}
+
func (s *apiSuite) TestPostSnapSetsUser(c *check.C) {
d := s.daemon(c)
ensureStateSoon = func(st *state.State) {}
@@ -3236,7 +3452,7 @@ func (s *apiSuite) TestAppIconGetNoApp(c
func (s *apiSuite) TestNotInstalledSnapIcon(c *check.C) {
info := &snap.Info{SuggestedName: "notInstalledSnap", Media: []snap.MediaInfo{{Type: "icon", URL: "icon.svg"}}}
iconfile := snapIcon(info)
- c.Check(iconfile, testutil.Contains, "icon.svg")
+ c.Check(iconfile, check.Equals, "")
}
func (s *apiSuite) TestInstallOnNonDevModeDistro(c *check.C) {
@@ -3933,7 +4149,20 @@ func (s *apiSuite) TestInterfacesLegacy(
d := s.daemon(c)
+ var anotherConsumerYaml = `
+name: another-consumer-%s
+version: 1
+apps:
+ app:
+plugs:
+ plug:
+ interface: test
+ key: value
+ label: label
+`
s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, fmt.Sprintf(anotherConsumerYaml, "def"))
+ s.mockSnap(c, fmt.Sprintf(anotherConsumerYaml, "abc"))
s.mockSnap(c, producerYaml)
repo := d.overlord.InterfaceManager().Repository()
@@ -3944,6 +4173,26 @@ func (s *apiSuite) TestInterfacesLegacy(
_, err := repo.Connect(connRef, nil, nil, nil, nil, nil)
c.Assert(err, check.IsNil)
+ st := s.d.overlord.State()
+ st.Lock()
+ st.Set("conns", map[string]interface{}{
+ "consumer:plug producer:slot": map[string]interface{}{
+ "interface": "test",
+ "auto": true,
+ },
+ "another-consumer-def:plug producer:slot": map[string]interface{}{
+ "interface": "test",
+ "by-gadget": true,
+ "auto": true,
+ },
+ "another-consumer-abc:plug producer:slot": map[string]interface{}{
+ "interface": "test",
+ "by-gadget": true,
+ "auto": true,
+ },
+ })
+ st.Unlock()
+
req, err := http.NewRequest("GET", "/v2/interfaces", nil)
c.Assert(err, check.IsNil)
rec := httptest.NewRecorder()
@@ -3956,6 +4205,28 @@ func (s *apiSuite) TestInterfacesLegacy(
"result": map[string]interface{}{
"plugs": []interface{}{
map[string]interface{}{
+ "snap": "another-consumer-abc",
+ "plug": "plug",
+ "interface": "test",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app"},
+ "label": "label",
+ "connections": []interface{}{
+ map[string]interface{}{"snap": "producer", "slot": "slot"},
+ },
+ },
+ map[string]interface{}{
+ "snap": "another-consumer-def",
+ "plug": "plug",
+ "interface": "test",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app"},
+ "label": "label",
+ "connections": []interface{}{
+ map[string]interface{}{"snap": "producer", "slot": "slot"},
+ },
+ },
+ map[string]interface{}{
"snap": "consumer",
"plug": "plug",
"interface": "test",
@@ -3976,6 +4247,8 @@ func (s *apiSuite) TestInterfacesLegacy(
"apps": []interface{}{"app"},
"label": "label",
"connections": []interface{}{
+ map[string]interface{}{"snap": "another-consumer-abc", "plug": "plug"},
+ map[string]interface{}{"snap": "another-consumer-def", "plug": "plug"},
map[string]interface{}{"snap": "consumer", "plug": "plug"},
},
},
@@ -4579,6 +4852,65 @@ func (s *apiSuite) TestDisconnectPlugFai
})
}
+func (s *apiSuite) TestDisconnectConflict(c *check.C) {
+ revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
+ defer revert()
+ d := s.daemon(c)
+
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, producerYaml)
+
+ repo := d.overlord.InterfaceManager().Repository()
+ connRef := &interfaces.ConnRef{
+ PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"},
+ SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"},
+ }
+ _, err := repo.Connect(connRef, nil, nil, nil, nil, nil)
+ c.Assert(err, check.IsNil)
+
+ st := d.overlord.State()
+ st.Lock()
+ st.Set("conns", map[string]interface{}{
+ "consumer:plug producer:slot": map[string]interface{}{
+ "interface": "test",
+ },
+ })
+ st.Unlock()
+
+ simulateConflict(d.overlord, "consumer")
+
+ action := &interfaceAction{
+ Action: "disconnect",
+ Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}},
+ Slots: []slotJSON{{Snap: "producer", Name: "slot"}},
+ }
+ text, err := json.Marshal(action)
+ c.Assert(err, check.IsNil)
+ buf := bytes.NewBuffer(text)
+ req, err := http.NewRequest("POST", "/v2/interfaces", buf)
+ c.Assert(err, check.IsNil)
+ rec := httptest.NewRecorder()
+ interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req)
+
+ c.Check(rec.Code, check.Equals, 409)
+
+ var body map[string]interface{}
+ err = json.Unmarshal(rec.Body.Bytes(), &body)
+ c.Check(err, check.IsNil)
+ c.Check(body, check.DeepEquals, map[string]interface{}{
+ "status-code": 409.,
+ "status": "Conflict",
+ "result": map[string]interface{}{
+ "message": `snap "consumer" has "manip" change in progress`,
+ "kind": "snap-change-conflict",
+ "value": map[string]interface{}{
+ "change-kind": "manip",
+ "snap-name": "consumer",
+ },
+ },
+ "type": "error"})
+}
+
func (s *apiSuite) TestDisconnectCoreSystemAlias(c *check.C) {
revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
defer revert()
@@ -4708,14 +5040,6 @@ func (s *apiSuite) TestUnsupportedInterf
})
}
-func (s *apiSuite) TestGetAsserts(c *check.C) {
- s.daemon(c)
- resp := assertsCmd.GET(assertsCmd, nil, nil).(*resp)
- c.Check(resp.Status, check.Equals, 200)
- c.Check(resp.Type, check.Equals, ResponseTypeSync)
- c.Check(resp.Result, check.DeepEquals, map[string][]string{"types": asserts.TypeNames()})
-}
-
func assertAdd(st *state.State, a asserts.Assertion) {
st.Lock()
defer st.Unlock()
@@ -4725,195 +5049,6 @@ func assertAdd(st *state.State, a assert
}
}
-func (s *apiSuite) TestAssertOK(c *check.C) {
- // Setup
- d := s.daemon(c)
- st := d.overlord.State()
- // add store key
- assertAdd(st, s.storeSigning.StoreAccountKey(""))
-
- acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "")
- buf := bytes.NewBuffer(asserts.Encode(acct))
- // Execute
- req, err := http.NewRequest("POST", "/v2/assertions", buf)
- c.Assert(err, check.IsNil)
- rsp := doAssert(assertsCmd, req, nil).(*resp)
- // Verify (external)
- c.Check(rsp.Type, check.Equals, ResponseTypeSync)
- c.Check(rsp.Status, check.Equals, 200)
- // Verify (internal)
- st.Lock()
- defer st.Unlock()
- _, err = assertstate.DB(st).Find(asserts.AccountType, map[string]string{
- "account-id": acct.AccountID(),
- })
- c.Check(err, check.IsNil)
-}
-
-func (s *apiSuite) TestAssertStreamOK(c *check.C) {
- // Setup
- d := s.daemon(c)
- st := d.overlord.State()
-
- acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "")
- buf := &bytes.Buffer{}
- enc := asserts.NewEncoder(buf)
- err := enc.Encode(acct)
- c.Assert(err, check.IsNil)
- err = enc.Encode(s.storeSigning.StoreAccountKey(""))
- c.Assert(err, check.IsNil)
-
- // Execute
- req, err := http.NewRequest("POST", "/v2/assertions", buf)
- c.Assert(err, check.IsNil)
- rsp := doAssert(assertsCmd, req, nil).(*resp)
- // Verify (external)
- c.Check(rsp.Type, check.Equals, ResponseTypeSync)
- c.Check(rsp.Status, check.Equals, 200)
- // Verify (internal)
- st.Lock()
- defer st.Unlock()
- _, err = assertstate.DB(st).Find(asserts.AccountType, map[string]string{
- "account-id": acct.AccountID(),
- })
- c.Check(err, check.IsNil)
-}
-
-func (s *apiSuite) TestAssertInvalid(c *check.C) {
- // Setup
- buf := bytes.NewBufferString("blargh")
- req, err := http.NewRequest("POST", "/v2/assertions", buf)
- c.Assert(err, check.IsNil)
- rec := httptest.NewRecorder()
- // Execute
- assertsCmd.POST(assertsCmd, req, nil).ServeHTTP(rec, req)
- // Verify (external)
- c.Check(rec.Code, check.Equals, 400)
- c.Check(rec.Body.String(), testutil.Contains,
- "cannot decode request body into assertions")
-}
-
-func (s *apiSuite) TestAssertError(c *check.C) {
- s.daemon(c)
- // Setup
- acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "")
- buf := bytes.NewBuffer(asserts.Encode(acct))
- req, err := http.NewRequest("POST", "/v2/assertions", buf)
- c.Assert(err, check.IsNil)
- rec := httptest.NewRecorder()
- // Execute
- assertsCmd.POST(assertsCmd, req, nil).ServeHTTP(rec, req)
- // Verify (external)
- c.Check(rec.Code, check.Equals, 400)
- c.Check(rec.Body.String(), testutil.Contains, "assert failed")
-}
-
-func (s *apiSuite) TestAssertsFindManyAll(c *check.C) {
- // Setup
- d := s.daemon(c)
- // add store key
- st := d.overlord.State()
- assertAdd(st, s.storeSigning.StoreAccountKey(""))
- acct := assertstest.NewAccount(s.storeSigning, "developer1", map[string]interface{}{
- "account-id": "developer1-id",
- }, "")
- assertAdd(st, acct)
-
- // Execute
- req, err := http.NewRequest("POST", "/v2/assertions/account", nil)
- c.Assert(err, check.IsNil)
- s.vars = map[string]string{"assertType": "account"}
- rec := httptest.NewRecorder()
- assertsFindManyCmd.GET(assertsFindManyCmd, req, nil).ServeHTTP(rec, req)
- // Verify
- c.Check(rec.Code, check.Equals, 200, check.Commentf("body %q", rec.Body))
- c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/x.ubuntu.assertion; bundle=y")
- c.Check(rec.HeaderMap.Get("X-Ubuntu-Assertions-Count"), check.Equals, "4")
- dec := asserts.NewDecoder(rec.Body)
- a1, err := dec.Decode()
- c.Assert(err, check.IsNil)
- c.Check(a1.Type(), check.Equals, asserts.AccountType)
-
- a2, err := dec.Decode()
- c.Assert(err, check.IsNil)
-
- a3, err := dec.Decode()
- c.Assert(err, check.IsNil)
-
- a4, err := dec.Decode()
- c.Assert(err, check.IsNil)
-
- _, err = dec.Decode()
- c.Assert(err, check.Equals, io.EOF)
-
- ids := []string{a1.(*asserts.Account).AccountID(), a2.(*asserts.Account).AccountID(), a3.(*asserts.Account).AccountID(), a4.(*asserts.Account).AccountID()}
- sort.Strings(ids)
- c.Check(ids, check.DeepEquals, []string{"can0nical", "canonical", "developer1-id", "generic"})
-}
-
-func (s *apiSuite) TestAssertsFindManyFilter(c *check.C) {
- // Setup
- d := s.daemon(c)
- // add store key
- st := d.overlord.State()
- assertAdd(st, s.storeSigning.StoreAccountKey(""))
- acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "")
- assertAdd(st, acct)
-
- // Execute
- req, err := http.NewRequest("POST", "/v2/assertions/account?username=developer1", nil)
- c.Assert(err, check.IsNil)
- s.vars = map[string]string{"assertType": "account"}
- rec := httptest.NewRecorder()
- assertsFindManyCmd.GET(assertsFindManyCmd, req, nil).ServeHTTP(rec, req)
- // Verify
- c.Check(rec.Code, check.Equals, 200, check.Commentf("body %q", rec.Body))
- c.Check(rec.HeaderMap.Get("X-Ubuntu-Assertions-Count"), check.Equals, "1")
- dec := asserts.NewDecoder(rec.Body)
- a1, err := dec.Decode()
- c.Assert(err, check.IsNil)
- c.Check(a1.Type(), check.Equals, asserts.AccountType)
- c.Check(a1.(*asserts.Account).Username(), check.Equals, "developer1")
- c.Check(a1.(*asserts.Account).AccountID(), check.Equals, acct.AccountID())
- _, err = dec.Decode()
- c.Check(err, check.Equals, io.EOF)
-}
-
-func (s *apiSuite) TestAssertsFindManyNoResults(c *check.C) {
- // Setup
- d := s.daemon(c)
- // add store key
- st := d.overlord.State()
- assertAdd(st, s.storeSigning.StoreAccountKey(""))
- acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "")
- assertAdd(st, acct)
-
- // Execute
- req, err := http.NewRequest("POST", "/v2/assertions/account?username=xyzzyx", nil)
- c.Assert(err, check.IsNil)
- s.vars = map[string]string{"assertType": "account"}
- rec := httptest.NewRecorder()
- assertsFindManyCmd.GET(assertsFindManyCmd, req, nil).ServeHTTP(rec, req)
- // Verify
- c.Check(rec.Code, check.Equals, 200, check.Commentf("body %q", rec.Body))
- c.Check(rec.HeaderMap.Get("X-Ubuntu-Assertions-Count"), check.Equals, "0")
- dec := asserts.NewDecoder(rec.Body)
- _, err = dec.Decode()
- c.Check(err, check.Equals, io.EOF)
-}
-
-func (s *apiSuite) TestAssertsInvalidType(c *check.C) {
- // Execute
- req, err := http.NewRequest("POST", "/v2/assertions/foo", nil)
- c.Assert(err, check.IsNil)
- s.vars = map[string]string{"assertType": "foo"}
- rec := httptest.NewRecorder()
- assertsFindManyCmd.GET(assertsFindManyCmd, req, nil).ServeHTTP(rec, req)
- // Verify
- c.Check(rec.Code, check.Equals, 400)
- c.Check(rec.Body.String(), testutil.Contains, "invalid assert type")
-}
-
func setupChanges(st *state.State) []string {
chg1 := st.NewChange("install", "install...")
chg1.Set("snap-names", []string{"funky-snap-name"})
@@ -6612,88 +6747,6 @@ func (s *apiSuite) TestSnapctlForbiddenE
c.Assert(rsp.Status, check.Equals, 403)
}
-var _ = check.Suite(&postDebugSuite{})
-
-type postDebugSuite struct {
- apiBaseSuite
-}
-
-func (s *postDebugSuite) TestPostDebugEnsureStateSoon(c *check.C) {
- s.daemonWithOverlordMock(c)
-
- soon := 0
- ensureStateSoon = func(st *state.State) {
- soon++
- ensureStateSoonImpl(st)
- }
-
- buf := bytes.NewBufferString(`{"action": "ensure-state-soon"}`)
- req, err := http.NewRequest("POST", "/v2/debug", buf)
- c.Assert(err, check.IsNil)
-
- rsp := postDebug(debugCmd, req, nil).(*resp)
-
- c.Check(rsp.Type, check.Equals, ResponseTypeSync)
- c.Check(rsp.Result, check.Equals, true)
- c.Check(soon, check.Equals, 1)
-}
-
-func (s *postDebugSuite) TestPostDebugGetBaseDeclaration(c *check.C) {
- _ = s.daemon(c)
-
- buf := bytes.NewBufferString(`{"action": "get-base-declaration"}`)
- req, err := http.NewRequest("POST", "/v2/debug", buf)
- c.Assert(err, check.IsNil)
-
- rsp := postDebug(debugCmd, req, nil).(*resp)
-
- c.Check(rsp.Type, check.Equals, ResponseTypeSync)
- c.Check(rsp.Result.(map[string]interface{})["base-declaration"],
- testutil.Contains, "type: base-declaration")
-}
-
-func (s *postDebugSuite) TestPostDebugConnectivityHappy(c *check.C) {
- _ = s.daemon(c)
-
- buf := bytes.NewBufferString(`{"action": "connectivity"}`)
- req, err := http.NewRequest("POST", "/v2/debug", buf)
- c.Assert(err, check.IsNil)
-
- s.connectivityResult = map[string]bool{
- "good.host.com": true,
- "another.good.host.com": true,
- }
-
- rsp := postDebug(debugCmd, req, nil).(*resp)
-
- c.Check(rsp.Type, check.Equals, ResponseTypeSync)
- c.Check(rsp.Result, check.DeepEquals, ConnectivityStatus{
- Connectivity: true,
- Unreachable: []string(nil),
- })
-}
-
-func (s *postDebugSuite) TestPostDebugConnectivityUnhappy(c *check.C) {
- _ = s.daemon(c)
-
- buf := bytes.NewBufferString(`{"action": "connectivity"}`)
- req, err := http.NewRequest("POST", "/v2/debug", buf)
- c.Assert(err, check.IsNil)
-
- s.connectivityResult = map[string]bool{
- "good.host.com": true,
- "bad.host.com": false,
- }
-
- rsp := postDebug(debugCmd, req, nil).(*resp)
-
- c.Check(rsp.Type, check.Equals, ResponseTypeSync)
- c.Check(rsp.Result, check.DeepEquals, ConnectivityStatus{
- Connectivity: false,
- Unreachable: []string{"bad.host.com"},
- })
-}
-
type appSuite struct {
apiBaseSuite
cmd *testutil.MockCmd
@@ -6928,7 +6981,7 @@ func (s *appSuite) TestAppInfosForOneSna
appInfos, rsp := appInfosFor(st, []string{"snap-a"}, appInfoOptions{service: true})
c.Assert(rsp, check.IsNil)
c.Assert(appInfos, check.HasLen, 2)
- sort.Sort(bySnapApp(appInfos))
+ sort.Sort(cmd.BySnapApp(appInfos))
c.Check(appInfos[0].Snap, check.DeepEquals, s.infoA)
c.Check(appInfos[0].Name, check.Equals, "svc1")
@@ -6941,7 +6994,7 @@ func (s *appSuite) TestAppInfosForMixedA
appInfos, rsp := appInfosFor(st, []string{"snap-a", "snap-a.svc1"}, appInfoOptions{service: true})
c.Assert(rsp, check.IsNil)
c.Assert(appInfos, check.HasLen, 2)
- sort.Sort(bySnapApp(appInfos))
+ sort.Sort(cmd.BySnapApp(appInfos))
c.Check(appInfos[0].Snap, check.DeepEquals, s.infoA)
c.Check(appInfos[0].Name, check.Equals, "svc1")
@@ -6963,7 +7016,7 @@ func (s *appSuite) TestAppInfosCleanupAn
}, appInfoOptions{service: true})
c.Assert(rsp, check.IsNil)
c.Assert(appInfos, check.HasLen, 3)
- sort.Sort(bySnapApp(appInfos))
+ sort.Sort(cmd.BySnapApp(appInfos))
c.Check(appInfos[0].Snap, check.DeepEquals, s.infoA)
c.Check(appInfos[0].Name, check.Equals, "svc1")
diff -pruN 2.37.4-1/daemon/daemon_test.go 2.39.2+19.10ubuntu1/daemon/daemon_test.go
--- 2.37.4-1/daemon/daemon_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/daemon/daemon_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -146,7 +146,7 @@ func (s *daemonSuite) TestCommandMethodD
c.Check(rec.Code, check.Equals, 401, check.Commentf(method))
rec = httptest.NewRecorder()
- req.RemoteAddr = "pid=100;uid=0;" + req.RemoteAddr
+ req.RemoteAddr = "pid=100;uid=0;socket=;"
cmd.ServeHTTP(rec, req)
c.Check(mck.lastMethod, check.Equals, method)
@@ -155,7 +155,7 @@ func (s *daemonSuite) TestCommandMethodD
req, err := http.NewRequest("POTATO", "", nil)
c.Assert(err, check.IsNil)
- req.RemoteAddr = "pid=100;uid=0;" + req.RemoteAddr
+ req.RemoteAddr = "pid=100;uid=0;socket=;"
rec := httptest.NewRecorder()
cmd.ServeHTTP(rec, req)
@@ -171,7 +171,7 @@ func (s *daemonSuite) TestCommandRestart
}
req, err := http.NewRequest("GET", "", nil)
c.Assert(err, check.IsNil)
- req.RemoteAddr = "pid=100;uid=0;" + req.RemoteAddr
+ req.RemoteAddr = "pid=100;uid=0;socket=;"
rec := httptest.NewRecorder()
cmd.ServeHTTP(rec, req)
@@ -215,7 +215,7 @@ func (s *daemonSuite) TestFillsWarnings(
}
req, err := http.NewRequest("GET", "", nil)
c.Assert(err, check.IsNil)
- req.RemoteAddr = "pid=100;uid=0;" + req.RemoteAddr
+ req.RemoteAddr = "pid=100;uid=0;socket=;"
rec := httptest.NewRecorder()
cmd.ServeHTTP(rec, req)
@@ -269,7 +269,7 @@ func (s *daemonSuite) TestGuestAccess(c
}
func (s *daemonSuite) TestSnapctlAccessSnapOKWithUser(c *check.C) {
- remoteAddr := "pid=100;uid=1000;socket=" + dirs.SnapSocket
+ remoteAddr := "pid=100;uid=1000;socket=" + dirs.SnapSocket + ";"
get := &http.Request{Method: "GET", RemoteAddr: remoteAddr}
put := &http.Request{Method: "PUT", RemoteAddr: remoteAddr}
pst := &http.Request{Method: "POST", RemoteAddr: remoteAddr}
@@ -283,7 +283,7 @@ func (s *daemonSuite) TestSnapctlAccessS
}
func (s *daemonSuite) TestSnapctlAccessSnapOKWithRoot(c *check.C) {
- remoteAddr := "pid=100;uid=0;socket=" + dirs.SnapSocket
+ remoteAddr := "pid=100;uid=0;socket=" + dirs.SnapSocket + ";"
get := &http.Request{Method: "GET", RemoteAddr: remoteAddr}
put := &http.Request{Method: "PUT", RemoteAddr: remoteAddr}
pst := &http.Request{Method: "POST", RemoteAddr: remoteAddr}
@@ -297,8 +297,8 @@ func (s *daemonSuite) TestSnapctlAccessS
}
func (s *daemonSuite) TestUserAccess(c *check.C) {
- get := &http.Request{Method: "GET", RemoteAddr: "pid=100;uid=42;"}
- put := &http.Request{Method: "PUT", RemoteAddr: "pid=100;uid=42;"}
+ get := &http.Request{Method: "GET", RemoteAddr: "pid=100;uid=42;socket=;"}
+ put := &http.Request{Method: "PUT", RemoteAddr: "pid=100;uid=42;socket=;"}
cmd := &Command{d: newTestDaemon(c)}
c.Check(cmd.canAccess(get, nil), check.Equals, accessUnauthorized)
@@ -321,8 +321,8 @@ func (s *daemonSuite) TestUserAccess(c *
}
func (s *daemonSuite) TestSuperAccess(c *check.C) {
- get := &http.Request{Method: "GET", RemoteAddr: "pid=100;uid=0;"}
- put := &http.Request{Method: "PUT", RemoteAddr: "pid=100;uid=0;"}
+ get := &http.Request{Method: "GET", RemoteAddr: "pid=100;uid=0;socket=;"}
+ put := &http.Request{Method: "PUT", RemoteAddr: "pid=100;uid=0;socket=;"}
cmd := &Command{d: newTestDaemon(c)}
c.Check(cmd.canAccess(get, nil), check.Equals, accessOK)
@@ -342,7 +342,7 @@ func (s *daemonSuite) TestSuperAccess(c
}
func (s *daemonSuite) TestPolkitAccess(c *check.C) {
- put := &http.Request{Method: "PUT", RemoteAddr: "pid=100;uid=42;"}
+ put := &http.Request{Method: "PUT", RemoteAddr: "pid=100;uid=42;socket=;"}
cmd := &Command{d: newTestDaemon(c), PolkitOK: "polkit.action"}
// polkit says user is not authorised
@@ -363,7 +363,7 @@ func (s *daemonSuite) TestPolkitAccess(c
}
func (s *daemonSuite) TestPolkitAccessForGet(c *check.C) {
- get := &http.Request{Method: "GET", RemoteAddr: "pid=100;uid=42;"}
+ get := &http.Request{Method: "GET", RemoteAddr: "pid=100;uid=42;socket=;"}
cmd := &Command{d: newTestDaemon(c), PolkitOK: "polkit.action"}
// polkit can grant authorisation for GET requests
@@ -379,7 +379,7 @@ func (s *daemonSuite) TestPolkitAccessFo
}
func (s *daemonSuite) TestPolkitInteractivity(c *check.C) {
- put := &http.Request{Method: "PUT", RemoteAddr: "pid=100;uid=42;", Header: make(http.Header)}
+ put := &http.Request{Method: "PUT", RemoteAddr: "pid=100;uid=42;socket=;", Header: make(http.Header)}
cmd := &Command{d: newTestDaemon(c), PolkitOK: "polkit.action"}
s.authorized = true
@@ -950,7 +950,7 @@ func (s *daemonSuite) TestShutdownServer
func doTestReq(c *check.C, cmd *Command, mth string) *httptest.ResponseRecorder {
req, err := http.NewRequest(mth, "", nil)
c.Assert(err, check.IsNil)
- req.RemoteAddr = "pid=100;uid=0;" + req.RemoteAddr
+ req.RemoteAddr = "pid=100;uid=0;socket=;"
rec := httptest.NewRecorder()
cmd.ServeHTTP(rec, req)
return rec
diff -pruN 2.37.4-1/daemon/export_api_asserts_test.go 2.39.2+19.10ubuntu1/daemon/export_api_asserts_test.go
--- 2.37.4-1/daemon/export_api_asserts_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/daemon/export_api_asserts_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,42 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package daemon
+
+import (
+ "net/http"
+
+ "github.com/snapcore/snapd/overlord/auth"
+)
+
+// TODO: this should be a general helper
+var AssertAdd = assertAdd
+
+func GetAssertTypeNames(c *Command, r *http.Request, user *auth.UserState) *resp {
+ return getAssertTypeNames(c, r, user).(*resp)
+}
+
+func DoAssert(c *Command, r *http.Request, user *auth.UserState) *resp {
+ return doAssert(c, r, user).(*resp)
+}
+
+var (
+ AssertsCmd = assertsCmd
+ AssertsFindManyCmd = assertsFindManyCmd
+)
diff -pruN 2.37.4-1/daemon/export_api_snap_file_test.go 2.39.2+19.10ubuntu1/daemon/export_api_snap_file_test.go
--- 2.37.4-1/daemon/export_api_snap_file_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/daemon/export_api_snap_file_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,24 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package daemon
+
+var SnapFileCmd = snapFileCmd
+
+var GetSnapFile = getSnapFile
diff -pruN 2.37.4-1/daemon/export_api_snapshots_test.go 2.39.2+19.10ubuntu1/daemon/export_api_snapshots_test.go
--- 2.37.4-1/daemon/export_api_snapshots_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/daemon/export_api_snapshots_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,105 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2018 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 .
+ *
+ */
+
+package daemon
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+
+ "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/overlord/state"
+)
+
+func MockSnapshotSave(newSave func(*state.State, []string, []string) (uint64, []string, *state.TaskSet, error)) (restore func()) {
+ oldSave := snapshotSave
+ snapshotSave = newSave
+ return func() {
+ snapshotSave = oldSave
+ }
+}
+
+func MockSnapshotList(newList func(context.Context, uint64, []string) ([]client.SnapshotSet, error)) (restore func()) {
+ oldList := snapshotList
+ snapshotList = newList
+ return func() {
+ snapshotList = oldList
+ }
+}
+
+func MockSnapshotCheck(newCheck func(*state.State, uint64, []string, []string) ([]string, *state.TaskSet, error)) (restore func()) {
+ oldCheck := snapshotCheck
+ snapshotCheck = newCheck
+ return func() {
+ snapshotCheck = oldCheck
+ }
+}
+
+func MockSnapshotRestore(newRestore func(*state.State, uint64, []string, []string) ([]string, *state.TaskSet, error)) (restore func()) {
+ oldRestore := snapshotRestore
+ snapshotRestore = newRestore
+ return func() {
+ snapshotRestore = oldRestore
+ }
+}
+
+func MockSnapshotForget(newForget func(*state.State, uint64, []string) ([]string, *state.TaskSet, error)) (restore func()) {
+ oldForget := snapshotForget
+ snapshotForget = newForget
+ return func() {
+ snapshotForget = oldForget
+ }
+}
+
+func MustUnmarshalSnapInstruction(c *check.C, jinst string) *snapInstruction {
+ var inst snapInstruction
+ if err := json.Unmarshal([]byte(jinst), &inst); err != nil {
+ c.Fatalf("cannot unmarshal %q into snapInstruction: %v", jinst, err)
+ }
+ return &inst
+}
+
+func MustUnmarshalSnapshotAction(c *check.C, jact string) *snapshotAction {
+ var act snapshotAction
+ if err := json.Unmarshal([]byte(jact), &act); err != nil {
+ c.Fatalf("cannot unmarshal %q into snapshotAction: %v", jact, err)
+ }
+ return &act
+}
+
+func (rsp *resp) ErrorResult() *errorResult {
+ return rsp.Result.(*errorResult)
+}
+
+func ListSnapshots(c *Command, r *http.Request, user *auth.UserState) *resp {
+ return listSnapshots(c, r, user).(*resp)
+}
+
+func ChangeSnapshots(c *Command, r *http.Request, user *auth.UserState) *resp {
+ return changeSnapshots(c, r, user).(*resp)
+}
+
+var (
+ SnapshotMany = snapshotMany
+ SnapshotCmd = snapshotCmd
+)
diff -pruN 2.37.4-1/daemon/export_snapshots_test.go 2.39.2+19.10ubuntu1/daemon/export_snapshots_test.go
--- 2.37.4-1/daemon/export_snapshots_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/daemon/export_snapshots_test.go 1970-01-01 00:00:00.000000000 +0000
@@ -1,112 +0,0 @@
-// -*- Mode: Go; indent-tabs-mode: t -*-
-
-/*
- * Copyright (C) 2018 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 .
- *
- */
-
-package daemon
-
-import (
- "encoding/json"
- "net/http"
-
- "golang.org/x/net/context"
- "gopkg.in/check.v1"
-
- "github.com/snapcore/snapd/client"
- "github.com/snapcore/snapd/overlord"
- "github.com/snapcore/snapd/overlord/auth"
- "github.com/snapcore/snapd/overlord/state"
-)
-
-func NewWithOverlord(o *overlord.Overlord) *Daemon {
- d := &Daemon{overlord: o}
- d.addRoutes()
- return d
-}
-
-func MockSnapshotSave(newSave func(*state.State, []string, []string) (uint64, []string, *state.TaskSet, error)) (restore func()) {
- oldSave := snapshotSave
- snapshotSave = newSave
- return func() {
- snapshotSave = oldSave
- }
-}
-
-func MockSnapshotList(newList func(context.Context, uint64, []string) ([]client.SnapshotSet, error)) (restore func()) {
- oldList := snapshotList
- snapshotList = newList
- return func() {
- snapshotList = oldList
- }
-}
-
-func MockSnapshotCheck(newCheck func(*state.State, uint64, []string, []string) ([]string, *state.TaskSet, error)) (restore func()) {
- oldCheck := snapshotCheck
- snapshotCheck = newCheck
- return func() {
- snapshotCheck = oldCheck
- }
-}
-
-func MockSnapshotRestore(newRestore func(*state.State, uint64, []string, []string) ([]string, *state.TaskSet, error)) (restore func()) {
- oldRestore := snapshotRestore
- snapshotRestore = newRestore
- return func() {
- snapshotRestore = oldRestore
- }
-}
-
-func MockSnapshotForget(newForget func(*state.State, uint64, []string) ([]string, *state.TaskSet, error)) (restore func()) {
- oldForget := snapshotForget
- snapshotForget = newForget
- return func() {
- snapshotForget = oldForget
- }
-}
-
-func MustUnmarshalSnapInstruction(c *check.C, jinst string) *snapInstruction {
- var inst snapInstruction
- if err := json.Unmarshal([]byte(jinst), &inst); err != nil {
- c.Fatalf("cannot unmarshal %q into snapInstruction: %v", jinst, err)
- }
- return &inst
-}
-
-func MustUnmarshalSnapshotAction(c *check.C, jact string) *snapshotAction {
- var act snapshotAction
- if err := json.Unmarshal([]byte(jact), &act); err != nil {
- c.Fatalf("cannot unmarshal %q into snapshotAction: %v", jact, err)
- }
- return &act
-}
-
-func (rsp *resp) ErrorResult() *errorResult {
- return rsp.Result.(*errorResult)
-}
-
-func ListSnapshots(c *Command, r *http.Request, user *auth.UserState) *resp {
- return listSnapshots(c, r, user).(*resp)
-}
-
-func ChangeSnapshots(c *Command, r *http.Request, user *auth.UserState) *resp {
- return changeSnapshots(c, r, user).(*resp)
-}
-
-var (
- SnapshotMany = snapshotMany
- SnapshotCmd = snapshotCmd
-)
diff -pruN 2.37.4-1/daemon/export_test.go 2.39.2+19.10ubuntu1/daemon/export_test.go
--- 2.37.4-1/daemon/export_test.go 1970-01-01 00:00:00.000000000 +0000
+++ 2.39.2+19.10ubuntu1/daemon/export_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -0,0 +1,43 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package daemon
+
+import (
+ "net/http"
+
+ "github.com/snapcore/snapd/overlord"
+)
+
+type Resp = resp
+type ErrorResult = errorResult
+
+func NewWithOverlord(o *overlord.Overlord) *Daemon {
+ d := &Daemon{overlord: o}
+ d.addRoutes()
+ return d
+}
+
+func MockMuxVars(vars func(*http.Request) map[string]string) (restore func()) {
+ old := muxVars
+ muxVars = vars
+ return func() {
+ muxVars = old
+ }
+}
diff -pruN 2.37.4-1/daemon/response.go 2.39.2+19.10ubuntu1/daemon/response.go
--- 2.37.4-1/daemon/response.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/daemon/response.go 2019-06-05 06:41:21.000000000 +0000
@@ -331,20 +331,6 @@ func AssertResponse(asserts []asserts.As
return &assertResponse{assertions: asserts, bundle: bundle}
}
-// InterfacesUnchanged is an error responder used when an operation
-// that would normally change interfaces finds it has nothing to do
-func InterfacesUnchanged(format string, v ...interface{}) Response {
- res := &errorResult{
- Message: fmt.Sprintf(format, v...),
- Kind: errorKindInterfacesUnchanged,
- }
- return &resp{
- Type: ResponseTypeError,
- Result: res,
- Status: 400,
- }
-}
-
func (ar assertResponse) ServeHTTP(w http.ResponseWriter, r *http.Request) {
t := asserts.MediaType
if ar.bundle {
@@ -496,6 +482,20 @@ func AuthCancelled(format string, v ...i
}
}
+// InterfacesUnchanged is an error responder used when an operation
+// that would normally change interfaces finds it has nothing to do
+func InterfacesUnchanged(format string, v ...interface{}) Response {
+ res := &errorResult{
+ Message: fmt.Sprintf(format, v...),
+ Kind: errorKindInterfacesUnchanged,
+ }
+ return &resp{
+ Type: ResponseTypeError,
+ Result: res,
+ Status: 400,
+ }
+}
+
func errToResponse(err error, snaps []string, fallback func(format string, v ...interface{}) Response, format string, v ...interface{}) Response {
var kind errorKind
var snapName string
diff -pruN 2.37.4-1/daemon/snap.go 2.39.2+19.10ubuntu1/daemon/snap.go
--- 2.37.4-1/daemon/snap.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/daemon/snap.go 2019-06-05 06:41:21.000000000 +0000
@@ -39,26 +39,25 @@ import (
var errNoSnap = errors.New("snap not installed")
// snapIcon tries to find the icon inside the snap
-func snapIcon(info *snap.Info) string {
- // XXX: copy of snap.Snap.Icon which will go away
+func snapIcon(info snap.PlaceInfo) string {
found, _ := filepath.Glob(filepath.Join(info.MountDir(), "meta", "gui", "icon.*"))
if len(found) == 0 {
- return info.Media.IconURL()
+ return ""
}
return found[0]
}
-func publisherAccount(st *state.State, info *snap.Info) (*snap.StoreAccount, error) {
- if info.SnapID == "" {
- return nil, nil
+func publisherAccount(st *state.State, snapID string) (snap.StoreAccount, error) {
+ if snapID == "" {
+ return snap.StoreAccount{}, nil
}
- pubAcct, err := assertstate.Publisher(st, info.SnapID)
+ pubAcct, err := assertstate.Publisher(st, snapID)
if err != nil {
- return nil, fmt.Errorf("cannot find publisher details: %v", err)
+ return snap.StoreAccount{}, fmt.Errorf("cannot find publisher details: %v", err)
}
- return &snap.StoreAccount{
+ return snap.StoreAccount{
ID: pubAcct.AccountID(),
Username: pubAcct.Username(),
DisplayName: pubAcct.DisplayName(),
@@ -67,9 +66,8 @@ func publisherAccount(st *state.State, i
}
type aboutSnap struct {
- info *snap.Info
- snapst *snapstate.SnapState
- publisher *snap.StoreAccount
+ info *snap.Info
+ snapst *snapstate.SnapState
}
// localSnapInfo returns the information about the current snap for the given name plus the SnapState with the active flag and other snap revisions.
@@ -91,15 +89,14 @@ func localSnapInfo(st *state.State, name
return aboutSnap{}, fmt.Errorf("cannot read snap details: %v", err)
}
- publisher, err := publisherAccount(st, info)
+ info.Publisher, err = publisherAccount(st, info.SnapID)
if err != nil {
return aboutSnap{}, err
}
return aboutSnap{
- info: info,
- snapst: &snapst,
- publisher: publisher,
+ info: info,
+ snapst: &snapst,
}, nil
}
@@ -121,7 +118,6 @@ func allLocalSnapInfos(st *state.State,
}
var aboutThis []aboutSnap
var info *snap.Info
- var publisher *snap.StoreAccount
var err error
if all {
for _, seq := range snapst.Sequence {
@@ -137,15 +133,17 @@ func allLocalSnapInfos(st *state.State,
// clear the error
err = nil
}
- publisher, err = publisherAccount(st, info)
- aboutThis = append(aboutThis, aboutSnap{info, snapst, publisher})
+ info.Publisher, err = publisherAccount(st, seq.SnapID)
+ if err != nil && firstErr == nil {
+ firstErr = err
+ }
+ aboutThis = append(aboutThis, aboutSnap{info, snapst})
}
} else {
info, err = snapst.CurrentInfo()
if err == nil {
- var publisher *snap.StoreAccount
- publisher, err = publisherAccount(st, info)
- aboutThis = append(aboutThis, aboutSnap{info, snapst, publisher})
+ info.Publisher, err = publisherAccount(st, info.SnapID)
+ aboutThis = append(aboutThis, aboutSnap{info, snapst})
}
}
@@ -162,19 +160,6 @@ func allLocalSnapInfos(st *state.State,
return about, firstErr
}
-type bySnapApp []*snap.AppInfo
-
-func (a bySnapApp) Len() int { return len(a) }
-func (a bySnapApp) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
-func (a bySnapApp) Less(i, j int) bool {
- iName := a[i].Snap.InstanceName()
- jName := a[j].Snap.InstanceName()
- if iName == jName {
- return a[i].Name < a[j].Name
- }
- return iName < jName
-}
-
// this differs from snap.SplitSnapApp in the handling of the
// snap-only case:
// snap.SplitSnapApp("foo") is ("foo", "foo"),
@@ -269,67 +254,34 @@ func appInfosFor(st *state.State, names
}
}
- sort.Sort(bySnapApp(appInfos))
+ sort.Sort(cmd.BySnapApp(appInfos))
return appInfos, nil
}
func mapLocal(about aboutSnap) *client.Snap {
localSnap, snapst := about.info, about.snapst
- status := "installed"
- if snapst.Active && localSnap.Revision == snapst.Current {
- status = "active"
- }
-
- snapapps := make([]*snap.AppInfo, 0, len(localSnap.Apps))
- for _, app := range localSnap.Apps {
- snapapps = append(snapapps, app)
- }
- sort.Sort(bySnapApp(snapapps))
-
- apps, err := cmd.ClientAppInfosFromSnapAppInfos(snapapps)
+ result, err := cmd.ClientSnapFromSnapInfo(localSnap)
if err != nil {
- logger.Noticef("cannot get full app info: %v", err)
+ logger.Noticef("cannot get full app info for snap %q: %v", localSnap.InstanceName(), err)
}
+ result.InstalledSize = localSnap.Size
- // TODO: expose aliases information and state?
+ if icon := snapIcon(localSnap); icon != "" {
+ result.Icon = icon
+ }
- publisherUsername := ""
- if about.publisher != nil {
- publisherUsername = about.publisher.Username
- }
- result := &client.Snap{
- Description: localSnap.Description(),
- Developer: publisherUsername,
- Publisher: about.publisher,
- Icon: snapIcon(localSnap),
- ID: localSnap.SnapID,
- InstallDate: localSnap.InstallDate(),
- InstalledSize: localSnap.Size,
- Name: localSnap.InstanceName(),
- Revision: localSnap.Revision,
- Status: status,
- Summary: localSnap.Summary(),
- Type: string(localSnap.Type),
- Base: localSnap.Base,
- Version: localSnap.Version,
- Channel: localSnap.Channel,
- TrackingChannel: snapst.Channel,
- IgnoreValidation: snapst.IgnoreValidation,
- Confinement: string(localSnap.Confinement),
- DevMode: snapst.DevMode,
- TryMode: snapst.TryMode,
- JailMode: snapst.JailMode,
- Private: localSnap.Private,
- Apps: apps,
- Broken: localSnap.Broken,
- Contact: localSnap.Contact,
- Title: localSnap.Title(),
- License: localSnap.License,
- CommonIDs: localSnap.CommonIDs,
- MountedFrom: localSnap.MountFile(),
+ result.Status = "installed"
+ if snapst.Active && localSnap.Revision == snapst.Current {
+ result.Status = "active"
}
+ result.TrackingChannel = snapst.Channel
+ result.IgnoreValidation = snapst.IgnoreValidation
+ result.DevMode = snapst.DevMode
+ result.TryMode = snapst.TryMode
+ result.JailMode = snapst.JailMode
+ result.MountedFrom = localSnap.MountFile()
if result.TryMode {
// Readlink instead of EvalSymlinks because it's only expected
// to be one level, and should still resolve if the target does
@@ -342,43 +294,15 @@ func mapLocal(about aboutSnap) *client.S
}
func mapRemote(remoteSnap *snap.Info) *client.Snap {
- status := "available"
- if remoteSnap.MustBuy {
- status = "priced"
+ result, err := cmd.ClientSnapFromSnapInfo(remoteSnap)
+ if err != nil {
+ logger.Noticef("cannot get full app info for snap %q: %v", remoteSnap.SnapName(), err)
}
-
- confinement := remoteSnap.Confinement
- if confinement == "" {
- confinement = snap.StrictConfinement
- }
-
- publisher := remoteSnap.Publisher
- result := &client.Snap{
- Description: remoteSnap.Description(),
- Developer: remoteSnap.Publisher.Username,
- Publisher: &publisher,
- DownloadSize: remoteSnap.Size,
- Icon: snapIcon(remoteSnap),
- ID: remoteSnap.SnapID,
- Name: remoteSnap.InstanceName(),
- Revision: remoteSnap.Revision,
- Status: status,
- Summary: remoteSnap.Summary(),
- Type: string(remoteSnap.Type),
- Base: remoteSnap.Base,
- Version: remoteSnap.Version,
- Channel: remoteSnap.Channel,
- Private: remoteSnap.Private,
- Confinement: string(confinement),
- Contact: remoteSnap.Contact,
- Title: remoteSnap.Title(),
- License: remoteSnap.License,
- Screenshots: remoteSnap.Media.Screenshots(),
- Media: remoteSnap.Media,
- Prices: remoteSnap.Prices,
- Channels: remoteSnap.Channels,
- Tracks: remoteSnap.Tracks,
- CommonIDs: remoteSnap.CommonIDs,
+ result.DownloadSize = remoteSnap.Size
+ if remoteSnap.MustBuy {
+ result.Status = "priced"
+ } else {
+ result.Status = "available"
}
return result
diff -pruN 2.37.4-1/daemon/ucrednet.go 2.39.2+19.10ubuntu1/daemon/ucrednet.go
--- 2.37.4-1/daemon/ucrednet.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/daemon/ucrednet.go 2019-06-05 06:41:21.000000000 +0000
@@ -23,8 +23,8 @@ import (
"errors"
"fmt"
"net"
+ "regexp"
"strconv"
- "strings"
sys "syscall"
)
@@ -35,29 +35,23 @@ const (
ucrednetNobody = uint32((1 << 32) - 1)
)
+var raddrRegexp = regexp.MustCompile(`^pid=(\d+);uid=(\d+);socket=([^;]*);$`)
+
func ucrednetGet(remoteAddr string) (pid int32, uid uint32, socket string, err error) {
+ // NOTE treat remoteAddr at one point included a user-controlled
+ // string. In case that happens again by accident, treat it as tainted,
+ // and be very suspicious of it.
pid = ucrednetNoProcess
uid = ucrednetNobody
- for _, token := range strings.Split(remoteAddr, ";") {
- if strings.HasPrefix(token, "pid=") {
- var v int64
- if v, err = strconv.ParseInt(token[4:], 10, 32); err == nil {
- pid = int32(v)
- } else {
- break
- }
- } else if strings.HasPrefix(token, "uid=") {
- var v uint64
- if v, err = strconv.ParseUint(token[4:], 10, 32); err == nil {
- uid = uint32(v)
- } else {
- break
- }
+ subs := raddrRegexp.FindStringSubmatch(remoteAddr)
+ if subs != nil {
+ if v, err := strconv.ParseInt(subs[1], 10, 32); err == nil {
+ pid = int32(v)
}
- if strings.HasPrefix(token, "socket=") {
- socket = token[7:]
+ if v, err := strconv.ParseUint(subs[2], 10, 32); err == nil {
+ uid = uint32(v)
}
-
+ socket = subs[3]
}
if pid == ucrednetNoProcess || uid == ucrednetNobody {
err = errNoID
@@ -85,6 +79,9 @@ type ucrednetAddr struct {
}
func (wa *ucrednetAddr) String() string {
+ // NOTE we drop the original (user-supplied) net.Addr from the
+ // serialization entirely. We carry it this far so it helps debugging
+ // (via %#v logging), but from here on in it's not helpful.
return wa.ucrednet.String()
}
diff -pruN 2.37.4-1/daemon/ucrednet_test.go 2.39.2+19.10ubuntu1/daemon/ucrednet_test.go
--- 2.37.4-1/daemon/ucrednet_test.go 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/daemon/ucrednet_test.go 2019-06-05 06:41:21.000000000 +0000
@@ -144,14 +144,14 @@ func (s *ucrednetSuite) TestUcredErrors(
}
func (s *ucrednetSuite) TestGetNoUid(c *check.C) {
- pid, uid, _, err := ucrednetGet("pid=100;uid=;")
+ pid, uid, _, err := ucrednetGet("pid=100;uid=;socket=;")
c.Check(err, check.Equals, errNoID)
- c.Check(pid, check.Equals, int32(100))
+ c.Check(pid, check.Equals, ucrednetNoProcess)
c.Check(uid, check.Equals, ucrednetNobody)
}
func (s *ucrednetSuite) TestGetBadUid(c *check.C) {
- pid, uid, _, err := ucrednetGet("pid=100;uid=hello;")
+ pid, uid, _, err := ucrednetGet("pid=100;uid=4294967296;socket=;")
c.Check(err, check.NotNil)
c.Check(pid, check.Equals, int32(100))
c.Check(uid, check.Equals, ucrednetNobody)
@@ -172,9 +172,17 @@ func (s *ucrednetSuite) TestGetNothing(c
}
func (s *ucrednetSuite) TestGet(c *check.C) {
- pid, uid, socket, err := ucrednetGet("pid=100;uid=42;socket=/run/snap.socket")
+ pid, uid, socket, err := ucrednetGet("pid=100;uid=42;socket=/run/snap.socket;")
c.Check(err, check.IsNil)
c.Check(pid, check.Equals, int32(100))
c.Check(uid, check.Equals, uint32(42))
c.Check(socket, check.Equals, "/run/snap.socket")
}
+
+func (s *ucrednetSuite) TestGetSneak(c *check.C) {
+ pid, uid, socket, err := ucrednetGet("pid=100;uid=42;socket=/run/snap.socket;pid=0;uid=0;socket=/tmp/my.socket")
+ c.Check(err, check.Equals, errNoID)
+ c.Check(pid, check.Equals, ucrednetNoProcess)
+ c.Check(uid, check.Equals, ucrednetNobody)
+ c.Check(socket, check.Equals, "")
+}
diff -pruN 2.37.4-1/data/selinux/snappy.fc 2.39.2+19.10ubuntu1/data/selinux/snappy.fc
--- 2.37.4-1/data/selinux/snappy.fc 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/data/selinux/snappy.fc 2019-06-05 06:41:21.000000000 +0000
@@ -18,30 +18,38 @@
HOME_DIR/snap(/.*)? gen_context(system_u:object_r:snappy_home_t,s0)
+/root/snap(/.*)? gen_context(system_u:object_r:snappy_home_t,s0)
-
-/usr/bin/snap -- gen_context(system_u:object_r:snappy_exec_t,s0)
-/usr/bin/snapctl -- gen_context(system_u:object_r:snappy_exec_t,s0)
+/usr/bin/snap -- gen_context(system_u:object_r:snappy_cli_exec_t,s0)
+/usr/bin/snapctl -- gen_context(system_u:object_r:snappy_cli_exec_t,s0)
ifdef(`distro_redhat',`
+/usr/libexec/snapd/snapctl -- gen_context(system_u:object_r:snappy_cli_exec_t,s0)
+/usr/libexec/snapd/snap-confine -- gen_context(system_u:object_r:snappy_confine_exec_t,s0)
+/usr/libexec/snapd/snap-update-ns -- gen_context(system_u:object_r:snappy_mount_exec_t,s0)
+/usr/libexec/snapd/snap-discard-ns -- gen_context(system_u:object_r:snappy_mount_exec_t,s0)
/usr/libexec/snapd/.* -- gen_context(system_u:object_r:snappy_exec_t,s0)
/etc/sysconfig/snapd -- gen_context(system_u:object_r:snappy_config_t,s0)
/usr/lib/systemd/system/snapd.* -- gen_context(system_u:object_r:snappy_unit_file_t,s0)
')
ifdef(`distro_debian',`
+/usr/lib/snapd/snapctl -- gen_context(system_u:object_r:snappy_cli_exec_t,s0)
+/usr/lib/snapd/snap-confine -- gen_context(system_u:object_r:snappy_confine_exec_t,s0)
+/usr/lib/snapd/snap-update-ns -- gen_context(system_u:object_r:snappy_mount_exec_t,s0)
+/usr/lib/snapd/snap-discard-ns -- gen_context(system_u:object_r:snappy_mount_exec_t,s0)
/usr/lib/snapd/.* -- gen_context(system_u:object_r:snappy_exec_t,s0)
/etc/default/snapd -- gen_context(system_u:object_r:snappy_config_t,s0)
/lib/systemd/system/snapd.* -- gen_context(system_u:object_r:snappy_unit_file_t,s0)
')
-/var/run/snapd(/.*)? -- gen_context(system_u:object_r:snappy_var_run_t,s0)
+/var/run/snapd(/.*)? gen_context(system_u:object_r:snappy_var_run_t,s0)
/var/run/snapd\.socket -s gen_context(system_u:object_r:snappy_var_run_t,s0)
/var/run/snapd-snap\.socket -s gen_context(system_u:object_r:snappy_var_run_t,s0)
/var/lib/snapd(/.*)? gen_context(system_u:object_r:snappy_var_lib_t,s0)
/var/cache/snapd(/.*)? gen_context(system_u:object_r:snappy_var_cache_t,s0)
/var/snap(/.*)? gen_context(system_u:object_r:snappy_var_t,s0)
-/run/snapd(/.*)? -- gen_context(system_u:object_r:snappy_var_run_t,s0)
+/run/snapd(/.*)? gen_context(system_u:object_r:snappy_var_run_t,s0)
/run/snapd\.socket -s gen_context(system_u:object_r:snappy_var_run_t,s0)
/run/snapd-snap\.socket -s gen_context(system_u:object_r:snappy_var_run_t,s0)
diff -pruN 2.37.4-1/data/selinux/snappy.if 2.39.2+19.10ubuntu1/data/selinux/snappy.if
--- 2.37.4-1/data/selinux/snappy.if 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/data/selinux/snappy.if 2019-06-05 06:41:21.000000000 +0000
@@ -184,43 +184,6 @@ interface(`snappy_dontaudit_manage_user_
########################################
##
-## Execute snappy home directory content.
-##
-##
-##
-## Domain allowed access.
-##
-##
-#
-interface(`snappy_exec_user_home_files',`
- gen_require(`
- type snappy_home_t;
- ')
-
- can_exec($1, snappy_home_t)
-')
-
-########################################
-##
-## Execmod snappy home directory content.
-##
-##
-##
-## Domain allowed access.
-##
-##
-#
-interface(`snappy_execmod_user_home_files',`
- gen_require(`
- type snappy_home_t;
- ')
-
- allow $1 snappy_home_t:file execmod;
-')
-
-
-########################################
-##
## Connect to snapd over a unix stream socket.
##
##
@@ -255,7 +218,6 @@ interface(`snappy_stream_connect',`
##
##
#
-
interface(`snappy_admin',`
gen_require(`
type snappy_t, snappy_config_t;
@@ -271,3 +233,60 @@ interface(`snappy_admin',`
files_list_pids($1, snappy_var_run_t);
admin_pattern($1, snappy_var_run_t);
')
+
+########################################
+##
+## Execute snappy CLI in the snappy_cli_t domain.
+##
+##
+##
+## Domain allowed to transition.
+##
+##
+#
+interface(`snappy_cli_domtrans',`
+ gen_require(`
+ type snappy_cli_t, snappy_cli_exec_t;
+ ')
+
+ corecmd_search_bin($1)
+ domtrans_pattern($1, snappy_cli_exec_t, snappy_cli_t)
+')
+
+########################################
+##
+## Execute snap-confine in the snappy_confine_t domain.
+##
+##
+##
+## Domain allowed to transition.
+##
+##
+#
+interface(`snappy_confine_domtrans',`
+ gen_require(`
+ type snappy_confine_t, snappy_confine_exec_t;
+ ')
+
+ corecmd_search_bin($1)
+ domtrans_pattern($1, snappy_confine_exec_t, snappy_confine_t)
+')
+
+########################################
+##
+## Execute snap-update-ns, snap-discard-ns in the snappy_mount_t domain.
+##
+##
+##
+## Domain allowed to transition.
+##
+##
+#
+interface(`snappy_mount_domtrans',`
+ gen_require(`
+ type snappy_mount_t, snappy_mount_exec_t;
+ ')
+
+ corecmd_search_bin($1)
+ domtrans_pattern($1, snappy_mount_exec_t, snappy_mount_t)
+')
diff -pruN 2.37.4-1/data/selinux/snappy.te 2.39.2+19.10ubuntu1/data/selinux/snappy.te
--- 2.37.4-1/data/selinux/snappy.te 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/data/selinux/snappy.te 2019-06-05 06:41:21.000000000 +0000
@@ -24,9 +24,13 @@ policy_module(snappy,0.0.14)
# Declarations
#
+attribute_role snappy_roles;
+
+# snapd
type snappy_t;
type snappy_exec_t;
init_daemon_domain(snappy_t, snappy_exec_t)
+role snappy_roles types snappy_t;
type snappy_config_t;
files_config_file(snappy_config_t)
@@ -51,15 +55,49 @@ files_pid_file(snappy_var_run_t)
type snappy_unit_file_t;
systemd_unit_file(snappy_unit_file_t)
+type snappy_tmp_t;
+files_tmp_file(snappy_tmp_t)
+
+# actual snap
+type snappy_snap_t;
+# XXX: snappy_snap_t should be declared a filesystem, but due to how modules are
+# handled we cannot generate required contexts with genfscon outside of core
+# policy
+# fs_type(snappy_snap_t)
+files_type(snappy_snap_t)
+files_mountpoint(snappy_snap_t)
+
+# CLI tools: snap, snapctl
+type snappy_cli_t;
+type snappy_cli_exec_t;
+domain_type(snappy_cli_t)
+domain_entry_file(snappy_cli_t, snappy_cli_exec_t)
+
+# helper tools: snap-{update,discard}-ns
+type snappy_mount_t;
+type snappy_mount_exec_t;
+domain_type(snappy_mount_t)
+domain_entry_file(snappy_mount_t, snappy_mount_exec_t)
+
+# helper tool: snap-confine
+type snappy_confine_t;
+type snappy_confine_exec_t;
+domain_type(snappy_confine_t)
+domain_entry_file(snappy_confine_t, snappy_confine_exec_t)
+
+type snappy_unconfined_snap_t;
+unconfined_domain(snappy_unconfined_snap_t)
+
########################################
#
-# snappy local policy
+# snappy snapd local policy
#
# For development purposes, snappy_t domain is to be marked permissive
permissive snappy_t;
# Allow transitions from init_t to snappy for sockets
+# init_named_socket_activation() is not supported by core policy in RHEL7
gen_require(` type init_t; type var_run_t; ')
filetrans_pattern(init_t, var_run_t, snappy_var_run_t, sock_file, "snapd.socket")
filetrans_pattern(init_t, var_run_t, snappy_var_run_t, sock_file, "snapd-snap.socket")
@@ -67,113 +105,94 @@ filetrans_pattern(init_t, var_run_t, sna
# Allow init_t to read snappy data
allow init_t snappy_var_lib_t:dir read;
-# Allow snapd to read init socket
-gen_require(` type init_t; ')
-allow snappy_t init_t:file { getattr open read };
-allow snappy_t init_t:lnk_file read;
-allow snappy_t init_t:unix_stream_socket connectto;
-
-# Allow snapd to read file contexts
-gen_require(` type file_context_t; ')
-allow snappy_t file_context_t:dir search;
-allow snappy_t file_context_t:file { map getattr open read };
-
# Allow snapd to read procfs
gen_require(` type proc_t; ')
allow snappy_t proc_t:file { getattr open read };
# Allow snapd to read sysfs
-gen_require(` type sysfs_t; ')
-allow snappy_t sysfs_t:dir read;
-allow snappy_t sysfs_t:file { getattr setattr open read write };
-allow snappy_t sysfs_t:lnk_file { getattr read };
-
+dev_read_sysfs(snappy_t)
+dev_search_sysfs(snappy_t)
# This silences a read AVC denial event on the lost+found directory.
gen_require(` type lost_found_t; ')
dontaudit snappy_t lost_found_t:dir read;
# Allow snapd to read SSL cert store
-gen_require(` type cert_t; ')
-allow snappy_t cert_t:dir { search open read };
-allow snappy_t cert_t:file { getattr open read };
-allow snappy_t cert_t:lnk_file { getattr open read };
+miscfiles_read_all_certs(snappy_t)
# Allow snapd to read config files
read_files_pattern(snappy_t, snappy_config_t, snappy_config_t)
# Allow snapd to manage snaps' homedir data
-manage_dirs_pattern(snappy_t, snappy_home_t, snappy_home_t)
-manage_files_pattern(snappy_t, snappy_home_t, snappy_home_t)
-manage_lnk_files_pattern(snappy_t, snappy_home_t, snappy_home_t)
+admin_pattern(snappy_t, snappy_home_t)
userdom_search_user_home_dirs(snappy_t)
-userdom_user_home_dir_filetrans(snappy_t, snappy_home_t, dir, "snap")
+userdom_list_user_home_dirs(snappy_t)
# Allow snapd to read DNS config
-gen_require(` type net_conf_t; ')
-allow snappy_t net_conf_t:file { getattr open read };
-allow snappy_t net_conf_t:lnk_file { read };
+sysnet_dns_name_resolve(snappy_t)
# When managed by NetworkManager, DNS config is in its rundata
gen_require(` type NetworkManager_var_run_t; ')
allow snappy_t NetworkManager_var_run_t:dir search;
# Allow snapd to read sysctl files
-gen_require(` type sysctl_net_t; ')
-allow snappy_t sysctl_net_t:dir search;
-allow snappy_t sysctl_net_t:file { open read };
+kernel_read_net_sysctls(snappy_t)
+kernel_search_network_sysctl(snappy_t)
+
+# Allow snapd to query SELinux status
+selinux_get_enforce_mode(snappy_t)
# Allow snapd to manage D-Bus config files for snaps
-gen_require(` type dbusd_etc_t; ')
-allow snappy_t dbusd_etc_t:dir { getattr open read search };
-allow snappy_t dbusd_etc_t:file { getattr open read write create rename unlink };
-allow snappy_t dbusd_etc_t:lnk_file { read };
-
-# Allow snapd to manage udev rules for snaps
-gen_require(` type udev_rules_t; type udev_exec_t; ')
-allow snappy_t udev_rules_t:dir { getattr open read write search add_name remove_name };
-allow snappy_t udev_rules_t:file { getattr open read write create rename unlink };
-allow snappy_t udev_exec_t:file { execute execute_no_trans getattr open read map };
-
-# Allow snapd to manipulate udev
-gen_require(` type udev_t; type udev_var_run_t; ')
-allow snappy_t udev_t:unix_stream_socket connectto;
-allow snappy_t udev_var_run_t:file { getattr open read };
-allow snappy_t udev_var_run_t:dir { read };
-allow snappy_t udev_var_run_t:sock_file { getattr open read write };
+optional_policy(`
+ dbus_read_config(snappy_t)
+ allow snappy_t dbusd_etc_t:file { write create rename unlink };
+ allow snappy_t dbusd_etc_t:lnk_file { read };
+')
+
+# Allow snapd to manage udev rules for snaps and trigger events
+optional_policy(`
+ udev_manage_rules_files(snappy_t)
+ udev_manage_pid_files(snappy_t)
+ udev_exec(snappy_t)
+ udev_domtrans(snappy_t)
+ udev_create_kobject_uevent_socket(snappy_t)
+')
+allow snappy_t self:netlink_kobject_uevent_socket { create_socket_perms read };
# Allow snapd to read/write systemd units and use systemctl for managing snaps
systemd_config_all_services(snappy_t)
systemd_manage_all_unit_files(snappy_t)
systemd_manage_all_unit_lnk_files(snappy_t)
systemd_exec_systemctl(snappy_t)
-
-# Allow snapd to mount snaps
-gen_require(` type mount_exec_t; ')
-allow snappy_t mount_exec_t:file { map execute execute_no_trans getattr open read };
+systemd_reload_all_services(snappy_t)
+init_reload_services(snappy_t)
+init_enable_services(snappy_t)
+init_disable_services(snappy_t)
# Allow snapd to execute unsquashfs
-gen_require(` type bin_t; ')
-allow snappy_t bin_t:file { map execute execute_no_trans };
+corecmd_exec_bin(snappy_t)
-# Allow snappy to exec snap-seccomp
-allow snappy_t snappy_exec_t:file { execute_no_trans };
+# Allow snappy to exec helpers
+can_exec(snappy_t, snappy_exec_t)
+can_exec(snappy_t, snappy_mount_exec_t)
+can_exec(snappy_t, snappy_cli_exec_t)
+corecmd_search_bin(snappy_t)
+# allow transition to snap cli domain
+snappy_cli_domtrans(snappy_t)
+# allow transition to mount helpers domain
+snappy_mount_domtrans(snappy_t)
+# allow transition to snap-confine domain
+snappy_confine_domtrans(snappy_t)
# Allow snapd to get FUSE device attributes
-gen_require(` type fuse_device_t; ')
-allow snappy_t fuse_device_t:chr_file getattr;
+storage_getattr_fuse_dev(snappy_t)
# Read l10n files?
miscfiles_read_localization(snappy_t)
-# Allow snapd to manage its socket files
-manage_files_pattern(snappy_t, snappy_var_run_t, snappy_var_run_t)
-
-# Allow snapd to manage mounts
-gen_require(` type fs_t; type mount_var_run_t; ')
-allow snappy_t fs_t:filesystem { mount unmount };
-allow snappy_t mount_var_run_t:dir { add_name remove_name write search };
-allow snappy_t mount_var_run_t:file { create getattr setattr open read write rename unlink lock };
+# Allow snapd to read its run files, those files are managed elsewhere
+read_files_pattern(snappy_t, snappy_var_run_t, snappy_var_run_t)
+getattr_files_pattern(snappy_t, snappy_var_run_t, snappy_var_run_t)
gen_require(` type user_tmp_t; ')
allow snappy_t user_tmp_t:dir { read };
@@ -181,62 +200,52 @@ allow snappy_t user_tmp_t:dir { read };
gen_require(` type systemd_unit_file_t; ')
allow snappy_t systemd_unit_file_t:dir { rmdir };
-gen_require(` type fixed_disk_device_t; ')
-allow snappy_t fixed_disk_device_t:blk_file { getattr };
-
-gen_require(` type loop_control_device_t; ')
-allow snappy_t loop_control_device_t:chr_file { getattr };
-
-gen_require(` type usr_t; ')
-allow snappy_t usr_t:dir { write };
-
gen_require(` type home_root_t; ')
allow snappy_t home_root_t:dir { read };
# Allow snapd to manage its persistent data
-manage_dirs_pattern(snappy_t, snappy_var_cache_t, snappy_var_cache_t)
-manage_files_pattern(snappy_t, snappy_var_cache_t, snappy_var_cache_t)
-manage_lnk_files_pattern(snappy_t, snappy_var_cache_t, snappy_var_cache_t)
-manage_dirs_pattern(snappy_t, snappy_var_lib_t, snappy_var_lib_t)
-manage_files_pattern(snappy_t, snappy_var_lib_t, snappy_var_lib_t)
-manage_lnk_files_pattern(snappy_t, snappy_var_lib_t, snappy_var_lib_t)
-manage_dirs_pattern(snappy_t, snappy_var_t, snappy_var_t)
-manage_files_pattern(snappy_t, snappy_var_t, snappy_var_t)
-manage_lnk_files_pattern(snappy_t, snappy_var_t, snappy_var_t)
+admin_pattern(snappy_t, snappy_var_cache_t)
+# for r/w to commands.db
+mmap_rw_files_pattern(snappy_t, snappy_var_cache_t, snappy_var_cache_t)
+admin_pattern(snappy_t, snappy_var_lib_t)
+# for r/w to errtracker.db
+mmap_rw_files_pattern(snappy_t, snappy_var_lib_t, snappy_var_lib_t)
+# snap data files
+admin_pattern(snappy_t, snappy_var_t)
+# auto transition /var/snap when created at runtime
+files_var_filetrans(snappy_t, snappy_var_t, dir, "snap")
+# some snaps may create character files, eg. lxd creates /dev/full in the
+# container's rootfs
+manage_chr_files_pattern(snappy_t, snappy_var_t, snappy_var_t)
+# And search/read mounted snaps
+allow snappy_t snappy_snap_t:dir { list_dir_perms };
+allow snappy_t snappy_snap_t:file { read_file_perms };
+allow snappy_t snappy_snap_t:lnk_file { read_lnk_file_perms };
# Grant snapd access to /tmp
-gen_require(` type tmp_t; ')
-allow snappy_t tmp_t:dir { getattr setattr add_name create read remove_name rmdir write };
-allow snappy_t tmp_t:file { getattr setattr create open unlink write };
+admin_pattern(snappy_t, snappy_tmp_t)
+files_tmp_filetrans(snappy_t, snappy_tmp_t, { file dir })
-# Allow snappy to mmap files in /var/cache
-allow snappy_t snappy_var_cache_t:file { map };
+# snap command completions, symlinks going back to snap mount directory
+gen_require(` type usr_t; ')
+allow snappy_t usr_t:dir { write remove_name add_name };
+allow snappy_t usr_t:lnk_file { create unlink };
# Allow snapd to use ssh-keygen
-gen_require(` type ssh_keygen_exec_t; ')
-allow snappy_t ssh_keygen_exec_t:file { execute execute_no_trans getattr open read map };
+ssh_exec_keygen(snappy_t)
# Allow snapd to access passwd file for lookup
-auth_read_passwd(snappy_t);
-
-# Until we can figure out how to apply the label to mounted snaps,
-# we need to grant snapd access to "unlabeled files"
-gen_require(` type unlabeled_t; ')
-allow snappy_t unlabeled_t:dir { getattr search open read };
-allow snappy_t unlabeled_t:lnk_file { getattr read };
-allow snappy_t unlabeled_t:file { getattr open read };
+auth_read_passwd(snappy_t)
-# Until we can figure out why some things are randomly getting unconfined_t,
-# we need to grant access to "unconfined" files
+# because /run/snapd/ns/*.mnt gets a label of the process context
gen_require(` type unconfined_t; ')
-allow snappy_t unconfined_t:dir { getattr search open read };
-allow snappy_t unconfined_t:file { getattr open read };
+allow snappy_t unconfined_t:file getattr;
+allow snappy_t snappy_confine_t:file getattr;
-logging_send_syslog_msg(snappy_t);
+logging_send_syslog_msg(snappy_t)
-allow snappy_t self:capability { sys_admin sys_chroot dac_override dac_read_search chown kill fowner fsetid mknod net_admin net_bind_service net_raw setfcap };
-allow snappy_t self:tun_socket relabelto;
-allow snappy_t self:process { getcap signal_perms setrlimit setfscreate };
+allow snappy_t self:capability { dac_read_search dac_override fowner };
+allow snappy_t self:process { setpgid };
# Various socket permissions
allow snappy_t self:fifo_file rw_fifo_file_perms;
@@ -282,3 +291,337 @@ optional_policy(`
sssd_read_public_files(snappy_t)
sssd_stream_connect(snappy_t)
')
+
+# for sanity checks
+optional_policy(`
+ mount_run(snappy_t, snappy_roles)
+')
+
+# snapd runs journalctl to fetch logs
+optional_policy(`
+ journalctl_run(snappy_t, snappy_roles)
+')
+
+# only pops up in cloud images where cloud-init.target is incorrectly labeled
+allow snappy_t init_var_run_t:lnk_file read;
+
+# snapd may need to run helpers from the snaps, eg. fontconfig helper
+can_exec(snappy_t, snappy_snap_t)
+# fontconfig cache setup
+miscfiles_manage_fonts_cache(snappy_t)
+files_var_filetrans(snappy_t, fonts_cache_t, dir, "fontconfig")
+# fontconfig cache is in /usr/lib/fontconfig/cache, but its labeling is
+# incorrect, see https://bugzilla.redhat.com/show_bug.cgi?id=1659905
+libs_manage_lib_dirs(snappy_t)
+libs_manage_lib_files(snappy_t)
+fs_getattr_xattr_fs(snappy_t)
+
+# snapd attempts to read /run/cloud-init/instance-data.json
+sysnet_read_config(snappy_t)
+# however older policy may be missing the transition rules, and
+# /run/cloud-init/instance-data.json ends up as var_run_t
+files_read_generic_pids(snappy_t)
+
+# snapd attempts to check /proc/sys/fs/may_detach_mounts during sanity testing
+kernel_read_fs_sysctls(snappy_t)
+
+# socket activated services may have their socket files created under
+# $SNAP_COMMON, but lacking auto transition, they end up labeled as var_t
+allow snappy_t var_t:sock_file unlink;
+
+# snapd picks the process start time from /proc//stat for polkit
+allow snappy_t unconfined_t:dir search;
+allow snappy_t unconfined_t:file { open read };
+
+# snapd executes runuser to take snapshots of user's snap data
+allow snappy_t self:capability { setuid setgid };
+allow snappy_t self:process setsched;
+# runuser uses kernel keyring
+allow snappy_t self:key { search write };
+# runuser logs to audit
+logging_send_audit_msgs(snappy_t)
+
+########################################
+#
+# snap-update-ns, snap-dicsard-ns local policy
+#
+permissive snappy_mount_t;
+
+role system_r types snappy_mount_t;
+
+admin_pattern(snappy_mount_t, snappy_var_run_t)
+files_pid_filetrans(snappy_mount_t, snappy_var_run_t, {file dir})
+
+# Allow snap-{update,discard}-ns to manage mounts
+gen_require(` type fs_t; type mount_var_run_t; ')
+allow snappy_mount_t fs_t:filesystem { mount unmount };
+allow snappy_mount_t mount_var_run_t:dir { add_name remove_name write search };
+allow snappy_mount_t mount_var_run_t:file { create getattr setattr open read write rename unlink lock };
+# for discard-ns, because a preserved mount ns is a bind-mounted /proc//ns/mnt
+gen_require(` type proc_t; ')
+allow snappy_mount_t proc_t:filesystem { getattr unmount };
+
+allow snappy_mount_t self:capability { sys_chroot sys_admin };
+
+manage_files_pattern(snappy_mount_t, snappy_snap_t, snappy_snap_t)
+manage_dirs_pattern(snappy_mount_t, snappy_snap_t, snappy_snap_t)
+
+read_files_pattern(snappy_mount_t, snappy_var_lib_t, snappy_var_lib_t)
+getattr_files_pattern(snappy_mount_t, snappy_var_lib_t, snappy_var_lib_t)
+read_lnk_files_pattern(snappy_mount_t, snappy_var_lib_t, snappy_var_lib_t)
+
+fs_getattr_all_fs(snappy_mount_t)
+fs_getattr_tmpfs(snappy_mount_t)
+fs_getattr_xattr_fs(snappy_mount_t)
+# freezer
+fs_manage_cgroup_dirs(snappy_mount_t)
+fs_manage_cgroup_files(snappy_mount_t)
+# TODO: further tweaks may be needed for layouts
+# reading tmpfs symlinks, eg. /etc/os-release
+fs_read_tmpfs_symlinks(snappy_mount_t)
+
+# because /run/snapd/ns/*.mnt gets a label of the process context
+gen_require(` type unconfined_t; ')
+allow snappy_mount_t unconfined_t:file { open read getattr };
+allow snappy_mount_t snappy_confine_t:file { open read getattr };
+
+# Go runtime artifacts
+kernel_read_system_state(snappy_mount_t)
+kernel_read_net_sysctls(snappy_mount_t)
+kernel_search_network_sysctl(snappy_mount_t)
+
+########################################
+#
+# snap-confine local policy
+#
+permissive snappy_confine_t;
+
+role system_r types snappy_confine_t;
+snappy_mount_domtrans(snappy_confine_t)
+allow snappy_confine_t snappy_mount_t:process2 nosuid_transition;
+
+admin_pattern(snappy_confine_t, snappy_var_run_t)
+
+allow snappy_confine_t snappy_var_lib_t:dir { list_dir_perms };
+allow snappy_confine_t snappy_var_lib_t:file { read_file_perms };
+allow snappy_confine_t snappy_var_lib_t:lnk_file { read_lnk_file_perms };
+
+files_pid_filetrans(snappy_confine_t, snappy_var_run_t, {file dir})
+
+allow snappy_confine_t snappy_home_t:dir { create_dir_perms list_dir_perms add_entry_dir_perms };
+allow snappy_confine_t snappy_home_t:file { read_file_perms };
+allow snappy_confine_t snappy_home_t:lnk_file { manage_lnk_file_perms };
+userdom_user_home_dir_filetrans(snappy_confine_t, snappy_home_t, dir, "snap")
+userdom_admin_home_dir_filetrans(snappy_confine_t, snappy_home_t, dir, "snap")
+
+allow snappy_confine_t snappy_snap_t:process transition;
+
+allow snappy_confine_t self:process { setexec };
+allow snappy_confine_t self:capability { setgid setuid sys_admin sys_chroot dac_read_search dac_override };
+
+init_read_state(snappy_confine_t)
+
+# libudev
+udev_manage_pid_dirs(snappy_confine_t)
+
+# basic access to system info in /proc
+kernel_read_system_state(snappy_confine_t)
+
+fs_getattr_all_fs(snappy_confine_t)
+dev_getattr_fs(snappy_confine_t)
+dev_getattr_sysfs_fs(snappy_confine_t)
+fs_getattr_cgroup(snappy_confine_t)
+fs_getattr_hugetlbfs(snappy_confine_t)
+fs_getattr_tmpfs(snappy_confine_t)
+fs_getattr_xattr_fs(snappy_confine_t)
+fs_manage_cgroup_dirs(snappy_confine_t)
+fs_write_cgroup_files(snappy_confine_t)
+kernel_getattr_debugfs(snappy_confine_t)
+kernel_getattr_proc(snappy_confine_t)
+term_getattr_pty_fs(snappy_confine_t)
+# term_getattr_generic_ptys() is not supported by core policy in RHEL7
+allow snappy_confine_t devpts_t:chr_file getattr;
+term_search_ptys(snappy_confine_t)
+
+# because /run/snapd/ns/*.mnt gets a label of the process context
+allow snappy_confine_t unconfined_t:file getattr;
+
+# mount ns setup
+gen_require(`
+ type ptmx_t;
+ type modules_object_t;
+ type ifconfig_var_run_t;
+ type var_log_t;
+')
+allow snappy_confine_t admin_home_t:dir mounton;
+allow snappy_confine_t bin_t:dir mounton;
+allow snappy_confine_t cert_t:dir { getattr mounton };
+allow snappy_confine_t device_t:filesystem unmount;
+allow snappy_confine_t devpts_t:dir mounton;
+allow snappy_confine_t etc_t:file mounton;
+allow snappy_confine_t home_root_t:dir mounton;
+allow snappy_confine_t ifconfig_var_run_t:dir mounton;
+allow snappy_confine_t modules_object_t:dir mounton;
+allow snappy_confine_t ptmx_t:chr_file { getattr mounton };
+allow snappy_confine_t snappy_snap_t:dir { mounton read };
+allow snappy_confine_t snappy_snap_t:file mounton;
+allow snappy_confine_t snappy_snap_t:lnk_file read;
+allow snappy_confine_t snappy_var_lib_t:dir mounton;
+allow snappy_confine_t snappy_var_run_t:dir mounton;
+allow snappy_confine_t snappy_var_run_t:file mounton;
+allow snappy_confine_t snappy_var_t:dir { getattr mounton search };
+allow snappy_confine_t tmp_t:dir { add_name create mounton remove_name rmdir setattr write read };
+allow snappy_confine_t usr_t:dir mounton;
+allow snappy_confine_t var_log_t:dir mounton;
+allow snappy_confine_t var_run_t:dir mounton;
+dev_mounton(snappy_confine_t)
+dev_mounton_sysfs(snappy_confine_t)
+dev_unmount_sysfs_fs(snappy_confine_t)
+files_mounton_etc(snappy_confine_t)
+files_mounton_mnt(snappy_confine_t)
+files_mounton_rootfs(snappy_confine_t)
+fs_unmount_xattr_fs(snappy_confine_t)
+kernel_mounton_proc(snappy_confine_t)
+kernel_unmount_proc(snappy_confine_t)
+seutil_read_file_contexts(snappy_confine_t)
+term_mount_pty_fs(snappy_confine_t)
+
+# device group
+fs_manage_cgroup_dirs(snappy_confine_t)
+fs_manage_cgroup_files(snappy_confine_t)
+
+# restoring file contexts
+seutil_read_file_contexts(snappy_confine_t)
+seutil_read_default_contexts(snappy_confine_t)
+seutil_read_config(snappy_confine_t)
+
+can_exec(snappy_confine_t, snappy_snap_t)
+read_files_pattern(snappy_confine_t, snappy_snap_t, snappy_snap_t)
+# and allow transition by snap-confine
+allow snappy_confine_t snappy_unconfined_snap_t:process { noatsecure rlimitinh siginh transition dyntransition };
+gen_require(` type unconfined_service_t; ')
+allow snappy_confine_t unconfined_service_t:process { noatsecure rlimitinh siginh transition dyntransition };
+
+########################################
+#
+# snap, snapctl local policy
+#
+permissive snappy_cli_t;
+
+role system_r types snappy_cli_t;
+snappy_confine_domtrans(snappy_cli_t)
+# services are started through 'snap run ...' wrapper
+snappy_cli_domtrans(init_t)
+
+relabel_dirs_pattern(snappy_cli_t, user_home_t, snappy_home_t)
+relabel_files_pattern(snappy_cli_t, user_home_t, snappy_home_t)
+relabel_dirs_pattern(snappy_cli_t, admin_home_t, snappy_home_t)
+relabel_files_pattern(snappy_cli_t, admin_home_t, snappy_home_t)
+
+manage_files_pattern(snappy_cli_t, snappy_home_t, snappy_home_t)
+manage_lnk_files_pattern(snappy_cli_t, snappy_home_t, snappy_home_t)
+manage_dirs_pattern(snappy_cli_t, snappy_home_t, snappy_home_t)
+userdom_user_home_dir_filetrans(snappy_cli_t, snappy_home_t, dir, "snap")
+userdom_admin_home_dir_filetrans(snappy_cli_t, snappy_home_t, dir, "snap")
+
+allow snappy_cli_t snappy_snap_t:dir {list_dir_perms };
+allow snappy_cli_t snappy_snap_t:file { read_file_perms };
+allow snappy_cli_t snappy_snap_t:lnk_file { read_lnk_file_perms };
+
+allow snappy_cli_t snappy_var_lib_t:dir { list_dir_perms };
+allow snappy_cli_t snappy_var_lib_t:file { read_file_perms };
+allow snappy_cli_t snappy_var_lib_t:lnk_file { read_lnk_file_perms };
+
+# allow reading passwd
+auth_read_passwd(snappy_cli_t)
+# allow reading sssd files
+optional_policy(`
+ sssd_read_public_files(snappy_cli_t)
+ sssd_stream_connect(snappy_cli_t)
+')
+
+# restorecon, matchpathcon
+seutil_domtrans_setfiles(snappy_cli_t)
+seutil_read_file_contexts(snappy_cli_t)
+seutil_read_default_contexts(snappy_cli_t)
+seutil_read_config(snappy_cli_t)
+selinux_load_policy(snappy_cli_t)
+selinux_validate_context(snappy_cli_t)
+corecmd_exec_bin(snappy_cli_t)
+
+allow snappy_cli_t proc_t:file { getattr open read };
+allow snappy_cli_t snappy_exec_t:file { read_file_perms };
+allow snappy_cli_t self:capability { dac_override };
+
+# go runtime poking at things
+init_ioctl_stream_sockets(snappy_cli_t)
+kernel_read_net_sysctls(snappy_cli_t)
+kernel_search_network_sysctl(snappy_cli_t)
+
+# talk to snapd
+snappy_stream_connect(snappy_cli_t)
+
+# check stuff in /run/user
+userdom_search_user_tmp_dirs(snappy_cli_t)
+
+# execute snapd internal tools
+# needed to grab a version information from snap-seccomp
+can_exec(snappy_cli_t, snappy_exec_t)
+
+########################################
+#
+# snappy (unconfined snap) local policy
+#
+permissive snappy_unconfined_snap_t;
+
+# allow unconfined snap service to run as a system service
+role system_r types snappy_unconfined_snap_t;
+can_exec(snappy_unconfined_snap_t, snappy_snap_t)
+domain_entry_file(snappy_unconfined_snap_t, snappy_snap_t)
+domain_entry_file(unconfined_service_t, snappy_snap_t)
+
+# for journald
+gen_require(` type syslogd_t; ')
+allow syslogd_t snappy_unconfined_snap_t:dir search_dir_perms;
+
+allow snappy_unconfined_snap_t self:process { fork getsched };
+
+# allow snappy_unconfined_snap_t snappy_snap_t:dir { list_dir_perms };
+# allow snappy_unconfined_snap_t snappy_snap_t:file { read_file_perms };
+# allow snappy_unconfined_snap_t snappy_snap_t:lnk_file { read_lnk_file_perms };
+
+# snap can carry services, which are then started by systemd, need to allow
+# systemd to manage them
+allow init_t snappy_unconfined_snap_t:dir search_dir_perms;
+allow init_t snappy_unconfined_snap_t:file { read_file_perms };
+allow init_t snappy_unconfined_snap_t:lnk_file { read_lnk_file_perms };
+allow init_t snappy_unconfined_snap_t:process { sigkill signull signal };
+
+########################################
+#
+# file/dir transitions for unconfined_t
+#
+
+# snap tools can be invoked by the regular user, make sure that things get
+# proper labels
+gen_require(` type unconfined_t; ')
+userdom_user_home_dir_filetrans(unconfined_t, snappy_home_t, dir, "snap")
+userdom_admin_home_dir_filetrans(unconfined_t, snappy_home_t, dir, "snap")
+files_pid_filetrans(unconfined_t, snappy_var_run_t, dir, "snapd")
+
+########################################
+#
+# extra policy for init_t
+#
+
+# support socket activation of snap services, for such snaps the socket file can
+# only be located under $SNAP_DATA or $SNAP_COMMON, both labeled as snappy_var_t;
+allow init_t snappy_var_t:dir manage_dir_perms;
+allow init_t snappy_var_t:sock_file manage_sock_file_perms;
+# the snap is started via `snap run ..`
+allow init_t snappy_cli_t:unix_stream_socket create_stream_socket_perms;
+# init_t will try to remount snap mount directory when starting services that
+# use Private* directives, while init_t is allowed to remount all fs, we cannot
+# declare fs_type(snappy_snap_t) outside of core policy, add explicit permission
+# instead
+allow init_t snappy_snap_t:filesystem remount;
diff -pruN 2.37.4-1/data/sysctl/rhel7-snap.conf 2.39.2+19.10ubuntu1/data/sysctl/rhel7-snap.conf
--- 2.37.4-1/data/sysctl/rhel7-snap.conf 2019-02-27 18:53:36.000000000 +0000
+++ 2.39.2+19.10ubuntu1/data/sysctl/rhel7-snap.conf 2019-06-05 06:41:21.000000000 +0000
@@ -2,3 +2,10 @@
# Unexpected "Device or resource busy" error when removing a directory
# see https://access.redhat.com/articles/3128691 for details
fs.may_detach_mounts=1
+
+# RHEL 7.4+ (and LXD) specific:
+# A configuration change was made in the 7.4 release, the default number of user
+# namespaces is set to zero, see https://access.redhat.com/solutions/3188102 for
+# details. For details about LXD side, see
+# https://discuss.linuxcontainers.org/t/lxd-on-centos-7/1250
+user.max_user_namespaces=15000
diff -pruN 2.37.4-1/debian/changelog 2.39.2+19.10ubuntu1/debian/changelog
--- 2.37.4-1/debian/changelog 2019-02-28 17:21:26.000000000 +0000
+++ 2.39.2+19.10ubuntu1/debian/changelog 2019-06-07 09:25:17.000000000 +0000
@@ -1,110 +1,3466 @@
-snapd (2.37.4-1) unstable; urgency=medium
+snapd (2.39.2+19.10ubuntu1) eoan; urgency=medium
- * New upstream release
- * d/patches0008-snap-squashsh-skip-TestBuildDate-on-Debian.patch: drop,
- fixed upstream
-
- -- Zygmunt Krynicki Thu, 28 Feb 2019 18:21:26 +0100
-
-snapd (2.37.3-1) unstable; urgency=medium
-
- * New upstream release
-
- -- Zygmunt Krynicki Tue, 19 Feb 2019 13:46:24 +0100
+ * Cherry pick https://github.com/snapcore/snapd/pull/6964 to fix
+ dep-wait in eoan
-snapd (2.37.2-1) unstable; urgency=medium
+ -- Michael Vogt Fri, 07 Jun 2019 11:25:17 +0200
- * New upstream releease.
+snapd (2.39.2+19.10) eoan; urgency=medium
- -- Michael Hudson-Doyle Thu, 07 Feb 2019 21:26:34 +1300
+ * New upstream release, LP: #1827495
+ - debian: rework how we run autopkgtests
+ - interfaces/docker-support: add overlayfs accesses for ubuntu core
+ - data/selinux: permit init_t to remount snappy_snap_t
+ - strutil/shlex: fix ineffassign
+ - packaging: fix build-depends on powerpc
-snapd (2.37.1-1) unstable; urgency=medium
+ -- Michael Vogt Wed, 05 Jun 2019 08:41:21 +0200
- * New upstream release.
- * d/patches/0009-interfaces-apparmor-mock-presence-of-overlayfs-root.patch:
- applied upstream
+snapd (2.39.1) xenial; urgency=medium
- -- Zygmunt Krynicki Tue, 29 Jan 2019 19:24:35 +0100
+ * New upstream release, LP: #1827495
+ - spread: enable Fedora 30
+ - cmd/snap-confine, data/selinux: cherry pick Fedora 30 fixes
+ - tests/unit/spread-shellcheck: temporary workaround for SC2251
+ - packaging: build empty package on powerpc
+ - interfaces: special-case "snapd" in sanitizeSlotReservedForOS*
+ helper
+ - cmd/snap: mangle descriptions that have indent > terminal width
+ - cmd/snap-confine: unshare per-user mount ns once
+ - tests: avoid adding spaces to the base snaps names
+ - systemd: workaround systemctl show quirks on older systemd
+ versions
-snapd (2.37-3) unstable; urgency=medium
+ -- Michael Vogt Wed, 29 May 2019 12:08:43 +0200
- * Fix --no-arch-any build.
+snapd (2.39) xenial; urgency=medium
- -- Michael Hudson-Doyle Thu, 24 Jan 2019 16:11:17 +1300
-
-snapd (2.37-2) unstable; urgency=medium
+ * New upstream release, LP: #1827495
+ - overlord/ifacestate: update static attributes of "content"
+ interface
+ - data/selinux: tweak the policy for runuser and s-c, interpret
+ audit entries
+ - snapshotstate: disable automatic snapshots on core for now
+ - overlord/corecfg: make expiration of automatic snapshots
+ configurable
+ - snapstate: auto-install snapd when needed
+ - interfaces: add support for the snapd snap in the dbus backend
+ - overlord/snapstate: tweak autorefresh logic if network is not
+ available
+ - interfaces/apparmor: allow running /usr/bin/od
+ - osutil,cmdutil: move CommandFromCore and make it use the snapd
+ snap (if available)
+ - daemon: also verify snap instructions for multi-snap requests
+ - data/selinux: allow snap-confine to mount on top of bin
+ - data/selinux: auto transition /var/snap to snappy_var_t
+ - cmd: add `snap debug validate-seed ` cmd
+ - interfaces/builtin/desktop: fonconfig v6/v7 cache handling on
+ Fedora
+ - interfaces/builtin/intel_mei: fix /dev/mei* AppArmor pattern
+ - tests: make snap-connections test work on boards with snaps pre-
+ installed
+ - tests: check for /snap/core16/current in core16-provided-by-core
+ - tests: run livepatch test on 18.04 as well
+ - devicestate: deal correctly with the "required" flag on Remodel
+ - snapstate,state: add TaskSet.AddAllWithEdges() and use in doUpdate
+ - snapstate: add new NoReRefresh flag and use in Remodel()
+ - many: allow core as a fallback for core16
+ - snapcraft: build static fontconfig in the snapd snap
+ - cmd/snap-confine: remove unused sc_open_snap_{update,discard}_ns
+ - data/selinux: allow snapd to execute runuser under snappy_t
+ - spread, tests: do not leave mislabeled files in restorecon test,
+ attempt to catch similar files
+ - interfaces: cleanup internal tool lookup in system-key
+ - many: move auth.AuthContext to store.DeviceAndAuthContext, the
+ implemention to a separate storecontext packageThis:
+ - overlord/devicestate: measurements around ensure and related tasks
+ - cmd: tweak internal tool lookup to accept more possible locations
+ - overlord/snapstate,snapshotstate: create snapshot on snap removal
+ - tests: run smoke tests on (almost) pristine systems
+ - tests: system disable ssh for config defaults in gadget
+ - cmd/debug: integrate new task timings with "snap debug timings"
+ - tests/upgrade/basic, packaging/fedoar: restore SELinux context of
+ /var/cache/fontconfig, patch pre-2.39 mount units
+ - image: simplify prefer local logic and fixes
+ - tests/main/selinux-lxd: make sure LXD from snaps works cleanly
+ with enforcing SELinux
+ - tests: deny ioctl - TIOCSTI with garbage in high bits
+ - overlord: factor out mocking of device service and gadget w.
+ prepare-device for registration tests
+ - data/selinux, tests/main/selinux-clean: fine tune the policy, make
+ sure that no denials are raised
+ - cmd/libsnap,osutil: fix parsing of mountinfo
+ - ubuntu: disable -buildmode=pie on armhf to fix memory issue
+ - overlord/snapstate: inhibit refresh for up to a week
+ - cmd/snap-confine: prevent cwd restore permission bypass
+ - overlord/ifacestate: introduce HotplugKey type use short key in
+ change summaries
+ - many: make Remodel() download everything first before installing
+ - tests: fixes discovered debugging refresh-app-awareness
+ - overlord/snapstate: track time of postponed refreshes
+ - snap-confine: set rootfs_dir in sc_invocation struct
+ - tests: run create-user on core devices
+ - boot: add flag file "meta/force-kernel-extraction"
+ - tests: add regression test for systemctl race fix
+ - overlord/snapshotstate: helpers for snapshot expirations
+ - overlord,tests: perform soft refresh check in doInstall
+ - tests: enable tests that write /etc/{hostname,timezone} on core18
+ - overlord/ifacestate: implement String() method of
+ HotplugDeviceInfo for better logs/messages
+ - cmd/snap-confine: move ubuntu-core fallback checks
+ - testutil: fix MockCmd for shellcheck 0.5
+ - snap, gadget: move gadget read/validation into separate package,
+ tweak naming
+ - tests: split travis spread execution in 2 jobs for ubuntu and non
+ ubuntu systems
+ - testutil: make mocked command work with shellcheck from snaps
+ - packaging/fedora, tests/upgrade/basic: patch existing mount units
+ with SELinux context on upgrade
+ - metautil, snap: extract yaml value normalization to a helper
+ package
+ - tests: use apt via eatmydata
+ - dirs,overlord/snapstate: add Soft and Hard refresh checks
+ - cmd/snap-confine: allow using tools from snapd snap
+ - cmd,interfaces: replace local helpers with cmd.InternalToolPath
+ - tweak: fix "make hack" on Fedora
+ - snap: add validation of gadget.yaml
+ - cmd/snap-update-ns: refactor of profile application
+ - cmd/snap,client,daemon,store: layout and sanity tweaks for
+ find/search options
+ - tests: add workaround for missing cache reset on older snapd
+ - interfaces: deal with the snapd snap correctly for apparmor 2.13
+ - release-tools: add debian-package-builder
+ - tests: enable opensuse 15 and add force-resolution installing
+ packages
+ - timings: AddTag helper
+ - testutil: run mocked commands through shellcheck
+ - overlord/snapshotstate: support auto flag
+ - client, daemon, store: search by common-id
+ - tests: all the systems for google backend with 6 workers
+ - interfaces: hotplug nested vm test, updated serial-port interface
+ for hotplug.
+ - sanity: use proper SELinux context when mounting squashfs
+ - cmd/libsnap: neuter variables in cleanup functions
+ - interfaces/adb-support: account for hubs on sysfs path
+ - interfaces/seccomp: regenerate changed profiles only
+ - snap: reject layouts to /lib/{firmware,modules}
+ - cmd/snap-confine, packaging: support SELinux
+ - selinux, systemd: support mount contexts for snap images
+ - interfaces/builtin/opengl: allow access to Tegra X1
+ - cmd/snap: make 'snap warnings' output yamlish
+ - tests: add check to detect a broken snap on reset
+ - interfaces: add one-plus devices to adb-support
+ - cmd: prevent umask from breaking snap-run chain
+ - tests/lib/pkgdb: allow downgrade when installing packages in
+ openSUSE
+ - cmd/snap-confine: use fixed private tmp directory
+ - snap: tweak parsing errors of gadget updates
+ - overlord/ifacemgr: basic measurements
+ - spread: refresh metadata on openSUSE
+ - cmd/snap-confine: pass sc_invocation instead of numerous args
+ around
+ - snap/gadget: introduce volume update info
+ - partition,bootloader: rename 'partition' package to 'bootloader'
+ - interfaces/builtin: add dev/pts/ptmx access to docker_support
+ - tests: restore sbuild test
+ - strutil: make SplitUnit public, allow negative numbers
+ - overlord/snapstate,: retry less for auto-stuff
+ - interfaces/builtin: add add exec "/" to docker-support
+ - cmd/snap: fix regression of snap saved command
+ - cmd/libsnap: rename C enum for feature flag
+ - cmd: typedef mountinfo structures
+ - tests/main/remodel: clean up before reverting the state
+ - cmd/snap-confine: umount scratch dir using UMOUNT_NOFOLLOW
+ - timings: add new helpers, Measurer interface and DurationThreshold
+ - cmd/snap-seccomp: version-info subcommand
+ - errortracker: fix panic in Report if db cannot be opened
+ - sandbox/seccomp: a helper package wrapping calls to snap-seccomp
+ - many: add /v2/model API, `snap remodel` CLI and spread test
+ - tests: enable opensuse tumbleweed back
+ - overlord/snapstate, store: set a header when auto-refreshing
+ - data/selinux, tests: refactor SELinux policy, add minimal tests
+ - spread: restore SELinux context when we mess with system files
+ - daemon/api: filter connections with hotplug-gone=true
+ - daemon: support returning assertion information as JSON with the
+ "json" query parameter
+ - cmd/snap: hide 'interfaces' command, show deprecation notice
+ - timings: base API for recording timings in state
+ - cmd/snap-confine: drop unused dependency on libseccomp
+ - interfaces/apparmor: factor out test boilerplate
+ - daemon: extract assertions api endpoint implementation into
+ api_asserts.go
+ - spread.yaml: bump delta reference
+ - cmd/snap-confine: track per-app and per-hook processes
+ - cmd/snap-confine: make sc_args helpers const-correct
+ - daemon: move a function that was between an other struct and its
+ methods
+ - overlord/snapstate: fix restoring of "old-current" revision config
+ in undoLinkSnap
+ - cmd/snap, client, daemon, ifacestate: show a leading attribute of
+ a connection
+ - cmd/snap-confine: call sc_should_use_normal_mode once
+ - cmd/snap-confine: populate enter_non_classic_execution_environment
+ - daemon: allow downloading snaps blobs via .../file
+ - cmd/snap-confine: introduce sc_invocation
+ - devicestate: add initial Remodel support
+ - snap: remove obsolete license-* fields in the yaml
+ - cmd/libsnap: add cgroup-pids-support module
+ - overlord/snapstate/backend: make LinkSnap clean up more
+ - snapstate: only keep 2 snaps on classic
+ - ctlcmd/tests: tests tweaks (followup to #6322)
+
+ -- Michael Vogt Fri, 03 May 2019 11:29:50 +0200
+
+snapd (2.38.1) xenial; urgency=medium
+
+ * New upstream release, LP: #1824394
+ - tests: add workaround for missing cache reset on older snapd
+ - ubuntu: disable -buildmode=pie on armhf to fix memory issue
+
+ -- Michael Vogt Thu, 11 Apr 2019 18:26:47 +0200
+
+snapd (2.38) xenial; urgency=medium
+
+ * New upstream release, LP: #1818648
+ - overlord/snapstate,: retry less for auto-stuff
+ - cmd/snap: fix regression of snap saved command
+ - interfaces/builtin: add dev/pts/ptmx access to docker_support
+ - overlord/snapstate, store: set a header when auto-refreshing
+ - interfaces/builtin: add add exec "/" to docker-support
+ - cmd/snap, client, daemon, ifacestate: show a leading attribute of
+ a connection
+ - interface: avahi-observe: Fixing socket permissions on 4.15
+ kernels
+ - tests: check that apt works before using it
+ - apparmor: support AppArmor 2.13
+ - snapstate: restart into the snapd snap on classic
+ - overlord/snapstate: during refresh, re-refresh on epoch bump
+ - cmd, daemon: split out the common bits of mapLocal and mapRemote
+ - cmd/snap-confine: chown private /tmp to root.root
+ - cmd/snap-confine: drop uid from random /tmp name
+ - overlord/hookstate: apply pending transaction changes onto
+ temporary configuration for snapctl get
+ - cmd/snap: `snap connections` command
+ - interfaces/greengrass_support: update accesses for GGC 1.8
+ - cmd/snap, daemon: make the connectivity check use GET
+ - interfaces/builtin,/udev: add spec support to disable udev +
+ device cgroup and use it for greengrass
+ - interfaces/intel-mei: small follow up tweaks
+ - ifacestate/tests: fix/improve udev mon test
+ - interfaces: add multipass-support interface
+ - tests/main/high-user-handling: fix the test for Go 1.12
+ - interfaces: add new intel-mei interface
+ - systemd: decrease the checker counter before unlocking otherwise
+ we can get spurious panics
+ - daemon/tests: fix race in the disconnect conflict test
+ - cmd/snap-confine: allow moving tasks to pids cgroup
+ - tests: enable opensuse tumbleweed on spread
+ - cmd/snap: fix `snap services` completion
+ - ifacestate/hotplug: integration with udev monitor
+ - packaging: build snapctl as a static binary
+ - packaging/opensuse: move most logic to snapd.mk
+ - overlord: fix ensure before slowness on Retry
+ - overlord/ifacestate: fix migration of connections on upgrade from
+ ubuntu-core
+ - daemon, client, cmd/snap: debug GETs ask aspects, not actions
+ - tests/main/desktop-portal-*: fix handling of python dependencies
+ - interfaces/wayland: allow wayland server snaps function on classic
+ too
+ - daemon, client, cmd/snap: snap debug base-declaration
+ - tests: run tests on opensuse leap 15.0 instead of 42.3
+ - cmd/snap: fix error messages for snapshots commands if ID is not
+ uint
+ - interfaces/seccomp: increase filter precision
+ - interfaces/network-manager: no peer label check for hostname1
+ - tests: add a tests for xdg-desktop-portal integration
+ - tests: not checking 'tracking channel' after refresh core on
+ nested execution
+ - tests: remove snapweb from tests
+ - snap, wrappers: support StartTimeout
+ - wrappers: Add an X-SnapInstanceName field to desktop files
+ - cmd/snap: produce better output for help on subcommands
+ - tests/main/nfs-support: use archive mode for creating fstab backup
+ - many: collect time each task runs and display it with `snap debug
+ timings `
+ - tests: add attribution to helper script
+ - daemon: make ucrednetGet not loop
+ - squashfs: unset SOURCE_DATE_EPOCH in the TestBuildDate test
+ - features,cmd/libsnap: add new feature "refresh-app-awareness"
+ - overlord: fix random typos
+ - interfaces/seccomp: generate global seccomp profile
+ - daemon/api: fix error case for disconnect conflict
+ - overlord/snapstate: add some randomness to the catalog refresh
+ - tests: disable trusty-proposed for now
+ - tests: fix upgrade-from-2.15 with kernel 4.15
+ - interfaces/apparmor: allow sending and receiving signals from
+ ourselves
+ - tests: split the test interfaces-many in 2 and remove snaps on
+ restore
+ - tests: use snap which takes 15 seconds to install on retryable-
+ error test
+ - packaging: avoid race in snapd.postinst
+ - overlord/snapstate: discard mount namespace when undoing 1st link
+ snap
+ - cmd/snap-confine: allow writes to /var/lib/**
+ - tests: stop catalog-update test for now
+ - tests/main/auto-refresh-private: make sure to actually download
+ with the expired macaroon
+ - many: save media info when installing, show it when listing
+ - userd: handle help urls which requires prepending XDG_DATA_DIRS
+ - tests: fix NFS home mocking
+ - tests: improve snaps-system-env test
+ - tests: pre-cache core on core18 systems
+ - interfaces/hotplug: renamed RequestedSlotSpec to ProposedSlot,
+ removed Specification
+ - debian: ensure leftover usr.lib.snapd.snap-confine is gone
+ - image,cmd/snap,tests: introduce support for modern prepare-image
+ --snap [=]
+ - overlord/ifacestate: tweak logic for generating unique slot names
+ - packaging: import debian salsa packaging work, add sbuild test and
+ use in spead
+ - overlord/ifacestate: hotplug-add-slot handler
+ - image,cmd/snap: simplify --classic-arch to --arch, expose
+ prepare-image
+ - tests: run test snap as user in the smoke test
+ - cmd/snap: tweak man output to have no doubled up .TP lines
+ - cmd/snap, overlord/snapstate: silently ignore classic flag when a
+ snap is strictly confined
+ - snap-confine: remove special handling of /var/lib/jenkins
+ - cmd/snap-confine: handle death of helper process
+ - packaging: disable systemd environment generator on 18.04
+ - snap-confine: fix classic snaps for users with /var/lib/* homedirs
+ - tests/prepare: prevent console-conf from running
+ - image: bootstrapToRootDir => setupSeed
+ - image,cmd/snap,tests: introduce prepare-image --classic
+ - tests: update smoke/sandbox test for armhf
+ - client, daemon: introduce helper for querying snapd API for the
+ list of slot/plug connections
+ - cmd/snap-confine: refactor and cleanup of seccomp loading
+ - snapstate, snap: allow update/switch requests with risk only
+ channel to DTRT
+ - interfaces: add network-manager-observe interface
+ - snap-confine: increase locking timeout to 30s
+ - snap-confine: fix incorrect "sanity timeout 3s" message
+ - snap-confine: provide proper error message on sc_sanity_timeout
+ - snapd,state: improve error message on state reading failure
+ - interfaces/apparmor: deny inet/inet6 in snap-update-ns profile
+ - snap: fix reexec from the snapd snap for classic snaps
+ - snap: fix hook autodiscovery for parallel installed snaps
+ - overlord/snapstate: format the refresh time for the log
+ - cmd/snap-confine: add special case for Jenkins
+ - snapcraft.yaml: fix XBuildDeb PATH for go-1.10
+ - overlord/snapstate: validate instance names early
+ - overlord/ifacestate: handler for hotplug-update-slot tasks
+ - polkit: cast pid to uint32 to keep polkit happy for now
+ - snap/naming: move various name validation helpers to separate
+ package
+ - tests: iterate getting journal logs to support delay on boards on
+ daemon-notify test
+ - cmd/snap: fix typo in cmd_wait.go
+ - snap/channel: improve channel parsing
+ - daemon, polkit: pid_t is signed
+ - daemon: introduce /v2/connections snapd API endpoint
+ - cmd/snap: small refactor of cmd_info's channel handling
+ - overlord/snapstate: use an ad-hoc error when no results
+ - cmd/snap: wrap "summary" better
+ - tests: workaround missing go dependencies in debian-9
+ - daemon: try to tidy up the icon stuff a little
+ - interfaces: add display-control interface
+ - snapcraft.yaml: fix snap building in launchpad
+ - tests: update fedora 29 workers to speed up the whole testing time
+ - interfaces: add u2f-devices interface and allow reading udev
+ +power_supply:* in hardware-observe
+ - cmd/snap-update-ns: save errno from strtoul
+ - tests: interfaces tests normalization
+ - many: cleanup golang.org/x/net/context
+ - tests: add spread test for system dbus interface
+ - tests: remove -o pipefail
+ - interfaces: add block-devices interface
+ - spread: enable upgrade suite on fedora
+ - tests/main/searching: video section got renamed to photo-and-video
+ - interfaces/home: use dac_read_search instead of dac_override with
+ 'read: all'
+ - snap: really run the RunSuite
+ - interfaces/camera: allow reading vendor/etc info from
+ /run/udev/data/+usb:*
+ - interfaces/dbus: be less strict about alternations for well-known
+ names
+ - interfaces/home: allow dac_override with 'read:
+ all'
+ - interfaces/pulseaudio: allow reading subdirectories of
+ /etc/pulse
+ - interfaces/system-observe: allow read on
+ /proc/locks
+ - run-checks: ensure we use go-1.10 if available
+ - tests: get test-snapd-dbus-{provider,consumer} from the beta
+ channel
+ - interfaces/apparmor: mock presence of overlayfs root
+ - spread: increase default kill-timeout to 30min
+ - tests: simplify interfaces-contacts-service test
+ - packaging/ubuntu: build with golang 1.10
+ - ifacestate/tests: extra test for hotplug-connect handler
+ - packaging: make sure that /var/lib/snapd/lib/glvnd is accounted
+ for
+ - overlord/snapstate/backend: call fontconfig helpers from the new
+ 'current'
+ - kvm: load required kernel modules if necessary
+ - cmd/snap: use a fake user for 'run' tests
+ - tests: update systems for google sru backend
+ - tests: fix install-snaps test by changing the snap info regex
+ - interfaces: helpers for sorting plug/slot/connection refs
+ - tests: moving core-snap-refresh-on-core test from main to nested
+ suite
+ - tests: fix daemon-notify test checking denials considering all the
+ log lines
+ - tests: skip lp-1802591 on "official" images
+ - tests: fix listing tests to match "snap list --unicode=never"
+ - debian: fix silly typo in the spread test invocation
+ - interface: raw-usb: Adding ttyACM ttyACA permissions
+ - tests: fix enable-disable-unit-gpio test on external boards
+ - overlord/ifacestate: helper API to obtain the state of connections
+ - tests: define new "tests/smoke" suite and use that for
+ autopkgtests
+ - cmd/snap-update-ns: explicitly check for return value from
+ parse_arg_u
+ - interfaces/builtin/opengl: allow access to NVIDIA VDPAU library
+ - tests: auto-clean the test directory
+ - cmd/snap: further tweak messaging; add a test
+ - overlord/ifacestate: handler for hotplug-connect task
+ - cmd/snap-confine: join freezer only after setting up user mount
+ - cmd/snap-confine: don't preemptively create .mnt files
+ - cmd/snap-update-ns: manually implement isspace
+ - cmd/snap-update-ns: let the go parser know we are parsing -u
+ - cmd/snap-discard-ns: fix name of user fstab files
+ - snapshotstate: don't task.Log without the lock
+ - tests: exclude some more slow tests from runs in autopkgtest
+ - many: remove .user-fstab files from /run/snapd/ns
+ - cmd/libsnap: pass --from-snap-confine when calling snap-update-ns
+ as user
+ - cmd/snap-update-ns: make freezer mockable
+ - cmd/snap-update-ns: move XDG code to dedicated file
+ - osutil: add helper for loading fstab from string
+ - cmd/snap-update-ns: move existing code around, renaming some
+ functions
+ - overlord/configstate/configcore: support - and _ in cloud init
+ field names
+ - * cmd/snap-confine: use makedev instead of MKDEV
+ - tests: review/fix the autopkgtest failures in disco
+ - overlord: drop old v1 store api support from managers test
+ - tests: new test for snapshots with more than 1 user
+
+ -- Michael Vogt Thu, 21 Mar 2019 10:55:27 +0100
+
+snapd (2.37.4) xenial; urgency=medium
+
+ * New upstream release, LP: #1817949
+ - squashfs: unset SOURCE_DATE_EPOCH in the TestBuildDate test
+ - overlord/ifacestate: fix migration of connections on upgrade from
+ ubuntu-core
+ - tests: fix upgrade-from-2.15 with kernel 4.15
+ - interfaces/seccomp: increase filter precision
+ - tests: remove snapweb from tests
+
+ -- Michael Vogt Wed, 27 Feb 2019 19:53:36 +0100
+
+snapd (2.37.3) xenial; urgency=medium
+
+ * New upstream release, LP: #1811233
+ - interfaces/seccomp: generate global seccomp profile
+ - overlord/snapstate: add some randomness to the catalog refresh
+ - tests: add upgrade test from 2.15.2ubuntu1 -> current snapd
+ - snap-confine: fix fallback to ubuntu-core
+ - packaging: avoid race in snapd.postinst
+ - overlord/snapstate: discard mount namespace when undoing 1st link
+ snap
+ - cmd/snap-confine: allow writes to /var/lib/** again
+ - tests: stop catalog-update/apt-hooks test until the catlog refresh
+ is randomized
+ - debian: ensure leftover usr.lib.snapd.snap-confine is gone
+
+ -- Michael Vogt Mon, 18 Feb 2019 17:17:33 +0100
+
+snapd (2.37.2) xenial; urgency=medium
+
+ * New upstream release, LP: #1811233
+ - cmd/snap, overlord/snapstate: silently ignore classic flag when a
+ snap is strictly confined
+ - snap-confine: remove special handling of /var/lib/jenkins
+ - cmd/snap-confine: handle death of helper process gracefully
+ - snap-confine: fix classic snaps for users with /var/lib/* homedirs
+ like jenkins/postgres
+ - packaging: disable systemd environment generator on 18.04
+ - tests: update smoke/sandbox test for armhf
+ - cmd/snap-confine: refactor and cleanup of seccomp loading
+ - snap-confine: increase locking timeout to 30s
+ - snap-confine: fix incorrect "sanity timeout 3s" message
+ - snap: fix hook autodiscovery for parallel installed snaps
+ - tests: iterate getting journal logs to support delay on boards on
+ daemon-notify test
+ - interfaces/apparmor: deny inet/inet6 in snap-update-ns profile
+ - interfaces: add u2f-devices interface
+
+ -- Michael Vogt Wed, 06 Feb 2019 10:08:07 +0100
+
+snapd (2.37.1) xenial; urgency=medium
+
+ * New upstream release, LP: #1811233
+ - cmd/snap-confine: add special case for Jenkins
+ - tests: workaround missing go dependencies in debian-9
+ - daemon, polkit: pid_t is signed
+ - interfaces: add display-control interface
+ - interfaces: add block-devices interface
+ - tests/main/searching: video section got renamed to photo-and-video
+ - interfaces/camera: allow reading vendor/etc info from
+ /run/udev/data/+usb
+ - interfaces/dbus: be less strict about alternations for well-known
+ names
+ - interfaces/home: allow dac_read_search with 'read: all'
+ - interfaces/pulseaudio: allow reading subdirectories of
+ /etc/pulse
+ - interfaces/system-observe: allow read on
+ /proc/locks
+ - tests: get test-snapd-dbus-{provider,consumer} from the beta
+ channel
+ - interfaces/apparmor: mock presence of overlayfs root
+ - packaging/{fedora,opensuse,ubuntu}: add /var/lib/snapd/lib/glvnd
+
+ -- Michael Vogt Tue, 29 Jan 2019 18:35:36 +0100
+
+snapd (2.37) xenial; urgency=medium
+
+ * New upstream release, LP: #1811233
+ - snapd: fix race in TestSanityFailGoesIntoDegradedMode test
+ - cmd: fix snap-device-helper to deal correctly with hooks
+ - tests: various fixes for external backend
+ - interface: raw-usb: Adding ttyACM[0-9]* as many serial devices
+ have device node /dev/ttyACM[0-9]
+ - tests: fix enable-disable-unit-gpio test on external boards
+ - tests: define new "tests/smoke" suite and use that for
+ autopkgtests
+ - interfaces/builtin/opengl: allow access to NVIDIA VDPAU
+ library
+ - snapshotstate: don't task.Log without the lock
+ - overlord/configstate/configcore: support - and _ in cloud init
+ field names
+ - cmd/snap-confine: use makedev instead of MKDEV
+ - tests: review/fix the autopkgtest failures in disco
+ - systemd: allow only a single daemon-reload at the same time
+ - cmd/snap: only auto-enable unicode to a tty
+ - cmd/snap: right-align revision and size in info's channel map
+ - dirs, interfaces/builtin/desktop: system fontconfig cache path is
+ different on Fedora
+ - tests: fix "No space left on device" issue on amazon-linux
+ - store: undo workaround for timezone-less released-at
+ - store, snap, cmd/snap: channels have released-at
+ - snap-confine: fix incorrect use "src" var in mount-support.c
+ - release: support probing SELinux state
+ - release-tools: display self-help
+ - interface: add new `{personal,system}-files` interface
+ - snap: give Epoch an Equal method
+ - many: remove unused interface code
+ - interfaces/many: use 'unsafe' with docker-support change_profile
+ rules
+ - run-checks: stop running HEAD of staticcheck
+ - release: use sync.Once around lazy intialized state
+ - overlord/ifacestate: include interface name in the hotplug-
+ disconnect task summary
+ - spread: show free space in debug output
+ - cmd/snap: attempt to restore SELinux context of snap user
+ directories
+ - image: do not write empty etc/cloud
+ - tests: skip snapd snap on reset for core systems
+ - cmd/snap-discard-ns: fix umount(2) typo
+ - overlord/ifacestate: hotplug-remove-slot task handler
+ - overlord/ifacestate: handler for hotplug-disconnect task
+ - ifacestate/hotplug: updateDevice helper
+ - tests: reset snapd state on tests restore
+ - interfaces: return security setup errors
+ - overlord: make InstallMany work like UpdateMany, issuing a single
+ request to get candidates
+ - systemd/systemd.go: add missing tests for systemd.IsActive
+ - overlord/ifacestate: addHotplugSeqWaitTask helper
+ - cmd/snap-confine: refactor call to snap-update-ns --user-mounts
+ - tests: new backend used to run upgrade test suite
+ - travis: short circuit failures in static and unit tests travis job
+ - cmd: automatically fix localized