summaryrefslogtreecommitdiff
diff options
authorJohn R. Lenton <jlenton@gmail.com>2018-03-02 13:00:20 +0000
committerJohn R. Lenton <jlenton@gmail.com>2018-03-02 13:00:20 +0000
commitca103056d91e3ad66623c2c4c9a2edde48623816 (patch)
treeb6891643d1e188f99fe654ed3033457a43592997
parent6b02a9f529ac8f0a21beed97968a77ffd82a2f0e (diff)
parent1faa0f1c46717766c7c090ffe1d324b12bda9260 (diff)
Merge remote-tracking branch 'upstream/master' into i18n-ng-awkwardness
-rw-r--r--data/udev/rules.d/66-snapd-autoimport.rules2
-rw-r--r--interfaces/apparmor/backend.go23
-rw-r--r--interfaces/apparmor/backend_test.go232
-rw-r--r--interfaces/apparmor/export_test.go9
-rw-r--r--interfaces/apparmor/template.go11
-rw-r--r--interfaces/system_key.go9
-rw-r--r--interfaces/system_key_test.go5
-rw-r--r--osutil/overlay.go79
-rw-r--r--osutil/overlay_test.go99
-rw-r--r--overlord/configstate/configcore/services.go13
-rw-r--r--overlord/configstate/configcore/services_test.go44
-rw-r--r--snap/info.go2
-rw-r--r--snap/info_snap_yaml.go3
-rw-r--r--snap/info_snap_yaml_test.go27
-rw-r--r--snap/squashfs/squashfs.go30
-rw-r--r--snap/squashfs/squashfs_test.go17
-rw-r--r--store/details_v2.go177
-rw-r--r--store/details_v2_test.go294
-rw-r--r--tests/main/interfaces-broadcom-asic-control/task.yaml5
-rw-r--r--tests/main/listing/task.yaml2
-rw-r--r--timeutil/export_test.go1
-rw-r--r--timeutil/human.go69
-rw-r--r--timeutil/human_test.go79
23 files changed, 1204 insertions, 28 deletions
diff --git a/data/udev/rules.d/66-snapd-autoimport.rules b/data/udev/rules.d/66-snapd-autoimport.rules
index f06a9f3ebf..11309cafaf 100644
--- a/data/udev/rules.d/66-snapd-autoimport.rules
+++ b/data/udev/rules.d/66-snapd-autoimport.rules
@@ -1,3 +1,3 @@
# probe for assertions, must run before udisks2
-ACTION=="add", SUBSYSTEM=="block" \
+ACTION=="add", SUBSYSTEM=="block", KERNEL!="loop*", KERNEL!="ram*" \
RUN+="/usr/bin/unshare -m /usr/bin/snap auto-import --mount=/dev/%k"
diff --git a/interfaces/apparmor/backend.go b/interfaces/apparmor/backend.go
index 891070ca7d..9515658bf4 100644
--- a/interfaces/apparmor/backend.go
+++ b/interfaces/apparmor/backend.go
@@ -57,8 +57,9 @@ import (
)
var (
- procSelfExe = "/proc/self/exe"
- isHomeUsingNFS = osutil.IsHomeUsingNFS
+ procSelfExe = "/proc/self/exe"
+ isHomeUsingNFS = osutil.IsHomeUsingNFS
+ isRootWritableOverlay = osutil.IsRootWritableOverlay
)
// Backend is responsible for maintaining apparmor profiles for snaps and parts of snapd.
@@ -110,6 +111,19 @@ func (b *Backend) Initialize() error {
logger.Noticef("snapd enabled NFS support, additional implicit network permissions granted")
}
+ // Check if '/' is on overlayfs. If so, add the necessary rules for
+ // upperdir and allow snap-confine to work.
+ if overlayRoot, err := isRootWritableOverlay(); err != nil {
+ logger.Noticef("cannot determine if root filesystem on overlay: %v", err)
+ } else if overlayRoot != "" {
+ snippet := strings.Replace(overlayRootSnippet, "###UPPERDIR###", overlayRoot, -1)
+ policy["overlay-root"] = &osutil.FileState{
+ Content: []byte(snippet),
+ Mode: 0644,
+ }
+ logger.Noticef("snapd enabled root filesystem on overlay support, additional upperdir permissions granted")
+ }
+
// Ensure that generated policy is what we computed above.
created, removed, err := osutil.EnsureDirState(dirs.SnapConfineAppArmorDir, glob, policy)
if err != nil {
@@ -423,6 +437,11 @@ func addContent(securityTag string, snapInfo *snap.Info, opts interfaces.Confine
if nfs, _ := isHomeUsingNFS(); nfs {
tagSnippets += nfsSnippet
}
+
+ if overlayRoot, _ := isRootWritableOverlay(); overlayRoot != "" {
+ snippet := strings.Replace(overlayRootSnippet, "###UPPERDIR###", overlayRoot, -1)
+ tagSnippets += snippet
+ }
}
return tagSnippets
}
diff --git a/interfaces/apparmor/backend_test.go b/interfaces/apparmor/backend_test.go
index 3ae042c16c..a8bd6b3dfc 100644
--- a/interfaces/apparmor/backend_test.go
+++ b/interfaces/apparmor/backend_test.go
@@ -436,6 +436,8 @@ func (s *backendSuite) TestCombineSnippets(c *C) {
defer restore()
restore = apparmor.MockIsHomeUsingNFS(func() (bool, error) { return false, nil })
defer restore()
+ restore = apparmor.MockIsRootWritableOverlay(func() (string, error) { return "", nil })
+ defer restore()
// NOTE: replace the real template with a shorter variant
restoreTemplate := apparmor.MockTemplate("\n" +
@@ -902,3 +904,233 @@ func (s *backendSuite) TestSetupSnapConfineGeneratedPolicyError5(c *C) {
// We didn't try to reload the policy.
c.Assert(cmd.Calls(), HasLen, 0)
}
+
+// snap-confine policy when overlay is not used.
+func (s *backendSuite) TestSetupSnapConfineGeneratedPolicyNoOverlay(c *C) {
+ // Make it appear as if overlay was not used.
+ restore := apparmor.MockIsRootWritableOverlay(func() (string, error) { return "", nil })
+ defer restore()
+
+ // Intercept interaction with apparmor_parser
+ cmd := testutil.MockCommand(c, "apparmor_parser", "")
+ defer cmd.Restore()
+
+ // Setup generated policy for snap-confine.
+ err := (&apparmor.Backend{}).Initialize()
+ c.Assert(err, IsNil)
+ c.Assert(cmd.Calls(), HasLen, 0)
+
+ // Because overlay is not used there are no local policy files but the
+ // directory was created.
+ files, err := ioutil.ReadDir(dirs.SnapConfineAppArmorDir)
+ c.Assert(err, IsNil)
+ c.Assert(files, HasLen, 0)
+
+ // The policy was not reloaded.
+ c.Assert(cmd.Calls(), HasLen, 0)
+}
+
+// Ensure that both names of the snap-confine apparmor profile are supported.
+
+func (s *backendSuite) TestSetupSnapConfineGeneratedPolicyWithOverlay1(c *C) {
+ s.testSetupSnapConfineGeneratedPolicyWithOverlay(c, "usr.lib.snapd.snap-confine")
+}
+
+func (s *backendSuite) TestSetupSnapConfineGeneratedPolicyWithOverlay2(c *C) {
+ s.testSetupSnapConfineGeneratedPolicyWithOverlay(c, "usr.lib.snapd.snap-confine.real")
+}
+
+// snap-confine policy when overlay is used and snapd has not re-executed.
+func (s *backendSuite) testSetupSnapConfineGeneratedPolicyWithOverlay(c *C, profileFname string) {
+ // Make it appear as if overlay workaround was needed.
+ restore := apparmor.MockIsRootWritableOverlay(func() (string, error) { return "/upper", nil })
+ defer restore()
+
+ // Intercept interaction with apparmor_parser
+ cmd := testutil.MockCommand(c, "apparmor_parser", "")
+ defer cmd.Restore()
+
+ // Intercept the /proc/self/exe symlink and point it to the distribution
+ // executable (the path doesn't matter as long as it is not from the
+ // mounted core snap). This indicates that snapd is not re-executing
+ // and that we should reload snap-confine profile.
+ fakeExe := filepath.Join(s.RootDir, "fake-proc-self-exe")
+ err := os.Symlink("/usr/lib/snapd/snapd", fakeExe)
+ c.Assert(err, IsNil)
+ restore = apparmor.MockProcSelfExe(fakeExe)
+ defer restore()
+
+ profilePath := filepath.Join(dirs.SystemApparmorDir, profileFname)
+
+ // Create the directory where system apparmor profiles are stored and write
+ // the system apparmor profile of snap-confine.
+ c.Assert(os.MkdirAll(dirs.SystemApparmorDir, 0755), IsNil)
+ c.Assert(ioutil.WriteFile(profilePath, []byte(""), 0644), IsNil)
+
+ // Setup generated policy for snap-confine.
+ err = (&apparmor.Backend{}).Initialize()
+ c.Assert(err, IsNil)
+
+ // Because overlay is being used, we have the extra policy file.
+ files, err := ioutil.ReadDir(dirs.SnapConfineAppArmorDir)
+ c.Assert(err, IsNil)
+ c.Assert(files, HasLen, 1)
+ c.Assert(files[0].Name(), Equals, "overlay-root")
+ c.Assert(files[0].Mode(), Equals, os.FileMode(0644))
+ c.Assert(files[0].IsDir(), Equals, false)
+
+ // The policy allows upperdir access.
+ data, err := ioutil.ReadFile(filepath.Join(dirs.SnapConfineAppArmorDir, files[0].Name()))
+ c.Assert(err, IsNil)
+ c.Assert(string(data), testutil.Contains, "\"/upper/{,**/}\" r,")
+
+ // The system apparmor profile of snap-confine was reloaded.
+ c.Assert(cmd.Calls(), HasLen, 1)
+ c.Assert(cmd.Calls(), DeepEquals, [][]string{{
+ "apparmor_parser", "--replace",
+ "-O", "no-expr-simplify",
+ "--write-cache",
+ "--cache-loc", dirs.SystemApparmorCacheDir,
+ profilePath,
+ }})
+}
+
+// snap-confine policy when overlay is used and snapd has re-executed.
+func (s *backendSuite) TestSetupSnapConfineGeneratedPolicyWithOverlayAndReExec(c *C) {
+ // Make it appear as if overlay workaround was needed.
+ restore := apparmor.MockIsRootWritableOverlay(func() (string, error) { return "/upper", nil })
+ defer restore()
+
+ // Intercept interaction with apparmor_parser
+ cmd := testutil.MockCommand(c, "apparmor_parser", "")
+ defer cmd.Restore()
+
+ // Intercept the /proc/self/exe symlink and point it to the snapd from the
+ // mounted core snap. This indicates that snapd has re-executed and
+ // should not reload snap-confine policy.
+ fakeExe := filepath.Join(s.RootDir, "fake-proc-self-exe")
+ err := os.Symlink(filepath.Join(dirs.SnapMountDir, "/core/1234/usr/lib/snapd/snapd"), fakeExe)
+ c.Assert(err, IsNil)
+ restore = apparmor.MockProcSelfExe(fakeExe)
+ defer restore()
+
+ // Setup generated policy for snap-confine.
+ err = (&apparmor.Backend{}).Initialize()
+ c.Assert(err, IsNil)
+
+ // Because overlay is being used, we have the extra policy file.
+ files, err := ioutil.ReadDir(dirs.SnapConfineAppArmorDir)
+ c.Assert(err, IsNil)
+ c.Assert(files, HasLen, 1)
+ c.Assert(files[0].Name(), Equals, "overlay-root")
+ c.Assert(files[0].Mode(), Equals, os.FileMode(0644))
+ c.Assert(files[0].IsDir(), Equals, false)
+
+ // The policy allows upperdir access
+ data, err := ioutil.ReadFile(filepath.Join(dirs.SnapConfineAppArmorDir, files[0].Name()))
+ c.Assert(err, IsNil)
+ c.Assert(string(data), testutil.Contains, "\"/upper/{,**/}\" r,")
+
+ // The distribution policy was not reloaded because snap-confine executes
+ // from core snap. This is handled separately by per-profile Setup.
+ c.Assert(cmd.Calls(), HasLen, 0)
+}
+
+type nfsAndOverlaySnippetsScenario struct {
+ opts interfaces.ConfinementOptions
+ overlaySnippet string
+ nfsSnippet string
+}
+
+var nfsAndOverlaySnippetsScenarios = []nfsAndOverlaySnippetsScenario{{
+ // By default apparmor is enforcing mode.
+ opts: interfaces.ConfinementOptions{},
+ overlaySnippet: `"/upper/{,**/}" r,`,
+ nfsSnippet: "network inet,\n network inet6,",
+}, {
+ // DevMode switches apparmor to non-enforcing (complain) mode.
+ opts: interfaces.ConfinementOptions{DevMode: true},
+ overlaySnippet: `"/upper/{,**/}" r,`,
+ nfsSnippet: "network inet,\n network inet6,",
+}, {
+ // JailMode switches apparmor to enforcing mode even in the presence of DevMode.
+ opts: interfaces.ConfinementOptions{DevMode: true, JailMode: true},
+ overlaySnippet: `"/upper/{,**/}" r,`,
+ nfsSnippet: "network inet,\n network inet6,",
+}, {
+ // Classic confinement (without jailmode) uses apparmor in complain mode by default and ignores all snippets.
+ opts: interfaces.ConfinementOptions{Classic: true},
+ overlaySnippet: "",
+ nfsSnippet: "",
+}, {
+ // Classic confinement in JailMode uses enforcing apparmor.
+ opts: interfaces.ConfinementOptions{Classic: true, JailMode: true},
+ // FIXME: logic in backend.addContent is wrong for this case
+ //overlaySnippet: `"/upper/{,**/}" r,`,
+ //nfsSnippet: "network inet,\n network inet6,",
+ overlaySnippet: "",
+ nfsSnippet: "",
+}}
+
+func (s *backendSuite) TestNFSAndOverlaySnippets(c *C) {
+ restore := release.MockAppArmorLevel(release.FullAppArmor)
+ defer restore()
+ restore = apparmor.MockIsHomeUsingNFS(func() (bool, error) { return true, nil })
+ defer restore()
+ restore = apparmor.MockIsRootWritableOverlay(func() (string, error) { return "/upper", nil })
+ defer restore()
+ s.Iface.AppArmorPermanentSlotCallback = func(spec *apparmor.Specification, slot *snap.SlotInfo) error {
+ return nil
+ }
+
+ for _, scenario := range nfsAndOverlaySnippetsScenarios {
+ snapInfo := s.InstallSnap(c, scenario.opts, ifacetest.SambaYamlV1, 1)
+ profile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.smbd")
+ c.Check(profile, testutil.FileContains, scenario.overlaySnippet)
+ c.Check(profile, testutil.FileContains, scenario.nfsSnippet)
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+var casperOverlaySnippetsScenarios = []nfsAndOverlaySnippetsScenario{{
+ // By default apparmor is enforcing mode.
+ opts: interfaces.ConfinementOptions{},
+ overlaySnippet: `"/upper/{,**/}" r,`,
+}, {
+ // DevMode switches apparmor to non-enforcing (complain) mode.
+ opts: interfaces.ConfinementOptions{DevMode: true},
+ overlaySnippet: `"/upper/{,**/}" r,`,
+}, {
+ // JailMode switches apparmor to enforcing mode even in the presence of DevMode.
+ opts: interfaces.ConfinementOptions{DevMode: true, JailMode: true},
+ overlaySnippet: `"/upper/{,**/}" r,`,
+}, {
+ // Classic confinement (without jailmode) uses apparmor in complain mode by default and ignores all snippets.
+ opts: interfaces.ConfinementOptions{Classic: true},
+ overlaySnippet: "",
+}, {
+ // Classic confinement in JailMode uses enforcing apparmor.
+ opts: interfaces.ConfinementOptions{Classic: true, JailMode: true},
+ // FIXME: logic in backend.addContent is wrong for this case
+ //overlaySnippet: `"/upper/{,**/}" r,`,
+ overlaySnippet: "",
+}}
+
+func (s *backendSuite) TestCasperOverlaySnippets(c *C) {
+ restore := release.MockAppArmorLevel(release.FullAppArmor)
+ defer restore()
+ restore = apparmor.MockIsHomeUsingNFS(func() (bool, error) { return false, nil })
+ defer restore()
+ restore = apparmor.MockIsRootWritableOverlay(func() (string, error) { return "/upper", nil })
+ defer restore()
+ s.Iface.AppArmorPermanentSlotCallback = func(spec *apparmor.Specification, slot *snap.SlotInfo) error {
+ return nil
+ }
+
+ for _, scenario := range casperOverlaySnippetsScenarios {
+ snapInfo := s.InstallSnap(c, scenario.opts, ifacetest.SambaYamlV1, 1)
+ profile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.smbd")
+ c.Check(profile, testutil.FileContains, scenario.overlaySnippet)
+ s.RemoveSnap(c, snapInfo)
+ }
+}
diff --git a/interfaces/apparmor/export_test.go b/interfaces/apparmor/export_test.go
index d09f65d8a1..e679bb6a34 100644
--- a/interfaces/apparmor/export_test.go
+++ b/interfaces/apparmor/export_test.go
@@ -38,6 +38,15 @@ func MockIsHomeUsingNFS(new func() (bool, error)) (restore func()) {
}
}
+// MockIsRootWritableOverlay mocks the real implementation of osutil.IsRootWritableOverlay
+func MockIsRootWritableOverlay(new func() (string, error)) (restore func()) {
+ old := isRootWritableOverlay
+ isRootWritableOverlay = new
+ return func() {
+ isRootWritableOverlay = old
+ }
+}
+
// MockProcSelfExe mocks the location of /proc/self/exe read by setupSnapConfineGeneratedPolicy.
func MockProcSelfExe(symlink string) (restore func()) {
old := procSelfExe
diff --git a/interfaces/apparmor/template.go b/interfaces/apparmor/template.go
index 728784d2bf..145ab1f20a 100644
--- a/interfaces/apparmor/template.go
+++ b/interfaces/apparmor/template.go
@@ -509,6 +509,17 @@ var nfsSnippet = `
network inet6,
`
+// overlayRootSnippet contains the extra permissions necessary for snap and
+// snap-confine to operate on systems where '/' is a writable overlay fs.
+// AppArmor requires directory reads for upperdir (but these aren't otherwise
+// visible to the snap). While we filter AppArmor regular expression (AARE)
+// characters elsewhere, we double quote the path in case UPPERDIR has spaces.
+var overlayRootSnippet = `
+ # snapd autogenerated workaround for systems using '/' on overlayfs. For
+ # details see: https://bugs.launchpad.net/apparmor/+bug/1703674
+ "###UPPERDIR###/{,**/}" r,
+`
+
// updateNSTemplate contains an apparmor profile for snap-update-ns.
// The template contains variable references to encode per-snap layout
// requirements so that snap-update-ns doesn't need to have broad permissions
diff --git a/interfaces/system_key.go b/interfaces/system_key.go
index 881f294055..cec587b234 100644
--- a/interfaces/system_key.go
+++ b/interfaces/system_key.go
@@ -39,6 +39,7 @@ type systemKey struct {
BuildID string `yaml:"build-id"`
AppArmorFeatures []string `yaml:"apparmor-features"`
NFSHome bool `yaml:"nfs-home"`
+ OverlayRoot string `yaml:"overlay-root"`
Core string `yaml:"core,omitempty"`
}
@@ -70,6 +71,14 @@ func generateSystemKey() *systemKey {
if err != nil {
logger.Noticef("cannot determine nfs usage in generateSystemKey: %v", err)
}
+
+ // Add if '/' is on overlayfs so we can add AppArmor rules for
+ // upperdir such that if this changes, we change our profile.
+ sk.OverlayRoot, err = osutil.IsRootWritableOverlay()
+ if err != nil {
+ logger.Noticef("cannot determine root filesystem on overlay in generateSystemKey: %v", err)
+ }
+
// Add the current Core path, we need this because we call helpers
// like snap-confine from core that will need an updated profile
// if it changes
diff --git a/interfaces/system_key_test.go b/interfaces/system_key_test.go
index f15f04acd7..10756bd146 100644
--- a/interfaces/system_key_test.go
+++ b/interfaces/system_key_test.go
@@ -69,9 +69,12 @@ func (s *systemKeySuite) TestInterfaceSystemKey(c *C) {
}
nfsHome, err := osutil.IsHomeUsingNFS()
c.Assert(err, IsNil)
+ overlayRoot, err := osutil.IsRootWritableOverlay()
+ c.Assert(err, IsNil)
c.Check(systemKey, Equals, fmt.Sprintf(`build-id: %s
apparmor-features:%snfs-home: %v
-`, s.buildID, apparmorFeaturesStr, nfsHome))
+overlay-root: "%v"
+`, s.buildID, apparmorFeaturesStr, nfsHome, overlayRoot))
}
func (ts *systemKeySuite) TestInterfaceDigest(c *C) {
diff --git a/osutil/overlay.go b/osutil/overlay.go
new file mode 100644
index 0000000000..c89f7c678b
--- /dev/null
+++ b/osutil/overlay.go
@@ -0,0 +1,79 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016-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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil
+
+import (
+ "fmt"
+ "strings"
+)
+
+// IsRootWritableOverlay detects if the current '/' is a writable overlay
+// (fstype is 'overlay' and 'upperdir' is specified) and returns upperdir or
+// the empty string if not used.
+//
+// Debian-based LiveCD systems use 'casper' to setup the mounts, and part of
+// this setup involves running mount commands to mount / on /cow as overlay and
+// results in AppArmor seeing '/upper' as the upperdir rather than '/cow/upper'
+// as seen in mountinfo. By the time snapd is run, we don't have enough
+// information to discover /cow through mount parent ID or st_dev (maj:min).
+// While overlay doesn't use the mount source for anything itself, casper sets
+// the mount source ('/cow' with the above) for its own purposes and we can
+// leverage this by stripping the mount source from the beginning of upperdir.
+//
+// https://www.kernel.org/doc/Documentation/filesystems/overlayfs.txt
+// man 5 proc
+//
+// Currently uses variables and Mock functions from nfs.go
+func IsRootWritableOverlay() (string, error) {
+ mountinfo, err := LoadMountInfo(procSelfMountInfo)
+ if err != nil {
+ return "", fmt.Errorf("cannot parse %s: %s", procSelfMountInfo, err)
+ }
+ for _, entry := range mountinfo {
+ if entry.FsType == "overlay" && entry.MountDir == "/" {
+ if dir, ok := entry.SuperOptions["upperdir"]; ok {
+ // upperdir must be an absolute path without
+ // any AppArmor regular expression (AARE)
+ // characters or double quotes to be considered
+ if !strings.HasPrefix(dir, "/") || strings.ContainsAny(dir, `?*[]{}^"`) {
+ continue
+ }
+ // if mount source is path, strip it from dir
+ // (for casper)
+ if strings.HasPrefix(entry.MountSource, "/") {
+ dir = strings.TrimPrefix(dir, strings.TrimRight(entry.MountSource, "/"))
+ }
+
+ dir = strings.TrimRight(dir, "/")
+
+ // The resulting trimmed dir must be an
+ // absolute path that is not '/'
+ if len(dir) < 2 || !strings.HasPrefix(dir, "/") {
+ continue
+ }
+
+ // Make sure trailing slashes are predicatably
+ // missing
+ return dir, nil
+ }
+ }
+ }
+ return "", nil
+}
diff --git a/osutil/overlay_test.go b/osutil/overlay_test.go
new file mode 100644
index 0000000000..371b60dcd2
--- /dev/null
+++ b/osutil/overlay_test.go
@@ -0,0 +1,99 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016-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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/osutil"
+)
+
+type overlaySuite struct{}
+
+var _ = Suite(&overlaySuite{})
+
+func (s *overlaySuite) TestIsRootWritableOverlay(c *C) {
+ cases := []struct {
+ mountinfo string
+ overlay string
+ errorPattern string
+ }{{
+ // Errors from parsing mountinfo are propagated.
+ mountinfo: "bad syntax",
+ errorPattern: "cannot parse .*/mountinfo.*, .*",
+ }, {
+ // overlay mounted on / are recognized
+ // casper mount source /cow
+ mountinfo: "31 1 0:26 / / rw,relatime shared:1 - overlay /cow rw,lowerdir=//filesystem.squashfs,upperdir=/cow/upper,workdir=/cow/work",
+ overlay: "/upper",
+ }, {
+ // casper mount source upperdir trailing slash
+ mountinfo: "31 1 0:26 / / rw,relatime shared:1 - overlay /cow rw,lowerdir=//filesystem.squashfs,upperdir=/cow/upper/,workdir=/cow/work",
+ overlay: "/upper",
+ }, {
+ // casper mount source trailing slash
+ mountinfo: "31 1 0:26 / / rw,relatime shared:1 - overlay /cow/ rw,lowerdir=//filesystem.squashfs,upperdir=/cow/upper,workdir=/cow/work",
+ overlay: "/upper",
+ }, {
+ // non-casper mount source
+ mountinfo: "31 1 0:26 / / rw,relatime shared:1 - overlay overlay rw,lowerdir=//filesystem.squashfs,upperdir=/cow/upper,workdir=/cow/work",
+ overlay: "/cow/upper",
+ }, {
+ // overlay mounted elsewhere are ignored
+ mountinfo: "31 1 0:26 /elsewhere /elsewhere rw,relatime shared:1 - overlay /cow rw,lowerdir=//filesystem.squashfs,upperdir=/cow/upper,workdir=/cow/work",
+ }, {
+ mountinfo: "31 1 0:26 /elsewhere /elsewhere rw,relatime shared:1 - overlay overlay rw,lowerdir=//filesystem.squashfs,upperdir=/cow/upper,workdir=/cow/work",
+ }, {
+ // casper overlay which results in empty upperdir are ignored
+ mountinfo: "31 1 0:26 / / rw,relatime shared:1 - overlay /upper rw,lowerdir=//filesystem.squashfs,upperdir=/upper,workdir=/cow/work",
+ }, {
+ // overlay with relative paths, AARE or double quotes are
+ // ignored
+ mountinfo: "31 1 0:26 / / rw,relatime shared:1 - overlay /cow rw,lowerdir=//filesystem.squashfs,upperdir=cow/upper,workdir=/cow/work",
+ }, {
+ mountinfo: "31 1 0:26 / / rw,relatime shared:1 - overlay /cow rw,lowerdir=//filesystem.squashfs,upperdir=/cow/bad?upper,workdir=/cow/work",
+ }, {
+ mountinfo: "31 1 0:26 / / rw,relatime shared:1 - overlay /cow rw,lowerdir=//filesystem.squashfs,upperdir=/cow/bad*upper,workdir=/cow/work",
+ }, {
+ mountinfo: "31 1 0:26 / / rw,relatime shared:1 - overlay /cow rw,lowerdir=//filesystem.squashfs,upperdir=/cow/bad[upper,workdir=/cow/work",
+ }, {
+ mountinfo: "31 1 0:26 / / rw,relatime shared:1 - overlay overlay rw,lowerdir=//filesystem.squashfs,upperdir=/cow/bad]upper,workdir=/cow/work",
+ }, {
+ mountinfo: "31 1 0:26 / / rw,relatime shared:1 - overlay overlay rw,lowerdir=//filesystem.squashfs,upperdir=/cow/bad{upper,workdir=/cow/work",
+ }, {
+ mountinfo: "31 1 0:26 / / rw,relatime shared:1 - overlay overlay rw,lowerdir=//filesystem.squashfs,upperdir=/cow/bad}upper,workdir=/cow/work",
+ }, {
+ mountinfo: "31 1 0:26 / / rw,relatime shared:1 - overlay overlay rw,lowerdir=//filesystem.squashfs,upperdir=/cow/bad^upper,workdir=/cow/work",
+ }, {
+ mountinfo: "31 1 0:26 / / rw,relatime shared:1 - overlay overlay rw,lowerdir=//filesystem.squashfs,upperdir=/cow/bad\"upper,workdir=/cow/work",
+ }}
+ for _, tc := range cases {
+ restore := osutil.MockMountInfo(tc.mountinfo)
+ defer restore()
+
+ overlay, err := osutil.IsRootWritableOverlay()
+ if tc.errorPattern != "" {
+ c.Assert(err, ErrorMatches, tc.errorPattern, Commentf("test case %#v", tc))
+ } else {
+ c.Assert(err, IsNil)
+ }
+ c.Assert(overlay, Equals, tc.overlay)
+ }
+}
diff --git a/overlord/configstate/configcore/services.go b/overlord/configstate/configcore/services.go
index f3046f171f..27e6df4090 100644
--- a/overlord/configstate/configcore/services.go
+++ b/overlord/configstate/configcore/services.go
@@ -35,9 +35,8 @@ func (l *sysdLogger) Notify(status string) {
// swtichDisableService switches a service in/out of disabled state
// where "true" means disabled and "false" means enabled.
-func switchDisableService(service, value string) error {
+func switchDisableService(serviceName, value string) error {
sysd := systemd.New(dirs.GlobalRootDir, &sysdLogger{})
- serviceName := fmt.Sprintf("%s.service", service)
switch value {
case "true":
@@ -62,16 +61,18 @@ func switchDisableService(service, value string) error {
}
// services that can be disabled
-var services = []string{"ssh", "rsyslog"}
-
func handleServiceDisableConfiguration(tr Conf) error {
+ var services = []struct{ configName, systemdName string }{
+ {"ssh", "sshd.service"},
+ {"rsyslog", "rsyslog.service"},
+ }
for _, service := range services {
- output, err := coreCfg(tr, fmt.Sprintf("service.%s.disable", service))
+ output, err := coreCfg(tr, fmt.Sprintf("service.%s.disable", service.configName))
if err != nil {
return err
}
if output != "" {
- if err := switchDisableService(service, output); err != nil {
+ if err := switchDisableService(service.systemdName, output); err != nil {
return err
}
}
diff --git a/overlord/configstate/configcore/services_test.go b/overlord/configstate/configcore/services_test.go
index 0afd2f510f..29782c988a 100644
--- a/overlord/configstate/configcore/services_test.go
+++ b/overlord/configstate/configcore/services_test.go
@@ -55,28 +55,28 @@ func (s *servicesSuite) TearDownTest(c *C) {
}
func (s *servicesSuite) TestConfigureServiceInvalidValue(c *C) {
- err := configcore.SwitchDisableService("ssh", "xxx")
+ err := configcore.SwitchDisableService("ssh.service", "xxx")
c.Check(err, ErrorMatches, `option "ssh.service" has invalid value "xxx"`)
}
func (s *servicesSuite) TestConfigureServiceNotDisabled(c *C) {
- err := configcore.SwitchDisableService("ssh", "false")
+ err := configcore.SwitchDisableService("sshd.service", "false")
c.Assert(err, IsNil)
c.Check(s.systemctlArgs, DeepEquals, [][]string{
- {"--root", dirs.GlobalRootDir, "unmask", "ssh.service"},
- {"--root", dirs.GlobalRootDir, "enable", "ssh.service"},
- {"start", "ssh.service"},
+ {"--root", dirs.GlobalRootDir, "unmask", "sshd.service"},
+ {"--root", dirs.GlobalRootDir, "enable", "sshd.service"},
+ {"start", "sshd.service"},
})
}
func (s *servicesSuite) TestConfigureServiceDisabled(c *C) {
- err := configcore.SwitchDisableService("ssh", "true")
+ err := configcore.SwitchDisableService("sshd.service", "true")
c.Assert(err, IsNil)
c.Check(s.systemctlArgs, DeepEquals, [][]string{
- {"--root", dirs.GlobalRootDir, "disable", "ssh.service"},
- {"--root", dirs.GlobalRootDir, "mask", "ssh.service"},
- {"stop", "ssh.service"},
- {"show", "--property=ActiveState", "ssh.service"},
+ {"--root", dirs.GlobalRootDir, "disable", "sshd.service"},
+ {"--root", dirs.GlobalRootDir, "mask", "sshd.service"},
+ {"stop", "sshd.service"},
+ {"show", "--property=ActiveState", "sshd.service"},
})
}
@@ -84,17 +84,23 @@ func (s *servicesSuite) TestConfigureServiceDisabledIntegration(c *C) {
restore := release.MockOnClassic(false)
defer restore()
- for _, srvName := range []string{"ssh", "rsyslog"} {
+ for _, service := range []struct {
+ cfgName string
+ systemdName string
+ }{
+ {"ssh", "sshd.service"},
+ {"rsyslog", "rsyslog.service"},
+ } {
s.systemctlArgs = nil
err := configcore.Run(&mockConf{
state: s.state,
conf: map[string]interface{}{
- fmt.Sprintf("service.%s.disable", srvName): true,
+ fmt.Sprintf("service.%s.disable", service.cfgName): true,
},
})
c.Assert(err, IsNil)
- srv := fmt.Sprintf("%s.service", srvName)
+ srv := service.systemdName
c.Check(s.systemctlArgs, DeepEquals, [][]string{
{"--root", dirs.GlobalRootDir, "disable", srv},
{"--root", dirs.GlobalRootDir, "mask", srv},
@@ -108,17 +114,23 @@ func (s *servicesSuite) TestConfigureServiceEnableIntegration(c *C) {
restore := release.MockOnClassic(false)
defer restore()
- for _, srvName := range []string{"ssh", "rsyslog"} {
+ for _, service := range []struct {
+ cfgName string
+ systemdName string
+ }{
+ {"ssh", "sshd.service"},
+ {"rsyslog", "rsyslog.service"},
+ } {
s.systemctlArgs = nil
err := configcore.Run(&mockConf{
state: s.state,
conf: map[string]interface{}{
- fmt.Sprintf("service.%s.disable", srvName): false,
+ fmt.Sprintf("service.%s.disable", service.cfgName): false,
},
})
c.Assert(err, IsNil)
- srv := fmt.Sprintf("%s.service", srvName)
+ srv := service.systemdName
c.Check(s.systemctlArgs, DeepEquals, [][]string{
{"--root", dirs.GlobalRootDir, "unmask", srv},
{"--root", dirs.GlobalRootDir, "enable", srv},
diff --git a/snap/info.go b/snap/info.go
index a108d09ff6..f266a828ae 100644
--- a/snap/info.go
+++ b/snap/info.go
@@ -559,6 +559,8 @@ type AppInfo struct {
Before []string
Timer *TimerInfo
+
+ Autostart string
}
// ScreenshotInfo provides information about a screenshot.
diff --git a/snap/info_snap_yaml.go b/snap/info_snap_yaml.go
index c0048cfeb7..9879d1c03e 100644
--- a/snap/info_snap_yaml.go
+++ b/snap/info_snap_yaml.go
@@ -83,6 +83,8 @@ type appYaml struct {
Before []string `yaml:"before,omitempty"`
Timer string `yaml:"timer,omitempty"`
+
+ Autostart string `yaml:"autostart,omitempty"`
}
type hookYaml struct {
@@ -299,6 +301,7 @@ func setAppsFromSnapYaml(y snapYaml, snap *Info) error {
RefreshMode: yApp.RefreshMode,
Before: yApp.Before,
After: yApp.After,
+ Autostart: yApp.Autostart,
}
if len(y.Plugs) > 0 || len(yApp.PlugNames) > 0 {
app.Plugs = make(map[string]*PlugInfo)
diff --git a/snap/info_snap_yaml_test.go b/snap/info_snap_yaml_test.go
index aac8c4b9e5..8eb582a6d5 100644
--- a/snap/info_snap_yaml_test.go
+++ b/snap/info_snap_yaml_test.go
@@ -1653,3 +1653,30 @@ apps:
app := info.Apps["foo"]
c.Check(app.Timer, DeepEquals, &snap.TimerInfo{App: app, Timer: "mon,10:00-12:00"})
}
+
+func (s *YamlSuite) TestSnapYamlAppAutostart(c *C) {
+ yAutostart := []byte(`name: wat
+version: 42
+apps:
+ foo:
+ command: bin/foo
+ autostart: foo.desktop
+
+`)
+ info, err := snap.InfoFromSnapYaml(yAutostart)
+ c.Assert(err, IsNil)
+ app := info.Apps["foo"]
+ c.Check(app.Autostart, Equals, "foo.desktop")
+
+ yNoAutostart := []byte(`name: wat
+version: 42
+apps:
+ foo:
+ command: bin/foo
+
+`)
+ info, err = snap.InfoFromSnapYaml(yNoAutostart)
+ c.Assert(err, IsNil)
+ app = info.Apps["foo"]
+ c.Check(app.Autostart, Equals, "")
+}
diff --git a/snap/squashfs/squashfs.go b/snap/squashfs/squashfs.go
index 369abd0506..379b53a001 100644
--- a/snap/squashfs/squashfs.go
+++ b/snap/squashfs/squashfs.go
@@ -28,6 +28,7 @@ import (
"path"
"path/filepath"
"regexp"
+ "time"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/strutil"
@@ -303,3 +304,32 @@ func (s *Snap) Build(buildDir string) error {
).Run()
})
}
+
+// BuildDate returns the "Creation or last append time" as reported by unsquashfs.
+func (s *Snap) BuildDate() time.Time {
+ return BuildDate(s.path)
+}
+
+// BuildDate returns the "Creation or last append time" as reported by unsquashfs.
+func BuildDate(path string) time.Time {
+ var t0 time.Time
+
+ const prefix = "Creation or last append time "
+ m := &strutil.MatchCounter{
+ Regexp: regexp.MustCompile("(?m)^" + prefix + ".*$"),
+ N: 1,
+ }
+
+ cmd := exec.Command("unsquashfs", "-s", path)
+ cmd.Stdout = m
+ cmd.Stderr = m
+ if err := cmd.Run(); err != nil {
+ return t0
+ }
+ matches, count := m.Matches()
+ if count != 1 {
+ return t0
+ }
+ t0, _ = time.Parse(time.ANSIC, matches[0][len(prefix):])
+ return t0
+}
diff --git a/snap/squashfs/squashfs_test.go b/snap/squashfs/squashfs_test.go
index 3fa5f052d9..8b07a25b26 100644
--- a/snap/squashfs/squashfs_test.go
+++ b/snap/squashfs/squashfs_test.go
@@ -21,6 +21,7 @@ package squashfs
import (
"io/ioutil"
+ "math"
"os"
"os/exec"
"path/filepath"
@@ -351,3 +352,19 @@ func (s *SquashfsTestSuite) TestUnsquashfsStderrWriter(c *C) {
}
}
}
+
+func (s *SquashfsTestSuite) TestBuildDate(c *C) {
+ // make a directory
+ d := c.MkDir()
+ // set its time waaay back
+ now := time.Now()
+ then := now.Add(-10000 * time.Hour)
+ c.Assert(os.Chtimes(d, then, then), IsNil)
+ // make a snap using this directory
+ filename := filepath.Join(c.MkDir(), "foo.snap")
+ snap := New(filename)
+ c.Assert(snap.Build(d), IsNil)
+ // and see it's BuildDate is _now_, not _then_.
+ c.Check(BuildDate(filename), Equals, snap.BuildDate())
+ c.Check(math.Abs(now.Sub(snap.BuildDate()).Seconds()) <= 61, Equals, true)
+}
diff --git a/store/details_v2.go b/store/details_v2.go
new file mode 100644
index 0000000000..0878a5e247
--- /dev/null
+++ b/store/details_v2.go
@@ -0,0 +1,177 @@
+// -*- 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package store
+
+import (
+ "fmt"
+ "strconv"
+
+ "github.com/snapcore/snapd/snap"
+)
+
+// storeSnap holds the information sent as JSON by the store for a snap.
+type storeSnap struct {
+ Architectures []string `json:"architectures"`
+ Base string `json:"base"`
+ Confinement string `json:"confinement"`
+ Contact string `json:"contact"`
+ CreatedAt string `json:"created-at"` // revision timestamp
+ Description string `json:"description"`
+ Download storeSnapDownload `json:"download"`
+ Epoch snap.Epoch `json:"epoch"`
+ License string `json:"license"`
+ Name string `json:"name"`
+ Prices map[string]string `json:"prices"` // currency->price, free: {"USD": "0"}
+ Private bool `json:"private"`
+ Publisher storeAccount `json:"publisher"`
+ Revision int `json:"revision"` // store revisions are ints starting at 1
+ SnapID string `json:"snap-id"`
+ SnapYAML string `json:"snap-yaml"` // optional
+ Summary string `json:"summary"`
+ Title string `json:"title"`
+ Type snap.Type `json:"type"`
+ Version string `json:"version"`
+
+ // TODO: not yet defined: channel map
+
+ // media
+ Media []storeSnapMedia `json:"media"`
+}
+
+type storeSnapDownload struct {
+ Sha3_384 string `json:"sha3-384"`
+ Size int64 `json:"size"`
+ URL string `json:"url"`
+ Deltas []storeSnapDelta `json:"deltas"`
+}
+
+type storeSnapDelta struct {
+ Format string `json:"format"`
+ Sha3_384 string `json:"sha3-384"`
+ Size int64 `json:"size"`
+ Source int `json:"source"`
+ Target int `json:"target"`
+ URL string `json:"url"`
+}
+
+type storeAccount struct {
+ ID string `json:"id"`
+ Username string `json:"username"`
+ DisplayName string `json:"display-name"`
+}
+
+type storeSnapMedia struct {
+ Type string `json:"type"` // icon/screenshot
+ URL string `json:"url"`
+ Width int64 `json:"width"`
+ Height int64 `json:"height"`
+}
+
+func infoFromStoreSnap(d *storeSnap) (*snap.Info, error) {
+ info := &snap.Info{}
+ info.RealName = d.Name
+ info.Revision = snap.R(d.Revision)
+ info.SnapID = d.SnapID
+ info.EditedTitle = d.Title
+ info.EditedSummary = d.Summary
+ info.EditedDescription = d.Description
+ info.Private = d.Private
+ info.Contact = d.Contact
+ info.Architectures = d.Architectures
+ info.Type = d.Type
+ info.Version = d.Version
+ info.Epoch = d.Epoch
+ info.Confinement = snap.ConfinementType(d.Confinement)
+ info.Base = d.Base
+ info.License = d.License
+ info.PublisherID = d.Publisher.ID
+ info.Publisher = d.Publisher.Username
+ info.DownloadURL = d.Download.URL
+ info.Size = d.Download.Size
+ info.Sha3_384 = d.Download.Sha3_384
+ if len(d.Download.Deltas) > 0 {
+ deltas := make([]snap.DeltaInfo, len(d.Download.Deltas))
+ for i, d := range d.Download.Deltas {
+ deltas[i] = snap.DeltaInfo{
+ FromRevision: d.Source,
+ ToRevision: d.Target,
+ Format: d.Format,
+ DownloadURL: d.URL,
+ Size: d.Size,
+ Sha3_384: d.Sha3_384,
+ }
+ }
+ info.Deltas = deltas
+ }
+
+ // fill in the plug/slot data
+ if rawYamlInfo, err := snap.InfoFromSnapYaml([]byte(d.SnapYAML)); err == nil {
+ if info.Plugs == nil {
+ info.Plugs = make(map[string]*snap.PlugInfo)
+ }
+ for k, v := range rawYamlInfo.Plugs {
+ info.Plugs[k] = v
+ info.Plugs[k].Snap = info
+ }
+ if info.Slots == nil {
+ info.Slots = make(map[string]*snap.SlotInfo)
+ }
+ for k, v := range rawYamlInfo.Slots {
+ info.Slots[k] = v
+ info.Slots[k].Snap = info
+ }
+ }
+
+ // convert prices
+ if len(d.Prices) > 0 {
+ prices := make(map[string]float64, len(d.Prices))
+ for currency, priceStr := range d.Prices {
+ price, err := strconv.ParseFloat(priceStr, 64)
+ if err != nil {
+ return nil, fmt.Errorf("cannot parse snap price: %v", err)
+ }
+ prices[currency] = price
+ }
+ info.Paid = true
+ info.Prices = prices
+ }
+
+ // media
+ screenshots := make([]snap.ScreenshotInfo, 0, len(d.Media))
+ for _, mediaObj := range d.Media {
+ switch mediaObj.Type {
+ case "icon":
+ if info.IconURL == "" {
+ info.IconURL = mediaObj.URL
+ }
+ case "screenshot":
+ screenshots = append(screenshots, snap.ScreenshotInfo{
+ URL: mediaObj.URL,
+ Width: mediaObj.Width,
+ Height: mediaObj.Height,
+ })
+ }
+ }
+ if len(screenshots) > 0 {
+ info.Screenshots = screenshots
+ }
+
+ return info, nil
+}
diff --git a/store/details_v2_test.go b/store/details_v2_test.go
new file mode 100644
index 0000000000..ce048c9f8f
--- /dev/null
+++ b/store/details_v2_test.go
@@ -0,0 +1,294 @@
+// -*- 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package store
+
+import (
+ "reflect"
+
+ "encoding/json"
+ "strings"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type detailsV2Suite struct{}
+
+var _ = Suite(&detailsV2Suite{})
+
+const (
+ coreStoreJSON = `{
+ "architectures": [
+ "amd64"
+ ],
+ "base": null,
+ "confinement": "strict",
+ "contact": "mailto:snappy-canonical-storeaccount@canonical.com",
+ "created-at": "2018-01-22T07:49:19.440720+00:00",
+ "description": "The core runtime environment for snapd",
+ "download": {
+ "sha3-384": "b691f6dde3d8022e4db563840f0ef82320cb824b6292ffd027dbc838535214dac31c3512c619beaf73f1aeaf35ac62d5",
+ "size": 85291008,
+ "url": "https://api.snapcraft.io/api/v1/snaps/download/99T7MUlRhtI3U0QFgl5mXXESAiSwt776_3887.snap",
+ "deltas": []
+ },
+ "epoch": {
+ "read": [0],
+ "write": [0]
+ },
+ "license": null,
+ "name": "core",
+ "prices": {},
+ "private": false,
+ "publisher": {
+ "id": "canonical",
+ "username": "canonical",
+ "display-name": "Canonical"
+ },
+ "revision": 3887,
+ "snap-id": "99T7MUlRhtI3U0QFgl5mXXESAiSwt776",
+ "summary": "snapd runtime environment",
+ "title": "core",
+ "type": "os",
+ "version": "16-2.30",
+ "media": []
+}`
+
+ thingyStoreJSON = `{
+ "architectures": [
+ "amd64"
+ ],
+ "base": "base-18",
+ "confinement": "strict",
+ "contact": "https://thingy.com",
+ "created-at": "2018-01-26T11:38:35.536410+00:00",
+ "description": "Useful thingy for thinging",
+ "download": {
+ "sha3-384": "a29f8d894c92ad19bb943764eb845c6bd7300f555ee9b9dbb460599fecf712775c0f3e2117b5c56b08fcb9d78fc8ae4d",
+ "size": 10000021,
+ "url": "https://api.snapcraft.io/api/v1/snaps/download/XYZEfjn4WJYnm0FzDKwqqRZZI77awQEV_21.snap",
+ "deltas": [
+ {
+ "format": "xdelta3",
+ "source": 19,
+ "target": 21,
+ "url": "https://api.snapcraft.io/api/v1/snaps/download/XYZEfjn4WJYnm0FzDKwqqRZZI77awQEV_19_21_xdelta3.delta",
+ "size": 9999,
+ "sha3-384": "29f8d894c92ad19bb943764eb845c6bd7300f555ee9b9dbb460599fecf712775c0f3e2117b5c56b08fcb9d78fc8ae4df"
+ }
+ ]
+ },
+ "epoch": {
+ "read": [0,1],
+ "write": [1]
+ },
+ "license": "Proprietary",
+ "name": "thingy",
+ "prices": {"USD": "9.99"},
+ "private": false,
+ "publisher": {
+ "id": "ZvtzsxbsHivZLdvzrt0iqW529riGLfXJ",
+ "username": "thingyinc",
+ "display-name": "Thingy Inc."
+ },
+ "revision": 21,
+ "snap-id": "XYZEfjn4WJYnm0FzDKwqqRZZI77awQEV",
+ "snap-yaml": "name: test-snapd-content-plug\nversion: 1.0\napps:\n content-plug:\n command: bin/content-plug\n plugs: [shared-content-plug]\nplugs:\n shared-content-plug:\n interface: content\n target: import\n content: mylib\n default-provider: test-snapd-content-slot\nslots:\n shared-content-slot:\n interface: content\n content: mylib\n read:\n - /\n",
+ "summary": "useful thingy",
+ "title": "thingy",
+ "type": "app",
+ "version": "9.50",
+ "media": [
+ {"type": "icon", "url": "https://dashboard.snapcraft.io/site_media/appmedia/2017/12/Thingy.png"},
+ {"type": "screenshot", "url": "https://dashboard.snapcraft.io/site_media/appmedia/2018/01/Thingy_01.png"},
+ {"type": "screenshot", "url": "https://dashboard.snapcraft.io/site_media/appmedia/2018/01/Thingy_02.png", "width": 600, "height": 200}
+ ]
+}`
+)
+
+func (s *detailsV2Suite) TestInfoFromStoreSnapSimple(c *C) {
+ var snp storeSnap
+ err := json.Unmarshal([]byte(coreStoreJSON), &snp)
+ c.Assert(err, IsNil)
+
+ info, err := infoFromStoreSnap(&snp)
+ c.Assert(err, IsNil)
+ c.Check(snap.Validate(info), IsNil)
+
+ c.Check(info, DeepEquals, &snap.Info{
+ Architectures: []string{"amd64"},
+ SideInfo: snap.SideInfo{
+ RealName: "core",
+ SnapID: "99T7MUlRhtI3U0QFgl5mXXESAiSwt776",
+ Revision: snap.R(3887),
+ Contact: "mailto:snappy-canonical-storeaccount@canonical.com",
+ EditedTitle: "core",
+ EditedSummary: "snapd runtime environment",
+ EditedDescription: "The core runtime environment for snapd",
+ Private: false,
+ Paid: false,
+ },
+ Epoch: *snap.E("0"),
+ Type: snap.TypeOS,
+ Version: "16-2.30",
+ Confinement: snap.StrictConfinement,
+ PublisherID: "canonical",
+ Publisher: "canonical",
+ DownloadInfo: snap.DownloadInfo{
+ DownloadURL: "https://api.snapcraft.io/api/v1/snaps/download/99T7MUlRhtI3U0QFgl5mXXESAiSwt776_3887.snap",
+ Sha3_384: "b691f6dde3d8022e4db563840f0ef82320cb824b6292ffd027dbc838535214dac31c3512c619beaf73f1aeaf35ac62d5",
+ Size: 85291008,
+ },
+ Plugs: make(map[string]*snap.PlugInfo),
+ Slots: make(map[string]*snap.SlotInfo),
+ })
+}
+
+func (s *detailsV2Suite) TestInfoFromStoreSnap(c *C) {
+ var snp storeSnap
+ // base, prices, media
+ err := json.Unmarshal([]byte(thingyStoreJSON), &snp)
+ c.Assert(err, IsNil)
+
+ info, err := infoFromStoreSnap(&snp)
+ c.Assert(err, IsNil)
+ c.Check(snap.Validate(info), IsNil)
+
+ info2 := *info
+ // clear recursive bits
+ info2.Plugs = nil
+ info2.Slots = nil
+ c.Check(&info2, DeepEquals, &snap.Info{
+ Architectures: []string{"amd64"},
+ Base: "base-18",
+ SideInfo: snap.SideInfo{
+ RealName: "thingy",
+ SnapID: "XYZEfjn4WJYnm0FzDKwqqRZZI77awQEV",
+ Revision: snap.R(21),
+ Contact: "https://thingy.com",
+ EditedTitle: "thingy",
+ EditedSummary: "useful thingy",
+ EditedDescription: "Useful thingy for thinging",
+ Private: false,
+ Paid: true,
+ },
+ Epoch: snap.Epoch{
+ Read: []uint32{0, 1},
+ Write: []uint32{1},
+ },
+ Type: snap.TypeApp,
+ Version: "9.50",
+ Confinement: snap.StrictConfinement,
+ License: "Proprietary",
+ PublisherID: "ZvtzsxbsHivZLdvzrt0iqW529riGLfXJ",
+ Publisher: "thingyinc",
+ DownloadInfo: snap.DownloadInfo{
+ DownloadURL: "https://api.snapcraft.io/api/v1/snaps/download/XYZEfjn4WJYnm0FzDKwqqRZZI77awQEV_21.snap",
+ Sha3_384: "a29f8d894c92ad19bb943764eb845c6bd7300f555ee9b9dbb460599fecf712775c0f3e2117b5c56b08fcb9d78fc8ae4d",
+ Size: 10000021,
+ Deltas: []snap.DeltaInfo{
+ {
+ Format: "xdelta3",
+ FromRevision: 19,
+ ToRevision: 21,
+ DownloadURL: "https://api.snapcraft.io/api/v1/snaps/download/XYZEfjn4WJYnm0FzDKwqqRZZI77awQEV_19_21_xdelta3.delta",
+ Size: 9999,
+ Sha3_384: "29f8d894c92ad19bb943764eb845c6bd7300f555ee9b9dbb460599fecf712775c0f3e2117b5c56b08fcb9d78fc8ae4df",
+ },
+ },
+ },
+ Prices: map[string]float64{
+ "USD": 9.99,
+ },
+ IconURL: "https://dashboard.snapcraft.io/site_media/appmedia/2017/12/Thingy.png",
+ Screenshots: []snap.ScreenshotInfo{
+ {URL: "https://dashboard.snapcraft.io/site_media/appmedia/2018/01/Thingy_01.png"},
+ {URL: "https://dashboard.snapcraft.io/site_media/appmedia/2018/01/Thingy_02.png", Width: 600, Height: 200},
+ },
+ })
+
+ // validate the plugs/slots
+ c.Assert(info.Plugs, HasLen, 1)
+ plug := info.Plugs["shared-content-plug"]
+ c.Check(plug.Name, Equals, "shared-content-plug")
+ c.Check(plug.Snap, Equals, info)
+ c.Check(plug.Apps, HasLen, 1)
+ c.Check(plug.Apps["content-plug"].Command, Equals, "bin/content-plug")
+
+ c.Assert(info.Slots, HasLen, 1)
+ slot := info.Slots["shared-content-slot"]
+ c.Check(slot.Name, Equals, "shared-content-slot")
+ c.Check(slot.Snap, Equals, info)
+ c.Check(slot.Apps, HasLen, 1)
+ c.Check(slot.Apps["content-plug"].Command, Equals, "bin/content-plug")
+
+ // private
+ err = json.Unmarshal([]byte(strings.Replace(thingyStoreJSON, `"private": false`, `"private": true`, 1)), &snp)
+ c.Assert(err, IsNil)
+
+ info, err = infoFromStoreSnap(&snp)
+ c.Assert(err, IsNil)
+ c.Check(snap.Validate(info), IsNil)
+
+ c.Check(info.Private, Equals, true)
+
+ // check that up to few exceptions info is filled
+ expectedZeroFields := []string{
+ "SuggestedName",
+ "Assumes",
+ "OriginalTitle",
+ "OriginalSummary",
+ "OriginalDescription",
+ "Environment",
+ "LicenseAgreement", // XXX go away?
+ "LicenseVersion", // XXX go away?
+ "Apps",
+ "LegacyAliases",
+ "Hooks",
+ "BadInterfaces",
+ "Broken",
+ "MustBuy",
+ "Channels", // TODO: support coming later
+ "Tracks", // TODO: support coming later
+ "Layout",
+ "SideInfo.Channel",
+ "DownloadInfo.AnonDownloadURL", // TODO: going away at some point
+ }
+ var checker func(string, reflect.Value)
+ checker = func(pfx string, x reflect.Value) {
+ t := x.Type()
+ for i := 0; i < x.NumField(); i++ {
+ f := t.Field(i)
+ v := x.Field(i)
+ if f.Anonymous {
+ checker(pfx+f.Name+".", v)
+ continue
+ }
+ if reflect.DeepEqual(v.Interface(), reflect.Zero(f.Type).Interface()) {
+ name := pfx + f.Name
+ c.Check(expectedZeroFields, testutil.Contains, name, Commentf("%s not set", name))
+ }
+ }
+ }
+ x := reflect.ValueOf(info).Elem()
+ checker("", x)
+}
diff --git a/tests/main/interfaces-broadcom-asic-control/task.yaml b/tests/main/interfaces-broadcom-asic-control/task.yaml
index cc82b688d6..b2b1bd589a 100644
--- a/tests/main/interfaces-broadcom-asic-control/task.yaml
+++ b/tests/main/interfaces-broadcom-asic-control/task.yaml
@@ -22,7 +22,6 @@ restore: |
clean_file /dev/linux-kernel-bde
clean_file /dev/linux-bcm-knet
- clean_dir /sys/devices/pci00test/
clean_file "/run/udev/data/+pci:0test"
execute: |
@@ -57,7 +56,9 @@ execute: |
done
fi
- test-snapd-broadcom-asic-control.sh -c "ls /sys/bus/pci/devices/"
+ if [ -d /sys/bus/pci/devices ]; then
+ test-snapd-broadcom-asic-control.sh -c "ls /sys/bus/pci/devices/"
+ fi
test-snapd-broadcom-asic-control.sh -c "cat /run/udev/data/+pci:0test"
if [ "$(snap debug confinement)" = partial ] ; then
diff --git a/tests/main/listing/task.yaml b/tests/main/listing/task.yaml
index 731393733a..48bd10c9fd 100644
--- a/tests/main/listing/task.yaml
+++ b/tests/main/listing/task.yaml
@@ -16,6 +16,8 @@ execute: |
elif [ "$SRU_VALIDATION" = "1" ]; then
echo "When sru validation is done the core snap is installed from the store"
expected='^core .* [0-9]{2}-[0-9.]+(~[a-z0-9]+)?(\+[0-9]+\.[0-9a-f]+)? +[0-9]+ +stable +canonical +core *$'
+ elif [ "$SPREAD_BACKEND" = "external" ]; then
+ expected='^core .* [0-9]{2}-[0-9.]+(~[a-z0-9]+)?(\+git[0-9]+\.[0-9a-f]+)? +[0-9]+ +(edge|beta|candidate|stable) +canonical +core *$'
else
expected='^core .* [0-9]{2}-[0-9.]+(~[a-z0-9]+)?(\+git[0-9]+\.[0-9a-f]+)? +[0-9]+ +edge +canonical +core *$'
fi
diff --git a/timeutil/export_test.go b/timeutil/export_test.go
index 8feda29ac0..3a9bb34863 100644
--- a/timeutil/export_test.go
+++ b/timeutil/export_test.go
@@ -23,6 +23,7 @@ import "time"
var (
ParseClockSpan = parseClockSpan
+ HumanTimeSince = humanTimeSince
)
func MockTimeNow(f func() time.Time) (restorer func()) {
diff --git a/timeutil/human.go b/timeutil/human.go
new file mode 100644
index 0000000000..c432c7cd3e
--- /dev/null
+++ b/timeutil/human.go
@@ -0,0 +1,69 @@
+// -*- 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package timeutil
+
+import (
+ "fmt"
+ "math"
+ "time"
+
+ "github.com/snapcore/snapd/i18n"
+)
+
+func noon(t time.Time) time.Time {
+ y, m, d := t.Date()
+ return time.Date(y, m, d, 12, 0, 0, 0, t.Location())
+}
+
+// Human turns the time into a relative expression of time meant for human
+// consumption.
+// Human(t) --> "today at 07:47 "
+func Human(then time.Time) string {
+ return humanTimeSince(then.Local(), time.Now().Local())
+}
+
+func ngd(d int) uint32 {
+ const max = 1000000
+ if d > max {
+ return uint32((d % max) + max)
+ }
+ return uint32(d)
+}
+
+func humanTimeSince(then, now time.Time) string {
+ d := int(math.Floor(noon(then).Sub(noon(now)).Hours() / 24))
+ switch {
+ case d < -1:
+ // TRANSLATORS: %d will be at least 2; the singular is only included to help gettext
+ return fmt.Sprintf(then.Format(i18n.NG("in %d day, at 15:04 MST", "%d days ago, at 15:04 MST", ngd(-d))), -d)
+ case d == -1:
+ return then.Format(i18n.G("yesterday at 15:04 MST"))
+ case d == 0:
+ return then.Format(i18n.G("today at 15:04 MST"))
+ case d == 1:
+ return then.Format(i18n.G("tomorrow at 15:04 MST"))
+ case d > 1:
+ // TRANSLATORS: %d will be at least 2; the singular is only included to help gettext
+ return fmt.Sprintf(then.Format(i18n.NG("in %d day, at 15:04 MST", "in %d days, at 15:04 MST", ngd(d))), d)
+ default:
+ // the following message is brought to you by Joel Armando, the self-described awesome and sexy mathematician.
+ panic("you have broken the law of trichotomy! ℤ is no longer totally ordered! chaos ensues!")
+ }
+}
diff --git a/timeutil/human_test.go b/timeutil/human_test.go
new file mode 100644
index 0000000000..bf58931a2c
--- /dev/null
+++ b/timeutil/human_test.go
@@ -0,0 +1,79 @@
+// -*- 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package timeutil_test
+
+import (
+ "time"
+
+ "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/timeutil"
+)
+
+type humanSuite struct {
+ beforeDSTbegins, afterDSTbegins, beforeDSTends, afterDSTends time.Time
+}
+
+var _ = check.Suite(&humanSuite{})
+
+func (s *humanSuite) SetUpSuite(c *check.C) {
+ loc, err := time.LoadLocation("Europe/London")
+ c.Assert(err, check.IsNil)
+
+ s.beforeDSTbegins = time.Date(2017, 3, 26, 0, 59, 0, 0, loc)
+ // note this is actually 2:01am DST
+ s.afterDSTbegins = time.Date(2017, 3, 26, 1, 1, 0, 0, loc)
+
+ // apparently no way to straight out initialise a time inside the DST overlap
+ s.beforeDSTends = time.Date(2017, 10, 29, 0, 59, 0, 0, loc).Add(60 * time.Minute)
+ s.afterDSTends = time.Date(2017, 10, 29, 1, 1, 0, 0, loc)
+
+ // sanity check
+ c.Check(s.beforeDSTbegins.Format("MST"), check.Equals, s.afterDSTends.Format("MST"))
+ c.Check(s.beforeDSTbegins.Format("MST"), check.Equals, "GMT")
+ c.Check(s.afterDSTbegins.Format("MST"), check.Equals, s.beforeDSTends.Format("MST"))
+ c.Check(s.afterDSTbegins.Format("MST"), check.Equals, "BST")
+
+ // “The month, day, hour, min, sec, and nsec values may be outside their
+ // usual ranges and will be normalized during the conversion.”
+ // so you can always add or subtract 1 from a day and it'll just work \o/
+ c.Check(time.Date(2017, -1, -1, -1, -1, -1, 0, loc), check.DeepEquals, time.Date(2016, 10, 29, 22, 58, 59, 0, loc))
+ c.Check(time.Date(2017, 13, 32, 25, 61, 63, 0, loc), check.DeepEquals, time.Date(2018, 2, 2, 2, 2, 3, 0, loc))
+}
+
+func (s *humanSuite) TestHumanTimeDST(c *check.C) {
+ c.Check(timeutil.HumanTimeSince(s.beforeDSTbegins, s.afterDSTbegins), check.Equals, "today at 00:59 GMT")
+ c.Check(timeutil.HumanTimeSince(s.beforeDSTends, s.afterDSTends), check.Equals, "today at 01:59 BST")
+ c.Check(timeutil.HumanTimeSince(s.beforeDSTbegins, s.afterDSTends), check.Equals, "218 days ago, at 00:59 GMT")
+}
+
+func (*humanSuite) TestHuman(c *check.C) {
+ now := time.Now()
+ timePart := now.Format("15:04 MST")
+ y, m, d := now.Date()
+ H, M, S := now.Clock()
+ loc := now.Location()
+
+ c.Check(timeutil.Human(time.Date(y, m, d-2, H, M, S, 0, loc)), check.Equals, "2 days ago, at "+timePart)
+ c.Check(timeutil.Human(time.Date(y, m, d-1, H, M, S, 0, loc)), check.Equals, "yesterday at "+timePart)
+ c.Check(timeutil.Human(now), check.Equals, "today at "+timePart)
+ c.Check(timeutil.Human(time.Date(y, m, d+1, H, M, S, 0, loc)), check.Equals, "tomorrow at "+timePart)
+ c.Check(timeutil.Human(time.Date(y, m, d+2, H, M, S, 0, loc)), check.Equals, "in 2 days, at "+timePart)
+}