diff options
| author | Zygmunt Krynicki <zygmunt.krynicki@canonical.com> | 2017-04-18 10:21:16 +0200 |
|---|---|---|
| committer | Zygmunt Krynicki <zygmunt.krynicki@canonical.com> | 2017-04-18 10:21:16 +0200 |
| commit | 7357dac122cc5164651704b2f02f9b7a6a89fa99 (patch) | |
| tree | 6fe68b02da9172f1a749528490523cf0a79ab06c | |
| parent | be6f4abcb5dbde5afd840b231a3b516fb664f757 (diff) | |
| parent | c8bd09c6eedf152f08739511a8295344e5d8a671 (diff) | |
Merge branch 'master' of github.com:snapcore/snapd into retry-logsretry-logs
115 files changed, 5503 insertions, 1371 deletions
diff --git a/.gitignore b/.gitignore index d8f7b6891a..0afd0c6cb0 100644 --- a/.gitignore +++ b/.gitignore @@ -37,9 +37,9 @@ data/systemd/*.service *.log *.trs -# Automake -Makefile -Makefile.in +# Automake for the cmd/ parts +cmd/Makefile +cmd/Makefile.in snap-confine-*.tar.gz .deps diff --git a/.travis.yml b/.travis.yml index e10101edc7..6c7d80a8b2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,9 @@ env: # SPREAD_LINODE_KEY - secure: "bzALrfNSLwM0bjceal1PU5rFErvqVhi00Sygx8jruo6htpZay3hrC2sHCKCQKPn1kvCfHidrHX1vnomg5N+B9o25GZEYSjKSGxuvdNDfCZYqPNjMbz5y7xXYfKWgyo+xtrKRM85Nqy121SfRz3KLDvrOLwwreb+pZv8DG1WraFTd7D6rK7nLnnYNUyw665XBMFVnM8ue3Zu9496Ih/TfQXhnNpsZY8xFWte4+cH7JvVCVTs8snjoGVZi3972PzinNkfBgJa24cUzxFMfiN/AwSBXJQKdVv+FsbB4uRgXAqTNwuus7PptiPNxpWWojuhm1Qgbk0XhGIdJxyUYkmNA4UrZ3C29nIRWbuAiHJ6ZWd1ur3dqphqOcgFInltSHkpfEdlL3YK4dCa2SmJESzotUGnyowCUUCXkWdDaZmFTwyK0Y6He9oyXDK5f+/U7SFlPvok0caJCvB9HbTQR1kYdh048I/R+Ht5QrFOZPk21DYWDOYhn7SzthBDZLsaL6n5gX7Y547SsL4B35YVbpaeHzccG6Mox8rI4bqlGFvP1U5i8uXD4uQjJChlVxpmozUEMok9T5RVediJs540p5uc8DQl48Nke02tXzC/XpGAvpnXT7eiiRNW67zOj2QcIV+ni3lBj3HvZeB9cgjzLNrZSl/t9vseqnNwQWpl3V6nd/bU=" +git: + quiet: true + install: - sudo apt-get update -qq - sudo apt-get install -qq squashfs-tools xdelta3 diff --git a/cmd/configure.ac b/cmd/configure.ac index e2b848bf00..39a6800349 100644 --- a/cmd/configure.ac +++ b/cmd/configure.ac @@ -23,7 +23,8 @@ AC_SYS_LARGEFILE AC_CHECK_HEADERS([fcntl.h limits.h stdlib.h string.h sys/mount.h unistd.h]) AC_CHECK_HEADERS([sys/quota.h], [], [AC_MSG_ERROR(sys/quota.h unavailable)]) AC_CHECK_HEADERS([xfs/xqm.h], [], [AC_MSG_ERROR(xfs/xqm.h unavailable)], -[[#define _FILE_OFFSET_BITS 64 +[[#define _GNU_SOURCE +#define _FILE_OFFSET_BITS 64 #include <xfs/xqm.h> ]]) diff --git a/cmd/snap-confine/mount-support.c b/cmd/snap-confine/mount-support.c index 4e681e9af2..2754f7afa5 100644 --- a/cmd/snap-confine/mount-support.c +++ b/cmd/snap-confine/mount-support.c @@ -127,15 +127,6 @@ static void setup_private_mount(const char *snap_name) if (chdir(pwd) != 0) die("cannot change current working directory to the original directory"); free(pwd); - - // ensure we set the various TMPDIRs to our newly created tmpdir - const char *tmpd[] = { "TMPDIR", "TEMPDIR", NULL }; - int i; - for (i = 0; tmpd[i] != NULL; i++) { - if (setenv(tmpd[i], "/tmp", 1) != 0) { - die("cannot set environment variable '%s'", tmpd[i]); - } - } } // TODO: fold this into bootstrap @@ -240,7 +231,7 @@ struct sc_mount_config { const char *rootfs_dir; // The struct is terminated with an entry with NULL path. const struct sc_mount *mounts; - bool on_classic; + bool on_classic_distro; }; /** @@ -397,7 +388,7 @@ static void sc_bootstrap_mount_namespace(const struct sc_mount_config *config) // uniform way after pivot_root but this is good enough and requires less // code changes the nvidia code assumes it has access to the existing // pre-pivot filesystem. - if (config->on_classic) { + if (config->on_classic_distro) { sc_mount_nvidia_driver(scratch_dir); } // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX @@ -527,8 +518,8 @@ void sc_populate_mount_ns(const char *snap_name) die("cannot get the current working directory"); } // Remember if we are on classic, some things behave differently there. - bool on_classic = is_running_on_classic_distribution(); - if (on_classic) { + bool on_classic_distro = is_running_on_classic_distribution(); + if (on_classic_distro) { const struct sc_mount mounts[] = { {"/dev"}, // because it contains devices on host OS {"/etc"}, // because that's where /etc/resolv.conf lives, perhaps a bad idea @@ -555,7 +546,7 @@ void sc_populate_mount_ns(const char *snap_name) struct sc_mount_config classic_config = { .rootfs_dir = sc_get_outer_core_mount_point(), .mounts = mounts, - .on_classic = true, + .on_classic_distro = true, }; sc_bootstrap_mount_namespace(&classic_config); } else { @@ -585,7 +576,7 @@ void sc_populate_mount_ns(const char *snap_name) setup_private_pts(); // setup quirks for specific snaps - if (on_classic) { + if (on_classic_distro) { sc_setup_quirks(); } // setup the security backend bind mounts diff --git a/cmd/snap-confine/seccomp-support.c b/cmd/snap-confine/seccomp-support.c index a08b23af9b..031f465ebd 100644 --- a/cmd/snap-confine/seccomp-support.c +++ b/cmd/snap-confine/seccomp-support.c @@ -34,9 +34,19 @@ #include <sys/types.h> #include <sys/utsname.h> #include <termios.h> -#include <xfs/xqm.h> #include <unistd.h> +// The XFS interface requires a 64 bit file system interface +// but we don't want to leak this anywhere else if not globally +// defined. +#ifndef _FILE_OFFSET_BITS +#define _FILE_OFFSET_BITS 64 +#include <xfs/xqm.h> +#undef _FILE_OFFSET_BITS +#else +#include <xfs/xqm.h> +#endif + #include <seccomp.h> #include "../libsnap-confine-private/secure-getenv.h" diff --git a/cmd/snap-confine/snap-confine.apparmor.in b/cmd/snap-confine/snap-confine.apparmor.in index 1413cf24cd..089196eaae 100644 --- a/cmd/snap-confine/snap-confine.apparmor.in +++ b/cmd/snap-confine/snap-confine.apparmor.in @@ -30,6 +30,7 @@ /dev/random r, /dev/urandom r, /dev/pts/[0-9]* rw, + /dev/tty rw, # cgroups capability sys_admin, diff --git a/cmd/snap-confine/snap-confine.c b/cmd/snap-confine/snap-confine.c index 9d07939942..cc5a716aff 100644 --- a/cmd/snap-confine/snap-confine.c +++ b/cmd/snap-confine/snap-confine.c @@ -175,6 +175,17 @@ int main(int argc, char **argv) "/usr/bin:" "/sbin:" "/bin:" "/usr/games:" "/usr/local/games", 1); + // Ensure we set the various TMPDIRs to /tmp. + // One of the parts of setting up the mount namespace is to create a private /tmp + // directory (this is done in sc_populate_mount_ns() above). The host environment + // may point to a directory not accessible by snaps so we need to reset it here. + const char *tmpd[] = { "TMPDIR", "TEMPDIR", NULL }; + int i; + for (i = 0; tmpd[i] != NULL; i++) { + if (setenv(tmpd[i], "/tmp", 1) != 0) { + die("cannot set environment variable '%s'", tmpd[i]); + } + } struct snappy_udev udev_s; if (snappy_udev_init(security_tag, &udev_s) == 0) setup_devices_cgroup(security_tag, &udev_s); diff --git a/cmd/snap-update-ns/bootstrap.c b/cmd/snap-update-ns/bootstrap.c new file mode 100644 index 0000000000..ada96cd517 --- /dev/null +++ b/cmd/snap-update-ns/bootstrap.c @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "bootstrap.h" + +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <sched.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +// bootstrap_errno contains a copy of errno if a system call fails. +int bootstrap_errno = 0; +// bootstrap_msg contains a static string if something fails. +const char* bootstrap_msg = NULL; + +// read_cmdline reads /proc/self/cmdline into the specified buffer, returning +// number of bytes read. +ssize_t read_cmdline(char* buf, size_t buf_size) +{ + int fd = open("/proc/self/cmdline", O_RDONLY | O_CLOEXEC | O_NOFOLLOW); + if (fd < 0) { + bootstrap_errno = errno; + bootstrap_msg = "cannot open /proc/self/cmdline"; + return -1; + } + memset(buf, 0, buf_size); + ssize_t num_read = read(fd, buf, buf_size); + if (num_read < 0) { + bootstrap_errno = errno; + bootstrap_msg = "cannot read /proc/self/cmdline"; + } else if (num_read == buf_size) { + bootstrap_errno = 0; + bootstrap_msg = "cannot fit all of /proc/self/cmdline, buffer too small"; + num_read = -1; + } + close(fd); + return num_read; +} + +// find_argv0 scans the command line buffer and looks for the 0st argument. +const char* +find_argv0(char* buf, size_t num_read) +{ + // cmdline is an array of NUL ('\0') separated strings. + size_t argv0_len = strnlen(buf, num_read); + if (argv0_len == num_read) { + // ensure that the buffer is properly terminated. + return NULL; + } + return buf; +} + +// find_snap_name scans the command line buffer and looks for the 1st argument. +// if the 1st argument exists but is empty NULL is returned. +const char* +find_snap_name(char* buf, size_t num_read) +{ + // cmdline is an array of NUL ('\0') separated strings. We can skip over + // the first entry (program name) and look at the second entry, in our case + // it should be the snap name. + size_t argv0_len = strnlen(buf, num_read); + if (argv0_len + 1 >= num_read) { + return NULL; + } + char* snap_name = &buf[argv0_len + 1]; + if (*snap_name == '\0') { + return NULL; + } + return snap_name; +} + +// setns_into_snap switches mount namespace into that of a given snap. +static int +setns_into_snap(const char* snap_name) +{ + // Construct the name of the .mnt file to open. + char buf[PATH_MAX]; + int n = snprintf(buf, sizeof buf, "/run/snapd/ns/%s.mnt", snap_name); + if (n >= sizeof buf || n < 0) { + bootstrap_errno = errno; + bootstrap_msg = "cannot format mount namespace file name"; + return -1; + } + + // Open the mount namespace file, note that we don't specify O_NOFOLLOW as + // that file is always a special, broken symbolic link. + int fd = open(buf, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + bootstrap_errno = errno; + bootstrap_msg = "cannot open mount namespace file"; + return -1; + } + + // Switch to the mount namespace of the given snap. + int err = setns(fd, CLONE_NEWNS); + if (err < 0) { + bootstrap_errno = errno; + bootstrap_msg = "cannot switch mount namespace"; + }; + + close(fd); + return err; +} + +// partially_validate_snap_name performs partial validation of the given name. +// The goal is to ensure that there are no / or .. in the name. +int partially_validate_snap_name(const char* snap_name) +{ + // NOTE: neither set bootstrap_{msg,errno} but the return value means that + // bootstrap does nothing. The name is re-validated by golang. + if (strstr(snap_name, "..") != NULL) { + return -1; + } + if (strchr(snap_name, '/') != NULL) { + return -1; + } +} + +// bootstrap prepares snap-update-ns to work in the namespace of the snap given +// on command line. +void bootstrap(void) +{ + // We don't have argc/argv so let's imitate that by reading cmdline + char cmdline[1024]; + memset(cmdline, 0, sizeof cmdline); + ssize_t num_read; + if ((num_read = read_cmdline(cmdline, sizeof cmdline)) < 0) { + return; + } + + // Find the name of the called program. If it is ending with "-test" then do nothing. + // NOTE: This lets us use cgo/go to write tests without running the bulk + // of the code automatically. In snapd we can just set the required + // environment variable. + const char* argv0 = find_argv0(cmdline, (size_t)num_read); + if (argv0 == NULL) { + bootstrap_errno = 0; + bootstrap_msg = "argv0 is corrupted"; + return; + } + const char* argv0_suffix_maybe = strstr(argv0, ".test"); + if (argv0_suffix_maybe != NULL && argv0_suffix_maybe[strlen(".test")] == '\0') { + bootstrap_errno = 0; + bootstrap_msg = "bootstrap is not enabled while testing"; + return; + } + + // Find the name of the snap by scanning the cmdline. If there's no snap + // name given, just bail out. The go parts will scan this too. + const char* snap_name = find_snap_name(cmdline, (size_t)num_read); + if (snap_name == NULL) { + return; + } + + // Look for known offenders in the snap name (.. and /) and do nothing if + // those are found. The golang code will validate snap name and print a + // proper error message but this just ensures we don't try to open / setns + // anything unusual. + if (partially_validate_snap_name(snap_name) < 0) { + return; + } + + // Switch the mount namespace to that of the snap given on command line. + if (setns_into_snap(snap_name) < 0) { + return; + } +} diff --git a/cmd/snap-update-ns/bootstrap.go b/cmd/snap-update-ns/bootstrap.go new file mode 100644 index 0000000000..b7239c0b03 --- /dev/null +++ b/cmd/snap-update-ns/bootstrap.go @@ -0,0 +1,85 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package main + +// Use a pre-main helper to switch the mount namespace. This is required as +// golang creates threads at will and setns(..., CLONE_NEWNS) fails if any +// threads apart from the main thread exist. + +/* + +#include <stdlib.h> +#include "bootstrap.h" + +__attribute__((constructor)) static void init(void) { + bootstrap(); +} + +// NOTE: do not add anything before the following `import "C"' +*/ +import "C" + +import ( + "fmt" + "syscall" + "unsafe" +) + +// Error returns error (if any) encountered in pre-main C code. +func BootstrapError() error { + if C.bootstrap_msg == nil { + return nil + } + errno := syscall.Errno(C.bootstrap_errno) + if errno != 0 { + return fmt.Errorf("%s: %s", C.GoString(C.bootstrap_msg), errno) + } + return fmt.Errorf("%s", C.GoString(C.bootstrap_msg)) +} + +// readCmdline is a wrapper around the C function read_cmdline. +func readCmdline(buf []byte) C.ssize_t { + return C.read_cmdline((*C.char)(unsafe.Pointer(&buf[0])), C.size_t(cap(buf))) +} + +// findArgv0 parses the argv-like array and finds the 0st argument. +func findArgv0(buf []byte) *string { + if ptr := C.find_argv0((*C.char)(unsafe.Pointer(&buf[0])), C.size_t(len(buf))); ptr != nil { + str := C.GoString(ptr) + return &str + } + return nil +} + +// findSnapName parses the argv-like array and finds the 1st argument. +func findSnapName(buf []byte) *string { + if ptr := C.find_snap_name((*C.char)(unsafe.Pointer(&buf[0])), C.size_t(len(buf))); ptr != nil { + str := C.GoString(ptr) + return &str + } + return nil +} + +// partiallyValidateSnapName checks if snap name is seemingly valid. +// The real part of the validation happens on the go side. +func partiallyValidateSnapName(snapName string) int { + cStr := C.CString(snapName) + return int(C.partially_validate_snap_name(cStr)) +} diff --git a/cmd/snap-update-ns/bootstrap.h b/cmd/snap-update-ns/bootstrap.h new file mode 100644 index 0000000000..4ae11368d9 --- /dev/null +++ b/cmd/snap-update-ns/bootstrap.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef SNAPD_CMD_SNAP_UPDATE_NS_H +#define SNAPD_CMD_SNAP_UPDATE_NS_H + +#define _GNU_SOURCE + +#include <unistd.h> + +extern int bootstrap_errno; +extern const char* bootstrap_msg; + +void bootstrap(void); +ssize_t read_cmdline(char* buf, size_t buf_size); +const char* find_argv0(char* buf, size_t num_read); +const char* find_snap_name(char* buf, size_t num_read); +int partially_validate_snap_name(const char* snap_name); + +#endif diff --git a/cmd/snap-update-ns/bootstrap_test.go b/cmd/snap-update-ns/bootstrap_test.go new file mode 100644 index 0000000000..a333ddd639 --- /dev/null +++ b/cmd/snap-update-ns/bootstrap_test.go @@ -0,0 +1,108 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package main_test + +import ( + "strings" + + . "gopkg.in/check.v1" + + update "github.com/snapcore/snapd/cmd/snap-update-ns" +) + +type bootstrapSuite struct{} + +var _ = Suite(&bootstrapSuite{}) + +func (s *bootstrapSuite) TestReadCmdLine(c *C) { + buf := make([]byte, 1024) + numRead := update.ReadCmdline(buf) + c.Assert(numRead, Not(Equals), -1) + c.Assert(numRead, Not(Equals), 1) + // Individual arguments are separated with NUL byte. + argv := strings.Split(string(buf[0:numRead]), "\x00") + // Smoke test, the actual value looks like + // "/tmp/go-build020699516/github.com/snapcore/snapd/cmd/snap-update-ns/_test/snap-update-ns.test" + c.Assert(strings.HasSuffix(argv[0], "snap-update-ns.test"), Equals, true, Commentf("argv[0] is %q", argv[0])) +} + +// Check that if there is only one argument we return nil. +func (s *bootstrapSuite) TestFindSnapName1(c *C) { + buf := []byte("arg0\x00") + result := update.FindSnapName(buf) + c.Assert(result, Equals, (*string)(nil)) +} + +// Check that if there are multiple arguments we return the 2nd one. +func (s *bootstrapSuite) TestFindSnapName2(c *C) { + buf := []byte("arg0\x00arg1\x00arg2\x00") + result := update.FindSnapName(buf) + c.Assert(result, Not(Equals), (*string)(nil)) + c.Assert(*result, Equals, "arg1") +} + +// Check that if the 1st argument in the buffer is not terminated we don't crash. +func (s *bootstrapSuite) TestFindSnapName3(c *C) { + buf := []byte("arg0") + result := update.FindSnapName(buf) + c.Assert(result, Equals, (*string)(nil)) +} + +// Check that if the 2nd argument in the buffer is not terminated we don't crash. +func (s *bootstrapSuite) TestFindSnapName4(c *C) { + buf := []byte("arg0\x00arg1") + result := update.FindSnapName(buf) + c.Assert(result, Not(Equals), (*string)(nil)) + c.Assert(*result, Equals, "arg1") +} + +// Check that if the 2nd argument an empty string we return NULL. +func (s *bootstrapSuite) TestFindSnapName5(c *C) { + buf := []byte("arg0\x00\x00") + result := update.FindSnapName(buf) + c.Assert(result, Equals, (*string)(nil)) +} + +// Check that if argv0 is returned as expected +func (s *bootstrapSuite) TestFindArgv0(c *C) { + buf := []byte("arg0\x00argv1\x00") + result := update.FindArgv0(buf) + c.Assert(result, NotNil) + c.Assert(*result, Equals, "arg0") +} + +// Check that if argv0 is unterminated we return NULL. +func (s *bootstrapSuite) TestFindArgv0Unterminated(c *C) { + buf := []byte("arg0") + result := update.FindArgv0(buf) + c.Assert(result, Equals, (*string)(nil)) +} + +// Check that PartiallyValidateSnapName rejects "/" and "..". +func (s *bootstrapSuite) TestPartiallyValidateSnapName(c *C) { + c.Assert(update.PartiallyValidateSnapName("hello-world"), Equals, 0) + c.Assert(update.PartiallyValidateSnapName("hello/world"), Equals, -1) + c.Assert(update.PartiallyValidateSnapName("hello..world"), Equals, -1) +} + +// Check that pre-go bootstrap code is disabled while testing. +func (s *bootstrapSuite) TestBootstrapDisabled(c *C) { + c.Assert(update.BootstrapError(), ErrorMatches, "bootstrap is not enabled while testing") +} diff --git a/cmd/snap-update-ns/export_test.go b/cmd/snap-update-ns/export_test.go new file mode 100644 index 0000000000..b9edb3db9e --- /dev/null +++ b/cmd/snap-update-ns/export_test.go @@ -0,0 +1,27 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package main + +var ( + ReadCmdline = readCmdline + FindArgv0 = findArgv0 + FindSnapName = findSnapName + PartiallyValidateSnapName = partiallyValidateSnapName +) diff --git a/cmd/snap-update-ns/main.go b/cmd/snap-update-ns/main.go index a1d62dc471..e153baa554 100644 --- a/cmd/snap-update-ns/main.go +++ b/cmd/snap-update-ns/main.go @@ -24,6 +24,8 @@ import ( "os" "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/snap" ) var opts struct { @@ -41,14 +43,22 @@ func main() { func parseArgs(args []string) error { parser := flags.NewParser(&opts, flags.HelpFlag|flags.PassDoubleDash|flags.PassAfterNonOption) - _, err := parser.ParseArgs(args) - return err + if _, err := parser.ParseArgs(args); err != nil { + return err + } + return snap.ValidateName(opts.Positionals.SnapName) } func run() error { if err := parseArgs(os.Args[1:]); err != nil { return err } - + // There is some C code that runs before main() is started. + // That code always runs and sets an error condition if it fails. + // Here we just check for the error. + if err := BootstrapError(); err != nil { + return err + } + // TODO: implement this return fmt.Errorf("not implemented") } diff --git a/cmd/snap/cmd_auto_import.go b/cmd/snap/cmd_auto_import.go index 542ccb2e2a..cac935f57c 100644 --- a/cmd/snap/cmd_auto_import.go +++ b/cmd/snap/cmd_auto_import.go @@ -84,6 +84,10 @@ func autoImportCandidates() ([]string, error) { if strings.HasPrefix(mountSrc, "/dev/loop") { continue } + // skip all ram disks (unless in tests) + if !osutil.GetenvBool("SNAPPY_TESTING") && strings.HasPrefix(mountSrc, "/dev/ram") { + continue + } mountPoint := l[4] cand := filepath.Join(mountPoint, autoImportsName) diff --git a/cmd/snap/cmd_changes.go b/cmd/snap/cmd_changes.go index 220c27e15c..adceb7cba1 100644 --- a/cmd/snap/cmd_changes.go +++ b/cmd/snap/cmd_changes.go @@ -50,9 +50,16 @@ type cmdChange struct { } `positional-args:"yes"` } +type cmdTasks struct { + Positional struct { + ID changeID `positional-arg-name:"<id>" required:"yes"` + } `positional-args:"yes"` +} + func init() { addCommand("changes", shortChangesHelp, longChangesHelp, func() flags.Commander { return &cmdChanges{} }, nil, nil) - addCommand("change", shortChangeHelp, longChangeHelp, func() flags.Commander { return &cmdChange{} }, nil, nil) + addCommand("change", shortChangeHelp, longChangeHelp, func() flags.Commander { return &cmdChange{} }, nil, nil).hidden = true + addCommand("tasks", shortChangeHelp, longChangeHelp, func() flags.Commander { return &cmdTasks{} }, nil, nil) } type changesByTime []*client.Change @@ -70,7 +77,7 @@ func (c *cmdChanges) Execute(args []string) error { if allDigits(c.Positional.Snap) { // TRANSLATORS: the %s is the argument given by the user to "snap changes" - return fmt.Errorf(i18n.G(`"snap changes" command expects a snap name, try: "snap change %s"`), c.Positional.Snap) + return fmt.Errorf(i18n.G(`"snap changes" command expects a snap name, try: "snap tasks %s"`), c.Positional.Snap) } if c.Positional.Snap == "everything" { @@ -113,9 +120,18 @@ func (c *cmdChanges) Execute(args []string) error { return nil } +func (c *cmdTasks) Execute([]string) error { + cli := Client() + return showChange(cli, c.Positional.ID) +} + func (c *cmdChange) Execute([]string) error { cli := Client() - chg, err := cli.Change(string(c.Positional.ID)) + return showChange(cli, c.Positional.ID) +} + +func showChange(cli *client.Client, chid changeID) error { + chg, err := cli.Change(string(chid)) if err != nil { return err } diff --git a/cmd/snap/cmd_changes_test.go b/cmd/snap/cmd_changes_test.go index f044ef7ef1..5c4b7690f1 100644 --- a/cmd/snap/cmd_changes_test.go +++ b/cmd/snap/cmd_changes_test.go @@ -42,23 +42,29 @@ var mockChangeJSON = `{"type": "sync", "result": { func (s *SnapSuite) TestChangeSimple(c *check.C) { n := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { - switch n { - case 0: + if n < 2 { c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/changes/42") fmt.Fprintln(w, mockChangeJSON) - default: + } else { c.Fatalf("expected to get 1 requests, now on %d", n+1) } n++ }) + expectedChange := `(?ms)Status +Spawn +Ready +Summary +Do +2016-04-21T01:02:03Z +2016-04-21T01:02:04Z +some summary +` rest, err := snap.Parser().ParseArgs([]string{"change", "42"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) - c.Check(s.Stdout(), check.Matches, `(?ms)Status +Spawn +Ready +Summary -Do +2016-04-21T01:02:03Z +2016-04-21T01:02:04Z +some summary -`) + c.Check(s.Stdout(), check.Matches, expectedChange) + c.Check(s.Stderr(), check.Equals, "") + + rest, err = snap.Parser().ParseArgs([]string{"tasks", "42"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, expectedChange) c.Check(s.Stderr(), check.Equals, "") } diff --git a/cmd/snap/cmd_run.go b/cmd/snap/cmd_run.go index b1244acd55..f210c89be9 100644 --- a/cmd/snap/cmd_run.go +++ b/cmd/snap/cmd_run.go @@ -216,6 +216,17 @@ func snapRunHook(snapName, snapRevision, hookName string) error { return runSnapConfine(info, hook.SecurityTag(), snapName, "", hook.Name, nil) } +var osReadlink = os.Readlink + +func isReexeced() bool { + exe, err := osReadlink("/proc/self/exe") + if err != nil { + logger.Noticef("cannot read /proc/self/exe: %v", err) + return false + } + return strings.HasPrefix(exe, dirs.SnapMountDir) +} + func runSnapConfine(info *snap.Info, securityTag, snapApp, command, hook string, args []string) error { snapConfine := filepath.Join(dirs.DistroLibExecDir, "snap-confine") if !osutil.FileExists(snapConfine) { @@ -249,5 +260,14 @@ func runSnapConfine(info *snap.Info, securityTag, snapApp, command, hook string, cmd = append(cmd, snapApp) cmd = append(cmd, args...) + // if we re-exec, we must run the snap-confine from the core snap + // as well, if they get out of sync, havoc will happen + if isReexeced() { + // run snap-confine from the core snap. that will work because + // snap-confine on the core snap is mostly statically linked + // (except libudev and libc) + cmd[0] = filepath.Join(dirs.SnapMountDir, "core/current", cmd[0]) + } + return syscallExec(cmd[0], cmd, snapenv.ExecEnv(info)) } diff --git a/cmd/snap/cmd_run_test.go b/cmd/snap/cmd_run_test.go index 4f80969f53..b2814420d5 100644 --- a/cmd/snap/cmd_run_test.go +++ b/cmd/snap/cmd_run_test.go @@ -437,3 +437,65 @@ func (s *SnapSuite) TestSnapRunSaneEnvironmentHandling(c *check.C) { c.Check(execEnv, check.Not(testutil.Contains), "SNAP_ARCH=PDP-7") c.Check(execEnv, testutil.Contains, "SNAP_THE_WORLD=YES") } + +func (s *SnapSuite) TestSnapRunIsReexeced(c *check.C) { + var osReadlinkResult string + restore := snaprun.MockOsReadlink(func(name string) (string, error) { + return osReadlinkResult, nil + }) + defer restore() + + for _, t := range []struct { + readlink string + expected bool + }{ + {filepath.Join(dirs.SnapMountDir, "/usr/lib/snapd/snapd"), true}, + {"/usr/lib/snapd/snapd", false}, + } { + osReadlinkResult = t.readlink + c.Check(snaprun.IsReexeced(), check.Equals, t.expected) + } +} + +func (s *SnapSuite) TestSnapRunAppIntegrationFromCore(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + defer mockSnapConfine()() + + si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R("x2"), + }) + err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // pretend to be running from core + restorer := snaprun.MockOsReadlink(func(string) (string, error) { + return filepath.Join(dirs.SnapMountDir, "/core/111//usr/bin/snap"), nil + }) + defer restorer() + + // redirect exec + execArg0 := "" + execArgs := []string{} + execEnv := []string{} + restorer = snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArg0 = arg0 + execArgs = args + execEnv = envv + return nil + }) + defer restorer() + + // and run it! + rest, err := snaprun.Parser().ParseArgs([]string{"run", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) + c.Check(execArg0, check.Equals, filepath.Join(dirs.SnapMountDir, "/core/current", dirs.DistroLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.SnapMountDir, "/core/current", dirs.DistroLibExecDir, "snap-confine"), + "snap.snapname.app", + filepath.Join(dirs.DistroLibExecDir, "snap-exec"), + "snapname.app", "--arg1", "arg2"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2") +} diff --git a/cmd/snap/cmd_snap_op.go b/cmd/snap/cmd_snap_op.go index 50095f4910..b67aa9d768 100644 --- a/cmd/snap/cmd_snap_op.go +++ b/cmd/snap/cmd_snap_op.go @@ -705,6 +705,20 @@ type cmdTry struct { } `positional-args:"yes"` } +func hasSnapcraftYaml() bool { + for _, loc := range []string{ + "snap/snapcraft.yaml", + "snapcraft.yaml", + ".snapcraft.yaml", + } { + if osutil.FileExists(loc) { + return true + } + } + + return false +} + func (x *cmdTry) Execute([]string) error { if err := x.validateMode(); err != nil { return err @@ -715,7 +729,7 @@ func (x *cmdTry) Execute([]string) error { x.setModes(opts) if name == "" { - if osutil.FileExists("snapcraft.yaml") && osutil.IsDirectory("prime") { + if hasSnapcraftYaml() && osutil.IsDirectory("prime") { name = "prime" } else { if osutil.FileExists("meta/snap.yaml") { diff --git a/cmd/snap/export_test.go b/cmd/snap/export_test.go index b229d0ff89..45aa0746f2 100644 --- a/cmd/snap/export_test.go +++ b/cmd/snap/export_test.go @@ -35,6 +35,7 @@ var ( SnapRunHook = snapRunHook Wait = wait ResolveApp = resolveApp + IsReexeced = isReexeced ) func MockPollTime(d time.Duration) (restore func()) { @@ -85,6 +86,14 @@ func MockMountInfoPath(newMountInfoPath string) (restore func()) { } } +func MockOsReadlink(f func(string) (string, error)) (restore func()) { + osReadlinkOrig := osReadlink + osReadlink = f + return func() { + osReadlink = osReadlinkOrig + } +} + var AutoImportCandidates = autoImportCandidates func AliasInfoLess(snapName1, alias1, app1, snapName2, alias2, app2 string) bool { diff --git a/daemon/api.go b/daemon/api.go index 065e7562cd..6865379688 100644 --- a/daemon/api.go +++ b/daemon/api.go @@ -239,13 +239,16 @@ func sysInfo(c *Command, r *http.Request, user *auth.UserState) Response { } m := map[string]interface{}{ - "series": release.Series, - "version": c.d.Version, - "os-release": release.ReleaseInfo, - "on-classic": release.OnClassic, - "managed": len(users) > 0, - + "series": release.Series, + "version": c.d.Version, + "os-release": release.ReleaseInfo, + "on-classic": release.OnClassic, + "managed": len(users) > 0, "kernel-version": release.KernelVersion(), + "locations": map[string]interface{}{ + "snap-mount-dir": dirs.SnapMountDir, + "snap-bin-dir": dirs.SnapBinariesDir, + }, } // TODO: set the store-id here from the model information @@ -1277,7 +1280,6 @@ func postSnaps(c *Command, r *http.Request, user *auth.UserState) Response { if err != nil { return BadRequest(err.Error()) } - flags.RemoveSnapPath = true if len(form.Value["action"]) > 0 && form.Value["action"][0] == "try" { if len(form.Value["snap-path"]) == 0 { @@ -1285,6 +1287,7 @@ func postSnaps(c *Command, r *http.Request, user *auth.UserState) Response { } return trySnap(c, r, user, form.Value["snap-path"][0], flags) } + flags.RemoveSnapPath = true // find the file for the "snap" form field var snapBody multipart.File @@ -2262,10 +2265,12 @@ func changeAliases(c *Command, r *http.Request, user *auth.UserState) Response { if err := decoder.Decode(&a); err != nil { return BadRequest("cannot decode request body into an alias action: %v", err) } - if len(a.Aliases) == 0 { - return BadRequest("at least one alias name is required") + if len(a.Aliases) != 0 { + return BadRequest("cannot interpret request, snaps can no longer be expected to declare their aliases") } + return BadRequest("cannot yet interpret request") + /* TODO: rework this var summary string var taskset *state.TaskSet var err error @@ -2298,6 +2303,7 @@ func changeAliases(c *Command, r *http.Request, user *auth.UserState) Response { state.EnsureBefore(0) return AsyncResponse(nil, &Meta{Change: change.ID()}) + */ } type aliasStatus struct { diff --git a/daemon/api_test.go b/daemon/api_test.go index e560e7dab3..7799611b99 100644 --- a/daemon/api_test.go +++ b/daemon/api_test.go @@ -238,6 +238,15 @@ func (s *apiBaseSuite) mkInstalled(c *check.C, name, developer, version string, return s.mkInstalledInState(c, nil, name, developer, version, revision, active, extraYaml) } +func (s *apiBaseSuite) mkInstalledDesktopFile(c *check.C, name, content string) string { + df := filepath.Join(dirs.SnapDesktopFilesDir, name) + err := os.MkdirAll(filepath.Dir(df), 0755) + c.Assert(err, check.IsNil) + err = ioutil.WriteFile(df, []byte(content), 0644) + c.Assert(err, check.IsNil) + return df +} + func (s *apiBaseSuite) mkInstalledInState(c *check.C, daemon *Daemon, name, developer, version string, revision snap.Revision, active bool, extraYaml string) *snap.Info { snapID := name + "-id" // Collect arguments into a snap.SideInfo structure @@ -347,7 +356,8 @@ func (s *apiSuite) TestSnapInfoOneIntegration(c *check.C) { // we have v0 [r5] installed s.mkInstalledInState(c, d, "foo", "bar", "v0", snap.R(5), false, "") // and v1 [r10] is current - s.mkInstalledInState(c, d, "foo", "bar", "v1", snap.R(10), true, "description: description\nsummary: summary") + s.mkInstalledInState(c, d, "foo", "bar", "v1", snap.R(10), true, "description: description\nsummary: summary\napps:\n cmd:\n command: some.cmd\n cmd2:\n command: other.cmd\n") + df := s.mkInstalledDesktopFile(c, "foo_cmd.desktop", "[Desktop]\nExec=foo.cmd %U") req, err := http.NewRequest("GET", "/v2/snaps/foo", nil) c.Assert(err, check.IsNil) @@ -388,9 +398,13 @@ func (s *apiSuite) TestSnapInfoOneIntegration(c *check.C) { "jailmode": false, "confinement": snap.StrictConfinement, "trymode": false, - "apps": []appJSON{}, - "broken": "", - "contact": "", + "apps": []appJSON{ + {Name: "cmd", DesktopFile: df}, + // no desktop file + {Name: "cmd2"}, + }, + "broken": "", + "contact": "", }, Meta: meta, } @@ -558,6 +572,10 @@ func (s *apiSuite) TestSysInfo(c *check.C) { }, "on-classic": true, "managed": false, + "locations": map[string]interface{}{ + "snap-mount-dir": dirs.SnapMountDir, + "snap-bin-dir": dirs.SnapBinariesDir, + }, } var rsp resp c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), check.IsNil) @@ -2009,8 +2027,7 @@ func (s *apiSuite) TestTrySnap(c *check.C) { d.overlord.Loop() defer d.overlord.Stop() - req, err := http.NewRequest("POST", "/v2/snaps", nil) - c.Assert(err, check.IsNil) + var err error // mock a try dir tryDir := c.MkDir() @@ -2020,6 +2037,42 @@ func (s *apiSuite) TestTrySnap(c *check.C) { err = ioutil.WriteFile(snapYaml, []byte("name: foo\nversion: 1.0\n"), 0644) c.Assert(err, check.IsNil) + reqForFlags := func(f snapstate.Flags) *http.Request { + b := "" + + "--hello\r\n" + + "Content-Disposition: form-data; name=\"action\"\r\n" + + "\r\n" + + "try\r\n" + + "--hello\r\n" + + "Content-Disposition: form-data; name=\"snap-path\"\r\n" + + "\r\n" + + tryDir + "\r\n" + + "--hello" + + snip := "\r\n" + + "Content-Disposition: form-data; name=%q\r\n" + + "\r\n" + + "true\r\n" + + "--hello" + + if f.DevMode { + b += fmt.Sprintf(snip, "devmode") + } + if f.JailMode { + b += fmt.Sprintf(snip, "jailmode") + } + if f.Classic { + b += fmt.Sprintf(snip, "classic") + } + b += "--\r\n" + + req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(b)) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "multipart/thing; boundary=hello") + + return req + } + for _, t := range []struct { coreInfoErr error nTasks int @@ -2067,7 +2120,7 @@ func (s *apiSuite) TestTrySnap(c *check.C) { } // try the snap (without an installed core) - rsp := trySnap(snapsCmd, req, nil, tryDir, t.flags).(*resp) + rsp := postSnaps(snapsCmd, reqForFlags(t.flags), nil).(*resp) c.Assert(rsp.Type, check.Equals, ResponseTypeAsync, check.Commentf(t.desc)) c.Assert(tryWasCalled, check.Equals, true, check.Commentf(t.desc)) @@ -4847,6 +4900,7 @@ func (s *apiSuite) TestAliasSuccess(c *check.C) { d.overlord.Loop() defer d.overlord.Stop() + // TODO: the details of this will change action := &aliasAction{ Action: "alias", Snap: "alias-snap", @@ -4859,10 +4913,15 @@ func (s *apiSuite) TestAliasSuccess(c *check.C) { c.Assert(err, check.IsNil) rec := httptest.NewRecorder() aliasesCmd.POST(aliasesCmd, req, nil).ServeHTTP(rec, req) - c.Check(rec.Code, check.Equals, 202) - var body map[string]interface{} - err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(rec.Code, check.Equals, 400) + var rsp resp + err = json.Unmarshal(rec.Body.Bytes(), &rsp) c.Check(err, check.IsNil) + c.Check(rsp.Result, check.DeepEquals, map[string]interface{}{ + "message": "cannot interpret request, snaps can no longer be expected to declare their aliases", + }) + + /* TODO: test the happy case again id := body["change"].(string) st := d.overlord.State() @@ -4880,6 +4939,7 @@ func (s *apiSuite) TestAliasSuccess(c *check.C) { // sanity check c.Check(osutil.IsSymlink(filepath.Join(dirs.SnapBinariesDir, "alias1")), check.Equals, true) + */ } func (s *apiSuite) TestAliasErrors(c *check.C) { @@ -4889,10 +4949,7 @@ func (s *apiSuite) TestAliasErrors(c *check.C) { mangle func(*aliasAction) err string }{ - {func(a *aliasAction) { a.Action = "" }, `unsupported alias action: ""`}, - {func(a *aliasAction) { a.Action = "what" }, `unsupported alias action: "what"`}, - {func(a *aliasAction) { a.Aliases = nil }, `at least one alias name is required`}, - {func(a *aliasAction) { a.Snap = "lalala" }, `cannot find snap "lalala"`}, + {func(a *aliasAction) { a.Aliases = nil }, `cannot yet interpret request`}, } for _, scen := range errScenarios { @@ -4916,124 +4973,6 @@ func (s *apiSuite) TestAliasErrors(c *check.C) { } } -func (s *apiSuite) TestUnaliasSuccess(c *check.C) { - err := os.MkdirAll(dirs.SnapBinariesDir, 0755) - c.Assert(err, check.IsNil) - d := s.daemon(c) - - s.mockSnap(c, aliasYaml) - - oldAutoAliases := snapstate.AutoAliases - snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) { - return nil, nil - } - defer func() { snapstate.AutoAliases = oldAutoAliases }() - - d.overlord.Loop() - defer d.overlord.Stop() - - action := &aliasAction{ - Action: "unalias", - Snap: "alias-snap", - Aliases: []string{"alias1"}, - } - text, err := json.Marshal(action) - c.Assert(err, check.IsNil) - buf := bytes.NewBuffer(text) - req, err := http.NewRequest("POST", "/v2/aliases", buf) - c.Assert(err, check.IsNil) - rec := httptest.NewRecorder() - aliasesCmd.POST(aliasesCmd, req, nil).ServeHTTP(rec, req) - c.Check(rec.Code, check.Equals, 202) - var body map[string]interface{} - err = json.Unmarshal(rec.Body.Bytes(), &body) - c.Check(err, check.IsNil) - id := body["change"].(string) - - st := d.overlord.State() - st.Lock() - chg := st.Change(id) - st.Unlock() - c.Assert(chg, check.NotNil) - - <-chg.Ready() - - st.Lock() - defer st.Unlock() - err = chg.Err() - c.Assert(err, check.IsNil) - - var allAliases map[string]map[string]string - err = st.Get("aliases", &allAliases) - c.Assert(err, check.IsNil) - c.Check(allAliases, check.DeepEquals, map[string]map[string]string{ - "alias-snap": {"alias1": "disabled"}, - }) - -} - -func (s *apiSuite) TestResetAliasSuccess(c *check.C) { - err := os.MkdirAll(dirs.SnapBinariesDir, 0755) - c.Assert(err, check.IsNil) - d := s.daemon(c) - - s.mockSnap(c, aliasYaml) - - oldAutoAliases := snapstate.AutoAliases - snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) { - return nil, nil - } - defer func() { snapstate.AutoAliases = oldAutoAliases }() - - d.overlord.Loop() - defer d.overlord.Stop() - - st := d.overlord.State() - st.Lock() - defer st.Unlock() - - st.Set("aliases", map[string]map[string]string{ - "alias-snap": { - "alias1": "disabled", - }, - }) - - action := &aliasAction{ - Action: "reset", - Snap: "alias-snap", - Aliases: []string{"alias1"}, - } - text, err := json.Marshal(action) - c.Assert(err, check.IsNil) - buf := bytes.NewBuffer(text) - req, err := http.NewRequest("POST", "/v2/aliases", buf) - c.Assert(err, check.IsNil) - rec := httptest.NewRecorder() - st.Unlock() - aliasesCmd.POST(aliasesCmd, req, nil).ServeHTTP(rec, req) - st.Lock() - c.Check(rec.Code, check.Equals, 202) - var body map[string]interface{} - err = json.Unmarshal(rec.Body.Bytes(), &body) - c.Check(err, check.IsNil) - id := body["change"].(string) - - chg := st.Change(id) - c.Assert(chg, check.NotNil) - - st.Unlock() - <-chg.Ready() - st.Lock() - - err = chg.Err() - c.Assert(err, check.IsNil) - - var allAliases map[string]map[string]string - err = st.Get("aliases", &allAliases) - c.Assert(err, check.IsNil) - c.Check(allAliases, check.HasLen, 0) -} - func (s *apiSuite) TestAliases(c *check.C) { d := s.daemon(c) diff --git a/daemon/snap.go b/daemon/snap.go index 6d4c411558..5bd7c64bed 100644 --- a/daemon/snap.go +++ b/daemon/snap.go @@ -24,8 +24,10 @@ import ( "fmt" "os" "path/filepath" + "sort" "time" + "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord/assertstate" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" @@ -157,9 +159,10 @@ func allLocalSnapInfos(st *state.State, all bool, wanted map[string]bool) ([]abo // appJSON contains the json for snap.AppInfo type appJSON struct { - Name string `json:"name"` - Daemon string `json:"daemon"` - Aliases []string `json:"aliases,omitempty"` + Name string `json:"name"` + Daemon string `json:"daemon"` + DesktopFile string `json:"desktop-file,omitempty"` + Aliases []string `json:"aliases,omitempty"` } // screenshotJSON contains the json for snap.ScreenshotInfo @@ -176,12 +179,24 @@ func mapLocal(about aboutSnap) map[string]interface{} { status = "active" } + appNames := make([]string, 0, len(localSnap.Apps)) + for appName := range localSnap.Apps { + appNames = append(appNames, appName) + } + sort.Strings(appNames) apps := make([]appJSON, 0, len(localSnap.Apps)) - for _, app := range localSnap.Apps { + for _, appName := range appNames { + app := localSnap.Apps[appName] + var installedDesktopFile string + if osutil.FileExists(app.DesktopFile()) { + installedDesktopFile = app.DesktopFile() + } + apps = append(apps, appJSON{ - Name: app.Name, - Daemon: app.Daemon, - Aliases: app.Aliases, + Name: app.Name, + Daemon: app.Daemon, + DesktopFile: installedDesktopFile, + Aliases: app.Aliases, }) } diff --git a/data/systemd/Makefile b/data/systemd/Makefile index b286001b68..8ebc6fe39b 100644 --- a/data/systemd/Makefile +++ b/data/systemd/Makefile @@ -33,7 +33,7 @@ SYSTEMD_UNITS := ${SYSTEMD_UNITS_GENERATED} snapd.refresh.timer snapd.socket all: ${SYSTEMD_UNITS} install: $(SYSTEMD_UNITS) - $(foreach u,$^,install -D -m 0644 ${u} ${DESTDIR}/${SYSTEMDSYSTEMUNITDIR}/${u};) + install -D -m 0644 -t ${DESTDIR}/${SYSTEMDSYSTEMUNITDIR} $^ clean: rm -f ${SYSTEMD_UNITS_GENERATED} diff --git a/dirs/dirs.go b/dirs/dirs.go index 25442b24b8..d32411466a 100644 --- a/dirs/dirs.go +++ b/dirs/dirs.go @@ -47,6 +47,7 @@ var ( SnapMetaDir string SnapdSocket string SnapSocket string + SnapRunDir string SnapRunNsDir string SnapSeedDir string @@ -63,6 +64,9 @@ var ( SnapDesktopFilesDir string SnapBusPolicyDir string + SystemApparmorDir string + SystemApparmorCacheDir string + CloudMetaDataFile string ClassicDir string @@ -73,6 +77,10 @@ var ( XdgRuntimeDirGlob string ) +const ( + defaultSnapMountDir = "/snap" +) + var ( // not exported because it does not honor the global rootdir snappyDir = filepath.Join("var", "lib", "snapd") @@ -100,6 +108,11 @@ func StripRootDir(dir string) string { return "/" + result } +// SupportsClassicConfinement returns true if the current directory layout supports classic confinement. +func SupportsClassicConfinement() bool { + return SnapMountDir == defaultSnapMountDir +} + // SetRootDir allows settings a new global root directory, this is useful // for e.g. chroot operations func SetRootDir(rootdir string) { @@ -112,7 +125,7 @@ func SetRootDir(rootdir string) { case "fedora", "centos", "rhel": SnapMountDir = filepath.Join(rootdir, "/var/lib/snapd/snap") default: - SnapMountDir = filepath.Join(rootdir, "/snap") + SnapMountDir = filepath.Join(rootdir, defaultSnapMountDir) } SnapDataDir = filepath.Join(rootdir, "/var/snap") @@ -125,7 +138,8 @@ func SetRootDir(rootdir string) { SnapMetaDir = filepath.Join(rootdir, snappyDir, "meta") SnapBlobDir = filepath.Join(rootdir, snappyDir, "snaps") SnapDesktopFilesDir = filepath.Join(rootdir, snappyDir, "desktop", "applications") - SnapRunNsDir = filepath.Join(rootdir, "/run/snapd/ns") + SnapRunDir = filepath.Join(rootdir, "/run/snapd") + SnapRunNsDir = filepath.Join(SnapRunDir, "/ns") // keep in sync with the debian/snapd.socket file: SnapdSocket = filepath.Join(rootdir, "/run/snapd.socket") @@ -143,6 +157,9 @@ func SetRootDir(rootdir string) { SnapServicesDir = filepath.Join(rootdir, "/etc/systemd/system") SnapBusPolicyDir = filepath.Join(rootdir, "/etc/dbus-1/system.d") + SystemApparmorDir = filepath.Join(rootdir, "/etc/apparmor.d") + SystemApparmorCacheDir = filepath.Join(rootdir, "/etc/apparmor.d/cache") + CloudMetaDataFile = filepath.Join(rootdir, "/var/lib/cloud/seed/nocloud-net/meta-data") SnapUdevRulesDir = filepath.Join(rootdir, "/etc/udev/rules.d") diff --git a/dirs/dirs_test.go b/dirs/dirs_test.go index 8e8f39aabb..761cdf1ade 100644 --- a/dirs/dirs_test.go +++ b/dirs/dirs_test.go @@ -25,6 +25,7 @@ import ( . "gopkg.in/check.v1" "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/release" ) // Hook up check.v1 into the "go test" runner @@ -47,3 +48,29 @@ func (s *DirsTestSuite) TestStripRootDir(c *C) { // strip only works on paths that begin with the global root directory c.Check(func() { dirs.StripRootDir("/other/foo/bar") }, Panics, `supplied path is not related to global root "/other/foo/bar"`) } + +func (s *DirsTestSuite) TestClassicConfinementSupport(c *C) { + dirs.SetRootDir("/") + c.Assert(dirs.SupportsClassicConfinement(), Equals, true) + dirs.SetRootDir("/alt") + c.Assert(dirs.SupportsClassicConfinement(), Equals, false) +} + +func (s *DirsTestSuite) TestClassicConfinementSupportOnSpecificDistributions(c *C) { + for _, current := range []struct { + Name string + Expected bool + }{ + {"fedora", false}, + {"rhel", false}, + {"centos", false}, + {"ubuntu", true}, + {"debian", true}, + {"suse", true}, + {"yocto", true}} { + reset := release.MockReleaseInfo(&release.OS{ID: current.Name}) + defer reset() + dirs.SetRootDir("/") + c.Assert(dirs.SupportsClassicConfinement(), Equals, current.Expected) + } +} diff --git a/errtracker/errtracker.go b/errtracker/errtracker.go index 591905f2b3..395abfe001 100644 --- a/errtracker/errtracker.go +++ b/errtracker/errtracker.go @@ -33,6 +33,7 @@ import ( "github.com/snapcore/snapd/arch" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/httputil" + "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/release" ) @@ -108,6 +109,15 @@ func Report(snap, errMsg, dupSig string, extra map[string]string) (string, error report[k] = v } } + + // see if we run in testing mode + if osutil.GetenvBool("SNAPPY_TESTING") { + logger.Noticef("errtracker.Report is *not* sent because SNAPPY_TESTING is set") + logger.Noticef("report: %v", report) + return "oops-not-sent", nil + } + + // send it for real reportBson, err := bson.Marshal(report) if err != nil { return "", err diff --git a/errtracker/errtracker_test.go b/errtracker/errtracker_test.go index 3a433dc045..26d238c4bb 100644 --- a/errtracker/errtracker_test.go +++ b/errtracker/errtracker_test.go @@ -24,6 +24,7 @@ import ( "io/ioutil" "net/http" "net/http/httptest" + "os" "path/filepath" "strings" "testing" @@ -49,6 +50,9 @@ type ErrtrackerTestSuite struct { var _ = Suite(&ErrtrackerTestSuite{}) +var truePath = osutil.LookPathDefault("true", "/bin/true") +var falsePath = osutil.LookPathDefault("false", "/bin/false") + func (s *ErrtrackerTestSuite) SetUpTest(c *C) { s.BaseTest.SetUpTest(c) @@ -56,16 +60,16 @@ func (s *ErrtrackerTestSuite) SetUpTest(c *C) { err := ioutil.WriteFile(p, []byte("bbb1a6a5bcdb418380056a2d759c3f7c"), 0644) c.Assert(err, IsNil) s.AddCleanup(errtracker.MockMachineIDPath(p)) - s.AddCleanup(errtracker.MockHostSnapd("/bin/true")) - s.AddCleanup(errtracker.MockCoreSnapd("/bin/false")) + s.AddCleanup(errtracker.MockHostSnapd(truePath)) + s.AddCleanup(errtracker.MockCoreSnapd(falsePath)) } func (s *ErrtrackerTestSuite) TestReport(c *C) { n := 0 identifier := "" - hostBuildID, err := osutil.ReadBuildID("/bin/true") + hostBuildID, err := osutil.ReadBuildID(truePath) c.Assert(err, IsNil) - coreBuildID, err := osutil.ReadBuildID("/bin/false") + coreBuildID, err := osutil.ReadBuildID(falsePath) c.Assert(err, IsNil) prev := errtracker.SnapdVersion @@ -136,3 +140,31 @@ func (s *ErrtrackerTestSuite) TestReport(c *C) { c.Check(id, Equals, "c14388aa-f78d-11e6-8df0-fa163eaf9b83 OOPSID") c.Check(n, Equals, 2) } + +func (s *ErrtrackerTestSuite) TestReportUnderTesting(c *C) { + os.Setenv("SNAPPY_TESTING", "1") + defer os.Unsetenv("SNAPPY_TESTING") + + n := 0 + prev := errtracker.SnapdVersion + defer func() { errtracker.SnapdVersion = prev }() + errtracker.SnapdVersion = "some-snapd-version" + + handler := func(w http.ResponseWriter, r *http.Request) { + n++ + } + + server := httptest.NewServer(http.HandlerFunc(handler)) + defer server.Close() + restorer := errtracker.MockCrashDbURL(server.URL) + defer restorer() + restorer = errtracker.MockTimeNow(func() time.Time { return time.Date(2017, 2, 17, 9, 51, 0, 0, time.UTC) }) + defer restorer() + + id, err := errtracker.Report("some-snap", "failed to do stuff", "[failed to do stuff]", map[string]string{ + "Channel": "beta", + }) + c.Check(err, IsNil) + c.Check(id, Equals, "oops-not-sent") + c.Check(n, Equals, 0) +} diff --git a/interfaces/apparmor/backend.go b/interfaces/apparmor/backend.go index 190fff11ee..f6da80ffa3 100644 --- a/interfaces/apparmor/backend.go +++ b/interfaces/apparmor/backend.go @@ -39,14 +39,19 @@ package apparmor import ( "fmt" + "io/ioutil" "os" + "os/exec" "path/filepath" "regexp" "sort" + "strings" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap" ) @@ -58,6 +63,69 @@ func (b *Backend) Name() interfaces.SecuritySystem { return interfaces.SecurityAppArmor } +// setupSnapConfineReexec will setup apparmor profiles on a classic +// system on the hosts /etc/apparmor.d directory. This is needed for +// running snap-confine from the core snap. +// +// Additionally it will cleanup stale apparmor profiles it created. +func setupSnapConfineReexec(snapInfo *snap.Info) error { + // cleanup old + apparmorProfilePathPattern := strings.Replace(filepath.Join(dirs.SnapMountDir, "/core/*/usr/lib/snapd/snap-confine"), "/", ".", -1)[1:] + + glob, err := filepath.Glob(filepath.Join(dirs.SystemApparmorDir, apparmorProfilePathPattern)) + if err != nil { + return err + } + + for _, path := range glob { + snapConfineInCore := "/" + strings.Replace(filepath.Base(path), ".", "/", -1) + if osutil.FileExists(snapConfineInCore) { + continue + } + + // not using apparmor.UnloadProfile() because it uses a + // different cachedir + if output, err := exec.Command("apparmor_parser", "-R", filepath.Base(path)).CombinedOutput(); err != nil { + logger.Noticef("cannot unload apparmor profile %s: %v", filepath.Base(path), osutil.OutputErr(output, err)) + } + if err := os.Remove(path); err != nil && !os.IsNotExist(err) { + return err + } + if err := os.Remove(filepath.Join(dirs.SystemApparmorCacheDir, filepath.Base(path))); err != nil && !os.IsNotExist(err) { + return err + } + } + + // add new confinement file + coreRoot := snapInfo.MountDir() + snapConfineInCore := filepath.Join(coreRoot, "usr/lib/snapd/snap-confine") + apparmorProfilePath := filepath.Join(dirs.SystemApparmorDir, strings.Replace(snapConfineInCore[1:], "/", ".", -1)) + + // we must test the ".real" suffix first, this is a workaround for + // https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=858004 + apparmorProfile, err := ioutil.ReadFile(filepath.Join(coreRoot, "/etc/apparmor.d/usr.lib.snapd.snap-confine.real")) + if os.IsNotExist(err) { + apparmorProfile, err = ioutil.ReadFile(filepath.Join(coreRoot, "/etc/apparmor.d/usr.lib.snapd.snap-confine")) + if err != nil { + return err + } + } + apparmorProfileForCore := strings.Replace(string(apparmorProfile), "/usr/lib/snapd/snap-confine", snapConfineInCore, -1) + + // /etc/apparmor.d is read/write OnClassic, so write out the + // new core's profile there + if err := osutil.AtomicWriteFile(apparmorProfilePath, []byte(apparmorProfileForCore), 0644, 0); err != nil { + return err + } + + // not using apparmor.LoadProfile() because it uses a different cachedir + if output, err := exec.Command("apparmor_parser", "--replace", "--write-cache", apparmorProfilePath, "--cache-loc", dirs.SystemApparmorCacheDir).CombinedOutput(); err != nil { + return fmt.Errorf("cannot replace snap-confine apparmor profile: %v", osutil.OutputErr(output, err)) + } + + return nil +} + // Setup creates and loads apparmor profiles specific to a given snap. // The snap can be in developer mode to make security violations non-fatal to // the offending application process. @@ -70,6 +138,14 @@ func (b *Backend) Setup(snapInfo *snap.Info, opts interfaces.ConfinementOptions, if err != nil { return fmt.Errorf("cannot obtain apparmor specification for snap %q: %s", snapName, err) } + + // core on classic is special + if snapName == "core" && release.OnClassic && !release.ReleaseInfo.ForceDevMode() { + if err := setupSnapConfineReexec(snapInfo); err != nil { + logger.Noticef("cannot create host snap-confine apparmor configuration: %s", err) + } + } + // Get the files that this snap should have content, err := b.deriveContent(spec.(*Specification), snapInfo, opts) if err != nil { diff --git a/interfaces/apparmor/backend_test.go b/interfaces/apparmor/backend_test.go index 45ff79c05b..1cbb51efa0 100644 --- a/interfaces/apparmor/backend_test.go +++ b/interfaces/apparmor/backend_test.go @@ -24,6 +24,7 @@ import ( "io/ioutil" "os" "path/filepath" + "strings" . "gopkg.in/check.v1" @@ -31,6 +32,8 @@ import ( "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/interfaces/apparmor" "github.com/snapcore/snapd/interfaces/ifacetest" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap/snaptest" "github.com/snapcore/snapd/testutil" ) @@ -396,3 +399,86 @@ func (s *backendSuite) TestCombineSnippets(c *C) { s.RemoveSnap(c, snapInfo) } } + +var coreYaml string = `name: core +version: 1 +` + +func (s *backendSuite) TestSetupHostSnapConfineApparmorForReexecCleans(c *C) { + restorer := release.MockOnClassic(true) + defer restorer() + + canaryName := strings.Replace(filepath.Join(dirs.SnapMountDir, "/core/2718/usr/lib/snapd/snap-confine"), "/", ".", -1)[1:] + canary := filepath.Join(dirs.SystemApparmorDir, canaryName) + err := os.MkdirAll(filepath.Dir(canary), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(canary, nil, 0644) + c.Assert(err, IsNil) + + // install the new core snap on classic triggers cleanup + s.InstallSnap(c, interfaces.ConfinementOptions{}, coreYaml, 111) + + c.Check(osutil.FileExists(canary), Equals, false) + c.Check(s.parserCmd.Calls(), DeepEquals, [][]string{ + {"apparmor_parser", "-R", canaryName}, + }) +} + +func (s *backendSuite) TestSetupHostSnapConfineApparmorForReexecWritesNew(c *C) { + restorer := release.MockOnClassic(true) + defer restorer() + + cmd := testutil.MockCommand(c, "apparmor_parser", "") + defer cmd.Restore() + + var mockAA = []byte(`# Author: Jamie Strandboge <jamie@canonical.com> +#include <tunables/global> + +/usr/lib/snapd/snap-confine (attach_disconnected) { + # We run privileged, so be fanatical about what we include and don't use + # any abstractions + /etc/ld.so.cache r, +} +`) + + err := os.MkdirAll(dirs.SystemApparmorDir, 0755) + c.Assert(err, IsNil) + + // meh, the paths/filenames are all complicated :/ + coreRoot := filepath.Join(dirs.SnapMountDir, "/core/111") + snapConfineApparmorInCore := filepath.Join(coreRoot, "/etc/apparmor.d/usr.lib.snapd.snap-confine.real") + err = os.MkdirAll(filepath.Dir(snapConfineApparmorInCore), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(snapConfineApparmorInCore, mockAA, 0644) + c.Assert(err, IsNil) + + // install the new core snap on classic triggers a new snap-confine + // for this snap-confine on core + s.InstallSnap(c, interfaces.ConfinementOptions{}, coreYaml, 111) + + newAA, err := filepath.Glob(filepath.Join(dirs.SystemApparmorDir, "*")) + c.Assert(err, IsNil) + + c.Assert(newAA, HasLen, 1) + c.Check(newAA[0], Matches, `.*/etc/apparmor.d/.*.snap.core.111.usr.lib.snapd.snap-confine`) + + content, err := ioutil.ReadFile(newAA[0]) + c.Assert(err, IsNil) + // this is the key, rewriting "/usr/lib/snapd/snap-confine + c.Check(string(content), testutil.Contains, "/snap/core/111/usr/lib/snapd/snap-confine (attach_disconnected) {") + // no other changes other than that to the input + c.Check(string(content), Equals, fmt.Sprintf(`# Author: Jamie Strandboge <jamie@canonical.com> +#include <tunables/global> + +%s/core/111/usr/lib/snapd/snap-confine (attach_disconnected) { + # We run privileged, so be fanatical about what we include and don't use + # any abstractions + /etc/ld.so.cache r, +} +`, dirs.SnapMountDir)) + + c.Check(cmd.Calls(), DeepEquals, [][]string{ + {"apparmor_parser", "--replace", "--write-cache", newAA[0], "--cache-loc", dirs.SystemApparmorCacheDir}, + }) + +} diff --git a/interfaces/apparmor/template.go b/interfaces/apparmor/template.go index 1424c4fb0f..9531072d2f 100644 --- a/interfaces/apparmor/template.go +++ b/interfaces/apparmor/template.go @@ -344,7 +344,7 @@ var defaultTemplate = ` # access in /dev/shm for shm_open() and files in subdirectories for open() /{dev,run}/shm/snap.@{SNAP_NAME}.** mrwlkix, # Also allow app-specific access for sem_open() - /{dev,run}/shm/sem.snap.@{SNAP_NAME}.* rwk, + /{dev,run}/shm/sem.snap.@{SNAP_NAME}.* mrwk, # Snap-specific XDG_RUNTIME_DIR that is based on the UID of the user owner /run/user/[0-9]*/snap.@{SNAP_NAME}/ rw, diff --git a/interfaces/builtin/all.go b/interfaces/builtin/all.go index 330c97a885..5f8741de27 100644 --- a/interfaces/builtin/all.go +++ b/interfaces/builtin/all.go @@ -57,6 +57,7 @@ var allInterfaces = []interfaces.Interface{ &SerialPortInterface{}, &ThumbnailerServiceInterface{}, &TimeControlInterface{}, + &Unity7Interface{}, &UDisks2Interface{}, &UbuntuDownloadManagerInterface{}, &Unity8Interface{}, @@ -77,6 +78,7 @@ var allInterfaces = []interfaces.Interface{ NewHardwareObserveInterface(), NewHomeInterface(), NewKernelModuleControlInterface(), + NewKubernetesSupportInterface(), NewLibvirtInterface(), NewLocaleControlInterface(), NewLogObserveInterface(), @@ -102,7 +104,6 @@ var allInterfaces = []interfaces.Interface{ NewTimeserverControlInterface(), NewTimezoneControlInterface(), NewTpmInterface(), - NewUnity7Interface(), NewUnity8CalendarInterface(), NewUnity8ContactsInterface(), NewX11Interface(), diff --git a/interfaces/builtin/all_test.go b/interfaces/builtin/all_test.go index 40f3ab9402..288309c3e6 100644 --- a/interfaces/builtin/all_test.go +++ b/interfaces/builtin/all_test.go @@ -63,6 +63,7 @@ func (s *AllSuite) TestInterfaces(c *C) { c.Check(all, Contains, &builtin.Unity8Interface{}) c.Check(all, Contains, &builtin.UpowerObserveInterface{}) c.Check(all, Contains, &builtin.UhidInterface{}) + c.Check(all, Contains, &builtin.Unity7Interface{}) c.Check(all, DeepContains, builtin.NewAccountControlInterface()) c.Check(all, DeepContains, builtin.NewAlsaInterface()) c.Check(all, DeepContains, builtin.NewAvahiObserveInterface()) @@ -75,6 +76,7 @@ func (s *AllSuite) TestInterfaces(c *C) { c.Check(all, DeepContains, builtin.NewGsettingsInterface()) c.Check(all, DeepContains, builtin.NewHomeInterface()) c.Check(all, DeepContains, builtin.NewKernelModuleControlInterface()) + c.Check(all, DeepContains, builtin.NewKubernetesSupportInterface()) c.Check(all, DeepContains, builtin.NewLocaleControlInterface()) c.Check(all, DeepContains, builtin.NewLogObserveInterface()) c.Check(all, DeepContains, builtin.NewMountObserveInterface()) @@ -96,7 +98,6 @@ func (s *AllSuite) TestInterfaces(c *C) { c.Check(all, DeepContains, builtin.NewTimeserverControlInterface()) c.Check(all, DeepContains, builtin.NewTimezoneControlInterface()) c.Check(all, DeepContains, builtin.NewTpmInterface()) - c.Check(all, DeepContains, builtin.NewUnity7Interface()) c.Check(all, DeepContains, builtin.NewUnity8CalendarInterface()) c.Check(all, DeepContains, builtin.NewUnity8ContactsInterface()) c.Check(all, DeepContains, builtin.NewX11Interface()) diff --git a/interfaces/builtin/basedeclaration.go b/interfaces/builtin/basedeclaration.go index 7995e83a7a..66a86f7ceb 100644 --- a/interfaces/builtin/basedeclaration.go +++ b/interfaces/builtin/basedeclaration.go @@ -151,6 +151,9 @@ plugs: kernel-module-control: allow-installation: false deny-auto-connection: true + kubernetes-support: + allow-installation: false + deny-auto-connection: true lxd-support: allow-installation: false deny-auto-connection: true @@ -334,6 +337,11 @@ slots: slot-snap-type: - core deny-auto-connection: true + kubernetes-support: + allow-installation: + slot-snap-type: + - core + deny-auto-connection: true libvirt: allow-installation: slot-snap-type: diff --git a/interfaces/builtin/basedeclaration_test.go b/interfaces/builtin/basedeclaration_test.go index f1ddbe6b87..518f48545b 100644 --- a/interfaces/builtin/basedeclaration_test.go +++ b/interfaces/builtin/basedeclaration_test.go @@ -363,6 +363,24 @@ plugs: c.Check(err, IsNil) } +func (s *baseDeclSuite) TestAutoConnectionKubernetesSupportOverride(c *C) { + cand := s.connectCand(c, "kubernetes-support", "", "") + err := cand.CheckAutoConnect() + c.Check(err, NotNil) + c.Assert(err, ErrorMatches, "auto-connection denied by plug rule of interface \"kubernetes-support\"") + + plugsSlots := ` +plugs: + kubernetes-support: + allow-auto-connection: true +` + + snapDecl := s.mockSnapDecl(c, "some-snap", "J60k4JY0HppjwOjW8dZdYc8obXKxujRu", "canonical", plugsSlots) + cand.PlugSnapDeclaration = snapDecl + err = cand.CheckAutoConnect() + c.Check(err, IsNil) +} + func (s *baseDeclSuite) TestAutoConnectionOverrideMultiple(c *C) { plugsSlots := ` plugs: @@ -427,6 +445,7 @@ var ( "hidraw": {"core", "gadget"}, "i2c": {"core", "gadget"}, "iio": {"core", "gadget"}, + "kubernetes-support": {"core"}, "location-control": {"app"}, "location-observe": {"app"}, "lxd-support": {"core"}, @@ -531,6 +550,7 @@ func (s *baseDeclSuite) TestPlugInstallation(c *C) { "classic-support": true, "docker-support": true, "kernel-module-control": true, + "kubernetes-support": true, "lxd-support": true, "snapd-control": true, "unity8": true, @@ -653,6 +673,7 @@ func (s *baseDeclSuite) TestSanity(c *C) { "core-support": true, "docker-support": true, "kernel-module-control": true, + "kubernetes-support": true, "lxd-support": true, "snapd-control": true, "unity8": true, diff --git a/interfaces/builtin/browser_support.go b/interfaces/builtin/browser_support.go index f1b7512c18..979194c569 100644 --- a/interfaces/builtin/browser_support.go +++ b/interfaces/builtin/browser_support.go @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -27,9 +27,8 @@ import ( "github.com/snapcore/snapd/interfaces/seccomp" ) -// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/apparmor/policygroups/ubuntu-core/16.04/log-observe const browserSupportConnectedPlugAppArmor = ` -# Description: Can access various APIs needed by modern browers (eg, Google +# Description: Can access various APIs needed by modern browsers (eg, Google # Chrome/Chromium and Mozilla) and file paths they expect. This interface is # transitional and is only in place while upstream's work to change their paths # and snappy is updated to properly mediate the APIs. @@ -44,12 +43,15 @@ owner /var/tmp/etilqs_* rw, # Chrome/Chromium should be modified to use snap.$SNAP_NAME.* or the snap # packaging adjusted to use LD_PRELOAD technique from LP: #1577514 -owner /{dev,run}/shm/{,.}org.chromium.Chromium.* rw, -owner /{dev,run}/shm/{,.}com.google.Chrome.* rw, +owner /{dev,run}/shm/{,.}org.chromium.Chromium.* mrw, +owner /{dev,run}/shm/{,.}com.google.Chrome.* mrw, # Allow reading platform files /run/udev/data/+platform:* r, +# Chromium content api in gnome-shell reads this +/etc/opt/chrome/{,**} r, + # Chrome/Chromium should be adjusted to not use gconf. It is only used with # legacy systems that don't have snapd deny dbus (send) @@ -190,11 +192,14 @@ owner @{PROC}/@{pid}/uid_map rw, owner @{PROC}/@{pid}/gid_map rw, # Webkit uses a particular SHM names # LP: 1578217 -owner /{dev,run}/shm/WK2SharedMemory.* rw, +owner /{dev,run}/shm/WK2SharedMemory.* mrw, + +# Chromium content api on (at least) later versions of Ubuntu just use this +owner /{dev,run}/shm/shmfd-* mrw, ` const browserSupportConnectedPlugSecComp = ` -# Description: Can access various APIs needed by modern browers (eg, Google +# Description: Can access various APIs needed by modern browsers (eg, Google # Chrome/Chromium and Mozilla) and file paths they expect. This interface is # transitional and is only in place while upstream's work to change their paths # and snappy is updated to properly mediate the APIs. diff --git a/interfaces/builtin/browser_support_test.go b/interfaces/builtin/browser_support_test.go index 3368002acd..1d479a95cd 100644 --- a/interfaces/builtin/browser_support_test.go +++ b/interfaces/builtin/browser_support_test.go @@ -109,7 +109,7 @@ func (s *BrowserSupportInterfaceSuite) TestConnectedPlugSnippetWithoutAttrib(c * c.Assert(err, IsNil) c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.other.app2"}) snippet := apparmorSpec.SnippetForTag("snap.other.app2") - c.Assert(string(snippet), testutil.Contains, `# Description: Can access various APIs needed by modern browers`) + c.Assert(string(snippet), testutil.Contains, `# Description: Can access various APIs needed by modern browsers`) c.Assert(string(snippet), Not(testutil.Contains), `capability sys_admin,`) c.Assert(string(snippet), testutil.Contains, `deny ptrace (trace) peer=snap.@{SNAP_NAME}.**`) @@ -118,7 +118,7 @@ func (s *BrowserSupportInterfaceSuite) TestConnectedPlugSnippetWithoutAttrib(c * c.Assert(err, IsNil) c.Assert(seccompSpec.SecurityTags(), DeepEquals, []string{"snap.other.app2"}) secCompSnippet := seccompSpec.SnippetForTag("snap.other.app2") - c.Assert(secCompSnippet, testutil.Contains, `# Description: Can access various APIs needed by modern browers`) + c.Assert(secCompSnippet, testutil.Contains, `# Description: Can access various APIs needed by modern browsers`) c.Assert(secCompSnippet, Not(testutil.Contains), `chroot`) } @@ -142,7 +142,7 @@ apps: c.Assert(err, IsNil) c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.browser-support-plug-snap.app2"}) snippet := apparmorSpec.SnippetForTag("snap.browser-support-plug-snap.app2") - c.Assert(snippet, testutil.Contains, `# Description: Can access various APIs needed by modern browers`) + c.Assert(snippet, testutil.Contains, `# Description: Can access various APIs needed by modern browsers`) c.Assert(snippet, Not(testutil.Contains), `capability sys_admin,`) c.Assert(snippet, testutil.Contains, `deny ptrace (trace) peer=snap.@{SNAP_NAME}.**`) @@ -151,7 +151,7 @@ apps: c.Assert(err, IsNil) c.Assert(seccompSpec.SecurityTags(), DeepEquals, []string{"snap.browser-support-plug-snap.app2"}) secCompSnippet := seccompSpec.SnippetForTag("snap.browser-support-plug-snap.app2") - c.Assert(secCompSnippet, testutil.Contains, `# Description: Can access various APIs needed by modern browers`) + c.Assert(secCompSnippet, testutil.Contains, `# Description: Can access various APIs needed by modern browsers`) c.Assert(secCompSnippet, Not(testutil.Contains), `chroot`) } @@ -174,7 +174,7 @@ apps: c.Assert(err, IsNil) c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.browser-support-plug-snap.app2"}) snippet := apparmorSpec.SnippetForTag("snap.browser-support-plug-snap.app2") - c.Assert(snippet, testutil.Contains, `# Description: Can access various APIs needed by modern browers`) + c.Assert(snippet, testutil.Contains, `# Description: Can access various APIs needed by modern browsers`) c.Assert(snippet, testutil.Contains, `ptrace (trace) peer=snap.@{SNAP_NAME}.**`) c.Assert(snippet, Not(testutil.Contains), `deny ptrace (trace) peer=snap.@{SNAP_NAME}.**`) @@ -183,7 +183,7 @@ apps: c.Assert(err, IsNil) c.Assert(seccompSpec.SecurityTags(), DeepEquals, []string{"snap.browser-support-plug-snap.app2"}) secCompSnippet := seccompSpec.SnippetForTag("snap.browser-support-plug-snap.app2") - c.Assert(secCompSnippet, testutil.Contains, `# Description: Can access various APIs needed by modern browers`) + c.Assert(secCompSnippet, testutil.Contains, `# Description: Can access various APIs needed by modern browsers`) c.Assert(secCompSnippet, testutil.Contains, `chroot`) } diff --git a/interfaces/builtin/core_support.go b/interfaces/builtin/core_support.go index 7e28fab4dc..ce382f171d 100644 --- a/interfaces/builtin/core_support.go +++ b/interfaces/builtin/core_support.go @@ -53,8 +53,11 @@ const coreSupportConnectedPlugAppArmor = ` # Allow modifying logind configuration. For now, allow reading all logind # configuration but only allow modifying NN-snap*.conf and snap*.conf files -# in /etc/systemd/logind.conf.d. +# in /etc/systemd/logind.conf.d. Also allow creating the logind.conf.d +# directory as it may not be there for existing installs (wirtable-path +# magic oddness). /etc/systemd/logind.conf r, +/etc/systemd/logind.conf.d/ rw, /etc/systemd/logind.conf.d/{,*} r, /etc/systemd/logind.conf.d/{,[0-9][0-9]-}snap*.conf w, @@ -79,7 +82,9 @@ owner /boot/uboot/config.txt.tmp rwk, func NewCoreSupportInterface() interfaces.Interface { return &commonInterface{ name: "core-support", - connectedPlugAppArmor: coreSupportConnectedPlugAppArmor, + // NOTE: cure-support implicitly contains the rules from network-bind. + connectedPlugAppArmor: coreSupportConnectedPlugAppArmor + networkBindConnectedPlugAppArmor, + connectedPlugSecComp: "" + networkBindConnectedPlugSecComp, reservedForOS: true, } } diff --git a/interfaces/builtin/docker_support.go b/interfaces/builtin/docker_support.go index 72540017b9..0d09f3b03d 100644 --- a/interfaces/builtin/docker_support.go +++ b/interfaces/builtin/docker_support.go @@ -101,7 +101,7 @@ signal (send) peer=docker-default, ptrace (read, trace) peer=docker-default, # Graph (storage) driver bits -/{dev,run}/shm/aufs.xino rw, +/{dev,run}/shm/aufs.xino mrw, /proc/fs/aufs/plink_maint w, /sys/fs/aufs/** r, diff --git a/interfaces/builtin/firewall_control.go b/interfaces/builtin/firewall_control.go index 995de968c1..24a600d8d3 100644 --- a/interfaces/builtin/firewall_control.go +++ b/interfaces/builtin/firewall_control.go @@ -68,11 +68,24 @@ unix (bind) type=stream addr="@xtables", @{PROC}/sys/net/netfilter/** r, @{PROC}/sys/net/nf_conntrack_max r, +# check the state of the Kmod modules +/sys/module/arp_tables/ r, +/sys/module/arp_tables/initstate r, +/sys/module/br_netfilter/ r, +/sys/module/br_netfilter/initstate r, +/sys/module/iptable_filter/ r, +/sys/module/iptable_filter/initstate r, +/sys/module/ip6table_filter/ r, +/sys/module/ip6table_filter/initstate r, + # read netfilter module parameters -/sys/module/nf_*/ r, -/sys/module/nf_*/parameters/{,*} r, +/sys/module/nf_*/ r, +/sys/module/nf_*/parameters/{,*} r, # various firewall related sysctl files +@{PROC}/sys/net/bridge/bridge-nf-call-arptables rw, +@{PROC}/sys/net/bridge/bridge-nf-call-iptables rw, +@{PROC}/sys/net/bridge/bridge-nf-call-ip6tables rw, @{PROC}/sys/net/ipv4/conf/*/rp_filter w, @{PROC}/sys/net/ipv{4,6}/conf/*/accept_source_route w, @{PROC}/sys/net/ipv{4,6}/conf/*/accept_redirects w, @@ -98,7 +111,11 @@ capset setuid ` -var firewallControlConnectedPlugKmod = []string{"ip6table_filter", "iptable_filter"} +var firewallControlConnectedPlugKmod = []string{ + "arp_tables", + "br_netfilter", + "ip6table_filter", + "iptable_filter"} // NewFirewallControlInterface returns a new "firewall-control" interface. func NewFirewallControlInterface() interfaces.Interface { diff --git a/interfaces/builtin/firewall_control_test.go b/interfaces/builtin/firewall_control_test.go index 927650b4b6..de3054bc00 100644 --- a/interfaces/builtin/firewall_control_test.go +++ b/interfaces/builtin/firewall_control_test.go @@ -106,6 +106,8 @@ func (s *FirewallControlInterfaceSuite) TestUsedSecuritySystems(c *C) { err = spec.AddConnectedPlug(s.iface, s.plug, s.slot) c.Assert(err, IsNil) c.Assert(spec.Modules(), DeepEquals, map[string]bool{ + "arp_tables": true, + "br_netfilter": true, "ip6table_filter": true, "iptable_filter": true, }) diff --git a/interfaces/builtin/kernel_module_control.go b/interfaces/builtin/kernel_module_control.go index 79bbc7bbbf..89bc4a1cfb 100644 --- a/interfaces/builtin/kernel_module_control.go +++ b/interfaces/builtin/kernel_module_control.go @@ -36,6 +36,9 @@ const kernelModuleControlConnectedPlugAppArmor = ` # /proc/sys/kernel/dmesg_restrict is '1' (syslog(2)). These operations are # required to verify kernel modules that are loaded. capability syslog, + + # Allow plug side to read information about loaded kernel modules + /sys/module/{,**} r, ` const kernelModuleControlConnectedPlugSecComp = ` diff --git a/interfaces/builtin/kubernetes_support.go b/interfaces/builtin/kubernetes_support.go new file mode 100644 index 0000000000..a14d042f30 --- /dev/null +++ b/interfaces/builtin/kubernetes_support.go @@ -0,0 +1,83 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package builtin + +import ( + "github.com/snapcore/snapd/interfaces" +) + +const kubernetesSupportConnectedPlugAppArmor = ` +# Description: can use kubernetes to control Docker containers. This interface +# is restricted because it gives wide ranging access to the host and other +# processes. + +# what is this for? +#include <abstractions/dbus-strict> + +# Allow reading all information regarding cgroups for all processes +capability sys_resource, +@{PROC}/@{pid}/cgroup r, +/sys/fs/cgroup/{,**} r, + +# Allow adjusting the OOM score for Docker containers. Note, this allows +# adjusting for all processes, not just containers. +@{PROC}/@{pid}/oom_score_adj rw, +/sys/kernel/mm/hugepages/ r, + +@{PROC}/sys/kernel/panic rw, +@{PROC}/sys/kernel/panic_on_oops rw, +@{PROC}/sys/kernel/keys/root_maxbytes r, +@{PROC}/sys/kernel/keys/root_maxkeys r, +@{PROC}/sys/vm/overcommit_memory rw, +@{PROC}/sys/vm/panic_on_oom r, + +@{PROC}/diskstats r, +@{PROC}/@{pid}/cmdline r, + +# Allow reading the state of modules kubernetes needs +/sys/module/llc/initstate r, +/sys/module/stp/initstate r, + +# Allow listing kernel modules. Note, seccomp blocks module loading syscalls +/sys/module/apparmor/parameters/enabled r, +/bin/kmod ixr, +/etc/modprobe.d/{,**} r, + +# Allow ptracing Docker containers +ptrace (read, trace) peer=docker-default, +ptrace (read, trace) peer=snap.docker.dockerd, + +# Should we have a 'privileged' mode for kubernetes like we do with +# docker-support? Right now kubernetes needs this which allows it to control +# all processes on the system and on <4.8 kernels, escape confinement. +ptrace (read, trace) peer=unconfined, +` + +var kubernetesSupportConnectedPlugKmod = []string{`llc`, `stp`} + +// NewKubernetesSupportInterface returns a new "openvswitch-support" interface. +func NewKubernetesSupportInterface() interfaces.Interface { + return &commonInterface{ + name: "kubernetes-support", + connectedPlugAppArmor: kubernetesSupportConnectedPlugAppArmor, + connectedPlugKModModules: kubernetesSupportConnectedPlugKmod, + reservedForOS: true, + } +} diff --git a/interfaces/builtin/kubernetes_support_test.go b/interfaces/builtin/kubernetes_support_test.go new file mode 100644 index 0000000000..cfb8917bb3 --- /dev/null +++ b/interfaces/builtin/kubernetes_support_test.go @@ -0,0 +1,104 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package builtin_test + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/apparmor" + "github.com/snapcore/snapd/interfaces/builtin" + "github.com/snapcore/snapd/interfaces/kmod" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/testutil" +) + +type KubernetesSupportInterfaceSuite struct { + iface interfaces.Interface + slot *interfaces.Slot + plug *interfaces.Plug +} + +const k8sMockPlugSnapInfoYaml = `name: other +version: 1.0 +apps: + app2: + command: foo + plugs: [kubernetes-support] +` + +var _ = Suite(&KubernetesSupportInterfaceSuite{}) + +func (s *KubernetesSupportInterfaceSuite) SetUpTest(c *C) { + s.iface = builtin.NewKubernetesSupportInterface() + s.slot = &interfaces.Slot{ + SlotInfo: &snap.SlotInfo{ + Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS}, + Name: "kubernetes-support", + Interface: "kubernetes-support", + }, + } + plugSnap := snaptest.MockInfo(c, k8sMockPlugSnapInfoYaml, nil) + s.plug = &interfaces.Plug{PlugInfo: plugSnap.Plugs["kubernetes-support"]} +} + +func (s *KubernetesSupportInterfaceSuite) TestName(c *C) { + c.Assert(s.iface.Name(), Equals, "kubernetes-support") +} + +func (s *KubernetesSupportInterfaceSuite) TestSanitizeSlot(c *C) { + err := s.iface.SanitizeSlot(s.slot) + c.Assert(err, IsNil) + err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{ + Snap: &snap.Info{SuggestedName: "some-snap"}, + Name: "kubernetes-support", + Interface: "kubernetes-support", + }}) + c.Assert(err, ErrorMatches, "kubernetes-support slots are reserved for the operating system snap") +} + +func (s *KubernetesSupportInterfaceSuite) TestSanitizePlug(c *C) { + err := s.iface.SanitizePlug(s.plug) + c.Assert(err, IsNil) +} + +func (s *KubernetesSupportInterfaceSuite) TestSanitizeIncorrectInterface(c *C) { + c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) }, + PanicMatches, `slot is not of interface "kubernetes-support"`) + c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) }, + PanicMatches, `plug is not of interface "kubernetes-support"`) +} + +func (s *KubernetesSupportInterfaceSuite) TestUsedSecuritySystems(c *C) { + kmodSpec := &kmod.Specification{} + err := kmodSpec.AddConnectedPlug(s.iface, s.plug, s.slot) + c.Assert(err, IsNil) + c.Assert(kmodSpec.Modules(), DeepEquals, map[string]bool{ + "llc": true, + "stp": true, + }) + + apparmorSpec := &apparmor.Specification{} + err = apparmorSpec.AddConnectedPlug(s.iface, s.plug, s.slot) + c.Assert(err, IsNil) + c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.other.app2"}) + c.Check(apparmorSpec.SnippetForTag("snap.other.app2"), testutil.Contains, "# Allow reading the state of modules kubernetes needs\n") +} diff --git a/interfaces/builtin/log_observe.go b/interfaces/builtin/log_observe.go index 80f26e31e4..2e1e6f49dd 100644 --- a/interfaces/builtin/log_observe.go +++ b/interfaces/builtin/log_observe.go @@ -32,6 +32,7 @@ const logObserveConnectedPlugAppArmor = ` /run/log/journal/ r, /run/log/journal/** r, /var/lib/systemd/catalog/database r, +/{,usr/}bin/journalctl ixr, # Allow sysctl -w kernel.printk_ratelimit=# /{,usr/}sbin/sysctl ixr, diff --git a/interfaces/builtin/mir.go b/interfaces/builtin/mir.go index 731699c5f3..38ff62ec65 100644 --- a/interfaces/builtin/mir.go +++ b/interfaces/builtin/mir.go @@ -35,7 +35,7 @@ const mirPermanentSlotAppArmor = ` capability sys_tty_config, /dev/tty[0-9]* rw, -/{dev,run}/shm/\#* rw, +/{dev,run}/shm/\#* mrw, /run/mir_socket rw, # Needed for mode setting via drmSetMaster() and drmDropMaster() diff --git a/interfaces/builtin/pulseaudio.go b/interfaces/builtin/pulseaudio.go index 96d42cc63d..f8632194a9 100644 --- a/interfaces/builtin/pulseaudio.go +++ b/interfaces/builtin/pulseaudio.go @@ -27,7 +27,7 @@ import ( ) const pulseaudioConnectedPlugAppArmor = ` -/{run,dev}/shm/pulse-shm-* rwk, +/{run,dev}/shm/pulse-shm-* mrwk, owner /{,var/}run/pulse/ r, owner /{,var/}run/pulse/native rwk, @@ -85,7 +85,7 @@ owner /{,var/}run/pulse/ rw, owner /{,var/}run/pulse/** rwk, # Shared memory based communication with clients -/{run,dev}/shm/pulse-shm-* rwk, +/{run,dev}/shm/pulse-shm-* mrwk, /usr/share/applications/ r, diff --git a/interfaces/builtin/unity7.go b/interfaces/builtin/unity7.go index c6a7a06dbe..3651a58a26 100644 --- a/interfaces/builtin/unity7.go +++ b/interfaces/builtin/unity7.go @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -20,14 +20,19 @@ package builtin import ( + "fmt" + "strings" + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/apparmor" + "github.com/snapcore/snapd/interfaces/seccomp" + "github.com/snapcore/snapd/snap" ) -// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/apparmor/policygroups/ubuntu-core/16.04/unity7 const unity7ConnectedPlugAppArmor = ` -# Description: Can access Unity7. Restricted because Unity 7 runs on X and -# requires access to various DBus services and this environment does not prevent -# eavesdropping or apps interfering with one another. +# Description: Can access Unity7. Note, Unity 7 runs on X and requires access +# to various DBus services and this environment does not prevent eavesdropping +# or apps interfering with one another. #include <abstractions/dbus-strict> #include <abstractions/dbus-session-strict> @@ -401,6 +406,42 @@ dbus (receive) member=Get* peer=(label=unconfined), +# unity messaging menu +# first, allow finding the desktop file +/usr/share/applications/ r, +# this leaks the names of snaps with desktop files +/var/lib/snapd/desktop/applications/ r, +/var/lib/snapd/desktop/applications/mimeinfo.cache r, +/var/lib/snapd/desktop/applications/@{SNAP_NAME}_*.desktop r, + +# then allow talking to Unity DBus service +dbus (send) + bus=session + interface=org.freedesktop.DBus.Properties + path=/com/canonical/indicator/messages/service + member=GetAll + peer=(label=unconfined), + +dbus (send) + bus=session + path=/com/canonical/indicator/messages/service + interface=com.canonical.indicator.messages.service + member={Register,Unregister}Application + peer=(label=unconfined), + +dbus (receive) + bus=session + interface=org.freedesktop.DBus.Properties + path=/com/canonical/indicator/messages/###UNITY_SNAP_NAME###_*_desktop + member=GetAll + peer=(label=unconfined), + +dbus (receive, send) + bus=session + interface=com.canonical.indicator.messages.application + path=/com/canonical/indicator/messages/###UNITY_SNAP_NAME###_*_desktop + peer=(label=unconfined), + # This rule is meant to be covered by abstractions/dbus-session-strict but # the unity launcher code has a typo that uses /org/freedesktop/dbus as the # path instead of /org/freedesktop/DBus, so we need to all it here. @@ -460,22 +501,60 @@ dbus (send) deny /{dev,run,var/run}/shm/lttng-ust-* rw, ` -// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/seccomp/policygroups/ubuntu-core/16.04/unity7 -const unity7ConnectedPlugSecComp = ` -# Description: Can access Unity7. Restricted because Unity 7 runs on X and -# requires access to various DBus services and this environment does not prevent -# eavesdropping or apps interfering with one another. +const unity7ConnectedPlugSeccomp = ` +# Description: Can access Unity7. Note, Unity 7 runs on X and requires access +# to various DBus services and this environment does not prevent eavesdropping +# or apps interfering with one another. # X shutdown ` -// NewUnity7Interface returns a new "unity7" interface. -func NewUnity7Interface() interfaces.Interface { - return &commonInterface{ - name: "unity7", - connectedPlugAppArmor: unity7ConnectedPlugAppArmor, - connectedPlugSecComp: unity7ConnectedPlugSecComp, - reservedForOS: true, +type Unity7Interface struct{} + +func (iface *Unity7Interface) Name() string { + return "unity7" +} + +func (iface *Unity7Interface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error { + // Unity7 will take the desktop filename and convert all '-' (and '.', + // but we don't care about that here because the rule above already + // does that) to '_'. Since we know that the desktop filename starts + // with the snap name, perform this conversion on the snap name. + new := strings.Replace(plug.Snap.Name(), "-", "_", -1) + old := "###UNITY_SNAP_NAME###" + snippet := strings.Replace(unity7ConnectedPlugAppArmor, old, new, -1) + spec.AddSnippet(snippet) + return nil +} + +func (iface *Unity7Interface) SecCompConnectedPlug(spec *seccomp.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error { + spec.AddSnippet(unity7ConnectedPlugSeccomp) + return nil +} + +func (iface *Unity7Interface) SanitizePlug(plug *interfaces.Plug) error { + if iface.Name() != plug.Interface { + panic(fmt.Sprintf("plug is not of interface %q", iface.Name())) } + + return nil +} + +func (iface *Unity7Interface) SanitizeSlot(slot *interfaces.Slot) error { + if iface.Name() != slot.Interface { + panic(fmt.Sprintf("slot is not of interface %q", iface.Name())) + } + + // Creation of the slot of this type is allowed only by the os snap + if !(slot.Snap.Type == snap.TypeOS) { + return fmt.Errorf("%s slots are reserved for the operating system snap", iface.Name()) + } + + return nil +} + +func (iface *Unity7Interface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool { + // allow what declarations allowed + return true } diff --git a/interfaces/builtin/unity7_test.go b/interfaces/builtin/unity7_test.go index 912ac2f1b3..106a8a833c 100644 --- a/interfaces/builtin/unity7_test.go +++ b/interfaces/builtin/unity7_test.go @@ -39,7 +39,7 @@ type Unity7InterfaceSuite struct { var _ = Suite(&Unity7InterfaceSuite{}) -const unity7mockPlugSnapInfoYaml = `name: other +const unity7mockPlugSnapInfoYaml = `name: other-snap version: 1.0 apps: app2: @@ -48,9 +48,7 @@ apps: ` func (s *Unity7InterfaceSuite) SetUpTest(c *C) { - s.iface = builtin.NewUnity7Interface() - plugSnap := snaptest.MockInfo(c, unity7mockPlugSnapInfoYaml, nil) - s.plug = &interfaces.Plug{PlugInfo: plugSnap.Plugs["unity7"]} + s.iface = &builtin.Unity7Interface{} s.slot = &interfaces.Slot{ SlotInfo: &snap.SlotInfo{ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS}, @@ -58,6 +56,8 @@ func (s *Unity7InterfaceSuite) SetUpTest(c *C) { Interface: "unity7", }, } + plugSnap := snaptest.MockInfo(c, unity7mockPlugSnapInfoYaml, nil) + s.plug = &interfaces.Plug{PlugInfo: plugSnap.Plugs["unity7"]} } func (s *Unity7InterfaceSuite) TestName(c *C) { @@ -81,9 +81,9 @@ func (s *Unity7InterfaceSuite) TestSanitizePlug(c *C) { } func (s *Unity7InterfaceSuite) TestSanitizeIncorrectInterface(c *C) { - c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) }, + c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other-snap"}}) }, PanicMatches, `slot is not of interface "unity7"`) - c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) }, + c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other-snap"}}) }, PanicMatches, `plug is not of interface "unity7"`) } @@ -92,13 +92,14 @@ func (s *Unity7InterfaceSuite) TestUsedSecuritySystems(c *C) { apparmorSpec := &apparmor.Specification{} err := apparmorSpec.AddConnectedPlug(s.iface, s.plug, s.slot) c.Assert(err, IsNil) - c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.other.app2"}) - c.Assert(apparmorSpec.SnippetForTag("snap.other.app2"), testutil.Contains, `/usr/share/pixmaps`) + c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.other-snap.app2"}) + c.Assert(apparmorSpec.SnippetForTag("snap.other-snap.app2"), testutil.Contains, `/usr/share/pixmaps`) + c.Assert(apparmorSpec.SnippetForTag("snap.other-snap.app2"), testutil.Contains, `path=/com/canonical/indicator/messages/other_snap_*_desktop`) // connected plugs have a non-nil security snippet for seccomp seccompSpec := &seccomp.Specification{} err = seccompSpec.AddConnectedPlug(s.iface, s.plug, s.slot) c.Assert(err, IsNil) - c.Assert(seccompSpec.SecurityTags(), DeepEquals, []string{"snap.other.app2"}) - c.Check(seccompSpec.SnippetForTag("snap.other.app2"), testutil.Contains, "shutdown\n") + c.Assert(seccompSpec.SecurityTags(), DeepEquals, []string{"snap.other-snap.app2"}) + c.Check(seccompSpec.SnippetForTag("snap.other-snap.app2"), testutil.Contains, "shutdown\n") } diff --git a/interfaces/core.go b/interfaces/core.go index d48c7bf7d2..bb1b3b6cdb 100644 --- a/interfaces/core.go +++ b/interfaces/core.go @@ -45,7 +45,7 @@ type PlugRef struct { } // String returns the "snap:plug" representation of a plug reference. -func (ref *PlugRef) String() string { +func (ref PlugRef) String() string { return fmt.Sprintf("%s:%s", ref.Snap, ref.Name) } @@ -67,7 +67,7 @@ type SlotRef struct { } // String returns the "snap:slot" representation of a slot reference. -func (ref *SlotRef) String() string { +func (ref SlotRef) String() string { return fmt.Sprintf("%s:%s", ref.Snap, ref.Name) } diff --git a/interfaces/core_test.go b/interfaces/core_test.go index a224b81a39..7b2d0aaadf 100644 --- a/interfaces/core_test.go +++ b/interfaces/core_test.go @@ -133,6 +133,8 @@ func (s *CoreSuite) TestPlugRef(c *C) { func (s *CoreSuite) TestPlugRefString(c *C) { ref := PlugRef{Snap: "snap", Name: "plug"} c.Check(ref.String(), Equals, "snap:plug") + refPtr := &PlugRef{Snap: "snap", Name: "plug"} + c.Check(refPtr.String(), Equals, "snap:plug") } // Slot.Ref works as expected @@ -147,6 +149,8 @@ func (s *CoreSuite) TestSlotRef(c *C) { func (s *CoreSuite) TestSlotRefString(c *C) { ref := SlotRef{Snap: "snap", Name: "slot"} c.Check(ref.String(), Equals, "snap:slot") + refPtr := &SlotRef{Snap: "snap", Name: "slot"} + c.Check(refPtr.String(), Equals, "snap:slot") } // ConnRef.ID works as expected diff --git a/interfaces/mount/entry.go b/interfaces/mount/entry.go index 4afb57f776..c176dc7061 100644 --- a/interfaces/mount/entry.go +++ b/interfaces/mount/entry.go @@ -20,10 +20,7 @@ package mount import ( - "bufio" - "bytes" "fmt" - "io" "strconv" "strings" "syscall" @@ -144,54 +141,6 @@ func ParseEntry(s string) (Entry, error) { return e, nil } -// LoadFSTab reads and parses an fstab-like file. -// -// The supported format is described by fstab(5). -func LoadFSTab(reader io.Reader) ([]Entry, error) { - var entries []Entry - scanner := bufio.NewScanner(reader) - for scanner.Scan() { - s := scanner.Text() - if i := strings.IndexByte(s, '#'); i != -1 { - s = s[0:i] - } - s = strings.TrimSpace(s) - if s == "" { - continue - } - entry, err := ParseEntry(s) - if err != nil { - return nil, err - } - entries = append(entries, entry) - } - if err := scanner.Err(); err != nil { - return nil, err - } - return entries, nil -} - -// SaveFSTab writes a list of entries to a fstab-like file. -// -// The supported format is described by fstab(5). -// -// Note that there is no support for comments, both the LoadFSTab function and -// SaveFSTab just ignore them. -// -// Note that there is no attempt to use atomic file write/rename tricks. The -// created file will typically live in /run/snapd/ns/$SNAP_NAME.fstab and will -// be done so, while holidng a flock-based-lock, by the snap-update-ns program. -func SaveFSTab(writer io.Writer, entries []Entry) error { - var buf bytes.Buffer - for i := range entries { - if _, err := fmt.Fprintf(&buf, "%s\n", entries[i]); err != nil { - return err - } - } - _, err := buf.WriteTo(writer) - return err -} - // OptsToFlags converts mount options strings to a mount flag. func OptsToFlags(opts []string) (flags int, err error) { for _, opt := range opts { diff --git a/interfaces/mount/entry_test.go b/interfaces/mount/entry_test.go index 3b443ecc7e..47715d17ae 100644 --- a/interfaces/mount/entry_test.go +++ b/interfaces/mount/entry_test.go @@ -20,8 +20,6 @@ package mount_test import ( - "bytes" - "strings" "syscall" . "gopkg.in/check.v1" @@ -153,52 +151,6 @@ func (s *entrySuite) TestParseEntry6(c *C) { c.Assert(e.CheckPassNumber, Equals, 7) } -// Test that empty fstab is parsed without errors -func (s *entrySuite) TestLoadFSTab1(c *C) { - entries, err := mount.LoadFSTab(strings.NewReader("")) - c.Assert(err, IsNil) - c.Assert(entries, HasLen, 0) -} - -// Test that '#'-comments are skipped -func (s *entrySuite) TestLoadFSTab2(c *C) { - entries, err := mount.LoadFSTab(strings.NewReader("# comment")) - c.Assert(err, IsNil) - c.Assert(entries, HasLen, 0) -} - -// Test that simple profile can be loaded correctly. -func (s *entrySuite) TestLoadFSTab3(c *C) { - entries, err := mount.LoadFSTab(strings.NewReader(` - name-1 dir-1 type-1 options-1 1 1 # 1st entry - name-2 dir-2 type-2 options-2 2 2 # 2nd entry`)) - c.Assert(err, IsNil) - c.Assert(entries, HasLen, 2) - c.Assert(entries, DeepEquals, []mount.Entry{ - {"name-1", "dir-1", "type-1", []string{"options-1"}, 1, 1}, - {"name-2", "dir-2", "type-2", []string{"options-2"}, 2, 2}, - }) -} - -// Test that writing an empty fstab file works correctly. -func (s *entrySuite) TestSaveFSTab1(c *C) { - var buf bytes.Buffer - mount.SaveFSTab(&buf, nil) - c.Assert(buf.String(), Equals, "") -} - -// Test that writing an trivial fstab file works correctly. -func (s *entrySuite) TestSaveFSTab2(c *C) { - var buf bytes.Buffer - mount.SaveFSTab(&buf, []mount.Entry{ - {"name-1", "dir-1", "type-1", []string{"options-1"}, 1, 1}, - {"name-2", "dir-2", "type-2", []string{"options-2"}, 2, 2}, - }) - c.Assert(buf.String(), Equals, ("" + - "name-1 dir-1 type-1 options-1 1 1\n" + - "name-2 dir-2 type-2 options-2 2 2\n")) -} - // Test (string) options -> (int) flag conversion code. func (s *entrySuite) TestOptsToFlags(c *C) { flags, err := mount.OptsToFlags(nil) diff --git a/interfaces/mount/mountinfo.go b/interfaces/mount/mountinfo.go new file mode 100644 index 0000000000..a160f1db43 --- /dev/null +++ b/interfaces/mount/mountinfo.go @@ -0,0 +1,123 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package mount + +import ( + "fmt" + "strconv" + "strings" +) + +// InfoEntry contains data from /proc/$PID/mountinfo +// +// For details please refer to mountinfo documentation at +// https://www.kernel.org/doc/Documentation/filesystems/proc.txt +type InfoEntry struct { + MountID int + ParentID int + DevMajor int + DevMinor int + Root string + MountDir string + MountOptions map[string]string + OptionalFields []string + FsType string + MountSource string + SuperOptions map[string]string +} + +// ParseInfoEntry parses a single line of /proc/$PID/mountinfo file. +func ParseInfoEntry(s string) (*InfoEntry, error) { + var e InfoEntry + var err error + fields := strings.Fields(s) + // The format is variable-length, but at least 10 fields are mandatory. + // The (7) below is a list of optional field which is terminated with (8). + // 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue + // (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11) + if len(fields) < 10 { + return nil, fmt.Errorf("incorrect number of fields, expected at least 10 but found %d", len(fields)) + } + // Parse MountID (decimal number). + e.MountID, err = strconv.Atoi(fields[0]) + if err != nil { + return nil, fmt.Errorf("cannot parse mount ID: %q", fields[0]) + } + // Parse ParentID (decimal number). + e.ParentID, err = strconv.Atoi(fields[1]) + if err != nil { + return nil, fmt.Errorf("cannot parse parent mount ID: %q", fields[1]) + } + // Parses DevMajor:DevMinor pair (decimal numbers separated by colon). + subFields := strings.FieldsFunc(fields[2], func(r rune) bool { return r == ':' }) + if len(subFields) != 2 { + return nil, fmt.Errorf("cannot parse device major:minor number pair: %q", fields[2]) + } + e.DevMajor, err = strconv.Atoi(subFields[0]) + if err != nil { + return nil, fmt.Errorf("cannot parse device major number: %q", subFields[0]) + } + e.DevMinor, err = strconv.Atoi(subFields[1]) + if err != nil { + return nil, fmt.Errorf("cannot parse device minor number: %q", subFields[1]) + } + // NOTE: All string fields use the same escape/unescape logic as fstab files. + // Parse Root, MountDir and MountOptions fields. + e.Root = unescape(fields[3]) + e.MountDir = unescape(fields[4]) + e.MountOptions = parseMountOpts(unescape(fields[5])) + // Optional fields are terminated with a "-" value and start + // after the mount options field. Skip ahead until we see the "-" + // marker. + var i int + for i = 6; i < len(fields) && fields[i] != "-"; i++ { + } + if i == len(fields) { + return nil, fmt.Errorf("list of optional fields is not terminated properly") + } + e.OptionalFields = fields[6:i] + for j := range e.OptionalFields { + e.OptionalFields[j] = unescape(e.OptionalFields[j]) + } + // Parse the last three fixed fields. + tailFields := fields[i+1:] + if len(tailFields) != 3 { + return nil, fmt.Errorf("incorrect number of tail fields, expected 3 but found %d", len(tailFields)) + } + e.FsType = unescape(tailFields[0]) + e.MountSource = unescape(tailFields[1]) + e.SuperOptions = parseMountOpts(unescape(tailFields[2])) + return &e, nil +} + +func parseMountOpts(opts string) map[string]string { + result := make(map[string]string) + for _, opt := range strings.Split(opts, ",") { + keyValue := strings.SplitN(opt, "=", 2) + key := keyValue[0] + if len(keyValue) == 2 { + value := keyValue[1] + result[key] = value + } else { + result[key] = "" + } + } + return result +} diff --git a/interfaces/mount/mountinfo_test.go b/interfaces/mount/mountinfo_test.go new file mode 100644 index 0000000000..8873c76912 --- /dev/null +++ b/interfaces/mount/mountinfo_test.go @@ -0,0 +1,119 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package mount_test + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces/mount" +) + +type mountinfoSuite struct{} + +var _ = Suite(&mountinfoSuite{}) + +// Check that parsing the example from kernel documentation works correctly. +func (s *mountinfoSuite) TestParseInfoEntry1(c *C) { + entry, err := mount.ParseInfoEntry( + "36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue") + c.Assert(err, IsNil) + c.Assert(entry.MountID, Equals, 36) + c.Assert(entry.ParentID, Equals, 35) + c.Assert(entry.DevMajor, Equals, 98) + c.Assert(entry.DevMinor, Equals, 0) + c.Assert(entry.Root, Equals, "/mnt1") + c.Assert(entry.MountDir, Equals, "/mnt2") + c.Assert(entry.MountOptions, DeepEquals, map[string]string{"rw": "", "noatime": ""}) + c.Assert(entry.OptionalFields, DeepEquals, []string{"master:1"}) + c.Assert(entry.FsType, Equals, "ext3") + c.Assert(entry.MountSource, Equals, "/dev/root") + c.Assert(entry.SuperOptions, DeepEquals, map[string]string{"rw": "", "errors": "continue"}) +} + +// Check that various combinations of optional fields are parsed correctly. +func (s *mountinfoSuite) TestParseInfoEntry2(c *C) { + // No optional fields. + entry, err := mount.ParseInfoEntry( + "36 35 98:0 /mnt1 /mnt2 rw,noatime - ext3 /dev/root rw,errors=continue") + c.Assert(err, IsNil) + c.Assert(entry.MountOptions, DeepEquals, map[string]string{"rw": "", "noatime": ""}) + c.Assert(entry.OptionalFields, HasLen, 0) + c.Assert(entry.FsType, Equals, "ext3") + // One optional field. + entry, err = mount.ParseInfoEntry( + "36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue") + c.Assert(err, IsNil) + c.Assert(entry.MountOptions, DeepEquals, map[string]string{"rw": "", "noatime": ""}) + c.Assert(entry.OptionalFields, DeepEquals, []string{"master:1"}) + c.Assert(entry.FsType, Equals, "ext3") + // Two optional fields. + entry, err = mount.ParseInfoEntry( + "36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 slave:2 - ext3 /dev/root rw,errors=continue") + c.Assert(err, IsNil) + c.Assert(entry.MountOptions, DeepEquals, map[string]string{"rw": "", "noatime": ""}) + c.Assert(entry.OptionalFields, DeepEquals, []string{"master:1", "slave:2"}) + c.Assert(entry.FsType, Equals, "ext3") +} + +// Check that white-space is unescaped correctly. +func (s *mountinfoSuite) TestParseInfoEntry3(c *C) { + entry, err := mount.ParseInfoEntry( + `36 35 98:0 /mnt\0401 /mnt\0402 rw\040,noatime mas\040ter:1 - ext\0403 /dev/ro\040ot rw\040,errors=continue`) + c.Assert(err, IsNil) + c.Assert(entry.MountID, Equals, 36) + c.Assert(entry.ParentID, Equals, 35) + c.Assert(entry.DevMajor, Equals, 98) + c.Assert(entry.DevMinor, Equals, 0) + c.Assert(entry.Root, Equals, "/mnt 1") + c.Assert(entry.MountDir, Equals, "/mnt 2") + c.Assert(entry.MountOptions, DeepEquals, map[string]string{"rw ": "", "noatime": ""}) + // This field is still escaped as it is space-separated and needs further parsing. + c.Assert(entry.OptionalFields, DeepEquals, []string{"mas ter:1"}) + c.Assert(entry.FsType, Equals, "ext 3") + c.Assert(entry.MountSource, Equals, "/dev/ro ot") + c.Assert(entry.SuperOptions, DeepEquals, map[string]string{"rw ": "", "errors": "continue"}) +} + +// Check that various malformed entries are detected. +func (s *mountinfoSuite) TestParseInfoEntry4(c *C) { + var err error + _, err = mount.ParseInfoEntry("36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue foo") + c.Assert(err, ErrorMatches, "incorrect number of tail fields, expected 3 but found 4") + _, err = mount.ParseInfoEntry("36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root") + c.Assert(err, ErrorMatches, "incorrect number of tail fields, expected 3 but found 2") + _, err = mount.ParseInfoEntry("36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3") + c.Assert(err, ErrorMatches, "incorrect number of fields, expected at least 10 but found 9") + _, err = mount.ParseInfoEntry("36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 -") + c.Assert(err, ErrorMatches, "incorrect number of fields, expected at least 10 but found 8") + _, err = mount.ParseInfoEntry("36 35 98:0 /mnt1 /mnt2 rw,noatime master:1") + c.Assert(err, ErrorMatches, "incorrect number of fields, expected at least 10 but found 7") + _, err = mount.ParseInfoEntry("36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 garbage1 garbage2 garbage3") + c.Assert(err, ErrorMatches, "list of optional fields is not terminated properly") + _, err = mount.ParseInfoEntry("foo 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue foo") + c.Assert(err, ErrorMatches, `cannot parse mount ID: "foo"`) + _, err = mount.ParseInfoEntry("36 bar 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue foo") + c.Assert(err, ErrorMatches, `cannot parse parent mount ID: "bar"`) + _, err = mount.ParseInfoEntry("36 35 froz:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue foo") + c.Assert(err, ErrorMatches, `cannot parse device major number: "froz"`) + _, err = mount.ParseInfoEntry("36 35 98:bot /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue foo") + c.Assert(err, ErrorMatches, `cannot parse device minor number: "bot"`) + _, err = mount.ParseInfoEntry("36 35 corrupt /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue foo") + c.Assert(err, ErrorMatches, `cannot parse device major:minor number pair: "corrupt"`) +} diff --git a/interfaces/mount/profile.go b/interfaces/mount/profile.go new file mode 100644 index 0000000000..e610fda4a7 --- /dev/null +++ b/interfaces/mount/profile.go @@ -0,0 +1,105 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package mount + +import ( + "bufio" + "bytes" + "fmt" + "io" + "os" + "strings" + + "github.com/snapcore/snapd/osutil" +) + +// Profile represents an array of mount entries. +type Profile struct { + Entries []Entry +} + +// LoadProfile loads a mount profile from a given file. +// +// The file may be absent, in such case an empty profile is returned without errors. +func LoadProfile(fname string) (*Profile, error) { + f, err := os.Open(fname) + if err != nil && os.IsNotExist(err) { + return &Profile{}, nil + } + if err != nil { + return nil, err + } + defer f.Close() + return ReadProfile(f) +} + +// Save saves a mount profile (fstab-like) to a given file. +// The profile is saved with an atomic write+rename+sync operation. +func (p *Profile) Save(fname string) error { + var buf bytes.Buffer + if _, err := p.WriteTo(&buf); err != nil { + return err + } + return osutil.AtomicWriteFile(fname, buf.Bytes(), 0600, osutil.AtomicWriteFlags(0)) +} + +// ReadProfile reads and parses a mount profile. +// +// The supported format is described by fstab(5). +func ReadProfile(reader io.Reader) (*Profile, error) { + var p Profile + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + s := scanner.Text() + if i := strings.IndexByte(s, '#'); i != -1 { + s = s[0:i] + } + s = strings.TrimSpace(s) + if s == "" { + continue + } + entry, err := ParseEntry(s) + if err != nil { + return nil, err + } + p.Entries = append(p.Entries, entry) + } + if err := scanner.Err(); err != nil { + return nil, err + } + return &p, nil +} + +// WriteTo writes a mount profile to the given writer. +// +// The supported format is described by fstab(5). +// Note that there is no support for comments. +func (p *Profile) WriteTo(writer io.Writer) (int64, error) { + var written int64 + for i := range p.Entries { + var n int + var err error + if n, err = fmt.Fprintf(writer, "%s\n", p.Entries[i]); err != nil { + return written, err + } + written += int64(n) + } + return written, nil +} diff --git a/interfaces/mount/profile_test.go b/interfaces/mount/profile_test.go new file mode 100644 index 0000000000..8201d31025 --- /dev/null +++ b/interfaces/mount/profile_test.go @@ -0,0 +1,127 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package mount_test + +import ( + "bytes" + "io/ioutil" + "path/filepath" + "strings" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces/mount" +) + +type profileSuite struct{} + +var _ = Suite(&profileSuite{}) + +// Test that loading a profile from inexisting file returns an empty profile. +func (s *profileSuite) TestLoadProfile1(c *C) { + dir := c.MkDir() + p, err := mount.LoadProfile(filepath.Join(dir, "missing")) + c.Assert(err, IsNil) + c.Assert(p.Entries, HasLen, 0) +} + +// Test that loading profile from a file works as expected. +func (s *profileSuite) TestLoadProfile2(c *C) { + dir := c.MkDir() + fname := filepath.Join(dir, "existing") + err := ioutil.WriteFile(fname, []byte("name-1 dir-1 type-1 options-1 1 1 # 1st entry"), 0644) + c.Assert(err, IsNil) + p, err := mount.LoadProfile(fname) + c.Assert(err, IsNil) + c.Assert(p.Entries, HasLen, 1) + c.Assert(p.Entries, DeepEquals, []mount.Entry{ + {Name: "name-1", Dir: "dir-1", Type: "type-1", Options: []string{"options-1"}, DumpFrequency: 1, CheckPassNumber: 1}, + }) +} + +// Test that saving a profile to a file works correctly. +func (s *profileSuite) TestSaveProfile1(c *C) { + dir := c.MkDir() + fname := filepath.Join(dir, "profile") + p := &mount.Profile{ + Entries: []mount.Entry{ + {Name: "name-1", Dir: "dir-1", Type: "type-1", Options: []string{"options-1"}, DumpFrequency: 1, CheckPassNumber: 1}, + }, + } + err := p.Save(fname) + c.Assert(err, IsNil) + data, err := ioutil.ReadFile(fname) + c.Assert(err, IsNil) + c.Assert(string(data), Equals, "name-1 dir-1 type-1 options-1 1 1\n") +} + +// Test that empty fstab is parsed without errors +func (s *profileSuite) TestReadProfile1(c *C) { + p, err := mount.ReadProfile(strings.NewReader("")) + c.Assert(err, IsNil) + c.Assert(p.Entries, HasLen, 0) +} + +// Test that '#'-comments are skipped +func (s *profileSuite) TestReadProfile2(c *C) { + p, err := mount.ReadProfile(strings.NewReader("# comment")) + c.Assert(err, IsNil) + c.Assert(p.Entries, HasLen, 0) +} + +// Test that simple profile can be loaded correctly. +func (s *profileSuite) TestReadProfile3(c *C) { + p, err := mount.ReadProfile(strings.NewReader(` + name-1 dir-1 type-1 options-1 1 1 # 1st entry + name-2 dir-2 type-2 options-2 2 2 # 2nd entry`)) + c.Assert(err, IsNil) + c.Assert(p.Entries, HasLen, 2) + c.Assert(p.Entries, DeepEquals, []mount.Entry{ + {Name: "name-1", Dir: "dir-1", Type: "type-1", Options: []string{"options-1"}, DumpFrequency: 1, CheckPassNumber: 1}, + {Name: "name-2", Dir: "dir-2", Type: "type-2", Options: []string{"options-2"}, DumpFrequency: 2, CheckPassNumber: 2}, + }) +} + +// Test that writing an empty fstab file works correctly. +func (s *profileSuite) TestWriteTo1(c *C) { + p := &mount.Profile{} + var buf bytes.Buffer + n, err := p.WriteTo(&buf) + c.Assert(err, IsNil) + c.Assert(n, Equals, int64(0)) + c.Assert(buf.String(), Equals, "") +} + +// Test that writing an trivial fstab file works correctly. +func (s *profileSuite) TestWriteTo2(c *C) { + p := &mount.Profile{ + Entries: []mount.Entry{ + {Name: "name-1", Dir: "dir-1", Type: "type-1", Options: []string{"options-1"}, DumpFrequency: 1, CheckPassNumber: 1}, + {Name: "name-2", Dir: "dir-2", Type: "type-2", Options: []string{"options-2"}, DumpFrequency: 2, CheckPassNumber: 2}, + }, + } + var buf bytes.Buffer + n, err := p.WriteTo(&buf) + c.Assert(err, IsNil) + c.Assert(n, Equals, int64(68)) + c.Assert(buf.String(), Equals, ("" + + "name-1 dir-1 type-1 options-1 1 1\n" + + "name-2 dir-2 type-2 options-2 2 2\n")) +} diff --git a/interfaces/repo.go b/interfaces/repo.go index 4e5bc5238f..f6e4361f97 100644 --- a/interfaces/repo.go +++ b/interfaces/repo.go @@ -139,8 +139,10 @@ func (r *Repository) AddPlug(plug *Plug) error { r.m.Lock() defer r.m.Unlock() + snapName := plug.Snap.Name() + // Reject snaps with invalid names - if err := snap.ValidateName(plug.Snap.Name()); err != nil { + if err := snap.ValidateName(snapName); err != nil { return err } // Reject plug with invalid names @@ -155,13 +157,16 @@ func (r *Repository) AddPlug(plug *Plug) error { if err := i.SanitizePlug(plug); err != nil { return fmt.Errorf("cannot add plug: %v", err) } - if _, ok := r.plugs[plug.Snap.Name()][plug.Name]; ok { - return fmt.Errorf("cannot add plug, snap %q already has plug %q", plug.Snap.Name(), plug.Name) + if _, ok := r.plugs[snapName][plug.Name]; ok { + return fmt.Errorf("snap %q has plugs conflicting on name %q", snapName, plug.Name) + } + if _, ok := r.slots[snapName][plug.Name]; ok { + return fmt.Errorf("snap %q has plug and slot conflicting on name %q", snapName, plug.Name) } - if r.plugs[plug.Snap.Name()] == nil { - r.plugs[plug.Snap.Name()] = make(map[string]*Plug) + if r.plugs[snapName] == nil { + r.plugs[snapName] = make(map[string]*Plug) } - r.plugs[plug.Snap.Name()][plug.Name] = plug + r.plugs[snapName][plug.Name] = plug return nil } @@ -233,8 +238,10 @@ func (r *Repository) AddSlot(slot *Slot) error { r.m.Lock() defer r.m.Unlock() + snapName := slot.Snap.Name() + // Reject snaps with invalid names - if err := snap.ValidateName(slot.Snap.Name()); err != nil { + if err := snap.ValidateName(snapName); err != nil { return err } // Reject plug with invalid names @@ -249,13 +256,16 @@ func (r *Repository) AddSlot(slot *Slot) error { if err := i.SanitizeSlot(slot); err != nil { return fmt.Errorf("cannot add slot: %v", err) } - if _, ok := r.slots[slot.Snap.Name()][slot.Name]; ok { - return fmt.Errorf("cannot add slot, snap %q already has slot %q", slot.Snap.Name(), slot.Name) + if _, ok := r.slots[snapName][slot.Name]; ok { + return fmt.Errorf("snap %q has slots conflicting on name %q", snapName, slot.Name) + } + if _, ok := r.plugs[snapName][slot.Name]; ok { + return fmt.Errorf("snap %q has plug and slot conflicting on name %q", snapName, slot.Name) } - if r.slots[slot.Snap.Name()] == nil { - r.slots[slot.Snap.Name()] = make(map[string]*Slot) + if r.slots[snapName] == nil { + r.slots[snapName] = make(map[string]*Slot) } - r.slots[slot.Snap.Name()][slot.Name] = slot + r.slots[snapName][slot.Name] = slot return nil } @@ -742,6 +752,11 @@ func (e *BadInterfacesError) Error() string { // Unknown interfaces and plugs/slots that don't validate are not added. // Information about those failures are returned to the caller. func (r *Repository) AddSnap(snapInfo *snap.Info) error { + err := snap.Validate(snapInfo) + if err != nil { + return err + } + r.m.Lock() defer r.m.Unlock() @@ -762,6 +777,11 @@ func (r *Repository) AddSnap(snapInfo *snap.Info) error { bad.issues[plugName] = "unknown interface" continue } + // Reject plug with invalid name + if err := ValidateName(plugName); err != nil { + bad.issues[plugName] = err.Error() + continue + } plug := &Plug{PlugInfo: plugInfo} if err := iface.SanitizePlug(plug); err != nil { bad.issues[plugName] = err.Error() @@ -779,6 +799,11 @@ func (r *Repository) AddSnap(snapInfo *snap.Info) error { bad.issues[slotName] = "unknown interface" continue } + // Reject slot with invalid name + if err := ValidateName(slotName); err != nil { + bad.issues[slotName] = err.Error() + continue + } slot := &Slot{SlotInfo: slotInfo} if err := iface.SanitizeSlot(slot); err != nil { bad.issues[slotName] = err.Error() diff --git a/interfaces/repo_test.go b/interfaces/repo_test.go index b2b5615845..c931c25b0c 100644 --- a/interfaces/repo_test.go +++ b/interfaces/repo_test.go @@ -190,15 +190,35 @@ func (s *RepositorySuite) TestAddPlug(c *C) { c.Assert(s.testRepo.Plug(s.plug.Snap.Name(), s.plug.Name), DeepEquals, s.plug) } -func (s *RepositorySuite) TestAddPlugClash(c *C) { +func (s *RepositorySuite) TestAddPlugClashingPlug(c *C) { err := s.testRepo.AddPlug(s.plug) c.Assert(err, IsNil) err = s.testRepo.AddPlug(s.plug) - c.Assert(err, ErrorMatches, `cannot add plug, snap "consumer" already has plug "plug"`) + c.Assert(err, ErrorMatches, `snap "consumer" has plugs conflicting on name "plug"`) c.Assert(s.testRepo.AllPlugs(""), HasLen, 1) c.Assert(s.testRepo.Plug(s.plug.Snap.Name(), s.plug.Name), DeepEquals, s.plug) } +func (s *RepositorySuite) TestAddPlugClashingSlot(c *C) { + snapInfo := &snap.Info{SuggestedName: "snap"} + plug := &Plug{PlugInfo: &snap.PlugInfo{ + Snap: snapInfo, + Name: "clashing", + Interface: "interface", + }} + slot := &Slot{SlotInfo: &snap.SlotInfo{ + Snap: snapInfo, + Name: "clashing", + Interface: "interface", + }} + err := s.testRepo.AddSlot(slot) + c.Assert(err, IsNil) + err = s.testRepo.AddPlug(plug) + c.Assert(err, ErrorMatches, `snap "snap" has plug and slot conflicting on name "clashing"`) + c.Assert(s.testRepo.AllSlots(""), HasLen, 1) + c.Assert(s.testRepo.Slot(slot.Snap.Name(), slot.Name), DeepEquals, slot) +} + func (s *RepositorySuite) TestAddPlugFailsWithInvalidSnapName(c *C) { plug := &Plug{ PlugInfo: &snap.PlugInfo{ @@ -477,13 +497,33 @@ func (s *RepositorySuite) TestAddSlotFailsWithInvalidSnapName(c *C) { c.Assert(s.emptyRepo.AllSlots(""), HasLen, 0) } -func (s *RepositorySuite) TestAddSlotFailsForDuplicates(c *C) { +func (s *RepositorySuite) TestAddSlotClashingSlot(c *C) { // Adding the first slot succeeds err := s.testRepo.AddSlot(s.slot) c.Assert(err, IsNil) // Adding the slot again fails with appropriate error err = s.testRepo.AddSlot(s.slot) - c.Assert(err, ErrorMatches, `cannot add slot, snap "producer" already has slot "slot"`) + c.Assert(err, ErrorMatches, `snap "producer" has slots conflicting on name "slot"`) +} + +func (s *RepositorySuite) TestAddSlotClashingPlug(c *C) { + snapInfo := &snap.Info{SuggestedName: "snap"} + plug := &Plug{PlugInfo: &snap.PlugInfo{ + Snap: snapInfo, + Name: "clashing", + Interface: "interface", + }} + slot := &Slot{SlotInfo: &snap.SlotInfo{ + Snap: snapInfo, + Name: "clashing", + Interface: "interface", + }} + err := s.testRepo.AddPlug(plug) + c.Assert(err, IsNil) + err = s.testRepo.AddSlot(slot) + c.Assert(err, ErrorMatches, `snap "snap" has plug and slot conflicting on name "clashing"`) + c.Assert(s.testRepo.AllPlugs(""), HasLen, 1) + c.Assert(s.testRepo.Plug(plug.Snap.Name(), plug.Name), DeepEquals, plug) } func (s *RepositorySuite) TestAddSlotFailsWithUnsanitizedSlot(c *C) { @@ -947,7 +987,7 @@ func (s *RepositorySuite) TestResolveDisconnectMatrixTypical(c *C) { c.Assert(s.testRepo.AddSnap(s.coreSnap), IsNil) c.Assert(s.testRepo.AddPlug(s.plug), IsNil) c.Assert(s.testRepo.AddSlot(s.slot), IsNil) - connRef := ConnRef{s.plug.Ref(), s.slot.Ref()} + connRef := ConnRef{PlugRef: s.plug.Ref(), SlotRef: s.slot.Ref()} c.Assert(s.testRepo.Connect(connRef), IsNil) scenarios := []struct { @@ -1178,13 +1218,13 @@ func (s *RepositorySuite) TestConnectedFindsConnections(c *C) { conns, err := s.testRepo.Connected(s.plug.Snap.Name(), s.plug.Name) c.Assert(err, IsNil) c.Check(conns, DeepEquals, []ConnRef{ - {s.plug.Ref(), s.slot.Ref()}, + {PlugRef: s.plug.Ref(), SlotRef: s.slot.Ref()}, }) conns, err = s.testRepo.Connected(s.slot.Snap.Name(), s.slot.Name) c.Assert(err, IsNil) c.Check(conns, DeepEquals, []ConnRef{ - {s.plug.Ref(), s.slot.Ref()}, + {PlugRef: s.plug.Ref(), SlotRef: s.slot.Ref()}, }) } @@ -1204,7 +1244,7 @@ func (s *RepositorySuite) TestConnectedFindsCoreSnap(c *C) { conns, err := s.testRepo.Connected("", s.slot.Name) c.Assert(err, IsNil) c.Check(conns, DeepEquals, []ConnRef{ - {s.plug.Ref(), slot.Ref()}, + {PlugRef: s.plug.Ref(), SlotRef: slot.Ref()}, }) } @@ -1215,7 +1255,7 @@ func (s *RepositorySuite) TestDisconnectAll(c *C) { c.Assert(s.testRepo.AddSlot(s.slot), IsNil) c.Assert(s.testRepo.Connect(ConnRef{PlugRef: s.plug.Ref(), SlotRef: s.slot.Ref()}), IsNil) - conns := []ConnRef{{s.plug.Ref(), s.slot.Ref()}} + conns := []ConnRef{{PlugRef: s.plug.Ref(), SlotRef: s.slot.Ref()}} s.testRepo.DisconnectAll(conns) c.Assert(s.testRepo.Interfaces(), DeepEquals, &Interfaces{ Plugs: []*Plug{{PlugInfo: s.plug.PlugInfo}}, @@ -1485,6 +1525,26 @@ apps: slots: [iface] ` +const testConsumerInvalidSlotNameYaml = ` +name: consumer +slots: + ttyS5: + interface: iface +apps: + app: + slots: [iface] +` + +const testConsumerInvalidPlugNameYaml = ` +name: consumer +plugs: + ttyS3: + interface: iface +apps: + app: + plugs: [iface] +` + func (s *AddRemoveSuite) addSnap(c *C, yaml string) (*snap.Info, error) { snapInfo := snaptest.MockInfo(c, yaml, nil) return snapInfo, s.repo.AddSnap(snapInfo) @@ -1497,6 +1557,18 @@ func (s *AddRemoveSuite) TestAddSnapAddsPlugs(c *C) { c.Assert(s.repo.Plug("consumer", "iface"), Not(IsNil)) } +func (s *AddRemoveSuite) TestAddSnapErrorsOnInvalidSlotNames(c *C) { + _, err := s.addSnap(c, testConsumerInvalidSlotNameYaml) + c.Assert(err, NotNil) + c.Check(err, ErrorMatches, `snap "consumer" has bad plugs or slots: ttyS5 \(invalid interface name: "ttyS5"\)`) +} + +func (s *AddRemoveSuite) TestAddSnapErrorsOnInvalidPlugNames(c *C) { + _, err := s.addSnap(c, testConsumerInvalidPlugNameYaml) + c.Assert(err, NotNil) + c.Check(err, ErrorMatches, `snap "consumer" has bad plugs or slots: ttyS3 \(invalid interface name: "ttyS3"\)`) +} + func (s *AddRemoveSuite) TestAddSnapErrorsOnExistingSnapPlugs(c *C) { _, err := s.addSnap(c, testConsumerYaml) c.Assert(err, IsNil) diff --git a/osutil/buildid_test.go b/osutil/buildid_test.go index b11a2e7aa8..5df65fa5c4 100644 --- a/osutil/buildid_test.go +++ b/osutil/buildid_test.go @@ -34,6 +34,10 @@ type buildIDSuite struct{} var _ = Suite(&buildIDSuite{}) +var truePath = osutil.LookPathDefault("true", "/bin/true") +var falsePath = osutil.LookPathDefault("false", "/bin/false") +var gccPath = osutil.LookPathDefault("gcc", "/usr/bin/gcc") + func buildID(c *C, fname string) string { output, err := exec.Command("file", fname).CombinedOutput() c.Assert(err, IsNil) @@ -46,7 +50,7 @@ func buildID(c *C, fname string) string { } func (s *buildIDSuite) TestReadBuildID(c *C) { - for _, fname := range []string{"/bin/true", "/bin/false"} { + for _, fname := range []string{truePath, falsePath} { id, err := osutil.ReadBuildID(fname) c.Assert(err, IsNil) @@ -56,7 +60,7 @@ func (s *buildIDSuite) TestReadBuildID(c *C) { func (s *buildIDSuite) TestReadBuildIDNoID(c *C) { stripedTruth := filepath.Join(c.MkDir(), "true") - osutil.CopyFile("/bin/true", stripedTruth, 0) + osutil.CopyFile(truePath, stripedTruth, 0) output, err := exec.Command("strip", "-R", ".note.gnu.build-id", stripedTruth).CombinedOutput() c.Assert(string(output), Equals, "") c.Assert(err, IsNil) @@ -67,14 +71,14 @@ func (s *buildIDSuite) TestReadBuildIDNoID(c *C) { } func (s *buildIDSuite) TestReadBuildIDmd5(c *C) { - if !osutil.FileExists("/usr/bin/gcc") { + if !osutil.FileExists(gccPath) { c.Skip("No gcc found") } md5Truth := filepath.Join(c.MkDir(), "true") err := ioutil.WriteFile(md5Truth+".c", []byte(`int main(){return 0;}`), 0644) c.Assert(err, IsNil) - output, err := exec.Command("gcc", "-Wl,-build-id=md5", "-xc", md5Truth+".c", "-o", md5Truth).CombinedOutput() + output, err := exec.Command(gccPath, "-Wl,-build-id=md5", "-xc", md5Truth+".c", "-o", md5Truth).CombinedOutput() c.Assert(string(output), Equals, "") c.Assert(err, IsNil) diff --git a/osutil/exec_test.go b/osutil/exec_test.go index 84f67f87aa..c0e725a3ef 100644 --- a/osutil/exec_test.go +++ b/osutil/exec_test.go @@ -71,11 +71,11 @@ func (s *commandFromCoreSuite) TestCommandFromCore(c *C) { root := filepath.Join(dirs.SnapMountDir, "/core/current") os.MkdirAll(filepath.Join(root, "/usr/bin"), 0755) - osutil.CopyFile("/bin/true", filepath.Join(root, "/usr/bin/xdelta3"), 0) + osutil.CopyFile(truePath, filepath.Join(root, "/usr/bin/xdelta3"), 0) cmd, err := osutil.CommandFromCore("/usr/bin/xdelta3", "--some-xdelta-arg") c.Assert(err, IsNil) - out, err := exec.Command("/bin/sh", "-c", "readelf -l /bin/true |grep interpreter:|cut -f2 -d:|cut -f1 -d]").Output() + out, err := exec.Command("/bin/sh", "-c", fmt.Sprintf("readelf -l %s |grep interpreter:|cut -f2 -d:|cut -f1 -d]", truePath)).Output() c.Assert(err, IsNil) interp := strings.TrimSpace(string(out)) @@ -93,7 +93,7 @@ func (s *commandFromCoreSuite) TestCommandFromCoreSymlinkCycle(c *C) { root := filepath.Join(dirs.SnapMountDir, "/core/current") os.MkdirAll(filepath.Join(root, "/usr/bin"), 0755) - osutil.CopyFile("/bin/true", filepath.Join(root, "/usr/bin/xdelta3"), 0) + osutil.CopyFile(truePath, filepath.Join(root, "/usr/bin/xdelta3"), 0) out, err := exec.Command("/bin/sh", "-c", "readelf -l /bin/true |grep interpreter:|cut -f2 -d:|cut -f1 -d]").Output() c.Assert(err, IsNil) diff --git a/osutil/stat.go b/osutil/stat.go index d7478c39e5..3ccd5fa723 100644 --- a/osutil/stat.go +++ b/osutil/stat.go @@ -63,3 +63,16 @@ func ExecutableExists(name string) bool { return err == nil } + +var lookPath func(name string) (string, error) = exec.LookPath + +// LookPathDefault searches for a given command name in all directories +// listed in the environment variable PATH and returns the found path or the +// provided default path. +func LookPathDefault(name string, defaultPath string) string { + p, err := lookPath(name) + if err != nil { + return defaultPath + } + return p +} diff --git a/osutil/stat_test.go b/osutil/stat_test.go index bb265c8822..2d0420491f 100644 --- a/osutil/stat_test.go +++ b/osutil/stat_test.go @@ -20,6 +20,7 @@ package osutil import ( + "fmt" "io/ioutil" "os" "path/filepath" @@ -89,3 +90,13 @@ func (ts *StatTestSuite) TestExecutableExists(c *C) { c.Assert(os.Chmod(fname, 0755), IsNil) c.Check(ExecutableExists("xyzzy"), Equals, true) } + +func (s *StatTestSuite) TestLookPathDefaultGivesCorrectPath(c *C) { + lookPath = func(name string) (string, error) { return "/bin/true", nil } + c.Assert(LookPathDefault("true", "/bin/foo"), Equals, "/bin/true") +} + +func (s *StatTestSuite) TestLookPathDefaultReturnsDefaultWhenNotFound(c *C) { + lookPath = func(name string) (string, error) { return "", fmt.Errorf("Not found") } + c.Assert(LookPathDefault("bar", "/bin/bla"), Equals, "/bin/bla") +} diff --git a/overlord/devicestate/crypto.go b/overlord/devicestate/crypto.go new file mode 100644 index 0000000000..0c2328a53d --- /dev/null +++ b/overlord/devicestate/crypto.go @@ -0,0 +1,80 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +/* + * Copyright (C) 2016-2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package devicestate + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strconv" + + "github.com/snapcore/snapd/osutil" +) + +func generateRSAKey(keyLength int) (*rsa.PrivateKey, error) { + // The temporary directory is created with mode + // 0700 by ioutil.TempDir, see: + // https://github.com/golang/go/blob/master/src/io/ioutil/tempfile.go#L84 + tempDir, err := ioutil.TempDir(os.TempDir(), "snapd") + if err != nil { + return nil, err + } + + defer os.RemoveAll(tempDir) + + rsaKeyFile := filepath.Join(tempDir, "rsa.key") + + cmd := exec.Command("ssh-keygen", "-t", "rsa", "-b", strconv.Itoa(keyLength), "-N", "", "-f", rsaKeyFile) + out, err := cmd.CombinedOutput() + if err != nil { + return nil, osutil.OutputErr(out, err) + } + + d, err := ioutil.ReadFile(rsaKeyFile) + if err != nil { + return nil, err + } + + blk, _ := pem.Decode(d) + if blk == nil { + return nil, errors.New("cannot decode PEM block") + } + + key, err := x509.ParsePKCS1PrivateKey(blk.Bytes) + if err != nil { + return nil, err + } + + err = key.Validate() + if err != nil { + return nil, err + } + + err = os.RemoveAll(tempDir) + if err != nil { + return nil, err + } + + return key, err +} diff --git a/overlord/devicestate/devicestate_test.go b/overlord/devicestate/devicestate_test.go index 76a17af137..0565b0fd49 100644 --- a/overlord/devicestate/devicestate_test.go +++ b/overlord/devicestate/devicestate_test.go @@ -26,6 +26,7 @@ import ( "io/ioutil" "net/http" "net/http/httptest" + "os" "sync" "testing" "time" @@ -72,6 +73,7 @@ type deviceMgrSuite struct { } var _ = Suite(&deviceMgrSuite{}) +var testKeyLength = 1024 type fakeStore struct { state *state.State @@ -129,10 +131,11 @@ func (sto *fakeStore) Sections(*auth.UserState) ([]string, error) { func (s *deviceMgrSuite) SetUpTest(c *C) { dirs.SetRootDir(c.MkDir()) + os.MkdirAll(dirs.SnapRunDir, 0755) s.restoreOnClassic = release.MockOnClassic(false) - rootPrivKey, _ := assertstest.GenerateKey(1024) + rootPrivKey, _ := assertstest.GenerateKey(testKeyLength) storePrivKey, _ := assertstest.GenerateKey(752) s.storeSigning = assertstest.NewStoreStack("canonical", rootPrivKey, storePrivKey) s.state = state.New(nil) @@ -289,7 +292,7 @@ func (s *deviceMgrSuite) setupCore(c *C, name, snapYaml string, snapContents str } func (s *deviceMgrSuite) TestFullDeviceRegistrationHappy(c *C) { - r1 := devicestate.MockKeyLength(752) + r1 := devicestate.MockKeyLength(testKeyLength) defer r1() s.reqID = "REQID-1" @@ -361,7 +364,7 @@ version: gadget } func (s *deviceMgrSuite) TestDoRequestSerialIdempotentAfterAddSerial(c *C) { - privKey, _ := assertstest.GenerateKey(1024) + privKey, _ := assertstest.GenerateKey(testKeyLength) s.reqID = "REQID-1" mockServer := s.mockServer(c) @@ -430,7 +433,7 @@ version: gadget } func (s *deviceMgrSuite) TestDoRequestSerialIdempotentAfterGotSerial(c *C) { - privKey, _ := assertstest.GenerateKey(1024) + privKey, _ := assertstest.GenerateKey(testKeyLength) s.reqID = "REQID-1" mockServer := s.mockServer(c) @@ -499,7 +502,7 @@ version: gadget } func (s *deviceMgrSuite) TestFullDeviceRegistrationPollHappy(c *C) { - r1 := devicestate.MockKeyLength(752) + r1 := devicestate.MockKeyLength(testKeyLength) defer r1() s.reqID = "REQID-POLL" @@ -575,7 +578,7 @@ version: gadget } func (s *deviceMgrSuite) TestFullDeviceRegistrationHappyPrepareDeviceHook(c *C) { - r1 := devicestate.MockKeyLength(752) + r1 := devicestate.MockKeyLength(testKeyLength) defer r1() s.reqID = "REQID-1" @@ -677,7 +680,7 @@ hooks: } func (s *deviceMgrSuite) TestFullDeviceRegistrationErrorBackoff(c *C) { - r1 := devicestate.MockKeyLength(752) + r1 := devicestate.MockKeyLength(testKeyLength) defer r1() s.reqID = "REQID-BADREQ" @@ -882,7 +885,7 @@ func (s *deviceMgrSuite) TestDeviceAssertionsDeviceSessionRequest(c *C) { // setup state as done by device initialisation s.state.Lock() - devKey, _ := assertstest.GenerateKey(1024) + devKey, _ := assertstest.GenerateKey(testKeyLength) encDevKey, err := asserts.EncodePublicKey(devKey.PublicKey()) c.Check(err, IsNil) seriala, err := s.storeSigning.Sign(asserts.SerialType, map[string]interface{}{ diff --git a/overlord/devicestate/export_test.go b/overlord/devicestate/export_test.go index 9e721ee006..f539eed406 100644 --- a/overlord/devicestate/export_test.go +++ b/overlord/devicestate/export_test.go @@ -27,6 +27,10 @@ import ( ) func MockKeyLength(n int) (restore func()) { + if n < 1024 { + panic("key length must be >= 1024") + } + oldKeyLength := keyLength keyLength = n return func() { diff --git a/overlord/devicestate/handlers.go b/overlord/devicestate/handlers.go index 82f9841edc..320379b5e9 100644 --- a/overlord/devicestate/handlers.go +++ b/overlord/devicestate/handlers.go @@ -1,5 +1,4 @@ // -*- Mode: Go; indent-tabs-mode: t -*- - /* * Copyright (C) 2016-2017 Canonical Ltd * @@ -21,8 +20,6 @@ package devicestate import ( "bytes" - "crypto/rand" - "crypto/rsa" "encoding/json" "errors" "fmt" @@ -85,7 +82,9 @@ func (m *DeviceManager) doGenerateDeviceKey(t *state.Task, _ *tomb.Tomb) error { return nil } - keyPair, err := rsa.GenerateKey(rand.Reader, keyLength) + st.Unlock() + keyPair, err := generateRSAKey(keyLength) + st.Lock() if err != nil { return fmt.Errorf("cannot generate device key pair: %v", err) } diff --git a/overlord/ifacestate/helpers.go b/overlord/ifacestate/helpers.go index c20a83e1c5..152a026a94 100644 --- a/overlord/ifacestate/helpers.go +++ b/overlord/ifacestate/helpers.go @@ -21,6 +21,7 @@ package ifacestate import ( "fmt" + "strings" "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/interfaces" @@ -47,6 +48,9 @@ func (m *InterfaceManager) initialize(extraInterfaces []interfaces.Interface, ex if err := m.addSnaps(); err != nil { return err } + if err := m.renameCorePlugConnection(); err != nil { + return err + } if err := m.reloadConnections(""); err != nil { return err } @@ -153,6 +157,35 @@ func (m *InterfaceManager) regenerateAllSecurityProfiles() error { return nil } +// renameCorePlugConnection renames one connection from "core-support" plug to +// slot so that the plug name is "core-support-plug" while the slot is +// unchanged. This matches a change introduced in 2.24, where the core snap no +// longer has the "core-support" plug as that was clashing with the slot with +// the same name. +func (m *InterfaceManager) renameCorePlugConnection() error { + conns, err := getConns(m.state) + if err != nil { + return err + } + const oldPlugName = "core-support" + const newPlugName = "core-support-plug" + // old connection, note that slotRef is the same in both + slotRef := interfaces.SlotRef{Snap: "core", Name: oldPlugName} + oldPlugRef := interfaces.PlugRef{Snap: "core", Name: oldPlugName} + oldConnRef := interfaces.ConnRef{PlugRef: oldPlugRef, SlotRef: slotRef} + oldID := oldConnRef.ID() + // if the old connection is saved, replace it with the new connection + if cState, ok := conns[oldID]; ok { + newPlugRef := interfaces.PlugRef{Snap: "core", Name: newPlugName} + newConnRef := interfaces.ConnRef{PlugRef: newPlugRef, SlotRef: slotRef} + newID := newConnRef.ID() + delete(conns, oldID) + conns[newID] = cState + setConns(m.state, conns) + } + return nil +} + // reloadConnections reloads connections stored in the state in the repository. // Using non-empty snapName the operation can be scoped to connections // affecting a given snap. @@ -300,7 +333,27 @@ func (m *InterfaceManager) autoConnect(task *state.Task, snapName string, blackl continue } candidates := m.repo.AutoConnectCandidateSlots(snapName, plug.Name, autochecker.check) + if len(candidates) == 0 { + continue + } + // If we are in a core transition we may have both the old ubuntu-core + // snap and the new core snap providing the same interface. In that + // situation we want to ignore any candidates in ubuntu-core and simply + // go with those from the new core snap. + if len(candidates) == 2 { + switch { + case candidates[0].Snap.Name() == "ubuntu-core" && candidates[1].Snap.Name() == "core": + candidates = candidates[1:2] + case candidates[1].Snap.Name() == "ubuntu-core" && candidates[0].Snap.Name() == "core": + candidates = candidates[0:1] + } + } if len(candidates) != 1 { + crefs := make([]string, 0, len(candidates)) + for _, candidate := range candidates { + crefs = append(crefs, candidate.Ref().String()) + } + task.Logf("cannot auto connect %s (plug auto-connection), candidates found: %q", plug.Ref(), strings.Join(crefs, ", ")) continue } slot := candidates[0] @@ -308,6 +361,7 @@ func (m *InterfaceManager) autoConnect(task *state.Task, snapName string, blackl key := connRef.ID() if _, ok := conns[key]; ok { // Suggested connection already exist so don't clobber it. + task.Logf("cannot auto connect %s to %s: (plug auto-connection), existing connection state %q in the way", connRef.PlugRef, connRef.SlotRef, key) continue } if err := m.repo.Connect(connRef); err != nil { @@ -324,7 +378,15 @@ func (m *InterfaceManager) autoConnect(task *state.Task, snapName string, blackl continue } candidates := m.repo.AutoConnectCandidatePlugs(snapName, slot.Name, autochecker.check) + if len(candidates) == 0 { + continue + } if len(candidates) != 1 { + crefs := make([]string, 0, len(candidates)) + for _, candidate := range candidates { + crefs = append(crefs, candidate.Ref().String()) + } + task.Logf("cannot auto connect %s (slot auto-connection), candidates found: %q", slot.Ref(), strings.Join(crefs, ", ")) continue } plug := candidates[0] @@ -332,6 +394,7 @@ func (m *InterfaceManager) autoConnect(task *state.Task, snapName string, blackl key := connRef.ID() if _, ok := conns[key]; ok { // Suggested connection already exist so don't clobber it. + task.Logf("cannot auto connect %s to %s: (slot auto-connection), existing connection state %q in the way", connRef.PlugRef, connRef.SlotRef, key) continue } if err := m.repo.Connect(connRef); err != nil { diff --git a/overlord/ifacestate/ifacestate_test.go b/overlord/ifacestate/ifacestate_test.go index 57f985709d..53c18e82ff 100644 --- a/overlord/ifacestate/ifacestate_test.go +++ b/overlord/ifacestate/ifacestate_test.go @@ -667,12 +667,18 @@ func (s *interfaceManagerSuite) addDiscardConnsChange(c *C, snapName string) *st return change } -var osSnapYaml = ` +var ubuntuCoreSnapYaml = ` name: ubuntu-core version: 1 type: os ` +var coreSnapYaml = ` +name: core +version: 1 +type: os +` + var sampleSnapYaml = ` name: snap version: 1 @@ -704,13 +710,20 @@ slots: attr2: value2 ` +var httpdSnapYaml = `name: httpd +version: 1 +plugs: + network: + interface: network +` + // The setup-profiles task will not auto-connect an plug that was previously // explicitly disconnected by the user. func (s *interfaceManagerSuite) TestDoSetupSnapSecurityHonorsDisconnect(c *C) { c.Skip("feature disabled until redesign/reimpl") // Add an OS snap as well as a sample snap with a "network" plug. // The plug is normally auto-connected. - s.mockSnap(c, osSnapYaml) + s.mockSnap(c, ubuntuCoreSnapYaml) snapInfo := s.mockSnap(c, sampleSnapYaml) // Initialize the manager. This registers the two snaps. @@ -749,7 +762,7 @@ func (s *interfaceManagerSuite) TestDoSetupSnapSecurityHonorsDisconnect(c *C) { // The setup-profiles task will auto-connect plugs with viable candidates. func (s *interfaceManagerSuite) TestDoSetupSnapSecurityAutoConnectsPlugs(c *C) { // Add an OS snap. - s.mockSnap(c, osSnapYaml) + s.mockSnap(c, ubuntuCoreSnapYaml) // Initialize the manager. This registers the OS snap. mgr := s.manager(c) @@ -796,7 +809,7 @@ func (s *interfaceManagerSuite) TestDoSetupSnapSecurityAutoConnectsSlots(c *C) { // Mock the interface that will be used by the test s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"}) // Add an OS snap. - s.mockSnap(c, osSnapYaml) + s.mockSnap(c, ubuntuCoreSnapYaml) // Add a consumer snap with unconnect plug (interface "test") s.mockSnap(c, consumerYaml) @@ -920,7 +933,7 @@ slots: // operates on or auto-connects to and will leave other state intact. func (s *interfaceManagerSuite) TestDoSetupSnapSecuirtyKeepsExistingConnectionState(c *C) { // Add an OS snap in place. - s.mockSnap(c, osSnapYaml) + s.mockSnap(c, ubuntuCoreSnapYaml) // Initialize the manager. This registers the two snaps. mgr := s.manager(c) @@ -976,7 +989,7 @@ func (s *interfaceManagerSuite) TestDoSetupProfilesAddsImplicitSlots(c *C) { mgr := s.manager(c) // Add an OS snap. - snapInfo := s.mockSnap(c, osSnapYaml) + snapInfo := s.mockSnap(c, ubuntuCoreSnapYaml) // Run the setup-profiles task and let it finish. change := s.addSetupSnapSecurityChange(c, &snapstate.SnapSetup{ @@ -1096,7 +1109,7 @@ func (s *interfaceManagerSuite) TestSetupProfilesHonorsDevMode(c *C) { // of the affected set. func (s *interfaceManagerSuite) TestSetupProfilesUsesFreshSnapInfo(c *C) { // Put the OS and the sample snaps in place. - coreSnapInfo := s.mockSnap(c, osSnapYaml) + coreSnapInfo := s.mockSnap(c, ubuntuCoreSnapYaml) oldSnapInfo := s.mockSnap(c, sampleSnapYaml) // Put connection information between the OS snap and the sample snap. @@ -1151,7 +1164,7 @@ func (s *interfaceManagerSuite) TestSetupProfilesUsesFreshSnapInfo(c *C) { // setup-profiles needs to setup security for connected slots after autoconnection func (s *interfaceManagerSuite) TestAutoConnectSetupSecurityForConnectedSlots(c *C) { // Add an OS snap. - coreSnapInfo := s.mockSnap(c, osSnapYaml) + coreSnapInfo := s.mockSnap(c, ubuntuCoreSnapYaml) // Initialize the manager. This registers the OS snap. mgr := s.manager(c) @@ -1646,7 +1659,7 @@ slots: } func (s *interfaceManagerSuite) TestCheckInterfacesConsidersImplicitSlots(c *C) { - snapInfo := s.mockSnap(c, osSnapYaml) + snapInfo := s.mockSnap(c, ubuntuCoreSnapYaml) s.state.Lock() defer s.state.Unlock() @@ -1740,26 +1753,9 @@ func (s *interfaceManagerSuite) TestUndoSetupProfilesOnRefresh(c *C) { c.Check(s.secBackend.SetupCalls[0].Options, Equals, interfaces.ConfinementOptions{}) } -var ubuntuCoreYaml = `name: ubuntu-core -version: 1 -type: os -` - -var coreYaml = `name: ubuntu-core -version: 1 -type: os -` - -var httpdSnapYaml = `name: httpd -version: 1 -plugs: - network: - interface: network -` - func (s *interfaceManagerSuite) TestManagerTransitionConnectionsCore(c *C) { - s.mockSnap(c, ubuntuCoreYaml) - s.mockSnap(c, coreYaml) + s.mockSnap(c, ubuntuCoreSnapYaml) + s.mockSnap(c, coreSnapYaml) s.mockSnap(c, httpdSnapYaml) mgr := s.manager(c) @@ -1797,8 +1793,8 @@ func (s *interfaceManagerSuite) TestManagerTransitionConnectionsCore(c *C) { } func (s *interfaceManagerSuite) TestManagerTransitionConnectionsCoreUndo(c *C) { - s.mockSnap(c, ubuntuCoreYaml) - s.mockSnap(c, coreYaml) + s.mockSnap(c, ubuntuCoreSnapYaml) + s.mockSnap(c, coreSnapYaml) s.mockSnap(c, httpdSnapYaml) mgr := s.manager(c) @@ -1841,3 +1837,109 @@ func (s *interfaceManagerSuite) TestManagerTransitionConnectionsCoreUndo(c *C) { }, }) } + +// Test "core-support" connections that loop back to core is +// renamed to match the rename of the plug. +func (s *interfaceManagerSuite) TestCoreConnectionsRenamed(c *C) { + // Put state with old connection data. + s.state.Lock() + s.state.Set("conns", map[string]interface{}{ + "core:core-support core:core-support": map[string]interface{}{ + "interface": "core-support", "auto": true, + }, + "snap:unrelated core:unrelated": map[string]interface{}{ + "interface": "unrelated", "auto": true, + }, + }) + s.state.Unlock() + + // Start the manager, this is where renames happen. + s.manager(c) + + // Check that "core-support" connection got renamed. + s.state.Lock() + var conns map[string]interface{} + err := s.state.Get("conns", &conns) + s.state.Unlock() + c.Assert(err, IsNil) + c.Assert(conns, DeepEquals, map[string]interface{}{ + "core:core-support-plug core:core-support": map[string]interface{}{ + "interface": "core-support", "auto": true, + }, + "snap:unrelated core:unrelated": map[string]interface{}{ + "interface": "unrelated", "auto": true, + }, + }) +} + +// Test that "network-bind" and "core-support" plugs are renamed to +// "network-bind-plug" and "core-support-plug" in order not to clash with slots +// with the same names. +func (s *interfaceManagerSuite) TestAutomaticCorePlugsRenamed(c *C) { + s.mockSnap(c, coreSnapYaml+` +plugs: + network-bind: + core-support: +`) + mgr := s.manager(c) + + // old plugs are gone + c.Assert(mgr.Repository().Plug("core", "network-bind"), IsNil) + c.Assert(mgr.Repository().Plug("core", "core-support"), IsNil) + // new plugs are present + c.Assert(mgr.Repository().Plug("core", "network-bind-plug"), Not(IsNil)) + c.Assert(mgr.Repository().Plug("core", "core-support-plug"), Not(IsNil)) + // slots are present and unchanged + c.Assert(mgr.Repository().Slot("core", "network-bind"), Not(IsNil)) + c.Assert(mgr.Repository().Slot("core", "core-support"), Not(IsNil)) +} + +func (s *interfaceManagerSuite) TestAutoConnectDuringCoreTransition(c *C) { + // Add both the old and new core snaps + s.mockSnap(c, ubuntuCoreSnapYaml) + s.mockSnap(c, coreSnapYaml) + + // Initialize the manager. This registers both of the core snaps. + mgr := s.manager(c) + + // Add a sample snap with a "network" plug which should be auto-connected. + // Normally it would not be auto connected because there are multiple + // provides but we have special support for this case so the old + // ubuntu-core snap is ignored and we pick the new core snap. + snapInfo := s.mockSnap(c, sampleSnapYaml) + + // Run the setup-snap-security task and let it finish. + change := s.addSetupSnapSecurityChange(c, &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{ + RealName: snapInfo.Name(), + Revision: snapInfo.Revision, + }, + }) + mgr.Ensure() + mgr.Wait() + mgr.Stop() + + s.state.Lock() + defer s.state.Unlock() + + // Ensure that the task succeeded. + c.Assert(change.Status(), Equals, state.DoneStatus) + + // Ensure that "network" is now saved in the state as auto-connected and + // that it is connected to the new core snap rather than the old + // ubuntu-core snap. + var conns map[string]interface{} + err := s.state.Get("conns", &conns) + c.Assert(err, IsNil) + c.Check(conns, DeepEquals, map[string]interface{}{ + "snap:network core:network": map[string]interface{}{ + "interface": "network", "auto": true, + }, + }) + + // Ensure that "network" is really connected. + repo := mgr.Repository() + plug := repo.Plug("snap", "network") + c.Assert(plug, Not(IsNil)) + c.Check(plug.Connections, HasLen, 1) +} diff --git a/overlord/managers_test.go b/overlord/managers_test.go index cad2af5fdd..8850046c57 100644 --- a/overlord/managers_test.go +++ b/overlord/managers_test.go @@ -1041,141 +1041,147 @@ apps: } func (ms *mgrsSuite) TestHappyAlias(c *C) { - st := ms.o.State() - st.Lock() - defer st.Unlock() - - fooYaml := `name: foo -version: 1.0 -apps: - foo: - command: bin/foo - aliases: [foo_] - bar: - command: bin/bar - aliases: [bar,bar1] -` - ms.installLocalTestSnap(c, fooYaml) - - ts, err := snapstate.Alias(st, "foo", []string{"foo_", "bar", "bar1"}) - c.Assert(err, IsNil) - chg := st.NewChange("alias", "...") - chg.AddAll(ts) - - st.Unlock() - err = ms.o.Settle() - st.Lock() - c.Assert(err, IsNil) - - c.Assert(chg.Err(), IsNil) - c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("alias change failed with: %v", chg.Err())) - - foo_Alias := filepath.Join(dirs.SnapBinariesDir, "foo_") - dest, err := os.Readlink(foo_Alias) - c.Assert(err, IsNil) - - c.Check(dest, Equals, "foo") - - barAlias := filepath.Join(dirs.SnapBinariesDir, "bar") - dest, err = os.Readlink(barAlias) - c.Assert(err, IsNil) - - c.Check(dest, Equals, "foo.bar") - - bar1Alias := filepath.Join(dirs.SnapBinariesDir, "bar1") - dest, err = os.Readlink(bar1Alias) - c.Assert(err, IsNil) - - c.Check(dest, Equals, "foo.bar") - - var allAliases map[string]map[string]string - err = st.Get("aliases", &allAliases) - c.Assert(err, IsNil) - c.Check(allAliases, DeepEquals, map[string]map[string]string{ - "foo": { - "foo_": "enabled", - "bar": "enabled", - "bar1": "enabled", - }, - }) + c.Skip("new semantics are wip") + /* + st := ms.o.State() + st.Lock() + defer st.Unlock() + + fooYaml := `name: foo + version: 1.0 + apps: + foo: + command: bin/foo + aliases: [foo_] + bar: + command: bin/bar + aliases: [bar,bar1] + ` + ms.installLocalTestSnap(c, fooYaml) + + ts, err := snapstate.Alias(st, "foo", []string{"foo_", "bar", "bar1"}) + c.Assert(err, IsNil) + chg := st.NewChange("alias", "...") + chg.AddAll(ts) + + st.Unlock() + err = ms.o.Settle() + st.Lock() + c.Assert(err, IsNil) + + c.Assert(chg.Err(), IsNil) + c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("alias change failed with: %v", chg.Err())) + + foo_Alias := filepath.Join(dirs.SnapBinariesDir, "foo_") + dest, err := os.Readlink(foo_Alias) + c.Assert(err, IsNil) + + c.Check(dest, Equals, "foo") + + barAlias := filepath.Join(dirs.SnapBinariesDir, "bar") + dest, err = os.Readlink(barAlias) + c.Assert(err, IsNil) + + c.Check(dest, Equals, "foo.bar") + + bar1Alias := filepath.Join(dirs.SnapBinariesDir, "bar1") + dest, err = os.Readlink(bar1Alias) + c.Assert(err, IsNil) + + c.Check(dest, Equals, "foo.bar") + + var allAliases map[string]map[string]string + err = st.Get("aliases", &allAliases) + c.Assert(err, IsNil) + c.Check(allAliases, DeepEquals, map[string]map[string]string{ + "foo": { + "foo_": "enabled", + "bar": "enabled", + "bar1": "enabled", + }, + }) - ms.removeSnap(c, "foo") + ms.removeSnap(c, "foo") - c.Check(osutil.IsSymlink(foo_Alias), Equals, false) - c.Check(osutil.IsSymlink(barAlias), Equals, false) - c.Check(osutil.IsSymlink(bar1Alias), Equals, false) + c.Check(osutil.IsSymlink(foo_Alias), Equals, false) + c.Check(osutil.IsSymlink(barAlias), Equals, false) + c.Check(osutil.IsSymlink(bar1Alias), Equals, false) - allAliases = nil - err = st.Get("aliases", &allAliases) - c.Assert(err, IsNil) - c.Check(allAliases, HasLen, 0) + allAliases = nil + err = st.Get("aliases", &allAliases) + c.Assert(err, IsNil) + c.Check(allAliases, HasLen, 0) + */ } func (ms *mgrsSuite) TestHappyUnalias(c *C) { - st := ms.o.State() - st.Lock() - defer st.Unlock() - - fooYaml := `name: foo -version: 1.0 -apps: - foo: - command: bin/foo - aliases: [foo_] -` - ms.installLocalTestSnap(c, fooYaml) - - ts, err := snapstate.Alias(st, "foo", []string{"foo_"}) - c.Assert(err, IsNil) - chg := st.NewChange("alias", "...") - chg.AddAll(ts) - - st.Unlock() - err = ms.o.Settle() - st.Lock() - c.Assert(err, IsNil) - - c.Assert(chg.Err(), IsNil) - c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("alias change failed with: %v", chg.Err())) - - foo_Alias := filepath.Join(dirs.SnapBinariesDir, "foo_") - dest, err := os.Readlink(foo_Alias) - c.Assert(err, IsNil) - - c.Check(dest, Equals, "foo") - - var allAliases map[string]map[string]string - err = st.Get("aliases", &allAliases) - c.Assert(err, IsNil) - c.Check(allAliases, DeepEquals, map[string]map[string]string{ - "foo": { - "foo_": "enabled", - }, - }) + c.Skip("new semantics are wip") + /* + st := ms.o.State() + st.Lock() + defer st.Unlock() + + fooYaml := `name: foo + version: 1.0 + apps: + foo: + command: bin/foo + aliases: [foo_] + ` + ms.installLocalTestSnap(c, fooYaml) + + ts, err := snapstate.Alias(st, "foo", []string{"foo_"}) + c.Assert(err, IsNil) + chg := st.NewChange("alias", "...") + chg.AddAll(ts) + + st.Unlock() + err = ms.o.Settle() + st.Lock() + c.Assert(err, IsNil) + + c.Assert(chg.Err(), IsNil) + c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("alias change failed with: %v", chg.Err())) + + foo_Alias := filepath.Join(dirs.SnapBinariesDir, "foo_") + dest, err := os.Readlink(foo_Alias) + c.Assert(err, IsNil) + + c.Check(dest, Equals, "foo") + + var allAliases map[string]map[string]string + err = st.Get("aliases", &allAliases) + c.Assert(err, IsNil) + c.Check(allAliases, DeepEquals, map[string]map[string]string{ + "foo": { + "foo_": "enabled", + }, + }) - ts, err = snapstate.Unalias(st, "foo", []string{"foo_"}) - c.Assert(err, IsNil) - chg = st.NewChange("unalias", "...") - chg.AddAll(ts) + ts, err = snapstate.Unalias(st, "foo", []string{"foo_"}) + c.Assert(err, IsNil) + chg = st.NewChange("unalias", "...") + chg.AddAll(ts) - st.Unlock() - err = ms.o.Settle() - st.Lock() - c.Assert(err, IsNil) + st.Unlock() + err = ms.o.Settle() + st.Lock() + c.Assert(err, IsNil) - c.Assert(chg.Err(), IsNil) - c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("unalias change failed with: %v", chg.Err())) + c.Assert(chg.Err(), IsNil) + c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("unalias change failed with: %v", chg.Err())) - c.Check(osutil.IsSymlink(foo_Alias), Equals, false) + c.Check(osutil.IsSymlink(foo_Alias), Equals, false) - allAliases = nil - err = st.Get("aliases", &allAliases) - c.Assert(err, IsNil) - c.Check(allAliases, DeepEquals, map[string]map[string]string{ - "foo": { - "foo_": "disabled", - }, - }) + allAliases = nil + err = st.Get("aliases", &allAliases) + c.Assert(err, IsNil) + c.Check(allAliases, DeepEquals, map[string]map[string]string{ + "foo": { + "foo_": "disabled", + }, + }) + */ } func (ms *mgrsSuite) TestHappyRemoteInstallAutoAliases(c *C) { diff --git a/overlord/overlord_test.go b/overlord/overlord_test.go index a010c02ea1..f1a5bee06e 100644 --- a/overlord/overlord_test.go +++ b/overlord/overlord_test.go @@ -370,11 +370,42 @@ func (ovs *overlordSuite) TestEnsureLoopPrune(c *C) { chg1.AddTask(t1) chg2 := st.NewChange("prune", "...") chg2.SetStatus(state.DoneStatus) + t0 := chg2.ReadyTime() st.Unlock() + // observe the loop cycles to detect when prune should have happened + pruneHappened := make(chan struct{}) + cycles := -1 + waitForPrune := func(_ *state.State) error { + if cycles == -1 { + if time.Since(t0) > 100*time.Millisecond { + cycles = 2 // wait a couple more loop cycles + } + return nil + } + if cycles > 0 { + cycles-- + if cycles == 0 { + close(pruneHappened) + } + } + return nil + } + witness := &witnessManager{ + ensureCallback: waitForPrune, + } + se := o.Engine() + se.AddManager(witness) + markSeeded(o) o.Loop() - time.Sleep(150 * time.Millisecond) + + select { + case <-pruneHappened: + case <-time.After(2 * time.Second): + c.Fatal("Pruning should have happened by now") + } + err = o.Stop() c.Assert(err, IsNil) diff --git a/overlord/snapstate/aliases.go b/overlord/snapstate/aliases.go index 2b611f2c94..703b3bed21 100644 --- a/overlord/snapstate/aliases.go +++ b/overlord/snapstate/aliases.go @@ -73,72 +73,9 @@ func setAliases(st *state.State, snapName string, aliases map[string]string) { st.Set("aliases", allAliases) } -// Alias enables the provided aliases for the snap with the given name. -func Alias(st *state.State, snapName string, aliases []string) (*state.TaskSet, error) { - var snapst SnapState - err := Get(st, snapName, &snapst) - if err == state.ErrNoState { - return nil, fmt.Errorf("cannot find snap %q", snapName) - } - if err != nil { - return nil, err - } - if !snapst.Active { - return nil, fmt.Errorf("enabling aliases for disabled snap %q not supported", snapName) - } - if err := CheckChangeConflict(st, snapName, nil); err != nil { - return nil, err - } - - snapsup := &SnapSetup{ - SideInfo: &snap.SideInfo{RealName: snapName}, - } - - alias := st.NewTask("alias", fmt.Sprintf(i18n.G("Enable aliases for snap %q"), snapsup.Name())) - alias.Set("snap-setup", &snapsup) - toEnable := map[string]string{} - for _, alias := range aliases { - toEnable[alias] = "enabled" - } - alias.Set("aliases", toEnable) - - return state.NewTaskSet(alias), nil -} - -// Unalias explicitly disables the provided aliases for the snap with the given name. -func Unalias(st *state.State, snapName string, aliases []string) (*state.TaskSet, error) { - var snapst SnapState - err := Get(st, snapName, &snapst) - if err == state.ErrNoState { - return nil, fmt.Errorf("cannot find snap %q", snapName) - } - if err != nil { - return nil, err - } - if !snapst.Active { - return nil, fmt.Errorf("disabling aliases for disabled snap %q not supported", snapName) - } - if err := CheckChangeConflict(st, snapName, nil); err != nil { - return nil, err - } - - snapsup := &SnapSetup{ - SideInfo: &snap.SideInfo{RealName: snapName}, - } - - alias := st.NewTask("alias", fmt.Sprintf(i18n.G("Disable aliases for snap %q"), snapsup.Name())) - alias.Set("snap-setup", &snapsup) - toDisable := map[string]string{} - for _, alias := range aliases { - toDisable[alias] = "disabled" - } - alias.Set("aliases", toDisable) - - return state.NewTaskSet(alias), nil -} +// TODO: reintroduce Alias, Unalias following the new meanings -// ResetAliases resets the provided aliases for the snap with the given name to their default state, enabled for auto-aliases, disabled otherwise. -func ResetAliases(st *state.State, snapName string, aliases []string) (*state.TaskSet, error) { +func resetAliases(st *state.State, snapName string, aliases []string) (*state.TaskSet, error) { var snapst SnapState err := Get(st, snapName, &snapst) if err == state.ErrNoState { @@ -470,48 +407,6 @@ func (m *SnapManager) doSetupAliases(t *state.Task, _ *tomb.Tomb) error { return m.backend.UpdateAliases(aliases, nil) } -func (m *SnapManager) undoSetupAliases(t *state.Task, _ *tomb.Tomb) error { - st := t.State() - st.Lock() - defer st.Unlock() - snapsup, snapst, err := snapSetupAndState(t) - if err != nil { - return err - } - snapName := snapsup.Name() - curInfo, err := snapst.CurrentInfo() - if err != nil { - return err - } - aliasStatuses, err := getAliases(st, snapName) - if err != nil && err != state.ErrNoState { - return err - } - var aliases []*backend.Alias - for alias, aliasStatus := range aliasStatuses { - if enabledAlias(aliasStatus) { - aliasApp := curInfo.Aliases[alias] - if aliasApp == nil { - // not a known alias, skip - continue - } - aliases = append(aliases, &backend.Alias{ - Name: alias, - Target: filepath.Base(aliasApp.WrapperPath()), - }) - } - } - st.Unlock() - rmAliases, err := m.backend.MatchingAliases(aliases) - st.Lock() - if err != nil { - return fmt.Errorf("cannot list aliases for snap %q: %v", snapName, err) - } - st.Unlock() - defer st.Lock() - return m.backend.UpdateAliases(nil, rmAliases) -} - func (m *SnapManager) doRemoveAliases(t *state.Task, _ *tomb.Tomb) error { st := t.State() st.Lock() diff --git a/overlord/snapstate/aliases_test.go b/overlord/snapstate/aliases_test.go index 0e4a741282..92f2fadfb6 100644 --- a/overlord/snapstate/aliases_test.go +++ b/overlord/snapstate/aliases_test.go @@ -20,7 +20,6 @@ package snapstate_test import ( - "fmt" "sort" . "gopkg.in/check.v1" @@ -119,12 +118,8 @@ func (s *snapmgrTestSuite) TestDoUndoSetupAliases(c *C) { aliases: []*backend.Alias{{"alias1", "alias-snap.cmd1"}}, }, { - op: "matching-aliases", - aliases: []*backend.Alias{{"alias1", "alias-snap.cmd1"}}, - }, - { - op: "update-aliases", - rmAliases: []*backend.Alias{{"alias1", "alias-snap.cmd1"}}, + op: "remove-snap-aliases", + name: "alias-snap", }, } // start with an easier-to-read error if this fails: @@ -133,24 +128,27 @@ func (s *snapmgrTestSuite) TestDoUndoSetupAliases(c *C) { } func (s *snapmgrTestSuite) TestAliasTasks(c *C) { - s.state.Lock() - defer s.state.Unlock() + c.Skip("new semantics are wip") + /* + s.state.Lock() + defer s.state.Unlock() - snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ - Sequence: []*snap.SideInfo{ - {RealName: "some-snap", Revision: snap.R(11)}, - }, - Current: snap.R(11), - Active: true, - }) + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "some-snap", Revision: snap.R(11)}, + }, + Current: snap.R(11), + Active: true, + }) - ts, err := snapstate.Alias(s.state, "some-snap", []string{"alias"}) - c.Assert(err, IsNil) + ts, err := snapstate.Alias(s.state, "some-snap", []string{"alias"}) + c.Assert(err, IsNil) - c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks())) - c.Assert(taskKinds(ts.Tasks()), DeepEquals, []string{ - "alias", - }) + c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks())) + c.Assert(taskKinds(ts.Tasks()), DeepEquals, []string{ + "alias", + }) + */ } func (s *snapmgrTestSuite) TestDoSetupAliasesAuto(c *C) { @@ -240,12 +238,8 @@ func (s *snapmgrTestSuite) TestDoUndoSetupAliasesAuto(c *C) { aliases: []*backend.Alias{{"alias1", "alias-snap.cmd1"}}, }, { - op: "matching-aliases", - aliases: []*backend.Alias{{"alias1", "alias-snap.cmd1"}}, - }, - { - op: "update-aliases", - rmAliases: []*backend.Alias{{"alias1", "alias-snap.cmd1"}}, + op: "remove-snap-aliases", + name: "alias-snap", }, } // start with an easier-to-read error if this fails: @@ -254,84 +248,93 @@ func (s *snapmgrTestSuite) TestDoUndoSetupAliasesAuto(c *C) { } func (s *snapmgrTestSuite) TestAliasRunThrough(c *C) { - s.state.Lock() - defer s.state.Unlock() + c.Skip("new semantics are wip") + /* + s.state.Lock() + defer s.state.Unlock() - snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ - Sequence: []*snap.SideInfo{ - {RealName: "alias-snap", Revision: snap.R(11)}, - }, - Current: snap.R(11), - Active: true, - }) + snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "alias-snap", Revision: snap.R(11)}, + }, + Current: snap.R(11), + Active: true, + }) - chg := s.state.NewChange("alias", "enable an alias") - ts, err := snapstate.Alias(s.state, "alias-snap", []string{"alias1"}) - c.Assert(err, IsNil) - chg.AddAll(ts) + chg := s.state.NewChange("alias", "enable an alias") + ts, err := snapstate.Alias(s.state, "alias-snap", []string{"alias1"}) + c.Assert(err, IsNil) + chg.AddAll(ts) - s.state.Unlock() - defer s.snapmgr.Stop() - s.settle() - s.state.Lock() + s.state.Unlock() + defer s.snapmgr.Stop() + s.settle() + s.state.Lock() - c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("%v", chg.Err())) - expected := fakeOps{ - { - op: "update-aliases", - aliases: []*backend.Alias{{"alias1", "alias-snap.cmd1"}}, - }, - } - // start with an easier-to-read error if this fails: - c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) - c.Assert(s.fakeBackend.ops, DeepEquals, expected) + c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("%v", chg.Err())) + expected := fakeOps{ + { + op: "update-aliases", + aliases: []*backend.Alias{{"alias1", "alias-snap.cmd1"}}, + }, + } + // start with an easier-to-read error if this fails: + c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) + c.Assert(s.fakeBackend.ops, DeepEquals, expected) - var allAliases map[string]map[string]string - err = s.state.Get("aliases", &allAliases) - c.Assert(err, IsNil) - c.Check(allAliases, DeepEquals, map[string]map[string]string{ - "alias-snap": {"alias1": "enabled"}, - }) + var allAliases map[string]map[string]string + err = s.state.Get("aliases", &allAliases) + c.Assert(err, IsNil) + c.Check(allAliases, DeepEquals, map[string]map[string]string{ + "alias-snap": {"alias1": "enabled"}, + }) + */ } func (s *snapmgrTestSuite) TestUpdateAliasChangeConflict(c *C) { - s.state.Lock() - defer s.state.Unlock() + c.Skip("new semantics are wip") + /* + s.state.Lock() + defer s.state.Unlock() - snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ - Active: true, - Sequence: []*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}}, - Current: snap.R(7), - SnapType: "app", - }) + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}}, + Current: snap.R(7), + SnapType: "app", + }) - ts, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{}) - c.Assert(err, IsNil) - // need a change to make the tasks visible - s.state.NewChange("update", "...").AddAll(ts) + ts, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{}) + c.Assert(err, IsNil) + // need a change to make the tasks visible + s.state.NewChange("update", "...").AddAll(ts) - _, err = snapstate.Alias(s.state, "some-snap", []string{"alias1"}) - c.Assert(err, ErrorMatches, `snap "some-snap" has changes in progress`) + _, err = snapstate.Alias(s.state, "some-snap", []string{"alias1"}) + c.Assert(err, ErrorMatches, `snap "some-snap" has changes in progress`) + */ } func (s *snapmgrTestSuite) TestUpdateUnaliasChangeConflict(c *C) { - s.state.Lock() - defer s.state.Unlock() + c.Skip("new semantics are wip") + /* + s.state.Lock() + defer s.state.Unlock() - snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ - Active: true, - Sequence: []*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}}, - Current: snap.R(7), - SnapType: "app", - }) + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}}, + Current: snap.R(7), + SnapType: "app", + }) - ts, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{}) - c.Assert(err, IsNil) - // need a change to make the tasks visible - s.state.NewChange("update", "...").AddAll(ts) + ts, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{}) + c.Assert(err, IsNil) + // need a change to make the tasks visible + s.state.NewChange("update", "...").AddAll(ts) - _, err = snapstate.Unalias(s.state, "some-snap", []string{"alias1"}) - c.Assert(err, ErrorMatches, `snap "some-snap" has changes in progress`) + _, err = snapstate.Unalias(s.state, "some-snap", []string{"alias1"}) + c.Assert(err, ErrorMatches, `snap "some-snap" has changes in progress`) + */ } func (s *snapmgrTestSuite) TestUpdateResetAliasesChangeConflict(c *C) { @@ -355,148 +358,163 @@ func (s *snapmgrTestSuite) TestUpdateResetAliasesChangeConflict(c *C) { } func (s *snapmgrTestSuite) TestAliasUpdateChangeConflict(c *C) { - s.state.Lock() - defer s.state.Unlock() + c.Skip("new semantics coming") + /* + s.state.Lock() + defer s.state.Unlock() - snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ - Active: true, - Sequence: []*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}}, - Current: snap.R(7), - SnapType: "app", - }) + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}}, + Current: snap.R(7), + SnapType: "app", + }) - ts, err := snapstate.Alias(s.state, "some-snap", []string{"alias1"}) - c.Assert(err, IsNil) - // need a change to make the tasks visible - s.state.NewChange("alias", "...").AddAll(ts) + ts, err := snapstate.Alias(s.state, "some-snap", []string{"alias1"}) + c.Assert(err, IsNil) + // need a change to make the tasks visible + s.state.NewChange("alias", "...").AddAll(ts) - _, err = snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{}) - c.Assert(err, ErrorMatches, `snap "some-snap" has changes in progress`) + _, err = snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{}) + c.Assert(err, ErrorMatches, `snap "some-snap" has changes in progress`) + */ } func (s *snapmgrTestSuite) TestAliasNoAlias(c *C) { - s.state.Lock() - defer s.state.Unlock() + c.Skip("should become a new app test") + /* + s.state.Lock() + defer s.state.Unlock() - snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ - Sequence: []*snap.SideInfo{ - {RealName: "some-snap", Revision: snap.R(11)}, - }, - Current: snap.R(11), - Active: true, - }) + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "some-snap", Revision: snap.R(11)}, + }, + Current: snap.R(11), + Active: true, + }) - chg := s.state.NewChange("alias", "enable an alias") - ts, err := snapstate.Alias(s.state, "some-snap", []string{"alias1"}) - c.Assert(err, IsNil) - chg.AddAll(ts) + chg := s.state.NewChange("alias", "enable an alias") + ts, err := snapstate.Alias(s.state, "some-snap", []string{"alias1"}) + c.Assert(err, IsNil) + chg.AddAll(ts) - s.state.Unlock() + s.state.Unlock() - s.snapmgr.Ensure() - s.snapmgr.Wait() + s.snapmgr.Ensure() + s.snapmgr.Wait() - s.state.Lock() + s.state.Lock() - c.Check(chg.Status(), Equals, state.ErrorStatus) - c.Check(chg.Err(), ErrorMatches, `(?s).*cannot enable alias "alias1" for "some-snap", no such alias.*`) + c.Check(chg.Status(), Equals, state.ErrorStatus) + c.Check(chg.Err(), ErrorMatches, `(?s).*cannot enable alias "alias1" for "some-snap", no such alias.*`) + */ } func (s *snapmgrTestSuite) TestAliasAliasConflict(c *C) { - s.state.Lock() - defer s.state.Unlock() + c.Skip("new semantics are wip") + /* + s.state.Lock() + defer s.state.Unlock() - snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ - Sequence: []*snap.SideInfo{ - {RealName: "alias-snap", Revision: snap.R(11)}, - }, - Current: snap.R(11), - Active: true, - }) - s.state.Set("aliases", map[string]map[string]string{ - "other-snap": {"alias1": "enabled"}, - }) + snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "alias-snap", Revision: snap.R(11)}, + }, + Current: snap.R(11), + Active: true, + }) + s.state.Set("aliases", map[string]map[string]string{ + "other-snap": {"alias1": "enabled"}, + }) - chg := s.state.NewChange("alias", "enable an alias") - ts, err := snapstate.Alias(s.state, "alias-snap", []string{"alias1"}) - c.Assert(err, IsNil) - chg.AddAll(ts) + chg := s.state.NewChange("alias", "enable an alias") + ts, err := snapstate.Alias(s.state, "alias-snap", []string{"alias1"}) + c.Assert(err, IsNil) + chg.AddAll(ts) - s.state.Unlock() + s.state.Unlock() - s.snapmgr.Ensure() - s.snapmgr.Wait() + s.snapmgr.Ensure() + s.snapmgr.Wait() - s.state.Lock() + s.state.Lock() - c.Check(chg.Status(), Equals, state.ErrorStatus) - c.Check(chg.Err(), ErrorMatches, `(?s).*cannot enable alias "alias1" for "alias-snap", already enabled for "other-snap".*`) + c.Check(chg.Status(), Equals, state.ErrorStatus) + c.Check(chg.Err(), ErrorMatches, `(?s).*cannot enable alias "alias1" for "alias-snap", already enabled for "other-snap".*`) + */ } func (s *snapmgrTestSuite) TestAliasAutoAliasConflict(c *C) { - s.state.Lock() - defer s.state.Unlock() + c.Skip("new semantics are wip") + /* + s.state.Lock() + defer s.state.Unlock() - snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ - Sequence: []*snap.SideInfo{ - {RealName: "alias-snap", Revision: snap.R(11)}, - }, - Current: snap.R(11), - Active: true, - }) - s.state.Set("aliases", map[string]map[string]string{ - "other-snap": {"alias1": "auto"}, - }) + snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "alias-snap", Revision: snap.R(11)}, + }, + Current: snap.R(11), + Active: true, + }) + s.state.Set("aliases", map[string]map[string]string{ + "other-snap": {"alias1": "auto"}, + }) - chg := s.state.NewChange("alias", "enable an alias") - ts, err := snapstate.Alias(s.state, "alias-snap", []string{"alias1"}) - c.Assert(err, IsNil) - chg.AddAll(ts) + chg := s.state.NewChange("alias", "enable an alias") + ts, err := snapstate.Alias(s.state, "alias-snap", []string{"alias1"}) + c.Assert(err, IsNil) + chg.AddAll(ts) - s.state.Unlock() + s.state.Unlock() - s.snapmgr.Ensure() - s.snapmgr.Wait() + s.snapmgr.Ensure() + s.snapmgr.Wait() - s.state.Lock() + s.state.Lock() - c.Check(chg.Status(), Equals, state.ErrorStatus) - c.Check(chg.Err(), ErrorMatches, `(?s).*cannot enable alias "alias1" for "alias-snap", already enabled for "other-snap".*`) + c.Check(chg.Status(), Equals, state.ErrorStatus) + c.Check(chg.Err(), ErrorMatches, `(?s).*cannot enable alias "alias1" for "alias-snap", already enabled for "other-snap".*`) + */ } func (s *snapmgrTestSuite) TestAliasSnapCommandSpaceConflict(c *C) { - s.state.Lock() - defer s.state.Unlock() + c.Skip("new semantics are wip") + /* + s.state.Lock() + defer s.state.Unlock() - snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ - Sequence: []*snap.SideInfo{ - {RealName: "alias-snap", Revision: snap.R(11)}, - }, - Current: snap.R(11), - Active: true, - }) - // the command namespace of this one will conflict - snapstate.Set(s.state, "alias1", &snapstate.SnapState{ - Sequence: []*snap.SideInfo{ - {RealName: "alias1", Revision: snap.R(3)}, - }, - Current: snap.R(3), - }) + snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "alias-snap", Revision: snap.R(11)}, + }, + Current: snap.R(11), + Active: true, + }) + // the command namespace of this one will conflict + snapstate.Set(s.state, "alias1", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "alias1", Revision: snap.R(3)}, + }, + Current: snap.R(3), + }) - chg := s.state.NewChange("alias", "enable an alias") - ts, err := snapstate.Alias(s.state, "alias-snap", []string{"alias1.cmd1"}) - c.Assert(err, IsNil) - chg.AddAll(ts) + chg := s.state.NewChange("alias", "enable an alias") + ts, err := snapstate.Alias(s.state, "alias-snap", []string{"alias1.cmd1"}) + c.Assert(err, IsNil) + chg.AddAll(ts) - s.state.Unlock() + s.state.Unlock() - s.snapmgr.Ensure() - s.snapmgr.Wait() + s.snapmgr.Ensure() + s.snapmgr.Wait() - s.state.Lock() + s.state.Lock() - c.Check(chg.Status(), Equals, state.ErrorStatus) - c.Check(chg.Err(), ErrorMatches, `(?s).*cannot enable alias "alias1.cmd1" for "alias-snap", it conflicts with the command namespace of installed snap "alias1".*`) + c.Check(chg.Status(), Equals, state.ErrorStatus) + c.Check(chg.Err(), ErrorMatches, `(?s).*cannot enable alias "alias1.cmd1" for "alias-snap", it conflicts with the command namespace of installed snap "alias1".*`) + */ } func (s *snapmgrTestSuite) TestDoClearAliases(c *C) { @@ -690,224 +708,6 @@ var statusesMatrix = []struct { {"alias5gone", "auto", "reset", "", "-"}, } -func (s *snapmgrTestSuite) TestAliasMatrixRunThrough(c *C) { - s.state.Lock() - defer s.state.Unlock() - - snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ - Sequence: []*snap.SideInfo{ - {RealName: "alias-snap", Revision: snap.R(11)}, - }, - Current: snap.R(11), - Active: true, - }) - - // alias1 is a non auto-alias - // alias5 is an auto-alias - // alias1gone is a non auto-alias and doesn't have an entry in the current snap revision anymore - // alias5gone is an auto-alias and doesn't have an entry in the current snap revision anymore - snapstate.AutoAliases = func(st *state.State, info *snap.Info) (map[string]string, error) { - c.Check(info.Name(), Equals, "alias-snap") - return map[string]string{"alias5": "cmd5", "alias5gone": "cmd5gone"}, nil - } - cmds := map[string]string{ - "alias1": "cmd1", - "alias5": "cmd5", - } - - defer s.snapmgr.Stop() - for _, scenario := range statusesMatrix { - scenAlias := scenario.alias - if scenario.beforeStatus != "" { - s.state.Set("aliases", map[string]map[string]string{ - "alias-snap": { - scenAlias: scenario.beforeStatus, - }, - }) - } else { - s.state.Set("aliases", nil) - } - - chg := s.state.NewChange("scenario", "...") - var err error - var ts *state.TaskSet - targets := []string{scenAlias} - switch scenario.action { - case "alias": - ts, err = snapstate.Alias(s.state, "alias-snap", targets) - case "unalias": - ts, err = snapstate.Unalias(s.state, "alias-snap", targets) - case "reset": - ts, err = snapstate.ResetAliases(s.state, "alias-snap", targets) - } - c.Assert(err, IsNil) - - chg.AddAll(ts) - - s.state.Unlock() - s.settle() - s.state.Lock() - - c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("%#v: %v", scenario, chg.Err())) - var aliases []*backend.Alias - var rmAliases []*backend.Alias - beAlias := &backend.Alias{Name: scenAlias, Target: fmt.Sprintf("alias-snap.%s", cmds[scenAlias])} - switch scenario.mutation { - case "-": - case "add": - aliases = []*backend.Alias{beAlias} - case "rm": - rmAliases = []*backend.Alias{beAlias} - } - - comm := Commentf("%#v", scenario) - expected := fakeOps{ - { - op: "update-aliases", - aliases: aliases, - rmAliases: rmAliases, - }, - } - // start with an easier-to-read error if this fails: - c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops(), comm) - c.Check(s.fakeBackend.ops, DeepEquals, expected, comm) - - var allAliases map[string]map[string]string - err = s.state.Get("aliases", &allAliases) - c.Assert(err, IsNil) - if scenario.status != "" { - c.Check(allAliases, DeepEquals, map[string]map[string]string{ - "alias-snap": {scenAlias: scenario.status}, - }, comm) - } else { - c.Check(allAliases, HasLen, 0, comm) - } - - s.fakeBackend.ops = nil - } -} - -func (s *snapmgrTestSuite) TestAliasMatrixTotalUndoRunThrough(c *C) { - s.state.Lock() - defer s.state.Unlock() - - snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ - Sequence: []*snap.SideInfo{ - {RealName: "alias-snap", Revision: snap.R(11)}, - }, - Current: snap.R(11), - Active: true, - }) - - // alias1 is a non auto-alias - // alias5 is an auto-alias - // alias1gone is a non auto-alias and doesn't have an entry in the snap anymore - // alias5gone is an auto-alias and doesn't have an entry in the snap any - snapstate.AutoAliases = func(st *state.State, info *snap.Info) (map[string]string, error) { - c.Check(info.Name(), Equals, "alias-snap") - return map[string]string{"alias5": "cmd5", "alias5gone": "cmd5gone"}, nil - } - cmds := map[string]string{ - "alias1": "cmd1", - "alias5": "cmd5", - } - - defer s.snapmgr.Stop() - for _, scenario := range statusesMatrix { - scenAlias := scenario.alias - if scenario.beforeStatus != "" { - s.state.Set("aliases", map[string]map[string]string{ - "alias-snap": { - scenAlias: scenario.beforeStatus, - }, - }) - } else { - s.state.Set("aliases", nil) - } - - chg := s.state.NewChange("scenario", "...") - var err error - var ts *state.TaskSet - targets := []string{scenAlias} - - switch scenario.action { - case "alias": - ts, err = snapstate.Alias(s.state, "alias-snap", targets) - case "unalias": - ts, err = snapstate.Unalias(s.state, "alias-snap", targets) - case "reset": - ts, err = snapstate.ResetAliases(s.state, "alias-snap", targets) - } - c.Assert(err, IsNil) - - chg.AddAll(ts) - - tasks := ts.Tasks() - last := tasks[len(tasks)-1] - - terr := s.state.NewTask("error-trigger", "provoking total undo") - terr.WaitFor(last) - chg.AddTask(terr) - - s.state.Unlock() - for i := 0; i < 3; i++ { - s.snapmgr.Ensure() - s.snapmgr.Wait() - } - s.state.Lock() - - c.Assert(chg.Status(), Equals, state.ErrorStatus, Commentf("%#v: %v", scenario, chg.Err())) - var aliases []*backend.Alias - var rmAliases []*backend.Alias - beAlias := &backend.Alias{Name: scenAlias, Target: fmt.Sprintf("alias-snap.%s", cmds[scenAlias])} - switch scenario.mutation { - case "-": - case "add": - aliases = []*backend.Alias{beAlias} - case "rm": - rmAliases = []*backend.Alias{beAlias} - } - - comm := Commentf("%#v", scenario) - expected := fakeOps{ - { - op: "update-aliases", - aliases: aliases, - rmAliases: rmAliases, - }, - { - op: "matching-aliases", - aliases: aliases, - }, - { - op: "missing-aliases", - aliases: rmAliases, - }, - { - op: "update-aliases", - aliases: rmAliases, - rmAliases: aliases, - }, - } - // start with an easier-to-read error if this fails: - c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops(), comm) - c.Check(s.fakeBackend.ops, DeepEquals, expected, comm) - - var allAliases map[string]map[string]string - err = s.state.Get("aliases", &allAliases) - c.Assert(err, IsNil) - if scenario.beforeStatus != "" { - c.Check(allAliases, DeepEquals, map[string]map[string]string{ - "alias-snap": {scenAlias: scenario.beforeStatus}, - }, comm) - } else { - c.Check(allAliases, HasLen, 0, comm) - } - - s.fakeBackend.ops = nil - } -} - func (s *snapmgrTestSuite) TestDisabledSnapResetAliasesRunThrough(c *C) { s.state.Lock() defer s.state.Unlock() @@ -1066,111 +866,114 @@ func (s *snapmgrTestSuite) TestDisabledSnapResetAliasesTotalUndoRunThrough(c *C) } func (s *snapmgrTestSuite) TestUnliasTotalUndoRunThroughAliasConflict(c *C) { - s.state.Lock() - defer s.state.Unlock() + c.Skip("new semantics are wip") + /* - snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ - Sequence: []*snap.SideInfo{ - {RealName: "alias-snap", Revision: snap.R(11)}, - }, - Current: snap.R(11), - Active: true, - }) - - defer s.snapmgr.Stop() - s.state.Set("aliases", map[string]map[string]string{ - "alias-snap": { - "alias1": "enabled", - }, - }) - - chg := s.state.NewChange("scenario", "...") - ts, err := snapstate.Unalias(s.state, "alias-snap", []string{"alias1"}) - c.Assert(err, IsNil) - - chg.AddAll(ts) - - tasks := ts.Tasks() - last := tasks[len(tasks)-1] - - grabAlias1 := func(t *state.Task, _ *tomb.Tomb) error { - st := t.State() - st.Lock() - defer st.Unlock() + s.state.Lock() + defer s.state.Unlock() - var allAliases map[string]map[string]string - err := st.Get("aliases", &allAliases) - c.Assert(err, IsNil) - c.Assert(allAliases, DeepEquals, map[string]map[string]string{ - "alias-snap": { - "alias1": "disabled", + snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "alias-snap", Revision: snap.R(11)}, }, + Current: snap.R(11), + Active: true, }) - st.Set("aliases", map[string]map[string]string{ + defer s.snapmgr.Stop() + s.state.Set("aliases", map[string]map[string]string{ "alias-snap": { - "alias1": "disabled", - }, - "other-snap": { "alias1": "enabled", }, }) - return nil - } - s.snapmgr.AddAdhocTaskHandler("grab-alias1", grabAlias1, nil) + chg := s.state.NewChange("scenario", "...") + ts, err := snapstate.Unalias(s.state, "alias-snap", []string{"alias1"}) + c.Assert(err, IsNil) - tgrab1 := s.state.NewTask("grab-alias1", "grab alias1 for other-snap") - tgrab1.WaitFor(last) - chg.AddTask(tgrab1) + chg.AddAll(ts) - terr := s.state.NewTask("error-trigger", "provoking total undo") - terr.WaitFor(tgrab1) - chg.AddTask(terr) + tasks := ts.Tasks() + last := tasks[len(tasks)-1] - s.state.Unlock() + grabAlias1 := func(t *state.Task, _ *tomb.Tomb) error { + st := t.State() + st.Lock() + defer st.Unlock() - for i := 0; i < 5; i++ { - s.snapmgr.Ensure() - s.snapmgr.Wait() - } + var allAliases map[string]map[string]string + err := st.Get("aliases", &allAliases) + c.Assert(err, IsNil) + c.Assert(allAliases, DeepEquals, map[string]map[string]string{ + "alias-snap": { + "alias1": "disabled", + }, + }) - s.state.Lock() + st.Set("aliases", map[string]map[string]string{ + "alias-snap": { + "alias1": "disabled", + }, + "other-snap": { + "alias1": "enabled", + }, + }) + return nil + } - c.Assert(chg.Status(), Equals, state.ErrorStatus, Commentf("%v", chg.Err())) - rmAliases := []*backend.Alias{{"alias1", "alias-snap.cmd1"}} + s.snapmgr.AddAdhocTaskHandler("grab-alias1", grabAlias1, nil) - expected := fakeOps{ - { - op: "update-aliases", - rmAliases: rmAliases, - }, - { - op: "matching-aliases", - }, - { - op: "missing-aliases", - }, - { - op: "update-aliases", - }, - } - // start with an easier-to-read error if this fails: - c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) - c.Assert(s.fakeBackend.ops, DeepEquals, expected) + tgrab1 := s.state.NewTask("grab-alias1", "grab alias1 for other-snap") + tgrab1.WaitFor(last) + chg.AddTask(tgrab1) - var allAliases map[string]map[string]string - err = s.state.Get("aliases", &allAliases) - c.Assert(err, IsNil) - c.Check(allAliases, DeepEquals, map[string]map[string]string{ - "other-snap": { - "alias1": "enabled", - }, - }) + terr := s.state.NewTask("error-trigger", "provoking total undo") + terr.WaitFor(tgrab1) + chg.AddTask(terr) - c.Check(last.Log(), HasLen, 1) - c.Check(last.Log()[0], Matches, `.* ERROR cannot enable alias "alias1" for "alias-snap", already enabled for "other-snap"`) + s.state.Unlock() + + for i := 0; i < 5; i++ { + s.snapmgr.Ensure() + s.snapmgr.Wait() + } + + s.state.Lock() + + c.Assert(chg.Status(), Equals, state.ErrorStatus, Commentf("%v", chg.Err())) + rmAliases := []*backend.Alias{{"alias1", "alias-snap.cmd1"}} + + expected := fakeOps{ + { + op: "update-aliases", + rmAliases: rmAliases, + }, + { + op: "matching-aliases", + }, + { + op: "missing-aliases", + }, + { + op: "update-aliases", + }, + } + // start with an easier-to-read error if this fails: + c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) + c.Assert(s.fakeBackend.ops, DeepEquals, expected) + + var allAliases map[string]map[string]string + err = s.state.Get("aliases", &allAliases) + c.Assert(err, IsNil) + c.Check(allAliases, DeepEquals, map[string]map[string]string{ + "other-snap": { + "alias1": "enabled", + }, + }) + c.Check(last.Log(), HasLen, 1) + c.Check(last.Log()[0], Matches, `.* ERROR cannot enable alias "alias1" for "alias-snap", already enabled for "other-snap"`) + */ } func (s *snapmgrTestSuite) TestAutoAliasesDelta(c *C) { diff --git a/overlord/snapstate/aliasesv2.go b/overlord/snapstate/aliasesv2.go index fa6111224e..14e988c69c 100644 --- a/overlord/snapstate/aliasesv2.go +++ b/overlord/snapstate/aliasesv2.go @@ -30,33 +30,26 @@ import ( "github.com/snapcore/snapd/strutil" ) -// AliasesStatus represents the global aliases status for a snap. -type AliasesStatus string - -// Possible global aliases statuses for a snap. -const ( - EnabledAliases AliasesStatus = "enabled" - DisabledAliases AliasesStatus = "disabled" -) - // AliasTarget carries the targets of an alias in the context of snap. // If Manual is set it is the target of an enabled manual alias. // Auto is set to the target for an automatic alias, enabled or -// disabled depending on the aliases status of the whole snap. +// disabled depending on the automatic aliases flag state. type AliasTarget struct { Manual string `json:"manual,omitempty"` Auto string `json:"auto,omitempty"` } -// Effective returns the target to use based on the aliasesStatus of the whole snap, returns "" if the alias is disabled. -func (at *AliasTarget) Effective(aliasesStatus AliasesStatus) string { +// Effective returns the target to use considering whether automatic +// aliases are disabled for the whole snap (autoDisabled), returns "" +// if the alias is disabled. +func (at *AliasTarget) Effective(autoDisabled bool) string { if at == nil { return "" } if at.Manual != "" { return at.Manual } - if aliasesStatus == EnabledAliases { + if !autoDisabled { return at.Auto } return "" @@ -67,10 +60,9 @@ func (at *AliasTarget) Effective(aliasesStatus AliasesStatus) string { type SnapState struct { ... - Aliases map[string]*AliasTarget `json:"aliases,omitempty"` - AliasesStatus AliasesStatus `json:"aliases-status,omitempty"` + Aliases map[string]*AliasTarget + AutoAliasesDisabled bool } -} There are two kinds of aliases: @@ -83,7 +75,7 @@ func (at *AliasTarget) Effective(aliasesStatus AliasesStatus) string { Further * all automatic aliases of a snap are either enabled - or disabled together (tracked with AliasesStatus) + or disabled together (tracked with AutoAliasesDisabled) * disabling a manual alias removes it from disk and state (for simplicity there is no disabled state for manual aliases) @@ -103,17 +95,17 @@ func composeTarget(snapName, targetApp string) string { } // applyAliasesChange applies the necessary changes to aliases on disk -// to go from prevAliases under the snap global prevStatus for -// automatic aliases to newAliases under newStatus for snapName. -// It assumes that conflicts have already been checked. -func applyAliasesChange(st *state.State, snapName string, prevStatus AliasesStatus, prevAliases map[string]*AliasTarget, newStatus AliasesStatus, newAliases map[string]*AliasTarget, be managerBackend) error { +// to go from prevAliases consindering the automatic aliases flag +// (prevAutoDisabled) to newAliases considering newAutoDisabled for +// snapName. It assumes that conflicts have already been checked. +func applyAliasesChange(st *state.State, snapName string, prevAutoDisabled bool, prevAliases map[string]*AliasTarget, newAutoDisabled bool, newAliases map[string]*AliasTarget, be managerBackend) error { var add, remove []*backend.Alias for alias, prevTargets := range prevAliases { if _, ok := newAliases[alias]; ok { continue } // gone - if effTgt := prevTargets.Effective(prevStatus); effTgt != "" { + if effTgt := prevTargets.Effective(prevAutoDisabled); effTgt != "" { remove = append(remove, &backend.Alias{ Name: alias, Target: composeTarget(snapName, effTgt), @@ -121,8 +113,8 @@ func applyAliasesChange(st *state.State, snapName string, prevStatus AliasesStat } } for alias, newTargets := range newAliases { - prevTgt := prevAliases[alias].Effective(prevStatus) - newTgt := newTargets.Effective(newStatus) + prevTgt := prevAliases[alias].Effective(prevAutoDisabled) + newTgt := newTargets.Effective(newAutoDisabled) if prevTgt == newTgt { // nothing to do continue @@ -286,18 +278,18 @@ func addAliasConflicts(st *state.State, skipSnap string, testAliases map[string] // skip continue } - status := snapst.AliasesStatus + autoDisabled := snapst.AutoAliasesDisabled var confls []string if len(snapst.Aliases) < len(testAliases) { for alias, target := range snapst.Aliases { - if testAliases[alias] && target.Effective(status) != "" { + if testAliases[alias] && target.Effective(autoDisabled) != "" { confls = append(confls, alias) } } } else { for alias := range testAliases { target := snapst.Aliases[alias] - if target != nil && target.Effective(status) != "" { + if target != nil && target.Effective(autoDisabled) != "" { confls = append(confls, alias) } } @@ -309,10 +301,10 @@ func addAliasConflicts(st *state.State, skipSnap string, testAliases map[string] return nil } -// checkAliasesStatConflicts checks candAliases under candStatus for -// conflicts against other snap aliases returning conflicting -// snaps and aliases for alias conflicts. -func checkAliasesConflicts(st *state.State, snapName string, candStatus AliasesStatus, candAliases map[string]*AliasTarget) (conflicts map[string][]string, err error) { +// checkAliasesStatConflicts checks candAliases considering +// candAutoDisabled for conflicts against other snap aliases returning +// conflicting snaps and aliases for alias conflicts. +func checkAliasesConflicts(st *state.State, snapName string, candAutoDisabled bool, candAliases map[string]*AliasTarget) (conflicts map[string][]string, err error) { var snapNames map[string]*json.RawMessage err = st.Get("snaps", &snapNames) if err != nil && err != state.ErrNoState { @@ -321,7 +313,7 @@ func checkAliasesConflicts(st *state.State, snapName string, candStatus AliasesS enabled := make(map[string]bool, len(candAliases)) for alias, candTarget := range candAliases { - if candTarget.Effective(candStatus) != "" { + if candTarget.Effective(candAutoDisabled) != "" { enabled[alias] = true } else { continue @@ -350,3 +342,39 @@ func checkAliasesConflicts(st *state.State, snapName string, candStatus AliasesS } return nil, nil } + +// disableAliases returns newAliases corresponding to the disabling of +// curAliases, for manual aliases that means removed. +func disableAliases(curAliases map[string]*AliasTarget) (newAliases map[string]*AliasTarget) { + newAliases = make(map[string]*AliasTarget, len(curAliases)) + for alias, curTarget := range curAliases { + if curTarget.Auto != "" { + newAliases[alias] = &AliasTarget{Auto: curTarget.Auto} + } + } + return newAliases +} + +// pruneAutoAliases returns newAliases by dropping the automatic +// aliases autoAliases from curAliases, used as the task +// prune-auto-aliases to handle transfers of automatic aliases in a +// refresh. +func pruneAutoAliases(st *state.State, curAliases map[string]*AliasTarget, autoAliases []string) (newAliases map[string]*AliasTarget) { + newAliases = make(map[string]*AliasTarget, len(curAliases)) + for alias, aliasTarget := range curAliases { + newAliases[alias] = aliasTarget + } + for _, alias := range autoAliases { + curTarget := curAliases[alias] + if curTarget == nil { + // nothing to do + continue + } + if curTarget.Manual == "" { + delete(newAliases, alias) + } else { + newAliases[alias] = &AliasTarget{Manual: curTarget.Manual} + } + } + return newAliases +} diff --git a/overlord/snapstate/aliasesv2_test.go b/overlord/snapstate/aliasesv2_test.go index a24a927684..8fea24ed70 100644 --- a/overlord/snapstate/aliasesv2_test.go +++ b/overlord/snapstate/aliasesv2_test.go @@ -61,28 +61,24 @@ func (s *snapmgrTestSuite) TestApplyAliasesChange(c *C) { Auto: "cmd1", } - const ( - en = snapstate.EnabledAliases - dis = snapstate.DisabledAliases - ) scenarios := []struct { - status snapstate.AliasesStatus - newStatus snapstate.AliasesStatus - target *snapstate.AliasTarget - newTarget *snapstate.AliasTarget - ops string + autoDisabled bool + newAutoDisabled bool + target *snapstate.AliasTarget + newTarget *snapstate.AliasTarget + ops string }{ - {en, en, nil, auto1, "add"}, - {en, dis, auto1, auto1, "rm"}, - {en, en, auto1, auto2, "rm add"}, - {en, en, auto1, nil, "rm"}, - {en, en, nil, manual1, "add"}, - {dis, dis, nil, manual1, "add"}, - {en, dis, auto1, manual2, "rm add"}, - {en, en, manual2, nil, "rm"}, - {en, en, manual2, auto1, "rm add"}, - {en, en, manual1, auto1, ""}, - {dis, en, manual1, auto1, ""}, + {false, false, nil, auto1, "add"}, + {false, true, auto1, auto1, "rm"}, + {false, false, auto1, auto2, "rm add"}, + {false, false, auto1, nil, "rm"}, + {false, false, nil, manual1, "add"}, + {true, true, nil, manual1, "add"}, + {false, true, auto1, manual2, "rm add"}, + {false, false, manual2, nil, "rm"}, + {false, false, manual2, auto1, "rm add"}, + {false, false, manual1, auto1, ""}, + {true, false, manual1, auto1, ""}, } for _, scenario := range scenarios { @@ -95,7 +91,7 @@ func (s *snapmgrTestSuite) TestApplyAliasesChange(c *C) { newAliases["myalias"] = scenario.newTarget } - err := snapstate.ApplyAliasesChange(s.state, "alias-snap1", scenario.status, prevAliases, scenario.newStatus, newAliases, s.fakeBackend) + err := snapstate.ApplyAliasesChange(s.state, "alias-snap1", scenario.autoDisabled, prevAliases, scenario.newAutoDisabled, newAliases, s.fakeBackend) c.Assert(err, IsNil) var add, rm []*backend.Alias @@ -134,7 +130,7 @@ func (s *snapmgrTestSuite) TestApplyAliasesChangeMulti(c *C) { "myalias1": {Auto: "alias-snap1"}, } - err := snapstate.ApplyAliasesChange(s.state, "alias-snap1", snapstate.EnabledAliases, prevAliases, snapstate.EnabledAliases, newAliases, s.fakeBackend) + err := snapstate.ApplyAliasesChange(s.state, "alias-snap1", false, prevAliases, false, newAliases, s.fakeBackend) c.Assert(err, IsNil) expected := fakeOps{ @@ -331,9 +327,9 @@ func (s *snapmgrTestSuite) TestCheckAliasesConflictsAgainstAliases(c *C) { Sequence: []*snap.SideInfo{ {RealName: "other-snap", Revision: snap.R(2)}, }, - Current: snap.R(2), - Active: true, - AliasesStatus: snapstate.DisabledAliases, + Current: snap.R(2), + Active: true, + AutoAliasesDisabled: true, Aliases: map[string]*snapstate.AliasTarget{ "alias1": {Auto: "cmd1"}, "alias2": {Auto: "cmd2"}, @@ -344,9 +340,9 @@ func (s *snapmgrTestSuite) TestCheckAliasesConflictsAgainstAliases(c *C) { Sequence: []*snap.SideInfo{ {RealName: "other-snap", Revision: snap.R(2)}, }, - Current: snap.R(2), - Active: true, - AliasesStatus: snapstate.EnabledAliases, + Current: snap.R(2), + Active: true, + AutoAliasesDisabled: false, Aliases: map[string]*snapstate.AliasTarget{ "alias2": {Auto: "cmd2"}, "alias3": {Auto: "cmd3"}, @@ -357,23 +353,23 @@ func (s *snapmgrTestSuite) TestCheckAliasesConflictsAgainstAliases(c *C) { Sequence: []*snap.SideInfo{ {RealName: "other-snap", Revision: snap.R(2)}, }, - Current: snap.R(2), - Active: true, - AliasesStatus: snapstate.DisabledAliases, + Current: snap.R(2), + Active: true, + AutoAliasesDisabled: true, Aliases: map[string]*snapstate.AliasTarget{ "alias4": {Manual: "cmd8"}, "alias5": {Auto: "cmd5"}, }, }) - confl, err := snapstate.CheckAliasesConflicts(s.state, "alias-snap", snapstate.EnabledAliases, map[string]*snapstate.AliasTarget{ + confl, err := snapstate.CheckAliasesConflicts(s.state, "alias-snap", false, map[string]*snapstate.AliasTarget{ "alias1": {Auto: "cmd1"}, "alias5": {Auto: "cmd5"}, }) c.Check(err, IsNil) c.Check(confl, IsNil) - confl, err = snapstate.CheckAliasesConflicts(s.state, "alias-snap", snapstate.EnabledAliases, map[string]*snapstate.AliasTarget{ + confl, err = snapstate.CheckAliasesConflicts(s.state, "alias-snap", false, map[string]*snapstate.AliasTarget{ "alias1": {Auto: "cmd1"}, "alias2": {Auto: "cmd2"}, "alias3": {Auto: "cmd3"}, @@ -386,7 +382,7 @@ func (s *snapmgrTestSuite) TestCheckAliasesConflictsAgainstAliases(c *C) { c.Check(which, DeepEquals, []string{"alias2", "alias3"}) c.Check(confl["other-snap3"], DeepEquals, []string{"alias4"}) - confl, err = snapstate.CheckAliasesConflicts(s.state, "alias-snap", snapstate.DisabledAliases, map[string]*snapstate.AliasTarget{ + confl, err = snapstate.CheckAliasesConflicts(s.state, "alias-snap", true, map[string]*snapstate.AliasTarget{ "alias1": {Auto: "cmd1"}, "alias2": {Auto: "cmd2"}, "alias3": {Auto: "cmd3"}, @@ -395,7 +391,7 @@ func (s *snapmgrTestSuite) TestCheckAliasesConflictsAgainstAliases(c *C) { c.Check(err, IsNil) c.Check(confl, IsNil) - confl, err = snapstate.CheckAliasesConflicts(s.state, "other-snap4", snapstate.EnabledAliases, map[string]*snapstate.AliasTarget{ + confl, err = snapstate.CheckAliasesConflicts(s.state, "other-snap4", false, map[string]*snapstate.AliasTarget{ "alias2": {Manual: "cmd12"}, }) c.Check(err, FitsTypeOf, &snapstate.AliasConflictError{}) @@ -434,30 +430,46 @@ func (s *snapmgrTestSuite) TestCheckAliasesConflictsAgainstSnaps(c *C) { SnapType: "app", }) - confl, err := snapstate.CheckAliasesConflicts(s.state, "alias-snap", snapstate.EnabledAliases, map[string]*snapstate.AliasTarget{ + confl, err := snapstate.CheckAliasesConflicts(s.state, "alias-snap", false, map[string]*snapstate.AliasTarget{ "alias1": {Auto: "cmd1"}, }) c.Check(err, IsNil) c.Check(confl, IsNil) - confl, err = snapstate.CheckAliasesConflicts(s.state, "alias-snap", snapstate.EnabledAliases, map[string]*snapstate.AliasTarget{ + confl, err = snapstate.CheckAliasesConflicts(s.state, "alias-snap", false, map[string]*snapstate.AliasTarget{ "alias1": {Auto: "cmd1"}, "some-snap": {Auto: "cmd1"}, }) c.Check(err, ErrorMatches, `cannot enable alias "some-snap" for "alias-snap", it conflicts with the command namespace of installed snap "some-snap"`) c.Check(confl, IsNil) - confl, err = snapstate.CheckAliasesConflicts(s.state, "alias-snap", snapstate.DisabledAliases, map[string]*snapstate.AliasTarget{ + confl, err = snapstate.CheckAliasesConflicts(s.state, "alias-snap", true, map[string]*snapstate.AliasTarget{ "alias1": {Auto: "cmd1"}, "some-snap": {Auto: "cmd1"}, }) c.Check(err, IsNil) c.Check(confl, IsNil) - confl, err = snapstate.CheckAliasesConflicts(s.state, "alias-snap", snapstate.EnabledAliases, map[string]*snapstate.AliasTarget{ + confl, err = snapstate.CheckAliasesConflicts(s.state, "alias-snap", false, map[string]*snapstate.AliasTarget{ "alias1": {Auto: "cmd1"}, "some-snap.foo": {Auto: "cmd1"}, }) c.Check(err, ErrorMatches, `cannot enable alias "some-snap.foo" for "alias-snap", it conflicts with the command namespace of installed snap "some-snap"`) c.Check(confl, IsNil) } + +func (s *snapmgrTestSuite) TestDisableAliases(c *C) { + aliases := map[string]*snapstate.AliasTarget{ + "alias1": {Auto: "cmd1"}, + "alias2": {Auto: "cmd2"}, + "alias3": {Manual: "manual3", Auto: "cmd3"}, + "alias4": {Manual: "manual4"}, + } + + dis := snapstate.DisableAliases(aliases) + c.Check(dis, DeepEquals, map[string]*snapstate.AliasTarget{ + "alias1": {Auto: "cmd1"}, + "alias2": {Auto: "cmd2"}, + "alias3": {Auto: "cmd3"}, + }) +} diff --git a/overlord/snapstate/backend_test.go b/overlord/snapstate/backend_test.go index 7dac836741..72c4b58da9 100644 --- a/overlord/snapstate/backend_test.go +++ b/overlord/snapstate/backend_test.go @@ -22,6 +22,7 @@ package snapstate_test import ( "errors" "fmt" + "sort" "golang.org/x/net/context" @@ -527,7 +528,23 @@ func (f *fakeSnappyBackend) MissingAliases(aliases []*backend.Alias) ([]*backend return aliases, nil } +type byAlias []*backend.Alias + +func (ba byAlias) Len() int { return len(ba) } +func (ba byAlias) Swap(i, j int) { ba[i], ba[j] = ba[j], ba[i] } +func (ba byAlias) Less(i, j int) bool { + return ba[i].Name < ba[j].Name +} + func (f *fakeSnappyBackend) UpdateAliases(add []*backend.Alias, remove []*backend.Alias) error { + if len(add) != 0 { + add = append([]*backend.Alias(nil), add...) + sort.Sort(byAlias(add)) + } + if len(remove) != 0 { + remove = append([]*backend.Alias(nil), remove...) + sort.Sort(byAlias(remove)) + } f.ops = append(f.ops, fakeOp{ op: "update-aliases", aliases: add, diff --git a/overlord/snapstate/export_test.go b/overlord/snapstate/export_test.go index 9abadae523..a702c20144 100644 --- a/overlord/snapstate/export_test.go +++ b/overlord/snapstate/export_test.go @@ -113,6 +113,8 @@ var ( CanDisable = canDisable CachedStore = cachedStore NameAndRevnoFromSnap = nameAndRevnoFromSnap + + ResetAliases = resetAliases ) func PreviousSideInfo(snapst *SnapState) *snap.SideInfo { @@ -125,4 +127,5 @@ var ( AutoAliasesDeltaV2 = autoAliasesDeltaV2 RefreshAliases = refreshAliases CheckAliasesConflicts = checkAliasesConflicts + DisableAliases = disableAliases ) diff --git a/overlord/snapstate/handlers.go b/overlord/snapstate/handlers.go index 35c438c0a8..9f7655c0f5 100644 --- a/overlord/snapstate/handlers.go +++ b/overlord/snapstate/handlers.go @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -30,7 +30,6 @@ import ( "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/logger" - "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord/configstate/config" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/release" @@ -148,10 +147,6 @@ func (m *SnapManager) undoPrepareSnap(t *state.Task, _ *tomb.Tomb) error { return err } - // report this error to an error tracker - if osutil.GetenvBool("SNAPPY_TESTING") { - return nil - } if snapsup.SideInfo == nil || snapsup.SideInfo.RealName == "" { return nil } @@ -824,3 +819,194 @@ func (m *SnapManager) doDiscardSnap(t *state.Task, _ *tomb.Tomb) error { Set(st, snapsup.Name(), snapst) return nil } + +// aliases v2 + +func (m *SnapManager) doSetAutoAliasesV2(t *state.Task, _ *tomb.Tomb) error { + st := t.State() + st.Lock() + defer st.Unlock() + snapsup, snapst, err := snapSetupAndState(t) + if err != nil { + return err + } + snapName := snapsup.Name() + curInfo, err := snapst.CurrentInfo() + if err != nil { + return err + } + + curAliases := snapst.Aliases + // TODO: implement --prefer/--unaliased logic + newAliases, err := refreshAliases(st, curInfo, curAliases) + if err != nil { + return err + } + _, err = checkAliasesConflicts(st, snapName, snapst.AutoAliasesDisabled, newAliases) + if err != nil { + return err + } + + t.Set("old-aliases-v2", curAliases) + // noop, except on first install where we need to set this here + snapst.AliasesPending = true + snapst.Aliases = newAliases + Set(st, snapName, snapst) + return nil +} + +func (m *SnapManager) doRemoveAliasesV2(t *state.Task, _ *tomb.Tomb) error { + st := t.State() + st.Lock() + defer st.Unlock() + snapsup, snapst, err := snapSetupAndState(t) + if err != nil { + return err + } + snapName := snapsup.Name() + + err = m.backend.RemoveSnapAliases(snapName) + if err != nil { + return err + } + + snapst.AliasesPending = true + Set(st, snapName, snapst) + return nil +} + +func (m *SnapManager) doSetupAliasesV2(t *state.Task, _ *tomb.Tomb) error { + st := t.State() + st.Lock() + defer st.Unlock() + snapsup, snapst, err := snapSetupAndState(t) + if err != nil { + return err + } + snapName := snapsup.Name() + curAliases := snapst.Aliases + + err = applyAliasesChange(st, snapName, true, nil, snapst.AutoAliasesDisabled, curAliases, m.backend) + if err != nil { + return err + } + + snapst.AliasesPending = false + Set(st, snapName, snapst) + return nil +} + +func (m *SnapManager) doRefreshAliasesV2(t *state.Task, _ *tomb.Tomb) error { + // TODO: share code with doSetAutoAliasesV2 once the logic is more complex + st := t.State() + st.Lock() + defer st.Unlock() + snapsup, snapst, err := snapSetupAndState(t) + if err != nil { + return err + } + snapName := snapsup.Name() + curInfo, err := snapst.CurrentInfo() + if err != nil { + return err + } + + autoDisabled := snapst.AutoAliasesDisabled + curAliases := snapst.Aliases + // TODO: implement --prefer/--unaliased logic + newAliases, err := refreshAliases(st, curInfo, curAliases) + if err != nil { + return err + } + _, err = checkAliasesConflicts(st, snapName, autoDisabled, newAliases) + if err != nil { + return err + } + + if !snapst.AliasesPending { + if err := applyAliasesChange(st, snapName, autoDisabled, curAliases, autoDisabled, newAliases, m.backend); err != nil { + return err + } + } + + t.Set("old-aliases-v2", curAliases) + snapst.Aliases = newAliases + Set(st, snapName, snapst) + return nil +} + +func (m *SnapManager) undoRefreshAliasesV2(t *state.Task, _ *tomb.Tomb) error { + st := t.State() + st.Lock() + defer st.Unlock() + var oldAliases map[string]*AliasTarget + err := t.Get("old-aliases-v2", &oldAliases) + if err == state.ErrNoState { + // nothing to do + return nil + } + if err != nil { + return err + } + snapsup, snapst, err := snapSetupAndState(t) + if err != nil { + return err + } + snapName := snapsup.Name() + curAutoDisabled := snapst.AutoAliasesDisabled + autoDisabled := curAutoDisabled + + // check if the old states creates conflicts now + _, err = checkAliasesConflicts(st, snapName, autoDisabled, oldAliases) + if _, ok := err.(*AliasConflictError); ok { + // best we can do is reinstate with all aliases disabled + t.Errorf("cannot reinstate alias state because of conflicts, disabling: %v", err) + oldAliases = disableAliases(oldAliases) + autoDisabled = true + } else if err != nil { + return err + } + + if !snapst.AliasesPending { + curAliases := snapst.Aliases + if err := applyAliasesChange(st, snapName, curAutoDisabled, curAliases, autoDisabled, oldAliases, m.backend); err != nil { + return err + } + } + + snapst.AutoAliasesDisabled = autoDisabled + snapst.Aliases = oldAliases + Set(st, snapName, snapst) + return nil +} + +func (m *SnapManager) doPruneAutoAliasesV2(t *state.Task, _ *tomb.Tomb) error { + st := t.State() + st.Lock() + defer st.Unlock() + snapsup, snapst, err := snapSetupAndState(t) + if err != nil { + return err + } + var which []string + err = t.Get("aliases", &which) + if err != nil { + return err + } + snapName := snapsup.Name() + autoDisabled := snapst.AutoAliasesDisabled + curAliases := snapst.Aliases + + newAliases := pruneAutoAliases(st, curAliases, which) + + if !snapst.AliasesPending { + if err := applyAliasesChange(st, snapName, autoDisabled, curAliases, autoDisabled, newAliases, m.backend); err != nil { + return err + } + } + + t.Set("old-aliases-v2", curAliases) + snapst.Aliases = newAliases + Set(st, snapName, snapst) + return nil +} diff --git a/overlord/snapstate/handlers_aliasesv2_test.go b/overlord/snapstate/handlers_aliasesv2_test.go new file mode 100644 index 0000000000..ed62eb55ff --- /dev/null +++ b/overlord/snapstate/handlers_aliasesv2_test.go @@ -0,0 +1,1372 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016-2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package snapstate_test + +import ( + . "gopkg.in/check.v1" + "gopkg.in/tomb.v2" + + "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/snapstate/backend" + "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/snap" +) + +func (s *snapmgrTestSuite) TestDoSetAutoAliasesV2(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.AutoAliases = func(st *state.State, info *snap.Info) (map[string]string, error) { + c.Check(info.Name(), Equals, "alias-snap") + return map[string]string{ + "alias1": "cmd1", + "alias2": "cmd2", + "alias4": "cmd4", + }, nil + } + + snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "alias-snap", Revision: snap.R(11)}, + }, + Current: snap.R(11), + Active: true, + AutoAliasesDisabled: false, + AliasesPending: true, + Aliases: map[string]*snapstate.AliasTarget{ + "alias1": {Auto: "cmd1"}, + "alias2": {Auto: "cmd2x"}, + "alias3": {Auto: "cmd3"}, + }, + }) + + t := s.state.NewTask("set-auto-aliases-v2", "test") + t.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{RealName: "alias-snap"}, + }) + chg := s.state.NewChange("dummy", "...") + chg.AddTask(t) + + s.state.Unlock() + + s.snapmgr.Ensure() + s.snapmgr.Wait() + + s.state.Lock() + + c.Check(t.Status(), Equals, state.DoneStatus, Commentf("%v", chg.Err())) + + var snapst snapstate.SnapState + err := snapstate.Get(s.state, "alias-snap", &snapst) + c.Assert(err, IsNil) + + c.Check(snapst.Aliases, DeepEquals, map[string]*snapstate.AliasTarget{ + "alias1": {Auto: "cmd1"}, + "alias2": {Auto: "cmd2"}, + "alias4": {Auto: "cmd4"}, + }) +} + +func (s *snapmgrTestSuite) TestDoSetAutoAliasesV2FirstInstall(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.AutoAliases = func(st *state.State, info *snap.Info) (map[string]string, error) { + c.Check(info.Name(), Equals, "alias-snap") + return map[string]string{ + "alias1": "cmd1", + "alias2": "cmd2", + "alias4": "cmd4", + }, nil + } + + snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "alias-snap", Revision: snap.R(11)}, + }, + Current: snap.R(11), + Active: true, + }) + + t := s.state.NewTask("set-auto-aliases-v2", "test") + t.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{RealName: "alias-snap"}, + }) + chg := s.state.NewChange("dummy", "...") + chg.AddTask(t) + + s.state.Unlock() + + s.snapmgr.Ensure() + s.snapmgr.Wait() + + s.state.Lock() + + c.Check(t.Status(), Equals, state.DoneStatus, Commentf("%v", chg.Err())) + + var snapst snapstate.SnapState + err := snapstate.Get(s.state, "alias-snap", &snapst) + c.Assert(err, IsNil) + + c.Check(snapst.AutoAliasesDisabled, Equals, false) + c.Check(snapst.AliasesPending, Equals, true) + c.Check(snapst.Aliases, DeepEquals, map[string]*snapstate.AliasTarget{ + "alias1": {Auto: "cmd1"}, + "alias2": {Auto: "cmd2"}, + "alias4": {Auto: "cmd4"}, + }) +} + +func (s *snapmgrTestSuite) TestDoUndoSetAutoAliasesV2(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.AutoAliases = func(st *state.State, info *snap.Info) (map[string]string, error) { + c.Check(info.Name(), Equals, "alias-snap") + return map[string]string{ + "alias1": "cmd1", + "alias2": "cmd2", + "alias4": "cmd4", + }, nil + } + + snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "alias-snap", Revision: snap.R(11)}, + }, + Current: snap.R(11), + Active: true, + AliasesPending: true, + Aliases: map[string]*snapstate.AliasTarget{ + "alias1": {Auto: "cmd1"}, + "alias2": {Auto: "cmd2x"}, + "alias3": {Auto: "cmd3"}, + }, + }) + + t := s.state.NewTask("set-auto-aliases-v2", "test") + t.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{RealName: "alias-snap"}, + }) + chg := s.state.NewChange("dummy", "...") + chg.AddTask(t) + + terr := s.state.NewTask("error-trigger", "provoking total undo") + terr.WaitFor(t) + chg.AddTask(terr) + + s.state.Unlock() + + for i := 0; i < 3; i++ { + s.snapmgr.Ensure() + s.snapmgr.Wait() + } + + s.state.Lock() + + c.Check(t.Status(), Equals, state.UndoneStatus, Commentf("%v", chg.Err())) + + var snapst snapstate.SnapState + err := snapstate.Get(s.state, "alias-snap", &snapst) + c.Assert(err, IsNil) + + c.Check(snapst.AliasesPending, Equals, true) + c.Check(snapst.Aliases, DeepEquals, map[string]*snapstate.AliasTarget{ + "alias1": {Auto: "cmd1"}, + "alias2": {Auto: "cmd2x"}, + "alias3": {Auto: "cmd3"}, + }) +} + +func (s *snapmgrTestSuite) TestDoSetAutoAliasesV2Conflict(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.AutoAliases = func(st *state.State, info *snap.Info) (map[string]string, error) { + c.Check(info.Name(), Equals, "alias-snap") + return map[string]string{ + "alias1": "cmd1", + "alias2": "cmd2", + "alias4": "cmd4", + }, nil + } + + snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "alias-snap", Revision: snap.R(11)}, + }, + Current: snap.R(11), + Active: true, + AliasesPending: true, + Aliases: map[string]*snapstate.AliasTarget{ + "alias1": {Auto: "cmd1"}, + "alias2": {Auto: "cmd2x"}, + "alias3": {Auto: "cmd3"}, + }, + }) + snapstate.Set(s.state, "other-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "other-snap", Revision: snap.R(3)}, + }, + Current: snap.R(3), + Active: true, + AliasesPending: true, + Aliases: map[string]*snapstate.AliasTarget{ + "alias4": {Auto: "cmd4"}, + }, + }) + + t := s.state.NewTask("set-auto-aliases-v2", "test") + t.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{RealName: "alias-snap"}, + }) + chg := s.state.NewChange("dummy", "...") + chg.AddTask(t) + + s.state.Unlock() + + s.snapmgr.Ensure() + s.snapmgr.Wait() + + s.state.Lock() + + c.Check(t.Status(), Equals, state.ErrorStatus, Commentf("%v", chg.Err())) + c.Check(chg.Err(), ErrorMatches, `(?s).*cannot enable alias "alias4" for "alias-snap", already enabled for "other-snap".*`) +} + +func (s *snapmgrTestSuite) TestDoUndoSetAutoAliasesV2Conflict(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.AutoAliases = func(st *state.State, info *snap.Info) (map[string]string, error) { + c.Check(info.Name(), Equals, "alias-snap") + return map[string]string{ + "alias1": "cmd1", + "alias2": "cmd2", + "alias4": "cmd4", + }, nil + } + + snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "alias-snap", Revision: snap.R(11)}, + }, + Current: snap.R(11), + Active: true, + AliasesPending: true, + Aliases: map[string]*snapstate.AliasTarget{ + "alias1": {Auto: "cmd1"}, + "alias2": {Auto: "cmd2x"}, + "alias3": {Auto: "cmd3"}, + }, + }) + + otherSnapState := &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "other-snap", Revision: snap.R(3)}, + }, + Current: snap.R(3), + Active: true, + AutoAliasesDisabled: false, + Aliases: map[string]*snapstate.AliasTarget{ + "alias5": {Auto: "cmd5"}, + }, + } + snapstate.Set(s.state, "other-snap", otherSnapState) + + grabAlias3 := func(t *state.Task, _ *tomb.Tomb) error { + st := t.State() + st.Lock() + defer st.Unlock() + + otherSnapState.Aliases = map[string]*snapstate.AliasTarget{ + "alias3": {Auto: "cmd3"}, + "alias5": {Auto: "cmd5"}, + } + snapstate.Set(s.state, "other-snap", otherSnapState) + + return nil + } + s.snapmgr.AddAdhocTaskHandler("grab-alias3", grabAlias3, nil) + + t := s.state.NewTask("set-auto-aliases-v2", "test") + t.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{RealName: "alias-snap"}, + }) + chg := s.state.NewChange("dummy", "...") + chg.AddTask(t) + + tgrab3 := s.state.NewTask("grab-alias3", "grab alias3 for other-snap") + tgrab3.WaitFor(t) + chg.AddTask(tgrab3) + + terr := s.state.NewTask("error-trigger", "provoking total undo") + terr.WaitFor(t) + chg.AddTask(terr) + + s.state.Unlock() + + for i := 0; i < 5; i++ { + s.snapmgr.Ensure() + s.snapmgr.Wait() + } + + s.state.Lock() + + c.Check(t.Status(), Equals, state.UndoneStatus, Commentf("%v", chg.Err())) + + var snapst snapstate.SnapState + err := snapstate.Get(s.state, "alias-snap", &snapst) + c.Assert(err, IsNil) + + c.Check(snapst.AutoAliasesDisabled, Equals, true) + c.Check(snapst.AliasesPending, Equals, true) + c.Check(snapst.Aliases, DeepEquals, map[string]*snapstate.AliasTarget{ + "alias1": {Auto: "cmd1"}, + "alias2": {Auto: "cmd2x"}, + "alias3": {Auto: "cmd3"}, + }) + + c.Check(t.Log(), HasLen, 1) + c.Check(t.Log()[0], Matches, `.* ERROR cannot reinstate alias state because of conflicts, disabling: cannot enable alias "alias3" for "alias-snap", already enabled for "other-snap".*`) +} + +func (s *snapmgrTestSuite) TestDoSetupAliasesV2(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "alias-snap", Revision: snap.R(11)}, + }, + Current: snap.R(11), + Active: true, + AutoAliasesDisabled: true, + AliasesPending: true, + Aliases: map[string]*snapstate.AliasTarget{ + "manual1": {Manual: "cmd1"}, + }, + }) + + t := s.state.NewTask("setup-aliases-v2", "test") + t.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{RealName: "alias-snap"}, + }) + chg := s.state.NewChange("dummy", "...") + chg.AddTask(t) + + s.state.Unlock() + + s.snapmgr.Ensure() + s.snapmgr.Wait() + + s.state.Lock() + + c.Check(t.Status(), Equals, state.DoneStatus) + expected := fakeOps{ + { + op: "update-aliases", + aliases: []*backend.Alias{{"manual1", "alias-snap.cmd1"}}, + }, + } + // start with an easier-to-read error if this fails: + c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) + c.Assert(s.fakeBackend.ops, DeepEquals, expected) + + var snapst snapstate.SnapState + err := snapstate.Get(s.state, "alias-snap", &snapst) + c.Assert(err, IsNil) + + c.Check(snapst.AutoAliasesDisabled, Equals, true) + c.Check(snapst.AliasesPending, Equals, false) +} + +func (s *snapmgrTestSuite) TestDoUndoSetupAliasesV2(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "alias-snap", Revision: snap.R(11)}, + }, + Current: snap.R(11), + Active: true, + AutoAliasesDisabled: true, + AliasesPending: true, + Aliases: map[string]*snapstate.AliasTarget{ + "manual1": {Manual: "cmd1"}, + }, + }) + + t := s.state.NewTask("setup-aliases-v2", "test") + t.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{RealName: "alias-snap"}, + }) + chg := s.state.NewChange("dummy", "...") + chg.AddTask(t) + + terr := s.state.NewTask("error-trigger", "provoking total undo") + terr.WaitFor(t) + chg.AddTask(terr) + + s.state.Unlock() + + for i := 0; i < 3; i++ { + s.snapmgr.Ensure() + s.snapmgr.Wait() + } + + s.state.Lock() + + c.Check(t.Status(), Equals, state.UndoneStatus) + expected := fakeOps{ + { + op: "update-aliases", + aliases: []*backend.Alias{{"manual1", "alias-snap.cmd1"}}, + }, + { + op: "remove-snap-aliases", + name: "alias-snap", + }, + } + // start with an easier-to-read error if this fails: + c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) + c.Assert(s.fakeBackend.ops, DeepEquals, expected) + + var snapst snapstate.SnapState + err := snapstate.Get(s.state, "alias-snap", &snapst) + c.Assert(err, IsNil) + + c.Check(snapst.AutoAliasesDisabled, Equals, true) + c.Check(snapst.AliasesPending, Equals, true) +} + +// TODO: test and fix idempotence of setup-aliases + +func (s *snapmgrTestSuite) TestDoSetupAliasesV2Auto(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "alias-snap", Revision: snap.R(11)}, + }, + Current: snap.R(11), + Active: true, + AutoAliasesDisabled: false, + AliasesPending: true, + Aliases: map[string]*snapstate.AliasTarget{ + "alias1": {Auto: "cmd1"}, + }, + }) + + t := s.state.NewTask("setup-aliases-v2", "test") + t.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{RealName: "alias-snap"}, + }) + chg := s.state.NewChange("dummy", "...") + chg.AddTask(t) + + s.state.Unlock() + + s.snapmgr.Ensure() + s.snapmgr.Wait() + + s.state.Lock() + + c.Check(t.Status(), Equals, state.DoneStatus) + expected := fakeOps{ + { + op: "update-aliases", + aliases: []*backend.Alias{{"alias1", "alias-snap.cmd1"}}, + }, + } + // start with an easier-to-read error if this fails: + c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) + c.Assert(s.fakeBackend.ops, DeepEquals, expected) + + var snapst snapstate.SnapState + err := snapstate.Get(s.state, "alias-snap", &snapst) + c.Assert(err, IsNil) + + c.Check(snapst.AutoAliasesDisabled, Equals, false) + c.Check(snapst.AliasesPending, Equals, false) +} + +func (s *snapmgrTestSuite) TestDoUndoSetupAliasesV2Auto(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "alias-snap", Revision: snap.R(11)}, + }, + Current: snap.R(11), + Active: true, + AutoAliasesDisabled: false, + AliasesPending: true, + Aliases: map[string]*snapstate.AliasTarget{ + "alias1": {Auto: "cmd1"}, + }, + }) + + t := s.state.NewTask("setup-aliases-v2", "test") + t.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{RealName: "alias-snap"}, + }) + chg := s.state.NewChange("dummy", "...") + chg.AddTask(t) + + terr := s.state.NewTask("error-trigger", "provoking total undo") + terr.WaitFor(t) + chg.AddTask(terr) + + s.state.Unlock() + + for i := 0; i < 3; i++ { + s.snapmgr.Ensure() + s.snapmgr.Wait() + } + + s.state.Lock() + + c.Check(t.Status(), Equals, state.UndoneStatus) + expected := fakeOps{ + { + op: "update-aliases", + aliases: []*backend.Alias{{"alias1", "alias-snap.cmd1"}}, + }, + { + op: "remove-snap-aliases", + name: "alias-snap", + }, + } + // start with an easier-to-read error if this fails: + c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) + c.Assert(s.fakeBackend.ops, DeepEquals, expected) + + var snapst snapstate.SnapState + err := snapstate.Get(s.state, "alias-snap", &snapst) + c.Assert(err, IsNil) + + c.Check(snapst.AutoAliasesDisabled, Equals, false) + c.Check(snapst.AliasesPending, Equals, true) +} + +func (s *snapmgrTestSuite) TestDoSetupAliasesV2Nothing(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "alias-snap", Revision: snap.R(11)}, + }, + Current: snap.R(11), + Active: true, + }) + + t := s.state.NewTask("setup-aliases-v2", "test") + t.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{RealName: "alias-snap"}, + }) + chg := s.state.NewChange("dummy", "...") + chg.AddTask(t) + + s.state.Unlock() + + for i := 0; i < 3; i++ { + s.snapmgr.Ensure() + s.snapmgr.Wait() + } + + s.state.Lock() + + c.Check(t.Status(), Equals, state.DoneStatus) + expected := fakeOps{ + { + op: "update-aliases", + }, + } + // start with an easier-to-read error if this fails: + c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) + c.Assert(s.fakeBackend.ops, DeepEquals, expected) + + var snapst snapstate.SnapState + err := snapstate.Get(s.state, "alias-snap", &snapst) + c.Assert(err, IsNil) + + c.Check(snapst.AutoAliasesDisabled, Equals, false) + c.Check(snapst.AliasesPending, Equals, false) +} + +func (s *snapmgrTestSuite) TestDoUndoSetupAliasesV2Nothing(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "alias-snap", Revision: snap.R(11)}, + }, + Current: snap.R(11), + Active: true, + }) + + t := s.state.NewTask("setup-aliases-v2", "test") + t.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{RealName: "alias-snap"}, + }) + chg := s.state.NewChange("dummy", "...") + chg.AddTask(t) + + terr := s.state.NewTask("error-trigger", "provoking total undo") + terr.WaitFor(t) + chg.AddTask(terr) + + s.state.Unlock() + + for i := 0; i < 3; i++ { + s.snapmgr.Ensure() + s.snapmgr.Wait() + } + + s.state.Lock() + + c.Check(t.Status(), Equals, state.UndoneStatus) + expected := fakeOps{ + { + op: "update-aliases", + }, + { + op: "remove-snap-aliases", + name: "alias-snap", + }, + } + // start with an easier-to-read error if this fails: + c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) + c.Assert(s.fakeBackend.ops, DeepEquals, expected) + + var snapst snapstate.SnapState + err := snapstate.Get(s.state, "alias-snap", &snapst) + c.Assert(err, IsNil) + + c.Check(snapst.AutoAliasesDisabled, Equals, false) + c.Check(snapst.AliasesPending, Equals, true) +} + +func (s *snapmgrTestSuite) TestDoPruneAutoAliasesV2Auto(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "alias-snap", Revision: snap.R(11)}, + }, + Current: snap.R(11), + Active: true, + AliasesPending: false, + Aliases: map[string]*snapstate.AliasTarget{ + "alias1": {Auto: "cmd1"}, + "alias2": {Auto: "cmd2"}, + "alias3": {Auto: "cmd3"}, + }, + }) + + t := s.state.NewTask("prune-auto-aliases-v2", "test") + t.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{RealName: "alias-snap"}, + }) + t.Set("aliases", []string{"alias2", "alias3"}) + chg := s.state.NewChange("dummy", "...") + chg.AddTask(t) + + s.state.Unlock() + + s.snapmgr.Ensure() + s.snapmgr.Wait() + + s.state.Lock() + + c.Check(t.Status(), Equals, state.DoneStatus, Commentf("%v", chg.Err())) + + expected := fakeOps{ + { + op: "update-aliases", + rmAliases: []*backend.Alias{ + {"alias2", "alias-snap.cmd2"}, + {"alias3", "alias-snap.cmd3"}, + }, + }, + } + // start with an easier-to-read error if this fails: + c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) + c.Assert(s.fakeBackend.ops, DeepEquals, expected) + + var snapst snapstate.SnapState + err := snapstate.Get(s.state, "alias-snap", &snapst) + c.Assert(err, IsNil) + + c.Check(snapst.Aliases, DeepEquals, map[string]*snapstate.AliasTarget{ + "alias1": {Auto: "cmd1"}, + }) +} + +func (s *snapmgrTestSuite) TestDoPruneAutoAliasesV2AutoPending(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "alias-snap", Revision: snap.R(11)}, + }, + Current: snap.R(11), + Active: true, + AliasesPending: true, + Aliases: map[string]*snapstate.AliasTarget{ + "alias1": {Auto: "cmd1"}, + "alias2": {Auto: "cmd2"}, + "alias3": {Auto: "cmd3"}, + }, + }) + + t := s.state.NewTask("prune-auto-aliases-v2", "test") + t.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{RealName: "alias-snap"}, + }) + t.Set("aliases", []string{"alias2", "alias3"}) + chg := s.state.NewChange("dummy", "...") + chg.AddTask(t) + + s.state.Unlock() + + s.snapmgr.Ensure() + s.snapmgr.Wait() + + s.state.Lock() + + c.Check(t.Status(), Equals, state.DoneStatus, Commentf("%v", chg.Err())) + + // pending: nothing to do on disk + c.Assert(s.fakeBackend.ops, HasLen, 0) + + var snapst snapstate.SnapState + err := snapstate.Get(s.state, "alias-snap", &snapst) + c.Assert(err, IsNil) + + c.Check(snapst.Aliases, DeepEquals, map[string]*snapstate.AliasTarget{ + "alias1": {Auto: "cmd1"}, + }) +} + +func (s *snapmgrTestSuite) TestDoPruneAutoAliasesV2ManualAndDisabled(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "alias-snap", Revision: snap.R(11)}, + }, + Current: snap.R(11), + Active: true, + AutoAliasesDisabled: true, + AliasesPending: false, + Aliases: map[string]*snapstate.AliasTarget{ + "alias1": {Auto: "cmd1"}, + "alias2": {Auto: "cmd2"}, + "alias3": {Manual: "cmdx", Auto: "cmd3"}, + "alias4": {Manual: "cmd4"}, + }, + }) + + t := s.state.NewTask("prune-auto-aliases-v2", "test") + t.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{RealName: "alias-snap"}, + }) + t.Set("aliases", []string{"alias2", "alias3", "alias4"}) + chg := s.state.NewChange("dummy", "...") + chg.AddTask(t) + + s.state.Unlock() + + s.snapmgr.Ensure() + s.snapmgr.Wait() + + s.state.Lock() + + c.Check(t.Status(), Equals, state.DoneStatus, Commentf("%v", chg.Err())) + + expected := fakeOps{ + { + op: "update-aliases", + }, + } + // start with an easier-to-read error if this fails: + c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) + c.Assert(s.fakeBackend.ops, DeepEquals, expected) + + var snapst snapstate.SnapState + err := snapstate.Get(s.state, "alias-snap", &snapst) + c.Assert(err, IsNil) + + c.Check(snapst.Aliases, DeepEquals, map[string]*snapstate.AliasTarget{ + "alias1": {Auto: "cmd1"}, + "alias3": {Manual: "cmdx"}, + "alias4": {Manual: "cmd4"}, + }) +} + +func (s *snapmgrTestSuite) TestDoRefreshAliasesV2(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.AutoAliases = func(st *state.State, info *snap.Info) (map[string]string, error) { + c.Check(info.Name(), Equals, "alias-snap") + return map[string]string{ + "alias1": "cmd1", + "alias2": "cmd2", + "alias4": "cmd4", + }, nil + } + + snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "alias-snap", Revision: snap.R(11)}, + }, + Current: snap.R(11), + Active: true, + AliasesPending: false, + Aliases: map[string]*snapstate.AliasTarget{ + "alias1": {Auto: "cmd1"}, + "alias2": {Auto: "cmd2x"}, + "alias3": {Auto: "cmd3"}, + }, + }) + + t := s.state.NewTask("refresh-aliases-v2", "test") + t.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{RealName: "alias-snap"}, + }) + chg := s.state.NewChange("dummy", "...") + chg.AddTask(t) + + s.state.Unlock() + + s.snapmgr.Ensure() + s.snapmgr.Wait() + + s.state.Lock() + + c.Check(t.Status(), Equals, state.DoneStatus, Commentf("%v", chg.Err())) + + expected := fakeOps{ + { + op: "update-aliases", + rmAliases: []*backend.Alias{ + {"alias2", "alias-snap.cmd2x"}, + {"alias3", "alias-snap.cmd3"}, + }, + aliases: []*backend.Alias{ + {"alias2", "alias-snap.cmd2"}, + {"alias4", "alias-snap.cmd4"}, + }, + }, + } + // start with an easier-to-read error if this fails: + c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) + c.Assert(s.fakeBackend.ops, DeepEquals, expected) + + var snapst snapstate.SnapState + err := snapstate.Get(s.state, "alias-snap", &snapst) + c.Assert(err, IsNil) + + c.Check(snapst.AutoAliasesDisabled, Equals, false) + c.Check(snapst.AliasesPending, Equals, false) + c.Check(snapst.Aliases, DeepEquals, map[string]*snapstate.AliasTarget{ + "alias1": {Auto: "cmd1"}, + "alias2": {Auto: "cmd2"}, + "alias4": {Auto: "cmd4"}, + }) +} + +func (s *snapmgrTestSuite) TestDoUndoRefreshAliasesV2(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.AutoAliases = func(st *state.State, info *snap.Info) (map[string]string, error) { + c.Check(info.Name(), Equals, "alias-snap") + return map[string]string{ + "alias1": "cmd1", + "alias2": "cmd2", + "alias4": "cmd4", + }, nil + } + + snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "alias-snap", Revision: snap.R(11)}, + }, + Current: snap.R(11), + Active: true, + AliasesPending: false, + Aliases: map[string]*snapstate.AliasTarget{ + "alias1": {Auto: "cmd1"}, + "alias2": {Auto: "cmd2x"}, + "alias3": {Auto: "cmd3"}, + }, + }) + + t := s.state.NewTask("refresh-aliases-v2", "test") + t.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{RealName: "alias-snap"}, + }) + chg := s.state.NewChange("dummy", "...") + chg.AddTask(t) + + terr := s.state.NewTask("error-trigger", "provoking total undo") + terr.WaitFor(t) + chg.AddTask(terr) + + s.state.Unlock() + + for i := 0; i < 3; i++ { + s.snapmgr.Ensure() + s.snapmgr.Wait() + } + + s.state.Lock() + + c.Check(t.Status(), Equals, state.UndoneStatus, Commentf("%v", chg.Err())) + + expected := fakeOps{ + { + op: "update-aliases", + rmAliases: []*backend.Alias{ + {"alias2", "alias-snap.cmd2x"}, + {"alias3", "alias-snap.cmd3"}, + }, + aliases: []*backend.Alias{ + {"alias2", "alias-snap.cmd2"}, + {"alias4", "alias-snap.cmd4"}, + }, + }, + { + op: "update-aliases", + aliases: []*backend.Alias{ + {"alias2", "alias-snap.cmd2x"}, + {"alias3", "alias-snap.cmd3"}, + }, + rmAliases: []*backend.Alias{ + {"alias2", "alias-snap.cmd2"}, + {"alias4", "alias-snap.cmd4"}, + }, + }, + } + // start with an easier-to-read error if this fails: + c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) + c.Assert(s.fakeBackend.ops, DeepEquals, expected) + + var snapst snapstate.SnapState + err := snapstate.Get(s.state, "alias-snap", &snapst) + c.Assert(err, IsNil) + + c.Check(snapst.AutoAliasesDisabled, Equals, false) + c.Check(snapst.AliasesPending, Equals, false) + c.Check(snapst.Aliases, DeepEquals, map[string]*snapstate.AliasTarget{ + "alias1": {Auto: "cmd1"}, + "alias2": {Auto: "cmd2x"}, + "alias3": {Auto: "cmd3"}, + }) +} + +func (s *snapmgrTestSuite) TestDoUndoRefreshAliasesV2FromEmpty(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.AutoAliases = func(st *state.State, info *snap.Info) (map[string]string, error) { + c.Check(info.Name(), Equals, "alias-snap") + return map[string]string{ + "alias1": "cmd1", + "alias2": "cmd2", + "alias4": "cmd4", + }, nil + } + + snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "alias-snap", Revision: snap.R(11)}, + }, + Current: snap.R(11), + Active: true, + AliasesPending: false, + }) + + t := s.state.NewTask("refresh-aliases-v2", "test") + t.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{RealName: "alias-snap"}, + }) + chg := s.state.NewChange("dummy", "...") + chg.AddTask(t) + + terr := s.state.NewTask("error-trigger", "provoking total undo") + terr.WaitFor(t) + chg.AddTask(terr) + + s.state.Unlock() + + for i := 0; i < 3; i++ { + s.snapmgr.Ensure() + s.snapmgr.Wait() + } + + s.state.Lock() + + c.Check(t.Status(), Equals, state.UndoneStatus, Commentf("%v", chg.Err())) + + expected := fakeOps{ + { + op: "update-aliases", + aliases: []*backend.Alias{ + {"alias1", "alias-snap.cmd1"}, + {"alias2", "alias-snap.cmd2"}, + {"alias4", "alias-snap.cmd4"}, + }, + }, + { + op: "update-aliases", + rmAliases: []*backend.Alias{ + {"alias1", "alias-snap.cmd1"}, + {"alias2", "alias-snap.cmd2"}, + {"alias4", "alias-snap.cmd4"}, + }, + }, + } + // start with an easier-to-read error if this fails: + c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) + c.Assert(s.fakeBackend.ops, DeepEquals, expected) + + var snapst snapstate.SnapState + err := snapstate.Get(s.state, "alias-snap", &snapst) + c.Assert(err, IsNil) + + c.Check(snapst.AutoAliasesDisabled, Equals, false) + c.Check(snapst.AliasesPending, Equals, false) + c.Check(snapst.Aliases, HasLen, 0) +} + +func (s *snapmgrTestSuite) TestDoRefreshAliasesV2Pending(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.AutoAliases = func(st *state.State, info *snap.Info) (map[string]string, error) { + c.Check(info.Name(), Equals, "alias-snap") + return map[string]string{ + "alias1": "cmd1", + "alias2": "cmd2", + "alias4": "cmd4", + }, nil + } + + snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "alias-snap", Revision: snap.R(11)}, + }, + Current: snap.R(11), + Active: true, + AliasesPending: true, + Aliases: map[string]*snapstate.AliasTarget{ + "alias1": {Auto: "cmd1"}, + "alias2": {Auto: "cmd2x"}, + "alias3": {Auto: "cmd3"}, + }, + }) + + t := s.state.NewTask("refresh-aliases-v2", "test") + t.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{RealName: "alias-snap"}, + }) + chg := s.state.NewChange("dummy", "...") + chg.AddTask(t) + + s.state.Unlock() + + s.snapmgr.Ensure() + s.snapmgr.Wait() + + s.state.Lock() + + c.Check(t.Status(), Equals, state.DoneStatus, Commentf("%v", chg.Err())) + + // pending: nothing to do on disk + c.Assert(s.fakeBackend.ops, HasLen, 0) + + var snapst snapstate.SnapState + err := snapstate.Get(s.state, "alias-snap", &snapst) + c.Assert(err, IsNil) + + c.Check(snapst.AutoAliasesDisabled, Equals, false) + c.Check(snapst.AliasesPending, Equals, true) + c.Check(snapst.Aliases, DeepEquals, map[string]*snapstate.AliasTarget{ + "alias1": {Auto: "cmd1"}, + "alias2": {Auto: "cmd2"}, + "alias4": {Auto: "cmd4"}, + }) +} + +func (s *snapmgrTestSuite) TestDoUndoRefreshAliasesV2Pending(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.AutoAliases = func(st *state.State, info *snap.Info) (map[string]string, error) { + c.Check(info.Name(), Equals, "alias-snap") + return map[string]string{ + "alias1": "cmd1", + "alias2": "cmd2", + "alias4": "cmd4", + }, nil + } + + snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "alias-snap", Revision: snap.R(11)}, + }, + Current: snap.R(11), + Active: true, + AliasesPending: true, + Aliases: map[string]*snapstate.AliasTarget{ + "alias1": {Auto: "cmd1"}, + "alias2": {Auto: "cmd2x"}, + "alias3": {Auto: "cmd3"}, + }, + }) + + t := s.state.NewTask("refresh-aliases-v2", "test") + t.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{RealName: "alias-snap"}, + }) + chg := s.state.NewChange("dummy", "...") + chg.AddTask(t) + + terr := s.state.NewTask("error-trigger", "provoking total undo") + terr.WaitFor(t) + chg.AddTask(terr) + + s.state.Unlock() + + for i := 0; i < 3; i++ { + s.snapmgr.Ensure() + s.snapmgr.Wait() + } + + s.state.Lock() + + c.Check(t.Status(), Equals, state.UndoneStatus, Commentf("%v", chg.Err())) + + // pending: nothing to do on disk + c.Assert(s.fakeBackend.ops, HasLen, 0) + + var snapst snapstate.SnapState + err := snapstate.Get(s.state, "alias-snap", &snapst) + c.Assert(err, IsNil) + + c.Check(snapst.AutoAliasesDisabled, Equals, false) + c.Check(snapst.AliasesPending, Equals, true) + c.Check(snapst.Aliases, DeepEquals, map[string]*snapstate.AliasTarget{ + "alias1": {Auto: "cmd1"}, + "alias2": {Auto: "cmd2x"}, + "alias3": {Auto: "cmd3"}, + }) +} + +func (s *snapmgrTestSuite) TestDoRefreshAliasesV2Conflict(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.AutoAliases = func(st *state.State, info *snap.Info) (map[string]string, error) { + c.Check(info.Name(), Equals, "alias-snap") + return map[string]string{ + "alias1": "cmd1", + "alias2": "cmd2", + "alias4": "cmd4", + }, nil + } + + snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "alias-snap", Revision: snap.R(11)}, + }, + Current: snap.R(11), + Active: true, + Aliases: map[string]*snapstate.AliasTarget{ + "alias1": {Auto: "cmd1"}, + "alias2": {Auto: "cmd2x"}, + "alias3": {Auto: "cmd3"}, + }, + }) + snapstate.Set(s.state, "other-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "other-snap", Revision: snap.R(3)}, + }, + Current: snap.R(3), + Active: true, + Aliases: map[string]*snapstate.AliasTarget{ + "alias4": {Auto: "cmd4"}, + }, + }) + + t := s.state.NewTask("refresh-aliases-v2", "test") + t.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{RealName: "alias-snap"}, + }) + chg := s.state.NewChange("dummy", "...") + chg.AddTask(t) + + s.state.Unlock() + + s.snapmgr.Ensure() + s.snapmgr.Wait() + + s.state.Lock() + + c.Check(t.Status(), Equals, state.ErrorStatus, Commentf("%v", chg.Err())) + c.Check(chg.Err(), ErrorMatches, `(?s).*cannot enable alias "alias4" for "alias-snap", already enabled for "other-snap".*`) +} + +func (s *snapmgrTestSuite) TestDoUndoRefreshAliasesV2Conflict(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.AutoAliases = func(st *state.State, info *snap.Info) (map[string]string, error) { + c.Check(info.Name(), Equals, "alias-snap") + return map[string]string{ + "alias1": "cmd1", + "alias2": "cmd2", + "alias4": "cmd4", + }, nil + } + + snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "alias-snap", Revision: snap.R(11)}, + }, + Current: snap.R(11), + Active: true, + AliasesPending: false, + Aliases: map[string]*snapstate.AliasTarget{ + "alias1": {Auto: "cmd1"}, + "alias2": {Auto: "cmd2x"}, + "alias3": {Auto: "cmd3"}, + }, + }) + + grabAlias3 := func(t *state.Task, _ *tomb.Tomb) error { + st := t.State() + st.Lock() + defer st.Unlock() + + snapstate.Set(s.state, "other-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "other-snap", Revision: snap.R(3)}, + }, + Current: snap.R(3), + Active: true, + AliasesPending: false, + Aliases: map[string]*snapstate.AliasTarget{ + "alias3": {Auto: "cmd3x"}, + }, + }) + + return nil + } + + s.snapmgr.AddAdhocTaskHandler("grab-alias3", grabAlias3, nil) + + t := s.state.NewTask("refresh-aliases-v2", "test") + t.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{RealName: "alias-snap"}, + }) + chg := s.state.NewChange("dummy", "...") + chg.AddTask(t) + + tgrab3 := s.state.NewTask("grab-alias3", "grab alias3 for other-snap") + tgrab3.WaitFor(t) + chg.AddTask(tgrab3) + + terr := s.state.NewTask("error-trigger", "provoking total undo") + terr.WaitFor(t) + chg.AddTask(terr) + + s.state.Unlock() + + for i := 0; i < 5; i++ { + s.snapmgr.Ensure() + s.snapmgr.Wait() + } + + s.state.Lock() + + c.Check(t.Status(), Equals, state.UndoneStatus, Commentf("%v", chg.Err())) + + expected := fakeOps{ + { + op: "update-aliases", + rmAliases: []*backend.Alias{ + {"alias2", "alias-snap.cmd2x"}, + {"alias3", "alias-snap.cmd3"}, + }, + aliases: []*backend.Alias{ + {"alias2", "alias-snap.cmd2"}, + {"alias4", "alias-snap.cmd4"}, + }, + }, + { + op: "update-aliases", + rmAliases: []*backend.Alias{ + {"alias1", "alias-snap.cmd1"}, + {"alias2", "alias-snap.cmd2"}, + {"alias4", "alias-snap.cmd4"}, + }, + }, + } + // start with an easier-to-read error if this fails: + c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) + c.Check(s.fakeBackend.ops, DeepEquals, expected) + + var snapst snapstate.SnapState + err := snapstate.Get(s.state, "alias-snap", &snapst) + c.Assert(err, IsNil) + + c.Check(snapst.AutoAliasesDisabled, Equals, true) + c.Check(snapst.Aliases, DeepEquals, map[string]*snapstate.AliasTarget{ + "alias1": {Auto: "cmd1"}, + "alias2": {Auto: "cmd2x"}, + "alias3": {Auto: "cmd3"}, + }) + + var snapst2 snapstate.SnapState + err = snapstate.Get(s.state, "other-snap", &snapst2) + c.Assert(err, IsNil) + + c.Check(snapst2.AutoAliasesDisabled, Equals, false) + c.Check(snapst2.Aliases, DeepEquals, map[string]*snapstate.AliasTarget{ + "alias3": {Auto: "cmd3x"}, + }) + + c.Check(t.Log(), HasLen, 1) + c.Check(t.Log()[0], Matches, `.* ERROR cannot reinstate alias state because of conflicts, disabling: cannot enable alias "alias3" for "alias-snap", already enabled for "other-snap".*`) +} diff --git a/overlord/snapstate/snapmgr.go b/overlord/snapstate/snapmgr.go index ed54e01b35..a0638a57b1 100644 --- a/overlord/snapstate/snapmgr.go +++ b/overlord/snapstate/snapmgr.go @@ -135,8 +135,9 @@ type SnapState struct { Channel string `json:"channel,omitempty"` Flags // aliases, see aliasesv2.go - Aliases map[string]*AliasTarget `json:"aliases,omitempty"` - AliasesStatus AliasesStatus `json:"aliases-status,omitempty"` + Aliases map[string]*AliasTarget `json:"aliases,omitempty"` + AutoAliasesDisabled bool `json:"auto-aliases-disabled,omitempty"` + AliasesPending bool `json:"aliases-pending,omitempty"` } // Type returns the type of the snap or an error. @@ -338,9 +339,16 @@ func Manager(st *state.State) (*SnapManager, error) { runner.AddHandler("alias", m.doAlias, m.undoAlias) runner.AddHandler("clear-aliases", m.doClearAliases, m.undoClearAliases) runner.AddHandler("set-auto-aliases", m.doSetAutoAliases, m.undoClearAliases) - runner.AddHandler("setup-aliases", m.doSetupAliases, m.undoSetupAliases) + runner.AddHandler("setup-aliases", m.doSetupAliases, m.doRemoveAliases) runner.AddHandler("remove-aliases", m.doRemoveAliases, m.doSetupAliases) + // XXX: WIP: aliases v2: temporary task names to be able to write tess until switching + runner.AddHandler("set-auto-aliases-v2", m.doSetAutoAliasesV2, m.undoRefreshAliasesV2) + runner.AddHandler("setup-aliases-v2", m.doSetupAliasesV2, m.doRemoveAliasesV2) + runner.AddHandler("refresh-aliases-v2", m.doRefreshAliasesV2, m.undoRefreshAliasesV2) + runner.AddHandler("prune-auto-aliases-v2", m.doPruneAutoAliasesV2, m.undoRefreshAliasesV2) + runner.AddHandler("remove-aliases-v2", m.doRemoveAliasesV2, m.doSetupAliasesV2) + // control serialisation runner.SetBlocked(m.blockedTask) @@ -356,6 +364,7 @@ func Manager(st *state.State) (*SnapManager, error) { } func diskAliasTask(t *state.Task) bool { + // TODO: aliases v2! kind := t.Kind() return kind == "setup-aliases" || kind == "remove-aliases" || kind == "alias" } diff --git a/overlord/snapstate/snapstate.go b/overlord/snapstate/snapstate.go index 306ef7c413..7bbe490de9 100644 --- a/overlord/snapstate/snapstate.go +++ b/overlord/snapstate/snapstate.go @@ -27,6 +27,7 @@ import ( "sort" "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/i18n/dumb" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/overlord/auth" @@ -55,8 +56,12 @@ func needsMaybeCore(typ snap.Type) int { } func doInstall(st *state.State, snapst *SnapState, snapsup *SnapSetup, flags int) (*state.TaskSet, error) { - if snapsup.Flags.Classic && !release.OnClassic { - return nil, fmt.Errorf("classic confinement is only supported on classic systems") + if snapsup.Flags.Classic { + if !release.OnClassic { + return nil, fmt.Errorf("classic confinement is only supported on classic systems") + } else if !dirs.SupportsClassicConfinement() { + return nil, fmt.Errorf("classic confinement is not yet supported on your distribution") + } } if !snapst.HasCurrent() { // install? // check that the snap command namespace doesn't conflict with an enabled alias @@ -614,7 +619,7 @@ func doUpdate(st *state.State, names []string, updates []*snap.Info, params func func applyAutoAliasesDelta(st *state.State, delta map[string][]string, op string, refreshAll bool, linkTs func(snapName string, ts *state.TaskSet)) (*state.TaskSet, error) { applyTs := state.NewTaskSet() for snapName, aliases := range delta { - ts, err := ResetAliases(st, snapName, aliases) + ts, err := resetAliases(st, snapName, aliases) if err != nil { if refreshAll { // doing "refresh all", just skip this snap diff --git a/overlord/snapstate/snapstate_test.go b/overlord/snapstate/snapstate_test.go index e9696eb012..0b53424d28 100644 --- a/overlord/snapstate/snapstate_test.go +++ b/overlord/snapstate/snapstate_test.go @@ -244,6 +244,22 @@ func (s *snapmgrTestSuite) TestInstallClassicConfinementFiltering(c *C) { c.Assert(err, IsNil) } +func (s *snapmgrTestSuite) TestInstallFailsWhenClassicSnapsAreNotSupported(c *C) { + s.state.Lock() + defer s.state.Unlock() + + reset := release.MockReleaseInfo(&release.OS{ + ID: "fedora", + }) + defer func() { reset(); dirs.SetRootDir("/") }() + + dirs.SetRootDir("/") + + _, err := snapstate.Install(s.state, "some-snap", "channel-for-classic", snap.R(0), s.user.ID, snapstate.Flags{Classic: true}) + c.Assert(err, Not(IsNil)) + c.Assert(err, DeepEquals, fmt.Errorf("classic confinement is not yet supported on your distribution")) +} + func (s *snapmgrTestSuite) TestInstallTasks(c *C) { s.state.Lock() defer s.state.Unlock() @@ -1722,10 +1738,8 @@ func (s *snapmgrTestSuite) TestUpdateTotalUndoRunThrough(c *C) { name: "/snap/some-snap/11", }, { - op: "matching-aliases", - }, - { - op: "update-aliases", + op: "remove-snap-aliases", + name: "some-snap", }, { op: "unlink-snap", @@ -3763,10 +3777,8 @@ func (s *snapmgrTestSuite) TestRevertTotalUndoRunThrough(c *C) { name: "/snap/some-snap/1", }, { - op: "matching-aliases", - }, - { - op: "update-aliases", + op: "remove-snap-aliases", + name: "some-snap", }, { op: "unlink-snap", diff --git a/packaging/ubuntu-14.04/changelog b/packaging/ubuntu-14.04/changelog index 1981865620..9f50c1d28a 100644 --- a/packaging/ubuntu-14.04/changelog +++ b/packaging/ubuntu-14.04/changelog @@ -1,3 +1,177 @@ +snapd (2.24~14.04) trusty; urgency=medium + + * New upstream release, LP: #1681799: + - interfaces/mount: add InfoEntry type + - many: fix plug auto-connect during core transition + - interfaces: fold network bind into core support with tests + - .travis.yml: add option to make raw log less noisy + - interfaces: adjust shm accesses to use 'm' for updated mmap kernel + mediation + - many: rename two core plugs that clash with slot names + - snap-confine,browser-support: /dev/tty for snap-confine, misc + browser-support for gnome-shell + - store: add download test with EOF in the middle + - tests: adjust to look for network-bind-plug + - store: make hash error message more accurate + - overlord/snapstate: simplify AliasesStatus down to just an + AutoAliasesDisabled bool flag (aliases v2) + - errtracker: never send errtracker reports when running under + SNAPPY_TESTING + - interfaces/repo: validate slot/plug names + - daemon: Give the snap directories via GET /v2/system-info + - interfaces/unity7: support unity messaging menu + - interfaces/mount: add high-level Profile functions + - git: ignore only the cmd/Makefile{,.in} + - cmd: explicitly set _GNU_SOURCE and _FILE_OFFSET_BITS for xfs + support + - daemon: add desktop file location for app to the API + - overlord,release: disable classic snap support when not possible + - overlord: fix TestEnsureLoopPrune not to be so racy + - many: abstract path to /bin/{true,false} + - data/systemd: tweak data/systemd/Makefile to be slightly simpler + - store: handle EOF via url.Error check + - packaging: use templates for relevant systemd units + - tests: run gccgo only on ubuntu-16.04-64 + - .travis.yml: remove travis matrix and do a single sequential run + - overlord/state: make sure that setting to nil a state key is + equivalent to deleting it + - tests: fix incorrect shell expression + - interfaces/mount: add OptsToFlags for converting arguments to + syscall… + - interfaces: add a joystick interface + - tests: enable docker test for more ubuntu-core systems + - tests: download and install additional dependencies when using + prepackaged snapd + - many: add support for partially static builds + - interfaces: allow slot to introspect dbus-daemon in dbus + interface, allow /usr/bin/arch by default + - interfaces/mount: fix golint issues + - interfaces/mount: add function for saving fstab-like file + - osutil: introducing GetenvInt64, like GetenvBool but Int64er. + - interfaces: drop udev tagging from framebuffer interface + - snapstate: more helpers to work with aliases state (aliases + v2) + - interfaces/mount: add function for parsing fstab-like file + - cmd: disable the re-associate fix as requested by jdstrand + - overlord/snapstate: unlock/relock the state less, especially not + across mutating the SnapState of a snap + - interfaces: allow executing ld.so (needed with new AppArmor base + abstraction) + - interfaces/mount: add function for parsing mount entries + - cmd: rework header check for xfs/xqm.h + - cmd: add poky to the list of distros which don't support reexec + - overlord: finish reorg, revert "be more conservative until we have + cut 2.23.x" + - cmd: select what socket to use in cmd/snap{,ctl} + - overlord: remove snap config values when snap is removed + - snapstate: introduce helper to apply to disk a alias states change + for a snap (aliases v2) + - configstate,hookstate: timeout the configure hook after 5 mins, + report failures to the errtracker + - interfaces/seccomp: add bind as part of the default seccomp policy + for hooks + - cmd: discard the C implementation of snap-update-ns + - tests: remove stale apt proxy leftover from cloud-init + - tests: move unity test to nightly suite + - interfaces: add support for location-observe for + dbus::ObjectManager session paths + - boot: log error in KernelOrOsRebootRequired + - interfaces: remove old API + - interfaces: use udev spec + - interfaces: convert systemd backend to new APIs + - osutil: add BootID + - tests: move docker test to new nightly suite + - interfaces/mount: compute mount changes required to transition + mount profiles + - data/selinux: add context definition for snapctl + - overlord: clean up organization under state packages + - overlord: make sure all managers packages have *state.go with the + main state manipulation/query APIs + - interfaces: use spec in the dbus backend + - store: download from authenticated URL if there is a device + session set + - tests: remove core_name variable + - interfaces: rename thumbnailer to thumbnailer-service + - interfaces: add chroot to base templates + - asserts: remove some unused things + - systemd: mount the squashfs with nodev + - overlord: when shutting down assume errors might be due to + cancellation so retry + - cmd: rename all unit tests to $command/unit-test + - cmd/snap: fix help string for version command + - asserts: don't allow revocations with other items for the same + developer + - tests: skip lp-1644439 test on older kernels + - interfaces: allow "sync" to be used by core support + - assertstate,snapstate: have assertstate.AutoAliases use the + "aliases" header + - interfaces: allow writing config.txt.tmp in the core-support + interface + - tests: adjust network-bind test + - interfaces: dbus backend spec + - asserts: introduce a snap-declaration "aliases" header to list + auto aliases with explicit targets + - cmd: enable large file support + - cmd/snap: handle missing snap-confine + - cmd/snap-confine: re-associate with pid-1 mount namespace if + required + - cmd/libsnap: make mountinfo structures public + - tests: fix interfaces-cups-control for zesty + - misc: revert "Log if the system goes into ForceDevMode" + - interfaces: seccomp tests cleanup + - cmd: validate SNAP_NAME + - interfaces: log if the system goes into ForceDevMode + - tests: fix classic-ubuntu-core-transition race + - interfaces: use apparmor spec in the apparmor backend + - interfaces: alphabetize framebuffer in base decl and add it to + all_test.go + - tests: add ubuntu-core-16-32 system to the external backend and + fix docker test + - cmd/libsnap: simplify sc_string_quote default case + - osutil: fix double expand in environment map code and add test + - interfaces: extend location-control out-of-process provider + support + - cmd/snap-update-ns: use bidirectional lists for mount entries + - tests: prevent automatic transition before setting the initial + state of the test + - release: detect if we are in ForcedDevMode by inspecting the + kernel + - tests: add core-snap-refresh test + - interfaces: add maliit input method interface + - interfaces: seccomp spec API tweaks for better tests + - interfaces: updates for mir-kiosk in browser-support, mir, opengl, + unity7 + - testutils: address review feedback from PR#2997 + - tests: specify the core version to be unsquashfs'ed in the + failover tests + - interfaces: use MockInfo in tests + - cmd/libsnap: add sc_quote_string + - cmd/snap-confine: use sc_do_umount everywhere + - interfaces: add unity8 plug permissions + - timeutil: a few helpers for the recurring events + - asserts: implement snap-developer type + - partition: deal with grub{,2}-editenv in tests + - many: add new (hidden) `snap debug ensure-state-soon` command and + use in tests + - interfaces/builtin: small refactor of dbus tests + - packaging, tests: use "systemctl list-unit-files --full" + everywhere + - many: some opensuse patches that are ready to go into master + - packaging: add opensuse permissions files + - client, daemon: move "snap list" name filtering into snapd. + - interfaces: use seccomp specs + - overlord/snapstate: small cleanup of + ensureForceDevmodeDropsDevmodeFromState + - interfaces/builtin/alsa: add read access to alsa state dir + - interfaces: use spec in kmod backend, updated firewall_control, + openvswitch_support, ppp + - cmd/snap-confine: use sc_do_mount everywhere + - tests: remove workaround for docker again, snap-declaration is + fixed now + - interfaces: interface to allow autopilot introspection + + -- Michael Vogt <michael.vogt@ubuntu.com> Tue, 11 Apr 2017 13:31:46 +0200 + snapd (2.23.6~14.04) trusty; urgency=medium * New upstream release, LP: #1665608 diff --git a/packaging/ubuntu-14.04/control b/packaging/ubuntu-14.04/control index a00cf72d85..d93df23129 100644 --- a/packaging/ubuntu-14.04/control +++ b/packaging/ubuntu-14.04/control @@ -23,6 +23,7 @@ Build-Depends: autoconf, libglib2.0-dev, libseccomp-dev (>= 2.1.1-1ubuntu1~trusty3), libudev-dev, + openssh-client, pkg-config, python3, python3-docutils, @@ -60,6 +61,7 @@ Depends: adduser, cgroup-lite, gnupg1 | gnupg, linux-generic-lts-xenial, + openssh-client, squashfs-tools, # only needed on trusty to pull in the right version. sudo (>= 1.8.9p5-1ubuntu1.3), diff --git a/packaging/ubuntu-16.04/changelog b/packaging/ubuntu-16.04/changelog index adbf47eb1b..565179d023 100644 --- a/packaging/ubuntu-16.04/changelog +++ b/packaging/ubuntu-16.04/changelog @@ -1,3 +1,177 @@ +snapd (2.24) xenial; urgency=medium + + * New upstream release, LP: #1681799: + - interfaces/mount: add InfoEntry type + - many: fix plug auto-connect during core transition + - interfaces: fold network bind into core support with tests + - .travis.yml: add option to make raw log less noisy + - interfaces: adjust shm accesses to use 'm' for updated mmap kernel + mediation + - many: rename two core plugs that clash with slot names + - snap-confine,browser-support: /dev/tty for snap-confine, misc + browser-support for gnome-shell + - store: add download test with EOF in the middle + - tests: adjust to look for network-bind-plug + - store: make hash error message more accurate + - overlord/snapstate: simplify AliasesStatus down to just an + AutoAliasesDisabled bool flag (aliases v2) + - errtracker: never send errtracker reports when running under + SNAPPY_TESTING + - interfaces/repo: validate slot/plug names + - daemon: Give the snap directories via GET /v2/system-info + - interfaces/unity7: support unity messaging menu + - interfaces/mount: add high-level Profile functions + - git: ignore only the cmd/Makefile{,.in} + - cmd: explicitly set _GNU_SOURCE and _FILE_OFFSET_BITS for xfs + support + - daemon: add desktop file location for app to the API + - overlord,release: disable classic snap support when not possible + - overlord: fix TestEnsureLoopPrune not to be so racy + - many: abstract path to /bin/{true,false} + - data/systemd: tweak data/systemd/Makefile to be slightly simpler + - store: handle EOF via url.Error check + - packaging: use templates for relevant systemd units + - tests: run gccgo only on ubuntu-16.04-64 + - .travis.yml: remove travis matrix and do a single sequential run + - overlord/state: make sure that setting to nil a state key is + equivalent to deleting it + - tests: fix incorrect shell expression + - interfaces/mount: add OptsToFlags for converting arguments to + syscall… + - interfaces: add a joystick interface + - tests: enable docker test for more ubuntu-core systems + - tests: download and install additional dependencies when using + prepackaged snapd + - many: add support for partially static builds + - interfaces: allow slot to introspect dbus-daemon in dbus + interface, allow /usr/bin/arch by default + - interfaces/mount: fix golint issues + - interfaces/mount: add function for saving fstab-like file + - osutil: introducing GetenvInt64, like GetenvBool but Int64er. + - interfaces: drop udev tagging from framebuffer interface + - snapstate: more helpers to work with aliases state (aliases + v2) + - interfaces/mount: add function for parsing fstab-like file + - cmd: disable the re-associate fix as requested by jdstrand + - overlord/snapstate: unlock/relock the state less, especially not + across mutating the SnapState of a snap + - interfaces: allow executing ld.so (needed with new AppArmor base + abstraction) + - interfaces/mount: add function for parsing mount entries + - cmd: rework header check for xfs/xqm.h + - cmd: add poky to the list of distros which don't support reexec + - overlord: finish reorg, revert "be more conservative until we have + cut 2.23.x" + - cmd: select what socket to use in cmd/snap{,ctl} + - overlord: remove snap config values when snap is removed + - snapstate: introduce helper to apply to disk a alias states change + for a snap (aliases v2) + - configstate,hookstate: timeout the configure hook after 5 mins, + report failures to the errtracker + - interfaces/seccomp: add bind as part of the default seccomp policy + for hooks + - cmd: discard the C implementation of snap-update-ns + - tests: remove stale apt proxy leftover from cloud-init + - tests: move unity test to nightly suite + - interfaces: add support for location-observe for + dbus::ObjectManager session paths + - boot: log error in KernelOrOsRebootRequired + - interfaces: remove old API + - interfaces: use udev spec + - interfaces: convert systemd backend to new APIs + - osutil: add BootID + - tests: move docker test to new nightly suite + - interfaces/mount: compute mount changes required to transition + mount profiles + - data/selinux: add context definition for snapctl + - overlord: clean up organization under state packages + - overlord: make sure all managers packages have *state.go with the + main state manipulation/query APIs + - interfaces: use spec in the dbus backend + - store: download from authenticated URL if there is a device + session set + - tests: remove core_name variable + - interfaces: rename thumbnailer to thumbnailer-service + - interfaces: add chroot to base templates + - asserts: remove some unused things + - systemd: mount the squashfs with nodev + - overlord: when shutting down assume errors might be due to + cancellation so retry + - cmd: rename all unit tests to $command/unit-test + - cmd/snap: fix help string for version command + - asserts: don't allow revocations with other items for the same + developer + - tests: skip lp-1644439 test on older kernels + - interfaces: allow "sync" to be used by core support + - assertstate,snapstate: have assertstate.AutoAliases use the + "aliases" header + - interfaces: allow writing config.txt.tmp in the core-support + interface + - tests: adjust network-bind test + - interfaces: dbus backend spec + - asserts: introduce a snap-declaration "aliases" header to list + auto aliases with explicit targets + - cmd: enable large file support + - cmd/snap: handle missing snap-confine + - cmd/snap-confine: re-associate with pid-1 mount namespace if + required + - cmd/libsnap: make mountinfo structures public + - tests: fix interfaces-cups-control for zesty + - misc: revert "Log if the system goes into ForceDevMode" + - interfaces: seccomp tests cleanup + - cmd: validate SNAP_NAME + - interfaces: log if the system goes into ForceDevMode + - tests: fix classic-ubuntu-core-transition race + - interfaces: use apparmor spec in the apparmor backend + - interfaces: alphabetize framebuffer in base decl and add it to + all_test.go + - tests: add ubuntu-core-16-32 system to the external backend and + fix docker test + - cmd/libsnap: simplify sc_string_quote default case + - osutil: fix double expand in environment map code and add test + - interfaces: extend location-control out-of-process provider + support + - cmd/snap-update-ns: use bidirectional lists for mount entries + - tests: prevent automatic transition before setting the initial + state of the test + - release: detect if we are in ForcedDevMode by inspecting the + kernel + - tests: add core-snap-refresh test + - interfaces: add maliit input method interface + - interfaces: seccomp spec API tweaks for better tests + - interfaces: updates for mir-kiosk in browser-support, mir, opengl, + unity7 + - testutils: address review feedback from PR#2997 + - tests: specify the core version to be unsquashfs'ed in the + failover tests + - interfaces: use MockInfo in tests + - cmd/libsnap: add sc_quote_string + - cmd/snap-confine: use sc_do_umount everywhere + - interfaces: add unity8 plug permissions + - timeutil: a few helpers for the recurring events + - asserts: implement snap-developer type + - partition: deal with grub{,2}-editenv in tests + - many: add new (hidden) `snap debug ensure-state-soon` command and + use in tests + - interfaces/builtin: small refactor of dbus tests + - packaging, tests: use "systemctl list-unit-files --full" + everywhere + - many: some opensuse patches that are ready to go into master + - packaging: add opensuse permissions files + - client, daemon: move "snap list" name filtering into snapd. + - interfaces: use seccomp specs + - overlord/snapstate: small cleanup of + ensureForceDevmodeDropsDevmodeFromState + - interfaces/builtin/alsa: add read access to alsa state dir + - interfaces: use spec in kmod backend, updated firewall_control, + openvswitch_support, ppp + - cmd/snap-confine: use sc_do_mount everywhere + - tests: remove workaround for docker again, snap-declaration is + fixed now + - interfaces: interface to allow autopilot introspection + + -- Michael Vogt <michael.vogt@ubuntu.com> Tue, 11 Apr 2017 13:31:46 +0200 + snapd (2.23.6) xenial; urgency=medium * New upstream release, LP: #1673568 diff --git a/packaging/ubuntu-16.04/control b/packaging/ubuntu-16.04/control index 565d66ac0b..628e17dbc3 100644 --- a/packaging/ubuntu-16.04/control +++ b/packaging/ubuntu-16.04/control @@ -23,6 +23,7 @@ Build-Depends: autoconf, libglib2.0-dev, libseccomp-dev, libudev-dev, + openssh-client, pkg-config, python3, python3-docutils, @@ -58,6 +59,7 @@ Depends: adduser, apparmor (>= 2.10.95-0ubuntu2.2), ca-certificates, gnupg1 | gnupg, + openssh-client, squashfs-tools, systemd, ${misc:Depends}, diff --git a/packaging/ubuntu-16.04/snap-confine.maintscript b/packaging/ubuntu-16.04/snap-confine.maintscript new file mode 100644 index 0000000000..16bc877472 --- /dev/null +++ b/packaging/ubuntu-16.04/snap-confine.maintscript @@ -0,0 +1 @@ +rm_conffile /etc/apparmor.d/usr.lib.snapd.snap-confine 2.23.6~ diff --git a/packaging/ubuntu-16.04/snapd.postrm b/packaging/ubuntu-16.04/snapd.postrm index 1f5df2afbe..5738507f6d 100644 --- a/packaging/ubuntu-16.04/snapd.postrm +++ b/packaging/ubuntu-16.04/snapd.postrm @@ -72,6 +72,9 @@ if [ "$1" = "purge" ]; then done umount -l /run/snapd/ns/ || true + echo "Removing extra snap-confine apparmor rules" + rm -f /etc/apparmor.d/snap.core.*.usr.lib.snapd.snap-confine + echo "Removing snapd state" rm -rf /var/lib/snapd fi diff --git a/snap/broken.go b/snap/broken.go index 52ebede3c5..4c3d7f1e53 100644 --- a/snap/broken.go +++ b/snap/broken.go @@ -62,3 +62,38 @@ func GuessAppsForBroken(info *Info) map[string]*AppInfo { return out } + +// renameClashingCorePlugs renames plugs that clash with slot names on core snap. +// +// Some released core snaps had explicitly defined plugs "network-bind" and +// "core-support" that clashed with implicit slots with the same names but this +// was not validated before. To avoid a flag day and any potential issues, +// transparently rename the two clashing plugs by appending the "-plug" suffix. +func (info *Info) renameClashingCorePlugs() { + if info.Name() == "core" && info.Type == TypeOS { + for _, plugName := range []string{"network-bind", "core-support"} { + info.renamePlug(plugName, plugName+"-plug") + } + } +} + +// renamePlug renames the plug from oldName to newName, if present. +func (info *Info) renamePlug(oldName, newName string) { + if plugInfo, ok := info.Plugs[oldName]; ok { + delete(info.Plugs, oldName) + info.Plugs[newName] = plugInfo + plugInfo.Name = newName + for _, appInfo := range info.Apps { + if _, ok := appInfo.Plugs[oldName]; ok { + delete(appInfo.Plugs, oldName) + appInfo.Plugs[newName] = plugInfo + } + } + for _, hookInfo := range info.Hooks { + if _, ok := hookInfo.Plugs[oldName]; ok { + delete(hookInfo.Plugs, oldName) + hookInfo.Plugs[newName] = plugInfo + } + } + } +} diff --git a/snap/broken_test.go b/snap/broken_test.go index 6a06adf958..17175ae03e 100644 --- a/snap/broken_test.go +++ b/snap/broken_test.go @@ -28,6 +28,7 @@ import ( "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" ) type brokenSuite struct{} @@ -70,3 +71,45 @@ func (s *brokenSuite) TestGuessAppsForBrokenServices(c *C) { c.Check(apps["foo"], DeepEquals, &snap.AppInfo{Snap: info, Name: "foo", Daemon: "simple"}) c.Check(apps["bar"], DeepEquals, &snap.AppInfo{Snap: info, Name: "bar", Daemon: "simple"}) } + +func (s *brokenSuite) TestRenamePlug(c *C) { + snapInfo := snaptest.MockInvalidInfo(c, `name: core +plugs: + old: + interface: iface +slots: + old: + interface: iface +apps: + app: +hooks: + configure: +`, nil) + c.Assert(snapInfo.Plugs["old"], Not(IsNil)) + c.Assert(snapInfo.Plugs["old"].Name, Equals, "old") + c.Assert(snapInfo.Slots["old"], Not(IsNil)) + c.Assert(snapInfo.Slots["old"].Name, Equals, "old") + c.Assert(snapInfo.Apps["app"].Plugs["old"], DeepEquals, snapInfo.Plugs["old"]) + c.Assert(snapInfo.Apps["app"].Slots["old"], DeepEquals, snapInfo.Slots["old"]) + c.Assert(snapInfo.Hooks["configure"].Plugs["old"], DeepEquals, snapInfo.Plugs["old"]) + + // Rename the plug now. + snapInfo.RenamePlug("old", "new") + + // Check that there's no trace of the old plug name. + c.Assert(snapInfo.Plugs["old"], IsNil) + c.Assert(snapInfo.Plugs["new"], Not(IsNil)) + c.Assert(snapInfo.Plugs["new"].Name, Equals, "new") + c.Assert(snapInfo.Apps["app"].Plugs["old"], IsNil) + c.Assert(snapInfo.Apps["app"].Plugs["new"], DeepEquals, snapInfo.Plugs["new"]) + c.Assert(snapInfo.Hooks["configure"].Plugs["old"], IsNil) + c.Assert(snapInfo.Hooks["configure"].Plugs["new"], DeepEquals, snapInfo.Plugs["new"]) + + // Check that slots with the old name are unaffected. + c.Assert(snapInfo.Slots["old"], Not(IsNil)) + c.Assert(snapInfo.Slots["old"].Name, Equals, "old") + c.Assert(snapInfo.Apps["app"].Slots["old"], DeepEquals, snapInfo.Slots["old"]) + + // Check that the rename made the snap valid now + c.Assert(snap.Validate(snapInfo), IsNil) +} diff --git a/snap/export_test.go b/snap/export_test.go index 57e65a36f1..aacd5e7642 100644 --- a/snap/export_test.go +++ b/snap/export_test.go @@ -30,3 +30,7 @@ func MockSupportedHookTypes(hookTypes []*HookType) (restore func()) { supportedHooks = hookTypes return func() { supportedHooks = old } } + +func (info *Info) RenamePlug(oldName, newName string) { + info.renamePlug(oldName, newName) +} diff --git a/snap/implicit.go b/snap/implicit.go index 8db7eaff26..493e98d0a4 100644 --- a/snap/implicit.go +++ b/snap/implicit.go @@ -45,6 +45,7 @@ var implicitSlots = []string{ "io-ports-control", "joystick", "kernel-module-control", + "kubernetes-support", "locale-control", "log-observe", "lxd-support", diff --git a/snap/info.go b/snap/info.go index 555bc60416..c18346bb71 100644 --- a/snap/info.go +++ b/snap/info.go @@ -402,6 +402,10 @@ func (app *AppInfo) SecurityTag() string { return AppSecurityTag(app.Snap.Name(), app.Name) } +func (app *AppInfo) DesktopFile() string { + return filepath.Join(dirs.SnapDesktopFilesDir, fmt.Sprintf("%s_%s.desktop", app.Snap.Name(), app.Name)) +} + // WrapperPath returns the path to wrapper invoking the app binary. func (app *AppInfo) WrapperPath() string { var binName string diff --git a/snap/info_snap_yaml.go b/snap/info_snap_yaml.go index d33e63d4a5..b32fe2d5d8 100644 --- a/snap/info_snap_yaml.go +++ b/snap/info_snap_yaml.go @@ -118,6 +118,9 @@ func InfoFromSnapYaml(yamlData []byte) (*Info, error) { // Bind unbound slots to all apps bindUnboundSlots(globalSlotNames, snap) + // Rename specific plugs on the core snap. + snap.renameClashingCorePlugs() + // FIXME: validation of the fields return snap, nil } diff --git a/snap/info_test.go b/snap/info_test.go index 9e769248de..e4f8a77131 100644 --- a/snap/info_test.go +++ b/snap/info_test.go @@ -132,6 +132,10 @@ version: 1 apps: app: command: foo + app2: + command: bar + sample: + command: foobar ` const sampleContents = "SNAP" @@ -501,3 +505,66 @@ func (s *infoSuite) TestDirAndFileMethods(c *C) { c.Check(info.CommonDataHomeDir(), Equals, "/home/*/snap/name/common") c.Check(info.XdgRuntimeDirs(), Equals, "/run/user/*/snap.name") } + +func makeFakeDesktopFile(c *C, name, content string) string { + df := filepath.Join(dirs.SnapDesktopFilesDir, name) + err := os.MkdirAll(filepath.Dir(df), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(df, []byte(content), 0644) + c.Assert(err, IsNil) + return df +} + +func (s *infoSuite) TestAppDesktopFile(c *C) { + snaptest.MockSnap(c, sampleYaml, sampleContents, &snap.SideInfo{}) + snapInfo, err := snap.ReadInfo("sample", &snap.SideInfo{}) + c.Assert(err, IsNil) + + c.Check(snapInfo.Name(), Equals, "sample") + c.Check(snapInfo.Apps["app"].DesktopFile(), Matches, `.*/var/lib/snapd/desktop/applications/sample_app.desktop`) + c.Check(snapInfo.Apps["sample"].DesktopFile(), Matches, `.*/var/lib/snapd/desktop/applications/sample_sample.desktop`) +} + +const coreSnapYaml = `name: core +type: os +plugs: + network-bind: + core-support: +` + +// reading snap via ReadInfoFromSnapFile renames clashing core plugs +func (s *infoSuite) TestReadInfoFromSnapFileRenamesCorePlus(c *C) { + snapPath := snaptest.MakeTestSnapWithFiles(c, coreSnapYaml, nil) + + snapf, err := snap.Open(snapPath) + c.Assert(err, IsNil) + + info, err := snap.ReadInfoFromSnapFile(snapf, nil) + c.Assert(err, IsNil) + c.Check(info.Plugs["network-bind"], IsNil) + c.Check(info.Plugs["core-support"], IsNil) + c.Check(info.Plugs["network-bind-plug"], NotNil) + c.Check(info.Plugs["core-support-plug"], NotNil) +} + +// reading snap via ReadInfo renames clashing core plugs +func (s *infoSuite) TestReadInfoRenamesCorePlugs(c *C) { + si := &snap.SideInfo{Revision: snap.R(42), RealName: "core"} + snaptest.MockSnap(c, coreSnapYaml, sampleContents, si) + info, err := snap.ReadInfo("core", si) + c.Assert(err, IsNil) + c.Check(info.Plugs["network-bind"], IsNil) + c.Check(info.Plugs["core-support"], IsNil) + c.Check(info.Plugs["network-bind-plug"], NotNil) + c.Check(info.Plugs["core-support-plug"], NotNil) +} + +// reading snap via InfoFromSnapYaml renames clashing core plugs +func (s *infoSuite) TestInfoFromSnapYamlRenamesCorePlugs(c *C) { + info, err := snap.InfoFromSnapYaml([]byte(coreSnapYaml)) + c.Assert(err, IsNil) + c.Check(info.Plugs["network-bind"], IsNil) + c.Check(info.Plugs["core-support"], IsNil) + c.Check(info.Plugs["network-bind-plug"], NotNil) + c.Check(info.Plugs["core-support-plug"], NotNil) +} diff --git a/snap/snaptest/snaptest.go b/snap/snaptest/snaptest.go index 1def2ddeed..989903f4b5 100644 --- a/snap/snaptest/snaptest.go +++ b/snap/snaptest/snaptest.go @@ -79,6 +79,23 @@ func MockInfo(c *check.C, yamlText string, sideInfo *snap.SideInfo) *snap.Info { return snapInfo } +// MockInvalidInfo parses the given snap.yaml text and returns the snap.Info object including the optional SideInfo. +// +// The result is just kept in memory, there is nothing kept on disk. If that is +// desired please use MockSnap instead. +func MockInvalidInfo(c *check.C, yamlText string, sideInfo *snap.SideInfo) *snap.Info { + if sideInfo == nil { + sideInfo = &snap.SideInfo{} + } + + snapInfo, err := snap.InfoFromSnapYaml([]byte(yamlText)) + c.Assert(err, check.IsNil) + snapInfo.SideInfo = *sideInfo + err = snap.Validate(snapInfo) + c.Assert(err, check.NotNil) + return snapInfo +} + // PopulateDir populates the directory with files specified as pairs of relative file path and its content. Useful to add extra files to a snap. func PopulateDir(dir string, files [][]string) { for _, filenameAndContent := range files { diff --git a/snap/snaptest/snaptest_test.go b/snap/snaptest/snaptest_test.go index 3765d2b784..370ff6fe74 100644 --- a/snap/snaptest/snaptest_test.go +++ b/snap/snaptest/snaptest_test.go @@ -88,3 +88,20 @@ func (s *snapTestSuite) TestMockInfo(c *C) { c.Check(snapInfo.Apps["app"].Command, Equals, "foo") c.Check(snapInfo.Plugs["network"].Interface, Equals, "network") } + +func (s *snapTestSuite) TestMockInvalidInfo(c *C) { + snapInfo := snaptest.MockInvalidInfo(c, sampleYaml+"\nslots:\n network:\n", &snap.SideInfo{Revision: snap.R(42)}) + // Data from YAML is used + c.Check(snapInfo.Name(), Equals, "sample") + // Data from SideInfo is used + c.Check(snapInfo.Revision, Equals, snap.R(42)) + // The YAML is *not* placed on disk + _, err := os.Stat(filepath.Join(dirs.SnapMountDir, "sample", "42", "meta", "snap.yaml")) + c.Assert(os.IsNotExist(err), Equals, true) + // More + c.Check(snapInfo.Apps["app"].Command, Equals, "foo") + c.Check(snapInfo.Plugs["network"].Interface, Equals, "network") + c.Check(snapInfo.Slots["network"].Interface, Equals, "network") + // They info object is not valid + c.Check(snap.Validate(snapInfo), ErrorMatches, `cannot have plug and slot with the same name: "network"`) +} diff --git a/spread.yaml b/spread.yaml index 556d320899..98b6e6866a 100644 --- a/spread.yaml +++ b/spread.yaml @@ -30,11 +30,12 @@ environment: SNAPPY_USE_STAGING_STORE: "$(HOST: if [ $SPREAD_REMOTE_STORE = staging ]; then echo 1; else echo 0; fi)" DELTA_REF: 2.17 DELTA_PREFIX: snapd-$DELTA_REF/ - SNAPD_PPA_VERSION: "$(HOST: echo $SPREAD_SNAPD_PPA_VERSION)" + SNAPD_PUBLISHED_VERSION: "$(HOST: echo $SPREAD_SNAPD_PUBLISHED_VERSION)" HTTP_PROXY: "$(HOST: echo $HTTP_PROXY)" HTTPS_PROXY: "$(HOST: echo $HTTPS_PROXY)" NO_PROXY: "127.0.0.1" NEW_CORE_CHANNEL: "$(HOST: echo $SPREAD_NEW_CORE_CHANNEL)" + SRU_VALIDATION: "$(HOST: echo ${SPREAD_SRU_VALIDATION:-0})" backends: linode: @@ -184,8 +185,14 @@ prepare-each: | dmesg -c > /dev/null debug-each: | + echo '# journal messages for snapd' journalctl -u snapd + echo '# apparmor denials ' dmesg | grep DENIED || true + echo '# seccomp denials (kills) ' + dmesg | grep 1326 || true + echo '# snap interfaces' + snap interfaces || true rename: # Move content into a directory, so that deltas computed by repack benefit diff --git a/store/store.go b/store/store.go index ecccf8f266..8b3b2ee1db 100644 --- a/store/store.go +++ b/store/store.go @@ -1266,7 +1266,7 @@ type HashError struct { } func (e HashError) Error() string { - return fmt.Sprintf("sha3-384 mismatch after patching %q: got %s but expected %s", e.name, e.sha3_384, e.targetSha3_384) + return fmt.Sprintf("sha3-384 mismatch for %q: got %s but expected %s", e.name, e.sha3_384, e.targetSha3_384) } // Download downloads the snap addressed by download info and returns its diff --git a/store/store_test.go b/store/store_test.go index aff445f882..cd0ac704f3 100644 --- a/store/store_test.go +++ b/store/store_test.go @@ -221,6 +221,10 @@ func (t *remoteRepoTestSuite) SetUpTest(c *C) { dirs.SetRootDir(c.MkDir()) c.Assert(os.MkdirAll(dirs.SnapMountDir, 0755), IsNil) + oldSnapdDebug := os.Getenv("SNAPD_DEBUG") + os.Setenv("SNAPD_DEBUG", "1") + t.AddCleanup(func() { os.Setenv("SNAPD_DEBUG", oldSnapdDebug) }) + t.logbuf = bytes.NewBuffer(nil) l, err := logger.NewConsoleLog(t.logbuf, logger.DefaultFlags) c.Assert(err, IsNil) @@ -326,6 +330,50 @@ func (t *remoteRepoTestSuite) TestDownloadRangeRequest(c *C) { c.Assert(string(content), Equals, partialContentStr+"was downloaded") } +func (t *remoteRepoTestSuite) TestDownloadEOFHandlesResumeHashCorrectly(c *C) { + n := 0 + var mockServer *httptest.Server + + // our mock download content + buf := make([]byte, 50000) + for i := range buf { + buf[i] = 'x' + } + h := crypto.SHA3_384.New() + io.Copy(h, bytes.NewBuffer(buf)) + + // raise an EOF shortly before the end + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + n++ + if n < 2 { + w.Header().Add("Content-Length", fmt.Sprintf("%d", len(buf))) + w.Write(buf[0 : len(buf)-5]) + mockServer.CloseClientConnections() + return + } + w.Write(buf[len(buf)-5:]) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + snap := &snap.Info{} + snap.RealName = "foo" + snap.AnonDownloadURL = mockServer.URL + snap.DownloadURL = "AUTH-URL" + snap.Sha3_384 = fmt.Sprintf("%x", h.Sum(nil)) + + targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap") + err := t.store.Download(context.TODO(), "foo", targetFn, &snap.DownloadInfo, nil, nil) + c.Assert(err, IsNil) + + content, err := ioutil.ReadFile(targetFn) + c.Assert(err, IsNil) + c.Assert(content, DeepEquals, buf) + + c.Assert(t.logbuf.String(), Matches, "(?s).*Retrying .* attempt 2, .*") +} + func (t *remoteRepoTestSuite) TestDownloadRangeRequestRetryOnHashError(c *C) { partialContentStr := "partial content " @@ -381,7 +429,7 @@ func (t *remoteRepoTestSuite) TestDownloadRangeRequestFailOnHashError(c *C) { err = t.store.Download(context.TODO(), "foo", targetFn, &snap.DownloadInfo, nil, nil) c.Assert(err, NotNil) - c.Assert(err, ErrorMatches, `sha3-384 mismatch after patching "foo": got 1234 but expected 5678`) + c.Assert(err, ErrorMatches, `sha3-384 mismatch for "foo": got 1234 but expected 5678`) c.Assert(n, Equals, 2) } @@ -2201,7 +2249,6 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreFindFails(c *C) { c.Assert(mockServer, NotNil) defer mockServer.Close() - var err error searchURI, err := url.Parse(mockServer.URL) c.Assert(err, IsNil) cfg := Config{ @@ -2224,7 +2271,6 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreFindBadContentType(c *C) { c.Assert(mockServer, NotNil) defer mockServer.Close() - var err error searchURI, err := url.Parse(mockServer.URL) c.Assert(err, IsNil) cfg := Config{ @@ -2249,7 +2295,6 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreFindBadBody(c *C) { c.Assert(mockServer, NotNil) defer mockServer.Close() - var err error searchURI, err := url.Parse(mockServer.URL) c.Assert(err, IsNil) cfg := Config{ @@ -2273,7 +2318,6 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreFind500(c *C) { c.Assert(mockServer, NotNil) defer mockServer.Close() - var err error searchURI, err := url.Parse(mockServer.URL) c.Assert(err, IsNil) cfg := Config{ @@ -2303,7 +2347,6 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreFind500once(c *C) { c.Assert(mockServer, NotNil) defer mockServer.Close() - var err error searchURI, err := url.Parse(mockServer.URL) c.Assert(err, IsNil) cfg := Config{ @@ -2348,7 +2391,6 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreFindAuthFailed(c *C) { c.Assert(mockPurchasesServer, NotNil) defer mockPurchasesServer.Close() - var err error searchURI, err := url.Parse(mockServer.URL) c.Assert(err, IsNil) ordersURI, err := url.Parse(mockPurchasesServer.URL + ordersPath) @@ -2459,7 +2501,6 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefresh(c *C) { c.Assert(mockServer, NotNil) defer mockServer.Close() - var err error bulkURI, err := url.Parse(mockServer.URL + "/updates/") c.Assert(err, IsNil) cfg := Config{ @@ -2510,7 +2551,6 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefreshRetryOnEOF(c * c.Assert(mockServer, NotNil) defer mockServer.Close() - var err error bulkURI, err := url.Parse(mockServer.URL + "/updates/") c.Assert(err, IsNil) cfg := Config{ @@ -2520,18 +2560,71 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefreshRetryOnEOF(c * repo := New(&cfg, authContext) c.Assert(repo, NotNil) - results, err := repo.ListRefresh([]*RefreshCandidate{ - { + results, err := repo.ListRefresh([]*RefreshCandidate{{ + SnapID: helloWorldSnapID, + Channel: "stable", + Revision: snap.R(1), + Epoch: "0", + }}, nil) + c.Assert(err, IsNil) + c.Assert(n, Equals, 4) + c.Assert(results, HasLen, 1) + c.Assert(results[0].Name(), Equals, "hello-world") +} + +func (t *remoteRepoTestSuite) TestUbuntuStoreUnexpectedEOFhandling(c *C) { + permanentlyBrokenSrvCalls := 0 + somewhatBrokenSrvCalls := 0 + + mockPermanentlyBrokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + permanentlyBrokenSrvCalls++ + w.Header().Add("Content-Length", "1000") + })) + mockSomewhatBrokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + somewhatBrokenSrvCalls++ + if somewhatBrokenSrvCalls > 3 { + io.WriteString(w, MockUpdatesJSON) + return + } + w.Header().Add("Content-Length", "1000") + })) + + queryServer := func(mockServer *httptest.Server) error { + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + bulkURI, err := url.Parse(mockServer.URL + "/updates/") + c.Assert(err, IsNil) + cfg := Config{ + BulkURI: bulkURI, + } + authContext := &testAuthContext{c: c, device: t.device} + repo := New(&cfg, authContext) + c.Assert(repo, NotNil) + + _, err = repo.ListRefresh([]*RefreshCandidate{{ SnapID: helloWorldSnapID, Channel: "stable", Revision: snap.R(1), Epoch: "0", }, - }, nil) + }, nil) + return err + } + + // Check that we really recognize unexpected EOF error by failing on all retries + err := queryServer(mockPermanentlyBrokenServer) + c.Assert(err, NotNil) + c.Assert(err, Equals, io.ErrUnexpectedEOF) + c.Assert(err, ErrorMatches, "unexpected EOF") + // check that we exhausted all retries (as defined by mocked retry strategy) + c.Assert(permanentlyBrokenSrvCalls, Equals, 5) + + // Check that we retry on unexpected EOF and eventually succeed + err = queryServer(mockSomewhatBrokenServer) c.Assert(err, IsNil) - c.Assert(n, Equals, 4) - c.Assert(results, HasLen, 1) - c.Assert(results[0].Name(), Equals, "hello-world") + // check that we retried 4 times + c.Assert(somewhatBrokenSrvCalls, Equals, 4) } func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefreshEOF(c *C) { @@ -2556,14 +2649,12 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefreshEOF(c *C) { repo := New(&cfg, authContext) c.Assert(repo, NotNil) - _, err = repo.ListRefresh([]*RefreshCandidate{ - { - SnapID: helloWorldSnapID, - Channel: "stable", - Revision: snap.R(1), - Epoch: "0", - }, - }, nil) + _, err = repo.ListRefresh([]*RefreshCandidate{{ + SnapID: helloWorldSnapID, + Channel: "stable", + Revision: snap.R(1), + Epoch: "0", + }}, nil) c.Assert(err, NotNil) c.Assert(err, ErrorMatches, `^Post http://127.0.0.1:.*?/updates/: EOF$`) c.Assert(n, Equals, 5) @@ -2591,14 +2682,12 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefreshUnauthorised(c repo := New(&cfg, authContext) c.Assert(repo, NotNil) - _, err = repo.ListRefresh([]*RefreshCandidate{ - { - SnapID: helloWorldSnapID, - Channel: "stable", - Revision: snap.R(24), - Epoch: "0", - }, - }, nil) + _, err = repo.ListRefresh([]*RefreshCandidate{{ + SnapID: helloWorldSnapID, + Channel: "stable", + Revision: snap.R(24), + Epoch: "0", + }}, nil) c.Assert(n, Equals, 1) c.Assert(err, ErrorMatches, `cannot query the store for updates: got unexpected HTTP status code 401 via POST to "http://.*?/updates/"`) } @@ -2611,7 +2700,6 @@ func (t *remoteRepoTestSuite) TestListRefresh500(c *C) { c.Assert(mockServer, NotNil) defer mockServer.Close() - var err error bulkURI, err := url.Parse(mockServer.URL + "/updates/") c.Assert(err, IsNil) cfg := Config{ @@ -2621,14 +2709,12 @@ func (t *remoteRepoTestSuite) TestListRefresh500(c *C) { repo := New(&cfg, authContext) c.Assert(repo, NotNil) - _, err = repo.ListRefresh([]*RefreshCandidate{ - { - SnapID: helloWorldSnapID, - Channel: "stable", - Revision: snap.R(24), - Epoch: "0", - }, - }, nil) + _, err = repo.ListRefresh([]*RefreshCandidate{{ + SnapID: helloWorldSnapID, + Channel: "stable", + Revision: snap.R(24), + Epoch: "0", + }}, nil) c.Assert(err, ErrorMatches, `cannot query the store for updates: got unexpected HTTP status code 500 via POST to "http://.*?/updates/"`) c.Assert(n, Equals, 5) } @@ -2643,7 +2729,6 @@ func (t *remoteRepoTestSuite) TestListRefresh500DurationExceeded(c *C) { c.Assert(mockServer, NotNil) defer mockServer.Close() - var err error bulkURI, err := url.Parse(mockServer.URL + "/updates/") c.Assert(err, IsNil) cfg := Config{ @@ -2653,14 +2738,12 @@ func (t *remoteRepoTestSuite) TestListRefresh500DurationExceeded(c *C) { repo := New(&cfg, authContext) c.Assert(repo, NotNil) - _, err = repo.ListRefresh([]*RefreshCandidate{ - { - SnapID: helloWorldSnapID, - Channel: "stable", - Revision: snap.R(24), - Epoch: "0", - }, - }, nil) + _, err = repo.ListRefresh([]*RefreshCandidate{{ + SnapID: helloWorldSnapID, + Channel: "stable", + Revision: snap.R(24), + Epoch: "0", + }}, nil) c.Assert(err, ErrorMatches, `cannot query the store for updates: got unexpected HTTP status code 500 via POST to "http://.*?/updates/"`) c.Assert(n, Equals, 1) } @@ -2691,7 +2774,6 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefreshSkipCurrent(c c.Assert(mockServer, NotNil) defer mockServer.Close() - var err error bulkURI, err := url.Parse(mockServer.URL + "/updates/") c.Assert(err, IsNil) cfg := Config{ @@ -2700,14 +2782,12 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefreshSkipCurrent(c repo := New(&cfg, nil) c.Assert(repo, NotNil) - results, err := repo.ListRefresh([]*RefreshCandidate{ - { - SnapID: helloWorldSnapID, - Channel: "stable", - Revision: snap.R(26), - Epoch: "0", - }, - }, nil) + results, err := repo.ListRefresh([]*RefreshCandidate{{ + SnapID: helloWorldSnapID, + Channel: "stable", + Revision: snap.R(26), + Epoch: "0", + }}, nil) c.Assert(err, IsNil) c.Assert(results, HasLen, 0) } @@ -2739,7 +2819,6 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefreshSkipBlocked(c c.Assert(mockServer, NotNil) defer mockServer.Close() - var err error bulkURI, err := url.Parse(mockServer.URL + "/updates/") c.Assert(err, IsNil) cfg := Config{ @@ -2748,15 +2827,13 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefreshSkipBlocked(c repo := New(&cfg, nil) c.Assert(repo, NotNil) - results, err := repo.ListRefresh([]*RefreshCandidate{ - { - SnapID: helloWorldSnapID, - Channel: "stable", - Revision: snap.R(25), - Epoch: "0", - Block: []snap.Revision{snap.R(26)}, - }, - }, nil) + results, err := repo.ListRefresh([]*RefreshCandidate{{ + SnapID: helloWorldSnapID, + Channel: "stable", + Revision: snap.R(25), + Epoch: "0", + Block: []snap.Revision{snap.R(26)}, + }}, nil) c.Assert(err, IsNil) c.Assert(results, HasLen, 0) } @@ -2847,7 +2924,6 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDefaultsDeltasOnClassicOn })) defer mockServer.Close() - var err error bulkURI, err := url.Parse(mockServer.URL + "/updates/") c.Assert(err, IsNil) cfg := Config{ @@ -2856,14 +2932,12 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDefaultsDeltasOnClassicOn repo := New(&cfg, nil) c.Assert(repo, NotNil) - repo.ListRefresh([]*RefreshCandidate{ - { - SnapID: helloWorldSnapID, - Channel: "stable", - Revision: snap.R(24), - Epoch: "0", - }, - }, nil) + repo.ListRefresh([]*RefreshCandidate{{ + SnapID: helloWorldSnapID, + Channel: "stable", + Revision: snap.R(24), + Epoch: "0", + }}, nil) } } @@ -2900,7 +2974,6 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefreshWithDeltas(c * c.Assert(mockServer, NotNil) defer mockServer.Close() - var err error bulkURI, err := url.Parse(mockServer.URL + "/updates/") c.Assert(err, IsNil) cfg := Config{ @@ -2909,14 +2982,12 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefreshWithDeltas(c * repo := New(&cfg, nil) c.Assert(repo, NotNil) - results, err := repo.ListRefresh([]*RefreshCandidate{ - { - SnapID: helloWorldSnapID, - Channel: "stable", - Revision: snap.R(24), - Epoch: "0", - }, - }, nil) + results, err := repo.ListRefresh([]*RefreshCandidate{{ + SnapID: helloWorldSnapID, + Channel: "stable", + Revision: snap.R(24), + Epoch: "0", + }}, nil) c.Assert(err, IsNil) c.Assert(results, HasLen, 1) c.Assert(results[0].Deltas, HasLen, 2) @@ -2974,7 +3045,6 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefreshWithoutDeltas( c.Assert(mockServer, NotNil) defer mockServer.Close() - var err error bulkURI, err := url.Parse(mockServer.URL + "/updates/") c.Assert(err, IsNil) cfg := Config{ @@ -2983,14 +3053,12 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefreshWithoutDeltas( repo := New(&cfg, nil) c.Assert(repo, NotNil) - results, err := repo.ListRefresh([]*RefreshCandidate{ - { - SnapID: helloWorldSnapID, - Channel: "stable", - Revision: snap.R(24), - Epoch: "0", - }, - }, nil) + results, err := repo.ListRefresh([]*RefreshCandidate{{ + SnapID: helloWorldSnapID, + Channel: "stable", + Revision: snap.R(24), + Epoch: "0", + }}, nil) c.Assert(err, IsNil) c.Assert(results, HasLen, 1) c.Assert(results[0].Deltas, HasLen, 0) @@ -3021,7 +3089,6 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryUpdateNotSendLocalRevs(c c.Assert(mockServer, NotNil) defer mockServer.Close() - var err error bulkURI, err := url.Parse(mockServer.URL + "/updates/") c.Assert(err, IsNil) cfg := Config{ @@ -3030,14 +3097,12 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryUpdateNotSendLocalRevs(c repo := New(&cfg, nil) c.Assert(repo, NotNil) - _, err = repo.ListRefresh([]*RefreshCandidate{ - { - SnapID: helloWorldSnapID, - Channel: "stable", - Revision: snap.R(-2), - Epoch: "0", - }, - }, nil) + _, err = repo.ListRefresh([]*RefreshCandidate{{ + SnapID: helloWorldSnapID, + Channel: "stable", + Revision: snap.R(-2), + Epoch: "0", + }}, nil) c.Assert(err, IsNil) } @@ -3154,7 +3219,6 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryAssertion(c *C) { c.Assert(mockServer, NotNil) defer mockServer.Close() - var err error assertionsURI, err := url.Parse(mockServer.URL + "/assertions/") c.Assert(err, IsNil) @@ -3182,7 +3246,6 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryAssertionNotFound(c *C) { c.Assert(mockServer, NotNil) defer mockServer.Close() - var err error assertionsURI, err := url.Parse(mockServer.URL + "/assertions/") c.Assert(err, IsNil) cfg := Config{ @@ -3209,7 +3272,6 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryAssertion500(c *C) { c.Assert(mockServer, NotNil) defer mockServer.Close() - var err error assertionsURI, err := url.Parse(mockServer.URL + "/assertions/") c.Assert(err, IsNil) cfg := Config{ @@ -3279,7 +3341,6 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreDecorateOrders(c *C) { c.Assert(mockPurchasesServer, NotNil) defer mockPurchasesServer.Close() - var err error ordersURI, err := url.Parse(mockPurchasesServer.URL + ordersPath) c.Assert(err, IsNil) @@ -3328,7 +3389,6 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreDecorateOrdersFailedAccess(c *C) { c.Assert(mockPurchasesServer, NotNil) defer mockPurchasesServer.Close() - var err error ordersURI, err := url.Parse(mockPurchasesServer.URL + ordersPath) c.Assert(err, IsNil) cfg := Config{ @@ -3446,7 +3506,6 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreDecorateOrdersSingle(c *C) { c.Assert(mockPurchasesServer, NotNil) defer mockPurchasesServer.Close() - var err error ordersURI, err := url.Parse(mockPurchasesServer.URL + ordersPath) c.Assert(err, IsNil) @@ -3496,7 +3555,6 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreDecorateOrdersSingleNotFound(c *C) c.Assert(mockPurchasesServer, NotNil) defer mockPurchasesServer.Close() - var err error ordersURI, err := url.Parse(mockPurchasesServer.URL + ordersPath) c.Assert(err, IsNil) @@ -3531,7 +3589,6 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreDecorateOrdersTokenExpired(c *C) { c.Assert(mockPurchasesServer, NotNil) defer mockPurchasesServer.Close() - var err error ordersURI, err := url.Parse(mockPurchasesServer.URL + ordersPath) c.Assert(err, IsNil) diff --git a/tests/lib/prepare-project.sh b/tests/lib/prepare-project.sh index 2011e3801b..8f0c350430 100644 --- a/tests/lib/prepare-project.sh +++ b/tests/lib/prepare-project.sh @@ -32,21 +32,26 @@ build_deb(){ cp ../*.deb $GOPATH } -download_from_ppa(){ - local ppa_version="$1" +download_from_published(){ + local published_version="$1" - # we need to install snap-confine and ubunntu-core-launcher for versions < 2.23 + curl -s -o pkg_page "https://launchpad.net/ubuntu/+source/snapd/$published_version" + + arch=$(dpkg --print-architecture) + build_id=$(sed -n 's|<a href="/ubuntu/+source/snapd/'"$published_version"'/+build/\(.*\)">'"$arch"'</a>|\1|p' pkg_page | sed -e 's/^[[:space:]]*//') + + # we need to download snap-confine and ubuntu-core-launcher for versions < 2.23 for pkg in snapd snap-confine ubuntu-core-launcher; do - file="${pkg}_${ppa_version}_$(dpkg --print-architecture).deb" - curl -L -o "$GOPATH/$file" "https://launchpad.net/~snappy-dev/+archive/ubuntu/snapd-${ppa_version}/+files/$file" + file="${pkg}_${published_version}_${arch}.deb" + curl -L -o "$GOPATH/$file" "https://launchpad.net/ubuntu/+source/snapd/${published_version}/+build/${build_id}/+files/${file}" done } -install_dependencies_from_ppa(){ - local ppa_version="$1" +install_dependencies_from_published(){ + local published_version="$1" for dep in snap-confine ubuntu-core-launcher; do - dpkg -i "${GOPATH}/${dep}_${ppa_version}_$(dpkg --print-architecture).deb" + dpkg -i "${GOPATH}/${dep}_${published_version}_$(dpkg --print-architecture).deb" done } @@ -129,12 +134,11 @@ if [ "$(which govendor)" = "" ]; then fi quiet govendor sync -if [ -z "$SNAPD_PPA_VERSION" ]; then +if [ -z "$SNAPD_PUBLISHED_VERSION" ]; then build_deb else - download_from_ppa "$SNAPD_PPA_VERSION" - - install_dependencies_from_ppa "$SNAPD_PPA_VERSION" + download_from_published "$SNAPD_PUBLISHED_VERSION" + install_dependencies_from_published "$SNAPD_PUBLISHED_VERSION" fi # Build snapbuild. diff --git a/tests/lib/prepare.sh b/tests/lib/prepare.sh index cc73322214..108a4854b7 100755 --- a/tests/lib/prepare.sh +++ b/tests/lib/prepare.sh @@ -25,16 +25,17 @@ update_core_snap_for_classic_reexec() { # Now unpack the core, inject the new snap-exec/snapctl into it unsquashfs "$snap" - cp /usr/lib/snapd/snap-exec squashfs-root/usr/lib/snapd/ - cp /usr/bin/snapctl squashfs-root/usr/bin/ + cp -a /usr/lib/snapd/snap-exec squashfs-root/usr/lib/snapd/ + cp -a /usr/bin/snapctl squashfs-root/usr/bin/ # also inject new version of snap-confine and snap-scard-ns - cp /usr/lib/snapd/snap-discard-ns squashfs-root/usr/lib/snapd/ - cp /usr/lib/snapd/snap-confine squashfs-root/usr/lib/snapd/ + cp -a /usr/lib/snapd/snap-discard-ns squashfs-root/usr/lib/snapd/ + cp -a /usr/lib/snapd/snap-confine squashfs-root/usr/lib/snapd/ + cp -a /etc/apparmor.d/usr.lib.snapd.snap-confine* squashfs-root/etc/apparmor.d/usr.lib.snapd.snap-confine.real # also add snap/snapd because we re-exec by default and want to test # this version - cp /usr/lib/snapd/snapd squashfs-root/usr/lib/snapd/ - cp /usr/lib/snapd/info squashfs-root/usr/lib/snapd/ - cp /usr/bin/snap squashfs-root/usr/bin/snap + cp -a /usr/lib/snapd/snapd squashfs-root/usr/lib/snapd/ + cp -a /usr/lib/snapd/info squashfs-root/usr/lib/snapd/ + cp -a /usr/bin/snap squashfs-root/usr/bin/snap # repack, cheating to speed things up (4sec vs 1.5min) mv "$snap" "${snap}.orig" mksnap_fast "squashfs-root" "$snap" @@ -52,6 +53,16 @@ update_core_snap_for_classic_reexec() { done } +upgrade_snapd_from_proposed(){ + apt install -y snapd + cp /etc/apt/sources.list sources.list.back + echo "deb http://archive.ubuntu.com/ubuntu/ $(lsb_release -c -s)-proposed restricted main multiverse universe" | tee /etc/apt/sources.list -a + apt update + apt install -y --only-upgrade snapd + mv sources.list.back /etc/apt/sources.list + apt update +} + prepare_each_classic() { mkdir -p /etc/systemd/system/snapd.service.d if [ -z "${SNAP_REEXEC:-}" ]; then @@ -69,7 +80,11 @@ EOF } prepare_classic() { - apt_install_local ${GOPATH}/snapd_*.deb + if [ "$SRU_VALIDATION" = "1" ]; then + upgrade_snapd_from_proposed + else + apt_install_local ${GOPATH}/snapd_*.deb + fi if snap --version |MATCH unknown; then echo "Package build incorrect, 'snap --version' mentions 'unknown'" snap --version diff --git a/tests/lib/snaps/test-snapd-kernel-module-control-consumer/snapcraft.yaml b/tests/lib/snaps/test-snapd-kernel-module-control-consumer/snapcraft.yaml new file mode 100644 index 0000000000..b8a138caeb --- /dev/null +++ b/tests/lib/snaps/test-snapd-kernel-module-control-consumer/snapcraft.yaml @@ -0,0 +1,24 @@ +name: test-snapd-kernel-module-control-consumer +version: 1.0 +summary: Basic kernel-module-control consumer snap +description: A basic snap declaring a plug on kernel-module-control + +apps: + lsmod: + command: bin/lsmod + plugs: [kernel-module-control] + insmod: + command: sbin/insmod + plugs: [kernel-module-control] + rmmod: + command: sbin/rmmod + plugs: [kernel-module-control] +parts: + build: + plugin: autotools + build-packages: [docbook-utils] + source: https://github.com/vadmium/module-init-tools + source-type: git + snap: + - bin/ + - sbin/ diff --git a/tests/main/alias/task.yaml b/tests/main/alias/task.yaml index 85ad118230..a106aa0266 100644 --- a/tests/main/alias/task.yaml +++ b/tests/main/alias/task.yaml @@ -1,10 +1,16 @@ summary: Check snap alias and snap unalias prepare: | + echo "New semantics are WIP, skipping for now" + exit 0 + . $TESTSLIB/snaps.sh install_local aliases execute: | + echo "New semantics are WIP, skipping for now" + exit 0 + echo "Sanity check" aliases.cmd1|MATCH "ok command 1" aliases.cmd2|MATCH "ok command 2" diff --git a/tests/main/change-errors/task.yaml b/tests/main/change-errors/task.yaml index 7832ec397a..e6b86c0590 100644 --- a/tests/main/change-errors/task.yaml +++ b/tests/main/change-errors/task.yaml @@ -1,7 +1,10 @@ -summary: Checks for cli errors of the change command. +summary: Checks for cli errors of the tasks / change command. execute: | - echo "When an invalid ID is given to the change command it shows an error" + echo "When an invalid ID is given to the tasks command it shows an error" + if snap tasks 10000000; then + echo "Expected error when trying change on invalid ID" && exit 1 + fi if snap change 10000000; then echo "Expected error when trying change on invalid ID" && exit 1 fi diff --git a/tests/main/classic-ubuntu-core-transition-auth/task.yaml b/tests/main/classic-ubuntu-core-transition-auth/task.yaml index a1a3625975..509e1d66a4 100644 --- a/tests/main/classic-ubuntu-core-transition-auth/task.yaml +++ b/tests/main/classic-ubuntu-core-transition-auth/task.yaml @@ -10,14 +10,18 @@ execute: | . "$TESTSLIB/apt.sh" echo "Ensure core is gone and we have ubuntu-core instead" dpkg --purge snapd - apt_install_local ${GOPATH}/snapd_*.deb + if [ "$SRU_VALIDATION" = "1" ]; then + upgrade_snapd_from_proposed + else + apt_install_local ${GOPATH}/snapd_*.deb + fi snap install --${CORE_CHANNEL} ubuntu-core mkdir -p /root/.snap/ echo '{}' > /root/.snap/auth.json mkdir -p /home/test/.snap/ echo '{}' > /home/test/.snap/auth.json - + echo "Ensure transition is triggered" snap debug ensure-state-soon @@ -30,3 +34,6 @@ execute: | echo "ubuntu-core still installed, transition failed" exit 1 fi + + echo "Ensure interfaces are connected" + snap interfaces | MATCH ":core-support.*core:core-support-plug" diff --git a/tests/main/classic-ubuntu-core-transition-two-cores/task.yaml b/tests/main/classic-ubuntu-core-transition-two-cores/task.yaml index dadb13d6a5..8a5a17e761 100644 --- a/tests/main/classic-ubuntu-core-transition-two-cores/task.yaml +++ b/tests/main/classic-ubuntu-core-transition-two-cores/task.yaml @@ -45,3 +45,6 @@ execute: | exit 1 fi snap interfaces |MATCH ":network.*test-snapd-python-webserver" + + echo "Ensure interfaces are connected" + snap interfaces | MATCH ":core-support.*core:core-support-plug" diff --git a/tests/main/classic-ubuntu-core-transition/task.yaml b/tests/main/classic-ubuntu-core-transition/task.yaml index 92da7b3b09..3e2bbc3ef1 100644 --- a/tests/main/classic-ubuntu-core-transition/task.yaml +++ b/tests/main/classic-ubuntu-core-transition/task.yaml @@ -30,7 +30,11 @@ execute: | . "$TESTSLIB/apt.sh" echo "Ensure core is gone and we have ubuntu-core instead" dpkg --purge snapd - apt_install_local ${GOPATH}/snapd_*.deb + if [ "$SRU_VALIDATION" = "1" ]; then + upgrade_snapd_from_proposed + else + apt_install_local ${GOPATH}/snapd_*.deb + fi # modify daemon state to set ubuntu-core-transition-last-retry-time to the # current time to prevent the ubuntu-core transition before the test snap is @@ -43,7 +47,8 @@ execute: | snap install --${CORE_CHANNEL} ubuntu-core snap install test-snapd-python-webserver - snap interfaces |MATCH ":network.*test-snapd-python-webserver" + snap interfaces | MATCH ":network +test-snapd-python-webserver" + snap interfaces | MATCH ":network-bind +test-snapd-python-webserver" echo "Ensure the webserver is working" wait_for_service snap.test-snapd-python-webserver.test-snapd-python-webserver @@ -69,7 +74,8 @@ execute: | echo "ubuntu-core still installed, transition failed" exit 1 fi - snap interfaces |MATCH ":network.*test-snapd-python-webserver" + snap interfaces | MATCH ":network +test-snapd-python-webserver" + snap interfaces | MATCH ":network-bind +test-snapd-python-webserver" echo "Ensure the webserver is still working" wait_for_service snap.test-snapd-python-webserver.test-snapd-python-webserver curl http://localhost | MATCH "XKCD rocks" @@ -78,3 +84,14 @@ execute: | wait_for_service snap.test-snapd-python-webserver.test-snapd-python-webserver echo "Ensure the webserver is working after a snap restart" curl http://localhost | MATCH "XKCD rocks" + + echo "Ensure interfaces are connected" + snap interfaces | MATCH ":core-support.*core:core-support-plug" + + echo "Ensure snap set core works" + snap set core system.power-key-action=ignore + if [ "$(snap get core system.power-key-action)" != "ignore" ]; then + echo "snap get did not return the expected result: " + snap get core system.power-key-action + exit 1 + fi diff --git a/tests/main/interfaces-kernel-module-control/task.yaml b/tests/main/interfaces-kernel-module-control/task.yaml new file mode 100644 index 0000000000..f07e7202c8 --- /dev/null +++ b/tests/main/interfaces-kernel-module-control/task.yaml @@ -0,0 +1,96 @@ +summary: Ensure that the kernel-module-control interface works. + +details: | + The kernel-module-control interface allows insertion, removal and querying + of modules. + + A snap which defines a kernel-module-control plug must be shown in the + interfaces list. The plug must not be autoconnected on install and, as + usual, must be able to be reconnected. + + A snap declaring a plug on this interface must be able to list the modules + loaded, insert and remove a module. For the test we use the binfmt_misc module. + +prepare: | + echo "Given a snap declaring a plug on the kernel-module-control interface is installed" + snap install --edge test-snapd-kernel-module-control-consumer + +restore: | + rm -f list.error remove.error + if lsmod | MATCH binfmt_misc && ! -f module_present; then + rmmod binfmt_misc + elif [ -f module_present ]; then + insmod /lib/modules/$(uname -r)/kernel/fs/binfmt_misc.ko + fi + if [ -f package_present ]; then + apt install -y binfmt-support + fi + rm -f module_present package_present + +debug: | + lsmod + +execute: | + CONNECTED_PATTERN=":kernel-module-control +test-snapd-kernel-module-control-consumer" + DISCONNECTED_PATTERN="\- +test-snapd-kernel-module-control-consumer:kernel-module-control" + + echo "The plug is disconnected by default" + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "===================================" + + echo "When the plug is connected" + snap connect test-snapd-kernel-module-control-consumer:kernel-module-control + snap interfaces | MATCH "$CONNECTED_PATTERN" + + echo "Then the snap is able to list the existing modules" + [ $(su -l -c "test-snapd-kernel-module-control-consumer.lsmod" test | wc -l) -gt 2 ] + + echo "And the snap is able to insert a module" + if lsmod | MATCH binfmt_misc; then + touch module_present + if ! rmmod binfmt_misc; then + # the module is being used + if apt list --installed binfmt-support | MATCH binfmt-support; then + touch package_present + apt remove -y binfmt-support + rmmod binfmt_misc + fi + fi + fi + lsmod | MATCH -v binfmt_misc + test-snapd-kernel-module-control-consumer.insmod /lib/modules/$(uname -r)/kernel/fs/binfmt_misc.ko + + echo "And the snap is able to remove a module" + test-snapd-kernel-module-control-consumer.rmmod binfmt_misc + lsmod | MATCH -v binfmt_misc + + echo "===================================" + + echo "When the plug is disconnected" + snap disconnect test-snapd-kernel-module-control-consumer:kernel-module-control + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "Then the snap is not able to list modules" + if su -l -c "test-snapd-kernel-module-control-consumer.lsmod 2>${PWD}/list.error" test; then + echo "Expected permission error listing modules with disconnected plug" + exit 1 + fi + cat list.error | MATCH "Permission denied" + + echo "And the snap is not able to insert a module" + if test-snapd-kernel-module-control-consumer.insmod /lib/modules/$(uname -r)/kernel/fs/binfmt_misc.ko; then + echo "Expected permission error inserting module with disconnected plug" + exit 1 + fi + + echo "And the snap is not able to remove a module" + # first we need to insert the module + lsmod | MATCH -v binfmt_misc + insmod /lib/modules/$(uname -r)/kernel/fs/binfmt_misc.ko + lsmod | MATCH binfmt_misc + if test-snapd-kernel-module-control-consumer.rmmod binfmt_misc 2>${PWD}/remove.error; then + echo "Expected permission error removing module with disconnected plug" + exit 1 + fi + cat remove.error | MATCH "Permission denied" diff --git a/tests/main/interfaces-network-bind/task.yaml b/tests/main/interfaces-network-bind/task.yaml index c96a09fbae..653043e34b 100644 --- a/tests/main/interfaces-network-bind/task.yaml +++ b/tests/main/interfaces-network-bind/task.yaml @@ -35,7 +35,7 @@ restore: | execute: | CONNECTED_PATTERN="(?s)Slot +Plug\n\ .*?\n\ - :network-bind +core,$SNAP_NAME" + :network-bind +$SNAP_NAME" DISCONNECTED_PATTERN="(?s)Slot +Plug\n\ .*?\n\ - +$SNAP_NAME:network-bind" diff --git a/tests/main/refresh-all-undo/task.yaml b/tests/main/refresh-all-undo/task.yaml index d8df39a9f4..b3e0bd8d5c 100644 --- a/tests/main/refresh-all-undo/task.yaml +++ b/tests/main/refresh-all-undo/task.yaml @@ -64,3 +64,6 @@ execute: | snap change $chg_id | MATCH "Undone.*Download snap \"${BAD_SNAP}\"" snap change $chg_id | MATCH "Done.*Download snap \"${GOOD_SNAP}\"" snap change $chg_id | MATCH "ERROR cannot verify snap \"test-snapd-tools\", no matching signatures found" + + echo "Verify the 'snap tasks' is the same as 'snap change'" + snap tasks $chg_id | MATCH "Undone.*Download snap \"${BAD_SNAP}\"" diff --git a/tests/main/security-setuid-root/task.yaml b/tests/main/security-setuid-root/task.yaml index e823dfe8ba..57e71e78e7 100644 --- a/tests/main/security-setuid-root/task.yaml +++ b/tests/main/security-setuid-root/task.yaml @@ -7,6 +7,15 @@ details: | prepare: | . $TESTSLIB/snaps.sh install_local test-snapd-tools + echo "Ensure the snap-confine profiles on core are not loaded" + for p in /etc/apparmor.d/snap.core.*.usr.lib.snapd.snap-confine; do + apparmor_parser -R "$p" + done +restore: | + echo "Ensure the snap-confine profiles are restored" + for p in /etc/apparmor.d/snap.core.*.usr.lib.snapd.snap-confine; do + apparmor_parser -r $p + done execute: | # NOTE: This has to run as the test user because the protection is only # active if user gains elevated permissions as a result of using setuid diff --git a/tests/main/snap-confine-from-core/task.yaml b/tests/main/snap-confine-from-core/task.yaml new file mode 100644 index 0000000000..d59b65c5ef --- /dev/null +++ b/tests/main/snap-confine-from-core/task.yaml @@ -0,0 +1,27 @@ +summary: Test that snap-confine is run from core on re-exec + +systems: [-ubuntu-core-16-*] + +prepare: | + echo "Installing test-snapd-tools" + snap install test-snapd-tools + echo "Breaking host snap-confine" + chmod 0755 /usr/lib/snapd/snap-confine + +restore: | + echo "Restoring host snap-confine" + chmod 4755 /usr/lib/snapd/snap-confine + +execute: | + if [ "${SNAP_REEXEC:-}" = "0" ]; then + echo "skipping test when SNAP_REEXEC is disabled" + exit 0 + fi + + echo "Ensure we re-exec by default" + snap list + journalctl | MATCH "DEBUG: restarting into" + + echo "Ensure snap-confine from the core snap is run" + # do not use "strace -f" for unknown reasons that hangs + test-snapd-tools.echo hello diff --git a/tests/main/snap-update-ns/task.yaml b/tests/main/snap-update-ns/task.yaml new file mode 100644 index 0000000000..6a0e8e1312 --- /dev/null +++ b/tests/main/snap-update-ns/task.yaml @@ -0,0 +1,28 @@ +summary: smoke test for snap-update-ns +details: | + Snapd is growing a new executable, snap-update-ns, to modify an existing + mount namespace. This is further documented on the forum here + https://forum.snapcraft.io/t/fixing-live-propagation-of-mount-changes/23 + + While the implementation matures this test checks that we call setns + correctly (and it doesn't fail) enough that we reach the "not implemented" + message that is currently in snap-updates-ns +environment: + # This is needed to enable the C part of snap-update-ns to work. + SNAPD_INTERNAL: x-switch-namespace=1, +prepare: | + . $TESTSLIB/snaps.sh + install_local test-snapd-tools +execute: | + # Ensure there is no (stale) mount namespace + rm -f /run/snapd/ns/test-snapd-tools.mnt + # Run snap-update-ns to see that we try to open the namespace file but fail (because it is not there yet). + /usr/lib/snapd/snap-update-ns test-snapd-tools | MATCH "cannot update snap namespace: cannot open mount namespace file: no such file or directory" + # Run a trivial command to build and preserve a mount namespace. + test-snapd-tools.cmd true + # Run snap-update-ns to see that setns part worked and we got to the 'not implemented' code. + /usr/lib/snapd/snap-update-ns test-snapd-tools | MATCH "cannot update snap namespace: not implemented" + # Run snap-discard-ns to discard the namespace we were working with. + /usr/lib/snapd/snap-discard-ns test-snapd-tools + # Run snap-update-ns to see that setns part fails because what we open is no longer a namespace. + /usr/lib/snapd/snap-update-ns test-snapd-tools | MATCH "cannot update snap namespace: cannot switch mount namespace: invalid argument" diff --git a/tests/nightly/unity/task.yaml b/tests/nightly/unity/task.yaml index a760793e99..51604543b1 100644 --- a/tests/nightly/unity/task.yaml +++ b/tests/nightly/unity/task.yaml @@ -6,12 +6,19 @@ environment: systems: [ubuntu-16.04-64] prepare: | + # the file /etc/init/tty1.conf is present in the default images, upstart + # (which is installed as a dependency of the required packages) ships it + # and doesn't install cleanly if that file is in place + mv /etc/init/tty1.conf /etc/init/tty1.conf.back + apt install -y --no-install-recommends x11-utils xvfb unity disabled_restore: | systemctl stop unity-app apt autoremove -y --purge x11-utils xvfb unity + mv /etc/init/tty1.conf.back /etc/init/tty1.conf + execute: | echo "Given a unity snap is installed" snap install ubuntu-clock-app |
