diff options
| author | John R. Lenton <jlenton@gmail.com> | 2018-03-02 13:00:20 +0000 |
|---|---|---|
| committer | John R. Lenton <jlenton@gmail.com> | 2018-03-02 13:00:20 +0000 |
| commit | ca103056d91e3ad66623c2c4c9a2edde48623816 (patch) | |
| tree | b6891643d1e188f99fe654ed3033457a43592997 | |
| parent | 6b02a9f529ac8f0a21beed97968a77ffd82a2f0e (diff) | |
| parent | 1faa0f1c46717766c7c090ffe1d324b12bda9260 (diff) | |
Merge remote-tracking branch 'upstream/master' into i18n-ng-awkwardness
| -rw-r--r-- | data/udev/rules.d/66-snapd-autoimport.rules | 2 | ||||
| -rw-r--r-- | interfaces/apparmor/backend.go | 23 | ||||
| -rw-r--r-- | interfaces/apparmor/backend_test.go | 232 | ||||
| -rw-r--r-- | interfaces/apparmor/export_test.go | 9 | ||||
| -rw-r--r-- | interfaces/apparmor/template.go | 11 | ||||
| -rw-r--r-- | interfaces/system_key.go | 9 | ||||
| -rw-r--r-- | interfaces/system_key_test.go | 5 | ||||
| -rw-r--r-- | osutil/overlay.go | 79 | ||||
| -rw-r--r-- | osutil/overlay_test.go | 99 | ||||
| -rw-r--r-- | overlord/configstate/configcore/services.go | 13 | ||||
| -rw-r--r-- | overlord/configstate/configcore/services_test.go | 44 | ||||
| -rw-r--r-- | snap/info.go | 2 | ||||
| -rw-r--r-- | snap/info_snap_yaml.go | 3 | ||||
| -rw-r--r-- | snap/info_snap_yaml_test.go | 27 | ||||
| -rw-r--r-- | snap/squashfs/squashfs.go | 30 | ||||
| -rw-r--r-- | snap/squashfs/squashfs_test.go | 17 | ||||
| -rw-r--r-- | store/details_v2.go | 177 | ||||
| -rw-r--r-- | store/details_v2_test.go | 294 | ||||
| -rw-r--r-- | tests/main/interfaces-broadcom-asic-control/task.yaml | 5 | ||||
| -rw-r--r-- | tests/main/listing/task.yaml | 2 | ||||
| -rw-r--r-- | timeutil/export_test.go | 1 | ||||
| -rw-r--r-- | timeutil/human.go | 69 | ||||
| -rw-r--r-- | timeutil/human_test.go | 79 |
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) +} |
