diff options
| author | Michael Vogt <mvo@ubuntu.com> | 2021-01-26 14:32:24 +0100 |
|---|---|---|
| committer | Michael Vogt <mvo@ubuntu.com> | 2021-01-26 14:32:24 +0100 |
| commit | 7a714c089bcf344b32a5f174c47b6167b7404da1 (patch) | |
| tree | 5f6af65ebab212f88280aa7be1fdfdad0d4fb7b2 | |
| parent | 7ec4e4028e085849c9b761c90ea96f29a26ed8ca (diff) | |
| parent | 93decd662055ad69f090a783576e8ed7c75e4bc9 (diff) | |
Merge remote-tracking branch 'upstream/master' into snapshots/forget-conflictsnapshots/forget-conflict
27 files changed, 847 insertions, 97 deletions
diff --git a/cmd/libsnap-confine-private/classic-test.c b/cmd/libsnap-confine-private/classic-test.c index 889b1f1fd6..5a7e098faa 100644 --- a/cmd/libsnap-confine-private/classic-test.c +++ b/cmd/libsnap-confine-private/classic-test.c @@ -176,6 +176,37 @@ static void test_is_on_custom_base(void) g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CORE_OTHER); } +static const char *os_release_debian_like_valid = "" + "ID=my-fun-distro\n" + "ID_LIKE=debian\n"; + +static const char *os_release_debian_like_quoted_valid = "" + "ID=my-fun-distro\n" + "ID_LIKE=\"debian\"\n"; + +/* actual debian only sets ID=debian */ +static const char *os_release_actual_debian_valid = "ID=debian\n"; + +static const char *os_release_invalid = "garbage\n"; + +static void test_is_debian_like(void) +{ + mock_os_release(os_release_debian_like_valid); + g_assert_true(sc_is_debian_like()); + + mock_os_release(os_release_debian_like_quoted_valid); + g_assert_true(sc_is_debian_like()); + + mock_os_release(os_release_actual_debian_valid); + g_assert_true(sc_is_debian_like()); + + mock_os_release(os_release_fedora_ws); + g_assert_false(sc_is_debian_like()); + + mock_os_release(os_release_invalid); + g_assert_false(sc_is_debian_like()); +} + static void __attribute__((constructor)) init(void) { g_test_add_func("/classic/on-classic", test_is_on_classic); @@ -187,4 +218,5 @@ static void __attribute__((constructor)) init(void) 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/is-debian-like", test_is_debian_like); } diff --git a/cmd/libsnap-confine-private/classic.c b/cmd/libsnap-confine-private/classic.c index cbfa1bafa1..720f47554c 100644 --- a/cmd/libsnap-confine-private/classic.c +++ b/cmd/libsnap-confine-private/classic.c @@ -1,6 +1,7 @@ #include "config.h" #include "classic.h" #include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/infofile.h" #include "../libsnap-confine-private/string-utils.h" #include <stdbool.h> @@ -56,3 +57,35 @@ sc_distro sc_classify_distro(void) return SC_DISTRO_CLASSIC; } } + +bool sc_is_debian_like(void) +{ + FILE *f SC_CLEANUP(sc_cleanup_file) = fopen(os_release, "r"); + if (f == NULL) { + return false; + } + const char *const id_keys_to_try[] = { + "ID", /* actual debian only sets ID */ + "ID_LIKE", /* distros based on debian */ + }; + size_t id_keys_to_try_len = + sizeof id_keys_to_try / sizeof *id_keys_to_try; + for (size_t i = 0; i < id_keys_to_try_len; i++) { + if (fseek(f, 0L, SEEK_SET) == -1) { + return false; + } + char *id_val SC_CLEANUP(sc_cleanup_string) = NULL; + struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL; + int rc = + sc_infofile_get_key(f, id_keys_to_try[i], &id_val, &err); + if (rc != 0) { + /* only if sc_infofile_get_key failed */ + return false; + } + if (sc_streq(id_val, "\"debian\"") + || sc_streq(id_val, "debian")) { + return true; + } + } + return false; +} diff --git a/cmd/libsnap-confine-private/classic.h b/cmd/libsnap-confine-private/classic.h index ee317f4401..2db073a1ef 100644 --- a/cmd/libsnap-confine-private/classic.h +++ b/cmd/libsnap-confine-private/classic.h @@ -30,4 +30,8 @@ typedef enum sc_distro { sc_distro sc_classify_distro(void); +// Returns true if it's a Debian-like distro as determined via /etc/os-release +// and the "ID_LIKE" key in there. +bool sc_is_debian_like(void); + #endif diff --git a/cmd/snap-confine/mount-support.c b/cmd/snap-confine/mount-support.c index 7f4a80a51f..298db6bbc0 100644 --- a/cmd/snap-confine/mount-support.c +++ b/cmd/snap-confine/mount-support.c @@ -322,16 +322,32 @@ static void sc_bootstrap_mount_namespace(const struct sc_mount_config *config) // - https://bugs.launchpad.net/snap-confine/+bug/1580018 // - https://bugzilla.opensuse.org/show_bug.cgi?id=1028568 const char *dirs_from_core[] = { - "/etc/alternatives", "/etc/ssl", "/etc/nsswitch.conf", - // Some specifc and privileged interfaces (e.g docker-support) give + "/etc/alternatives", "/etc/nsswitch.conf", + // Some specific and privileged interfaces (e.g docker-support) give // access to apparmor_parser from the base snap which at a minimum // needs to use matching configuration from the base snap instead // of from the users host system. "/etc/apparmor", "/etc/apparmor.d", + // Use ssl certs from the base by default unless + // using Debian/Ubuntu classic (see below) + "/etc/ssl", NULL }; + for (const char **dirs = dirs_from_core; *dirs != NULL; dirs++) { const char *dir = *dirs; + + // Special case for ubuntu/debian based + // classic distros that use the core* snap: + // here we use the host /etc/ssl + // to support custom ca-cert setups + if (sc_streq(dir, "/etc/ssl") && + config->distro == SC_DISTRO_CLASSIC && + sc_is_debian_like() && + sc_startswith(config->base_snap_name, "core")) { + continue; + } + if (access(dir, F_OK) != 0) { continue; } diff --git a/cmd/snap-repair/export_test.go b/cmd/snap-repair/export_test.go index 03b5e9fa43..0ce74903ba 100644 --- a/cmd/snap-repair/export_test.go +++ b/cmd/snap-repair/export_test.go @@ -86,6 +86,10 @@ func (run *Runner) BrandModel() (brand, model string) { return run.state.Device.Brand, run.state.Device.Model } +func (run *Runner) BaseMode() (base, mode string) { + return run.state.Device.Base, run.state.Device.Mode +} + func (run *Runner) SetStateModified(modified bool) { run.stateModified = modified } diff --git a/cmd/snap-repair/runner.go b/cmd/snap-repair/runner.go index c75c9f73af..ffa7566003 100644 --- a/cmd/snap-repair/runner.go +++ b/cmd/snap-repair/runner.go @@ -790,12 +790,18 @@ func findDevInfo16() (*deviceInfo, error) { return nil, err } - // get the base snap as well + // get the base snap as well, on uc16 it won't be specified in the model + // assertion and instead will be empty, so in this case we replace it with + // "core" + base := modelAs.Base() + if modelAs.Base() == "" { + base = "core" + } return &deviceInfo{ Brand: modelAs.BrandID(), Model: modelAs.Model(), - Base: modelAs.Base(), + Base: base, // Mode is unset on uc16/uc18 }, nil } @@ -900,8 +906,47 @@ func (run *Runner) Applicable(headers map[string]interface{}) bool { } } - // TODO:UC20: need to consider filtering by bases and modes in the assertion - // here + // also filter by base snaps and modes + bases, err := stringList(headers, "bases") + if err != nil { + return false + } + + if len(bases) != 0 && !strutil.ListContains(bases, run.state.Device.Base) { + return false + } + + modes, err := stringList(headers, "modes") + if err != nil { + return false + } + + // modes is slightly more nuanced, if the modes setting in the assertion + // header is unset, then it means it runs on all uc16/uc18 devices, but only + // during run mode on uc20 devices + if run.state.Device.Mode == "" { + // uc16 / uc18 device, the assertion is only applicable to us if modes + // is unset + if len(modes) != 0 { + return false + } + // else modes is unset and still applies to us + } else { + // uc20 device + switch { + case len(modes) == 0 && run.state.Device.Mode != "run": + // if modes is unset, then it is only applicable if we are + // in run mode + return false + case len(modes) != 0 && !strutil.ListContains(modes, run.state.Device.Mode): + // modes was specified and our current mode is not in the header, so + // not applicable to us + return false + } + // other cases are either that we are in run mode and modes is unset (in + // which case it is applicable) or modes is set to something with our + // current mode in the list (also in which case it is applicable) + } return true } diff --git a/cmd/snap-repair/runner_test.go b/cmd/snap-repair/runner_test.go index 467bbd7df0..069cdf8127 100644 --- a/cmd/snap-repair/runner_test.go +++ b/cmd/snap-repair/runner_test.go @@ -154,12 +154,36 @@ func (s *baseRunnerSuite) signSeqRepairs(c *C, repairs []string) []string { return seq } -const freshStateJSON = `{"device":{"brand":"my-brand","model":"my-model","base":"","mode":""},"time-lower-bound":"2017-08-11T15:49:49Z"}` +func checkStateJSON(c *C, file string, exp map[string]interface{}) { + stateFile := map[string]interface{}{} + b, err := ioutil.ReadFile(file) + c.Assert(err, IsNil) + err = json.Unmarshal(b, &stateFile) + c.Assert(err, IsNil) + c.Check(stateFile, DeepEquals, exp) +} func (s *baseRunnerSuite) freshState(c *C) { + // assume base: core18 + s.freshStateWithBaseAndMode(c, "core18", "") +} + +func (s *baseRunnerSuite) freshStateWithBaseAndMode(c *C, base, mode string) { err := os.MkdirAll(dirs.SnapRepairDir, 0775) c.Assert(err, IsNil) - err = ioutil.WriteFile(dirs.SnapRepairStateFile, []byte(freshStateJSON), 0600) + stateJSON := map[string]interface{}{ + "device": map[string]string{ + "brand": "my-brand", + "model": "my-model", + "base": base, + "mode": mode, + }, + "time-lower-bound": "2017-08-11T15:49:49Z", + } + b, err := json.Marshal(stateJSON) + c.Assert(err, IsNil) + + err = ioutil.WriteFile(dirs.SnapRepairStateFile, b, 0600) c.Assert(err, IsNil) } @@ -660,43 +684,102 @@ func (s *runnerSuite) TestSaveState(c *C) { err = runner.SaveState() c.Assert(err, IsNil) - c.Check(dirs.SnapRepairStateFile, testutil.FileEquals, `{"device":{"brand":"my-brand","model":"my-model","base":"","mode":""},"sequences":{"canonical":[{"sequence":1,"revision":3,"status":0}]},"time-lower-bound":"2017-08-11T15:49:49Z"}`) + exp := map[string]interface{}{ + "device": map[string]interface{}{ + "brand": "my-brand", + "model": "my-model", + "base": "core18", + "mode": "", + }, + "sequences": map[string]interface{}{ + "canonical": []interface{}{ + map[string]interface{}{ + // all json numbers are floats + "sequence": 1.0, + "revision": 3.0, + "status": 0.0, + }, + }, + }, + "time-lower-bound": "2017-08-11T15:49:49Z", + } + + checkStateJSON(c, dirs.SnapRepairStateFile, exp) +} + +type dev struct { + base string + mode string } func (s *runnerSuite) TestApplicable(c *C) { - s.freshState(c) - runner := repair.NewRunner() - err := runner.LoadState() - c.Assert(err, IsNil) scenarios := []struct { + device *dev headers map[string]interface{} applicable bool }{ - {nil, true}, - {map[string]interface{}{"series": []interface{}{"18"}}, false}, - {map[string]interface{}{"series": []interface{}{"18", "16"}}, true}, - {map[string]interface{}{"series": "18"}, false}, - {map[string]interface{}{"series": []interface{}{18}}, false}, - {map[string]interface{}{"architectures": []interface{}{arch.DpkgArchitecture()}}, true}, - {map[string]interface{}{"architectures": []interface{}{"other-arch"}}, false}, - {map[string]interface{}{"architectures": []interface{}{"other-arch", arch.DpkgArchitecture()}}, true}, - {map[string]interface{}{"architectures": arch.DpkgArchitecture()}, false}, - {map[string]interface{}{"models": []interface{}{"my-brand/my-model"}}, true}, - {map[string]interface{}{"models": []interface{}{"other-brand/other-model"}}, false}, - {map[string]interface{}{"models": []interface{}{"other-brand/other-model", "my-brand/my-model"}}, true}, - {map[string]interface{}{"models": "my-brand/my-model"}, false}, + {nil, nil, true}, + {nil, map[string]interface{}{"series": []interface{}{"18"}}, false}, + {nil, map[string]interface{}{"series": []interface{}{"18", "16"}}, true}, + {nil, map[string]interface{}{"series": "18"}, false}, + {nil, map[string]interface{}{"series": []interface{}{18}}, false}, + {nil, map[string]interface{}{"architectures": []interface{}{arch.DpkgArchitecture()}}, true}, + {nil, map[string]interface{}{"architectures": []interface{}{"other-arch"}}, false}, + {nil, map[string]interface{}{"architectures": []interface{}{"other-arch", arch.DpkgArchitecture()}}, true}, + {nil, map[string]interface{}{"architectures": arch.DpkgArchitecture()}, false}, + {nil, map[string]interface{}{"models": []interface{}{"my-brand/my-model"}}, true}, + {nil, map[string]interface{}{"models": []interface{}{"other-brand/other-model"}}, false}, + {nil, map[string]interface{}{"models": []interface{}{"other-brand/other-model", "my-brand/my-model"}}, true}, + {nil, map[string]interface{}{"models": "my-brand/my-model"}, false}, + // modes for uc16 / uc18 devices + {nil, map[string]interface{}{"modes": []interface{}{}}, true}, + {nil, map[string]interface{}{"modes": []interface{}{"run"}}, false}, + {nil, map[string]interface{}{"modes": []interface{}{"recover"}}, false}, + {nil, map[string]interface{}{"modes": []interface{}{"run", "recover"}}, false}, + // run mode for uc20 devices + {&dev{mode: "run"}, map[string]interface{}{"modes": []interface{}{}}, true}, + {&dev{mode: "run"}, map[string]interface{}{"modes": []interface{}{"run"}}, true}, + {&dev{mode: "run"}, map[string]interface{}{"modes": []interface{}{"recover"}}, false}, + {&dev{mode: "run"}, map[string]interface{}{"modes": []interface{}{"run", "recover"}}, true}, + // recover mode for uc20 devices + {&dev{mode: "recover"}, map[string]interface{}{"modes": []interface{}{}}, false}, + {&dev{mode: "recover"}, map[string]interface{}{"modes": []interface{}{"run"}}, false}, + {&dev{mode: "recover"}, map[string]interface{}{"modes": []interface{}{"recover"}}, true}, + {&dev{mode: "recover"}, map[string]interface{}{"modes": []interface{}{"run", "recover"}}, true}, + // bases for uc16 devices + {&dev{base: "core"}, map[string]interface{}{"bases": []interface{}{"core"}}, true}, + {&dev{base: "core"}, map[string]interface{}{"bases": []interface{}{"core18"}}, false}, + {&dev{base: "core"}, map[string]interface{}{"bases": []interface{}{"core", "core18"}}, true}, + // bases for uc18 devices + {&dev{base: "core18"}, map[string]interface{}{"bases": []interface{}{"core18"}}, true}, + {&dev{base: "core18"}, map[string]interface{}{"bases": []interface{}{"core"}}, false}, + {&dev{base: "core18"}, map[string]interface{}{"bases": []interface{}{"core", "core18"}}, true}, + // bases for uc20 devices + {&dev{base: "core20"}, map[string]interface{}{"bases": []interface{}{"core20"}}, true}, + {&dev{base: "core20"}, map[string]interface{}{"bases": []interface{}{"core"}}, false}, + {&dev{base: "core20"}, map[string]interface{}{"bases": []interface{}{"core", "core20"}}, true}, // model prefix matches - {map[string]interface{}{"models": []interface{}{"my-brand/*"}}, true}, - {map[string]interface{}{"models": []interface{}{"my-brand/my-mod*"}}, true}, - {map[string]interface{}{"models": []interface{}{"my-brand/xxx*"}}, false}, - {map[string]interface{}{"models": []interface{}{"my-brand/my-mod*", "my-brand/xxx*"}}, true}, - {map[string]interface{}{"models": []interface{}{"my*"}}, false}, - {map[string]interface{}{"disabled": "true"}, false}, - {map[string]interface{}{"disabled": "false"}, true}, + {nil, map[string]interface{}{"models": []interface{}{"my-brand/*"}}, true}, + {nil, map[string]interface{}{"models": []interface{}{"my-brand/my-mod*"}}, true}, + {nil, map[string]interface{}{"models": []interface{}{"my-brand/xxx*"}}, false}, + {nil, map[string]interface{}{"models": []interface{}{"my-brand/my-mod*", "my-brand/xxx*"}}, true}, + {nil, map[string]interface{}{"models": []interface{}{"my*"}}, false}, + {nil, map[string]interface{}{"disabled": "true"}, false}, + {nil, map[string]interface{}{"disabled": "false"}, true}, } for _, scen := range scenarios { + if scen.device == nil { + s.freshState(c) + } else { + s.freshStateWithBaseAndMode(c, scen.device.base, scen.device.mode) + } + + runner := repair.NewRunner() + err := runner.LoadState() + c.Assert(err, IsNil) + ok := runner.Applicable(scen.headers) c.Check(ok, Equals, scen.applicable, Commentf("%v", scen)) } @@ -1116,17 +1199,21 @@ func (s *runnerSuite) TestNextNotFound(c *C) { runner.BaseURL = mustParseURL(mockServer.URL) runner.LoadState() - // sanity - c.Check(dirs.SnapRepairStateFile, testutil.FileEquals, freshStateJSON) - _, err := runner.Next("canonical") c.Assert(err, Equals, repair.ErrRepairNotFound) // we saved new time lower bound - t1 := runner.TimeLowerBound() - expected := strings.Replace(freshStateJSON, "2017-08-11T15:49:49Z", t1.Format(time.RFC3339), 1) - c.Check(expected, Not(Equals), freshStateJSON) - c.Check(dirs.SnapRepairStateFile, testutil.FileEquals, expected) + stateFileExp := map[string]interface{}{ + "device": map[string]interface{}{ + "brand": "my-brand", + "model": "my-model", + "base": "core18", + "mode": "", + }, + "time-lower-bound": runner.TimeLowerBound().Format(time.RFC3339), + } + + checkStateJSON(c, dirs.SnapRepairStateFile, stateFileExp) } func (s *runnerSuite) TestNextSaveStateError(c *C) { @@ -1346,6 +1433,348 @@ AXNpZw==`} c.Check(filepath.Join(dirs.SnapRepairRunDir, "canonical", "1", "r0.script"), testutil.FileEquals, "#!/bin/sh\nexit 0\n") } +func (s *runnerSuite) TestRepairModesAndBases(c *C) { + repairTempl := `type: repair +authority-id: canonical +brand-id: canonical +repair-id: 1 +summary: uc20 recovery repair +timestamp: 2017-07-03T12:00:00Z +body-length: 17 +sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj +%[1]s +#!/bin/sh +exit 0 + + +AXNpZw== + ` + + r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) + defer r1() + r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) + defer r2() + + tt := []struct { + device *dev + modes []string + bases []string + shouldRun bool + comment string + }{ + // uc20 recover mode assertion + { + &dev{"core20", "recover"}, + []string{"recover"}, + []string{"core20"}, + true, + "uc20 recover mode w/ uc20 recover mode assertion", + }, + { + &dev{"core20", "run"}, + []string{"recover"}, + []string{"core20"}, + false, + "uc20 run mode w/ uc20 recover mode assertion", + }, + { + &dev{base: "core"}, + []string{"recover"}, + []string{"core20"}, + false, + "uc16 w/ uc20 recover mode assertion", + }, + { + &dev{base: "core18"}, + []string{"recover"}, + []string{"core20"}, + false, + "uc18 w/ uc20 recover mode assertion", + }, + + // uc20 run mode assertion + { + &dev{"core20", "recover"}, + []string{"run"}, + []string{"core20"}, + false, + "uc20 recover mode w/ uc20 run mode assertion", + }, + { + &dev{"core20", "run"}, + []string{"run"}, + []string{"core20"}, + true, + "uc20 run mode w/ uc20 run mode assertion", + }, + { + &dev{base: "core"}, + []string{"run"}, + []string{"core20"}, + false, + "uc16 w/ uc20 run mode assertion", + }, + { + &dev{base: "core18"}, + []string{"run"}, + []string{"core20"}, + false, + "uc18 w/ uc20 run mode assertion", + }, + + // all uc20 modes assertion + { + &dev{"core20", "recover"}, + []string{"run", "recover"}, + []string{"core20"}, + true, + "uc20 recover mode w/ all uc20 modes assertion", + }, + { + &dev{"core20", "run"}, + []string{"run", "recover"}, + []string{"core20"}, + true, + "uc20 run mode w/ all uc20 modes assertion", + }, + { + &dev{base: "core"}, + []string{"run", "recover"}, + []string{"core20"}, + false, + "uc16 w/ all uc20 modes assertion", + }, + { + &dev{base: "core18"}, + []string{"run", "recover"}, + []string{"core20"}, + false, + "uc18 w/ all uc20 modes assertion", + }, + + // alternate uc20 run mode only assertion + { + &dev{"core20", "recover"}, + []string{}, + []string{"core20"}, + false, + "uc20 recover mode w/ alternate uc20 run mode assertion", + }, + { + &dev{"core20", "run"}, + []string{}, + []string{"core20"}, + true, + "uc20 run mode w/ alternate uc20 run mode assertion", + }, + { + &dev{base: "core"}, + []string{}, + []string{"core20"}, + false, + "uc16 w/ alternate uc20 run mode assertion", + }, + { + &dev{base: "core18"}, + []string{}, + []string{"core20"}, + false, + "uc18 w/ alternate uc20 run mode assertion", + }, + { + &dev{base: "core"}, + []string{"run"}, + []string{}, + false, + "uc16 w/ uc20 run mode assertion", + }, + { + &dev{base: "core18"}, + []string{"run"}, + []string{}, + false, + "uc16 w/ uc20 run mode assertion", + }, + + // all except uc20 recover mode assertion + { + &dev{"core20", "recover"}, + []string{}, + []string{}, + false, + "uc20 recover mode w/ all except uc20 recover mode assertion", + }, + { + &dev{"core20", "run"}, + []string{}, + []string{}, + true, + "uc20 run mode w/ all except uc20 recover mode assertion", + }, + { + &dev{base: "core"}, + []string{}, + []string{}, + true, + "uc16 w/ all except uc20 recover mode assertion", + }, + { + &dev{base: "core18"}, + []string{}, + []string{}, + true, + "uc18 w/ all except uc20 recover mode assertion", + }, + + // uc16 and uc18 assertion + { + &dev{"core20", "recover"}, + []string{}, + []string{"core", "core18"}, + false, + "uc20 recover mode w/ uc16 and uc18 assertion", + }, + { + &dev{"core20", "run"}, + []string{}, + []string{"core", "core18"}, + false, + "uc20 run mode w/ uc16 and uc18 assertion", + }, + { + &dev{base: "core"}, + []string{}, + []string{"core", "core18"}, + true, + "uc16 w/ uc16 and uc18 assertion", + }, + { + &dev{base: "core18"}, + []string{}, + []string{"core", "core18"}, + true, + "uc18 w/ uc16 and uc18 assertion", + }, + + // just uc16 assertion + { + &dev{"core20", "recover"}, + []string{}, + []string{"core"}, + false, + "uc20 recover mode w/ just uc16 assertion", + }, + { + &dev{"core20", "run"}, + []string{}, + []string{"core"}, + false, + "uc20 run mode w/ just uc16 assertion", + }, + { + &dev{base: "core"}, + []string{}, + []string{"core"}, + true, + "uc16 w/ just uc16 assertion", + }, + { + &dev{base: "core18"}, + []string{}, + []string{"core"}, + false, + "uc18 w/ just uc16 assertion", + }, + + // just uc18 assertion + { + &dev{"core20", "recover"}, + []string{}, + []string{"core18"}, + false, + "uc20 recover mode w/ just uc18 assertion", + }, + { + &dev{"core20", "run"}, + []string{}, + []string{"core18"}, + false, + "uc20 run mode w/ just uc18 assertion", + }, + { + &dev{base: "core"}, + []string{}, + []string{"core18"}, + false, + "uc16 w/ just uc18 assertion", + }, + { + &dev{base: "core18"}, + []string{}, + []string{"core18"}, + true, + "uc18 w/ just uc18 assertion", + }, + } + for _, t := range tt { + comment := Commentf(t.comment) + cleanups := []func(){} + + // generate the assertion with the bases and modes + basesStr := "" + if len(t.bases) != 0 { + basesStr = "bases:\n" + for _, base := range t.bases { + basesStr += " - " + base + "\n" + } + } + modesStr := "" + if len(t.modes) != 0 { + modesStr = "modes:\n" + for _, mode := range t.modes { + modesStr += " - " + mode + "\n" + } + } + + seqRepairs := s.signSeqRepairs(c, []string{fmt.Sprintf(repairTempl, basesStr+modesStr)}) + + mockServer := makeMockServer(c, &seqRepairs, false) + cleanups = append(cleanups, func() { mockServer.Close() }) + + if t.device == nil { + s.freshState(c) + } else { + s.freshStateWithBaseAndMode(c, t.device.base, t.device.mode) + } + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + runner.LoadState() + + script := filepath.Join(dirs.SnapRepairRunDir, "canonical", "1", "r0.script") + + rpr, err := runner.Next("canonical") + if t.shouldRun { + c.Assert(err, IsNil, comment) + + // run the repair and make sure the script is there + err = rpr.Run() + c.Assert(err, IsNil, comment) + c.Check(script, testutil.FileEquals, "#!/bin/sh\nexit 0\n", comment) + + // remove the script for the next iteration + cleanups = append(cleanups, func() { c.Assert(os.RemoveAll(dirs.SnapRepairRunDir), IsNil) }) + } else { + c.Assert(err, Equals, repair.ErrRepairNotFound, comment) + c.Check(script, testutil.FileAbsent, comment) + } + + for _, r := range cleanups { + r() + } + } +} + func makeMockRepair(script string) string { return fmt.Sprintf(`type: repair authority-id: canonical @@ -1661,6 +2090,11 @@ type shared1620RunnerSuite struct { baseRunnerSuite writeSeedAssert func(c *C, fname string, a asserts.Assertion) + + // this is so we can check device details that will be different in the + // 20 version of tests from the 16 version of tests + expBase string + expMode string } func (s *shared1620RunnerSuite) TestTLSTime(c *C) { @@ -1697,6 +2131,10 @@ func (s *shared1620RunnerSuite) TestLoadStateInitState(c *C) { c.Check(brand, Equals, "my-brand") c.Check(model, Equals, "my-model-2") + base, mode := runner.BaseMode() + c.Check(base, Equals, s.expBase) + c.Check(mode, Equals, s.expMode) + c.Check(runner.TimeLowerBound().Equal(s.seedTime), Equals, true) } @@ -1709,6 +2147,9 @@ var _ = Suite(&runner16Suite{}) func (s *runner16Suite) SetUpTest(c *C) { s.shared1620RunnerSuite.SetUpTest(c) + s.shared1620RunnerSuite.expBase = "core" + s.shared1620RunnerSuite.expMode = "" + s.seedAssertsDir = filepath.Join(dirs.SnapSeedDir, "assertions") // dummy seed yaml @@ -1792,6 +2233,9 @@ base=core20_1.snap func (s *runner20Suite) SetUpTest(c *C) { s.shared1620RunnerSuite.SetUpTest(c) + s.shared1620RunnerSuite.expBase = "core20" + s.shared1620RunnerSuite.expMode = "run" + s.seedAssertsDir = filepath.Join(dirs.SnapSeedDir, "/systems/20201212/assertions") err := os.MkdirAll(s.seedAssertsDir, 0755) c.Assert(err, IsNil) diff --git a/gadget/install/content.go b/gadget/install/content.go index 535bd4dddd..74095f3ff6 100644 --- a/gadget/install/content.go +++ b/gadget/install/content.go @@ -28,6 +28,7 @@ import ( "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/gadget" "github.com/snapcore/snapd/gadget/internal" + "github.com/snapcore/snapd/gadget/quantity" "github.com/snapcore/snapd/logger" ) @@ -38,11 +39,13 @@ func init() { } // makeFilesystem creates a filesystem on the on-disk structure, according -// to the filesystem type defined in the gadget. -func makeFilesystem(ds *gadget.OnDiskStructure) error { +// to the filesystem type defined in the gadget. If sectorSize is specified, +// that sector size is used when creating the filesystem, otherwise if it is +// zero, automatic values are used instead. +func makeFilesystem(ds *gadget.OnDiskStructure, sectorSize quantity.Size) error { if ds.HasFilesystem() { logger.Debugf("create %s filesystem on %s with label %q", ds.VolumeStructure.Filesystem, ds.Node, ds.VolumeStructure.Label) - if err := internal.Mkfs(ds.VolumeStructure.Filesystem, ds.Node, ds.VolumeStructure.Label, ds.Size); err != nil { + if err := internal.Mkfs(ds.VolumeStructure.Filesystem, ds.Node, ds.VolumeStructure.Label, ds.Size, sectorSize); err != nil { return err } if err := udevTrigger(ds.Node); err != nil { diff --git a/gadget/install/install.go b/gadget/install/install.go index 088fd83e32..16b60a2599 100644 --- a/gadget/install/install.go +++ b/gadget/install/install.go @@ -160,8 +160,8 @@ func Run(model gadget.Model, gadgetRoot, device string, options Options, observe logger.Noticef("encrypted device %v", part.Node) } - if err := makeFilesystem(&part); err != nil { - return nil, err + if err := makeFilesystem(&part, lv.SectorSize); err != nil { + return nil, fmt.Errorf("cannot make filesystem for partition %s: %v", part.Role, err) } if err := writeContent(&part, gadgetRoot, observer); err != nil { diff --git a/gadget/internal/mkfs.go b/gadget/internal/mkfs.go index 867784d4fb..c6f3d6544a 100644 --- a/gadget/internal/mkfs.go +++ b/gadget/internal/mkfs.go @@ -30,7 +30,7 @@ import ( "github.com/snapcore/snapd/osutil" ) -type MkfsFunc func(imgFile, label, contentsRootDir string, deviceSize quantity.Size) error +type MkfsFunc func(imgFile, label, contentsRootDir string, deviceSize, sectorSize quantity.Size) error var ( mkfsHandlers = map[string]MkfsFunc{ @@ -40,42 +40,54 @@ var ( ) // Mkfs creates a filesystem of given type and provided label in the device or -// file. The device size provides hints for additional tuning of the created -// filesystem. -func Mkfs(typ, img, label string, deviceSize quantity.Size) error { - return MkfsWithContent(typ, img, label, "", deviceSize) +// file. The device size and sector size provides hints for additional tuning of +// the created filesystem. +func Mkfs(typ, img, label string, deviceSize, sectorSize quantity.Size) error { + return MkfsWithContent(typ, img, label, "", deviceSize, sectorSize) } -// Mkfs creates a filesystem of given type and provided label in the device or -// file. The filesystem is populated with contents of contentRootDir. The device -// size provides hints for additional tuning of the created filesystem. -func MkfsWithContent(typ, img, label, contentRootDir string, deviceSize quantity.Size) error { +// MkfsWithContent creates a filesystem of given type and provided label in the +// device or file. The filesystem is populated with contents of contentRootDir. +// The device size provides hints for additional tuning of the created +// filesystem. +func MkfsWithContent(typ, img, label, contentRootDir string, deviceSize, sectorSize quantity.Size) error { h, ok := mkfsHandlers[typ] if !ok { return fmt.Errorf("cannot create unsupported filesystem %q", typ) } - return h(img, label, contentRootDir, deviceSize) + return h(img, label, contentRootDir, deviceSize, sectorSize) } // mkfsExt4 creates an EXT4 filesystem in given image file, with an optional // filesystem label, and populates it with the contents of provided root // directory. -func mkfsExt4(img, label, contentsRootDir string, deviceSize quantity.Size) error { +func mkfsExt4(img, label, contentsRootDir string, deviceSize, sectorSize quantity.Size) error { // Originally taken from ubuntu-image // Switched to use mkfs defaults for https://bugs.launchpad.net/snappy/+bug/1878374 // For caveats/requirements in case we need support for older systems: // https://github.com/snapcore/snapd/pull/6997#discussion_r293967140 mkfsArgs := []string{"mkfs.ext4"} + const size32MiB = 32 * quantity.SizeMiB if deviceSize != 0 && deviceSize <= size32MiB { - // With the default of 4096 bytes, the minimal journal size is - // 4M, meaning we loose a lot of usable space. Try to follow the + // With the default block size of 4096 bytes, the minimal journal size + // is 4M, meaning we loose a lot of usable space. Try to follow the // e2fsprogs upstream and use a 1k block size for smaller // filesystems, note that this may cause issues like // https://bugs.launchpad.net/ubuntu/+source/lvm2/+bug/1817097 // if one migrates the filesystem to a device with a different // block size - mkfsArgs = append(mkfsArgs, "-b", "1024") + + // though note if the sector size was specified (i.e. non-zero) and + // larger than 1K, then we need to use that, since you can't create + // a filesystem with a block-size smaller than the sector-size + // see e2fsprogs source code: + // https://github.com/tytso/e2fsprogs/blob/0d47f5ab05177c1861f16bb3644a47018e6be1d0/misc/mke2fs.c#L2151-L2156 + defaultSectorSize := 1 * quantity.SizeKiB + if sectorSize > 1024 { + defaultSectorSize = sectorSize + } + mkfsArgs = append(mkfsArgs, "-b", defaultSectorSize.String()) } if contentsRootDir != "" { // mkfs.ext4 can populate the filesystem with contents of given @@ -106,11 +118,22 @@ func mkfsExt4(img, label, contentsRootDir string, deviceSize quantity.Size) erro // mkfsVfat creates a VFAT filesystem in given image file, with an optional // filesystem label, and populates it with the contents of provided root // directory. -func mkfsVfat(img, label, contentsRootDir string, deviceSize quantity.Size) error { - // taken from ubuntu-image +func mkfsVfat(img, label, contentsRootDir string, deviceSize, sectorSize quantity.Size) error { + // 512B logical sector size by default, unless the specified sector size is + // larger than 512, in which case use the sector size + // mkfs.vfat will automatically increase the block size to the internal + // sector size of the disk if the specified block size is too small, but + // be paranoid and always set the block size to that of the sector size if + // we know the sector size is larger than the default 512 (originally from + // ubuntu-image). see dosfstools: + // https://github.com/dosfstools/dosfstools/blob/e579a7df89bb3a6df08847d45c70c8ebfabca7d2/src/mkfs.fat.c#L1892-L1898 + defaultSectorSize := quantity.Size(512) + if sectorSize > defaultSectorSize { + defaultSectorSize = sectorSize + } mkfsArgs := []string{ - // 512B logical sector size - "-S", "512", + // options taken from ubuntu-image, except the sector size + "-S", defaultSectorSize.String(), // 1 sector per cluster "-s", "1", // 32b FAT size diff --git a/gadget/internal/mkfs_test.go b/gadget/internal/mkfs_test.go index 99da8e6e99..8c5dac11c9 100644 --- a/gadget/internal/mkfs_test.go +++ b/gadget/internal/mkfs_test.go @@ -63,7 +63,7 @@ func (m *mkfsSuite) TestMkfsExt4Happy(c *C) { cmd := testutil.MockCommand(c, "fakeroot", "") defer cmd.Restore() - err := internal.MkfsWithContent("ext4", "foo.img", "my-label", "contents", 0) + err := internal.MkfsWithContent("ext4", "foo.img", "my-label", "contents", 0, 0) c.Assert(err, IsNil) c.Check(cmd.Calls(), DeepEquals, [][]string{ { @@ -78,7 +78,7 @@ func (m *mkfsSuite) TestMkfsExt4Happy(c *C) { cmd.ForgetCalls() // empty label - err = internal.MkfsWithContent("ext4", "foo.img", "", "contents", 0) + err = internal.MkfsWithContent("ext4", "foo.img", "", "contents", 0, 0) c.Assert(err, IsNil) c.Check(cmd.Calls(), DeepEquals, [][]string{ { @@ -92,7 +92,7 @@ func (m *mkfsSuite) TestMkfsExt4Happy(c *C) { cmd.ForgetCalls() // no content - err = internal.Mkfs("ext4", "foo.img", "my-label", 0) + err = internal.Mkfs("ext4", "foo.img", "my-label", 0, 0) c.Assert(err, IsNil) c.Check(cmd.Calls(), DeepEquals, [][]string{ { @@ -109,7 +109,7 @@ func (m *mkfsSuite) TestMkfsExt4WithSize(c *C) { cmd := testutil.MockCommand(c, "fakeroot", "") defer cmd.Restore() - err := internal.MkfsWithContent("ext4", "foo.img", "my-label", "contents", 250*1024*1024) + err := internal.MkfsWithContent("ext4", "foo.img", "my-label", "contents", 250*1024*1024, 0) c.Assert(err, IsNil) c.Check(cmd.Calls(), DeepEquals, [][]string{ { @@ -124,7 +124,7 @@ func (m *mkfsSuite) TestMkfsExt4WithSize(c *C) { cmd.ForgetCalls() // empty label - err = internal.MkfsWithContent("ext4", "foo.img", "", "contents", 32*1024*1024) + err = internal.MkfsWithContent("ext4", "foo.img", "", "contents", 32*1024*1024, 0) c.Assert(err, IsNil) c.Check(cmd.Calls(), DeepEquals, [][]string{ { @@ -137,13 +137,44 @@ func (m *mkfsSuite) TestMkfsExt4WithSize(c *C) { }) cmd.ForgetCalls() + + // with sector size of 512 + err = internal.MkfsWithContent("ext4", "foo.img", "", "contents", 32*1024*1024, 512) + c.Assert(err, IsNil) + c.Check(cmd.Calls(), DeepEquals, [][]string{ + { + "fakeroot", + "mkfs.ext4", + "-b", "1024", + "-d", "contents", + "foo.img", + }, + }) + + cmd.ForgetCalls() + + // with sector size of 4096 + err = internal.MkfsWithContent("ext4", "foo.img", "", "contents", 32*1024*1024, 4096) + c.Assert(err, IsNil) + c.Check(cmd.Calls(), DeepEquals, [][]string{ + { + "fakeroot", + "mkfs.ext4", + "-b", "4096", + "-d", "contents", + "foo.img", + }, + }) + + cmd.ForgetCalls() + } func (m *mkfsSuite) TestMkfsExt4Error(c *C) { cmd := testutil.MockCommand(c, "fakeroot", "echo 'command failed'; exit 1") defer cmd.Restore() - err := internal.MkfsWithContent("ext4", "foo.img", "my-label", "contents", 0) + err := internal.MkfsWithContent("ext4", "foo.img", "my-label", "contents", 0, 0) c.Assert(err, ErrorMatches, "command failed") } @@ -154,7 +185,7 @@ func (m *mkfsSuite) TestMkfsVfatHappySimple(c *C) { cmd := testutil.MockCommand(c, "mkfs.vfat", "") defer cmd.Restore() - err := internal.MkfsWithContent("vfat", "foo.img", "my-label", d, 0) + err := internal.MkfsWithContent("vfat", "foo.img", "my-label", d, 0, 0) c.Assert(err, IsNil) c.Check(cmd.Calls(), DeepEquals, [][]string{ { @@ -170,7 +201,7 @@ func (m *mkfsSuite) TestMkfsVfatHappySimple(c *C) { cmd.ForgetCalls() // empty label - err = internal.MkfsWithContent("vfat", "foo.img", "", d, 0) + err = internal.MkfsWithContent("vfat", "foo.img", "", d, 0, 0) c.Assert(err, IsNil) c.Check(cmd.Calls(), DeepEquals, [][]string{ { @@ -185,7 +216,7 @@ func (m *mkfsSuite) TestMkfsVfatHappySimple(c *C) { cmd.ForgetCalls() // no content - err = internal.Mkfs("vfat", "foo.img", "my-label", 0) + err = internal.Mkfs("vfat", "foo.img", "my-label", 0, 0) c.Assert(err, IsNil) c.Check(cmd.Calls(), DeepEquals, [][]string{ { @@ -205,7 +236,23 @@ func (m *mkfsSuite) TestMkfsVfatWithSize(c *C) { cmd := testutil.MockCommand(c, "mkfs.vfat", "") defer cmd.Restore() - err := internal.MkfsWithContent("vfat", "foo.img", "my-label", d, 32*1024*1024) + err := internal.MkfsWithContent("vfat", "foo.img", "my-label", d, 32*1024*1024, 0) + c.Assert(err, IsNil) + c.Check(cmd.Calls(), DeepEquals, [][]string{ + { + "mkfs.vfat", + "-S", "512", + "-s", "1", + "-F", "32", + "-n", "my-label", + "foo.img", + }, + }) + + cmd.ForgetCalls() + + // with sector size of 512 + err = internal.MkfsWithContent("vfat", "foo.img", "my-label", d, 32*1024*1024, 512) c.Assert(err, IsNil) c.Check(cmd.Calls(), DeepEquals, [][]string{ { @@ -217,6 +264,23 @@ func (m *mkfsSuite) TestMkfsVfatWithSize(c *C) { "foo.img", }, }) + + cmd.ForgetCalls() + + // with sector size of 4096 + err = internal.MkfsWithContent("vfat", "foo.img", "my-label", d, 32*1024*1024, 4096) + c.Assert(err, IsNil) + c.Check(cmd.Calls(), DeepEquals, [][]string{ + { + "mkfs.vfat", + "-S", "4096", + "-s", "1", + "-F", "32", + "-n", "my-label", + "foo.img", + }, + }) + } func (m *mkfsSuite) TestMkfsVfatHappyContents(c *C) { @@ -230,7 +294,7 @@ func (m *mkfsSuite) TestMkfsVfatHappyContents(c *C) { cmdMcopy := testutil.MockCommand(c, "mcopy", "") defer cmdMcopy.Restore() - err := internal.MkfsWithContent("vfat", "foo.img", "my-label", d, 0) + err := internal.MkfsWithContent("vfat", "foo.img", "my-label", d, 0, 0) c.Assert(err, IsNil) c.Assert(cmdMkfs.Calls(), HasLen, 1) @@ -245,7 +309,7 @@ func (m *mkfsSuite) TestMkfsVfatErrorSimpleFail(c *C) { cmd := testutil.MockCommand(c, "mkfs.vfat", "echo 'failed'; false") defer cmd.Restore() - err := internal.MkfsWithContent("vfat", "foo.img", "my-label", d, 0) + err := internal.MkfsWithContent("vfat", "foo.img", "my-label", d, 0, 0) c.Assert(err, ErrorMatches, "failed") } @@ -253,7 +317,7 @@ func (m *mkfsSuite) TestMkfsVfatErrorUnreadableDir(c *C) { cmd := testutil.MockCommand(c, "mkfs.vfat", "") defer cmd.Restore() - err := internal.MkfsWithContent("vfat", "foo.img", "my-label", "dir-does-not-exist", 0) + err := internal.MkfsWithContent("vfat", "foo.img", "my-label", "dir-does-not-exist", 0, 0) c.Assert(err, ErrorMatches, "cannot list directory contents: .* no such file or directory") c.Assert(cmd.Calls(), HasLen, 1) } @@ -268,7 +332,7 @@ func (m *mkfsSuite) TestMkfsVfatErrorInMcopy(c *C) { cmdMcopy := testutil.MockCommand(c, "mcopy", "echo 'hard fail'; exit 1") defer cmdMcopy.Restore() - err := internal.MkfsWithContent("vfat", "foo.img", "my-label", d, 0) + err := internal.MkfsWithContent("vfat", "foo.img", "my-label", d, 0, 0) c.Assert(err, ErrorMatches, "cannot populate vfat filesystem with contents: hard fail") c.Assert(cmdMkfs.Calls(), HasLen, 1) c.Assert(cmdMcopy.Calls(), HasLen, 1) @@ -281,7 +345,7 @@ func (m *mkfsSuite) TestMkfsVfatHappyNoContents(c *C) { cmdMcopy := testutil.MockCommand(c, "mcopy", "") defer cmdMcopy.Restore() - err := internal.MkfsWithContent("vfat", "foo.img", "my-label", "", 0) + err := internal.MkfsWithContent("vfat", "foo.img", "my-label", "", 0, 0) c.Assert(err, IsNil) c.Assert(cmdMkfs.Calls(), HasLen, 1) // mcopy was not called @@ -289,10 +353,10 @@ func (m *mkfsSuite) TestMkfsVfatHappyNoContents(c *C) { } func (m *mkfsSuite) TestMkfsInvalidFs(c *C) { - err := internal.MkfsWithContent("no-fs", "foo.img", "my-label", "", 0) + err := internal.MkfsWithContent("no-fs", "foo.img", "my-label", "", 0, 0) c.Assert(err, ErrorMatches, `cannot create unsupported filesystem "no-fs"`) - err = internal.Mkfs("no-fs", "foo.img", "my-label", 0) + err = internal.Mkfs("no-fs", "foo.img", "my-label", 0, 0) c.Assert(err, ErrorMatches, `cannot create unsupported filesystem "no-fs"`) } diff --git a/overlord/devicestate/devicemgr.go b/overlord/devicestate/devicemgr.go index f2e3a96dbe..a19c5f2b69 100644 --- a/overlord/devicestate/devicemgr.go +++ b/overlord/devicestate/devicemgr.go @@ -674,6 +674,9 @@ func (m *DeviceManager) ensureCloudInitRestricted() error { // already been permanently disabled, nothing to do m.cloudInitAlreadyRestricted = true return nil + case sysconfig.CloudInitNotFound: + // no cloud init at all + statusMsg = "not found" case sysconfig.CloudInitUntriggered: // hasn't been used statusMsg = "reported to be in disabled state" diff --git a/overlord/devicestate/devicestate_cloudinit_test.go b/overlord/devicestate/devicestate_cloudinit_test.go index 558108d5a8..3040243430 100644 --- a/overlord/devicestate/devicestate_cloudinit_test.go +++ b/overlord/devicestate/devicestate_cloudinit_test.go @@ -1137,3 +1137,29 @@ fi`, cloudInitScriptStateFile)) // we now have a message about restricting c.Assert(strings.TrimSpace(s.logbuf.String()), Matches, `.*System initialized, cloud-init reported to be done, set datasource_list to \[ NoCloud \] and disabled auto-import by filesystem label`) } +func (s *cloudInitSuite) TestCloudInitHappyNotFound(c *C) { + // pretend that cloud-init was not found on PATH + statusCalls := 0 + r := devicestate.MockCloudInitStatus(func() (sysconfig.CloudInitState, error) { + statusCalls++ + return sysconfig.CloudInitNotFound, nil + }) + defer r() + + restrictCalls := 0 + r = devicestate.MockRestrictCloudInit(func(state sysconfig.CloudInitState, opts *sysconfig.CloudInitRestrictOptions) (sysconfig.CloudInitRestrictionResult, error) { + restrictCalls++ + // there was no cloud-init binary, so we explicitly disabled it + // if it reappears in future + return sysconfig.CloudInitRestrictionResult{ + Action: "disable", + }, nil + }) + defer r() + + err := devicestate.EnsureCloudInitRestricted(s.mgr) + c.Assert(err, IsNil) + c.Assert(statusCalls, Equals, 1) + c.Assert(restrictCalls, Equals, 1) + c.Assert(strings.TrimSpace(s.logbuf.String()), Matches, `.*System initialized, cloud-init not found, disabled permanently`) +} diff --git a/packaging/ubuntu-16.04/changelog b/packaging/ubuntu-16.04/changelog index 5d2a3f8373..a2e0e31e4f 100644 --- a/packaging/ubuntu-16.04/changelog +++ b/packaging/ubuntu-16.04/changelog @@ -1,3 +1,9 @@ +snapd (2.49~rc1) xenial; urgency=medium + + * New 2.49~rc1 release candidate + + -- Michael Vogt <michael.vogt@ubuntu.com> Mon, 25 Jan 2021 16:38:38 +0100 + snapd (2.48.2) xenial; urgency=medium * New upstream release, LP: #1906690 diff --git a/sysconfig/cloudinit.go b/sysconfig/cloudinit.go index 0eee0bf692..8702a87ada 100644 --- a/sysconfig/cloudinit.go +++ b/sysconfig/cloudinit.go @@ -29,6 +29,7 @@ import ( "regexp" "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/strutil" ) @@ -195,6 +196,9 @@ const ( // states, as we are conservative in assuming that cloud-init is doing // something. CloudInitEnabled + // CloudInitNotFound is when there is no cloud-init executable on the + // device. + CloudInitNotFound // CloudInitErrored is when cloud-init tried to run, but failed or had invalid // configuration. CloudInitErrored @@ -221,7 +225,13 @@ func CloudInitStatus() (CloudInitState, error) { return CloudInitDisabledPermanently, nil } - out, err := exec.Command("cloud-init", "status").CombinedOutput() + ciBinary, err := exec.LookPath("cloud-init") + if err != nil { + logger.Noticef("cannot locate cloud-init executable: %v", err) + return CloudInitNotFound, nil + } + + out, err := exec.Command(ciBinary, "status").CombinedOutput() if err != nil { return CloudInitErrored, osutil.OutputErr(out, err) } @@ -315,7 +325,7 @@ func RestrictCloudInit(state CloudInitState, opts *CloudInitRestrictOptions) (Cl return res, fmt.Errorf("cannot restrict cloud-init in error or enabled state") } fallthrough - case CloudInitUntriggered: + case CloudInitUntriggered, CloudInitNotFound: fallthrough default: res.Action = "disable" diff --git a/sysconfig/cloudinit_test.go b/sysconfig/cloudinit_test.go index 04bdb251d5..29bdba45af 100644 --- a/sysconfig/cloudinit_test.go +++ b/sysconfig/cloudinit_test.go @@ -318,6 +318,19 @@ fi } } +func (s *sysconfigSuite) TestCloudInitNotFoundStatus(c *C) { + emptyDir := c.MkDir() + oldPath := os.Getenv("PATH") + defer func() { + c.Assert(os.Setenv("PATH", oldPath), IsNil) + }() + os.Setenv("PATH", emptyDir) + + status, err := sysconfig.CloudInitStatus() + c.Assert(err, IsNil) + c.Check(status, Equals, sysconfig.CloudInitNotFound) +} + var gceCloudInitStatusJSON = `{ "v1": { "datasource": "DataSourceGCE", @@ -518,6 +531,12 @@ func (s *sysconfigSuite) TestRestrictCloudInit(c *C) { expAction: "disable", expDisableFile: true, }, + { + comment: "no cloud-init in $PATH", + state: sysconfig.CloudInitNotFound, + expAction: "disable", + expDisableFile: true, + }, } for _, t := range tt { diff --git a/tests/lib/tools/tests.pkgs.dnf-yum.sh b/tests/lib/tools/tests.pkgs.dnf-yum.sh index 0efd635c21..115023c83a 100644 --- a/tests/lib/tools/tests.pkgs.dnf-yum.sh +++ b/tests/lib/tools/tests.pkgs.dnf-yum.sh @@ -15,11 +15,7 @@ remap_one() { echo "python3-gobject" ;; test-snapd-pkg-1) - if [ "$(command -v dnf)" != "" ]; then - echo "robotfindskitten" - else - echo "libXft" - fi + echo "freeglut" ;; test-snapd-pkg-2) echo "texlive-base" diff --git a/tests/main/ca-certs-for-snaps/task.yaml b/tests/main/ca-certs-for-snaps/task.yaml new file mode 100644 index 0000000000..4588c1400c --- /dev/null +++ b/tests/main/ca-certs-for-snaps/task.yaml @@ -0,0 +1,29 @@ +summary: Ensure that /etc/ssl customizations are available for snaps + +details: | + The host may have custom ssl certificates in the /etc/ssl directory. + These should be visible from inside the snaps too. + +prepare: | + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh + echo "Mock having a custom certificate in /etc/ssl" + if os.query is-classic && test -d /etc/ssl; then + touch /etc/ssl/file-from-host + fi + +restore: | + rm -f /etc/ssl/file-from-host + +execute: | + #shellcheck source=tests/lib/dirs.sh + . "$TESTSLIB/dirs.sh" + + # XXX: update when we get more systems that support custom ssl + # certificate usage + if [[ "$SPREAD_SYSTEM" = ubuntu-[0-9]* ]] || [[ "$SPREAD_SYSTEM" = debian-* ]]; then + echo "Ensure the hosts /etc/ssl is visible for classic Ubuntu/Debian" + test-snapd-sh.sh -c "test -e /etc/ssl/file-from-host" + else + echo "The hosts /etc/ssl is not used here" + test-snapd-sh.sh -c "test ! -e /etc/ssl/file-from-host" + fi diff --git a/tests/main/dirs-not-shared-with-host/task.yaml b/tests/main/dirs-not-shared-with-host/task.yaml index 0cc3c29c94..9d5fc2dec5 100644 --- a/tests/main/dirs-not-shared-with-host/task.yaml +++ b/tests/main/dirs-not-shared-with-host/task.yaml @@ -8,10 +8,11 @@ details: | consistent behaviour across various distributions. environment: - DIRECTORY/ssl: /etc/ssl + DIRECTORY/apparmord: /etc/apparmor.d # This test only applies to classic systems -systems: [-ubuntu-core-*] +# fedora, centos, amazon: do not have /etc/apparmor.d +systems: [-ubuntu-core-*, -fedora-*, -centos-*, -amazon-linux-*] prepare: | echo "Having installed the test snap" diff --git a/tests/main/mount-ns/google.ubuntu-16.04-64/PER-SNAP-16.expected.txt b/tests/main/mount-ns/google.ubuntu-16.04-64/PER-SNAP-16.expected.txt index a05bd21c08..0578b6a83b 100644 --- a/tests/main/mount-ns/google.ubuntu-16.04-64/PER-SNAP-16.expected.txt +++ b/tests/main/mount-ns/google.ubuntu-16.04-64/PER-SNAP-16.expected.txt @@ -8,7 +8,6 @@ +0:-29 / /dev/shm rw,nosuid,nodev master:7 - tmpfs tmpfs rw -1:-4 /etc /etc rw,relatime master:-6 - ext4 /dev/sda1 rw,data=ordered +2:+0 /etc/nsswitch.conf /etc/nsswitch.conf ro,nodev,relatime master:+16 - squashfs /dev/loop0 ro -+0:+0 /etc/ssl /etc/ssl ro,nodev,relatime master:+0 - squashfs /dev/loop0 ro -2:+0 /home /home rw,relatime master:-16 - ext4 /dev/sda1 rw,data=ordered +0:+0 /lib/firmware /lib/firmware rw,relatime master:+0 - ext4 /dev/sda1 rw,data=ordered +0:+0 /lib/modules /lib/modules rw,relatime master:+0 - ext4 /dev/sda1 rw,data=ordered diff --git a/tests/main/mount-ns/google.ubuntu-16.04-64/PER-SNAP-18.expected.txt b/tests/main/mount-ns/google.ubuntu-16.04-64/PER-SNAP-18.expected.txt index f036cf9f1a..69f621b15a 100644 --- a/tests/main/mount-ns/google.ubuntu-16.04-64/PER-SNAP-18.expected.txt +++ b/tests/main/mount-ns/google.ubuntu-16.04-64/PER-SNAP-18.expected.txt @@ -8,7 +8,6 @@ +0:-29 / /dev/shm rw,nosuid,nodev master:7 - tmpfs tmpfs rw -1:-4 /etc /etc rw,relatime master:-6 - ext4 /dev/sda1 rw,data=ordered +2:+1 /etc/nsswitch.conf /etc/nsswitch.conf ro,nodev,relatime master:+17 - squashfs /dev/loop1 ro -+0:+0 /etc/ssl /etc/ssl ro,nodev,relatime master:+0 - squashfs /dev/loop1 ro -2:-1 /home /home rw,relatime master:-17 - ext4 /dev/sda1 rw,data=ordered +0:+0 /lib/firmware /lib/firmware rw,relatime master:+0 - ext4 /dev/sda1 rw,data=ordered +0:+0 /lib/modules /lib/modules rw,relatime master:+0 - ext4 /dev/sda1 rw,data=ordered diff --git a/tests/main/mount-ns/google.ubuntu-16.04-64/PER-USER-16.expected.txt b/tests/main/mount-ns/google.ubuntu-16.04-64/PER-USER-16.expected.txt index e6637c58e3..56891b8d41 100644 --- a/tests/main/mount-ns/google.ubuntu-16.04-64/PER-USER-16.expected.txt +++ b/tests/main/mount-ns/google.ubuntu-16.04-64/PER-USER-16.expected.txt @@ -8,7 +8,6 @@ +0:-29 / /dev/shm rw,nosuid,nodev master:7 - tmpfs tmpfs rw -1:-4 /etc /etc rw,relatime master:-6 - ext4 /dev/sda1 rw,data=ordered +2:+0 /etc/nsswitch.conf /etc/nsswitch.conf ro,nodev,relatime master:+16 - squashfs /dev/loop0 ro -+0:+0 /etc/ssl /etc/ssl ro,nodev,relatime master:+0 - squashfs /dev/loop0 ro -2:+0 /home /home rw,relatime master:-16 - ext4 /dev/sda1 rw,data=ordered +0:+0 /lib/firmware /lib/firmware rw,relatime master:+0 - ext4 /dev/sda1 rw,data=ordered +0:+0 /lib/modules /lib/modules rw,relatime master:+0 - ext4 /dev/sda1 rw,data=ordered diff --git a/tests/main/mount-ns/google.ubuntu-16.04-64/PER-USER-18.expected.txt b/tests/main/mount-ns/google.ubuntu-16.04-64/PER-USER-18.expected.txt index 4a65a1e029..b1d4bd721f 100644 --- a/tests/main/mount-ns/google.ubuntu-16.04-64/PER-USER-18.expected.txt +++ b/tests/main/mount-ns/google.ubuntu-16.04-64/PER-USER-18.expected.txt @@ -8,7 +8,6 @@ +0:-29 / /dev/shm rw,nosuid,nodev master:7 - tmpfs tmpfs rw -1:-4 /etc /etc rw,relatime master:-6 - ext4 /dev/sda1 rw,data=ordered +2:+1 /etc/nsswitch.conf /etc/nsswitch.conf ro,nodev,relatime master:+17 - squashfs /dev/loop1 ro -+0:+0 /etc/ssl /etc/ssl ro,nodev,relatime master:+0 - squashfs /dev/loop1 ro -2:-1 /home /home rw,relatime master:-17 - ext4 /dev/sda1 rw,data=ordered +0:+0 /lib/firmware /lib/firmware rw,relatime master:+0 - ext4 /dev/sda1 rw,data=ordered +0:+0 /lib/modules /lib/modules rw,relatime master:+0 - ext4 /dev/sda1 rw,data=ordered diff --git a/tests/main/mount-ns/google.ubuntu-18.04-64/PER-SNAP-16.expected.txt b/tests/main/mount-ns/google.ubuntu-18.04-64/PER-SNAP-16.expected.txt index 5c5f7bdb4d..5b9d2a2a17 100644 --- a/tests/main/mount-ns/google.ubuntu-18.04-64/PER-SNAP-16.expected.txt +++ b/tests/main/mount-ns/google.ubuntu-18.04-64/PER-SNAP-16.expected.txt @@ -10,7 +10,6 @@ +2:+0 /etc/apparmor /etc/apparmor ro,nodev,relatime master:+14 - squashfs /dev/loop0 ro +0:+0 /etc/apparmor.d /etc/apparmor.d ro,nodev,relatime master:+0 - squashfs /dev/loop0 ro +0:+0 /etc/nsswitch.conf /etc/nsswitch.conf ro,nodev,relatime master:+0 - squashfs /dev/loop0 ro -+0:+0 /etc/ssl /etc/ssl ro,nodev,relatime master:+0 - squashfs /dev/loop0 ro -2:+0 /home /home rw,relatime master:-14 - ext4 /dev/sda1 rw +0:+0 /lib/firmware /lib/firmware rw,relatime master:+0 - ext4 /dev/sda1 rw +0:+0 /lib/modules /lib/modules rw,relatime master:+0 - ext4 /dev/sda1 rw diff --git a/tests/main/mount-ns/google.ubuntu-18.04-64/PER-SNAP-18.expected.txt b/tests/main/mount-ns/google.ubuntu-18.04-64/PER-SNAP-18.expected.txt index 5612310ba0..ca2b9d2cc9 100644 --- a/tests/main/mount-ns/google.ubuntu-18.04-64/PER-SNAP-18.expected.txt +++ b/tests/main/mount-ns/google.ubuntu-18.04-64/PER-SNAP-18.expected.txt @@ -10,7 +10,6 @@ +2:+1 /etc/apparmor /etc/apparmor ro,nodev,relatime master:+15 - squashfs /dev/loop1 ro +0:+0 /etc/apparmor.d /etc/apparmor.d ro,nodev,relatime master:+0 - squashfs /dev/loop1 ro +0:+0 /etc/nsswitch.conf /etc/nsswitch.conf ro,nodev,relatime master:+0 - squashfs /dev/loop1 ro -+0:+0 /etc/ssl /etc/ssl ro,nodev,relatime master:+0 - squashfs /dev/loop1 ro -2:-1 /home /home rw,relatime master:-15 - ext4 /dev/sda1 rw +0:+0 /lib/firmware /lib/firmware rw,relatime master:+0 - ext4 /dev/sda1 rw +0:+0 /lib/modules /lib/modules rw,relatime master:+0 - ext4 /dev/sda1 rw diff --git a/tests/main/mount-ns/google.ubuntu-18.04-64/PER-USER-16.expected.txt b/tests/main/mount-ns/google.ubuntu-18.04-64/PER-USER-16.expected.txt index 250dbfb488..ce950ba38d 100644 --- a/tests/main/mount-ns/google.ubuntu-18.04-64/PER-USER-16.expected.txt +++ b/tests/main/mount-ns/google.ubuntu-18.04-64/PER-USER-16.expected.txt @@ -10,7 +10,6 @@ +2:+0 /etc/apparmor /etc/apparmor ro,nodev,relatime master:+14 - squashfs /dev/loop0 ro +0:+0 /etc/apparmor.d /etc/apparmor.d ro,nodev,relatime master:+0 - squashfs /dev/loop0 ro +0:+0 /etc/nsswitch.conf /etc/nsswitch.conf ro,nodev,relatime master:+0 - squashfs /dev/loop0 ro -+0:+0 /etc/ssl /etc/ssl ro,nodev,relatime master:+0 - squashfs /dev/loop0 ro -2:+0 /home /home rw,relatime master:-14 - ext4 /dev/sda1 rw +0:+0 /lib/firmware /lib/firmware rw,relatime master:+0 - ext4 /dev/sda1 rw +0:+0 /lib/modules /lib/modules rw,relatime master:+0 - ext4 /dev/sda1 rw diff --git a/tests/main/mount-ns/google.ubuntu-18.04-64/PER-USER-18.expected.txt b/tests/main/mount-ns/google.ubuntu-18.04-64/PER-USER-18.expected.txt index 83fc895dac..035878bc97 100644 --- a/tests/main/mount-ns/google.ubuntu-18.04-64/PER-USER-18.expected.txt +++ b/tests/main/mount-ns/google.ubuntu-18.04-64/PER-USER-18.expected.txt @@ -10,7 +10,6 @@ +2:+1 /etc/apparmor /etc/apparmor ro,nodev,relatime master:+15 - squashfs /dev/loop1 ro +0:+0 /etc/apparmor.d /etc/apparmor.d ro,nodev,relatime master:+0 - squashfs /dev/loop1 ro +0:+0 /etc/nsswitch.conf /etc/nsswitch.conf ro,nodev,relatime master:+0 - squashfs /dev/loop1 ro -+0:+0 /etc/ssl /etc/ssl ro,nodev,relatime master:+0 - squashfs /dev/loop1 ro -2:-1 /home /home rw,relatime master:-15 - ext4 /dev/sda1 rw +0:+0 /lib/firmware /lib/firmware rw,relatime master:+0 - ext4 /dev/sda1 rw +0:+0 /lib/modules /lib/modules rw,relatime master:+0 - ext4 /dev/sda1 rw |
