diff options
| author | Michael Vogt <mvo@ubuntu.com> | 2018-08-20 14:48:00 +0200 |
|---|---|---|
| committer | Michael Vogt <mvo@ubuntu.com> | 2018-08-20 14:48:00 +0200 |
| commit | 6110ed56fc51eca0f2ed32dbc0c317588b711321 (patch) | |
| tree | ad4d39fb1ebb972826df22705d6d04fef0879e5d | |
| parent | 032dc6c90c36344bc92902ed9a58ec496a67e526 (diff) | |
| parent | 8907449841ab5431335c29efa5062aa7001af461 (diff) | |
Merge remote-tracking branch 'upstream/master' into release-2.35release-2.35
156 files changed, 4217 insertions, 935 deletions
diff --git a/cmd/libsnap-confine-private/classic-test.c b/cmd/libsnap-confine-private/classic-test.c index 55c6a2fb73..cf6e5bcc0e 100644 --- a/cmd/libsnap-confine-private/classic-test.c +++ b/cmd/libsnap-confine-private/classic-test.c @@ -20,56 +20,104 @@ #include <glib.h> -const char *os_release_classic = "" +/* restore_os_release is an internal helper for mock_os_release */ +static void restore_os_release(gpointer * old) +{ + unlink(os_release); + os_release = (const char *)old; +} + +/* mock_os_release replaces the presence and contents of /etc/os-release + as seen by classic.c. The mocked value may be NULL to have the code refer + to an absent file. */ +static void mock_os_release(const char *mocked) +{ + const char *old = os_release; + if (mocked != NULL) { + os_release = "os-release.test"; + g_file_set_contents(os_release, mocked, -1, NULL); + } else { + os_release = "os-release.missing"; + } + g_test_queue_destroy((GDestroyNotify) restore_os_release, + (gpointer) old); +} + +/* restore_meta_snap_yaml is an internal helper for mock_meta_snap_yaml */ +static void restore_meta_snap_yaml(gpointer * old) +{ + unlink(meta_snap_yaml); + meta_snap_yaml = (const char *)old; +} + +/* mock_meta_snap_yaml replaces the presence and contents of /meta/snap.yaml + as seen by classic.c. The mocked value may be NULL to have the code refer + to an absent file. */ +static void mock_meta_snap_yaml(const char *mocked) +{ + const char *old = meta_snap_yaml; + if (mocked != NULL) { + meta_snap_yaml = "snap-yaml.test"; + g_file_set_contents(meta_snap_yaml, mocked, -1, NULL); + } else { + meta_snap_yaml = "snap-yaml.missing"; + } + g_test_queue_destroy((GDestroyNotify) restore_meta_snap_yaml, + (gpointer) old); +} + +static const char *os_release_classic = "" "NAME=\"Ubuntu\"\n" "VERSION=\"17.04 (Zesty Zapus)\"\n" "ID=ubuntu\n" "ID_LIKE=debian\n"; static void test_is_on_classic(void) { - g_file_set_contents("os-release.classic", os_release_classic, - strlen(os_release_classic), NULL); - os_release = "os-release.classic"; + mock_os_release(os_release_classic); + mock_meta_snap_yaml(NULL); g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CLASSIC); - unlink("os-release.classic"); } -const char *os_release_core16 = "" +static const char *os_release_core16 = "" "NAME=\"Ubuntu Core\"\n" "VERSION_ID=\"16\"\n" "ID=ubuntu-core\n"; +static const char *meta_snap_yaml_core16 = "" + "name: core\n" + "version: 16-something\n" "type: core\n" "architectures: [amd64]\n"; + static void test_is_on_core_on16(void) { - g_file_set_contents("os-release.core", os_release_core16, - strlen(os_release_core16), NULL); - os_release = "os-release.core"; + mock_os_release(os_release_core16); + mock_meta_snap_yaml(meta_snap_yaml_core16); g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CORE16); - unlink("os-release.core"); } -const char *os_release_core18 = "" +static const char *os_release_core18 = "" "NAME=\"Ubuntu Core\"\n" "VERSION_ID=\"18\"\n" "ID=ubuntu-core\n"; +static const char *meta_snap_yaml_core18 = "" + "name: core18\n" "type: base\n" "architectures: [amd64]\n"; + static void test_is_on_core_on18(void) { - g_file_set_contents("os-release.core", os_release_core18, - strlen(os_release_core18), NULL); - os_release = "os-release.core"; + mock_os_release(os_release_core18); + mock_meta_snap_yaml(meta_snap_yaml_core18); g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CORE_OTHER); - unlink("os-release.core"); } const char *os_release_core20 = "" "NAME=\"Ubuntu Core\"\n" "VERSION_ID=\"20\"\n" "ID=ubuntu-core\n"; +static const char *meta_snap_yaml_core20 = "" + "name: core20\n" "type: base\n" "architectures: [amd64]\n"; + static void test_is_on_core_on20(void) { - g_file_set_contents("os-release.core", os_release_core20, - strlen(os_release_core20), NULL); - os_release = "os-release.core"; + mock_os_release(os_release_core20); + mock_meta_snap_yaml(meta_snap_yaml_core20); g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CORE_OTHER); - unlink("os-release.core"); } -const char *os_release_classic_with_long_line = "" +static const char *os_release_classic_with_long_line = "" "NAME=\"Ubuntu\"\n" "VERSION=\"17.04 (Zesty Zapus)\"\n" "ID=ubuntu\n" @@ -78,36 +126,54 @@ const char *os_release_classic_with_long_line = "" static void test_is_on_classic_with_long_line(void) { - g_file_set_contents("os-release.classic-with-long-line", - os_release_classic, strlen(os_release_classic), - NULL); - os_release = "os-release.classic-with-long-line"; + mock_os_release(os_release_classic_with_long_line); + mock_meta_snap_yaml(NULL); g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CLASSIC); - unlink("os-release.classic-with-long-line"); } -const char *os_release_fedora_base = "" +static const char *os_release_fedora_base = "" "NAME=Fedora\nID=fedora\nVARIANT_ID=snappy\n"; +static const char *meta_snap_yaml_fedora_base = "" + "name: fedora29\n" "type: base\n" "architectures: [amd64]\n"; + static void test_is_on_fedora_base(void) { - g_file_set_contents("os-release.core", os_release_fedora_base, - strlen(os_release_fedora_base), NULL); - os_release = "os-release.core"; + mock_os_release(os_release_fedora_base); + mock_meta_snap_yaml(meta_snap_yaml_fedora_base); g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CORE_OTHER); - unlink("os-release.core"); } -const char *os_release_fedora_ws = "" +static const char *os_release_fedora_ws = "" "NAME=Fedora\nID=fedora\nVARIANT_ID=workstation\n"; static void test_is_on_fedora_ws(void) { - g_file_set_contents("os-release.core", os_release_fedora_ws, - strlen(os_release_fedora_ws), NULL); - os_release = "os-release.core"; + mock_os_release(os_release_fedora_ws); + mock_meta_snap_yaml(NULL); + g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CLASSIC); +} + +static const char *os_release_custom = "" + "NAME=\"Custom Distribution\"\nID=custom\n"; + +static const char *meta_snap_yaml_custom = "" + "name: custom\n" + "version: rolling\n" + "summary: Runtime environment based on Custom Distribution\n" + "type: base\n" "architectures: [amd64]\n"; + +static void test_is_on_custom_base(void) +{ + mock_os_release(os_release_custom); + + /* Without /meta/snap.yaml we treat "Custom Distribution" as classic. */ + mock_meta_snap_yaml(NULL); g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CLASSIC); - unlink("os-release.core"); + + /* With /meta/snap.yaml we treat it as core instead. */ + mock_meta_snap_yaml(meta_snap_yaml_custom); + g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CORE_OTHER); } static void test_should_use_normal_mode(void) @@ -132,6 +198,7 @@ static void __attribute__ ((constructor)) init(void) g_test_add_func("/classic/on-core-on20", test_is_on_core_on20); g_test_add_func("/classic/on-fedora-base", test_is_on_fedora_base); g_test_add_func("/classic/on-fedora-ws", test_is_on_fedora_ws); + g_test_add_func("/classic/on-custom-base", test_is_on_custom_base); g_test_add_func("/classic/should-use-normal-mode", test_should_use_normal_mode); } diff --git a/cmd/libsnap-confine-private/classic.c b/cmd/libsnap-confine-private/classic.c index 81baaca962..57f681c3ab 100644 --- a/cmd/libsnap-confine-private/classic.c +++ b/cmd/libsnap-confine-private/classic.c @@ -8,7 +8,8 @@ #include <string.h> #include <unistd.h> -char *os_release = "/etc/os-release"; +static const char *os_release = "/etc/os-release"; +static const char *meta_snap_yaml = "/meta/snap.yaml"; sc_distro sc_classify_distro(void) { @@ -38,6 +39,14 @@ sc_distro sc_classify_distro(void) } } + if (!is_core) { + /* Since classic systems don't have a /meta/snap.yaml file the simple + presence of that file qualifies as SC_DISTRO_CORE_OTHER. */ + if (access(meta_snap_yaml, F_OK) == 0) { + is_core = true; + } + } + if (is_core) { if (core_version == 16) { return SC_DISTRO_CORE16; diff --git a/cmd/libsnap-confine-private/snap-test.c b/cmd/libsnap-confine-private/snap-test.c index e4042129f7..b605e1257e 100644 --- a/cmd/libsnap-confine-private/snap-test.c +++ b/cmd/libsnap-confine-private/snap-test.c @@ -30,6 +30,12 @@ static void test_verify_security_tag(void) g_assert_true(verify_security_tag("snap.f00.bar-baz1", "f00")); g_assert_true(verify_security_tag("snap.foo.hook.bar", "foo")); g_assert_true(verify_security_tag("snap.foo.hook.bar-baz", "foo")); + g_assert_true(verify_security_tag + ("snap.foo_instance.bar-baz", "foo_instance")); + g_assert_true(verify_security_tag + ("snap.foo_instance.hook.bar-baz", "foo_instance")); + g_assert_true(verify_security_tag + ("snap.foo_bar.hook.bar-baz", "foo_bar")); // Now, test the names we know are bad g_assert_false(verify_security_tag @@ -62,31 +68,63 @@ static void test_verify_security_tag(void) g_assert_false(verify_security_tag("snap..name.app", ".name")); g_assert_false(verify_security_tag("snap.name..app", "name.")); g_assert_false(verify_security_tag("snap.name.app..", "name")); + // These contain invalid instance key + g_assert_false(verify_security_tag("snap.foo_.bar-baz", "foo")); + g_assert_false(verify_security_tag + ("snap.foo_toolonginstance.bar-baz", "foo")); + g_assert_false(verify_security_tag + ("snap.foo_inst@nace.bar-baz", "foo")); + g_assert_false(verify_security_tag + ("snap.foo_in-stan-ce.bar-baz", "foo")); + g_assert_false(verify_security_tag("snap.foo_in stan.bar-baz", "foo")); // Test names that are both good, but snap name doesn't match security tag g_assert_false(verify_security_tag("snap.foo.hook.bar", "fo")); g_assert_false(verify_security_tag("snap.foo.hook.bar", "fooo")); g_assert_false(verify_security_tag("snap.foo.hook.bar", "snap")); g_assert_false(verify_security_tag("snap.foo.hook.bar", "bar")); + g_assert_false(verify_security_tag("snap.foo_instance.bar", "foo_bar")); // Regression test 12to8 g_assert_true(verify_security_tag("snap.12to8.128to8", "12to8")); g_assert_true(verify_security_tag("snap.123test.123test", "123test")); g_assert_true(verify_security_tag ("snap.123test.hook.configure", "123test")); +} +static void test_sc_is_hook_security_tag(void) +{ + // First, test the names we know are good + g_assert_true(sc_is_hook_security_tag("snap.foo.hook.bar")); + g_assert_true(sc_is_hook_security_tag("snap.foo.hook.bar-baz")); + g_assert_true(sc_is_hook_security_tag + ("snap.foo_instance.hook.bar-baz")); + g_assert_true(sc_is_hook_security_tag("snap.foo_bar.hook.bar-baz")); + + // Now, test the names we know are not valid hook security tags + g_assert_false(sc_is_hook_security_tag("snap.foo_instance.bar-baz")); + g_assert_false(sc_is_hook_security_tag("snap.name.app!hook.foo")); + g_assert_false(sc_is_hook_security_tag("snap.name.app.hook!foo")); + g_assert_false(sc_is_hook_security_tag("snap.name.app.hook.-foo")); + g_assert_false(sc_is_hook_security_tag("snap.name.app.hook.f00")); } -static void test_sc_snap_name_validate(void) +static void test_sc_snap_or_instance_name_validate(gconstpointer data) { + typedef void (*validate_func_t) (const char *, struct sc_error **); + + validate_func_t validate = (validate_func_t) data; + bool is_instance = + (validate == sc_instance_name_validate) ? true : false; + struct sc_error *err = NULL; // Smoke test, a valid snap name - sc_snap_name_validate("hello-world", &err); + validate("hello-world", &err); g_assert_null(err); // Smoke test: invalid character - sc_snap_name_validate("hello world", &err); + validate("hello world", &err); g_assert_nonnull(err); g_assert_true(sc_error_match (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); @@ -95,7 +133,7 @@ static void test_sc_snap_name_validate(void) sc_error_free(err); // Smoke test: no letters - sc_snap_name_validate("", &err); + validate("", &err); g_assert_nonnull(err); g_assert_true(sc_error_match (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); @@ -104,7 +142,7 @@ static void test_sc_snap_name_validate(void) sc_error_free(err); // Smoke test: leading dash - sc_snap_name_validate("-foo", &err); + validate("-foo", &err); g_assert_nonnull(err); g_assert_true(sc_error_match (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); @@ -113,7 +151,7 @@ static void test_sc_snap_name_validate(void) sc_error_free(err); // Smoke test: trailing dash - sc_snap_name_validate("foo-", &err); + validate("foo-", &err); g_assert_nonnull(err); g_assert_true(sc_error_match (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); @@ -122,7 +160,7 @@ static void test_sc_snap_name_validate(void) sc_error_free(err); // Smoke test: double dash - sc_snap_name_validate("f--oo", &err); + validate("f--oo", &err); g_assert_nonnull(err); g_assert_true(sc_error_match (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); @@ -131,11 +169,22 @@ static void test_sc_snap_name_validate(void) sc_error_free(err); // Smoke test: NULL name is not valid - sc_snap_name_validate(NULL, &err); + validate(NULL, &err); g_assert_nonnull(err); - g_assert_true(sc_error_match - (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); - g_assert_cmpstr(sc_error_msg(err), ==, "snap name cannot be NULL"); + // the only case when instance name validation diverges from snap name + // validation + if (!is_instance) { + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); + g_assert_cmpstr(sc_error_msg(err), ==, + "snap name cannot be NULL"); + } else { + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, + SC_SNAP_INVALID_INSTANCE_NAME)); + g_assert_cmpstr(sc_error_msg(err), ==, + "snap instance name cannot be NULL"); + } sc_error_free(err); const char *valid_names[] = { @@ -146,7 +195,7 @@ static void test_sc_snap_name_validate(void) }; for (size_t i = 0; i < sizeof valid_names / sizeof *valid_names; ++i) { g_test_message("checking valid snap name: %s", valid_names[i]); - sc_snap_name_validate(valid_names[i], &err); + validate(valid_names[i], &err); g_assert_null(err); } const char *invalid_names[] = { @@ -175,16 +224,16 @@ static void test_sc_snap_name_validate(void) ++i) { g_test_message("checking invalid snap name: >%s<", invalid_names[i]); - sc_snap_name_validate(invalid_names[i], &err); + validate(invalid_names[i], &err); g_assert_nonnull(err); g_assert_true(sc_error_match (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); sc_error_free(err); } // Regression test: 12to8 and 123test - sc_snap_name_validate("12to8", &err); + validate("12to8", &err); g_assert_null(err); - sc_snap_name_validate("123test", &err); + validate("123test", &err); g_assert_null(err); // In case we switch to a regex, here's a test that could break things. @@ -194,7 +243,7 @@ static void test_sc_snap_name_validate(void) g_assert_nonnull(memcpy(varname, good_bad_name, i)); varname[i] = 0; g_test_message("checking valid snap name: >%s<", varname); - sc_snap_name_validate(varname, &err); + validate(varname, &err); g_assert_null(err); sc_error_free(err); } @@ -214,6 +263,94 @@ static void test_sc_snap_name_validate__respects_error_protocol(void) ("snap name must use lower case letters, digits or dashes\n"); } +static void test_sc_instance_name_validate(void) +{ + struct sc_error *err = NULL; + + sc_instance_name_validate("hello-world", &err); + g_assert_null(err); + sc_instance_name_validate("hello-world_foo", &err); + g_assert_null(err); + + // just the separator + sc_instance_name_validate("_", &err); + g_assert_nonnull(err); + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); + g_assert_cmpstr(sc_error_msg(err), ==, + "snap name must contain at least one letter"); + sc_error_free(err); + + // just name, with separator, missing instance key + sc_instance_name_validate("hello-world_", &err); + g_assert_nonnull(err); + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_INSTANCE_KEY)); + g_assert_cmpstr(sc_error_msg(err), ==, + "instance key must contain at least one letter or digit"); + sc_error_free(err); + + // only separator and instance key, missing name + sc_instance_name_validate("_bar", &err); + g_assert_nonnull(err); + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); + g_assert_cmpstr(sc_error_msg(err), ==, + "snap name must contain at least one letter"); + sc_error_free(err); + + sc_instance_name_validate("", &err); + g_assert_nonnull(err); + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); + g_assert_cmpstr(sc_error_msg(err), ==, + "snap name must contain at least one letter"); + sc_error_free(err); + + // third separator + sc_instance_name_validate("foo_bar_baz", &err); + g_assert_nonnull(err); + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_INSTANCE_NAME)); + g_assert_cmpstr(sc_error_msg(err), ==, + "snap instance name can contain only one underscore"); + sc_error_free(err); + + const char *valid_names[] = { + "a", "aa", "aaa", "aaaa", + "a_a", "aa_1", "a_123", "a_0123456789", + }; + for (size_t i = 0; i < sizeof valid_names / sizeof *valid_names; ++i) { + g_test_message("checking valid instance name: %s", + valid_names[i]); + sc_instance_name_validate(valid_names[i], &err); + g_assert_null(err); + } + const char *invalid_names[] = { + // only letters and digits in the instance key + "a_--23))", "a_ ", "a_091234#", "a_123_456", + // up to 10 characters for the instance key + "a_01234567891", "a_0123456789123", + // snap name must not be more than 40 characters, regardless of instance + // key + "01234567890123456789012345678901234567890_foobar", + "01234567890123456789-01234567890123456789_foobar", + // instance key must be plain ASCII + "foobar_日本語", + // way too many underscores + "foobar_baz_zed_daz", + "foobar______", + }; + for (size_t i = 0; i < sizeof invalid_names / sizeof *invalid_names; + ++i) { + g_test_message("checking invalid instance name: >%s<", + invalid_names[i]); + sc_instance_name_validate(invalid_names[i], &err); + g_assert_nonnull(err); + sc_error_free(err); + } +} + static void test_sc_snap_drop_instance_key_no_dest(void) { if (g_test_subprocess()) { @@ -392,10 +529,21 @@ static void test_sc_snap_split_instance_name_basic(void) static void __attribute__ ((constructor)) init(void) { g_test_add_func("/snap/verify_security_tag", test_verify_security_tag); - g_test_add_func("/snap/sc_snap_name_validate", - test_sc_snap_name_validate); + g_test_add_func("/snap/sc_is_hook_security_tag", + test_sc_is_hook_security_tag); + + g_test_add_data_func("/snap/sc_snap_name_validate", + sc_snap_name_validate, + test_sc_snap_or_instance_name_validate); g_test_add_func("/snap/sc_snap_name_validate/respects_error_protocol", test_sc_snap_name_validate__respects_error_protocol); + + g_test_add_data_func("/snap/sc_instance_name_validate/just_name", + sc_instance_name_validate, + test_sc_snap_or_instance_name_validate); + g_test_add_func("/snap/sc_instance_name_validate/full", + test_sc_instance_name_validate); + g_test_add_func("/snap/sc_snap_drop_instance_key/basic", test_sc_snap_drop_instance_key_basic); g_test_add_func("/snap/sc_snap_drop_instance_key/no_dest", @@ -406,6 +554,7 @@ static void __attribute__ ((constructor)) init(void) test_sc_snap_drop_instance_key_short_dest); g_test_add_func("/snap/sc_snap_drop_instance_key/short_dest2", test_sc_snap_drop_instance_key_short_dest2); + g_test_add_func("/snap/sc_snap_split_instance_name/basic", test_sc_snap_split_instance_name_basic); g_test_add_func("/snap/sc_snap_split_instance_name/trailing_nil", diff --git a/cmd/libsnap-confine-private/snap.c b/cmd/libsnap-confine-private/snap.c index 8619d7849f..f4fb8507f9 100644 --- a/cmd/libsnap-confine-private/snap.c +++ b/cmd/libsnap-confine-private/snap.c @@ -22,6 +22,7 @@ #include <stddef.h> #include <stdlib.h> #include <string.h> +#include <ctype.h> #include "utils.h" #include "string-utils.h" @@ -30,7 +31,7 @@ bool verify_security_tag(const char *security_tag, const char *snap_name) { const char *whitelist_re = - "^snap\\.([a-z0-9](-?[a-z0-9])*)\\.([a-zA-Z0-9](-?[a-zA-Z0-9])*|hook\\.[a-z](-?[a-z])*)$"; + "^snap\\.([a-z0-9](-?[a-z0-9])*(_[a-z0-9]{1,10})?)\\.([a-zA-Z0-9](-?[a-zA-Z0-9])*|hook\\.[a-z](-?[a-z])*)$"; regex_t re; if (regcomp(&re, whitelist_re, REG_EXTENDED) != 0) die("can not compile regex %s", whitelist_re); @@ -56,7 +57,7 @@ bool verify_security_tag(const char *security_tag, const char *snap_name) bool sc_is_hook_security_tag(const char *security_tag) { const char *whitelist_re = - "^snap\\.[a-z](-?[a-z0-9])*\\.(hook\\.[a-z](-?[a-z])*)$"; + "^snap\\.[a-z](-?[a-z0-9])*(_[a-z0-9]{1,10})?\\.(hook\\.[a-z](-?[a-z])*)$"; regex_t re; if (regcomp(&re, whitelist_re, REG_EXTENDED | REG_NOSUB) != 0) @@ -97,6 +98,94 @@ static int skip_one_char(const char **p, char c) return 0; } +void sc_instance_name_validate(const char *instance_name, + struct sc_error **errorp) +{ + // NOTE: This function should be synchronized with the two other + // implementations: validate_instance_name and snap.ValidateInstanceName. + struct sc_error *err = NULL; + + // Ensure that name is not NULL + if (instance_name == NULL) { + err = + sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_INSTANCE_NAME, + "snap instance name cannot be NULL"); + goto out; + } + // 40 char snap_name + '_' + 10 char instance_key + 1 extra overflow + 1 + // NULL + char s[53] = { 0 }; + strncpy(s, instance_name, sizeof(s) - 1); + + char *t = s; + const char *snap_name = strsep(&t, "_"); + const char *instance_key = strsep(&t, "_"); + const char *third_separator = strsep(&t, "_"); + if (third_separator != NULL) { + err = + sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_INSTANCE_NAME, + "snap instance name can contain only one underscore"); + goto out; + } + + sc_snap_name_validate(snap_name, &err); + if (err != NULL) { + goto out; + } + // When the instance_name is a normal snap name, instance_key will be + // NULL, so only validate instance_key when we found one. + if (instance_key != NULL) { + sc_instance_key_validate(instance_key, &err); + } + + out: + sc_error_forward(errorp, err); +} + +void sc_instance_key_validate(const char *instance_key, + struct sc_error **errorp) +{ + // NOTE: see snap.ValidateInstanceName for reference of a valid instance key + // format + struct sc_error *err = NULL; + + // Ensure that name is not NULL + if (instance_key == NULL) { + err = sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME, + "instance key cannot be NULL"); + goto out; + } + // This is a regexp-free routine hand-coding the following pattern: + // + // "^[a-z]{1,10}$" + // + // The only motivation for not using regular expressions is so that we don't + // run untrusted input against a potentially complex regular expression + // engine. + int i = 0; + for (i = 0; instance_key[i] != '\0'; i++) { + if (islower(instance_key[i]) || isdigit(instance_key[i])) { + continue; + } + err = + sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_INSTANCE_KEY, + "instance key must use lower case letters or digits"); + goto out; + } + + if (i == 0) { + err = + sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_INSTANCE_KEY, + "instance key must contain at least one letter or digit"); + } else if (i > 10) { + err = + sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_INSTANCE_KEY, + "instance key must be shorter than 10 characters"); + } + out: + sc_error_forward(errorp, err); +} + void sc_snap_name_validate(const char *snap_name, struct sc_error **errorp) { // NOTE: This function should be synchronized with the two other diff --git a/cmd/libsnap-confine-private/snap.h b/cmd/libsnap-confine-private/snap.h index ef694c83d7..49851289f2 100644 --- a/cmd/libsnap-confine-private/snap.h +++ b/cmd/libsnap-confine-private/snap.h @@ -31,6 +31,10 @@ enum { /** The name of the snap is not valid. */ SC_SNAP_INVALID_NAME = 1, + /** The instance key of the snap is not valid. */ + SC_SNAP_INVALID_INSTANCE_KEY = 2, + /** The instance of the snap is not valid. */ + SC_SNAP_INVALID_INSTANCE_NAME = 3, }; /** @@ -45,6 +49,31 @@ enum { void sc_snap_name_validate(const char *snap_name, struct sc_error **errorp); /** + * Validate the given instance key. + * + * Valid instance key cannot be NULL and must match a regular expression + * describing the strict naming requirements. Please refer to snapd source code + * for details. + * + * The error protocol is observed so if the caller doesn't provide an outgoing + * error pointer the function will die on any error. + **/ +void sc_instance_key_validate(const char *instance_key, + struct sc_error **errorp); + +/** + * Validate the given snap instance name. + * + * Valid instance name must be composed of a valid snap name and a valid + * instance key. + * + * The error protocol is observed so if the caller doesn't provide an outgoing + * error pointer the function will die on any error. + **/ +void sc_instance_name_validate(const char *instance_name, + struct sc_error **errorp); + +/** * Validate security tag against strict naming requirements and snap name. * * The executable name is of form: diff --git a/cmd/snap-exec/main.go b/cmd/snap-exec/main.go index 8a5c865af7..a8e640e787 100644 --- a/cmd/snap-exec/main.go +++ b/cmd/snap-exec/main.go @@ -39,8 +39,9 @@ var syscallExec = syscall.Exec // commandline args var opts struct { - Command string `long:"command" description:"use a different command like {stop,post-stop} from the app"` - Hook string `long:"hook" description:"hook to run" hidden:"yes"` + Command string `long:"command" description:"use a different command like {stop,post-stop} from the app"` + SkipCommandChain bool `long:"skip-command-chain" description:"do not run command chain"` + Hook string `long:"hook" description:"hook to run" hidden:"yes"` } func init() { @@ -92,7 +93,7 @@ func run() error { return execHook(snapApp, revision, opts.Hook) } - return execApp(snapApp, revision, opts.Command, extraArgs) + return execApp(snapApp, revision, opts.Command, extraArgs, opts.SkipCommandChain) } const defaultShell = "/bin/bash" @@ -124,6 +125,17 @@ func findCommand(app *snap.AppInfo, command string) (string, error) { return cmd, nil } +func commandChain(app *snap.AppInfo) []string { + chain := make([]string, 0, len(app.CommandChain)) + snapMountDir := app.Snap.MountDir() + + for _, element := range app.CommandChain { + chain = append(chain, filepath.Join(snapMountDir, element)) + } + + return chain +} + // expandEnvCmdArgs takes the string list of commandline arguments // and expands any $VAR with the given var from the env argument. func expandEnvCmdArgs(args []string, env map[string]string) []string { @@ -139,7 +151,7 @@ func expandEnvCmdArgs(args []string, env map[string]string) []string { return cmdArgs } -func execApp(snapApp, revision, command string, args []string) error { +func execApp(snapApp, revision, command string, args []string, skipCommandChain bool) error { rev, err := snap.ParseRevision(revision) if err != nil { return fmt.Errorf("cannot parse revision %q: %s", revision, err) @@ -200,6 +212,11 @@ func execApp(snapApp, revision, command string, args []string) error { } fullCmd = append(fullCmd, cmdArgs...) fullCmd = append(fullCmd, args...) + + if !skipCommandChain { + fullCmd = append(commandChain(app), fullCmd...) + } + if err := syscallExec(fullCmd[0], fullCmd, env); err != nil { return fmt.Errorf("cannot exec %q: %s", fullCmd[0], err) } diff --git a/cmd/snap-exec/main_test.go b/cmd/snap-exec/main_test.go index 879f5d87dd..c1d678adbd 100644 --- a/cmd/snap-exec/main_test.go +++ b/cmd/snap-exec/main_test.go @@ -65,6 +65,11 @@ apps: BASE_PATH: /some/path LD_LIBRARY_PATH: ${BASE_PATH}/lib MY_PATH: $PATH + app2: + command: run-app2 + stop-command: stop-app2 + post-stop-command: post-stop-app2 + command-chain: [chain1, chain2] nostop: command: nostop `) @@ -146,7 +151,7 @@ func (s *snapExecSuite) TestSnapExecAppIntegration(c *C) { defer restore() // launch and verify its run the right way - err := snapExec.ExecApp("snapname.app", "42", "stop", []string{"arg1", "arg2"}) + err := snapExec.ExecApp("snapname.app", "42", "stop", []string{"arg1", "arg2"}, false) c.Assert(err, IsNil) c.Check(execArgv0, Equals, fmt.Sprintf("%s/snapname/42/stop-app", dirs.SnapMountDir)) c.Check(execArgs, DeepEquals, []string{execArgv0, "arg1", "arg2"}) @@ -155,6 +160,58 @@ func (s *snapExecSuite) TestSnapExecAppIntegration(c *C) { c.Check(execEnv, testutil.Contains, fmt.Sprintf("MY_PATH=%s", os.Getenv("PATH"))) } +func (s *snapExecSuite) TestSnapExecAppCommandChainIntegration(c *C) { + dirs.SetRootDir(c.MkDir()) + snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R("42"), + }) + + execArgv0 := "" + execArgs := []string{} + restore := snapExec.MockSyscallExec(func(argv0 string, argv []string, env []string) error { + execArgv0 = argv0 + execArgs = argv + return nil + }) + defer restore() + + chain1_path := fmt.Sprintf("%s/snapname/42/chain1", dirs.SnapMountDir) + chain2_path := fmt.Sprintf("%s/snapname/42/chain2", dirs.SnapMountDir) + app_path := fmt.Sprintf("%s/snapname/42/run-app2", dirs.SnapMountDir) + stop_path := fmt.Sprintf("%s/snapname/42/stop-app2", dirs.SnapMountDir) + post_stop_path := fmt.Sprintf("%s/snapname/42/post-stop-app2", dirs.SnapMountDir) + + for _, t := range []struct { + cmd string + args []string + skipCommandChain bool + expected []string + }{ + // Normal command + {expected: []string{chain1_path, chain2_path, app_path}}, + {skipCommandChain: true, expected: []string{app_path}}, + {args: []string{"arg1", "arg2"}, expected: []string{chain1_path, chain2_path, app_path, "arg1", "arg2"}}, + {args: []string{"arg1", "arg2"}, skipCommandChain: true, expected: []string{app_path, "arg1", "arg2"}}, + + // Stop command + {cmd: "stop", expected: []string{chain1_path, chain2_path, stop_path}}, + {cmd: "stop", skipCommandChain: true, expected: []string{stop_path}}, + {cmd: "stop", args: []string{"arg1", "arg2"}, expected: []string{chain1_path, chain2_path, stop_path, "arg1", "arg2"}}, + {cmd: "stop", args: []string{"arg1", "arg2"}, skipCommandChain: true, expected: []string{stop_path, "arg1", "arg2"}}, + + // Post-stop command + {cmd: "post-stop", expected: []string{chain1_path, chain2_path, post_stop_path}}, + {cmd: "post-stop", skipCommandChain: true, expected: []string{post_stop_path}}, + {cmd: "post-stop", args: []string{"arg1", "arg2"}, expected: []string{chain1_path, chain2_path, post_stop_path, "arg1", "arg2"}}, + {cmd: "post-stop", args: []string{"arg1", "arg2"}, skipCommandChain: true, expected: []string{post_stop_path, "arg1", "arg2"}}, + } { + err := snapExec.ExecApp("snapname.app2", "42", t.cmd, t.args, t.skipCommandChain) + c.Assert(err, IsNil) + c.Check(execArgv0, Equals, t.expected[0]) + c.Check(execArgs, DeepEquals, t.expected) + } +} + func (s *snapExecSuite) TestSnapExecHookIntegration(c *C) { dirs.SetRootDir(c.MkDir()) snaptest.MockSnap(c, string(mockHookYaml), &snap.SideInfo{ @@ -306,11 +363,25 @@ func (s *snapExecSuite) TestSnapExecShellIntegration(c *C) { defer restore() // launch and verify its run the right way - err := snapExec.ExecApp("snapname.app", "42", "shell", []string{"-c", "echo foo"}) + err := snapExec.ExecApp("snapname.app", "42", "shell", []string{"-c", "echo foo"}, false) c.Assert(err, IsNil) c.Check(execArgv0, Equals, "/bin/bash") c.Check(execArgs, DeepEquals, []string{execArgv0, "-c", "echo foo"}) c.Check(execEnv, testutil.Contains, "LD_LIBRARY_PATH=/some/path/lib") + + // launch and verify shell still runs the command chain + err = snapExec.ExecApp("snapname.app2", "42", "shell", []string{"-c", "echo foo"}, false) + c.Assert(err, IsNil) + chain1 := fmt.Sprintf("%s/snapname/42/chain1", dirs.SnapMountDir) + chain2 := fmt.Sprintf("%s/snapname/42/chain2", dirs.SnapMountDir) + c.Check(execArgv0, Equals, chain1) + c.Check(execArgs, DeepEquals, []string{chain1, chain2, "/bin/bash", "-c", "echo foo"}) + + // also verify that it supports skipping the command chain + err = snapExec.ExecApp("snapname.app2", "42", "shell", []string{"-c", "echo foo"}, true) + c.Assert(err, IsNil) + c.Check(execArgv0, Equals, "/bin/bash") + c.Check(execArgs, DeepEquals, []string{execArgv0, "-c", "echo foo"}) } func (s *snapExecSuite) TestSnapExecAppIntegrationWithVars(c *C) { @@ -335,7 +406,7 @@ func (s *snapExecSuite) TestSnapExecAppIntegrationWithVars(c *C) { defer os.Unsetenv("SNAP_DATA") // launch and verify its run the right way - err := snapExec.ExecApp("snapname.app", "42", "", []string{"user-arg1"}) + err := snapExec.ExecApp("snapname.app", "42", "", []string{"user-arg1"}, false) c.Assert(err, IsNil) c.Check(execArgv0, Equals, fmt.Sprintf("%s/snapname/42/run-app", dirs.SnapMountDir)) c.Check(execArgs, DeepEquals, []string{execArgv0, "cmd-arg1", "/var/snap/snapname/42", "user-arg1"}) diff --git a/cmd/snap-update-ns/bootstrap.c b/cmd/snap-update-ns/bootstrap.c index 1dbb7bbff1..98c78b8d8e 100644 --- a/cmd/snap-update-ns/bootstrap.c +++ b/cmd/snap-update-ns/bootstrap.c @@ -24,6 +24,7 @@ #include "bootstrap.h" +#include <ctype.h> #include <errno.h> #include <fcntl.h> #include <grp.h> @@ -247,6 +248,82 @@ int validate_snap_name(const char* snap_name) return 0; } +static int instance_key_validate(const char *instance_key) +{ + // NOTE: see snap.ValidateInstanceName for reference of a valid instance key + // format + + // Ensure that name is not NULL + if (instance_key == NULL) { + bootstrap_msg = "instance key cannot be NULL"; + return -1; + } + + // This is a regexp-free routine hand-coding the following pattern: + // + // "^[a-z]{1,10}$" + // + // The only motivation for not using regular expressions is so that we don't + // run untrusted input against a potentially complex regular expression + // engine. + int i = 0; + for (i = 0; instance_key[i] != '\0'; i++) { + if (islower(instance_key[i]) || isdigit(instance_key[i])) { + continue; + } + bootstrap_msg = "instance key must use lower case letters or digits"; + return -1; + } + + if (i == 0) { + bootstrap_msg = "instance key must contain at least one letter or digit"; + return -1; + } else if (i > 10) { + bootstrap_msg = "instance key must be shorter than 10 characters"; + return -1; + } + return 0; +} + +// validate_instance_name performs full validation of the given snap instance name. +int validate_instance_name(const char* instance_name) +{ + // NOTE: This function should be synchronized with the two other + // implementations: sc_instance_name_validate and snap.ValidateInstanceName. + + if (instance_name == NULL) { + bootstrap_msg = "snap instance name cannot be NULL"; + return -1; + } + + // 40 char snap_name + '_' + 10 char instance_key + 1 extra overflow + 1 + // NULL + char s[53] = {0}; + strncpy(s, instance_name, sizeof(s)-1); + + char *t = s; + const char *snap_name = strsep(&t, "_"); + const char *instance_key = strsep(&t, "_"); + const char *third_separator = strsep(&t, "_"); + if (third_separator != NULL) { + bootstrap_msg = "snap instance name can contain only one underscore"; + return -1; + } + + if (validate_snap_name(snap_name) < 0) { + return -1; + } + + // When the instance_name is a normal snap name, instance_key will be + // NULL, so only validate instance_key when we found one. + if (instance_key != NULL && instance_key_validate(instance_key) < 0) { + return -1; + } + + return 0; +} + + // process_arguments parses given a command line // argc and argv are defined as for the main() function void process_arguments(int argc, char *const *argv, const char** snap_name_out, bool* should_setns_out, bool* process_user_fstab) @@ -312,11 +389,11 @@ void process_arguments(int argc, char *const *argv, const char** snap_name_out, return; } - // Ensure that the snap name is valid so that we don't blindly setns into + // Ensure that the snap instance name is valid so that we don't blindly setns into // something that is controlled by a potential attacker. - if (validate_snap_name(snap_name) < 0) { + if (validate_instance_name(snap_name) < 0) { bootstrap_errno = 0; - // bootstap_msg is set by validate_snap_name; + // bootstap_msg is set by validate_instance_name; return; } // We have a valid snap name now so let's store it. diff --git a/cmd/snap-update-ns/bootstrap.go b/cmd/snap-update-ns/bootstrap.go index 48a8ad4afb..3b937ee77f 100644 --- a/cmd/snap-update-ns/bootstrap.go +++ b/cmd/snap-update-ns/bootstrap.go @@ -73,6 +73,11 @@ func BootstrapError() error { return fmt.Errorf("%s", C.GoString(C.bootstrap_msg)) } +func clearBootstrapError() { + C.bootstrap_msg = nil + C.bootstrap_errno = 0 +} + // END IMPORTANT func makeArgv(args []string) []*C.char { @@ -90,12 +95,12 @@ func freeArgv(argv []*C.char) { } } -// validateSnapName checks if snap name is valid. +// validateInstanceName checks if snap instance name is valid. // This also sets bootstrap_msg on failure. -func validateSnapName(snapName string) int { - cStr := C.CString(snapName) +func validateInstanceName(instanceName string) int { + cStr := C.CString(instanceName) defer C.free(unsafe.Pointer(cStr)) - return int(C.validate_snap_name(cStr)) + return int(C.validate_instance_name(cStr)) } // processArguments parses commnad line arguments. diff --git a/cmd/snap-update-ns/bootstrap.h b/cmd/snap-update-ns/bootstrap.h index ba2850704d..da9f40a544 100644 --- a/cmd/snap-update-ns/bootstrap.h +++ b/cmd/snap-update-ns/bootstrap.h @@ -28,6 +28,6 @@ extern const char* bootstrap_msg; void bootstrap(int argc, char **argv, char **envp); void process_arguments(int argc, char *const *argv, const char** snap_name_out, bool* should_setns_out, bool* process_user_fstab); -int validate_snap_name(const char* snap_name); +int validate_instance_name(const char* instance_name); #endif diff --git a/cmd/snap-update-ns/bootstrap_test.go b/cmd/snap-update-ns/bootstrap_test.go index de2f5afb5b..7d97f38975 100644 --- a/cmd/snap-update-ns/bootstrap_test.go +++ b/cmd/snap-update-ns/bootstrap_test.go @@ -31,12 +31,31 @@ var _ = Suite(&bootstrapSuite{}) // Check that ValidateSnapName rejects "/" and "..". func (s *bootstrapSuite) TestValidateSnapName(c *C) { - c.Assert(update.ValidateSnapName("hello-world"), Equals, 0) - c.Assert(update.ValidateSnapName("hello/world"), Equals, -1) - c.Assert(update.ValidateSnapName("hello..world"), Equals, -1) - c.Assert(update.ValidateSnapName("INVALID"), Equals, -1) - c.Assert(update.ValidateSnapName("-invalid"), Equals, -1) - c.Assert(update.ValidateSnapName(""), Equals, -1) + c.Assert(update.ValidateInstanceName("hello-world"), Equals, 0) + c.Assert(update.ValidateInstanceName("a123456789012345678901234567890123456789"), Equals, 0) + c.Assert(update.ValidateInstanceName("a123456789012345678901234567890123456789_0123456789"), Equals, 0) + c.Assert(update.ValidateInstanceName("a123456789012345678901234567890123456789_01234567890"), Equals, -1) + c.Assert(update.ValidateInstanceName("hello/world"), Equals, -1) + c.Assert(update.ValidateInstanceName("hello..world"), Equals, -1) + c.Assert(update.ValidateInstanceName("hello-world_foo"), Equals, 0) + c.Assert(update.ValidateInstanceName("foo_0123456789"), Equals, 0) + c.Assert(update.ValidateInstanceName("foo_1234abcd"), Equals, 0) + c.Assert(update.ValidateInstanceName("a123456789012345678901234567890123456789"), Equals, 0) + c.Assert(update.ValidateInstanceName("a123456789012345678901234567890123456789_0123456789"), Equals, 0) + + c.Assert(update.ValidateInstanceName("INVALID"), Equals, -1) + c.Assert(update.ValidateInstanceName("-invalid"), Equals, -1) + c.Assert(update.ValidateInstanceName(""), Equals, -1) + c.Assert(update.ValidateInstanceName("hello-world_"), Equals, -1) + c.Assert(update.ValidateInstanceName("_foo"), Equals, -1) + c.Assert(update.ValidateInstanceName("foo_01234567890"), Equals, -1) + c.Assert(update.ValidateInstanceName("foo_123_456"), Equals, -1) + c.Assert(update.ValidateInstanceName("foo__456"), Equals, -1) + c.Assert(update.ValidateInstanceName("foo_"), Equals, -1) + c.Assert(update.ValidateInstanceName("hello-world_foo_foo"), Equals, -1) + c.Assert(update.ValidateInstanceName("foo01234567890012345678900123456789001234567890"), Equals, -1) + c.Assert(update.ValidateInstanceName("foo01234567890012345678900123456789001234567890_foo"), Equals, -1) + c.Assert(update.ValidateInstanceName("a123456789012345678901234567890123456789_0123456789_"), Equals, -1) } // Test various cases of command line handling. @@ -56,6 +75,7 @@ func (s *bootstrapSuite) TestProcessArguments(c *C) { {[]string{"argv0"}, "", false, false, "snap name not provided"}, // Snap name is parsed correctly. {[]string{"argv0", "snapname"}, "snapname", true, false, ""}, + {[]string{"argv0", "snapname_instance"}, "snapname_instance", true, false, ""}, // Onlye one snap name is allowed. {[]string{"argv0", "snapone", "snaptwo"}, "", false, false, "too many positional arguments"}, // Snap name is validated correctly. @@ -64,6 +84,8 @@ func (s *bootstrapSuite) TestProcessArguments(c *C) { {[]string{"argv0", "invalid-"}, "", false, false, "snap name cannot end with a dash"}, {[]string{"argv0", "@invalid"}, "", false, false, "snap name must use lower case letters, digits or dashes"}, {[]string{"argv0", "INVALID"}, "", false, false, "snap name must use lower case letters, digits or dashes"}, + {[]string{"argv0", "foo_01234567890"}, "", false, false, "instance key must be shorter than 10 characters"}, + {[]string{"argv0", "foo_0123456_2"}, "", false, false, "snap instance name can contain only one underscore"}, // The option --from-snap-confine disables setns. {[]string{"argv0", "--from-snap-confine", "snapname"}, "snapname", false, false, ""}, {[]string{"argv0", "snapname", "--from-snap-confine"}, "snapname", false, false, ""}, @@ -75,6 +97,7 @@ func (s *bootstrapSuite) TestProcessArguments(c *C) { {[]string{"argv0", "--from-snap-confine", "-invalid", "snapname"}, "", false, false, "unsupported option"}, } for _, tc := range cases { + update.ClearBootstrapError() snapName, shouldSetNs, userFstab := update.ProcessArguments(tc.cmdline) err := update.BootstrapError() comment := Commentf("failed with cmdline %q, expected error pattern %q, actual error %q", diff --git a/cmd/snap-update-ns/export_test.go b/cmd/snap-update-ns/export_test.go index fcff72d559..e3ba02a6ab 100644 --- a/cmd/snap-update-ns/export_test.go +++ b/cmd/snap-update-ns/export_test.go @@ -30,8 +30,8 @@ import ( var ( // change - ValidateSnapName = validateSnapName - ProcessArguments = processArguments + ValidateInstanceName = validateInstanceName + ProcessArguments = processArguments // freezer FreezeSnapProcesses = freezeSnapProcesses ThawSnapProcesses = thawSnapProcesses @@ -42,6 +42,9 @@ var ( // main ComputeAndSaveChanges = computeAndSaveChanges ApplyUserFstab = applyUserFstab + + // bootstrap + ClearBootstrapError = clearBootstrapError ) // SystemCalls encapsulates various system interactions performed by this module. diff --git a/cmd/snap/cmd_get.go b/cmd/snap/cmd_get.go index 0a9e869260..11082f63f7 100644 --- a/cmd/snap/cmd_get.go +++ b/cmd/snap/cmd_get.go @@ -22,14 +22,12 @@ package main import ( "encoding/json" "fmt" - "os" "sort" "strings" "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/i18n" - "golang.org/x/crypto/ssh/terminal" ) var shortGetHelp = i18n.G("Print configuration options") @@ -176,10 +174,6 @@ func (x *cmdGet) outputList(conf map[string]interface{}) error { return nil } -var isTerminal = func() bool { - return terminal.IsTerminal(int(os.Stdin.Fd())) -} - // outputDefault will be used when no commandline switch to override the // output where used. The output follows the following rules: // - a single key with a string value is printed directly @@ -204,7 +198,7 @@ func (x *cmdGet) outputDefault(conf map[string]interface{}, snapName string, con // conf looks like a map if cfg, ok := confToPrint.(map[string]interface{}); ok { - if isTerminal() { + if isStdinTTY { return x.outputList(cfg) } diff --git a/cmd/snap/cmd_get_test.go b/cmd/snap/cmd_get_test.go index 251ffd5ecc..6ab143ff09 100644 --- a/cmd/snap/cmd_get_test.go +++ b/cmd/snap/cmd_get_test.go @@ -109,7 +109,7 @@ func (s *SnapSuite) runTests(cmds []getCmdArgs, c *C) { c.Logf("Test: %s", test.args) - restore := snapset.MockIsTerminal(test.isTerminal) + restore := snapset.MockIsStdinTTY(test.isTerminal) defer restore() _, err := snapset.Parser().ParseArgs(strings.Fields(test.args)) diff --git a/cmd/snap/cmd_run.go b/cmd/snap/cmd_run.go index 0006fd4d0a..b30d62fae6 100644 --- a/cmd/snap/cmd_run.go +++ b/cmd/snap/cmd_run.go @@ -55,10 +55,12 @@ var ( ) type cmdRun struct { - Command string `long:"command" hidden:"yes"` - HookName string `long:"hook" hidden:"yes"` - Revision string `short:"r" default:"unset" hidden:"yes"` - Shell bool `long:"shell" ` + Command string `long:"command" hidden:"yes"` + HookName string `long:"hook" hidden:"yes"` + Revision string `short:"r" default:"unset" hidden:"yes"` + Shell bool `long:"shell" ` + SkipCommandChain bool `long:"skip-command-chain"` + // This options is both a selector (use or don't use strace) and it // can also carry extra options for strace. This is why there is // "default" and "optional-value" to distinguish this. @@ -81,14 +83,15 @@ and environment. func() flags.Commander { return &cmdRun{} }, map[string]string{ - "command": i18n.G("Alternative command to run"), - "hook": i18n.G("Hook to run"), - "r": i18n.G("Use a specific snap revision when running hook"), - "shell": i18n.G("Run a shell instead of the command (useful for debugging)"), - "strace": i18n.G("Run the command under strace (useful for debugging). Extra strace options can be specified as well here. Pass --raw to strace early snap helpers."), - "gdb": i18n.G("Run the command with gdb"), - "timer": i18n.G("Run as a timer service with given schedule"), - "parser-ran": "", + "command": i18n.G("Alternative command to run"), + "hook": i18n.G("Hook to run"), + "r": i18n.G("Use a specific snap revision when running hook"), + "shell": i18n.G("Run a shell instead of the command (useful for debugging)"), + "skip-command-chain": i18n.G("Do not run the command chain (useful for debugging)"), + "strace": i18n.G("Run the command under strace (useful for debugging). Extra strace options can be specified as well here. Pass --raw to strace early snap helpers."), + "gdb": i18n.G("Run the command with gdb"), + "timer": i18n.G("Run as a timer service with given schedule"), + "parser-ran": "", }, nil) } @@ -749,6 +752,9 @@ func (x *cmdRun) runSnapConfine(info *snap.Info, securityTag, snapApp, hook stri if x.Command != "" { cmd = append(cmd, "--command="+x.Command) } + if x.SkipCommandChain { + cmd = append(cmd, "--skip-command-chain") + } if hook != "" { cmd = append(cmd, "--hook="+hook) diff --git a/cmd/snap/cmd_run_test.go b/cmd/snap/cmd_run_test.go index 234076045a..4f7012abb6 100644 --- a/cmd/snap/cmd_run_test.go +++ b/cmd/snap/cmd_run_test.go @@ -136,6 +136,39 @@ func (s *SnapSuite) TestSnapRunAppIntegration(c *check.C) { c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2") } +func (s *SnapSuite) TestSnapRunAppIntegrationSkipCommandChain(c *check.C) { + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R("x2"), + }) + + // 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", "--skip-command-chain", "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.DistroLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), + "snap.snapname.app", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "--skip-command-chain", "snapname.app", "--arg1", "arg2"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2") +} + func (s *SnapSuite) TestSnapRunClassicAppIntegration(c *check.C) { defer mockSnapConfine(dirs.DistroLibExecDir)() diff --git a/cmd/snap/cmd_snap_op.go b/cmd/snap/cmd_snap_op.go index e3043ca26b..79b4903ef0 100644 --- a/cmd/snap/cmd_snap_op.go +++ b/cmd/snap/cmd_snap_op.go @@ -466,6 +466,15 @@ func (x *cmdInstall) Execute([]string) error { x.setModes(opts) names := remoteSnapNames(x.Positional.Snaps) + if len(names) == 0 { + return errors.New(i18n.G("cannot install zero snaps")) + } + for _, name := range names { + if len(name) == 0 { + return errors.New(i18n.G("cannot install snap with empty name")) + } + } + if len(names) == 1 { return x.installOne(names[0], x.Name, opts) } diff --git a/cmd/snap/cmd_snap_op_test.go b/cmd/snap/cmd_snap_op_test.go index 156ff3c5b2..e84bbfcd1f 100644 --- a/cmd/snap/cmd_snap_op_test.go +++ b/cmd/snap/cmd_snap_op_test.go @@ -1468,6 +1468,15 @@ func (s *SnapOpSuite) TestInstallMany(c *check.C) { c.Check(n, check.Equals, total) } +func (s *SnapOpSuite) TestInstallZeroEmpty(c *check.C) { + _, err := snap.Parser().ParseArgs([]string{"install"}) + c.Assert(err, check.ErrorMatches, "cannot install zero snaps") + _, err = snap.Parser().ParseArgs([]string{"install", ""}) + c.Assert(err, check.ErrorMatches, "cannot install snap with empty name") + _, err = snap.Parser().ParseArgs([]string{"install", "", "bar"}) + c.Assert(err, check.ErrorMatches, "cannot install snap with empty name") +} + func (s *SnapOpSuite) TestNoWait(c *check.C) { s.srv.checker = func(r *http.Request) {} diff --git a/cmd/snap/cmd_wait.go b/cmd/snap/cmd_wait.go index 12b3f6a1cc..069469ea0d 100644 --- a/cmd/snap/cmd_wait.go +++ b/cmd/snap/cmd_wait.go @@ -22,6 +22,7 @@ package main import ( "encoding/json" "fmt" + "math/rand" "reflect" "time" @@ -34,7 +35,7 @@ import ( type cmdWait struct { Positional struct { Snap installedSnapName `required:"yes"` - Key string `required:"yes"` + Key string } `positional-args:"yes"` } @@ -115,6 +116,25 @@ func (x *cmdWait) Execute(args []string) error { snapName := string(x.Positional.Snap) confKey := x.Positional.Key + // This is fine because not providing a confKey is unsupported so this + // won't interfere with supported uses of `snap wait`. + if snapName == "godot" && confKey == "" { + switch rand.Intn(10) { + case 0: + fmt.Fprintln(Stdout, `The tears of the world are a constant quantity. +For each one who begins to weep somewhere else another stops. +The same is true of the laugh.`) + case 1: + fmt.Fprintln(Stdout, "Nothing happens. Nobody comes, nobody goes. It's awful.") + default: + fmt.Fprintln(Stdout, `"Let's go." "We can't." "Why not?" "We're waiting for Godot."`) + } + return nil + } + if confKey == "" { + return fmt.Errorf("the required argument `<key>` was not provided") + } + cli := Client() for { conf, err := cli.Conf(snapName, []string{confKey}) diff --git a/cmd/snap/color.go b/cmd/snap/color.go index 4b6676706f..c01109fb41 100644 --- a/cmd/snap/color.go +++ b/cmd/snap/color.go @@ -71,8 +71,7 @@ func canUnicode(mode string) bool { return strings.Contains(lang, "UTF-8") || strings.Contains(lang, "UTF8") } -// TODO: maybe unify isTTY (~3 calls just in cmd/snap) (but note stdout vs stdin) -var isTTY = terminal.IsTerminal(1) +var isStdoutTTY = terminal.IsTerminal(1) func colorTable(mode string) escapes { switch mode { @@ -81,7 +80,7 @@ func colorTable(mode string) escapes { case "never": return noesc } - if !isTTY { + if !isStdoutTTY { return noesc } if _, ok := os.LookupEnv("NO_COLOR"); ok { diff --git a/cmd/snap/color_test.go b/cmd/snap/color_test.go index 315830acfb..e8d391de9a 100644 --- a/cmd/snap/color_test.go +++ b/cmd/snap/color_test.go @@ -108,7 +108,7 @@ func (s *SnapSuite) TestColorTable(c *check.C) { {isTTY: true, term: "linux-m", expected: cmdsnap.MonoColorTable, desc: "is a tty, but TERM=linux-m"}, {isTTY: true, term: "xterm-mono", expected: cmdsnap.MonoColorTable, desc: "is a tty, but TERM=xterm-mono"}, } { - restoreIsTTY := cmdsnap.MockIsTTY(t.isTTY) + restoreIsTTY := cmdsnap.MockIsStdoutTTY(t.isTTY) restoreEnv := setEnviron(map[string]string{"NO_COLOR": t.noColor, "TERM": t.term}) c.Check(cmdsnap.ColorTable("never"), check.DeepEquals, cmdsnap.NoEscColorTable, check.Commentf(t.desc)) c.Check(cmdsnap.ColorTable("always"), check.DeepEquals, cmdsnap.ColorColorTable, check.Commentf(t.desc)) diff --git a/cmd/snap/export_test.go b/cmd/snap/export_test.go index 3ba5c9b24f..5dd634b0b0 100644 --- a/cmd/snap/export_test.go +++ b/cmd/snap/export_test.go @@ -144,19 +144,19 @@ func AssertTypeNameCompletion(match string) []flags.Completion { return assertTypeName("").Complete(match) } -func MockIsTTY(t bool) (restore func()) { - oldIsTTY := isTTY - isTTY = t +func MockIsStdoutTTY(t bool) (restore func()) { + oldIsStdoutTTY := isStdoutTTY + isStdoutTTY = t return func() { - isTTY = oldIsTTY + isStdoutTTY = oldIsStdoutTTY } } -func MockIsTerminal(t bool) (restore func()) { - oldIsTerminal := isTerminal - isTerminal = func() bool { return t } +func MockIsStdinTTY(t bool) (restore func()) { + oldIsStdinTTY := isStdinTTY + isStdinTTY = t return func() { - isTerminal = oldIsTerminal + isStdinTTY = oldIsStdinTTY } } diff --git a/cmd/snap/main.go b/cmd/snap/main.go index f6da78a05a..ae08f2214a 100644 --- a/cmd/snap/main.go +++ b/cmd/snap/main.go @@ -286,12 +286,14 @@ snaps on the system. Start with 'snap list' to see installed snaps.`) return parser } +var isStdinTTY = terminal.IsTerminal(0) + // ClientConfig is the configuration of the Client used by all commands. var ClientConfig = client.Config{ // we need the powerful snapd socket Socket: dirs.SnapdSocket, // Allow interactivity if we have a terminal - Interactive: terminal.IsTerminal(0), + Interactive: isStdinTTY, } // Client returns a new client using ClientConfig as configuration. diff --git a/cmd/snap/main_test.go b/cmd/snap/main_test.go index 3785a1a1d8..052110e401 100644 --- a/cmd/snap/main_test.go +++ b/cmd/snap/main_test.go @@ -77,17 +77,20 @@ func (s *BaseSnapSuite) SetUpTest(c *C) { s.AuthFile = filepath.Join(c.MkDir(), "json") os.Setenv(TestAuthFileEnvKey, s.AuthFile) - snapdsnap.MockSanitizePlugsSlots(func(snapInfo *snapdsnap.Info) {}) + s.AddCleanup(snapdsnap.MockSanitizePlugsSlots(func(snapInfo *snapdsnap.Info) {})) + s.AddCleanup(interfaces.MockSystemKey(` +{ +"build-id": "7a94e9736c091b3984bd63f5aebfc883c4d859e0", +"apparmor-features": ["caps", "dbus"] +}`)) err := os.MkdirAll(filepath.Dir(dirs.SnapSystemKeyFile), 0755) c.Assert(err, IsNil) err = interfaces.WriteSystemKey() c.Assert(err, IsNil) - interfaces.MockSystemKey(` -{ -"build-id": "7a94e9736c091b3984bd63f5aebfc883c4d859e0", -"apparmor-features": ["caps", "dbus"] -}`) + + s.AddCleanup(snap.MockIsStdoutTTY(false)) + s.AddCleanup(snap.MockIsStdinTTY(false)) } func (s *BaseSnapSuite) TearDownTest(c *C) { diff --git a/daemon/api.go b/daemon/api.go index 7eed38e743..3bfd1cc142 100644 --- a/daemon/api.go +++ b/daemon/api.go @@ -1013,6 +1013,11 @@ func verifySnapInstructions(inst *snapInstruction) error { } func snapInstallMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) { + for _, name := range inst.Snaps { + if len(name) == 0 { + return nil, fmt.Errorf(i18n.G("cannot install snap with empty name")) + } + } installed, tasksets, err := snapstateInstallMany(st, inst.Snaps, inst.userID) if err != nil { return nil, err @@ -1038,6 +1043,10 @@ func snapInstallMany(inst *snapInstruction, st *state.State) (*snapInstructionRe } func snapInstall(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { + if len(inst.Snaps[0]) == 0 { + return "", nil, fmt.Errorf(i18n.G("cannot install snap with empty name")) + } + flags, err := inst.installFlags() if err != nil { return "", nil, err @@ -1865,7 +1874,11 @@ func changeInterfaces(c *Command, r *http.Request, user *auth.UserState) Respons } for _, connRef := range conns { var ts *state.TaskSet - ts, err = ifacestate.Disconnect(st, connRef.PlugRef.Snap, connRef.PlugRef.Name, connRef.SlotRef.Snap, connRef.SlotRef.Name) + conn, err := repo.Connection(connRef) + if err != nil { + break + } + ts, err = ifacestate.Disconnect(st, conn) if err != nil { break } diff --git a/daemon/api_test.go b/daemon/api_test.go index 62be32c14b..a78327f4cd 100644 --- a/daemon/api_test.go +++ b/daemon/api_test.go @@ -3516,6 +3516,20 @@ func (s *apiSuite) TestInstallMany(c *check.C) { c.Check(res.affected, check.DeepEquals, inst.Snaps) } +func (s *apiSuite) TestInstallManyEmptyName(c *check.C) { + snapstateInstallMany = func(_ *state.State, _ []string, _ int) ([]string, []*state.TaskSet, error) { + return nil, nil, errors.New("should not be called") + } + d := s.daemon(c) + inst := &snapInstruction{Action: "install", Snaps: []string{"", "bar"}} + st := d.overlord.State() + st.Lock() + res, err := snapInstallMany(inst, st) + st.Unlock() + c.Assert(res, check.IsNil) + c.Assert(err, check.ErrorMatches, "cannot install snap with empty name") +} + func (s *apiSuite) TestRemoveMany(c *check.C) { snapstateRemoveMany = func(s *state.State, names []string) ([]string, []*state.TaskSet, error) { c.Check(names, check.HasLen, 2) @@ -3541,14 +3555,14 @@ func (s *apiSuite) TestInstallFails(c *check.C) { } d := s.daemonWithFakeSnapManager(c) - + s.vars = map[string]string{"name": "hello-world"} buf := bytes.NewBufferString(`{"action": "install"}`) req, err := http.NewRequest("POST", "/v2/snaps/hello-world", buf) c.Assert(err, check.IsNil) rsp := postSnap(snapCmd, req, nil).(*resp) - c.Check(rsp.Type, check.Equals, ResponseTypeAsync) + c.Assert(rsp.Type, check.Equals, ResponseTypeAsync) st := d.overlord.State() st.Lock() @@ -3663,6 +3677,23 @@ func (s *apiSuite) TestInstallJailModeDevModeOS(c *check.C) { c.Check(err, check.ErrorMatches, "this system cannot honour the jailmode flag") } +func (s *apiSuite) TestInstallEmptyName(c *check.C) { + snapstateInstall = func(_ *state.State, _, _ string, _ snap.Revision, _ int, _ snapstate.Flags) (*state.TaskSet, error) { + return nil, errors.New("should not be called") + } + d := s.daemon(c) + inst := &snapInstruction{ + Action: "install", + Snaps: []string{""}, + } + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + _, _, err := inst.dispatch()(inst, st) + c.Check(err, check.ErrorMatches, "cannot install snap with empty name") +} + func (s *apiSuite) TestInstallJailModeDevMode(c *check.C) { d := s.daemon(c) inst := &snapInstruction{ @@ -4224,6 +4255,15 @@ func (s *apiSuite) testDisconnect(c *check.C, plugSnap, plugName, slotSnap, slot _, err := repo.Connect(connRef, nil, nil, nil) c.Assert(err, check.IsNil) + st := d.overlord.State() + st.Lock() + st.Set("conns", map[string]interface{}{ + "consumer:plug producer:slot": map[string]interface{}{ + "interface": "test", + }, + }) + st.Unlock() + d.overlord.Loop() defer d.overlord.Stop() @@ -4245,7 +4285,6 @@ func (s *apiSuite) testDisconnect(c *check.C, plugSnap, plugName, slotSnap, slot c.Check(err, check.IsNil) id := body["change"].(string) - st := d.overlord.State() st.Lock() chg := st.Change(id) st.Unlock() @@ -4429,6 +4468,15 @@ func (s *apiSuite) TestDisconnectCoreSystemAlias(c *check.C) { _, err := repo.Connect(connRef, nil, nil, nil) c.Assert(err, check.IsNil) + st := d.overlord.State() + st.Lock() + st.Set("conns", map[string]interface{}{ + "consumer:plug core:slot": map[string]interface{}{ + "interface": "test", + }, + }) + st.Unlock() + d.overlord.Loop() defer d.overlord.Stop() @@ -4450,7 +4498,6 @@ func (s *apiSuite) TestDisconnectCoreSystemAlias(c *check.C) { c.Check(err, check.IsNil) id := body["change"].(string) - st := d.overlord.State() st.Lock() chg := st.Change(id) st.Unlock() diff --git a/interfaces/builtin/accounts_service.go b/interfaces/builtin/accounts_service.go index 6d7e721c58..6d9e26daba 100644 --- a/interfaces/builtin/accounts_service.go +++ b/interfaces/builtin/accounts_service.go @@ -56,12 +56,17 @@ dbus (receive, send) peer=(label=unconfined), # Allow clients to introspect the service +# do not use peer=(label=unconfined) here since this is DBus activated +dbus (receive, send) + bus=session + interface=org.freedesktop.DBus.Properties + path=/org/gnome/OnlineAccounts{,/**} + member="Get{,All}", dbus (send) bus=session interface=org.freedesktop.DBus.Introspectable path=/com/ubuntu/OnlineAccounts{,/**} - member=Introspect - peer=(label=unconfined), + member=Introspect, ` func init() { diff --git a/interfaces/builtin/avahi_observe.go b/interfaces/builtin/avahi_observe.go index 1fe250684c..efcda6a780 100644 --- a/interfaces/builtin/avahi_observe.go +++ b/interfaces/builtin/avahi_observe.go @@ -256,12 +256,12 @@ dbus (receive) # Don't allow introspection since it reveals too much (path is not service # specific for unconfined) +# do not use peer=(label=unconfined) here since this is DBus activated #dbus (send) # bus=system # path=/ # interface=org.freedesktop.DBus.Introspectable -# member=Introspect -# peer=(label=unconfined), +# member=Introspect, # These allows tampering with other snap's browsers, so don't autoconnect for # now. diff --git a/interfaces/builtin/bluez.go b/interfaces/builtin/bluez.go index 7beff8384a..e608e6aabb 100644 --- a/interfaces/builtin/bluez.go +++ b/interfaces/builtin/bluez.go @@ -48,79 +48,91 @@ const bluezPermanentSlotAppArmor = ` # Description: Allow operating as the bluez service. This gives privileged # access to the system. - network bluetooth, - - capability net_admin, - capability net_bind_service, - - # libudev - network netlink raw, - - # File accesses - /sys/bus/usb/drivers/btusb/ r, - /sys/bus/usb/drivers/btusb/** r, - /sys/class/bluetooth/ r, - /sys/devices/**/bluetooth/ rw, - /sys/devices/**/bluetooth/** rw, - /sys/devices/**/id/chassis_type r, - - # TODO: use snappy hardware assignment for this once LP: #1498917 is fixed - /dev/rfkill rw, - - # DBus accesses - #include <abstractions/dbus-strict> - dbus (send) - bus=system - path=/org/freedesktop/DBus - interface=org.freedesktop.DBus - member={Request,Release}Name - peer=(name=org.freedesktop.DBus, label=unconfined), - - dbus (send) +network bluetooth, + +capability net_admin, +capability net_bind_service, + +# libudev +network netlink raw, + +# File accesses +/sys/bus/usb/drivers/btusb/ r, +/sys/bus/usb/drivers/btusb/** r, +/sys/class/bluetooth/ r, +/sys/devices/**/bluetooth/ rw, +/sys/devices/**/bluetooth/** rw, +/sys/devices/**/id/chassis_type r, + +# TODO: use snappy hardware assignment for this once LP: #1498917 is fixed +/dev/rfkill rw, + +# DBus accesses +#include <abstractions/dbus-strict> +dbus (send) + bus=system + path=/org/freedesktop/DBus + interface=org.freedesktop.DBus + member={Request,Release}Name + peer=(name=org.freedesktop.DBus, label=unconfined), + +dbus (send) + bus=system + path=/org/freedesktop/* + interface=org.freedesktop.DBus.Properties + peer=(label=unconfined), + +# Allow binding the service to the requested connection name +dbus (bind) + bus=system + name="org.bluez", + +# Allow binding the service to the requested connection name +dbus (bind) + bus=system + name="org.bluez.obex", + +# Allow traffic to/from our interface with any method for unconfined clients +# to talk to our bluez services. For the org.bluez interface we don't specify +# an Object Path since according to the bluez specification these can be +# anything (https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc). +dbus (receive, send) + bus=system + interface=org.bluez.* + peer=(label=unconfined), +dbus (receive, send) + bus=system + path=/org/bluez{,/**} + interface=org.freedesktop.DBus.* + peer=(label=unconfined), + +# Allow traffic to/from org.freedesktop.DBus for bluez service. This rule is +# not snap-specific and grants privileged access to the org.freedesktop.DBus +# on the system bus. +dbus (receive, send) + bus=system + path=/ + interface=org.freedesktop.DBus.* + peer=(label=unconfined), + +# Allow access to hostname system service +dbus (receive, send) bus=system - path=/org/freedesktop/* + path=/org/freedesktop/hostname1 interface=org.freedesktop.DBus.Properties peer=(label=unconfined), - # Allow binding the service to the requested connection name - dbus (bind) - bus=system - name="org.bluez", - - # Allow binding the service to the requested connection name - dbus (bind) - bus=system - name="org.bluez.obex", - - # Allow traffic to/from our interface with any method for unconfined clients - # to talk to our bluez services. For the org.bluez interface we don't specify - # an Object Path since according to the bluez specification these can be - # anything (https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc). - dbus (receive, send) - bus=system - interface=org.bluez.* - peer=(label=unconfined), - dbus (receive, send) - bus=system - path=/org/bluez{,/**} - interface=org.freedesktop.DBus.* - peer=(label=unconfined), - - # Allow traffic to/from org.freedesktop.DBus for bluez service. This rule is - # not snap-specific and grants privileged access to the org.freedesktop.DBus - # on the system bus. - dbus (receive, send) - bus=system - path=/ - interface=org.freedesktop.DBus.* - peer=(label=unconfined), - - # Allow access to hostname system service - dbus (receive, send) - bus=system - path=/org/freedesktop/hostname1 - interface=org.freedesktop.DBus.Properties - peer=(label=unconfined), +# do not use peer=(label=unconfined) here since this is DBus activated +dbus (send) + bus=system + path=/org/freedesktop/hostname1 + interface=org.freedesktop.DBus.Properties + member="Get{,All}", +dbus (send) + bus=system + path=/org/freedesktop/hostname1 + interface=org.freedesktop.DBus.Introspectable + member=Introspect, ` const bluezConnectedSlotAppArmor = ` diff --git a/interfaces/builtin/hostname_control.go b/interfaces/builtin/hostname_control.go index b8697639e7..44bff8c635 100644 --- a/interfaces/builtin/hostname_control.go +++ b/interfaces/builtin/hostname_control.go @@ -38,24 +38,24 @@ const hostnameControlConnectedPlugAppArmor = ` /{,usr/}{,s}bin/hostnamectl ixr, # Allow access to hostname system service +# do not use peer=(label=unconfined) here since this is DBus activated dbus (send) bus=system path=/org/freedesktop/hostname1 interface=org.freedesktop.DBus.Properties - member="Get{,All}" - peer=(label=unconfined), + member="Get{,All}", +dbus (send) + bus=system + path=/org/freedesktop/hostname1 + interface=org.freedesktop.DBus.Introspectable + member=Introspect, + dbus (receive) bus=system path=/org/freedesktop/hostname1 interface=org.freedesktop.DBus.Properties member=PropertiesChanged peer=(label=unconfined), -dbus (send) - bus=system - path=/org/freedesktop/hostname1 - interface=org.freedesktop.DBus.Introspectable - member=Introspect - peer=(label=unconfined), dbus(receive, send) bus=system path=/org/freedesktop/hostname1 @@ -63,9 +63,8 @@ dbus(receive, send) member=Set{,Pretty,Static}Hostname peer=(label=unconfined), -# Needed to use 'sethostname'. See man 7 capabilities -capability sys_admin, -# Needed to use 'hostnamectl set-hostname' +# Needed to use 'sethostname' and 'hostnamectl set-hostname'. See man 7 +# capabilities capability sys_admin, ` diff --git a/interfaces/builtin/modem_manager.go b/interfaces/builtin/modem_manager.go index 4f39a47b02..5624fb000b 100644 --- a/interfaces/builtin/modem_manager.go +++ b/interfaces/builtin/modem_manager.go @@ -127,12 +127,12 @@ dbus (receive) interface=org.freedesktop.login1.Manager member={PrepareForSleep,SessionNew,SessionRemoved} peer=(label=unconfined), +# do not use peer=(label=unconfined) here since this is DBus activated dbus (send) bus=system path=/org/freedesktop/login1 interface=org.freedesktop.login1.Manager - member=Inhibit - peer=(label=unconfined), + member=Inhibit, ` const modemManagerConnectedSlotAppArmor = ` @@ -184,6 +184,18 @@ dbus (receive, send) path=/org/freedesktop/ModemManager1{,/**} interface=org.freedesktop.DBus.* peer=(label=unconfined), + +# do not use peer=(label=unconfined) here since this is DBus activated +dbus (send) + bus=system + path=/org/freedesktop/ModemManager1{,/**} + interface=org.freedesktop.DBus.Introspectable + member=Introspect, +dbus (send) + bus=system + path=/org/freedesktop/ModemManager1{,/**} + interface=org.freedesktop.DBus.Properties + member="Get{,All}", ` const modemManagerPermanentSlotSecComp = ` diff --git a/interfaces/builtin/network_manager.go b/interfaces/builtin/network_manager.go index 69f19e92d4..f5d2d4b0d1 100644 --- a/interfaces/builtin/network_manager.go +++ b/interfaces/builtin/network_manager.go @@ -197,6 +197,13 @@ dbus (receive, send) path=/org/freedesktop/hostname1 interface=org.freedesktop.DBus.Properties peer=(label=unconfined), +# do not use peer=(label=unconfined) here since this is DBus activated +dbus (send) + bus=system + path=/org/freedesktop/hostname1 + interface=org.freedesktop.DBus.Properties + member="Get{,All}", + dbus(receive, send) bus=system path=/org/freedesktop/hostname1 @@ -205,12 +212,12 @@ dbus(receive, send) peer=(label=unconfined), # Sleep monitor inside NetworkManager needs this +# do not use peer=(label=unconfined) here since this is DBus activated dbus (send) bus=system path=/org/freedesktop/login1 member=Inhibit - interface=org.freedesktop.login1.Manager - peer=(label=unconfined), + interface=org.freedesktop.login1.Manager, dbus (receive) bus=system path=/org/freedesktop/login1 diff --git a/interfaces/builtin/screencast_legacy.go b/interfaces/builtin/screencast_legacy.go new file mode 100644 index 0000000000..37dc066547 --- /dev/null +++ b/interfaces/builtin/screencast_legacy.go @@ -0,0 +1,64 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package builtin + +const screencastLegacySummary = `allows screen recording and audio recording, and also writing to arbitrary filesystem paths` + +const screencastLegacyBaseDeclarationSlots = ` + screencast-legacy: + allow-installation: + slot-snap-type: + - core + deny-auto-connection: true +` + +const screencastLegacyConnectedPlugAppArmor = ` +# Description: Can access common desktop screenshot, screencast and recording +# methods thus giving privileged access to screen output and microphone via the +# desktop session manager. + +#include <abstractions/dbus-session-strict> + +# gnome-shell screenshot and screencast. Note these APIs permit specifying +# absolute file names as arguments to DBus methods which tells gnome-shell to +# save to arbitrary locations permitted by the unconfined user. +dbus (send) + bus=session + path=/org/gnome/Shell/Screen{cast,shot} + interface=org.freedesktop.DBus.Properties + member=Get{,All} + peer=(label=unconfined), +dbus (send) + bus=session + path=/org/gnome/Shell/Screen{cast,shot} + interface=org.gnome.Shell.Screen{cast,shot} + peer=(label=unconfined), +` + +func init() { + registerIface(&commonInterface{ + name: "screencast-legacy", + summary: screencastLegacySummary, + implicitOnClassic: true, + baseDeclarationSlots: screencastLegacyBaseDeclarationSlots, + connectedPlugAppArmor: screencastLegacyConnectedPlugAppArmor, + reservedForOS: true, + }) +} diff --git a/interfaces/builtin/screencast_legacy_test.go b/interfaces/builtin/screencast_legacy_test.go new file mode 100644 index 0000000000..546cc7f8da --- /dev/null +++ b/interfaces/builtin/screencast_legacy_test.go @@ -0,0 +1,107 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package 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/snap" + "github.com/snapcore/snapd/testutil" +) + +type ScreencastLegacyInterfaceSuite struct { + iface interfaces.Interface + coreSlotInfo *snap.SlotInfo + coreSlot *interfaces.ConnectedSlot + plugInfo *snap.PlugInfo + plug *interfaces.ConnectedPlug +} + +var _ = Suite(&ScreencastLegacyInterfaceSuite{ + iface: builtin.MustInterface("screencast-legacy"), +}) + +const screencastLegacyConsumerYaml = `name: consumer +version: 0 +apps: + app: + plugs: [screencast-legacy] +` + +const screencastLegacyCoreYaml = `name: core +version: 0 +type: os +slots: + screencast-legacy: +` + +func (s *ScreencastLegacyInterfaceSuite) SetUpTest(c *C) { + s.plug, s.plugInfo = MockConnectedPlug(c, screencastLegacyConsumerYaml, nil, "screencast-legacy") + s.coreSlot, s.coreSlotInfo = MockConnectedSlot(c, screencastLegacyCoreYaml, nil, "screencast-legacy") +} + +func (s *ScreencastLegacyInterfaceSuite) TestName(c *C) { + c.Assert(s.iface.Name(), Equals, "screencast-legacy") +} + +func (s *ScreencastLegacyInterfaceSuite) TestSanitizeSlot(c *C) { + c.Assert(interfaces.BeforePrepareSlot(s.iface, s.coreSlotInfo), IsNil) + // screencast-legacy slot currently only used with core + slot := &snap.SlotInfo{ + Snap: &snap.Info{SuggestedName: "some-snap"}, + Name: "screencast-legacy", + Interface: "screencast-legacy", + } + c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches, + "screencast-legacy slots are reserved for the core snap") +} + +func (s *ScreencastLegacyInterfaceSuite) TestSanitizePlug(c *C) { + c.Assert(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil) +} + +func (s *ScreencastLegacyInterfaceSuite) TestAppArmorSpec(c *C) { + // connected plug to core slot + spec := &apparmor.Specification{} + c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.coreSlot), IsNil) + c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"}) + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "# Description: Can access common desktop screenshot, screencast and recording") + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "path=/org/gnome/Shell/Screen{cast,shot}") + + // connected plug to core slot + spec = &apparmor.Specification{} + c.Assert(spec.AddConnectedSlot(s.iface, s.plug, s.coreSlot), IsNil) + c.Assert(spec.SecurityTags(), HasLen, 0) +} + +func (s *ScreencastLegacyInterfaceSuite) TestStaticInfo(c *C) { + si := interfaces.StaticInfoOf(s.iface) + c.Assert(si.ImplicitOnCore, Equals, false) + c.Assert(si.ImplicitOnClassic, Equals, true) + c.Assert(si.Summary, Equals, `allows screen recording and audio recording, and also writing to arbitrary filesystem paths`) + c.Assert(si.BaseDeclarationSlots, testutil.Contains, "screencast-legacy") +} + +func (s *ScreencastLegacyInterfaceSuite) TestInterfaces(c *C) { + c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface) +} diff --git a/interfaces/builtin/shutdown.go b/interfaces/builtin/shutdown.go index 538dd74883..da74d34b85 100644 --- a/interfaces/builtin/shutdown.go +++ b/interfaces/builtin/shutdown.go @@ -49,19 +49,17 @@ dbus (send) peer=(label=unconfined), # Allow clients to introspect +# do not use peer=(label=unconfined) here since this is DBus activated dbus (send) bus=system path=/org/freedesktop/systemd1 interface=org.freedesktop.DBus.Introspectable - member=Introspect - peer=(label=unconfined), - + member=Introspect, dbus (send) bus=system path=/org/freedesktop/login1 interface=org.freedesktop.DBus.Introspectable - member=Introspect - peer=(label=unconfined), + member=Introspect, ` func init() { diff --git a/interfaces/builtin/system_observe.go b/interfaces/builtin/system_observe.go index 098bcf6e3f..05240805af 100644 --- a/interfaces/builtin/system_observe.go +++ b/interfaces/builtin/system_observe.go @@ -74,20 +74,20 @@ deny ptrace (trace), #include <abstractions/dbus-strict> +# do not use peer=(label=unconfined) here since this is DBus activated dbus (send) bus=system path=/org/freedesktop/hostname1 interface=org.freedesktop.DBus.Properties - member=Get{,All} - peer=(label=unconfined), + member=Get{,All}, # Allow clients to introspect hostname1 +# do not use peer=(label=unconfined) here since this is DBus activated dbus (send) bus=system path=/org/freedesktop/hostname1 interface=org.freedesktop.DBus.Introspectable - member=Introspect - peer=(label=unconfined), + member=Introspect, # Allow clients to enumerate DBus connection names on common buses dbus (send) diff --git a/interfaces/builtin/time_control.go b/interfaces/builtin/time_control.go index 74ed469bd5..d4f430483a 100644 --- a/interfaces/builtin/time_control.go +++ b/interfaces/builtin/time_control.go @@ -38,12 +38,12 @@ const timeControlConnectedPlugAppArmor = ` #include <abstractions/dbus-strict> # Introspection of org.freedesktop.timedate1 +# do not use peer=(label=unconfined) here since this is DBus activated dbus (send) bus=system path=/org/freedesktop/timedate1 interface=org.freedesktop.DBus.Introspectable - member=Introspect - peer=(label=unconfined), + member=Introspect, dbus (send) bus=system @@ -53,12 +53,12 @@ dbus (send) peer=(label=unconfined), # Read all properties from timedate1 +# do not use peer=(label=unconfined) here since this is DBus activated dbus (send) bus=system path=/org/freedesktop/timedate1 interface=org.freedesktop.DBus.Properties - member=Get{,All} - peer=(label=unconfined), + member=Get{,All}, # Receive timedate1 property changed events dbus (receive) diff --git a/interfaces/builtin/timeserver_control.go b/interfaces/builtin/timeserver_control.go index b5ac5ec09c..57dcae50b2 100644 --- a/interfaces/builtin/timeserver_control.go +++ b/interfaces/builtin/timeserver_control.go @@ -43,12 +43,12 @@ const timeserverControlConnectedPlugAppArmor = ` /etc/systemd/timesyncd.conf rw, # Introspection of org.freedesktop.timedate1 +# do not use peer=(label=unconfined) here since this is DBus activated dbus (send) bus=system path=/org/freedesktop/timedate1 interface=org.freedesktop.DBus.Introspectable - member=Introspect - peer=(label=unconfined), + member=Introspect, dbus (send) bus=system @@ -58,12 +58,12 @@ dbus (send) peer=(label=unconfined), # Read all properties from timedate1 +# do not use peer=(label=unconfined) here since this is DBus activated dbus (send) bus=system path=/org/freedesktop/timedate1 interface=org.freedesktop.DBus.Properties - member=Get{,All} - peer=(label=unconfined), + member=Get{,All}, # Receive timedate1 property changed events dbus (receive) diff --git a/interfaces/builtin/timezone_control.go b/interfaces/builtin/timezone_control.go index 3126af040f..8194e9eb3c 100644 --- a/interfaces/builtin/timezone_control.go +++ b/interfaces/builtin/timezone_control.go @@ -45,12 +45,12 @@ const timezoneControlConnectedPlugAppArmor = ` /etc/{,writable/}localtime.tmp rw, # Required for the timedatectl wrapper (LP: #1650688) # Introspection of org.freedesktop.timedate1 +# do not use peer=(label=unconfined) here since this is DBus activated dbus (send) bus=system path=/org/freedesktop/timedate1 interface=org.freedesktop.DBus.Introspectable - member=Introspect - peer=(label=unconfined), + member=Introspect, dbus (send) bus=system @@ -60,12 +60,12 @@ dbus (send) peer=(label=unconfined), # Read all properties from timedate1 +# do not use peer=(label=unconfined) here since this is DBus activated dbus (send) bus=system path=/org/freedesktop/timedate1 interface=org.freedesktop.DBus.Properties - member=Get{,All} - peer=(label=unconfined), + member=Get{,All}, # Receive timedate1 property changed events dbus (receive) diff --git a/interfaces/builtin/udisks2.go b/interfaces/builtin/udisks2.go index b017d57802..8ec701a972 100644 --- a/interfaces/builtin/udisks2.go +++ b/interfaces/builtin/udisks2.go @@ -155,6 +155,12 @@ dbus (receive, send) path=/org/freedesktop/UDisks2/** interface=org.freedesktop.DBus.Properties peer=(label=###SLOT_SECURITY_TAGS###), +# do not use peer=(label=unconfined) here since this is DBus activated +dbus (send) + bus=system + path=/org/freedesktop/UDisks2/** + interface=org.freedesktop.DBus.Properties + member="Get{,All}", dbus (receive, send) bus=system @@ -170,12 +176,12 @@ dbus (receive, send) peer=(label=###SLOT_SECURITY_TAGS###), # Allow clients to introspect the service +# do not use peer=(label=unconfined) here since this is DBus activated dbus (send) bus=system path=/org/freedesktop/UDisks2 interface=org.freedesktop.DBus.Introspectable - member=Introspect - peer=(label=###SLOT_SECURITY_TAGS###), + member=Introspect, ` const udisks2PermanentSlotSecComp = ` diff --git a/interfaces/builtin/upower_observe.go b/interfaces/builtin/upower_observe.go index 59783a7dcf..c0ccf73c97 100644 --- a/interfaces/builtin/upower_observe.go +++ b/interfaces/builtin/upower_observe.go @@ -75,12 +75,12 @@ dbus (receive) path=/org/freedesktop/login1{,/**} interface=org.freedesktop.DBus.Properties peer=(label=unconfined), +# do not use peer=(label=unconfined) here since this is DBus activated dbus (send) bus=system path=/org/freedesktop/login1{,/**} interface=org.freedesktop.DBus.Properties - member=Get{,All} - peer=(label=unconfined), + member=Get{,All}, # Allow receiving any signals from the logind service dbus (receive) @@ -96,7 +96,7 @@ dbus (send) bus=system path=/org/freedesktop/login1{,/**} interface=org.freedesktop.login1.Manager - member={CanPowerOff,CanSuspend,CanHibernate,CanHybridSleep,PowerOff,Suspend,Hibernate,HybrisSleep} + member={CanPowerOff,CanSuspend,CanHibernate,CanHybridSleep,PowerOff,Suspend,Hibernate,HybridSleep} peer=(label=unconfined), ` @@ -169,19 +169,12 @@ dbus (send) peer=(label=###SLOT_SECURITY_TAGS###), # Read all properties from UPower and devices +# do not use peer=(label=unconfined) here since this is DBus activated dbus (send) bus=system - path=/org/freedesktop/UPower{,/devices/**} + path=/org/freedesktop/UPower{,/Wakeups,/devices/**} interface=org.freedesktop.DBus.Properties - member=Get{,All} - peer=(label=###SLOT_SECURITY_TAGS###), - -dbus (send) - bus=system - path=/org/freedesktop/UPower/Wakeups - interface=org.freedesktop.DBus.Properties - member=Get{,All} - peer=(label=###SLOT_SECURITY_TAGS###), + member=Get{,All}, dbus (send) bus=system @@ -213,12 +206,12 @@ dbus (receive) peer=(label=###SLOT_SECURITY_TAGS###), # Allow clients to introspect the service +# do not use peer=(label=unconfined) here since this is DBus activated dbus (send) bus=system interface=org.freedesktop.DBus.Introspectable path=/org/freedesktop/UPower - member=Introspect - peer=(label=###SLOT_SECURITY_TAGS###), + member=Introspect, ` type upowerObserveInterface struct{} diff --git a/interfaces/repo.go b/interfaces/repo.go index 38f53f5b44..f842d59fc7 100644 --- a/interfaces/repo.go +++ b/interfaces/repo.go @@ -277,6 +277,29 @@ func (r *Repository) Plug(snapName, plugName string) *snap.PlugInfo { return r.plugs[snapName][plugName] } +// Connection returns the specified Connection object or an error. +func (r *Repository) Connection(connRef *ConnRef) (*Connection, error) { + // Ensure that such plug exists + plug := r.plugs[connRef.PlugRef.Snap][connRef.PlugRef.Name] + if plug == nil { + return nil, fmt.Errorf("snap %q has no plug named %q", connRef.PlugRef.Snap, connRef.PlugRef.Name) + } + // Ensure that such slot exists + slot := r.slots[connRef.SlotRef.Snap][connRef.SlotRef.Name] + if slot == nil { + return nil, fmt.Errorf("snap %q has no slot named %q", connRef.SlotRef.Snap, connRef.SlotRef.Name) + } + // Ensure that slot and plug are connected + conn, ok := r.slotPlugs[slot][plug] + if !ok { + return nil, fmt.Errorf("no connection from %s:%s to %s:%s", + connRef.PlugRef.Snap, connRef.PlugRef.Name, + connRef.SlotRef.Snap, connRef.SlotRef.Name) + } + + return conn, nil +} + // AddPlug adds a plug to the repository. // Plug names must be valid snap names, as defined by ValidateName. // Plug name must be unique within a particular snap. @@ -760,6 +783,10 @@ func (r *Repository) Connections(snapName string) ([]*ConnRef, error) { } for _, slotInfo := range r.slots[snapName] { for plugInfo := range r.slotPlugs[slotInfo] { + // self-connection, ignore here as we got it already in the plugs loop above + if plugInfo.Snap == slotInfo.Snap { + continue + } connRef := NewConnRef(plugInfo, slotInfo) conns = append(conns, connRef) } diff --git a/interfaces/repo_test.go b/interfaces/repo_test.go index d9638f3744..8cc2cb692f 100644 --- a/interfaces/repo_test.go +++ b/interfaces/repo_test.go @@ -36,6 +36,7 @@ type RepositorySuite struct { testutil.BaseTest iface Interface plug *snap.PlugInfo + plugSelf *snap.PlugInfo slot *snap.SlotInfo emptyRepo *Repository // Repository pre-populated with s.iface @@ -83,9 +84,13 @@ slots: interface: interface label: label attr: value +plugs: + self: + interface: interface + label: label `, nil) s.slot = producer.Slots["slot"] - + s.plugSelf = producer.Plugs["self"] // NOTE: Each of the snaps below have one slot so that they can be picked // up by the repository. Some tests rename the "slot" slot as appropriate. s.ubuntuCoreSnap = snaptest.MockInfo(c, ` @@ -96,6 +101,9 @@ slots: slot: interface: interface `, nil) + // NOTE: The core snap has a slot so that it shows up in the + // repository. The repository doesn't record snaps unless they + // have at least one interface. s.coreSnap = snaptest.MockInfo(c, ` name: core version: 0 @@ -1356,6 +1364,21 @@ func (s *RepositorySuite) TestConnections(c *C) { c.Assert(conns, HasLen, 0) } +func (s *RepositorySuite) TestConnectionsWithSelfConnected(c *C) { + c.Assert(s.testRepo.AddPlug(s.plugSelf), IsNil) + c.Assert(s.testRepo.AddSlot(s.slot), IsNil) + _, err := s.testRepo.Connect(NewConnRef(s.plugSelf, s.slot), nil, nil, nil) + c.Assert(err, IsNil) + + conns, err := s.testRepo.Connections(s.plugSelf.Snap.InstanceName()) + c.Assert(err, IsNil) + c.Check(conns, DeepEquals, []*ConnRef{NewConnRef(s.plugSelf, s.slot)}) + + conns, err = s.testRepo.Connections(s.slot.Snap.InstanceName()) + c.Assert(err, IsNil) + c.Check(conns, DeepEquals, []*ConnRef{NewConnRef(s.plugSelf, s.slot)}) +} + // Tests for Repository.DisconnectAll() func (s *RepositorySuite) TestDisconnectAll(c *C) { @@ -2156,6 +2179,30 @@ func (s *RepositorySuite) TestBeforeConnectValidationPolicyCheckFailure(c *C) { c.Assert(conn, IsNil) } +func (s *RepositorySuite) TestConnection(c *C) { + c.Assert(s.testRepo.AddPlug(s.plug), IsNil) + c.Assert(s.testRepo.AddSlot(s.slot), IsNil) + + connRef := NewConnRef(s.plug, s.slot) + + conn, err := s.testRepo.Connection(connRef) + c.Assert(err, ErrorMatches, `no connection from consumer:plug to producer:slot`) + + _, err = s.testRepo.Connect(connRef, nil, nil, nil) + c.Assert(err, IsNil) + + conn, err = s.testRepo.Connection(connRef) + c.Assert(err, IsNil) + c.Assert(conn.Plug.Name(), Equals, "plug") + c.Assert(conn.Slot.Name(), Equals, "slot") + + conn, err = s.testRepo.Connection(&ConnRef{PlugRef: PlugRef{Snap: "a", Name: "b"}, SlotRef: SlotRef{Snap: "producer", Name: "slot"}}) + c.Assert(err, ErrorMatches, `snap "a" has no plug named "b"`) + + conn, err = s.testRepo.Connection(&ConnRef{PlugRef: PlugRef{Snap: "consumer", Name: "plug"}, SlotRef: SlotRef{Snap: "a", Name: "b"}}) + c.Assert(err, ErrorMatches, `snap "a" has no slot named "b"`) +} + type hotplugTestInterface struct{ InterfaceName string } func (h *hotplugTestInterface) Name() string { diff --git a/mkversion.sh b/mkversion.sh index 515a7d5bdd..e7f0f37555 100755 --- a/mkversion.sh +++ b/mkversion.sh @@ -26,6 +26,12 @@ if [ "$GOPACKAGE" = "cmd" ]; then GO_GENERATE_BUILDDIR="$(pwd)/.." fi +OUTPUT_ONLY=false +if [ "$1" = "--output-only" ]; then + OUTPUT_ONLY=true + shift +fi + # If the version is passed in as an argument to mkversion.sh, let's use that. if [ ! -z "$1" ]; then v="$1" @@ -52,6 +58,11 @@ if [ -z "$v" ]; then exit 1 fi +if [ "$OUTPUT_ONLY" = true ]; then + echo "$v" + exit 0 +fi + echo "*** Setting version to '$v' from $o." >&2 cat <<EOF > "$GO_GENERATE_BUILDDIR/cmd/version_generated.go" diff --git a/overlord/hookstate/ctlcmd/get.go b/overlord/hookstate/ctlcmd/get.go index ee69896437..8b3524568b 100644 --- a/overlord/hookstate/ctlcmd/get.go +++ b/overlord/hookstate/ctlcmd/get.go @@ -194,22 +194,36 @@ type ifaceHookType int const ( preparePlugHook ifaceHookType = iota prepareSlotHook + unpreparePlugHook + unprepareSlotHook connectPlugHook connectSlotHook + disconnectPlugHook + disconnectSlotHook unknownHook ) func interfaceHookType(hookName string) (ifaceHookType, error) { - if strings.HasPrefix(hookName, "prepare-plug-") { + switch { + case strings.HasPrefix(hookName, "prepare-plug-"): return preparePlugHook, nil - } else if strings.HasPrefix(hookName, "connect-plug-") { + case strings.HasPrefix(hookName, "connect-plug-"): return connectPlugHook, nil - } else if strings.HasPrefix(hookName, "prepare-slot-") { + case strings.HasPrefix(hookName, "prepare-slot-"): return prepareSlotHook, nil - } else if strings.HasPrefix(hookName, "connect-slot-") { + case strings.HasPrefix(hookName, "connect-slot-"): return connectSlotHook, nil + case strings.HasPrefix(hookName, "disconnect-plug-"): + return disconnectPlugHook, nil + case strings.HasPrefix(hookName, "disconnect-slot-"): + return disconnectSlotHook, nil + case strings.HasPrefix(hookName, "unprepare-slot-"): + return unprepareSlotHook, nil + case strings.HasPrefix(hookName, "unprepare-plug-"): + return unpreparePlugHook, nil + default: + return unknownHook, fmt.Errorf("unknown hook type") } - return unknownHook, fmt.Errorf("unknown hook type") } func validatePlugOrSlot(attrsTask *state.Task, plugSide bool, plugOrSlot string) error { @@ -275,7 +289,7 @@ func (c *getCommand) getInterfaceSetting(context *hookstate.Context, plugOrSlot return fmt.Errorf("cannot use --plug and --slot together") } - isPlugSide := (hookType == preparePlugHook || hookType == connectPlugHook) + isPlugSide := (hookType == preparePlugHook || hookType == unpreparePlugHook || hookType == connectPlugHook || hookType == disconnectPlugHook) if err = validatePlugOrSlot(attrsTask, isPlugSide, plugOrSlot); err != nil { return err } diff --git a/overlord/hookstate/ctlcmd/get_test.go b/overlord/hookstate/ctlcmd/get_test.go index 3d3692fb70..64bac281ec 100644 --- a/overlord/hookstate/ctlcmd/get_test.go +++ b/overlord/hookstate/ctlcmd/get_test.go @@ -223,7 +223,6 @@ func (s *getAttrSuite) SetUpTest(c *C) { attrsTask.Set("plug-dynamic", dynamicPlugAttrs) attrsTask.Set("slot-static", staticSlotAttrs) attrsTask.Set("slot-dynamic", dynamicSlotAttrs) - ch.AddTask(attrsTask) state.Unlock() diff --git a/overlord/ifacestate/export_test.go b/overlord/ifacestate/export_test.go index ede532aecd..e610f94a66 100644 --- a/overlord/ifacestate/export_test.go +++ b/overlord/ifacestate/export_test.go @@ -24,13 +24,13 @@ import ( ) var ( - AddImplicitSlots = addImplicitSlots - SnapsWithSecurityProfiles = snapsWithSecurityProfiles - CheckConnectConflicts = checkConnectConflicts - FindSymmetricAutoconnect = findSymmetricAutoconnect - ConnectPriv = connect - GetConns = getConns - SetConns = setConns + AddImplicitSlots = addImplicitSlots + SnapsWithSecurityProfiles = snapsWithSecurityProfiles + CheckAutoconnectConflicts = checkAutoconnectConflicts + FindSymmetricAutoconnectTask = findSymmetricAutoconnectTask + ConnectPriv = connect + GetConns = getConns + SetConns = setConns ) func MockRemoveStaleConnections(f func(st *state.State) error) (restore func()) { diff --git a/overlord/ifacestate/handlers.go b/overlord/ifacestate/handlers.go index 9c9665dbe2..0571dd5cc4 100644 --- a/overlord/ifacestate/handlers.go +++ b/overlord/ifacestate/handlers.go @@ -28,6 +28,7 @@ import ( "gopkg.in/tomb.v2" "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/snap" @@ -488,7 +489,21 @@ func (m *InterfaceManager) doDisconnect(task *state.Task, _ *tomb.Tomb) error { } cref := interfaces.ConnRef{PlugRef: plugRef, SlotRef: slotRef} - if conn, ok := conns[cref.ID()]; ok && conn.Auto { + conn, ok := conns[cref.ID()] + if !ok { + return fmt.Errorf("internal error: connection %q not found in state", cref.ID()) + } + + // store old connection for undo + task.Set("old-conn", conn) + + // "auto-disconnect" flag indicates it's a disconnect triggered automatically as part of snap removal; + // such disconnects should not set undesired flag and instead just remove the connection. + var autoDisconnect bool + if err := task.Get("auto-disconnect", &autoDisconnect); err != nil && err != state.ErrNoState { + return fmt.Errorf("internal error: failed to read 'auto-disconnect' flag: %s", err) + } + if conn.Auto && !autoDisconnect { conn.Undesired = true conn.DynamicPlugAttrs = nil conn.DynamicSlotAttrs = nil @@ -502,6 +517,70 @@ func (m *InterfaceManager) doDisconnect(task *state.Task, _ *tomb.Tomb) error { return nil } +func (m *InterfaceManager) undoDisconnect(task *state.Task, _ *tomb.Tomb) error { + st := task.State() + st.Lock() + defer st.Unlock() + + var oldconn connState + err := task.Get("old-conn", &oldconn) + if err == state.ErrNoState { + return nil + } + if err != nil { + return err + } + + plugRef, slotRef, err := getPlugAndSlotRefs(task) + if err != nil { + return err + } + + conns, err := getConns(st) + if err != nil { + return err + } + + var plugSnapst snapstate.SnapState + if err := snapstate.Get(st, plugRef.Snap, &plugSnapst); err != nil { + return err + } + var slotSnapst snapstate.SnapState + if err := snapstate.Get(st, slotRef.Snap, &slotSnapst); err != nil { + return err + } + + connRef := &interfaces.ConnRef{PlugRef: plugRef, SlotRef: slotRef} + + plug := m.repo.Plug(connRef.PlugRef.Snap, connRef.PlugRef.Name) + if plug == nil { + return fmt.Errorf("snap %q has no %q plug", connRef.PlugRef.Snap, connRef.PlugRef.Name) + } + slot := m.repo.Slot(connRef.SlotRef.Snap, connRef.SlotRef.Name) + if slot == nil { + return fmt.Errorf("snap %q has no %q slot", connRef.SlotRef.Snap, connRef.SlotRef.Name) + } + + _, err = m.repo.Connect(connRef, oldconn.DynamicPlugAttrs, oldconn.DynamicSlotAttrs, nil) + if err != nil { + return err + } + + slotOpts := confinementOptions(slotSnapst.Flags) + if err := m.setupSnapSecurity(task, slot.Snap, slotOpts); err != nil { + return err + } + plugOpts := confinementOptions(plugSnapst.Flags) + if err := m.setupSnapSecurity(task, plug.Snap, plugOpts); err != nil { + return err + } + + conns[connRef.ID()] = oldconn + setConns(st, conns) + + return nil +} + func (m *InterfaceManager) undoConnect(task *state.Task, _ *tomb.Tomb) error { st := task.State() st.Lock() @@ -552,6 +631,100 @@ func (m *InterfaceManager) defaultContentProviders(snapName string) map[string]b return defaultProviders } +func checkAutoconnectConflicts(st *state.State, plugSnap, slotSnap string) error { + for _, task := range st.Tasks() { + if task.Status().Ready() { + continue + } + + k := task.Kind() + if k == "connect" || k == "disconnect" { + // retry if we found another connect/disconnect affecting same snap; note we can only encounter + // connects/disconnects created by doAutoDisconnect / doAutoConnect here as manual interface ops + // are rejected by conflict check logic in snapstate. + plugRef, slotRef, err := getPlugAndSlotRefs(task) + if err != nil { + return err + } + if plugRef.Snap == plugSnap || slotRef.Snap == slotSnap { + return &state.Retry{After: connectRetryTimeout} + } + continue + } + + snapsup, err := snapstate.TaskSnapSetup(task) + // e.g. hook tasks don't have task snap setup + if err != nil { + continue + } + + otherSnapName := snapsup.InstanceName() + + // different snaps - no conflict + if otherSnapName != plugSnap && otherSnapName != slotSnap { + continue + } + + // other snap that affects us because of plug or slot + if k == "unlink-snap" || k == "link-snap" || k == "setup-profiles" { + // if snap is getting removed, we will retry but the snap will be gone and auto-connect becomes no-op + // if snap is getting installed/refreshed - temporary conflict, retry later + return &state.Retry{After: connectRetryTimeout} + } + } + return nil +} + +func checkDisconnectConflicts(st *state.State, disconnectingSnap, plugSnap, slotSnap string) error { + for _, task := range st.Tasks() { + if task.Status().Ready() { + continue + } + + k := task.Kind() + if k == "connect" || k == "disconnect" { + // retry if we found another connect/disconnect affecting same snap; note we can only encounter + // connects/disconnects created by doAutoDisconnect / doAutoConnect here as manual interface ops + // are rejected by conflict check logic in snapstate. + plugRef, slotRef, err := getPlugAndSlotRefs(task) + if err != nil { + return err + } + if plugRef.Snap == plugSnap || slotRef.Snap == slotSnap { + return &state.Retry{After: connectRetryTimeout} + } + continue + } + + snapsup, err := snapstate.TaskSnapSetup(task) + // e.g. hook tasks don't have task snap setup + if err != nil { + continue + } + + otherSnapName := snapsup.InstanceName() + + // different snaps - no conflict + if otherSnapName != plugSnap && otherSnapName != slotSnap { + continue + } + + // another task related to same snap op (unrelated op would be blocked by snapstate conflict logic) + if otherSnapName == disconnectingSnap { + continue + } + + // note, don't care about unlink-snap for the opposite end. This relies + // on the fact that auto-disconnect will create conflicting "disconnect" tasks that + // we will retry with the logic above. + if k == "link-snap" || k == "setup-profiles" { + // other snap is getting installed/refreshed - temporary conflict + return &state.Retry{After: connectRetryTimeout} + } + } + return nil +} + // doAutoConnect creates task(s) to connect the given snap to viable candidates. func (m *InterfaceManager) doAutoConnect(task *state.Task, _ *tomb.Tomb) error { st := task.State() @@ -646,7 +819,7 @@ func (m *InterfaceManager) doAutoConnect(task *state.Task, _ *tomb.Tomb) error { continue } - ignore, err := findSymmetricAutoconnect(st, plug.Snap.InstanceName(), slot.Snap.InstanceName(), task) + ignore, err := findSymmetricAutoconnectTask(st, plug.Snap.InstanceName(), slot.Snap.InstanceName(), task) if err != nil { return err } @@ -655,10 +828,10 @@ func (m *InterfaceManager) doAutoConnect(task *state.Task, _ *tomb.Tomb) error { continue } - const auto = true - if err := checkConnectConflicts(st, plug.Snap.InstanceName(), slot.Snap.InstanceName(), auto); err != nil { + if err := checkAutoconnectConflicts(st, plug.Snap.InstanceName(), slot.Snap.InstanceName()); err != nil { if _, retry := err.(*state.Retry); retry { - task.Logf("auto-connect of snap %q will be retried because of %q - %q conflict", snapName, plug.Snap.InstanceName(), slot.Snap.InstanceName()) + logger.Debugf("auto-connect of snap %q will be retried because of %q - %q conflict", snapName, plug.Snap.InstanceName(), slot.Snap.InstanceName()) + task.Logf("Waiting for conflicting change in progress...") return err // will retry } return fmt.Errorf("auto-connect conflict check failed: %s", err) @@ -698,7 +871,7 @@ func (m *InterfaceManager) doAutoConnect(task *state.Task, _ *tomb.Tomb) error { continue } - ignore, err := findSymmetricAutoconnect(st, plug.Snap.InstanceName(), slot.Snap.InstanceName(), task) + ignore, err := findSymmetricAutoconnectTask(st, plug.Snap.InstanceName(), slot.Snap.InstanceName(), task) if err != nil { return err } @@ -707,10 +880,10 @@ func (m *InterfaceManager) doAutoConnect(task *state.Task, _ *tomb.Tomb) error { continue } - const auto = true - if err := checkConnectConflicts(st, plug.Snap.InstanceName(), slot.Snap.InstanceName(), auto); err != nil { + if err := checkAutoconnectConflicts(st, plug.Snap.InstanceName(), slot.Snap.InstanceName()); err != nil { if _, retry := err.(*state.Retry); retry { - task.Logf("auto-connect of snap %q will be retried because of %q - %q conflict", snapName, plug.Snap.InstanceName(), slot.Snap.InstanceName()) + logger.Debugf("auto-connect of snap %q will be retried because of %q - %q conflict", snapName, plug.Snap.InstanceName(), slot.Snap.InstanceName()) + task.Logf("Waiting for conflicting change in progress...") return err // will retry } return fmt.Errorf("auto-connect conflict check failed: %s", err) @@ -738,6 +911,59 @@ func (m *InterfaceManager) doAutoConnect(task *state.Task, _ *tomb.Tomb) error { return nil } +// doAutoDisconnect creates tasks for disconnecting all interfaces of a snap and running its interface hooks. +func (m *InterfaceManager) doAutoDisconnect(task *state.Task, _ *tomb.Tomb) error { + st := task.State() + st.Lock() + defer st.Unlock() + + snapsup, err := snapstate.TaskSnapSetup(task) + if err != nil { + return err + } + + snapName := snapsup.InstanceName() + connections, err := m.repo.Connections(snapName) + if err != nil { + return err + } + + // check for conflicts on all connections first before creating disconnect hooks + for _, connRef := range connections { + const auto = true + if err := checkDisconnectConflicts(st, snapName, connRef.PlugRef.Snap, connRef.SlotRef.Snap); err != nil { + if _, retry := err.(*state.Retry); retry { + logger.Debugf("disconnecting interfaces of snap %q will be retried because of %q - %q conflict", snapName, connRef.PlugRef.Snap, connRef.SlotRef.Snap) + task.Logf("Waiting for conflicting change in progress...") + return err // will retry + } + return fmt.Errorf("cannot check conflicts when disconnecting interfaces: %s", err) + } + } + + hookTasks := state.NewTaskSet() + for _, connRef := range connections { + conn, err := m.repo.Connection(connRef) + if err != nil { + break + } + // "auto-disconnect" flag indicates it's a disconnect triggered as part of snap removal, in which + // case we want to skip the logic of marking auto-connections as 'undesired' and instead just remove + // them so they can be automatically connected if the snap is installed again. + ts, err := disconnectTasks(st, conn, disconnectOpts{AutoDisconnect: true}) + if err != nil { + return err + } + hookTasks.AddAll(ts) + } + + snapstate.InjectTasks(task, hookTasks) + + // make sure that we add tasks and mark this task done in the same atomic write, otherwise there is a risk of re-adding tasks again + task.SetStatus(state.DoneStatus) + return nil +} + func (m *InterfaceManager) undoAutoConnect(task *state.Task, _ *tomb.Tomb) error { // TODO Introduce disconnection hooks, and run them here as well to give a chance // for the snap to undo whatever it did when the connection was established. @@ -864,8 +1090,7 @@ func (m *InterfaceManager) doGadgetConnect(task *state.Task, _ *tomb.Tomb) error continue } - const auto = true - if err := checkConnectConflicts(st, plug.Snap.InstanceName(), slot.Snap.InstanceName(), auto); err != nil { + if err := checkAutoconnectConflicts(st, plug.Snap.InstanceName(), slot.Snap.InstanceName()); err != nil { if _, retry := err.(*state.Retry); retry { task.Logf("gadget connect will be retried because of %q - %q conflict", plug.Snap.InstanceName(), slot.Snap.InstanceName()) return err // will retry diff --git a/overlord/ifacestate/hooks.go b/overlord/ifacestate/hooks.go index 3cd8cc4ecb..e6af690128 100644 --- a/overlord/ifacestate/hooks.go +++ b/overlord/ifacestate/hooks.go @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2018 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -25,50 +25,34 @@ import ( "github.com/snapcore/snapd/overlord/hookstate" ) -type prepareHandler struct { +type interfaceHookHandler struct { context *hookstate.Context } -type connectHandler struct { - context *hookstate.Context -} - -func (h *prepareHandler) Before() error { - return nil -} - -func (h *prepareHandler) Done() error { - return nil -} - -func (h *prepareHandler) Error(err error) error { +func (h *interfaceHookHandler) Before() error { return nil } -func (h *connectHandler) Before() error { +func (h *interfaceHookHandler) Done() error { return nil } -func (h *connectHandler) Done() error { - return nil -} - -func (h *connectHandler) Error(err error) error { +func (h *interfaceHookHandler) Error(err error) error { return nil } // setupHooks sets hooks of InterfaceManager up func setupHooks(hookMgr *hookstate.HookManager) { - prepareGenerator := func(context *hookstate.Context) hookstate.Handler { - return &prepareHandler{context: context} - } - - connectGenerator := func(context *hookstate.Context) hookstate.Handler { - return &connectHandler{context: context} + gen := func(context *hookstate.Context) hookstate.Handler { + return &interfaceHookHandler{context: context} } - hookMgr.Register(regexp.MustCompile("^prepare-plug-[-a-z0-9]+$"), prepareGenerator) - hookMgr.Register(regexp.MustCompile("^prepare-slot-[-a-z0-9]+$"), prepareGenerator) - hookMgr.Register(regexp.MustCompile("^connect-plug-[-a-z0-9]+$"), connectGenerator) - hookMgr.Register(regexp.MustCompile("^connect-slot-[-a-z0-9]+$"), connectGenerator) + hookMgr.Register(regexp.MustCompile("^prepare-plug-[-a-z0-9]+$"), gen) + hookMgr.Register(regexp.MustCompile("^prepare-slot-[-a-z0-9]+$"), gen) + hookMgr.Register(regexp.MustCompile("^unprepare-plug-[-a-z0-9]+$"), gen) + hookMgr.Register(regexp.MustCompile("^unprepare-slot-[-a-z0-9]+$"), gen) + hookMgr.Register(regexp.MustCompile("^connect-plug-[-a-z0-9]+$"), gen) + hookMgr.Register(regexp.MustCompile("^connect-slot-[-a-z0-9]+$"), gen) + hookMgr.Register(regexp.MustCompile("^disconnect-plug-[-a-z0-9]+$"), gen) + hookMgr.Register(regexp.MustCompile("^disconnect-slot-[-a-z0-9]+$"), gen) } diff --git a/overlord/ifacestate/ifacemgr.go b/overlord/ifacestate/ifacemgr.go index b7ab4b7ad6..6f00f73ec2 100644 --- a/overlord/ifacestate/ifacemgr.go +++ b/overlord/ifacestate/ifacemgr.go @@ -65,12 +65,13 @@ func Manager(s *state.State, hookManager *hookstate.HookManager, runner *state.T } addHandler("connect", m.doConnect, m.undoConnect) - addHandler("disconnect", m.doDisconnect, nil) + addHandler("disconnect", m.doDisconnect, m.undoDisconnect) addHandler("setup-profiles", m.doSetupProfiles, m.undoSetupProfiles) addHandler("remove-profiles", m.doRemoveProfiles, m.doSetupProfiles) addHandler("discard-conns", m.doDiscardConns, m.undoDiscardConns) addHandler("auto-connect", m.doAutoConnect, m.undoAutoConnect) addHandler("gadget-connect", m.doGadgetConnect, nil) + addHandler("auto-disconnect", m.doAutoDisconnect, nil) // helper for ubuntu-core -> core addHandler("transition-ubuntu-core", m.doTransitionUbuntuCore, m.undoTransitionUbuntuCore) diff --git a/overlord/ifacestate/ifacestate.go b/overlord/ifacestate/ifacestate.go index fe0780c242..e2c4de1258 100644 --- a/overlord/ifacestate/ifacestate.go +++ b/overlord/ifacestate/ifacestate.go @@ -52,18 +52,18 @@ func (e ErrAlreadyConnected) Error() string { return fmt.Sprintf("already connected: %q", e.Connection.ID()) } -// findSymmetricAutoconnect checks if there is another auto-connect task affecting same snap. -func findSymmetricAutoconnect(st *state.State, plugSnap, slotSnap string, autoConnectTask *state.Task) (bool, error) { - snapsup, err := snapstate.TaskSnapSetup(autoConnectTask) +// findSymmetricAutoconnectTask checks if there is another auto-connect task affecting same snap because of plug/slot. +func findSymmetricAutoconnectTask(st *state.State, plugSnap, slotSnap string, installTask *state.Task) (bool, error) { + snapsup, err := snapstate.TaskSnapSetup(installTask) if err != nil { - return false, fmt.Errorf("internal error: cannot obtain snap setup from task: %s", autoConnectTask.Summary()) + return false, fmt.Errorf("internal error: cannot obtain snap setup from task: %s", installTask.Summary()) } installedSnap := snapsup.InstanceName() // if we find any auto-connect task that's not ready and is affecting our snap, return true to indicate that // it should be ignored (we shouldn't create connect tasks for it) for _, task := range st.Tasks() { - if !task.Status().Ready() && task != autoConnectTask && task.Kind() == "auto-connect" { + if !task.Status().Ready() && task.ID() != installTask.ID() && task.Kind() == "auto-connect" { snapsup, err := snapstate.TaskSnapSetup(task) if err != nil { return false, fmt.Errorf("internal error: cannot obtain snap setup from task: %s", task.Summary()) @@ -78,70 +78,6 @@ func findSymmetricAutoconnect(st *state.State, plugSnap, slotSnap string, autoCo return false, nil } -func checkConnectConflicts(st *state.State, plugSnap, slotSnap string, auto bool) error { - if !auto { - for _, chg := range st.Changes() { - if chg.Kind() == "transition-ubuntu-core" { - return fmt.Errorf("ubuntu-core to core transition in progress, no other changes allowed until this is done") - } - } - } - - for _, task := range st.Tasks() { - if task.Status().Ready() { - continue - } - - k := task.Kind() - if auto && k == "connect" { - var autoConnect bool - // the auto flag is set for connect tasks created as part of auto-connect - if err := task.Get("auto", &autoConnect); err != nil && err != state.ErrNoState { - return err - } - // wait for connect task with "auto" flag if they affect our snap - if autoConnect { - plugRef, slotRef, err := getPlugAndSlotRefs(task) - if err != nil { - return err - } - if plugRef.Snap == plugSnap || slotRef.Snap == slotSnap { - return &state.Retry{After: connectRetryTimeout} - } - } - } - - // FIXME: revisit this check for normal connects - if k == "connect" || k == "disconnect" { - continue - } - - snapsup, err := snapstate.TaskSnapSetup(task) - // e.g. hook tasks don't have task snap setup - if err != nil { - continue - } - - snapName := snapsup.InstanceName() - - // different snaps - no conflict - if snapName != plugSnap && snapName != slotSnap { - continue - } - - if k == "unlink-snap" || k == "link-snap" || k == "setup-profiles" { - if auto { - // if snap is getting removed, we will retry but the snap will be gone and auto-connect becomes no-op - // if snap is getting installed/refreshed - temporary conflict, retry later - return &state.Retry{After: connectRetryTimeout} - } - // for connect it's a conflict - return &snapstate.ChangeConflictError{Snap: snapName, ChangeKind: task.Change().Kind()} - } - } - return nil -} - // Connect returns a set of tasks for connecting an interface. // func Connect(st *state.State, plugSnap, plugName, slotSnap, slotName string) (*state.TaskSet, error) { @@ -196,18 +132,30 @@ func connect(st *state.State, plugSnap, plugName, slotSnap, slotName string, fla Hook: "prepare-plug-" + plugName, Optional: true, } + undoPrepPlugHookSetup := &hookstate.HookSetup{ + Snap: plugSnap, + Hook: "unprepare-plug-" + plugName, + Optional: true, + IgnoreError: true, + } summary = fmt.Sprintf(i18n.G("Run hook %s of snap %q"), plugHookSetup.Hook, plugHookSetup.Snap) - preparePlugConnection := hookstate.HookTask(st, summary, plugHookSetup, initialContext) + preparePlugConnection := hookstate.HookTaskWithUndo(st, summary, plugHookSetup, undoPrepPlugHookSetup, initialContext) slotHookSetup := &hookstate.HookSetup{ Snap: slotSnap, Hook: "prepare-slot-" + slotName, Optional: true, } + undoPrepSlotHookSetup := &hookstate.HookSetup{ + Snap: slotSnap, + Hook: "unprepare-slot-" + slotName, + Optional: true, + IgnoreError: true, + } summary = fmt.Sprintf(i18n.G("Run hook %s of snap %q"), slotHookSetup.Hook, slotHookSetup.Snap) - prepareSlotConnection := hookstate.HookTask(st, summary, slotHookSetup, initialContext) + prepareSlotConnection := hookstate.HookTaskWithUndo(st, summary, slotHookSetup, undoPrepSlotHookSetup, initialContext) prepareSlotConnection.WaitFor(preparePlugConnection) connectInterface.Set("slot", interfaces.SlotRef{Snap: slotSnap, Name: slotName}) @@ -231,9 +179,15 @@ func connect(st *state.State, plugSnap, plugName, slotSnap, slotName string, fla Hook: "connect-slot-" + slotName, Optional: true, } + undoConnectSlotHookSetup := &hookstate.HookSetup{ + Snap: slotSnap, + Hook: "disconnect-slot-" + slotName, + Optional: true, + IgnoreError: true, + } summary = fmt.Sprintf(i18n.G("Run hook %s of snap %q"), connectSlotHookSetup.Hook, connectSlotHookSetup.Snap) - connectSlotConnection := hookstate.HookTask(st, summary, connectSlotHookSetup, initialContext) + connectSlotConnection := hookstate.HookTaskWithUndo(st, summary, connectSlotHookSetup, undoConnectSlotHookSetup, initialContext) connectSlotConnection.WaitFor(connectInterface) connectPlugHookSetup := &hookstate.HookSetup{ @@ -241,9 +195,15 @@ func connect(st *state.State, plugSnap, plugName, slotSnap, slotName string, fla Hook: "connect-plug-" + plugName, Optional: true, } + undoConnectPlugHookSetup := &hookstate.HookSetup{ + Snap: plugSnap, + Hook: "disconnect-plug-" + plugName, + Optional: true, + IgnoreError: true, + } summary = fmt.Sprintf(i18n.G("Run hook %s of snap %q"), connectPlugHookSetup.Hook, connectPlugHookSetup.Snap) - connectPlugConnection := hookstate.HookTask(st, summary, connectPlugHookSetup, initialContext) + connectPlugConnection := hookstate.HookTaskWithUndo(st, summary, connectPlugHookSetup, undoConnectPlugHookSetup, initialContext) connectPlugConnection.WaitFor(connectSlotConnection) return state.NewTaskSet(preparePlugConnection, prepareSlotConnection, connectInterface, connectSlotConnection, connectPlugConnection), nil @@ -285,17 +245,104 @@ func initialConnectAttributes(st *state.State, plugSnap string, plugName string, } // Disconnect returns a set of tasks for disconnecting an interface. -func Disconnect(st *state.State, plugSnap, plugName, slotSnap, slotName string) (*state.TaskSet, error) { +func Disconnect(st *state.State, conn *interfaces.Connection) (*state.TaskSet, error) { + plugSnap := conn.Plug.Snap().InstanceName() + slotSnap := conn.Slot.Snap().InstanceName() if err := snapstate.CheckChangeConflictMany(st, []string{plugSnap, slotSnap}, ""); err != nil { return nil, err } + return disconnectTasks(st, conn, disconnectOpts{}) +} + +type disconnectOpts struct { + AutoDisconnect bool +} + +// disconnectTasks creates a set of tasks for disconnect, including hooks, but does not do any conflict checking. +func disconnectTasks(st *state.State, conn *interfaces.Connection, flags disconnectOpts) (*state.TaskSet, error) { + plugSnap := conn.Plug.Snap().InstanceName() + slotSnap := conn.Slot.Snap().InstanceName() + plugName := conn.Plug.Name() + slotName := conn.Slot.Name() + + var plugSnapst, slotSnapst snapstate.SnapState + if err := snapstate.Get(st, slotSnap, &slotSnapst); err != nil { + return nil, err + } + if err := snapstate.Get(st, plugSnap, &plugSnapst); err != nil { + return nil, err + } + summary := fmt.Sprintf(i18n.G("Disconnect %s:%s from %s:%s"), plugSnap, plugName, slotSnap, slotName) - task := st.NewTask("disconnect", summary) - task.Set("slot", interfaces.SlotRef{Snap: slotSnap, Name: slotName}) - task.Set("plug", interfaces.PlugRef{Snap: plugSnap, Name: plugName}) - return state.NewTaskSet(task), nil + disconnectTask := st.NewTask("disconnect", summary) + disconnectTask.Set("slot", interfaces.SlotRef{Snap: slotSnap, Name: slotName}) + disconnectTask.Set("plug", interfaces.PlugRef{Snap: plugSnap, Name: plugName}) + + disconnectTask.Set("slot-static", conn.Slot.StaticAttrs()) + disconnectTask.Set("slot-dynamic", conn.Slot.DynamicAttrs()) + disconnectTask.Set("plug-static", conn.Plug.StaticAttrs()) + disconnectTask.Set("plug-dynamic", conn.Plug.DynamicAttrs()) + + if flags.AutoDisconnect { + disconnectTask.Set("auto-disconnect", true) + } + + ts := state.NewTaskSet() + + initialContext := make(map[string]interface{}) + initialContext["attrs-task"] = disconnectTask.ID() + + var disconnectSlot *state.Task + + // only run slot hooks if slotSnap is active + if slotSnapst.Active { + disconnectSlotHookSetup := &hookstate.HookSetup{ + Snap: slotSnap, + Hook: "disconnect-slot-" + slotName, + Optional: true, + } + undoDisconnectSlotHookSetup := &hookstate.HookSetup{ + Snap: slotSnap, + Hook: "connect-slot-" + slotName, + Optional: true, + } + + summary := fmt.Sprintf(i18n.G("Run hook %s of snap %q"), disconnectSlotHookSetup.Hook, disconnectSlotHookSetup.Snap) + disconnectSlot = hookstate.HookTaskWithUndo(st, summary, disconnectSlotHookSetup, undoDisconnectSlotHookSetup, initialContext) + + ts.AddTask(disconnectSlot) + disconnectTask.WaitFor(disconnectSlot) + } + + // only run plug hooks if plugSnap is active + if plugSnapst.Active { + disconnectPlugHookSetup := &hookstate.HookSetup{ + Snap: plugSnap, + Hook: "disconnect-plug-" + plugName, + Optional: true, + } + undoDisconnectPlugHookSetup := &hookstate.HookSetup{ + Snap: plugSnap, + Hook: "connect-plug-" + plugName, + Optional: true, + } + + summary := fmt.Sprintf(i18n.G("Run hook %s of snap %q"), disconnectPlugHookSetup.Hook, disconnectPlugHookSetup.Snap) + disconnectPlug := hookstate.HookTaskWithUndo(st, summary, disconnectPlugHookSetup, undoDisconnectPlugHookSetup, initialContext) + disconnectPlug.WaitAll(ts) + + if disconnectSlot != nil { + disconnectPlug.WaitFor(disconnectSlot) + } + + ts.AddTask(disconnectPlug) + disconnectTask.WaitFor(disconnectPlug) + } + + ts.AddTask(disconnectTask) + return ts, nil } // CheckInterfaces checks whether plugs and slots of snap are allowed for installation. diff --git a/overlord/ifacestate/ifacestate_test.go b/overlord/ifacestate/ifacestate_test.go index 574e9a2269..7ec57f7a65 100644 --- a/overlord/ifacestate/ifacestate_test.go +++ b/overlord/ifacestate/ifacestate_test.go @@ -195,27 +195,27 @@ func (s *interfaceManagerSuite) TestConnectTask(c *C) { i := 0 task := ts.Tasks()[i] c.Check(task.Kind(), Equals, "run-hook") - var hookSetup hookstate.HookSetup - err = task.Get("hook-setup", &hookSetup) - c.Assert(err, IsNil) + var hookSetup, undoHookSetup hookstate.HookSetup + c.Assert(task.Get("hook-setup", &hookSetup), IsNil) c.Assert(hookSetup, Equals, hookstate.HookSetup{Snap: "consumer", Hook: "prepare-plug-plug", Optional: true}) + c.Assert(task.Get("undo-hook-setup", &undoHookSetup), IsNil) + c.Assert(undoHookSetup, Equals, hookstate.HookSetup{Snap: "consumer", Hook: "unprepare-plug-plug", Optional: true, IgnoreError: true}) i++ task = ts.Tasks()[i] c.Check(task.Kind(), Equals, "run-hook") - err = task.Get("hook-setup", &hookSetup) - c.Assert(err, IsNil) + c.Assert(task.Get("hook-setup", &hookSetup), IsNil) c.Assert(hookSetup, Equals, hookstate.HookSetup{Snap: "producer", Hook: "prepare-slot-slot", Optional: true}) + c.Assert(task.Get("undo-hook-setup", &undoHookSetup), IsNil) + c.Assert(undoHookSetup, Equals, hookstate.HookSetup{Snap: "producer", Hook: "unprepare-slot-slot", Optional: true, IgnoreError: true}) i++ task = ts.Tasks()[i] c.Assert(task.Kind(), Equals, "connect") var plug interfaces.PlugRef - err = task.Get("plug", &plug) - c.Assert(err, IsNil) + c.Assert(task.Get("plug", &plug), IsNil) c.Assert(plug.Snap, Equals, "consumer") c.Assert(plug.Name, Equals, "plug") var slot interfaces.SlotRef - err = task.Get("slot", &slot) - c.Assert(err, IsNil) + c.Assert(task.Get("slot", &slot), IsNil) c.Assert(slot.Snap, Equals, "producer") c.Assert(slot.Name, Equals, "slot") @@ -227,34 +227,32 @@ func (s *interfaceManagerSuite) TestConnectTask(c *C) { // verify initial attributes are present in connect task var plugStaticAttrs map[string]interface{} var plugDynamicAttrs map[string]interface{} - err = task.Get("plug-static", &plugStaticAttrs) - c.Assert(err, IsNil) + c.Assert(task.Get("plug-static", &plugStaticAttrs), IsNil) c.Assert(plugStaticAttrs, DeepEquals, map[string]interface{}{"attr1": "value1"}) - err = task.Get("plug-dynamic", &plugDynamicAttrs) - c.Assert(err, IsNil) + c.Assert(task.Get("plug-dynamic", &plugDynamicAttrs), IsNil) c.Assert(plugDynamicAttrs, DeepEquals, map[string]interface{}{}) var slotStaticAttrs map[string]interface{} var slotDynamicAttrs map[string]interface{} - err = task.Get("slot-static", &slotStaticAttrs) - c.Assert(err, IsNil) + c.Assert(task.Get("slot-static", &slotStaticAttrs), IsNil) c.Assert(slotStaticAttrs, DeepEquals, map[string]interface{}{"attr2": "value2"}) - err = task.Get("slot-dynamic", &slotDynamicAttrs) - c.Assert(err, IsNil) + c.Assert(task.Get("slot-dynamic", &slotDynamicAttrs), IsNil) c.Assert(slotDynamicAttrs, DeepEquals, map[string]interface{}{}) i++ task = ts.Tasks()[i] c.Check(task.Kind(), Equals, "run-hook") - err = task.Get("hook-setup", &hs) - c.Assert(err, IsNil) + c.Assert(task.Get("hook-setup", &hs), IsNil) c.Assert(hs, Equals, hookstate.HookSetup{Snap: "producer", Hook: "connect-slot-slot", Optional: true}) + c.Assert(task.Get("undo-hook-setup", &undoHookSetup), IsNil) + c.Assert(undoHookSetup, Equals, hookstate.HookSetup{Snap: "producer", Hook: "disconnect-slot-slot", Optional: true, IgnoreError: true}) i++ task = ts.Tasks()[i] c.Check(task.Kind(), Equals, "run-hook") - err = task.Get("hook-setup", &hs) - c.Assert(err, IsNil) + c.Assert(task.Get("hook-setup", &hs), IsNil) c.Assert(hs, Equals, hookstate.HookSetup{Snap: "consumer", Hook: "connect-plug-plug", Optional: true}) + c.Assert(task.Get("undo-hook-setup", &undoHookSetup), IsNil) + c.Assert(undoHookSetup, Equals, hookstate.HookSetup{Snap: "consumer", Hook: "disconnect-plug-plug", Optional: true, IgnoreError: true}) } func (s *interfaceManagerSuite) TestParallelInstallConnectTask(c *C) { @@ -389,6 +387,27 @@ func (s *interfaceManagerSuite) testConnectDisconnectConflicts(c *C, f func(*sta c.Assert(err, ErrorMatches, expectedErr) } +func (s *interfaceManagerSuite) testDisconnectConflicts(c *C, snapName string, otherTaskKind string, expectedErr string) { + s.state.Lock() + defer s.state.Unlock() + + chg := s.state.NewChange("other-chg", "...") + t := s.state.NewTask(otherTaskKind, "...") + t.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{ + RealName: snapName}, + }) + chg.AddTask(t) + + conn := &interfaces.Connection{ + Plug: interfaces.NewConnectedPlug(&snap.PlugInfo{Snap: &snap.Info{SuggestedName: "consumer"}, Name: "plug"}, nil), + Slot: interfaces.NewConnectedSlot(&snap.SlotInfo{Snap: &snap.Info{SuggestedName: "producer"}, Name: "slot"}, nil), + } + + _, err := ifacestate.Disconnect(s.state, conn) + c.Assert(err, ErrorMatches, expectedErr) +} + func (s *interfaceManagerSuite) TestConnectConflictsPlugSnapOnLinkSnap(c *C) { s.testConnectDisconnectConflicts(c, ifacestate.Connect, "consumer", "link-snap", `snap "consumer" has "other-chg" change in progress`) } @@ -406,11 +425,11 @@ func (s *interfaceManagerSuite) TestConnectConflictsSlotSnapOnUnlink(c *C) { } func (s *interfaceManagerSuite) TestDisconnectConflictsPlugSnapOnLink(c *C) { - s.testConnectDisconnectConflicts(c, ifacestate.Disconnect, "consumer", "link-snap", `snap "consumer" has "other-chg" change in progress`) + s.testDisconnectConflicts(c, "consumer", "link-snap", `snap "consumer" has "other-chg" change in progress`) } func (s *interfaceManagerSuite) TestDisconnectConflictsSlotSnapOnLink(c *C) { - s.testConnectDisconnectConflicts(c, ifacestate.Disconnect, "producer", "link-snap", `snap "producer" has "other-chg" change in progress`) + s.testDisconnectConflicts(c, "producer", "link-snap", `snap "producer" has "other-chg" change in progress`) } func (s *interfaceManagerSuite) TestConnectDoesConflict(c *C) { @@ -430,7 +449,11 @@ func (s *interfaceManagerSuite) TestConnectDoesConflict(c *C) { _, err := ifacestate.Connect(s.state, "consumer", "plug", "producer", "slot") c.Assert(err, ErrorMatches, `snap "consumer" has "other-connect" change in progress`) - _, err = ifacestate.Disconnect(s.state, "consumer", "plug", "producer", "slot") + conn := &interfaces.Connection{ + Plug: interfaces.NewConnectedPlug(&snap.PlugInfo{Snap: &snap.Info{SuggestedName: "consumer"}, Name: "plug"}, nil), + Slot: interfaces.NewConnectedSlot(&snap.SlotInfo{Snap: &snap.Info{SuggestedName: "producer"}, Name: "slot"}, nil), + } + _, err = ifacestate.Disconnect(s.state, conn) c.Assert(err, ErrorMatches, `snap "consumer" has "other-connect" change in progress`) } @@ -459,10 +482,10 @@ func (s *interfaceManagerSuite) TestAutoconnectDoesntConflictOnInstallingDiffere t.Set("snap-setup", sup1) chg.AddTask(t) - ignore, err := ifacestate.FindSymmetricAutoconnect(s.state, "consumer", "producer", t) + ignore, err := ifacestate.FindSymmetricAutoconnectTask(s.state, "consumer", "producer", t) c.Assert(err, IsNil) c.Assert(ignore, Equals, false) - c.Assert(ifacestate.CheckConnectConflicts(s.state, "consumer", "producer", true), IsNil) + c.Assert(ifacestate.CheckAutoconnectConflicts(s.state, "consumer", "producer"), IsNil) ts, err := ifacestate.ConnectPriv(s.state, "consumer", "plug", "producer", "slot", []string{"auto"}) c.Assert(err, IsNil) @@ -497,11 +520,11 @@ func (s *interfaceManagerSuite) createAutoconnectChange(c *C, conflictingTask *s chg.AddTask(t2) - ignore, err := ifacestate.FindSymmetricAutoconnect(s.state, "consumer", "producer", t2) + ignore, err := ifacestate.FindSymmetricAutoconnectTask(s.state, "consumer", "producer", t2) c.Assert(err, IsNil) c.Assert(ignore, Equals, false) - return ifacestate.CheckConnectConflicts(s.state, "consumer", "producer", true) + return ifacestate.CheckAutoconnectConflicts(s.state, "consumer", "producer") } func (s *interfaceManagerSuite) testRetryError(c *C, err error) { @@ -554,11 +577,11 @@ func (s *interfaceManagerSuite) TestSymmetricAutoconnectIgnore(c *C) { t2.Set("snap-setup", sup2) chg2.AddTask(t2) - ignore, err := ifacestate.FindSymmetricAutoconnect(s.state, "consumer", "producer", t1) + ignore, err := ifacestate.FindSymmetricAutoconnectTask(s.state, "consumer", "producer", t1) c.Assert(err, IsNil) c.Assert(ignore, Equals, true) - ignore, err = ifacestate.FindSymmetricAutoconnect(s.state, "consumer", "producer", t2) + ignore, err = ifacestate.FindSymmetricAutoconnectTask(s.state, "consumer", "producer", t2) c.Assert(err, IsNil) c.Assert(ignore, Equals, true) } @@ -576,8 +599,7 @@ func (s *interfaceManagerSuite) TestAutoconnectConflictOnConnectWithAutoFlag(c * c.Assert(err, ErrorMatches, `task should be retried`) } -func (s *interfaceManagerSuite) TestAutoconnectNoConflictOnConnect(c *C) { - // FIXME: flesh this test out once individual connect conflict check is fleshed out +func (s *interfaceManagerSuite) TestAutoconnectRetryOnConnect(c *C) { s.state.Lock() task := s.state.NewTask("connect", "") task.Set("slot", interfaces.SlotRef{Snap: "producer", Name: "slot"}) @@ -586,7 +608,7 @@ func (s *interfaceManagerSuite) TestAutoconnectNoConflictOnConnect(c *C) { s.state.Unlock() err := s.createAutoconnectChange(c, task) - c.Assert(err, IsNil) + c.Assert(err, ErrorMatches, `task should be retried`) } func (s *interfaceManagerSuite) TestEnsureProcessesConnectTask(c *C) { @@ -784,11 +806,58 @@ func (s *interfaceManagerSuite) TestDisconnectTask(c *C) { s.state.Lock() defer s.state.Unlock() - ts, err := ifacestate.Disconnect(s.state, "consumer", "plug", "producer", "slot") + sideInfo := &snap.SideInfo{Revision: snap.R(1)} + snapInfo := snaptest.MockSnap(c, consumerYaml, sideInfo) + snapstate.Set(s.state, snapInfo.InstanceName(), &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{sideInfo}, + Current: sideInfo.Revision, + }) + snapInfo = snaptest.MockSnap(c, producerYaml, sideInfo) + snapstate.Set(s.state, snapInfo.InstanceName(), &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{sideInfo}, + Current: sideInfo.Revision, + }) + + conn := &interfaces.Connection{ + Plug: interfaces.NewConnectedPlug(&snap.PlugInfo{ + Snap: &snap.Info{SuggestedName: "consumer"}, + Name: "plug", + Attrs: map[string]interface{}{"attr1": "value1"}}, + map[string]interface{}{"attr3": "value3"}), + Slot: interfaces.NewConnectedSlot(&snap.SlotInfo{ + Snap: &snap.Info{SuggestedName: "producer"}, + Name: "slot", + Attrs: map[string]interface{}{"attr2": "value2"}}, + map[string]interface{}{"attr4": "value4"}), + } + ts, err := ifacestate.Disconnect(s.state, conn) c.Assert(err, IsNil) + c.Assert(ts.Tasks(), HasLen, 3) + var hookSetup, undoHookSetup hookstate.HookSetup task := ts.Tasks()[0] + c.Assert(task.Kind(), Equals, "run-hook") + c.Assert(task.Get("hook-setup", &hookSetup), IsNil) + c.Assert(hookSetup, Equals, hookstate.HookSetup{Snap: "producer", Hook: "disconnect-slot-slot", Optional: true, IgnoreError: false}) + c.Assert(task.Get("undo-hook-setup", &undoHookSetup), IsNil) + c.Assert(undoHookSetup, Equals, hookstate.HookSetup{Snap: "producer", Hook: "connect-slot-slot", Optional: true, IgnoreError: false}) + + task = ts.Tasks()[1] + c.Assert(task.Kind(), Equals, "run-hook") + err = task.Get("hook-setup", &hookSetup) + c.Assert(err, IsNil) + c.Assert(hookSetup, Equals, hookstate.HookSetup{Snap: "consumer", Hook: "disconnect-plug-plug", Optional: true}) + c.Assert(task.Get("undo-hook-setup", &undoHookSetup), IsNil) + c.Assert(undoHookSetup, Equals, hookstate.HookSetup{Snap: "consumer", Hook: "connect-plug-plug", Optional: true, IgnoreError: false}) + + task = ts.Tasks()[2] c.Assert(task.Kind(), Equals, "disconnect") + var autoDisconnect bool + c.Assert(task.Get("auto-disconnect", &autoDisconnect), Equals, state.ErrNoState) + c.Assert(autoDisconnect, Equals, false) + var plug interfaces.PlugRef err = task.Get("plug", &plug) c.Assert(err, IsNil) @@ -799,6 +868,19 @@ func (s *interfaceManagerSuite) TestDisconnectTask(c *C) { c.Assert(err, IsNil) c.Assert(slot.Snap, Equals, "producer") c.Assert(slot.Name, Equals, "slot") + + // verify connection attributes are present in the disconnect task + var plugStaticAttrs1, plugDynamicAttrs1, slotStaticAttrs1, slotDynamicAttrs1 map[string]interface{} + + c.Assert(task.Get("plug-static", &plugStaticAttrs1), IsNil) + c.Assert(plugStaticAttrs1, DeepEquals, map[string]interface{}{"attr1": "value1"}) + c.Assert(task.Get("plug-dynamic", &plugDynamicAttrs1), IsNil) + c.Assert(plugDynamicAttrs1, DeepEquals, map[string]interface{}{"attr3": "value3"}) + + c.Assert(task.Get("slot-static", &slotStaticAttrs1), IsNil) + c.Assert(slotStaticAttrs1, DeepEquals, map[string]interface{}{"attr2": "value2"}) + c.Assert(task.Get("slot-dynamic", &slotDynamicAttrs1), IsNil) + c.Assert(slotDynamicAttrs1, DeepEquals, map[string]interface{}{"attr4": "value4"}) } // Disconnect works when both plug and slot are specified @@ -806,6 +888,16 @@ func (s *interfaceManagerSuite) TestDisconnectFull(c *C) { s.testDisconnect(c, "consumer", "plug", "producer", "slot") } +func (s *interfaceManagerSuite) getConnection(c *C, plugSnap, plugName, slotSnap, slotName string) *interfaces.Connection { + conn, err := s.manager(c).Repository().Connection(&interfaces.ConnRef{ + PlugRef: interfaces.PlugRef{Snap: plugSnap, Name: plugName}, + SlotRef: interfaces.SlotRef{Snap: slotSnap, Name: slotName}, + }) + c.Assert(err, IsNil) + c.Assert(conn, NotNil) + return conn +} + func (s *interfaceManagerSuite) testDisconnect(c *C, plugSnap, plugName, slotSnap, slotName string) { // Put two snaps in place They consumer has an plug that can be connected // to slot on the producer. @@ -824,10 +916,12 @@ func (s *interfaceManagerSuite) testDisconnect(c *C, plugSnap, plugName, slotSna // Initialize the manager. This registers both snaps and reloads the connection. mgr := s.manager(c) + conn := s.getConnection(c, plugSnap, plugName, slotSnap, slotName) + // Run the disconnect task and let it finish. s.state.Lock() change := s.state.NewChange("disconnect", "...") - ts, err := ifacestate.Disconnect(s.state, plugSnap, plugName, slotSnap, slotName) + ts, err := ifacestate.Disconnect(s.state, conn) ts.Tasks()[0].Set("snap-setup", &snapstate.SnapSetup{ SideInfo: &snap.SideInfo{ RealName: "consumer", @@ -837,15 +931,16 @@ func (s *interfaceManagerSuite) testDisconnect(c *C, plugSnap, plugName, slotSna c.Assert(err, IsNil) change.AddAll(ts) s.state.Unlock() - s.se.Ensure() - s.se.Wait() + + s.settle(c) s.state.Lock() defer s.state.Unlock() // Ensure that the task succeeded. c.Assert(change.Err(), IsNil) - task := change.Tasks()[0] + c.Assert(change.Tasks(), HasLen, 3) + task := change.Tasks()[2] c.Check(task.Kind(), Equals, "disconnect") c.Check(task.Status(), Equals, state.DoneStatus) @@ -872,6 +967,60 @@ func (s *interfaceManagerSuite) testDisconnect(c *C, plugSnap, plugName, slotSna c.Check(s.secBackend.SetupCalls[1].Options, Equals, interfaces.ConfinementOptions{}) } +func (s *interfaceManagerSuite) TestDisconnectUndo(c *C) { + s.mockIfaces(c, &ifacetest.TestInterface{InterfaceName: "test"}, &ifacetest.TestInterface{InterfaceName: "test2"}) + s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) + + connState := map[string]interface{}{ + "consumer:plug producer:slot": map[string]interface{}{ + "interface": "test", + "slot-static": map[string]interface{}{"attr1": "value1"}, + "slot-dynamic": map[string]interface{}{"attr2": "value2"}, + "plug-static": map[string]interface{}{"attr3": "value3"}, + "plug-dynamic": map[string]interface{}{"attr4": "value4"}, + }, + } + + s.state.Lock() + s.state.Set("conns", connState) + s.state.Unlock() + + // Initialize the manager. This registers both snaps and reloads the connection. + _ = s.manager(c) + + conn := s.getConnection(c, "consumer", "plug", "producer", "slot") + + // Run the disconnect task and let it finish. + s.state.Lock() + change := s.state.NewChange("disconnect", "...") + ts, err := ifacestate.Disconnect(s.state, conn) + + c.Assert(err, IsNil) + change.AddAll(ts) + terr := s.state.NewTask("error-trigger", "provoking total undo") + terr.WaitAll(ts) + change.AddTask(terr) + c.Assert(change.Tasks(), HasLen, 4) + s.state.Unlock() + + s.settle(c) + + s.state.Lock() + defer s.state.Unlock() + + // Ensure that disconnect tasks were undone + for _, t := range ts.Tasks() { + c.Assert(t.Status(), Equals, state.UndoneStatus) + } + + var conns map[string]interface{} + c.Assert(s.state.Get("conns", &conns), IsNil) + c.Assert(conns, DeepEquals, connState) + + _ = s.getConnection(c, "consumer", "plug", "producer", "slot") +} + func (s *interfaceManagerSuite) TestStaleConnectionsIgnoredInReloadConnections(c *C) { s.mockIfaces(c, &ifacetest.TestInterface{InterfaceName: "test"}) @@ -2035,9 +2184,10 @@ func (s *interfaceManagerSuite) TestDisconnectSetsUpSecurity(c *C) { s.state.Unlock() s.manager(c) + conn := s.getConnection(c, "consumer", "plug", "producer", "slot") s.state.Lock() - ts, err := ifacestate.Disconnect(s.state, "consumer", "plug", "producer", "slot") + ts, err := ifacestate.Disconnect(s.state, conn) c.Assert(err, IsNil) ts.Tasks()[0].Set("snap-setup", &snapstate.SnapSetup{ SideInfo: &snap.SideInfo{ @@ -2049,9 +2199,7 @@ func (s *interfaceManagerSuite) TestDisconnectSetsUpSecurity(c *C) { change.AddAll(ts) s.state.Unlock() - s.se.Ensure() - s.se.Wait() - s.se.Stop() + s.settle(c) s.state.Lock() defer s.state.Unlock() @@ -2080,8 +2228,9 @@ func (s *interfaceManagerSuite) TestDisconnectTracksConnectionsInState(c *C) { s.manager(c) + conn := s.getConnection(c, "consumer", "plug", "producer", "slot") s.state.Lock() - ts, err := ifacestate.Disconnect(s.state, "consumer", "plug", "producer", "slot") + ts, err := ifacestate.Disconnect(s.state, conn) c.Assert(err, IsNil) ts.Tasks()[0].Set("snap-setup", &snapstate.SnapSetup{ SideInfo: &snap.SideInfo{ @@ -2093,9 +2242,7 @@ func (s *interfaceManagerSuite) TestDisconnectTracksConnectionsInState(c *C) { change.AddAll(ts) s.state.Unlock() - s.se.Ensure() - s.se.Wait() - s.se.Stop() + s.settle(c) s.state.Lock() defer s.state.Unlock() @@ -2121,7 +2268,12 @@ func (s *interfaceManagerSuite) TestDisconnectDisablesAutoConnect(c *C) { s.manager(c) s.state.Lock() - ts, err := ifacestate.Disconnect(s.state, "consumer", "plug", "producer", "slot") + conn := &interfaces.Connection{ + Plug: interfaces.NewConnectedPlug(&snap.PlugInfo{Snap: &snap.Info{SuggestedName: "consumer"}, Name: "plug"}, nil), + Slot: interfaces.NewConnectedSlot(&snap.SlotInfo{Snap: &snap.Info{SuggestedName: "producer"}, Name: "slot"}, nil), + } + + ts, err := ifacestate.Disconnect(s.state, conn) c.Assert(err, IsNil) ts.Tasks()[0].Set("snap-setup", &snapstate.SnapSetup{ SideInfo: &snap.SideInfo{ @@ -2133,9 +2285,7 @@ func (s *interfaceManagerSuite) TestDisconnectDisablesAutoConnect(c *C) { change.AddAll(ts) s.state.Unlock() - s.se.Ensure() - s.se.Wait() - s.se.Stop() + s.settle(c) s.state.Lock() defer s.state.Unlock() @@ -3072,6 +3222,143 @@ func (s *interfaceManagerSuite) TestSnapsWithSecurityProfiles(c *C) { }) } +func (s *interfaceManagerSuite) TestDisconnectInterfaces(c *C) { + s.mockIfaces(c, &ifacetest.TestInterface{InterfaceName: "test"}) + _ = s.manager(c) + + consumerInfo := s.mockSnap(c, consumerYaml) + producerInfo := s.mockSnap(c, producerYaml) + + s.state.Lock() + + sup := &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{ + RealName: "consumer"}, + } + + repo := s.manager(c).Repository() + c.Assert(repo.AddSnap(consumerInfo), IsNil) + c.Assert(repo.AddSnap(producerInfo), IsNil) + + plugDynAttrs := map[string]interface{}{ + "attr3": "value3", + } + slotDynAttrs := map[string]interface{}{ + "attr4": "value4", + } + repo.Connect(&interfaces.ConnRef{ + PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, + SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, + }, plugDynAttrs, slotDynAttrs, nil) + + chg := s.state.NewChange("install", "") + t := s.state.NewTask("auto-disconnect", "") + t.Set("snap-setup", sup) + chg.AddTask(t) + + s.state.Unlock() + + s.se.Ensure() + s.se.Wait() + + s.state.Lock() + defer s.state.Unlock() + + ht := t.HaltTasks() + c.Assert(ht, HasLen, 3) + + c.Assert(ht[2].Kind(), Equals, "disconnect") + var autoDisconnect bool + c.Assert(ht[2].Get("auto-disconnect", &autoDisconnect), IsNil) + c.Assert(autoDisconnect, Equals, true) + var plugDynamic, slotDynamic, plugStatic, slotStatic map[string]interface{} + c.Assert(ht[2].Get("plug-static", &plugStatic), IsNil) + c.Assert(ht[2].Get("plug-dynamic", &plugDynamic), IsNil) + c.Assert(ht[2].Get("slot-static", &slotStatic), IsNil) + c.Assert(ht[2].Get("slot-dynamic", &slotDynamic), IsNil) + + c.Assert(plugStatic, DeepEquals, map[string]interface{}{"attr1": "value1"}) + c.Assert(slotStatic, DeepEquals, map[string]interface{}{"attr2": "value2"}) + c.Assert(plugDynamic, DeepEquals, map[string]interface{}{"attr3": "value3"}) + c.Assert(slotDynamic, DeepEquals, map[string]interface{}{"attr4": "value4"}) + + var expectedHooks = []struct{ snap, hook string }{ + {snap: "producer", hook: "disconnect-slot-slot"}, + {snap: "consumer", hook: "disconnect-plug-plug"}, + } + + for i := 0; i < 2; i++ { + var hsup hookstate.HookSetup + c.Assert(ht[i].Kind(), Equals, "run-hook") + c.Assert(ht[i].Get("hook-setup", &hsup), IsNil) + + c.Assert(hsup.Snap, Equals, expectedHooks[i].snap) + c.Assert(hsup.Hook, Equals, expectedHooks[i].hook) + } +} + +func (s *interfaceManagerSuite) testDisconnectInterfacesRetry(c *C, conflictingKind string) { + s.mockIfaces(c, &ifacetest.TestInterface{InterfaceName: "test"}) + _ = s.manager(c) + + consumerInfo := s.mockSnap(c, consumerYaml) + producerInfo := s.mockSnap(c, producerYaml) + + supprod := &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{ + RealName: "producer"}, + } + + s.state.Lock() + + repo := s.manager(c).Repository() + c.Assert(repo.AddSnap(consumerInfo), IsNil) + c.Assert(repo.AddSnap(producerInfo), IsNil) + + repo.Connect(&interfaces.ConnRef{ + PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, + SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, + }, nil, nil, nil) + + sup := &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{ + RealName: "consumer"}, + } + + chg2 := s.state.NewChange("remove", "") + t2 := s.state.NewTask("auto-disconnect", "") + t2.Set("snap-setup", sup) + chg2.AddTask(t2) + + // create conflicting task + chg1 := s.state.NewChange("conflicting change", "") + t1 := s.state.NewTask(conflictingKind, "") + t1.Set("snap-setup", supprod) + chg1.AddTask(t1) + t3 := s.state.NewTask("other", "") + t1.WaitFor(t3) + chg1.AddTask(t3) + t3.SetStatus(state.HoldStatus) + + s.state.Unlock() + s.se.Ensure() + s.se.Wait() + + s.state.Lock() + defer s.state.Unlock() + + c.Assert(strings.Join(t2.Log(), ""), Matches, `.*Waiting for conflicting change in progress...`) + c.Assert(t2.Status(), Equals, state.DoingStatus) +} + +func (s *interfaceManagerSuite) TestDisconnectInterfacesRetryLink(c *C) { + s.testDisconnectInterfacesRetry(c, "link-snap") +} + +func (s *interfaceManagerSuite) TestDisconnectInterfacesRetrySetupProfiles(c *C) { + s.testDisconnectInterfacesRetry(c, "setup-profiles") +} + func (s *interfaceManagerSuite) setupGadgetConnect(c *C) { s.mockIfaces(c, &ifacetest.TestInterface{InterfaceName: "test"}) s.mockSnapDecl(c, "consumer", "publisher1", nil) diff --git a/overlord/managers_test.go b/overlord/managers_test.go index ae17c1788e..9d6259142a 100644 --- a/overlord/managers_test.go +++ b/overlord/managers_test.go @@ -2205,6 +2205,14 @@ func (ms *mgrsSuite) TestRemoveAndInstallWithAutoconnectHappy(c *C) { c.Assert(chg2.Status(), Equals, state.DoneStatus, Commentf("install-snap change failed with: %v", chg.Err())) } +const otherSnapYaml = `name: other-snap +version: 1.0 +apps: + baz: + command: bin/bar + plugs: [media-hub] +` + func (ms *mgrsSuite) TestUpdateManyWithAutoconnect(c *C) { const someSnapYaml = `name: some-snap version: 1.0 @@ -2214,13 +2222,7 @@ apps: plugs: [network,home] slots: [media-hub] ` - const otherSnapYaml = `name: other-snap -version: 1.0 -apps: - baz: - command: bin/bar - plugs: [media-hub] -` + const coreSnapYaml = `name: core type: os version: @VERSION@` @@ -2335,21 +2337,15 @@ version: @VERSION@` c.Assert(connections, HasLen, 3) } -func (ms *mgrsSuite) testUpdateWithAutoconnectRetry(c *C, updateSnapName, removeSnapName string) { - const someSnapYaml = `name: some-snap +const someSnapYaml = `name: some-snap version: 1.0 apps: foo: command: bin/bar slots: [media-hub] ` - const otherSnapYaml = `name: other-snap -version: 1.0 -apps: - baz: - command: bin/bar - plugs: [media-hub] -` + +func (ms *mgrsSuite) testUpdateWithAutoconnectRetry(c *C, updateSnapName, removeSnapName string) { snapPath, _ := ms.makeStoreTestSnap(c, someSnapYaml, "40") ms.serveSnap(snapPath, "40") @@ -2421,7 +2417,7 @@ apps: var retryCheck bool for _, t := range st.Tasks() { if t.Kind() == "auto-connect" { - c.Assert(strings.Join(t.Log(), ""), Matches, `.*auto-connect of snap .* will be retried because of "other-snap" - "some-snap" conflict`) + c.Assert(strings.Join(t.Log(), ""), Matches, `.*Waiting for conflicting change in progress...`) retryCheck = true } } @@ -2449,3 +2445,163 @@ func (ms *mgrsSuite) TestUpdateWithAutoconnectRetrySlotSide(c *C) { func (ms *mgrsSuite) TestUpdateWithAutoconnectRetryPlugSide(c *C) { ms.testUpdateWithAutoconnectRetry(c, "other-snap", "some-snap") } + +func (ms *mgrsSuite) TestDisconnectIgnoredOnSymmetricRemove(c *C) { + const someSnapYaml = `name: some-snap +version: 1.0 +apps: + foo: + command: bin/bar + slots: [media-hub] +` + const otherSnapYaml = `name: other-snap +version: 1.0 +apps: + baz: + command: bin/bar + plugs: [media-hub] +` + st := ms.o.State() + st.Lock() + defer st.Unlock() + + st.Set("conns", map[string]interface{}{ + "other-snap:media-hub some-snap:media-hub": map[string]interface{}{"interface": "media-hub", "auto": false}, + }) + + si := &snap.SideInfo{RealName: "some-snap", SnapID: fakeSnapID("some-snap"), Revision: snap.R(1)} + snapInfo := snaptest.MockSnap(c, someSnapYaml, si) + c.Assert(snapInfo.Slots, HasLen, 1) + + oi := &snap.SideInfo{RealName: "other-snap", SnapID: fakeSnapID("other-snap"), Revision: snap.R(1)} + otherInfo := snaptest.MockSnap(c, otherSnapYaml, oi) + c.Assert(otherInfo.Plugs, HasLen, 1) + + snapstate.Set(st, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si}, + Current: snap.R(1), + SnapType: "app", + }) + snapstate.Set(st, "other-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{oi}, + Current: snap.R(1), + SnapType: "app", + }) + + repo := ms.o.InterfaceManager().Repository() + + // add snaps to the repo to have plugs/slots + c.Assert(repo.AddSnap(snapInfo), IsNil) + c.Assert(repo.AddSnap(otherInfo), IsNil) + repo.Connect(&interfaces.ConnRef{ + PlugRef: interfaces.PlugRef{Snap: "other-snap", Name: "media-hub"}, + SlotRef: interfaces.SlotRef{Snap: "some-snap", Name: "media-hub"}, + }, nil, nil, nil) + + ts, err := snapstate.Remove(st, "some-snap", snap.R(0)) + c.Assert(err, IsNil) + chg := st.NewChange("uninstall", "...") + chg.AddAll(ts) + + // remove other-snap + ts2, err := snapstate.Remove(st, "other-snap", snap.R(0)) + c.Assert(err, IsNil) + chg2 := st.NewChange("uninstall", "...") + chg2.AddAll(ts2) + + st.Unlock() + err = ms.o.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil) + + c.Assert(chg.Status(), Equals, state.DoneStatus) + + // check connections + var conns map[string]interface{} + st.Get("conns", &conns) + c.Assert(conns, HasLen, 0) + + var disconnectInterfacesCount, slotHookCount, plugHookCount int + for _, t := range st.Tasks() { + if t.Kind() == "auto-disconnect" { + disconnectInterfacesCount++ + } + if t.Kind() == "run-hook" { + var hsup hookstate.HookSetup + c.Assert(t.Get("hook-setup", &hsup), IsNil) + if hsup.Hook == "disconnect-plug-media-hub" { + plugHookCount++ + } + if hsup.Hook == "disconnect-slot-media-hub" { + slotHookCount++ + } + } + } + c.Assert(plugHookCount, Equals, 1) + c.Assert(slotHookCount, Equals, 1) + c.Assert(disconnectInterfacesCount, Equals, 2) + + var snst snapstate.SnapState + err = snapstate.Get(st, "other-snap", &snst) + c.Assert(err, Equals, state.ErrNoState) + _, err = repo.Connected("other-snap", "media-hub") + c.Assert(err, ErrorMatches, `snap "other-snap" has no plug or slot named "media-hub"`) +} + +func (ms *mgrsSuite) TestDisconnectOnUninstallRemovesAutoconnection(c *C) { + st := ms.o.State() + st.Lock() + defer st.Unlock() + + st.Set("conns", map[string]interface{}{ + "other-snap:media-hub some-snap:media-hub": map[string]interface{}{"interface": "media-hub", "auto": true}, + }) + + si := &snap.SideInfo{RealName: "some-snap", SnapID: fakeSnapID("some-snap"), Revision: snap.R(1)} + snapInfo := snaptest.MockSnap(c, someSnapYaml, si) + + oi := &snap.SideInfo{RealName: "other-snap", SnapID: fakeSnapID("other-snap"), Revision: snap.R(1)} + otherInfo := snaptest.MockSnap(c, otherSnapYaml, oi) + + snapstate.Set(st, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si}, + Current: snap.R(1), + SnapType: "app", + }) + snapstate.Set(st, "other-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{oi}, + Current: snap.R(1), + SnapType: "app", + }) + + repo := ms.o.InterfaceManager().Repository() + + // add snaps to the repo to have plugs/slots + c.Assert(repo.AddSnap(snapInfo), IsNil) + c.Assert(repo.AddSnap(otherInfo), IsNil) + repo.Connect(&interfaces.ConnRef{ + PlugRef: interfaces.PlugRef{Snap: "other-snap", Name: "media-hub"}, + SlotRef: interfaces.SlotRef{Snap: "some-snap", Name: "media-hub"}, + }, nil, nil, nil) + + ts, err := snapstate.Remove(st, "some-snap", snap.R(0)) + c.Assert(err, IsNil) + chg := st.NewChange("uninstall", "...") + chg.AddAll(ts) + + st.Unlock() + err = ms.o.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil) + + c.Assert(chg.Status(), Equals, state.DoneStatus) + + // check connections; auto-connection should be removed completely from conns on uninstall. + var conns map[string]interface{} + st.Get("conns", &conns) + c.Assert(conns, HasLen, 0) +} diff --git a/overlord/snapstate/backend.go b/overlord/snapstate/backend.go index 5fec77e0c4..11e91f8bf0 100644 --- a/overlord/snapstate/backend.go +++ b/overlord/snapstate/backend.go @@ -54,7 +54,7 @@ type StoreService interface { type managerBackend interface { // install releated - SetupSnap(snapFilePath string, si *snap.SideInfo, meter progress.Meter) (snap.Type, error) + SetupSnap(snapFilePath, instanceName string, si *snap.SideInfo, meter progress.Meter) (snap.Type, error) CopySnapData(newSnap, oldSnap *snap.Info, meter progress.Meter) error LinkSnap(info *snap.Info, model *asserts.Model) error StartServices(svcs []*snap.AppInfo, meter progress.Meter) error diff --git a/overlord/snapstate/backend/setup.go b/overlord/snapstate/backend/setup.go index 3a673b1799..83bce9b08f 100644 --- a/overlord/snapstate/backend/setup.go +++ b/overlord/snapstate/backend/setup.go @@ -30,13 +30,17 @@ import ( ) // SetupSnap does prepare and mount the snap for further processing. -func (b Backend) SetupSnap(snapFilePath string, sideInfo *snap.SideInfo, meter progress.Meter) (snapType snap.Type, err error) { +func (b Backend) SetupSnap(snapFilePath, instanceName string, sideInfo *snap.SideInfo, meter progress.Meter) (snapType snap.Type, err error) { // This assumes that the snap was already verified or --dangerous was used. s, snapf, oErr := OpenSnapFile(snapFilePath, sideInfo) if oErr != nil { return snapType, oErr } + + // update instance key to what was requested + _, s.InstanceKey = snap.SplitInstanceName(instanceName) + instdir := s.MountDir() defer func() { diff --git a/overlord/snapstate/backend/setup_test.go b/overlord/snapstate/backend/setup_test.go index 10889aee35..46c2de24d6 100644 --- a/overlord/snapstate/backend/setup_test.go +++ b/overlord/snapstate/backend/setup_test.go @@ -80,7 +80,7 @@ func (s *setupSuite) TestSetupDoUndoSimple(c *C) { Revision: snap.R(14), } - snapType, err := s.be.SetupSnap(snapPath, &si, progress.Null) + snapType, err := s.be.SetupSnap(snapPath, "hello", &si, progress.Null) c.Assert(err, IsNil) c.Check(snapType, Equals, snap.TypeApp) @@ -108,6 +108,41 @@ func (s *setupSuite) TestSetupDoUndoSimple(c *C) { } +func (s *setupSuite) TestSetupDoUndoInstance(c *C) { + snapPath := makeTestSnap(c, helloYaml1) + + si := snap.SideInfo{ + RealName: "hello", + Revision: snap.R(14), + } + + snapType, err := s.be.SetupSnap(snapPath, "hello_instance", &si, progress.Null) + c.Assert(err, IsNil) + c.Check(snapType, Equals, snap.TypeApp) + + // after setup the snap file is in the right dir + c.Assert(osutil.FileExists(filepath.Join(dirs.SnapBlobDir, "hello_instance_14.snap")), Equals, true) + + // ensure the right unit is created + mup := systemd.MountUnitPath(filepath.Join(dirs.StripRootDir(dirs.SnapMountDir), "hello_instance/14")) + c.Assert(mup, testutil.FileMatches, fmt.Sprintf("(?ms).*^Where=%s", filepath.Join(dirs.StripRootDir(dirs.SnapMountDir), "hello_instance/14"))) + c.Assert(mup, testutil.FileMatches, "(?ms).*^What=/var/lib/snapd/snaps/hello_instance_14.snap") + + minInfo := snap.MinimalPlaceInfo("hello_instance", snap.R(14)) + // mount dir was created + c.Assert(osutil.FileExists(minInfo.MountDir()), Equals, true) + + // undo undoes the mount unit and the instdir creation + err = s.be.UndoSetupSnap(minInfo, "app", progress.Null) + c.Assert(err, IsNil) + + l, _ := filepath.Glob(filepath.Join(dirs.SnapServicesDir, "*.mount")) + c.Assert(l, HasLen, 0) + c.Assert(osutil.FileExists(minInfo.MountDir()), Equals, false) + + c.Assert(osutil.FileExists(minInfo.MountFile()), Equals, false) +} + func (s *setupSuite) TestSetupDoUndoKernelUboot(c *C) { bootloader := boottest.NewMockBootloader("mock", c.MkDir()) partition.ForceBootloader(bootloader) @@ -132,7 +167,7 @@ type: kernel Revision: snap.R(140), } - snapType, err := s.be.SetupSnap(snapPath, &si, progress.Null) + snapType, err := s.be.SetupSnap(snapPath, "kernel", &si, progress.Null) c.Assert(err, IsNil) c.Check(snapType, Equals, snap.TypeKernel) l, _ := filepath.Glob(filepath.Join(bootloader.Dir(), "*")) @@ -177,11 +212,11 @@ type: kernel Revision: snap.R(140), } - _, err := s.be.SetupSnap(snapPath, &si, progress.Null) + _, err := s.be.SetupSnap(snapPath, "kernel", &si, progress.Null) c.Assert(err, IsNil) // retry run - _, err = s.be.SetupSnap(snapPath, &si, progress.Null) + _, err = s.be.SetupSnap(snapPath, "kernel", &si, progress.Null) c.Assert(err, IsNil) minInfo := snap.MinimalPlaceInfo("kernel", snap.R(140)) @@ -226,7 +261,7 @@ type: kernel Revision: snap.R(140), } - _, err := s.be.SetupSnap(snapPath, &si, progress.Null) + _, err := s.be.SetupSnap(snapPath, "kernel", &si, progress.Null) c.Assert(err, IsNil) minInfo := snap.MinimalPlaceInfo("kernel", snap.R(140)) @@ -266,7 +301,7 @@ func (s *setupSuite) TestSetupCleanupAfterFail(c *C) { }) defer r() - _, err := s.be.SetupSnap(snapPath, &si, progress.Null) + _, err := s.be.SetupSnap(snapPath, "hello", &si, progress.Null) c.Assert(err, ErrorMatches, "failed") // everything is gone diff --git a/overlord/snapstate/backend_test.go b/overlord/snapstate/backend_test.go index 305d11f530..17463d9043 100644 --- a/overlord/snapstate/backend_test.go +++ b/overlord/snapstate/backend_test.go @@ -30,6 +30,7 @@ import ( "golang.org/x/net/context" "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord/auth" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/snapstate/backend" @@ -95,6 +96,7 @@ func (ops fakeOps) First(op string) *fakeOp { type fakeDownload struct { name string macaroon string + target string } type byName []store.CurrentSnap @@ -141,6 +143,10 @@ func (f *fakeStore) pokeStateLock() { func (f *fakeStore) SnapInfo(spec store.SnapSpec, user *auth.UserState) (*snap.Info, error) { f.pokeStateLock() + _, instanceKey := snap.SplitInstanceName(spec.Name) + if instanceKey != "" { + return nil, fmt.Errorf("internal error: unexpected instance name: %q", spec.Name) + } sspec := snapSpec{ Name: spec.Name, } @@ -381,9 +387,11 @@ func (f *fakeStore) SnapAction(ctx context.Context, currentSnaps []*store.Curren return nil, fmt.Errorf("internal error: action without instance name") } + snapName, instanceKey := snap.SplitInstanceName(a.InstanceName) + if a.Action == "install" { spec := snapSpec{ - Name: snap.InstanceSnap(a.InstanceName), + Name: snapName, Channel: a.Channel, Revision: a.Revision, } @@ -401,6 +409,7 @@ func (f *fakeStore) SnapAction(ctx context.Context, currentSnaps []*store.Curren if !a.Revision.Unset() { info.Channel = "" } + info.InstanceKey = instanceKey res = append(res, info) continue } @@ -449,6 +458,7 @@ func (f *fakeStore) SnapAction(ctx context.Context, currentSnaps []*store.Curren if !a.Revision.Unset() { info.Channel = "" } + info.InstanceKey = instanceKey res = append(res, info) } @@ -478,6 +488,9 @@ func (f *fakeStore) SuggestedCurrency() string { func (f *fakeStore) Download(ctx context.Context, name, targetFn string, snapInfo *snap.DownloadInfo, pb progress.Meter, user *auth.UserState) error { f.pokeStateLock() + if _, key := snap.SplitInstanceName(name); key != "" { + return fmt.Errorf("internal error: unsupported download with instance name %q", name) + } var macaroon string if user != nil { macaroon = user.StoreMacaroon @@ -485,6 +498,7 @@ func (f *fakeStore) Download(ctx context.Context, name, targetFn string, snapInf f.downloads = append(f.downloads, fakeDownload{ macaroon: macaroon, name: name, + target: targetFn, }) f.fakeBackend.ops = append(f.fakeBackend.ops, fakeOp{op: "storesvc-download", name: name}) @@ -536,16 +550,34 @@ func (f *fakeSnappyBackend) OpenSnapFile(snapFilePath string, si *snap.SideInfo) op.sinfo = *si } - name := filepath.Base(snapFilePath) - if idx := strings.IndexByte(name, '_'); idx > -1 { - name = name[:idx] + var name string + if !osutil.IsDirectory(snapFilePath) { + name = filepath.Base(snapFilePath) + split := strings.Split(name, "_") + if len(split) >= 2 { + // <snap>_<rev>.snap + // <snap>_<instance-key>_<rev>.snap + name = split[0] + } + } else { + // for snap try only + snapf, err := snap.Open(snapFilePath) + if err != nil { + return nil, nil, err + } + + info, err := snap.ReadInfoFromSnapFile(snapf, si) + if err != nil { + return nil, nil, err + } + name = info.SuggestedName } f.ops = append(f.ops, op) return &snap.Info{SuggestedName: name, Architectures: []string{"all"}}, f.emptyContainer, nil } -func (f *fakeSnappyBackend) SetupSnap(snapFilePath string, si *snap.SideInfo, p progress.Meter) (snap.Type, error) { +func (f *fakeSnappyBackend) SetupSnap(snapFilePath, instanceName string, si *snap.SideInfo, p progress.Meter) (snap.Type, error) { p.Notify("setup-snap") revno := snap.R(0) if si != nil { @@ -553,6 +585,7 @@ func (f *fakeSnappyBackend) SetupSnap(snapFilePath string, si *snap.SideInfo, p } f.ops = append(f.ops, fakeOp{ op: "setup-snap", + name: instanceName, path: snapFilePath, revno: revno, }) @@ -573,17 +606,19 @@ func (f *fakeSnappyBackend) ReadInfo(name string, si *snap.SideInfo) (*snap.Info if name == "not-there" && si.Revision == snap.R(2) { return nil, &snap.NotFoundError{Snap: name, Revision: si.Revision} } + snapName, instanceKey := snap.SplitInstanceName(name) // naive emulation for now, always works info := &snap.Info{ - SuggestedName: name, + SuggestedName: snapName, SideInfo: *si, Architectures: []string{"all"}, Type: snap.TypeApp, } - if strings.Contains(name, "alias-snap") { - name = "alias-snap" + if strings.Contains(snapName, "alias-snap") { + // only for the switch below + snapName = "alias-snap" } - switch name { + switch snapName { case "gadget": info.Type = snap.TypeGadget case "core": @@ -619,6 +654,7 @@ apps: info.SideInfo = *si } + info.InstanceKey = instanceKey return info, nil } @@ -706,6 +742,7 @@ func (f *fakeSnappyBackend) UndoSetupSnap(s snap.PlaceInfo, typ snap.Type, p pro p.Notify("setup-snap") f.ops = append(f.ops, fakeOp{ op: "undo-setup-snap", + name: s.InstanceName(), path: s.MountDir(), stype: typ, }) diff --git a/overlord/snapstate/check_snap.go b/overlord/snapstate/check_snap.go index a041da5fc4..ccf6d62e2e 100644 --- a/overlord/snapstate/check_snap.go +++ b/overlord/snapstate/check_snap.go @@ -192,7 +192,7 @@ func validateContainer(c snap.Container, s *snap.Info, logf func(format string, } // checkSnap ensures that the snap can be installed. -func checkSnap(st *state.State, snapFilePath string, si *snap.SideInfo, curInfo *snap.Info, flags Flags) error { +func checkSnap(st *state.State, snapFilePath, instanceName string, si *snap.SideInfo, curInfo *snap.Info, flags Flags) error { // This assumes that the snap was already verified or --dangerous was used. s, c, err := openSnapFile(snapFilePath, si) @@ -208,9 +208,15 @@ func checkSnap(st *state.State, snapFilePath string, si *snap.SideInfo, curInfo return err } + snapName, instanceKey := snap.SplitInstanceName(instanceName) + // update instance key to what was requested + s.InstanceKey = instanceKey + st.Lock() defer st.Unlock() + // allow registered checks to run first as they may produce more + // precise errors for _, check := range checkSnapCallbacks { err := check(st, s, curInfo, flags) if err != nil { @@ -218,6 +224,10 @@ func checkSnap(st *state.State, snapFilePath string, si *snap.SideInfo, curInfo } } + if snapName != s.SnapName() { + return fmt.Errorf("cannot install snap %q using instance name %q", s.SnapName(), instanceName) + } + return nil } diff --git a/overlord/snapstate/check_snap_test.go b/overlord/snapstate/check_snap_test.go index 0c137a7a55..b6d0296525 100644 --- a/overlord/snapstate/check_snap_test.go +++ b/overlord/snapstate/check_snap_test.go @@ -78,7 +78,7 @@ architectures: restore := snapstate.MockOpenSnapFile(openSnapFile) defer restore() - err = snapstate.CheckSnap(s.st, "snap-path", nil, nil, snapstate.Flags{}) + err = snapstate.CheckSnap(s.st, "snap-path", "hello", nil, nil, snapstate.Flags{}) errorMsg := fmt.Sprintf(`snap "hello" supported architectures (yadayada, blahblah) are incompatible with this system (%s)`, arch.UbuntuArchitecture()) c.Assert(err.Error(), Equals, errorMsg) @@ -168,7 +168,7 @@ func (s *checkSnapSuite) TestCheckSnapAssumes(c *C) { } restore := snapstate.MockOpenSnapFile(openSnapFile) defer restore() - err = snapstate.CheckSnap(s.st, "snap-path", nil, nil, snapstate.Flags{}) + err = snapstate.CheckSnap(s.st, "snap-path", "foo", nil, nil, snapstate.Flags{}) if test.error != "" { c.Check(err, ErrorMatches, test.error) } else { @@ -202,7 +202,7 @@ version: 1.0` r2 := snapstate.MockCheckSnapCallbacks([]snapstate.CheckSnapCallback{checkCb}) defer r2() - err := snapstate.CheckSnap(s.st, "snap-path", si, nil, snapstate.Flags{}) + err := snapstate.CheckSnap(s.st, "snap-path", "foo", si, nil, snapstate.Flags{}) c.Check(err, IsNil) c.Check(checkCbCalled, Equals, true) @@ -229,7 +229,7 @@ version: 1.0` defer r2() snapstate.AddCheckSnapCallback(checkCb) - err = snapstate.CheckSnap(s.st, "snap-path", nil, nil, snapstate.Flags{}) + err = snapstate.CheckSnap(s.st, "snap-path", "foo", nil, nil, snapstate.Flags{}) c.Check(err, Equals, fail) } @@ -270,7 +270,7 @@ version: 2 defer restore() st.Unlock() - err = snapstate.CheckSnap(st, "snap-path", nil, nil, snapstate.Flags{}) + err = snapstate.CheckSnap(st, "snap-path", "gadget", nil, nil, snapstate.Flags{}) st.Lock() c.Check(err, IsNil) } @@ -312,7 +312,7 @@ version: 2 defer restore() st.Unlock() - err = snapstate.CheckSnap(st, "snap-path", nil, nil, snapstate.Flags{}) + err = snapstate.CheckSnap(st, "snap-path", "gadget", nil, nil, snapstate.Flags{}) st.Lock() c.Check(err, IsNil) } @@ -353,7 +353,7 @@ version: 2 defer restore() st.Unlock() - err = snapstate.CheckSnap(st, "snap-path", nil, nil, snapstate.Flags{}) + err = snapstate.CheckSnap(st, "snap-path", "gadget", nil, nil, snapstate.Flags{}) st.Lock() c.Check(err, ErrorMatches, `cannot replace signed gadget snap with an unasserted one`) } @@ -394,7 +394,7 @@ version: 2 defer restore() st.Unlock() - err = snapstate.CheckSnap(st, "snap-path", nil, nil, snapstate.Flags{}) + err = snapstate.CheckSnap(st, "snap-path", "gadget", nil, nil, snapstate.Flags{}) st.Lock() c.Check(err, ErrorMatches, "cannot replace gadget snap with a different one") } @@ -436,7 +436,7 @@ version: 2 defer restore() st.Unlock() - err = snapstate.CheckSnap(st, "snap-path", nil, nil, snapstate.Flags{}) + err = snapstate.CheckSnap(st, "snap-path", "gadget", nil, nil, snapstate.Flags{}) st.Lock() c.Check(err, ErrorMatches, "cannot replace gadget snap with a different one") } @@ -464,7 +464,7 @@ version: 1 defer restore() st.Unlock() - err = snapstate.CheckSnap(st, "snap-path", nil, nil, snapstate.Flags{}) + err = snapstate.CheckSnap(st, "snap-path", "gadget", nil, nil, snapstate.Flags{}) st.Lock() c.Check(err, IsNil) } @@ -485,7 +485,7 @@ confinement: devmode restore := snapstate.MockOpenSnapFile(openSnapFile) defer restore() - err = snapstate.CheckSnap(s.st, "snap-path", nil, nil, snapstate.Flags{}) + err = snapstate.CheckSnap(s.st, "snap-path", "hello", nil, nil, snapstate.Flags{}) c.Assert(err, ErrorMatches, ".* requires devmode or confinement override") } @@ -509,7 +509,7 @@ confinement: classic restore = release.MockOnClassic(true) defer restore() - err = snapstate.CheckSnap(s.st, "snap-path", nil, nil, snapstate.Flags{}) + err = snapstate.CheckSnap(s.st, "snap-path", "hello", nil, nil, snapstate.Flags{}) c.Assert(err, ErrorMatches, ".* requires classic confinement") } @@ -533,7 +533,7 @@ confinement: classic restore = release.MockOnClassic(false) defer restore() - err = snapstate.CheckSnap(s.st, "snap-path", nil, nil, snapstate.Flags{Classic: true}) + err = snapstate.CheckSnap(s.st, "snap-path", "hello", nil, nil, snapstate.Flags{Classic: true}) c.Assert(err, ErrorMatches, ".* requires classic confinement which is only available on classic systems") } @@ -575,7 +575,7 @@ version: 2 defer restore() st.Unlock() - err = snapstate.CheckSnap(st, "snap-path", nil, nil, snapstate.Flags{}) + err = snapstate.CheckSnap(st, "snap-path", "kernel", nil, nil, snapstate.Flags{}) st.Lock() c.Check(err, IsNil) } @@ -617,7 +617,7 @@ version: 2 defer restore() st.Unlock() - err = snapstate.CheckSnap(st, "snap-path", nil, nil, snapstate.Flags{}) + err = snapstate.CheckSnap(st, "snap-path", "kernel", nil, nil, snapstate.Flags{}) st.Lock() c.Check(err, ErrorMatches, "cannot replace kernel snap with a different one") } @@ -642,7 +642,7 @@ base: some-base defer restore() st.Unlock() - err = snapstate.CheckSnap(st, "snap-path", nil, nil, snapstate.Flags{}) + err = snapstate.CheckSnap(st, "snap-path", "requires-base", nil, nil, snapstate.Flags{}) st.Lock() c.Check(err, ErrorMatches, "cannot find required base \"some-base\"") } @@ -680,7 +680,7 @@ base: some-base defer restore() st.Unlock() - err = snapstate.CheckSnap(st, "snap-path", nil, nil, snapstate.Flags{}) + err = snapstate.CheckSnap(st, "snap-path", "requires-base", nil, nil, snapstate.Flags{}) st.Lock() c.Check(err, IsNil) } @@ -695,3 +695,73 @@ func emptyContainer(c *C) *snapdir.SnapDir { c.Assert(ioutil.WriteFile(filepath.Join(d, "meta", "snap.yaml"), nil, 0444), IsNil) return snapdir.New(d) } + +func (s *checkSnapSuite) TestCheckSnapInstanceName(c *C) { + st := state.New(nil) + st.Lock() + defer st.Unlock() + + si := &snap.SideInfo{RealName: "foo", Revision: snap.R(1), SnapID: "some-base-id"} + info := snaptest.MockSnap(c, ` +name: foo +version: 1 +`, si) + snapstate.Set(st, "foo", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si}, + Current: si.Revision, + }) + + var openSnapFile = func(path string, si *snap.SideInfo) (*snap.Info, snap.Container, error) { + return info, emptyContainer(c), nil + } + restore := snapstate.MockOpenSnapFile(openSnapFile) + defer restore() + + st.Unlock() + err := snapstate.CheckSnap(st, "snap-path", "foo_instance", nil, nil, snapstate.Flags{}) + st.Lock() + c.Check(err, IsNil) + + st.Unlock() + err = snapstate.CheckSnap(st, "snap-path", "bar_instance", nil, nil, snapstate.Flags{}) + st.Lock() + c.Check(err, ErrorMatches, `cannot install snap "foo" using instance name "bar_instance"`) + + st.Unlock() + err = snapstate.CheckSnap(st, "snap-path", "other-name", nil, nil, snapstate.Flags{}) + st.Lock() + c.Check(err, ErrorMatches, `cannot install snap "foo" using instance name "other-name"`) +} + +func (s *checkSnapSuite) TestCheckSnapCheckCallInstanceKeySet(c *C) { + const yaml = `name: foo +version: 1.0` + + si := &snap.SideInfo{ + SnapID: "snap-id", + } + + var openSnapFile = func(path string, si *snap.SideInfo) (*snap.Info, snap.Container, error) { + info := snaptest.MockInfo(c, yaml, si) + return info, emptyContainer(c), nil + } + r1 := snapstate.MockOpenSnapFile(openSnapFile) + defer r1() + + checkCbCalled := false + checkCb := func(st *state.State, s, cur *snap.Info, flags snapstate.Flags) error { + c.Assert(s.InstanceName(), Equals, "foo_instance") + c.Assert(s.SnapName(), Equals, "foo") + c.Assert(s.SnapID, Equals, "snap-id") + checkCbCalled = true + return nil + } + r2 := snapstate.MockCheckSnapCallbacks([]snapstate.CheckSnapCallback{checkCb}) + defer r2() + + err := snapstate.CheckSnap(s.st, "snap-path", "foo_instance", si, nil, snapstate.Flags{}) + c.Check(err, IsNil) + + c.Check(checkCbCalled, Equals, true) +} diff --git a/overlord/snapstate/handlers.go b/overlord/snapstate/handlers.go index ca7f7b4281..0ccd493de2 100644 --- a/overlord/snapstate/handlers.go +++ b/overlord/snapstate/handlers.go @@ -429,10 +429,10 @@ func (m *SnapManager) doDownloadSnap(t *state.Task, tomb *tomb.Tomb) error { if err != nil { return err } - err = theStore.Download(tomb.Context(nil), snapsup.InstanceName(), targetFn, &storeInfo.DownloadInfo, meter, user) + err = theStore.Download(tomb.Context(nil), snapsup.SnapName(), targetFn, &storeInfo.DownloadInfo, meter, user) snapsup.SideInfo = &storeInfo.SideInfo } else { - err = theStore.Download(tomb.Context(nil), snapsup.InstanceName(), targetFn, snapsup.DownloadInfo, meter, user) + err = theStore.Download(tomb.Context(nil), snapsup.SnapName(), targetFn, snapsup.DownloadInfo, meter, user) } if err != nil { return err @@ -466,14 +466,14 @@ func (m *SnapManager) doMountSnap(t *state.Task, _ *tomb.Tomb) error { m.backend.CurrentInfo(curInfo) - if err := checkSnap(t.State(), snapsup.SnapPath, snapsup.SideInfo, curInfo, snapsup.Flags); err != nil { + if err := checkSnap(t.State(), snapsup.SnapPath, snapsup.InstanceName(), snapsup.SideInfo, curInfo, snapsup.Flags); err != nil { return err } pb := NewTaskProgressAdapterUnlocked(t) // TODO Use snapsup.Revision() to obtain the right info to mount // instead of assuming the candidate is the right one. - snapType, err := m.backend.SetupSnap(snapsup.SnapPath, snapsup.SideInfo, pb) + snapType, err := m.backend.SetupSnap(snapsup.SnapPath, snapsup.InstanceName(), snapsup.SideInfo, pb) if err != nil { return err } @@ -761,6 +761,8 @@ func (m *SnapManager) doLinkSnap(t *state.Task, _ *tomb.Tomb) error { snapst.UserID = snapsup.UserID } } + // keep instance key + snapst.InstanceKey = snapsup.InstanceKey newInfo, err := readInfo(snapsup.InstanceName(), cand, 0) if err != nil { diff --git a/overlord/snapstate/handlers_mount_test.go b/overlord/snapstate/handlers_mount_test.go index 0ef5f1663a..8bca852a55 100644 --- a/overlord/snapstate/handlers_mount_test.go +++ b/overlord/snapstate/handlers_mount_test.go @@ -112,11 +112,13 @@ func (s *mountSnapSuite) TestDoUndoMountSnap(c *C) { }, { op: "setup-snap", + name: "core", path: testSnap, revno: snap.R(2), }, { op: "undo-setup-snap", + name: "core", path: filepath.Join(dirs.SnapMountDir, "core/2"), stype: "os", }, @@ -170,11 +172,13 @@ func (s *mountSnapSuite) TestDoMountSnapError(c *C) { }, { op: "setup-snap", + name: "borken", path: testSnap, revno: snap.R(2), }, { op: "undo-setup-snap", + name: "borken", path: filepath.Join(dirs.SnapMountDir, "borken/2"), stype: "app", }, @@ -230,11 +234,13 @@ func (s *mountSnapSuite) TestDoMountSnapErrorNotFound(c *C) { }, { op: "setup-snap", + name: "not-there", path: testSnap, revno: snap.R(2), }, { op: "undo-setup-snap", + name: "not-there", path: filepath.Join(dirs.SnapMountDir, "not-there/2"), stype: "app", }, @@ -304,6 +310,7 @@ func (s *mountSnapSuite) TestDoMountNotMountedRetryRetry(c *C) { }, { op: "setup-snap", + name: "not-there", path: testSnap, revno: snap.R(2), }, diff --git a/overlord/snapstate/snapstate.go b/overlord/snapstate/snapstate.go index 772eeaef19..6a337054bf 100644 --- a/overlord/snapstate/snapstate.go +++ b/overlord/snapstate/snapstate.go @@ -452,14 +452,30 @@ func defaultContentPlugProviders(st *state.State, info *snap.Info) []string { return out } +func getFeatureFlagBool(tr *config.Transaction, flag string) (bool, error) { + var v interface{} = false + if err := tr.GetMaybe("core", flag, &v); err != nil { + return false, err + } + switch value := v.(type) { + case string: + if value == "" { + return false, nil + } + case bool: + return value, nil + } + return false, fmt.Errorf("internal error: feature flag %v has unexpected value %#v (%T)", flag, v, v) +} + // validateFeatureFlags validates the given snap only uses experimental // features that are enabled by the user. func validateFeatureFlags(st *state.State, info *snap.Info) error { tr := config.NewTransaction(st) if len(info.Layout) > 0 { - var flag bool - if err := tr.GetMaybe("core", "experimental.layouts", &flag); err != nil { + flag, err := getFeatureFlagBool(tr, "experimental.layouts") + if err != nil { return err } if !flag { @@ -468,8 +484,8 @@ func validateFeatureFlags(st *state.State, info *snap.Info) error { } if info.InstanceKey != "" { - var flag bool - if err := tr.GetMaybe("core", "experimental.parallel-instances", &flag); err != nil { + flag, err := getFeatureFlagBool(tr, "experimental.parallel-instances") + if err != nil { return err } if !flag { @@ -588,6 +604,7 @@ func Install(st *state.State, name, channel string, revision snap.Revision, user SideInfo: &info.SideInfo, Type: info.Type, PlugsOnly: len(info.Slots) == 0, + InstanceKey: info.InstanceKey, } return doInstall(st, &snapst, snapsup, 0) @@ -747,6 +764,7 @@ func doUpdate(st *state.State, names []string, updates []*snap.Info, params func SideInfo: &update.SideInfo, Type: update.Type, PlugsOnly: len(update.Slots) == 0, + InstanceKey: update.InstanceKey, } ts, err := doInstall(st, snapst, snapsup, 0) @@ -844,6 +862,8 @@ func applyAutoAliasesDelta(st *state.State, delta map[string][]string, op string snapsup := &SnapSetup{ SideInfo: &snap.SideInfo{RealName: snapName}, + // TODO parallel-install: review and update the aliases + // code in the context of parallel installs } alias := st.NewTask(kind, fmt.Sprintf(msg, snapsup.InstanceName())) alias.Set("snap-setup", &snapsup) @@ -999,8 +1019,9 @@ func Switch(st *state.State, name, channel string) (*state.TaskSet, error) { } snapsup := &SnapSetup{ - SideInfo: snapst.CurrentSideInfo(), - Channel: channel, + SideInfo: snapst.CurrentSideInfo(), + Channel: channel, + InstanceKey: snapst.InstanceKey, } switchSnap := st.NewTask("switch-snap", fmt.Sprintf(i18n.G("Switch snap %q to %s"), snapsup.InstanceName(), snapsup.Channel)) @@ -1068,8 +1089,9 @@ func Update(st *state.State, name, channel string, revision snap.Revision, userI } snapsup := &SnapSetup{ - SideInfo: snapst.CurrentSideInfo(), - Flags: snapst.Flags.ForSnapSetup(), + SideInfo: snapst.CurrentSideInfo(), + Flags: snapst.Flags.ForSnapSetup(), + InstanceKey: snapst.InstanceKey, } if snapst.Channel != channel { @@ -1197,10 +1219,11 @@ func Enable(st *state.State, name string) (*state.TaskSet, error) { } snapsup := &SnapSetup{ - SideInfo: snapst.CurrentSideInfo(), - Flags: snapst.Flags.ForSnapSetup(), - Type: info.Type, - PlugsOnly: len(info.Slots) == 0, + SideInfo: snapst.CurrentSideInfo(), + Flags: snapst.Flags.ForSnapSetup(), + Type: info.Type, + PlugsOnly: len(info.Slots) == 0, + InstanceKey: snapst.InstanceKey, } prepareSnap := st.NewTask("prepare-snap", fmt.Sprintf(i18n.G("Prepare snap %q (%s)"), snapsup.InstanceName(), snapst.Current)) @@ -1254,11 +1277,12 @@ func Disable(st *state.State, name string) (*state.TaskSet, error) { snapsup := &SnapSetup{ SideInfo: &snap.SideInfo{ - RealName: name, + RealName: snap.InstanceSnap(name), Revision: snapst.Current, }, - Type: info.Type, - PlugsOnly: len(info.Slots) == 0, + Type: info.Type, + PlugsOnly: len(info.Slots) == 0, + InstanceKey: snapst.InstanceKey, } stopSnapServices := st.NewTask("stop-snap-services", fmt.Sprintf(i18n.G("Stop snap %q (%s) services"), snapsup.InstanceName(), snapst.Current)) @@ -1459,11 +1483,12 @@ func Remove(st *state.State, name string, revision snap.Revision) (*state.TaskSe // main/current SnapSetup snapsup := SnapSetup{ SideInfo: &snap.SideInfo{ - RealName: name, + RealName: snap.InstanceSnap(name), Revision: revision, }, - Type: info.Type, - PlugsOnly: len(info.Slots) == 0, + Type: info.Type, + PlugsOnly: len(info.Slots) == 0, + InstanceKey: snapst.InstanceKey, } // trigger remove @@ -1485,15 +1510,29 @@ func Remove(st *state.State, name string, revision snap.Revision) (*state.TaskSe removeHook = SetupRemoveHook(st, snapsup.InstanceName()) } - if active { // unlink - var prev *state.Task - - stopSnapServices := st.NewTask("stop-snap-services", fmt.Sprintf(i18n.G("Stop snap %q services"), name)) + var prev *state.Task + var stopSnapServices *state.Task + if active { + stopSnapServices = st.NewTask("stop-snap-services", fmt.Sprintf(i18n.G("Stop snap %q services"), name)) stopSnapServices.Set("snap-setup", snapsup) stopSnapServices.Set("stop-reason", snap.StopReasonRemove) + addNext(state.NewTaskSet(stopSnapServices)) prev = stopSnapServices + } + + if removeAll { + // run disconnect hooks + disconnect := st.NewTask("auto-disconnect", fmt.Sprintf(i18n.G("Disconnect interfaces of snap %q"), snapsup.InstanceName())) + disconnect.Set("snap-setup", snapsup) + if prev != nil { + disconnect.WaitFor(prev) + } + addNext(state.NewTaskSet(disconnect)) + prev = disconnect + } - tasks := []*state.Task{stopSnapServices} + if active { // unlink + var tasks []*state.Task if removeHook != nil { tasks = append(tasks, removeHook) removeHook.WaitFor(prev) @@ -1524,14 +1563,6 @@ func Remove(st *state.State, name string, revision snap.Revision) (*state.TaskSe si := seq[i] addNext(removeInactiveRevision(st, name, si.Revision)) } - - discardConns := st.NewTask("discard-conns", fmt.Sprintf(i18n.G("Discard interface connections for snap %q (%s)"), name, revision)) - discardConns.Set("snap-setup", &SnapSetup{ - SideInfo: &snap.SideInfo{ - RealName: name, - }, - }) - addNext(state.NewTaskSet(discardConns)) } else { addNext(removeInactiveRevision(st, name, revision)) } @@ -1540,11 +1571,13 @@ func Remove(st *state.State, name string, revision snap.Revision) (*state.TaskSe } func removeInactiveRevision(st *state.State, name string, revision snap.Revision) *state.TaskSet { + snapName, instanceKey := snap.SplitInstanceName(name) snapsup := SnapSetup{ SideInfo: &snap.SideInfo{ - RealName: name, + RealName: snapName, Revision: revision, }, + InstanceKey: instanceKey, } clearData := st.NewTask("clear-snap", fmt.Sprintf(i18n.G("Remove data for snap %q (%s)"), name, revision)) @@ -1635,10 +1668,11 @@ func RevertToRevision(st *state.State, name string, rev snap.Revision, flags Fla } snapsup := &SnapSetup{ - SideInfo: snapst.Sequence[i], - Flags: flags.ForSnapSetup(), - Type: info.Type, - PlugsOnly: len(info.Slots) == 0, + SideInfo: snapst.Sequence[i], + Flags: flags.ForSnapSetup(), + Type: info.Type, + PlugsOnly: len(info.Slots) == 0, + InstanceKey: snapst.InstanceKey, } return doInstall(st, &snapst, snapsup, 0) } diff --git a/overlord/snapstate/snapstate_test.go b/overlord/snapstate/snapstate_test.go index 768f62ecbf..8100f74702 100644 --- a/overlord/snapstate/snapstate_test.go +++ b/overlord/snapstate/snapstate_test.go @@ -202,6 +202,7 @@ func AddForeignTaskHandlers(runner *state.TaskRunner, tracker ForeignTaskTracker } runner.AddHandler("setup-profiles", fakeHandler, fakeHandler) runner.AddHandler("auto-connect", fakeHandler, nil) + runner.AddHandler("auto-disconnect", fakeHandler, nil) runner.AddHandler("remove-profiles", fakeHandler, fakeHandler) runner.AddHandler("discard-conns", fakeHandler, fakeHandler) runner.AddHandler("validate-snap", fakeHandler, nil) @@ -427,13 +428,13 @@ func verifyUpdateTasks(c *C, opts, discards int, ts *state.TaskSet, st *state.St func verifyRemoveTasks(c *C, ts *state.TaskSet) { c.Assert(taskKinds(ts.Tasks()), DeepEquals, []string{ "stop-snap-services", + "auto-disconnect", "run-hook[remove]", "remove-aliases", "unlink-snap", "remove-profiles", "clear-snap", "discard-snap", - "discard-conns", }) verifyStopReason(c, ts, "remove") } @@ -1887,6 +1888,7 @@ func (s *snapmgrTestSuite) TestInstallRunThrough(c *C) { c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{{ macaroon: s.user.StoreMacaroon, name: "some-snap", + target: filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap"), }}) expected := fakeOps{ { @@ -1928,6 +1930,7 @@ func (s *snapmgrTestSuite) TestInstallRunThrough(c *C) { }, { op: "setup-snap", + name: "some-snap", path: filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap"), revno: snap.R(11), }, @@ -2014,6 +2017,7 @@ func (s *snapmgrTestSuite) TestInstallRunThrough(c *C) { c.Assert(err, IsNil) snapst := snaps["some-snap"] + c.Assert(snapst, NotNil) c.Assert(snapst.Active, Equals, true) c.Assert(snapst.Channel, Equals, "some-channel") c.Assert(snapst.Sequence[0], DeepEquals, &snap.SideInfo{ @@ -2025,6 +2029,174 @@ func (s *snapmgrTestSuite) TestInstallRunThrough(c *C) { c.Assert(snapst.Required, Equals, false) } +func (s *snapmgrTestSuite) TestParallelInstanceInstallRunThrough(c *C) { + s.state.Lock() + defer s.state.Unlock() + + tr := config.NewTransaction(s.state) + tr.Set("core", "experimental.parallel-instances", true) + tr.Commit() + + chg := s.state.NewChange("install", "install a snap") + ts, err := snapstate.Install(s.state, "some-snap_instance", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{}) + c.Assert(err, IsNil) + chg.AddAll(ts) + + s.state.Unlock() + s.settle(c) + s.state.Lock() + + // ensure all our tasks ran + c.Assert(chg.Err(), IsNil) + c.Assert(chg.IsReady(), Equals, true) + c.Check(snapstate.Installing(s.state), Equals, false) + c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{{ + macaroon: s.user.StoreMacaroon, + // TODO parallel-install: fix once store changes are in place + name: "some-snap", + target: filepath.Join(dirs.SnapBlobDir, "some-snap_instance_11.snap"), + }}) + expected := fakeOps{ + { + op: "storesvc-snap-action", + userID: 1, + }, + { + op: "storesvc-snap-action:action", + action: store.SnapAction{ + Action: "install", + InstanceName: "some-snap_instance", + Channel: "some-channel", + }, + revno: snap.R(11), + userID: 1, + }, + { + op: "storesvc-download", + name: "some-snap", + }, + { + op: "validate-snap:Doing", + name: "some-snap_instance", + revno: snap.R(11), + }, + { + op: "current", + old: "<no-current>", + }, + { + op: "open-snap-file", + path: filepath.Join(dirs.SnapBlobDir, "some-snap_instance_11.snap"), + sinfo: snap.SideInfo{ + RealName: "some-snap", + SnapID: "some-snap-id", + Channel: "some-channel", + Revision: snap.R(11), + }, + }, + { + op: "setup-snap", + name: "some-snap_instance", + path: filepath.Join(dirs.SnapBlobDir, "some-snap_instance_11.snap"), + revno: snap.R(11), + }, + { + op: "copy-data", + path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/11"), + old: "<no-old>", + }, + { + op: "setup-profiles:Doing", + name: "some-snap_instance", + revno: snap.R(11), + }, + { + op: "candidate", + sinfo: snap.SideInfo{ + RealName: "some-snap", + SnapID: "some-snap-id", + Channel: "some-channel", + Revision: snap.R(11), + }, + }, + { + op: "link-snap", + path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/11"), + }, + { + op: "auto-connect:Doing", + name: "some-snap_instance", + revno: snap.R(11), + }, + { + op: "update-aliases", + }, + { + op: "cleanup-trash", + name: "some-snap_instance", + revno: snap.R(11), + }, + } + // 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) + + // check progress + ta := ts.Tasks() + task := ta[1] + _, cur, total := task.Progress() + c.Assert(cur, Equals, s.fakeStore.fakeCurrentProgress) + c.Assert(total, Equals, s.fakeStore.fakeTotalProgress) + c.Check(task.Summary(), Equals, `Download snap "some-snap_instance" (11) from channel "some-channel"`) + + // check link/start snap summary + linkTask := ta[len(ta)-7] + c.Check(linkTask.Summary(), Equals, `Make snap "some-snap_instance" (11) available to the system`) + startTask := ta[len(ta)-2] + c.Check(startTask.Summary(), Equals, `Start snap "some-snap_instance" (11) services`) + + // verify snap-setup in the task state + var snapsup snapstate.SnapSetup + err = task.Get("snap-setup", &snapsup) + c.Assert(err, IsNil) + c.Assert(snapsup, DeepEquals, snapstate.SnapSetup{ + Channel: "some-channel", + UserID: s.user.ID, + SnapPath: filepath.Join(dirs.SnapBlobDir, "some-snap_instance_11.snap"), + DownloadInfo: &snap.DownloadInfo{ + DownloadURL: "https://some-server.com/some/path.snap", + }, + SideInfo: snapsup.SideInfo, + Type: snap.TypeApp, + PlugsOnly: true, + InstanceKey: "instance", + }) + c.Assert(snapsup.SideInfo, DeepEquals, &snap.SideInfo{ + RealName: "some-snap", + Channel: "some-channel", + Revision: snap.R(11), + SnapID: "some-snap-id", + }) + + // verify snaps in the system state + var snaps map[string]*snapstate.SnapState + err = s.state.Get("snaps", &snaps) + c.Assert(err, IsNil) + + snapst := snaps["some-snap_instance"] + c.Assert(snapst, NotNil) + c.Assert(snapst.Active, Equals, true) + c.Assert(snapst.Channel, Equals, "some-channel") + c.Assert(snapst.Sequence[0], DeepEquals, &snap.SideInfo{ + RealName: "some-snap", + SnapID: "some-snap-id", + Channel: "some-channel", + Revision: snap.R(11), + }) + c.Assert(snapst.Required, Equals, false) + c.Assert(snapst.InstanceKey, Equals, "instance") +} + func (s *snapmgrTestSuite) TestInstallWithRevisionRunThrough(c *C) { s.state.Lock() defer s.state.Unlock() @@ -2046,6 +2218,7 @@ func (s *snapmgrTestSuite) TestInstallWithRevisionRunThrough(c *C) { c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{{ macaroon: s.user.StoreMacaroon, name: "some-snap", + target: filepath.Join(dirs.SnapBlobDir, "some-snap_42.snap"), }}) expected := fakeOps{ { @@ -2086,6 +2259,7 @@ func (s *snapmgrTestSuite) TestInstallWithRevisionRunThrough(c *C) { }, { op: "setup-snap", + name: "some-snap", path: filepath.Join(dirs.SnapBlobDir, "some-snap_42.snap"), revno: snap.R(42), }, @@ -2170,6 +2344,7 @@ func (s *snapmgrTestSuite) TestInstallWithRevisionRunThrough(c *C) { c.Assert(err, IsNil) snapst := snaps["some-snap"] + c.Assert(snapst, NotNil) c.Assert(snapst.Active, Equals, true) c.Assert(snapst.Channel, Equals, "some-channel") c.Assert(snapst.Sequence[0], DeepEquals, &snap.SideInfo{ @@ -2279,6 +2454,7 @@ func (s *snapmgrTestSuite) TestUpdateRunThrough(c *C) { }, { op: "setup-snap", + name: "services-snap", path: filepath.Join(dirs.SnapBlobDir, "services-snap_11.snap"), revno: snap.R(11), }, @@ -2340,6 +2516,7 @@ func (s *snapmgrTestSuite) TestUpdateRunThrough(c *C) { c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{{ macaroon: s.user.StoreMacaroon, name: "services-snap", + target: filepath.Join(dirs.SnapBlobDir, "services-snap_11.snap"), }}) // start with an easier-to-read error if this fails: c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) @@ -2403,6 +2580,223 @@ func (s *snapmgrTestSuite) TestUpdateRunThrough(c *C) { }) } +func (s *snapmgrTestSuite) TestParallelInstanceUpdateRunThrough(c *C) { + // use services-snap here to make sure services would be stopped/started appropriately + si := snap.SideInfo{ + RealName: "services-snap", + Revision: snap.R(7), + SnapID: "services-snap-id", + } + snaptest.MockSnapInstance(c, "services-snap_instance", `name: services-snap`, &si) + fi, err := os.Stat(snap.MountFile("services-snap_instance", si.Revision)) + c.Assert(err, IsNil) + refreshedDate := fi.ModTime() + // look at disk + r := snapstate.MockRevisionDate(nil) + defer r() + + s.state.Lock() + defer s.state.Unlock() + + tr := config.NewTransaction(s.state) + tr.Set("core", "experimental.parallel-instances", true) + tr.Commit() + + snapstate.Set(s.state, "services-snap_instance", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{&si}, + Current: si.Revision, + SnapType: "app", + Channel: "stable", + InstanceKey: "instance", + }) + + chg := s.state.NewChange("refresh", "refresh a snap") + ts, err := snapstate.Update(s.state, "services-snap_instance", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{}) + c.Assert(err, IsNil) + chg.AddAll(ts) + + s.state.Unlock() + s.settle(c) + s.state.Lock() + + expected := fakeOps{ + { + op: "storesvc-snap-action", + curSnaps: []store.CurrentSnap{{ + InstanceName: "services-snap_instance", + SnapID: "services-snap-id", + Revision: snap.R(7), + TrackingChannel: "stable", + RefreshedDate: refreshedDate, + }}, + userID: 1, + }, + { + op: "storesvc-snap-action:action", + action: store.SnapAction{ + Action: "refresh", + SnapID: "services-snap-id", + InstanceName: "services-snap_instance", + Channel: "some-channel", + Flags: store.SnapActionEnforceValidation, + }, + revno: snap.R(11), + userID: 1, + }, + { + op: "storesvc-download", + name: "services-snap", + }, + { + op: "validate-snap:Doing", + name: "services-snap_instance", + revno: snap.R(11), + }, + { + op: "current", + old: filepath.Join(dirs.SnapMountDir, "services-snap_instance/7"), + }, + { + op: "open-snap-file", + path: filepath.Join(dirs.SnapBlobDir, "services-snap_instance_11.snap"), + sinfo: snap.SideInfo{ + RealName: "services-snap", + SnapID: "services-snap-id", + Channel: "some-channel", + Revision: snap.R(11), + }, + }, + { + op: "setup-snap", + name: "services-snap_instance", + path: filepath.Join(dirs.SnapBlobDir, "services-snap_instance_11.snap"), + revno: snap.R(11), + }, + { + op: "stop-snap-services:refresh", + path: filepath.Join(dirs.SnapMountDir, "services-snap_instance/7"), + }, + { + op: "remove-snap-aliases", + name: "services-snap_instance", + }, + { + op: "unlink-snap", + path: filepath.Join(dirs.SnapMountDir, "services-snap_instance/7"), + }, + { + op: "copy-data", + path: filepath.Join(dirs.SnapMountDir, "services-snap_instance/11"), + old: filepath.Join(dirs.SnapMountDir, "services-snap_instance/7"), + }, + { + op: "setup-profiles:Doing", + name: "services-snap_instance", + revno: snap.R(11), + }, + { + op: "candidate", + sinfo: snap.SideInfo{ + RealName: "services-snap", + SnapID: "services-snap-id", + Channel: "some-channel", + Revision: snap.R(11), + }, + }, + { + op: "link-snap", + path: filepath.Join(dirs.SnapMountDir, "services-snap_instance/11"), + }, + { + op: "auto-connect:Doing", + name: "services-snap_instance", + revno: snap.R(11), + }, + { + op: "update-aliases", + }, + { + op: "start-snap-services", + path: filepath.Join(dirs.SnapMountDir, "services-snap_instance/11"), + }, + { + op: "cleanup-trash", + name: "services-snap_instance", + revno: snap.R(11), + }, + } + + // ensure all our tasks ran + c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{{ + macaroon: s.user.StoreMacaroon, + name: "services-snap", + target: filepath.Join(dirs.SnapBlobDir, "services-snap_instance_11.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) + + // check progress + task := ts.Tasks()[1] + _, cur, total := task.Progress() + c.Assert(cur, Equals, s.fakeStore.fakeCurrentProgress) + c.Assert(total, Equals, s.fakeStore.fakeTotalProgress) + + // verify snapSetup info + var snapsup snapstate.SnapSetup + err = task.Get("snap-setup", &snapsup) + c.Assert(err, IsNil) + c.Assert(snapsup, DeepEquals, snapstate.SnapSetup{ + Channel: "some-channel", + UserID: s.user.ID, + + SnapPath: filepath.Join(dirs.SnapBlobDir, "services-snap_instance_11.snap"), + DownloadInfo: &snap.DownloadInfo{ + DownloadURL: "https://some-server.com/some/path.snap", + }, + SideInfo: snapsup.SideInfo, + Type: snap.TypeApp, + PlugsOnly: true, + InstanceKey: "instance", + }) + c.Assert(snapsup.SideInfo, DeepEquals, &snap.SideInfo{ + RealName: "services-snap", + Revision: snap.R(11), + Channel: "some-channel", + SnapID: "services-snap-id", + }) + + // verify services stop reason + verifyStopReason(c, ts, "refresh") + + // check post-refresh hook + task = ts.Tasks()[14] + c.Assert(task.Kind(), Equals, "run-hook") + c.Assert(task.Summary(), Matches, `Run post-refresh hook of "services-snap_instance" snap if present`) + + // verify snaps in the system state + var snapst snapstate.SnapState + err = snapstate.Get(s.state, "services-snap_instance", &snapst) + c.Assert(err, IsNil) + + c.Assert(snapst.InstanceKey, Equals, "instance") + c.Assert(snapst.Active, Equals, true) + c.Assert(snapst.Sequence, HasLen, 2) + c.Assert(snapst.Sequence[0], DeepEquals, &snap.SideInfo{ + RealName: "services-snap", + SnapID: "services-snap-id", + Channel: "", + Revision: snap.R(7), + }) + c.Assert(snapst.Sequence[1], DeepEquals, &snap.SideInfo{ + RealName: "services-snap", + Channel: "some-channel", + SnapID: "services-snap-id", + Revision: snap.R(11), + }) +} + func (s *snapmgrTestSuite) TestUpdateWithNewBase(c *C) { si := &snap.SideInfo{ RealName: "some-snap", @@ -2433,8 +2827,8 @@ func (s *snapmgrTestSuite) TestUpdateWithNewBase(c *C) { s.state.Lock() c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{ - {macaroon: s.user.StoreMacaroon, name: "some-base"}, - {macaroon: s.user.StoreMacaroon, name: "some-snap"}, + {macaroon: s.user.StoreMacaroon, name: "some-base", target: filepath.Join(dirs.SnapBlobDir, "some-base_11.snap")}, + {macaroon: s.user.StoreMacaroon, name: "some-snap", target: filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap")}, }) } @@ -2479,7 +2873,7 @@ func (s *snapmgrTestSuite) TestUpdateWithAlreadyInstalledBase(c *C) { s.state.Lock() c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{ - {macaroon: s.user.StoreMacaroon, name: "some-snap"}, + {macaroon: s.user.StoreMacaroon, name: "some-snap", target: filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap")}, }) } @@ -2516,8 +2910,8 @@ func (s *snapmgrTestSuite) TestUpdateWithNewDefaultProvider(c *C) { s.state.Lock() c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{ - {macaroon: s.user.StoreMacaroon, name: "snap-content-plug"}, - {macaroon: s.user.StoreMacaroon, name: "snap-content-slot"}, + {macaroon: s.user.StoreMacaroon, name: "snap-content-plug", target: filepath.Join(dirs.SnapBlobDir, "snap-content-plug_11.snap")}, + {macaroon: s.user.StoreMacaroon, name: "snap-content-slot", target: filepath.Join(dirs.SnapBlobDir, "snap-content-slot_11.snap")}, }) } @@ -2565,7 +2959,7 @@ func (s *snapmgrTestSuite) TestUpdateWithInstalledDefaultProvider(c *C) { s.state.Lock() c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{ - {macaroon: s.user.StoreMacaroon, name: "snap-content-plug"}, + {macaroon: s.user.StoreMacaroon, name: "snap-content-plug", target: filepath.Join(dirs.SnapBlobDir, "snap-content-plug_11.snap")}, }) } @@ -2605,6 +2999,7 @@ func (s *snapmgrTestSuite) TestUpdateRememberedUserRunThrough(c *C) { c.Check(s.fakeStore.downloads[0], DeepEquals, fakeDownload{ macaroon: "macaroon", name: "some-snap", + target: filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap"), }, Commentf(snapName)) } } @@ -2646,6 +3041,7 @@ func (s *snapmgrTestSuite) TestUpdateToRevisionRememberedUserRunThrough(c *C) { c.Check(s.fakeStore.downloads[0], DeepEquals, fakeDownload{ macaroon: "macaroon", name: "some-snap", + target: filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap"), }, Commentf(snapName)) } } @@ -2722,7 +3118,11 @@ func (s *snapmgrTestSuite) TestUpdateManyMultipleCredsNoUserRunThrough(c *C) { seen[snapID] = op.userID case "storesvc-download": snapName := op.name - c.Check(s.fakeStore.downloads[di], DeepEquals, fakeDownload{ + fakeDl := s.fakeStore.downloads[di] + // check target path separately and clear it + c.Check(fakeDl.target, Matches, filepath.Join(dirs.SnapBlobDir, fmt.Sprintf("%s_[0-9]+.snap", snapName))) + fakeDl.target = "" + c.Check(fakeDl, DeepEquals, fakeDownload{ macaroon: macaroonMap[snapName], name: snapName, }, Commentf(snapName)) @@ -2812,7 +3212,11 @@ func (s *snapmgrTestSuite) TestUpdateManyMultipleCredsUserRunThrough(c *C) { seen[snapIDuserID{snapID: snapID, userID: op.userID}] = true case "storesvc-download": snapName := op.name - c.Check(s.fakeStore.downloads[di], DeepEquals, fakeDownload{ + fakeDl := s.fakeStore.downloads[di] + // check target path separately and clear it + c.Check(fakeDl.target, Matches, filepath.Join(dirs.SnapBlobDir, fmt.Sprintf("%s_[0-9]+.snap", snapName))) + fakeDl.target = "" + c.Check(fakeDl, DeepEquals, fakeDownload{ macaroon: macaroonMap[snapName], name: snapName, }, Commentf(snapName)) @@ -2913,7 +3317,11 @@ func (s *snapmgrTestSuite) TestUpdateManyMultipleCredsUserWithNoStoreAuthRunThro seen[snapID] = op.userID case "storesvc-download": snapName := op.name - c.Check(s.fakeStore.downloads[di], DeepEquals, fakeDownload{ + fakeDl := s.fakeStore.downloads[di] + // check target path separately and clear it + c.Check(fakeDl.target, Matches, filepath.Join(dirs.SnapBlobDir, fmt.Sprintf("%s_[0-9]+.snap", snapName))) + fakeDl.target = "" + c.Check(fakeDl, DeepEquals, fakeDownload{ macaroon: macaroonMap[snapName], name: snapName, }, Commentf(snapName)) @@ -3005,6 +3413,7 @@ func (s *snapmgrTestSuite) TestUpdateUndoRunThrough(c *C) { }, { op: "setup-snap", + name: "some-snap", path: filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap"), revno: snap.R(11), }, @@ -3062,6 +3471,7 @@ func (s *snapmgrTestSuite) TestUpdateUndoRunThrough(c *C) { }, { op: "undo-setup-snap", + name: "some-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/11"), stype: "app", }, @@ -3071,6 +3481,7 @@ func (s *snapmgrTestSuite) TestUpdateUndoRunThrough(c *C) { c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{{ macaroon: s.user.StoreMacaroon, name: "some-snap", + target: filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap"), }}) // start with an easier-to-read error if this fails: c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) @@ -3178,6 +3589,7 @@ func (s *snapmgrTestSuite) TestUpdateTotalUndoRunThrough(c *C) { }, { op: "setup-snap", + name: "some-snap", path: filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap"), revno: snap.R(11), }, @@ -3248,6 +3660,7 @@ func (s *snapmgrTestSuite) TestUpdateTotalUndoRunThrough(c *C) { }, { op: "undo-setup-snap", + name: "some-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/11"), stype: "app", }, @@ -3257,6 +3670,7 @@ func (s *snapmgrTestSuite) TestUpdateTotalUndoRunThrough(c *C) { c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{{ macaroon: s.user.StoreMacaroon, name: "some-snap", + target: filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap"), }}) // friendlier failure first c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) @@ -4455,6 +4869,7 @@ version: 1.0`) { // and setup-snap op: "setup-snap", + name: "mock", path: mockSnap, revno: snap.R("x1"), }, @@ -4562,6 +4977,7 @@ version: 1.0`) c.Check(ops[0].old, Equals, filepath.Join(dirs.SnapMountDir, "mock/x2")) // and setup-snap c.Check(ops[1].op, Equals, "setup-snap") + c.Check(ops[1].name, Matches, "mock") c.Check(ops[1].path, Matches, `.*/mock_1.0_all.snap`) c.Check(ops[1].revno, Equals, snap.R("x3")) // and cleanup @@ -4658,6 +5074,7 @@ version: 1.0`) { // and setup-snap op: "setup-snap", + name: "mock", path: mockSnap, revno: snap.R("x1"), }, @@ -4753,6 +5170,7 @@ version: 1.0`) c.Check(s.fakeBackend.ops[0].old, Equals, "<no-current>") // and setup-snap c.Check(s.fakeBackend.ops[1].op, Equals, "setup-snap") + c.Check(s.fakeBackend.ops[1].name, Equals, "some-snap") c.Check(s.fakeBackend.ops[1].path, Matches, `.*/orig-name_1.0_all.snap`) c.Check(s.fakeBackend.ops[1].revno, Equals, snap.R(42)) @@ -4817,6 +5235,11 @@ func (s *snapmgrTestSuite) TestRemoveRunThrough(c *C) { expected := fakeOps{ { + op: "auto-disconnect:Doing", + name: "some-snap", + revno: snap.R(7), + }, + { op: "remove-snap-aliases", name: "some-snap", }, @@ -4846,10 +5269,6 @@ func (s *snapmgrTestSuite) TestRemoveRunThrough(c *C) { op: "discard-namespace", name: "some-snap", }, - { - op: "discard-conns:Doing", - name: "some-snap", - }, } // start with an easier-to-read error if this fails: c.Check(len(s.fakeBackend.ops), Equals, len(expected)) @@ -4901,6 +5320,133 @@ func (s *snapmgrTestSuite) TestRemoveRunThrough(c *C) { c.Assert(err, Equals, state.ErrNoState) } +func (s *snapmgrTestSuite) TestParallelInstanceRemoveRunThrough(c *C) { + si := snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(7), + } + + s.state.Lock() + defer s.state.Unlock() + + // pretend we have both a regular snap and a parallel instance + snapstate.Set(s.state, "some-snap_instance", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{&si}, + Current: si.Revision, + SnapType: "app", + InstanceKey: "instance", + }) + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{&si}, + Current: si.Revision, + SnapType: "app", + }) + + chg := s.state.NewChange("remove", "remove a snap") + ts, err := snapstate.Remove(s.state, "some-snap_instance", snap.R(0)) + c.Assert(err, IsNil) + chg.AddAll(ts) + + s.state.Unlock() + s.settle(c) + s.state.Lock() + + expected := fakeOps{ + { + op: "auto-disconnect:Doing", + name: "some-snap_instance", + revno: snap.R(7), + }, + { + op: "remove-snap-aliases", + name: "some-snap_instance", + }, + { + op: "unlink-snap", + path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/7"), + }, + { + op: "remove-profiles:Doing", + name: "some-snap_instance", + revno: snap.R(7), + }, + { + op: "remove-snap-data", + path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/7"), + }, + { + op: "remove-snap-common-data", + path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/7"), + }, + { + op: "remove-snap-files", + path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/7"), + stype: "app", + }, + { + op: "discard-namespace", + name: "some-snap_instance", + }, + } + // start with an easier-to-read error if this fails: + c.Check(len(s.fakeBackend.ops), Equals, len(expected)) + c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) + c.Check(s.fakeBackend.ops, DeepEquals, expected) + + // verify snapSetup info + tasks := ts.Tasks() + for _, t := range tasks { + if t.Kind() == "run-hook" { + continue + } + snapsup, err := snapstate.TaskSnapSetup(t) + c.Assert(err, IsNil) + + var expSnapSetup *snapstate.SnapSetup + switch t.Kind() { + case "discard-conns": + expSnapSetup = &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{ + RealName: "some-snap", + }, + InstanceKey: "instance", + } + case "clear-snap", "discard-snap": + expSnapSetup = &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(7), + }, + InstanceKey: "instance", + } + default: + expSnapSetup = &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(7), + }, + Type: snap.TypeApp, + PlugsOnly: true, + InstanceKey: "instance", + } + + } + + c.Check(snapsup, DeepEquals, expSnapSetup, Commentf(t.Kind())) + } + + // verify snaps in the system state + var snapst snapstate.SnapState + err = snapstate.Get(s.state, "some-snap_instance", &snapst) + c.Assert(err, Equals, state.ErrNoState) + + // the non-instance snap is still there + err = snapstate.Get(s.state, "some-snap", &snapst) + c.Assert(err, IsNil) +} + func (s *snapmgrTestSuite) TestRemoveWithManyRevisionsRunThrough(c *C) { si3 := snap.SideInfo{ RealName: "some-snap", @@ -4939,6 +5485,11 @@ func (s *snapmgrTestSuite) TestRemoveWithManyRevisionsRunThrough(c *C) { expected := fakeOps{ { + op: "auto-disconnect:Doing", + name: "some-snap", + revno: snap.R(7), + }, + { op: "remove-snap-aliases", name: "some-snap", }, @@ -4986,10 +5537,6 @@ func (s *snapmgrTestSuite) TestRemoveWithManyRevisionsRunThrough(c *C) { op: "discard-namespace", name: "some-snap", }, - { - op: "discard-conns:Doing", - name: "some-snap", - }, } // start with an easier-to-read error if this fails: c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) @@ -5150,6 +5697,11 @@ func (s *snapmgrTestSuite) TestRemoveLastRevisionRunThrough(c *C) { c.Check(len(s.fakeBackend.ops), Equals, 5) expected := fakeOps{ { + op: "auto-disconnect:Doing", + name: "some-snap", + revno: snap.R(2), + }, + { op: "remove-snap-data", path: filepath.Join(dirs.SnapMountDir, "some-snap/2"), }, @@ -5166,10 +5718,6 @@ func (s *snapmgrTestSuite) TestRemoveLastRevisionRunThrough(c *C) { op: "discard-namespace", name: "some-snap", }, - { - op: "discard-conns:Doing", - name: "some-snap", - }, } // start with an easier-to-read error if this fails: c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) @@ -5192,6 +5740,10 @@ func (s *snapmgrTestSuite) TestRemoveLastRevisionRunThrough(c *C) { if t.Kind() != "discard-conns" { expSnapSetup.SideInfo.Revision = snap.R(2) } + if t.Kind() == "auto-disconnect" { + expSnapSetup.PlugsOnly = true + expSnapSetup.Type = "app" + } c.Check(snapsup, DeepEquals, expSnapSetup, Commentf(t.Kind())) } @@ -5762,6 +6314,112 @@ func (s *snapmgrTestSuite) TestRevertRunThrough(c *C) { c.Assert(snapst.Block(), DeepEquals, []snap.Revision{snap.R(7)}) } +func (s *snapmgrTestSuite) TestParallelInstanceRevertRunThrough(c *C) { + si := snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(7), + } + siOld := snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(2), + } + + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "some-snap_instance", &snapstate.SnapState{ + Active: true, + SnapType: "app", + Sequence: []*snap.SideInfo{&siOld, &si}, + Current: si.Revision, + InstanceKey: "instance", + }) + + // another snap withouth instance key + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + SnapType: "app", + Sequence: []*snap.SideInfo{&siOld, &si}, + Current: si.Revision, + }) + + chg := s.state.NewChange("revert", "revert a snap backwards") + ts, err := snapstate.Revert(s.state, "some-snap_instance", snapstate.Flags{}) + c.Assert(err, IsNil) + chg.AddAll(ts) + + s.state.Unlock() + defer s.se.Stop() + s.settle(c) + s.state.Lock() + + expected := fakeOps{ + { + op: "remove-snap-aliases", + name: "some-snap_instance", + }, + { + op: "unlink-snap", + path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/7"), + }, + { + op: "setup-profiles:Doing", + name: "some-snap_instance", + revno: snap.R(2), + }, + { + op: "candidate", + sinfo: snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(2), + }, + }, + { + op: "link-snap", + path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/2"), + }, + { + op: "auto-connect:Doing", + name: "some-snap_instance", + revno: snap.R(2), + }, + { + 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) + + // verify that the R(2) version is active now and R(7) is still there + var snapst snapstate.SnapState + err = snapstate.Get(s.state, "some-snap_instance", &snapst) + c.Assert(err, IsNil) + + c.Assert(snapst.Active, Equals, true) + c.Assert(snapst.Current, Equals, snap.R(2)) + c.Assert(snapst.InstanceKey, Equals, "instance") + c.Assert(snapst.Sequence, HasLen, 2) + c.Assert(snapst.Sequence[0], DeepEquals, &snap.SideInfo{ + RealName: "some-snap", + Channel: "", + Revision: snap.R(2), + }) + c.Assert(snapst.Sequence[1], DeepEquals, &snap.SideInfo{ + RealName: "some-snap", + Channel: "", + Revision: snap.R(7), + }) + c.Assert(snapst.Block(), DeepEquals, []snap.Revision{snap.R(7)}) + + // non instance snap is not affected + var nonInstanceSnapst snapstate.SnapState + err = snapstate.Get(s.state, "some-snap", &nonInstanceSnapst) + c.Assert(err, IsNil) + c.Assert(nonInstanceSnapst.Current, Equals, snap.R(7)) + +} + func (s *snapmgrTestSuite) TestRevertWithLocalRevisionRunThrough(c *C) { si := snap.SideInfo{ RealName: "some-snap", @@ -6257,6 +6915,167 @@ func (s *snapmgrTestSuite) TestDisableRunThrough(c *C) { }) } +func (s *snapmgrTestSuite) TestParallelInstanceEnableRunThrough(c *C) { + si := snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(7), + Channel: "edge", + SnapID: "foo", + } + + s.state.Lock() + defer s.state.Unlock() + + flags := snapstate.Flags{ + DevMode: true, + JailMode: true, + Classic: true, + TryMode: true, + Required: true, + } + snapstate.Set(s.state, "some-snap_instance", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{&si}, + Current: si.Revision, + Active: false, + Channel: "edge", + Flags: flags, + AliasesPending: true, + AutoAliasesDisabled: true, + InstanceKey: "instance", + }) + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{&si}, + Current: si.Revision, + Active: false, + Channel: "edge", + Flags: flags, + AliasesPending: true, + AutoAliasesDisabled: true, + }) + + chg := s.state.NewChange("enable", "enable a snap") + ts, err := snapstate.Enable(s.state, "some-snap_instance") + c.Assert(err, IsNil) + chg.AddAll(ts) + + s.state.Unlock() + s.settle(c) + s.state.Lock() + + expected := fakeOps{ + { + op: "setup-profiles:Doing", + name: "some-snap_instance", + revno: snap.R(7), + }, + { + op: "candidate", + sinfo: si, + }, + { + op: "link-snap", + path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/7"), + }, + { + op: "auto-connect:Doing", + name: "some-snap_instance", + revno: snap.R(7), + }, + { + 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, "some-snap_instance", &snapst) + c.Assert(err, IsNil) + c.Check(snapst.Flags, DeepEquals, flags) + + c.Assert(snapst.InstanceKey, Equals, "instance") + c.Assert(snapst.Active, Equals, true) + c.Assert(snapst.AliasesPending, Equals, false) + c.Assert(snapst.AutoAliasesDisabled, Equals, true) + + info, err := snapst.CurrentInfo() + c.Assert(err, IsNil) + c.Assert(info.Channel, Equals, "edge") + c.Assert(info.SnapID, Equals, "foo") + + // the non-parallel instance is still disabled + snapst = snapstate.SnapState{} + err = snapstate.Get(s.state, "some-snap", &snapst) + c.Assert(err, IsNil) + c.Assert(snapst.InstanceKey, Equals, "") + c.Assert(snapst.Active, Equals, false) +} + +func (s *snapmgrTestSuite) TestParallelInstanceDisableRunThrough(c *C) { + si := snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(7), + } + + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{&si}, + Current: si.Revision, + Active: true, + }) + snapstate.Set(s.state, "some-snap_instance", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{&si}, + Current: si.Revision, + Active: true, + InstanceKey: "instance", + }) + + chg := s.state.NewChange("disable", "disable a snap") + ts, err := snapstate.Disable(s.state, "some-snap_instance") + c.Assert(err, IsNil) + chg.AddAll(ts) + + s.state.Unlock() + s.settle(c) + s.state.Lock() + + expected := fakeOps{ + { + op: "remove-snap-aliases", + name: "some-snap_instance", + }, + { + op: "unlink-snap", + path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/7"), + }, + { + op: "remove-profiles:Doing", + name: "some-snap_instance", + revno: snap.R(7), + }, + } + // 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, "some-snap_instance", &snapst) + c.Assert(err, IsNil) + c.Assert(snapst.InstanceKey, Equals, "instance") + c.Assert(snapst.Active, Equals, false) + c.Assert(snapst.AliasesPending, Equals, true) + + // the non-parallel instance is still active + snapst = snapstate.SnapState{} + err = snapstate.Get(s.state, "some-snap", &snapst) + c.Assert(err, IsNil) + c.Assert(snapst.InstanceKey, Equals, "") + c.Assert(snapst.Active, Equals, true) +} + func (s *snapmgrTestSuite) TestSwitchRunThrough(c *C) { si := snap.SideInfo{ RealName: "some-snap", @@ -6299,6 +7118,61 @@ func (s *snapmgrTestSuite) TestSwitchRunThrough(c *C) { c.Assert(info.Channel, Equals, "edge") } +func (s *snapmgrTestSuite) TestParallelInstallSwitchRunThrough(c *C) { + si := snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(7), + Channel: "edge", + SnapID: "foo", + } + + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{&si}, + Current: si.Revision, + Channel: "edge", + }) + + snapstate.Set(s.state, "some-snap_instance", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{&si}, + Current: si.Revision, + Channel: "edge", + InstanceKey: "instance", + }) + + chg := s.state.NewChange("switch-snap", "switch snap to some-channel") + ts, err := snapstate.Switch(s.state, "some-snap_instance", "some-channel") + c.Assert(err, IsNil) + chg.AddAll(ts) + + s.state.Unlock() + defer s.se.Stop() + s.settle(c) + s.state.Lock() + + // switch is not really really doing anything backend related + c.Assert(s.fakeBackend.ops, HasLen, 0) + + // ensure the desired channel has changed + var snapst snapstate.SnapState + err = snapstate.Get(s.state, "some-snap_instance", &snapst) + c.Assert(err, IsNil) + c.Assert(snapst.Channel, Equals, "some-channel") + + // ensure the current info has not changed + info, err := snapst.CurrentInfo() + c.Assert(err, IsNil) + c.Assert(info.Channel, Equals, "edge") + + // Ensure that the non-intance snap is unchanged + var nonInstanceSnapst snapstate.SnapState + err = snapstate.Get(s.state, "some-snap", &nonInstanceSnapst) + c.Assert(err, IsNil) + c.Assert(nonInstanceSnapst.Channel, Equals, "edge") +} + func (s *snapmgrTestSuite) TestDisableDoesNotEnableAgain(c *C) { si := snap.SideInfo{ RealName: "some-snap", @@ -6375,6 +7249,7 @@ func (s *snapmgrTestSuite) TestUndoMountSnapFailsInCopyData(c *C) { }, { op: "setup-snap", + name: "some-snap", path: filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap"), revno: snap.R(11), }, @@ -6385,6 +7260,7 @@ func (s *snapmgrTestSuite) TestUndoMountSnapFailsInCopyData(c *C) { }, { op: "undo-setup-snap", + name: "some-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/11"), stype: "app", }, @@ -8390,13 +9266,13 @@ func (s *snapmgrTestSuite) TestRemoveMany(c *C) { for i, ts := range tts { c.Assert(taskKinds(ts.Tasks()), DeepEquals, []string{ "stop-snap-services", + "auto-disconnect", "run-hook[remove]", "remove-aliases", "unlink-snap", "remove-profiles", "clear-snap", "discard-snap", - "discard-conns", }) verifyStopReason(c, ts, "remove") // check that tasksets are in separate lanes @@ -8798,6 +9674,7 @@ func (s *snapmgrTestSuite) TestTransitionCoreRunThrough(c *C) { name: "core", // the transition has no user associcated with it macaroon: "", + target: filepath.Join(dirs.SnapBlobDir, "core_11.snap"), }}) expected := fakeOps{ { @@ -8840,6 +9717,7 @@ func (s *snapmgrTestSuite) TestTransitionCoreRunThrough(c *C) { }, { op: "setup-snap", + name: "core", path: filepath.Join(dirs.SnapBlobDir, "core_11.snap"), revno: snap.R(11), }, @@ -8879,6 +9757,11 @@ func (s *snapmgrTestSuite) TestTransitionCoreRunThrough(c *C) { name: "ubuntu-core", }, { + op: "auto-disconnect:Doing", + name: "ubuntu-core", + revno: snap.R(1), + }, + { op: "remove-snap-aliases", name: "ubuntu-core", }, @@ -8909,10 +9792,6 @@ func (s *snapmgrTestSuite) TestTransitionCoreRunThrough(c *C) { name: "ubuntu-core", }, { - op: "discard-conns:Doing", - name: "ubuntu-core", - }, - { op: "cleanup-trash", name: "core", revno: snap.R(11), @@ -8964,6 +9843,11 @@ func (s *snapmgrTestSuite) TestTransitionCoreRunThroughWithCore(c *C) { name: "ubuntu-core", }, { + op: "auto-disconnect:Doing", + name: "ubuntu-core", + revno: snap.R(1), + }, + { op: "remove-snap-aliases", name: "ubuntu-core", }, @@ -8993,10 +9877,6 @@ func (s *snapmgrTestSuite) TestTransitionCoreRunThroughWithCore(c *C) { op: "discard-namespace", name: "ubuntu-core", }, - { - op: "discard-conns:Doing", - name: "ubuntu-core", - }, } // start with an easier-to-read error if this fails: c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) @@ -9427,10 +10307,12 @@ func (s *snapmgrTestSuite) TestInstallWithoutCoreRunThrough1(c *C) { { macaroon: s.user.StoreMacaroon, name: "core", + target: filepath.Join(dirs.SnapBlobDir, "core_11.snap"), }, { macaroon: s.user.StoreMacaroon, name: "some-snap", + target: filepath.Join(dirs.SnapBlobDir, "some-snap_42.snap"), }}) expected := fakeOps{ // we check the snap @@ -9489,6 +10371,7 @@ func (s *snapmgrTestSuite) TestInstallWithoutCoreRunThrough1(c *C) { }, { op: "setup-snap", + name: "core", path: filepath.Join(dirs.SnapBlobDir, "core_11.snap"), revno: snap.R(11), }, @@ -9548,6 +10431,7 @@ func (s *snapmgrTestSuite) TestInstallWithoutCoreRunThrough1(c *C) { }, { op: "setup-snap", + name: "some-snap", path: filepath.Join(dirs.SnapBlobDir, "some-snap_42.snap"), revno: snap.R(42), }, @@ -9606,6 +10490,7 @@ func (s *snapmgrTestSuite) TestInstallWithoutCoreRunThrough1(c *C) { c.Assert(err, IsNil) snapst := snaps["core"] + c.Assert(snapst, NotNil) c.Assert(snapst.Active, Equals, true) c.Assert(snapst.Channel, Equals, "stable") c.Assert(snapst.Sequence[0], DeepEquals, &snap.SideInfo{ @@ -9853,6 +10738,7 @@ func (s *snapmgrTestSuite) TestInstallWithoutCoreConflictingInstall(c *C) { c.Assert(err, IsNil) snapst := snaps["core"] + c.Assert(snapst, NotNil) c.Assert(snapst.Active, Equals, true) c.Assert(snapst.Sequence[0], DeepEquals, &snap.SideInfo{ RealName: "core", @@ -9860,6 +10746,7 @@ func (s *snapmgrTestSuite) TestInstallWithoutCoreConflictingInstall(c *C) { }) snapst = snaps["some-snap"] + c.Assert(snapst, NotNil) c.Assert(snapst.Active, Equals, true) c.Assert(snapst.Sequence[0], DeepEquals, &snap.SideInfo{ RealName: "some-snap", @@ -10032,6 +10919,7 @@ func (s *snapmgrTestSuite) TestInstallDefaultProviderRunThrough(c *C) { }, }, { op: "setup-snap", + name: "snap-content-slot", path: filepath.Join(dirs.SnapBlobDir, "snap-content-slot_11.snap"), revno: snap.R(11), }, { @@ -10079,6 +10967,7 @@ func (s *snapmgrTestSuite) TestInstallDefaultProviderRunThrough(c *C) { }, }, { op: "setup-snap", + name: "snap-content-plug", path: filepath.Join(dirs.SnapBlobDir, "snap-content-plug_42.snap"), revno: snap.R(42), }, { @@ -10378,8 +11267,27 @@ func (s *snapmgrTestSuite) TestInstallLayoutsChecksFeatureFlag(c *C) { _, err := snapstate.Install(s.state, "some-snap", "channel-for-layout", snap.R(0), s.user.ID, snapstate.Flags{}) c.Assert(err, ErrorMatches, "experimental feature disabled - test it by setting 'experimental.layouts' to true") - // enable layouts + // check various forms of disabling tr := config.NewTransaction(s.state) + tr.Set("core", "experimental.layouts", false) + tr.Commit() + _, err = snapstate.Install(s.state, "some-snap", "channel-for-layout", snap.R(0), s.user.ID, snapstate.Flags{}) + c.Assert(err, ErrorMatches, "experimental feature disabled - test it by setting 'experimental.layouts' to true") + + tr = config.NewTransaction(s.state) + tr.Set("core", "experimental.layouts", "") + tr.Commit() + _, err = snapstate.Install(s.state, "some-snap", "channel-for-layout", snap.R(0), s.user.ID, snapstate.Flags{}) + c.Assert(err, ErrorMatches, "experimental feature disabled - test it by setting 'experimental.layouts' to true") + + tr = config.NewTransaction(s.state) + tr.Set("core", "experimental.layouts", nil) + tr.Commit() + _, err = snapstate.Install(s.state, "some-snap", "channel-for-layout", snap.R(0), s.user.ID, snapstate.Flags{}) + c.Assert(err, ErrorMatches, "experimental feature disabled - test it by setting 'experimental.layouts' to true") + + // enable layouts + tr = config.NewTransaction(s.state) tr.Set("core", "experimental.layouts", true) tr.Commit() @@ -10477,8 +11385,37 @@ func (s *snapmgrTestSuite) TestParallelInstallValidateFeatureFlag(c *C) { err := snapstate.ValidateFeatureFlags(s.state, info) c.Assert(err, ErrorMatches, `experimental feature disabled - test it by setting 'experimental.parallel-instances' to true`) - // enable parallel instances + // various forms of disabling tr := config.NewTransaction(s.state) + tr.Set("core", "experimental.parallel-instances", false) + tr.Commit() + + err = snapstate.ValidateFeatureFlags(s.state, info) + c.Assert(err, ErrorMatches, `experimental feature disabled - test it by setting 'experimental.parallel-instances' to true`) + + tr = config.NewTransaction(s.state) + tr.Set("core", "experimental.parallel-instances", "") + tr.Commit() + + err = snapstate.ValidateFeatureFlags(s.state, info) + c.Assert(err, ErrorMatches, `experimental feature disabled - test it by setting 'experimental.parallel-instances' to true`) + + tr = config.NewTransaction(s.state) + tr.Set("core", "experimental.parallel-instances", nil) + tr.Commit() + + err = snapstate.ValidateFeatureFlags(s.state, info) + c.Assert(err, ErrorMatches, `experimental feature disabled - test it by setting 'experimental.parallel-instances' to true`) + + tr = config.NewTransaction(s.state) + tr.Set("core", "experimental.parallel-instances", "veryfalse") + tr.Commit() + + err = snapstate.ValidateFeatureFlags(s.state, info) + c.Assert(err, ErrorMatches, `internal error: feature flag experimental.parallel-instances has unexpected value "veryfalse" \(string\)`) + + // enable parallel instances + tr = config.NewTransaction(s.state) tr.Set("core", "experimental.parallel-instances", true) tr.Commit() diff --git a/packaging/opensuse/snapd.spec b/packaging/opensuse/snapd.spec index bf85db3ef1..d0609ac3c9 100644 --- a/packaging/opensuse/snapd.spec +++ b/packaging/opensuse/snapd.spec @@ -194,13 +194,11 @@ go install -s -v -p 4 -x -tags withtestkeys github.com/snapcore/snapd/cmd/snapd %gobuild cmd/snap %gobuild cmd/snapctl # build snap-exec and snap-update-ns completely static for base snaps -CGO_ENABLED=0 %gobuild cmd/snap-exec -# gobuild --ldflags '-extldflags "-static"' bin/snap-update-ns -# FIXME: ^ this doesn't work yet, it's going to be fixed with another PR. -%gobuild cmd/snap-update-ns - -# This is ok because snap-seccomp only requires static linking when it runs from the core-snap via re-exec. -sed -e "s/-Bstatic -lseccomp/-Bstatic/g" -i %{_builddir}/go/src/%{provider_prefix}/cmd/snap-seccomp/main.go +# NOTE: openSUSE's golang rpm helpers pass -buildmode=pie, but glibc is not +# built with -fPIC so it'll blow up during linking, need to pass +# -buildmode=default to override this +%gobuild -buildmode=default -ldflags=-extldflags=-static cmd/snap-exec +%gobuild -buildmode=default -ldflags=-extldflags=-static cmd/snap-update-ns # build snap-seccomp %gobuild cmd/snap-seccomp diff --git a/snap/hooktypes.go b/snap/hooktypes.go index 280c0d1ec1..97b59825a7 100644 --- a/snap/hooktypes.go +++ b/snap/hooktypes.go @@ -31,7 +31,9 @@ var supportedHooks = []*HookType{ NewHookType(regexp.MustCompile("^post-refresh$")), NewHookType(regexp.MustCompile("^remove$")), NewHookType(regexp.MustCompile("^prepare-(?:plug|slot)-[-a-z0-9]+$")), + NewHookType(regexp.MustCompile("^unprepare-(?:plug|slot)-[-a-z0-9]+$")), NewHookType(regexp.MustCompile("^connect-(?:plug|slot)-[-a-z0-9]+$")), + NewHookType(regexp.MustCompile("^disconnect-(?:plug|slot)-[-a-z0-9]+$")), } // HookType represents a pattern of supported hook names. diff --git a/snap/info.go b/snap/info.go index e7ce222eac..a7d713e09f 100644 --- a/snap/info.go +++ b/snap/info.go @@ -685,6 +685,7 @@ type AppInfo struct { Name string LegacyAliases []string // FIXME: eventually drop this Command string + CommandChain []string CommonID string Daemon string diff --git a/snap/info_snap_yaml.go b/snap/info_snap_yaml.go index 005d6bd72d..1a32e47306 100644 --- a/snap/info_snap_yaml.go +++ b/snap/info_snap_yaml.go @@ -58,7 +58,8 @@ type snapYaml struct { type appYaml struct { Aliases []string `yaml:"aliases,omitempty"` - Command string `yaml:"command"` + Command string `yaml:"command"` + CommandChain []string `yaml:"command-chain,omitempty"` Daemon string `yaml:"daemon"` @@ -294,6 +295,7 @@ func setAppsFromSnapYaml(y snapYaml, snap *Info) error { Name: appName, LegacyAliases: yApp.Aliases, Command: yApp.Command, + CommandChain: yApp.CommandChain, Daemon: yApp.Daemon, StopTimeout: yApp.StopTimeout, StopCommand: yApp.StopCommand, diff --git a/snap/info_snap_yaml_test.go b/snap/info_snap_yaml_test.go index 3a57fdc4cf..fdd5433bb0 100644 --- a/snap/info_snap_yaml_test.go +++ b/snap/info_snap_yaml_test.go @@ -1751,3 +1751,17 @@ apps: Equals, true) } + +func (s *YamlSuite) TestSnapYamlCommandChain(c *C) { + yAutostart := []byte(`name: wat +version: 42 +apps: + foo: + command: bin/foo + command-chain: [chain1, chain2] +`) + info, err := snap.InfoFromSnapYaml(yAutostart) + c.Assert(err, IsNil) + app := info.Apps["foo"] + c.Check(app.CommandChain, DeepEquals, []string{"chain1", "chain2"}) +} diff --git a/snap/info_test.go b/snap/info_test.go index e5c26a7d3d..b03362c2e6 100644 --- a/snap/info_test.go +++ b/snap/info_test.go @@ -185,6 +185,7 @@ apps: command: bar sample: command: foobar + command-chain: [chain] ` func (s *infoSuite) TestReadInfo(c *C) { @@ -200,6 +201,7 @@ func (s *infoSuite) TestReadInfo(c *C) { c.Check(snapInfo2.Summary(), Equals, "esummary") c.Check(snapInfo2.Apps["app"].Command, Equals, "foo") + c.Check(snapInfo2.Apps["sample"].CommandChain, DeepEquals, []string{"chain"}) c.Check(snapInfo2, DeepEquals, snapInfo1) } @@ -218,6 +220,7 @@ func (s *infoSuite) TestReadInfoWithInstance(c *C) { c.Check(snapInfo2.Summary(), Equals, "instance summary") c.Check(snapInfo2.Apps["app"].Command, Equals, "foo") + c.Check(snapInfo2.Apps["sample"].CommandChain, DeepEquals, []string{"chain"}) c.Check(snapInfo2, DeepEquals, snapInfo1) } diff --git a/snap/validate.go b/snap/validate.go index 90544dc62b..06eddd29de 100644 --- a/snap/validate.go +++ b/snap/validate.go @@ -584,8 +584,10 @@ func validateAppTimer(app *AppInfo) error { // appContentWhitelist is the whitelist of legal chars in the "apps" // section of snap.yaml. Do not allow any of [',",`] here or snap-exec -// will get confused. +// will get confused. chainContentWhitelist is the same, but for the +// command-chain, which also doesn't allow whitespace. var appContentWhitelist = regexp.MustCompile(`^[A-Za-z0-9/. _#:$-]*$`) +var commandChainContentWhitelist = regexp.MustCompile(`^[A-Za-z0-9/._#:$-]*$`) // ValidAppName tells whether a string is a valid application name. func ValidAppName(n string) bool { @@ -623,6 +625,13 @@ func ValidateApp(app *AppInfo) error { } } + // Also validate the command chain + for _, value := range app.CommandChain { + if err := validateField("command-chain", value, commandChainContentWhitelist); err != nil { + return err + } + } + // Socket activation requires the "network-bind" plug if len(app.Sockets) > 0 { if _, ok := app.Plugs["network-bind"]; !ok { diff --git a/snap/validate_test.go b/snap/validate_test.go index 9c1e7daf98..944887b38a 100644 --- a/snap/validate_test.go +++ b/snap/validate_test.go @@ -439,10 +439,13 @@ func (s *ValidateSuite) TestAppWhitelistWithVars(c *C) { func (s *ValidateSuite) TestAppWhitelistIllegal(c *C) { c.Check(ValidateApp(&AppInfo{Name: "x\n"}), NotNil) c.Check(ValidateApp(&AppInfo{Name: "test!me"}), NotNil) + c.Check(ValidateApp(&AppInfo{Name: "test'me"}), NotNil) c.Check(ValidateApp(&AppInfo{Name: "foo", Command: "foo\n"}), NotNil) c.Check(ValidateApp(&AppInfo{Name: "foo", StopCommand: "foo\n"}), NotNil) c.Check(ValidateApp(&AppInfo{Name: "foo", PostStopCommand: "foo\n"}), NotNil) c.Check(ValidateApp(&AppInfo{Name: "foo", BusName: "foo\n"}), NotNil) + c.Check(ValidateApp(&AppInfo{Name: "foo", CommandChain: []string{"bar'baz"}}), NotNil) + c.Check(ValidateApp(&AppInfo{Name: "foo", CommandChain: []string{"bar baz"}}), NotNil) } func (s *ValidateSuite) TestAppDaemonValue(c *C) { diff --git a/snapcraft.yaml b/snapcraft.yaml index 0d76744c17..9de7386a87 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -9,8 +9,7 @@ description: | Start with 'snap list' to see installed snaps. version: set-by-version-script-thxbye version-script: | - # extract the version from the debian/changelog - dpkg-parsechangelog -Sversion + ./mkversion.sh --output-only grade: devel parts: diff --git a/tests/lib/journalctl.sh b/tests/lib/journalctl.sh index 4b21370528..9f74e93722 100644 --- a/tests/lib/journalctl.sh +++ b/tests/lib/journalctl.sh @@ -47,12 +47,19 @@ check_journalctl_log(){ } get_journalctl_log(){ - cursor=$(tail -n1 "$JOURNALCTL_CURSOR_FILE") + cursor="" + if [ -f "$JOURNALCTL_CURSOR_FILE" ]; then + cursor=$(tail -n1 "$JOURNALCTL_CURSOR_FILE") + fi get_journalctl_log_from_cursor "$cursor" "$@" } get_journalctl_log_from_cursor(){ cursor=$1 shift - journalctl "$@" --cursor "$cursor" + if [ -z "$cursor" ]; then + journalctl "$@" + else + journalctl "$@" --cursor "$cursor" + fi } diff --git a/tests/lib/pkgdb.sh b/tests/lib/pkgdb.sh index 9fe12607b4..84cfd96d93 100755 --- a/tests/lib/pkgdb.sh +++ b/tests/lib/pkgdb.sh @@ -490,6 +490,7 @@ pkg_dependencies_ubuntu_generic(){ pkg-config python3-docutils udev + udisks2 upower uuid-runtime " @@ -513,16 +514,14 @@ pkg_dependencies_ubuntu_classic(){ case "$SPREAD_SYSTEM" in ubuntu-14.04-*) - echo " - linux-image-extra-$(uname -r) - " + pkg_linux_image_extra ;; ubuntu-16.04-32) echo " evolution-data-server gnome-online-accounts - linux-image-extra-$(uname -r) " + pkg_linux_image_extra ;; ubuntu-16.04-64) echo " @@ -531,17 +530,15 @@ pkg_dependencies_ubuntu_classic(){ gnome-online-accounts kpartx libvirt-bin - linux-image-extra-$(uname -r) nfs-kernel-server qemu x11-utils xvfb " + pkg_linux_image_extra ;; ubuntu-17.10-64) - echo " - linux-image-extra-4.13.0-16-generic - " + pkg_linux_image_extra ;; ubuntu-18.04-64) echo " @@ -562,11 +559,24 @@ pkg_dependencies_ubuntu_classic(){ esac } +pkg_linux_image_extra (){ + if apt-cache show "linux-image-extra-$(uname -r)" > /dev/null 2>&1; then + echo "linux-image-extra-$(uname -r)"; + else + if apt-cache show "linux-modules-extra-$(uname -r)" > /dev/null 2>&1; then + echo "linux-modules-extra-$(uname -r)"; + else + echo "cannot find a matching kernel modules package"; + exit 1; + fi; + fi +} + pkg_dependencies_ubuntu_core(){ echo " - linux-image-extra-$(uname -r) pollinate " + pkg_linux_image_extra } pkg_dependencies_fedora(){ @@ -586,6 +596,7 @@ pkg_dependencies_fedora(){ python3-yaml redhat-lsb-core rpm-build + udisks2 xdg-user-dirs " } @@ -607,6 +618,7 @@ pkg_dependencies_amazon(){ xdg-user-dirs grub2-tools nc + udisks2 " } @@ -625,6 +637,7 @@ pkg_dependencies_opensuse(){ python3-yaml netcat-openbsd osc + udisks2 uuidd xdg-utils xdg-user-dirs @@ -654,6 +667,7 @@ pkg_dependencies_arch(){ squashfs-tools shellcheck strace + udisks2 xdg-user-dirs xfsprogs " diff --git a/tests/lib/prepare.sh b/tests/lib/prepare.sh index fa5f019d16..3d5681e940 100755 --- a/tests/lib/prepare.sh +++ b/tests/lib/prepare.sh @@ -410,7 +410,7 @@ EOF fi # extra_snap should contain only ONE snap - if "${#extra_snap[@]}" -ne 1; then + if [ "${#extra_snap[@]}" -ne 1 ]; then echo "unexpected number of globbed snaps: ${extra_snap[*]}" exit 1 fi diff --git a/tests/lib/snaps/basic-iface-hooks-consumer/meta/hooks/configure b/tests/lib/snaps/basic-iface-hooks-consumer/meta/hooks/configure new file mode 100755 index 0000000000..163dbaccca --- /dev/null +++ b/tests/lib/snaps/basic-iface-hooks-consumer/meta/hooks/configure @@ -0,0 +1,3 @@ +#!/bin/sh + +# intentionally empty, needed to drive the tests via snapctl. diff --git a/tests/lib/snaps/basic-iface-hooks-consumer/meta/hooks/connect-plug-consumer b/tests/lib/snaps/basic-iface-hooks-consumer/meta/hooks/connect-plug-consumer index d759a888a8..4a2f63aae9 100755 --- a/tests/lib/snaps/basic-iface-hooks-consumer/meta/hooks/connect-plug-consumer +++ b/tests/lib/snaps/basic-iface-hooks-consumer/meta/hooks/connect-plug-consumer @@ -59,4 +59,10 @@ if snapctl set :consumer consumer-attr-4=foo; then exit 1 fi +output=$(snapctl get fail) +if [ "$output" = "connect" ]; then + echo "Failing connect hook as requested" + exit 1 +fi + touch "$SNAP_COMMON/connect-plug-consumer-done" diff --git a/tests/lib/snaps/basic-iface-hooks-consumer/meta/hooks/disconnect-plug-consumer b/tests/lib/snaps/basic-iface-hooks-consumer/meta/hooks/disconnect-plug-consumer new file mode 100755 index 0000000000..f9e278ad17 --- /dev/null +++ b/tests/lib/snaps/basic-iface-hooks-consumer/meta/hooks/disconnect-plug-consumer @@ -0,0 +1,61 @@ +#!/bin/sh +set -x + +echo "Getting attributes from disconnect-plug-consumer hook" + +if ! output=$(snapctl get --slot :consumer producer-attr-1); then + echo "Expected disconnect-plug-consumer to be able to read the value of 'producer-attr-1' attribute of the slot" + exit 1 +fi +expected_output="producer-value-1" +if [ "$output" != "$expected_output" ]; then + echo "Expected output to be '$expected_output', but it was '$output'" + exit 1 +fi + +# Read 'before-connect' attribute of the slot +if ! output=$(snapctl get --slot :consumer before-connect); then + echo "Expected disconnect-plug-consumer be able to read the value of the 'before-connect' attribute of the slot" + exit 1 +fi +expected_output="slot-changed(producer-value)" +if [ "$output" != "$expected_output" ]; then + echo "Expected output to be '$expected_output', but it was '$output'" + exit 1 +fi + +# Read own 'consumer-attr-1' attribute +if ! output=$(snapctl get :consumer consumer-attr-1); then + echo "Expected connect-plug-foo be able to read the value of own 'consumer-attr-1' attribute" + exit 1 +fi +expected_output="consumer-value-1" +if [ "$output" != "$expected_output" ]; then + echo "Expected output to be '$expected_output', but it was '$output'" + exit 1 +fi + +# Read own 'before-connect' attribute +if ! output=$(snapctl get :consumer before-connect); then + echo "Expected disconnect-plug-consumer be able to read the value of own 'before-connect' attribute" + exit 1 +fi +expected_output="plug-changed(consumer-value)" +if [ "$output" != "$expected_output" ]; then + echo "Expected output to be '$expected_output', but it was '$output'" + exit 1 +fi + +# Failure on unknown plug +if snapctl get :unknown target; then + echo "Expected snapctl get to fail on unknown plug" + exit 1 +fi + +# Attributes cannot be set in connect- or disconnect- hooks +if snapctl set :consumer consumer-attr-4=foo; then + echo "Expected snapctl set to fail when run from connect-plug or disconnect-plug hook" + exit 1 +fi + +touch "$SNAP_COMMON/disconnect-plug-consumer-done" diff --git a/tests/lib/snaps/basic-iface-hooks-consumer/meta/hooks/unprepare-plug-consumer b/tests/lib/snaps/basic-iface-hooks-consumer/meta/hooks/unprepare-plug-consumer new file mode 100755 index 0000000000..fea3578ff2 --- /dev/null +++ b/tests/lib/snaps/basic-iface-hooks-consumer/meta/hooks/unprepare-plug-consumer @@ -0,0 +1,5 @@ +#!/bin/sh + +# Read own 'before-connect' attribute +output=$(snapctl get :consumer before-connect) +echo "$output" > $SNAP_COMMON/unprepare-plug-consumer-done diff --git a/tests/lib/snaps/basic-iface-hooks-producer/meta/hooks/configure b/tests/lib/snaps/basic-iface-hooks-producer/meta/hooks/configure new file mode 100755 index 0000000000..96b4b06ad4 --- /dev/null +++ b/tests/lib/snaps/basic-iface-hooks-producer/meta/hooks/configure @@ -0,0 +1 @@ +#!/bin/sh \ No newline at end of file diff --git a/tests/lib/snaps/basic-iface-hooks-producer/meta/hooks/disconnect-slot-producer b/tests/lib/snaps/basic-iface-hooks-producer/meta/hooks/disconnect-slot-producer new file mode 100755 index 0000000000..d3ebe5462c --- /dev/null +++ b/tests/lib/snaps/basic-iface-hooks-producer/meta/hooks/disconnect-slot-producer @@ -0,0 +1,52 @@ +#!/bin/sh +set -x + +echo "Getting attributes from disconnect-slot-producer hook" + +# Read 'consumer-attr-1' attribute of the plug +if ! output=$(snapctl get --plug :producer consumer-attr-1); then + echo "Expected disconnect-slot-producer be able to read the value of the 'consumer-attr-1' attribute of the plug" + exit 1 +fi +expected_output="consumer-value-1" +if [ "$output" != "$expected_output" ]; then + echo "Expected output to be '$expected_output', but it was '$output'" + exit 1 +fi + +# Read own 'before-connect' attribute +if ! output=$(snapctl get :producer before-connect); then + echo "Expected disconnect-slot-producer to be able to read the value of own 'before-connect' attribute" + exit 1 +fi +expected_output="slot-changed(producer-value)" +if [ "$output" != "$expected_output" ]; then + echo "Expected output to be '$expected_output', but it was '$output'" + exit 1 +fi + +# Read 'before-connect' attribute of the plug +if ! output=$(snapctl get --plug :producer before-connect); then + echo "Expected disconnect-slot-producer to be able to read the value of 'before-connect' attribute of the plug" + exit 1 +fi +expected_output="plug-changed(consumer-value)" +if [ "$output" != "$expected_output" ]; then + echo "Expected output to be '$expected_output', but it was '$output'" + exit 1 +fi + +# Failure on unknown slot +if snapctl get :unknown consumer-attr-1; then + echo "Expected snapctl get to fail on unknown slot" + exit 1 +fi + +# Attributes cannot be set in connect- or disconnect- hooks +if snapctl set :producer consumer-attr-4=foo; then + echo "Expected snapctl set to fail when run from connect-slot or disconnect-slot hook" + exit 1 +fi + +touch "$SNAP_COMMON/disconnect-slot-producer-done" + diff --git a/tests/lib/snaps/basic-iface-hooks-producer/meta/hooks/unprepare-slot-producer b/tests/lib/snaps/basic-iface-hooks-producer/meta/hooks/unprepare-slot-producer new file mode 100755 index 0000000000..4c9996138e --- /dev/null +++ b/tests/lib/snaps/basic-iface-hooks-producer/meta/hooks/unprepare-slot-producer @@ -0,0 +1,3 @@ +#!/bin/sh + +touch "$SNAP_COMMON/unprepare-slot-producer-done" \ No newline at end of file diff --git a/tests/lib/snaps/command-chain/chain1 b/tests/lib/snaps/command-chain/chain1 new file mode 100755 index 0000000000..6bcf996a26 --- /dev/null +++ b/tests/lib/snaps/command-chain/chain1 @@ -0,0 +1,5 @@ +#!/bin/sh + +export CHAIN_1_RAN=1 +printf "chain1 " +exec "$@" diff --git a/tests/lib/snaps/command-chain/chain2 b/tests/lib/snaps/command-chain/chain2 new file mode 100755 index 0000000000..f75047f179 --- /dev/null +++ b/tests/lib/snaps/command-chain/chain2 @@ -0,0 +1,5 @@ +#!/bin/sh + +export CHAIN_2_RAN=1 +printf "chain2 " +exec "$@" diff --git a/tests/lib/snaps/command-chain/hello b/tests/lib/snaps/command-chain/hello new file mode 100755 index 0000000000..dc58c79ddd --- /dev/null +++ b/tests/lib/snaps/command-chain/hello @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "hello" diff --git a/tests/lib/snaps/command-chain/meta/icon.png b/tests/lib/snaps/command-chain/meta/icon.png Binary files differnew file mode 100644 index 0000000000..1ec92f1241 --- /dev/null +++ b/tests/lib/snaps/command-chain/meta/icon.png diff --git a/tests/lib/snaps/command-chain/meta/snap.yaml b/tests/lib/snaps/command-chain/meta/snap.yaml new file mode 100644 index 0000000000..ad5e22ebef --- /dev/null +++ b/tests/lib/snaps/command-chain/meta/snap.yaml @@ -0,0 +1,9 @@ +name: command-chain +version: 1.0 +summary: Command chain snap +description: A buildable snap that uses command chain + +apps: + hello: + command: hello + command-chain: [chain1, chain2] diff --git a/tests/lib/snaps/snap-hooks/meta/hooks/remove b/tests/lib/snaps/snap-hooks/meta/hooks/remove index 493f092ca9..2b3af25886 100755 --- a/tests/lib/snaps/snap-hooks/meta/hooks/remove +++ b/tests/lib/snaps/snap-hooks/meta/hooks/remove @@ -1,5 +1,5 @@ #!/bin/sh RETVAL=$(snapctl get exitcode) -echo "$RETVAL" > /root/remove-hook-executed +echo "$RETVAL" > $SNAP_USER_COMMON/remove-hook-executed exit "$RETVAL" diff --git a/tests/lib/snaps/test-snapd-juju-client-observe/bin/sh b/tests/lib/snaps/test-snapd-juju-client-observe/bin/sh new file mode 100755 index 0000000000..73837f7d62 --- /dev/null +++ b/tests/lib/snaps/test-snapd-juju-client-observe/bin/sh @@ -0,0 +1,3 @@ +#!/bin/sh + +exec /bin/sh "$@" diff --git a/tests/lib/snaps/test-snapd-juju-client-observe/meta/snap.yaml b/tests/lib/snaps/test-snapd-juju-client-observe/meta/snap.yaml new file mode 100644 index 0000000000..78e62db702 --- /dev/null +++ b/tests/lib/snaps/test-snapd-juju-client-observe/meta/snap.yaml @@ -0,0 +1,9 @@ +name: test-snapd-juju-client-observe +version: 1.0 +summary: Basic juju-client-observe snap +description: A basic snap which allows reading juju client configuration + +apps: + sh: + command: bin/sh + plugs: [juju-client-observe] diff --git a/tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml b/tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml index a7d20e2517..0027761d20 100644 --- a/tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml +++ b/tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml @@ -260,6 +260,9 @@ apps: screen-inhibit-control: command: bin/run plugs: [ screen-inhibit-control ] + screencast-legacy: + command: bin/run + plugs: [ screencast-legacy ] shutdown: command: bin/run plugs: [ shutdown ] diff --git a/tests/lib/snaps/test-snapd-service/bin/stop-stop-mode b/tests/lib/snaps/test-snapd-service/bin/stop-stop-mode index 43d21e4389..2dd0cd7aa9 100755 --- a/tests/lib/snaps/test-snapd-service/bin/stop-stop-mode +++ b/tests/lib/snaps/test-snapd-service/bin/stop-stop-mode @@ -1,4 +1,4 @@ #!/bin/sh echo "stop $1 process" -rm -f $SNAP_COMMON/ready +rm -f "$SNAP_COMMON/ready" diff --git a/tests/lib/snaps/test-snapd-udisks2/snapcraft.yaml b/tests/lib/snaps/test-snapd-udisks2/snapcraft.yaml new file mode 100644 index 0000000000..2f5cb5ed5e --- /dev/null +++ b/tests/lib/snaps/test-snapd-udisks2/snapcraft.yaml @@ -0,0 +1,17 @@ +name: test-snapd-udisks2 +version: 1.0 +summary: Basic udisks2 snap +description: A basic snap which allow operating as or interacting with the UDisks2 service +grade: stable +confinement: strict + +apps: + udisksctl: + command: udisksctl + plugs: [udisks2] + +parts: + copy: + plugin: dump + source: . + stage-packages: [udisks2] diff --git a/tests/lib/snaps/test-snapd-udisks2/udisksctl b/tests/lib/snaps/test-snapd-udisks2/udisksctl new file mode 100755 index 0000000000..cca2442724 --- /dev/null +++ b/tests/lib/snaps/test-snapd-udisks2/udisksctl @@ -0,0 +1,3 @@ +#!/bin/bash + +udisksctl "$@" diff --git a/tests/main/command-chain/task.yaml b/tests/main/command-chain/task.yaml new file mode 100644 index 0000000000..2fb5572b47 --- /dev/null +++ b/tests/main/command-chain/task.yaml @@ -0,0 +1,26 @@ +summary: Check that command-chain is properly supported + +prepare: | + echo "Build command chain snap" + snap pack "$TESTSLIB/snaps/command-chain" + snap install --dangerous command-chain_1.0_all.snap + +restore: | + rm -f command-chain_1.0_all.snap + +execute: | + echo "Test that command-chain actually runs as expected" + [ "$(command-chain.hello)" = "chain1 chain2 hello" ] + + echo "Ensure that the command-chain is run with 'snap run --shell' as well" + [ "$(snap run --shell command-chain.hello -c 'echo "shell"')" = "chain1 chain2 shell" ] + env="$(snap run --shell command-chain.hello -c 'env')" + echo "$env" | MATCH '^CHAIN_1_RAN=1$' + echo "$env" | MATCH '^CHAIN_2_RAN=1$' + + echo "Also ensure that 'snap run' supports skipping the command chain" + [ "$(snap run --skip-command-chain command-chain.hello)" = "hello" ] + [ "$(snap run --shell --skip-command-chain command-chain.hello -c 'echo "shell"')" = "shell" ] + env="$(snap run --shell --skip-command-chain command-chain.hello -c 'env')" + echo "$env" | MATCH -v '^CHAIN_1_RAN=1$' + echo "$env" | MATCH -v '^CHAIN_2_RAN=1$' diff --git a/tests/main/fedora-base-smoke/task.yaml b/tests/main/fedora-base-smoke/task.yaml index d19e1695e1..29f2bcf1bc 100644 --- a/tests/main/fedora-base-smoke/task.yaml +++ b/tests/main/fedora-base-smoke/task.yaml @@ -1,11 +1,15 @@ summary: smoke test for Fedora 29 base snap -# The hello-fedora snap is not yet available for i386 -systems: [-*-32] + +# The hello-fedora snap is just available for amd64 architecture +systems: [-*-32, -*-arm*, -*-ppc64el, -*-s390x] + # not available on most arches in autopkgtest backends: [-autopkgtest] + details: | Smoke test for checking if we can run hello-world like application against a Fedora 29 base snap correctly. + execute: | # This is explicit because fedora29 snap is still in edge. snap install --edge fedora29 diff --git a/tests/main/install-refresh-remove-hooks/task.yaml b/tests/main/install-refresh-remove-hooks/task.yaml index 34b04b0447..f24aaab6d9 100644 --- a/tests/main/install-refresh-remove-hooks/task.yaml +++ b/tests/main/install-refresh-remove-hooks/task.yaml @@ -1,7 +1,7 @@ summary: Check install, remove and pre-refresh/post-refresh hooks. environment: - REMOVE_HOOK_FILE: "$HOME/remove-hook-executed" + REMOVE_HOOK_FILE: "$HOME/snap/snap-hooks/common/remove-hook-executed" restore: | rm -f "$REMOVE_HOOK_FILE" diff --git a/tests/main/interfaces-hooks/task.yaml b/tests/main/interfaces-hooks/task.yaml index 282c44cab6..cf1ae0671e 100644 --- a/tests/main/interfaces-hooks/task.yaml +++ b/tests/main/interfaces-hooks/task.yaml @@ -18,12 +18,20 @@ restore: | rm -f "$PRODUCER_DATA/prepare-slot-producer-done" rm -f "$CONSUMER_DATA/connect-plug-consumer-done" rm -f "$PRODUCER_DATA/connect-slot-producer-done" + rm -f "$CONSUMER_DATA/disconnect-plug-consumer-done" + rm -f "$PRODUCER_DATA/disconnect-slot-producer-done" + rm -f "$CONSUMER_DATA/unprepare-plug-consumer-done" + rm -f "$PRODUCER_DATA/unprepare-slot-producer-done" snap remove basic-iface-hooks-consumer snap remove basic-iface-hooks-producer execute: | #shellcheck source=tests/lib/snaps.sh . "$TESTSLIB/snaps.sh" + remove_markers() { + rm -f "$CONSUMER_DATA"/*-plug-*done + rm -f "$PRODUCER_DATA"/*-slot-*done + } check_attributes(){ # static values should have the values defined in snap's yaml jq -r '.data["conns"]["basic-iface-hooks-consumer:consumer basic-iface-hooks-producer:producer"]["plug-static"]["consumer-attr-1"]' /var/lib/snapd/state.json | MATCH "consumer-value-1" @@ -56,3 +64,39 @@ execute: | echo "Verify static and dynamic attributes have expected values" check_attributes systemctl start snapd.service snapd.socket + + remove_markers + + # make sure disconnect hooks are executed + snap disconnect basic-iface-hooks-consumer:consumer basic-iface-hooks-producer:producer + snap change --last=disconnect | MATCH "Run hook disconnect-slot-producer of snap \"basic-iface-hooks-producer" + snap change --last=disconnect | MATCH "Run hook disconnect-plug-consumer of snap \"basic-iface-hooks-consumer" + + [ -f "$CONSUMER_DATA/disconnect-plug-consumer-done" ] + [ -f "$PRODUCER_DATA/disconnect-slot-producer-done" ] + + remove_markers + + # make connect hooks fail, check that undo hooks were executed + snap set basic-iface-hooks-consumer fail=connect + if snap connect basic-iface-hooks-consumer:consumer basic-iface-hooks-producer:producer ; then + echo "Expected snap connect to fail" + exit 1 + fi + + [ -f "$CONSUMER_DATA/unprepare-plug-consumer-done" ] + [ -f "$PRODUCER_DATA/unprepare-slot-producer-done" ] + [ -f "$PRODUCER_DATA/connect-slot-producer-done" ] + [ -f "$PRODUCER_DATA/disconnect-slot-producer-done" ] + + MATCH "plug-changed.consumer-value" < "$CONSUMER_DATA/unprepare-plug-consumer-done" + + remove_markers + + # disconnect hooks should be executed on snap removal + snap set basic-iface-hooks-consumer fail=none + snap connect basic-iface-hooks-consumer:consumer basic-iface-hooks-producer:producer + snap remove basic-iface-hooks-consumer + [ -f "$PRODUCER_DATA/disconnect-slot-producer-done" ] + snap change --last=remove | MATCH "Run hook disconnect-slot-producer of snap \"basic-iface-hooks-producer" + snap change --last=remove | MATCH "Run hook disconnect-plug-consumer of snap \"basic-iface-hooks-consumer" diff --git a/tests/main/interfaces-juju-client-observe/task.yaml b/tests/main/interfaces-juju-client-observe/task.yaml new file mode 100644 index 0000000000..b517b87583 --- /dev/null +++ b/tests/main/interfaces-juju-client-observe/task.yaml @@ -0,0 +1,52 @@ +summary: Ensure that the juju client observe interface works. + +details: | + The juju-client-observe interface allows access to the juju client configuration + +# The interface is not defined for ubuntu core systems +systems: [-ubuntu-core-*] + +prepare: | + # shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB/snaps.sh" + install_local test-snapd-juju-client-observe + + # shellcheck source=tests/lib/files.sh + . "$TESTSLIB/files.sh" + + ensure_dir_exists_backup_real "$HOME"/.local/share/juju + ensure_file_exists_backup_real "$HOME"/.local/share/juju/juju.conf + +restore: | + rm -f call.error + + # shellcheck source=tests/lib/files.sh + . "$TESTSLIB/files.sh" + + # Delete the created juju dir and configuration files + clean_file "$HOME"/.local/share/juju/juju.conf + clean_dir "$HOME"/.local/share/juju + +execute: | + echo "The interface is not connected by default" + snap interfaces -i juju-client-observe | MATCH -- '- +test-snapd-juju-client-observe:juju-client-observe' + + echo "When the interface is connected" + snap connect test-snapd-juju-client-observe:juju-client-observe + + echo "Then the snap is able to access the juju client configuration" + test-snapd-juju-client-observe.sh -c "cat $HOME/.local/share/juju/juju.conf" + + echo "When the plug is disconnected" + snap disconnect test-snapd-juju-client-observe:juju-client-observe + + if [ "$(snap debug confinement)" = partial ]; then + exit 0 + fi + + echo "Then the snap is not able to read the juju client configuration" + if test-snapd-juju-client-observe.sh -c "cat $HOME/.local/share/juju/juju.conf" 2>call.error; then + echo "Expected permission error accessing to input device" + exit 1 + fi + MATCH "Permission denied" < call.error diff --git a/tests/main/interfaces-snapd-control-with-manage/task.yaml b/tests/main/interfaces-snapd-control-with-manage/task.yaml index 3511ec1514..c5b29d02e4 100644 --- a/tests/main/interfaces-snapd-control-with-manage/task.yaml +++ b/tests/main/interfaces-snapd-control-with-manage/task.yaml @@ -17,7 +17,7 @@ prepare: | fi echo "Ensure jq is installed" - if ! which jq; then + if ! command -v jq; then snap install --devmode jq fi @@ -25,12 +25,14 @@ prepare: | snap ack "$TESTSLIB/assertions/testrootorg-store.account-key" - . $TESTSLIB/store.sh - setup_fake_store $BLOB_DIR + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + setup_fake_store "$BLOB_DIR" - . $TESTSLIB/snaps.sh + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh snap_path=$(make_snap test-snapd-control-consumer) - make_snap_installable $BLOB_DIR ${snap_path} + make_snap_installable "$BLOB_DIR" "${snap_path}" cat > snap-decl.json <<'EOF' { "format": "1", @@ -47,15 +49,16 @@ prepare: | } EOF fakestore new-snap-declaration --dir "${BLOB_DIR}" --snap-decl-json snap-decl.json - snap ack ${BLOB_DIR}/asserts/*.snap-declaration + snap ack "${BLOB_DIR}"/asserts/*.snap-declaration restore: | if [ "$TRUST_TEST_KEYS" = "false" ]; then echo "This test needs test keys to be trusted" exit fi - . $TESTSLIB/store.sh - teardown_fake_store $BLOB_DIR + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + teardown_fake_store "$BLOB_DIR" debug: | jq .data.auth.device /var/lib/snapd/state.json @@ -105,13 +108,13 @@ execute: | systemctl start snapd.socket snapd.service echo "Ensure that last-refresh-hit happens" - for i in $(seq 120); do - if jq '.data["last-refresh-hints"]' /var/lib/snapd/state.json | grep $(date +%Y); then + for _ in $(seq 120); do + if jq '.data["last-refresh-hints"]' /var/lib/snapd/state.json | grep "$(date +%Y)"; then break fi sleep 1 done - jq '.data["last-refresh-hints"]' /var/lib/snapd/state.json | grep $(date +%Y) + jq '.data["last-refresh-hints"]' /var/lib/snapd/state.json | grep "$(date +%Y)" # prevent refreshes again systemctl stop snapd.socket snapd.service diff --git a/tests/main/interfaces-ssh-keys/task.yaml b/tests/main/interfaces-ssh-keys/task.yaml index 48e6afbd7d..fafc1427e5 100644 --- a/tests/main/interfaces-ssh-keys/task.yaml +++ b/tests/main/interfaces-ssh-keys/task.yaml @@ -9,7 +9,8 @@ environment: TESTKEY: "$HOME/.ssh/testkey" prepare: | - . $TESTSLIB/snaps.sh + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh install_local test-snapd-ssh-keys if [ -d "$KEYSDIR" ]; then @@ -29,7 +30,7 @@ restore: | execute: | echo "The interface is not connected by default" - snap interfaces -i ssh-keys | MATCH "\- +test-snapd-ssh-keys:ssh-keys" + snap interfaces -i ssh-keys | MATCH -- '^- +test-snapd-ssh-keys:ssh-keys' echo "When the interface is connected" snap connect test-snapd-ssh-keys:ssh-keys @@ -50,7 +51,7 @@ execute: | snap disconnect test-snapd-ssh-keys:ssh-keys echo "Then the snap is not able to read a ssh private key" - if test-snapd-ssh-keys.sh -c "cat $TESTKEY" 2>${PWD}/call.error; then + if test-snapd-ssh-keys.sh -c "cat $TESTKEY" 2>"${PWD}"/call.error; then echo "Expected permission error accessing to ssh" exit 1 fi diff --git a/tests/main/interfaces-ssh-public-keys/task.yaml b/tests/main/interfaces-ssh-public-keys/task.yaml index 73e9818dfd..c80c0de404 100644 --- a/tests/main/interfaces-ssh-public-keys/task.yaml +++ b/tests/main/interfaces-ssh-public-keys/task.yaml @@ -9,6 +9,7 @@ environment: TESTKEY: "/$HOME/.ssh/testkey" prepare: | + #shellcheck source=tests/lib/snaps.sh . "$TESTSLIB/snaps.sh" install_local test-snapd-ssh-public-keys @@ -30,8 +31,8 @@ restore: | execute: | echo "The interface is not connected by default" - snap interfaces -i ssh-public-keys | MATCH "\- +test-snapd-ssh-public-keys:ssh-public-keys" - + snap interfaces -i ssh-public-keys | MATCH -- '^- +test-snapd-ssh-public-keys:ssh-public-keys' + echo "When the interface is connected" snap connect test-snapd-ssh-public-keys:ssh-public-keys @@ -46,7 +47,7 @@ execute: | fi echo "And then the snap is not able to access to private keys" - if test-snapd-ssh-public-keys.sh -c "cat $TESTKEY" 2>${PWD}/call.error; then + if test-snapd-ssh-public-keys.sh -c "cat $TESTKEY" 2>"${PWD}"/call.error; then echo "Expected permission error accessing to ssh" exit 1 fi @@ -56,7 +57,7 @@ execute: | snap disconnect test-snapd-ssh-public-keys:ssh-public-keys echo "Then the snap is not able to access the ssh public keys" - if test-snapd-ssh-public-keys.sh -c "cat $TESTKEY.pub" 2>${PWD}/call.error; then + if test-snapd-ssh-public-keys.sh -c "cat $TESTKEY.pub" 2>"${PWD}"/call.error; then echo "Expected permission error accessing to ssh" exit 1 fi diff --git a/tests/main/interfaces-system-observe/task.yaml b/tests/main/interfaces-system-observe/task.yaml index 2cfd4fea25..1d37ae3080 100644 --- a/tests/main/interfaces-system-observe/task.yaml +++ b/tests/main/interfaces-system-observe/task.yaml @@ -21,14 +21,14 @@ prepare: | fi restore: | - rm -f *.error + rm -f ./*.error if [[ "$SPREAD_SYSTEM" != ubuntu-14.04-* ]]; then systemctl stop systemd-hostnamed fi execute: | echo "The interface is disconnected by default" - snap interfaces -i system-observe | MATCH "^\- +test-snapd-system-observe-consumer:system-observe" + snap interfaces -i system-observe | MATCH -- '^- +test-snapd-system-observe-consumer:system-observe' echo "When the interface is connected" snap connect test-snapd-system-observe-consumer:system-observe @@ -52,7 +52,7 @@ execute: | echo "Expected error with plug disconnected" exit 1 fi - cat consumer.error | MATCH "Permission denied" + MATCH "Permission denied" < consumer.error if [[ "$SPREAD_SYSTEM" != ubuntu-14.04-* ]]; then echo "And the snap is not able to introspect hostname1" @@ -60,6 +60,6 @@ execute: | echo "Expected error with plug disconnected" exit 1 fi - cat introspect.error | MATCH "Permission denied" + MATCH "Permission denied" < introspect.error fi fi diff --git a/tests/main/interfaces-time-control/task.yaml b/tests/main/interfaces-time-control/task.yaml index 31105f5b53..40908e0bf9 100644 --- a/tests/main/interfaces-time-control/task.yaml +++ b/tests/main/interfaces-time-control/task.yaml @@ -9,7 +9,8 @@ details: | systems: [-opensuse-*,-fedora-*,-ubuntu-core-*,-ubuntu-14.04-*,-*-s390x,-arch-*] prepare: | - . $TESTSLIB/snaps.sh + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh # Install a snap declaring a plug on time-control install_local test-snapd-timedate-control-consumer @@ -29,7 +30,7 @@ execute: | snap connect test-snapd-timedate-control-consumer:netlink-audit echo "The interface is disconnected by default" - snap interfaces -i time-control | MATCH "\- +test-snapd-timedate-control-consumer:time-control" + snap interfaces -i time-control | MATCH -- '^- +test-snapd-timedate-control-consumer:time-control' # When the interface is connected snap connect test-snapd-timedate-control-consumer:time-control @@ -58,7 +59,7 @@ execute: | # Disconnect the interface and check access to timedatectl status snap disconnect test-snapd-timedate-control-consumer:time-control - if test-snapd-timedate-control-consumer.timedatectl-time status 2>${PWD}/call.error; then + if test-snapd-timedate-control-consumer.timedatectl-time status 2>"${PWD}"/call.error; then echo "Expected permission error calling timedatectl status with disconnected plug" exit 1 fi diff --git a/tests/main/interfaces-timezone-control/task.yaml b/tests/main/interfaces-timezone-control/task.yaml index 69988df1dd..1f1a6138c5 100644 --- a/tests/main/interfaces-timezone-control/task.yaml +++ b/tests/main/interfaces-timezone-control/task.yaml @@ -11,7 +11,8 @@ details: | systems: [-ubuntu-core-18-*] prepare: | - . $TESTSLIB/snaps.sh + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh # Install a snap declaring a plug on timezone-control install_local test-snapd-timedate-control-consumer @@ -26,7 +27,7 @@ restore: | execute: | echo "The interface is disconnected by default" - snap interfaces -i timezone-control | MATCH "\- +test-snapd-timedate-control-consumer:timezone-control" + snap interfaces -i timezone-control | MATCH -- '^- +test-snapd-timedate-control-consumer:timezone-control' echo "When the interface is connected" snap connect test-snapd-timedate-control-consumer:timezone-control @@ -44,11 +45,11 @@ execute: | fi # Set the timezone1 as timezone and check the status - test-snapd-timedate-control-consumer.timedatectl-timezone set-timezone $timezone1 + test-snapd-timedate-control-consumer.timedatectl-timezone set-timezone "$timezone1" [ "$(test-snapd-timedate-control-consumer.timedatectl-timezone status | grep -oP 'Time zone: \K(.*)(?= \()')" = "$timezone1" ] # Set the timezone2 as timezone and check the status - test-snapd-timedate-control-consumer.timedatectl-timezone set-timezone $timezone2 + test-snapd-timedate-control-consumer.timedatectl-timezone set-timezone "$timezone2" [ "$(test-snapd-timedate-control-consumer.timedatectl-timezone status | grep -oP 'Time zone: \K(.*)(?= \()')" = "$timezone2" ] if [ "$(snap debug confinement)" = partial ] ; then @@ -57,7 +58,7 @@ execute: | # Disconnect the interface and check access to timedatectl status snap disconnect test-snapd-timedate-control-consumer:timezone-control - if test-snapd-timedate-control-consumer.timedatectl-timezone status 2>${PWD}/call.error; then + if test-snapd-timedate-control-consumer.timedatectl-timezone status 2>"${PWD}"/call.error; then echo "Expected permission error calling timedatectl status with disconnected plug" exit 1 fi diff --git a/tests/main/interfaces-udev/task.yaml b/tests/main/interfaces-udev/task.yaml index 98954d0688..89c0392fce 100644 --- a/tests/main/interfaces-udev/task.yaml +++ b/tests/main/interfaces-udev/task.yaml @@ -11,7 +11,8 @@ details: | more interfaces declare udev snippets. prepare: | - . $TESTSLIB/snaps.sh + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh echo "Given a snap declaring a slot with associated udev rules is installed" install_local modem-manager-consumer diff --git a/tests/main/interfaces-udisks2/task.yaml b/tests/main/interfaces-udisks2/task.yaml new file mode 100644 index 0000000000..088872b8eb --- /dev/null +++ b/tests/main/interfaces-udisks2/task.yaml @@ -0,0 +1,59 @@ +summary: Ensure that the udisks2 interface works. + +details: | + The udisks2 interface allows operating as or interacting with the UDisks2 service + +# Interfaces not defined for ubuntu core systems +systems: [-ubuntu-core-*] + +prepare: | + snap install test-snapd-udisks2 + +environment: + FS_PATH: "$(pwd)/dev0-fake0" + MMCBLK_PATH: /dev/mmcblk-fake0 + +restore: | + rm -f call.error + losetup -d "$MMCBLK_PATH" || true + rm -f "$MMCBLK_PATH" "$FS_PATH" + +execute: | + echo "The interface is not connected by default" + snap interfaces -i udisks2 | MATCH -- "- +test-snapd-udisks2:udisks2" + + echo "When the interface is connected" + snap connect test-snapd-udisks2:udisks2 + + echo "Check it is possible to see the udisks2 stauts" + test-snapd-udisks2.udisksctl status | MATCH "MODEL" + + echo "Check it is possible to dump all the udisks objects info" + test-snapd-udisks2.udisksctl dump | MATCH "org.freedesktop.UDisks2.Manager" + + echo "Check we can mount/unmount a block device using the snap" + # create a 10M filesystem in pwd + dd if=/dev/zero of="$FS_PATH" bs=1M count=10 + mkfs.ext4 -F "$FS_PATH" + # create the loopback block device + mknod "$MMCBLK_PATH" b 7 200 + losetup "$MMCBLK_PATH" "$FS_PATH" + + device="$(losetup -j "$FS_PATH" | cut -d: -f1)" + + test-snapd-udisks2.udisksctl mount -b "$device" -t ext4 | MATCH "Mounted /dev/" + test-snapd-udisks2.udisksctl unmount -b "$device" | MATCH "Unmounted /dev/" + + if [ "$(snap debug confinement)" = partial ] ; then + exit 0 + fi + + echo "When the plug is disconnected" + snap disconnect test-snapd-udisks2:udisks2 + + echo "Then the snap is not able to check udisks2 status" + if test-snapd-udisks2.udisksctl status 2> call.error; then + echo "Expected permission error calling udisksctl status with disconnected plug" + exit 1 + fi + MATCH "Permission denied" < call.error diff --git a/tests/main/interfaces-uhid/task.yaml b/tests/main/interfaces-uhid/task.yaml index 716e8b7a0b..1258844151 100644 --- a/tests/main/interfaces-uhid/task.yaml +++ b/tests/main/interfaces-uhid/task.yaml @@ -17,7 +17,7 @@ restore: | execute: | echo "The plug is not connected by default" - snap interfaces -i uhid | MATCH "\- +test-snapd-uhid:uhid" + snap interfaces -i uhid | MATCH -- '^- +test-snapd-uhid:uhid' echo "When the plug is connected" snap connect test-snapd-uhid:uhid @@ -38,7 +38,7 @@ execute: | snap disconnect test-snapd-uhid:uhid echo "Then the snap is not able to create/destroy a device on /dev/uhid" - if test-snapd-uhid.test-device 2>${PWD}/call.error; then + if test-snapd-uhid.test-device 2>"${PWD}"/call.error; then echo "Expected permission error calling uhid with disconnected plug" exit 1 fi diff --git a/tests/main/interfaces-upower-observe/task.yaml b/tests/main/interfaces-upower-observe/task.yaml index 19548a13cd..f30e5899fe 100644 --- a/tests/main/interfaces-upower-observe/task.yaml +++ b/tests/main/interfaces-upower-observe/task.yaml @@ -45,7 +45,7 @@ execute: | echo "When the plug is connected the snap is able to dump info about the upower devices" expected="/org/freedesktop/UPower/devices/DisplayDevice.*" - for i in $(seq 20); do + for _ in $(seq 20); do if ! test-snapd-upower-observe-consumer.upower --dump | MATCH "$expected"; then sleep 1 fi @@ -57,9 +57,9 @@ execute: | snap disconnect test-snapd-upower-observe-consumer:upower-observe echo "Then the snap is not able to dump info about the upower devices" - if test-snapd-upower-observe-consumer.upower --dump 2>${PWD}/upower.error; then + if test-snapd-upower-observe-consumer.upower --dump 2>"${PWD}"/upower.error; then echo "Expected permission error accessing upower info with disconnected plug" exit 1 fi - cat upower.error | MATCH "Permission denied" + MATCH "Permission denied" < upower.error fi diff --git a/tests/main/interfaces-wayland/task.yaml b/tests/main/interfaces-wayland/task.yaml index 41249ffe9a..cfe543f007 100644 --- a/tests/main/interfaces-wayland/task.yaml +++ b/tests/main/interfaces-wayland/task.yaml @@ -4,7 +4,8 @@ summary: Ensure that the wayland interface works systems: [ ubuntu-1*-*64 ] prepare: | - . $TESTSLIB/pkgdb.sh + #shellcheck source=tests/lib/pkgdb.sh + . "$TESTSLIB"/pkgdb.sh snap install --edge test-snapd-wayland restore: | diff --git a/tests/main/kernel-snap-refresh-on-core/task.yaml b/tests/main/kernel-snap-refresh-on-core/task.yaml index f2b0957afa..09388b9034 100644 --- a/tests/main/kernel-snap-refresh-on-core/task.yaml +++ b/tests/main/kernel-snap-refresh-on-core/task.yaml @@ -39,7 +39,8 @@ execute: | exit 0 fi - . $TESTSLIB/boot.sh + #shellcheck source=tests/lib/boot.sh + . "$TESTSLIB"/boot.sh if [ "$SPREAD_REBOOT" = 0 ]; then # ensure we have a good starting place @@ -47,7 +48,7 @@ execute: | test-snapd-tools.echo hello | MATCH hello # go to known good starting place - snap refresh pc-kernel --${KERNEL_CHANNEL} + snap refresh pc-kernel "--${KERNEL_CHANNEL}" REBOOT elif [ "$SPREAD_REBOOT" = 1 ]; then # from our good starting place we refresh @@ -58,7 +59,7 @@ execute: | cat /proc/version_signature > prevKernelSignature # refresh - snap refresh pc-kernel --${NEW_KERNEL_CHANNEL} + snap refresh pc-kernel "--${NEW_KERNEL_CHANNEL}" # check boot env vars snap list | awk "/^pc-kernel / {print(\$3)}" > nextBoot diff --git a/tests/main/known-remote/task.yaml b/tests/main/known-remote/task.yaml index 0ceba7d0ee..8a6ba4a36f 100644 --- a/tests/main/known-remote/task.yaml +++ b/tests/main/known-remote/task.yaml @@ -2,7 +2,7 @@ summary: Check snap known --store execute: | echo "Check getting assertion from the store" output=$(snap known --remote model series=16 brand-id=canonical model=pi2) - echo $output |MATCH "type: model" - echo $output |MATCH "series: 16" - echo $output |MATCH "brand-id: canonical" - echo $output |MATCH "model: pi2" + echo "$output" |MATCH "type: model" + echo "$output" |MATCH "series: 16" + echo "$output" |MATCH "brand-id: canonical" + echo "$output" |MATCH "model: pi2" diff --git a/tests/main/layout/task.yaml b/tests/main/layout/task.yaml index e4f418b81f..c59dc9adc3 100644 --- a/tests/main/layout/task.yaml +++ b/tests/main/layout/task.yaml @@ -7,7 +7,8 @@ details: | prepare: | echo "Ensure feature flag is enabled" snap set core experimental.layouts=true - . $TESTSLIB/snaps.sh + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh install_local test-snapd-layout debug: | ls -ld /etc || : @@ -15,7 +16,8 @@ debug: | ls -ld /etc/demo.conf || : ls -ld /etc/demo.cfg || : execute: | - . $TESTSLIB/snaps.sh + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh for i in $(seq 2); do if [ "$i" -eq 2 ]; then echo "The snap works across refreshes" @@ -30,6 +32,7 @@ execute: | test-snapd-layout.sh -c "test -d /etc/demo" test-snapd-layout.sh -c "test -f /etc/demo.conf" test-snapd-layout.sh -c "test -h /etc/demo.cfg" + #shellcheck disable=SC2016 test "$(test-snapd-layout.sh -c "readlink /etc/demo.cfg")" = "$(test-snapd-layout.sh -c 'echo $SNAP_COMMON/etc/demo.conf')" test-snapd-layout.sh -c "test -d /usr/share/demo" test-snapd-layout.sh -c "test -d /var/lib/demo" @@ -57,19 +60,24 @@ execute: | echo "and the writes go to the right place in the backing store" test-snapd-layout.sh -c "echo foo-1 > /etc/demo/writable" + #shellcheck disable=SC2016 test "$(test-snapd-layout.sh -c 'cat $SNAP_COMMON/etc/demo/writable')" = "foo-1" test-snapd-layout.sh -c "echo foo-2 > /etc/demo.conf" + #shellcheck disable=SC2016 test "$(test-snapd-layout.sh -c 'cat $SNAP_COMMON/etc/demo.conf')" = "foo-2" # NOTE: this is a symlink to demo.conf, effectively test-snapd-layout.sh -c "echo foo-3 > /etc/demo.cfg" + #shellcheck disable=SC2016 test "$(test-snapd-layout.sh -c 'cat $SNAP_COMMON/etc/demo.conf')" = "foo-3" test-snapd-layout.sh -c "echo foo-4 > /var/lib/demo/writable" + #shellcheck disable=SC2016 test "$(test-snapd-layout.sh -c 'cat $SNAP_DATA/var/lib/demo/writable')" = "foo-4" test-snapd-layout.sh -c "echo foo-5 > /var/cache/demo/writable" + #shellcheck disable=SC2016 test "$(test-snapd-layout.sh -c 'cat $SNAP_DATA/var/cache/demo/writable')" = "foo-5" echo "layout locations pointing to SNAP are readable" @@ -78,6 +86,7 @@ execute: | test-snapd-layout.sh -c "test -r /opt/demo/file" echo "layout locations in dynamically created SNAP directories are writable" + # shellcheck disable=SC2016 test-snapd-layout.sh -c 'test -w $SNAP/bin-very-weird-place' test-snapd-layout.sh -c 'test -w /bin/very-weird-place' done diff --git a/tests/main/listing/task.yaml b/tests/main/listing/task.yaml index 492ba5b27b..a54fcdfd78 100644 --- a/tests/main/listing/task.yaml +++ b/tests/main/listing/task.yaml @@ -1,7 +1,8 @@ summary: Check snap listings prepare: | - . $TESTSLIB/snaps.sh + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh install_local test-snapd-tools # autopkgtest run only a subset of tests that deals with the integration @@ -16,6 +17,7 @@ execute: | # most core versions should be like "16-2", so [0-9]{2}-[0-9.]+ # but edge will have a timestamp in there, "16.2+201701010932", so add an optional \+[0-9]+ to the end # *current* edge also has .git. and a hash snippet, so add an optional .git.[0-9a-f]+ to the already optional timestamp + #shellcheck disable=SC2166 if [ "$SPREAD_BACKEND" = "linode" -o "$SPREAD_BACKEND" = "google" -o "$SPREAD_BACKEND" == "qemu" ] && [ "$SPREAD_SYSTEM" = "ubuntu-core-16-64" ]; then echo "With customized images the core snap is sideloaded" expected='^core .* [0-9]{2}-[0-9.]+(~[a-z0-9]+)?(\+git[0-9]+\.[0-9a-f]+)? +x[0-9]+ +- +- +core *$' @@ -33,17 +35,18 @@ execute: | snap list | MATCH '^test-snapd-tools +[0-9]+(\.[0-9]+)* +x[0-9]+ +- +- +- *$' echo "Install test-snapd-tools again" - . $TESTSLIB/snaps.sh + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh install_local test-snapd-tools echo "And run snap list --all" output=$(snap list --all |grep test-snapd-tools) if [ "$(grep -c test-snapd-tools <<< "$output")" != "2" ]; then echo "Expected two test-snapd-tools in the output, got:" - echo $output + echo "$output" exit 1 fi if [ "$(grep -c disabled <<< "$output")" != "1" ]; then echo "Expected one disabled line in in the output, got:" - echo $output + echo "$output" exit 1 fi diff --git a/tests/main/login/task.yaml b/tests/main/login/task.yaml index 7d6d7825bf..2d56221a0d 100644 --- a/tests/main/login/task.yaml +++ b/tests/main/login/task.yaml @@ -21,7 +21,7 @@ execute: | if [ -n "$SPREAD_STORE_USER" ] && [ -n "$SPREAD_STORE_PASSWORD" ]; then echo "Checking successful login" - expect -d -f $TESTSLIB/successful_login.exp + expect -d -f "$TESTSLIB"/successful_login.exp output=$(snap managed) if [ "$output" != "true" ]; then diff --git a/tests/main/lxd/task.yaml b/tests/main/lxd/task.yaml index 73d7ffd6a2..79af7c00b2 100644 --- a/tests/main/lxd/task.yaml +++ b/tests/main/lxd/task.yaml @@ -2,10 +2,7 @@ summary: Ensure that lxd works # only run this on ubuntu 16+, lxd will not work on !ubuntu systems # currently nor on ubuntu 14.04 -#systems: [ubuntu-16*, ubuntu-core-*] - -# FIXME LXD has some issue on Google images. -systems: [ubuntu-16.04-32, ubuntu-core-16-*] +systems: [ubuntu-16*, ubuntu-18*, ubuntu-2*, ubuntu-core-*] # autopkgtest run only a subset of tests that deals with the integration # with the distro @@ -17,8 +14,15 @@ kill-timeout: 25m # Start before anything else as it can take a really long time. priority: 1000 +prepare: | + # using apt here is ok because this test only runs on ubuntu + echo "Remove any installed debs (some images carry them) to ensure we test the snap" + if command -v apt; then + apt autoremove -y lxd + fi + restore: | - if [[ $(ls -1 "$GOHOME"/snapd_*.deb | wc -l || echo 0) -eq 0 ]]; then + if [[ "$(find "$GOHOME" -name 'snapd_*.deb' | wc -l || echo 0)" -eq 0 ]]; then exit fi @@ -33,17 +37,17 @@ debug: | get_journalctl_log -u snap.lxd.daemon.service execute: | - if [[ $(ls -1 "$GOHOME"/snapd_*.deb | wc -l || echo 0) -eq 0 ]]; then + if [[ "$(find "$GOHOME" -name 'snapd_*.deb' | wc -l || echo 0)" -eq 0 ]]; then echo "No run lxd test when there are not .deb files built" exit fi wait_for_lxd(){ - while ! printf "GET / HTTP/1.0\n\n" | nc -U /var/snap/lxd/common/lxd/unix.socket | MATCH "200 OK"; do sleep 1; done + while ! printf 'GET / HTTP/1.0\n\n' | nc -U /var/snap/lxd/common/lxd/unix.socket | MATCH '200 OK'; do sleep 1; done } echo "Install lxd" - snap install lxd + snap install --candidate lxd echo "Create a trivial container using the lxd snap" wait_for_lxd @@ -51,13 +55,16 @@ execute: | echo "Setting up proxy for lxc" if [ -n "${http_proxy:-}" ]; then - lxd.lxc config set core.proxy_http $http_proxy + lxd.lxc config set core.proxy_http "$http_proxy" fi if [ -n "${https_proxy:-}" ]; then - lxd.lxc config set core.proxy_https $http_proxy + lxd.lxc config set core.proxy_https "$http_proxy" fi - lxd.lxc launch ubuntu:16.04 my-ubuntu + # The snapd package we build as part of the tests will only run on the + # distro we build on. So we need to launch the right ubuntu version. + . /etc/os-release + lxd.lxc launch ubuntu:${VERSION_ID} my-ubuntu echo "Ensure we can run things inside" lxd.lxc exec my-ubuntu echo hello | MATCH hello @@ -67,8 +74,8 @@ execute: | echo "Install snapd" lxd.lxc exec my-ubuntu -- mkdir -p "$GOHOME" - lxd.lxc file push "$GOHOME"/snapd_*.deb my-ubuntu/$GOPATH/ - lxd.lxc exec my-ubuntu -- dpkg -i "$GOHOME"/snapd_*.deb + lxd.lxc file push "$GOHOME"/snapd_*.deb "my-ubuntu/$GOPATH/" + lxd.lxc exec my-ubuntu -- apt install -y "$GOHOME"/snapd_*.deb echo "Setting up proxy *inside* the container" if [ -n "${http_proxy:-}" ]; then diff --git a/tests/main/media-sharing/task.yaml b/tests/main/media-sharing/task.yaml index a1f9409280..eccf117ba6 100644 --- a/tests/main/media-sharing/task.yaml +++ b/tests/main/media-sharing/task.yaml @@ -5,19 +5,23 @@ details: | Fedora and other systems that build udisks without --enable-fhs-media flag use /run/media path instead. prepare: | - . $TESTSLIB/snaps.sh - . $TESTSLIB/dirs.sh + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh + #shellcheck source=tests/lib/dirs.sh + . "$TESTSLIB"/dirs.sh install_local_devmode test-snapd-tools mkdir -p ${MEDIA_DIR}/src mkdir -p ${MEDIA_DIR}/dst touch ${MEDIA_DIR}/src/canary execute: | - . $TESTSLIB/dirs.sh + #shellcheck source=tests/lib/dirs.sh + . "$TESTSLIB"/dirs.sh test ! -e ${MEDIA_DIR}/dst/canary test-snapd-tools.cmd mount --bind ${MEDIA_DIR}/src ${MEDIA_DIR}/dst test -e ${MEDIA_DIR}/dst/canary restore: | - . $TESTSLIB/dirs.sh + #shellcheck source=tests/lib/dirs.sh + . "$TESTSLIB"/dirs.sh # If this doesn't work maybe it is because the test didn't execute correctly umount ${MEDIA_DIR}/dst || true rm -f ${MEDIA_DIR}/src/canary diff --git a/tests/main/nfs-support/task.yaml b/tests/main/nfs-support/task.yaml index 6abe9f5ee9..8ec5dae86e 100644 --- a/tests/main/nfs-support/task.yaml +++ b/tests/main/nfs-support/task.yaml @@ -5,6 +5,7 @@ details: | permissions sufficient for NFS to operate. systems: [ubuntu-16.04-64] # TODO: expand this list prepare: | + #shellcheck source=tests/lib/snaps.sh . "$TESTSLIB/snaps.sh" install_local test-snapd-sh @@ -39,6 +40,7 @@ execute: | ensure_extra_perms # As a non-root user perform a write over NFS-mounted /home + #shellcheck disable=SC2016 su -c 'snap run test-snapd-sh.with-home-plug -c "touch \$SNAP_USER_DATA/smoke-nfs3-tcp"' test # Unmount /home and restart snapd so that we can check another thing. @@ -59,6 +61,7 @@ execute: | ensure_extra_perms # As a non-root user perform a write over NFS-mounted /home + #shellcheck disable=SC2016 su -c 'snap run test-snapd-sh.with-home-plug -c "touch \$SNAP_USER_DATA/smoke-nfs3-udp"' test # Unmount /home and restart snapd so that we can check another thing. @@ -79,6 +82,7 @@ execute: | ensure_extra_perms # As a non-root user perform a write over NFS-mounted /home + #shellcheck disable=SC2016 su -c 'snap run test-snapd-sh.with-home-plug -c "touch \$SNAP_USER_DATA/smoke-nfs4"' test # Unmount /home and restart snapd so that we can check another thing. @@ -100,6 +104,7 @@ execute: | systemctl restart snapd ensure_extra_perms restore: | + #shellcheck source=tests/lib/pkgdb.sh . "$TESTSLIB/pkgdb.sh" # Unmount NFS mount over /home if one exists. diff --git a/tests/main/op-install-failed-undone/task.yaml b/tests/main/op-install-failed-undone/task.yaml index 79775c9bf5..b4e9b767ab 100644 --- a/tests/main/op-install-failed-undone/task.yaml +++ b/tests/main/op-install-failed-undone/task.yaml @@ -3,23 +3,26 @@ summary: Check that all tasks of a failed installtion are undone systems: [-ubuntu-core-*] restore: | - . $TESTSLIB/dirs.sh + #shellcheck source=tests/lib/dirs.sh + . "$TESTSLIB"/dirs.sh rm -rf $SNAP_MOUNT_DIR/test-snapd-tools execute: | check_empty_glob(){ local base_path=$1 local glob=$2 - [ $(find $base_path -maxdepth 1 -name "$glob" | wc -l) -eq 0 ] + [ "$(find "$base_path" -maxdepth 1 -name "$glob" | wc -l)" -eq 0 ] } - . $TESTSLIB/dirs.sh + #shellcheck source=tests/lib/dirs.sh + . "$TESTSLIB"/dirs.sh echo "Given we make a snap uninstallable" mkdir -p $SNAP_MOUNT_DIR/test-snapd-tools/current/foo echo "And we try to install it" - . $TESTSLIB/snaps.sh + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh if install_local test-snapd-tools; then echo "A snap shouldn't be installable if its mount point is busy" exit 1 @@ -30,23 +33,23 @@ execute: | echo "And the installation task is reported as an error" failed_task_id=$(snap changes | perl -ne 'print $1 if /(\d+) +Error.*?Install \"test-snapd-tools\" snap/') - if [ -z $failed_task_id ]; then + if [ -z "$failed_task_id" ]; then echo "Installation task should be reported as error" exit 1 fi echo "And the Mount subtask is actually undone" - snap change $failed_task_id | grep -Pq "Undone +.*?Mount snap \"test-snapd-tools\"" + snap change "$failed_task_id" | grep -Pq "Undone +.*?Mount snap \"test-snapd-tools\"" check_empty_glob $SNAP_MOUNT_DIR/test-snapd-tools [0-9]+ check_empty_glob /var/lib/snapd/snaps test-snapd-tools_[0-9]+.snap echo "And the Data Copy subtask is actually undone" - snap change $failed_task_id | grep -Pq "Undone +.*?Copy snap \"test-snapd-tools\" data" - check_empty_glob $HOME/snap/test-snapd-tools [0-9]+ + snap change "$failed_task_id" | grep -Pq "Undone +.*?Copy snap \"test-snapd-tools\" data" + check_empty_glob "$HOME"/snap/test-snapd-tools [0-9]+ check_empty_glob /var/snap/test-snapd-tools [0-9]+ echo "And the Security Profiles Setup subtask is actually undone" - snap change $failed_task_id | grep -Pq "Undone +.*?Setup snap \"test-snapd-tools\" \(unset\) security profiles" + snap change "$failed_task_id" | grep -Pq 'Undone +.*?Setup snap "test-snapd-tools" \(unset\) security profiles' check_empty_glob /var/lib/snapd/apparmor/profiles snap.test-snapd-tools.* check_empty_glob /var/lib/snapd/seccomp/bpf snap.test-snapd-tools.* check_empty_glob /etc/dbus-1/system.d snap.test-snapd-tools.*.conf diff --git a/tests/main/op-remove-retry/task.yaml b/tests/main/op-remove-retry/task.yaml index 3c8b67653e..fa9a5286fe 100644 --- a/tests/main/op-remove-retry/task.yaml +++ b/tests/main/op-remove-retry/task.yaml @@ -10,10 +10,12 @@ execute: | while ! snap changes | grep -Pq "$expected"; do sleep 1; done } - . $TESTSLIB/systemd.sh + #shellcheck source=tests/lib/systemd.sh + . "$TESTSLIB"/systemd.sh echo "Given a snap is installed" - . $TESTSLIB/snaps.sh + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh install_local test-snapd-tools echo "And its mount point is kept busy" @@ -23,7 +25,7 @@ execute: | MARKER=/var/snap/test-snapd-tools/current/block-running rm -f $MARKER - systemd_create_and_start_unit unmount-blocker "$(which test-snapd-tools.block)" + systemd_create_and_start_unit unmount-blocker "$(command -v test-snapd-tools.block)" wait_for_service unmount-blocker active while [ ! -f $MARKER ]; do sleep 1; done diff --git a/tests/main/op-remove/task.yaml b/tests/main/op-remove/task.yaml index 8391758796..47a2b8eeae 100644 --- a/tests/main/op-remove/task.yaml +++ b/tests/main/op-remove/task.yaml @@ -5,30 +5,31 @@ restore: | rm -f stderr.out execute: | - . $TESTSLIB/dirs.sh + #shellcheck source=tests/lib/dirs.sh + . "$TESTSLIB"/dirs.sh snap_revisions(){ local snap_name=$1 - echo -n $(find $SNAP_MOUNT_DIR/"$snap_name"/ -maxdepth 1 -type d -name "x*" | wc -l) + echo -n "$(find "$SNAP_MOUNT_DIR/$snap_name/" -maxdepth 1 -type d -name "x*" | wc -l)" } echo "Given two revisions of a snap have been installed" - snap pack $TESTSLIB/snaps/basic + snap pack "$TESTSLIB"/snaps/basic snap install --dangerous basic_1.0_all.snap snap install --dangerous basic_1.0_all.snap echo "Then the two revisions are available on disk" - [ $(snap_revisions basic) = "2" ] + [ "$(snap_revisions basic)" = "2" ] echo "When the snap is removed" snap remove basic echo "Then the two revisions are removed from disk" - [ $(snap_revisions basic) = "0" ] + [ "$(snap_revisions basic)" = "0" ] echo "When the snap is removed again, snap exits with status 0" snap remove basic 2> stderr.out - cat stderr.out | MATCH 'snap "basic" is not installed' + MATCH 'snap "basic" is not installed' < stderr.out echo "Install a snap that uses a base" diff --git a/tests/main/postrm-purge/task.yaml b/tests/main/postrm-purge/task.yaml index 956d121cce..8b7f9fc872 100644 --- a/tests/main/postrm-purge/task.yaml +++ b/tests/main/postrm-purge/task.yaml @@ -4,12 +4,14 @@ systems: [-ubuntu-core-*] execute: | echo "When some snaps are installed" - . $TESTSLIB/snaps.sh + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh install_local test-snapd-tools snap install test-snapd-control-consumer snap install test-snapd-auto-aliases - . $TESTSLIB/dirs.sh + #shellcheck source=tests/lib/dirs.sh + . "$TESTSLIB"/dirs.sh # purge is performed while/after removing the package systemctl stop snapd.service snapd.socket @@ -18,10 +20,10 @@ execute: | # tool if [[ "$SPREAD_SYSTEM" = ubuntu-* || "$SPREAD_SYSTEM" = debian-* ]]; then # only available on trusty - if [ -x ${SPREAD_PATH}/debian/snapd.prerm ]; then - sh -x ${SPREAD_PATH}/debian/snapd.prerm + if [ -x "${SPREAD_PATH}/debian/snapd.prerm" ]; then + sh -x "${SPREAD_PATH}/debian/snapd.prerm" fi - sh -x ${SPREAD_PATH}/debian/snapd.postrm purge + sh -x "${SPREAD_PATH}/debian/snapd.postrm" purge else ${LIBEXECDIR}/snapd/snap-mgmt --purge fi @@ -30,7 +32,7 @@ execute: | for d in $SNAP_MOUNT_DIR /var/snap; do if [ -d "$d" ]; then echo "$d is not removed" - ls -lR $d + ls -lR "$d" exit 1 fi done diff --git a/tests/main/prefer/task.yaml b/tests/main/prefer/task.yaml index 535543141c..b1fb133a0e 100644 --- a/tests/main/prefer/task.yaml +++ b/tests/main/prefer/task.yaml @@ -1,6 +1,7 @@ summary: Simple snap prefer test execute: | - . $TESTSLIB/dirs.sh + #shellcheck source=tests/lib/dirs.sh + . "$TESTSLIB"/dirs.sh echo "Install the snap with auto-aliases" snap install test-snapd-auto-aliases diff --git a/tests/main/prepare-image-grub/task.yaml b/tests/main/prepare-image-grub/task.yaml index b4a39edf58..17c1d66646 100644 --- a/tests/main/prepare-image-grub/task.yaml +++ b/tests/main/prepare-image-grub/task.yaml @@ -19,8 +19,9 @@ prepare: | exit fi - . $TESTSLIB/store.sh - setup_fake_store $STORE_DIR + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + setup_fake_store "$STORE_DIR" restore: | if [ "$TRUST_TEST_KEYS" = "false" ]; then @@ -28,9 +29,10 @@ restore: | exit fi - . $TESTSLIB/store.sh - teardown_fake_store $STORE_DIR - rm -rf $ROOT + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + teardown_fake_store "$STORE_DIR" + rm -rf "$ROOT" execute: | if [ "$TRUST_TEST_KEYS" = "false" ]; then @@ -39,8 +41,8 @@ execute: | fi echo Expose the needed assertions through the fakestore - cp $TESTSLIB/assertions/developer1.account $STORE_DIR/asserts - cp $TESTSLIB/assertions/developer1.account-key $STORE_DIR/asserts + cp "$TESTSLIB"/assertions/developer1.account "$STORE_DIR/asserts" + cp "$TESTSLIB"/assertions/developer1.account-key "$STORE_DIR/asserts" # have snap use the fakestore for assertions (but nothing else) export SNAPPY_FORCE_SAS_URL=http://$STORE_ADDR @@ -48,23 +50,23 @@ execute: | su -c "SNAPPY_USE_STAGING_STORE=$SNAPPY_USE_STAGING_STORE snap prepare-image --channel edge --extra-snaps snapweb $TESTSLIB/assertions/developer1-pc.model $ROOT" test echo Verifying the result - ls -lR $IMAGE + ls -lR "$IMAGE" for f in pc pc-kernel core snapweb; do - ls $IMAGE/var/lib/snapd/seed/snaps/${f}*.snap + ls "$IMAGE"/var/lib/snapd/seed/snaps/"${f}"*.snap done - MATCH snap_core=core < $IMAGE/boot/grub/grubenv - MATCH snap_kernel=pc-kernel < $IMAGE/boot/grub/grubenv + MATCH snap_core=core < "$IMAGE/boot/grub/grubenv" + MATCH snap_kernel=pc-kernel < "$IMAGE/boot/grub/grubenv" # check copied assertions - cmp $TESTSLIB/assertions/developer1-pc.model $IMAGE/var/lib/snapd/seed/assertions/model - cmp $TESTSLIB/assertions/developer1.account $IMAGE/var/lib/snapd/seed/assertions/developer1.account + cmp "$TESTSLIB"/assertions/developer1-pc.model "$IMAGE/var/lib/snapd/seed/assertions/model" + cmp "$TESTSLIB"/assertions/developer1.account "$IMAGE/var/lib/snapd/seed/assertions/developer1.account" echo Verify the unpacked gadget - ls -lR $GADGET - ls $GADGET/meta/snap.yaml + ls -lR "$GADGET" + ls "$GADGET/meta/snap.yaml" echo "Verify that we have valid looking seed.yaml" - cat $IMAGE/var/lib/snapd/seed/seed.yaml + cat "$IMAGE/var/lib/snapd/seed/seed.yaml" # snap-id of core if [ "$REMOTE_STORE" = production ]; then @@ -72,13 +74,13 @@ execute: | else core_snap_id="xMNMpEm0COPZy7jq9YRwWVLCD9q5peow" fi - MATCH "snap-id: ${core_snap_id}" < $IMAGE/var/lib/snapd/seed/seed.yaml + MATCH "snap-id: ${core_snap_id}" < "$IMAGE/var/lib/snapd/seed/seed.yaml" for snap in pc pc-kernel core; do - MATCH "name: $snap" < $IMAGE/var/lib/snapd/seed/seed.yaml + MATCH "name: $snap" < "$IMAGE/var/lib/snapd/seed/seed.yaml" done echo "Verify that we got some snap assertions" for name in pc pc-kernel core; do - cat $IMAGE/var/lib/snapd/seed/assertions/* | MATCH "snap-name: $name" + cat "$IMAGE"/var/lib/snapd/seed/assertions/* | MATCH "snap-name: $name" done diff --git a/tests/main/prepare-image-uboot/task.yaml b/tests/main/prepare-image-uboot/task.yaml index 5285c77fe8..d4d544037f 100644 --- a/tests/main/prepare-image-uboot/task.yaml +++ b/tests/main/prepare-image-uboot/task.yaml @@ -10,16 +10,16 @@ environment: GADGET: /home/test/tmp/gadget prepare: | - mkdir -p $ROOT - chown test:test $ROOT + mkdir -p "$ROOT" + chown test:test "$ROOT" restore: | - rm -rf $ROOT + rm -rf "$ROOT" execute: | # TODO: switch to a prebuilt properly signed model assertion once we can do that consistently echo Creating model assertion - cat > $ROOT/model.assertion <<EOF + cat > "$ROOT/model.assertion" <<EOF type: model series: 16 authority-id: my-brand @@ -41,24 +41,24 @@ execute: | su -c "SNAPPY_USE_STAGING_STORE=$SNAPPY_USE_STAGING_STORE snap prepare-image --channel edge --extra-snaps snapweb $ROOT/model.assertion $ROOT" test echo Verifying the result - ls -lR $IMAGE + ls -lR "$IMAGE" for f in pi2 pi2-kernel core snapweb; do - ls $IMAGE/var/lib/snapd/seed/snaps/${f}*.snap + ls "$IMAGE/var/lib/snapd/seed/snaps/${f}"*.snap done - MATCH snap_core=core < $IMAGE/boot/uboot/uboot.env - MATCH snap_kernel=pi2-kernel < $IMAGE/boot/uboot/uboot.env + MATCH snap_core=core < "$IMAGE/boot/uboot/uboot.env" + MATCH snap_kernel=pi2-kernel < "$IMAGE/boot/uboot/uboot.env" echo Verify that the kernel is available unpacked - ls $IMAGE/boot/uboot/pi2-kernel_*.snap/kernel.img - ls $IMAGE/boot/uboot/pi2-kernel_*.snap/initrd.img - ls $IMAGE/boot/uboot/pi2-kernel_*.snap/dtbs/ + ls "$IMAGE"/boot/uboot/pi2-kernel_*.snap/kernel.img + ls "$IMAGE"/boot/uboot/pi2-kernel_*.snap/initrd.img + ls "$IMAGE"/boot/uboot/pi2-kernel_*.snap/dtbs/ echo Verify the unpacked gadget - ls -lR $GADGET - ls $GADGET/meta/snap.yaml + ls -lR "$GADGET" + ls "$GADGET/meta/snap.yaml" echo Verify that we have valid looking seed.yaml - cat $IMAGE/var/lib/snapd/seed/seed.yaml + cat "$IMAGE/var/lib/snapd/seed/seed.yaml" # snap-id of core if [ "$REMOTE_STORE" = staging ]; then core_id="xMNMpEm0COPZy7jq9YRwWVLCD9q5peow" @@ -66,12 +66,12 @@ execute: | core_id="99T7MUlRhtI3U0QFgl5mXXESAiSwt776" fi - MATCH "snap-id: $core_id" < $IMAGE/var/lib/snapd/seed/seed.yaml + MATCH "snap-id: $core_id" < "$IMAGE/var/lib/snapd/seed/seed.yaml" for snap in pi2 pi2-kernel core; do - MATCH "name: $snap" < $IMAGE/var/lib/snapd/seed/seed.yaml + MATCH "name: $snap" < "$IMAGE/var/lib/snapd/seed/seed.yaml" done echo "Verify that we got some snap assertions" for name in pi2 pi2-kernel core; do - cat $IMAGE/var/lib/snapd/seed/assertions/* | MATCH "snap-name: $name" + cat "$IMAGE"/var/lib/snapd/seed/assertions/* | MATCH "snap-name: $name" done diff --git a/tests/main/refresh-all-undo/task.yaml b/tests/main/refresh-all-undo/task.yaml index 78a9385e57..497dcdb0ee 100644 --- a/tests/main/refresh-all-undo/task.yaml +++ b/tests/main/refresh-all-undo/task.yaml @@ -13,15 +13,16 @@ prepare: | exit fi - . $TESTSLIB/store.sh + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh echo "Given two snaps are installed" for snap in $GOOD_SNAP $BAD_SNAP; do - snap install $snap + snap install "$snap" done echo "And the daemon is configured to point to the fake store" - setup_fake_store $BLOB_DIR + setup_fake_store "$BLOB_DIR" restore: | if [ "$TRUST_TEST_KEYS" = "false" ]; then @@ -29,9 +30,10 @@ restore: | exit fi - . $TESTSLIB/store.sh - teardown_fake_store $BLOB_DIR - rm -rf $BLOB_DIR + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + teardown_fake_store "$BLOB_DIR" + rm -rf "$BLOB_DIR" execute: | if [ "$TRUST_TEST_KEYS" = "false" ]; then @@ -43,15 +45,17 @@ execute: | snap refresh 2>&1 | MATCH "All snaps up to date" echo "When the store is configured to make them refreshable" - . $TESTSLIB/files.sh - . $TESTSLIB/store.sh + #shellcheck source=tests/lib/files.sh + . "$TESTSLIB"/files.sh + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh init_fake_refreshes "$BLOB_DIR" "$GOOD_SNAP" - wait_for_file "$BLOB_DIR"/"${GOOD_SNAP}"*fake1*.snap 4 .5 + wait_for_file "$BLOB_DIR/${GOOD_SNAP}"*fake1*.snap 4 .5 init_fake_refreshes "$BLOB_DIR" "$BAD_SNAP" - wait_for_file "$BLOB_DIR"/"${BAD_SNAP}"*fake1*.snap 4 .5 + wait_for_file "$BLOB_DIR/${BAD_SNAP}"*fake1*.snap 4 .5 echo "When a snap is broken" - echo "i-am-broken-now" >> $BLOB_DIR/${BAD_SNAP}*fake1*.snap + echo "i-am-broken-now" >> "$BLOB_DIR/${BAD_SNAP}"*fake1*.snap echo "And a refresh is performed" if snap refresh ; then @@ -65,16 +69,17 @@ execute: | echo "But the bad snap did not get updated" snap list | MATCH -E "${BAD_SNAP}"| MATCH -v "fake" - . $TESTSLIB/changes.sh + #shellcheck source=tests/lib/changes.sh + . "$TESTSLIB"/changes.sh chg_id=$(change_id "Refresh snap" Error) echo "Verify the snap change" - 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" + 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}\"" + snap tasks "$chg_id" | MATCH "Undone.*Download snap \"${BAD_SNAP}\"" echo "Verify the 'snap tasks --last' shows last refresh change" snap tasks --last=refresh | MATCH "Undone.*Download snap \"${BAD_SNAP}\"" diff --git a/tests/main/refresh-all/task.yaml b/tests/main/refresh-all/task.yaml index 5b61c29be8..5084fad659 100644 --- a/tests/main/refresh-all/task.yaml +++ b/tests/main/refresh-all/task.yaml @@ -16,7 +16,8 @@ prepare: | exit fi - . $TESTSLIB/store.sh + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh echo "Given two snaps are installed" for snap in test-snapd-tools test-snapd-python-webserver; do @@ -24,7 +25,7 @@ prepare: | done echo "And the daemon is configured to point to the fake store" - setup_fake_store $BLOB_DIR + setup_fake_store "$BLOB_DIR" restore: | if [ "$TRUST_TEST_KEYS" = "false" ]; then @@ -32,9 +33,10 @@ restore: | exit fi - . $TESTSLIB/store.sh - teardown_fake_store $BLOB_DIR - rm -rf $BLOB_DIR + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + teardown_fake_store "$BLOB_DIR" + rm -rf "$BLOB_DIR" execute: | if [ "$TRUST_TEST_KEYS" = "false" ]; then @@ -46,12 +48,14 @@ execute: | snap refresh 2>&1 | MATCH "All snaps up to date." echo "When the store is configured to make them refreshable" - . $TESTSLIB/files.sh - . $TESTSLIB/store.sh - init_fake_refreshes $BLOB_DIR test-snapd-tools - wait_for_file $BLOB_DIR/test-snapd-tools*fake1*.snap 4 .5 - init_fake_refreshes $BLOB_DIR test-snapd-python-webserver - wait_for_file $BLOB_DIR/test-snapd-python-webserver*fake1*.snap 4 .5 + #shellcheck source=tests/lib/files.sh + . "$TESTSLIB"/files.sh + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + init_fake_refreshes "$BLOB_DIR" test-snapd-tools + wait_for_file "$BLOB_DIR"/test-snapd-tools*fake1*.snap 4 .5 + init_fake_refreshes "$BLOB_DIR" test-snapd-python-webserver + wait_for_file "$BLOB_DIR"/test-snapd-python-webserver*fake1*.snap 4 .5 echo "And a refresh is performed" snap refresh diff --git a/tests/main/refresh-amend/task.yaml b/tests/main/refresh-amend/task.yaml index 0196761efd..cc7b9b8f6e 100644 --- a/tests/main/refresh-amend/task.yaml +++ b/tests/main/refresh-amend/task.yaml @@ -14,7 +14,7 @@ execute: | echo "snap refresh should error but did not" exit 1 fi - cat stderr.out | MATCH 'local snap "test-snapd-only-in-edge" is unknown to the store' + MATCH 'local snap "test-snapd-only-in-edge" is unknown to the store' < stderr.out echo "A refresh with --amend is not enough, the channel needs to be added" if snap refresh --amend test-snapd-only-in-edge 2> stderr.out; then diff --git a/tests/main/refresh-delta-from-core/task.yaml b/tests/main/refresh-delta-from-core/task.yaml index 28f7192bb5..569299c755 100644 --- a/tests/main/refresh-delta-from-core/task.yaml +++ b/tests/main/refresh-delta-from-core/task.yaml @@ -13,7 +13,7 @@ prepare: | fi echo "Given a snap is installed" - snap install --edge $SNAP_NAME + snap install --edge "$SNAP_NAME" restore: | if [ -e /usr/bin/xdelta3.disabled ]; then @@ -25,6 +25,6 @@ execute: | . "$TESTSLIB/journalctl.sh" echo "When the snap is refreshed" - snap refresh --beta $SNAP_NAME + snap refresh --beta "$SNAP_NAME" echo "Then deltas are successfully applied" get_journalctl_log -u snapd | MATCH "Successfully applied delta" diff --git a/tests/main/refresh-delta/task.yaml b/tests/main/refresh-delta/task.yaml index a48c9ab7b4..2d1b9bc838 100644 --- a/tests/main/refresh-delta/task.yaml +++ b/tests/main/refresh-delta/task.yaml @@ -16,14 +16,14 @@ prepare: | # r3 -> r5b # echo "Given a snap is installed" - snap install --edge $SNAP_NAME + snap install --edge "$SNAP_NAME" execute: | # shellcheck source=tests/lib/journalctl.sh . "$TESTSLIB/journalctl.sh" echo "When the snap is refreshed" - snap refresh --beta $SNAP_NAME + snap refresh --beta "$SNAP_NAME" echo "Then deltas are successfully applied" get_journalctl_log -u snapd | MATCH "Successfully applied delta" diff --git a/tests/main/refresh-devmode/task.yaml b/tests/main/refresh-devmode/task.yaml index 770eb7ff68..90d32a09a0 100644 --- a/tests/main/refresh-devmode/task.yaml +++ b/tests/main/refresh-devmode/task.yaml @@ -34,12 +34,14 @@ prepare: | snap install --devmode test-snapd-tools if [ "$STORE_TYPE" = "fake" ]; then - . $TESTSLIB/store.sh - setup_fake_store $BLOB_DIR + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + setup_fake_store "$BLOB_DIR" echo "And a new version of that snap put in the controlled store" - . $TESTSLIB/store.sh - init_fake_refreshes $BLOB_DIR test-snapd-tools + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + init_fake_refreshes "$BLOB_DIR" test-snapd-tools fi restore: | @@ -54,8 +56,9 @@ restore: | echo "This test needs test keys to be trusted" exit fi - . $TESTSLIB/store.sh - teardown_fake_store $BLOB_DIR + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + teardown_fake_store "$BLOB_DIR" fi execute: | @@ -80,7 +83,7 @@ execute: | # echo "=================================" echo "When the snap is refreshed" - snap refresh --devmode --channel=edge $SNAP_NAME + snap refresh --devmode --channel=edge "$SNAP_NAME" echo "Then the new version is listed" expected="$SNAP_NAME +$SNAP_VERSION_PATTERN .*devmode" diff --git a/tests/main/refresh-undo/task.yaml b/tests/main/refresh-undo/task.yaml index af8f156f14..79741dbe3c 100644 --- a/tests/main/refresh-undo/task.yaml +++ b/tests/main/refresh-undo/task.yaml @@ -15,8 +15,8 @@ environment: prepare: | echo "Given a good (v1) and a bad (v2) snap" - snap pack $TESTSLIB/snaps/$SNAP_NAME_GOOD - snap pack $TESTSLIB/snaps/$SNAP_NAME_BAD + snap pack "$TESTSLIB/snaps/$SNAP_NAME_GOOD" + snap pack "$TESTSLIB/snaps/$SNAP_NAME_BAD" debug: | # shellcheck source=tests/lib/journalctl.sh @@ -37,12 +37,12 @@ execute: | done } echo "When we install v1" - snap install --dangerous ${SNAP_FILE_GOOD} + snap install --dangerous "${SNAP_FILE_GOOD}" echo "The v1 service started correctly" wait_for_service_status "service v1" echo "When we refresh to v2" - if snap install --dangerous ${SNAP_FILE_BAD}; then + if snap install --dangerous "${SNAP_FILE_BAD}"; then echo "The ${SNAP_FILE_BAD} snap should not install cleanly, test broken" exit 1 fi diff --git a/tests/main/refresh/task.yaml b/tests/main/refresh/task.yaml index 8bc6bd3991..545b90f4ba 100644 --- a/tests/main/refresh/task.yaml +++ b/tests/main/refresh/task.yaml @@ -31,7 +31,7 @@ prepare: | fi flags= - if [[ $SNAP_NAME =~ classic ]]; then + if [[ "$SNAP_NAME" =~ classic ]]; then case "$SPREAD_SYSTEM" in ubuntu-core-*|fedora-*|arch-*) exit @@ -41,15 +41,17 @@ prepare: | fi echo "Given a snap is installed" - snap install $flags $SNAP_NAME + snap install $flags "$SNAP_NAME" if [ "$STORE_TYPE" = "fake" ]; then - . $TESTSLIB/store.sh - setup_fake_store $BLOB_DIR + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + setup_fake_store "$BLOB_DIR" echo "And a new version of that snap put in the controlled store" - . $TESTSLIB/store.sh - init_fake_refreshes $BLOB_DIR $SNAP_NAME + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + init_fake_refreshes "$BLOB_DIR" "$SNAP_NAME" fi restore: | @@ -65,8 +67,9 @@ restore: | echo "This test needs test keys to be trusted" exit fi - . $TESTSLIB/store.sh - teardown_fake_store $BLOB_DIR + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + teardown_fake_store "$BLOB_DIR" fi execute: | @@ -83,7 +86,7 @@ execute: | fi fi - if [[ $SNAP_NAME =~ classic ]]; then + if [[ "$SNAP_NAME" =~ classic ]]; then case "$SPREAD_SYSTEM" in ubuntu-core-*|fedora-*|arch-*) exit @@ -99,33 +102,34 @@ execute: | # echo "=================================" echo "When the snap is refreshed" - snap refresh --channel=edge $SNAP_NAME + snap refresh --channel=edge "$SNAP_NAME" echo "Then the new version is listed" expected="$SNAP_NAME +$SNAP_VERSION_PATTERN" snap list | grep -Pzq "$expected" echo "When a snap is refreshed and has no update it exit 0" - snap refresh $SNAP_NAME 2>stderr.out - cat stderr.out | MATCH "snap \"$SNAP_NAME\" has no updates available" + snap refresh "$SNAP_NAME" 2>stderr.out + MATCH "snap \"$SNAP_NAME\" has no updates available" < stderr.out echo "classic snaps " echo "When multiple snaps have no update we have a good message" - . $TESTSLIB/snaps.sh + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh install_local basic - snap refresh $SNAP_NAME basic 2>&1 | MATCH "All snaps up to date." + snap refresh "$SNAP_NAME" basic 2>&1 | MATCH "All snaps up to date." echo "When moving to stable" - snap refresh --stable $SNAP_NAME - snap info $SNAP_NAME | MATCH "tracking: +stable" + snap refresh --stable "$SNAP_NAME" + snap info "$SNAP_NAME" | MATCH "tracking: +stable" - snap refresh --candidate $SNAP_NAME 2>&1 | MATCH "$SNAP_NAME \(candidate\).*" - snap info $SNAP_NAME | MATCH "tracking: +candidate" + snap refresh --candidate "$SNAP_NAME" 2>&1 | MATCH "$SNAP_NAME \\(candidate\\).*" + snap info "$SNAP_NAME" | MATCH "tracking: +candidate" echo "When multiple snaps are refreshed we error if we have unknown names" if snap refresh core invälid-snap-name 2> out.err; then echo "snap refresh invalid-snap-name should fail but it did not?" exit 1 fi - cat out.err | tr '\n' ' ' | tr -s ' ' | MATCH 'cannot refresh .* is not installed' + tr '\n' ' ' < out.err | tr -s ' ' | MATCH 'cannot refresh .* is not installed' diff --git a/tests/main/regression-home-snap-root-owned/task.yaml b/tests/main/regression-home-snap-root-owned/task.yaml index 9da584d15a..d99e0e860e 100644 --- a/tests/main/regression-home-snap-root-owned/task.yaml +++ b/tests/main/regression-home-snap-root-owned/task.yaml @@ -4,22 +4,24 @@ prepare: | # ensure we have no snap user data directory yet rm -rf /home/test/snap rm -rf /root/snap - . $TESTSLIB/snaps.sh + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh install_local test-snapd-tools execute: | - . $TESTSLIB/dirs.sh + #shellcheck source=tests/lib/dirs.sh + . "$TESTSLIB"/dirs.sh # run a snap command via sudo output=$(su -l -c "sudo $SNAP_MOUNT_DIR/bin/test-snapd-tools.env" test) # ensure SNAP_USER_DATA points to the right place - echo $output | MATCH SNAP_USER_DATA=/root/snap/test-snapd-tools/x[0-9]+ - echo $output | MATCH HOME=/root/snap/test-snapd-tools/x[0-9]+ - echo $output | MATCH SNAP_USER_COMMON=/root/snap/test-snapd-tools/common + echo "$output" | MATCH SNAP_USER_DATA=/root/snap/test-snapd-tools/x[0-9]+ + echo "$output" | MATCH HOME=/root/snap/test-snapd-tools/x[0-9]+ + echo "$output" | MATCH SNAP_USER_COMMON=/root/snap/test-snapd-tools/common echo "Verify that the /root/snap directory created and root owned" - if [ $(stat -c '%U' /root/snap) != "root" ]; then + if [ "$(stat -c '%U' /root/snap)" != "root" ]; then echo "The /root/snap directory is not owned by root" ls -ld $SNAP_MOUNT_DIR/snap exit 1 @@ -27,7 +29,7 @@ execute: | echo "Verify that there is no /home/test/snap appearing" if [ -e /home/test/snap ]; then - user=$(stat -c '%U' /home/test/snap) + user="$(stat -c '%U' /home/test/snap)" echo "An unexpected /home/test/snap directory got created (owner $user)" ls -ld /home/test/snap exit 1 diff --git a/tests/main/remove-errors/task.yaml b/tests/main/remove-errors/task.yaml index a473f750b6..421fb9e6d8 100644 --- a/tests/main/remove-errors/task.yaml +++ b/tests/main/remove-errors/task.yaml @@ -5,13 +5,15 @@ systems: [-ubuntu-core-18-*] execute: | echo "Given a core snap is installed" + #shellcheck source=tests/lib/snaps.sh . "$TESTSLIB/snaps.sh" install_local test-snapd-tools + #shellcheck source=tests/lib/names.sh . "$TESTSLIB/names.sh" echo "Ensure the important snaps can not be removed" for sn in core $kernel_name $gadget_name; do - if snap remove $sn; then + if snap remove "$sn"; then echo "It should not be possible to remove $sn" exit 1 fi diff --git a/tests/main/revert-devmode/task.yaml b/tests/main/revert-devmode/task.yaml index 3977d685b4..9725cd6454 100644 --- a/tests/main/revert-devmode/task.yaml +++ b/tests/main/revert-devmode/task.yaml @@ -23,12 +23,14 @@ prepare: | snap install --devmode test-snapd-tools if [ "$STORE_TYPE" = "fake" ]; then - . $TESTSLIB/store.sh - setup_fake_store $BLOB_DIR + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + setup_fake_store "$BLOB_DIR" echo "And a new version of that snap put in the controlled store" - . $TESTSLIB/store.sh - init_fake_refreshes $BLOB_DIR test-snapd-tools + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + init_fake_refreshes "$BLOB_DIR" test-snapd-tools fi restore: | @@ -43,8 +45,9 @@ restore: | echo "This test needs test keys to be trusted" exit fi - . $TESTSLIB/store.sh - teardown_fake_store $BLOB_DIR + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + teardown_fake_store "$BLOB_DIR" fi execute: | @@ -61,7 +64,8 @@ execute: | fi fi - . $TESTSLIB/dirs.sh + #shellcheck source=tests/lib/dirs.sh + . "$TESTSLIB"/dirs.sh echo "When a refresh is made" snap refresh --devmode --edge test-snapd-tools @@ -80,7 +84,7 @@ execute: | snap list|MATCH 'test-snapd-tools .* devmode' echo "When the latest revision is installed again" - snap remove --revision=$LATEST test-snapd-tools + snap remove --revision="$LATEST" test-snapd-tools snap refresh --edge test-snapd-tools if [ "$(snap debug confinement)" = strict ] ; then diff --git a/tests/main/revert-sideload/task.yaml b/tests/main/revert-sideload/task.yaml index 24a7259965..ef085c86ae 100644 --- a/tests/main/revert-sideload/task.yaml +++ b/tests/main/revert-sideload/task.yaml @@ -1,7 +1,7 @@ summary: Checks for snap sideload reverts prepare: | - snap pack $TESTSLIB/snaps/basic + snap pack "$TESTSLIB"/snaps/basic restore: | rm -f ./basic_1.0_all.snap diff --git a/tests/main/revert/task.yaml b/tests/main/revert/task.yaml index b290595832..d5e28eaa86 100644 --- a/tests/main/revert/task.yaml +++ b/tests/main/revert/task.yaml @@ -23,12 +23,14 @@ prepare: | snap install test-snapd-tools if [ "$STORE_TYPE" = "fake" ]; then - . $TESTSLIB/store.sh - setup_fake_store $BLOB_DIR + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + setup_fake_store "$BLOB_DIR" echo "And a new version of that snap put in the controlled store" - . $TESTSLIB/store.sh - init_fake_refreshes $BLOB_DIR test-snapd-tools + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + init_fake_refreshes "$BLOB_DIR" test-snapd-tools fi restore: | @@ -43,8 +45,9 @@ restore: | echo "This test needs test keys to be trusted" exit fi - . $TESTSLIB/store.sh - teardown_fake_store $BLOB_DIR + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + teardown_fake_store "$BLOB_DIR" fi execute: | @@ -67,13 +70,14 @@ execute: | exit 1 fi - . $TESTSLIB/dirs.sh + #shellcheck source=tests/lib/dirs.sh + . "$TESTSLIB"/dirs.sh echo "When a refresh is made" snap refresh --edge test-snapd-tools echo "Then the new version is installed" - snap list | MATCH "test-snapd-tools +[0-9]+\.[0-9]+\+fake1" + snap list | MATCH -- 'test-snapd-tools +[0-9]+\.[0-9]+\+fake1' echo "And the snap runs" test-snapd-tools.echo hello|MATCH hello @@ -82,11 +86,11 @@ execute: | snap revert test-snapd-tools echo "Then the old version is active" - snap list | MATCH "test-snapd-tools +[0-9]+\.[0-9]+ " + snap list | MATCH -- 'test-snapd-tools +[0-9]+\.[0-9]+ ' echo "And the data directories are present" - ls $SNAP_MOUNT_DIR/test-snapd-tools | MATCH current - ls /var/snap/test-snapd-tools | MATCH current + find $SNAP_MOUNT_DIR/test-snapd-tools -maxdepth 1 | MATCH current + find /var/snap/test-snapd-tools -maxdepth 1 | MATCH current echo "And the snap runs confined" snap list|MATCH 'test-snapd-tools.* -$' @@ -102,8 +106,8 @@ execute: | echo "And a refresh doesn't update the snap" snap refresh - snap list | MATCH "test-snapd-tools +[0-9]+\.[0-9]+ " + snap list | MATCH -- 'test-snapd-tools +[0-9]+\.[0-9]+ ' echo "Unless the snap is asked for explicitly" snap refresh --edge test-snapd-tools - snap list | MATCH "test-snapd-tools +[0-9]+\.[0-9]+\+fake1" + snap list | MATCH -- 'test-snapd-tools +[0-9]+\.[0-9]+\+fake1' diff --git a/tests/main/searching/task.yaml b/tests/main/searching/task.yaml index 713962c378..63eb861fbe 100644 --- a/tests/main/searching/task.yaml +++ b/tests/main/searching/task.yaml @@ -12,9 +12,7 @@ restore: | execute: | echo "List all featured snaps" - expected="(?s).*Name +Version +Publisher +Notes +Summary *\n\ - (.*?\n)?\ - .*" + expected='(?s).*Name +Version +Publisher +Notes +Summary *\n(.*?\n)?.*' snap find > featured.txt if ! grep -Pzq "$expected" < featured.txt; then echo "expected out put $expected not found in:" @@ -24,12 +22,12 @@ execute: | MATCH "No search term specified. Here are some interesting snaps" < featured.txt MATCH "Provide a search term for more specific results." < featured.txt - if [ $(snap find | wc -l) -gt 50 ]; then + if [ "$(snap find | wc -l)" -gt 50 ]; then echo "Found more than 50 featured apps, this seems bogus:" snap find exit 1 fi - if [ $(snap find | wc -l) -lt 2 ]; then + if [ "$(snap find | wc -l)" -lt 2 ]; then echo "Not found any featured app, this seems bogus:" snap find exit 1 @@ -38,28 +36,22 @@ execute: | echo "Exact matches" for snapName in test-snapd-tools test-snapd-python-webserver do - expected="(?s)Name +Version +Publisher +Notes +Summary *\n\ - (.*?\n)?\ - $snapName +.*? *\n\ - .*" + expected="(?s)Name +Version +Publisher +Notes +Summary *\\n(.*?\\n)?${snapName} +.*? *\\n.*" snap find $snapName | grep -Pzq "$expected" done echo "Partial terms work too" - expected="(?s)Name +Version +Publisher +Notes +Summary *\n\ - (.*?\n)?\ - test-snapd-tools +.*? *\n\ - .*" + expected='(?s)Name +Version +Publisher +Notes +Summary *\n(.*?\n)?test-snapd-tools +.*? *\n.*' snap find test-snapd- | grep -Pzq "$expected" echo "List of snaps in a section works" # NOTE: this shows featured snaps which change all the time, do not # make any assumptions about the contents - test $(snap find --section=featured | wc -l) -gt 1 + test "$(snap find --section=featured | wc -l)" -gt 1 # TODO: discuss with the store how we can make this test stable, i.e. # that section/snap changes do not break us - if [ $(uname -m) = "x86_64" ]; then + if [ "$(uname -m)" = "x86_64" ]; then snap find --section=video vlc | MATCH vlc else snap find --section=video vlc 2>&1 | MATCH 'No matching snaps' @@ -69,4 +61,4 @@ execute: | if snap find " " | grep "status code 403"; then echo 'snap find " " returns non user friendly error with whitespace query' exit 1 - fi \ No newline at end of file + fi diff --git a/tests/main/security-apparmor/task.yaml b/tests/main/security-apparmor/task.yaml index b2082aa138..fdee9fdee5 100644 --- a/tests/main/security-apparmor/task.yaml +++ b/tests/main/security-apparmor/task.yaml @@ -2,7 +2,8 @@ summary: Check basic apparmor confinement rules. prepare: | echo "Given a basic snap is installed" - . $TESTSLIB/snaps.sh + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh install_local test-snapd-tools execute: | if [ "$(snap debug confinement)" = partial ] ; then diff --git a/tests/main/security-device-cgroups-classic/task.yaml b/tests/main/security-device-cgroups-classic/task.yaml index 7191cfc2eb..872fafc781 100644 --- a/tests/main/security-device-cgroups-classic/task.yaml +++ b/tests/main/security-device-cgroups-classic/task.yaml @@ -18,7 +18,8 @@ prepare: | fi echo "Given a snap declaring a plug on framebuffer is installed in classic" - . $TESTSLIB/snaps.sh + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh install_local_classic test-classic-cgroup restore: | @@ -27,11 +28,12 @@ restore: | fi execute: | - . $TESTSLIB/dirs.sh + #shellcheck source=tests/lib/dirs.sh + . "$TESTSLIB"/dirs.sh # classic snaps don't use 'plugs', so just test the accesses after install echo "the classic snap can access the framebuffer" "$SNAP_MOUNT_DIR"/bin/test-classic-cgroup.read-fb 2>&1 | MATCH -v '(Permission denied|Operation not permitted)' echo "the classic snap can access other devices" - test "`$SNAP_MOUNT_DIR/bin/test-classic-cgroup.read-kmsg`" + test "$($SNAP_MOUNT_DIR/bin/test-classic-cgroup.read-kmsg)" diff --git a/tests/main/security-device-cgroups-devmode/task.yaml b/tests/main/security-device-cgroups-devmode/task.yaml index 0259c4ee37..c4bdb1833f 100644 --- a/tests/main/security-device-cgroups-devmode/task.yaml +++ b/tests/main/security-device-cgroups-devmode/task.yaml @@ -14,7 +14,8 @@ prepare: | fi echo "Given a snap declaring a plug on framebuffer is installed in devmode" - . $TESTSLIB/snaps.sh + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh install_local_devmode test-devmode-cgroup restore: | @@ -23,7 +24,8 @@ restore: | fi execute: | - . $TESTSLIB/dirs.sh + #shellcheck source=tests/lib/dirs.sh + . "$TESTSLIB"/dirs.sh echo "And the framebuffer plug is connected" snap connect test-devmode-cgroup:framebuffer @@ -31,7 +33,7 @@ execute: | "$SNAP_MOUNT_DIR"/bin/test-devmode-cgroup.read-fb 2>&1 | MATCH -v '(Permission denied|Operation not permitted)' echo "the devmode snap can access other devices" - test "`$SNAP_MOUNT_DIR/bin/test-devmode-cgroup.read-kmsg`" + test "$($SNAP_MOUNT_DIR/bin/test-devmode-cgroup.read-kmsg)" echo "And the framebuffer plug is disconnected" snap disconnect test-devmode-cgroup:framebuffer @@ -39,4 +41,4 @@ execute: | "$SNAP_MOUNT_DIR"/bin/test-devmode-cgroup.read-fb 2>&1 | MATCH -v '(Permission denied|Operation not permitted)' echo "the devmode snap can access other devices" - test "`$SNAP_MOUNT_DIR/bin/test-devmode-cgroup.read-kmsg`" + test "$($SNAP_MOUNT_DIR/bin/test-devmode-cgroup.read-kmsg)" diff --git a/tests/main/security-device-cgroups-jailmode/task.yaml b/tests/main/security-device-cgroups-jailmode/task.yaml index e2ed362a7c..9604f46715 100644 --- a/tests/main/security-device-cgroups-jailmode/task.yaml +++ b/tests/main/security-device-cgroups-jailmode/task.yaml @@ -17,7 +17,8 @@ prepare: | fi echo "Given a snap declaring a plug on framebuffer is installed in jailmode" - . $TESTSLIB/snaps.sh + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh install_local_jailmode test-devmode-cgroup restore: | @@ -26,7 +27,8 @@ restore: | fi execute: | - . $TESTSLIB/dirs.sh + #shellcheck source=tests/lib/dirs.sh + . "$TESTSLIB"/dirs.sh echo "And the framebuffer plug is connected" snap connect test-devmode-cgroup:framebuffer diff --git a/tests/main/security-device-cgroups-serial-port/task.yaml b/tests/main/security-device-cgroups-serial-port/task.yaml index fca36bc891..e3c5a5acb0 100644 --- a/tests/main/security-device-cgroups-serial-port/task.yaml +++ b/tests/main/security-device-cgroups-serial-port/task.yaml @@ -22,7 +22,8 @@ restore: | execute: | echo "Given a snap is installed" - . $TESTSLIB/snaps.sh + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh install_local test-snapd-tools echo "Then the device is not assigned to that snap" diff --git a/tests/main/security-device-cgroups-strict/task.yaml b/tests/main/security-device-cgroups-strict/task.yaml index c3937cd9d7..912413ec0c 100644 --- a/tests/main/security-device-cgroups-strict/task.yaml +++ b/tests/main/security-device-cgroups-strict/task.yaml @@ -16,7 +16,8 @@ prepare: | fi echo "Given a snap declaring a plug on framebuffer is installed in strict" - . $TESTSLIB/snaps.sh + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh install_local test-strict-cgroup restore: | @@ -25,7 +26,8 @@ restore: | fi execute: | - . $TESTSLIB/dirs.sh + #shellcheck source=tests/lib/dirs.sh + . "$TESTSLIB"/dirs.sh echo "And the framebuffer plug is connected" snap connect test-strict-cgroup:framebuffer diff --git a/tests/main/security-device-cgroups/task.yaml b/tests/main/security-device-cgroups/task.yaml index f4d4936a93..914a6ce68e 100644 --- a/tests/main/security-device-cgroups/task.yaml +++ b/tests/main/security-device-cgroups/task.yaml @@ -77,11 +77,12 @@ execute: | fi echo "Given a snap is installed" - . $TESTSLIB/snaps.sh + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh install_local test-snapd-tools echo "Then the device is not assigned to that snap" - ! udevadm info $UDEVADM_PATH | MATCH "E: TAGS=.*snap_test-snapd-tools_env" + ! udevadm info "$UDEVADM_PATH" | MATCH "E: TAGS=.*snap_test-snapd-tools_env" echo "And the device is not shown in the snap device list" # FIXME: this is, apparently, a layered can of worms. Zyga says he needs to fix it. @@ -100,10 +101,10 @@ execute: | udevadm settle echo "Then the device is shown as assigned to the snap" - udevadm info $UDEVADM_PATH | MATCH "E: TAGS=.*snap_test-snapd-tools_env" + udevadm info "$UDEVADM_PATH" | MATCH "E: TAGS=.*snap_test-snapd-tools_env" echo "And other devices are not shown as assigned to the snap" - udevadm info $OTHER_UDEVADM_PATH | MATCH -v "E: TAGS=.*snap_test-snapd-tools_env" + udevadm info "$OTHER_UDEVADM_PATH" | MATCH -v "E: TAGS=.*snap_test-snapd-tools_env" echo "=================================================" diff --git a/tests/main/security-devpts/task.yaml b/tests/main/security-devpts/task.yaml index 6d633b07b7..95083dea41 100644 --- a/tests/main/security-devpts/task.yaml +++ b/tests/main/security-devpts/task.yaml @@ -6,7 +6,8 @@ execute: | fi echo "Given a basic snap is installed" - . $TESTSLIB/snaps.sh + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh install_local test-snapd-devpts echo "When no plugs are not connected" diff --git a/tests/main/security-private-tmp/task.yaml b/tests/main/security-private-tmp/task.yaml index 7b20a18cc8..d705327002 100644 --- a/tests/main/security-private-tmp/task.yaml +++ b/tests/main/security-private-tmp/task.yaml @@ -8,21 +8,23 @@ environment: prepare: | echo "Given a basic snap is installed" - . $TESTSLIB/snaps.sh + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh install_local test-snapd-tools echo "And another basic snap is installed" - mkdir -p $SNAP_INSTALL_DIR - cp -ra $TESTSLIB/snaps/test-snapd-tools/* $SNAP_INSTALL_DIR - sed -i 's/test-snapd-tools/not-test-snapd-tools/g' $SNAP_INSTALL_DIR/meta/snap.yaml - snap pack $SNAP_INSTALL_DIR + mkdir -p "$SNAP_INSTALL_DIR" + cp -ra "$TESTSLIB"/snaps/test-snapd-tools/* "$SNAP_INSTALL_DIR" + sed -i 's/test-snapd-tools/not-test-snapd-tools/g' "$SNAP_INSTALL_DIR/meta/snap.yaml" + snap pack "$SNAP_INSTALL_DIR" snap install --dangerous not-test-snapd-tools_1.0_all.snap restore: | - rm -rf not-test-snapd-tools_1.0_all.snap "$SNAP_INSTALL_DIR" /tmp/foo *stat.error + rm -rf not-test-snapd-tools_1.0_all.snap "$SNAP_INSTALL_DIR" /tmp/foo ./*stat.error execute: | - . $TESTSLIB/dirs.sh + #shellcheck source=tests/lib/dirs.sh + . "$TESTSLIB"/dirs.sh echo "When a temporary file is created by one snap" expect -d -f tmp-create.exp diff --git a/tests/main/security-profiles/task.yaml b/tests/main/security-profiles/task.yaml index 654b06c236..5b9380db6e 100644 --- a/tests/main/security-profiles/task.yaml +++ b/tests/main/security-profiles/task.yaml @@ -1,7 +1,7 @@ summary: Check security profile generation for apps and hooks. prepare: | - snap pack $TESTSLIB/snaps/basic-hooks + snap pack "$TESTSLIB"/snaps/basic-hooks restore: | rm -f basic-hooks_1.0_all.snap @@ -13,13 +13,14 @@ execute: | seccomp_profile_directory="/var/lib/snapd/seccomp/bpf" echo "Security profiles are generated and loaded for apps" - . $TESTSLIB/snaps.sh + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh install_local test-snapd-tools loaded_profiles=$(cat /sys/kernel/security/apparmor/profiles) for profile in snap.test-snapd-tools.block snap.test-snapd-tools.cat snap.test-snapd-tools.echo snap.test-snapd-tools.fail snap.test-snapd-tools.success do - MATCH "^${profile} \(enforce\)$" <<<"$loaded_profiles" + MATCH "^${profile} \\(enforce\\)$" <<<"$loaded_profiles" [ -f "$seccomp_profile_directory/${profile}.bin" ] done @@ -27,5 +28,5 @@ execute: | snap install --dangerous basic-hooks_1.0_all.snap loaded_profiles=$(cat /sys/kernel/security/apparmor/profiles) - echo "$loaded_profiles" | MATCH "^snap.basic-hooks.hook.configure \(enforce\)$" + echo "$loaded_profiles" | MATCH '^snap.basic-hooks.hook.configure \(enforce\)$' [ -f "$seccomp_profile_directory/snap.basic-hooks.hook.configure.bin" ] diff --git a/tests/main/security-setuid-root/task.yaml b/tests/main/security-setuid-root/task.yaml index 486106cb17..1c963acb02 100644 --- a/tests/main/security-setuid-root/task.yaml +++ b/tests/main/security-setuid-root/task.yaml @@ -14,7 +14,8 @@ details: | the usual location (/usr/lib/snapd/snap-confine). As a security precaution it should detect and refuse to run if invoked from the core snap. prepare: | - . $TESTSLIB/snaps.sh + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh install_local test-snapd-tools echo "Ensure the snap-confine profiles on core are not loaded" for p in /var/lib/snapd/apparmor/profiles/snap-confine.*; do @@ -23,10 +24,11 @@ prepare: | restore: | echo "Ensure the snap-confine profiles are restored" for p in /var/lib/snapd/apparmor/profiles/snap-confine.*; do - apparmor_parser -r $p + apparmor_parser -r "$p" done execute: | - . $TESTSLIB/dirs.sh + #shellcheck source=tests/lib/dirs.sh + . "$TESTSLIB"/dirs.sh # 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 @@ -37,7 +39,7 @@ execute: | fi su test -c "sh -c \"SNAP_NAME=test-snapd-tools $SNAP_MOUNT_DIR/core/current/usr/lib/snapd/snap-confine snap.test-snapd-tools.cmd /bin/true 2>&1\"" | MATCH "Refusing to continue to avoid permission escalation attacks" debug: | - ls -ld $SNAP_MOUNT_DIR/core/current/usr/lib/snapd/snap-confine || true - ls -ld $SNAP_MOUNT_DIR/ubuntu-core/current/usr/lib/snapd/snap-confine || true + ls -ld "$SNAP_MOUNT_DIR/core/current/usr/lib/snapd/snap-confine" || true + ls -ld "$SNAP_MOUNT_DIR/ubuntu-core/current/usr/lib/snapd/snap-confine" || true ls -ld /usr/lib/snapd/snap-confine || true snap list diff --git a/tests/main/security-udev-input-subsystem/task.yaml b/tests/main/security-udev-input-subsystem/task.yaml index 00e653e1d9..d49d35f7f2 100644 --- a/tests/main/security-udev-input-subsystem/task.yaml +++ b/tests/main/security-udev-input-subsystem/task.yaml @@ -50,7 +50,7 @@ execute: | echo "When the mir plug is disconnected" snap disconnect test-snapd-udev-input-subsystem:mir-plug test-snapd-udev-input-subsystem:mir-slot - snap interfaces -i mir | MATCH "\- +test-snapd-udev-input-subsystem:mir-plug" + snap interfaces -i mir | MATCH -- '- +test-snapd-udev-input-subsystem:mir-plug' echo "The snap's plug still cannot access an evdev keyboard" if test-snapd-udev-input-subsystem.plug 2>"${PWD}"/call.error; then @@ -61,7 +61,7 @@ execute: | MATCH "Permission denied" < call.error echo "When the time-control plug is disconnected" - snap interfaces -i time-control | MATCH ":time-control +\-" + snap interfaces -i time-control | MATCH ':time-control +-' echo "The snap's time-control plug cannot access an evdev keyboard when disconnected" if test-snapd-udev-input-subsystem.plug-with-time-control 2>"${PWD}"/call.error; then diff --git a/tests/main/server-snap/task.yaml b/tests/main/server-snap/task.yaml index 2078982797..c85e58ac1c 100644 --- a/tests/main/server-snap/task.yaml +++ b/tests/main/server-snap/task.yaml @@ -18,14 +18,14 @@ environment: warn-timeout: 3m prepare: | - snap install $SNAP_NAME + snap install "$SNAP_NAME" cat > request.txt <<EOF GET / HTTP/1.0 EOF echo "Wait for the service to be listening, limited to the task kill-timeout" # shellcheck source=tests/lib/network.sh - . $TESTSLIB/network.sh + . "$TESTSLIB"/network.sh wait_listen_port "$PORT" restore: | @@ -34,6 +34,6 @@ restore: | execute: | response=$(nc -w 5 -"$IP_VERSION" "$LOCALHOST" "$PORT" < request.txt) - statusPattern="(?s)HTTP\/1\.0 200 OK\n*" + statusPattern='(?s)HTTP\/1\.0 200 OK\n*' echo "$response" | grep -Pzq "$statusPattern" echo "$response" | grep -Pzq "$TEXT" diff --git a/tests/main/set-proxy-store/task.yaml b/tests/main/set-proxy-store/task.yaml index e74c23edda..60b4055670 100644 --- a/tests/main/set-proxy-store/task.yaml +++ b/tests/main/set-proxy-store/task.yaml @@ -12,10 +12,11 @@ prepare: | fi echo "Given a snap is installed" - snap install $SNAP_NAME + snap install "$SNAP_NAME" - . $TESTSLIB/store.sh - setup_fake_store $BLOB_DIR + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + setup_fake_store "$BLOB_DIR" # undo the setup through envvars systemctl stop snapd.service snapd.socket rm /etc/systemd/system/snapd.service.d/store.conf @@ -23,16 +24,18 @@ prepare: | systemctl start snapd.socket # prepare bundle - cat $TESTSLIB/assertions/testrootorg-store.account-key >fake.store + cat "$TESTSLIB"/assertions/testrootorg-store.account-key >fake.store + #shellcheck disable=SC2129 echo >>fake.store - cat $TESTSLIB/assertions/developer1.account >>fake.store + cat "$TESTSLIB"/assertions/developer1.account >>fake.store + #shellcheck disable=SC2129 echo >>fake.store - cat $TESTSLIB/assertions/fake.store >>fake.store + cat "$TESTSLIB"/assertions/fake.store >>fake.store echo "Ack fake store assertion" snap ack fake.store echo "And a new version of that snap put in the controlled store" - init_fake_refreshes $BLOB_DIR $SNAP_NAME + init_fake_refreshes "$BLOB_DIR" "$SNAP_NAME" restore: | rm -f fake.store @@ -44,8 +47,9 @@ restore: | snap set core proxy.store= - . $TESTSLIB/store.sh - teardown_fake_store $BLOB_DIR + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + teardown_fake_store "$BLOB_DIR" execute: | if [ "$TRUST_TEST_KEYS" = "false" ]; then @@ -68,7 +72,7 @@ execute: | snap set core proxy.store=fake echo "Now we can proceed with the refresh from the fakestore" - snap refresh $SNAP_NAME + snap refresh "$SNAP_NAME" echo "Then the new version is listed" snap list | grep -Pzq "$expected" diff --git a/tests/main/snap-advise-command/task.yaml b/tests/main/snap-advise-command/task.yaml index 1a68a9b5c4..e07b7faa86 100644 --- a/tests/main/snap-advise-command/task.yaml +++ b/tests/main/snap-advise-command/task.yaml @@ -15,7 +15,7 @@ restore: | execute: | echo "wait for snapd to pull in the commands data" echo "(it will do that on startup)" - for i in $(seq 120); do + for _ in $(seq 120); do if stat /var/cache/snapd/commands.db; then break fi @@ -23,17 +23,17 @@ execute: | done stat /var/cache/snapd/commands.db echo "Ensure the database is readable by a regular user" - if [ $(stat -c "%a" /var/cache/snapd/commands.db) != "644" ]; then + if [ "$(stat -c '%a' /var/cache/snapd/commands.db)" != "644" ]; then echo "incorrect permissions for /var/cache/snapd/commands.db" echo "expected 0644 got:" stat /var/cache/snapd/commands.db exit 1 fi - echo "Ensure `snap advise-snap --command` lookup works" + echo "Ensure 'snap advise-snap --command' lookup works" snap advise-snap --command test-snapd-tools.echo | MATCH test-snapd-tools - echo "Ensure `advise-snap --command` works as command-not-found symlink" + echo "Ensure 'advise-snap --command' works as command-not-found symlink" ln -s /usr/bin/snap /usr/lib/command-not-found /usr/lib/command-not-found test-snapd-tools.echo | MATCH test-snapd-tools diff --git a/tests/unit/spread-shellcheck/can-fail b/tests/unit/spread-shellcheck/can-fail index eeea62d9ba..dd7bf5ef9e 100644 --- a/tests/unit/spread-shellcheck/can-fail +++ b/tests/unit/spread-shellcheck/can-fail @@ -1,57 +1,3 @@ -tests/main/interfaces-snapd-control-with-manage/task.yaml -tests/main/interfaces-ssh-keys/task.yaml -tests/main/interfaces-ssh-public-keys/task.yaml -tests/main/interfaces-system-observe/task.yaml -tests/main/interfaces-time-control/task.yaml -tests/main/interfaces-timezone-control/task.yaml -tests/main/interfaces-udev/task.yaml -tests/main/interfaces-uhid/task.yaml -tests/main/interfaces-upower-observe/task.yaml -tests/main/interfaces-wayland/task.yaml -tests/main/kernel-snap-refresh-on-core/task.yaml -tests/main/known-remote/task.yaml -tests/main/layout/task.yaml -tests/main/listing/task.yaml -tests/main/login/task.yaml -tests/main/lxd/task.yaml -tests/main/media-sharing/task.yaml -tests/main/nfs-support/task.yaml -tests/main/op-install-failed-undone/task.yaml -tests/main/op-remove-retry/task.yaml -tests/main/op-remove/task.yaml -tests/main/postrm-purge/task.yaml -tests/main/prefer/task.yaml -tests/main/prepare-image-grub/task.yaml -tests/main/prepare-image-uboot/task.yaml -tests/main/refresh-all-undo/task.yaml -tests/main/refresh-all/task.yaml -tests/main/refresh-amend/task.yaml -tests/main/refresh-delta-from-core/task.yaml -tests/main/refresh-delta/task.yaml -tests/main/refresh-devmode/task.yaml -tests/main/refresh-undo/task.yaml -tests/main/refresh/task.yaml -tests/main/regression-home-snap-root-owned/task.yaml -tests/main/remove-errors/task.yaml -tests/main/revert-devmode/task.yaml -tests/main/revert-sideload/task.yaml -tests/main/revert/task.yaml -tests/main/searching/task.yaml -tests/main/security-apparmor/task.yaml -tests/main/security-device-cgroups-classic/task.yaml -tests/main/security-device-cgroups-devmode/task.yaml -tests/main/security-device-cgroups-jailmode/task.yaml -tests/main/security-device-cgroups-serial-port/task.yaml -tests/main/security-device-cgroups-strict/task.yaml -tests/main/security-device-cgroups/task.yaml -tests/main/security-devpts/task.yaml -tests/main/security-private-tmp/task.yaml -tests/main/security-profiles/task.yaml -tests/main/security-setuid-root/task.yaml -tests/main/security-udev-input-subsystem/task.yaml -tests/main/server-snap/task.yaml -tests/main/set-proxy-store/task.yaml -tests/main/snap-advise-command/task.yaml tests/main/snap-auto-import-asserts-spools/task.yaml tests/main/snap-auto-import-asserts/task.yaml tests/main/snap-auto-mount/task.yaml diff --git a/testutil/dbustest.go b/testutil/dbustest.go index 17c7036732..b8c6afe1ed 100644 --- a/testutil/dbustest.go +++ b/testutil/dbustest.go @@ -20,6 +20,7 @@ package testutil import ( + "bufio" "fmt" "os" "os/exec" @@ -50,10 +51,17 @@ func (s *DBusTest) SetUpSuite(c *C) { } s.tmpdir = c.MkDir() - s.dbusDaemon = exec.Command("dbus-daemon", "--session", fmt.Sprintf("--address=unix:%s/user_bus_socket", s.tmpdir)) - err := s.dbusDaemon.Start() + s.dbusDaemon = exec.Command("dbus-daemon", "--session", "--print-address", fmt.Sprintf("--address=unix:path=%s/user_bus_socket", s.tmpdir)) + pout, err := s.dbusDaemon.StdoutPipe() c.Assert(err, IsNil) + err = s.dbusDaemon.Start() + c.Assert(err, IsNil) + + scanner := bufio.NewScanner(pout) + scanner.Scan() + c.Assert(scanner.Err(), IsNil) s.oldSessionBusEnv = os.Getenv("DBUS_SESSION_BUS_ADDRESS") + os.Setenv("DBUS_SESSION_BUS_ADDRESS", scanner.Text()) s.SessionBus, err = dbus.SessionBus() c.Assert(err, IsNil) @@ -64,6 +72,8 @@ func (s *DBusTest) TearDownSuite(c *C) { if s.dbusDaemon != nil && s.dbusDaemon.Process != nil { err := s.dbusDaemon.Process.Kill() c.Assert(err, IsNil) + err = s.dbusDaemon.Wait() // do cleanup + c.Assert(err, ErrorMatches, `signal: killed`) } } |
