snapd-2.37.4~14.04.1/0000775000000000000000000000000013435556260010541 5ustar snapd-2.37.4~14.04.1/i18n/0000775000000000000000000000000013435556260011320 5ustar snapd-2.37.4~14.04.1/i18n/i18n.go0000664000000000000000000000561513435556260012435 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2015 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 . * */ package i18n //go:generate update-pot import ( "fmt" "os" "path/filepath" "strings" "github.com/ojii/gettext.go" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil" ) // TEXTDOMAIN is the message domain used by snappy; see dgettext(3) // for more information. var ( TEXTDOMAIN = "snappy" locale gettext.Catalog translations gettext.Translations ) func init() { bindTextDomain(TEXTDOMAIN, "/usr/share/locale") setLocale("") } func langpackResolver(baseRoot string, locale string, domain string) string { // first check for the real locale (e.g. de_DE) // then try to simplify the locale (e.g. de_DE -> de) locales := []string{locale, strings.SplitN(locale, "_", 2)[0]} for _, locale := range locales { r := filepath.Join(locale, "LC_MESSAGES", fmt.Sprintf("%s.mo", domain)) // look into the core snaps first for translations, // then the main system candidateDirs := []string{ filepath.Join(dirs.SnapMountDir, "/core/current/", baseRoot), baseRoot, } for _, root := range candidateDirs { // ubuntu uses /usr/lib/locale-langpack and patches the glibc gettext // implementation langpack := filepath.Join(root, "..", "locale-langpack", r) if osutil.FileExists(langpack) { return langpack } regular := filepath.Join(root, r) if osutil.FileExists(regular) { return regular } } } return "" } func bindTextDomain(domain, dir string) { translations = gettext.NewTranslations(dir, domain, langpackResolver) } func setLocale(loc string) { if loc == "" { loc = os.Getenv("LC_MESSAGES") if loc == "" { loc = os.Getenv("LANG") } } // de_DE.UTF-8, de_DE@euro all need to get simplified loc = strings.Split(loc, "@")[0] loc = strings.Split(loc, ".")[0] locale = translations.Locale(loc) } // G is the shorthand for Gettext func G(msgid string) string { return locale.Gettext(msgid) } // https://www.gnu.org/software/gettext/manual/html_node/Plural-forms.html // (search for 1000) func ngn(d int) uint32 { const max = 1000000 if d < 0 { d = -d } if d > max { return uint32((d % max) + max) } return uint32(d) } // NG is the shorthand for NGettext func NG(msgid string, msgidPlural string, n int) string { return locale.NGettext(msgid, msgidPlural, ngn(n)) } snapd-2.37.4~14.04.1/i18n/xgettext-go/0000775000000000000000000000000013435556260013577 5ustar snapd-2.37.4~14.04.1/i18n/xgettext-go/main.go0000664000000000000000000002123513435556260015055 0ustar package main import ( "bytes" "fmt" "go/ast" "go/parser" "go/token" "io" "io/ioutil" "log" "os" "sort" "strings" "time" "github.com/jessevdk/go-flags" ) type msgID struct { msgidPlural string comment string fname string line int formatHint string } var msgIDs map[string][]msgID func formatComment(com string) string { out := "" for _, rawline := range strings.Split(com, "\n") { line := rawline line = strings.TrimPrefix(line, "//") line = strings.TrimPrefix(line, "/*") line = strings.TrimSuffix(line, "*/") line = strings.TrimSpace(line) if line != "" { out += fmt.Sprintf("#. %s\n", line) } } return out } func findCommentsForTranslation(fset *token.FileSet, f *ast.File, posCall token.Position) string { com := "" for _, cg := range f.Comments { // search for all comments in the previous line for i := len(cg.List) - 1; i >= 0; i-- { c := cg.List[i] posComment := fset.Position(c.End()) //println(posCall.Line, posComment.Line, c.Text) if posCall.Line == posComment.Line+1 { posCall = posComment com = fmt.Sprintf("%s\n%s", c.Text, com) } } } // only return if we have a matching prefix formatedComment := formatComment(com) needle := fmt.Sprintf("#. %s", opts.AddCommentsTag) if !strings.HasPrefix(formatedComment, needle) { formatedComment = "" } return formatedComment } func constructValue(val interface{}) string { switch val.(type) { case *ast.BasicLit: return val.(*ast.BasicLit).Value // this happens for constructs like: // gettext.Gettext("foo" + "bar") case *ast.BinaryExpr: // we only support string concat if val.(*ast.BinaryExpr).Op != token.ADD { return "" } left := constructValue(val.(*ast.BinaryExpr).X) // strip right " (or `) left = left[0 : len(left)-1] right := constructValue(val.(*ast.BinaryExpr).Y) // strip left " (or `) right = right[1:] return left + right default: panic(fmt.Sprintf("unknown type: %v", val)) } } func inspectNodeForTranslations(fset *token.FileSet, f *ast.File, n ast.Node) bool { // FIXME: this assume we always have a "gettext.Gettext" style keyword l := strings.Split(opts.Keyword, ".") gettextSelector := l[0] gettextFuncName := l[1] l = strings.Split(opts.KeywordPlural, ".") gettextSelectorPlural := l[0] gettextFuncNamePlural := l[1] switch x := n.(type) { case *ast.CallExpr: if sel, ok := x.Fun.(*ast.SelectorExpr); ok { i18nStr := "" i18nStrPlural := "" if sel.Sel.Name == gettextFuncNamePlural && sel.X.(*ast.Ident).Name == gettextSelectorPlural { i18nStr = x.Args[0].(*ast.BasicLit).Value i18nStrPlural = x.Args[1].(*ast.BasicLit).Value } if sel.Sel.Name == gettextFuncName && sel.X.(*ast.Ident).Name == gettextSelector { i18nStr = constructValue(x.Args[0]) } formatI18nStr := func(s string) string { if s == "" { return "" } // the "`" is special if s[0] == '`' { // replace inner " with \" s = strings.Replace(s, "\"", "\\\"", -1) // replace \n with \\n s = strings.Replace(s, "\n", "\\n", -1) } // strip leading and trailing " (or `) s = s[1 : len(s)-1] return s } // FIXME: too simplistic(?), no %% is considered formatHint := "" if strings.Contains(i18nStr, "%") || strings.Contains(i18nStrPlural, "%") { // well, not quite correct but close enough formatHint = "c-format" } if i18nStr != "" { msgidStr := formatI18nStr(i18nStr) posCall := fset.Position(n.Pos()) msgIDs[msgidStr] = append(msgIDs[msgidStr], msgID{ formatHint: formatHint, msgidPlural: formatI18nStr(i18nStrPlural), fname: posCall.Filename, line: posCall.Line, comment: findCommentsForTranslation(fset, f, posCall), }) } } } return true } func processFiles(args []string) error { // go over the input files msgIDs = make(map[string][]msgID) fset := token.NewFileSet() for _, fname := range args { if err := processSingleGoSource(fset, fname); err != nil { return err } } return nil } func processSingleGoSource(fset *token.FileSet, fname string) error { fnameContent, err := ioutil.ReadFile(fname) if err != nil { return err } // Create the AST by parsing src. f, err := parser.ParseFile(fset, fname, fnameContent, parser.ParseComments) if err != nil { return err } ast.Inspect(f, func(n ast.Node) bool { return inspectNodeForTranslations(fset, f, n) }) return nil } var formatTime = func() string { return time.Now().Format("2006-01-02 15:04-0700") } // mustFprintf will write the given format string to the given // writer. Any error will make it panic. func mustFprintf(w io.Writer, format string, a ...interface{}) { _, err := fmt.Fprintf(w, format, a...) if err != nil { panic(fmt.Sprintf("cannot write output: %v", err)) } } func writePotFile(out io.Writer) { header := fmt.Sprintf(`# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "Project-Id-Version: %s\n" "Report-Msgid-Bugs-To: %s\n" "POT-Creation-Date: %s\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" `, opts.PackageName, opts.MsgIDBugsAddress, formatTime()) mustFprintf(out, "%s", header) // yes, this is the way to do it in go sortedKeys := []string{} for k := range msgIDs { sortedKeys = append(sortedKeys, k) } if opts.SortOutput { sort.Strings(sortedKeys) } // FIXME: use template here? for _, k := range sortedKeys { msgidList := msgIDs[k] for _, msgid := range msgidList { if opts.AddComments || opts.AddCommentsTag != "" { mustFprintf(out, "%s", msgid.comment) } } if !opts.NoLocation { mustFprintf(out, "#:") for _, msgid := range msgidList { mustFprintf(out, " %s:%d", msgid.fname, msgid.line) } mustFprintf(out, "\n") } msgid := msgidList[0] if msgid.formatHint != "" { mustFprintf(out, "#, %s\n", msgid.formatHint) } var formatOutput = func(in string) string { // split string with \n into multiple lines // to make the output nicer out := strings.Replace(in, "\\n", "\\n\"\n \"", -1) // cleanup too aggressive splitting (empty "" lines) return strings.TrimSuffix(out, "\"\n \"") } mustFprintf(out, "msgid \"%v\"\n", formatOutput(k)) if msgid.msgidPlural != "" { mustFprintf(out, "msgid_plural \"%v\"\n", formatOutput(msgid.msgidPlural)) mustFprintf(out, "msgstr[0] \"\"\n") mustFprintf(out, "msgstr[1] \"\"\n") } else { mustFprintf(out, "msgstr \"\"\n") } mustFprintf(out, "\n") } } // FIXME: this must be setable via go-flags var opts struct { FilesFrom string `short:"f" long:"files-from" description:"get list of input files from FILE"` Output string `short:"o" long:"output" description:"output to specified file"` AddComments bool `short:"c" long:"add-comments" description:"place all comment blocks preceding keyword lines in output file"` AddCommentsTag string `long:"add-comments-tag" description:"place comment blocks starting with TAG and prceding keyword lines in output file"` SortOutput bool `short:"s" long:"sort-output" description:"generate sorted output"` NoLocation bool `long:"no-location" description:"do not write '#: filename:line' lines"` MsgIDBugsAddress string `long:"msgid-bugs-address" default:"EMAIL" description:"set report address for msgid bugs"` PackageName string `long:"package-name" description:"set package name in output"` Keyword string `short:"k" long:"keyword" default:"gettext.Gettext" description:"look for WORD as the keyword for singular strings"` KeywordPlural string `long:"keyword-plural" default:"gettext.NGettext" description:"look for WORD as the keyword for plural strings"` } func main() { // parse args args, err := flags.ParseArgs(&opts, os.Args) if err != nil { log.Fatalf("ParseArgs failed %s", err) } var files []string if opts.FilesFrom != "" { content, err := ioutil.ReadFile(opts.FilesFrom) if err != nil { log.Fatalf("cannot read file %v: %v", opts.FilesFrom, err) } content = bytes.TrimSpace(content) files = strings.Split(string(content), "\n") } else { files = args[1:] } if err := processFiles(files); err != nil { log.Fatalf("processFiles failed with: %s", err) } out := os.Stdout if opts.Output != "" { var err error out, err = os.Create(opts.Output) if err != nil { log.Fatalf("failed to create %s: %s", opts.Output, err) } } writePotFile(out) } snapd-2.37.4~14.04.1/i18n/xgettext-go/main_test.go0000664000000000000000000002303013435556260016107 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2015 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 . * */ package main import ( "bytes" "fmt" "io/ioutil" "os" "path/filepath" "testing" . "gopkg.in/check.v1" "github.com/snapcore/snapd/testutil" ) // Hook up check.v1 into the "go test" runner func TestT(t *testing.T) { TestingT(t) } type xgettextTestSuite struct { } var _ = Suite(&xgettextTestSuite{}) // test helper func makeGoSourceFile(c *C, content []byte) string { fname := filepath.Join(c.MkDir(), "foo.go") err := ioutil.WriteFile(fname, []byte(content), 0644) c.Assert(err, IsNil) return fname } func (s *xgettextTestSuite) SetUpTest(c *C) { // our test defaults opts.NoLocation = false opts.AddCommentsTag = "TRANSLATORS:" opts.Keyword = "i18n.G" opts.KeywordPlural = "i18n.NG" opts.SortOutput = true opts.PackageName = "snappy" opts.MsgIDBugsAddress = "snappy-devel@lists.ubuntu.com" // mock time formatTime = func() string { return "2015-06-30 14:48+0200" } } func (s *xgettextTestSuite) TestFormatComment(c *C) { var tests = []struct { in string out string }{ {in: "// foo ", out: "#. foo\n"}, {in: "/* foo */", out: "#. foo\n"}, {in: "/* foo\n */", out: "#. foo\n"}, {in: "/* foo\nbar */", out: "#. foo\n#. bar\n"}, } for _, test := range tests { c.Assert(formatComment(test.in), Equals, test.out) } } func (s *xgettextTestSuite) TestProcessFilesSimple(c *C) { fname := makeGoSourceFile(c, []byte(`package main func main() { // TRANSLATORS: foo comment i18n.G("foo") } `)) err := processFiles([]string{fname}) c.Assert(err, IsNil) c.Assert(msgIDs, DeepEquals, map[string][]msgID{ "foo": { { comment: "#. TRANSLATORS: foo comment\n", fname: fname, line: 5, }, }, }) } func (s *xgettextTestSuite) TestProcessFilesMultiple(c *C) { fname := makeGoSourceFile(c, []byte(`package main func main() { // TRANSLATORS: foo comment i18n.G("foo") // TRANSLATORS: bar comment i18n.G("foo") } `)) err := processFiles([]string{fname}) c.Assert(err, IsNil) c.Assert(msgIDs, DeepEquals, map[string][]msgID{ "foo": { { comment: "#. TRANSLATORS: foo comment\n", fname: fname, line: 5, }, { comment: "#. TRANSLATORS: bar comment\n", fname: fname, line: 8, }, }, }) } const header = `# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "Project-Id-Version: snappy\n" "Report-Msgid-Bugs-To: snappy-devel@lists.ubuntu.com\n" "POT-Creation-Date: 2015-06-30 14:48+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" ` func (s *xgettextTestSuite) TestWriteOutputSimple(c *C) { msgIDs = map[string][]msgID{ "foo": { { fname: "fname", line: 2, comment: "#. foo\n", }, }, } out := bytes.NewBuffer([]byte("")) writePotFile(out) expected := fmt.Sprintf(`%s #. foo #: fname:2 msgid "foo" msgstr "" `, header) c.Assert(out.String(), Equals, expected) } func (s *xgettextTestSuite) TestWriteOutputMultiple(c *C) { msgIDs = map[string][]msgID{ "foo": { { fname: "fname", line: 2, comment: "#. comment1\n", }, { fname: "fname", line: 4, comment: "#. comment2\n", }, }, } out := bytes.NewBuffer([]byte("")) writePotFile(out) expected := fmt.Sprintf(`%s #. comment1 #. comment2 #: fname:2 fname:4 msgid "foo" msgstr "" `, header) c.Assert(out.String(), Equals, expected) } func (s *xgettextTestSuite) TestWriteOutputNoComment(c *C) { msgIDs = map[string][]msgID{ "foo": { { fname: "fname", line: 2, }, }, } out := bytes.NewBuffer([]byte("")) writePotFile(out) expected := fmt.Sprintf(`%s #: fname:2 msgid "foo" msgstr "" `, header) c.Assert(out.String(), Equals, expected) } func (s *xgettextTestSuite) TestWriteOutputNoLocation(c *C) { msgIDs = map[string][]msgID{ "foo": { { fname: "fname", line: 2, }, }, } opts.NoLocation = true out := bytes.NewBuffer([]byte("")) writePotFile(out) expected := fmt.Sprintf(`%s msgid "foo" msgstr "" `, header) c.Assert(out.String(), Equals, expected) } func (s *xgettextTestSuite) TestWriteOutputFormatHint(c *C) { msgIDs = map[string][]msgID{ "foo": { { fname: "fname", line: 2, formatHint: "c-format", }, }, } out := bytes.NewBuffer([]byte("")) writePotFile(out) expected := fmt.Sprintf(`%s #: fname:2 #, c-format msgid "foo" msgstr "" `, header) c.Assert(out.String(), Equals, expected) } func (s *xgettextTestSuite) TestWriteOutputPlural(c *C) { msgIDs = map[string][]msgID{ "foo": { { msgidPlural: "plural", fname: "fname", line: 2, }, }, } out := bytes.NewBuffer([]byte("")) writePotFile(out) expected := fmt.Sprintf(`%s #: fname:2 msgid "foo" msgid_plural "plural" msgstr[0] "" msgstr[1] "" `, header) c.Assert(out.String(), Equals, expected) } func (s *xgettextTestSuite) TestWriteOutputSorted(c *C) { msgIDs = map[string][]msgID{ "aaa": { { fname: "fname", line: 2, }, }, "zzz": { { fname: "fname", line: 2, }, }, } opts.SortOutput = true // we need to run this a bunch of times as the ordering might // be right by pure chance for i := 0; i < 10; i++ { out := bytes.NewBuffer([]byte("")) writePotFile(out) expected := fmt.Sprintf(`%s #: fname:2 msgid "aaa" msgstr "" #: fname:2 msgid "zzz" msgstr "" `, header) c.Assert(out.String(), Equals, expected) } } func (s *xgettextTestSuite) TestIntegration(c *C) { fname := makeGoSourceFile(c, []byte(`package main func main() { // TRANSLATORS: foo comment // with multiple lines i18n.G("foo") // this comment has no translators tag i18n.G("abc") // TRANSLATORS: plural i18n.NG("singular", "plural", 99) i18n.G("zz %s") } `)) // a real integration test :) outName := filepath.Join(c.MkDir(), "snappy.pot") os.Args = []string{"test-binary", "--output", outName, "--keyword", "i18n.G", "--keyword-plural", "i18n.NG", "--msgid-bugs-address", "snappy-devel@lists.ubuntu.com", "--package-name", "snappy", fname, } main() // verify its what we expect c.Assert(outName, testutil.FileEquals, fmt.Sprintf(`%s #: %[2]s:9 msgid "abc" msgstr "" #. TRANSLATORS: foo comment #. with multiple lines #: %[2]s:6 msgid "foo" msgstr "" #. TRANSLATORS: plural #: %[2]s:12 msgid "singular" msgid_plural "plural" msgstr[0] "" msgstr[1] "" #: %[2]s:14 #, c-format msgid "zz %%s" msgstr "" `, header, fname)) } func (s *xgettextTestSuite) TestProcessFilesConcat(c *C) { fname := makeGoSourceFile(c, []byte(`package main func main() { // TRANSLATORS: foo comment i18n.G("foo\n" + "bar\n" + "baz") } `)) err := processFiles([]string{fname}) c.Assert(err, IsNil) c.Assert(msgIDs, DeepEquals, map[string][]msgID{ "foo\\nbar\\nbaz": { { comment: "#. TRANSLATORS: foo comment\n", fname: fname, line: 5, }, }, }) } func (s *xgettextTestSuite) TestProcessFilesWithQuote(c *C) { fname := makeGoSourceFile(c, []byte(fmt.Sprintf(`package main func main() { i18n.G(%[1]s foo "bar"%[1]s) } `, "`"))) err := processFiles([]string{fname}) c.Assert(err, IsNil) out := bytes.NewBuffer([]byte("")) writePotFile(out) expected := fmt.Sprintf(`%s #: %[2]s:4 msgid " foo \"bar\"" msgstr "" `, header, fname) c.Check(out.String(), Equals, expected) } func (s *xgettextTestSuite) TestWriteOutputMultilines(c *C) { msgIDs = map[string][]msgID{ "foo\\nbar\\nbaz": { { fname: "fname", line: 2, comment: "#. foo\n", }, }, } out := bytes.NewBuffer([]byte("")) writePotFile(out) expected := fmt.Sprintf(`%s #. foo #: fname:2 msgid "foo\n" "bar\n" "baz" msgstr "" `, header) c.Assert(out.String(), Equals, expected) } func (s *xgettextTestSuite) TestWriteOutputTidy(c *C) { msgIDs = map[string][]msgID{ "foo\\nbar\\nbaz": { { fname: "fname", line: 2, }, }, "zzz\\n": { { fname: "fname", line: 4, }, }, } out := bytes.NewBuffer([]byte("")) writePotFile(out) expected := fmt.Sprintf(`%s #: fname:2 msgid "foo\n" "bar\n" "baz" msgstr "" #: fname:4 msgid "zzz\n" msgstr "" `, header) c.Assert(out.String(), Equals, expected) } func (s *xgettextTestSuite) TestProcessFilesWithDoubleQuote(c *C) { fname := makeGoSourceFile(c, []byte(`package main func main() { i18n.G("foo \"bar\"") } `)) err := processFiles([]string{fname}) c.Assert(err, IsNil) out := bytes.NewBuffer([]byte("")) writePotFile(out) expected := fmt.Sprintf(`%s #: %[2]s:4 msgid "foo \"bar\"" msgstr "" `, header, fname) c.Check(out.String(), Equals, expected) } snapd-2.37.4~14.04.1/i18n/i18n_test.go0000664000000000000000000001060213435556260013464 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2015 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 . * */ package i18n import ( "io/ioutil" "os" "os/exec" "path/filepath" "testing" . "gopkg.in/check.v1" "github.com/snapcore/snapd/dirs" ) // Hook up check.v1 into the "go test" runner func Test(t *testing.T) { TestingT(t) } var mockLocalePo = []byte(` msgid "" msgstr "" "Project-Id-Version: snappy-test\n" "Report-Msgid-Bugs-To: snappy-devel@lists.ubuntu.com\n" "POT-Creation-Date: 2015-06-16 09:08+0200\n" "Language: en_DK\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;>\n" msgid "plural_1" msgid_plural "plural_2" msgstr[0] "translated plural_1" msgstr[1] "translated plural_2" msgid "singular" msgstr "translated singular" `) func makeMockTranslations(c *C, localeDir string) { fullLocaleDir := filepath.Join(localeDir, "en_DK", "LC_MESSAGES") err := os.MkdirAll(fullLocaleDir, 0755) c.Assert(err, IsNil) po := filepath.Join(fullLocaleDir, "snappy-test.po") mo := filepath.Join(fullLocaleDir, "snappy-test.mo") err = ioutil.WriteFile(po, mockLocalePo, 0644) c.Assert(err, IsNil) cmd := exec.Command("msgfmt", po, "--output-file", mo) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err = cmd.Run() c.Assert(err, IsNil) } type i18nTestSuite struct { origLang string origLcMessages string } var _ = Suite(&i18nTestSuite{}) func (s *i18nTestSuite) SetUpTest(c *C) { // this dir contains a special hand-crafted en_DK/snappy-test.mo // file localeDir := c.MkDir() makeMockTranslations(c, localeDir) // we use a custom test mo file TEXTDOMAIN = "snappy-test" s.origLang = os.Getenv("LANG") s.origLcMessages = os.Getenv("LC_MESSAGES") bindTextDomain("snappy-test", localeDir) os.Setenv("LANG", "en_DK.UTF-8") setLocale("") } func (s *i18nTestSuite) TearDownTest(c *C) { os.Setenv("LANG", s.origLang) os.Setenv("LC_MESSAGES", s.origLcMessages) } func (s *i18nTestSuite) TestTranslatedSingular(c *C) { // no G() to avoid adding the test string to snappy-pot var Gtest = G c.Assert(Gtest("singular"), Equals, "translated singular") } func (s *i18nTestSuite) TestTranslatesPlural(c *C) { // no NG() to avoid adding the test string to snappy-pot var NGtest = NG c.Assert(NGtest("plural_1", "plural_2", 1), Equals, "translated plural_1") } func (s *i18nTestSuite) TestTranslatedMissingLangNoCrash(c *C) { setLocale("invalid") // no G() to avoid adding the test string to snappy-pot var Gtest = G c.Assert(Gtest("singular"), Equals, "singular") } func (s *i18nTestSuite) TestInvalidTextDomainDir(c *C) { bindTextDomain("snappy-test", "/random/not/existing/dir") setLocale("invalid") // no G() to avoid adding the test string to snappy-pot var Gtest = G c.Assert(Gtest("singular"), Equals, "singular") } func (s *i18nTestSuite) TestLangpackResolverFromLangpack(c *C) { root := c.MkDir() localeDir := filepath.Join(root, "/usr/share/locale") err := os.MkdirAll(localeDir, 0755) c.Assert(err, IsNil) d := filepath.Join(root, "/usr/share/locale-langpack") makeMockTranslations(c, d) bindTextDomain("snappy-test", localeDir) setLocale("") // no G() to avoid adding the test string to snappy-pot var Gtest = G c.Assert(Gtest("singular"), Equals, "translated singular", Commentf("test with %q failed", d)) } func (s *i18nTestSuite) TestLangpackResolverFromCore(c *C) { origSnapMountDir := dirs.SnapMountDir defer func() { dirs.SnapMountDir = origSnapMountDir }() dirs.SnapMountDir = c.MkDir() d := filepath.Join(dirs.SnapMountDir, "/core/current/usr/share/locale") makeMockTranslations(c, d) bindTextDomain("snappy-test", "/usr/share/locale") setLocale("") // no G() to avoid adding the test string to snappy-pot var Gtest = G c.Assert(Gtest("singular"), Equals, "translated singular", Commentf("test with %q failed", d)) } snapd-2.37.4~14.04.1/.travis.yml0000664000000000000000000001041113435556260012647 0ustar language: go git: quiet: true jobs: include: - stage: quick name: go 1.6/xenial static and unit test suites dist: xenial go: "1.6" before_install: - sudo apt --quiet -o Dpkg::Progress-Fancy=false update install: - sudo apt --quiet -o Dpkg::Progress-Fancy=false build-dep snapd - ./get-deps.sh script: - set -e - ./run-checks --static - ./run-checks --short-unit - stage: quick go: "1.10" name: OSX build and minimal runtime sanity check os: osx addons: homebrew: packages: [squashfs] install: - ./get-deps.sh # extra dependency on darwin: - go get golang.org/x/sys/unix before_script: - ./mkversion.sh - go build -o /tmp/snp ./cmd/snap script: - /tmp/snp download hello - /tmp/snp version - /tmp/snp pack tests/lib/snaps/test-snapd-tools/ /tmp - stage: quick name: CLA check if: type = pull_request language: bash addons: apt: packages: python-launchpadlib script: - ./tests/lib/cla_check.py - stage: integration name: spread os: linux addons: apt: packages: - xdelta3 install: # override the default install for language:go - true script: - ./run-checks --spread env: global: # SPREAD_LINODE_KEY - secure: "bzALrfNSLwM0bjceal1PU5rFErvqVhi00Sygx8jruo6htpZay3hrC2sHCKCQKPn1kvCfHidrHX1vnomg5N+B9o25GZEYSjKSGxuvdNDfCZYqPNjMbz5y7xXYfKWgyo+xtrKRM85Nqy121SfRz3KLDvrOLwwreb+pZv8DG1WraFTd7D6rK7nLnnYNUyw665XBMFVnM8ue3Zu9496Ih/TfQXhnNpsZY8xFWte4+cH7JvVCVTs8snjoGVZi3972PzinNkfBgJa24cUzxFMfiN/AwSBXJQKdVv+FsbB4uRgXAqTNwuus7PptiPNxpWWojuhm1Qgbk0XhGIdJxyUYkmNA4UrZ3C29nIRWbuAiHJ6ZWd1ur3dqphqOcgFInltSHkpfEdlL3YK4dCa2SmJESzotUGnyowCUUCXkWdDaZmFTwyK0Y6He9oyXDK5f+/U7SFlPvok0caJCvB9HbTQR1kYdh048I/R+Ht5QrFOZPk21DYWDOYhn7SzthBDZLsaL6n5gX7Y547SsL4B35YVbpaeHzccG6Mox8rI4bqlGFvP1U5i8uXD4uQjJChlVxpmozUEMok9T5RVediJs540p5uc8DQl48Nke02tXzC/XpGAvpnXT7eiiRNW67zOj2QcIV+ni3lBj3HvZeB9cgjzLNrZSl/t9vseqnNwQWpl3V6nd/bU=" # SPREAD_STORE_USER - secure: "LjqfvJ2xz/7cxt1Cywaw5l8gaj5jOhUsf502UeaH+rOnj+9tCdWTtyP8U4nOjjQwiJ0xuygba+FgdnXEyxV+THeXHOF69SRF/1N8JIc3i9G6JK/CqDfFTRMqiRaCf5u7KuOrYZ0ssYNBXyZ8X4Ahls3uFu2DgEuAim1J6wOVSgIoUkduLVrbsn6uB9G5Uuc+C4NMA3TH21IJ6ct35t3T+/EjvoGUHcKtoOsPXdBZvz96xw5mKGIBaLpZdy5WxmhPUsz3MIlZgvi4DR3YIa/9u+QoGNU05f8upJRhwdwkuu9vJwqekXNXDJi/ZGlpkkAPx0feJbyhtz68551Pn1TtmA3TS5JtuMeMZWxCL9SudA7/C3oBRNGnKI3LwvP20pPjdlEYMOCq/oHlxoJylGVdpynZXTtaFS+s4Qhnr+WuNcG3zFa9bJvXPyy1vxPKcjI2DojneTrCTW/L6zg7tBIVQGzTxmC7QWsbTvOQzu+YICyeeS3g+iJ+QyP6+/oTyER3a3vmZCtXqsBJTznesS0SL5AkK+8moBGct96S6kT55XCDVgThWV0OGH6l4LwVSOjPioNzXNhVLZ8GKkXrMZXKSaWAeYptzWl4Gfz0Y4nFCu3aqIOyie7janPPgeEL0E2ZjndIs+ZigtN1LCol+GJN7fXzUFy8Fichqhhwvb3YLyE=" # SPREAD_STORE_PASSWORD - secure: "Le4CMhklfadi4aBQIEaEMbsFIB608GOvSHjVUxkDxkkUAVwl/Ov4Dni5d0Tn4e/xcxPkcm+pPg64dn0Jxzwx6XfWlxhWC10vYh+/GjpZW1znahtb/Gf9CNZOJJEy5LSeI7/uJ3LYcFd0FU0EJSerNeQJc5d8jmJH8UnuqObHOk29YD//XILiLRa1XALEimwXeQyGQePBmDTxPQQv1VLFjgfaJa5Xy55Us7AKTML2V7lhaeKCSEIp3x9liLAtnKlJhyXaXO/e4b3ZJTgXwYh+vENK1E2pxalpjBNPaJNkvtbsjFtYNXJoXca+hBVs5Sq1PCBhkEGxqFUsD8VLQd+MEXp4MYOF5fBhxIa3qOSjtuR+WmZ9G6fEysEBV6Y3F3D6HYWTpNkcHNXJCwdtOM+n92zNEBDIrufwzTPpyJXpoxZCCXrk3HHRdyDktvJYLrHdn1bM19mgYguesMZHTC5xMD6ifwdRoylmApjImXOvVxf2HdQiNvNLDqvaHgmYwNfl0+KbaVz+O2EDPCRnT5wOCpSeSUet47EPITdjr5OnTwLpOVaY+iSvn90EUB/8+ZU01TRYgc+6VNPHokLVjuiQJSrE4yTx/c2MnY9eRaOosVXngYfoS/L3XwDwZiQoeLZs04bScvxzGQIGCJ+CBzNPENtZ4AUh55Yl/vVNReZJeaY=" # SPREAD_GOOGLE_KEY - secure: "dIA2HrartowFL2Gl5jXiVMd9hIJyIeummYwxeBL9MzO48E/BIJyIGHudEOo8oCnZ5a0yb8TqYgND2FCgJU1V5I2LyxH6T9kizHjtmIGgeM4qlEGKRlptb2v7DFkaHeW4Mpp4gLk8hYIeWyq9OR+SlK6f0Jj049LLKfQoX6GzTPug5+MMEQOJs55OJ6f6gvCv2o3oj6WFybaohMCO4GbNYQSPLwheyTSkT0efnW9QqTN0w62pDMqscVURO90/CUeZyCcXw2uOBegwPNTBoo/+4+nZsfSNeupV8wX4vVYL0ZFL6IO3mViDoZBD4SGTNF/9x8Lc1WeKm9HlELzy5krdLqsvdV/fQSWhBzwkdykKVA3Aae5dAMIGRt7e5bJaUg+/HdtOgA5jr+qey/c/BN11MyaSOMNPNGjRuv9NAcEjxoN2JkiDXfpA3lE9kjd7TBTexGe4RJGJLJjT9s8XxdKufBfruC/yhVGdVkRoc2tsAJPZ72Ds9qH0FH28zNFAgAitCLDfInjhPMPvZJhb3Bqx5P/0DE5zUbduE9kYK0iiZRJ4AaytQy+R4nJCXE42mWv5cxoE84opVqO9cBu1TPCC8gTRQFWpJt1rP+DvwjaFiswvptG8obxNpHmkhcItPGmRVN9P9Yjd9nHvegS83tsbrd2KOyMmCk3/1KWhLufisHE=" snapd-2.37.4~14.04.1/spread-shellcheck0000775000000000000000000001757413435556260014066 0ustar #!/usr/bin/env python3 # 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 . import logging import os import subprocess import argparse from concurrent.futures import ThreadPoolExecutor from multiprocessing import cpu_count import yaml # default shell for shellcheck SHELLCHECK_SHELL = os.getenv('SHELLCHECK_SHELL', 'bash') # set to non-empty to ignore all errors NO_FAIL = os.getenv('NO_FAIL') # set to non empty to enable 'set -x' D = os.getenv('D') # set to non-empty to enable verbose logging V = os.getenv('V') # set to a number to use these many threads N = int(os.getenv('N') or cpu_count()) # file with list of files that can fail validation CAN_FAIL = os.getenv('CAN_FAIL') # names of sections SECTIONS = ['prepare', 'prepare-each', 'restore', 'restore-each', 'debug', 'debug-each', 'execute', 'repack'] def parse_arguments(): parser = argparse.ArgumentParser(description='spread shellcheck helper') parser.add_argument('-s', '--shell', default='bash', help='shell') parser.add_argument('-n', '--no-errors', action='store_true', default=False, help='ignore all errors ') parser.add_argument('-v', '--verbose', action='store_true', default=False, help='verbose logging') parser.add_argument('--can-fail', default=None, help=('file with list of files that are can fail ' 'validation')) parser.add_argument('-P', '--max-procs', default=N, type=int, metavar='N', help='run these many shellchecks in parallel (default: %(default)s)') parser.add_argument('paths', nargs='+', help='paths to check') return parser.parse_args() class ShellcheckRunError(Exception): def __init__(self, stderr): super().__init__() self.stderr = stderr class ShellcheckError(Exception): def __init__(self, path): super().__init__() self.sectionerrors = {} self.path = path def addfailure(self, section, error): self.sectionerrors[section] = error def __len__(self): return len(self.sectionerrors) class ShellcheckFailures(Exception): def __init__(self, failures=None): super().__init__() self.failures = set() if failures: self.failures = set(failures) def merge(self, otherfailures): self.failures = self.failures.union(otherfailures.failures) def __len__(self): return len(self.failures) def intersection(self, other): return self.failures.intersection(other) def difference(self, other): return self.failures.difference(other) def __iter__(self): return iter(self.failures) def checksection(data): # spread shell snippets are executed under 'set -e' shell, make sure # shellcheck knows about that data = 'set -e\n' + data proc = subprocess.Popen("shellcheck -s {} -x -".format(SHELLCHECK_SHELL), stdout=subprocess.PIPE, stdin=subprocess.PIPE, shell=True) stdout, _ = proc.communicate(input=data.encode('utf-8'), timeout=10) if proc.returncode != 0: raise ShellcheckRunError(stdout) def checkfile(path): logging.debug("checking file %s", path) with open(path) as inf: data = yaml.load(inf) errors = ShellcheckError(path) for section in SECTIONS: if section not in data: continue try: logging.debug("%s: checking section %s", path, section) checksection(data[section]) except ShellcheckRunError as serr: errors.addfailure(section, serr.stderr.decode('utf-8')) if path.endswith('spread.yaml') and 'suites' in data: # check suites for suite in data['suites'].keys(): for section in SECTIONS: if section not in data['suites'][suite]: continue try: logging.debug("%s (suite %s): checking section %s", path, suite, section) checksection(data['suites'][suite][section]) except ShellcheckRunError as serr: errors.addfailure('suites/' + suite + '/' + section, serr.stderr.decode('utf-8')) if errors: raise errors def findfiles(indir): for root, _, files in os.walk(indir, topdown=True): for name in files: if name in ['spread.yaml', 'task.yaml']: yield os.path.join(root, name) def checkpath(loc, max_workers): if os.path.isdir(loc): # setup iterator locations = findfiles(loc) else: locations = [loc] failed = [] def check1path(path): try: checkfile(path) except ShellcheckError as err: return err return None with ThreadPoolExecutor(max_workers=max_workers) as executor: for serr in executor.map(check1path, locations): if serr is None: continue logging.error(('shellcheck failed for file %s in sections: ' '%s; error log follows'), serr.path, ', '.join(serr.sectionerrors.keys())) for section, error in serr.sectionerrors.items(): logging.error("%s: section '%s':\n%s", serr.path, section, error) failed.append(serr.path) if failed: raise ShellcheckFailures(failures=failed) def loadfilelist(flistpath): flist = set() with open(flistpath) as inf: for line in inf: if not line.startswith('#'): flist.add(line.strip()) return flist def main(opts): paths = opts.paths or ['.'] failures = ShellcheckFailures() for pth in paths: try: checkpath(pth, opts.max_procs) except ShellcheckFailures as sf: failures.merge(sf) if failures: if opts.can_fail: can_fail = loadfilelist(opts.can_fail) unexpected = failures.difference(can_fail) if unexpected: logging.error(('validation failed for the following ' 'non-whitelisted files:\n%s'), '\n'.join([' - ' + f for f in sorted(unexpected)])) raise SystemExit(1) did_not_fail = can_fail - failures.intersection(can_fail) if did_not_fail: logging.error(('the following files are whitelisted ' 'but validated successfully:\n%s'), '\n'.join([' - ' + f for f in sorted(did_not_fail)])) raise SystemExit(1) # no unexpected failures return logging.error('validation failed for the following files:\n%s', '\n'.join([' - ' + f for f in sorted(failures)])) if NO_FAIL or opts.no_errors: logging.warning("ignoring errors") else: raise SystemExit(1) if __name__ == '__main__': opts = parse_arguments() if opts.verbose or D or V: lvl = logging.DEBUG else: lvl = logging.INFO logging.basicConfig(level=lvl) if CAN_FAIL: opts.can_fail = CAN_FAIL if NO_FAIL: opts.no_errors = True main(opts) snapd-2.37.4~14.04.1/snap/0000775000000000000000000000000013435556260011502 5ustar snapd-2.37.4~14.04.1/snap/info_snap_yaml.go0000664000000000000000000004351013435556260015032 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2016 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 . * */ package snap import ( "fmt" "os" "sort" "strconv" "strings" "gopkg.in/yaml.v2" "github.com/snapcore/snapd/strutil" "github.com/snapcore/snapd/timeout" ) type snapYaml struct { Name string `yaml:"name"` Version string `yaml:"version"` Type Type `yaml:"type"` Architectures []string `yaml:"architectures,omitempty"` Assumes []string `yaml:"assumes"` Title string `yaml:"title"` Description string `yaml:"description"` Summary string `yaml:"summary"` License string `yaml:"license,omitempty"` LicenseAgreement string `yaml:"license-agreement,omitempty"` LicenseVersion string `yaml:"license-version,omitempty"` Epoch Epoch `yaml:"epoch,omitempty"` Base string `yaml:"base,omitempty"` Confinement ConfinementType `yaml:"confinement,omitempty"` Environment strutil.OrderedMap `yaml:"environment,omitempty"` Plugs map[string]interface{} `yaml:"plugs,omitempty"` Slots map[string]interface{} `yaml:"slots,omitempty"` Apps map[string]appYaml `yaml:"apps,omitempty"` Hooks map[string]hookYaml `yaml:"hooks,omitempty"` Layout map[string]layoutYaml `yaml:"layout,omitempty"` // TypoLayouts is used to detect the use of the incorrect plural form of "layout" TypoLayouts typoDetector `yaml:"layouts,omitempty"` } type typoDetector struct { Hint string } func (td *typoDetector) UnmarshalYAML(func(interface{}) error) error { return fmt.Errorf("typo detected: %s", td.Hint) } type appYaml struct { Aliases []string `yaml:"aliases,omitempty"` Command string `yaml:"command"` CommandChain []string `yaml:"command-chain,omitempty"` Daemon string `yaml:"daemon"` StopCommand string `yaml:"stop-command,omitempty"` ReloadCommand string `yaml:"reload-command,omitempty"` PostStopCommand string `yaml:"post-stop-command,omitempty"` StopTimeout timeout.Timeout `yaml:"stop-timeout,omitempty"` WatchdogTimeout timeout.Timeout `yaml:"watchdog-timeout,omitempty"` Completer string `yaml:"completer,omitempty"` RefreshMode string `yaml:"refresh-mode,omitempty"` StopMode StopModeType `yaml:"stop-mode,omitempty"` RestartCond RestartCondition `yaml:"restart-condition,omitempty"` RestartDelay timeout.Timeout `yaml:"restart-delay,omitempty"` SlotNames []string `yaml:"slots,omitempty"` PlugNames []string `yaml:"plugs,omitempty"` BusName string `yaml:"bus-name,omitempty"` CommonID string `yaml:"common-id,omitempty"` Environment strutil.OrderedMap `yaml:"environment,omitempty"` Sockets map[string]socketsYaml `yaml:"sockets,omitempty"` After []string `yaml:"after,omitempty"` Before []string `yaml:"before,omitempty"` Timer string `yaml:"timer,omitempty"` Autostart string `yaml:"autostart,omitempty"` } type hookYaml struct { PlugNames []string `yaml:"plugs,omitempty"` SlotNames []string `yaml:"slots,omitempty"` Environment strutil.OrderedMap `yaml:"environment,omitempty"` CommandChain []string `yaml:"command-chain,omitempty"` } type layoutYaml struct { Bind string `yaml:"bind,omitempty"` BindFile string `yaml:"bind-file,omitempty"` Type string `yaml:"type,omitempty"` User string `yaml:"user,omitempty"` Group string `yaml:"group,omitempty"` Mode string `yaml:"mode,omitempty"` Symlink string `yaml:"symlink,omitempty"` } type socketsYaml struct { ListenStream string `yaml:"listen-stream,omitempty"` SocketMode os.FileMode `yaml:"socket-mode,omitempty"` } // InfoFromSnapYaml creates a new info based on the given snap.yaml data func InfoFromSnapYaml(yamlData []byte) (*Info, error) { var y snapYaml // Customize hints for the typo detector. y.TypoLayouts.Hint = `use singular "layout" instead of plural "layouts"` err := yaml.Unmarshal(yamlData, &y) if err != nil { return nil, fmt.Errorf("cannot parse snap.yaml: %s", err) } snap := infoSkeletonFromSnapYaml(y) // Collect top-level definitions of plugs and slots if err := setPlugsFromSnapYaml(y, snap); err != nil { return nil, err } if err := setSlotsFromSnapYaml(y, snap); err != nil { return nil, err } // At this point snap.Plugs and snap.Slots only contain globally-declared // plugs and slots, so copy them for later. snap.toplevelPlugs = make([]*PlugInfo, 0, len(snap.Plugs)) snap.toplevelSlots = make([]*SlotInfo, 0, len(snap.Slots)) for _, plug := range snap.Plugs { snap.toplevelPlugs = append(snap.toplevelPlugs, plug) } for _, slot := range snap.Slots { snap.toplevelSlots = append(snap.toplevelSlots, slot) } // Collect all apps, their aliases and hooks if err := setAppsFromSnapYaml(y, snap); err != nil { return nil, err } setHooksFromSnapYaml(y, snap) // Bind unbound plugs to all apps and hooks bindUnboundPlugs(snap) // Bind unbound slots to all apps and hooks bindUnboundSlots(snap) // Collect layout elements. if y.Layout != nil { snap.Layout = make(map[string]*Layout, len(y.Layout)) for path, l := range y.Layout { var mode os.FileMode = 0755 if l.Mode != "" { m, err := strconv.ParseUint(l.Mode, 8, 32) if err != nil { return nil, err } mode = os.FileMode(m) } user := "root" if l.User != "" { user = l.User } group := "root" if l.Group != "" { group = l.Group } snap.Layout[path] = &Layout{ Snap: snap, Path: path, Bind: l.Bind, Type: l.Type, Symlink: l.Symlink, BindFile: l.BindFile, User: user, Group: group, Mode: mode, } } } // Rename specific plugs on the core snap. snap.renameClashingCorePlugs() snap.BadInterfaces = make(map[string]string) SanitizePlugsSlots(snap) // FIXME: validation of the fields return snap, nil } // infoSkeletonFromSnapYaml initializes an Info without apps, hook, plugs, or // slots func infoSkeletonFromSnapYaml(y snapYaml) *Info { // Prepare defaults architectures := []string{"all"} if len(y.Architectures) != 0 { architectures = y.Architectures } typ := TypeApp if y.Type != "" { typ = y.Type } // TODO: once we have epochs transition to the snapd type for real if y.Name == "snapd" { typ = TypeSnapd } if len(y.Epoch.Read) == 0 { // normalize y.Epoch.Read = []uint32{0} y.Epoch.Write = []uint32{0} } confinement := StrictConfinement if y.Confinement != "" { confinement = y.Confinement } // Construct snap skeleton without apps, hooks, plugs, or slots snap := &Info{ SuggestedName: y.Name, Version: y.Version, Type: typ, Architectures: architectures, Assumes: y.Assumes, OriginalTitle: y.Title, OriginalDescription: y.Description, OriginalSummary: y.Summary, License: y.License, LicenseAgreement: y.LicenseAgreement, LicenseVersion: y.LicenseVersion, Epoch: y.Epoch, Confinement: confinement, Base: y.Base, Apps: make(map[string]*AppInfo), LegacyAliases: make(map[string]*AppInfo), Hooks: make(map[string]*HookInfo), Plugs: make(map[string]*PlugInfo), Slots: make(map[string]*SlotInfo), Environment: y.Environment, } sort.Strings(snap.Assumes) return snap } func setPlugsFromSnapYaml(y snapYaml, snap *Info) error { for name, data := range y.Plugs { iface, label, attrs, err := convertToSlotOrPlugData("plug", name, data) if err != nil { return err } snap.Plugs[name] = &PlugInfo{ Snap: snap, Name: name, Interface: iface, Attrs: attrs, Label: label, } if len(y.Apps) > 0 { snap.Plugs[name].Apps = make(map[string]*AppInfo) } if len(y.Hooks) > 0 { snap.Plugs[name].Hooks = make(map[string]*HookInfo) } } return nil } func setSlotsFromSnapYaml(y snapYaml, snap *Info) error { for name, data := range y.Slots { iface, label, attrs, err := convertToSlotOrPlugData("slot", name, data) if err != nil { return err } snap.Slots[name] = &SlotInfo{ Snap: snap, Name: name, Interface: iface, Attrs: attrs, Label: label, } if len(y.Apps) > 0 { snap.Slots[name].Apps = make(map[string]*AppInfo) } if len(y.Hooks) > 0 { snap.Slots[name].Hooks = make(map[string]*HookInfo) } } return nil } func setAppsFromSnapYaml(y snapYaml, snap *Info) error { for appName, yApp := range y.Apps { // Collect all apps app := &AppInfo{ Snap: snap, Name: appName, LegacyAliases: yApp.Aliases, Command: yApp.Command, CommandChain: yApp.CommandChain, Daemon: yApp.Daemon, StopTimeout: yApp.StopTimeout, StopCommand: yApp.StopCommand, ReloadCommand: yApp.ReloadCommand, PostStopCommand: yApp.PostStopCommand, RestartCond: yApp.RestartCond, RestartDelay: yApp.RestartDelay, BusName: yApp.BusName, CommonID: yApp.CommonID, Environment: yApp.Environment, Completer: yApp.Completer, StopMode: yApp.StopMode, RefreshMode: yApp.RefreshMode, Before: yApp.Before, After: yApp.After, Autostart: yApp.Autostart, WatchdogTimeout: yApp.WatchdogTimeout, } if len(y.Plugs) > 0 || len(yApp.PlugNames) > 0 { app.Plugs = make(map[string]*PlugInfo) } if len(y.Slots) > 0 || len(yApp.SlotNames) > 0 { app.Slots = make(map[string]*SlotInfo) } if len(yApp.Sockets) > 0 { app.Sockets = make(map[string]*SocketInfo, len(yApp.Sockets)) } snap.Apps[appName] = app for _, alias := range app.LegacyAliases { if snap.LegacyAliases[alias] != nil { return fmt.Errorf("cannot set %q as alias for both %q and %q", alias, snap.LegacyAliases[alias].Name, appName) } snap.LegacyAliases[alias] = app } // Bind all plugs/slots listed in this app for _, plugName := range yApp.PlugNames { plug, ok := snap.Plugs[plugName] if !ok { // Create implicit plug definitions if required plug = &PlugInfo{ Snap: snap, Name: plugName, Interface: plugName, Apps: make(map[string]*AppInfo), } snap.Plugs[plugName] = plug } app.Plugs[plugName] = plug plug.Apps[appName] = app } for _, slotName := range yApp.SlotNames { slot, ok := snap.Slots[slotName] if !ok { slot = &SlotInfo{ Snap: snap, Name: slotName, Interface: slotName, Apps: make(map[string]*AppInfo), } snap.Slots[slotName] = slot } app.Slots[slotName] = slot slot.Apps[appName] = app } for name, data := range yApp.Sockets { app.Sockets[name] = &SocketInfo{ App: app, Name: name, ListenStream: data.ListenStream, SocketMode: data.SocketMode, } } if yApp.Timer != "" { app.Timer = &TimerInfo{ App: app, Timer: yApp.Timer, } } // collect all common IDs if app.CommonID != "" { snap.CommonIDs = append(snap.CommonIDs, app.CommonID) } } return nil } func setHooksFromSnapYaml(y snapYaml, snap *Info) { for hookName, yHook := range y.Hooks { if !IsHookSupported(hookName) { continue } // Collect all hooks hook := &HookInfo{ Snap: snap, Name: hookName, Environment: yHook.Environment, CommandChain: yHook.CommandChain, Explicit: true, } if len(y.Plugs) > 0 || len(yHook.PlugNames) > 0 { hook.Plugs = make(map[string]*PlugInfo) } if len(y.Slots) > 0 || len(yHook.SlotNames) > 0 { hook.Slots = make(map[string]*SlotInfo) } snap.Hooks[hookName] = hook // Bind all plugs/slots listed in this hook for _, plugName := range yHook.PlugNames { plug, ok := snap.Plugs[plugName] if !ok { // Create implicit plug definitions if required plug = &PlugInfo{ Snap: snap, Name: plugName, Interface: plugName, Hooks: make(map[string]*HookInfo), } snap.Plugs[plugName] = plug } else if plug.Hooks == nil { plug.Hooks = make(map[string]*HookInfo) } hook.Plugs[plugName] = plug plug.Hooks[hookName] = hook } for _, slotName := range yHook.SlotNames { slot, ok := snap.Slots[slotName] if !ok { // Create implicit slot definitions if required slot = &SlotInfo{ Snap: snap, Name: slotName, Interface: slotName, Hooks: make(map[string]*HookInfo), } snap.Slots[slotName] = slot } else if slot.Hooks == nil { slot.Hooks = make(map[string]*HookInfo) } hook.Slots[slotName] = slot slot.Hooks[hookName] = hook } } } func bindUnboundPlugs(snap *Info) { for plugName, plug := range snap.Plugs { // A plug is considered unbound if it isn't being used by any apps // or hooks. In which case we bind them to all apps and hooks. if len(plug.Apps) == 0 && len(plug.Hooks) == 0 { for appName, app := range snap.Apps { app.Plugs[plugName] = plug plug.Apps[appName] = app } for hookName, hook := range snap.Hooks { hook.Plugs[plugName] = plug plug.Hooks[hookName] = hook } } } } func bindUnboundSlots(snap *Info) { for slotName, slot := range snap.Slots { // A slot is considered unbound if it isn't being used by any apps // or hooks. In which case we bind them to all apps and hooks. if len(slot.Apps) == 0 && len(slot.Hooks) == 0 { for appName, app := range snap.Apps { app.Slots[slotName] = slot slot.Apps[appName] = app } for hookName, hook := range snap.Hooks { hook.Slots[slotName] = slot slot.Hooks[hookName] = hook } } } } // bindImplicitHooks binds all global plugs and slots to implicit hooks func bindImplicitHooks(snap *Info) { for _, plug := range snap.toplevelPlugs { for hookName, hook := range snap.Hooks { if hook.Explicit { continue } if hook.Plugs == nil { hook.Plugs = make(map[string]*PlugInfo) } hook.Plugs[plug.Name] = plug if plug.Hooks == nil { plug.Hooks = make(map[string]*HookInfo) } plug.Hooks[hookName] = hook } } for _, slot := range snap.toplevelSlots { for hookName, hook := range snap.Hooks { if hook.Explicit { continue } if hook.Slots == nil { hook.Slots = make(map[string]*SlotInfo) } hook.Slots[slot.Name] = slot if slot.Hooks == nil { slot.Hooks = make(map[string]*HookInfo) } slot.Hooks[hookName] = hook } } } func convertToSlotOrPlugData(plugOrSlot, name string, data interface{}) (iface, label string, attrs map[string]interface{}, err error) { iface = name switch data.(type) { case string: return data.(string), "", nil, nil case nil: return name, "", nil, nil case map[interface{}]interface{}: for keyData, valueData := range data.(map[interface{}]interface{}) { key, ok := keyData.(string) if !ok { err := fmt.Errorf("%s %q has attribute that is not a string (found %T)", plugOrSlot, name, keyData) return "", "", nil, err } if strings.HasPrefix(key, "$") { err := fmt.Errorf("%s %q uses reserved attribute %q", plugOrSlot, name, key) return "", "", nil, err } switch key { case "interface": value, ok := valueData.(string) if !ok { err := fmt.Errorf("interface name on %s %q is not a string (found %T)", plugOrSlot, name, valueData) return "", "", nil, err } iface = value case "label": value, ok := valueData.(string) if !ok { err := fmt.Errorf("label of %s %q is not a string (found %T)", plugOrSlot, name, valueData) return "", "", nil, err } label = value default: if attrs == nil { attrs = make(map[string]interface{}) } value, err := normalizeYamlValue(valueData) if err != nil { return "", "", nil, fmt.Errorf("attribute %q of %s %q: %v", key, plugOrSlot, name, err) } attrs[key] = value } } return iface, label, attrs, nil default: err := fmt.Errorf("%s %q has malformed definition (found %T)", plugOrSlot, name, data) return "", "", nil, err } } // normalizeYamlValue validates values and returns a normalized version of it (map[interface{}]interface{} is turned into map[string]interface{}) func normalizeYamlValue(v interface{}) (interface{}, error) { switch x := v.(type) { case string: return x, nil case bool: return x, nil case int: return int64(x), nil case int64: return x, nil case float64: return x, nil case float32: return float64(x), nil case []interface{}: l := make([]interface{}, len(x)) for i, el := range x { el, err := normalizeYamlValue(el) if err != nil { return nil, err } l[i] = el } return l, nil case map[interface{}]interface{}: m := make(map[string]interface{}, len(x)) for k, item := range x { kStr, ok := k.(string) if !ok { return nil, fmt.Errorf("non-string key: %v", k) } item, err := normalizeYamlValue(item) if err != nil { return nil, err } m[kStr] = item } return m, nil case map[string]interface{}: m := make(map[string]interface{}, len(x)) for k, item := range x { item, err := normalizeYamlValue(item) if err != nil { return nil, err } m[k] = item } return m, nil default: return nil, fmt.Errorf("invalid scalar: %v", v) } } snapd-2.37.4~14.04.1/snap/implicit.go0000664000000000000000000000454713435556260013655 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package snap import ( "fmt" "io/ioutil" "github.com/snapcore/snapd/osutil" ) // addImplicitHooks adds hooks from the installed snap's hookdir to the snap info. // // Existing hooks (i.e. ones defined in the YAML) are not changed; only missing // hooks are added. func addImplicitHooks(snapInfo *Info) error { // First of all, check to ensure the hooks directory exists. If it doesn't, // it's not an error-- there's just nothing to do. hooksDir := snapInfo.HooksDir() if !osutil.IsDirectory(hooksDir) { return nil } fileInfos, err := ioutil.ReadDir(hooksDir) if err != nil { return fmt.Errorf("unable to read hooks directory: %s", err) } for _, fileInfo := range fileInfos { addHookIfValid(snapInfo, fileInfo.Name()) } return nil } // addImplicitHooksFromContainer adds hooks from the snap file's hookdir to the snap info. // // Existing hooks (i.e. ones defined in the YAML) are not changed; only missing // hooks are added. func addImplicitHooksFromContainer(snapInfo *Info, snapf Container) error { // Read the hooks directory. If this fails we assume the hooks directory // doesn't exist, which means there are no implicit hooks to load (not an // error). fileNames, err := snapf.ListDir("meta/hooks") if err != nil { return nil } for _, fileName := range fileNames { addHookIfValid(snapInfo, fileName) } return nil } func addHookIfValid(snapInfo *Info, hookName string) { // Verify that the hook name is actually supported. If not, ignore it. if !IsHookSupported(hookName) { return } // Don't overwrite a hook that has already been loaded from the YAML if _, ok := snapInfo.Hooks[hookName]; !ok { snapInfo.Hooks[hookName] = &HookInfo{Snap: snapInfo, Name: hookName} } } snapd-2.37.4~14.04.1/snap/channel.go0000664000000000000000000001016113435556260013440 0ustar // -*- 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 . * */ package snap import ( "fmt" "strings" "github.com/snapcore/snapd/arch" "github.com/snapcore/snapd/strutil" ) var channelRisks = []string{"stable", "candidate", "beta", "edge"} // Channel identifies and describes completely a store channel. type Channel struct { Architecture string `json:"architecture"` Name string `json:"name"` Track string `json:"track"` Risk string `json:"risk"` Branch string `json:"branch,omitempty"` } // ParseChannel parses a string representing a store channel and includes the given architecture, if architecture is "" the system architecture is included. func ParseChannel(s string, architecture string) (Channel, error) { if s == "" { return Channel{}, fmt.Errorf("channel name cannot be empty") } p := strings.Split(s, "/") var risk, track, branch string switch len(p) { default: return Channel{}, fmt.Errorf("channel name has too many components: %s", s) case 3: track, risk, branch = p[0], p[1], p[2] case 2: if strutil.ListContains(channelRisks, p[0]) { risk, branch = p[0], p[1] } else { track, risk = p[0], p[1] } case 1: if strutil.ListContains(channelRisks, p[0]) { risk = p[0] } else { track = p[0] risk = "stable" } } if !strutil.ListContains(channelRisks, risk) { return Channel{}, fmt.Errorf("invalid risk in channel name: %s", s) } if architecture == "" { architecture = arch.UbuntuArchitecture() } return Channel{ Architecture: architecture, Track: track, Risk: risk, Branch: branch, }.Clean(), nil } // Clean returns a Channel with a normalized track and name. func (c Channel) Clean() Channel { track := c.Track if track == "latest" { track = "" } // normalized name name := c.Risk if track != "" { name = track + "/" + name } if c.Branch != "" { name = name + "/" + c.Branch } return Channel{ Architecture: c.Architecture, Name: name, Track: track, Risk: c.Risk, Branch: c.Branch, } } func (c Channel) String() string { return c.Name } // Full returns the full name of the channel, inclusive the default track "latest". func (c *Channel) Full() string { if c.Track == "" { return "latest/" + c.Name } return c.String() } func riskLevel(risk string) int { for i, r := range channelRisks { if r == risk { return i } } return -1 } // ChannelMatch represents on which fields two channels are matching. type ChannelMatch struct { Architecture bool Track bool Risk bool } // String returns the string represantion of the match, results can be: // "architecture:track:risk" // "architecture:track" // "architecture:risk" // "track:risk" // "architecture" // "track" // "risk" // "" func (cm ChannelMatch) String() string { matching := []string{} if cm.Architecture { matching = append(matching, "architecture") } if cm.Track { matching = append(matching, "track") } if cm.Risk { matching = append(matching, "risk") } return strings.Join(matching, ":") } // Match returns a ChannelMatch of which fields among architecture,track,risk match between c and c1 store channels, risk is matched taking channel inheritance into account and considering c the requested channel. func (c *Channel) Match(c1 *Channel) ChannelMatch { requestedRiskLevel := riskLevel(c.Risk) rl1 := riskLevel(c1.Risk) return ChannelMatch{ Architecture: c.Architecture == c1.Architecture, Track: c.Track == c1.Track, Risk: requestedRiskLevel >= rl1, } } snapd-2.37.4~14.04.1/snap/epoch_test.go0000664000000000000000000003662213435556260014177 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package snap_test import ( "encoding/json" "github.com/snapcore/snapd/snap" "gopkg.in/check.v1" "gopkg.in/yaml.v2" ) type epochSuite struct{} var _ = check.Suite(&epochSuite{}) var ( // some duplication here maybe epochZeroStar = `0\* is an invalid epoch` hugeEpochNumber = `epoch numbers must be less than 2³², but got .*` badEpochNumber = `epoch numbers must be base 10 with no zero padding, but got .*` badEpochList = "epoch read/write attributes must be lists of epoch numbers" emptyEpochList = "epoch list cannot be explicitly empty" epochListNotIncreasing = "epoch list must be a strictly increasing sequence" epochListJustRidiculouslyLong = "epoch list must not have more than 10 entries" noEpochIntersection = "epoch read and write lists must have a non-empty intersection" ) func (s epochSuite) TestBadEpochs(c *check.C) { type Tt struct { s string e string y int } tests := []Tt{ {s: `"rubbish"`, e: badEpochNumber}, // SISO {s: `0xA`, e: badEpochNumber, y: 1}, // no hex {s: `"0xA"`, e: badEpochNumber}, // {s: `001`, e: badEpochNumber, y: 1}, // no octal, in fact no zero prefixes at all {s: `"001"`, e: badEpochNumber}, // {s: `{"read": 5}`, e: badEpochList}, // when split, must be list {s: `{"write": 5}`, e: badEpochList}, // {s: `{"read": "5"}`, e: badEpochList}, // {s: `{"write": "5"}`, e: badEpochList}, // {s: `{"read": "1*"}`, e: badEpochList}, // what {s: `{"read": [-1]}`, e: badEpochNumber}, // negative not allowed {s: `{"write": [-1]}`, e: badEpochNumber}, // {s: `{"read": ["-1"]}`, e: badEpochNumber}, // {s: `{"write": ["-1"]}`, e: badEpochNumber}, // {s: `{"read": ["yes"]}`, e: badEpochNumber}, // must be numbers {s: `{"write": ["yes"]}`, e: badEpochNumber}, // {s: `{"read": ["Ⅰ","Ⅱ"]}`, e: badEpochNumber}, // not roman numerals you idiot {s: `{"read": [0xA]}`, e: badEpochNumber, y: 1}, // {s: `{"read": [010]}`, e: badEpochNumber, y: 1}, // {s: `{"read": [9999999999]}`, e: hugeEpochNumber}, // you done yet? {s: `"0*"`, e: epochZeroStar}, // 0* means nothing {s: `"42**"`, e: badEpochNumber}, // N** is dead {s: `{"read": []}`, e: emptyEpochList}, // explicitly empty is bad {s: `{"write": []}`, e: emptyEpochList}, // {s: `{"read": [1,2,4,3]}`, e: epochListNotIncreasing}, // must be ordered {s: `{"read": [1,2,2,3]}`, e: epochListNotIncreasing}, // must be strictly increasing {s: `{"write": [4,3,2,1]}`, e: epochListNotIncreasing}, // ...*increasing* {s: `{"read": [0], "write": [1]}`, e: noEpochIntersection}, // must have at least one in common {s: `{"read": [0,1,2,3,4,5,6,7,8,9,10], "write": [0,1,2,3,4,5,6,7,8,9,10]}`, e: epochListJustRidiculouslyLong}, // must have <10 elements } for _, test := range tests { var v snap.Epoch err := yaml.Unmarshal([]byte(test.s), &v) c.Check(err, check.ErrorMatches, test.e, check.Commentf("YAML: %#q", test.s)) if test.y == 1 { continue } err = json.Unmarshal([]byte(test.s), &v) c.Check(err, check.ErrorMatches, test.e, check.Commentf("JSON: %#q", test.s)) } } func (s epochSuite) TestGoodEpochs(c *check.C) { type Tt struct { s string e snap.Epoch y int } tests := []Tt{ {s: `0`, e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}, y: 1}, {s: `""`, e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}}, {s: `"0"`, e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}}, {s: `{}`, e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}}, {s: `"2*"`, e: snap.Epoch{Read: []uint32{1, 2}, Write: []uint32{2}}}, {s: `{"read": [2]}`, e: snap.Epoch{Read: []uint32{2}, Write: []uint32{2}}}, {s: `{"read": [1, 2]}`, e: snap.Epoch{Read: []uint32{1, 2}, Write: []uint32{2}}}, {s: `{"write": [2]}`, e: snap.Epoch{Read: []uint32{2}, Write: []uint32{2}}}, {s: `{"write": [1, 2]}`, e: snap.Epoch{Read: []uint32{1, 2}, Write: []uint32{1, 2}}}, {s: `{"read": [2,4,8], "write": [2,3,5]}`, e: snap.Epoch{Read: []uint32{2, 4, 8}, Write: []uint32{2, 3, 5}}}, } for _, test := range tests { var v snap.Epoch err := yaml.Unmarshal([]byte(test.s), &v) c.Check(err, check.IsNil, check.Commentf("YAML: %s", test.s)) c.Check(v, check.DeepEquals, test.e) if test.y > 0 { continue } err = json.Unmarshal([]byte(test.s), &v) c.Check(err, check.IsNil, check.Commentf("JSON: %s", test.s)) c.Check(v, check.DeepEquals, test.e) } } func (s epochSuite) TestGoodEpochsInSnapYAML(c *check.C) { defer snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})() type Tt struct { s string e snap.Epoch } tests := []Tt{ {s: ``, e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}}, {s: `epoch: null`, e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}}, {s: `epoch: 0`, e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}}, {s: `epoch: "0"`, e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}}, {s: `epoch: {}`, e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}}, {s: `epoch: "2*"`, e: snap.Epoch{Read: []uint32{1, 2}, Write: []uint32{2}}}, {s: `epoch: {"read": [2]}`, e: snap.Epoch{Read: []uint32{2}, Write: []uint32{2}}}, {s: `epoch: {"read": [1, 2]}`, e: snap.Epoch{Read: []uint32{1, 2}, Write: []uint32{2}}}, {s: `epoch: {"write": [2]}`, e: snap.Epoch{Read: []uint32{2}, Write: []uint32{2}}}, {s: `epoch: {"write": [1, 2]}`, e: snap.Epoch{Read: []uint32{1, 2}, Write: []uint32{1, 2}}}, {s: `epoch: {"read": [2,4,8], "write": [2,3,5]}`, e: snap.Epoch{Read: []uint32{2, 4, 8}, Write: []uint32{2, 3, 5}}}, } for _, test := range tests { info, err := snap.InfoFromSnapYaml([]byte(test.s)) c.Check(err, check.IsNil, check.Commentf("YAML: %s", test.s)) c.Check(info.Epoch, check.DeepEquals, test.e) } } func (s epochSuite) TestGoodEpochsInJSON(c *check.C) { type Tt struct { s string e snap.Epoch } type Tinfo struct { Epoch snap.Epoch `json:"epoch"` } tests := []Tt{ // {} should give snap.Epoch{Read: []uint32{0}, Write: []uint32{0}} but needs an UnmarshalJSON on the parent {s: `{"epoch": null}`, e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}}, {s: `{"epoch": "0"}`, e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}}, {s: `{"epoch": {}}`, e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}}, {s: `{"epoch": "2*"}`, e: snap.Epoch{Read: []uint32{1, 2}, Write: []uint32{2}}}, {s: `{"epoch": {"read": [0]}}`, e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}}, {s: `{"epoch": {"write": [0]}}`, e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}}, {s: `{"epoch": {"read": [2]}}`, e: snap.Epoch{Read: []uint32{2}, Write: []uint32{2}}}, {s: `{"epoch": {"read": [1, 2]}}`, e: snap.Epoch{Read: []uint32{1, 2}, Write: []uint32{2}}}, {s: `{"epoch": {"write": [2]}}`, e: snap.Epoch{Read: []uint32{2}, Write: []uint32{2}}}, {s: `{"epoch": {"write": [1, 2]}}`, e: snap.Epoch{Read: []uint32{1, 2}, Write: []uint32{1, 2}}}, {s: `{"epoch": {"read": [2,4,8], "write": [2,3,5]}}`, e: snap.Epoch{Read: []uint32{2, 4, 8}, Write: []uint32{2, 3, 5}}}, } for _, test := range tests { var info Tinfo err := json.Unmarshal([]byte(test.s), &info) c.Check(err, check.IsNil, check.Commentf("JSON: %s", test.s)) c.Check(info.Epoch, check.DeepEquals, test.e, check.Commentf("JSON: %s", test.s)) } } func (s *epochSuite) TestEpochValidate(c *check.C) { validEpochs := []snap.Epoch{ {}, {Read: []uint32{0}, Write: []uint32{0}}, {Read: []uint32{0, 1}, Write: []uint32{1}}, {Read: []uint32{1}, Write: []uint32{1}}, {Read: []uint32{399, 400}, Write: []uint32{400}}, {Read: []uint32{1, 2, 3}, Write: []uint32{1, 2, 3}}, } for _, epoch := range validEpochs { err := epoch.Validate() c.Check(err, check.IsNil, check.Commentf("%s", epoch)) } invalidEpochs := []struct { epoch snap.Epoch err string }{ {epoch: snap.Epoch{Read: []uint32{}}, err: emptyEpochList}, {epoch: snap.Epoch{Write: []uint32{}}, err: emptyEpochList}, {epoch: snap.Epoch{Read: []uint32{}, Write: []uint32{}}, err: emptyEpochList}, {epoch: snap.Epoch{Read: []uint32{1}, Write: []uint32{2}}, err: noEpochIntersection}, {epoch: snap.Epoch{Read: []uint32{1, 3, 5}, Write: []uint32{2, 4, 6}}, err: noEpochIntersection}, {epoch: snap.Epoch{Read: []uint32{1, 2, 3}, Write: []uint32{3, 2, 1}}, err: epochListNotIncreasing}, {epoch: snap.Epoch{Read: []uint32{3, 2, 1}, Write: []uint32{1, 2, 3}}, err: epochListNotIncreasing}, {epoch: snap.Epoch{Read: []uint32{3, 2, 1}, Write: []uint32{3, 2, 1}}, err: epochListNotIncreasing}, {epoch: snap.Epoch{Read: []uint32{0, 0, 0}, Write: []uint32{0}}, err: epochListNotIncreasing}, {epoch: snap.Epoch{Read: []uint32{0}, Write: []uint32{0, 0, 0}}, err: epochListNotIncreasing}, {epoch: snap.Epoch{Read: []uint32{0, 0, 0}, Write: []uint32{0, 0, 0}}, err: epochListNotIncreasing}, {epoch: snap.Epoch{ Read: []uint32{0}, Write: []uint32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, }, err: epochListJustRidiculouslyLong}, {epoch: snap.Epoch{ Read: []uint32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, Write: []uint32{0}, }, err: epochListJustRidiculouslyLong}, {epoch: snap.Epoch{ Read: []uint32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, Write: []uint32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, }, err: epochListJustRidiculouslyLong}, } for _, test := range invalidEpochs { err := test.epoch.Validate() c.Check(err, check.ErrorMatches, test.err, check.Commentf("%s", test.epoch)) } } func (s *epochSuite) TestEpochString(c *check.C) { tests := []struct { e snap.Epoch s string }{ {e: snap.Epoch{}, s: "0"}, {e: snap.Epoch{Read: []uint32{0}}, s: "0"}, {e: snap.Epoch{Write: []uint32{0}}, s: "0"}, {e: snap.Epoch{Read: []uint32{0}, Write: []uint32{}}, s: "0"}, {e: snap.Epoch{Read: []uint32{}, Write: []uint32{0}}, s: "0"}, {e: snap.Epoch{Read: []uint32{}, Write: []uint32{}}, s: "0"}, {e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}, s: "0"}, {e: snap.Epoch{Read: []uint32{0, 1}, Write: []uint32{1}}, s: "1*"}, {e: snap.Epoch{Read: []uint32{1}, Write: []uint32{1}}, s: "1"}, {e: snap.Epoch{Read: []uint32{399, 400}, Write: []uint32{400}}, s: "400*"}, {e: snap.Epoch{Read: []uint32{1, 2, 3}, Write: []uint32{1, 2, 3}}, s: `{"read":[1,2,3],"write":[1,2,3]}`}, } for _, test := range tests { c.Check(test.e.String(), check.Equals, test.s, check.Commentf(test.s)) } } func (s *epochSuite) TestEpochMarshal(c *check.C) { tests := []struct { e snap.Epoch s string }{ {e: snap.Epoch{}, s: `{"read":[0],"write":[0]}`}, {e: snap.Epoch{Read: []uint32{0}}, s: `{"read":[0],"write":[0]}`}, {e: snap.Epoch{Write: []uint32{0}}, s: `{"read":[0],"write":[0]}`}, {e: snap.Epoch{Read: []uint32{0}, Write: []uint32{}}, s: `{"read":[0],"write":[0]}`}, {e: snap.Epoch{Read: []uint32{}, Write: []uint32{0}}, s: `{"read":[0],"write":[0]}`}, {e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}, s: `{"read":[0],"write":[0]}`}, {e: snap.Epoch{Read: []uint32{0, 1}, Write: []uint32{1}}, s: `{"read":[0,1],"write":[1]}`}, {e: snap.Epoch{Read: []uint32{1}, Write: []uint32{1}}, s: `{"read":[1],"write":[1]}`}, {e: snap.Epoch{Read: []uint32{399, 400}, Write: []uint32{400}}, s: `{"read":[399,400],"write":[400]}`}, {e: snap.Epoch{Read: []uint32{1, 2, 3}, Write: []uint32{1, 2, 3}}, s: `{"read":[1,2,3],"write":[1,2,3]}`}, } for _, test := range tests { bs, err := test.e.MarshalJSON() c.Assert(err, check.IsNil) c.Check(string(bs), check.Equals, test.s, check.Commentf(test.s)) bs, err = json.Marshal(test.e) c.Assert(err, check.IsNil) c.Check(string(bs), check.Equals, test.s, check.Commentf(test.s)) } } func (s *epochSuite) TestE(c *check.C) { tests := []struct { e snap.Epoch s string }{ {s: "0", e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}}, {s: "1", e: snap.Epoch{Read: []uint32{1}, Write: []uint32{1}}}, {s: "1*", e: snap.Epoch{Read: []uint32{0, 1}, Write: []uint32{1}}}, {s: "400*", e: snap.Epoch{Read: []uint32{399, 400}, Write: []uint32{400}}}, } for _, test := range tests { c.Check(snap.E(test.s), check.DeepEquals, test.e, check.Commentf(test.s)) c.Check(test.e.String(), check.Equals, test.s, check.Commentf(test.s)) } } func (s *epochSuite) TestIsZero(c *check.C) { for _, e := range []*snap.Epoch{ nil, {}, {Read: []uint32{0}}, {Write: []uint32{0}}, {Read: []uint32{0}, Write: []uint32{}}, {Read: []uint32{}, Write: []uint32{0}}, {Read: []uint32{0}, Write: []uint32{0}}, } { c.Check(e.IsZero(), check.Equals, true, check.Commentf("%#v", e)) } for _, e := range []*snap.Epoch{ {Read: []uint32{0, 1}, Write: []uint32{0}}, {Read: []uint32{1}, Write: []uint32{1, 2}}, } { c.Check(e.IsZero(), check.Equals, false, check.Commentf("%#v", e)) } } func (s *epochSuite) TestCanRead(c *check.C) { tests := []struct { a, b snap.Epoch ab, ba bool }{ {ab: true, ba: true}, // test for empty epoch {a: snap.E("0"), ab: true, ba: true}, // hybrid empty / zero {a: snap.E("0"), b: snap.E("1"), ab: false, ba: false}, {a: snap.E("0"), b: snap.E("1*"), ab: false, ba: true}, {a: snap.E("0"), b: snap.E("2*"), ab: false, ba: false}, { a: snap.Epoch{Read: []uint32{1, 2, 3}, Write: []uint32{2}}, b: snap.Epoch{Read: []uint32{1, 3, 4}, Write: []uint32{4}}, ab: false, ba: false, }, { a: snap.Epoch{Read: []uint32{1, 2, 3}, Write: []uint32{3}}, b: snap.Epoch{Read: []uint32{1, 2, 3}, Write: []uint32{2}}, ab: true, ba: true, }, } for i, test := range tests { c.Assert(test.a.CanRead(test.b), check.Equals, test.ab, check.Commentf("ab/%d", i)) c.Assert(test.b.CanRead(test.a), check.Equals, test.ba, check.Commentf("ba/%d", i)) } } func (s *epochSuite) TestEqual(c *check.C) { tests := []struct { a, b *snap.Epoch eq bool }{ {a: &snap.Epoch{}, b: nil, eq: true}, {a: &snap.Epoch{Read: []uint32{}, Write: []uint32{}}, b: nil, eq: true}, {a: &snap.Epoch{Read: []uint32{1}, Write: []uint32{1}}, b: &snap.Epoch{Read: []uint32{1}, Write: []uint32{1}}, eq: true}, {a: &snap.Epoch{Read: []uint32{0, 1}, Write: []uint32{1}}, b: &snap.Epoch{Read: []uint32{0, 1}, Write: []uint32{1}}, eq: true}, {a: &snap.Epoch{Read: []uint32{0, 1}, Write: []uint32{1}}, b: &snap.Epoch{Read: []uint32{1}, Write: []uint32{1}}, eq: false}, {a: &snap.Epoch{Read: []uint32{1, 2, 3, 4}, Write: []uint32{7}}, b: &snap.Epoch{Read: []uint32{1, 2, 3, 7}, Write: []uint32{7}}, eq: false}, } for i, test := range tests { c.Check(test.a.Equal(test.b), check.Equals, test.eq, check.Commentf("ab/%d", i)) c.Check(test.b.Equal(test.a), check.Equals, test.eq, check.Commentf("ab/%d", i)) } } snapd-2.37.4~14.04.1/snap/info.go0000664000000000000000000011102413435556260012763 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2016 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 . * */ package snap import ( "bytes" "fmt" "io/ioutil" "os" "path/filepath" "reflect" "sort" "strings" "time" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil/sys" "github.com/snapcore/snapd/strutil" "github.com/snapcore/snapd/timeout" ) // PlaceInfo offers all the information about where a snap and its data are located and exposed in the filesystem. type PlaceInfo interface { // InstanceName returns the name of the snap decorated with instance // key, if any. InstanceName() string // SnapName returns the name of the snap. SnapName() string // MountDir returns the base directory of the snap. MountDir() string // MountFile returns the path where the snap file that is mounted is installed. MountFile() string // HooksDir returns the directory containing the snap's hooks. HooksDir() string // DataDir returns the data directory of the snap. DataDir() string // UserDataDir returns the per user data directory of the snap. UserDataDir(home string) string // CommonDataDir returns the data directory common across revisions of the snap. CommonDataDir() string // UserCommonDataDir returns the per user data directory common across revisions of the snap. UserCommonDataDir(home string) string // UserXdgRuntimeDir returns the per user XDG_RUNTIME_DIR directory UserXdgRuntimeDir(userID sys.UserID) string // DataHomeDir returns the a glob that matches all per user data directories of a snap. DataHomeDir() string // CommonDataHomeDir returns a glob that matches all per user data directories common across revisions of the snap. CommonDataHomeDir() string // XdgRuntimeDirs returns a glob that matches all XDG_RUNTIME_DIR directories for all users of the snap. XdgRuntimeDirs() string } // MinimalPlaceInfo returns a PlaceInfo with just the location information for a snap of the given name and revision. func MinimalPlaceInfo(name string, revision Revision) PlaceInfo { storeName, instanceKey := SplitInstanceName(name) return &Info{SideInfo: SideInfo{RealName: storeName, Revision: revision}, InstanceKey: instanceKey} } // BaseDir returns the system level directory of given snap. func BaseDir(name string) string { return filepath.Join(dirs.SnapMountDir, name) } // MountDir returns the base directory where it gets mounted of the snap with the given name and revision. func MountDir(name string, revision Revision) string { return filepath.Join(BaseDir(name), revision.String()) } // MountFile returns the path where the snap file that is mounted is installed. func MountFile(name string, revision Revision) string { return filepath.Join(dirs.SnapBlobDir, fmt.Sprintf("%s_%s.snap", name, revision)) } // ScopedSecurityTag returns the snap-specific, scope specific, security tag. func ScopedSecurityTag(snapName, scopeName, suffix string) string { return fmt.Sprintf("snap.%s.%s.%s", snapName, scopeName, suffix) } // SecurityTag returns the snap-specific security tag. func SecurityTag(snapName string) string { return fmt.Sprintf("snap.%s", snapName) } // AppSecurityTag returns the application-specific security tag. func AppSecurityTag(snapName, appName string) string { return fmt.Sprintf("%s.%s", SecurityTag(snapName), appName) } // HookSecurityTag returns the hook-specific security tag. func HookSecurityTag(snapName, hookName string) string { return ScopedSecurityTag(snapName, "hook", hookName) } // NoneSecurityTag returns the security tag for interfaces that // are not associated to an app or hook in the snap. func NoneSecurityTag(snapName, uniqueName string) string { return ScopedSecurityTag(snapName, "none", uniqueName) } // BaseDataDir returns the base directory for snap data locations. func BaseDataDir(name string) string { return filepath.Join(dirs.SnapDataDir, name) } // DataDir returns the data directory for given snap name and revision. The name can be // either a snap name or snap instance name. func DataDir(name string, revision Revision) string { return filepath.Join(BaseDataDir(name), revision.String()) } // CommonDataDir returns the common data directory for given snap name. The name // can be either a snap name or snap instance name. func CommonDataDir(name string) string { return filepath.Join(dirs.SnapDataDir, name, "common") } // HooksDir returns the directory containing the snap's hooks for given snap // name. The name can be either a snap name or snap instance name. func HooksDir(name string, revision Revision) string { return filepath.Join(MountDir(name, revision), "meta", "hooks") } // UserDataDir returns the user-specific data directory for given snap name. The // name can be either a snap name or snap instance name. func UserDataDir(home string, name string, revision Revision) string { return filepath.Join(home, dirs.UserHomeSnapDir, name, revision.String()) } // UserCommonDataDir returns the user-specific common data directory for given // snap name. The name can be either a snap name or snap instance name. func UserCommonDataDir(home string, name string) string { return filepath.Join(home, dirs.UserHomeSnapDir, name, "common") } // UserSnapDir returns the user-specific directory for given // snap name. The name can be either a snap name or snap instance name. func UserSnapDir(home string, name string) string { return filepath.Join(home, dirs.UserHomeSnapDir, name) } // UserXdgRuntimeDir returns the user-specific XDG_RUNTIME_DIR directory for // given snap name. The name can be either a snap name or snap instance name. func UserXdgRuntimeDir(euid sys.UserID, name string) string { return filepath.Join(dirs.XdgRuntimeDirBase, fmt.Sprintf("%d/snap.%s", euid, name)) } // SideInfo holds snap metadata that is crucial for the tracking of // snaps and for the working of the system offline and which is not // included in snap.yaml or for which the store is the canonical // source overriding snap.yaml content. // // It can be marshalled and will be stored in the system state for // each currently installed snap revision so it needs to be evolved // carefully. // // Information that can be taken directly from snap.yaml or that comes // from the store but is not required for working offline should not // end up in SideInfo. type SideInfo struct { RealName string `yaml:"name,omitempty" json:"name,omitempty"` SnapID string `yaml:"snap-id" json:"snap-id"` Revision Revision `yaml:"revision" json:"revision"` Channel string `yaml:"channel,omitempty" json:"channel,omitempty"` Contact string `yaml:"contact,omitempty" json:"contact,omitempty"` EditedTitle string `yaml:"title,omitempty" json:"title,omitempty"` EditedSummary string `yaml:"summary,omitempty" json:"summary,omitempty"` EditedDescription string `yaml:"description,omitempty" json:"description,omitempty"` Private bool `yaml:"private,omitempty" json:"private,omitempty"` Paid bool `yaml:"paid,omitempty" json:"paid,omitempty"` } // Info provides information about snaps. type Info struct { SuggestedName string InstanceKey string Version string Type Type Architectures []string Assumes []string OriginalTitle string OriginalSummary string OriginalDescription string Environment strutil.OrderedMap LicenseAgreement string LicenseVersion string License string Epoch Epoch Base string Confinement ConfinementType Apps map[string]*AppInfo LegacyAliases map[string]*AppInfo // FIXME: eventually drop this Hooks map[string]*HookInfo Plugs map[string]*PlugInfo Slots map[string]*SlotInfo toplevelPlugs []*PlugInfo toplevelSlots []*SlotInfo // Plugs or slots with issues (they are not included in Plugs or Slots) BadInterfaces map[string]string // slot or plug => message // The information in all the remaining fields is not sourced from the snap blob itself. SideInfo // Broken marks whether the snap is broken and the reason. Broken string // The information in these fields is ephemeral, available only from the store. DownloadInfo Prices map[string]float64 MustBuy bool Publisher StoreAccount Media MediaInfos // The flattended channel map with $track/$risk Channels map[string]*ChannelSnapInfo // The ordered list of tracks that contain channels Tracks []string Layout map[string]*Layout // The list of common-ids from all apps of the snap CommonIDs []string } // StoreAccount holds information about a store account, for example // of snap publisher. type StoreAccount struct { ID string `json:"id"` Username string `json:"username"` DisplayName string `json:"display-name"` Validation string `json:"validation,omitempty"` } // Layout describes a single element of the layout section. type Layout struct { Snap *Info Path string `json:"path"` Bind string `json:"bind,omitempty"` BindFile string `json:"bind-file,omitempty"` Type string `json:"type,omitempty"` User string `json:"user,omitempty"` Group string `json:"group,omitempty"` Mode os.FileMode `json:"mode,omitempty"` Symlink string `json:"symlink,omitempty"` } // String returns a simple textual representation of a layout. func (l *Layout) String() string { var buf bytes.Buffer fmt.Fprintf(&buf, "%s: ", l.Path) switch { case l.Bind != "": fmt.Fprintf(&buf, "bind %s", l.Bind) case l.BindFile != "": fmt.Fprintf(&buf, "bind-file %s", l.BindFile) case l.Symlink != "": fmt.Fprintf(&buf, "symlink %s", l.Symlink) case l.Type != "": fmt.Fprintf(&buf, "type %s", l.Type) default: fmt.Fprintf(&buf, "???") } if l.User != "root" && l.User != "" { fmt.Fprintf(&buf, ", user: %s", l.User) } if l.Group != "root" && l.Group != "" { fmt.Fprintf(&buf, ", group: %s", l.Group) } if l.Mode != 0755 { fmt.Fprintf(&buf, ", mode: %#o", l.Mode) } return buf.String() } // ChannelSnapInfo is the minimum information that can be used to clearly // distinguish different revisions of the same snap. type ChannelSnapInfo struct { Revision Revision `json:"revision"` Confinement ConfinementType `json:"confinement"` Version string `json:"version"` Channel string `json:"channel"` Epoch Epoch `json:"epoch"` Size int64 `json:"size"` ReleasedAt time.Time `json:"released-at"` } // InstanceName returns the blessed name of the snap decorated with instance // key, if any. func (s *Info) InstanceName() string { return InstanceName(s.SnapName(), s.InstanceKey) } // SnapName returns the global blessed name of the snap. func (s *Info) SnapName() string { if s.RealName != "" { return s.RealName } return s.SuggestedName } // Title returns the blessed title for the snap. func (s *Info) Title() string { if s.EditedTitle != "" { return s.EditedTitle } return s.OriginalTitle } // Summary returns the blessed summary for the snap. func (s *Info) Summary() string { if s.EditedSummary != "" { return s.EditedSummary } return s.OriginalSummary } // Description returns the blessed description for the snap. func (s *Info) Description() string { if s.EditedDescription != "" { return s.EditedDescription } return s.OriginalDescription } // MountDir returns the base directory of the snap where it gets mounted. func (s *Info) MountDir() string { return MountDir(s.InstanceName(), s.Revision) } // MountFile returns the path where the snap file that is mounted is installed. func (s *Info) MountFile() string { return MountFile(s.InstanceName(), s.Revision) } // HooksDir returns the directory containing the snap's hooks. func (s *Info) HooksDir() string { return HooksDir(s.InstanceName(), s.Revision) } // DataDir returns the data directory of the snap. func (s *Info) DataDir() string { return DataDir(s.InstanceName(), s.Revision) } // UserDataDir returns the user-specific data directory of the snap. func (s *Info) UserDataDir(home string) string { return UserDataDir(home, s.InstanceName(), s.Revision) } // UserCommonDataDir returns the user-specific data directory common across revision of the snap. func (s *Info) UserCommonDataDir(home string) string { return UserCommonDataDir(home, s.InstanceName()) } // CommonDataDir returns the data directory common across revisions of the snap. func (s *Info) CommonDataDir() string { return CommonDataDir(s.InstanceName()) } // DataHomeDir returns the per user data directory of the snap. func (s *Info) DataHomeDir() string { return filepath.Join(dirs.SnapDataHomeGlob, s.InstanceName(), s.Revision.String()) } // CommonDataHomeDir returns the per user data directory common across revisions of the snap. func (s *Info) CommonDataHomeDir() string { return filepath.Join(dirs.SnapDataHomeGlob, s.InstanceName(), "common") } // UserXdgRuntimeDir returns the XDG_RUNTIME_DIR directory of the snap for a particular user. func (s *Info) UserXdgRuntimeDir(euid sys.UserID) string { return UserXdgRuntimeDir(euid, s.InstanceName()) } // XdgRuntimeDirs returns the XDG_RUNTIME_DIR directories for all users of the snap. func (s *Info) XdgRuntimeDirs() string { return filepath.Join(dirs.XdgRuntimeDirGlob, fmt.Sprintf("snap.%s", s.InstanceName())) } // NeedsDevMode returns whether the snap needs devmode. func (s *Info) NeedsDevMode() bool { return s.Confinement == DevModeConfinement } // NeedsClassic returns whether the snap needs classic confinement consent. func (s *Info) NeedsClassic() bool { return s.Confinement == ClassicConfinement } // Services returns a list of the apps that have "daemon" set. func (s *Info) Services() []*AppInfo { svcs := make([]*AppInfo, 0, len(s.Apps)) for _, app := range s.Apps { if !app.IsService() { continue } svcs = append(svcs, app) } return svcs } // ExpandSnapVariables resolves $SNAP, $SNAP_DATA and $SNAP_COMMON inside the // snap's mount namespace. func (s *Info) ExpandSnapVariables(path string) string { return os.Expand(path, func(v string) string { switch v { case "SNAP": // NOTE: We use dirs.CoreSnapMountDir here as the path used will be always // inside the mount namespace snap-confine creates and there we will // always have a /snap directory available regardless if the system // we're running on supports this or not. return filepath.Join(dirs.CoreSnapMountDir, s.SnapName(), s.Revision.String()) case "SNAP_DATA": return DataDir(s.SnapName(), s.Revision) case "SNAP_COMMON": return CommonDataDir(s.SnapName()) } return "" }) } // InstallDate returns the "install date" of the snap. // // If the snap is not active, it'll return a zero time; otherwise // it'll return the modtime of the "current" symlink. Sneaky. func (s *Info) InstallDate() time.Time { dir, rev := filepath.Split(s.MountDir()) cur := filepath.Join(dir, "current") tag, err := os.Readlink(cur) if err == nil && tag == rev { if st, err := os.Lstat(cur); err == nil { return st.ModTime() } } return time.Time{} } // IsActive returns whether this snap revision is active. func (s *Info) IsActive() bool { dir, rev := filepath.Split(s.MountDir()) cur := filepath.Join(dir, "current") tag, err := os.Readlink(cur) return err == nil && tag == rev } // BadInterfacesSummary returns a summary of the problems of bad plugs // and slots in the snap. func BadInterfacesSummary(snapInfo *Info) string { inverted := make(map[string][]string) for name, reason := range snapInfo.BadInterfaces { inverted[reason] = append(inverted[reason], name) } var buf bytes.Buffer fmt.Fprintf(&buf, "snap %q has bad plugs or slots: ", snapInfo.InstanceName()) reasons := make([]string, 0, len(inverted)) for reason := range inverted { reasons = append(reasons, reason) } sort.Strings(reasons) for _, reason := range reasons { names := inverted[reason] sort.Strings(names) for i, name := range names { if i > 0 { buf.WriteString(", ") } buf.WriteString(name) } fmt.Fprintf(&buf, " (%s); ", reason) } return strings.TrimSuffix(buf.String(), "; ") } // DownloadInfo contains the information to download a snap. // It can be marshalled. type DownloadInfo struct { AnonDownloadURL string `json:"anon-download-url,omitempty"` DownloadURL string `json:"download-url,omitempty"` Size int64 `json:"size,omitempty"` Sha3_384 string `json:"sha3-384,omitempty"` // The server can include information about available deltas for a given // snap at a specific revision during refresh. Currently during refresh the // server will provide single matching deltas only, from the clients // revision to the target revision when available, per requested format. Deltas []DeltaInfo `json:"deltas,omitempty"` } // DeltaInfo contains the information to download a delta // from one revision to another. type DeltaInfo struct { FromRevision int `json:"from-revision,omitempty"` ToRevision int `json:"to-revision,omitempty"` Format string `json:"format,omitempty"` AnonDownloadURL string `json:"anon-download-url,omitempty"` DownloadURL string `json:"download-url,omitempty"` Size int64 `json:"size,omitempty"` Sha3_384 string `json:"sha3-384,omitempty"` } // sanity check that Info is a PlaceInfo var _ PlaceInfo = (*Info)(nil) // PlugInfo provides information about a plug. type PlugInfo struct { Snap *Info Name string Interface string Attrs map[string]interface{} Label string Apps map[string]*AppInfo Hooks map[string]*HookInfo } func lookupAttr(attrs map[string]interface{}, path string) (interface{}, bool) { var v interface{} comps := strings.FieldsFunc(path, func(r rune) bool { return r == '.' }) if len(comps) == 0 { return nil, false } v = attrs for _, comp := range comps { m, ok := v.(map[string]interface{}) if !ok { return nil, false } v, ok = m[comp] if !ok { return nil, false } } return v, true } func getAttribute(snapName string, ifaceName string, attrs map[string]interface{}, key string, val interface{}) error { v, ok := lookupAttr(attrs, key) if !ok { return fmt.Errorf("snap %q does not have attribute %q for interface %q", snapName, key, ifaceName) } rt := reflect.TypeOf(val) if rt.Kind() != reflect.Ptr || val == nil { return fmt.Errorf("internal error: cannot get %q attribute of interface %q with non-pointer value", key, ifaceName) } if reflect.TypeOf(v) != rt.Elem() { return fmt.Errorf("snap %q has interface %q with invalid value type for %q attribute", snapName, ifaceName, key) } rv := reflect.ValueOf(val) rv.Elem().Set(reflect.ValueOf(v)) return nil } func (plug *PlugInfo) Attr(key string, val interface{}) error { return getAttribute(plug.Snap.InstanceName(), plug.Interface, plug.Attrs, key, val) } func (plug *PlugInfo) Lookup(key string) (interface{}, bool) { return lookupAttr(plug.Attrs, key) } // SecurityTags returns security tags associated with a given plug. func (plug *PlugInfo) SecurityTags() []string { tags := make([]string, 0, len(plug.Apps)+len(plug.Hooks)) for _, app := range plug.Apps { tags = append(tags, app.SecurityTag()) } for _, hook := range plug.Hooks { tags = append(tags, hook.SecurityTag()) } sort.Strings(tags) return tags } // String returns the representation of the plug as snap:plug string. func (plug *PlugInfo) String() string { return fmt.Sprintf("%s:%s", plug.Snap.InstanceName(), plug.Name) } func (slot *SlotInfo) Attr(key string, val interface{}) error { return getAttribute(slot.Snap.InstanceName(), slot.Interface, slot.Attrs, key, val) } func (slot *SlotInfo) Lookup(key string) (interface{}, bool) { return lookupAttr(slot.Attrs, key) } // SecurityTags returns security tags associated with a given slot. func (slot *SlotInfo) SecurityTags() []string { tags := make([]string, 0, len(slot.Apps)) for _, app := range slot.Apps { tags = append(tags, app.SecurityTag()) } for _, hook := range slot.Hooks { tags = append(tags, hook.SecurityTag()) } sort.Strings(tags) return tags } // String returns the representation of the slot as snap:slot string. func (slot *SlotInfo) String() string { return fmt.Sprintf("%s:%s", slot.Snap.InstanceName(), slot.Name) } // SlotInfo provides information about a slot. type SlotInfo struct { Snap *Info Name string Interface string Attrs map[string]interface{} Label string Apps map[string]*AppInfo Hooks map[string]*HookInfo // HotplugKey is a unique key built by the slot's interface // using properties of a hotplugged device so that the same // slot may be made available if the device is reinserted. // It's empty for regular slots. HotplugKey string } // SocketInfo provides information on application sockets. type SocketInfo struct { App *AppInfo Name string ListenStream string SocketMode os.FileMode } // TimerInfo provides information on application timer. type TimerInfo struct { App *AppInfo Timer string } // StopModeType is the type for the "stop-mode:" of a snap app type StopModeType string // KillAll returns if the stop-mode means all processes should be killed // when the service is stopped or just the main process. func (st StopModeType) KillAll() bool { return string(st) == "" || strings.HasSuffix(string(st), "-all") } // KillSignal returns the signal that should be used to kill the process // (or an empty string if no signal is needed) func (st StopModeType) KillSignal() string { if st.Validate() != nil || st == "" { return "" } return strings.ToUpper(strings.TrimSuffix(string(st), "-all")) } func (st StopModeType) Validate() error { switch st { case "", "sigterm", "sigterm-all", "sighup", "sighup-all", "sigusr1", "sigusr1-all", "sigusr2", "sigusr2-all": // valid return nil } return fmt.Errorf(`"stop-mode" field contains invalid value %q`, st) } // AppInfo provides information about an app. type AppInfo struct { Snap *Info Name string LegacyAliases []string // FIXME: eventually drop this Command string CommandChain []string CommonID string Daemon string StopTimeout timeout.Timeout WatchdogTimeout timeout.Timeout StopCommand string ReloadCommand string PostStopCommand string RestartCond RestartCondition RestartDelay timeout.Timeout Completer string RefreshMode string StopMode StopModeType // TODO: this should go away once we have more plumbing and can change // things vs refactor // https://github.com/snapcore/snapd/pull/794#discussion_r58688496 BusName string Plugs map[string]*PlugInfo Slots map[string]*SlotInfo Sockets map[string]*SocketInfo Environment strutil.OrderedMap // list of other service names that this service will start after or // before After []string Before []string Timer *TimerInfo Autostart string } // ScreenshotInfo provides information about a screenshot. type ScreenshotInfo struct { URL string `json:"url"` Width int64 `json:"width,omitempty"` Height int64 `json:"height,omitempty"` Note string `json:"note,omitempty"` } type MediaInfo struct { Type string `json:"type"` URL string `json:"url"` Width int64 `json:"width,omitempty"` Height int64 `json:"height,omitempty"` } type MediaInfos []MediaInfo const ScreenshotsDeprecationNotice = `'screenshots' is deprecated; use 'media' instead. More info at https://forum.snapcraft.io/t/8086` func (mis MediaInfos) Screenshots() []ScreenshotInfo { shots := make([]ScreenshotInfo, 0, len(mis)) for _, mi := range mis { if mi.Type != "screenshot" { continue } shots = append(shots, ScreenshotInfo{ URL: mi.URL, Width: mi.Width, Height: mi.Height, Note: ScreenshotsDeprecationNotice, }) } return shots } func (mis MediaInfos) IconURL() string { for _, mi := range mis { if mi.Type == "icon" { return mi.URL } } return "" } // HookInfo provides information about a hook. type HookInfo struct { Snap *Info Name string Plugs map[string]*PlugInfo Slots map[string]*SlotInfo Environment strutil.OrderedMap CommandChain []string Explicit bool } // File returns the path to the *.socket file func (socket *SocketInfo) File() string { return filepath.Join(dirs.SnapServicesDir, socket.App.SecurityTag()+"."+socket.Name+".socket") } // File returns the path to the *.timer file func (timer *TimerInfo) File() string { return filepath.Join(dirs.SnapServicesDir, timer.App.SecurityTag()+".timer") } func (app *AppInfo) String() string { return JoinSnapApp(app.Snap.InstanceName(), app.Name) } // SecurityTag returns application-specific security tag. // // Security tags are used by various security subsystems as "profile names" and // sometimes also as a part of the file name. func (app *AppInfo) SecurityTag() string { return AppSecurityTag(app.Snap.InstanceName(), app.Name) } // DesktopFile returns the path to the installed optional desktop file for the application. func (app *AppInfo) DesktopFile() string { return filepath.Join(dirs.SnapDesktopFilesDir, fmt.Sprintf("%s_%s.desktop", app.Snap.InstanceName(), app.Name)) } // WrapperPath returns the path to wrapper invoking the app binary. func (app *AppInfo) WrapperPath() string { return filepath.Join(dirs.SnapBinariesDir, JoinSnapApp(app.Snap.InstanceName(), app.Name)) } // CompleterPath returns the path to the completer snippet for the app binary. func (app *AppInfo) CompleterPath() string { return filepath.Join(dirs.CompletersDir, JoinSnapApp(app.Snap.InstanceName(), app.Name)) } func (app *AppInfo) launcherCommand(command string) string { if command != "" { command = " " + command } if app.Name == app.Snap.SnapName() { return fmt.Sprintf("/usr/bin/snap run%s %s", command, app.Snap.InstanceName()) } return fmt.Sprintf("/usr/bin/snap run%s %s.%s", command, app.Snap.InstanceName(), app.Name) } // LauncherCommand returns the launcher command line to use when invoking the app binary. func (app *AppInfo) LauncherCommand() string { if app.Timer != nil { return app.launcherCommand(fmt.Sprintf("--timer=%q", app.Timer.Timer)) } return app.launcherCommand("") } // LauncherStopCommand returns the launcher command line to use when invoking the app stop command binary. func (app *AppInfo) LauncherStopCommand() string { return app.launcherCommand("--command=stop") } // LauncherReloadCommand returns the launcher command line to use when invoking the app stop command binary. func (app *AppInfo) LauncherReloadCommand() string { return app.launcherCommand("--command=reload") } // LauncherPostStopCommand returns the launcher command line to use when invoking the app post-stop command binary. func (app *AppInfo) LauncherPostStopCommand() string { return app.launcherCommand("--command=post-stop") } // ServiceName returns the systemd service name for the daemon app. func (app *AppInfo) ServiceName() string { return app.SecurityTag() + ".service" } // ServiceFile returns the systemd service file path for the daemon app. func (app *AppInfo) ServiceFile() string { return filepath.Join(dirs.SnapServicesDir, app.ServiceName()) } // Env returns the app specific environment overrides func (app *AppInfo) Env() []string { appEnv := app.Snap.Environment.Copy() for _, k := range app.Environment.Keys() { appEnv.Set(k, app.Environment.Get(k)) } return envFromMap(appEnv) } // IsService returns whether app represents a daemon/service. func (app *AppInfo) IsService() bool { return app.Daemon != "" } // SecurityTag returns the hook-specific security tag. // // Security tags are used by various security subsystems as "profile names" and // sometimes also as a part of the file name. func (hook *HookInfo) SecurityTag() string { return HookSecurityTag(hook.Snap.InstanceName(), hook.Name) } // Env returns the hook-specific environment overrides func (hook *HookInfo) Env() []string { hookEnv := hook.Snap.Environment.Copy() for _, k := range hook.Environment.Keys() { hookEnv.Set(k, hook.Environment.Get(k)) } return envFromMap(hookEnv) } func envFromMap(envMap *strutil.OrderedMap) []string { env := []string{} for _, k := range envMap.Keys() { env = append(env, fmt.Sprintf("%s=%s", k, envMap.Get(k))) } return env } func infoFromSnapYamlWithSideInfo(meta []byte, si *SideInfo) (*Info, error) { info, err := InfoFromSnapYaml(meta) if err != nil { return nil, err } if si != nil { info.SideInfo = *si } return info, nil } // BrokenSnapError describes an error that refers to a snap that warrants the "broken" note. type BrokenSnapError interface { error Broken() string } type NotFoundError struct { Snap string Revision Revision // Path encodes the path that triggered the not-found error. // It may refer to a file inside the snap or to the snap file itself. Path string } func (e NotFoundError) Error() string { if e.Path != "" { return fmt.Sprintf("cannot find installed snap %q at revision %s: missing file %s", e.Snap, e.Revision, e.Path) } return fmt.Sprintf("cannot find installed snap %q at revision %s", e.Snap, e.Revision) } func (e NotFoundError) Broken() string { return e.Error() } type invalidMetaError struct { Snap string Revision Revision Msg string } func (e invalidMetaError) Error() string { return fmt.Sprintf("cannot use installed snap %q at revision %s: %s", e.Snap, e.Revision, e.Msg) } func (e invalidMetaError) Broken() string { return e.Error() } func MockSanitizePlugsSlots(f func(snapInfo *Info)) (restore func()) { old := SanitizePlugsSlots SanitizePlugsSlots = f return func() { SanitizePlugsSlots = old } } var SanitizePlugsSlots = func(snapInfo *Info) { panic("SanitizePlugsSlots function not set") } // ReadInfo reads the snap information for the installed snap with the given name and given side-info. func ReadInfo(name string, si *SideInfo) (*Info, error) { snapYamlFn := filepath.Join(MountDir(name, si.Revision), "meta", "snap.yaml") meta, err := ioutil.ReadFile(snapYamlFn) if os.IsNotExist(err) { return nil, &NotFoundError{Snap: name, Revision: si.Revision, Path: snapYamlFn} } if err != nil { return nil, err } info, err := infoFromSnapYamlWithSideInfo(meta, si) if err != nil { return nil, &invalidMetaError{Snap: name, Revision: si.Revision, Msg: err.Error()} } _, instanceKey := SplitInstanceName(name) info.InstanceKey = instanceKey err = addImplicitHooks(info) if err != nil { return nil, &invalidMetaError{Snap: name, Revision: si.Revision, Msg: err.Error()} } bindImplicitHooks(info) mountFile := MountFile(name, si.Revision) st, err := os.Lstat(mountFile) if os.IsNotExist(err) { // This can happen when "snap try" mode snap is moved around. The mount // is still in place (it's a bind mount, it doesn't care about the // source moving) but the symlink in /var/lib/snapd/snaps is now // dangling. return nil, &NotFoundError{Snap: name, Revision: si.Revision, Path: mountFile} } if err != nil { return nil, err } // If the file is a regular file than it must be a squashfs file that is // used as the backing store for the snap. The size of that file is the // size of the snap. if st.Mode().IsRegular() { info.Size = st.Size() } return info, nil } // ReadCurrentInfo reads the snap information from the installed snap in 'current' revision func ReadCurrentInfo(snapName string) (*Info, error) { curFn := filepath.Join(dirs.SnapMountDir, snapName, "current") realFn, err := os.Readlink(curFn) if err != nil { return nil, fmt.Errorf("cannot find current revision for snap %s: %s", snapName, err) } rev := filepath.Base(realFn) revision, err := ParseRevision(rev) if err != nil { return nil, fmt.Errorf("cannot read revision %s: %s", rev, err) } return ReadInfo(snapName, &SideInfo{Revision: revision}) } // ReadInfoFromSnapFile reads the snap information from the given Container // and completes it with the given side-info if this is not nil. func ReadInfoFromSnapFile(snapf Container, si *SideInfo) (*Info, error) { meta, err := snapf.ReadFile("meta/snap.yaml") if err != nil { return nil, err } info, err := infoFromSnapYamlWithSideInfo(meta, si) if err != nil { return nil, err } info.Size, err = snapf.Size() if err != nil { return nil, err } err = addImplicitHooksFromContainer(info, snapf) if err != nil { return nil, err } bindImplicitHooks(info) err = Validate(info) if err != nil { return nil, err } return info, nil } // InstallDate returns the "install date" of the snap. // // If the snap is not active, it'll return a zero time; otherwise // it'll return the modtime of the "current" symlink. func InstallDate(name string) time.Time { cur := filepath.Join(dirs.SnapMountDir, name, "current") if st, err := os.Lstat(cur); err == nil { return st.ModTime() } return time.Time{} } // SplitSnapApp will split a string of the form `snap.app` into // the `snap` and the `app` part. It also deals with the special // case of snapName == appName. func SplitSnapApp(snapApp string) (snap, app string) { l := strings.SplitN(snapApp, ".", 2) if len(l) < 2 { return l[0], InstanceSnap(l[0]) } return l[0], l[1] } // JoinSnapApp produces a full application wrapper name from the // `snap` and the `app` part. It also deals with the special // case of snapName == appName. func JoinSnapApp(snap, app string) string { storeName, instanceKey := SplitInstanceName(snap) if storeName == app { return InstanceName(app, instanceKey) } return fmt.Sprintf("%s.%s", snap, app) } // InstanceSnap splits the instance name and returns the name of the snap. func InstanceSnap(instanceName string) string { snapName, _ := SplitInstanceName(instanceName) return snapName } // SplitInstanceName splits the instance name and returns the snap name and the // instance key. func SplitInstanceName(instanceName string) (snapName, instanceKey string) { split := strings.SplitN(instanceName, "_", 2) snapName = split[0] if len(split) > 1 { instanceKey = split[1] } return snapName, instanceKey } // InstanceName takes the snap name and the instance key and returns an instance // name of the snap. func InstanceName(snapName, instanceKey string) string { if instanceKey != "" { return fmt.Sprintf("%s_%s", snapName, instanceKey) } return snapName } // ByType supports sorting the given slice of snap info by types. The most // important types will come first. type ByType []*Info func (r ByType) Len() int { return len(r) } func (r ByType) Swap(i, j int) { r[i], r[j] = r[j], r[i] } func (r ByType) Less(i, j int) bool { return r[i].Type.SortsBefore(r[j].Type) } func SortServices(apps []*AppInfo) (sorted []*AppInfo, err error) { nameToApp := make(map[string]*AppInfo, len(apps)) for _, app := range apps { nameToApp[app.Name] = app } // list of successors of given app successors := make(map[string][]*AppInfo, len(apps)) // count of predecessors (i.e. incoming edges) of given app predecessors := make(map[string]int, len(apps)) for _, app := range apps { for _, other := range app.After { predecessors[app.Name]++ successors[other] = append(successors[other], app) } for _, other := range app.Before { predecessors[other]++ successors[app.Name] = append(successors[app.Name], nameToApp[other]) } } // list of apps without predecessors (no incoming edges) queue := make([]*AppInfo, 0, len(apps)) for _, app := range apps { if predecessors[app.Name] == 0 { queue = append(queue, app) } } // Kahn: // see https://dl.acm.org/citation.cfm?doid=368996.369025 // https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm // // Apps without predecessors are 'top' nodes. On each iteration, take // the next 'top' node, and decrease the predecessor count of each // successor app. Once that successor app has no more predecessors, take // it out of the predecessors set and add it to the queue of 'top' // nodes. for len(queue) > 0 { app := queue[0] queue = queue[1:] for _, successor := range successors[app.Name] { predecessors[successor.Name]-- if predecessors[successor.Name] == 0 { delete(predecessors, successor.Name) queue = append(queue, successor) } } sorted = append(sorted, app) } if len(predecessors) != 0 { // apps with predecessors unaccounted for are a part of // dependency cycle unsatisifed := bytes.Buffer{} for name := range predecessors { if unsatisifed.Len() > 0 { unsatisifed.WriteString(", ") } unsatisifed.WriteString(name) } return nil, fmt.Errorf("applications are part of a before/after cycle: %s", unsatisifed.String()) } return sorted, nil } snapd-2.37.4~14.04.1/snap/container.go0000664000000000000000000002012713435556260014015 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2015 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 . * */ package snap import ( "bytes" "errors" "fmt" "os" "path/filepath" "strings" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/snap/snapdir" "github.com/snapcore/snapd/snap/squashfs" ) // Container is the interface to interact with the low-level snap files. type Container interface { // Size returns the size of the snap in bytes. Size() (int64, error) // ReadFile returns the content of a single file from the snap. ReadFile(relative string) ([]byte, error) // Walk is like filepath.Walk, without the ordering guarantee. Walk(relative string, walkFn filepath.WalkFunc) error // ListDir returns the content of a single directory inside the snap. ListDir(path string) ([]string, error) // Install copies the snap file to targetPath (and possibly unpacks it to mountDir) Install(targetPath, mountDir string) error // Unpack unpacks the src parts to the dst directory Unpack(src, dst string) error } // backend implements a specific snap format type snapFormat struct { magic []byte open func(fn string) (Container, error) } // formatHandlers is the registry of known formats, squashfs is the only one atm. var formatHandlers = []snapFormat{ {squashfs.Magic, func(p string) (Container, error) { return squashfs.New(p), nil }}, } // Open opens a given snap file with the right backend. func Open(path string) (Container, error) { if osutil.IsDirectory(path) { if osutil.FileExists(filepath.Join(path, "meta", "snap.yaml")) { return snapdir.New(path), nil } return nil, NotSnapError{Path: path} } // open the file and check magic f, err := os.Open(path) if err != nil { return nil, fmt.Errorf("cannot open snap: %v", err) } defer f.Close() header := make([]byte, 20) if _, err := f.ReadAt(header, 0); err != nil { return nil, fmt.Errorf("cannot read snap: %v", err) } for _, h := range formatHandlers { if bytes.HasPrefix(header, h.magic) { return h.open(path) } } return nil, fmt.Errorf("cannot open snap: unknown header: %q", header) } var ( // ErrBadModes is returned by ValidateContainer when the container has files with the wrong file modes for their role ErrBadModes = errors.New("snap is unusable due to bad permissions") // ErrMissingPaths is returned by ValidateContainer when the container is missing required files or directories ErrMissingPaths = errors.New("snap is unusable due to missing files") ) // ValidateContainer does a minimal sanity check on the container. func ValidateContainer(c Container, s *Info, logf func(format string, v ...interface{})) error { // needsrx keeps track of things that need to have at least 0555 perms needsrx := map[string]bool{ ".": true, "meta": true, } // needsx keeps track of things that need to have at least 0111 perms needsx := map[string]bool{} // needsr keeps track of things that need to have at least 0444 perms needsr := map[string]bool{ "meta/snap.yaml": true, } // needsf keeps track of things that need to be regular files (or symlinks to regular files) needsf := map[string]bool{} // noskipd tracks directories we want to descend into despite not being in needs* noskipd := map[string]bool{} for _, app := range s.Apps { // for non-services, paths go into the needsrx bag because users // need rx perms to execute it bag := needsrx paths := []string{app.Command} if app.IsService() { // services' paths just need to not be skipped by the validator bag = noskipd // additional paths to check for services: // XXX maybe have a method on app to keep this in sync paths = append(paths, app.StopCommand, app.ReloadCommand, app.PostStopCommand) } for _, path := range paths { path = normPath(path) if path == "" { continue } needsf[path] = true if app.IsService() { needsx[path] = true } for ; path != "."; path = filepath.Dir(path) { bag[path] = true } } // completer is special :-/ if path := normPath(app.Completer); path != "" { needsr[path] = true for path = filepath.Dir(path); path != "."; path = filepath.Dir(path) { needsrx[path] = true } } } // note all needsr so far need to be regular files (or symlinks) for k := range needsr { needsf[k] = true } // thing can get jumbled up for path := range needsrx { delete(needsx, path) delete(needsr, path) } for path := range needsx { if needsr[path] { delete(needsx, path) delete(needsr, path) needsrx[path] = true } } seen := make(map[string]bool, len(needsx)+len(needsrx)+len(needsr)) // bad modes are logged instead of being returned because the end user // can do nothing with the info (and the developer can read the logs) hasBadModes := false err := c.Walk(".", func(path string, info os.FileInfo, err error) error { if err != nil { return err } mode := info.Mode() if needsrx[path] || needsx[path] || needsr[path] { seen[path] = true } if !needsrx[path] && !needsx[path] && !needsr[path] && !strings.HasPrefix(path, "meta/") { if mode.IsDir() { if noskipd[path] { return nil } return filepath.SkipDir } return nil } if needsrx[path] || mode.IsDir() { if mode.Perm()&0555 != 0555 { logf("in snap %q: %q should be world-readable and executable, and isn't: %s", s.InstanceName(), path, mode) hasBadModes = true } } else { if needsf[path] { // this assumes that if it's a symlink it's OK. Arguably we // should instead follow the symlink. We'd have to expose // Lstat(), and guard against loops, and ... huge can of // worms, and as this validator is meant as a developer aid // more than anything else, not worth it IMHO (as I can't // imagine this happening by accident). if mode&(os.ModeDir|os.ModeNamedPipe|os.ModeSocket|os.ModeDevice) != 0 { logf("in snap %q: %q should be a regular file (or a symlink) and isn't", s.InstanceName(), path) hasBadModes = true } } if needsx[path] || strings.HasPrefix(path, "meta/hooks/") { if mode.Perm()&0111 == 0 { logf("in snap %q: %q should be executable, and isn't: %s", s.InstanceName(), path, mode) hasBadModes = true } } else { // in needsr, or under meta but not a hook if mode.Perm()&0444 != 0444 { logf("in snap %q: %q should be world-readable, and isn't: %s", s.InstanceName(), path, mode) hasBadModes = true } } } return nil }) if err != nil { return err } if len(seen) != len(needsx)+len(needsrx)+len(needsr) { for _, needs := range []map[string]bool{needsx, needsrx, needsr} { for path := range needs { if !seen[path] { logf("in snap %q: path %q does not exist", s.InstanceName(), path) } } } return ErrMissingPaths } if hasBadModes { return ErrBadModes } return nil } // normPath is a helper for validateContainer. It takes a relative path (e.g. an // app's RestartCommand, which might be empty to mean there is no such thing), // and cleans it. // // * empty paths are returned as is // * if the path is not relative, it's initial / is dropped // * if the path goes "outside" (ie starts with ../), the empty string is // returned (i.e. "ignore") // * if there's a space in the command, ignore the rest of the string // (see also cmd/snap-exec/main.go's comment about strings.Split) func normPath(path string) string { if path == "" { return "" } path = strings.TrimPrefix(filepath.Clean(path), "/") if strings.HasPrefix(path, "../") { // not something inside the snap return "" } if idx := strings.IndexByte(path, ' '); idx > -1 { return path[:idx] } return path } snapd-2.37.4~14.04.1/snap/snapenv/0000775000000000000000000000000013435556260013154 5ustar snapd-2.37.4~14.04.1/snap/snapenv/snapenv_test.go0000664000000000000000000002001313435556260016210 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2015 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 . * */ package snapenv import ( "fmt" "os" "os/user" "strings" "testing" . "gopkg.in/check.v1" "github.com/snapcore/snapd/arch" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil/sys" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/testutil" ) func Test(t *testing.T) { TestingT(t) } type HTestSuite struct { testutil.BaseTest } var _ = Suite(&HTestSuite{}) var mockYaml = []byte(`name: snapname version: 1.0 apps: app: command: run-app hooks: configure: `) var mockSnapInfo = &snap.Info{ SuggestedName: "foo", Version: "1.0", SideInfo: snap.SideInfo{ Revision: snap.R(17), }, } var mockClassicSnapInfo = &snap.Info{ SuggestedName: "foo", Version: "1.0", SideInfo: snap.SideInfo{ Revision: snap.R(17), }, Confinement: snap.ClassicConfinement, } func (s *HTestSuite) SetUpTest(c *C) { s.BaseTest.SetUpTest(c) s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})) } func (s *HTestSuite) TearDownTest(c *C) { s.BaseTest.TearDownTest(c) } func (ts *HTestSuite) TestBasic(c *C) { env := basicEnv(mockSnapInfo) c.Assert(env, DeepEquals, map[string]string{ "SNAP": fmt.Sprintf("%s/foo/17", dirs.CoreSnapMountDir), "SNAP_ARCH": arch.UbuntuArchitecture(), "SNAP_COMMON": "/var/snap/foo/common", "SNAP_DATA": "/var/snap/foo/17", "SNAP_LIBRARY_PATH": "/var/lib/snapd/lib/gl:/var/lib/snapd/lib/gl32:/var/lib/snapd/void", "SNAP_NAME": "foo", "SNAP_INSTANCE_NAME": "foo", "SNAP_INSTANCE_KEY": "", "SNAP_REEXEC": "", "SNAP_REVISION": "17", "SNAP_VERSION": "1.0", }) } func (ts *HTestSuite) TestUser(c *C) { env := userEnv(mockSnapInfo, "/root") c.Assert(env, DeepEquals, map[string]string{ "HOME": "/root/snap/foo/17", "SNAP_USER_COMMON": "/root/snap/foo/common", "SNAP_USER_DATA": "/root/snap/foo/17", "XDG_RUNTIME_DIR": fmt.Sprintf("/run/user/%d/snap.foo", sys.Geteuid()), }) } func (ts *HTestSuite) TestUserForClassicConfinement(c *C) { env := userEnv(mockClassicSnapInfo, "/root") c.Assert(env, DeepEquals, map[string]string{ // NOTE HOME Is absent! we no longer override it "SNAP_USER_COMMON": "/root/snap/foo/common", "SNAP_USER_DATA": "/root/snap/foo/17", "XDG_RUNTIME_DIR": fmt.Sprintf("/run/user/%d/snap.foo", sys.Geteuid()), }) } func (s *HTestSuite) TestSnapRunSnapExecEnv(c *C) { info, err := snap.InfoFromSnapYaml(mockYaml) c.Assert(err, IsNil) info.SideInfo.Revision = snap.R(42) usr, err := user.Current() c.Assert(err, IsNil) homeEnv := os.Getenv("HOME") defer os.Setenv("HOME", homeEnv) for _, withHomeEnv := range []bool{true, false} { if !withHomeEnv { os.Setenv("HOME", "") } env := snapEnv(info) c.Check(env, DeepEquals, map[string]string{ "SNAP_ARCH": arch.UbuntuArchitecture(), "SNAP_LIBRARY_PATH": "/var/lib/snapd/lib/gl:/var/lib/snapd/lib/gl32:/var/lib/snapd/void", "SNAP_NAME": "snapname", "SNAP_INSTANCE_NAME": "snapname", "SNAP_INSTANCE_KEY": "", "SNAP_REEXEC": "", "SNAP_REVISION": "42", "SNAP_VERSION": "1.0", "SNAP": fmt.Sprintf("%s/snapname/42", dirs.CoreSnapMountDir), "SNAP_COMMON": "/var/snap/snapname/common", "SNAP_DATA": "/var/snap/snapname/42", "SNAP_USER_COMMON": fmt.Sprintf("%s/snap/snapname/common", usr.HomeDir), "SNAP_USER_DATA": fmt.Sprintf("%s/snap/snapname/42", usr.HomeDir), "XDG_RUNTIME_DIR": fmt.Sprintf("/run/user/%d/snap.snapname", sys.Geteuid()), "HOME": fmt.Sprintf("%s/snap/snapname/42", usr.HomeDir), }) } } func (s *HTestSuite) TestParallelInstallSnapRunSnapExecEnv(c *C) { info, err := snap.InfoFromSnapYaml(mockYaml) c.Assert(err, IsNil) info.SideInfo.Revision = snap.R(42) usr, err := user.Current() c.Assert(err, IsNil) homeEnv := os.Getenv("HOME") defer os.Setenv("HOME", homeEnv) // pretend it's snapname_foo info.InstanceKey = "foo" for _, withHomeEnv := range []bool{true, false} { if !withHomeEnv { os.Setenv("HOME", "") } env := snapEnv(info) c.Check(env, DeepEquals, map[string]string{ "SNAP_ARCH": arch.UbuntuArchitecture(), "SNAP_LIBRARY_PATH": "/var/lib/snapd/lib/gl:/var/lib/snapd/lib/gl32:/var/lib/snapd/void", "SNAP_NAME": "snapname", "SNAP_INSTANCE_NAME": "snapname_foo", "SNAP_INSTANCE_KEY": "foo", "SNAP_REEXEC": "", "SNAP_REVISION": "42", "SNAP_VERSION": "1.0", // Those are mapped to snap-specific directories by // mount namespace setup "SNAP": fmt.Sprintf("%s/snapname/42", dirs.CoreSnapMountDir), "SNAP_COMMON": "/var/snap/snapname/common", "SNAP_DATA": "/var/snap/snapname/42", // User's data directories are not mapped to // snap-specific ones "SNAP_USER_COMMON": fmt.Sprintf("%s/snap/snapname_foo/common", usr.HomeDir), "SNAP_USER_DATA": fmt.Sprintf("%s/snap/snapname_foo/42", usr.HomeDir), "XDG_RUNTIME_DIR": fmt.Sprintf("/run/user/%d/snap.snapname_foo", sys.Geteuid()), "HOME": fmt.Sprintf("%s/snap/snapname_foo/42", usr.HomeDir), }) } } func (ts *HTestSuite) TestParallelInstallUser(c *C) { info := *mockSnapInfo info.InstanceKey = "bar" env := userEnv(&info, "/root") c.Assert(env, DeepEquals, map[string]string{ "HOME": "/root/snap/foo_bar/17", "SNAP_USER_COMMON": "/root/snap/foo_bar/common", "SNAP_USER_DATA": "/root/snap/foo_bar/17", "XDG_RUNTIME_DIR": fmt.Sprintf("/run/user/%d/snap.foo_bar", sys.Geteuid()), }) } func (ts *HTestSuite) TestParallelInstallUserForClassicConfinement(c *C) { info := *mockClassicSnapInfo info.InstanceKey = "bar" env := userEnv(&info, "/root") c.Assert(env, DeepEquals, map[string]string{ // NOTE HOME Is absent! we no longer override it "SNAP_USER_COMMON": "/root/snap/foo_bar/common", "SNAP_USER_DATA": "/root/snap/foo_bar/17", "XDG_RUNTIME_DIR": fmt.Sprintf("/run/user/%d/snap.foo_bar", sys.Geteuid()), }) } func envValue(env []string, key string) (bool, string) { for _, item := range env { if strings.HasPrefix(item, key+"=") { return true, strings.SplitN(item, "=", 2)[1] } } return false, "" } func (s *HTestSuite) TestExtraEnvForExecEnv(c *C) { info, err := snap.InfoFromSnapYaml(mockYaml) c.Assert(err, IsNil) info.SideInfo.Revision = snap.R(42) env := ExecEnv(info, map[string]string{"FOO": "BAR"}) found, val := envValue(env, "FOO") c.Assert(found, Equals, true) c.Assert(val, Equals, "BAR") } func setenvWithReset(s *HTestSuite, key string, val string) { tmpdirEnv, tmpdirFound := os.LookupEnv("TMPDIR") os.Setenv("TMPDIR", "/var/tmp") if tmpdirFound { s.AddCleanup(func() { os.Setenv("TMPDIR", tmpdirEnv) }) } else { s.AddCleanup(func() { os.Unsetenv("TMPDIR") }) } } func (s *HTestSuite) TestExecEnvNoRenameTMPDIRForNonClassic(c *C) { setenvWithReset(s, "TMPDIR", "/var/tmp") env := ExecEnv(mockSnapInfo, map[string]string{}) found, val := envValue(env, "TMPDIR") c.Assert(found, Equals, true) c.Assert(val, Equals, "/var/tmp") found, _ = envValue(env, PreservedUnsafePrefix+"TMPDIR") c.Assert(found, Equals, false) } func (s *HTestSuite) TestExecEnvRenameTMPDIRForClassic(c *C) { setenvWithReset(s, "TMPDIR", "/var/tmp") env := ExecEnv(mockClassicSnapInfo, map[string]string{}) found, _ := envValue(env, "TMPDIR") c.Assert(found, Equals, false) found, val := envValue(env, PreservedUnsafePrefix+"TMPDIR") c.Assert(found, Equals, true) c.Assert(val, Equals, "/var/tmp") } snapd-2.37.4~14.04.1/snap/snapenv/snapenv.go0000664000000000000000000001572013435556260015162 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2015 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 . * */ package snapenv import ( "fmt" "os" "os/user" "path/filepath" "strings" "github.com/snapcore/snapd/arch" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil/sys" "github.com/snapcore/snapd/snap" ) type preserveUnsafeEnvFlag int8 const ( discardUnsafeFlag preserveUnsafeEnvFlag = iota preserveUnsafeFlag ) // ExecEnv returns the full environment that is required for // snap-{confine,exec}(like SNAP_{NAME,REVISION} etc are all set). // // It merges it with the existing os.Environ() and ensures the SNAP_* // overrides the any pre-existing environment variables. For a classic // snap, environment variables that are usually stripped out by ld.so // when starting a setuid process are renamed by prepending // PreservedUnsafePrefix -- which snap-exec will remove, restoring the // variables to their original names. // // With the extra parameter additional environment variables can be // supplied which will be set in the execution environment. func ExecEnv(info *snap.Info, extra map[string]string) []string { // merge environment and the snap environment, note that the // snap environment overrides pre-existing env entries preserve := discardUnsafeFlag if info.NeedsClassic() { preserve = preserveUnsafeFlag } env := envMap(os.Environ(), preserve) snapEnv := snapEnv(info) for k, v := range snapEnv { env[k] = v } for k, v := range extra { env[k] = v } return envFromMap(env) } // snapEnv returns the extra environment that is required for // snap-{confine,exec} to work. func snapEnv(info *snap.Info) map[string]string { var home string usr, err := user.Current() if err == nil { home = usr.HomeDir } env := basicEnv(info) if home != "" { for k, v := range userEnv(info, home) { env[k] = v } } return env } // basicEnv returns the app-level environment variables for a snap. // Despite this being a bit snap-specific, this is in helpers.go because it's // used by so many other modules, we run into circular dependencies if it's // somewhere more reasonable like the snappy module. func basicEnv(info *snap.Info) map[string]string { return map[string]string{ // This uses CoreSnapMountDir because the computed environment // variables are conveyed to the started application process which // shall *either* execute with the new mount namespace where snaps are // always mounted on /snap OR it is a classically confined snap where // /snap is a part of the distribution package. // // For parallel-installs the mount namespace setup is making the // environment of each snap instance appear as if it's the only // snap, i.e. SNAP paths point to the same locations within the // mount namespace "SNAP": filepath.Join(dirs.CoreSnapMountDir, info.SnapName(), info.Revision.String()), "SNAP_COMMON": snap.CommonDataDir(info.SnapName()), "SNAP_DATA": snap.DataDir(info.SnapName(), info.Revision), "SNAP_NAME": info.SnapName(), "SNAP_INSTANCE_NAME": info.InstanceName(), "SNAP_INSTANCE_KEY": info.InstanceKey, "SNAP_VERSION": info.Version, "SNAP_REVISION": info.Revision.String(), "SNAP_ARCH": arch.UbuntuArchitecture(), // see https://github.com/snapcore/snapd/pull/2732#pullrequestreview-18827193 "SNAP_LIBRARY_PATH": "/var/lib/snapd/lib/gl:/var/lib/snapd/lib/gl32:/var/lib/snapd/void", "SNAP_REEXEC": os.Getenv("SNAP_REEXEC"), } } // userEnv returns the user-level environment variables for a snap. // Despite this being a bit snap-specific, this is in helpers.go because it's // used by so many other modules, we run into circular dependencies if it's // somewhere more reasonable like the snappy module. func userEnv(info *snap.Info, home string) map[string]string { // To keep things simple the user variables always point to the // instance-specific directories. result := map[string]string{ "SNAP_USER_COMMON": info.UserCommonDataDir(home), "SNAP_USER_DATA": info.UserDataDir(home), "XDG_RUNTIME_DIR": info.UserXdgRuntimeDir(sys.Geteuid()), } // For non-classic snaps, we set HOME but on classic allow snaps to see real HOME if !info.NeedsClassic() { result["HOME"] = info.UserDataDir(home) } return result } // Environment variables glibc strips out when running a setuid binary. // Taken from https://sourceware.org/git/?p=glibc.git;a=blob_plain;f=sysdeps/generic/unsecvars.h;hb=HEAD // TODO: use go generate to obtain this list at build time. var unsafeEnv = map[string]bool{ "GCONV_PATH": true, "GETCONF_DIR": true, "GLIBC_TUNABLES": true, "HOSTALIASES": true, "LD_AUDIT": true, "LD_DEBUG": true, "LD_DEBUG_OUTPUT": true, "LD_DYNAMIC_WEAK": true, "LD_HWCAP_MASK": true, "LD_LIBRARY_PATH": true, "LD_ORIGIN_PATH": true, "LD_PRELOAD": true, "LD_PROFILE": true, "LD_SHOW_AUXV": true, "LD_USE_LOAD_BIAS": true, "LOCALDOMAIN": true, "LOCPATH": true, "MALLOC_TRACE": true, "NIS_PATH": true, "NLSPATH": true, "RESOLV_HOST_CONF": true, "RES_OPTIONS": true, "TMPDIR": true, "TZDIR": true, } const PreservedUnsafePrefix = "SNAP_SAVED_" // envMap creates a map from the given environment string list, // e.g. the list returned from os.Environ(). If preserveUnsafeVars // rename variables that will be stripped out by the dynamic linker // executing the setuid snap-confine by prepending their names with // PreservedUnsafePrefix. func envMap(env []string, preserveUnsafeEnv preserveUnsafeEnvFlag) map[string]string { envMap := map[string]string{} for _, kv := range env { // snap-exec unconditionally renames variables // starting with PreservedUnsafePrefix so skip any // that are already present in the environment to // avoid confusion. if strings.HasPrefix(kv, PreservedUnsafePrefix) { continue } l := strings.SplitN(kv, "=", 2) if len(l) < 2 { continue // strange } k, v := l[0], l[1] if preserveUnsafeEnv == preserveUnsafeFlag && unsafeEnv[k] { k = PreservedUnsafePrefix + k } envMap[k] = v } return envMap } // envFromMap creates a list of strings of the form k=v from a dict. This is // useful in combination with envMap to create an environment suitable to // pass to e.g. syscall.Exec() func envFromMap(em map[string]string) []string { var out []string for k, v := range em { out = append(out, fmt.Sprintf("%s=%s", k, v)) } return out } snapd-2.37.4~14.04.1/snap/types_test.go0000664000000000000000000001443013435556260014236 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2015 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 . * */ package snap import ( "encoding/json" "fmt" "gopkg.in/yaml.v2" . "gopkg.in/check.v1" ) type typeSuite struct{} var _ = Suite(&typeSuite{}) func (s *typeSuite) TestJSONerr(c *C) { var t Type err := json.Unmarshal([]byte("false"), &t) c.Assert(err, NotNil) } func (s *typeSuite) TestJsonMarshalTypes(c *C) { out, err := json.Marshal(TypeApp) c.Assert(err, IsNil) c.Check(string(out), Equals, "\"app\"") out, err = json.Marshal(TypeGadget) c.Assert(err, IsNil) c.Check(string(out), Equals, "\"gadget\"") out, err = json.Marshal(TypeOS) c.Assert(err, IsNil) c.Check(string(out), Equals, "\"os\"") out, err = json.Marshal(TypeKernel) c.Assert(err, IsNil) c.Check(string(out), Equals, "\"kernel\"") out, err = json.Marshal(TypeBase) c.Assert(err, IsNil) c.Check(string(out), Equals, "\"base\"") out, err = json.Marshal(TypeSnapd) c.Assert(err, IsNil) c.Check(string(out), Equals, "\"snapd\"") } func (s *typeSuite) TestJsonUnmarshalTypes(c *C) { var st Type err := json.Unmarshal([]byte("\"application\""), &st) c.Assert(err, IsNil) c.Check(st, Equals, TypeApp) err = json.Unmarshal([]byte("\"app\""), &st) c.Assert(err, IsNil) c.Check(st, Equals, TypeApp) err = json.Unmarshal([]byte("\"gadget\""), &st) c.Assert(err, IsNil) c.Check(st, Equals, TypeGadget) err = json.Unmarshal([]byte("\"os\""), &st) c.Assert(err, IsNil) c.Check(st, Equals, TypeOS) err = json.Unmarshal([]byte("\"kernel\""), &st) c.Assert(err, IsNil) c.Check(st, Equals, TypeKernel) err = json.Unmarshal([]byte("\"base\""), &st) c.Assert(err, IsNil) c.Check(st, Equals, TypeBase) err = json.Unmarshal([]byte("\"snapd\""), &st) c.Assert(err, IsNil) c.Check(st, Equals, TypeSnapd) } func (s *typeSuite) TestJsonUnmarshalInvalidTypes(c *C) { invalidTypes := []string{"foo", "-app", "gadget_"} var st Type for _, invalidType := range invalidTypes { err := json.Unmarshal([]byte(fmt.Sprintf("%q", invalidType)), &st) c.Assert(err, NotNil, Commentf("Expected '%s' to be an invalid type", invalidType)) } } func (s *typeSuite) TestYamlMarshalTypes(c *C) { out, err := yaml.Marshal(TypeApp) c.Assert(err, IsNil) c.Check(string(out), Equals, "app\n") out, err = yaml.Marshal(TypeGadget) c.Assert(err, IsNil) c.Check(string(out), Equals, "gadget\n") out, err = yaml.Marshal(TypeOS) c.Assert(err, IsNil) c.Check(string(out), Equals, "os\n") out, err = yaml.Marshal(TypeKernel) c.Assert(err, IsNil) c.Check(string(out), Equals, "kernel\n") out, err = yaml.Marshal(TypeBase) c.Assert(err, IsNil) c.Check(string(out), Equals, "base\n") } func (s *typeSuite) TestYamlUnmarshalTypes(c *C) { var st Type err := yaml.Unmarshal([]byte("application"), &st) c.Assert(err, IsNil) c.Check(st, Equals, TypeApp) err = yaml.Unmarshal([]byte("app"), &st) c.Assert(err, IsNil) c.Check(st, Equals, TypeApp) err = yaml.Unmarshal([]byte("gadget"), &st) c.Assert(err, IsNil) c.Check(st, Equals, TypeGadget) err = yaml.Unmarshal([]byte("os"), &st) c.Assert(err, IsNil) c.Check(st, Equals, TypeOS) err = yaml.Unmarshal([]byte("kernel"), &st) c.Assert(err, IsNil) c.Check(st, Equals, TypeKernel) err = yaml.Unmarshal([]byte("base"), &st) c.Assert(err, IsNil) c.Check(st, Equals, TypeBase) } func (s *typeSuite) TestYamlUnmarshalInvalidTypes(c *C) { invalidTypes := []string{"foo", "-app", "gadget_"} var st Type for _, invalidType := range invalidTypes { err := yaml.Unmarshal([]byte(invalidType), &st) c.Assert(err, NotNil, Commentf("Expected '%s' to be an invalid type", invalidType)) } } func (s *typeSuite) TestYamlMarshalConfinementTypes(c *C) { out, err := yaml.Marshal(DevModeConfinement) c.Assert(err, IsNil) c.Check(string(out), Equals, "devmode\n") out, err = yaml.Marshal(StrictConfinement) c.Assert(err, IsNil) c.Check(string(out), Equals, "strict\n") } func (s *typeSuite) TestYamlUnmarshalConfinementTypes(c *C) { var confinementType ConfinementType err := yaml.Unmarshal([]byte("devmode"), &confinementType) c.Assert(err, IsNil) c.Check(confinementType, Equals, DevModeConfinement) err = yaml.Unmarshal([]byte("strict"), &confinementType) c.Assert(err, IsNil) c.Check(confinementType, Equals, StrictConfinement) } func (s *typeSuite) TestYamlUnmarshalInvalidConfinementTypes(c *C) { var invalidConfinementTypes = []string{ "foo", "strict-", "_devmode", } var confinementType ConfinementType for _, thisConfinementType := range invalidConfinementTypes { err := yaml.Unmarshal([]byte(thisConfinementType), &confinementType) c.Assert(err, NotNil, Commentf("Expected '%s' to be an invalid confinement type", thisConfinementType)) } } func (s *typeSuite) TestJsonMarshalConfinementTypes(c *C) { out, err := json.Marshal(DevModeConfinement) c.Assert(err, IsNil) c.Check(string(out), Equals, "\"devmode\"") out, err = json.Marshal(StrictConfinement) c.Assert(err, IsNil) c.Check(string(out), Equals, "\"strict\"") } func (s *typeSuite) TestJsonUnmarshalConfinementTypes(c *C) { var confinementType ConfinementType err := json.Unmarshal([]byte("\"devmode\""), &confinementType) c.Assert(err, IsNil) c.Check(confinementType, Equals, DevModeConfinement) err = json.Unmarshal([]byte("\"strict\""), &confinementType) c.Assert(err, IsNil) c.Check(confinementType, Equals, StrictConfinement) } func (s *typeSuite) TestJsonUnmarshalInvalidConfinementTypes(c *C) { var invalidConfinementTypes = []string{ "foo", "strict-", "_devmode", } var confinementType ConfinementType for _, thisConfinementType := range invalidConfinementTypes { err := json.Unmarshal([]byte(fmt.Sprintf("%q", thisConfinementType)), &confinementType) c.Assert(err, NotNil, Commentf("Expected '%s' to be an invalid confinement type", thisConfinementType)) } } snapd-2.37.4~14.04.1/snap/types.go0000664000000000000000000000721413435556260013201 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2015 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 . * */ package snap import ( "encoding/json" "fmt" ) // Type represents the kind of snap (app, core, gadget, os, kernel) type Type string // The various types of snap parts we support const ( TypeApp Type = "app" TypeGadget Type = "gadget" TypeKernel Type = "kernel" TypeBase Type = "base" TypeSnapd Type = "snapd" // FIXME: this really should be TypeCore TypeOS Type = "os" ) // This is the sort order from least important to most important for // types. On e.g. firstboot this will be used to order the snaps this // way. var typeOrder = map[Type]int{ TypeApp: 50, TypeGadget: 40, TypeBase: 30, TypeKernel: 20, TypeOS: 10, TypeSnapd: 0, } func (m Type) SortsBefore(other Type) bool { return typeOrder[m] < typeOrder[other] } // UnmarshalJSON sets *m to a copy of data. func (m *Type) UnmarshalJSON(data []byte) error { var str string if err := json.Unmarshal(data, &str); err != nil { return err } return m.fromString(str) } // UnmarshalYAML so Type implements yaml's Unmarshaler interface func (m *Type) UnmarshalYAML(unmarshal func(interface{}) error) error { var str string if err := unmarshal(&str); err != nil { return err } return m.fromString(str) } // fromString converts str to Type and sets *m to it if validations pass func (m *Type) fromString(str string) error { t := Type(str) // this is a workaround as the store sends "application" but snappy uses // "app" for TypeApp if str == "application" { t = TypeApp } if t != TypeApp && t != TypeGadget && t != TypeOS && t != TypeKernel && t != TypeBase && t != TypeSnapd { return fmt.Errorf("invalid snap type: %q", str) } *m = t return nil } // ConfinementType represents the kind of confinement supported by the snap // (devmode only, or strict confinement) type ConfinementType string // The various confinement types we support const ( DevModeConfinement ConfinementType = "devmode" ClassicConfinement ConfinementType = "classic" StrictConfinement ConfinementType = "strict" ) // UnmarshalJSON sets *confinementType to a copy of data, assuming validation passes func (confinementType *ConfinementType) UnmarshalJSON(data []byte) error { var s string if err := json.Unmarshal(data, &s); err != nil { return err } return confinementType.fromString(s) } // UnmarshalYAML so ConfinementType implements yaml's Unmarshaler interface func (confinementType *ConfinementType) UnmarshalYAML(unmarshal func(interface{}) error) error { var s string if err := unmarshal(&s); err != nil { return err } return confinementType.fromString(s) } func (confinementType *ConfinementType) fromString(str string) error { c := ConfinementType(str) if c != DevModeConfinement && c != ClassicConfinement && c != StrictConfinement { return fmt.Errorf("invalid confinement type: %q", str) } *confinementType = c return nil } type ServiceStopReason string const ( StopReasonRefresh ServiceStopReason = "refresh" StopReasonRemove ServiceStopReason = "remove" StopReasonDisable ServiceStopReason = "disable" ) snapd-2.37.4~14.04.1/snap/seed_yaml_test.go0000664000000000000000000000371013435556260015033 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2016 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 . * */ package snap_test import ( "io/ioutil" "path/filepath" . "gopkg.in/check.v1" "github.com/snapcore/snapd/snap" ) type seedYamlTestSuite struct{} var _ = Suite(&seedYamlTestSuite{}) var mockSeedYaml = []byte(` snaps: - name: foo snap-id: snapidsnapidsnapid channel: stable devmode: true file: foo_1.0_all.snap - name: local unasserted: true file: local.snap `) func (s *seedYamlTestSuite) TestSimple(c *C) { fn := filepath.Join(c.MkDir(), "seed.yaml") err := ioutil.WriteFile(fn, mockSeedYaml, 0644) c.Assert(err, IsNil) seed, err := snap.ReadSeedYaml(fn) c.Assert(err, IsNil) c.Assert(seed.Snaps, HasLen, 2) c.Assert(seed.Snaps[0], DeepEquals, &snap.SeedSnap{ File: "foo_1.0_all.snap", Name: "foo", SnapID: "snapidsnapidsnapid", Channel: "stable", DevMode: true, }) c.Assert(seed.Snaps[1], DeepEquals, &snap.SeedSnap{ File: "local.snap", Name: "local", Unasserted: true, }) } var badMockSeedYaml = []byte(` snaps: - name: foo file: foo/bar.snap `) func (s *seedYamlTestSuite) TestNoPathAllowed(c *C) { fn := filepath.Join(c.MkDir(), "seed.yaml") err := ioutil.WriteFile(fn, badMockSeedYaml, 0644) c.Assert(err, IsNil) _, err = snap.ReadSeedYaml(fn) c.Assert(err, ErrorMatches, `"foo/bar.snap" must be a filename, not a path`) } snapd-2.37.4~14.04.1/snap/epoch.go0000664000000000000000000002264213435556260013135 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package snap import ( "encoding/json" "fmt" "strconv" "github.com/snapcore/snapd/logger" ) // An Epoch represents the ability of the snap to read and write its data. Most // developers need not worry about it, and snaps default to the 0th epoch, and // users are only offered refreshes to epoch 0 snaps. Once an epoch bump is in // order, there's a simplified expression they can use which should cover the // majority of the cases: // // epoch: N // // means a snap can read/write exactly the Nth epoch's data, and // // epoch: N* // // means a snap can additionally read (N-1)th epoch's data, which means it's a // snap that can migrate epochs (so a user on epoch 0 can get offered a refresh // to a snap on epoch 1*). // // If the above is not enough, a developer can explicitly describe what epochs a // snap can read and write: // // epoch: // read: [1, 2, 3] // write: [1, 3] // // the read attribute defaults to the value of the write attribute, and the // write attribute defaults to the last item in the read attribute. If both are // unset, it's the same as not specifying an epoch at all (i.e. epoch: 0). The // lists must not have more than 10 elements, they must be strictly increasing, // and there must be a non-empty intersection between them. // // Epoch numbers must be written in base 10, with no zero padding. type Epoch struct { Read []uint32 `yaml:"read"` Write []uint32 `yaml:"write"` } // E returns the epoch represented by the expression s. It's meant for use in // testing, as it panics at the first sign of trouble. func E(s string) Epoch { var e Epoch if err := e.fromString(s); err != nil { panic(fmt.Errorf("%q: %v", s, err)) } return e } func (e *Epoch) fromString(s string) error { if len(s) == 0 || s == "0" { e.Read = []uint32{0} e.Write = []uint32{0} return nil } star := false if s[len(s)-1] == '*' { star = true s = s[:len(s)-1] } n, err := parseInt(s) if err != nil { return err } if star { if n == 0 { return &EpochError{Message: epochZeroStar} } e.Read = []uint32{n - 1, n} } else { e.Read = []uint32{n} } e.Write = []uint32{n} return nil } func (e *Epoch) fromStructured(structured structuredEpoch) error { if structured.Read == nil { if structured.Write == nil { structured.Write = []uint32{0} } structured.Read = structured.Write } else if len(structured.Read) == 0 { // this means they explicitly set it to []. Bad they! return &EpochError{Message: emptyEpochList} } if structured.Write == nil { structured.Write = structured.Read[len(structured.Read)-1:] } else if len(structured.Write) == 0 { return &EpochError{Message: emptyEpochList} } p := &Epoch{Read: structured.Read, Write: structured.Write} if err := p.Validate(); err != nil { return err } *e = *p return nil } func (e *Epoch) UnmarshalJSON(bs []byte) error { return e.UnmarshalYAML(func(v interface{}) error { return json.Unmarshal(bs, &v) }) } func (e *Epoch) UnmarshalYAML(unmarshal func(interface{}) error) error { var shortEpoch string if err := unmarshal(&shortEpoch); err == nil { return e.fromString(shortEpoch) } var structured structuredEpoch if err := unmarshal(&structured); err != nil { return err } return e.fromStructured(structured) } // IsZero checks whether a snap's epoch is not set (or is set to the default // value of "0"). Also zero are some epochs that would be normalized to "0", // such as {"read": 0}, as well as some invalid ones like {"read": []}. func (e *Epoch) IsZero() bool { if e == nil { return true } rZero := len(e.Read) == 0 || (len(e.Read) == 1 && e.Read[0] == 0) wZero := len(e.Write) == 0 || (len(e.Write) == 1 && e.Write[0] == 0) return rZero && wZero } func epochListEq(a, b []uint32) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true } func (e *Epoch) Equal(other *Epoch) bool { if e.IsZero() { return other.IsZero() } return epochListEq(e.Read, other.Read) && epochListEq(e.Write, other.Write) } // Validate checks that the epoch makes sense. func (e *Epoch) Validate() error { if (e.Read != nil && len(e.Read) == 0) || (e.Write != nil && len(e.Write) == 0) { // these are invalid, but if both are true then IsZero will be true. // In practice this check is redundant because it's caught in deserialise. // Belts-and-suspenders all the way down. return &EpochError{Message: emptyEpochList} } if e.IsZero() { return nil } if len(e.Read) > 10 || len(e.Write) > 10 { return &EpochError{Message: epochListJustRidiculouslyLong} } if !isIncreasing(e.Read) || !isIncreasing(e.Write) { return &EpochError{Message: epochListNotIncreasing} } if intersect(e.Read, e.Write) { return nil } return &EpochError{Message: noEpochIntersection} } func (e *Epoch) simplify() interface{} { if e.IsZero() { return "0" } if len(e.Write) == 1 && len(e.Read) == 1 && e.Read[0] == e.Write[0] { return strconv.FormatUint(uint64(e.Read[0]), 10) } if len(e.Write) == 1 && len(e.Read) == 2 && e.Read[0]+1 == e.Read[1] && e.Read[1] == e.Write[0] { return strconv.FormatUint(uint64(e.Read[1]), 10) + "*" } return &structuredEpoch{Read: e.Read, Write: e.Write} } func (e Epoch) MarshalJSON() ([]byte, error) { se := &structuredEpoch{Read: e.Read, Write: e.Write} if len(se.Read) == 0 { se.Read = uint32slice{0} } if len(se.Write) == 0 { se.Write = uint32slice{0} } return json.Marshal(se) } func (Epoch) MarshalYAML() (interface{}, error) { panic("unexpected attempt to marshal an Epoch to YAML") } func (e Epoch) String() string { i := e.simplify() if s, ok := i.(string); ok { return s } buf, err := json.Marshal(i) if err != nil { // can this happen? logger.Noticef("trying to marshal %#v, simplified to %#v, got %v", e, i, err) return "-1" } return string(buf) } // CanRead checks whether this epoch can read the data written by the // other one. func (e *Epoch) CanRead(other Epoch) bool { // the intersection between e.Read and other.Write needs to be non-empty // normalize (empty epoch should be treated like "0" here) var rs, ws []uint32 if e != nil { rs = e.Read } ws = other.Write if len(rs) == 0 { rs = []uint32{0} } if len(ws) == 0 { ws = []uint32{0} } return intersect(rs, ws) } func intersect(rs, ws []uint32) bool { // O(𝑚𝑛) instead of O(𝑚log𝑛) for the binary search we could do, but // 𝑚 and 𝑛 < 10, so the simple solution is good enough (and if that // alone makes you nervous, know that it is ~2× faster in the worst // case; bisect starts being faster at ~50 entries). for _, r := range rs { for _, w := range ws { if r == w { return true } } } return false } // EpochError tracks the details of a failed epoch parse or validation. type EpochError struct { Message string } func (e EpochError) Error() string { return e.Message } const ( epochZeroStar = "0* is an invalid epoch" hugeEpochNumber = "epoch numbers must be less than 2³², but got %q" badEpochNumber = "epoch numbers must be base 10 with no zero padding, but got %q" badEpochList = "epoch read/write attributes must be lists of epoch numbers" emptyEpochList = "epoch list cannot be explicitly empty" epochListNotIncreasing = "epoch list must be a strictly increasing sequence" epochListJustRidiculouslyLong = "epoch list must not have more than 10 entries" noEpochIntersection = "epoch read and write lists must have a non-empty intersection" ) func parseInt(s string) (uint32, error) { if !(len(s) > 1 && s[0] == '0') { u, err := strconv.ParseUint(s, 10, 32) if err == nil { return uint32(u), nil } if e, ok := err.(*strconv.NumError); ok { if e.Err == strconv.ErrRange { return 0, &EpochError{ Message: fmt.Sprintf(hugeEpochNumber, s), } } } } return 0, &EpochError{ Message: fmt.Sprintf(badEpochNumber, s), } } type uint32slice []uint32 func (z *uint32slice) UnmarshalYAML(unmarshal func(interface{}) error) error { var ss []string if err := unmarshal(&ss); err != nil { return &EpochError{Message: badEpochList} } x := make([]uint32, len(ss)) for i, s := range ss { n, err := parseInt(s) if err != nil { return err } x[i] = n } *z = x return nil } func (z *uint32slice) UnmarshalJSON(bs []byte) error { var ss []json.RawMessage if err := json.Unmarshal(bs, &ss); err != nil { return &EpochError{Message: badEpochList} } x := make([]uint32, len(ss)) for i, s := range ss { n, err := parseInt(string(s)) if err != nil { return err } x[i] = n } *z = x return nil } func isIncreasing(z []uint32) bool { if len(z) < 2 { return true } for i := range z[1:] { if z[i] >= z[i+1] { return false } } return true } type structuredEpoch struct { Read uint32slice `json:"read"` Write uint32slice `json:"write"` } snapd-2.37.4~14.04.1/snap/implicit_test.go0000664000000000000000000000143113435556260014701 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package snap_test import ( . "gopkg.in/check.v1" ) type implicitSuite struct{} var _ = Suite(&implicitSuite{}) snapd-2.37.4~14.04.1/snap/revision.go0000664000000000000000000000545413435556260013677 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2016 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 . * */ package snap import ( "fmt" "strconv" ) // Keep this in sync between snap and client packages. type Revision struct { N int } func (r Revision) String() string { if r.N == 0 { return "unset" } if r.N < 0 { return fmt.Sprintf("x%d", -r.N) } return strconv.Itoa(int(r.N)) } func (r Revision) Unset() bool { return r.N == 0 } func (r Revision) Local() bool { return r.N < 0 } func (r Revision) Store() bool { return r.N > 0 } func (r Revision) MarshalJSON() ([]byte, error) { return []byte(`"` + r.String() + `"`), nil } func (r *Revision) UnmarshalYAML(unmarshal func(interface{}) error) error { var s string if err := unmarshal(&s); err != nil { return err } return r.UnmarshalJSON([]byte(`"` + s + `"`)) } func (r Revision) MarshalYAML() (interface{}, error) { return r.String(), nil } func (r *Revision) UnmarshalJSON(data []byte) error { if len(data) > 0 && data[0] == '"' && data[len(data)-1] == '"' { parsed, err := ParseRevision(string(data[1 : len(data)-1])) if err == nil { *r = parsed return nil } } else { n, err := strconv.ParseInt(string(data), 10, 64) if err == nil { r.N = int(n) return nil } } return fmt.Errorf("invalid snap revision: %q", data) } // ParseRevisions returns the representation in r as a revision. // See R for a function more suitable for hardcoded revisions. func ParseRevision(s string) (Revision, error) { if s == "unset" { return Revision{}, nil } if s != "" && s[0] == 'x' { i, err := strconv.Atoi(s[1:]) if err == nil && i > 0 { return Revision{-i}, nil } } i, err := strconv.Atoi(s) if err == nil && i > 0 { return Revision{i}, nil } return Revision{}, fmt.Errorf("invalid snap revision: %#v", s) } // R returns a Revision given an int or a string. // Providing an invalid revision type or value causes a runtime panic. // See ParseRevision for a polite function that does not panic. func R(r interface{}) Revision { switch r := r.(type) { case string: revision, err := ParseRevision(r) if err != nil { panic(err) } return revision case int: return Revision{r} default: panic(fmt.Errorf("cannot use %v (%T) as a snap revision", r, r)) } } snapd-2.37.4~14.04.1/snap/broken_test.go0000664000000000000000000001075713435556260014362 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2016 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 . * */ package snap_test import ( "io/ioutil" "os" "path/filepath" . "gopkg.in/check.v1" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snaptest" ) type brokenSuite struct{} var _ = Suite(&brokenSuite{}) func (s *brokenSuite) SetUpTest(c *C) { dirs.SetRootDir(c.MkDir()) } func (s *brokenSuite) TearDownTest(c *C) { dirs.SetRootDir("") } func touch(c *C, path string) { err := os.MkdirAll(filepath.Dir(path), 0755) c.Assert(err, IsNil) err = ioutil.WriteFile(path, nil, 0644) c.Assert(err, IsNil) } func (s *brokenSuite) TestGuessAppsForBrokenBinaries(c *C) { touch(c, filepath.Join(dirs.SnapBinariesDir, "foo")) touch(c, filepath.Join(dirs.SnapBinariesDir, "foo.bar")) touch(c, filepath.Join(dirs.SnapBinariesDir, "foo_instance")) touch(c, filepath.Join(dirs.SnapBinariesDir, "foo_instance.baz")) info := &snap.Info{SuggestedName: "foo"} apps := snap.GuessAppsForBroken(info) c.Check(apps, HasLen, 2) c.Check(apps["foo"], DeepEquals, &snap.AppInfo{Snap: info, Name: "foo"}) c.Check(apps["bar"], DeepEquals, &snap.AppInfo{Snap: info, Name: "bar"}) info = &snap.Info{SuggestedName: "foo", InstanceKey: "instance"} apps = snap.GuessAppsForBroken(info) c.Check(apps, HasLen, 2) c.Check(apps["foo"], DeepEquals, &snap.AppInfo{Snap: info, Name: "foo"}) c.Check(apps["baz"], DeepEquals, &snap.AppInfo{Snap: info, Name: "baz"}) } func (s *brokenSuite) TestGuessAppsForBrokenServices(c *C) { touch(c, filepath.Join(dirs.SnapServicesDir, "snap.foo.foo.service")) touch(c, filepath.Join(dirs.SnapServicesDir, "snap.foo.bar.service")) touch(c, filepath.Join(dirs.SnapServicesDir, "snap.foo_instance.foo.service")) touch(c, filepath.Join(dirs.SnapServicesDir, "snap.foo_instance.baz.service")) info := &snap.Info{SuggestedName: "foo"} apps := snap.GuessAppsForBroken(info) c.Check(apps, HasLen, 2) c.Check(apps["foo"], DeepEquals, &snap.AppInfo{Snap: info, Name: "foo", Daemon: "simple"}) c.Check(apps["bar"], DeepEquals, &snap.AppInfo{Snap: info, Name: "bar", Daemon: "simple"}) info = &snap.Info{SuggestedName: "foo", InstanceKey: "instance"} apps = snap.GuessAppsForBroken(info) c.Check(apps, HasLen, 2) c.Check(apps["foo"], DeepEquals, &snap.AppInfo{Snap: info, Name: "foo", Daemon: "simple"}) c.Check(apps["baz"], DeepEquals, &snap.AppInfo{Snap: info, Name: "baz", Daemon: "simple"}) } func (s *brokenSuite) TestForceRenamePlug(c *C) { snapInfo := snaptest.MockInvalidInfo(c, `name: core version: 0 plugs: old: interface: iface slots: old: interface: iface apps: app: hooks: configure: `, nil) c.Assert(snapInfo.Plugs["old"], Not(IsNil)) c.Assert(snapInfo.Plugs["old"].Name, Equals, "old") c.Assert(snapInfo.Slots["old"], Not(IsNil)) c.Assert(snapInfo.Slots["old"].Name, Equals, "old") c.Assert(snapInfo.Apps["app"].Plugs["old"], DeepEquals, snapInfo.Plugs["old"]) c.Assert(snapInfo.Apps["app"].Slots["old"], DeepEquals, snapInfo.Slots["old"]) c.Assert(snapInfo.Hooks["configure"].Plugs["old"], DeepEquals, snapInfo.Plugs["old"]) // Rename the plug now. snapInfo.ForceRenamePlug("old", "new") // Check that there's no trace of the old plug name. c.Assert(snapInfo.Plugs["old"], IsNil) c.Assert(snapInfo.Plugs["new"], Not(IsNil)) c.Assert(snapInfo.Plugs["new"].Name, Equals, "new") c.Assert(snapInfo.Apps["app"].Plugs["old"], IsNil) c.Assert(snapInfo.Apps["app"].Plugs["new"], DeepEquals, snapInfo.Plugs["new"]) c.Assert(snapInfo.Hooks["configure"].Plugs["old"], IsNil) c.Assert(snapInfo.Hooks["configure"].Plugs["new"], DeepEquals, snapInfo.Plugs["new"]) // Check that slots with the old name are unaffected. c.Assert(snapInfo.Slots["old"], Not(IsNil)) c.Assert(snapInfo.Slots["old"].Name, Equals, "old") c.Assert(snapInfo.Apps["app"].Slots["old"], DeepEquals, snapInfo.Slots["old"]) // Check that the rename made the snap valid now c.Assert(snap.Validate(snapInfo), IsNil) } snapd-2.37.4~14.04.1/snap/squashfs/0000775000000000000000000000000013435556260013337 5ustar snapd-2.37.4~14.04.1/snap/squashfs/squashfs.go0000664000000000000000000002344213435556260015530 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-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 . * */ package squashfs import ( "bufio" "fmt" "io/ioutil" "os" "os/exec" "path" "path/filepath" "regexp" "time" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/strutil" ) // Magic is the magic prefix of squashfs snap files. var Magic = []byte{'h', 's', 'q', 's'} // Snap is the squashfs based snap. type Snap struct { path string } // Path returns the path of the backing file. func (s *Snap) Path() string { return s.path } // New returns a new Squashfs snap. func New(snapPath string) *Snap { return &Snap{path: snapPath} } var osLink = os.Link var osutilCommandFromCore = osutil.CommandFromCore func (s *Snap) Install(targetPath, mountDir string) error { // ensure mount-point and blob target dir. for _, dir := range []string{mountDir, filepath.Dir(targetPath)} { if err := os.MkdirAll(dir, 0755); err != nil { return err } } // This is required so that the tests can simulate a mounted // snap when we "install" a squashfs snap in the tests. // We can not mount it for real in the tests, so we just unpack // it to the location which is good enough for the tests. if osutil.GetenvBool("SNAPPY_SQUASHFS_UNPACK_FOR_TESTS") { if err := s.Unpack("*", mountDir); err != nil { return err } } // nothing to do, happens on e.g. first-boot when we already // booted with the OS snap but its also in the seed.yaml if s.path == targetPath || osutil.FilesAreEqual(s.path, targetPath) { return nil } // try to (hard)link the file, but go on to trying to copy it // if it fails for whatever reason // // link(2) returns EPERM on filesystems that don't support // hard links (like vfat), so checking the error here doesn't // make sense vs just trying to copy it. if err := osLink(s.path, targetPath); err == nil { return nil } // if the file is a seed, but the hardlink failed, symlinking it // saves the copy (which in livecd is expensive) so try that next if filepath.Dir(s.path) == dirs.SnapSeedDir && os.Symlink(s.path, targetPath) == nil { return nil } return osutil.CopyFile(s.path, targetPath, osutil.CopyFlagPreserveAll|osutil.CopyFlagSync) } // unsquashfsStderrWriter is a helper that captures errors from // unsquashfs on stderr. Because unsquashfs will potentially // (e.g. on out-of-diskspace) report an error on every single // file we limit the reported error lines to 4. // // unsquashfs does not exit with an exit code for write errors // (e.g. no space left on device). There is an upstream PR // to fix this https://github.com/plougher/squashfs-tools/pull/46 // // However in the meantime we can detect errors by looking // on stderr for "failed" which is pretty consistently used in // the unsquashfs.c source in case of errors. type unsquashfsStderrWriter struct { strutil.MatchCounter } var unsquashfsStderrRegexp = regexp.MustCompile(`(?m).*\b[Ff]ailed\b.*`) func newUnsquashfsStderrWriter() *unsquashfsStderrWriter { return &unsquashfsStderrWriter{strutil.MatchCounter{ Regexp: unsquashfsStderrRegexp, N: 4, // note Err below uses this value }} } func (u *unsquashfsStderrWriter) Err() error { // here we use that our N is 4. errors, count := u.Matches() switch count { case 0: return nil case 1: return fmt.Errorf("failed: %q", errors[0]) case 2, 3, 4: return fmt.Errorf("failed: %s, and %q", strutil.Quoted(errors[:len(errors)-1]), errors[len(errors)-1]) default: // count > len(matches) extra := count - len(errors) return fmt.Errorf("failed: %s, and %d more", strutil.Quoted(errors), extra) } } func (s *Snap) Unpack(src, dstDir string) error { usw := newUnsquashfsStderrWriter() cmd := exec.Command("unsquashfs", "-n", "-f", "-d", dstDir, s.path, src) cmd.Stderr = usw if err := cmd.Run(); err != nil { return err } if usw.Err() != nil { return fmt.Errorf("cannot extract %q to %q: %v", src, dstDir, usw.Err()) } return nil } // Size returns the size of a squashfs snap. func (s *Snap) Size() (size int64, err error) { st, err := os.Stat(s.path) if err != nil { return 0, err } return st.Size(), nil } // ReadFile returns the content of a single file inside a squashfs snap. func (s *Snap) ReadFile(filePath string) (content []byte, err error) { tmpdir, err := ioutil.TempDir("", "read-file") if err != nil { return nil, err } defer os.RemoveAll(tmpdir) unpackDir := filepath.Join(tmpdir, "unpack") if err := exec.Command("unsquashfs", "-n", "-i", "-d", unpackDir, s.path, filePath).Run(); err != nil { return nil, err } return ioutil.ReadFile(filepath.Join(unpackDir, filePath)) } // skipper is used to track directories that should be skipped // // Given sk := make(skipper), if you sk.Add("foo/bar"), then // sk.Has("foo/bar") is true, but also sk.Has("foo/bar/baz") // // It could also be a map[string]bool, but because it's only supposed // to be checked through its Has method as above, the small added // complexity of it being a map[string]struct{} lose to the associated // space savings. type skipper map[string]struct{} func (sk skipper) Add(path string) { sk[filepath.Clean(path)] = struct{}{} } func (sk skipper) Has(path string) bool { for p := filepath.Clean(path); p != "." && p != "/"; p = filepath.Dir(p) { if _, ok := sk[p]; ok { return true } } return false } // Walk (part of snap.Container) is like filepath.Walk, without the ordering guarantee. func (s *Snap) Walk(relative string, walkFn filepath.WalkFunc) error { relative = filepath.Clean(relative) if relative == "" || relative == "/" { relative = "." } else if relative[0] == '/' { // I said relative, darn it :-) relative = relative[1:] } var cmd *exec.Cmd if relative == "." { cmd = exec.Command("unsquashfs", "-no-progress", "-dest", ".", "-ll", s.path) } else { cmd = exec.Command("unsquashfs", "-no-progress", "-dest", ".", "-ll", s.path, relative) } cmd.Env = []string{"TZ=UTC"} stdout, err := cmd.StdoutPipe() if err != nil { return walkFn(relative, nil, err) } if err := cmd.Start(); err != nil { return walkFn(relative, nil, err) } defer cmd.Process.Kill() scanner := bufio.NewScanner(stdout) // skip the header for scanner.Scan() { if len(scanner.Bytes()) == 0 { break } } skipper := make(skipper) for scanner.Scan() { st, err := fromRaw(scanner.Bytes()) if err != nil { err = walkFn(relative, nil, err) if err != nil { return err } } else { path := filepath.Join(relative, st.Path()) if skipper.Has(path) { continue } err = walkFn(path, st, nil) if err != nil { if err == filepath.SkipDir && st.IsDir() { skipper.Add(path) } else { return err } } } } if err := scanner.Err(); err != nil { return walkFn(relative, nil, err) } if err := cmd.Wait(); err != nil { return walkFn(relative, nil, err) } return nil } // ListDir returns the content of a single directory inside a squashfs snap. func (s *Snap) ListDir(dirPath string) ([]string, error) { output, err := exec.Command( "unsquashfs", "-no-progress", "-dest", "_", "-l", s.path, dirPath).CombinedOutput() if err != nil { return nil, osutil.OutputErr(output, err) } prefixPath := path.Join("_", dirPath) pattern, err := regexp.Compile("(?m)^" + regexp.QuoteMeta(prefixPath) + "/([^/\r\n]+)$") if err != nil { return nil, fmt.Errorf("internal error: cannot compile squashfs list dir regexp for %q: %s", dirPath, err) } var directoryContents []string for _, groups := range pattern.FindAllSubmatch(output, -1) { if len(groups) > 1 { directoryContents = append(directoryContents, string(groups[1])) } } return directoryContents, nil } // Build builds the snap. func (s *Snap) Build(sourceDir, snapType string, excludeFiles ...string) error { fullSnapPath, err := filepath.Abs(s.path) if err != nil { return err } cmd, err := osutilCommandFromCore(dirs.SnapMountDir, "/usr/bin/mksquashfs") if err != nil { cmd = exec.Command("mksquashfs") } cmd.Args = append(cmd.Args, ".", fullSnapPath, "-noappend", "-comp", "xz", "-no-fragments", "-no-progress", ) if len(excludeFiles) > 0 { cmd.Args = append(cmd.Args, "-wildcards") for _, excludeFile := range excludeFiles { cmd.Args = append(cmd.Args, "-ef", excludeFile) } } if snapType != "os" && snapType != "core" && snapType != "base" { cmd.Args = append(cmd.Args, "-all-root", "-no-xattrs") } return osutil.ChDir(sourceDir, func() error { output, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("mksquashfs call failed: %s", osutil.OutputErr(output, err)) } return nil }) } // BuildDate returns the "Creation or last append time" as reported by unsquashfs. func (s *Snap) BuildDate() time.Time { return BuildDate(s.path) } // BuildDate returns the "Creation or last append time" as reported by unsquashfs. func BuildDate(path string) time.Time { var t0 time.Time const prefix = "Creation or last append time " m := &strutil.MatchCounter{ Regexp: regexp.MustCompile("(?m)^" + prefix + ".*$"), N: 1, } cmd := exec.Command("unsquashfs", "-n", "-s", path) cmd.Env = []string{"TZ=UTC"} cmd.Stdout = m cmd.Stderr = m if err := cmd.Run(); err != nil { return t0 } matches, count := m.Matches() if count != 1 { return t0 } t0, _ = time.Parse(time.ANSIC, matches[0][len(prefix):]) return t0 } snapd-2.37.4~14.04.1/snap/squashfs/stat_test.go0000664000000000000000000002362413435556260015707 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017-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 . * */ package squashfs_test import ( "fmt" "math" "os" "time" . "gopkg.in/check.v1" "github.com/snapcore/snapd/snap/squashfs" ) func (s *SquashfsTestSuite) TestStatBadNodes(c *C) { badlines := map[string][]string{ "node": { // size, but device "brwxrwxr-x u/u 53595 2017-12-08 11:19 .", "crwxrwxr-x u/u 53595 2017-12-08 11:19 .", // node info is noise "brwxrwxr-x u/u noise 2017-12-08 11:19 .", "crwxrwxr-x u/u noise 2017-12-08 11:19 .", // major is noise "brwxrwxr-x u/u noise, 1 2017-12-08 11:19 .", "crwxrwxr-x u/u noise, 1 2017-12-08 11:19 .", // minor is noise "brwxrwxr-x u/u 1, noise 2017-12-08 11:19 .", "crwxrwxr-x u/u 1, noise 2017-12-08 11:19 .", }, "size": { // size is noise "drwxrwxr-x u/g noise 2017-12-08 11:19 .", "drwxrwxr-x u/g 1noise 2017-12-08 11:19 .", // size too big "drwxrwxr-x u/g 36893488147419103232 2017-12-08 11:19 .", }, "line": { // shorter than the minimum: "-rw-r--r-- too/short 20 2017-12-08 11:19 ./", // truncated: "drwxrwxr-x", "drwxrwxr-x ", "drwxrwxr-x u/u", "drwxrwxr-x u/g ", "drwxrwxr-x u/g 53595", "drwxrwxr-x u/g 53595 ", "drwxrwxr-x u/g 53595 2017-12-08 11:19", "drwxrwxr-x u/g 53595 2017-12-08 11:19 ", // mode keeps on going "drwxrwxr-xr-x u/g 53595 2017-12-08 11:19 .", // spurious padding: "drwxrwxr-x u/u 53595 2017-12-08 11:19 .", // missing size "drwxrwxr-x u/u 2017-12-08 11:19 ", // everything is size "drwxrwxr-x u/u 111111111111111111111111111111111111", }, "mode": { // zombie file type? "zrwxrwxr-x u/g 53595 2017-12-08 11:19 .", // strange permissions "dbwxrwxr-x u/g 53595 2017-12-08 11:19 .", "drbxrwxr-x u/g 53595 2017-12-08 11:19 .", "drwbrwxr-x u/g 53595 2017-12-08 11:19 .", "drwxbwxr-x u/g 53595 2017-12-08 11:19 .", "drwxrbxr-x u/g 53595 2017-12-08 11:19 .", "drwxrwbr-x u/g 53595 2017-12-08 11:19 .", "drwxrwxb-x u/g 53595 2017-12-08 11:19 .", "drwxrwxrbx u/g 53595 2017-12-08 11:19 .", "drwxrwxr-b u/g 53595 2017-12-08 11:19 .", }, "owner": { "-rw-r--r-- some.user.with.a.much.too.long.name/some.group.with.a.much.too.long.name 20 2017-12-08 11:19 ./foo", "-rw-r--r-- nogroup/ 20 2017-12-08 11:19 ./foo", "-rw-r--r-- noslash 20 2017-12-08 11:19 ./foo", "-rw-r--r-- this.line.finishes.before.finishing.owner", }, "time": { // time is bonkers: "drwxrwxr-x u/u 53595 2017-bonkers-what .", }, "path": { // path doesn't start with "." "drwxrwxr-x u/g 53595 2017-12-08 11:19 foo", }, } for kind, lines := range badlines { for _, line := range lines { com := Commentf("%q (expected bad %s)", line, kind) st, err := squashfs.FromRaw([]byte(line)) c.Assert(err, NotNil, com) c.Check(st, IsNil, com) c.Check(err, ErrorMatches, fmt.Sprintf("cannot parse %s: .*", kind)) } } } func (s *SquashfsTestSuite) TestStatUserGroup(c *C) { usergroups := [][2]string{ {"u", "g"}, {"user", "group"}, {"some.user.with.a.veery.long.name", "group"}, {"user", "some.group.with.a.very.long.name"}, {"some.user.with.a.veery.long.name", "some.group.with.a.very.long.name"}, } for _, ug := range usergroups { user, group := ug[0], ug[1] raw := []byte(fmt.Sprintf("-rw-r--r-- %s/%s 20 2017-12-08 11:19 ./foo", user, group)) com := Commentf("%q", raw) c.Assert(len(user) <= 32, Equals, true, com) c.Assert(len(group) <= 32, Equals, true, com) st, err := squashfs.FromRaw(raw) c.Assert(err, IsNil, com) c.Check(st.Mode(), Equals, os.FileMode(0644), com) c.Check(st.Path(), Equals, "/foo", com) c.Check(st.User(), Equals, user, com) c.Check(st.Group(), Equals, group, com) c.Check(st.Size(), Equals, int64(20), com) c.Check(st.ModTime(), Equals, time.Date(2017, 12, 8, 11, 19, 0, 0, time.UTC), com) } } func (s *SquashfsTestSuite) TestStatPath(c *C) { paths := [][]byte{ []byte("hello"), []byte(" this is/ a path/(somehow)"), {239, 191, 190}, {0355, 0240, 0200, 0355, 0260, 0200}, } for _, path := range paths { raw := []byte(fmt.Sprintf("-rw-r--r-- user/group 20 2017-12-08 11:19 ./%s", path)) com := Commentf("%q", raw) st, err := squashfs.FromRaw(raw) c.Assert(err, IsNil, com) c.Check(st.Mode(), Equals, os.FileMode(0644), com) c.Check(st.Path(), Equals, fmt.Sprintf("/%s", path), com) c.Check(st.User(), Equals, "user", com) c.Check(st.Group(), Equals, "group", com) c.Check(st.Size(), Equals, int64(20), com) c.Check(st.ModTime(), Equals, time.Date(2017, 12, 8, 11, 19, 0, 0, time.UTC), com) } } func (s *SquashfsTestSuite) TestStatBlock(c *C) { line := "brw-rw---- root/disk 7, 0 2017-12-05 10:29 ./dev/loop0" st, err := squashfs.FromRaw([]byte(line)) c.Assert(err, IsNil) c.Check(st.Mode(), Equals, os.FileMode(0660|os.ModeDevice)) c.Check(st.Path(), Equals, "/dev/loop0") c.Check(st.User(), Equals, "root") c.Check(st.Group(), Equals, "disk") c.Check(st.Size(), Equals, int64(0)) c.Check(st.ModTime(), Equals, time.Date(2017, 12, 5, 10, 29, 0, 0, time.UTC)) // note the major and minor numbers are ignored (for now) } func (s *SquashfsTestSuite) TestStatCharacter(c *C) { line := "crw-rw---- root/audio 14, 3 2017-12-05 10:29 ./dev/dsp" st, err := squashfs.FromRaw([]byte(line)) c.Assert(err, IsNil) c.Check(st.Mode(), Equals, os.FileMode(0660|os.ModeCharDevice)) c.Check(st.Path(), Equals, "/dev/dsp") c.Check(st.User(), Equals, "root") c.Check(st.Group(), Equals, "audio") c.Check(st.Size(), Equals, int64(0)) c.Check(st.ModTime(), Equals, time.Date(2017, 12, 5, 10, 29, 0, 0, time.UTC)) // note the major and minor numbers are ignored (for now) } func (s *SquashfsTestSuite) TestStatSymlink(c *C) { line := "lrwxrwxrwx root/root 4 2017-12-05 10:29 ./var/run -> /run" st, err := squashfs.FromRaw([]byte(line)) c.Assert(err, IsNil) c.Check(st.Mode(), Equals, os.FileMode(0777|os.ModeSymlink)) c.Check(st.Path(), Equals, "/var/run") c.Check(st.User(), Equals, "root") c.Check(st.Group(), Equals, "root") c.Check(st.Size(), Equals, int64(4)) c.Check(st.ModTime(), Equals, time.Date(2017, 12, 5, 10, 29, 0, 0, time.UTC)) } func (s *SquashfsTestSuite) TestStatNamedPipe(c *C) { line := "prw-rw-r-- john/john 0 2018-01-09 10:24 ./afifo" st, err := squashfs.FromRaw([]byte(line)) c.Assert(err, IsNil) c.Check(st.Mode(), Equals, os.FileMode(0664|os.ModeNamedPipe)) c.Check(st.Path(), Equals, "/afifo") c.Check(st.User(), Equals, "john") c.Check(st.Group(), Equals, "john") c.Check(st.Size(), Equals, int64(0)) c.Check(st.ModTime(), Equals, time.Date(2018, 1, 9, 10, 24, 0, 0, time.UTC)) } func (s *SquashfsTestSuite) TestStatSocket(c *C) { line := "srwxrwxr-x john/john 0 2018-01-09 10:24 ./asock" st, err := squashfs.FromRaw([]byte(line)) c.Assert(err, IsNil) c.Check(st.Mode(), Equals, os.FileMode(0775|os.ModeSocket)) c.Check(st.Path(), Equals, "/asock") c.Check(st.User(), Equals, "john") c.Check(st.Group(), Equals, "john") c.Check(st.Size(), Equals, int64(0)) c.Check(st.ModTime(), Equals, time.Date(2018, 1, 9, 10, 24, 0, 0, time.UTC)) } func (s *SquashfsTestSuite) TestStatLength(c *C) { ns := []int64{ 0, 1024, math.MaxInt32, math.MaxInt64, } for _, n := range ns { raw := []byte(fmt.Sprintf("-rw-r--r-- user/group %16d 2017-12-08 11:19 ./some filename", n)) com := Commentf("%q", raw) st, err := squashfs.FromRaw(raw) c.Assert(err, IsNil, com) c.Check(st.Mode(), Equals, os.FileMode(0644), com) c.Check(st.Path(), Equals, "/some filename", com) c.Check(st.User(), Equals, "user", com) c.Check(st.Group(), Equals, "group", com) c.Check(st.Size(), Equals, n, com) c.Check(st.ModTime(), Equals, time.Date(2017, 12, 8, 11, 19, 0, 0, time.UTC), com) } } func (s *SquashfsTestSuite) TestStatModeBits(c *C) { for i := os.FileMode(0); i <= 0777; i++ { raw := []byte(fmt.Sprintf("%s user/group 53595 2017-12-08 11:19 ./yadda", i)) com := Commentf("%q vs %o", raw, i) st, err := squashfs.FromRaw(raw) c.Assert(err, IsNil, com) c.Check(st.Mode(), Equals, i, com) c.Check(st.Path(), Equals, "/yadda", com) c.Check(st.User(), Equals, "user", com) c.Check(st.Group(), Equals, "group", com) c.Check(st.Size(), Equals, int64(53595), com) c.Check(st.ModTime(), Equals, time.Date(2017, 12, 8, 11, 19, 0, 0, time.UTC), com) jRaw := make([]byte, len(raw)) for j := 01000 + i; j <= 07777; j += 01000 { // this silliness only needed because os.FileMode's String() throws away sticky/setuid/setgid bits copy(jRaw, raw) if j&01000 != 0 { if j&0001 != 0 { jRaw[9] = 't' } else { jRaw[9] = 'T' } } if j&02000 != 0 { if j&0010 != 0 { jRaw[6] = 's' } else { jRaw[6] = 'S' } } if j&04000 != 0 { if j&0100 != 0 { jRaw[3] = 's' } else { jRaw[3] = 'S' } } com := Commentf("%q vs %o", jRaw, j) st, err := squashfs.FromRaw(jRaw) c.Assert(err, IsNil, com) c.Check(st.Mode(), Equals, j, com) } } } snapd-2.37.4~14.04.1/snap/squashfs/squashfs_test.go0000664000000000000000000004376313435556260016577 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2015 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 . * */ package squashfs_test import ( "errors" "io/ioutil" "math" "os" "os/exec" "path/filepath" "strings" "testing" "time" . "gopkg.in/check.v1" "gopkg.in/yaml.v2" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/snap/snapdir" "github.com/snapcore/snapd/snap/squashfs" "github.com/snapcore/snapd/testutil" ) // Hook up check.v1 into the "go test" runner func Test(t *testing.T) { TestingT(t) } type SquashfsTestSuite struct { oldStdout, oldStderr, outf *os.File } var _ = Suite(&SquashfsTestSuite{}) func makeSnap(c *C, manifest, data string) *squashfs.Snap { cur, _ := os.Getwd() return makeSnapInDir(c, cur, manifest, data) } func makeSnapContents(c *C, manifest, data string) string { tmp := c.MkDir() err := os.MkdirAll(filepath.Join(tmp, "meta", "hooks", "dir"), 0755) c.Assert(err, IsNil) // our regular snap.yaml err = ioutil.WriteFile(filepath.Join(tmp, "meta", "snap.yaml"), []byte(manifest), 0644) c.Assert(err, IsNil) // some hooks err = ioutil.WriteFile(filepath.Join(tmp, "meta", "hooks", "foo-hook"), nil, 0755) c.Assert(err, IsNil) err = ioutil.WriteFile(filepath.Join(tmp, "meta", "hooks", "bar-hook"), nil, 0755) c.Assert(err, IsNil) // And a file in another directory in there, just for testing (not a valid // hook) err = ioutil.WriteFile(filepath.Join(tmp, "meta", "hooks", "dir", "baz"), nil, 0755) c.Assert(err, IsNil) // some empty directories err = os.MkdirAll(filepath.Join(tmp, "food", "bard", "bazd"), 0755) c.Assert(err, IsNil) // some data err = ioutil.WriteFile(filepath.Join(tmp, "data.bin"), []byte(data), 0644) c.Assert(err, IsNil) return tmp } func makeSnapInDir(c *C, dir, manifest, data string) *squashfs.Snap { snapType := "app" var m struct { Type string `yaml:"type"` } if err := yaml.Unmarshal([]byte(manifest), &m); err == nil && m.Type != "" { snapType = m.Type } tmp := makeSnapContents(c, manifest, data) // build it snap := squashfs.New(filepath.Join(dir, "foo.snap")) err := snap.Build(tmp, snapType) c.Assert(err, IsNil) return snap } func (s *SquashfsTestSuite) SetUpTest(c *C) { d := c.MkDir() dirs.SetRootDir(d) err := os.Chdir(d) c.Assert(err, IsNil) s.outf, err = ioutil.TempFile(c.MkDir(), "") c.Assert(err, IsNil) s.oldStdout, s.oldStderr = os.Stdout, os.Stderr os.Stdout, os.Stderr = s.outf, s.outf } func (s *SquashfsTestSuite) TearDownTest(c *C) { os.Stdout, os.Stderr = s.oldStdout, s.oldStderr // this ensures things were quiet _, err := s.outf.Seek(0, 0) c.Assert(err, IsNil) outbuf, err := ioutil.ReadAll(s.outf) c.Assert(err, IsNil) c.Check(string(outbuf), Equals, "") } func (s *SquashfsTestSuite) TestInstallSimpleNoCp(c *C) { // mock cp but still cp cmd := testutil.MockCommand(c, "cp", `#!/bin/sh exec /bin/cp "$@" `) defer cmd.Restore() // mock link but still link linked := 0 r := squashfs.MockLink(func(a, b string) error { linked++ return os.Link(a, b) }) defer r() snap := makeSnap(c, "name: test", "") targetPath := filepath.Join(c.MkDir(), "target.snap") mountDir := c.MkDir() err := snap.Install(targetPath, mountDir) c.Assert(err, IsNil) c.Check(osutil.FileExists(targetPath), Equals, true) c.Check(linked, Equals, 1) c.Check(cmd.Calls(), HasLen, 0) } func noLink() func() { return squashfs.MockLink(func(string, string) error { return errors.New("no.") }) } func (s *SquashfsTestSuite) TestInstallNotCopyTwice(c *C) { // first, disable os.Link defer noLink()() // then, mock cp but still cp cmd := testutil.MockCommand(c, "cp", `#!/bin/sh exec /bin/cp "$@" `) defer cmd.Restore() snap := makeSnap(c, "name: test2", "") targetPath := filepath.Join(c.MkDir(), "target.snap") mountDir := c.MkDir() err := snap.Install(targetPath, mountDir) c.Assert(err, IsNil) c.Check(cmd.Calls(), HasLen, 1) err = snap.Install(targetPath, mountDir) c.Assert(err, IsNil) c.Check(cmd.Calls(), HasLen, 1) // and not 2 \o/ } func (s *SquashfsTestSuite) TestInstallSeedNoLink(c *C) { defer noLink()() c.Assert(os.MkdirAll(dirs.SnapSeedDir, 0755), IsNil) snap := makeSnapInDir(c, dirs.SnapSeedDir, "name: test2", "") targetPath := filepath.Join(c.MkDir(), "target.snap") _, err := os.Lstat(targetPath) c.Check(os.IsNotExist(err), Equals, true) err = snap.Install(targetPath, c.MkDir()) c.Assert(err, IsNil) c.Check(osutil.IsSymlink(targetPath), Equals, true) // \o/ } func (s *SquashfsTestSuite) TestPath(c *C) { p := "/path/to/foo.snap" snap := squashfs.New("/path/to/foo.snap") c.Assert(snap.Path(), Equals, p) } func (s *SquashfsTestSuite) TestReadFile(c *C) { snap := makeSnap(c, "name: foo", "") content, err := snap.ReadFile("meta/snap.yaml") c.Assert(err, IsNil) c.Assert(string(content), Equals, "name: foo") } func (s *SquashfsTestSuite) TestListDir(c *C) { snap := makeSnap(c, "name: foo", "") fileNames, err := snap.ListDir("meta/hooks") c.Assert(err, IsNil) c.Assert(len(fileNames), Equals, 3) c.Check(fileNames[0], Equals, "bar-hook") c.Check(fileNames[1], Equals, "dir") c.Check(fileNames[2], Equals, "foo-hook") } func (s *SquashfsTestSuite) TestWalk(c *C) { sub := "." snap := makeSnap(c, "name: foo", "") sqw := map[string]os.FileInfo{} snap.Walk(sub, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if path == "food" { return filepath.SkipDir } sqw[path] = info return nil }) base := c.MkDir() c.Assert(snap.Unpack("*", base), IsNil) sdw := map[string]os.FileInfo{} snapdir.New(base).Walk(sub, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if path == "food" { return filepath.SkipDir } sdw[path] = info return nil }) fpw := map[string]os.FileInfo{} filepath.Walk(filepath.Join(base, sub), func(path string, info os.FileInfo, err error) error { if err != nil { return err } path, err = filepath.Rel(base, path) if err != nil { return err } if path == "food" { return filepath.SkipDir } fpw[path] = info return nil }) for k := range fpw { squashfs.Alike(sqw[k], fpw[k], c, Commentf(k)) squashfs.Alike(sdw[k], fpw[k], c, Commentf(k)) } for k := range sqw { squashfs.Alike(fpw[k], sqw[k], c, Commentf(k)) squashfs.Alike(sdw[k], sqw[k], c, Commentf(k)) } for k := range sdw { squashfs.Alike(fpw[k], sdw[k], c, Commentf(k)) squashfs.Alike(sqw[k], sdw[k], c, Commentf(k)) } } // TestUnpackGlob tests the internal unpack func (s *SquashfsTestSuite) TestUnpackGlob(c *C) { data := "some random data" snap := makeSnap(c, "", data) outputDir := c.MkDir() err := snap.Unpack("data*", outputDir) c.Assert(err, IsNil) // this is the file we expect c.Assert(filepath.Join(outputDir, "data.bin"), testutil.FileEquals, data) // ensure glob was honored c.Assert(osutil.FileExists(filepath.Join(outputDir, "meta/snap.yaml")), Equals, false) } func (s *SquashfsTestSuite) TestUnpackDetectsFailures(c *C) { mockUnsquashfs := testutil.MockCommand(c, "unsquashfs", ` cat >&2 <&2 exit 1 `) defer mockUnsquashfs.Restore() data := "mock kernel snap" dir := makeSnapContents(c, "", data) snap := squashfs.New("foo.snap") c.Check(snap.Build(dir, "kernel"), ErrorMatches, `mksquashfs call failed: Yeah, nah.`) } func (s *SquashfsTestSuite) TestUnsquashfsStderrWriter(c *C) { for _, t := range []struct { inp []string expectedErr string }{ { inp: []string{"failed to write something\n"}, expectedErr: `failed: "failed to write something"`, }, { inp: []string{"fai", "led to write", " something\nunrelated\n"}, expectedErr: `failed: "failed to write something"`, }, { inp: []string{"failed to write\nfailed to read\n"}, expectedErr: `failed: "failed to write", and "failed to read"`, }, { inp: []string{"failed 1\nfailed 2\n3 failed\n"}, expectedErr: `failed: "failed 1", "failed 2", and "3 failed"`, }, { inp: []string{"failed 1\nfailed 2\n3 Failed\n4 Failed\n"}, expectedErr: `failed: "failed 1", "failed 2", "3 Failed", and "4 Failed"`, }, { inp: []string{"failed 1\nfailed 2\n3 Failed\n4 Failed\nfailed #5\n"}, expectedErr: `failed: "failed 1", "failed 2", "3 Failed", "4 Failed", and 1 more`, }, } { usw := squashfs.NewUnsquashfsStderrWriter() for _, l := range t.inp { usw.Write([]byte(l)) } if t.expectedErr != "" { c.Check(usw.Err(), ErrorMatches, t.expectedErr, Commentf("inp: %q failed", t.inp)) } else { c.Check(usw.Err(), IsNil) } } } func (s *SquashfsTestSuite) TestBuildDate(c *C) { // This env is used in reproducible builds and will force // squashfs to use a specific date. We need to unset it // for this specific test. if oldEnv := os.Getenv("SOURCE_DATE_EPOCH"); oldEnv != "" { os.Unsetenv("SOURCE_DATE_EPOCH") defer func() { os.Setenv("SOURCE_DATE_EPOCH", oldEnv) }() } // make a directory d := c.MkDir() // set its time waaay back now := time.Now() then := now.Add(-10000 * time.Hour) c.Assert(os.Chtimes(d, then, then), IsNil) // make a snap using this directory filename := filepath.Join(c.MkDir(), "foo.snap") snap := squashfs.New(filename) c.Assert(snap.Build(d, "app"), IsNil) // and see it's BuildDate is _now_, not _then_. c.Check(squashfs.BuildDate(filename), Equals, snap.BuildDate()) c.Check(math.Abs(now.Sub(snap.BuildDate()).Seconds()) <= 61, Equals, true, Commentf("Unexpected build date %s", snap.BuildDate())) } snapd-2.37.4~14.04.1/snap/squashfs/stat.go0000664000000000000000000001620013435556260014640 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package squashfs import ( "fmt" "os" "path/filepath" "strconv" "strings" "time" ) type SnapFileOwner struct { UID uint32 GID uint32 } type stat struct { path string size int64 mode os.FileMode mtime time.Time user string group string } func (s stat) Name() string { return filepath.Base(s.path) } func (s stat) Size() int64 { return s.size } func (s stat) Mode() os.FileMode { return s.mode } func (s stat) ModTime() time.Time { return s.mtime } func (s stat) IsDir() bool { return s.mode.IsDir() } func (s stat) Sys() interface{} { return nil } func (s stat) Path() string { return s.path } // not path of os.FileInfo const minLen = len("drwxrwxr-x u/g 53595 2017-12-08 11:19 .") func fromRaw(raw []byte) (*stat, error) { if len(raw) < minLen { return nil, errBadLine(raw) } st := &stat{} parsers := []func([]byte) (int, error){ // first, the file mode, e.g. "-rwxr-xr-x" st.parseMode, // next, user/group info st.parseOwner, // next'll come the size or the node type st.parseSize, // and then the time st.parseTimeUTC, // and finally the path st.parsePath, } p := 0 for _, parser := range parsers { n, err := parser(raw[p:]) if err != nil { return nil, err } p += n if p < len(raw) && raw[p] != ' ' { return nil, errBadLine(raw) } p++ } if st.mode&os.ModeSymlink != 0 { // the symlink *could* be from a file called "foo -> bar" to // another called "baz -> quux" in which case the following // would be wrong, but so be it. idx := strings.Index(st.path, " -> ") if idx < 0 { return nil, errBadPath(raw) } st.path = st.path[:idx] } return st, nil } type statError struct { part string raw []byte } func (e statError) Error() string { return fmt.Sprintf("cannot parse %s: %q", e.part, e.raw) } func errBadLine(raw []byte) statError { return statError{ part: "line", raw: raw, } } func errBadMode(raw []byte) statError { return statError{ part: "mode", raw: raw, } } func errBadOwner(raw []byte) statError { return statError{ part: "owner", raw: raw, } } func errBadNode(raw []byte) statError { return statError{ part: "node", raw: raw, } } func errBadSize(raw []byte) statError { return statError{ part: "size", raw: raw, } } func errBadTime(raw []byte) statError { return statError{ part: "time", raw: raw, } } func errBadPath(raw []byte) statError { return statError{ part: "path", raw: raw, } } func (st *stat) parseTimeUTC(raw []byte) (int, error) { const timelen = 16 t, err := time.Parse("2006-01-02 15:04", string(raw[:timelen])) if err != nil { return 0, errBadTime(raw) } st.mtime = t return timelen, nil } func (st *stat) parseMode(raw []byte) (int, error) { switch raw[0] { case '-': // 0 case 'd': st.mode |= os.ModeDir case 's': st.mode |= os.ModeSocket case 'c': st.mode |= os.ModeCharDevice case 'b': st.mode |= os.ModeDevice case 'p': st.mode |= os.ModeNamedPipe case 'l': st.mode |= os.ModeSymlink default: return 0, errBadMode(raw) } for i := 0; i < 3; i++ { m, err := modeFromTriplet(raw[1+3*i:4+3*i], uint(2-i)) if err != nil { return 0, err } st.mode |= m } // always this length (1+3*3==10) return 10, nil } func (st *stat) parseOwner(raw []byte) (int, error) { var p, ui, uj, gi, gj int // first check it's sane (at least two non-space chars) if raw[0] == ' ' || raw[1] == ' ' { return 0, errBadLine(raw) } ui = 0 // from useradd(8): Usernames may only be up to 32 characters long. // from groupadd(8): Groupnames may only be up to 32 characters long. // +1 for the separator, +1 for the ending space maxL := 66 if len(raw) < maxL { maxL = len(raw) } out: for p = ui; p < maxL; p++ { switch raw[p] { case '/': uj = p gi = p + 1 case ' ': gj = p break out } } if uj == 0 || gj == 0 || gi == gj { return 0, errBadOwner(raw) } st.user, st.group = string(raw[ui:uj]), string(raw[gi:gj]) return p, nil } func modeFromTriplet(trip []byte, shift uint) (os.FileMode, error) { var mode os.FileMode high := false if len(trip) != 3 { panic("bad triplet length") } switch trip[0] { case '-': // 0 case 'r': mode |= 4 default: return 0, errBadMode(trip) } switch trip[1] { case '-': // 0 case 'w': mode |= 2 default: return 0, errBadMode(trip) } switch trip[2] { case '-': // 0 case 'x': mode |= 1 case 'S', 'T': high = true case 's', 't': mode |= 1 high = true default: return 0, errBadMode(trip) } mode <<= 3 * shift if high { mode |= (01000 << shift) } return mode, nil } func (st *stat) parseSize(raw []byte) (int, error) { // the "size" column, for regular files, is the file size in bytes: // -rwxr-xr-x user/group 53595 2017-12-08 11:19 ./yadda // ^^^^^ like this // for devices, though, it's the major, minor of the node: // crw-rw---- root/audio 14, 3 2017-12-05 10:29 ./dev/dsp // ^^^^^^ like so // (for other things it is a size, although what the size is // _of_ is left as an exercise for the reader) isNode := st.mode&(os.ModeDevice|os.ModeCharDevice) != 0 p := 0 maxP := len(raw) - len("2006-01-02 15:04 .") for raw[p] == ' ' { if p >= maxP { return 0, errBadLine(raw) } p++ } ni := p for raw[p] >= '0' && raw[p] <= '9' { if p >= maxP { return 0, errBadLine(raw) } p++ } if p == ni { if isNode { return 0, errBadNode(raw) } return 0, errBadSize(raw) } if isNode { if raw[p] != ',' { return 0, errBadNode(raw) } p++ // drop the space before the minor mode for raw[p] == ' ' { p++ } // drop the minor mode for raw[p] >= '0' && raw[p] <= '9' { p++ } if raw[p] != ' ' { return 0, errBadNode(raw) } } else { if raw[p] != ' ' { return 0, errBadSize(raw) } // note that, much as it makes very little sense, the arch- // dependent st_size is never an unsigned 64 bit quantity. // It's one of unsigned long, long long, or just off_t. // // Also note os.FileInfo's Size needs to return an int64, and // squashfs's inode->data (where it stores sizes for regular // files) is a long long. sz, err := strconv.ParseInt(string(raw[ni:p]), 10, 64) if err != nil { return 0, errBadSize(raw) } st.size = sz } return p, nil } func (st *stat) parsePath(raw []byte) (int, error) { if raw[0] != '.' { return 0, errBadPath(raw) } if len(raw[1:]) == 0 { st.path = "/" } else { st.path = string(raw[1:]) } return len(raw), nil } snapd-2.37.4~14.04.1/snap/squashfs/export_test.go0000664000000000000000000000421413435556260016247 0ustar // -*- 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 . * */ package squashfs import ( "os" "os/exec" "time" "gopkg.in/check.v1" ) var ( FromRaw = fromRaw NewUnsquashfsStderrWriter = newUnsquashfsStderrWriter ) func (s stat) User() string { return s.user } func (s stat) Group() string { return s.group } func MockLink(newLink func(string, string) error) (restore func()) { oldLink := osLink osLink = newLink return func() { osLink = oldLink } } func MockFromCore(newFromCore func(string, string, ...string) (*exec.Cmd, error)) (restore func()) { oldFromCore := osutilCommandFromCore osutilCommandFromCore = newFromCore return func() { osutilCommandFromCore = oldFromCore } } // Alike compares to os.FileInfo to determine if they are sufficiently // alike to say they refer to the same thing. func Alike(a, b os.FileInfo, c *check.C, comment check.CommentInterface) { c.Check(a, check.NotNil, comment) c.Check(b, check.NotNil, comment) if a == nil || b == nil { return } // the .Name() of the root will be different on non-squashfs things _, asq := a.(*stat) _, bsq := b.(*stat) if !((asq && a.Name() == "/") || (bsq && b.Name() == "/")) { c.Check(a.Name(), check.Equals, b.Name(), comment) } c.Check(a.Mode(), check.Equals, b.Mode(), comment) if a.Mode().IsRegular() { c.Check(a.Size(), check.Equals, b.Size(), comment) } am := a.ModTime().UTC().Truncate(time.Minute) bm := b.ModTime().UTC().Truncate(time.Minute) c.Check(am.Equal(bm), check.Equals, true, check.Commentf("%s != %s (%s)", am, bm, comment)) } snapd-2.37.4~14.04.1/snap/info_snap_yaml_test.go0000664000000000000000000013721113435556260016073 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2016 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 . * */ package snap_test import ( "regexp" "testing" "time" . "gopkg.in/check.v1" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/strutil" "github.com/snapcore/snapd/timeout" "github.com/snapcore/snapd/testutil" ) // Hook up check.v1 into the "go test" runner func Test(t *testing.T) { TestingT(t) } type InfoSnapYamlTestSuite struct { testutil.BaseTest } var _ = Suite(&InfoSnapYamlTestSuite{}) var mockYaml = []byte(`name: foo version: 1.0 type: app `) func (s *InfoSnapYamlTestSuite) SetUpTest(c *C) { s.BaseTest.SetUpTest(c) s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})) } func (s *InfoSnapYamlTestSuite) TearDownTest(c *C) { s.BaseTest.TearDownTest(c) } func (s *InfoSnapYamlTestSuite) TestSimple(c *C) { info, err := snap.InfoFromSnapYaml(mockYaml) c.Assert(err, IsNil) c.Assert(info.InstanceName(), Equals, "foo") c.Assert(info.Version, Equals, "1.0") c.Assert(info.Type, Equals, snap.TypeApp) c.Assert(info.Epoch, DeepEquals, snap.E("0")) } func (s *InfoSnapYamlTestSuite) TestSnapdTypeAddedByMagic(c *C) { info, err := snap.InfoFromSnapYaml([]byte(`name: snapd version: 1.0`)) c.Assert(err, IsNil) c.Assert(info.InstanceName(), Equals, "snapd") c.Assert(info.Version, Equals, "1.0") c.Assert(info.Type, Equals, snap.TypeSnapd) } func (s *InfoSnapYamlTestSuite) TestFail(c *C) { _, err := snap.InfoFromSnapYaml([]byte("random-crap")) c.Assert(err, ErrorMatches, "(?m)cannot parse snap.yaml:.*") } type YamlSuite struct { restore func() testutil.BaseTest } var _ = Suite(&YamlSuite{}) func (s *YamlSuite) SetUpTest(c *C) { s.BaseTest.SetUpTest(c) s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})) hookType := snap.NewHookType(regexp.MustCompile(".*")) s.restore = snap.MockSupportedHookTypes([]*snap.HookType{hookType}) } func (s *YamlSuite) TearDownTest(c *C) { s.BaseTest.TearDownTest(c) s.restore() } func (s *YamlSuite) TestUnmarshalGarbage(c *C) { _, err := snap.InfoFromSnapYaml([]byte(`"`)) c.Assert(err, ErrorMatches, ".*: yaml: found unexpected end of stream") } func (s *YamlSuite) TestUnmarshalEmpty(c *C) { info, err := snap.InfoFromSnapYaml([]byte(``)) c.Assert(err, IsNil) c.Assert(info.Plugs, HasLen, 0) c.Assert(info.Slots, HasLen, 0) c.Assert(info.Apps, HasLen, 0) } // Tests focusing on plugs func (s *YamlSuite) TestUnmarshalStandaloneImplicitPlug(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap plugs: network-client: `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 1) c.Check(info.Slots, HasLen, 0) c.Assert(info.Plugs["network-client"], DeepEquals, &snap.PlugInfo{ Snap: info, Name: "network-client", Interface: "network-client", }) } func (s *YamlSuite) TestUnmarshalStandaloneAbbreviatedPlug(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap plugs: net: network-client `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 1) c.Check(info.Slots, HasLen, 0) c.Assert(info.Plugs["net"], DeepEquals, &snap.PlugInfo{ Snap: info, Name: "net", Interface: "network-client", }) } func (s *YamlSuite) TestUnmarshalStandaloneMinimalisticPlug(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap plugs: net: interface: network-client `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 1) c.Check(info.Slots, HasLen, 0) c.Assert(info.Plugs["net"], DeepEquals, &snap.PlugInfo{ Snap: info, Name: "net", Interface: "network-client", }) } func (s *YamlSuite) TestUnmarshalStandaloneCompletePlug(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap plugs: net: interface: network-client ipv6-aware: true `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 1) c.Check(info.Slots, HasLen, 0) c.Assert(info.Plugs["net"], DeepEquals, &snap.PlugInfo{ Snap: info, Name: "net", Interface: "network-client", Attrs: map[string]interface{}{"ipv6-aware": true}, }) } func (s *YamlSuite) TestUnmarshalStandalonePlugWithIntAndListAndMap(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap plugs: iface: interface: complex i: 3 l: [1,2,3] m: a: A b: B `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 1) c.Check(info.Slots, HasLen, 0) c.Assert(info.Plugs["iface"], DeepEquals, &snap.PlugInfo{ Snap: info, Name: "iface", Interface: "complex", Attrs: map[string]interface{}{ "i": int64(3), "l": []interface{}{int64(1), int64(2), int64(3)}, "m": map[string]interface{}{"a": "A", "b": "B"}, }, }) } func (s *YamlSuite) TestUnmarshalLastPlugDefinitionWins(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap plugs: net: interface: network-client attr: 1 net: interface: network-client attr: 2 `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 1) c.Check(info.Slots, HasLen, 0) c.Assert(info.Plugs["net"], DeepEquals, &snap.PlugInfo{ Snap: info, Name: "net", Interface: "network-client", Attrs: map[string]interface{}{"attr": int64(2)}, }) } func (s *YamlSuite) TestUnmarshalPlugsExplicitlyDefinedImplicitlyBoundToApps(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap plugs: network-client: apps: app: `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 1) c.Check(info.Slots, HasLen, 0) c.Check(info.Apps, HasLen, 1) plug := info.Plugs["network-client"] app := info.Apps["app"] c.Assert(plug, DeepEquals, &snap.PlugInfo{ Snap: info, Name: "network-client", Interface: "network-client", Apps: map[string]*snap.AppInfo{app.Name: app}, }) c.Assert(app, DeepEquals, &snap.AppInfo{ Snap: info, Name: "app", Plugs: map[string]*snap.PlugInfo{plug.Name: plug}, }) } func (s *YamlSuite) TestUnmarshalGlobalPlugBoundToOneApp(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap plugs: network-client: apps: with-plug: plugs: [network-client] without-plug: `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 1) c.Check(info.Slots, HasLen, 0) c.Check(info.Apps, HasLen, 2) plug := info.Plugs["network-client"] withPlugApp := info.Apps["with-plug"] withoutPlugApp := info.Apps["without-plug"] c.Assert(plug, DeepEquals, &snap.PlugInfo{ Snap: info, Name: "network-client", Interface: "network-client", Apps: map[string]*snap.AppInfo{withPlugApp.Name: withPlugApp}, }) c.Assert(withPlugApp, DeepEquals, &snap.AppInfo{ Snap: info, Name: "with-plug", Plugs: map[string]*snap.PlugInfo{plug.Name: plug}, }) c.Assert(withoutPlugApp, DeepEquals, &snap.AppInfo{ Snap: info, Name: "without-plug", Plugs: map[string]*snap.PlugInfo{}, }) } func (s *YamlSuite) TestUnmarshalPlugsExplicitlyDefinedExplicitlyBoundToApps(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap plugs: net: network-client apps: app: plugs: ["net"] `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 1) c.Check(info.Slots, HasLen, 0) c.Check(info.Apps, HasLen, 1) plug := info.Plugs["net"] app := info.Apps["app"] c.Assert(plug, DeepEquals, &snap.PlugInfo{ Snap: info, Name: "net", Interface: "network-client", Apps: map[string]*snap.AppInfo{app.Name: app}, }) c.Assert(app, DeepEquals, &snap.AppInfo{ Snap: info, Name: "app", Plugs: map[string]*snap.PlugInfo{plug.Name: plug}, }) } func (s *YamlSuite) TestUnmarshalPlugsImplicitlyDefinedExplicitlyBoundToApps(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap apps: app: plugs: ["network-client"] `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 1) c.Check(info.Slots, HasLen, 0) c.Check(info.Apps, HasLen, 1) plug := info.Plugs["network-client"] app := info.Apps["app"] c.Assert(plug, DeepEquals, &snap.PlugInfo{ Snap: info, Name: "network-client", Interface: "network-client", Apps: map[string]*snap.AppInfo{app.Name: app}, }) c.Assert(app, DeepEquals, &snap.AppInfo{ Snap: info, Name: "app", Plugs: map[string]*snap.PlugInfo{plug.Name: plug}, }) } func (s *YamlSuite) TestUnmarshalPlugWithoutInterfaceName(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap plugs: network-client: ipv6-aware: true `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 1) c.Check(info.Slots, HasLen, 0) c.Check(info.Apps, HasLen, 0) c.Assert(info.Plugs["network-client"], DeepEquals, &snap.PlugInfo{ Snap: info, Name: "network-client", Interface: "network-client", Attrs: map[string]interface{}{"ipv6-aware": true}, }) } func (s *YamlSuite) TestUnmarshalPlugWithLabel(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap plugs: bool-file: label: Disk I/O indicator `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 1) c.Check(info.Slots, HasLen, 0) c.Check(info.Apps, HasLen, 0) c.Assert(info.Plugs["bool-file"], DeepEquals, &snap.PlugInfo{ Snap: info, Name: "bool-file", Interface: "bool-file", Label: "Disk I/O indicator", }) } func (s *YamlSuite) TestUnmarshalCorruptedPlugWithNonStringInterfaceName(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. _, err := snap.InfoFromSnapYaml([]byte(` name: snap plugs: net: interface: 1.0 ipv6-aware: true `)) c.Assert(err, ErrorMatches, `interface name on plug "net" is not a string \(found float64\)`) } func (s *YamlSuite) TestUnmarshalCorruptedPlugWithNonStringLabel(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. _, err := snap.InfoFromSnapYaml([]byte(` name: snap plugs: bool-file: label: 1.0 `)) c.Assert(err, ErrorMatches, `label of plug "bool-file" is not a string \(found float64\)`) } func (s *YamlSuite) TestUnmarshalCorruptedPlugWithNonStringAttributes(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. _, err := snap.InfoFromSnapYaml([]byte(` name: snap plugs: net: 1: ok `)) c.Assert(err, ErrorMatches, `plug "net" has attribute that is not a string \(found int\)`) } func (s *YamlSuite) TestUnmarshalCorruptedPlugWithUnexpectedType(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. _, err := snap.InfoFromSnapYaml([]byte(` name: snap plugs: net: 5 `)) c.Assert(err, ErrorMatches, `plug "net" has malformed definition \(found int\)`) } func (s *YamlSuite) TestUnmarshalReservedPlugAttribute(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. _, err := snap.InfoFromSnapYaml([]byte(` name: snap plugs: serial: interface: serial-port $baud-rate: [9600] `)) c.Assert(err, ErrorMatches, `plug "serial" uses reserved attribute "\$baud-rate"`) } func (s *YamlSuite) TestUnmarshalInvalidPlugAttribute(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. _, err := snap.InfoFromSnapYaml([]byte(` name: snap plugs: serial: interface: serial-port foo: null `)) c.Assert(err, ErrorMatches, `attribute "foo" of plug \"serial\": invalid scalar:.*`) } func (s *YamlSuite) TestUnmarshalInvalidAttributeMapKey(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. _, err := snap.InfoFromSnapYaml([]byte(` name: snap plugs: serial: interface: serial-port bar: baz: - 1: A `)) c.Assert(err, ErrorMatches, `attribute "bar" of plug \"serial\": non-string key: 1`) } // Tests focusing on slots func (s *YamlSuite) TestUnmarshalStandaloneImplicitSlot(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap slots: network-client: `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 0) c.Check(info.Slots, HasLen, 1) c.Assert(info.Slots["network-client"], DeepEquals, &snap.SlotInfo{ Snap: info, Name: "network-client", Interface: "network-client", }) } func (s *YamlSuite) TestUnmarshalStandaloneAbbreviatedSlot(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap slots: net: network-client `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 0) c.Check(info.Slots, HasLen, 1) c.Assert(info.Slots["net"], DeepEquals, &snap.SlotInfo{ Snap: info, Name: "net", Interface: "network-client", }) } func (s *YamlSuite) TestUnmarshalStandaloneMinimalisticSlot(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap slots: net: interface: network-client `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 0) c.Check(info.Slots, HasLen, 1) c.Assert(info.Slots["net"], DeepEquals, &snap.SlotInfo{ Snap: info, Name: "net", Interface: "network-client", }) } func (s *YamlSuite) TestUnmarshalStandaloneCompleteSlot(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap slots: net: interface: network-client ipv6-aware: true `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 0) c.Check(info.Slots, HasLen, 1) c.Assert(info.Slots["net"], DeepEquals, &snap.SlotInfo{ Snap: info, Name: "net", Interface: "network-client", Attrs: map[string]interface{}{"ipv6-aware": true}, }) } func (s *YamlSuite) TestUnmarshalStandaloneSlotWithIntAndListAndMap(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap slots: iface: interface: complex i: 3 l: [1,2] m: a: "A" `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 0) c.Check(info.Slots, HasLen, 1) c.Assert(info.Slots["iface"], DeepEquals, &snap.SlotInfo{ Snap: info, Name: "iface", Interface: "complex", Attrs: map[string]interface{}{ "i": int64(3), "l": []interface{}{int64(1), int64(2)}, "m": map[string]interface{}{"a": "A"}, }, }) } func (s *YamlSuite) TestUnmarshalLastSlotDefinitionWins(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap slots: net: interface: network-client attr: 1 net: interface: network-client attr: 2 `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 0) c.Check(info.Slots, HasLen, 1) c.Assert(info.Slots["net"], DeepEquals, &snap.SlotInfo{ Snap: info, Name: "net", Interface: "network-client", Attrs: map[string]interface{}{"attr": int64(2)}, }) } func (s *YamlSuite) TestUnmarshalSlotsExplicitlyDefinedImplicitlyBoundToApps(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap slots: network-client: apps: app: `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 0) c.Check(info.Slots, HasLen, 1) c.Check(info.Apps, HasLen, 1) slot := info.Slots["network-client"] app := info.Apps["app"] c.Assert(slot, DeepEquals, &snap.SlotInfo{ Snap: info, Name: "network-client", Interface: "network-client", Apps: map[string]*snap.AppInfo{app.Name: app}, }) c.Assert(app, DeepEquals, &snap.AppInfo{ Snap: info, Name: "app", Slots: map[string]*snap.SlotInfo{slot.Name: slot}, }) } func (s *YamlSuite) TestUnmarshalSlotsExplicitlyDefinedExplicitlyBoundToApps(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap slots: net: network-client apps: app: slots: ["net"] `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 0) c.Check(info.Slots, HasLen, 1) c.Check(info.Apps, HasLen, 1) slot := info.Slots["net"] app := info.Apps["app"] c.Assert(slot, DeepEquals, &snap.SlotInfo{ Snap: info, Name: "net", Interface: "network-client", Apps: map[string]*snap.AppInfo{app.Name: app}, }) c.Assert(app, DeepEquals, &snap.AppInfo{ Snap: info, Name: "app", Slots: map[string]*snap.SlotInfo{slot.Name: slot}, }) } func (s *YamlSuite) TestUnmarshalSlotsImplicitlyDefinedExplicitlyBoundToApps(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap apps: app: slots: ["network-client"] `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 0) c.Check(info.Slots, HasLen, 1) c.Check(info.Apps, HasLen, 1) slot := info.Slots["network-client"] app := info.Apps["app"] c.Assert(slot, DeepEquals, &snap.SlotInfo{ Snap: info, Name: "network-client", Interface: "network-client", Apps: map[string]*snap.AppInfo{app.Name: app}, }) c.Assert(app, DeepEquals, &snap.AppInfo{ Snap: info, Name: "app", Slots: map[string]*snap.SlotInfo{slot.Name: slot}, }) } func (s *YamlSuite) TestUnmarshalSlotWithoutInterfaceName(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap slots: network-client: ipv6-aware: true `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 0) c.Check(info.Slots, HasLen, 1) c.Check(info.Apps, HasLen, 0) c.Assert(info.Slots["network-client"], DeepEquals, &snap.SlotInfo{ Snap: info, Name: "network-client", Interface: "network-client", Attrs: map[string]interface{}{"ipv6-aware": true}, }) } func (s *YamlSuite) TestUnmarshalSlotWithLabel(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap slots: led0: interface: bool-file label: Front panel LED (red) `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 0) c.Check(info.Slots, HasLen, 1) c.Check(info.Apps, HasLen, 0) c.Assert(info.Slots["led0"], DeepEquals, &snap.SlotInfo{ Snap: info, Name: "led0", Interface: "bool-file", Label: "Front panel LED (red)", }) } func (s *YamlSuite) TestUnmarshalGlobalSlotsBindToHooks(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap slots: test-slot: hooks: test-hook: `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 0) c.Check(info.Slots, HasLen, 1) c.Check(info.Apps, HasLen, 0) c.Check(info.Hooks, HasLen, 1) slot, ok := info.Slots["test-slot"] c.Assert(ok, Equals, true, Commentf("Expected slots to include 'test-slot'")) hook, ok := info.Hooks["test-hook"] c.Assert(ok, Equals, true, Commentf("Expected hooks to include 'test-hook'")) c.Check(slot, DeepEquals, &snap.SlotInfo{ Snap: info, Name: "test-slot", Interface: "test-slot", Hooks: map[string]*snap.HookInfo{hook.Name: hook}, }) c.Check(hook, DeepEquals, &snap.HookInfo{ Snap: info, Name: "test-hook", Slots: map[string]*snap.SlotInfo{slot.Name: slot}, Explicit: true, }) } func (s *YamlSuite) TestUnmarshalHookWithSlot(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap hooks: test-hook: slots: [test-slot] `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 0) c.Check(info.Slots, HasLen, 1) c.Check(info.Apps, HasLen, 0) c.Check(info.Hooks, HasLen, 1) slot, ok := info.Slots["test-slot"] c.Assert(ok, Equals, true, Commentf("Expected slots to include 'test-slot'")) hook, ok := info.Hooks["test-hook"] c.Assert(ok, Equals, true, Commentf("Expected hooks to include 'test-hook'")) c.Check(slot, DeepEquals, &snap.SlotInfo{ Snap: info, Name: "test-slot", Interface: "test-slot", Hooks: map[string]*snap.HookInfo{hook.Name: hook}, }) c.Check(hook, DeepEquals, &snap.HookInfo{ Snap: info, Name: "test-hook", Slots: map[string]*snap.SlotInfo{slot.Name: slot}, Explicit: true, }) } func (s *YamlSuite) TestUnmarshalCorruptedSlotWithNonStringInterfaceName(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. _, err := snap.InfoFromSnapYaml([]byte(` name: snap slots: net: interface: 1.0 ipv6-aware: true `)) c.Assert(err, ErrorMatches, `interface name on slot "net" is not a string \(found float64\)`) } func (s *YamlSuite) TestUnmarshalCorruptedSlotWithNonStringLabel(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. _, err := snap.InfoFromSnapYaml([]byte(` name: snap slots: bool-file: label: 1.0 `)) c.Assert(err, ErrorMatches, `label of slot "bool-file" is not a string \(found float64\)`) } func (s *YamlSuite) TestUnmarshalCorruptedSlotWithNonStringAttributes(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. _, err := snap.InfoFromSnapYaml([]byte(` name: snap slots: net: 1: ok `)) c.Assert(err, ErrorMatches, `slot "net" has attribute that is not a string \(found int\)`) } func (s *YamlSuite) TestUnmarshalCorruptedSlotWithUnexpectedType(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. _, err := snap.InfoFromSnapYaml([]byte(` name: snap slots: net: 5 `)) c.Assert(err, ErrorMatches, `slot "net" has malformed definition \(found int\)`) } func (s *YamlSuite) TestUnmarshalReservedSlotAttribute(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. _, err := snap.InfoFromSnapYaml([]byte(` name: snap slots: serial: interface: serial-port $baud-rate: [9600] `)) c.Assert(err, ErrorMatches, `slot "serial" uses reserved attribute "\$baud-rate"`) } func (s *YamlSuite) TestUnmarshalInvalidSlotAttribute(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. _, err := snap.InfoFromSnapYaml([]byte(` name: snap slots: serial: interface: serial-port foo: null `)) c.Assert(err, ErrorMatches, `attribute "foo" of slot \"serial\": invalid scalar:.*`) } func (s *YamlSuite) TestUnmarshalHook(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap hooks: test-hook: `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 0) c.Check(info.Slots, HasLen, 0) c.Check(info.Apps, HasLen, 0) c.Check(info.Hooks, HasLen, 1) hook, ok := info.Hooks["test-hook"] c.Assert(ok, Equals, true, Commentf("Expected hooks to include 'test-hook'")) c.Check(hook, DeepEquals, &snap.HookInfo{ Snap: info, Name: "test-hook", Plugs: nil, Explicit: true, }) } func (s *YamlSuite) TestUnmarshalUnsupportedHook(c *C) { s.restore() hookType := snap.NewHookType(regexp.MustCompile("not-test-hook")) s.restore = snap.MockSupportedHookTypes([]*snap.HookType{hookType}) // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap hooks: test-hook: `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 0) c.Check(info.Slots, HasLen, 0) c.Check(info.Apps, HasLen, 0) c.Check(info.Hooks, HasLen, 0, Commentf("Expected no hooks to be loaded")) } func (s *YamlSuite) TestUnmarshalHookFiltersOutUnsupportedHooks(c *C) { s.restore() hookType := snap.NewHookType(regexp.MustCompile("test-.*")) s.restore = snap.MockSupportedHookTypes([]*snap.HookType{hookType}) // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap hooks: test-hook: foo-hook: `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 0) c.Check(info.Slots, HasLen, 0) c.Check(info.Apps, HasLen, 0) c.Check(info.Hooks, HasLen, 1) hook, ok := info.Hooks["test-hook"] c.Assert(ok, Equals, true, Commentf("Expected hooks to include 'test-hook'")) c.Check(hook, DeepEquals, &snap.HookInfo{ Snap: info, Name: "test-hook", Plugs: nil, Explicit: true, }) } func (s *YamlSuite) TestUnmarshalHookWithPlug(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap hooks: test-hook: plugs: [test-plug] `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 1) c.Check(info.Slots, HasLen, 0) c.Check(info.Apps, HasLen, 0) c.Check(info.Hooks, HasLen, 1) plug, ok := info.Plugs["test-plug"] c.Assert(ok, Equals, true, Commentf("Expected plugs to include 'test-plug'")) hook, ok := info.Hooks["test-hook"] c.Assert(ok, Equals, true, Commentf("Expected hooks to include 'test-hook'")) c.Check(plug, DeepEquals, &snap.PlugInfo{ Snap: info, Name: "test-plug", Interface: "test-plug", Hooks: map[string]*snap.HookInfo{hook.Name: hook}, }) c.Check(hook, DeepEquals, &snap.HookInfo{ Snap: info, Name: "test-hook", Plugs: map[string]*snap.PlugInfo{plug.Name: plug}, Explicit: true, }) } func (s *YamlSuite) TestUnmarshalGlobalPlugsBindToHooks(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap plugs: test-plug: hooks: test-hook: `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 1) c.Check(info.Slots, HasLen, 0) c.Check(info.Apps, HasLen, 0) c.Check(info.Hooks, HasLen, 1) plug, ok := info.Plugs["test-plug"] c.Assert(ok, Equals, true, Commentf("Expected plugs to include 'test-plug'")) hook, ok := info.Hooks["test-hook"] c.Assert(ok, Equals, true, Commentf("Expected hooks to include 'test-hook'")) c.Check(plug, DeepEquals, &snap.PlugInfo{ Snap: info, Name: "test-plug", Interface: "test-plug", Hooks: map[string]*snap.HookInfo{hook.Name: hook}, }) c.Check(hook, DeepEquals, &snap.HookInfo{ Snap: info, Name: "test-hook", Plugs: map[string]*snap.PlugInfo{plug.Name: plug}, Explicit: true, }) } func (s *YamlSuite) TestUnmarshalGlobalPlugBoundToOneHook(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap plugs: test-plug: hooks: with-plug: plugs: [test-plug] without-plug: `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 1) c.Check(info.Slots, HasLen, 0) c.Check(info.Apps, HasLen, 0) c.Check(info.Hooks, HasLen, 2) plug := info.Plugs["test-plug"] withPlugHook := info.Hooks["with-plug"] withoutPlugHook := info.Hooks["without-plug"] c.Assert(plug, DeepEquals, &snap.PlugInfo{ Snap: info, Name: "test-plug", Interface: "test-plug", Hooks: map[string]*snap.HookInfo{withPlugHook.Name: withPlugHook}, }) c.Assert(withPlugHook, DeepEquals, &snap.HookInfo{ Snap: info, Name: "with-plug", Plugs: map[string]*snap.PlugInfo{plug.Name: plug}, Explicit: true, }) c.Assert(withoutPlugHook, DeepEquals, &snap.HookInfo{ Snap: info, Name: "without-plug", Plugs: map[string]*snap.PlugInfo{}, Explicit: true, }) } func (s *YamlSuite) TestUnmarshalExplicitGlobalPlugBoundToHook(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap plugs: test-plug: test-interface hooks: test-hook: plugs: ["test-plug"] `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 1) c.Check(info.Slots, HasLen, 0) c.Check(info.Apps, HasLen, 0) c.Check(info.Hooks, HasLen, 1) plug, ok := info.Plugs["test-plug"] c.Assert(ok, Equals, true, Commentf("Expected plugs to include 'test-plug'")) hook, ok := info.Hooks["test-hook"] c.Assert(ok, Equals, true, Commentf("Expected hooks to include 'test-hook'")) c.Check(plug, DeepEquals, &snap.PlugInfo{ Snap: info, Name: "test-plug", Interface: "test-interface", Hooks: map[string]*snap.HookInfo{hook.Name: hook}, }) c.Check(hook, DeepEquals, &snap.HookInfo{ Snap: info, Name: "test-hook", Plugs: map[string]*snap.PlugInfo{plug.Name: plug}, Explicit: true, }) } func (s *YamlSuite) TestUnmarshalGlobalPlugBoundToHookNotApp(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: snap plugs: test-plug: hooks: test-hook: plugs: [test-plug] apps: test-app: `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "snap") c.Check(info.Plugs, HasLen, 1) c.Check(info.Slots, HasLen, 0) c.Check(info.Apps, HasLen, 1) c.Check(info.Hooks, HasLen, 1) plug := info.Plugs["test-plug"] hook := info.Hooks["test-hook"] app := info.Apps["test-app"] c.Assert(plug, DeepEquals, &snap.PlugInfo{ Snap: info, Name: "test-plug", Interface: "test-plug", Apps: map[string]*snap.AppInfo{}, Hooks: map[string]*snap.HookInfo{hook.Name: hook}, }) c.Assert(hook, DeepEquals, &snap.HookInfo{ Snap: info, Name: "test-hook", Plugs: map[string]*snap.PlugInfo{plug.Name: plug}, Explicit: true, }) c.Assert(app, DeepEquals, &snap.AppInfo{ Snap: info, Name: "test-app", Plugs: map[string]*snap.PlugInfo{}, }) } func (s *YamlSuite) TestUnmarshalComplexExample(c *C) { // NOTE: yaml content cannot use tabs, indent the section with spaces. info, err := snap.InfoFromSnapYaml([]byte(` name: foo version: 1.2 title: Foo summary: foo app type: app epoch: 1* confinement: devmode license: GPL-3.0 description: | Foo provides useful services apps: daemon: command: foo --daemon plugs: [network, network-bind] slots: [foo-socket-slot] foo: command: fooctl plugs: [foo-socket-plug] hooks: test-hook: plugs: [foo-socket-plug] slots: [foo-socket-slot] plugs: foo-socket-plug: interface: socket # $protocol: foo logging: interface: syslog slots: foo-socket-slot: interface: socket path: $SNAP_DATA/socket protocol: foo tracing: interface: ptrace `)) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "foo") c.Check(info.Version, Equals, "1.2") c.Check(info.Type, Equals, snap.TypeApp) c.Check(info.Epoch, DeepEquals, snap.E("1*")) c.Check(info.Confinement, Equals, snap.DevModeConfinement) c.Check(info.Title(), Equals, "Foo") c.Check(info.Summary(), Equals, "foo app") c.Check(info.Description(), Equals, "Foo provides useful services\n") c.Check(info.Apps, HasLen, 2) c.Check(info.Plugs, HasLen, 4) c.Check(info.Slots, HasLen, 2) // these don't come from snap.yaml c.Check(info.Publisher, Equals, snap.StoreAccount{}) c.Check(info.Channel, Equals, "") c.Check(info.License, Equals, "GPL-3.0") app1 := info.Apps["daemon"] app2 := info.Apps["foo"] hook := info.Hooks["test-hook"] plug1 := info.Plugs["network"] plug2 := info.Plugs["network-bind"] plug3 := info.Plugs["foo-socket-plug"] plug4 := info.Plugs["logging"] slot1 := info.Slots["foo-socket-slot"] slot2 := info.Slots["tracing"] // app1 ("daemon") has three plugs ("network", "network-bind", "logging") // and two slots ("foo-socket", "tracing"). The slot "tracing" and plug // "logging" are global, everything else is app-bound. c.Assert(app1, Not(IsNil)) c.Check(app1.Snap, Equals, info) c.Check(app1.Name, Equals, "daemon") c.Check(app1.Command, Equals, "foo --daemon") c.Check(app1.Plugs, DeepEquals, map[string]*snap.PlugInfo{ plug1.Name: plug1, plug2.Name: plug2, plug4.Name: plug4}) c.Check(app1.Slots, DeepEquals, map[string]*snap.SlotInfo{ slot1.Name: slot1, slot2.Name: slot2}) // app2 ("foo") has two plugs ("foo-socket", "logging") and one slot // ("tracing"). The slot "tracing" and plug "logging" are global while // "foo-socket" is app-bound. c.Assert(app2, Not(IsNil)) c.Check(app2.Snap, Equals, info) c.Check(app2.Name, Equals, "foo") c.Check(app2.Command, Equals, "fooctl") c.Check(app2.Plugs, DeepEquals, map[string]*snap.PlugInfo{ plug3.Name: plug3, plug4.Name: plug4}) c.Check(app2.Slots, DeepEquals, map[string]*snap.SlotInfo{ slot2.Name: slot2}) // hook1 has two plugs ("foo-socket", "logging") and two slots ("foo-socket", "tracing"). // The plug "logging" and slot "tracing" are global while "foo-socket" is hook-bound. c.Assert(hook, NotNil) c.Check(hook.Snap, Equals, info) c.Check(hook.Name, Equals, "test-hook") c.Check(hook.Plugs, DeepEquals, map[string]*snap.PlugInfo{ plug3.Name: plug3, plug4.Name: plug4}) c.Check(hook.Slots, DeepEquals, map[string]*snap.SlotInfo{ slot1.Name: slot1, slot2.Name: slot2}) // plug1 ("network") is implicitly defined and app-bound to "daemon" c.Assert(plug1, Not(IsNil)) c.Check(plug1.Snap, Equals, info) c.Check(plug1.Name, Equals, "network") c.Check(plug1.Interface, Equals, "network") c.Check(plug1.Attrs, HasLen, 0) c.Check(plug1.Label, Equals, "") c.Check(plug1.Apps, DeepEquals, map[string]*snap.AppInfo{app1.Name: app1}) // plug2 ("network-bind") is implicitly defined and app-bound to "daemon" c.Assert(plug2, Not(IsNil)) c.Check(plug2.Snap, Equals, info) c.Check(plug2.Name, Equals, "network-bind") c.Check(plug2.Interface, Equals, "network-bind") c.Check(plug2.Attrs, HasLen, 0) c.Check(plug2.Label, Equals, "") c.Check(plug2.Apps, DeepEquals, map[string]*snap.AppInfo{app1.Name: app1}) // plug3 ("foo-socket") is app-bound to "foo" c.Assert(plug3, Not(IsNil)) c.Check(plug3.Snap, Equals, info) c.Check(plug3.Name, Equals, "foo-socket-plug") c.Check(plug3.Interface, Equals, "socket") c.Check(plug3.Attrs, HasLen, 0) c.Check(plug3.Label, Equals, "") c.Check(plug3.Apps, DeepEquals, map[string]*snap.AppInfo{app2.Name: app2}) // plug4 ("logging") is global so it is bound to all apps c.Assert(plug4, Not(IsNil)) c.Check(plug4.Snap, Equals, info) c.Check(plug4.Name, Equals, "logging") c.Check(plug4.Interface, Equals, "syslog") c.Check(plug4.Attrs, HasLen, 0) c.Check(plug4.Label, Equals, "") c.Check(plug4.Apps, DeepEquals, map[string]*snap.AppInfo{ app1.Name: app1, app2.Name: app2}) // slot1 ("foo-socket") is app-bound to "daemon" c.Assert(slot1, Not(IsNil)) c.Check(slot1.Snap, Equals, info) c.Check(slot1.Name, Equals, "foo-socket-slot") c.Check(slot1.Interface, Equals, "socket") c.Check(slot1.Attrs, DeepEquals, map[string]interface{}{ "protocol": "foo", "path": "$SNAP_DATA/socket"}) c.Check(slot1.Label, Equals, "") c.Check(slot1.Apps, DeepEquals, map[string]*snap.AppInfo{app1.Name: app1}) // slot2 ("tracing") is global so it is bound to all apps c.Assert(slot2, Not(IsNil)) c.Check(slot2.Snap, Equals, info) c.Check(slot2.Name, Equals, "tracing") c.Check(slot2.Interface, Equals, "ptrace") c.Check(slot2.Attrs, HasLen, 0) c.Check(slot2.Label, Equals, "") c.Check(slot2.Apps, DeepEquals, map[string]*snap.AppInfo{ app1.Name: app1, app2.Name: app2}) } // type and architectures func (s *YamlSuite) TestSnapYamlTypeDefault(c *C) { y := []byte(`name: binary version: 1.0 `) info, err := snap.InfoFromSnapYaml(y) c.Assert(err, IsNil) c.Assert(info.Type, Equals, snap.TypeApp) } func (s *YamlSuite) TestSnapYamlEpochDefault(c *C) { y := []byte(`name: binary version: 1.0 `) info, err := snap.InfoFromSnapYaml(y) c.Assert(err, IsNil) c.Assert(info.Epoch, DeepEquals, snap.E("0")) } func (s *YamlSuite) TestSnapYamlConfinementDefault(c *C) { y := []byte(`name: binary version: 1.0 `) info, err := snap.InfoFromSnapYaml(y) c.Assert(err, IsNil) c.Assert(info.Confinement, Equals, snap.StrictConfinement) } func (s *YamlSuite) TestSnapYamlMultipleArchitecturesParsing(c *C) { y := []byte(`name: binary version: 1.0 architectures: [i386, armhf] `) info, err := snap.InfoFromSnapYaml(y) c.Assert(err, IsNil) c.Assert(info.Architectures, DeepEquals, []string{"i386", "armhf"}) } func (s *YamlSuite) TestSnapYamlSingleArchitecturesParsing(c *C) { y := []byte(`name: binary version: 1.0 architectures: [i386] `) info, err := snap.InfoFromSnapYaml(y) c.Assert(err, IsNil) c.Assert(info.Architectures, DeepEquals, []string{"i386"}) } func (s *YamlSuite) TestSnapYamlAssumesParsing(c *C) { y := []byte(`name: binary version: 1.0 assumes: [feature2, feature1] `) info, err := snap.InfoFromSnapYaml(y) c.Assert(err, IsNil) c.Assert(info.Assumes, DeepEquals, []string{"feature1", "feature2"}) } func (s *YamlSuite) TestSnapYamlNoArchitecturesParsing(c *C) { y := []byte(`name: binary version: 1.0 `) info, err := snap.InfoFromSnapYaml(y) c.Assert(err, IsNil) c.Assert(info.Architectures, DeepEquals, []string{"all"}) } func (s *YamlSuite) TestSnapYamlBadArchitectureParsing(c *C) { y := []byte(`name: binary version: 1.0 architectures: armhf: no `) _, err := snap.InfoFromSnapYaml(y) c.Assert(err, NotNil) } func (s *YamlSuite) TestSnapYamlLicenseParsing(c *C) { y := []byte(` name: foo version: 1.0 license-agreement: explicit license-version: 12`) info, err := snap.InfoFromSnapYaml(y) c.Assert(err, IsNil) c.Assert(info.LicenseAgreement, Equals, "explicit") c.Assert(info.LicenseVersion, Equals, "12") } // apps func (s *YamlSuite) TestSimpleAppExample(c *C) { y := []byte(`name: wat version: 42 apps: cm: command: cm0 `) info, err := snap.InfoFromSnapYaml(y) c.Assert(err, IsNil) c.Check(info.Apps, DeepEquals, map[string]*snap.AppInfo{ "cm": { Snap: info, Name: "cm", Command: "cm0", }, }) } func (s *YamlSuite) TestDaemonEverythingExample(c *C) { y := []byte(`name: wat version: 42 apps: svc: command: svc1 description: svc one stop-timeout: 25s daemon: forking stop-command: stop-cmd post-stop-command: post-stop-cmd restart-condition: on-abnormal bus-name: busName sockets: sock1: listen-stream: $SNAP_DATA/sock1.socket socket-mode: 0666 `) info, err := snap.InfoFromSnapYaml(y) c.Assert(err, IsNil) app := snap.AppInfo{ Snap: info, Name: "svc", Command: "svc1", Daemon: "forking", RestartCond: snap.RestartOnAbnormal, StopTimeout: timeout.Timeout(25 * time.Second), StopCommand: "stop-cmd", PostStopCommand: "post-stop-cmd", BusName: "busName", Sockets: map[string]*snap.SocketInfo{}, } app.Sockets["sock1"] = &snap.SocketInfo{ App: &app, Name: "sock1", ListenStream: "$SNAP_DATA/sock1.socket", SocketMode: 0666, } c.Check(info.Apps, DeepEquals, map[string]*snap.AppInfo{"svc": &app}) } func (s *YamlSuite) TestDaemonListenStreamAsInteger(c *C) { y := []byte(`name: wat version: 42 apps: svc: command: svc sockets: sock: listen-stream: 8080 `) info, err := snap.InfoFromSnapYaml(y) c.Assert(err, IsNil) app := snap.AppInfo{ Snap: info, Name: "svc", Command: "svc", Sockets: map[string]*snap.SocketInfo{}, } app.Sockets["sock"] = &snap.SocketInfo{ App: &app, Name: "sock", ListenStream: "8080", } c.Check(info.Apps, DeepEquals, map[string]*snap.AppInfo{ "svc": &app, }) } func (s *YamlSuite) TestDaemonInvalidSocketMode(c *C) { y := []byte(`name: wat version: 42 apps: svc: command: svc sockets: sock: listen-stream: 8080 socket-mode: asdfasdf `) _, err := snap.InfoFromSnapYaml(y) c.Check(err.Error(), Equals, "cannot parse snap.yaml: yaml: unmarshal errors:\n"+ " line 9: cannot unmarshal !!str `asdfasdf` into os.FileMode") } func (s *YamlSuite) TestSnapYamlGlobalEnvironment(c *C) { y := []byte(` name: foo version: 1.0 environment: foo: bar baz: boom `) info, err := snap.InfoFromSnapYaml(y) c.Assert(err, IsNil) c.Assert(info.Environment, DeepEquals, *strutil.NewOrderedMap("foo", "bar", "baz", "boom")) } func (s *YamlSuite) TestSnapYamlPerAppEnvironment(c *C) { y := []byte(` name: foo version: 1.0 apps: foo: environment: k1: v1 k2: v2 `) info, err := snap.InfoFromSnapYaml(y) c.Assert(err, IsNil) c.Assert(info.Apps["foo"].Environment, DeepEquals, *strutil.NewOrderedMap("k1", "v1", "k2", "v2")) } func (s *YamlSuite) TestSnapYamlPerHookEnvironment(c *C) { y := []byte(` name: foo version: 1.0 hooks: foo: environment: k1: v1 k2: v2 `) info, err := snap.InfoFromSnapYaml(y) c.Assert(err, IsNil) c.Assert(info.Hooks["foo"].Environment, DeepEquals, *strutil.NewOrderedMap("k1", "v1", "k2", "v2")) } // classic confinement func (s *YamlSuite) TestClassicConfinement(c *C) { y := []byte(` name: foo confinement: classic `) info, err := snap.InfoFromSnapYaml(y) c.Assert(err, IsNil) c.Assert(info.Confinement, Equals, snap.ClassicConfinement) } func (s *YamlSuite) TestSnapYamlAliases(c *C) { y := []byte(` name: foo version: 1.0 apps: foo: aliases: [foo] bar: aliases: [bar, bar1] `) info, err := snap.InfoFromSnapYaml(y) c.Assert(err, IsNil) c.Check(info.Apps["foo"].LegacyAliases, DeepEquals, []string{"foo"}) c.Check(info.Apps["bar"].LegacyAliases, DeepEquals, []string{"bar", "bar1"}) c.Check(info.LegacyAliases, DeepEquals, map[string]*snap.AppInfo{ "foo": info.Apps["foo"], "bar": info.Apps["bar"], "bar1": info.Apps["bar"], }) } func (s *YamlSuite) TestSnapYamlAliasesConflict(c *C) { y := []byte(` name: foo version: 1.0 apps: foo: aliases: [bar] bar: aliases: [bar] `) _, err := snap.InfoFromSnapYaml(y) c.Assert(err, ErrorMatches, `cannot set "bar" as alias for both ("foo" and "bar"|"bar" and "foo")`) } func (s *YamlSuite) TestSnapYamlAppStartOrder(c *C) { y := []byte(`name: wat version: 42 apps: foo: after: [bar, zed] bar: before: [foo] baz: after: [foo] zed: `) info, err := snap.InfoFromSnapYaml(y) c.Assert(err, IsNil) c.Check(info.Apps, DeepEquals, map[string]*snap.AppInfo{ "foo": { Snap: info, Name: "foo", After: []string{"bar", "zed"}, }, "bar": { Snap: info, Name: "bar", Before: []string{"foo"}, }, "baz": { Snap: info, Name: "baz", After: []string{"foo"}, }, "zed": { Snap: info, Name: "zed", }, }) } func (s *YamlSuite) TestSnapYamlWatchdog(c *C) { y := []byte(` name: foo version: 1.0 apps: foo: watchdog-timeout: 12s `) info, err := snap.InfoFromSnapYaml(y) c.Assert(err, IsNil) c.Check(info.Apps["foo"].WatchdogTimeout, Equals, timeout.Timeout(12*time.Second)) } func (s *YamlSuite) TestLayout(c *C) { y := []byte(` name: foo version: 1.0 layout: /usr/share/foo: bind: $SNAP/usr/share/foo /usr/share/bar: symlink: $SNAP/usr/share/bar /etc/froz: bind-file: $SNAP/etc/froz `) info, err := snap.InfoFromSnapYaml(y) c.Assert(err, IsNil) c.Assert(info.Layout["/usr/share/foo"], DeepEquals, &snap.Layout{ Snap: info, Path: "/usr/share/foo", Bind: "$SNAP/usr/share/foo", User: "root", Group: "root", Mode: 0755, }) c.Assert(info.Layout["/usr/share/bar"], DeepEquals, &snap.Layout{ Snap: info, Path: "/usr/share/bar", Symlink: "$SNAP/usr/share/bar", User: "root", Group: "root", Mode: 0755, }) c.Assert(info.Layout["/etc/froz"], DeepEquals, &snap.Layout{ Snap: info, Path: "/etc/froz", BindFile: "$SNAP/etc/froz", User: "root", Group: "root", Mode: 0755, }) } func (s *YamlSuite) TestLayoutsWithTypo(c *C) { y := []byte(` name: foo version: 1.0 layouts: /usr/share/foo: bind: $SNAP/usr/share/foo `) info, err := snap.InfoFromSnapYaml(y) c.Assert(err, ErrorMatches, `cannot parse snap.yaml: typo detected: use singular "layout" instead of plural "layouts"`) c.Assert(info, IsNil) } func (s *YamlSuite) TestSnapYamlAppTimer(c *C) { y := []byte(`name: wat version: 42 apps: foo: daemon: oneshot timer: mon,10:00-12:00 `) info, err := snap.InfoFromSnapYaml(y) c.Assert(err, IsNil) app := info.Apps["foo"] c.Check(app.Timer, DeepEquals, &snap.TimerInfo{App: app, Timer: "mon,10:00-12:00"}) } func (s *YamlSuite) TestSnapYamlAppAutostart(c *C) { yAutostart := []byte(`name: wat version: 42 apps: foo: command: bin/foo autostart: foo.desktop `) info, err := snap.InfoFromSnapYaml(yAutostart) c.Assert(err, IsNil) app := info.Apps["foo"] c.Check(app.Autostart, Equals, "foo.desktop") yNoAutostart := []byte(`name: wat version: 42 apps: foo: command: bin/foo `) info, err = snap.InfoFromSnapYaml(yNoAutostart) c.Assert(err, IsNil) app = info.Apps["foo"] c.Check(app.Autostart, Equals, "") } func (s *YamlSuite) TestSnapYamlAppCommonID(c *C) { yAutostart := []byte(`name: wat version: 42 apps: foo: command: bin/foo common-id: org.foo bar: command: bin/foo common-id: org.bar baz: command: bin/foo `) info, err := snap.InfoFromSnapYaml(yAutostart) c.Assert(err, IsNil) c.Check(info.Apps["foo"].CommonID, Equals, "org.foo") c.Check(info.Apps["bar"].CommonID, Equals, "org.bar") c.Check(info.Apps["baz"].CommonID, Equals, "") c.Assert(info.CommonIDs, HasLen, 2) c.Assert((info.CommonIDs[0] == "org.foo" && info.CommonIDs[1] == "org.bar") || (info.CommonIDs[1] == "org.foo" && info.CommonIDs[0] == "org.bar"), Equals, true) } func (s *YamlSuite) TestSnapYamlCommandChain(c *C) { yAutostart := []byte(`name: wat version: 42 apps: foo: command: bin/foo command-chain: [chain1, chain2] hooks: configure: command-chain: [hookchain1, hookchain2] `) info, err := snap.InfoFromSnapYaml(yAutostart) c.Assert(err, IsNil) app := info.Apps["foo"] c.Check(app.CommandChain, DeepEquals, []string{"chain1", "chain2"}) hook := info.Hooks["configure"] c.Check(hook.CommandChain, DeepEquals, []string{"hookchain1", "hookchain2"}) } func (s *YamlSuite) TestSnapYamlRestartDelay(c *C) { yAutostart := []byte(`name: wat version: 42 apps: foo: command: bin/foo daemon: simple restart-delay: 12s `) info, err := snap.InfoFromSnapYaml(yAutostart) c.Assert(err, IsNil) app := info.Apps["foo"] c.Assert(app, NotNil) c.Check(app.RestartDelay, Equals, timeout.Timeout(12*time.Second)) } snapd-2.37.4~14.04.1/snap/gadget_test.go0000664000000000000000000002447513435556260014337 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2016 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 . * */ package snap_test import ( "io/ioutil" "path/filepath" "strings" . "gopkg.in/check.v1" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snaptest" ) type gadgetYamlTestSuite struct { } var _ = Suite(&gadgetYamlTestSuite{}) var mockGadgetSnapYaml = ` name: canonical-pc type: gadget ` var mockGadgetYaml = []byte(` defaults: system: something: true connections: - plug: snapid1:plg1 slot: snapid2:slot - plug: snapid3:process-control - plug: snapid4:pctl4 slot: system:process-control volumes: volumename: schema: mbr bootloader: u-boot id: id,guid structure: - filesystem-label: system-boot offset: 12345 offset-write: 777 size: 88888 type: id,guid id: id,guid filesystem: vfat content: - source: subdir/ target: / unpack: false - image: foo.img offset: 4321 offset-write: 8888 size: 88888 unpack: false `) var mockMultiVolumeGadgetYaml = []byte(` device-tree: frobinator-3000.dtb device-tree-origin: kernel volumes: frobinator-3000-image: bootloader: u-boot schema: mbr structure: - name: system-boot type: 0C filesystem: vfat filesystem-label: system-boot size: 128M role: system-boot content: - source: splash.bmp target: . - name: writable type: 83 filesystem: ext4 filesystem-label: writable size: 380M role: system-data u-boot-frobinator-3000: structure: - name: u-boot type: bare size: 623000 offset: 0 content: - image: u-boot.imz `) var mockClassicGadgetYaml = []byte(` defaults: system: something: true otheridididididididididididididi: foo: bar: baz `) func (s *gadgetYamlTestSuite) SetUpTest(c *C) { dirs.SetRootDir(c.MkDir()) } func (s *gadgetYamlTestSuite) TearDownTest(c *C) { dirs.SetRootDir("/") } func (s *gadgetYamlTestSuite) TestReadGadgetNotAGadget(c *C) { info := snaptest.MockInfo(c, ` name: other version: 0 `, &snap.SideInfo{Revision: snap.R(42)}) _, err := snap.ReadGadgetInfo(info, false) c.Assert(err, ErrorMatches, "cannot read gadget snap details: not a gadget snap") } func (s *gadgetYamlTestSuite) TestReadGadgetYamlMissing(c *C) { info := snaptest.MockSnap(c, mockGadgetSnapYaml, &snap.SideInfo{Revision: snap.R(42)}) _, err := snap.ReadGadgetInfo(info, false) c.Assert(err, ErrorMatches, ".*meta/gadget.yaml: no such file or directory") } func (s *gadgetYamlTestSuite) TestReadGadgetYamlOnClassicOptional(c *C) { info := snaptest.MockSnap(c, mockGadgetSnapYaml, &snap.SideInfo{Revision: snap.R(42)}) gi, err := snap.ReadGadgetInfo(info, true) c.Assert(err, IsNil) c.Check(gi, NotNil) } func (s *gadgetYamlTestSuite) TestReadGadgetYamlOnClassicEmptyIsValid(c *C) { info := snaptest.MockSnap(c, mockGadgetSnapYaml, &snap.SideInfo{Revision: snap.R(42)}) err := ioutil.WriteFile(filepath.Join(info.MountDir(), "meta", "gadget.yaml"), nil, 0644) c.Assert(err, IsNil) ginfo, err := snap.ReadGadgetInfo(info, true) c.Assert(err, IsNil) c.Assert(ginfo, DeepEquals, &snap.GadgetInfo{}) } func (s *gadgetYamlTestSuite) TestReadGadgetYamlOnClassicOnylDefaultsIsValid(c *C) { info := snaptest.MockSnap(c, mockGadgetSnapYaml, &snap.SideInfo{Revision: snap.R(42)}) err := ioutil.WriteFile(filepath.Join(info.MountDir(), "meta", "gadget.yaml"), mockClassicGadgetYaml, 0644) c.Assert(err, IsNil) ginfo, err := snap.ReadGadgetInfo(info, true) c.Assert(err, IsNil) c.Assert(ginfo, DeepEquals, &snap.GadgetInfo{ Defaults: map[string]map[string]interface{}{ "system": {"something": true}, "otheridididididididididididididi": {"foo": map[string]interface{}{"bar": "baz"}}, }, }) } func (s *gadgetYamlTestSuite) TestReadGadgetYamlValid(c *C) { info := snaptest.MockSnap(c, mockGadgetSnapYaml, &snap.SideInfo{Revision: snap.R(42)}) err := ioutil.WriteFile(filepath.Join(info.MountDir(), "meta", "gadget.yaml"), mockGadgetYaml, 0644) c.Assert(err, IsNil) ginfo, err := snap.ReadGadgetInfo(info, false) c.Assert(err, IsNil) c.Assert(ginfo, DeepEquals, &snap.GadgetInfo{ Defaults: map[string]map[string]interface{}{ "system": {"something": true}, }, Connections: []snap.GadgetConnection{ {Plug: snap.GadgetConnectionPlug{SnapID: "snapid1", Plug: "plg1"}, Slot: snap.GadgetConnectionSlot{SnapID: "snapid2", Slot: "slot"}}, {Plug: snap.GadgetConnectionPlug{SnapID: "snapid3", Plug: "process-control"}, Slot: snap.GadgetConnectionSlot{SnapID: "system", Slot: "process-control"}}, {Plug: snap.GadgetConnectionPlug{SnapID: "snapid4", Plug: "pctl4"}, Slot: snap.GadgetConnectionSlot{SnapID: "system", Slot: "process-control"}}, }, Volumes: map[string]snap.GadgetVolume{ "volumename": { Schema: "mbr", Bootloader: "u-boot", ID: "id,guid", Structure: []snap.VolumeStructure{ { Label: "system-boot", Offset: "12345", OffsetWrite: "777", Size: "88888", Type: "id,guid", ID: "id,guid", Filesystem: "vfat", Content: []snap.VolumeContent{ { Source: "subdir/", Target: "/", Unpack: false, }, { Image: "foo.img", Offset: "4321", OffsetWrite: "8888", Size: "88888", Unpack: false, }, }, }, }, }, }, }) } func (s *gadgetYamlTestSuite) TestReadMultiVolumeGadgetYamlValid(c *C) { info := snaptest.MockSnap(c, mockGadgetSnapYaml, &snap.SideInfo{Revision: snap.R(42)}) err := ioutil.WriteFile(filepath.Join(info.MountDir(), "meta", "gadget.yaml"), mockMultiVolumeGadgetYaml, 0644) c.Assert(err, IsNil) ginfo, err := snap.ReadGadgetInfo(info, false) c.Assert(err, IsNil) c.Check(ginfo.Volumes, HasLen, 2) c.Assert(ginfo, DeepEquals, &snap.GadgetInfo{ Volumes: map[string]snap.GadgetVolume{ "frobinator-3000-image": { Schema: "mbr", Bootloader: "u-boot", Structure: []snap.VolumeStructure{ { Label: "system-boot", Size: "128M", Filesystem: "vfat", Type: "0C", Content: []snap.VolumeContent{ { Source: "splash.bmp", Target: ".", }, }, }, { Label: "writable", Type: "83", Filesystem: "ext4", Size: "380M", }, }, }, "u-boot-frobinator-3000": { Structure: []snap.VolumeStructure{ { Type: "bare", Size: "623000", Offset: "0", Content: []snap.VolumeContent{ { Image: "u-boot.imz", }, }, }, }, }, }, }) } func (s *gadgetYamlTestSuite) TestReadGadgetYamlInvalidBootloader(c *C) { info := snaptest.MockSnap(c, mockGadgetSnapYaml, &snap.SideInfo{Revision: snap.R(42)}) mockGadgetYamlBroken := []byte(` volumes: name: bootloader: silo `) err := ioutil.WriteFile(filepath.Join(info.MountDir(), "meta", "gadget.yaml"), mockGadgetYamlBroken, 0644) c.Assert(err, IsNil) _, err = snap.ReadGadgetInfo(info, false) c.Assert(err, ErrorMatches, "cannot read gadget snap details: bootloader must be one of grub, u-boot or android-boot") } func (s *gadgetYamlTestSuite) TestReadGadgetYamlEmptydBootloader(c *C) { info := snaptest.MockSnap(c, mockGadgetSnapYaml, &snap.SideInfo{Revision: snap.R(42)}) mockGadgetYamlBroken := []byte(` volumes: name: bootloader: `) err := ioutil.WriteFile(filepath.Join(info.MountDir(), "meta", "gadget.yaml"), mockGadgetYamlBroken, 0644) c.Assert(err, IsNil) _, err = snap.ReadGadgetInfo(info, false) c.Assert(err, ErrorMatches, "cannot read gadget snap details: bootloader not declared in any volume") } func (s *gadgetYamlTestSuite) TestReadGadgetYamlMissingBootloader(c *C) { info := snaptest.MockSnap(c, mockGadgetSnapYaml, &snap.SideInfo{Revision: snap.R(42)}) err := ioutil.WriteFile(filepath.Join(info.MountDir(), "meta", "gadget.yaml"), nil, 0644) c.Assert(err, IsNil) _, err = snap.ReadGadgetInfo(info, false) c.Assert(err, ErrorMatches, "cannot read gadget snap details: bootloader not declared in any volume") } func (s *gadgetYamlTestSuite) TestReadGadgetYamlInvalidDefaultsKey(c *C) { info := snaptest.MockSnap(c, mockGadgetSnapYaml, &snap.SideInfo{Revision: snap.R(42)}) mockGadgetYamlBroken := []byte(` defaults: foo: x: 1 `) err := ioutil.WriteFile(filepath.Join(info.MountDir(), "meta", "gadget.yaml"), mockGadgetYamlBroken, 0644) c.Assert(err, IsNil) _, err = snap.ReadGadgetInfo(info, false) c.Assert(err, ErrorMatches, `default stanza not keyed by "system" or snap-id: foo`) } func (s *gadgetYamlTestSuite) TestReadGadgetYamlInvalidConnection(c *C) { info := snaptest.MockSnap(c, mockGadgetSnapYaml, &snap.SideInfo{Revision: snap.R(42)}) mockGadgetYamlBroken := ` connections: - @INVALID@ ` tests := []struct { invalidConn string expectedErr string }{ {``, `gadget connection plug cannot be empty`}, {`foo:bar baz:quux`, `(?s).*unmarshal errors:.*`}, {`plug: foo:`, `.*mapping values are not allowed in this context`}, {`plug: ":"`, `.*in gadget connection plug: expected "\(\|system\):name" not ":"`}, {`slot: "foo:"`, `.*in gadget connection slot: expected "\(\|system\):name" not "foo:"`}, {`slot: foo:bar`, `gadget connection plug cannot be empty`}, } for _, t := range tests { mockGadgetYamlBroken := strings.Replace(mockGadgetYamlBroken, "@INVALID@", t.invalidConn, 1) err := ioutil.WriteFile(filepath.Join(info.MountDir(), "meta", "gadget.yaml"), []byte(mockGadgetYamlBroken), 0644) c.Assert(err, IsNil) _, err = snap.ReadGadgetInfo(info, false) c.Check(err, ErrorMatches, t.expectedErr) } } snapd-2.37.4~14.04.1/snap/validate.go0000664000000000000000000006775313435556260013644 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package snap import ( "errors" "fmt" "os" "path/filepath" "regexp" "sort" "strconv" "strings" "unicode/utf8" "github.com/snapcore/snapd/spdx" "github.com/snapcore/snapd/strutil" "github.com/snapcore/snapd/timeutil" ) // Regular expressions describing correct identifiers. var validHookName = regexp.MustCompile("^[a-z](?:-?[a-z0-9])*$") // The fixed length of valid snap IDs. const validSnapIDLength = 32 // almostValidName is part of snap and socket name validation. // the full regexp we could use, "^(?:[a-z0-9]+-?)*[a-z](?:-?[a-z0-9])*$", is // O(2ⁿ) on the length of the string in python. An equivalent regexp that // doesn't have the nested quantifiers that trip up Python's re would be // "^(?:[a-z0-9]|(?<=[a-z0-9])-)*[a-z](?:[a-z0-9]|-(?=[a-z0-9]))*$", but Go's // regexp package doesn't support look-aheads nor look-behinds, so in order to // have a unified implementation in the Go and Python bits of the project // we're doing it this way instead. Check the length (if applicable), check // this regexp, then check the dashes. // This still leaves sc_snap_name_validate (in cmd/snap-confine/snap.c) and // snap_validate (cmd/snap-update-ns/bootstrap.c) with their own handcrafted // validators. var almostValidName = regexp.MustCompile("^[a-z0-9-]*[a-z][a-z0-9-]*$") // validInstanceKey is a regular expression describing valid snap instance key var validInstanceKey = regexp.MustCompile("^[a-z0-9]{1,10}$") // isValidName checks snap and socket socket identifiers. func isValidName(name string) bool { if !almostValidName.MatchString(name) { return false } if name[0] == '-' || name[len(name)-1] == '-' || strings.Contains(name, "--") { return false } return true } // ValidateInstanceName checks if a string can be used as a snap instance name. func ValidateInstanceName(instanceName string) error { // NOTE: This function should be synchronized with the two other // implementations: sc_instance_name_validate and validate_instance_name . pos := strings.IndexByte(instanceName, '_') if pos == -1 { // just store name return ValidateName(instanceName) } storeName := instanceName[:pos] instanceKey := instanceName[pos+1:] if err := ValidateName(storeName); err != nil { return err } if !validInstanceKey.MatchString(instanceKey) { return fmt.Errorf("invalid instance key: %q", instanceKey) } return nil } // ValidateName checks if a string can be used as a snap name. func ValidateName(name string) error { // NOTE: This function should be synchronized with the two other // implementations: sc_snap_name_validate and validate_snap_name . if len(name) < 2 || len(name) > 40 || !isValidName(name) { return fmt.Errorf("invalid snap name: %q", name) } return nil } // Regular expression describing correct plug, slot and interface names. var validPlugSlotIfaceName = regexp.MustCompile("^[a-z](?:-?[a-z0-9])*$") // ValidatePlugName checks if a string can be used as a slot name. // // Slot names and plug names within one snap must have unique names. // This is not enforced by this function but is enforced by snap-level // validation. func ValidatePlugName(name string) error { if !validPlugSlotIfaceName.MatchString(name) { return fmt.Errorf("invalid plug name: %q", name) } return nil } // ValidateSlotName checks if a string can be used as a slot name. // // Slot names and plug names within one snap must have unique names. // This is not enforced by this function but is enforced by snap-level // validation. func ValidateSlotName(name string) error { if !validPlugSlotIfaceName.MatchString(name) { return fmt.Errorf("invalid slot name: %q", name) } return nil } // ValidateInterfaceName checks if a string can be used as an interface name. func ValidateInterfaceName(name string) error { if !validPlugSlotIfaceName.MatchString(name) { return fmt.Errorf("invalid interface name: %q", name) } return nil } // NB keep this in sync with snapcraft and the review tools :-) var isValidVersion = regexp.MustCompile("^[a-zA-Z0-9](?:[a-zA-Z0-9:.+~-]{0,30}[a-zA-Z0-9+~])?$").MatchString var isNonGraphicalASCII = regexp.MustCompile("[^[:graph:]]").MatchString var isInvalidFirstVersionChar = regexp.MustCompile("^[^a-zA-Z0-9]").MatchString var isInvalidLastVersionChar = regexp.MustCompile("[^a-zA-Z0-9+~]$").MatchString var invalidMiddleVersionChars = regexp.MustCompile("[^a-zA-Z0-9:.+~-]+").FindAllString // ValidateVersion checks if a string is a valid snap version. func ValidateVersion(version string) error { if !isValidVersion(version) { // maybe it was too short? if len(version) == 0 { return errors.New("invalid snap version: cannot be empty") } if isNonGraphicalASCII(version) { // note that while this way of quoting the version can produce ugly // output in some cases (e.g. if you're trying to set a version to // "hello😁", seeing “invalid version "hello😁"” could be clearer than // “invalid snap version "hello\U0001f601"”), in a lot of more // interesting cases you _need_ to have the thing that's not ASCII // pointed out: homoglyphs and near-homoglyphs are too hard to spot // otherwise. Take for example a version of "аерс". Or "v1.0‑x". return fmt.Errorf("invalid snap version %s: must be printable, non-whitespace ASCII", strconv.QuoteToASCII(version)) } // now we know it's a non-empty ASCII string, we can get serious var reasons []string // ... too long? if len(version) > 32 { reasons = append(reasons, fmt.Sprintf("cannot be longer than 32 characters (got: %d)", len(version))) } // started with a symbol? if isInvalidFirstVersionChar(version) { // note that we can only say version[0] because we know it's ASCII :-) reasons = append(reasons, fmt.Sprintf("must start with an ASCII alphanumeric (and not %q)", version[0])) } if len(version) > 1 { if isInvalidLastVersionChar(version) { tpl := "must end with an ASCII alphanumeric or one of '+' or '~' (and not %q)" reasons = append(reasons, fmt.Sprintf(tpl, version[len(version)-1])) } if len(version) > 2 { if all := invalidMiddleVersionChars(version[1:len(version)-1], -1); len(all) > 0 { reasons = append(reasons, fmt.Sprintf("contains invalid characters: %s", strutil.Quoted(all))) } } } switch len(reasons) { case 0: // huh return fmt.Errorf("invalid snap version %q", version) case 1: return fmt.Errorf("invalid snap version %q: %s", version, reasons[0]) default: reasons, last := reasons[:len(reasons)-1], reasons[len(reasons)-1] return fmt.Errorf("invalid snap version %q: %s, and %s", version, strings.Join(reasons, ", "), last) } } return nil } // ValidateLicense checks if a string is a valid SPDX expression. func ValidateLicense(license string) error { if err := spdx.ValidateLicense(license); err != nil { return fmt.Errorf("cannot validate license %q: %s", license, err) } return nil } // ValidateHook validates the content of the given HookInfo func ValidateHook(hook *HookInfo) error { valid := validHookName.MatchString(hook.Name) if !valid { return fmt.Errorf("invalid hook name: %q", hook.Name) } // Also validate the command chain for _, value := range hook.CommandChain { if !commandChainContentWhitelist.MatchString(value) { return fmt.Errorf("hook command-chain contains illegal %q (legal: '%s')", value, commandChainContentWhitelist) } } return nil } var validAlias = regexp.MustCompile("^[a-zA-Z0-9][-_.a-zA-Z0-9]*$") // ValidateAlias checks if a string can be used as an alias name. func ValidateAlias(alias string) error { valid := validAlias.MatchString(alias) if !valid { return fmt.Errorf("invalid alias name: %q", alias) } return nil } // validateSocketName checks if a string ca be used as a name for a socket (for // socket activation). func validateSocketName(name string) error { if !isValidName(name) { return fmt.Errorf("invalid socket name: %q", name) } return nil } // validateSocketmode checks that the socket mode is a valid file mode. func validateSocketMode(mode os.FileMode) error { if mode > 0777 { return fmt.Errorf("cannot use mode: %04o", mode) } return nil } // validateSocketAddr checks that the value of socket addresses. func validateSocketAddr(socket *SocketInfo, fieldName string, address string) error { if address == "" { return fmt.Errorf("%q is not defined", fieldName) } switch address[0] { case '/', '$': return validateSocketAddrPath(socket, fieldName, address) case '@': return validateSocketAddrAbstract(socket, fieldName, address) default: return validateSocketAddrNet(socket, fieldName, address) } } func validateSocketAddrPath(socket *SocketInfo, fieldName string, path string) error { if clean := filepath.Clean(path); clean != path { return fmt.Errorf("invalid %q: %q should be written as %q", fieldName, path, clean) } if !(strings.HasPrefix(path, "$SNAP_DATA/") || strings.HasPrefix(path, "$SNAP_COMMON/")) { return fmt.Errorf( "invalid %q: must have a prefix of $SNAP_DATA or $SNAP_COMMON", fieldName) } return nil } func validateSocketAddrAbstract(socket *SocketInfo, fieldName string, path string) error { // this comes from snap declaration, so the prefix can only be the snap // name at this point prefix := fmt.Sprintf("@snap.%s.", socket.App.Snap.SnapName()) if !strings.HasPrefix(path, prefix) { return fmt.Errorf("path for %q must be prefixed with %q", fieldName, prefix) } return nil } func validateSocketAddrNet(socket *SocketInfo, fieldName string, address string) error { lastIndex := strings.LastIndex(address, ":") if lastIndex >= 0 { if err := validateSocketAddrNetHost(socket, fieldName, address[:lastIndex]); err != nil { return err } return validateSocketAddrNetPort(socket, fieldName, address[lastIndex+1:]) } // Address only contains a port return validateSocketAddrNetPort(socket, fieldName, address) } func validateSocketAddrNetHost(socket *SocketInfo, fieldName string, address string) error { validAddresses := []string{"127.0.0.1", "[::1]", "[::]"} for _, valid := range validAddresses { if address == valid { return nil } } return fmt.Errorf("invalid %q address %q, must be one of: %s", fieldName, address, strings.Join(validAddresses, ", ")) } func validateSocketAddrNetPort(socket *SocketInfo, fieldName string, port string) error { var val uint64 var err error retErr := fmt.Errorf("invalid %q port number %q", fieldName, port) if val, err = strconv.ParseUint(port, 10, 16); err != nil { return retErr } if val < 1 || val > 65535 { return retErr } return nil } func validateDescription(descr string) error { if count := utf8.RuneCountInString(descr); count > 4096 { return fmt.Errorf("description can have up to 4096 codepoints, got %d", count) } return nil } func validateTitle(title string) error { if count := utf8.RuneCountInString(title); count > 40 { return fmt.Errorf("title can have up to 40 codepoints, got %d", count) } return nil } // Validate verifies the content in the info. func Validate(info *Info) error { name := info.InstanceName() if name == "" { return errors.New("snap name cannot be empty") } if err := ValidateName(info.SnapName()); err != nil { return err } if err := ValidateInstanceName(name); err != nil { return err } if err := validateTitle(info.Title()); err != nil { return err } if err := validateDescription(info.Description()); err != nil { return err } if err := ValidateVersion(info.Version); err != nil { return err } if err := info.Epoch.Validate(); err != nil { return err } if license := info.License; license != "" { if err := ValidateLicense(license); err != nil { return err } } // validate app entries for _, app := range info.Apps { if err := ValidateApp(app); err != nil { return fmt.Errorf("invalid definition of application %q: %v", app.Name, err) } } // validate apps ordering according to after/before if err := validateAppOrderCycles(info.Services()); err != nil { return err } // validate aliases for alias, app := range info.LegacyAliases { if !validAlias.MatchString(alias) { return fmt.Errorf("cannot have %q as alias name for app %q - use only letters, digits, dash, underscore and dot characters", alias, app.Name) } } // validate hook entries for _, hook := range info.Hooks { if err := ValidateHook(hook); err != nil { return err } } // Ensure that plugs and slots have appropriate names and interface names. if err := plugsSlotsInterfacesNames(info); err != nil { return err } // Ensure that plug and slot have unique names. if err := plugsSlotsUniqueNames(info); err != nil { return err } // validate that bases do not have base fields if info.Type == TypeOS || info.Type == TypeBase { if info.Base != "" { return fmt.Errorf(`cannot have "base" field on %q snap %q`, info.Type, info.InstanceName()) } } // ensure that common-id(s) are unique if err := ValidateCommonIDs(info); err != nil { return err } return ValidateLayoutAll(info) } // ValidateLayoutAll validates the consistency of all the layout elements in a snap. func ValidateLayoutAll(info *Info) error { paths := make([]string, 0, len(info.Layout)) for _, layout := range info.Layout { paths = append(paths, layout.Path) } sort.Strings(paths) // Validate that each source path is used consistently as a file or as a directory. sourceKindMap := make(map[string]string) for _, path := range paths { layout := info.Layout[path] if layout.Bind != "" { // Layout refers to a directory. sourcePath := info.ExpandSnapVariables(layout.Bind) if kind, ok := sourceKindMap[sourcePath]; ok { if kind != "dir" { return fmt.Errorf("layout %q refers to directory %q but another layout treats it as file", layout.Path, layout.Bind) } } sourceKindMap[sourcePath] = "dir" } if layout.BindFile != "" { // Layout refers to a file. sourcePath := info.ExpandSnapVariables(layout.BindFile) if kind, ok := sourceKindMap[sourcePath]; ok { if kind != "file" { return fmt.Errorf("layout %q refers to file %q but another layout treats it as a directory", layout.Path, layout.BindFile) } } sourceKindMap[sourcePath] = "file" } } // Validate each layout item and collect resulting constraints. constraints := make([]LayoutConstraint, 0, len(info.Layout)) for _, path := range paths { layout := info.Layout[path] if err := ValidateLayout(layout, constraints); err != nil { return err } constraints = append(constraints, layout.constraint()) } return nil } func plugsSlotsInterfacesNames(info *Info) error { for plugName, plug := range info.Plugs { if err := ValidatePlugName(plugName); err != nil { return err } if err := ValidateInterfaceName(plug.Interface); err != nil { return fmt.Errorf("invalid interface name %q for plug %q", plug.Interface, plugName) } } for slotName, slot := range info.Slots { if err := ValidateSlotName(slotName); err != nil { return err } if err := ValidateInterfaceName(slot.Interface); err != nil { return fmt.Errorf("invalid interface name %q for slot %q", slot.Interface, slotName) } } return nil } func plugsSlotsUniqueNames(info *Info) error { // we could choose the smaller collection if we wanted to optimize this check for plugName := range info.Plugs { if info.Slots[plugName] != nil { return fmt.Errorf("cannot have plug and slot with the same name: %q", plugName) } } return nil } func validateField(name, cont string, whitelist *regexp.Regexp) error { if !whitelist.MatchString(cont) { return fmt.Errorf("app description field '%s' contains illegal %q (legal: '%s')", name, cont, whitelist) } return nil } func validateAppSocket(socket *SocketInfo) error { if err := validateSocketName(socket.Name); err != nil { return err } if err := validateSocketMode(socket.SocketMode); err != nil { return err } return validateSocketAddr(socket, "listen-stream", socket.ListenStream) } // validateAppOrderCycles checks for cycles in app ordering dependencies func validateAppOrderCycles(apps []*AppInfo) error { if _, err := SortServices(apps); err != nil { return err } return nil } func validateAppOrderNames(app *AppInfo, dependencies []string) error { // we must be a service to request ordering if len(dependencies) > 0 && !app.IsService() { return errors.New("must be a service to define before/after ordering") } for _, dep := range dependencies { // dependency is not defined other, ok := app.Snap.Apps[dep] if !ok { return fmt.Errorf("before/after references a missing application %q", dep) } if !other.IsService() { return fmt.Errorf("before/after references a non-service application %q", dep) } } return nil } func validateAppWatchdog(app *AppInfo) error { if app.WatchdogTimeout == 0 { // no watchdog return nil } if !app.IsService() { return errors.New("watchdog-timeout is only applicable to services") } if app.WatchdogTimeout < 0 { return errors.New("watchdog-timeout cannot be negative") } return nil } func validateAppTimer(app *AppInfo) error { if app.Timer == nil { return nil } if !app.IsService() { return errors.New("timer is only applicable to services") } if _, err := timeutil.ParseSchedule(app.Timer.Timer); err != nil { return fmt.Errorf("timer has invalid format: %v", err) } return nil } func validateAppRestart(app *AppInfo) error { // app.RestartCond value is validated when unmarshalling if app.RestartDelay == 0 && app.RestartCond == "" { return nil } if app.RestartDelay != 0 { if !app.IsService() { return errors.New("restart-delay is only applicable to services") } if app.RestartDelay < 0 { return errors.New("restart-delay cannot be negative") } } if app.RestartCond != "" { if !app.IsService() { return errors.New("restart-condition is only applicable to services") } } return nil } // 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. 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/._#:$-]*$`) var validAppName = regexp.MustCompile("^[a-zA-Z0-9](?:-?[a-zA-Z0-9])*$").MatchString // ValidAppName tells whether a string is a valid application name. func ValidAppName(n string) bool { return validAppName(n) } // ValidateApp verifies the content in the app info. func ValidateApp(app *AppInfo) error { switch app.Daemon { case "", "simple", "forking", "oneshot", "dbus", "notify": // valid default: return fmt.Errorf(`"daemon" field contains invalid value %q`, app.Daemon) } // Validate app name if !ValidAppName(app.Name) { return fmt.Errorf("cannot have %q as app name - use letters, digits, and dash as separator", app.Name) } // Validate the rest of the app info checks := map[string]string{ "command": app.Command, "stop-command": app.StopCommand, "reload-command": app.ReloadCommand, "post-stop-command": app.PostStopCommand, "bus-name": app.BusName, } for name, value := range checks { if err := validateField(name, value, appContentWhitelist); err != nil { return err } } // 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 { return fmt.Errorf(`"network-bind" interface plug is required when sockets are used`) } } for _, socket := range app.Sockets { if err := validateAppSocket(socket); err != nil { return fmt.Errorf("invalid definition of socket %q: %v", socket.Name, err) } } if err := validateAppRestart(app); err != nil { return err } if err := validateAppOrderNames(app, app.Before); err != nil { return err } if err := validateAppOrderNames(app, app.After); err != nil { return err } if err := validateAppWatchdog(app); err != nil { return err } // validate stop-mode if err := app.StopMode.Validate(); err != nil { return err } // validate refresh-mode switch app.RefreshMode { case "", "endure", "restart": // valid default: return fmt.Errorf(`"refresh-mode" field contains invalid value %q`, app.RefreshMode) } if app.StopMode != "" && app.Daemon == "" { return fmt.Errorf(`"stop-mode" cannot be used for %q, only for services`, app.Name) } if app.RefreshMode != "" && app.Daemon == "" { return fmt.Errorf(`"refresh-mode" cannot be used for %q, only for services`, app.Name) } return validateAppTimer(app) } // ValidatePathVariables ensures that given path contains only $SNAP, $SNAP_DATA or $SNAP_COMMON. func ValidatePathVariables(path string) error { for path != "" { start := strings.IndexRune(path, '$') if start < 0 { break } path = path[start+1:] end := strings.IndexFunc(path, func(c rune) bool { return (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && c != '_' }) if end < 0 { end = len(path) } v := path[:end] if v != "SNAP" && v != "SNAP_DATA" && v != "SNAP_COMMON" { return fmt.Errorf("reference to unknown variable %q", "$"+v) } path = path[end:] } return nil } func isAbsAndClean(path string) bool { return (filepath.IsAbs(path) || strings.HasPrefix(path, "$")) && filepath.Clean(path) == path } // LayoutConstraint abstracts validation of conflicting layout elements. type LayoutConstraint interface { IsOffLimits(path string) bool } // mountedTree represents a mounted file-system tree or a bind-mounted directory. type mountedTree string // IsOffLimits returns true if the mount point is (perhaps non-proper) prefix of a given path. func (mountPoint mountedTree) IsOffLimits(path string) bool { return strings.HasPrefix(path, string(mountPoint)+"/") || path == string(mountPoint) } // mountedFile represents a bind-mounted file. type mountedFile string // IsOffLimits returns true if the mount point is (perhaps non-proper) prefix of a given path. func (mountPoint mountedFile) IsOffLimits(path string) bool { return strings.HasPrefix(path, string(mountPoint)+"/") || path == string(mountPoint) } // symlinkFile represents a layout using symbolic link. type symlinkFile string // IsOffLimits returns true for mounted files if a path is identical to the path of the mount point. func (mountPoint symlinkFile) IsOffLimits(path string) bool { return strings.HasPrefix(path, string(mountPoint)+"/") || path == string(mountPoint) } func (layout *Layout) constraint() LayoutConstraint { path := layout.Snap.ExpandSnapVariables(layout.Path) if layout.Symlink != "" { return symlinkFile(path) } else if layout.BindFile != "" { return mountedFile(path) } return mountedTree(path) } // ValidateLayout ensures that the given layout contains only valid subset of constructs. func ValidateLayout(layout *Layout, constraints []LayoutConstraint) error { si := layout.Snap // Rules for validating layouts: // // * source of mount --bind must be in on of $SNAP, $SNAP_DATA or $SNAP_COMMON // * target of symlink must in in one of $SNAP, $SNAP_DATA, or $SNAP_COMMON // * may not mount on top of an existing layout mountpoint mountPoint := layout.Path if mountPoint == "" { return errors.New("layout cannot use an empty path") } if err := ValidatePathVariables(mountPoint); err != nil { return fmt.Errorf("layout %q uses invalid mount point: %s", layout.Path, err) } mountPoint = si.ExpandSnapVariables(mountPoint) if !isAbsAndClean(mountPoint) { return fmt.Errorf("layout %q uses invalid mount point: must be absolute and clean", layout.Path) } for _, path := range []string{"/proc", "/sys", "/dev", "/run", "/boot", "/lost+found", "/media", "/var/lib/snapd", "/var/snap"} { // We use the mountedTree constraint as this has the right semantics. if mountedTree(path).IsOffLimits(mountPoint) { return fmt.Errorf("layout %q in an off-limits area", layout.Path) } } for _, constraint := range constraints { if constraint.IsOffLimits(mountPoint) { return fmt.Errorf("layout %q underneath prior layout item %q", layout.Path, constraint) } } var nused int if layout.Bind != "" { nused++ } if layout.BindFile != "" { nused++ } if layout.Type != "" { nused++ } if layout.Symlink != "" { nused++ } if nused != 1 { return fmt.Errorf("layout %q must define a bind mount, a filesystem mount or a symlink", layout.Path) } if layout.Bind != "" || layout.BindFile != "" { mountSource := layout.Bind + layout.BindFile if err := ValidatePathVariables(mountSource); err != nil { return fmt.Errorf("layout %q uses invalid bind mount source %q: %s", layout.Path, mountSource, err) } mountSource = si.ExpandSnapVariables(mountSource) if !isAbsAndClean(mountSource) { return fmt.Errorf("layout %q uses invalid bind mount source %q: must be absolute and clean", layout.Path, mountSource) } // Bind mounts *must* use $SNAP, $SNAP_DATA or $SNAP_COMMON as bind // mount source. This is done so that snaps cannot bypass restrictions // by mounting something outside into their own space. if !strings.HasPrefix(mountSource, si.ExpandSnapVariables("$SNAP")) && !strings.HasPrefix(mountSource, si.ExpandSnapVariables("$SNAP_DATA")) && !strings.HasPrefix(mountSource, si.ExpandSnapVariables("$SNAP_COMMON")) { return fmt.Errorf("layout %q uses invalid bind mount source %q: must start with $SNAP, $SNAP_DATA or $SNAP_COMMON", layout.Path, mountSource) } } switch layout.Type { case "tmpfs": case "": // nothing to do default: return fmt.Errorf("layout %q uses invalid filesystem %q", layout.Path, layout.Type) } if layout.Symlink != "" { oldname := layout.Symlink if err := ValidatePathVariables(oldname); err != nil { return fmt.Errorf("layout %q uses invalid symlink old name %q: %s", layout.Path, oldname, err) } oldname = si.ExpandSnapVariables(oldname) if !isAbsAndClean(oldname) { return fmt.Errorf("layout %q uses invalid symlink old name %q: must be absolute and clean", layout.Path, oldname) } // Symlinks *must* use $SNAP, $SNAP_DATA or $SNAP_COMMON as oldname. // This is done so that snaps cannot attempt to bypass restrictions // by mounting something outside into their own space. if !strings.HasPrefix(oldname, si.ExpandSnapVariables("$SNAP")) && !strings.HasPrefix(oldname, si.ExpandSnapVariables("$SNAP_DATA")) && !strings.HasPrefix(oldname, si.ExpandSnapVariables("$SNAP_COMMON")) { return fmt.Errorf("layout %q uses invalid symlink old name %q: must start with $SNAP, $SNAP_DATA or $SNAP_COMMON", layout.Path, oldname) } } // When new users and groups are supported those must be added to interfaces/mount/spec.go as well. // For now only "root" is allowed (and default). switch layout.User { case "root", "": // TODO: allow declared snap user and group names. default: return fmt.Errorf("layout %q uses invalid user %q", layout.Path, layout.User) } switch layout.Group { case "root", "": default: return fmt.Errorf("layout %q uses invalid group %q", layout.Path, layout.Group) } if layout.Mode&01777 != layout.Mode { return fmt.Errorf("layout %q uses invalid mode %#o", layout.Path, layout.Mode) } return nil } func ValidateCommonIDs(info *Info) error { seen := make(map[string]string, len(info.Apps)) for _, app := range info.Apps { if app.CommonID != "" { if other, was := seen[app.CommonID]; was { return fmt.Errorf("application %q common-id %q must be unique, already used by application %q", app.Name, app.CommonID, other) } seen[app.CommonID] = app.Name } } return nil } snapd-2.37.4~14.04.1/snap/snaptest/0000775000000000000000000000000013435556260013343 5ustar snapd-2.37.4~14.04.1/snap/snaptest/snaptest_test.go0000664000000000000000000001542413435556260016600 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package snaptest_test import ( "os" "path/filepath" "testing" . "gopkg.in/check.v1" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snaptest" "github.com/snapcore/snapd/testutil" ) func TestSnapTest(t *testing.T) { TestingT(t) } const sampleYaml = ` name: sample version: 1 apps: app: command: foo plugs: network: interface: network ` type snapTestSuite struct{} var _ = Suite(&snapTestSuite{}) func (s *snapTestSuite) SetUpTest(c *C) { dirs.SetRootDir(c.MkDir()) } func (s *snapTestSuite) TearDownTest(c *C) { dirs.SetRootDir("") } func (s *snapTestSuite) TestMockSnap(c *C) { snapInfo := snaptest.MockSnap(c, sampleYaml, &snap.SideInfo{Revision: snap.R(42)}) // Data from YAML is used c.Check(snapInfo.InstanceName(), Equals, "sample") // Data from SideInfo is used c.Check(snapInfo.Revision, Equals, snap.R(42)) // The YAML is placed on disk c.Check(filepath.Join(dirs.SnapMountDir, "sample", "42", "meta", "snap.yaml"), testutil.FileEquals, sampleYaml) // More c.Check(snapInfo.Apps["app"].Command, Equals, "foo") c.Check(snapInfo.Plugs["network"].Interface, Equals, "network") } func (s *snapTestSuite) TestMockSnapInstance(c *C) { snapInfo := snaptest.MockSnapInstance(c, "sample_instance", sampleYaml, &snap.SideInfo{Revision: snap.R(42)}) // Data from YAML and parameters is used c.Check(snapInfo.InstanceName(), Equals, "sample_instance") c.Check(snapInfo.SnapName(), Equals, "sample") c.Check(snapInfo.InstanceKey, Equals, "instance") // Data from SideInfo is used c.Check(snapInfo.Revision, Equals, snap.R(42)) // The YAML is placed on disk c.Check(filepath.Join(dirs.SnapMountDir, "sample_instance", "42", "meta", "snap.yaml"), testutil.FileEquals, sampleYaml) // More c.Check(snapInfo.Apps["app"].Command, Equals, "foo") c.Check(snapInfo.Plugs["network"].Interface, Equals, "network") } func (s *snapTestSuite) TestMockSnapCurrent(c *C) { snapInfo := snaptest.MockSnapCurrent(c, sampleYaml, &snap.SideInfo{Revision: snap.R(42)}) // Data from YAML is used c.Check(snapInfo.InstanceName(), Equals, "sample") // Data from SideInfo is used c.Check(snapInfo.Revision, Equals, snap.R(42)) // The YAML is placed on disk c.Check(filepath.Join(dirs.SnapMountDir, "sample", "42", "meta", "snap.yaml"), testutil.FileEquals, sampleYaml) link, err := os.Readlink(filepath.Join(dirs.SnapMountDir, "sample", "current")) c.Check(err, IsNil) c.Check(link, Equals, "42") } func (s *snapTestSuite) TestMockSnapInstanceCurrent(c *C) { snapInfo := snaptest.MockSnapInstanceCurrent(c, "sample_instance", sampleYaml, &snap.SideInfo{Revision: snap.R(42)}) // Data from YAML and parameters is used c.Check(snapInfo.InstanceName(), Equals, "sample_instance") c.Check(snapInfo.SnapName(), Equals, "sample") c.Check(snapInfo.InstanceKey, Equals, "instance") // Data from SideInfo is used c.Check(snapInfo.Revision, Equals, snap.R(42)) // The YAML is placed on disk c.Check(filepath.Join(dirs.SnapMountDir, "sample_instance", "42", "meta", "snap.yaml"), testutil.FileEquals, sampleYaml) link, err := os.Readlink(filepath.Join(dirs.SnapMountDir, "sample_instance", "current")) c.Check(err, IsNil) c.Check(link, Equals, filepath.Join(dirs.SnapMountDir, "sample_instance", "42")) } func (s *snapTestSuite) TestMockInfo(c *C) { snapInfo := snaptest.MockInfo(c, sampleYaml, &snap.SideInfo{Revision: snap.R(42)}) // Data from YAML is used c.Check(snapInfo.InstanceName(), Equals, "sample") // Data from SideInfo is used c.Check(snapInfo.Revision, Equals, snap.R(42)) // The YAML is *not* placed on disk _, err := os.Stat(filepath.Join(dirs.SnapMountDir, "sample", "42", "meta", "snap.yaml")) c.Assert(os.IsNotExist(err), Equals, true) // More c.Check(snapInfo.Apps["app"].Command, Equals, "foo") c.Check(snapInfo.Plugs["network"].Interface, Equals, "network") } func (s *snapTestSuite) TestMockInvalidInfo(c *C) { snapInfo := snaptest.MockInvalidInfo(c, sampleYaml+"\nslots:\n network:\n", &snap.SideInfo{Revision: snap.R(42)}) // Data from YAML is used c.Check(snapInfo.InstanceName(), Equals, "sample") // Data from SideInfo is used c.Check(snapInfo.Revision, Equals, snap.R(42)) // The YAML is *not* placed on disk _, err := os.Stat(filepath.Join(dirs.SnapMountDir, "sample", "42", "meta", "snap.yaml")) c.Assert(os.IsNotExist(err), Equals, true) // More c.Check(snapInfo.Apps["app"].Command, Equals, "foo") c.Check(snapInfo.Plugs["network"].Interface, Equals, "network") c.Check(snapInfo.Slots["network"].Interface, Equals, "network") // They info object is not valid c.Check(snap.Validate(snapInfo), ErrorMatches, `cannot have plug and slot with the same name: "network"`) } func (s *snapTestSuite) TestRenameSlot(c *C) { snapInfo := snaptest.MockInfo(c, `name: core version: 0 slots: old: interface: interface slot: interface: interface plugs: plug: interface: interface apps: app: hooks: configure: `, nil) // Rename "old" to "plug" err := snaptest.RenameSlot(snapInfo, "old", "plug") c.Assert(err, ErrorMatches, `cannot rename slot "old" to "plug": existing plug with that name`) // Rename "old" to "slot" err = snaptest.RenameSlot(snapInfo, "old", "slot") c.Assert(err, ErrorMatches, `cannot rename slot "old" to "slot": existing slot with that name`) // Rename "old" to "bad name" err = snaptest.RenameSlot(snapInfo, "old", "bad name") c.Assert(err, ErrorMatches, `cannot rename slot "old" to "bad name": invalid slot name: "bad name"`) // Rename "old" to "new" err = snaptest.RenameSlot(snapInfo, "old", "new") c.Assert(err, IsNil) // Check that there's no trace of the old slot name. c.Assert(snapInfo.Slots["old"], IsNil) c.Assert(snapInfo.Slots["new"], NotNil) c.Assert(snapInfo.Slots["new"].Name, Equals, "new") c.Assert(snapInfo.Apps["app"].Slots["old"], IsNil) c.Assert(snapInfo.Apps["app"].Slots["new"], DeepEquals, snapInfo.Slots["new"]) c.Assert(snapInfo.Hooks["configure"].Slots["old"], IsNil) c.Assert(snapInfo.Hooks["configure"].Slots["new"], DeepEquals, snapInfo.Slots["new"]) // Rename "old" to "new" (again) err = snaptest.RenameSlot(snapInfo, "old", "new") c.Assert(err, ErrorMatches, `cannot rename slot "old" to "new": no such slot`) } snapd-2.37.4~14.04.1/snap/snaptest/snaptest.go0000664000000000000000000002107713435556260015542 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ // Package snaptest contains helper functions for mocking snaps. package snaptest import ( "fmt" "io/ioutil" "os" "path/filepath" "gopkg.in/check.v1" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/pack" ) func mockSnap(c *check.C, instanceName, yamlText string, sideInfo *snap.SideInfo) *snap.Info { c.Assert(sideInfo, check.Not(check.IsNil)) restoreSanitize := snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}) defer restoreSanitize() // Parse the yaml (we need the Name). snapInfo, err := snap.InfoFromSnapYaml([]byte(yamlText)) c.Assert(err, check.IsNil) // Set SideInfo so that we can use MountDir below snapInfo.SideInfo = *sideInfo if instanceName != "" { // Set the snap instance name snapName, instanceKey := snap.SplitInstanceName(instanceName) snapInfo.InstanceKey = instanceKey // Make sure snap name/instance name checks out c.Assert(snapInfo.InstanceName(), check.Equals, instanceName) c.Assert(snapInfo.SnapName(), check.Equals, snapName) } // Put the YAML on disk, in the right spot. metaDir := filepath.Join(snapInfo.MountDir(), "meta") err = os.MkdirAll(metaDir, 0755) c.Assert(err, check.IsNil) err = ioutil.WriteFile(filepath.Join(metaDir, "snap.yaml"), []byte(yamlText), 0644) c.Assert(err, check.IsNil) // Write the .snap to disk err = os.MkdirAll(filepath.Dir(snapInfo.MountFile()), 0755) c.Assert(err, check.IsNil) snapContents := fmt.Sprintf("%s-%s-%s", sideInfo.RealName, sideInfo.SnapID, sideInfo.Revision) err = ioutil.WriteFile(snapInfo.MountFile(), []byte(snapContents), 0644) c.Assert(err, check.IsNil) snapInfo.Size = int64(len(snapContents)) return snapInfo } // MockSnap puts a snap.yaml file on disk so to mock an installed snap, based on the provided arguments. // // The caller is responsible for mocking root directory with dirs.SetRootDir() // and for altering the overlord state if required. func MockSnap(c *check.C, yamlText string, sideInfo *snap.SideInfo) *snap.Info { return mockSnap(c, "", yamlText, sideInfo) } // MockSnapInstance puts a snap.yaml file on disk so to mock an installed snap // instance, based on the provided arguments. // // The caller is responsible for mocking root directory with dirs.SetRootDir() // and for altering the overlord state if required. func MockSnapInstance(c *check.C, instanceName, yamlText string, sideInfo *snap.SideInfo) *snap.Info { return mockSnap(c, instanceName, yamlText, sideInfo) } // MockSnapCurrent does the same as MockSnap but additionally creates the // 'current' symlink. // // The caller is responsible for mocking root directory with dirs.SetRootDir() // and for altering the overlord state if required. func MockSnapCurrent(c *check.C, yamlText string, sideInfo *snap.SideInfo) *snap.Info { si := MockSnap(c, yamlText, sideInfo) err := os.Symlink(filepath.Base(si.MountDir()), filepath.Join(si.MountDir(), "../current")) c.Assert(err, check.IsNil) return si } // MockSnapInstanceCurrent does the same as MockSnapInstance but additionally // creates the 'current' symlink. // // The caller is responsible for mocking root directory with dirs.SetRootDir() // and for altering the overlord state if required. func MockSnapInstanceCurrent(c *check.C, instanceName, yamlText string, sideInfo *snap.SideInfo) *snap.Info { si := MockSnapInstance(c, instanceName, yamlText, sideInfo) err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) c.Assert(err, check.IsNil) return si } // MockInfo parses the given snap.yaml text and returns a validated snap.Info object including the optional SideInfo. // // The result is just kept in memory, there is nothing kept on disk. If that is // desired please use MockSnap instead. func MockInfo(c *check.C, yamlText string, sideInfo *snap.SideInfo) *snap.Info { if sideInfo == nil { sideInfo = &snap.SideInfo{} } restoreSanitize := snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}) defer restoreSanitize() snapInfo, err := snap.InfoFromSnapYaml([]byte(yamlText)) c.Assert(err, check.IsNil) snapInfo.SideInfo = *sideInfo err = snap.Validate(snapInfo) c.Assert(err, check.IsNil) return snapInfo } // MockInvalidInfo parses the given snap.yaml text and returns the snap.Info object including the optional SideInfo. // // The result is just kept in memory, there is nothing kept on disk. If that is // desired please use MockSnap instead. func MockInvalidInfo(c *check.C, yamlText string, sideInfo *snap.SideInfo) *snap.Info { if sideInfo == nil { sideInfo = &snap.SideInfo{} } restoreSanitize := snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}) defer restoreSanitize() snapInfo, err := snap.InfoFromSnapYaml([]byte(yamlText)) c.Assert(err, check.IsNil) snapInfo.SideInfo = *sideInfo err = snap.Validate(snapInfo) c.Assert(err, check.NotNil) return snapInfo } // PopulateDir populates the directory with files specified as pairs of relative file path and its content. Useful to add extra files to a snap. func PopulateDir(dir string, files [][]string) { for _, filenameAndContent := range files { filename := filenameAndContent[0] content := filenameAndContent[1] fpath := filepath.Join(dir, filename) err := os.MkdirAll(filepath.Dir(fpath), 0755) if err != nil { panic(err) } err = ioutil.WriteFile(fpath, []byte(content), 0755) if err != nil { panic(err) } } } // MakeTestSnapWithFiles makes a squashfs snap file with the given // snap.yaml content and optional extras files specified as pairs of // relative file path and its content. func MakeTestSnapWithFiles(c *check.C, snapYamlContent string, files [][]string) (snapFilePath string) { tmpdir := c.MkDir() snapSource := filepath.Join(tmpdir, "snapsrc") err := os.MkdirAll(filepath.Join(snapSource, "meta"), 0755) if err != nil { panic(err) } snapYamlFn := filepath.Join(snapSource, "meta", "snap.yaml") err = ioutil.WriteFile(snapYamlFn, []byte(snapYamlContent), 0644) if err != nil { panic(err) } PopulateDir(snapSource, files) restoreSanitize := snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}) defer restoreSanitize() err = osutil.ChDir(snapSource, func() error { var err error snapFilePath, err = pack.Snap(snapSource, "", "") return err }) if err != nil { panic(err) } return filepath.Join(snapSource, snapFilePath) } // MustParseChannel parses a string representing a store channel and // includes the given architecture, if architecture is "" the system // architecture is included. It panics on error. func MustParseChannel(s string, architecture string) snap.Channel { c, err := snap.ParseChannel(s, architecture) if err != nil { panic(err) } return c } // RenameSlot renames gives an existing slot a new name. // // The new slot name cannot clash with an existing plug or slot and must // be a valid slot name. func RenameSlot(snapInfo *snap.Info, oldName, newName string) error { if snapInfo.Slots[oldName] == nil { return fmt.Errorf("cannot rename slot %q to %q: no such slot", oldName, newName) } if err := snap.ValidateSlotName(newName); err != nil { return fmt.Errorf("cannot rename slot %q to %q: %s", oldName, newName, err) } if oldName == newName { return nil } if snapInfo.Slots[newName] != nil { return fmt.Errorf("cannot rename slot %q to %q: existing slot with that name", oldName, newName) } if snapInfo.Plugs[newName] != nil { return fmt.Errorf("cannot rename slot %q to %q: existing plug with that name", oldName, newName) } // Rename the slot. slotInfo := snapInfo.Slots[oldName] snapInfo.Slots[newName] = slotInfo delete(snapInfo.Slots, oldName) slotInfo.Name = newName // Update references to the slot in all applications and hooks. for _, appInfo := range snapInfo.Apps { if _, ok := appInfo.Slots[oldName]; ok { delete(appInfo.Slots, oldName) appInfo.Slots[newName] = slotInfo } } for _, hookInfo := range snapInfo.Hooks { if _, ok := hookInfo.Slots[oldName]; ok { delete(hookInfo.Slots, oldName) hookInfo.Slots[newName] = slotInfo } } return nil } snapd-2.37.4~14.04.1/snap/channel_test.go0000664000000000000000000001254113435556260014503 0ustar // -*- 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 . * */ package snap_test import ( . "gopkg.in/check.v1" "github.com/snapcore/snapd/arch" "github.com/snapcore/snapd/snap" ) type storeChannelSuite struct{} var _ = Suite(&storeChannelSuite{}) func (s storeChannelSuite) TestParseChannel(c *C) { ch, err := snap.ParseChannel("stable", "") c.Assert(err, IsNil) c.Check(ch, DeepEquals, snap.Channel{ Architecture: arch.UbuntuArchitecture(), Name: "stable", Track: "", Risk: "stable", Branch: "", }) ch, err = snap.ParseChannel("latest/stable", "") c.Assert(err, IsNil) c.Check(ch, DeepEquals, snap.Channel{ Architecture: arch.UbuntuArchitecture(), Name: "stable", Track: "", Risk: "stable", Branch: "", }) ch, err = snap.ParseChannel("1.0/edge", "") c.Assert(err, IsNil) c.Check(ch, DeepEquals, snap.Channel{ Architecture: arch.UbuntuArchitecture(), Name: "1.0/edge", Track: "1.0", Risk: "edge", Branch: "", }) ch, err = snap.ParseChannel("1.0", "") c.Assert(err, IsNil) c.Check(ch, DeepEquals, snap.Channel{ Architecture: arch.UbuntuArchitecture(), Name: "1.0/stable", Track: "1.0", Risk: "stable", Branch: "", }) ch, err = snap.ParseChannel("1.0/beta/foo", "") c.Assert(err, IsNil) c.Check(ch, DeepEquals, snap.Channel{ Architecture: arch.UbuntuArchitecture(), Name: "1.0/beta/foo", Track: "1.0", Risk: "beta", Branch: "foo", }) ch, err = snap.ParseChannel("candidate/foo", "") c.Assert(err, IsNil) c.Check(ch, DeepEquals, snap.Channel{ Architecture: arch.UbuntuArchitecture(), Name: "candidate/foo", Track: "", Risk: "candidate", Branch: "foo", }) ch, err = snap.ParseChannel("candidate/foo", "other-arch") c.Assert(err, IsNil) c.Check(ch, DeepEquals, snap.Channel{ Architecture: "other-arch", Name: "candidate/foo", Track: "", Risk: "candidate", Branch: "foo", }) } func (s storeChannelSuite) TestClean(c *C) { ch := snap.Channel{ Architecture: "arm64", Track: "latest", Name: "latest/stable", Risk: "stable", } cleanedCh := ch.Clean() c.Check(cleanedCh, Not(DeepEquals), c) c.Check(cleanedCh, DeepEquals, snap.Channel{ Architecture: "arm64", Track: "", Name: "stable", Risk: "stable", }) } func (s storeChannelSuite) TestParseChannelErrors(c *C) { _, err := snap.ParseChannel("", "") c.Check(err, ErrorMatches, "channel name cannot be empty") _, err = snap.ParseChannel("1.0////", "") c.Check(err, ErrorMatches, "channel name has too many components: 1.0////") _, err = snap.ParseChannel("1.0/cand", "invalid risk in channel name: 1.0/cand") c.Check(err, ErrorMatches, "invalid risk in channel name: 1.0/cand") } func (s *storeChannelSuite) TestString(c *C) { tests := []struct { channel string str string }{ {"stable", "stable"}, {"latest/stable", "stable"}, {"1.0/edge", "1.0/edge"}, {"1.0/beta/foo", "1.0/beta/foo"}, {"1.0", "1.0/stable"}, {"candidate/foo", "candidate/foo"}, } for _, t := range tests { ch, err := snap.ParseChannel(t.channel, "") c.Assert(err, IsNil) c.Check(ch.String(), Equals, t.str) } } func (s *storeChannelSuite) TestFull(c *C) { tests := []struct { channel string str string }{ {"stable", "latest/stable"}, {"latest/stable", "latest/stable"}, {"1.0/edge", "1.0/edge"}, {"1.0/beta/foo", "1.0/beta/foo"}, {"1.0", "1.0/stable"}, {"candidate/foo", "latest/candidate/foo"}, } for _, t := range tests { ch, err := snap.ParseChannel(t.channel, "") c.Assert(err, IsNil) c.Check(ch.Full(), Equals, t.str) } } func (s *storeChannelSuite) TestMatch(c *C) { tests := []struct { req string c1 string sameArch bool res string }{ {"stable", "stable", true, "architecture:track:risk"}, {"stable", "beta", true, "architecture:track"}, {"beta", "stable", true, "architecture:track:risk"}, {"stable", "edge", false, "track"}, {"edge", "stable", false, "track:risk"}, {"1.0/stable", "1.0/edge", true, "architecture:track"}, {"1.0/edge", "stable", true, "architecture:risk"}, {"1.0/edge", "stable", false, "risk"}, {"1.0/stable", "stable", false, "risk"}, {"1.0/stable", "beta", false, ""}, {"1.0/stable", "2.0/beta", false, ""}, {"2.0/stable", "2.0/beta", false, "track"}, {"1.0/stable", "2.0/beta", true, "architecture"}, } for _, t := range tests { reqArch := "amd64" c1Arch := "amd64" if !t.sameArch { c1Arch = "arm64" } req, err := snap.ParseChannel(t.req, reqArch) c.Assert(err, IsNil) c1, err := snap.ParseChannel(t.c1, c1Arch) c.Assert(err, IsNil) c.Check(req.Match(&c1).String(), Equals, t.res) } } snapd-2.37.4~14.04.1/snap/restartcond.go0000664000000000000000000000413513435556260014364 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package snap import ( "errors" ) // RestartCondition encapsulates the different systemd 'restart' options type RestartCondition string // These are the supported restart conditions const ( RestartNever RestartCondition = "never" RestartOnSuccess RestartCondition = "on-success" RestartOnFailure RestartCondition = "on-failure" RestartOnAbnormal RestartCondition = "on-abnormal" RestartOnAbort RestartCondition = "on-abort" RestartOnWatchdog RestartCondition = "on-watchdog" RestartAlways RestartCondition = "always" ) var RestartMap = map[string]RestartCondition{ "no": RestartNever, "never": RestartNever, "on-success": RestartOnSuccess, "on-failure": RestartOnFailure, "on-abnormal": RestartOnAbnormal, "on-abort": RestartOnAbort, "on-watchdog": RestartOnWatchdog, "always": RestartAlways, } // ErrUnknownRestartCondition is returned when trying to unmarshal an unknown restart condition var ErrUnknownRestartCondition = errors.New("invalid restart condition") func (rc RestartCondition) String() string { if rc == "never" { return "no" } return string(rc) } // UnmarshalYAML so RestartCondition implements yaml's Unmarshaler interface func (rc *RestartCondition) UnmarshalYAML(unmarshal func(interface{}) error) error { var v string if err := unmarshal(&v); err != nil { return err } nrc, ok := RestartMap[v] if !ok { return ErrUnknownRestartCondition } *rc = nrc return nil } snapd-2.37.4~14.04.1/snap/container_test.go0000664000000000000000000002630313435556260015056 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package snap_test import ( "io/ioutil" "os" "path/filepath" "syscall" . "gopkg.in/check.v1" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snapdir" "github.com/snapcore/snapd/testutil" ) type FileSuite struct{} var _ = Suite(&FileSuite{}) func (s *FileSuite) TestFileOpenForSnapDir(c *C) { sd := c.MkDir() snapYaml := filepath.Join(sd, "meta", "snap.yaml") err := os.MkdirAll(filepath.Dir(snapYaml), 0755) c.Assert(err, IsNil) err = ioutil.WriteFile(snapYaml, []byte(`name: foo`), 0644) c.Assert(err, IsNil) f, err := snap.Open(sd) c.Assert(err, IsNil) c.Assert(f, FitsTypeOf, &snapdir.SnapDir{}) } func (s *FileSuite) TestFileOpenForSnapDirErrors(c *C) { _, err := snap.Open(c.MkDir()) c.Assert(err, FitsTypeOf, snap.NotSnapError{}) c.Assert(err, ErrorMatches, `"/.*" is not a snap or snapdir`) } type validateSuite struct { testutil.BaseTest log func(string, ...interface{}) } var _ = Suite(&validateSuite{}) func discard(string, ...interface{}) {} func (s *validateSuite) SetUpTest(c *C) { s.BaseTest.SetUpTest(c) s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})) } func (s *validateSuite) TearDownTest(c *C) { s.BaseTest.TearDownTest(c) } func (s *validateSuite) TestValidateContainerReallyEmptyFails(c *C) { const yaml = `name: empty-snap version: 1 ` d := c.MkDir() // the snap dir is a 0700 directory with nothing in it info, err := snap.InfoFromSnapYaml([]byte(yaml)) c.Assert(err, IsNil) err = snap.ValidateContainer(snapdir.New(d), info, discard) c.Check(err, Equals, snap.ErrMissingPaths) } func (s *validateSuite) TestValidateContainerEmptyButBadPermFails(c *C) { const yaml = `name: empty-snap version: 1 ` d := c.MkDir() stat, err := os.Stat(d) c.Assert(err, IsNil) c.Check(stat.Mode().Perm(), Equals, os.FileMode(0700)) // just to be sure c.Assert(os.Mkdir(filepath.Join(d, "meta"), 0755), IsNil) c.Assert(ioutil.WriteFile(filepath.Join(d, "meta", "snap.yaml"), nil, 0444), IsNil) // snapdir has /meta/snap.yaml, but / is 0700 info, err := snap.InfoFromSnapYaml([]byte(yaml)) c.Assert(err, IsNil) err = snap.ValidateContainer(snapdir.New(d), info, discard) c.Check(err, Equals, snap.ErrBadModes) } func (s *validateSuite) TestValidateContainerMissingSnapYamlFails(c *C) { const yaml = `name: empty-snap version: 1 ` d := c.MkDir() c.Assert(os.Chmod(d, 0755), IsNil) c.Assert(os.Mkdir(filepath.Join(d, "meta"), 0755), IsNil) // snapdir's / and /meta are 0755 (i.e. OK), but no /meta/snap.yaml info, err := snap.InfoFromSnapYaml([]byte(yaml)) c.Assert(err, IsNil) err = snap.ValidateContainer(snapdir.New(d), info, discard) c.Check(err, Equals, snap.ErrMissingPaths) } func (s *validateSuite) TestValidateContainerSnapYamlBadPermsFails(c *C) { const yaml = `name: empty-snap version: 1 ` d := c.MkDir() c.Assert(os.Chmod(d, 0755), IsNil) c.Assert(os.Mkdir(filepath.Join(d, "meta"), 0755), IsNil) c.Assert(ioutil.WriteFile(filepath.Join(d, "meta", "snap.yaml"), nil, 0), IsNil) // snapdir's / and /meta are 0755 (i.e. OK), // /meta/snap.yaml exists, but isn't readable info, err := snap.InfoFromSnapYaml([]byte(yaml)) c.Assert(err, IsNil) err = snap.ValidateContainer(snapdir.New(d), info, discard) c.Check(err, Equals, snap.ErrBadModes) } func (s *validateSuite) TestValidateContainerSnapYamlNonRegularFails(c *C) { const yaml = `name: empty-snap version: 1 ` d := c.MkDir() c.Assert(os.Chmod(d, 0755), IsNil) c.Assert(os.Mkdir(filepath.Join(d, "meta"), 0755), IsNil) c.Assert(syscall.Mkfifo(filepath.Join(d, "meta", "snap.yaml"), 0444), IsNil) // snapdir's / and /meta are 0755 (i.e. OK), // /meta/snap.yaml exists, is readable, but isn't a file info, err := snap.InfoFromSnapYaml([]byte(yaml)) c.Assert(err, IsNil) err = snap.ValidateContainer(snapdir.New(d), info, discard) c.Check(err, Equals, snap.ErrBadModes) } // emptyContainer returns a minimal container that passes // ValidateContainer: / and /meta exist and are 0755, and // /meta/snap.yaml is a regular world-readable file. func emptyContainer(c *C) *snapdir.SnapDir { d := c.MkDir() c.Assert(os.Chmod(d, 0755), IsNil) c.Assert(os.Mkdir(filepath.Join(d, "meta"), 0755), IsNil) c.Assert(ioutil.WriteFile(filepath.Join(d, "meta", "snap.yaml"), nil, 0444), IsNil) return snapdir.New(d) } func (s *validateSuite) TestValidateContainerMinimalOKPermWorks(c *C) { const yaml = `name: empty-snap version: 1 ` d := emptyContainer(c) // snapdir's / and /meta are 0755 (i.e. OK), // /meta/snap.yaml exists, is readable regular file // (this could be considered a test of emptyContainer) info, err := snap.InfoFromSnapYaml([]byte(yaml)) c.Assert(err, IsNil) err = snap.ValidateContainer(d, info, discard) c.Check(err, IsNil) } func (s *validateSuite) TestValidateContainerMissingAppsFails(c *C) { const yaml = `name: empty-snap version: 1 apps: foo: command: foo ` d := emptyContainer(c) // snapdir is empty: no apps info, err := snap.InfoFromSnapYaml([]byte(yaml)) c.Assert(err, IsNil) err = snap.ValidateContainer(d, info, discard) c.Check(err, Equals, snap.ErrMissingPaths) } func (s *validateSuite) TestValidateContainerBadAppPermsFails(c *C) { const yaml = `name: empty-snap version: 1 apps: foo: command: foo ` d := emptyContainer(c) c.Assert(ioutil.WriteFile(filepath.Join(d.Path(), "foo"), nil, 0444), IsNil) // snapdir contains the app, but the app is not executable info, err := snap.InfoFromSnapYaml([]byte(yaml)) c.Assert(err, IsNil) err = snap.ValidateContainer(d, info, discard) c.Check(err, Equals, snap.ErrBadModes) } func (s *validateSuite) TestValidateContainerBadAppDirPermsFails(c *C) { const yaml = `name: empty-snap version: 1 apps: foo: command: apps/foo ` d := emptyContainer(c) c.Assert(os.Mkdir(filepath.Join(d.Path(), "apps"), 0700), IsNil) c.Assert(ioutil.WriteFile(filepath.Join(d.Path(), "apps", "foo"), nil, 0555), IsNil) // snapdir contains executable app, but path to executable isn't rx info, err := snap.InfoFromSnapYaml([]byte(yaml)) c.Assert(err, IsNil) err = snap.ValidateContainer(d, info, discard) c.Check(err, Equals, snap.ErrBadModes) } func (s *validateSuite) TestValidateContainerBadSvcPermsFails(c *C) { const yaml = `name: empty-snap version: 1 apps: bar: command: svcs/bar daemon: simple ` d := emptyContainer(c) c.Assert(os.Mkdir(filepath.Join(d.Path(), "svcs"), 0755), IsNil) c.Assert(ioutil.WriteFile(filepath.Join(d.Path(), "svcs", "bar"), nil, 0), IsNil) // snapdir contains service, but it isn't executable info, err := snap.InfoFromSnapYaml([]byte(yaml)) c.Assert(err, IsNil) err = snap.ValidateContainer(d, info, discard) c.Check(err, Equals, snap.ErrBadModes) } func (s *validateSuite) TestValidateContainerCompleterFails(c *C) { const yaml = `name: empty-snap version: 1 apps: foo: command: cmds/foo completer: comp/foo.sh ` d := emptyContainer(c) c.Assert(os.Mkdir(filepath.Join(d.Path(), "cmds"), 0755), IsNil) c.Assert(ioutil.WriteFile(filepath.Join(d.Path(), "cmds", "foo"), nil, 0555), IsNil) c.Assert(os.Mkdir(filepath.Join(d.Path(), "comp"), 0755), IsNil) // snapdir contains executable app, in a rx path, but refers // to a completer that doesn't exist info, err := snap.InfoFromSnapYaml([]byte(yaml)) c.Assert(err, IsNil) err = snap.ValidateContainer(d, info, discard) c.Check(err, Equals, snap.ErrMissingPaths) } func (s *validateSuite) TestValidateContainerBadAppPathOK(c *C) { // we actually support this, but don't validate it here const yaml = `name: empty-snap version: 1 apps: foo: command: ../../../bin/echo ` d := emptyContainer(c) // snapdir does not contain the app, but the command is // "outside" so it might be OK info, err := snap.InfoFromSnapYaml([]byte(yaml)) c.Assert(err, IsNil) err = snap.ValidateContainer(d, info, discard) c.Check(err, IsNil) } func (s *validateSuite) TestValidateContainerSymlinksFails(c *C) { c.Skip("checking symlink targets not implemented yet") const yaml = `name: empty-snap version: 1 apps: foo: command: foo ` d := emptyContainer(c) fn := filepath.Join(d.Path(), "foo") c.Assert(ioutil.WriteFile(fn+".real", nil, 0444), IsNil) c.Assert(os.Symlink(fn+".real", fn), IsNil) // snapdir contains a command that's a symlink to a file that's not world-rx info, err := snap.InfoFromSnapYaml([]byte(yaml)) c.Assert(err, IsNil) err = snap.ValidateContainer(d, info, discard) c.Check(err, Equals, snap.ErrBadModes) } func (s *validateSuite) TestValidateContainerSymlinksOK(c *C) { const yaml = `name: empty-snap version: 1 apps: foo: command: foo ` d := emptyContainer(c) fn := filepath.Join(d.Path(), "foo") c.Assert(ioutil.WriteFile(fn+".real", nil, 0555), IsNil) c.Assert(os.Symlink(fn+".real", fn), IsNil) // snapdir contains a command that's a symlink to a file that's world-rx info, err := snap.InfoFromSnapYaml([]byte(yaml)) c.Assert(err, IsNil) err = snap.ValidateContainer(d, info, discard) c.Check(err, IsNil) } func (s *validateSuite) TestValidateContainerAppsOK(c *C) { const yaml = `name: empty-snap version: 1 apps: foo: command: cmds/foo completer: comp/foo.sh bar: command: svcs/bar daemon: simple baz: command: cmds/foo --with=baz quux: command: cmds/foo daemon: simple meep: command: comp/foo.sh daemon: simple ` d := emptyContainer(c) c.Assert(os.Mkdir(filepath.Join(d.Path(), "cmds"), 0755), IsNil) c.Assert(ioutil.WriteFile(filepath.Join(d.Path(), "cmds", "foo"), nil, 0555), IsNil) c.Assert(os.Mkdir(filepath.Join(d.Path(), "comp"), 0755), IsNil) c.Assert(ioutil.WriteFile(filepath.Join(d.Path(), "comp", "foo.sh"), nil, 0555), IsNil) c.Assert(os.Mkdir(filepath.Join(d.Path(), "svcs"), 0700), IsNil) c.Assert(ioutil.WriteFile(filepath.Join(d.Path(), "svcs", "bar"), nil, 0500), IsNil) c.Assert(os.Mkdir(filepath.Join(d.Path(), "garbage"), 0755), IsNil) c.Assert(os.Mkdir(filepath.Join(d.Path(), "garbage", "zero"), 0), IsNil) defer os.Chmod(filepath.Join(d.Path(), "garbage", "zero"), 0755) // snapdir contains: // * a command that's world-rx, and its directory is // world-rx, and its completer is world-r in a world-rx // directory // * a service that's root-executable, and its directory is // not readable nor searchable - and that's OK! (NOTE as // this test should pass as non-rooot, the directory is 0700 // instead of 0000) // * a command with arguments // * a service that is also a command // * a service that is also a completer (WAT) // * an extra directory only root can look at (this would fail // if not running the suite as root, and SkipDir didn't // work) info, err := snap.InfoFromSnapYaml([]byte(yaml)) c.Assert(err, IsNil) err = snap.ValidateContainer(d, info, discard) c.Check(err, IsNil) } snapd-2.37.4~14.04.1/snap/seed_yaml.go0000664000000000000000000000435113435556260013776 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2016 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 . * */ package snap import ( "fmt" "io/ioutil" "strings" "gopkg.in/yaml.v2" "github.com/snapcore/snapd/osutil" ) // SeedSnap points to a snap in the seed to install, together with // assertions (or alone if unasserted is true) it will be used to // drive the installation and ultimately set SideInfo/SnapState for it. type SeedSnap struct { Name string `yaml:"name"` // cross-reference/audit SnapID string `yaml:"snap-id,omitempty"` // bits that are orthongonal/not in assertions Channel string `yaml:"channel,omitempty"` DevMode bool `yaml:"devmode,omitempty"` Classic bool `yaml:"classic,omitempty"` Private bool `yaml:"private,omitempty"` Contact string `yaml:"contact,omitempty"` // no assertions are available in the seed for this snap Unasserted bool `yaml:"unasserted,omitempty"` File string `yaml:"file"` } type Seed struct { Snaps []*SeedSnap `yaml:"snaps"` } func ReadSeedYaml(fn string) (*Seed, error) { yamlData, err := ioutil.ReadFile(fn) if err != nil { return nil, fmt.Errorf("cannot read seed yaml: %s", fn) } var seed Seed if err := yaml.Unmarshal(yamlData, &seed); err != nil { return nil, fmt.Errorf("cannot unmarshal %q: %s", yamlData, err) } // validate for _, sn := range seed.Snaps { if strings.Contains(sn.File, "/") { return nil, fmt.Errorf("%q must be a filename, not a path", sn.File) } } return &seed, nil } func (seed *Seed) Write(seedFn string) error { data, err := yaml.Marshal(&seed) if err != nil { return err } if err := osutil.AtomicWriteFile(seedFn, data, 0644, 0); err != nil { return err } return nil } snapd-2.37.4~14.04.1/snap/validate_test.go0000664000000000000000000012764713435556260014702 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package snap_test import ( "fmt" "regexp" "strconv" "strings" . "gopkg.in/check.v1" . "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/testutil" ) type ValidateSuite struct { testutil.BaseTest } var _ = Suite(&ValidateSuite{}) func createSampleApp() *AppInfo { socket := &SocketInfo{ Name: "sock", ListenStream: "$SNAP_COMMON/socket", } app := &AppInfo{ Snap: &Info{ SideInfo: SideInfo{ RealName: "mysnap", Revision: R(20), }, }, Name: "foo", Plugs: map[string]*PlugInfo{"network-bind": {}}, Sockets: map[string]*SocketInfo{ "sock": socket, }, } socket.App = app return app } func (s *ValidateSuite) SetUpTest(c *C) { s.BaseTest.SetUpTest(c) s.BaseTest.AddCleanup(MockSanitizePlugsSlots(func(snapInfo *Info) {})) } func (s *ValidateSuite) TearDownTest(c *C) { s.BaseTest.TearDownTest(c) } func (s *ValidateSuite) TestValidateName(c *C) { validNames := []string{ "aa", "aaa", "aaaa", "a-a", "aa-a", "a-aa", "a-b-c", "a0", "a-0", "a-0a", "01game", "1-or-2", // a regexp stresser "u-94903713687486543234157734673284536758", } for _, name := range validNames { err := ValidateName(name) c.Assert(err, IsNil) } invalidNames := []string{ // name cannot be empty "", // too short (min 2 chars) "a", // names cannot be too long "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "xxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx", "1111111111111111111111111111111111111111x", "x1111111111111111111111111111111111111111", "x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x", // a regexp stresser "u-9490371368748654323415773467328453675-", // dashes alone are not a name "-", "--", // double dashes in a name are not allowed "a--a", // name should not end with a dash "a-", // name cannot have any spaces in it "a ", " a", "a a", // a number alone is not a name "0", "123", // identifier must be plain ASCII "日本語", "한글", "ру́сский язы́к", } for _, name := range invalidNames { err := ValidateName(name) c.Assert(err, ErrorMatches, `invalid snap name: ".*"`) } } func (s *ValidateSuite) TestValidateInstanceName(c *C) { validNames := []string{ // plain names are also valid instance names "aa", "aaa", "aaaa", "a-a", "aa-a", "a-aa", "a-b-c", // snap instance "foo_bar", "foo_0123456789", "01game_0123456789", "foo_1", "foo_1234abcd", } for _, name := range validNames { err := ValidateInstanceName(name) c.Assert(err, IsNil) } invalidNames := []string{ // invalid names are also invalid instance names, just a few // samples "", "a", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "xxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx", "a--a", "a-", "a ", " a", "a a", "_", "ру́сский_язы́к", } for _, name := range invalidNames { err := ValidateInstanceName(name) c.Assert(err, ErrorMatches, `invalid snap name: ".*"`) } invalidInstanceKeys := []string{ // the snap names are valid, but instance keys are not "foo_", "foo_1-23", "foo_01234567890", "foo_123_456", "foo__bar", } for _, name := range invalidInstanceKeys { err := ValidateInstanceName(name) c.Assert(err, ErrorMatches, `invalid instance key: ".*"`) } } func (s *ValidateSuite) TestValidateVersion(c *C) { validVersions := []string{ "0", "v1.0", "0.12+16.04.20160126-0ubuntu1", "1:6.0.1+r16-3", "1.0~", "1.0+", "README.~1~", "a+++++++++++++++++++++++++++++++", "AZaz:.+~-123", } for _, version := range validVersions { err := ValidateVersion(version) c.Assert(err, IsNil) } invalidVersionsTable := [][2]string{ {"~foo", `must start with an ASCII alphanumeric (and not '~')`}, {"+foo", `must start with an ASCII alphanumeric (and not '+')`}, {"foo:", `must end with an ASCII alphanumeric or one of '+' or '~' (and not ':')`}, {"foo.", `must end with an ASCII alphanumeric or one of '+' or '~' (and not '.')`}, {"foo-", `must end with an ASCII alphanumeric or one of '+' or '~' (and not '-')`}, {"horrible_underscores", `contains invalid characters: "_"`}, {"foo($bar^baz$)meep", `contains invalid characters: "($", "^", "$)"`}, {"árbol", `must be printable, non-whitespace ASCII`}, {"日本語", `must be printable, non-whitespace ASCII`}, {"한글", `must be printable, non-whitespace ASCII`}, {"ру́сский язы́к", `must be printable, non-whitespace ASCII`}, {"~foo$bar:", `must start with an ASCII alphanumeric (and not '~'),` + ` must end with an ASCII alphanumeric or one of '+' or '~' (and not ':'),` + ` and contains invalid characters: "$"`}, } for _, t := range invalidVersionsTable { version, reason := t[0], t[1] err := ValidateVersion(version) c.Assert(err, NotNil) c.Assert(err.Error(), Equals, fmt.Sprintf("invalid snap version %s: %s", strconv.QuoteToASCII(version), reason)) } // version cannot be empty c.Assert(ValidateVersion(""), ErrorMatches, `invalid snap version: cannot be empty`) // version length cannot be >32 c.Assert(ValidateVersion("this-version-is-a-little-bit-older"), ErrorMatches, `invalid snap version "this-version-is-a-little-bit-older": cannot be longer than 32 characters \(got: 34\)`) } func (s *ValidateSuite) TestValidateLicense(c *C) { validLicenses := []string{ "GPL-3.0", "(GPL-3.0)", "GPL-3.0+", "GPL-3.0 AND GPL-2.0", "GPL-3.0 OR GPL-2.0", "MIT OR (GPL-3.0 AND GPL-2.0)", "MIT OR(GPL-3.0 AND GPL-2.0)", } for _, epoch := range validLicenses { err := ValidateLicense(epoch) c.Assert(err, IsNil) } invalidLicenses := []string{ "GPL~3.0", "3.0-GPL", "(GPL-3.0", "(GPL-3.0))", "GPL-3.0++", "+GPL-3.0", "GPL-3.0 GPL-2.0", } for _, epoch := range invalidLicenses { err := ValidateLicense(epoch) c.Assert(err, NotNil) } } func (s *ValidateSuite) TestValidateHook(c *C) { validHooks := []*HookInfo{ {Name: "a"}, {Name: "aaa"}, {Name: "a-a"}, {Name: "aa-a"}, {Name: "a-aa"}, {Name: "a-b-c"}, {Name: "valid", CommandChain: []string{"valid"}}, } for _, hook := range validHooks { err := ValidateHook(hook) c.Assert(err, IsNil) } invalidHooks := []*HookInfo{ {Name: ""}, {Name: "a a"}, {Name: "a--a"}, {Name: "-a"}, {Name: "a-"}, {Name: "0"}, {Name: "123"}, {Name: "123abc"}, {Name: "日本語"}, } for _, hook := range invalidHooks { err := ValidateHook(hook) c.Assert(err, ErrorMatches, `invalid hook name: ".*"`) } invalidHooks = []*HookInfo{ {Name: "valid", CommandChain: []string{"in'valid"}}, {Name: "valid", CommandChain: []string{"in valid"}}, } for _, hook := range invalidHooks { err := ValidateHook(hook) c.Assert(err, ErrorMatches, `hook command-chain contains illegal.*`) } } // ValidateApp func (s *ValidateSuite) TestValidateAppName(c *C) { validAppNames := []string{ "1", "a", "aa", "aaa", "aaaa", "Aa", "aA", "1a", "a1", "1-a", "a-1", "a-a", "aa-a", "a-aa", "a-b-c", "0a-a", "a-0a", } for _, name := range validAppNames { c.Check(ValidateApp(&AppInfo{Name: name}), IsNil) } invalidAppNames := []string{ "", "-", "--", "a--a", "a-", "a ", " a", "a a", "日本語", "한글", "ру́сский язы́к", "ໄຂ່​ອີ​ສ​ເຕີ້", ":a", "a:", "a:a", "_a", "a_", "a_a", } for _, name := range invalidAppNames { err := ValidateApp(&AppInfo{Name: name}) c.Assert(err, ErrorMatches, `cannot have ".*" as app name.*`) } } func (s *ValidateSuite) TestValidateAppSockets(c *C) { app := createSampleApp() app.Sockets["sock"].SocketMode = 0600 c.Check(ValidateApp(app), IsNil) } func (s *ValidateSuite) TestValidateAppSocketsEmptyPermsOk(c *C) { app := createSampleApp() c.Check(ValidateApp(app), IsNil) } func (s *ValidateSuite) TestValidateAppSocketsWrongPerms(c *C) { app := createSampleApp() app.Sockets["sock"].SocketMode = 1234 err := ValidateApp(app) c.Assert(err, ErrorMatches, `invalid definition of socket "sock": cannot use mode: 2322`) } func (s *ValidateSuite) TestValidateAppSocketsMissingNetworkBindPlug(c *C) { app := createSampleApp() delete(app.Plugs, "network-bind") err := ValidateApp(app) c.Assert( err, ErrorMatches, `"network-bind" interface plug is required when sockets are used`) } func (s *ValidateSuite) TestValidateAppSocketsEmptyListenStream(c *C) { app := createSampleApp() app.Sockets["sock"].ListenStream = "" err := ValidateApp(app) c.Assert(err, ErrorMatches, `invalid definition of socket "sock": "listen-stream" is not defined`) } func (s *ValidateSuite) TestValidateAppSocketsInvalidName(c *C) { app := createSampleApp() app.Sockets["sock"].Name = "invalid name" err := ValidateApp(app) c.Assert(err, ErrorMatches, `invalid definition of socket "invalid name": invalid socket name: "invalid name"`) } func (s *ValidateSuite) TestValidateAppSocketsValidListenStreamAddresses(c *C) { app := createSampleApp() validListenAddresses := []string{ // socket paths using variables as prefix "$SNAP_DATA/my.socket", "$SNAP_COMMON/my.socket", // abstract sockets "@snap.mysnap.my.socket", // addresses and ports "1", "1023", "1024", "65535", "127.0.0.1:8080", "[::]:8080", "[::1]:8080", } socket := app.Sockets["sock"] for _, validAddress := range validListenAddresses { socket.ListenStream = validAddress err := ValidateApp(app) c.Check(err, IsNil, Commentf(validAddress)) } } func (s *ValidateSuite) TestValidateAppSocketsInvalidListenStreamPath(c *C) { app := createSampleApp() invalidListenAddresses := []string{ // socket paths out of the snap dirs "/some/path/my.socket", "/var/snap/mysnap/20/my.socket", // path is correct but has hardcoded prefix } socket := app.Sockets["sock"] for _, invalidAddress := range invalidListenAddresses { socket.ListenStream = invalidAddress err := ValidateApp(app) c.Assert(err, ErrorMatches, `invalid definition of socket "sock": invalid "listen-stream": must have a prefix of .*`) } } func (s *ValidateSuite) TestValidateAppSocketsInvalidListenStreamPathContainsDots(c *C) { app := createSampleApp() app.Sockets["sock"].ListenStream = "$SNAP/../some.path" err := ValidateApp(app) c.Assert( err, ErrorMatches, `invalid definition of socket "sock": invalid "listen-stream": "\$SNAP/../some.path" should be written as "some.path"`) } func (s *ValidateSuite) TestValidateAppSocketsInvalidListenStreamPathPrefix(c *C) { app := createSampleApp() invalidListenAddresses := []string{ "$SNAP/my.socket", // snap dir is not writable "$SOMEVAR/my.socket", } socket := app.Sockets["sock"] for _, invalidAddress := range invalidListenAddresses { socket.ListenStream = invalidAddress err := ValidateApp(app) c.Assert( err, ErrorMatches, `invalid definition of socket "sock": invalid "listen-stream": must have a prefix of \$SNAP_DATA or \$SNAP_COMMON`) } } func (s *ValidateSuite) TestValidateAppSocketsInvalidListenStreamAbstractSocket(c *C) { app := createSampleApp() invalidListenAddresses := []string{ "@snap.mysnap", "@snap.mysnap\000.foo", "@snap.notmysnap.my.socket", "@some.other.name", "@snap.myappiswrong.foo", } socket := app.Sockets["sock"] for _, invalidAddress := range invalidListenAddresses { socket.ListenStream = invalidAddress err := ValidateApp(app) c.Assert(err, ErrorMatches, `invalid definition of socket "sock": path for "listen-stream" must be prefixed with.*`) } } func (s *ValidateSuite) TestValidateAppSocketsInvalidListenStreamAddress(c *C) { app := createSampleApp() invalidListenAddresses := []string{ "10.0.1.1:8080", "[fafa::baba]:8080", "127.0.0.1\000:8080", "127.0.0.1::8080", } socket := app.Sockets["sock"] for _, invalidAddress := range invalidListenAddresses { socket.ListenStream = invalidAddress err := ValidateApp(app) c.Assert(err, ErrorMatches, `invalid definition of socket "sock": invalid "listen-stream" address ".*", must be one of: 127\.0\.0\.1, \[::1\], \[::\]`) } } func (s *ValidateSuite) TestValidateAppSocketsInvalidListenStreamPort(c *C) { app := createSampleApp() invalidPorts := []string{ "0", "66536", "-8080", "12312345345", "[::]:-123", "[::1]:3452345234", "invalid", "[::]:invalid", } socket := app.Sockets["sock"] for _, invalidPort := range invalidPorts { socket.ListenStream = invalidPort err := ValidateApp(app) c.Assert(err, ErrorMatches, `invalid definition of socket "sock": invalid "listen-stream" port number.*`) } } func (s *ValidateSuite) TestAppWhitelistSimple(c *C) { c.Check(ValidateApp(&AppInfo{Name: "foo", Command: "foo"}), IsNil) c.Check(ValidateApp(&AppInfo{Name: "foo", StopCommand: "foo"}), IsNil) c.Check(ValidateApp(&AppInfo{Name: "foo", PostStopCommand: "foo"}), IsNil) } func (s *ValidateSuite) TestAppWhitelistWithVars(c *C) { c.Check(ValidateApp(&AppInfo{Name: "foo", Command: "foo $SNAP_DATA"}), IsNil) c.Check(ValidateApp(&AppInfo{Name: "foo", StopCommand: "foo $SNAP_DATA"}), IsNil) c.Check(ValidateApp(&AppInfo{Name: "foo", PostStopCommand: "foo $SNAP_DATA"}), IsNil) } 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) { for _, t := range []struct { daemon string ok bool }{ // good {"", true}, {"simple", true}, {"forking", true}, {"oneshot", true}, {"dbus", true}, {"notify", true}, // bad {"invalid-thing", false}, } { if t.ok { c.Check(ValidateApp(&AppInfo{Name: "foo", Daemon: t.daemon}), IsNil) } else { c.Check(ValidateApp(&AppInfo{Name: "foo", Daemon: t.daemon}), ErrorMatches, fmt.Sprintf(`"daemon" field contains invalid value %q`, t.daemon)) } } } func (s *ValidateSuite) TestAppStopMode(c *C) { // check services for _, t := range []struct { stopMode StopModeType ok bool }{ // good {"", true}, {"sigterm", true}, {"sigterm-all", true}, {"sighup", true}, {"sighup-all", true}, {"sigusr1", true}, {"sigusr1-all", true}, {"sigusr2", true}, {"sigusr2-all", true}, // bad {"invalid-thing", false}, } { if t.ok { c.Check(ValidateApp(&AppInfo{Name: "foo", Daemon: "simple", StopMode: t.stopMode}), IsNil) } else { c.Check(ValidateApp(&AppInfo{Name: "foo", Daemon: "simple", StopMode: t.stopMode}), ErrorMatches, fmt.Sprintf(`"stop-mode" field contains invalid value %q`, t.stopMode)) } } // non-services cannot have a stop-mode err := ValidateApp(&AppInfo{Name: "foo", Daemon: "", StopMode: "sigterm"}) c.Check(err, ErrorMatches, `"stop-mode" cannot be used for "foo", only for services`) } func (s *ValidateSuite) TestAppRefreshMode(c *C) { // check services for _, t := range []struct { refreshMode string ok bool }{ // good {"", true}, {"endure", true}, {"restart", true}, // bad {"invalid-thing", false}, } { if t.ok { c.Check(ValidateApp(&AppInfo{Name: "foo", Daemon: "simple", RefreshMode: t.refreshMode}), IsNil) } else { c.Check(ValidateApp(&AppInfo{Name: "foo", Daemon: "simple", RefreshMode: t.refreshMode}), ErrorMatches, fmt.Sprintf(`"refresh-mode" field contains invalid value %q`, t.refreshMode)) } } // non-services cannot have a refresh-mode err := ValidateApp(&AppInfo{Name: "foo", Daemon: "", RefreshMode: "endure"}) c.Check(err, ErrorMatches, `"refresh-mode" cannot be used for "foo", only for services`) } func (s *ValidateSuite) TestAppWhitelistError(c *C) { err := ValidateApp(&AppInfo{Name: "foo", Command: "x\n"}) c.Assert(err, NotNil) c.Check(err.Error(), Equals, `app description field 'command' contains illegal "x\n" (legal: '^[A-Za-z0-9/. _#:$-]*$')`) } // Validate func (s *ValidateSuite) TestDetectIllegalYamlBinaries(c *C) { info, err := InfoFromSnapYaml([]byte(`name: foo version: 1.0 apps: tes!me: command: someething `)) c.Assert(err, IsNil) err = Validate(info) c.Check(err, NotNil) } func (s *ValidateSuite) TestDetectIllegalYamlService(c *C) { info, err := InfoFromSnapYaml([]byte(`name: foo version: 1.0 apps: tes!me: command: something daemon: forking `)) c.Assert(err, IsNil) err = Validate(info) c.Check(err, NotNil) } func (s *ValidateSuite) TestIllegalSnapName(c *C) { info, err := InfoFromSnapYaml([]byte(`name: foo.something version: 1.0 `)) c.Assert(err, IsNil) err = Validate(info) c.Check(err, ErrorMatches, `invalid snap name: "foo.something"`) } func (s *ValidateSuite) TestValidateChecksName(c *C) { info, err := InfoFromSnapYaml([]byte(` version: 1.0 `)) c.Assert(err, IsNil) err = Validate(info) c.Check(err, ErrorMatches, `snap name cannot be empty`) } func (s *ValidateSuite) TestIllegalSnapEpoch(c *C) { _, err := InfoFromSnapYaml([]byte(`name: foo version: 1.0 epoch: 0* `)) c.Assert(err, ErrorMatches, `.*invalid epoch.*`) } func (s *ValidateSuite) TestMissingSnapEpochIsOkay(c *C) { info, err := InfoFromSnapYaml([]byte(`name: foo version: 1.0 `)) c.Assert(err, IsNil) c.Assert(Validate(info), IsNil) } func (s *ValidateSuite) TestIllegalSnapLicense(c *C) { info, err := InfoFromSnapYaml([]byte(`name: foo version: 1.0 license: GPL~3.0 `)) c.Assert(err, IsNil) err = Validate(info) c.Check(err, ErrorMatches, `cannot validate license "GPL~3.0": unknown license: GPL~3.0`) } func (s *ValidateSuite) TestMissingSnapLicenseIsOkay(c *C) { info, err := InfoFromSnapYaml([]byte(`name: foo version: 1.0 `)) c.Assert(err, IsNil) c.Assert(Validate(info), IsNil) } func (s *ValidateSuite) TestIllegalHookName(c *C) { hookType := NewHookType(regexp.MustCompile(".*")) restore := MockSupportedHookTypes([]*HookType{hookType}) defer restore() info, err := InfoFromSnapYaml([]byte(`name: foo version: 1.0 hooks: 123abc: `)) c.Assert(err, IsNil) err = Validate(info) c.Check(err, ErrorMatches, `invalid hook name: "123abc"`) } func (s *ValidateSuite) TestPlugSlotNamesUnique(c *C) { info, err := InfoFromSnapYaml([]byte(`name: snap version: 0 plugs: foo: slots: foo: `)) c.Assert(err, IsNil) err = Validate(info) c.Check(err, ErrorMatches, `cannot have plug and slot with the same name: "foo"`) } func (s *ValidateSuite) TestIllegalAliasName(c *C) { info, err := InfoFromSnapYaml([]byte(`name: foo version: 1.0 apps: foo: aliases: [foo$] `)) c.Assert(err, IsNil) err = Validate(info) c.Check(err, ErrorMatches, `cannot have "foo\$" as alias name for app "foo" - use only letters, digits, dash, underscore and dot characters`) } func (s *ValidateSuite) TestValidateAlias(c *C) { validAliases := []string{ "a", "aa", "aaa", "aaaa", "a-a", "aa-a", "a-aa", "a-b-c", "a0", "a-0", "a-0a", "a_a", "aa_a", "a_aa", "a_b_c", "a0", "a_0", "a_0a", "01game", "1-or-2", "0.1game", "1_or_2", } for _, alias := range validAliases { err := ValidateAlias(alias) c.Assert(err, IsNil) } invalidAliases := []string{ "", "_foo", "-foo", ".foo", "foo$", } for _, alias := range invalidAliases { err := ValidateAlias(alias) c.Assert(err, ErrorMatches, `invalid alias name: ".*"`) } } func (s *ValidateSuite) TestValidatePlugSlotName(c *C) { const yaml1 = ` name: invalid-plugs version: 1 plugs: p--lug: null ` info, err := InfoFromSnapYamlWithSideInfo([]byte(yaml1), nil) c.Assert(err, IsNil) c.Assert(info.Plugs, HasLen, 1) err = Validate(info) c.Assert(err, ErrorMatches, `invalid plug name: "p--lug"`) const yaml2 = ` name: invalid-slots version: 1 slots: s--lot: null ` info, err = InfoFromSnapYamlWithSideInfo([]byte(yaml2), nil) c.Assert(err, IsNil) c.Assert(info.Slots, HasLen, 1) err = Validate(info) c.Assert(err, ErrorMatches, `invalid slot name: "s--lot"`) const yaml3 = ` name: invalid-plugs-iface version: 1 plugs: plug: interface: i--face ` info, err = InfoFromSnapYamlWithSideInfo([]byte(yaml3), nil) c.Assert(err, IsNil) c.Assert(info.Plugs, HasLen, 1) err = Validate(info) c.Assert(err, ErrorMatches, `invalid interface name "i--face" for plug "plug"`) const yaml4 = ` name: invalid-slots-iface version: 1 slots: slot: interface: i--face ` info, err = InfoFromSnapYamlWithSideInfo([]byte(yaml4), nil) c.Assert(err, IsNil) c.Assert(info.Slots, HasLen, 1) err = Validate(info) c.Assert(err, ErrorMatches, `invalid interface name "i--face" for slot "slot"`) } type testConstraint string func (constraint testConstraint) IsOffLimits(path string) bool { return true } func (s *ValidateSuite) TestValidateLayout(c *C) { si := &Info{SuggestedName: "foo"} // Several invalid layouts. c.Check(ValidateLayout(&Layout{Snap: si}, nil), ErrorMatches, "layout cannot use an empty path") c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo"}, nil), ErrorMatches, `layout "/foo" must define a bind mount, a filesystem mount or a symlink`) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo", Bind: "/bar", Type: "tmpfs"}, nil), ErrorMatches, `layout "/foo" must define a bind mount, a filesystem mount or a symlink`) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo", Bind: "/bar", BindFile: "/froz"}, nil), ErrorMatches, `layout "/foo" must define a bind mount, a filesystem mount or a symlink`) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo", Symlink: "/bar", BindFile: "/froz"}, nil), ErrorMatches, `layout "/foo" must define a bind mount, a filesystem mount or a symlink`) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo", Type: "tmpfs", BindFile: "/froz"}, nil), ErrorMatches, `layout "/foo" must define a bind mount, a filesystem mount or a symlink`) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo", Bind: "/bar", Symlink: "/froz"}, nil), ErrorMatches, `layout "/foo" must define a bind mount, a filesystem mount or a symlink`) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo", Type: "tmpfs", Symlink: "/froz"}, nil), ErrorMatches, `layout "/foo" must define a bind mount, a filesystem mount or a symlink`) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo", Type: "ext4"}, nil), ErrorMatches, `layout "/foo" uses invalid filesystem "ext4"`) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo/bar", Type: "tmpfs", User: "foo"}, nil), ErrorMatches, `layout "/foo/bar" uses invalid user "foo"`) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo/bar", Type: "tmpfs", Group: "foo"}, nil), ErrorMatches, `layout "/foo/bar" uses invalid group "foo"`) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo", Type: "tmpfs", Mode: 02755}, nil), ErrorMatches, `layout "/foo" uses invalid mode 02755`) c.Check(ValidateLayout(&Layout{Snap: si, Path: "$FOO", Type: "tmpfs"}, nil), ErrorMatches, `layout "\$FOO" uses invalid mount point: reference to unknown variable "\$FOO"`) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo", Bind: "$BAR"}, nil), ErrorMatches, `layout "/foo" uses invalid bind mount source "\$BAR": reference to unknown variable "\$BAR"`) c.Check(ValidateLayout(&Layout{Snap: si, Path: "$SNAP/evil", Bind: "/etc"}, nil), ErrorMatches, `layout "\$SNAP/evil" uses invalid bind mount source "/etc": must start with \$SNAP, \$SNAP_DATA or \$SNAP_COMMON`) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo", Symlink: "$BAR"}, nil), ErrorMatches, `layout "/foo" uses invalid symlink old name "\$BAR": reference to unknown variable "\$BAR"`) c.Check(ValidateLayout(&Layout{Snap: si, Path: "$SNAP/evil", Symlink: "/etc"}, nil), ErrorMatches, `layout "\$SNAP/evil" uses invalid symlink old name "/etc": must start with \$SNAP, \$SNAP_DATA or \$SNAP_COMMON`) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo/bar", Bind: "$SNAP/bar/foo"}, []LayoutConstraint{testConstraint("/foo")}), ErrorMatches, `layout "/foo/bar" underneath prior layout item "/foo"`) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/dev", Type: "tmpfs"}, nil), ErrorMatches, `layout "/dev" in an off-limits area`) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/dev/foo", Type: "tmpfs"}, nil), ErrorMatches, `layout "/dev/foo" in an off-limits area`) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/proc", Type: "tmpfs"}, nil), ErrorMatches, `layout "/proc" in an off-limits area`) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/sys", Type: "tmpfs"}, nil), ErrorMatches, `layout "/sys" in an off-limits area`) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/run", Type: "tmpfs"}, nil), ErrorMatches, `layout "/run" in an off-limits area`) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/boot", Type: "tmpfs"}, nil), ErrorMatches, `layout "/boot" in an off-limits area`) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/lost+found", Type: "tmpfs"}, nil), ErrorMatches, `layout "/lost\+found" in an off-limits area`) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/media", Type: "tmpfs"}, nil), ErrorMatches, `layout "/media" in an off-limits area`) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/var/snap", Type: "tmpfs"}, nil), ErrorMatches, `layout "/var/snap" in an off-limits area`) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/var/lib/snapd", Type: "tmpfs"}, nil), ErrorMatches, `layout "/var/lib/snapd" in an off-limits area`) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/var/lib/snapd/hostfs", Type: "tmpfs"}, nil), ErrorMatches, `layout "/var/lib/snapd/hostfs" in an off-limits area`) // Several valid layouts. c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo", Type: "tmpfs", Mode: 01755}, nil), IsNil) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/tmp", Type: "tmpfs"}, nil), IsNil) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/usr", Bind: "$SNAP/usr"}, nil), IsNil) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/var", Bind: "$SNAP_DATA/var"}, nil), IsNil) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/var", Bind: "$SNAP_COMMON/var"}, nil), IsNil) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/etc/foo.conf", Symlink: "$SNAP_DATA/etc/foo.conf"}, nil), IsNil) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/a/b", Type: "tmpfs", User: "root"}, nil), IsNil) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/a/b", Type: "tmpfs", Group: "root"}, nil), IsNil) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/a/b", Type: "tmpfs", Mode: 0655}, nil), IsNil) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/usr", Symlink: "$SNAP/usr"}, nil), IsNil) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/var", Symlink: "$SNAP_DATA/var"}, nil), IsNil) c.Check(ValidateLayout(&Layout{Snap: si, Path: "/var", Symlink: "$SNAP_COMMON/var"}, nil), IsNil) c.Check(ValidateLayout(&Layout{Snap: si, Path: "$SNAP/data", Symlink: "$SNAP_DATA"}, nil), IsNil) } func (s *ValidateSuite) TestValidateLayoutAll(c *C) { // /usr/foo prevents /usr/foo/bar from being valid (tmpfs) const yaml1 = ` name: broken-layout-1 layout: /usr/foo: type: tmpfs /usr/foo/bar: type: tmpfs ` const yaml1rev = ` name: broken-layout-1 layout: /usr/foo/bar: type: tmpfs /usr/foo: type: tmpfs ` for _, yaml := range []string{yaml1, yaml1rev} { info, err := InfoFromSnapYamlWithSideInfo([]byte(yaml), &SideInfo{Revision: R(42)}) c.Assert(err, IsNil) c.Assert(info.Layout, HasLen, 2) err = ValidateLayoutAll(info) c.Assert(err, ErrorMatches, `layout "/usr/foo/bar" underneath prior layout item "/usr/foo"`) } // Same as above but with bind-mounts instead of filesystem mounts. const yaml2 = ` name: broken-layout-2 layout: /usr/foo: bind: $SNAP /usr/foo/bar: bind: $SNAP ` const yaml2rev = ` name: broken-layout-2 layout: /usr/foo/bar: bind: $SNAP /usr/foo: bind: $SNAP ` for _, yaml := range []string{yaml2, yaml2rev} { info, err := InfoFromSnapYamlWithSideInfo([]byte(yaml), &SideInfo{Revision: R(42)}) c.Assert(err, IsNil) c.Assert(info.Layout, HasLen, 2) err = ValidateLayoutAll(info) c.Assert(err, ErrorMatches, `layout "/usr/foo/bar" underneath prior layout item "/usr/foo"`) } // /etc/foo (directory) is not clashing with /etc/foo.conf (file) const yaml3 = ` name: valid-layout-1 layout: /etc/foo: bind: $SNAP_DATA/foo /etc/foo.conf: symlink: $SNAP_DATA/foo.conf ` const yaml3rev = ` name: valid-layout-1 layout: /etc/foo.conf: symlink: $SNAP_DATA/foo.conf /etc/foo: bind: $SNAP_DATA/foo ` for _, yaml := range []string{yaml3, yaml3rev} { info, err := InfoFromSnapYamlWithSideInfo([]byte(yaml), &SideInfo{Revision: R(42)}) c.Assert(err, IsNil) c.Assert(info.Layout, HasLen, 2) err = ValidateLayoutAll(info) c.Assert(err, IsNil) } // /etc/foo file is not clashing with /etc/foobar const yaml4 = ` name: valid-layout-2 layout: /etc/foo: symlink: $SNAP_DATA/foo /etc/foobar: symlink: $SNAP_DATA/foobar ` const yaml4rev = ` name: valid-layout-2 layout: /etc/foobar: symlink: $SNAP_DATA/foobar /etc/foo: symlink: $SNAP_DATA/foo ` for _, yaml := range []string{yaml4, yaml4rev} { info, err := InfoFromSnapYamlWithSideInfo([]byte(yaml), &SideInfo{Revision: R(42)}) c.Assert(err, IsNil) c.Assert(info.Layout, HasLen, 2) err = ValidateLayoutAll(info) c.Assert(err, IsNil) } // /etc/foo file is also clashing with /etc/foo/bar const yaml5 = ` name: valid-layout-2 layout: /usr/foo: symlink: $SNAP_DATA/foo /usr/foo/bar: bind: $SNAP_DATA/foo/bar ` const yaml5rev = ` name: valid-layout-2 layout: /usr/foo/bar: bind: $SNAP_DATA/foo/bar /usr/foo: symlink: $SNAP_DATA/foo ` for _, yaml := range []string{yaml5, yaml5rev} { info, err := InfoFromSnapYamlWithSideInfo([]byte(yaml), &SideInfo{Revision: R(42)}) c.Assert(err, IsNil) c.Assert(info.Layout, HasLen, 2) err = ValidateLayoutAll(info) c.Assert(err, ErrorMatches, `layout "/usr/foo/bar" underneath prior layout item "/usr/foo"`) } const yaml6 = ` name: tricky-layout-1 layout: /etc/norf: bind: $SNAP/etc/norf /etc/norf: bind-file: $SNAP/etc/norf ` info, err := InfoFromSnapYamlWithSideInfo([]byte(yaml6), &SideInfo{Revision: R(42)}) c.Assert(err, IsNil) c.Assert(info.Layout, HasLen, 1) err = ValidateLayoutAll(info) c.Assert(err, IsNil) c.Assert(info.Layout["/etc/norf"].Bind, Equals, "") c.Assert(info.Layout["/etc/norf"].BindFile, Equals, "$SNAP/etc/norf") // Two layouts refer to the same path as a directory and a file. const yaml7 = ` name: clashing-source-path-1 layout: /etc/norf: bind: $SNAP/etc/norf /etc/corge: bind-file: $SNAP/etc/norf ` info, err = InfoFromSnapYamlWithSideInfo([]byte(yaml7), &SideInfo{Revision: R(42)}) c.Assert(err, IsNil) c.Assert(info.Layout, HasLen, 2) err = ValidateLayoutAll(info) c.Assert(err, ErrorMatches, `layout "/etc/norf" refers to directory "\$SNAP/etc/norf" but another layout treats it as file`) // Two layouts refer to the same path as a directory and a file (other way around). const yaml8 = ` name: clashing-source-path-2 layout: /etc/norf: bind-file: $SNAP/etc/norf /etc/corge: bind: $SNAP/etc/norf ` info, err = InfoFromSnapYamlWithSideInfo([]byte(yaml8), &SideInfo{Revision: R(42)}) c.Assert(err, IsNil) c.Assert(info.Layout, HasLen, 2) err = ValidateLayoutAll(info) c.Assert(err, ErrorMatches, `layout "/etc/norf" refers to file "\$SNAP/etc/norf" but another layout treats it as a directory`) // Two layouts refer to the same path, but one uses variable and the other doesn't. const yaml9 = ` name: clashing-source-path-3 layout: /etc/norf: bind-file: $SNAP/etc/norf /etc/corge: bind: /snap/clashing-source-path-3/42/etc/norf ` info, err = InfoFromSnapYamlWithSideInfo([]byte(yaml9), &SideInfo{Revision: R(42)}) c.Assert(err, IsNil) c.Assert(info.Layout, HasLen, 2) err = ValidateLayoutAll(info) c.Assert(err, ErrorMatches, `layout "/etc/norf" refers to file "\$SNAP/etc/norf" but another layout treats it as a directory`) // Same source path referred from a bind mount and symlink doesn't clash. const yaml10 = ` name: non-clashing-source-1 layout: /etc/norf: bind: $SNAP/etc/norf /etc/corge: symlink: $SNAP/etc/norf ` info, err = InfoFromSnapYamlWithSideInfo([]byte(yaml10), &SideInfo{Revision: R(42)}) c.Assert(err, IsNil) c.Assert(info.Layout, HasLen, 2) err = ValidateLayoutAll(info) c.Assert(err, IsNil) // Same source path referred from a file bind mount and symlink doesn't clash. const yaml11 = ` name: non-clashing-source-1 layout: /etc/norf: bind-file: $SNAP/etc/norf /etc/corge: symlink: $SNAP/etc/norf ` info, err = InfoFromSnapYamlWithSideInfo([]byte(yaml11), &SideInfo{Revision: R(42)}) c.Assert(err, IsNil) c.Assert(info.Layout, HasLen, 2) err = ValidateLayoutAll(info) c.Assert(err, IsNil) } func (s *ValidateSuite) TestValidateSocketName(c *C) { validNames := []string{ "a", "aa", "aaa", "aaaa", "a-a", "aa-a", "a-aa", "a-b-c", "a0", "a-0", "a-0a", "01game", "1-or-2", } for _, name := range validNames { err := ValidateSocketName(name) c.Assert(err, IsNil) } invalidNames := []string{ // name cannot be empty "", // dashes alone are not a name "-", "--", // double dashes in a name are not allowed "a--a", // name should not end with a dash "a-", // name cannot have any spaces in it "a ", " a", "a a", // a number alone is not a name "0", "123", // identifier must be plain ASCII "日本語", "한글", "ру́сский язы́к", // no null chars in the string are allowed "aa-a\000-b", } for _, name := range invalidNames { err := ValidateSocketName(name) c.Assert(err, ErrorMatches, `invalid socket name: ".*"`) } } func (s *YamlSuite) TestValidateAppStartupOrder(c *C) { meta := []byte(` name: foo version: 1.0 `) fooAfterBaz := []byte(` apps: foo: after: [baz] daemon: simple bar: daemon: forking `) fooBeforeBaz := []byte(` apps: foo: before: [baz] daemon: simple bar: daemon: forking `) fooNotADaemon := []byte(` apps: foo: after: [bar] bar: daemon: forking `) fooBarNotADaemon := []byte(` apps: foo: after: [bar] daemon: forking bar: `) fooSelfCycle := []byte(` apps: foo: after: [foo] daemon: forking bar: `) // cycle between foo and bar badOrder1 := []byte(` apps: foo: after: [bar] daemon: forking bar: after: [foo] daemon: forking `) // conflicting schedule for baz badOrder2 := []byte(` apps: foo: before: [bar] daemon: forking bar: after: [foo] daemon: forking baz: before: [foo] after: [bar] daemon: forking `) // conflicting schedule for baz badOrder3Cycle := []byte(` apps: foo: before: [bar] after: [zed] daemon: forking bar: before: [baz] daemon: forking baz: before: [zed] daemon: forking zed: daemon: forking `) goodOrder1 := []byte(` apps: foo: after: [bar, zed] daemon: oneshot bar: before: [foo] daemon: dbus baz: after: [foo] daemon: forking zed: daemon: dbus `) goodOrder2 := []byte(` apps: foo: after: [baz] daemon: oneshot bar: before: [baz] daemon: dbus baz: daemon: forking zed: daemon: dbus after: [foo, bar, baz] `) tcs := []struct { name string desc []byte err string }{{ name: "foo after baz", desc: fooAfterBaz, err: `invalid definition of application "foo": before/after references a missing application "baz"`, }, { name: "foo before baz", desc: fooBeforeBaz, err: `invalid definition of application "foo": before/after references a missing application "baz"`, }, { name: "foo not a daemon", desc: fooNotADaemon, err: `invalid definition of application "foo": must be a service to define before/after ordering`, }, { name: "foo wants bar, bar not a daemon", desc: fooBarNotADaemon, err: `invalid definition of application "foo": before/after references a non-service application "bar"`, }, { name: "bad order 1", desc: badOrder1, err: `applications are part of a before/after cycle: (foo, bar)|(bar, foo)`, }, { name: "bad order 2", desc: badOrder2, err: `applications are part of a before/after cycle: ((foo|bar|baz)(, )?){3}`, }, { name: "bad order 3 - cycle", desc: badOrder3Cycle, err: `applications are part of a before/after cycle: ((foo|bar|baz|zed)(, )?){4}`, }, { name: "all good, 3 apps", desc: goodOrder1, }, { name: "all good, 4 apps", desc: goodOrder2, }, { name: "self cycle", desc: fooSelfCycle, err: `applications are part of a before/after cycle: foo`}, } for _, tc := range tcs { c.Logf("trying %q", tc.name) info, err := InfoFromSnapYaml(append(meta, tc.desc...)) c.Assert(err, IsNil) err = Validate(info) if tc.err != "" { c.Assert(err, ErrorMatches, tc.err) } else { c.Assert(err, IsNil) } } } func (s *ValidateSuite) TestValidateAppWatchdog(c *C) { meta := []byte(` name: foo version: 1.0 `) fooAllGood := []byte(` apps: foo: daemon: simple watchdog-timeout: 12s `) fooNotADaemon := []byte(` apps: foo: watchdog-timeout: 12s `) fooNegative := []byte(` apps: foo: daemon: simple watchdog-timeout: -12s `) tcs := []struct { name string desc []byte err string }{{ name: "foo all good", desc: fooAllGood, }, { name: "foo not a service", desc: fooNotADaemon, err: `watchdog-timeout is only applicable to services`, }, { name: "negative timeout", desc: fooNegative, err: `watchdog-timeout cannot be negative`, }} for _, tc := range tcs { c.Logf("trying %q", tc.name) info, err := InfoFromSnapYaml(append(meta, tc.desc...)) c.Assert(err, IsNil) c.Assert(info, NotNil) err = Validate(info) if tc.err != "" { c.Assert(err, ErrorMatches, `invalid definition of application "foo": `+tc.err) } else { c.Assert(err, IsNil) } } } func (s *YamlSuite) TestValidateAppTimer(c *C) { meta := []byte(` name: foo version: 1.0 `) allGood := []byte(` apps: foo: daemon: simple timer: 10:00-12:00 `) notAService := []byte(` apps: foo: timer: 10:00-12:00 `) badTimer := []byte(` apps: foo: daemon: oneshot timer: mon,10:00-12:00,mon2-wed3 `) tcs := []struct { name string desc []byte err string }{{ name: "all correct", desc: allGood, }, { name: "not a service", desc: notAService, err: `timer is only applicable to services`, }, { name: "invalid timer", desc: badTimer, err: `timer has invalid format: cannot parse "mon2-wed3": invalid schedule fragment`, }} for _, tc := range tcs { c.Logf("trying %q", tc.name) info, err := InfoFromSnapYaml(append(meta, tc.desc...)) c.Assert(err, IsNil) err = Validate(info) if tc.err != "" { c.Assert(err, ErrorMatches, `invalid definition of application "foo": `+tc.err) } else { c.Assert(err, IsNil) } } } func (s *ValidateSuite) TestValidateOsCannotHaveBase(c *C) { info, err := InfoFromSnapYaml([]byte(`name: foo version: 1.0 type: os base: bar `)) c.Assert(err, IsNil) err = Validate(info) c.Check(err, ErrorMatches, `cannot have "base" field on "os" snap "foo"`) } func (s *ValidateSuite) TestValidateBaseCannotHaveBase(c *C) { info, err := InfoFromSnapYaml([]byte(`name: foo version: 1.0 type: base base: bar `)) c.Assert(err, IsNil) err = Validate(info) c.Check(err, ErrorMatches, `cannot have "base" field on "base" snap "foo"`) } func (s *ValidateSuite) TestValidateCommonIDs(c *C) { meta := ` name: foo version: 1.0 ` good := meta + ` apps: foo: common-id: org.foo.foo bar: common-id: org.foo.bar baz: ` bad := meta + ` apps: foo: common-id: org.foo.foo bar: common-id: org.foo.foo baz: ` for i, tc := range []struct { meta string err string }{ {good, ""}, {bad, `application ("bar" common-id "org.foo.foo" must be unique, already used by application "foo"|"foo" common-id "org.foo.foo" must be unique, already used by application "bar")`}, } { c.Logf("tc #%v", i) info, err := InfoFromSnapYaml([]byte(tc.meta)) c.Assert(err, IsNil) err = Validate(info) if tc.err == "" { c.Assert(err, IsNil) } else { c.Assert(err, NotNil) c.Check(err, ErrorMatches, tc.err) } } } func (s *validateSuite) TestValidateDescription(c *C) { for _, s := range []string{ "xx", // boringest ASCII "🐧🐧", // len("🐧🐧") == 8 "á", // á (combining) } { c.Check(ValidateDescription(s), IsNil) c.Check(ValidateDescription(strings.Repeat(s, 2049)), ErrorMatches, `description can have up to 4096 codepoints, got 4098`) c.Check(ValidateDescription(strings.Repeat(s, 2048)), IsNil) } } func (s *validateSuite) TestValidateTitle(c *C) { for _, s := range []string{ "xx", // boringest ASCII "🐧🐧", // len("🐧🐧") == 8 "á", // á (combining) } { c.Check(ValidateTitle(strings.Repeat(s, 21)), ErrorMatches, `title can have up to 40 codepoints, got 42`) c.Check(ValidateTitle(strings.Repeat(s, 20)), IsNil) } } func (s *validateSuite) TestValidatePlugSlotName(c *C) { validNames := []string{ "a", "aa", "aaa", "aaaa", "a-a", "aa-a", "a-aa", "a-b-c", "a0", "a-0", "a-0a", } for _, name := range validNames { c.Assert(ValidatePlugName(name), IsNil) c.Assert(ValidateSlotName(name), IsNil) c.Assert(ValidateInterfaceName(name), IsNil) } invalidNames := []string{ // name cannot be empty "", // dashes alone are not a name "-", "--", // double dashes in a name are not allowed "a--a", // name should not end with a dash "a-", // name cannot have any spaces in it "a ", " a", "a a", // a number alone is not a name "0", "123", // identifier must be plain ASCII "日本語", "한글", "ру́сский язы́к", } for _, name := range invalidNames { c.Assert(ValidatePlugName(name), ErrorMatches, `invalid plug name: ".*"`) c.Assert(ValidateSlotName(name), ErrorMatches, `invalid slot name: ".*"`) c.Assert(ValidateInterfaceName(name), ErrorMatches, `invalid interface name: ".*"`) } } func (s *ValidateSuite) TestValidateSnapInstanceNameBadSnapName(c *C) { info, err := InfoFromSnapYaml([]byte(`name: foo_bad version: 1.0 `)) c.Assert(err, IsNil) err = Validate(info) c.Check(err, ErrorMatches, `invalid snap name: "foo_bad"`) } func (s *ValidateSuite) TestValidateSnapInstanceNameBadInstanceKey(c *C) { info, err := InfoFromSnapYaml([]byte(`name: foo version: 1.0 `)) c.Assert(err, IsNil) for _, s := range []string{"toolonginstance", "ABCD", "_", "inst@nce", "012345678901"} { info.InstanceKey = s err = Validate(info) c.Check(err, ErrorMatches, fmt.Sprintf(`invalid instance key: %q`, s)) } } func (s *ValidateSuite) TestValidateAppRestart(c *C) { meta := []byte(` name: foo version: 1.0 `) fooAllGood := []byte(` apps: foo: daemon: simple restart-condition: on-abort restart-delay: 12s `) fooAllGoodDefault := []byte(` apps: foo: daemon: simple `) fooAllGoodJustDelay := []byte(` apps: foo: daemon: simple restart-delay: 12s `) fooConditionNotADaemon := []byte(` apps: foo: restart-condition: on-abort `) fooDelayNotADaemon := []byte(` apps: foo: restart-delay: 12s `) fooNegativeDelay := []byte(` apps: foo: daemon: simple restart-delay: -12s `) tcs := []struct { name string desc []byte err string }{{ name: "foo all good", desc: fooAllGood, }, { name: "foo all good with default values", desc: fooAllGoodDefault, }, { name: "foo all good with restart-delay only", desc: fooAllGoodJustDelay, }, { name: "foo restart-delay but not a service", desc: fooDelayNotADaemon, err: `restart-delay is only applicable to services`, }, { name: "foo restart-delay but not a service", desc: fooConditionNotADaemon, err: `restart-condition is only applicable to services`, }, { name: "negative restart-delay", desc: fooNegativeDelay, err: `restart-delay cannot be negative`, }} for _, tc := range tcs { c.Logf("trying %q", tc.name) info, err := InfoFromSnapYaml(append(meta, tc.desc...)) c.Assert(err, IsNil) c.Assert(info, NotNil) err = Validate(info) if tc.err != "" { c.Assert(err, ErrorMatches, `invalid definition of application "foo": `+tc.err) } else { c.Assert(err, IsNil) } } } snapd-2.37.4~14.04.1/snap/revision_test.go0000664000000000000000000000761313435556260014735 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2016 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 . * */ package snap_test import ( "encoding/json" "strconv" . "gopkg.in/check.v1" "gopkg.in/yaml.v2" "github.com/snapcore/snapd/snap" ) // Keep this in sync between snap and client packages. type revisionSuite struct{} var _ = Suite(&revisionSuite{}) func (s revisionSuite) TestString(c *C) { c.Assert(snap.R(0).String(), Equals, "unset") c.Assert(snap.R(10).String(), Equals, "10") c.Assert(snap.R(-9).String(), Equals, "x9") } func (s revisionSuite) TestUnset(c *C) { c.Assert(snap.R(0).Unset(), Equals, true) c.Assert(snap.R(10).Unset(), Equals, false) c.Assert(snap.R(-9).Unset(), Equals, false) } func (s revisionSuite) TestLocal(c *C) { c.Assert(snap.R(0).Local(), Equals, false) c.Assert(snap.R(10).Local(), Equals, false) c.Assert(snap.R(-9).Local(), Equals, true) } func (s revisionSuite) TestStore(c *C) { c.Assert(snap.R(0).Store(), Equals, false) c.Assert(snap.R(10).Store(), Equals, true) c.Assert(snap.R(-9).Store(), Equals, false) } func (s revisionSuite) TestJSON(c *C) { for _, n := range []int{0, 10, -9} { r := snap.R(n) data, err := json.Marshal(snap.R(n)) c.Assert(err, IsNil) c.Assert(string(data), Equals, `"`+r.String()+`"`) var got snap.Revision err = json.Unmarshal(data, &got) c.Assert(err, IsNil) c.Assert(got, Equals, r) got = snap.Revision{} err = json.Unmarshal([]byte(strconv.Itoa(r.N)), &got) c.Assert(err, IsNil) c.Assert(got, Equals, r) } } func (s revisionSuite) TestYAML(c *C) { for _, v := range []struct { n int s string }{ {0, "unset"}, {10, `"10"`}, {-9, "x9"}, } { r := snap.R(v.n) data, err := yaml.Marshal(snap.R(v.n)) c.Assert(err, IsNil) c.Assert(string(data), Equals, v.s+"\n") var got snap.Revision err = yaml.Unmarshal(data, &got) c.Assert(err, IsNil) c.Assert(got, Equals, r) got = snap.Revision{} err = json.Unmarshal([]byte(strconv.Itoa(r.N)), &got) c.Assert(err, IsNil) c.Assert(got, Equals, r) } } func (s revisionSuite) ParseRevision(c *C) { type testItem struct { s string n int e string } var tests = []testItem{{ s: "unset", n: 0, }, { s: "x1", n: -1, }, { s: "1", n: 1, }, { s: "x-1", e: `invalid snap revision: "x-1"`, }, { s: "x0", e: `invalid snap revision: "x0"`, }, { s: "-1", e: `invalid snap revision: "-1"`, }, { s: "0", e: `invalid snap revision: "0"`, }} for _, test := range tests { r, err := snap.ParseRevision(test.s) if test.e != "" { c.Assert(err.Error(), Equals, test.e) continue } c.Assert(r, Equals, snap.R(test.n)) } } func (s *revisionSuite) TestR(c *C) { type testItem struct { v interface{} n int e string } var tests = []testItem{{ v: 0, n: 0, }, { v: -1, n: -1, }, { v: 1, n: 1, }, { v: "unset", n: 0, }, { v: "x1", n: -1, }, { v: "1", n: 1, }, { v: "x-1", e: `invalid snap revision: "x-1"`, }, { v: "x0", e: `invalid snap revision: "x0"`, }, { v: "-1", e: `invalid snap revision: "-1"`, }, { v: "0", e: `invalid snap revision: "0"`, }, { v: int64(1), e: `cannot use 1 \(int64\) as a snap revision`, }} for _, test := range tests { if test.e != "" { f := func() { snap.R(test.v) } c.Assert(f, PanicMatches, test.e) continue } c.Assert(snap.R(test.v), Equals, snap.R(test.n)) } } snapd-2.37.4~14.04.1/snap/pack/0000775000000000000000000000000013435556260012420 5ustar snapd-2.37.4~14.04.1/snap/pack/pack_test.go0000664000000000000000000001737013435556260014734 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2016 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 . * */ package pack_test import ( "fmt" "io/ioutil" "os" "os/exec" "path/filepath" "regexp" "strings" "testing" . "gopkg.in/check.v1" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/pack" "github.com/snapcore/snapd/snap/squashfs" "github.com/snapcore/snapd/testutil" ) func Test(t *testing.T) { TestingT(t) } type packSuite struct { testutil.BaseTest } var _ = Suite(&packSuite{}) func (s *packSuite) SetUpTest(c *C) { s.BaseTest.SetUpTest(c) s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})) // chdir into a tempdir pwd, err := os.Getwd() c.Assert(err, IsNil) s.AddCleanup(func() { os.Chdir(pwd) }) err = os.Chdir(c.MkDir()) c.Assert(err, IsNil) // use fake root dirs.SetRootDir(c.MkDir()) } func (s *packSuite) TearDownTest(c *C) { s.BaseTest.TearDownTest(c) } func makeExampleSnapSourceDir(c *C, snapYamlContent string) string { tempdir := c.MkDir() c.Assert(os.Chmod(tempdir, 0755), IsNil) // use meta/snap.yaml metaDir := filepath.Join(tempdir, "meta") err := os.Mkdir(metaDir, 0755) c.Assert(err, IsNil) err = ioutil.WriteFile(filepath.Join(metaDir, "snap.yaml"), []byte(snapYamlContent), 0644) c.Assert(err, IsNil) const helloBinContent = `#!/bin/sh printf "hello world" ` // an example binary binDir := filepath.Join(tempdir, "bin") err = os.Mkdir(binDir, 0755) c.Assert(err, IsNil) err = ioutil.WriteFile(filepath.Join(binDir, "hello-world"), []byte(helloBinContent), 0755) c.Assert(err, IsNil) // unusual permissions for dir tmpDir := filepath.Join(tempdir, "tmp") err = os.Mkdir(tmpDir, 0755) c.Assert(err, IsNil) // avoid umask err = os.Chmod(tmpDir, 01777) c.Assert(err, IsNil) // and file someFile := filepath.Join(tempdir, "file-with-perm") err = ioutil.WriteFile(someFile, []byte(""), 0666) c.Assert(err, IsNil) err = os.Chmod(someFile, 0666) c.Assert(err, IsNil) // an example symlink err = os.Symlink("bin/hello-world", filepath.Join(tempdir, "symlink")) c.Assert(err, IsNil) return tempdir } func (s *packSuite) TestPackNoManifestFails(c *C) { sourceDir := makeExampleSnapSourceDir(c, "{name: hello, version: 0}") c.Assert(os.Remove(filepath.Join(sourceDir, "meta", "snap.yaml")), IsNil) _, err := pack.Snap(sourceDir, "", "") c.Assert(err, ErrorMatches, `.*/meta/snap\.yaml: no such file or directory`) } func (s *packSuite) TestPackMissingAppFails(c *C) { sourceDir := makeExampleSnapSourceDir(c, `name: hello version: 0 apps: foo: command: bin/hello-world `) c.Assert(os.Remove(filepath.Join(sourceDir, "bin", "hello-world")), IsNil) _, err := pack.Snap(sourceDir, "", "") c.Assert(err, Equals, snap.ErrMissingPaths) } func (s *packSuite) TestValidateMissingAppFailsWithErrMissingPaths(c *C) { sourceDir := makeExampleSnapSourceDir(c, `name: hello version: 0 apps: foo: command: bin/hello-world `) c.Assert(os.Remove(filepath.Join(sourceDir, "bin", "hello-world")), IsNil) err := pack.CheckSkeleton(sourceDir) c.Assert(err, Equals, snap.ErrMissingPaths) } func (s *packSuite) TestPackExcludesBackups(c *C) { sourceDir := makeExampleSnapSourceDir(c, "{name: hello, version: 0}") target := c.MkDir() // add a backup file c.Assert(ioutil.WriteFile(filepath.Join(sourceDir, "foo~"), []byte("hi"), 0755), IsNil) snapfile, err := pack.Snap(sourceDir, c.MkDir(), "") c.Assert(err, IsNil) c.Assert(squashfs.New(snapfile).Unpack("*", target), IsNil) cmd := exec.Command("diff", "-qr", sourceDir, target) cmd.Env = append(cmd.Env, "LANG=C") out, err := cmd.Output() c.Check(err, NotNil) c.Check(string(out), Matches, `(?m)Only in \S+: foo~`) } func (s *packSuite) TestPackExcludesTopLevelDEBIAN(c *C) { sourceDir := makeExampleSnapSourceDir(c, "{name: hello, version: 0}") target := c.MkDir() // add a toplevel DEBIAN c.Assert(os.MkdirAll(filepath.Join(sourceDir, "DEBIAN", "foo"), 0755), IsNil) // and a non-toplevel DEBIAN c.Assert(os.MkdirAll(filepath.Join(sourceDir, "bar", "DEBIAN", "baz"), 0755), IsNil) snapfile, err := pack.Snap(sourceDir, c.MkDir(), "") c.Assert(err, IsNil) c.Assert(squashfs.New(snapfile).Unpack("*", target), IsNil) cmd := exec.Command("diff", "-qr", sourceDir, target) cmd.Env = append(cmd.Env, "LANG=C") out, err := cmd.Output() c.Check(err, NotNil) c.Check(string(out), Matches, `(?m)Only in \S+: DEBIAN`) // but *only one* DEBIAN is skipped c.Check(strings.Count(string(out), "Only in"), Equals, 1) } func (s *packSuite) TestPackExcludesWholeDirs(c *C) { sourceDir := makeExampleSnapSourceDir(c, "{name: hello, version: 0}") target := c.MkDir() // add a file inside a skipped dir c.Assert(os.Mkdir(filepath.Join(sourceDir, ".bzr"), 0755), IsNil) c.Assert(ioutil.WriteFile(filepath.Join(sourceDir, ".bzr", "foo"), []byte("hi"), 0755), IsNil) snapfile, err := pack.Snap(sourceDir, c.MkDir(), "") c.Assert(err, IsNil) c.Assert(squashfs.New(snapfile).Unpack("*", target), IsNil) out, _ := exec.Command("find", sourceDir).Output() c.Check(string(out), Not(Equals), "") cmd := exec.Command("diff", "-qr", sourceDir, target) cmd.Env = append(cmd.Env, "LANG=C") out, err = cmd.Output() c.Check(err, NotNil) c.Check(string(out), Matches, `(?m)Only in \S+: \.bzr`) } func (s *packSuite) TestDebArchitecture(c *C) { c.Check(pack.DebArchitecture(&snap.Info{Architectures: []string{"foo"}}), Equals, "foo") c.Check(pack.DebArchitecture(&snap.Info{Architectures: []string{"foo", "bar"}}), Equals, "multi") c.Check(pack.DebArchitecture(&snap.Info{Architectures: nil}), Equals, "all") } func (s *packSuite) TestPackSimple(c *C) { sourceDir := makeExampleSnapSourceDir(c, `name: hello version: 1.0.1 architectures: ["i386", "amd64"] integration: app: apparmor-profile: meta/hello.apparmor `) outputDir := filepath.Join(c.MkDir(), "output") absSnapFile := filepath.Join(c.MkDir(), "foo.snap") type T struct { outputDir, filename, expected string } table := []T{ // no output dir, no filename -> default in . {"", "", "hello_1.0.1_multi.snap"}, // no output dir, relative filename -> filename in . {"", "foo.snap", "foo.snap"}, // no putput dir, absolute filename -> absolute filename {"", absSnapFile, absSnapFile}, // output dir, no filename -> default in outputdir {outputDir, "", filepath.Join(outputDir, "hello_1.0.1_multi.snap")}, // output dir, relative filename -> filename in outputDir {filepath.Join(outputDir, "inner"), "../foo.snap", filepath.Join(outputDir, "foo.snap")}, // output dir, absolute filename -> absolute filename {outputDir, absSnapFile, absSnapFile}, } for i, t := range table { comm := Commentf("%d", i) resultSnap, err := pack.Snap(sourceDir, t.outputDir, t.filename) c.Assert(err, IsNil, comm) // check that there is result _, err = os.Stat(resultSnap) c.Assert(err, IsNil, comm) c.Assert(resultSnap, Equals, t.expected, comm) // check that the content looks sane output, err := exec.Command("unsquashfs", "-ll", resultSnap).CombinedOutput() c.Assert(err, IsNil, comm) for _, needle := range []string{ "meta/snap.yaml", "bin/hello-world", "symlink -> bin/hello-world", } { expr := fmt.Sprintf(`(?ms).*%s.*`, regexp.QuoteMeta(needle)) c.Assert(string(output), Matches, expr, comm) } } } snapd-2.37.4~14.04.1/snap/pack/pack.go0000664000000000000000000001036513435556260013672 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package pack import ( "fmt" "io" "io/ioutil" "os" "path/filepath" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snapdir" "github.com/snapcore/snapd/snap/squashfs" ) // this could be shipped as a file like "info", and save on the memory and the // overhead of creating and removing the tempfile, but on darwin we can't AFAIK // easily know where it's been placed. As it's not really that big, just // shipping it in memory seems fine for now. // from click's click.build.ClickBuilderBase, and there from // @Dpkg::Source::Package::tar_ignore_default_pattern; // changed to match squashfs's "-wildcards" syntax // // for anchored vs non-anchored syntax see RELEASE_README in squashfs-tools. const excludesContent = ` # "anchored", only match at top level DEBIAN .arch-ids .arch-inventory .bzr .bzr-builddeb .bzr.backup .bzr.tags .bzrignore .cvsignore .git .gitattributes .gitignore .gitmodules .hg .hgignore .hgsigs .hgtags .shelf .svn CVS DEADJOE RCS _MTN _darcs {arch} .snapignore # "non-anchored", match anywhere ... .[#~]* ... *.snap ... *.click ... .*.sw? ... *~ ... ,,* ` // small helper that returns the architecture of the snap, or "multi" if it's multiple arches func debArchitecture(info *snap.Info) string { switch len(info.Architectures) { case 0: return "all" case 1: return info.Architectures[0] default: return "multi" } } // CheckSkeleton attempts to validate snap data in source directory func CheckSkeleton(sourceDir string) error { _, err := loadAndValidate(sourceDir) return err } func loadAndValidate(sourceDir string) (*snap.Info, error) { // ensure we have valid content yaml, err := ioutil.ReadFile(filepath.Join(sourceDir, "meta", "snap.yaml")) if err != nil { return nil, err } info, err := snap.InfoFromSnapYaml(yaml) if err != nil { return nil, err } if err := snap.Validate(info); err != nil { return nil, fmt.Errorf("cannot validate snap %q: %v", info.InstanceName(), err) } if err := snap.ValidateContainer(snapdir.New(sourceDir), info, logger.Noticef); err != nil { return nil, err } return info, nil } func snapPath(info *snap.Info, targetDir, snapName string) string { if snapName == "" { snapName = fmt.Sprintf("%s_%s_%v.snap", info.InstanceName(), info.Version, debArchitecture(info)) } if targetDir != "" && !filepath.IsAbs(snapName) { snapName = filepath.Join(targetDir, snapName) } return snapName } func prepare(sourceDir, targetDir string) (*snap.Info, error) { info, err := loadAndValidate(sourceDir) if err != nil { return nil, err } if targetDir != "" { if err := os.MkdirAll(targetDir, 0755); err != nil { return nil, err } } return info, nil } func excludesFile() (filename string, err error) { tmpf, err := ioutil.TempFile("", ".snap-pack-exclude-") if err != nil { return "", err } // inspited by ioutil.WriteFile n, err := tmpf.Write([]byte(excludesContent)) if err == nil && n < len(excludesContent) { err = io.ErrShortWrite } if err1 := tmpf.Close(); err == nil { err = err1 } if err == nil { filename = tmpf.Name() } return filename, err } // Snap the given sourceDirectory and return the generated // snap file func Snap(sourceDir, targetDir, snapName string) (string, error) { info, err := prepare(sourceDir, targetDir) if err != nil { return "", err } excludes, err := excludesFile() if err != nil { return "", err } defer os.Remove(excludes) snapName = snapPath(info, targetDir, snapName) d := squashfs.New(snapName) if err = d.Build(sourceDir, string(info.Type), excludes); err != nil { return "", err } return snapName, nil } snapd-2.37.4~14.04.1/snap/pack/export_test.go0000664000000000000000000000133213435556260015326 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package pack var ( DebArchitecture = debArchitecture ) snapd-2.37.4~14.04.1/snap/snapdir/0000775000000000000000000000000013435556260013142 5ustar snapd-2.37.4~14.04.1/snap/snapdir/snapdir_test.go0000664000000000000000000000752513435556260016201 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2015 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 . * */ package snapdir_test import ( "fmt" "io/ioutil" "math/rand" "os" "path/filepath" "testing" "time" "github.com/snapcore/snapd/snap/snapdir" . "gopkg.in/check.v1" ) // Hook up check.v1 into the "go test" runner func Test(t *testing.T) { TestingT(t) } type SnapdirTestSuite struct { } var _ = Suite(&SnapdirTestSuite{}) func (s *SnapdirTestSuite) TestReadFile(c *C) { d := c.MkDir() needle := []byte(`stuff`) err := ioutil.WriteFile(filepath.Join(d, "foo"), needle, 0644) c.Assert(err, IsNil) snap := snapdir.New(d) content, err := snap.ReadFile("foo") c.Assert(err, IsNil) c.Assert(content, DeepEquals, needle) } func (s *SnapdirTestSuite) TestListDir(c *C) { d := c.MkDir() err := os.MkdirAll(filepath.Join(d, "test"), 0755) c.Assert(err, IsNil) err = ioutil.WriteFile(filepath.Join(d, "test", "test1"), nil, 0644) c.Assert(err, IsNil) err = ioutil.WriteFile(filepath.Join(d, "test", "test2"), nil, 0644) c.Assert(err, IsNil) snap := snapdir.New(d) fileNames, err := snap.ListDir("test") c.Assert(err, IsNil) c.Assert(fileNames, HasLen, 2) c.Check(fileNames[0], Equals, "test1") c.Check(fileNames[1], Equals, "test2") } func (s *SnapdirTestSuite) TestInstall(c *C) { tryBaseDir := c.MkDir() snap := snapdir.New(tryBaseDir) varLibSnapd := c.MkDir() targetPath := filepath.Join(varLibSnapd, "foo_1.0.snap") err := snap.Install(targetPath, "unused-mount-dir") c.Assert(err, IsNil) symlinkTarget, err := filepath.EvalSymlinks(targetPath) c.Assert(err, IsNil) c.Assert(symlinkTarget, Equals, tryBaseDir) } func walkEqual(tryBaseDir, sub string, c *C) { fpw := map[string]os.FileInfo{} filepath.Walk(filepath.Join(tryBaseDir, sub), func(path string, info os.FileInfo, err error) error { if err != nil { return err } path, err = filepath.Rel(tryBaseDir, path) if err != nil { return err } fpw[path] = info return nil }) sdw := map[string]os.FileInfo{} snap := snapdir.New(tryBaseDir) snap.Walk(sub, func(path string, info os.FileInfo, err error) error { if err != nil { return err } sdw[path] = info return nil }) for k, v := range fpw { c.Check(os.SameFile(sdw[k], v), Equals, true, Commentf(k)) } for k, v := range sdw { c.Check(os.SameFile(fpw[k], v), Equals, true, Commentf(k)) } } func (s *SnapdirTestSuite) TestWalk(c *C) { // probably already done elsewhere, but just in case rand.Seed(time.Now().UTC().UnixNano()) // https://en.wikipedia.org/wiki/Metasyntactic_variable ns := []string{ "foobar", "foo", "bar", "baz", "qux", "quux", "quuz", "corge", "grault", "garply", "waldo", "fred", "plugh", "xyzzy", "thud", "wibble", "wobble", "wubble", "flob", "blep", "blah", "boop", } p := 1.0 / float32(len(ns)) var f func(string, int) f = func(d string, n int) { for _, b := range ns { d1 := filepath.Join(d, fmt.Sprintf("%s%d", b, n)) c.Assert(os.Mkdir(d1, 0755), IsNil) if n < 20 && rand.Float32() < p { f(d1, n+1) } } } subs := make([]string, len(ns)+3) // three ways of saying the same thing ¯\_(ツ)_/¯ copy(subs, []string{"/", ".", ""}) copy(subs[3:], ns) for i := 0; i < 10; i++ { tryBaseDir := c.MkDir() f(tryBaseDir, 1) for _, sub := range subs { walkEqual(tryBaseDir, sub, c) } } } snapd-2.37.4~14.04.1/snap/snapdir/snapdir.go0000664000000000000000000001002213435556260015124 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2015 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 . * */ package snapdir import ( "fmt" "io" "io/ioutil" "os" "path/filepath" ) // SnapDir is the snapdir based snap. type SnapDir struct { path string } // Path returns the path of the backing container. func (s *SnapDir) Path() string { return s.path } // New returns a new snap directory container. func New(path string) *SnapDir { return &SnapDir{path: path} } func (s *SnapDir) Size() (size int64, err error) { totalSize := int64(0) f := func(_ string, info os.FileInfo, err error) error { totalSize += info.Size() return err } filepath.Walk(s.path, f) return totalSize, nil } func (s *SnapDir) Install(targetPath, mountDir string) error { return os.Symlink(s.path, targetPath) } func (s *SnapDir) ReadFile(file string) (content []byte, err error) { return ioutil.ReadFile(filepath.Join(s.path, file)) } func littleWalk(dirPath string, dirHandle *os.File, dirstack *[]string, walkFn filepath.WalkFunc) error { const numSt = 100 sts, err := dirHandle.Readdir(numSt) if err != nil { return err } for _, st := range sts { path := filepath.Join(dirPath, st.Name()) if err := walkFn(path, st, nil); err != nil { if st.IsDir() && err == filepath.SkipDir { // caller wants to skip this directory continue } return err } else if st.IsDir() { *dirstack = append(*dirstack, path) } } return nil } // Walk (part of snap.Container) is like filepath.Walk, without the ordering guarantee. func (s *SnapDir) Walk(relative string, walkFn filepath.WalkFunc) error { relative = filepath.Clean(relative) if relative == "" || relative == "/" { relative = "." } else if relative[0] == '/' { // I said relative, darn it :-) relative = relative[1:] } root := filepath.Join(s.path, relative) // we could just filepath.Walk(root, walkFn), but that doesn't scale // well to insanely big directories as it reads the whole directory, // in order to sort it. This Walk doesn't do that. // // Also the directory is always relative to the top of the container // for us, which would make it a little more messy to get right. f, err := os.Open(root) if err != nil { return walkFn(relative, nil, err) } defer func() { if f != nil { f.Close() } }() st, err := f.Stat() if err != nil { return walkFn(relative, nil, err) } err = walkFn(relative, st, nil) if err != nil { return err } if !st.IsDir() { return nil } var dirstack []string for { if err := littleWalk(relative, f, &dirstack, walkFn); err != nil { if err != io.EOF { err = walkFn(relative, nil, err) if err != nil { return err } } if len(dirstack) == 0 { // finished break } f.Close() f = nil for f == nil && len(dirstack) > 0 { relative = dirstack[0] f, err = os.Open(filepath.Join(s.path, relative)) if err != nil { err = walkFn(relative, nil, err) if err != nil { return err } } dirstack = dirstack[1:] } if f == nil { break } continue } } return nil } func (s *SnapDir) ListDir(path string) ([]string, error) { fileInfos, err := ioutil.ReadDir(filepath.Join(s.path, path)) if err != nil { return nil, err } var fileNames []string for _, fileInfo := range fileInfos { fileNames = append(fileNames, fileInfo.Name()) } return fileNames, nil } func (s *SnapDir) Unpack(src, dstDir string) error { return fmt.Errorf("unpack is not supported with snaps of type snapdir") } snapd-2.37.4~14.04.1/snap/info_test.go0000664000000000000000000014403013435556260014025 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2016 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 . * */ package snap_test import ( "fmt" "io/ioutil" "os" "path/filepath" "regexp" "sort" "strings" . "gopkg.in/check.v1" "gopkg.in/yaml.v2" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snaptest" "github.com/snapcore/snapd/snap/squashfs" "github.com/snapcore/snapd/testutil" ) type infoSuite struct { testutil.BaseTest } type infoSimpleSuite struct{} var _ = Suite(&infoSuite{}) var _ = Suite(&infoSimpleSuite{}) func (s *infoSimpleSuite) SetUpTest(c *C) { dirs.SetRootDir(c.MkDir()) } func (s *infoSimpleSuite) TearDownTest(c *C) { dirs.SetRootDir("") } func (s *infoSimpleSuite) TestReadInfoPanicsIfSanitizeUnset(c *C) { si := &snap.SideInfo{Revision: snap.R(1)} snaptest.MockSnap(c, sampleYaml, si) c.Assert(func() { snap.ReadInfo("sample", si) }, Panics, `SanitizePlugsSlots function not set`) } func (s *infoSuite) SetUpTest(c *C) { s.BaseTest.SetUpTest(c) dirs.SetRootDir(c.MkDir()) hookType := snap.NewHookType(regexp.MustCompile(".*")) s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})) s.BaseTest.AddCleanup(snap.MockSupportedHookTypes([]*snap.HookType{hookType})) } func (s *infoSuite) TearDownTest(c *C) { s.BaseTest.TearDownTest(c) dirs.SetRootDir("") } func (s *infoSuite) TestSideInfoOverrides(c *C) { info := &snap.Info{ SuggestedName: "name", OriginalSummary: "summary", OriginalDescription: "desc", } info.SideInfo = snap.SideInfo{ RealName: "newname", EditedSummary: "fixed summary", EditedDescription: "fixed desc", Revision: snap.R(1), SnapID: "snapidsnapidsnapidsnapidsnapidsn", } c.Check(info.InstanceName(), Equals, "newname") c.Check(info.Summary(), Equals, "fixed summary") c.Check(info.Description(), Equals, "fixed desc") c.Check(info.Revision, Equals, snap.R(1)) c.Check(info.SnapID, Equals, "snapidsnapidsnapidsnapidsnapidsn") } func (s *infoSuite) TestAppInfoSecurityTag(c *C) { appInfo := &snap.AppInfo{Snap: &snap.Info{SuggestedName: "http"}, Name: "GET"} c.Check(appInfo.SecurityTag(), Equals, "snap.http.GET") } func (s *infoSuite) TestPlugSlotSecurityTags(c *C) { info, err := snap.InfoFromSnapYaml([]byte(`name: name apps: app1: app2: hooks: hook1: plugs: plug: slots: slot: `)) c.Assert(err, IsNil) c.Assert(info.Plugs["plug"].SecurityTags(), DeepEquals, []string{ "snap.name.app1", "snap.name.app2", "snap.name.hook.hook1"}) c.Assert(info.Slots["slot"].SecurityTags(), DeepEquals, []string{ "snap.name.app1", "snap.name.app2", "snap.name.hook.hook1"}) } func (s *infoSuite) TestAppInfoWrapperPath(c *C) { info, err := snap.InfoFromSnapYaml([]byte(`name: foo apps: foo: bar: `)) c.Assert(err, IsNil) c.Check(info.Apps["bar"].WrapperPath(), Equals, filepath.Join(dirs.SnapBinariesDir, "foo.bar")) c.Check(info.Apps["foo"].WrapperPath(), Equals, filepath.Join(dirs.SnapBinariesDir, "foo")) } func (s *infoSuite) TestAppInfoCompleterPath(c *C) { info, err := snap.InfoFromSnapYaml([]byte(`name: foo apps: foo: bar: `)) c.Assert(err, IsNil) c.Check(info.Apps["bar"].CompleterPath(), Equals, filepath.Join(dirs.CompletersDir, "foo.bar")) c.Check(info.Apps["foo"].CompleterPath(), Equals, filepath.Join(dirs.CompletersDir, "foo")) } func (s *infoSuite) TestAppInfoLauncherCommand(c *C) { dirs.SetRootDir("") info, err := snap.InfoFromSnapYaml([]byte(`name: foo apps: foo: command: foo-bin bar: command: bar-bin -x baz: command: bar-bin -x timer: 10:00-12:00,,mon,12:00~14:00 `)) c.Assert(err, IsNil) info.Revision = snap.R(42) c.Check(info.Apps["bar"].LauncherCommand(), Equals, "/usr/bin/snap run foo.bar") c.Check(info.Apps["bar"].LauncherStopCommand(), Equals, "/usr/bin/snap run --command=stop foo.bar") c.Check(info.Apps["bar"].LauncherReloadCommand(), Equals, "/usr/bin/snap run --command=reload foo.bar") c.Check(info.Apps["bar"].LauncherPostStopCommand(), Equals, "/usr/bin/snap run --command=post-stop foo.bar") c.Check(info.Apps["foo"].LauncherCommand(), Equals, "/usr/bin/snap run foo") c.Check(info.Apps["baz"].LauncherCommand(), Equals, `/usr/bin/snap run --timer="10:00-12:00,,mon,12:00~14:00" foo.baz`) // snap with instance key info.InstanceKey = "instance" c.Check(info.Apps["bar"].LauncherCommand(), Equals, "/usr/bin/snap run foo_instance.bar") c.Check(info.Apps["bar"].LauncherStopCommand(), Equals, "/usr/bin/snap run --command=stop foo_instance.bar") c.Check(info.Apps["bar"].LauncherReloadCommand(), Equals, "/usr/bin/snap run --command=reload foo_instance.bar") c.Check(info.Apps["bar"].LauncherPostStopCommand(), Equals, "/usr/bin/snap run --command=post-stop foo_instance.bar") c.Check(info.Apps["foo"].LauncherCommand(), Equals, "/usr/bin/snap run foo_instance") c.Check(info.Apps["baz"].LauncherCommand(), Equals, `/usr/bin/snap run --timer="10:00-12:00,,mon,12:00~14:00" foo_instance.baz`) } const sampleYaml = ` name: sample version: 1 apps: app: command: foo app2: command: bar sample: command: foobar command-chain: [chain] hooks: configure: command-chain: [hookchain] ` func (s *infoSuite) TestReadInfo(c *C) { si := &snap.SideInfo{Revision: snap.R(42), EditedSummary: "esummary"} snapInfo1 := snaptest.MockSnap(c, sampleYaml, si) snapInfo2, err := snap.ReadInfo("sample", si) c.Assert(err, IsNil) c.Check(snapInfo2.InstanceName(), Equals, "sample") c.Check(snapInfo2.Revision, Equals, snap.R(42)) 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.Hooks["configure"].CommandChain, DeepEquals, []string{"hookchain"}) c.Check(snapInfo2, DeepEquals, snapInfo1) } func (s *infoSuite) TestReadInfoWithInstance(c *C) { si := &snap.SideInfo{Revision: snap.R(42), EditedSummary: "instance summary"} snapInfo1 := snaptest.MockSnapInstance(c, "sample_instance", sampleYaml, si) snapInfo2, err := snap.ReadInfo("sample_instance", si) c.Assert(err, IsNil) c.Check(snapInfo2.InstanceName(), Equals, "sample_instance") c.Check(snapInfo2.SnapName(), Equals, "sample") c.Check(snapInfo2.Revision, Equals, snap.R(42)) 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.Hooks["configure"].CommandChain, DeepEquals, []string{"hookchain"}) c.Check(snapInfo2, DeepEquals, snapInfo1) } func (s *infoSuite) TestReadCurrentInfo(c *C) { si := &snap.SideInfo{Revision: snap.R(42)} snapInfo1 := snaptest.MockSnapCurrent(c, sampleYaml, si) snapInfo2, err := snap.ReadCurrentInfo("sample") c.Assert(err, IsNil) c.Check(snapInfo2.InstanceName(), Equals, "sample") c.Check(snapInfo2.Revision, Equals, snap.R(42)) c.Check(snapInfo2, DeepEquals, snapInfo1) snapInfo3, err := snap.ReadCurrentInfo("not-sample") c.Check(snapInfo3, IsNil) c.Assert(err, ErrorMatches, `cannot find current revision for snap not-sample:.*`) } func (s *infoSuite) TestReadCurrentInfoWithInstance(c *C) { si := &snap.SideInfo{Revision: snap.R(42)} snapInfo1 := snaptest.MockSnapInstanceCurrent(c, "sample_instance", sampleYaml, si) snapInfo2, err := snap.ReadCurrentInfo("sample_instance") c.Assert(err, IsNil) c.Check(snapInfo2.InstanceName(), Equals, "sample_instance") c.Check(snapInfo2.SnapName(), Equals, "sample") c.Check(snapInfo2.Revision, Equals, snap.R(42)) c.Check(snapInfo2, DeepEquals, snapInfo1) snapInfo3, err := snap.ReadCurrentInfo("sample_other") c.Check(snapInfo3, IsNil) c.Assert(err, ErrorMatches, `cannot find current revision for snap sample_other:.*`) } func (s *infoSuite) TestInstallDate(c *C) { si := &snap.SideInfo{Revision: snap.R(1)} info := snaptest.MockSnap(c, sampleYaml, si) // not current -> Zero c.Check(info.InstallDate().IsZero(), Equals, true) c.Check(snap.InstallDate(info.InstanceName()).IsZero(), Equals, true) mountdir := info.MountDir() dir, rev := filepath.Split(mountdir) c.Assert(os.MkdirAll(dir, 0755), IsNil) cur := filepath.Join(dir, "current") c.Assert(os.Symlink(rev, cur), IsNil) st, err := os.Lstat(cur) c.Assert(err, IsNil) instTime := st.ModTime() // sanity c.Check(instTime.IsZero(), Equals, false) c.Check(info.InstallDate().Equal(instTime), Equals, true) c.Check(snap.InstallDate(info.InstanceName()).Equal(instTime), Equals, true) } func (s *infoSuite) TestReadInfoNotFound(c *C) { si := &snap.SideInfo{Revision: snap.R(42), EditedSummary: "esummary"} info, err := snap.ReadInfo("sample", si) c.Check(info, IsNil) c.Check(err, ErrorMatches, `cannot find installed snap "sample" at revision 42: missing file .*sample/42/meta/snap.yaml`) bse, ok := err.(snap.BrokenSnapError) c.Assert(ok, Equals, true) c.Check(bse.Broken(), Equals, bse.Error()) } func (s *infoSuite) TestReadInfoUnreadable(c *C) { si := &snap.SideInfo{Revision: snap.R(42), EditedSummary: "esummary"} c.Assert(os.MkdirAll(filepath.Join(snap.MinimalPlaceInfo("sample", si.Revision).MountDir(), "meta", "snap.yaml"), 0755), IsNil) info, err := snap.ReadInfo("sample", si) c.Check(info, IsNil) // TODO: maybe improve this error message c.Check(err, ErrorMatches, ".* is a directory") } func (s *infoSuite) TestReadInfoUnparsable(c *C) { si := &snap.SideInfo{Revision: snap.R(42), EditedSummary: "esummary"} p := filepath.Join(snap.MinimalPlaceInfo("sample", si.Revision).MountDir(), "meta", "snap.yaml") c.Assert(os.MkdirAll(filepath.Dir(p), 0755), IsNil) c.Assert(ioutil.WriteFile(p, []byte(`- :`), 0644), IsNil) info, err := snap.ReadInfo("sample", si) c.Check(info, IsNil) // TODO: maybe improve this error message c.Check(err, ErrorMatches, `cannot use installed snap "sample" at revision 42: cannot parse snap.yaml: yaml: .*`) bse, ok := err.(snap.BrokenSnapError) c.Assert(ok, Equals, true) c.Check(bse.Broken(), Equals, bse.Error()) } func (s *infoSuite) TestReadInfoUnfindable(c *C) { si := &snap.SideInfo{Revision: snap.R(42), EditedSummary: "esummary"} p := filepath.Join(snap.MinimalPlaceInfo("sample", si.Revision).MountDir(), "meta", "snap.yaml") c.Assert(os.MkdirAll(filepath.Dir(p), 0755), IsNil) c.Assert(ioutil.WriteFile(p, []byte(``), 0644), IsNil) info, err := snap.ReadInfo("sample", si) c.Check(err, ErrorMatches, `cannot find installed snap "sample" at revision 42: missing file .*var/lib/snapd/snaps/sample_42.snap`) c.Check(info, IsNil) } func (s *infoSuite) TestReadInfoDanglingSymlink(c *C) { si := &snap.SideInfo{Revision: snap.R(42), EditedSummary: "esummary"} mpi := snap.MinimalPlaceInfo("sample", si.Revision) p := filepath.Join(mpi.MountDir(), "meta", "snap.yaml") c.Assert(os.MkdirAll(filepath.Dir(p), 0755), IsNil) c.Assert(ioutil.WriteFile(p, []byte(`name: test`), 0644), IsNil) c.Assert(os.MkdirAll(filepath.Dir(mpi.MountFile()), 0755), IsNil) c.Assert(os.Symlink("/dangling", mpi.MountFile()), IsNil) info, err := snap.ReadInfo("sample", si) c.Check(err, IsNil) c.Check(info.SnapName(), Equals, "test") c.Check(info.Revision, Equals, snap.R(42)) c.Check(info.Summary(), Equals, "esummary") c.Check(info.Size, Equals, int64(0)) } // makeTestSnap here can also be used to produce broken snaps (differently from snaptest.MakeTestSnapWithFiles)! func makeTestSnap(c *C, snapYaml string) string { var m struct { Type string `yaml:"type"` } yaml.Unmarshal([]byte(snapYaml), &m) // yes, ignore the error tmp := c.MkDir() snapSource := filepath.Join(tmp, "snapsrc") err := os.MkdirAll(filepath.Join(snapSource, "meta"), 0755) c.Assert(err, IsNil) // our regular snap.yaml err = ioutil.WriteFile(filepath.Join(snapSource, "meta", "snap.yaml"), []byte(snapYaml), 0644) c.Assert(err, IsNil) dest := filepath.Join(tmp, "foo.snap") snap := squashfs.New(dest) err = snap.Build(snapSource, m.Type) c.Assert(err, IsNil) return dest } // produce descrs for empty hooks suitable for snaptest.PopulateDir func emptyHooks(hookNames ...string) (emptyHooks [][]string) { for _, hookName := range hookNames { emptyHooks = append(emptyHooks, []string{filepath.Join("meta", "hooks", hookName), ""}) } return } func (s *infoSuite) TestReadInfoFromSnapFile(c *C) { yaml := `name: foo version: 1.0 type: app epoch: 1* confinement: devmode` snapPath := snaptest.MakeTestSnapWithFiles(c, yaml, nil) snapf, err := snap.Open(snapPath) c.Assert(err, IsNil) info, err := snap.ReadInfoFromSnapFile(snapf, nil) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "foo") c.Check(info.Version, Equals, "1.0") c.Check(info.Type, Equals, snap.TypeApp) c.Check(info.Revision, Equals, snap.R(0)) c.Check(info.Epoch.String(), Equals, "1*") c.Check(info.Confinement, Equals, snap.DevModeConfinement) c.Check(info.NeedsDevMode(), Equals, true) c.Check(info.NeedsClassic(), Equals, false) } func (s *infoSuite) TestReadInfoFromClassicSnapFile(c *C) { yaml := `name: foo version: 1.0 type: app confinement: classic` snapPath := snaptest.MakeTestSnapWithFiles(c, yaml, nil) snapf, err := snap.Open(snapPath) c.Assert(err, IsNil) info, err := snap.ReadInfoFromSnapFile(snapf, nil) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "foo") c.Check(info.Version, Equals, "1.0") c.Check(info.Type, Equals, snap.TypeApp) c.Check(info.Revision, Equals, snap.R(0)) c.Check(info.Confinement, Equals, snap.ClassicConfinement) c.Check(info.NeedsDevMode(), Equals, false) c.Check(info.NeedsClassic(), Equals, true) } func (s *infoSuite) TestReadInfoFromSnapFileMissingEpoch(c *C) { yaml := `name: foo version: 1.0 type: app` snapPath := snaptest.MakeTestSnapWithFiles(c, yaml, nil) snapf, err := snap.Open(snapPath) c.Assert(err, IsNil) info, err := snap.ReadInfoFromSnapFile(snapf, nil) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "foo") c.Check(info.Version, Equals, "1.0") c.Check(info.Type, Equals, snap.TypeApp) c.Check(info.Revision, Equals, snap.R(0)) c.Check(info.Epoch.String(), Equals, "0") // Defaults to 0 c.Check(info.Confinement, Equals, snap.StrictConfinement) c.Check(info.NeedsDevMode(), Equals, false) } func (s *infoSuite) TestReadInfoFromSnapFileWithSideInfo(c *C) { yaml := `name: foo version: 1.0 type: app` snapPath := snaptest.MakeTestSnapWithFiles(c, yaml, nil) snapf, err := snap.Open(snapPath) c.Assert(err, IsNil) info, err := snap.ReadInfoFromSnapFile(snapf, &snap.SideInfo{ RealName: "baz", Revision: snap.R(42), }) c.Assert(err, IsNil) c.Check(info.InstanceName(), Equals, "baz") c.Check(info.Version, Equals, "1.0") c.Check(info.Type, Equals, snap.TypeApp) c.Check(info.Revision, Equals, snap.R(42)) } func (s *infoSuite) TestReadInfoFromSnapFileValidates(c *C) { yaml := `name: foo.bar version: 1.0 type: app` snapPath := makeTestSnap(c, yaml) snapf, err := snap.Open(snapPath) c.Assert(err, IsNil) _, err = snap.ReadInfoFromSnapFile(snapf, nil) c.Assert(err, ErrorMatches, `invalid snap name.*`) } func (s *infoSuite) TestReadInfoFromSnapFileCatchesInvalidType(c *C) { yaml := `name: foo version: 1.0 type: foo` snapPath := makeTestSnap(c, yaml) snapf, err := snap.Open(snapPath) c.Assert(err, IsNil) _, err = snap.ReadInfoFromSnapFile(snapf, nil) c.Assert(err, ErrorMatches, ".*invalid snap type.*") } func (s *infoSuite) TestReadInfoFromSnapFileCatchesInvalidConfinement(c *C) { yaml := `name: foo version: 1.0 confinement: foo` snapPath := makeTestSnap(c, yaml) snapf, err := snap.Open(snapPath) c.Assert(err, IsNil) _, err = snap.ReadInfoFromSnapFile(snapf, nil) c.Assert(err, ErrorMatches, ".*invalid confinement type.*") } func (s *infoSuite) TestAppEnvSimple(c *C) { yaml := `name: foo version: 1.0 type: app environment: global-k: global-v apps: foo: environment: app-k: app-v ` info, err := snap.InfoFromSnapYaml([]byte(yaml)) c.Assert(err, IsNil) env := info.Apps["foo"].Env() sort.Strings(env) c.Check(env, DeepEquals, []string{ "app-k=app-v", "global-k=global-v", }) } func (s *infoSuite) TestAppEnvOverrideGlobal(c *C) { yaml := `name: foo version: 1.0 type: app environment: global-k: global-v global-and-local: global-v apps: foo: environment: app-k: app-v global-and-local: local-v ` info, err := snap.InfoFromSnapYaml([]byte(yaml)) c.Assert(err, IsNil) env := info.Apps["foo"].Env() sort.Strings(env) c.Check(env, DeepEquals, []string{ "app-k=app-v", "global-and-local=local-v", "global-k=global-v", }) } func (s *infoSuite) TestHookEnvSimple(c *C) { yaml := `name: foo version: 1.0 type: app environment: global-k: global-v hooks: foo: environment: app-k: app-v ` info, err := snap.InfoFromSnapYaml([]byte(yaml)) c.Assert(err, IsNil) env := info.Hooks["foo"].Env() sort.Strings(env) c.Check(env, DeepEquals, []string{ "app-k=app-v", "global-k=global-v", }) } func (s *infoSuite) TestHookEnvOverrideGlobal(c *C) { yaml := `name: foo version: 1.0 type: app environment: global-k: global-v global-and-local: global-v hooks: foo: environment: app-k: app-v global-and-local: local-v ` info, err := snap.InfoFromSnapYaml([]byte(yaml)) c.Assert(err, IsNil) env := info.Hooks["foo"].Env() sort.Strings(env) c.Check(env, DeepEquals, []string{ "app-k=app-v", "global-and-local=local-v", "global-k=global-v", }) } func (s *infoSuite) TestSplitSnapApp(c *C) { for _, t := range []struct { in string out []string }{ // normal cases {"foo.bar", []string{"foo", "bar"}}, {"foo.bar.baz", []string{"foo", "bar.baz"}}, // special case, snapName == appName {"foo", []string{"foo", "foo"}}, // snap instance names {"foo_instance.bar", []string{"foo_instance", "bar"}}, {"foo_instance.bar.baz", []string{"foo_instance", "bar.baz"}}, {"foo_instance", []string{"foo_instance", "foo"}}, } { snap, app := snap.SplitSnapApp(t.in) c.Check([]string{snap, app}, DeepEquals, t.out) } } func (s *infoSuite) TestJoinSnapApp(c *C) { for _, t := range []struct { in []string out string }{ // normal cases {[]string{"foo", "bar"}, "foo.bar"}, {[]string{"foo", "bar-baz"}, "foo.bar-baz"}, // special case, snapName == appName {[]string{"foo", "foo"}, "foo"}, // snap instance names {[]string{"foo_instance", "bar"}, "foo_instance.bar"}, {[]string{"foo_instance", "bar-baz"}, "foo_instance.bar-baz"}, {[]string{"foo_instance", "foo"}, "foo_instance"}, } { snapApp := snap.JoinSnapApp(t.in[0], t.in[1]) c.Check(snapApp, Equals, t.out) } } func ExampleSplitSnapApp() { fmt.Println(snap.SplitSnapApp("hello-world.env")) // Output: hello-world env } func ExampleSplitSnapApp_short() { fmt.Println(snap.SplitSnapApp("hello-world")) // Output: hello-world hello-world } func (s *infoSuite) TestReadInfoFromSnapFileCatchesInvalidHook(c *C) { yaml := `name: foo version: 1.0 hooks: 123abc:` snapPath := makeTestSnap(c, yaml) snapf, err := snap.Open(snapPath) c.Assert(err, IsNil) _, err = snap.ReadInfoFromSnapFile(snapf, nil) c.Assert(err, ErrorMatches, ".*invalid hook name.*") } func (s *infoSuite) TestReadInfoFromSnapFileCatchesInvalidImplicitHook(c *C) { yaml := `name: foo version: 1.0` snapPath := snaptest.MakeTestSnapWithFiles(c, yaml, emptyHooks("123abc")) snapf, err := snap.Open(snapPath) c.Assert(err, IsNil) _, err = snap.ReadInfoFromSnapFile(snapf, nil) c.Assert(err, ErrorMatches, ".*invalid hook name.*") } func (s *infoSuite) checkInstalledSnapAndSnapFile(c *C, instanceName, yaml string, contents string, hooks []string, checker func(c *C, info *snap.Info)) { // First check installed snap sideInfo := &snap.SideInfo{Revision: snap.R(42)} info0 := snaptest.MockSnapInstance(c, instanceName, yaml, sideInfo) snaptest.PopulateDir(info0.MountDir(), emptyHooks(hooks...)) info, err := snap.ReadInfo(info0.InstanceName(), sideInfo) c.Check(err, IsNil) checker(c, info) // Now check snap file snapPath := snaptest.MakeTestSnapWithFiles(c, yaml, emptyHooks(hooks...)) snapf, err := snap.Open(snapPath) c.Assert(err, IsNil) info, err = snap.ReadInfoFromSnapFile(snapf, nil) c.Check(err, IsNil) checker(c, info) } func (s *infoSuite) TestReadInfoNoHooks(c *C) { yaml := `name: foo version: 1.0` s.checkInstalledSnapAndSnapFile(c, "foo", yaml, "SNAP", nil, func(c *C, info *snap.Info) { // Verify that no hooks were loaded for this snap c.Check(info.Hooks, HasLen, 0) }) } func (s *infoSuite) TestReadInfoSingleImplicitHook(c *C) { yaml := `name: foo version: 1.0` s.checkInstalledSnapAndSnapFile(c, "foo", yaml, "SNAP", []string{"test-hook"}, func(c *C, info *snap.Info) { // Verify that the `test-hook` hook has now been loaded, and that it has // no associated plugs. c.Check(info.Hooks, HasLen, 1) verifyImplicitHook(c, info, "test-hook", nil) }) } func (s *infoSuite) TestReadInfoMultipleImplicitHooks(c *C) { yaml := `name: foo version: 1.0` s.checkInstalledSnapAndSnapFile(c, "foo", yaml, "SNAP", []string{"foo", "bar"}, func(c *C, info *snap.Info) { // Verify that both hooks have now been loaded, and that neither have any // associated plugs. c.Check(info.Hooks, HasLen, 2) verifyImplicitHook(c, info, "foo", nil) verifyImplicitHook(c, info, "bar", nil) }) } func (s *infoSuite) TestReadInfoInvalidImplicitHook(c *C) { hookType := snap.NewHookType(regexp.MustCompile("foo")) s.BaseTest.AddCleanup(snap.MockSupportedHookTypes([]*snap.HookType{hookType})) yaml := `name: foo version: 1.0` s.checkInstalledSnapAndSnapFile(c, "foo", yaml, "SNAP", []string{"foo", "bar"}, func(c *C, info *snap.Info) { // Verify that only foo has been loaded, not bar c.Check(info.Hooks, HasLen, 1) verifyImplicitHook(c, info, "foo", nil) }) } func (s *infoSuite) TestReadInfoImplicitAndExplicitHooks(c *C) { yaml := `name: foo version: 1.0 hooks: explicit: plugs: [test-plug] slots: [test-slot]` s.checkInstalledSnapAndSnapFile(c, "foo", yaml, "SNAP", []string{"explicit", "implicit"}, func(c *C, info *snap.Info) { // Verify that the `implicit` hook has now been loaded, and that it has // no associated plugs. Also verify that the `explicit` hook is still // valid. c.Check(info.Hooks, HasLen, 2) verifyImplicitHook(c, info, "implicit", nil) verifyExplicitHook(c, info, "explicit", []string{"test-plug"}, []string{"test-slot"}) }) } func (s *infoSuite) TestReadInfoExplicitHooks(c *C) { yaml := `name: foo version: 1.0 plugs: test-plug: slots: test-slot: hooks: explicit: ` s.checkInstalledSnapAndSnapFile(c, "foo", yaml, "SNAP", []string{"explicit"}, func(c *C, info *snap.Info) { c.Check(info.Hooks, HasLen, 1) verifyExplicitHook(c, info, "explicit", []string{"test-plug"}, []string{"test-slot"}) }) } func (s *infoSuite) TestParallelInstanceReadInfoImplicitAndExplicitHooks(c *C) { yaml := `name: foo version: 1.0 hooks: explicit: plugs: [test-plug] slots: [test-slot]` s.checkInstalledSnapAndSnapFile(c, "foo_instance", yaml, "SNAP", []string{"explicit", "implicit"}, func(c *C, info *snap.Info) { c.Check(info.Hooks, HasLen, 2) verifyImplicitHook(c, info, "implicit", nil) verifyExplicitHook(c, info, "explicit", []string{"test-plug"}, []string{"test-slot"}) }) } func (s *infoSuite) TestReadInfoImplicitHookWithTopLevelPlugSlots(c *C) { yaml := `name: foo version: 1.0 plugs: test-plug: slots: test-slot: hooks: explicit: plugs: [test-plug,other-plug] slots: [test-slot,other-slot] ` yaml2 := `name: foo version: 1.0 plugs: test-plug: slots: test-slot: ` s.checkInstalledSnapAndSnapFile(c, "foo", yaml, "SNAP", []string{"implicit"}, func(c *C, info *snap.Info) { c.Check(info.Hooks, HasLen, 2) implicitHook := info.Hooks["implicit"] c.Assert(implicitHook, NotNil) c.Assert(implicitHook.Explicit, Equals, false) c.Assert(implicitHook.Plugs, HasLen, 1) c.Assert(implicitHook.Slots, HasLen, 1) c.Check(info.Plugs, HasLen, 2) c.Check(info.Slots, HasLen, 2) plug := info.Plugs["test-plug"] c.Assert(plug, NotNil) c.Assert(implicitHook.Plugs["test-plug"], DeepEquals, plug) slot := info.Slots["test-slot"] c.Assert(slot, NotNil) c.Assert(implicitHook.Slots["test-slot"], DeepEquals, slot) explicitHook := info.Hooks["explicit"] c.Assert(explicitHook, NotNil) c.Assert(explicitHook.Explicit, Equals, true) c.Assert(explicitHook.Plugs, HasLen, 2) c.Assert(explicitHook.Slots, HasLen, 2) plug = info.Plugs["test-plug"] c.Assert(plug, NotNil) c.Assert(explicitHook.Plugs["test-plug"], DeepEquals, plug) slot = info.Slots["test-slot"] c.Assert(slot, NotNil) c.Assert(explicitHook.Slots["test-slot"], DeepEquals, slot) }) s.checkInstalledSnapAndSnapFile(c, "foo", yaml2, "SNAP", []string{"implicit"}, func(c *C, info *snap.Info) { c.Check(info.Hooks, HasLen, 1) implicitHook := info.Hooks["implicit"] c.Assert(implicitHook, NotNil) c.Assert(implicitHook.Explicit, Equals, false) c.Assert(implicitHook.Plugs, HasLen, 1) c.Assert(implicitHook.Slots, HasLen, 1) c.Check(info.Plugs, HasLen, 1) c.Check(info.Slots, HasLen, 1) plug := info.Plugs["test-plug"] c.Assert(plug, NotNil) c.Assert(implicitHook.Plugs["test-plug"], DeepEquals, plug) slot := info.Slots["test-slot"] c.Assert(slot, NotNil) c.Assert(implicitHook.Slots["test-slot"], DeepEquals, slot) }) } func verifyImplicitHook(c *C, info *snap.Info, hookName string, plugNames []string) { hook := info.Hooks[hookName] c.Assert(hook, NotNil, Commentf("Expected hooks to contain %q", hookName)) c.Check(hook.Name, Equals, hookName) if len(plugNames) == 0 { c.Check(hook.Plugs, IsNil) } for _, plugName := range plugNames { // Verify that the HookInfo and PlugInfo point to each other plug := hook.Plugs[plugName] c.Assert(plug, NotNil, Commentf("Expected hook plugs to contain %q", plugName)) c.Check(plug.Name, Equals, plugName) c.Check(plug.Hooks, HasLen, 1) hook = plug.Hooks[hookName] c.Assert(hook, NotNil, Commentf("Expected plug to be associated with hook %q", hookName)) c.Check(hook.Name, Equals, hookName) // Verify also that the hook plug made it into info.Plugs c.Check(info.Plugs[plugName], DeepEquals, plug) } } func verifyExplicitHook(c *C, info *snap.Info, hookName string, plugNames []string, slotNames []string) { hook := info.Hooks[hookName] c.Assert(hook, NotNil, Commentf("Expected hooks to contain %q", hookName)) c.Check(hook.Name, Equals, hookName) c.Check(hook.Plugs, HasLen, len(plugNames)) c.Check(hook.Slots, HasLen, len(slotNames)) for _, plugName := range plugNames { // Verify that the HookInfo and PlugInfo point to each other plug := hook.Plugs[plugName] c.Assert(plug, NotNil, Commentf("Expected hook plugs to contain %q", plugName)) c.Check(plug.Name, Equals, plugName) c.Check(plug.Hooks, HasLen, 1) hook = plug.Hooks[hookName] c.Assert(hook, NotNil, Commentf("Expected plug to be associated with hook %q", hookName)) c.Check(hook.Name, Equals, hookName) // Verify also that the hook plug made it into info.Plugs c.Check(info.Plugs[plugName], DeepEquals, plug) } for _, slotName := range slotNames { // Verify that the HookInfo and SlotInfo point to each other slot := hook.Slots[slotName] c.Assert(slot, NotNil, Commentf("Expected hook slots to contain %q", slotName)) c.Check(slot.Name, Equals, slotName) c.Check(slot.Hooks, HasLen, 1) hook = slot.Hooks[hookName] c.Assert(hook, NotNil, Commentf("Expected slot to be associated with hook %q", hookName)) c.Check(hook.Name, Equals, hookName) // Verify also that the hook plug made it into info.Slots c.Check(info.Slots[slotName], DeepEquals, slot) } } func (s *infoSuite) TestMinimalInfoDirAndFileMethods(c *C) { dirs.SetRootDir("") info := snap.MinimalPlaceInfo("name", snap.R("1")) s.testDirAndFileMethods(c, info) } func (s *infoSuite) TestDirAndFileMethods(c *C) { dirs.SetRootDir("") info := &snap.Info{SuggestedName: "name"} info.SideInfo = snap.SideInfo{Revision: snap.R(1)} s.testDirAndFileMethods(c, info) } func (s *infoSuite) testDirAndFileMethods(c *C, info snap.PlaceInfo) { c.Check(info.MountDir(), Equals, fmt.Sprintf("%s/name/1", dirs.SnapMountDir)) c.Check(info.MountFile(), Equals, "/var/lib/snapd/snaps/name_1.snap") c.Check(info.HooksDir(), Equals, fmt.Sprintf("%s/name/1/meta/hooks", dirs.SnapMountDir)) c.Check(info.DataDir(), Equals, "/var/snap/name/1") c.Check(info.UserDataDir("/home/bob"), Equals, "/home/bob/snap/name/1") c.Check(info.UserCommonDataDir("/home/bob"), Equals, "/home/bob/snap/name/common") c.Check(info.CommonDataDir(), Equals, "/var/snap/name/common") c.Check(info.UserXdgRuntimeDir(12345), Equals, "/run/user/12345/snap.name") // XXX: Those are actually a globs, not directories c.Check(info.DataHomeDir(), Equals, "/home/*/snap/name/1") c.Check(info.CommonDataHomeDir(), Equals, "/home/*/snap/name/common") c.Check(info.XdgRuntimeDirs(), Equals, "/run/user/*/snap.name") } func (s *infoSuite) TestMinimalInfoDirAndFileMethodsParallelInstall(c *C) { dirs.SetRootDir("") info := snap.MinimalPlaceInfo("name_instance", snap.R("1")) s.testInstanceDirAndFileMethods(c, info) } func (s *infoSuite) TestDirAndFileMethodsParallelInstall(c *C) { dirs.SetRootDir("") info := &snap.Info{SuggestedName: "name", InstanceKey: "instance"} info.SideInfo = snap.SideInfo{Revision: snap.R(1)} s.testInstanceDirAndFileMethods(c, info) } func (s *infoSuite) testInstanceDirAndFileMethods(c *C, info snap.PlaceInfo) { c.Check(info.MountDir(), Equals, fmt.Sprintf("%s/name_instance/1", dirs.SnapMountDir)) c.Check(info.MountFile(), Equals, "/var/lib/snapd/snaps/name_instance_1.snap") c.Check(info.HooksDir(), Equals, fmt.Sprintf("%s/name_instance/1/meta/hooks", dirs.SnapMountDir)) c.Check(info.DataDir(), Equals, "/var/snap/name_instance/1") c.Check(info.UserDataDir("/home/bob"), Equals, "/home/bob/snap/name_instance/1") c.Check(info.UserCommonDataDir("/home/bob"), Equals, "/home/bob/snap/name_instance/common") c.Check(info.CommonDataDir(), Equals, "/var/snap/name_instance/common") c.Check(info.UserXdgRuntimeDir(12345), Equals, "/run/user/12345/snap.name_instance") // XXX: Those are actually a globs, not directories c.Check(info.DataHomeDir(), Equals, "/home/*/snap/name_instance/1") c.Check(info.CommonDataHomeDir(), Equals, "/home/*/snap/name_instance/common") c.Check(info.XdgRuntimeDirs(), Equals, "/run/user/*/snap.name_instance") } func makeFakeDesktopFile(c *C, name, content string) string { df := filepath.Join(dirs.SnapDesktopFilesDir, name) err := os.MkdirAll(filepath.Dir(df), 0755) c.Assert(err, IsNil) err = ioutil.WriteFile(df, []byte(content), 0644) c.Assert(err, IsNil) return df } func (s *infoSuite) TestAppDesktopFile(c *C) { snaptest.MockSnap(c, sampleYaml, &snap.SideInfo{}) snapInfo, err := snap.ReadInfo("sample", &snap.SideInfo{}) c.Assert(err, IsNil) c.Check(snapInfo.InstanceName(), Equals, "sample") c.Check(snapInfo.Apps["app"].DesktopFile(), Matches, `.*/var/lib/snapd/desktop/applications/sample_app.desktop`) c.Check(snapInfo.Apps["sample"].DesktopFile(), Matches, `.*/var/lib/snapd/desktop/applications/sample_sample.desktop`) // snap with instance key snapInfo.InstanceKey = "instance" c.Check(snapInfo.InstanceName(), Equals, "sample_instance") c.Check(snapInfo.Apps["app"].DesktopFile(), Matches, `.*/var/lib/snapd/desktop/applications/sample_instance_app.desktop`) c.Check(snapInfo.Apps["sample"].DesktopFile(), Matches, `.*/var/lib/snapd/desktop/applications/sample_instance_sample.desktop`) } const coreSnapYaml = `name: core version: 0 type: os plugs: network-bind: core-support: ` // reading snap via ReadInfoFromSnapFile renames clashing core plugs func (s *infoSuite) TestReadInfoFromSnapFileRenamesCorePlus(c *C) { snapPath := snaptest.MakeTestSnapWithFiles(c, coreSnapYaml, nil) snapf, err := snap.Open(snapPath) c.Assert(err, IsNil) info, err := snap.ReadInfoFromSnapFile(snapf, nil) c.Assert(err, IsNil) c.Check(info.Plugs["network-bind"], IsNil) c.Check(info.Plugs["core-support"], IsNil) c.Check(info.Plugs["network-bind-plug"], NotNil) c.Check(info.Plugs["core-support-plug"], NotNil) } // reading snap via ReadInfo renames clashing core plugs func (s *infoSuite) TestReadInfoRenamesCorePlugs(c *C) { si := &snap.SideInfo{Revision: snap.R(42), RealName: "core"} snaptest.MockSnap(c, coreSnapYaml, si) info, err := snap.ReadInfo("core", si) c.Assert(err, IsNil) c.Check(info.Plugs["network-bind"], IsNil) c.Check(info.Plugs["core-support"], IsNil) c.Check(info.Plugs["network-bind-plug"], NotNil) c.Check(info.Plugs["core-support-plug"], NotNil) } // reading snap via InfoFromSnapYaml renames clashing core plugs func (s *infoSuite) TestInfoFromSnapYamlRenamesCorePlugs(c *C) { info, err := snap.InfoFromSnapYaml([]byte(coreSnapYaml)) c.Assert(err, IsNil) c.Check(info.Plugs["network-bind"], IsNil) c.Check(info.Plugs["core-support"], IsNil) c.Check(info.Plugs["network-bind-plug"], NotNil) c.Check(info.Plugs["core-support-plug"], NotNil) } func (s *infoSuite) TestInfoServices(c *C) { info, err := snap.InfoFromSnapYaml([]byte(`name: pans apps: svc1: daemon: potato svc2: daemon: no app1: app2: `)) c.Assert(err, IsNil) svcNames := []string{} svcs := info.Services() for i := range svcs { svcNames = append(svcNames, svcs[i].ServiceName()) } sort.Strings(svcNames) c.Check(svcNames, DeepEquals, []string{ "snap.pans.svc1.service", "snap.pans.svc2.service", }) // snap with instance info.InstanceKey = "instance" svcNames = []string{} for i := range info.Services() { svcNames = append(svcNames, svcs[i].ServiceName()) } sort.Strings(svcNames) c.Check(svcNames, DeepEquals, []string{ "snap.pans_instance.svc1.service", "snap.pans_instance.svc2.service", }) } func (s *infoSuite) TestAppInfoIsService(c *C) { info, err := snap.InfoFromSnapYaml([]byte(`name: pans apps: svc1: daemon: potato svc2: daemon: no app1: app2: `)) c.Assert(err, IsNil) svc := info.Apps["svc1"] c.Check(svc.IsService(), Equals, true) c.Check(svc.ServiceName(), Equals, "snap.pans.svc1.service") c.Check(svc.ServiceFile(), Equals, dirs.GlobalRootDir+"/etc/systemd/system/snap.pans.svc1.service") c.Check(info.Apps["svc2"].IsService(), Equals, true) c.Check(info.Apps["app1"].IsService(), Equals, false) c.Check(info.Apps["app1"].IsService(), Equals, false) // snap with instance key info.InstanceKey = "instance" c.Check(svc.ServiceName(), Equals, "snap.pans_instance.svc1.service") c.Check(svc.ServiceFile(), Equals, dirs.GlobalRootDir+"/etc/systemd/system/snap.pans_instance.svc1.service") } func (s *infoSuite) TestAppInfoStringer(c *C) { info, err := snap.InfoFromSnapYaml([]byte(`name: asnap apps: one: daemon: simple `)) c.Assert(err, IsNil) c.Check(fmt.Sprintf("%q", info.Apps["one"].String()), Equals, `"asnap.one"`) } func (s *infoSuite) TestSocketFile(c *C) { info, err := snap.InfoFromSnapYaml([]byte(`name: pans apps: app1: daemon: true sockets: sock1: listen-stream: /tmp/sock1.socket `)) c.Assert(err, IsNil) app := info.Apps["app1"] socket := app.Sockets["sock1"] c.Check(socket.File(), Equals, dirs.GlobalRootDir+"/etc/systemd/system/snap.pans.app1.sock1.socket") // snap with instance key info.InstanceKey = "instance" c.Check(socket.File(), Equals, dirs.GlobalRootDir+"/etc/systemd/system/snap.pans_instance.app1.sock1.socket") } func (s *infoSuite) TestTimerFile(c *C) { info, err := snap.InfoFromSnapYaml([]byte(`name: pans apps: app1: daemon: true timer: mon,10:00-12:00 `)) c.Assert(err, IsNil) app := info.Apps["app1"] timerFile := app.Timer.File() c.Check(timerFile, Equals, dirs.GlobalRootDir+"/etc/systemd/system/snap.pans.app1.timer") c.Check(strings.TrimSuffix(app.ServiceFile(), ".service")+".timer", Equals, timerFile) // snap with instance key info.InstanceKey = "instance" c.Check(app.Timer.File(), Equals, dirs.GlobalRootDir+"/etc/systemd/system/snap.pans_instance.app1.timer") } func (s *infoSuite) TestLayoutParsing(c *C) { info, err := snap.InfoFromSnapYaml([]byte(`name: layout-demo layout: /usr: bind: $SNAP/usr /mytmp: type: tmpfs mode: 1777 /mylink: symlink: /link/target `)) c.Assert(err, IsNil) layout := info.Layout c.Assert(layout, NotNil) c.Check(layout["/usr"], DeepEquals, &snap.Layout{ Snap: info, Path: "/usr", User: "root", Group: "root", Mode: 0755, Bind: "$SNAP/usr", }) c.Check(layout["/mytmp"], DeepEquals, &snap.Layout{ Snap: info, Path: "/mytmp", Type: "tmpfs", User: "root", Group: "root", Mode: 01777, }) c.Check(layout["/mylink"], DeepEquals, &snap.Layout{ Snap: info, Path: "/mylink", User: "root", Group: "root", Mode: 0755, Symlink: "/link/target", }) } func (s *infoSuite) TestPlugInfoString(c *C) { plug := &snap.PlugInfo{Snap: &snap.Info{SuggestedName: "snap"}, Name: "plug"} c.Assert(plug.String(), Equals, "snap:plug") } func (s *infoSuite) TestSlotInfoString(c *C) { slot := &snap.SlotInfo{Snap: &snap.Info{SuggestedName: "snap"}, Name: "slot"} c.Assert(slot.String(), Equals, "snap:slot") } func (s *infoSuite) TestPlugInfoAttr(c *C) { var val string var intVal int plug := &snap.PlugInfo{Snap: &snap.Info{SuggestedName: "snap"}, Name: "plug", Interface: "interface", Attrs: map[string]interface{}{"key": "value", "number": int(123)}} c.Assert(plug.Attr("key", &val), IsNil) c.Check(val, Equals, "value") c.Assert(plug.Attr("number", &intVal), IsNil) c.Check(intVal, Equals, 123) c.Check(plug.Attr("key", &intVal), ErrorMatches, `snap "snap" has interface "interface" with invalid value type for "key" attribute`) c.Check(plug.Attr("unknown", &val), ErrorMatches, `snap "snap" does not have attribute "unknown" for interface "interface"`) c.Check(plug.Attr("key", intVal), ErrorMatches, `internal error: cannot get "key" attribute of interface "interface" with non-pointer value`) } func (s *infoSuite) TestSlotInfoAttr(c *C) { var val string var intVal int slot := &snap.SlotInfo{Snap: &snap.Info{SuggestedName: "snap"}, Name: "plug", Interface: "interface", Attrs: map[string]interface{}{"key": "value", "number": int(123)}} c.Assert(slot.Attr("key", &val), IsNil) c.Check(val, Equals, "value") c.Assert(slot.Attr("number", &intVal), IsNil) c.Check(intVal, Equals, 123) c.Check(slot.Attr("key", &intVal), ErrorMatches, `snap "snap" has interface "interface" with invalid value type for "key" attribute`) c.Check(slot.Attr("unknown", &val), ErrorMatches, `snap "snap" does not have attribute "unknown" for interface "interface"`) c.Check(slot.Attr("key", intVal), ErrorMatches, `internal error: cannot get "key" attribute of interface "interface" with non-pointer value`) } func (s *infoSuite) TestDottedPathSlot(c *C) { attrs := map[string]interface{}{ "nested": map[string]interface{}{ "foo": "bar", }, } slot := &snap.SlotInfo{Attrs: attrs} c.Assert(slot, NotNil) v, ok := slot.Lookup("nested.foo") c.Assert(ok, Equals, true) c.Assert(v, Equals, "bar") v, ok = slot.Lookup("nested") c.Assert(ok, Equals, true) c.Assert(v, DeepEquals, map[string]interface{}{ "foo": "bar", }) _, ok = slot.Lookup("x") c.Assert(ok, Equals, false) _, ok = slot.Lookup("..") c.Assert(ok, Equals, false) _, ok = slot.Lookup("nested.foo.x") c.Assert(ok, Equals, false) _, ok = slot.Lookup("nested.x") c.Assert(ok, Equals, false) } func (s *infoSuite) TestDottedPathPlug(c *C) { attrs := map[string]interface{}{ "nested": map[string]interface{}{ "foo": "bar", }, } plug := &snap.PlugInfo{Attrs: attrs} c.Assert(plug, NotNil) v, ok := plug.Lookup("nested") c.Assert(ok, Equals, true) c.Assert(v, DeepEquals, map[string]interface{}{ "foo": "bar", }) v, ok = plug.Lookup("nested.foo") c.Assert(ok, Equals, true) c.Assert(v, Equals, "bar") _, ok = plug.Lookup("x") c.Assert(ok, Equals, false) _, ok = plug.Lookup("..") c.Assert(ok, Equals, false) _, ok = plug.Lookup("nested.foo.x") c.Assert(ok, Equals, false) } func (s *infoSuite) TestExpandSnapVariables(c *C) { dirs.SetRootDir("") info, err := snap.InfoFromSnapYaml([]byte(`name: foo`)) c.Assert(err, IsNil) info.Revision = snap.R(42) c.Assert(info.ExpandSnapVariables("$SNAP/stuff"), Equals, "/snap/foo/42/stuff") c.Assert(info.ExpandSnapVariables("$SNAP_DATA/stuff"), Equals, "/var/snap/foo/42/stuff") c.Assert(info.ExpandSnapVariables("$SNAP_COMMON/stuff"), Equals, "/var/snap/foo/common/stuff") c.Assert(info.ExpandSnapVariables("$GARBAGE/rocks"), Equals, "/rocks") info.InstanceKey = "instance" // Despite setting the instance key the variables expand to the same // value as before. This is because they are used from inside the mount // namespace of the instantiated snap where the mount backend will // ensure that the regular (non-instance) paths contain // instance-specific code and data. c.Assert(info.ExpandSnapVariables("$SNAP/stuff"), Equals, "/snap/foo/42/stuff") c.Assert(info.ExpandSnapVariables("$SNAP_DATA/stuff"), Equals, "/var/snap/foo/42/stuff") c.Assert(info.ExpandSnapVariables("$SNAP_COMMON/stuff"), Equals, "/var/snap/foo/common/stuff") c.Assert(info.ExpandSnapVariables("$GARBAGE/rocks"), Equals, "/rocks") } func (s *infoSuite) TestStopModeTypeKillMode(c *C) { for _, t := range []struct { stopMode string killall bool }{ {"", true}, {"sigterm", false}, {"sigterm-all", true}, {"sighup", false}, {"sighup-all", true}, {"sigusr1", false}, {"sigusr1-all", true}, {"sigusr2", false}, {"sigusr2-all", true}, } { c.Check(snap.StopModeType(t.stopMode).KillAll(), Equals, t.killall, Commentf("wrong KillAll for %v", t.stopMode)) } } func (s *infoSuite) TestStopModeTypeKillSignal(c *C) { for _, t := range []struct { stopMode string killSig string }{ {"", ""}, {"sigterm", "SIGTERM"}, {"sigterm-all", "SIGTERM"}, {"sighup", "SIGHUP"}, {"sighup-all", "SIGHUP"}, {"sigusr1", "SIGUSR1"}, {"sigusr1-all", "SIGUSR1"}, {"sigusr2", "SIGUSR2"}, {"sigusr2-all", "SIGUSR2"}, } { c.Check(snap.StopModeType(t.stopMode).KillSignal(), Equals, t.killSig) } } func (s *infoSuite) TestSplitInstanceName(c *C) { snapName, instanceKey := snap.SplitInstanceName("foo_bar") c.Check(snapName, Equals, "foo") c.Check(instanceKey, Equals, "bar") snapName, instanceKey = snap.SplitInstanceName("foo") c.Check(snapName, Equals, "foo") c.Check(instanceKey, Equals, "") // all following instance names are invalid snapName, instanceKey = snap.SplitInstanceName("_bar") c.Check(snapName, Equals, "") c.Check(instanceKey, Equals, "bar") snapName, instanceKey = snap.SplitInstanceName("foo___bar_bar") c.Check(snapName, Equals, "foo") c.Check(instanceKey, Equals, "__bar_bar") snapName, instanceKey = snap.SplitInstanceName("") c.Check(snapName, Equals, "") c.Check(instanceKey, Equals, "") } func (s *infoSuite) TestInstanceSnapName(c *C) { c.Check(snap.InstanceSnap("foo_bar"), Equals, "foo") c.Check(snap.InstanceSnap("foo"), Equals, "foo") c.Check(snap.InstanceName("foo", "bar"), Equals, "foo_bar") c.Check(snap.InstanceName("foo", ""), Equals, "foo") } func (s *infoSuite) TestInstanceNameInSnapInfo(c *C) { info := &snap.Info{ SuggestedName: "snap-name", InstanceKey: "foo", } c.Check(info.InstanceName(), Equals, "snap-name_foo") c.Check(info.SnapName(), Equals, "snap-name") info.InstanceKey = "" c.Check(info.InstanceName(), Equals, "snap-name") c.Check(info.SnapName(), Equals, "snap-name") } func (s *infoSuite) TestIsActive(c *C) { info1 := snaptest.MockSnap(c, sampleYaml, &snap.SideInfo{Revision: snap.R(1)}) info2 := snaptest.MockSnap(c, sampleYaml, &snap.SideInfo{Revision: snap.R(2)}) // no current -> not active c.Check(info1.IsActive(), Equals, false) c.Check(info2.IsActive(), Equals, false) mountdir := info1.MountDir() dir, rev := filepath.Split(mountdir) c.Assert(os.MkdirAll(dir, 0755), IsNil) cur := filepath.Join(dir, "current") c.Assert(os.Symlink(rev, cur), IsNil) // is current -> is active c.Check(info1.IsActive(), Equals, true) c.Check(info2.IsActive(), Equals, false) } func (s *infoSuite) TestDirAndFileHelpers(c *C) { dirs.SetRootDir("") c.Check(snap.MountDir("name", snap.R(1)), Equals, fmt.Sprintf("%s/name/1", dirs.SnapMountDir)) c.Check(snap.MountFile("name", snap.R(1)), Equals, "/var/lib/snapd/snaps/name_1.snap") c.Check(snap.HooksDir("name", snap.R(1)), Equals, fmt.Sprintf("%s/name/1/meta/hooks", dirs.SnapMountDir)) c.Check(snap.DataDir("name", snap.R(1)), Equals, "/var/snap/name/1") c.Check(snap.CommonDataDir("name"), Equals, "/var/snap/name/common") c.Check(snap.UserDataDir("/home/bob", "name", snap.R(1)), Equals, "/home/bob/snap/name/1") c.Check(snap.UserCommonDataDir("/home/bob", "name"), Equals, "/home/bob/snap/name/common") c.Check(snap.UserXdgRuntimeDir(12345, "name"), Equals, "/run/user/12345/snap.name") c.Check(snap.UserSnapDir("/home/bob", "name"), Equals, "/home/bob/snap/name") c.Check(snap.MountDir("name_instance", snap.R(1)), Equals, fmt.Sprintf("%s/name_instance/1", dirs.SnapMountDir)) c.Check(snap.MountFile("name_instance", snap.R(1)), Equals, "/var/lib/snapd/snaps/name_instance_1.snap") c.Check(snap.HooksDir("name_instance", snap.R(1)), Equals, fmt.Sprintf("%s/name_instance/1/meta/hooks", dirs.SnapMountDir)) c.Check(snap.DataDir("name_instance", snap.R(1)), Equals, "/var/snap/name_instance/1") c.Check(snap.CommonDataDir("name_instance"), Equals, "/var/snap/name_instance/common") c.Check(snap.UserDataDir("/home/bob", "name_instance", snap.R(1)), Equals, "/home/bob/snap/name_instance/1") c.Check(snap.UserCommonDataDir("/home/bob", "name_instance"), Equals, "/home/bob/snap/name_instance/common") c.Check(snap.UserXdgRuntimeDir(12345, "name_instance"), Equals, "/run/user/12345/snap.name_instance") c.Check(snap.UserSnapDir("/home/bob", "name_instance"), Equals, "/home/bob/snap/name_instance") } func (s *infoSuite) TestSortByType(c *C) { infos := []*snap.Info{ {SuggestedName: "app1", Type: "app"}, {SuggestedName: "os1", Type: "os"}, {SuggestedName: "base1", Type: "base"}, {SuggestedName: "gadget1", Type: "gadget"}, {SuggestedName: "kernel1", Type: "kernel"}, {SuggestedName: "app2", Type: "app"}, {SuggestedName: "os2", Type: "os"}, {SuggestedName: "snapd", Type: "snapd"}, {SuggestedName: "base2", Type: "base"}, {SuggestedName: "gadget2", Type: "gadget"}, {SuggestedName: "kernel2", Type: "kernel"}, } sort.Stable(snap.ByType(infos)) c.Check(infos, DeepEquals, []*snap.Info{ {SuggestedName: "snapd", Type: "snapd"}, {SuggestedName: "os1", Type: "os"}, {SuggestedName: "os2", Type: "os"}, {SuggestedName: "kernel1", Type: "kernel"}, {SuggestedName: "kernel2", Type: "kernel"}, {SuggestedName: "base1", Type: "base"}, {SuggestedName: "base2", Type: "base"}, {SuggestedName: "gadget1", Type: "gadget"}, {SuggestedName: "gadget2", Type: "gadget"}, {SuggestedName: "app1", Type: "app"}, {SuggestedName: "app2", Type: "app"}, }) } func (s *infoSuite) TestSortByTypeAgain(c *C) { core := &snap.Info{Type: snap.TypeOS} base := &snap.Info{Type: snap.TypeBase} app := &snap.Info{Type: snap.TypeApp} snapd := &snap.Info{} snapd.SideInfo = snap.SideInfo{RealName: "snapd"} byType := func(snaps ...*snap.Info) []*snap.Info { sort.Stable(snap.ByType(snaps)) return snaps } c.Check(byType(base, core), DeepEquals, []*snap.Info{core, base}) c.Check(byType(app, core), DeepEquals, []*snap.Info{core, app}) c.Check(byType(app, base), DeepEquals, []*snap.Info{base, app}) c.Check(byType(app, base, core), DeepEquals, []*snap.Info{core, base, app}) c.Check(byType(app, core, base), DeepEquals, []*snap.Info{core, base, app}) c.Check(byType(app, core, base, snapd), DeepEquals, []*snap.Info{snapd, core, base, app}) c.Check(byType(app, snapd, core, base), DeepEquals, []*snap.Info{snapd, core, base, app}) } func (s *infoSuite) TestMedia(c *C) { c.Check(snap.MediaInfos{}.Screenshots(), HasLen, 0) c.Check(snap.MediaInfos{}.IconURL(), Equals, "") media := snap.MediaInfos{ { Type: "screenshot", URL: "https://example.com/shot1.svg", }, { Type: "icon", URL: "https://example.com/icon.png", }, { Type: "screenshot", URL: "https://example.com/shot2.svg", Width: 42, Height: 17, }, } c.Check(media.IconURL(), Equals, "https://example.com/icon.png") c.Check(media.Screenshots(), DeepEquals, []snap.ScreenshotInfo{ { URL: "https://example.com/shot1.svg", Note: snap.ScreenshotsDeprecationNotice, }, { URL: "https://example.com/shot2.svg", Width: 42, Height: 17, Note: snap.ScreenshotsDeprecationNotice, }, }) } func (s *infoSuite) TestSortApps(c *C) { tcs := []struct { err string apps []*snap.AppInfo sorted []string }{{ apps: []*snap.AppInfo{ {Name: "bar", Before: []string{"baz"}}, {Name: "baz", After: []string{"bar", "foo"}}, {Name: "foo"}, }, sorted: []string{"bar", "foo", "baz"}, }, { apps: []*snap.AppInfo{ {Name: "foo", After: []string{"bar", "zed"}}, {Name: "bar", Before: []string{"foo"}}, {Name: "baz", After: []string{"foo"}}, {Name: "zed"}, }, sorted: []string{"bar", "zed", "foo", "baz"}, }, { apps: []*snap.AppInfo{ {Name: "foo", After: []string{"baz"}}, {Name: "bar", Before: []string{"baz"}}, {Name: "baz"}, {Name: "zed", After: []string{"foo", "bar", "baz"}}, }, sorted: []string{"bar", "baz", "foo", "zed"}, }, { apps: []*snap.AppInfo{ {Name: "foo", Before: []string{"bar"}, After: []string{"zed"}}, {Name: "bar", Before: []string{"baz"}}, {Name: "baz", Before: []string{"zed"}}, {Name: "zed"}, }, err: `applications are part of a before/after cycle: ((foo|bar|baz|zed)(, )?){4}`, }, { apps: []*snap.AppInfo{ {Name: "foo", Before: []string{"bar"}}, {Name: "bar", Before: []string{"foo"}}, {Name: "baz", Before: []string{"foo"}, After: []string{"bar"}}, }, err: `applications are part of a before/after cycle: ((foo|bar|baz)(, )?){3}`, }, { apps: []*snap.AppInfo{ {Name: "baz", After: []string{"bar"}}, {Name: "foo"}, {Name: "bar", After: []string{"foo"}}, }, sorted: []string{"foo", "bar", "baz"}, }} for _, tc := range tcs { sorted, err := snap.SortServices(tc.apps) if tc.err != "" { c.Assert(err, ErrorMatches, tc.err) } else { c.Assert(err, IsNil) c.Assert(sorted, HasLen, len(tc.sorted)) sortedNames := make([]string, len(sorted)) for i, app := range sorted { sortedNames[i] = app.Name } c.Assert(sortedNames, DeepEquals, tc.sorted) } } } snapd-2.37.4~14.04.1/snap/errors.go0000664000000000000000000000240013435556260013341 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2016 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 . * */ package snap import "fmt" type AlreadyInstalledError struct { Snap string } func (e AlreadyInstalledError) Error() string { return fmt.Sprintf("snap %q is already installed", e.Snap) } type NotInstalledError struct { Snap string Rev Revision } func (e NotInstalledError) Error() string { if e.Rev.Unset() { return fmt.Sprintf("snap %q is not installed", e.Snap) } return fmt.Sprintf("revision %s of snap %q is not installed", e.Rev, e.Snap) } type NotSnapError struct { Path string } func (e NotSnapError) Error() string { return fmt.Sprintf("%q is not a snap or snapdir", e.Path) } snapd-2.37.4~14.04.1/snap/export_test.go0000664000000000000000000000176113435556260014416 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package snap var ( ValidateSocketName = validateSocketName ValidateDescription = validateDescription ValidateTitle = validateTitle InfoFromSnapYamlWithSideInfo = infoFromSnapYamlWithSideInfo ) func (info *Info) ForceRenamePlug(oldName, newName string) { info.forceRenamePlug(oldName, newName) } snapd-2.37.4~14.04.1/snap/restartcond_test.go0000664000000000000000000000247113435556260015424 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2016 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 . * */ package snap_test import ( . "gopkg.in/check.v1" "gopkg.in/yaml.v2" "github.com/snapcore/snapd/snap" ) type restartcondSuite struct{} var _ = Suite(&restartcondSuite{}) func (*restartcondSuite) TestRestartCondUnmarshal(c *C) { for name, cond := range snap.RestartMap { bs := []byte(name) var rc snap.RestartCondition c.Check(yaml.Unmarshal(bs, &rc), IsNil) c.Check(rc, Equals, cond, Commentf(name)) } } func (restartcondSuite) TestRestartCondString(c *C) { for name, cond := range snap.RestartMap { if name == "never" { c.Check(cond.String(), Equals, "no") } else { c.Check(cond.String(), Equals, name, Commentf(name)) } } } snapd-2.37.4~14.04.1/snap/broken.go0000664000000000000000000000636013435556260013316 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2016 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 . * */ package snap import ( "fmt" "path/filepath" "strings" "github.com/snapcore/snapd/dirs" ) // GuessAppsForBroken guesses what apps and services a broken snap has // on the system by searching for matches based on the snap name in // the snap binaries and service file directories. It returns a // mapping from app names to partial AppInfo. func GuessAppsForBroken(info *Info) map[string]*AppInfo { out := make(map[string]*AppInfo) // guess binaries first name := info.InstanceName() for _, p := range []string{name, fmt.Sprintf("%s.*", name)} { matches, _ := filepath.Glob(filepath.Join(dirs.SnapBinariesDir, p)) for _, m := range matches { l := strings.SplitN(filepath.Base(m), ".", 2) var appname string if len(l) == 1 { // when app is named the same as snap, it will // be available under '' name, if the snap // was installed with instance key, the app will // be named `_' appname = InstanceSnap(l[0]) } else { appname = l[1] } out[appname] = &AppInfo{ Snap: info, Name: appname, } } } // guess the services next matches, _ := filepath.Glob(filepath.Join(dirs.SnapServicesDir, fmt.Sprintf("snap.%s.*.service", name))) for _, m := range matches { appname := strings.Split(m, ".")[2] out[appname] = &AppInfo{ Snap: info, Name: appname, Daemon: "simple", } } return out } // renameClashingCorePlugs renames plugs that clash with slot names on core snap. // // Some released core snaps had explicitly defined plugs "network-bind" and // "core-support" that clashed with implicit slots with the same names but this // was not validated before. To avoid a flag day and any potential issues, // transparently rename the two clashing plugs by appending the "-plug" suffix. func (info *Info) renameClashingCorePlugs() { if info.InstanceName() == "core" && info.Type == TypeOS { for _, plugName := range []string{"network-bind", "core-support"} { info.forceRenamePlug(plugName, plugName+"-plug") } } } // forceRenamePlug renames the plug from oldName to newName, if present. func (info *Info) forceRenamePlug(oldName, newName string) { if plugInfo, ok := info.Plugs[oldName]; ok { delete(info.Plugs, oldName) info.Plugs[newName] = plugInfo plugInfo.Name = newName for _, appInfo := range info.Apps { if _, ok := appInfo.Plugs[oldName]; ok { delete(appInfo.Plugs, oldName) appInfo.Plugs[newName] = plugInfo } } for _, hookInfo := range info.Hooks { if _, ok := hookInfo.Plugs[oldName]; ok { delete(hookInfo.Plugs, oldName) hookInfo.Plugs[newName] = plugInfo } } } } snapd-2.37.4~14.04.1/snap/gadget.go0000664000000000000000000001404513435556260013270 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package snap import ( "fmt" "io/ioutil" "os" "path/filepath" "strings" "gopkg.in/yaml.v2" ) type GadgetInfo struct { Volumes map[string]GadgetVolume `yaml:"volumes,omitempty"` // Default configuration for snaps (snap-id => key => value). Defaults map[string]map[string]interface{} `yaml:"defaults,omitempty"` Connections []GadgetConnection `yaml:"connections"` } type GadgetVolume struct { Schema string `yaml:"schema"` Bootloader string `yaml:"bootloader"` ID string `yaml:"id"` Structure []VolumeStructure `yaml:"structure"` } // TODO Offsets and sizes are strings to support unit suffixes. // Is that a good idea? *2^N or *10^N? We'll probably want a richer // type when we actually handle these. type VolumeStructure struct { Label string `yaml:"filesystem-label"` Offset string `yaml:"offset"` OffsetWrite string `yaml:"offset-write"` Size string `yaml:"size"` Type string `yaml:"type"` ID string `yaml:"id"` Filesystem string `yaml:"filesystem"` Content []VolumeContent `yaml:"content"` } type VolumeContent struct { Source string `yaml:"source"` Target string `yaml:"target"` Image string `yaml:"image"` Offset string `yaml:"offset"` OffsetWrite string `yaml:"offset-write"` Size string `yaml:"size"` Unpack bool `yaml:"unpack"` } // GadgetConnect describes an interface connection requested by the gadget // between seeded snaps. The syntax is of a mapping like: // // plug: (|system):plug // [slot: (|system):slot] // // "system" indicates a system plug or slot. // Fully omitting the slot part indicates a system slot with the same name // as the plug. type GadgetConnection struct { Plug GadgetConnectionPlug `yaml:"plug"` Slot GadgetConnectionSlot `yaml:"slot"` } type GadgetConnectionPlug struct { SnapID string Plug string } func (gcplug *GadgetConnectionPlug) Empty() bool { return gcplug.SnapID == "" && gcplug.Plug == "" } func (gcplug *GadgetConnectionPlug) UnmarshalYAML(unmarshal func(interface{}) error) error { var s string if err := unmarshal(&s); err != nil { return err } snapID, name, err := parseSnapIDColonName(s) if err != nil { return fmt.Errorf("in gadget connection plug: %v", err) } gcplug.SnapID = snapID gcplug.Plug = name return nil } type GadgetConnectionSlot struct { SnapID string Slot string } func (gcslot *GadgetConnectionSlot) Empty() bool { return gcslot.SnapID == "" && gcslot.Slot == "" } func (gcslot *GadgetConnectionSlot) UnmarshalYAML(unmarshal func(interface{}) error) error { var s string if err := unmarshal(&s); err != nil { return err } snapID, name, err := parseSnapIDColonName(s) if err != nil { return fmt.Errorf("in gadget connection slot: %v", err) } gcslot.SnapID = snapID gcslot.Slot = name return nil } func parseSnapIDColonName(s string) (snapID, name string, err error) { parts := strings.Split(s, ":") if len(parts) == 2 { snapID = parts[0] name = parts[1] } if snapID == "" || name == "" { return "", "", fmt.Errorf(`expected "(|system):name" not %q`, s) } return snapID, name, nil } func systemOrSnapID(s string) bool { if s != "system" && len(s) != validSnapIDLength { return false } return true } // ReadGadgetInfo reads the gadget specific metadata from gadget.yaml // in the snap. classic set to true means classic rules apply, // i.e. content/presence of gadget.yaml is fully optional. func ReadGadgetInfo(info *Info, classic bool) (*GadgetInfo, error) { const errorFormat = "cannot read gadget snap details: %s" if info.Type != TypeGadget { return nil, fmt.Errorf(errorFormat, "not a gadget snap") } var gi GadgetInfo gadgetYamlFn := filepath.Join(info.MountDir(), "meta", "gadget.yaml") gmeta, err := ioutil.ReadFile(gadgetYamlFn) if classic && os.IsNotExist(err) { // gadget.yaml is optional for classic gadgets return &gi, nil } if err != nil { return nil, fmt.Errorf(errorFormat, err) } if err := yaml.Unmarshal(gmeta, &gi); err != nil { return nil, fmt.Errorf(errorFormat, err) } for k, v := range gi.Defaults { if !systemOrSnapID(k) { return nil, fmt.Errorf(`default stanza not keyed by "system" or snap-id: %s`, k) } dflt, err := normalizeYamlValue(v) if err != nil { return nil, fmt.Errorf("default value %q of %q: %v", v, k, err) } gi.Defaults[k] = dflt.(map[string]interface{}) } for i, gconn := range gi.Connections { if gconn.Plug.Empty() { return nil, fmt.Errorf("gadget connection plug cannot be empty") } if gconn.Slot.Empty() { gi.Connections[i].Slot.SnapID = "system" gi.Connections[i].Slot.Slot = gconn.Plug.Plug } } if classic && len(gi.Volumes) == 0 { // volumes can be left out on classic // can still specify defaults though return &gi, nil } // basic validation var bootloadersFound int for _, v := range gi.Volumes { switch v.Bootloader { case "": // pass case "grub", "u-boot", "android-boot": bootloadersFound += 1 default: return nil, fmt.Errorf(errorFormat, "bootloader must be one of grub, u-boot or android-boot") } } switch { case bootloadersFound == 0: return nil, fmt.Errorf(errorFormat, "bootloader not declared in any volume") case bootloadersFound > 1: return nil, fmt.Errorf(errorFormat, fmt.Sprintf("too many (%d) bootloaders declared", bootloadersFound)) } return &gi, nil } snapd-2.37.4~14.04.1/snap/hooktypes.go0000664000000000000000000000453213435556260014062 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package snap import ( "regexp" ) var supportedHooks = []*HookType{ NewHookType(regexp.MustCompile("^prepare-device$")), NewHookType(regexp.MustCompile("^configure$")), NewHookType(regexp.MustCompile("^install$")), NewHookType(regexp.MustCompile("^pre-refresh$")), 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. type HookType struct { pattern *regexp.Regexp } // NewHookType returns a new HookType with the given pattern. func NewHookType(pattern *regexp.Regexp) *HookType { return &HookType{ pattern: pattern, } } // Match returns true if the given hook name matches this hook type. func (hookType HookType) Match(hookName string) bool { return hookType.pattern.MatchString(hookName) } // IsHookSupported returns true if the given hook name matches one of the // supported hooks. func IsHookSupported(hookName string) bool { for _, hookType := range supportedHooks { if hookType.Match(hookName) { return true } } return false } func MockSupportedHookTypes(hookTypes []*HookType) (restore func()) { old := supportedHooks supportedHooks = hookTypes return func() { supportedHooks = old } } func MockAppendSupportedHookTypes(hookTypes []*HookType) (restore func()) { old := supportedHooks supportedHooks = append(supportedHooks, hookTypes...) return func() { supportedHooks = old } } snapd-2.37.4~14.04.1/cmd/0000775000000000000000000000000013435556260011304 5ustar snapd-2.37.4~14.04.1/cmd/snap-update-ns/0000775000000000000000000000000013435556260014143 5ustar snapd-2.37.4~14.04.1/cmd/snap-update-ns/freezer.go0000664000000000000000000000506413435556260016141 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main import ( "bytes" "fmt" "io/ioutil" "os" "path/filepath" "time" ) var freezerCgroupDir = "/sys/fs/cgroup/freezer" // freezeSnapProcesses freezes all the processes originating from the given snap. // Processes are frozen regardless of which particular snap application they // originate from. func freezeSnapProcesses(snapName string) error { fname := filepath.Join(freezerCgroupDir, fmt.Sprintf("snap.%s", snapName), "freezer.state") if err := ioutil.WriteFile(fname, []byte("FROZEN"), 0644); err != nil && os.IsNotExist(err) { // When there's no freezer cgroup we don't have to freeze anything. // This can happen when no process belonging to a given snap has been // started yet. return nil } else if err != nil { return fmt.Errorf("cannot freeze processes of snap %q, %v", snapName, err) } for i := 0; i < 30; i++ { data, err := ioutil.ReadFile(fname) if err != nil { return fmt.Errorf("cannot determine the freeze state of processes of snap %q, %v", snapName, err) } // If the cgroup is still freezing then wait a moment and try again. if bytes.Equal(data, []byte("FREEZING")) { time.Sleep(100 * time.Millisecond) continue } return nil } // If we got here then we timed out after seeing FREEZING for too long. thawSnapProcesses(snapName) // ignore the error, this is best-effort. return fmt.Errorf("cannot finish freezing processes of snap %q", snapName) } func thawSnapProcesses(snapName string) error { fname := filepath.Join(freezerCgroupDir, fmt.Sprintf("snap.%s", snapName), "freezer.state") if err := ioutil.WriteFile(fname, []byte("THAWED"), 0644); err != nil && os.IsNotExist(err) { // When there's no freezer cgroup we don't have to thaw anything. // This can happen when no process belonging to a given snap has been // started yet. return nil } else if err != nil { return fmt.Errorf("cannot thaw processes of snap %q", snapName) } return nil } snapd-2.37.4~14.04.1/cmd/snap-update-ns/utils_test.go0000664000000000000000000014473313435556260016705 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main_test import ( "bytes" "fmt" "io/ioutil" "os" "path/filepath" "syscall" . "gopkg.in/check.v1" update "github.com/snapcore/snapd/cmd/snap-update-ns" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/osutil/sys" "github.com/snapcore/snapd/testutil" ) type utilsSuite struct { testutil.BaseTest sys *testutil.SyscallRecorder log *bytes.Buffer as *update.Assumptions } var _ = Suite(&utilsSuite{}) func (s *utilsSuite) SetUpTest(c *C) { s.BaseTest.SetUpTest(c) s.sys = &testutil.SyscallRecorder{} s.BaseTest.AddCleanup(update.MockSystemCalls(s.sys)) buf, restore := logger.MockLogger() s.BaseTest.AddCleanup(restore) s.log = buf s.as = &update.Assumptions{} } func (s *utilsSuite) TearDownTest(c *C) { s.BaseTest.TearDownTest(c) s.sys.CheckForStrayDescriptors(c) } // secure-mkdir-all // Ensure that we reject unclean paths. func (s *utilsSuite) TestSecureMkdirAllUnclean(c *C) { err := update.MkdirAll("/unclean//path", 0755, 123, 456, nil) c.Assert(err, ErrorMatches, `cannot split unclean path .*`) c.Assert(s.sys.RCalls(), HasLen, 0) } // Ensure that we refuse to create a directory with an relative path. func (s *utilsSuite) TestSecureMkdirAllRelative(c *C) { err := update.MkdirAll("rel/path", 0755, 123, 456, nil) c.Assert(err, ErrorMatches, `cannot create directory with relative path: "rel/path"`) c.Assert(s.sys.RCalls(), HasLen, 0) } // Ensure that we can "create the root directory. func (s *utilsSuite) TestSecureMkdirAllLevel0(c *C) { c.Assert(update.MkdirAll("/", 0755, 123, 456, nil), IsNil) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `close 3`}, }) } // Ensure that we can create a directory in the top-level directory. func (s *utilsSuite) TestSecureMkdirAllLevel1(c *C) { c.Assert(update.MkdirAll("/path", 0755, 123, 456, nil), IsNil) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "path" 0755`}, {C: `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `fchown 4 123 456`}, {C: `close 4`}, {C: `close 3`}, }) } // Ensure that we can create a directory two levels from the top-level directory. func (s *utilsSuite) TestSecureMkdirAllLevel2(c *C) { c.Assert(update.MkdirAll("/path/to", 0755, 123, 456, nil), IsNil) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "path" 0755`}, {C: `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `fchown 4 123 456`}, {C: `close 3`}, {C: `mkdirat 4 "to" 0755`}, {C: `openat 4 "to" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `fchown 3 123 456`}, {C: `close 3`}, {C: `close 4`}, }) } // Ensure that we can create a directory three levels from the top-level directory. func (s *utilsSuite) TestSecureMkdirAllLevel3(c *C) { c.Assert(update.MkdirAll("/path/to/something", 0755, 123, 456, nil), IsNil) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "path" 0755`}, {C: `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `fchown 4 123 456`}, {C: `mkdirat 4 "to" 0755`}, {C: `openat 4 "to" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5}, {C: `fchown 5 123 456`}, {C: `close 4`}, {C: `close 3`}, {C: `mkdirat 5 "something" 0755`}, {C: `openat 5 "something" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `fchown 3 123 456`}, {C: `close 3`}, {C: `close 5`}, }) } // Ensure that writes to /etc/demo are interrupted if /etc is restricted. func (s *utilsSuite) TestSecureMkdirAllWithRestrictedEtc(c *C) { s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.SquashfsMagic}) s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.Ext4Magic}) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST) rs := s.as.RestrictionsFor("/etc/demo") err := update.MkdirAll("/etc/demo", 0755, 123, 456, rs) c.Assert(err, ErrorMatches, `cannot write to "/etc/demo" because it would affect the host in "/etc"`) c.Assert(err.(*update.TrespassingError).ViolatedPath, Equals, "/etc") c.Assert(err.(*update.TrespassingError).DesiredPath, Equals, "/etc/demo") c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, // we are inspecting the type of the filesystem we are about to perform operation on. {C: `fstatfs 3 `, R: syscall.Statfs_t{Type: update.SquashfsMagic}}, {C: `fstat 3 `, R: syscall.Stat_t{}}, {C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST}, {C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `close 3`}, // ext4 is writable, refuse further operations. {C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.Ext4Magic}}, {C: `fstat 4 `, R: syscall.Stat_t{}}, {C: `close 4`}, }) } // Ensure that writes to /etc/demo allowed if /etc is unrestricted. func (s *utilsSuite) TestSecureMkdirAllWithUnrestrictedEtc(c *C) { defer s.as.MockUnrestrictedPaths("/etc")() // Mark /etc as unrestricted. s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST) rs := s.as.RestrictionsFor("/etc/demo") c.Assert(update.MkdirAll("/etc/demo", 0755, 123, 456, rs), IsNil) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, // We are not interested in the type of filesystem at / {C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST}, {C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, // We are not interested in the type of filesystem at /etc {C: `close 3`}, {C: `mkdirat 4 "demo" 0755`}, {C: `openat 4 "demo" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `fchown 3 123 456`}, {C: `close 3`}, {C: `close 4`}, }) } // Ensure that we can detect read only filesystems. func (s *utilsSuite) TestSecureMkdirAllROFS(c *C) { s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) // just realistic s.sys.InsertFault(`mkdirat 4 "path" 0755`, syscall.EROFS) err := update.MkdirAll("/rofs/path", 0755, 123, 456, nil) c.Assert(err, ErrorMatches, `cannot operate on read-only filesystem at /rofs`) c.Assert(err.(*update.ReadOnlyFsError).Path, Equals, "/rofs") c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `close 3`}, {C: `mkdirat 4 "path" 0755`, E: syscall.EROFS}, {C: `close 4`}, }) } // Ensure that we don't chown existing directories. func (s *utilsSuite) TestSecureMkdirAllExistingDirsDontChown(c *C) { s.sys.InsertFault(`mkdirat 3 "abs" 0755`, syscall.EEXIST) s.sys.InsertFault(`mkdirat 4 "path" 0755`, syscall.EEXIST) err := update.MkdirAll("/abs/path", 0755, 123, 456, nil) c.Assert(err, IsNil) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "abs" 0755`, E: syscall.EEXIST}, {C: `openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `close 3`}, {C: `mkdirat 4 "path" 0755`, E: syscall.EEXIST}, {C: `openat 4 "path" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `close 3`}, {C: `close 4`}, }) } // Ensure that we we close everything when mkdirat fails. func (s *utilsSuite) TestSecureMkdirAllMkdiratError(c *C) { s.sys.InsertFault(`mkdirat 3 "abs" 0755`, errTesting) err := update.MkdirAll("/abs", 0755, 123, 456, nil) c.Assert(err, ErrorMatches, `cannot create directory "/abs": testing`) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "abs" 0755`, E: errTesting}, {C: `close 3`}, }) } // Ensure that we we close everything when fchown fails. func (s *utilsSuite) TestSecureMkdirAllFchownError(c *C) { s.sys.InsertFault(`fchown 4 123 456`, errTesting) err := update.MkdirAll("/path", 0755, 123, 456, nil) c.Assert(err, ErrorMatches, `cannot chown directory "/path" to 123.456: testing`) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "path" 0755`}, {C: `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `fchown 4 123 456`, E: errTesting}, {C: `close 4`}, {C: `close 3`}, }) } // Check error path when we cannot open root directory. func (s *utilsSuite) TestSecureMkdirAllOpenRootError(c *C) { s.sys.InsertFault(`open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, errTesting) err := update.MkdirAll("/abs/path", 0755, 123, 456, nil) c.Assert(err, ErrorMatches, "cannot open root directory: testing") c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, E: errTesting}, }) } // Check error path when we cannot open non-root directory. func (s *utilsSuite) TestSecureMkdirAllOpenError(c *C) { s.sys.InsertFault(`openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, errTesting) err := update.MkdirAll("/abs/path", 0755, 123, 456, nil) c.Assert(err, ErrorMatches, `cannot open directory "/abs": testing`) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "abs" 0755`}, {C: `openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, E: errTesting}, {C: `close 3`}, }) } func (s *utilsSuite) TestPlanWritableMimic(c *C) { s.sys.InsertSysLstatResult(`lstat "/foo" `, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}) restore := update.MockReadDir(func(dir string) ([]os.FileInfo, error) { c.Assert(dir, Equals, "/foo") return []os.FileInfo{ testutil.FakeFileInfo("file", 0), testutil.FakeFileInfo("dir", os.ModeDir), testutil.FakeFileInfo("symlink", os.ModeSymlink), testutil.FakeFileInfo("error-symlink-readlink", os.ModeSymlink), // NOTE: None of the filesystem entries below are supported because // they cannot be placed inside snaps or can only be created at // runtime in areas that are already writable and this would never // have to be handled in a writable mimic. testutil.FakeFileInfo("block-dev", os.ModeDevice), testutil.FakeFileInfo("char-dev", os.ModeDevice|os.ModeCharDevice), testutil.FakeFileInfo("socket", os.ModeSocket), testutil.FakeFileInfo("pipe", os.ModeNamedPipe), }, nil }) defer restore() restore = update.MockReadlink(func(name string) (string, error) { switch name { case "/foo/symlink": return "target", nil case "/foo/error-symlink-readlink": return "", errTesting } panic("unexpected") }) defer restore() changes, err := update.PlanWritableMimic("/foo", "/foo/bar") c.Assert(err, IsNil) c.Assert(changes, DeepEquals, []*update.Change{ // Store /foo in /tmp/.snap/foo while we set things up {Entry: osutil.MountEntry{Name: "/foo", Dir: "/tmp/.snap/foo", Options: []string{"rbind"}}, Action: update.Mount}, // Put a tmpfs over /foo {Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/foo", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/foo/bar", "mode=0755", "uid=0", "gid=0"}}, Action: update.Mount}, // Bind mount files and directories over. Note that files are identified by x-snapd.kind=file option. {Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/file", Dir: "/foo/file", Options: []string{"bind", "x-snapd.kind=file", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, {Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/dir", Dir: "/foo/dir", Options: []string{"rbind", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, // Create symlinks. // Bad symlinks and all other file types are skipped and not // recorded in mount changes. {Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/symlink", Dir: "/foo/symlink", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=target", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, // Unmount the safe-keeping directory {Entry: osutil.MountEntry{Name: "none", Dir: "/tmp/.snap/foo", Options: []string{"x-snapd.detach"}}, Action: update.Unmount}, }) } func (s *utilsSuite) TestPlanWritableMimicErrors(c *C) { s.sys.InsertSysLstatResult(`lstat "/foo" `, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}) restore := update.MockReadDir(func(dir string) ([]os.FileInfo, error) { c.Assert(dir, Equals, "/foo") return nil, errTesting }) defer restore() restore = update.MockReadlink(func(name string) (string, error) { return "", errTesting }) defer restore() changes, err := update.PlanWritableMimic("/foo", "/foo/bar") c.Assert(err, ErrorMatches, "testing") c.Assert(changes, HasLen, 0) } func (s *utilsSuite) TestExecWirableMimicSuccess(c *C) { // This plan is the same as in the test above. This is what comes out of planWritableMimic. plan := []*update.Change{ {Entry: osutil.MountEntry{Name: "/foo", Dir: "/tmp/.snap/foo", Options: []string{"rbind"}}, Action: update.Mount}, {Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/foo", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, {Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/file", Dir: "/foo/file", Options: []string{"bind", "x-snapd.kind=file", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, {Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/dir", Dir: "/foo/dir", Options: []string{"rbind", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, {Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/symlink", Dir: "/foo/symlink", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=target", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, {Entry: osutil.MountEntry{Name: "none", Dir: "/tmp/.snap/foo", Options: []string{"x-snapd.detach"}}, Action: update.Unmount}, } // Mock the act of performing changes, each of the change we perform is coming from the plan. restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) { c.Assert(plan, testutil.DeepContains, chg) return nil, nil }) defer restore() // The executed plan leaves us with a simplified view of the plan that is suitable for undo. undoPlan, err := update.ExecWritableMimic(plan, s.as) c.Assert(err, IsNil) c.Assert(undoPlan, DeepEquals, []*update.Change{ {Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/foo", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, {Entry: osutil.MountEntry{Name: "/foo/file", Dir: "/foo/file", Options: []string{"bind", "x-snapd.kind=file", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, {Entry: osutil.MountEntry{Name: "/foo/dir", Dir: "/foo/dir", Options: []string{"rbind", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar", "x-snapd.detach"}}, Action: update.Mount}, }) } func (s *utilsSuite) TestExecWirableMimicErrorWithRecovery(c *C) { // This plan is the same as in the test above. This is what comes out of planWritableMimic. plan := []*update.Change{ {Entry: osutil.MountEntry{Name: "/foo", Dir: "/tmp/.snap/foo", Options: []string{"rbind"}}, Action: update.Mount}, {Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/foo", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, {Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/file", Dir: "/foo/file", Options: []string{"bind", "x-snapd.kind=file", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, {Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/symlink", Dir: "/foo/symlink", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=target", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, // NOTE: the next perform will fail. Notably the symlink did not fail. {Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/dir", Dir: "/foo/dir", Options: []string{"rbind"}}, Action: update.Mount}, {Entry: osutil.MountEntry{Name: "none", Dir: "/tmp/.snap/foo", Options: []string{"x-snapd.detach"}}, Action: update.Unmount}, } // Mock the act of performing changes. Before we inject a failure we ensure // that each of the change we perform is coming from the plan. For the // purpose of the test the change that bind mounts the "dir" over itself // will fail and will trigger an recovery path. The changes performed in // the recovery path are recorded. var recoveryPlan []*update.Change recovery := false restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) { if !recovery { c.Assert(plan, testutil.DeepContains, chg) if chg.Entry.Name == "/tmp/.snap/foo/dir" { recovery = true // switch to recovery mode return nil, errTesting } } else { recoveryPlan = append(recoveryPlan, chg) } return nil, nil }) defer restore() // The executed plan fails, leaving us with the error and an empty undo plan. undoPlan, err := update.ExecWritableMimic(plan, s.as) c.Assert(err, Equals, errTesting) c.Assert(undoPlan, HasLen, 0) // The changes we managed to perform were undone correctly. c.Assert(recoveryPlan, DeepEquals, []*update.Change{ // NOTE: there is no symlink undo entry as it is implicitly undone by unmounting the tmpfs. {Entry: osutil.MountEntry{Name: "/foo/file", Dir: "/foo/file", Options: []string{"bind", "x-snapd.kind=file", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Unmount}, {Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/foo", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Unmount}, {Entry: osutil.MountEntry{Name: "/foo", Dir: "/tmp/.snap/foo", Options: []string{"rbind", "x-snapd.detach"}}, Action: update.Unmount}, }) } func (s *utilsSuite) TestExecWirableMimicErrorNothingDone(c *C) { // This plan is the same as in the test above. This is what comes out of planWritableMimic. plan := []*update.Change{ {Entry: osutil.MountEntry{Name: "/foo", Dir: "/tmp/.snap/foo", Options: []string{"rbind"}}, Action: update.Mount}, {Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/foo", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, {Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/file", Dir: "/foo/file", Options: []string{"bind", "x-snapd.kind=file", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, {Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/dir", Dir: "/foo/dir", Options: []string{"rbind", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, {Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/symlink", Dir: "/foo/symlink", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=target", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, {Entry: osutil.MountEntry{Name: "none", Dir: "/tmp/.snap/foo", Options: []string{"x-snapd.detach"}}, Action: update.Unmount}, } // Mock the act of performing changes and just fail on any request. restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) { return nil, errTesting }) defer restore() // The executed plan fails, the recovery didn't fail (it's empty) so we just return that error. undoPlan, err := update.ExecWritableMimic(plan, s.as) c.Assert(err, Equals, errTesting) c.Assert(undoPlan, HasLen, 0) } func (s *utilsSuite) TestExecWirableMimicErrorCannotUndo(c *C) { // This plan is the same as in the test above. This is what comes out of planWritableMimic. plan := []*update.Change{ {Entry: osutil.MountEntry{Name: "/foo", Dir: "/tmp/.snap/foo", Options: []string{"rbind"}}, Action: update.Mount}, {Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/foo", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, {Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/file", Dir: "/foo/file", Options: []string{"bind", "x-snapd.kind=file", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, {Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/dir", Dir: "/foo/dir", Options: []string{"rbind", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, {Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/symlink", Dir: "/foo/symlink", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=target", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, {Entry: osutil.MountEntry{Name: "none", Dir: "/tmp/.snap/foo", Options: []string{"x-snapd.detach"}}, Action: update.Unmount}, } // Mock the act of performing changes. After performing the first change // correctly we will fail forever (this includes the recovery path) so the // execute function ends up in a situation where it cannot perform the // recovery path and will have to return a fatal error. i := -1 restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) { i++ if i > 0 { return nil, fmt.Errorf("failure-%d", i) } return nil, nil }) defer restore() // The plan partially succeeded and we cannot undo those changes. _, err := update.ExecWritableMimic(plan, s.as) c.Assert(err, ErrorMatches, `cannot undo change ".*" while recovering from earlier error failure-1: failure-2`) c.Assert(err, FitsTypeOf, &update.FatalError{}) } // realSystemSuite is not isolated / mocked from the system. type realSystemSuite struct { as *update.Assumptions } var _ = Suite(&realSystemSuite{}) func (s *realSystemSuite) SetUpTest(c *C) { s.as = &update.Assumptions{} s.as.AddUnrestrictedPaths("/tmp") } // Check that we can actually create directories. // This doesn't test the chown logic as that requires root. func (s *realSystemSuite) TestSecureMkdirAllForReal(c *C) { d := c.MkDir() // Create d (which already exists) with mode 0777 (but c.MkDir() used 0700 // internally and since we are not creating the directory we should not be // changing that. c.Assert(update.MkdirAll(d, 0777, sys.FlagID, sys.FlagID, nil), IsNil) fi, err := os.Stat(d) c.Assert(err, IsNil) c.Check(fi.IsDir(), Equals, true) c.Check(fi.Mode().Perm(), Equals, os.FileMode(0700)) // Create d1, which is a simple subdirectory, with a distinct mode and // check that it was applied. Note that default umask 022 is subtracted so // effective directory has different permissions. d1 := filepath.Join(d, "subdir") c.Assert(update.MkdirAll(d1, 0707, sys.FlagID, sys.FlagID, nil), IsNil) fi, err = os.Stat(d1) c.Assert(err, IsNil) c.Check(fi.IsDir(), Equals, true) c.Check(fi.Mode().Perm(), Equals, os.FileMode(0705)) // Create d2, which is a deeper subdirectory, with another distinct mode // and check that it was applied. d2 := filepath.Join(d, "subdir/subdir/subdir") c.Assert(update.MkdirAll(d2, 0750, sys.FlagID, sys.FlagID, nil), IsNil) fi, err = os.Stat(d2) c.Assert(err, IsNil) c.Check(fi.IsDir(), Equals, true) c.Check(fi.Mode().Perm(), Equals, os.FileMode(0750)) } // secure-mkfile-all // Ensure that we reject unclean paths. func (s *utilsSuite) TestSecureMkfileAllUnclean(c *C) { err := update.MkfileAll("/unclean//path", 0755, 123, 456, nil) c.Assert(err, ErrorMatches, `cannot split unclean path .*`) c.Assert(s.sys.RCalls(), HasLen, 0) } // Ensure that we refuse to create a file with an relative path. func (s *utilsSuite) TestSecureMkfileAllRelative(c *C) { err := update.MkfileAll("rel/path", 0755, 123, 456, nil) c.Assert(err, ErrorMatches, `cannot create file with relative path: "rel/path"`) c.Assert(s.sys.RCalls(), HasLen, 0) } // Ensure that we refuse creating the root directory as a file. func (s *utilsSuite) TestSecureMkfileAllLevel0(c *C) { err := update.MkfileAll("/", 0755, 123, 456, nil) c.Assert(err, ErrorMatches, `cannot create non-file path: "/"`) c.Assert(s.sys.RCalls(), HasLen, 0) } // Ensure that we can create a file in the top-level directory. func (s *utilsSuite) TestSecureMkfileAllLevel1(c *C) { c.Assert(update.MkfileAll("/path", 0755, 123, 456, nil), IsNil) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, R: 4}, {C: `fchown 4 123 456`}, {C: `close 4`}, {C: `close 3`}, }) } // Ensure that we can create a file two levels from the top-level directory. func (s *utilsSuite) TestSecureMkfileAllLevel2(c *C) { c.Assert(update.MkfileAll("/path/to", 0755, 123, 456, nil), IsNil) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "path" 0755`}, {C: `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `fchown 4 123 456`}, {C: `close 3`}, {C: `openat 4 "to" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, R: 3}, {C: `fchown 3 123 456`}, {C: `close 3`}, {C: `close 4`}, }) } // Ensure that we can create a file three levels from the top-level directory. func (s *utilsSuite) TestSecureMkfileAllLevel3(c *C) { c.Assert(update.MkfileAll("/path/to/something", 0755, 123, 456, nil), IsNil) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "path" 0755`}, {C: `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `fchown 4 123 456`}, {C: `mkdirat 4 "to" 0755`}, {C: `openat 4 "to" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5}, {C: `fchown 5 123 456`}, {C: `close 4`}, {C: `close 3`}, {C: `openat 5 "something" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, R: 3}, {C: `fchown 3 123 456`}, {C: `close 3`}, {C: `close 5`}, }) } // Ensure that we can detect read only filesystems. func (s *utilsSuite) TestSecureMkfileAllROFS(c *C) { s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) // just realistic s.sys.InsertFault(`openat 4 "path" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, syscall.EROFS) err := update.MkfileAll("/rofs/path", 0755, 123, 456, nil) c.Check(err, ErrorMatches, `cannot operate on read-only filesystem at /rofs`) c.Assert(err.(*update.ReadOnlyFsError).Path, Equals, "/rofs") c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `close 3`}, {C: `openat 4 "path" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, E: syscall.EROFS}, {C: `close 4`}, }) } // Ensure that we don't chown existing files or directories. func (s *utilsSuite) TestSecureMkfileAllExistingDirsDontChown(c *C) { s.sys.InsertFault(`mkdirat 3 "abs" 0755`, syscall.EEXIST) s.sys.InsertFault(`openat 4 "path" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, syscall.EEXIST) err := update.MkfileAll("/abs/path", 0755, 123, 456, nil) c.Check(err, IsNil) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "abs" 0755`, E: syscall.EEXIST}, {C: `openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `close 3`}, {C: `openat 4 "path" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, E: syscall.EEXIST}, {C: `openat 4 "path" O_NOFOLLOW|O_CLOEXEC 0`, R: 3}, {C: `close 3`}, {C: `close 4`}, }) } // Ensure that we we close everything when openat fails. func (s *utilsSuite) TestSecureMkfileAllOpenat2ndError(c *C) { s.sys.InsertFault(`openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, syscall.EEXIST) s.sys.InsertFault(`openat 3 "abs" O_NOFOLLOW|O_CLOEXEC 0`, errTesting) err := update.MkfileAll("/abs", 0755, 123, 456, nil) c.Assert(err, ErrorMatches, `cannot open file "/abs": testing`) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, E: syscall.EEXIST}, {C: `openat 3 "abs" O_NOFOLLOW|O_CLOEXEC 0`, E: errTesting}, {C: `close 3`}, }) } // Ensure that we we close everything when openat (non-exclusive) fails. func (s *utilsSuite) TestSecureMkfileAllOpenatError(c *C) { s.sys.InsertFault(`openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, errTesting) err := update.MkfileAll("/abs", 0755, 123, 456, nil) c.Assert(err, ErrorMatches, `cannot open file "/abs": testing`) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, E: errTesting}, {C: `close 3`}, }) } // Ensure that we we close everything when fchown fails. func (s *utilsSuite) TestSecureMkfileAllFchownError(c *C) { s.sys.InsertFault(`fchown 4 123 456`, errTesting) err := update.MkfileAll("/path", 0755, 123, 456, nil) c.Assert(err, ErrorMatches, `cannot chown file "/path" to 123.456: testing`) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, R: 4}, {C: `fchown 4 123 456`, E: errTesting}, {C: `close 4`}, {C: `close 3`}, }) } // Check error path when we cannot open root directory. func (s *utilsSuite) TestSecureMkfileAllOpenRootError(c *C) { s.sys.InsertFault(`open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, errTesting) err := update.MkfileAll("/abs/path", 0755, 123, 456, nil) c.Assert(err, ErrorMatches, "cannot open root directory: testing") c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, E: errTesting}, }) } // Check error path when we cannot open non-root directory. func (s *utilsSuite) TestSecureMkfileAllOpenError(c *C) { s.sys.InsertFault(`openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, errTesting) err := update.MkfileAll("/abs/path", 0755, 123, 456, nil) c.Assert(err, ErrorMatches, `cannot open directory "/abs": testing`) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "abs" 0755`}, {C: `openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, E: errTesting}, {C: `close 3`}, }) } // We want to create a symlink in $SNAP_DATA and that's fine. func (s *utilsSuite) TestSecureMksymlinkAllInSnapData(c *C) { s.sys.InsertFault(`mkdirat 3 "var" 0755`, syscall.EEXIST) s.sys.InsertFault(`mkdirat 4 "snap" 0755`, syscall.EEXIST) s.sys.InsertFault(`mkdirat 5 "foo" 0755`, syscall.EEXIST) s.sys.InsertFault(`mkdirat 6 "42" 0755`, syscall.EEXIST) err := update.MksymlinkAll("/var/snap/foo/42/symlink", 0755, 0, 0, "/oldname", nil) c.Assert(err, IsNil) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "var" 0755`, E: syscall.EEXIST}, {C: `openat 3 "var" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `mkdirat 4 "snap" 0755`, E: syscall.EEXIST}, {C: `openat 4 "snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5}, {C: `mkdirat 5 "foo" 0755`, E: syscall.EEXIST}, {C: `openat 5 "foo" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 6}, {C: `mkdirat 6 "42" 0755`, E: syscall.EEXIST}, {C: `openat 6 "42" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 7}, {C: `close 6`}, {C: `close 5`}, {C: `close 4`}, {C: `close 3`}, {C: `symlinkat "/oldname" 7 "symlink"`}, {C: `close 7`}, }) } // We want to create a symlink in /etc but the host filesystem would be affected. func (s *utilsSuite) TestSecureMksymlinkAllInEtc(c *C) { s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.SquashfsMagic}) s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.Ext4Magic}) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST) rs := s.as.RestrictionsFor("/etc/symlink") err := update.MksymlinkAll("/etc/symlink", 0755, 0, 0, "/oldname", rs) c.Assert(err, ErrorMatches, `cannot write to "/etc/symlink" because it would affect the host in "/etc"`) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `fstatfs 3 `, R: syscall.Statfs_t{Type: update.SquashfsMagic}}, {C: `fstat 3 `, R: syscall.Stat_t{}}, {C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST}, {C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `close 3`}, {C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.Ext4Magic}}, {C: `fstat 4 `, R: syscall.Stat_t{}}, {C: `close 4`}, }) } // We want to create a symlink deep in /etc but the host filesystem would be affected. // This just shows that we pick the right place to construct the mimic func (s *utilsSuite) TestSecureMksymlinkAllDeepInEtc(c *C) { s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.SquashfsMagic}) s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.Ext4Magic}) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST) rs := s.as.RestrictionsFor("/etc/some/other/stuff/symlink") err := update.MksymlinkAll("/etc/some/other/stuff/symlink", 0755, 0, 0, "/oldname", rs) c.Assert(err, ErrorMatches, `cannot write to "/etc/some/other/stuff/symlink" because it would affect the host in "/etc"`) c.Assert(err.(*update.TrespassingError).ViolatedPath, Equals, "/etc") c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `fstatfs 3 `, R: syscall.Statfs_t{Type: update.SquashfsMagic}}, {C: `fstat 3 `, R: syscall.Stat_t{}}, {C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST}, {C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.Ext4Magic}}, {C: `fstat 4 `, R: syscall.Stat_t{}}, {C: `close 4`}, {C: `close 3`}, }) } // We want to create a file in /etc but the host filesystem would be affected. func (s *utilsSuite) TestSecureMkfileAllInEtc(c *C) { s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.SquashfsMagic}) s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.Ext4Magic}) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST) rs := s.as.RestrictionsFor("/etc/file") err := update.MkfileAll("/etc/file", 0755, 0, 0, rs) c.Assert(err, ErrorMatches, `cannot write to "/etc/file" because it would affect the host in "/etc"`) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `fstatfs 3 `, R: syscall.Statfs_t{Type: update.SquashfsMagic}}, {C: `fstat 3 `, R: syscall.Stat_t{}}, {C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST}, {C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `close 3`}, {C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.Ext4Magic}}, {C: `fstat 4 `, R: syscall.Stat_t{}}, {C: `close 4`}, }) } // We want to create a directory in /etc but the host filesystem would be affected. func (s *utilsSuite) TestSecureMkdirAllInEtc(c *C) { s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.SquashfsMagic}) s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.Ext4Magic}) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST) rs := s.as.RestrictionsFor("/etc/dir") err := update.MkdirAll("/etc/dir", 0755, 0, 0, rs) c.Assert(err, ErrorMatches, `cannot write to "/etc/dir" because it would affect the host in "/etc"`) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `fstatfs 3 `, R: syscall.Statfs_t{Type: update.SquashfsMagic}}, {C: `fstat 3 `, R: syscall.Stat_t{}}, {C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST}, {C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `close 3`}, {C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.Ext4Magic}}, {C: `fstat 4 `, R: syscall.Stat_t{}}, {C: `close 4`}, }) } // We want to create a directory in /snap/foo/42/dir and want to know what happens. func (s *utilsSuite) TestSecureMkdirAllInSNAP(c *C) { // Allow creating directories under /snap/ related to this snap ("foo"). // This matches what is done inside main(). restore := s.as.MockUnrestrictedPaths("/snap/foo") defer restore() s.sys.InsertFault(`mkdirat 3 "snap" 0755`, syscall.EEXIST) s.sys.InsertFault(`mkdirat 4 "foo" 0755`, syscall.EEXIST) s.sys.InsertFault(`mkdirat 5 "42" 0755`, syscall.EEXIST) rs := s.as.RestrictionsFor("/snap/foo/42/dir") err := update.MkdirAll("/snap/foo/42/dir", 0755, 0, 0, rs) c.Assert(err, IsNil) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "snap" 0755`, E: syscall.EEXIST}, {C: `openat 3 "snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `mkdirat 4 "foo" 0755`, E: syscall.EEXIST}, {C: `openat 4 "foo" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5}, {C: `mkdirat 5 "42" 0755`, E: syscall.EEXIST}, {C: `openat 5 "42" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 6}, {C: `close 5`}, {C: `close 4`}, {C: `close 3`}, {C: `mkdirat 6 "dir" 0755`}, {C: `openat 6 "dir" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `fchown 3 0 0`}, {C: `close 3`}, {C: `close 6`}, }) } // We want to create a symlink in /etc which is a tmpfs that we mounted so that is ok. func (s *utilsSuite) TestSecureMksymlinkAllInEtcAfterMimic(c *C) { // Because /etc is not on a list of unrestricted paths the write to // /etc/symlink must be validated with step-by-step operation. rootStatfs := syscall.Statfs_t{Type: update.SquashfsMagic, Flags: update.StReadOnly} rootStat := syscall.Stat_t{} etcStatfs := syscall.Statfs_t{Type: update.TmpfsMagic} etcStat := syscall.Stat_t{} s.as.AddChange(&update.Change{Action: update.Mount, Entry: osutil.MountEntry{Dir: "/etc", Type: "tmpfs", Name: "tmpfs"}}) s.sys.InsertFstatfsResult(`fstatfs 3 `, rootStatfs) s.sys.InsertFstatResult(`fstat 3 `, rootStat) s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST) s.sys.InsertFstatfsResult(`fstatfs 4 `, etcStatfs) s.sys.InsertFstatResult(`fstat 4 `, etcStat) rs := s.as.RestrictionsFor("/etc/symlink") err := update.MksymlinkAll("/etc/symlink", 0755, 0, 0, "/oldname", rs) c.Assert(err, IsNil) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `fstatfs 3 `, R: rootStatfs}, {C: `fstat 3 `, R: rootStat}, {C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST}, {C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `close 3`}, {C: `fstatfs 4 `, R: etcStatfs}, {C: `fstat 4 `, R: etcStat}, {C: `symlinkat "/oldname" 4 "symlink"`}, {C: `close 4`}, }) } // We want to create a file in /etc which is a tmpfs created by snapd so that's okay. func (s *utilsSuite) TestSecureMkfileAllInEtcAfterMimic(c *C) { s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.SquashfsMagic}) s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.TmpfsMagic}) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST) s.as.AddChange(&update.Change{Action: update.Mount, Entry: osutil.MountEntry{Dir: "/etc", Type: "tmpfs", Name: "tmpfs"}}) rs := s.as.RestrictionsFor("/etc/file") err := update.MkfileAll("/etc/file", 0755, 0, 0, rs) c.Assert(err, IsNil) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `fstatfs 3 `, R: syscall.Statfs_t{Type: update.SquashfsMagic}}, {C: `fstat 3 `, R: syscall.Stat_t{}}, {C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST}, {C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `close 3`}, {C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.TmpfsMagic}}, {C: `fstat 4 `, R: syscall.Stat_t{}}, {C: `openat 4 "file" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, R: 3}, {C: `fchown 3 0 0`}, {C: `close 3`}, {C: `close 4`}, }) } // We want to create a directory in /etc which is a tmpfs created by snapd so that is ok. func (s *utilsSuite) TestSecureMkdirAllInEtcAfterMimic(c *C) { s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.SquashfsMagic}) s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.TmpfsMagic}) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST) s.as.AddChange(&update.Change{Action: update.Mount, Entry: osutil.MountEntry{Dir: "/etc", Type: "tmpfs", Name: "tmpfs"}}) rs := s.as.RestrictionsFor("/etc/dir") err := update.MkdirAll("/etc/dir", 0755, 0, 0, rs) c.Assert(err, IsNil) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `fstatfs 3 `, R: syscall.Statfs_t{Type: update.SquashfsMagic}}, {C: `fstat 3 `, R: syscall.Stat_t{}}, {C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST}, {C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `close 3`}, {C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.TmpfsMagic}}, {C: `fstat 4 `, R: syscall.Stat_t{}}, {C: `mkdirat 4 "dir" 0755`}, {C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `fchown 3 0 0`}, {C: `close 3`}, {C: `close 4`}, }) } // Check that we can actually create files. // This doesn't test the chown logic as that requires root. func (s *realSystemSuite) TestSecureMkfileAllForReal(c *C) { d := c.MkDir() // Create f1, which is a simple subdirectory, with a distinct mode and // check that it was applied. Note that default umask 022 is subtracted so // effective directory has different permissions. f1 := filepath.Join(d, "file") c.Assert(update.MkfileAll(f1, 0707, sys.FlagID, sys.FlagID, nil), IsNil) fi, err := os.Stat(f1) c.Assert(err, IsNil) c.Check(fi.Mode().IsRegular(), Equals, true) c.Check(fi.Mode().Perm(), Equals, os.FileMode(0705)) // Create f2, which is a deeper subdirectory, with another distinct mode // and check that it was applied. f2 := filepath.Join(d, "subdir/subdir/file") c.Assert(update.MkfileAll(f2, 0750, sys.FlagID, sys.FlagID, nil), IsNil) fi, err = os.Stat(f2) c.Assert(err, IsNil) c.Check(fi.Mode().IsRegular(), Equals, true) c.Check(fi.Mode().Perm(), Equals, os.FileMode(0750)) } // Check that we can actually create symlinks. // This doesn't test the chown logic as that requires root. func (s *realSystemSuite) TestSecureMksymlinkAllForReal(c *C) { d := c.MkDir() // Create symlink f1 that points to "oldname" and check that it // is correct. Note that symlink permissions are always set to 0777 f1 := filepath.Join(d, "symlink") err := update.MksymlinkAll(f1, 0755, sys.FlagID, sys.FlagID, "oldname", nil) c.Assert(err, IsNil) fi, err := os.Lstat(f1) c.Assert(err, IsNil) c.Check(fi.Mode()&os.ModeSymlink, Equals, os.ModeSymlink) c.Check(fi.Mode().Perm(), Equals, os.FileMode(0777)) target, err := os.Readlink(f1) c.Assert(err, IsNil) c.Check(target, Equals, "oldname") // Create an identical symlink to see that it doesn't fail. err = update.MksymlinkAll(f1, 0755, sys.FlagID, sys.FlagID, "oldname", nil) c.Assert(err, IsNil) // Create a different symlink and see that it fails now err = update.MksymlinkAll(f1, 0755, sys.FlagID, sys.FlagID, "other", nil) c.Assert(err, ErrorMatches, `cannot create symbolic link ".*/symlink": existing symbolic link in the way`) // Create an file and check that it clashes with a symlink we attempt to create. f2 := filepath.Join(d, "file") err = update.MkfileAll(f2, 0755, sys.FlagID, sys.FlagID, nil) c.Assert(err, IsNil) err = update.MksymlinkAll(f2, 0755, sys.FlagID, sys.FlagID, "oldname", nil) c.Assert(err, ErrorMatches, `cannot create symbolic link ".*/file": existing file in the way`) // Create an file and check that it clashes with a symlink we attempt to create. f3 := filepath.Join(d, "dir") err = update.MkdirAll(f3, 0755, sys.FlagID, sys.FlagID, nil) c.Assert(err, IsNil) err = update.MksymlinkAll(f3, 0755, sys.FlagID, sys.FlagID, "oldname", nil) c.Assert(err, ErrorMatches, `cannot create symbolic link ".*/dir": existing file in the way`) err = update.MksymlinkAll("/", 0755, sys.FlagID, sys.FlagID, "oldname", nil) c.Assert(err, ErrorMatches, `cannot create non-file path: "/"`) } func (s *utilsSuite) TestCleanTrailingSlash(c *C) { // This is a sanity test for the use of filepath.Clean in secureMk{dir,file}All c.Assert(filepath.Clean("/path/"), Equals, "/path") c.Assert(filepath.Clean("path/"), Equals, "path") c.Assert(filepath.Clean("path/."), Equals, "path") c.Assert(filepath.Clean("path/.."), Equals, ".") c.Assert(filepath.Clean("other/path/.."), Equals, "other") } // secure-open-path func (s *utilsSuite) TestSecureOpenPath(c *C) { stat := syscall.Stat_t{Mode: syscall.S_IFDIR} s.sys.InsertFstatResult("fstat 5 ", stat) fd, err := update.OpenPath("/foo/bar") c.Assert(err, IsNil) defer s.sys.Close(fd) c.Assert(fd, Equals, 5) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "foo" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, {C: `openat 4 "bar" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, {C: `fstat 5 `, R: stat}, {C: `close 4`}, {C: `close 3`}, }) } func (s *utilsSuite) TestSecureOpenPathSingleSegment(c *C) { stat := syscall.Stat_t{Mode: syscall.S_IFDIR} s.sys.InsertFstatResult("fstat 4 ", stat) fd, err := update.OpenPath("/foo") c.Assert(err, IsNil) defer s.sys.Close(fd) c.Assert(fd, Equals, 4) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "foo" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, {C: `fstat 4 `, R: stat}, {C: `close 3`}, }) } func (s *utilsSuite) TestSecureOpenPathRoot(c *C) { stat := syscall.Stat_t{Mode: syscall.S_IFDIR} s.sys.InsertFstatResult("fstat 3 ", stat) fd, err := update.OpenPath("/") c.Assert(err, IsNil) defer s.sys.Close(fd) c.Assert(fd, Equals, 3) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `fstat 3 `, R: stat}, }) } func (s *realSystemSuite) TestSecureOpenPathDirectory(c *C) { path := filepath.Join(c.MkDir(), "test") c.Assert(os.Mkdir(path, 0755), IsNil) fd, err := update.OpenPath(path) c.Assert(err, IsNil) defer syscall.Close(fd) // check that the file descriptor is for the expected path origDir, err := os.Getwd() c.Assert(err, IsNil) defer os.Chdir(origDir) c.Assert(syscall.Fchdir(fd), IsNil) cwd, err := os.Getwd() c.Assert(err, IsNil) c.Check(cwd, Equals, path) } func (s *realSystemSuite) TestSecureOpenPathRelativePath(c *C) { fd, err := update.OpenPath("relative/path") c.Check(fd, Equals, -1) c.Check(err, ErrorMatches, "path .* is not absolute") } func (s *realSystemSuite) TestSecureOpenPathUncleanPath(c *C) { base := c.MkDir() path := filepath.Join(base, "test") c.Assert(os.Mkdir(path, 0755), IsNil) fd, err := update.OpenPath(base + "//test") c.Check(fd, Equals, -1) c.Check(err, ErrorMatches, `cannot open path: cannot iterate over unclean path ".*//test"`) fd, err = update.OpenPath(base + "/./test") c.Check(fd, Equals, -1) c.Check(err, ErrorMatches, `cannot open path: cannot iterate over unclean path ".*/./test"`) fd, err = update.OpenPath(base + "/test/../test") c.Check(fd, Equals, -1) c.Check(err, ErrorMatches, `cannot open path: cannot iterate over unclean path ".*/test/../test"`) } func (s *realSystemSuite) TestSecureOpenPathFile(c *C) { path := filepath.Join(c.MkDir(), "file.txt") c.Assert(ioutil.WriteFile(path, []byte("hello"), 0644), IsNil) fd, err := update.OpenPath(path) c.Assert(err, IsNil) defer syscall.Close(fd) // Check that the file descriptor matches the file. var pathStat, fdStat syscall.Stat_t c.Assert(syscall.Stat(path, &pathStat), IsNil) c.Assert(syscall.Fstat(fd, &fdStat), IsNil) c.Check(pathStat, Equals, fdStat) } func (s *realSystemSuite) TestSecureOpenPathNotFound(c *C) { path := filepath.Join(c.MkDir(), "test") fd, err := update.OpenPath(path) c.Check(fd, Equals, -1) c.Check(err, ErrorMatches, "no such file or directory") } func (s *realSystemSuite) TestSecureOpenPathSymlink(c *C) { base := c.MkDir() dir := filepath.Join(base, "test") c.Assert(os.Mkdir(dir, 0755), IsNil) symlink := filepath.Join(base, "symlink") c.Assert(os.Symlink(dir, symlink), IsNil) fd, err := update.OpenPath(symlink) c.Check(fd, Equals, -1) c.Check(err, ErrorMatches, `".*" is a symbolic link`) } func (s *realSystemSuite) TestSecureOpenPathSymlinkedParent(c *C) { base := c.MkDir() dir := filepath.Join(base, "dir1") symlink := filepath.Join(base, "symlink") path := filepath.Join(dir, "dir2") symlinkedPath := filepath.Join(symlink, "dir2") c.Assert(os.Mkdir(dir, 0755), IsNil) c.Assert(os.Symlink(dir, symlink), IsNil) c.Assert(os.Mkdir(path, 0755), IsNil) fd, err := update.OpenPath(symlinkedPath) c.Check(fd, Equals, -1) c.Check(err, ErrorMatches, "not a directory") } snapd-2.37.4~14.04.1/cmd/snap-update-ns/secure_bindmount_test.go0000664000000000000000000002066513435556260021107 0ustar // -*- 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 . * */ package main_test import ( "syscall" . "gopkg.in/check.v1" update "github.com/snapcore/snapd/cmd/snap-update-ns" "github.com/snapcore/snapd/testutil" ) type secureBindMountSuite struct { testutil.BaseTest sys *testutil.SyscallRecorder } var _ = Suite(&secureBindMountSuite{}) func (s *secureBindMountSuite) SetUpTest(c *C) { s.BaseTest.SetUpTest(c) s.sys = &testutil.SyscallRecorder{} s.BaseTest.AddCleanup(update.MockSystemCalls(s.sys)) } func (s *secureBindMountSuite) TearDownTest(c *C) { s.sys.CheckForStrayDescriptors(c) s.BaseTest.TearDownTest(c) } func (s *secureBindMountSuite) TestMount(c *C) { s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{}) s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{}) err := update.BindMount("/source/dir", "/target/dir", syscall.MS_BIND) c.Assert(err, IsNil) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, {C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, {C: `fstat 5 `, R: syscall.Stat_t{}}, {C: `close 4`}, // "/source" {C: `close 3`}, // "/" {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, {C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, {C: `fstat 6 `, R: syscall.Stat_t{}}, {C: `close 4`}, // "/target" {C: `close 3`}, // "/" {C: `mount "/proc/self/fd/5" "/proc/self/fd/6" "" MS_BIND ""`}, {C: `close 6`}, // "/target/dir" {C: `close 5`}, // "/source/dir" }) } func (s *secureBindMountSuite) TestMountRecursive(c *C) { s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{}) s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{}) err := update.BindMount("/source/dir", "/target/dir", syscall.MS_BIND|syscall.MS_REC) c.Assert(err, IsNil) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, {C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, {C: `fstat 5 `, R: syscall.Stat_t{}}, {C: `close 4`}, // "/source" {C: `close 3`}, // "/" {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, {C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, {C: `fstat 6 `, R: syscall.Stat_t{}}, {C: `close 4`}, // "/target" {C: `close 3`}, // "/" {C: `mount "/proc/self/fd/5" "/proc/self/fd/6" "" MS_BIND|MS_REC ""`}, {C: `close 6`}, // "/target/dir" {C: `close 5`}, // "/source/dir" }) } func (s *secureBindMountSuite) TestMountReadOnly(c *C) { s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{}) s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{}) s.sys.InsertFstatResult(`fstat 7 `, syscall.Stat_t{}) err := update.BindMount("/source/dir", "/target/dir", syscall.MS_BIND|syscall.MS_RDONLY) c.Assert(err, IsNil) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, {C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, {C: `fstat 5 `, R: syscall.Stat_t{}}, {C: `close 4`}, // "/source" {C: `close 3`}, // "/" {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, {C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, {C: `fstat 6 `, R: syscall.Stat_t{}}, {C: `close 4`}, // "/target" {C: `close 3`}, // "/" {C: `mount "/proc/self/fd/5" "/proc/self/fd/6" "" MS_BIND ""`}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, {C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7}, {C: `fstat 7 `, R: syscall.Stat_t{}}, {C: `close 4`}, // "/target" {C: `close 3`}, // "/" {C: `mount "none" "/proc/self/fd/7" "" MS_REMOUNT|MS_BIND|MS_RDONLY ""`}, {C: `close 7`}, // "/target/dir" {C: `close 6`}, // "/target/dir" {C: `close 5`}, // "/source/dir" }) } func (s *secureBindMountSuite) TestBindFlagRequired(c *C) { err := update.BindMount("/source/dir", "/target/dir", syscall.MS_REC) c.Assert(err, ErrorMatches, "cannot perform non-bind mount operation") c.Check(s.sys.RCalls(), HasLen, 0) } func (s *secureBindMountSuite) TestMountReadOnlyRecursive(c *C) { err := update.BindMount("/source/dir", "/target/dir", syscall.MS_BIND|syscall.MS_RDONLY|syscall.MS_REC) c.Assert(err, ErrorMatches, "cannot use MS_RDONLY and MS_REC together") c.Check(s.sys.RCalls(), HasLen, 0) } func (s *secureBindMountSuite) TestBindMountFails(c *C) { s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{}) s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{}) s.sys.InsertFault(`mount "/proc/self/fd/5" "/proc/self/fd/6" "" MS_BIND ""`, errTesting) err := update.BindMount("/source/dir", "/target/dir", syscall.MS_BIND|syscall.MS_RDONLY) c.Assert(err, ErrorMatches, "testing") c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, {C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, {C: `fstat 5 `, R: syscall.Stat_t{}}, {C: `close 4`}, // "/source" {C: `close 3`}, // "/" {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, {C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, {C: `fstat 6 `, R: syscall.Stat_t{}}, {C: `close 4`}, // "/target" {C: `close 3`}, // "/" {C: `mount "/proc/self/fd/5" "/proc/self/fd/6" "" MS_BIND ""`, E: errTesting}, {C: `close 6`}, // "/target/dir" {C: `close 5`}, // "/source/dir" }) } func (s *secureBindMountSuite) TestRemountReadOnlyFails(c *C) { s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{}) s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{}) s.sys.InsertFstatResult(`fstat 7 `, syscall.Stat_t{}) s.sys.InsertFault(`mount "none" "/proc/self/fd/7" "" MS_REMOUNT|MS_BIND|MS_RDONLY ""`, errTesting) err := update.BindMount("/source/dir", "/target/dir", syscall.MS_BIND|syscall.MS_RDONLY) c.Assert(err, ErrorMatches, "testing") c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, {C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, {C: `fstat 5 `, R: syscall.Stat_t{}}, {C: `close 4`}, // "/source" {C: `close 3`}, // "/" {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, {C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, {C: `fstat 6 `, R: syscall.Stat_t{}}, {C: `close 4`}, // "/target" {C: `close 3`}, // "/" {C: `mount "/proc/self/fd/5" "/proc/self/fd/6" "" MS_BIND ""`}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, {C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7}, {C: `fstat 7 `, R: syscall.Stat_t{}}, {C: `close 4`}, // "/target" {C: `close 3`}, // "/" {C: `mount "none" "/proc/self/fd/7" "" MS_REMOUNT|MS_BIND|MS_RDONLY ""`, E: errTesting}, {C: `unmount "/proc/self/fd/7" UMOUNT_NOFOLLOW|MNT_DETACH`}, {C: `close 7`}, // "/target/dir" {C: `close 6`}, // "/target/dir" {C: `close 5`}, // "/source/dir" }) } snapd-2.37.4~14.04.1/cmd/snap-update-ns/change.go0000664000000000000000000004017713435556260015730 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main import ( "errors" "fmt" "os" "path/filepath" "sort" "strings" "syscall" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" ) // Action represents a mount action (mount, remount, unmount, etc). type Action string const ( // Keep indicates that a given mount entry should be kept as-is. Keep Action = "keep" // Mount represents an action that results in mounting something somewhere. Mount Action = "mount" // Unmount represents an action that results in unmounting something from somewhere. Unmount Action = "unmount" // Remount when needed ) var ( // ErrIgnoredMissingMount is returned when a mount entry has // been marked with x-snapd.ignore-missing, and the mount // source or target do not exist. ErrIgnoredMissingMount = errors.New("mount source or target are missing") ) // Change describes a change to the mount table (action and the entry to act on). type Change struct { Entry osutil.MountEntry Action Action } // String formats mount change to a human-readable line. func (c Change) String() string { return fmt.Sprintf("%s (%s)", c.Action, c.Entry) } // changePerform is Change.Perform that can be mocked for testing. var changePerform func(*Change, *Assumptions) ([]*Change, error) // mimicRequired provides information if an error warrants a writable mimic. // // The returned path is the location where a mimic should be constructed. func mimicRequired(err error) (needsMimic bool, path string) { switch err.(type) { case *ReadOnlyFsError: rofsErr := err.(*ReadOnlyFsError) return true, rofsErr.Path case *TrespassingError: tErr := err.(*TrespassingError) return true, tErr.ViolatedPath } return false, "" } func (c *Change) createPath(path string, pokeHoles bool, as *Assumptions) ([]*Change, error) { // If we've been asked to create a missing path, and the mount // entry uses the ignore-missing option, return an error. if c.Entry.XSnapdIgnoreMissing() { return nil, ErrIgnoredMissingMount } var err error var changes []*Change // In case we need to create something, some constants. const ( mode = 0755 uid = 0 gid = 0 ) // If the element doesn't exist we can attempt to create it. We will // create the parent directory and then the final element relative to it. // The traversed space may be writable so we just try to create things // first. kind := c.Entry.XSnapdKind() // TODO: re-factor this, if possible, with inspection and preemptive // creation after the current release ships. This should be possible but // will affect tests heavily (churn, not safe before release). rs := as.RestrictionsFor(path) switch kind { case "": err = MkdirAll(path, mode, uid, gid, rs) case "file": err = MkfileAll(path, mode, uid, gid, rs) case "symlink": err = MksymlinkAll(path, mode, uid, gid, c.Entry.XSnapdSymlink(), rs) } if needsMimic, mimicPath := mimicRequired(err); needsMimic && pokeHoles { // If the error can be recovered by using a writable mimic // then construct one and try again. changes, err = createWritableMimic(mimicPath, path, as) if err != nil { err = fmt.Errorf("cannot create writable mimic over %q: %s", mimicPath, err) } else { // Try once again. Note that we care *just* about the error. We have already // performed the hole poking and thus additional changes must be nil. _, err = c.createPath(path, false, as) } } return changes, err } func (c *Change) ensureTarget(as *Assumptions) ([]*Change, error) { var changes []*Change kind := c.Entry.XSnapdKind() path := c.Entry.Dir // We use lstat to ensure that we don't follow a symlink in case one was // set up by the snap. Note that at the time this is run, all the snap's // processes are frozen but if the path is a directory controlled by the // user (typically in /home) then we may still race with user processes // that change it. fi, err := osLstat(path) if err == nil { // If the element already exists we just need to ensure it is of // the correct type. The desired type depends on the kind of entry // we are working with. switch kind { case "": if !fi.Mode().IsDir() { err = fmt.Errorf("cannot use %q as mount point: not a directory", path) } case "file": if !fi.Mode().IsRegular() { err = fmt.Errorf("cannot use %q as mount point: not a regular file", path) } case "symlink": if fi.Mode()&os.ModeSymlink == os.ModeSymlink { // Create path verifies the symlink or fails if it is not what we wanted. _, err = c.createPath(path, false, as) } else { err = fmt.Errorf("cannot create symlink in %q: existing file in the way", path) } } } else if os.IsNotExist(err) { changes, err = c.createPath(path, true, as) } else { // If we cannot inspect the element let's just bail out. err = fmt.Errorf("cannot inspect %q: %v", path, err) } return changes, err } func (c *Change) ensureSource(as *Assumptions) ([]*Change, error) { var changes []*Change // We only have to do ensure bind mount source exists. // This also rules out symlinks. flags, _ := osutil.MountOptsToCommonFlags(c.Entry.Options) if flags&syscall.MS_BIND == 0 { return nil, nil } kind := c.Entry.XSnapdKind() path := c.Entry.Name fi, err := osLstat(path) if err == nil { // If the element already exists we just need to ensure it is of // the correct type. The desired type depends on the kind of entry // we are working with. switch kind { case "": if !fi.Mode().IsDir() { err = fmt.Errorf("cannot use %q as bind-mount source: not a directory", path) } case "file": if !fi.Mode().IsRegular() { err = fmt.Errorf("cannot use %q as bind-mount source: not a regular file", path) } } } else if os.IsNotExist(err) { // NOTE: This createPath is using pokeHoles, to make read-only places // writable, but only for layouts and not for other (typically content // sharing) mount entries. // // This is done because the changes made with pokeHoles=true are only // visible in this current mount namespace and are not generally // visible from other snaps because they inhabit different namespaces. // // In other words, changes made here are only observable by the single // snap they apply to. As such they are useless for content sharing but // very much useful to layouts. pokeHoles := c.Entry.XSnapdOrigin() == "layout" changes, err = c.createPath(path, pokeHoles, as) } else { // If we cannot inspect the element let's just bail out. err = fmt.Errorf("cannot inspect %q: %v", path, err) } return changes, err } // changePerformImpl is the real implementation of Change.Perform func changePerformImpl(c *Change, as *Assumptions) (changes []*Change, err error) { if c.Action == Mount { var changesSource, changesTarget []*Change // We may be asked to bind mount a file, bind mount a directory, mount // a filesystem over a directory, or create a symlink (which is abusing // the "mount" concept slightly). That actual operation is performed in // c.lowLevelPerform. Here we just set the stage to make that possible. // // As a result of this ensure call we may need to make the medium writable // and that's why we may return more changes as a result of performing this // one. changesTarget, err = c.ensureTarget(as) // NOTE: we are collecting changes even if things fail. This is so that // upper layers can perform undo correctly. changes = append(changes, changesTarget...) if err != nil { return changes, err } // At this time we can be sure that the target element (for files and // directories) exists and is of the right type or that it (for // symlinks) doesn't exist but the parent directory does. // This property holds as long as we don't interact with locations that // are under the control of regular (non-snap) processes that are not // suspended and may be racing with us. changesSource, err = c.ensureSource(as) // NOTE: we are collecting changes even if things fail. This is so that // upper layers can perform undo correctly. changes = append(changes, changesSource...) if err != nil { return changes, err } } // Perform the underlying mount / unmount / unlink call. err = c.lowLevelPerform(as) return changes, err } func init() { changePerform = changePerformImpl } // Perform executes the desired mount or unmount change using system calls. // Filesystems that depend on helper programs or multiple independent calls to // the kernel (--make-shared, for example) are unsupported. // // Perform may synthesize *additional* changes that were necessary to perform // this change (such as mounted tmpfs or overlayfs). func (c *Change) Perform(as *Assumptions) ([]*Change, error) { return changePerform(c, as) } // lowLevelPerform is simple bridge from Change to mount / unmount syscall. func (c *Change) lowLevelPerform(as *Assumptions) error { var err error switch c.Action { case Mount: kind := c.Entry.XSnapdKind() switch kind { case "symlink": // symlinks are handled in createInode directly, nothing to do here. case "", "file": flags, unparsed := osutil.MountOptsToCommonFlags(c.Entry.Options) // Use Secure.BindMount for bind mounts if flags&syscall.MS_BIND == syscall.MS_BIND { err = BindMount(c.Entry.Name, c.Entry.Dir, uint(flags)) } else { err = sysMount(c.Entry.Name, c.Entry.Dir, c.Entry.Type, uintptr(flags), strings.Join(unparsed, ",")) } logger.Debugf("mount %q %q %q %d %q (error: %v)", c.Entry.Name, c.Entry.Dir, c.Entry.Type, uintptr(flags), strings.Join(unparsed, ","), err) if err == nil { as.AddChange(c) } } return err case Unmount: kind := c.Entry.XSnapdKind() switch kind { case "symlink": err = osRemove(c.Entry.Dir) logger.Debugf("remove %q (error: %v)", c.Entry.Dir, err) case "", "file": // Detach the mount point instead of unmounting it if requested. flags := umountNoFollow if c.Entry.XSnapdDetach() { flags |= syscall.MNT_DETACH } // Perform the raw unmount operation. err = sysUnmount(c.Entry.Dir, flags) if err == nil { as.AddChange(c) } logger.Debugf("umount %q (error: %v)", c.Entry.Dir, err) if err != nil { return err } // Open a path of the file we are considering the removal of. path := c.Entry.Dir var fd int fd, err = OpenPath(path) if err != nil { return err } defer sysClose(fd) // Don't attempt to remove anything from squashfs. var statfsBuf syscall.Statfs_t err = sysFstatfs(fd, &statfsBuf) if err != nil { return err } if statfsBuf.Type == SquashfsMagic { return nil } if kind == "file" { // Don't attempt to remove non-empty files since they cannot be // the placeholders we created. var statBuf syscall.Stat_t err = sysFstat(fd, &statBuf) if err != nil { return err } if statBuf.Size != 0 { return nil } } // Remove the file or directory while using the full path. There's // no way to avoid a race here since there's no way to unlink a // file solely by file descriptor. err = osRemove(path) // Unpack the low-level error that osRemove wraps into PathError. if packed, ok := err.(*os.PathError); ok { err = packed.Err } // If we were removing a directory but it was not empty then just // ignore the error. This is the equivalent of the non-empty file // check we do above. See rmdir(2) for explanation why we accept // more than one errno value. if kind == "" && (err == syscall.ENOTEMPTY || err == syscall.EEXIST) { return nil } } return err case Keep: return nil } return fmt.Errorf("cannot process mount change: unknown action: %q", c.Action) } // NeededChanges computes the changes required to change current to desired mount entries. // // The current and desired profiles is a fstab like list of mount entries. The // lists are processed and a "diff" of mount changes is produced. The mount // changes, when applied in order, transform the current profile into the // desired profile. func NeededChanges(currentProfile, desiredProfile *osutil.MountProfile) []*Change { // Copy both profiles as we will want to mutate them. current := make([]osutil.MountEntry, len(currentProfile.Entries)) copy(current, currentProfile.Entries) desired := make([]osutil.MountEntry, len(desiredProfile.Entries)) copy(desired, desiredProfile.Entries) // Clean the directory part of both profiles. This is done so that we can // easily test if a given directory is a subdirectory with // strings.HasPrefix coupled with an extra slash character. for i := range current { current[i].Dir = filepath.Clean(current[i].Dir) } for i := range desired { desired[i].Dir = filepath.Clean(desired[i].Dir) } // Sort both lists by directory name with implicit trailing slash. sort.Sort(byOriginAndMagicDir(current)) sort.Sort(byOriginAndMagicDir(desired)) // Construct a desired directory map. desiredMap := make(map[string]*osutil.MountEntry) for i := range desired { desiredMap[desired[i].Dir] = &desired[i] } // Indexed by mount point path. reuse := make(map[string]bool) // Indexed by entry ID desiredIDs := make(map[string]bool) var skipDir string // Collect the IDs of desired changes. // We need that below to keep implicit changes from the current profile. for i := range desired { desiredIDs[desired[i].XSnapdEntryID()] = true } // Compute reusable entries: those which are equal in current and desired and which // are not prefixed by another entry that changed. for i := range current { dir := current[i].Dir if skipDir != "" && strings.HasPrefix(dir, skipDir) { logger.Debugf("skipping entry %q", current[i]) continue } skipDir = "" // reset skip prefix as it no longer applies // Reuse synthetic entries if their needed-by entry is desired. // Synthetic entries cannot exist on their own and always couple to a // non-synthetic entry. // NOTE: Synthetic changes have a special purpose. // // They are a "shadow" of mount events that occurred to allow one of // the desired mount entries to be possible. The changes have only one // goal: tell snap-update-ns how those mount events can be undone in // case they are no longer needed. The actual changes may have been // different and may have involved steps not represented as synthetic // mount entires as long as those synthetic entries can be undone to // reverse the effect. In reality each non-tmpfs synthetic entry was // constructed using a temporary bind mount that contained the original // mount entries of a directory that was hidden with a tmpfs, but this // fact was lost. if current[i].XSnapdSynthetic() && desiredIDs[current[i].XSnapdNeededBy()] { logger.Debugf("reusing synthetic entry %q", current[i]) reuse[dir] = true continue } // Reuse entries that are desired and identical in the current profile. if entry, ok := desiredMap[dir]; ok && current[i].Equal(entry) { logger.Debugf("reusing unchanged entry %q", current[i]) reuse[dir] = true continue } skipDir = strings.TrimSuffix(dir, "/") + "/" } logger.Debugf("desiredIDs: %v", desiredIDs) logger.Debugf("reuse: %v", reuse) // We are now ready to compute the necessary mount changes. var changes []*Change // Unmount entries not reused in reverse to handle children before their parent. for i := len(current) - 1; i >= 0; i-- { if reuse[current[i].Dir] { changes = append(changes, &Change{Action: Keep, Entry: current[i]}) } else { changes = append(changes, &Change{Action: Unmount, Entry: current[i]}) } } // Mount desired entries not reused. for i := range desired { if !reuse[desired[i].Dir] { changes = append(changes, &Change{Action: Mount, Entry: desired[i]}) } } return changes } snapd-2.37.4~14.04.1/cmd/snap-update-ns/trespassing.go0000664000000000000000000002337413435556260017045 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017-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 . * */ package main import ( "fmt" "path/filepath" "strings" "syscall" "github.com/snapcore/snapd/logger" ) // Assumptions track the assumptions about the state of the filesystem. // // Assumptions constitute the global part of the write restriction management. // Assumptions are global in the sense that they span multiple distinct write // operations. In contrast, Restrictions track per-operation state. type Assumptions struct { unrestrictedPaths []string pastChanges []*Change // verifiedDevices represents the set of devices that are verified as a tmpfs // that was mounted by snapd. Those are only discovered on-demand. The // major:minor number is packed into one uint64 as in syscall.Stat_t.Dev // field. verifiedDevices map[uint64]bool } // AddUnrestrictedPaths adds a list of directories where writing is allowed // even if it would hit the real host filesystem (or transit through the host // filesystem). This is intended to be used with certain well-known locations // such as /tmp, $SNAP_DATA and $SNAP. func (as *Assumptions) AddUnrestrictedPaths(paths ...string) { as.unrestrictedPaths = append(as.unrestrictedPaths, paths...) } // isRestricted checks whether a path falls under restricted writing scheme. // // Provided path is the full, absolute path of the entity that needs to be // created (directory, file or symbolic link). func (as *Assumptions) isRestricted(path string) bool { // Anything rooted at one of the unrestricted paths is not restricted. // Those are for things like /var/snap/, for example. for _, p := range as.unrestrictedPaths { if p == "/" || p == path || strings.HasPrefix(path, filepath.Clean(p)+"/") { return false } } // All other paths are restricted return true } // MockUnrestrictedPaths replaces the set of path paths without any restrictions. func (as *Assumptions) MockUnrestrictedPaths(paths ...string) (restore func()) { old := as.unrestrictedPaths as.unrestrictedPaths = paths return func() { as.unrestrictedPaths = old } } // AddChange records the fact that a change was applied to the system. func (as *Assumptions) AddChange(change *Change) { as.pastChanges = append(as.pastChanges, change) } // canWriteToDirectory returns true if writing to a given directory is allowed. // // Writing is allowed in one of thee cases: // 1) The directory is in one of the explicitly permitted locations. // This is the strongest permission as it explicitly allows writing to // places that may show up on the host, one of the examples being $SNAP_DATA. // 2) The directory is on a read-only filesystem. // 3) The directory is on a tmpfs created by snapd. func (as *Assumptions) canWriteToDirectory(dirFd int, dirName string) (bool, error) { if !as.isRestricted(dirName) { return true, nil } var fsData syscall.Statfs_t if err := sysFstatfs(dirFd, &fsData); err != nil { return false, fmt.Errorf("cannot fstatfs %q: %s", dirName, err) } var fileData syscall.Stat_t if err := sysFstat(dirFd, &fileData); err != nil { return false, fmt.Errorf("cannot fstat %q: %s", dirName, err) } // Writing to read only directories is allowed because EROFS is handled // by each of the writing helpers already. if ok := isReadOnly(dirName, &fsData); ok { return true, nil } // Writing to a trusted tmpfs is allowed because those are not leaking to // the host. Also, each time we find a good tmpfs we explicitly remember the device major/minor, if as.verifiedDevices[fileData.Dev] { return true, nil } if ok := isPrivateTmpfsCreatedBySnapd(dirName, &fsData, &fileData, as.pastChanges); ok { if as.verifiedDevices == nil { as.verifiedDevices = make(map[uint64]bool) } // Don't record 0:0 as those are all to easy to add in tests and would // skew tests using zero-initialized structures. Real device numbers // are not zero either so this is not a test-only conditional. if fileData.Dev != 0 { as.verifiedDevices[fileData.Dev] = true } return true, nil } // If writing is not not allowed by one of the three rules above then it is // disallowed. return false, nil } // RestrictionsFor computes restrictions for the desired path. func (as *Assumptions) RestrictionsFor(desiredPath string) *Restrictions { // Writing to a restricted path results in step-by-step validation of each // directory, starting from the root of the file system. Unless writing is // allowed a mimic must be constructed to ensure that writes are not visible in // undesired locations of the host filesystem. if as.isRestricted(desiredPath) { return &Restrictions{assumptions: as, desiredPath: desiredPath, restricted: true} } return nil } // Restrictions contains meta-data of a compound write operation. // // This structure helps functions that write to the filesystem to keep track of // the ultimate destination across several calls (e.g. the function that // creates a file needs to call helpers to create subsequent directories). // Keeping track of the desired path aids in constructing useful error // messages. // // In addition the structure keeps track of the restricted write mode flag which // is based on the full path of the desired object being constructed. This allows // various write helpers to avoid trespassing on host filesystem in places that // are not expected to be written to by snapd (e.g. outside of $SNAP_DATA). type Restrictions struct { assumptions *Assumptions desiredPath string restricted bool } // Check verifies whether writing to a directory would trespass on the host. // // The check is only performed in restricted mode. If the check fails a // TrespassingError is returned. func (rs *Restrictions) Check(dirFd int, dirName string) error { if rs == nil || !rs.restricted { return nil } // In restricted mode check the directory before attempting to write to it. ok, err := rs.assumptions.canWriteToDirectory(dirFd, dirName) if ok || err != nil { return err } if dirName == "/" { // If writing to / is not allowed then we are in a tough spot because // we cannot construct a writable mimic over /. This should never // happen in normal circumstances because the root filesystem is some // kind of base snap. return fmt.Errorf("cannot recover from trespassing over /") } logger.Debugf("trespassing violated %q while striving to %q", dirName, rs.desiredPath) logger.Debugf("restricted mode: %#v", rs.restricted) logger.Debugf("unrestricted paths: %q", rs.assumptions.unrestrictedPaths) logger.Debugf("verified devices: %v", rs.assumptions.verifiedDevices) logger.Debugf("past changes: %v", rs.assumptions.pastChanges) return &TrespassingError{ViolatedPath: filepath.Clean(dirName), DesiredPath: rs.desiredPath} } // Lift lifts write restrictions for the desired path. // // This function should be called when, as subsequent components of a path are // either discovered or created, the conditions for using restricted mode are // no longer true. func (rs *Restrictions) Lift() { if rs != nil { rs.restricted = false } } // TrespassingError is an error when filesystem operation would affect the host. type TrespassingError struct { ViolatedPath string DesiredPath string } // Error returns a formatted error message. func (e *TrespassingError) Error() string { return fmt.Sprintf("cannot write to %q because it would affect the host in %q", e.DesiredPath, e.ViolatedPath) } // isReadOnly checks whether the underlying filesystem is read only or is mounted as such. func isReadOnly(dirName string, fsData *syscall.Statfs_t) bool { // If something is mounted with f_flags & ST_RDONLY then is read-only. if fsData.Flags&StReadOnly == StReadOnly { return true } // If something is a known read-only file-system then it is safe. // Older copies of snapd were not mounting squashfs as read only. if fsData.Type == SquashfsMagic { return true } return false } // isPrivateTmpfsCreatedBySnapd checks whether a directory resides on a tmpfs mounted by snapd // // The function inspects the directory and a list of changes that were applied // to the mount namespace. A directory is trusted if it is a tmpfs that was // mounted by snap-confine or snapd-update-ns. Note that sub-directories of a // trusted tmpfs are not considered trusted by this function. func isPrivateTmpfsCreatedBySnapd(dirName string, fsData *syscall.Statfs_t, fileData *syscall.Stat_t, changes []*Change) bool { // If something is not a tmpfs it cannot be the trusted tmpfs we are looking for. if fsData.Type != TmpfsMagic { return false } // Any of the past changes that mounted a tmpfs exactly at the directory we // are inspecting is considered as trusted. This is conservative because it // doesn't trust sub-directories of a trusted tmpfs. This approach is // sufficient for the intended use. // // The algorithm goes over all the changes in reverse and picks up the // first tmpfs mount or unmount action that matches the directory name. // The set of constraints in snap-update-ns and snapd prevent from mounting // over an existing mount point so we don't need to consider e.g. a bind // mount shadowing an active tmpfs. for i := len(changes) - 1; i >= 0; i-- { change := changes[i] if change.Entry.Type == "tmpfs" && change.Entry.Dir == dirName { return change.Action == Mount } } return false } snapd-2.37.4~14.04.1/cmd/snap-update-ns/bootstrap.c0000664000000000000000000003352613435556260016335 0ustar /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ // IMPORTANT: all the code in this file may be run with elevated privileges // when invoking snap-update-ns from the setuid snap-confine. // // This file is a preprocessor for snap-update-ns' main() function. It will // perform input validation and clear the environment so that snap-update-ns' // go code runs with safe inputs when called by the setuid() snap-confine. #include "bootstrap.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // bootstrap_errno contains a copy of errno if a system call fails. int bootstrap_errno = 0; // bootstrap_msg contains a static string if something fails. const char *bootstrap_msg = NULL; // setns_into_snap switches mount namespace into that of a given snap. static int setns_into_snap(const char *snap_name) { // Construct the name of the .mnt file to open. char buf[PATH_MAX] = { 0, }; int n = snprintf(buf, sizeof buf, "/run/snapd/ns/%s.mnt", snap_name); if (n >= sizeof buf || n < 0) { bootstrap_errno = 0; bootstrap_msg = "cannot format mount namespace file name"; return -1; } // Open the mount namespace file. int fd = open(buf, O_RDONLY | O_CLOEXEC | O_NOFOLLOW); if (fd < 0) { bootstrap_errno = errno; bootstrap_msg = "cannot open mount namespace file"; return -1; } // Switch to the mount namespace of the given snap. int err = setns(fd, CLONE_NEWNS); if (err < 0) { bootstrap_errno = errno; bootstrap_msg = "cannot switch mount namespace"; }; close(fd); return err; } // switch_to_privileged_user drops to the real user ID while retaining // CAP_SYS_ADMIN, for operations such as mount(). static int switch_to_privileged_user() { uid_t real_uid; gid_t real_gid; real_uid = getuid(); if (real_uid == 0) { // We're running as root: no need to switch IDs return 0; } real_gid = getgid(); // _LINUX_CAPABILITY_VERSION_3 valid for kernel >= 2.6.26. See // https://github.com/torvalds/linux/blob/master/kernel/capability.c struct __user_cap_header_struct hdr = { _LINUX_CAPABILITY_VERSION_3, 0 }; struct __user_cap_data_struct data[2] = { {0} }; data[0].effective = (CAP_TO_MASK(CAP_SYS_ADMIN) | CAP_TO_MASK(CAP_SETUID) | CAP_TO_MASK(CAP_SETGID)); data[0].permitted = data[0].effective; data[0].inheritable = 0; data[1].effective = 0; data[1].permitted = 0; data[1].inheritable = 0; if (capset(&hdr, data) != 0) { bootstrap_errno = errno; bootstrap_msg = "cannot set permitted capabilities mask"; return -1; } if (prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0) != 0) { bootstrap_errno = errno; bootstrap_msg = "cannot tell kernel to keep capabilities over setuid"; return -1; } if (setgroups(1, &real_gid) != 0) { bootstrap_errno = errno; bootstrap_msg = "cannot drop supplementary groups"; return -1; } if (setgid(real_gid) != 0) { bootstrap_errno = errno; bootstrap_msg = "cannot switch to real group ID"; return -1; } if (setuid(real_uid) != 0) { bootstrap_errno = errno; bootstrap_msg = "cannot switch to real user ID"; return -1; } // After changing uid, our effective capabilities were dropped. // Reacquire CAP_SYS_ADMIN, and discard CAP_SETUID/CAP_SETGID. data[0].effective = CAP_TO_MASK(CAP_SYS_ADMIN); data[0].permitted = data[0].effective; if (capset(&hdr, data) != 0) { bootstrap_errno = errno; bootstrap_msg = "cannot enable capabilities after switching to real user"; return -1; } return 0; } // TODO: reuse the code from snap-confine, if possible. static int skip_lowercase_letters(const char **p) { int skipped = 0; const char *c; for (c = *p; *c >= 'a' && *c <= 'z'; ++c) { skipped += 1; } *p = (*p) + skipped; return skipped; } // TODO: reuse the code from snap-confine, if possible. static int skip_digits(const char **p) { int skipped = 0; const char *c; for (c = *p; *c >= '0' && *c <= '9'; ++c) { skipped += 1; } *p = (*p) + skipped; return skipped; } // TODO: reuse the code from snap-confine, if possible. static int skip_one_char(const char **p, char c) { if (**p == c) { *p += 1; return 1; } return 0; } // validate_snap_name performs full validation of the given name. int validate_snap_name(const char *snap_name) { // NOTE: This function should be synchronized with the two other // implementations: sc_snap_name_validate and snap.ValidateName. // Ensure that name is not NULL if (snap_name == NULL) { bootstrap_msg = "snap name cannot be NULL"; return -1; } // This is a regexp-free routine hand-codes the following pattern: // // "^([a-z0-9]+-?)*[a-z](-?[a-z0-9])*$" // // The only motivation for not using regular expressions is so that we // don't run untrusted input against a potentially complex regular // expression engine. const char *p = snap_name; if (skip_one_char(&p, '-')) { bootstrap_msg = "snap name cannot start with a dash"; return -1; } bool got_letter = false; int n = 0, m; for (; *p != '\0';) { if ((m = skip_lowercase_letters(&p)) > 0) { n += m; got_letter = true; continue; } if ((m = skip_digits(&p)) > 0) { n += m; continue; } if (skip_one_char(&p, '-') > 0) { n++; if (*p == '\0') { bootstrap_msg = "snap name cannot end with a dash"; return -1; } if (skip_one_char(&p, '-') > 0) { bootstrap_msg = "snap name cannot contain two consecutive dashes"; return -1; } continue; } bootstrap_msg = "snap name must use lower case letters, digits or dashes"; return -1; } if (!got_letter) { bootstrap_msg = "snap name must contain at least one letter"; return -1; } if (n < 2) { bootstrap_msg = "snap name must be longer than 1 character"; return -1; } if (n > 40) { bootstrap_msg = "snap name must be shorter than 40 characters"; return -1; } bootstrap_msg = NULL; 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; } // parse the -u argument, returns -1 on failure or 0 on success. static int parse_arg_u(int argc, char * const *argv, int *optind, unsigned long *uid_out) { if (*optind + 1 == argc || argv[*optind + 1] == NULL) { bootstrap_msg = "-u requires an argument"; bootstrap_errno = 0; return -1; } const char *uid_text = argv[*optind + 1]; errno = 0; char *uid_text_end = NULL; unsigned long parsed_uid = strtoul(uid_text, &uid_text_end, 10); if ( /* Reject overflow in parsed representation */ (parsed_uid == ULONG_MAX && errno != 0) /* Reject leading whitespace allowed by strtoul. */ || (isspace(*uid_text)) /* Reject empty string. */ || (*uid_text == '\0') /* Reject partially parsed strings. */ || (*uid_text != '\0' && uid_text_end != NULL && *uid_text_end != '\0')) { bootstrap_msg = "cannot parse user id"; bootstrap_errno = errno; return -1; } if ((long)parsed_uid < 0) { bootstrap_msg = "user id cannot be negative"; bootstrap_errno = 0; return -1; } if (uid_out != NULL) { *uid_out = parsed_uid; } *optind += 1; // Account for the argument to -u. 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, unsigned long * uid_out) { // Find the name of the called program. If it is ending with ".test" then do nothing. // NOTE: This lets us use cgo/go to write tests without running the bulk // of the code automatically. // if (argv == NULL || argc < 1) { bootstrap_errno = 0; bootstrap_msg = "argv0 is corrupted"; return; } const char *argv0 = argv[0]; const char *argv0_suffix_maybe = strstr(argv0, ".test"); if (argv0_suffix_maybe != NULL && argv0_suffix_maybe[strlen(".test")] == '\0') { bootstrap_errno = 0; bootstrap_msg = "bootstrap is not enabled while testing"; return; } bool should_setns = true; bool user_fstab = false; const char *snap_name = NULL; // Sanity check the command line arguments. The go parts will // scan this too. int i; for (i = 1; i < argc; i++) { const char *arg = argv[i]; if (arg[0] == '-') { /* We have an option */ if (!strcmp(arg, "--from-snap-confine")) { // When we are running under "--from-snap-confine" // option skip the setns call as snap-confine has // already placed us in the right namespace. should_setns = false; } else if (!strcmp(arg, "--user-mounts")) { user_fstab = true; // Processing the user-fstab file implies we're being // called from snap-confine. should_setns = false; } else if (!strcmp(arg, "-u")) { if (parse_arg_u(argc, argv, &i, uid_out)) { return; } // Providing an user identifier implies we are performing an // update of a specific user mount namespace and that we are // invoked from snapd and we should setns ourselves. When // invoked from snap-confine we are only called with // --from-snap-confine and with --user-mounts. should_setns = true; user_fstab = true; } else { bootstrap_errno = 0; bootstrap_msg = "unsupported option"; return; } } else { // We expect a single positional argument: the snap name if (snap_name != NULL) { bootstrap_errno = 0; bootstrap_msg = "too many positional arguments"; return; } snap_name = arg; } } // If there's no snap name given, just bail out. if (snap_name == NULL) { bootstrap_errno = 0; bootstrap_msg = "snap name not provided"; return; } // 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_instance_name(snap_name) < 0) { bootstrap_errno = 0; // bootstap_msg is set by validate_instance_name; return; } // We have a valid snap name now so let's store it. if (snap_name_out != NULL) { *snap_name_out = snap_name; } if (should_setns_out != NULL) { *should_setns_out = should_setns; } if (process_user_fstab != NULL) { *process_user_fstab = user_fstab; } bootstrap_errno = 0; bootstrap_msg = NULL; } // bootstrap prepares snap-update-ns to work in the namespace of the snap given // on command line. void bootstrap(int argc, char **argv, char **envp) { // We may have been started via a setuid-root snap-confine. In order to // prevent environment-based attacks we start by erasing all environment // variables. char *snapd_debug = getenv("SNAPD_DEBUG"); if (clearenv() != 0) { bootstrap_errno = 0; bootstrap_msg = "bootstrap could not clear the environment"; return; } if (snapd_debug != NULL) { setenv("SNAPD_DEBUG", snapd_debug, 0); } // Analyze the read process cmdline to find the snap name and decide if we // should use setns to jump into the mount namespace of a particular snap. // This is spread out for easier testability. const char *snap_name = NULL; bool should_setns = false; bool process_user_fstab = false; unsigned long uid = 0; process_arguments(argc, argv, &snap_name, &should_setns, &process_user_fstab, &uid); if (process_user_fstab) { switch_to_privileged_user(); // switch_to_privileged_user sets bootstrap_{errno,msg} } else if (snap_name != NULL && should_setns) { setns_into_snap(snap_name); // setns_into_snap sets bootstrap_{errno,msg} } } snapd-2.37.4~14.04.1/cmd/snap-update-ns/change_test.go0000664000000000000000000033034313435556260016764 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main_test import ( "errors" "os" "strings" "syscall" . "gopkg.in/check.v1" update "github.com/snapcore/snapd/cmd/snap-update-ns" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/testutil" ) type changeSuite struct { testutil.BaseTest sys *testutil.SyscallRecorder as *update.Assumptions } var ( errTesting = errors.New("testing") ) var _ = Suite(&changeSuite{}) func (s *changeSuite) SetUpTest(c *C) { s.BaseTest.SetUpTest(c) // Mock and record system interactions. s.sys = &testutil.SyscallRecorder{} s.BaseTest.AddCleanup(update.MockSystemCalls(s.sys)) s.as = &update.Assumptions{} } func (s *changeSuite) TearDownTest(c *C) { s.BaseTest.TearDownTest(c) s.sys.CheckForStrayDescriptors(c) } func (s *changeSuite) TestFakeFileInfo(c *C) { c.Assert(testutil.FileInfoDir.IsDir(), Equals, true) c.Assert(testutil.FileInfoFile.IsDir(), Equals, false) c.Assert(testutil.FileInfoSymlink.IsDir(), Equals, false) } func (s *changeSuite) TestString(c *C) { change := update.Change{ Entry: osutil.MountEntry{Dir: "/a/b", Name: "/dev/sda1"}, Action: update.Mount, } c.Assert(change.String(), Equals, "mount (/dev/sda1 /a/b none defaults 0 0)") } // When there are no profiles we don't do anything. func (s *changeSuite) TestNeededChangesNoProfiles(c *C) { current := &osutil.MountProfile{} desired := &osutil.MountProfile{} changes := update.NeededChanges(current, desired) c.Assert(changes, IsNil) } // When the profiles are the same we don't do anything. func (s *changeSuite) TestNeededChangesNoChange(c *C) { current := &osutil.MountProfile{Entries: []osutil.MountEntry{{Dir: "/common/stuff"}}} desired := &osutil.MountProfile{Entries: []osutil.MountEntry{{Dir: "/common/stuff"}}} changes := update.NeededChanges(current, desired) c.Assert(changes, DeepEquals, []*update.Change{ {Entry: osutil.MountEntry{Dir: "/common/stuff"}, Action: update.Keep}, }) } // When the content interface is connected we should mount the new entry. func (s *changeSuite) TestNeededChangesTrivialMount(c *C) { current := &osutil.MountProfile{} desired := &osutil.MountProfile{Entries: []osutil.MountEntry{{Dir: "/common/stuff"}}} changes := update.NeededChanges(current, desired) c.Assert(changes, DeepEquals, []*update.Change{ {Entry: desired.Entries[0], Action: update.Mount}, }) } // When the content interface is disconnected we should unmount the mounted entry. func (s *changeSuite) TestNeededChangesTrivialUnmount(c *C) { current := &osutil.MountProfile{Entries: []osutil.MountEntry{{Dir: "/common/stuff"}}} desired := &osutil.MountProfile{} changes := update.NeededChanges(current, desired) c.Assert(changes, DeepEquals, []*update.Change{ {Entry: current.Entries[0], Action: update.Unmount}, }) } // When umounting we unmount children before parents. func (s *changeSuite) TestNeededChangesUnmountOrder(c *C) { current := &osutil.MountProfile{Entries: []osutil.MountEntry{ {Dir: "/common/stuff/extra"}, {Dir: "/common/stuff"}, }} desired := &osutil.MountProfile{} changes := update.NeededChanges(current, desired) c.Assert(changes, DeepEquals, []*update.Change{ {Entry: osutil.MountEntry{Dir: "/common/stuff/extra"}, Action: update.Unmount}, {Entry: osutil.MountEntry{Dir: "/common/stuff"}, Action: update.Unmount}, }) } // When mounting we mount the parents before the children. func (s *changeSuite) TestNeededChangesMountOrder(c *C) { current := &osutil.MountProfile{} desired := &osutil.MountProfile{Entries: []osutil.MountEntry{ {Dir: "/common/stuff/extra"}, {Dir: "/common/stuff"}, }} changes := update.NeededChanges(current, desired) c.Assert(changes, DeepEquals, []*update.Change{ {Entry: osutil.MountEntry{Dir: "/common/stuff"}, Action: update.Mount}, {Entry: osutil.MountEntry{Dir: "/common/stuff/extra"}, Action: update.Mount}, }) } // When parent changes we don't reuse its children func (s *changeSuite) TestNeededChangesChangedParentSameChild(c *C) { current := &osutil.MountProfile{Entries: []osutil.MountEntry{ {Dir: "/common/stuff", Name: "/dev/sda1"}, {Dir: "/common/stuff/extra"}, {Dir: "/common/unrelated"}, }} desired := &osutil.MountProfile{Entries: []osutil.MountEntry{ {Dir: "/common/stuff", Name: "/dev/sda2"}, {Dir: "/common/stuff/extra"}, {Dir: "/common/unrelated"}, }} changes := update.NeededChanges(current, desired) c.Assert(changes, DeepEquals, []*update.Change{ {Entry: osutil.MountEntry{Dir: "/common/unrelated"}, Action: update.Keep}, {Entry: osutil.MountEntry{Dir: "/common/stuff/extra"}, Action: update.Unmount}, {Entry: osutil.MountEntry{Dir: "/common/stuff", Name: "/dev/sda1"}, Action: update.Unmount}, {Entry: osutil.MountEntry{Dir: "/common/stuff", Name: "/dev/sda2"}, Action: update.Mount}, {Entry: osutil.MountEntry{Dir: "/common/stuff/extra"}, Action: update.Mount}, }) } // When child changes we don't touch the unchanged parent func (s *changeSuite) TestNeededChangesSameParentChangedChild(c *C) { current := &osutil.MountProfile{Entries: []osutil.MountEntry{ {Dir: "/common/stuff"}, {Dir: "/common/stuff/extra", Name: "/dev/sda1"}, {Dir: "/common/unrelated"}, }} desired := &osutil.MountProfile{Entries: []osutil.MountEntry{ {Dir: "/common/stuff"}, {Dir: "/common/stuff/extra", Name: "/dev/sda2"}, {Dir: "/common/unrelated"}, }} changes := update.NeededChanges(current, desired) c.Assert(changes, DeepEquals, []*update.Change{ {Entry: osutil.MountEntry{Dir: "/common/unrelated"}, Action: update.Keep}, {Entry: osutil.MountEntry{Dir: "/common/stuff/extra", Name: "/dev/sda1"}, Action: update.Unmount}, {Entry: osutil.MountEntry{Dir: "/common/stuff"}, Action: update.Keep}, {Entry: osutil.MountEntry{Dir: "/common/stuff/extra", Name: "/dev/sda2"}, Action: update.Mount}, }) } // Unused bind mount farms are unmounted. func (s *changeSuite) TestNeededChangesTmpfsBindMountFarmUnused(c *C) { current := &osutil.MountProfile{Entries: []osutil.MountEntry{{ // The tmpfs that lets us write into immutable squashfs. We mock // x-snapd.needed-by to the last entry in the current profile (the bind // mount). Mark it synthetic since it is a helper mount that is needed // to facilitate the following mounts. Name: "tmpfs", Dir: "/snap/name/42/subdir", Type: "tmpfs", Options: []string{"x-snapd.needed-by=/snap/name/42/subdir", "x-snapd.synthetic"}, }, { // A bind mount to preserve a directory hidden by the tmpfs (the mount // point is created elsewhere). We mock x-snapd.needed-by to the // location of the bind mount below that is no longer desired. Name: "/var/lib/snapd/hostfs/snap/name/42/subdir/existing", Dir: "/snap/name/42/subdir/existing", Options: []string{"bind", "ro", "x-snapd.needed-by=/snap/name/42/subdir", "x-snapd.synthetic"}, }, { // A bind mount to put some content from another snap. The bind mount // is nothing special but the fact that it is possible is the reason // the two entries above exist. The mount point (created) is created // elsewhere. Name: "/snap/other/123/libs", Dir: "/snap/name/42/subdir/created", Options: []string{"bind", "ro"}, }}} desired := &osutil.MountProfile{} changes := update.NeededChanges(current, desired) c.Assert(changes, DeepEquals, []*update.Change{ {Entry: osutil.MountEntry{ Name: "/var/lib/snapd/hostfs/snap/name/42/subdir/existing", Dir: "/snap/name/42/subdir/existing", Options: []string{"bind", "ro", "x-snapd.needed-by=/snap/name/42/subdir", "x-snapd.synthetic"}, }, Action: update.Unmount}, {Entry: osutil.MountEntry{ Name: "/snap/other/123/libs", Dir: "/snap/name/42/subdir/created", Options: []string{"bind", "ro"}, }, Action: update.Unmount}, {Entry: osutil.MountEntry{ Name: "tmpfs", Dir: "/snap/name/42/subdir", Type: "tmpfs", Options: []string{"x-snapd.needed-by=/snap/name/42/subdir", "x-snapd.synthetic"}, }, Action: update.Unmount}, }) } func (s *changeSuite) TestNeededChangesTmpfsBindMountFarmUsed(c *C) { // NOTE: the current profile is the same as in the test // TestNeededChangesTmpfsBindMountFarmUnused written above. current := &osutil.MountProfile{Entries: []osutil.MountEntry{{ Name: "tmpfs", Dir: "/snap/name/42/subdir", Type: "tmpfs", Options: []string{"x-snapd.needed-by=/snap/name/42/subdir/created", "x-snapd.synthetic"}, }, { Name: "/var/lib/snapd/hostfs/snap/name/42/subdir/existing", Dir: "/snap/name/42/subdir/existing", Options: []string{"bind", "ro", "x-snapd.needed-by=/snap/name/42/subdir/created", "x-snapd.synthetic"}, }, { Name: "/snap/other/123/libs", Dir: "/snap/name/42/subdir/created", Options: []string{"bind", "ro"}, }}} desired := &osutil.MountProfile{Entries: []osutil.MountEntry{{ // This is the only entry that we explicitly want but in order to // support it we need to keep the remaining implicit entries. Name: "/snap/other/123/libs", Dir: "/snap/name/42/subdir/created", Options: []string{"bind", "ro"}, }}} changes := update.NeededChanges(current, desired) c.Assert(changes, DeepEquals, []*update.Change{ {Entry: osutil.MountEntry{ Name: "/var/lib/snapd/hostfs/snap/name/42/subdir/existing", Dir: "/snap/name/42/subdir/existing", Options: []string{"bind", "ro", "x-snapd.needed-by=/snap/name/42/subdir/created", "x-snapd.synthetic"}, }, Action: update.Keep}, {Entry: osutil.MountEntry{ Name: "/snap/other/123/libs", Dir: "/snap/name/42/subdir/created", Options: []string{"bind", "ro"}, }, Action: update.Keep}, {Entry: osutil.MountEntry{ Name: "tmpfs", Dir: "/snap/name/42/subdir", Type: "tmpfs", Options: []string{"x-snapd.needed-by=/snap/name/42/subdir/created", "x-snapd.synthetic"}, }, Action: update.Keep}, }) } // cur = ['/a/b', '/a/b-1', '/a/b-1/3', '/a/b/c'] // des = ['/a/b', '/a/b-1', '/a/b/c' // // We are smart about comparing entries as directories. Here even though "/a/b" // is a prefix of "/a/b-1" it is correctly reused. func (s *changeSuite) TestNeededChangesSmartEntryComparison(c *C) { current := &osutil.MountProfile{Entries: []osutil.MountEntry{ {Dir: "/a/b", Name: "/dev/sda1"}, {Dir: "/a/b-1"}, {Dir: "/a/b-1/3"}, {Dir: "/a/b/c"}, }} desired := &osutil.MountProfile{Entries: []osutil.MountEntry{ {Dir: "/a/b", Name: "/dev/sda2"}, {Dir: "/a/b-1"}, {Dir: "/a/b/c"}, }} changes := update.NeededChanges(current, desired) c.Assert(changes, DeepEquals, []*update.Change{ {Entry: osutil.MountEntry{Dir: "/a/b/c"}, Action: update.Unmount}, {Entry: osutil.MountEntry{Dir: "/a/b", Name: "/dev/sda1"}, Action: update.Unmount}, {Entry: osutil.MountEntry{Dir: "/a/b-1/3"}, Action: update.Unmount}, {Entry: osutil.MountEntry{Dir: "/a/b-1"}, Action: update.Keep}, {Entry: osutil.MountEntry{Dir: "/a/b", Name: "/dev/sda2"}, Action: update.Mount}, {Entry: osutil.MountEntry{Dir: "/a/b/c"}, Action: update.Mount}, }) } // Parallel instance changes are executed first func (s *changeSuite) TestNeededChangesParallelInstancesManyComeFirst(c *C) { desired := &osutil.MountProfile{Entries: []osutil.MountEntry{ {Dir: "/common/stuff", Name: "/dev/sda1"}, {Dir: "/common/stuff/extra"}, {Dir: "/common/unrelated"}, {Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, {Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, }} changes := update.NeededChanges(&osutil.MountProfile{}, desired) c.Assert(changes, DeepEquals, []*update.Change{ {Entry: osutil.MountEntry{Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Mount}, {Entry: osutil.MountEntry{Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Mount}, {Entry: osutil.MountEntry{Dir: "/common/stuff", Name: "/dev/sda1"}, Action: update.Mount}, {Entry: osutil.MountEntry{Dir: "/common/stuff/extra"}, Action: update.Mount}, {Entry: osutil.MountEntry{Dir: "/common/unrelated"}, Action: update.Mount}, }) } // Parallel instance changes are kept if already present func (s *changeSuite) TestNeededChangesParallelInstancesKeep(c *C) { desired := &osutil.MountProfile{Entries: []osutil.MountEntry{ {Dir: "/common/stuff", Name: "/dev/sda1"}, {Dir: "/common/unrelated"}, {Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, {Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, }} current := &osutil.MountProfile{Entries: []osutil.MountEntry{ {Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, {Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, }} changes := update.NeededChanges(current, desired) c.Assert(changes, DeepEquals, []*update.Change{ {Entry: osutil.MountEntry{Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Keep}, {Entry: osutil.MountEntry{Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Keep}, {Entry: osutil.MountEntry{Dir: "/common/stuff", Name: "/dev/sda1"}, Action: update.Mount}, {Entry: osutil.MountEntry{Dir: "/common/unrelated"}, Action: update.Mount}, }) } // Parallel instance with mounts inside func (s *changeSuite) TestNeededChangesParallelInstancesInsideMount(c *C) { desired := &osutil.MountProfile{Entries: []osutil.MountEntry{ {Dir: "/foo/bar/baz"}, {Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, {Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, }} current := &osutil.MountProfile{Entries: []osutil.MountEntry{ {Dir: "/foo/bar/zed"}, {Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, {Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, }} changes := update.NeededChanges(current, desired) c.Assert(changes, DeepEquals, []*update.Change{ {Entry: osutil.MountEntry{Dir: "/foo/bar/zed"}, Action: update.Unmount}, {Entry: osutil.MountEntry{Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Keep}, {Entry: osutil.MountEntry{Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Keep}, {Entry: osutil.MountEntry{Dir: "/foo/bar/baz"}, Action: update.Mount}, }) } func mustReadProfile(profileStr string) *osutil.MountProfile { profile, err := osutil.ReadMountProfile(strings.NewReader(profileStr)) if err != nil { panic(err) } return profile } func (s *changeSuite) TestRuntimeUsingSymlinks(c *C) { // We start with a runtime shared from one snap to another and then exposed // to /opt with a symbolic link. This is the initial state of the // application in version v1. initial := mustReadProfile("") desired_v1 := mustReadProfile( "none /opt/runtime none x-snapd.kind=symlink,x-snapd.symlink=/snap/app/x1/runtime,x-snapd.origin=layout 0 0\n" + "/snap/runtime/x1/opt/runtime /snap/app/x1/runtime none bind,ro 0 0\n") // The changes we compute are trivial, simply perform each operation in order. changes := update.NeededChanges(initial, desired_v1) c.Assert(changes, DeepEquals, []*update.Change{ {Entry: desired_v1.Entries[0], Action: update.Mount}, {Entry: desired_v1.Entries[1], Action: update.Mount}, }) // After performing both changes we have a new synthesized entry. We get an // extra writable mimic over /opt so that we can add our symlink. The // content sharing into $SNAP is applied as expected since the snap ships // the required mount point. current_v1 := mustReadProfile( "/snap/runtime/x1/opt/runtime /snap/app/x1/runtime none bind,ro 0 0\n" + "none /opt/runtime none x-snapd.kind=symlink,x-snapd.symlink=/snap/app/x1/runtime,x-snapd.origin=layout 0 0\n" + "tmpfs /opt tmpfs x-snapd.synthetic,x-snapd.needed-by=/opt/runtime,mode=0755,uid=0,gid=0 0") // We now proceed to replace app v1 with v2 which uses a bind mount instead // of a symlink. First, let's start with the updated desired profile: desired_v2 := mustReadProfile( "/snap/app/x2/runtime /opt/runtime none rbind,rw,x-snapd.origin=layout 0 0\n" + "/snap/runtime/x1/opt/runtime /snap/app/x2/runtime none bind,ro 0 0\n") // Let's see what the update algorithm thinks. changes = update.NeededChanges(current_v1, desired_v2) c.Assert(changes, DeepEquals, []*update.Change{ // We are dropping the content interface bind mount because app changed revision {Entry: current_v1.Entries[0], Action: update.Unmount}, // We are also dropping the symlink we had in /opt/runtime {Entry: current_v1.Entries[1], Action: update.Unmount}, // But, we are keeping the /opt tmpfs because we still want /opt/runtime to exist (neat!) {Entry: current_v1.Entries[2], Action: update.Keep}, // We are adding a new bind mount for /opt/runtime {Entry: desired_v2.Entries[0], Action: update.Mount}, // We also adding the updated path of the content interface (for revision x2) {Entry: desired_v2.Entries[1], Action: update.Mount}, }) // After performing all those changes this is the profile we observe. current_v2 := mustReadProfile( "tmpfs /opt tmpfs x-snapd.synthetic,x-snapd.needed-by=/opt/runtime,mode=0755,uid=0,gid=0 0 0\n" + "/snap/app/x2/runtime /opt/runtime none rbind,rw,x-snapd.origin=layout 0 0\n" + "/snap/runtime/x1/opt/runtime /snap/app/x2/runtime none bind,ro 0 0\n") // So far so good. To trigger the issue we now revert or refresh to v1 // again. Let's see what happens here. The desired profiles are already // known so let's see what the algorithm thinks now. changes = update.NeededChanges(current_v2, desired_v1) c.Assert(changes, DeepEquals, []*update.Change{ // We are, again, dropping the content interface bind mount because app changed revision {Entry: current_v2.Entries[2], Action: update.Unmount}, // We are also dropping the bind mount from /opt/runtime since we want a symlink instead {Entry: current_v2.Entries[1], Action: update.Unmount}, // Again, we reuse the tmpfs. {Entry: current_v2.Entries[0], Action: update.Keep}, // We are providing a symlink /opt/runtime -> to $SNAP/runtime. {Entry: desired_v1.Entries[0], Action: update.Mount}, // We are bind mounting the runtime from another snap into $SNAP/runtime {Entry: desired_v1.Entries[1], Action: update.Mount}, }) // The problem is that the tmpfs contains leftovers from the things we // created and those prevent the execution of this mount profile. } // ######################################## // Topic: mounting & unmounting filesystems // ######################################## // Change.Perform returns errors from os.Lstat (apart from ErrNotExist) func (s *changeSuite) TestPerformFilesystemMountLstatError(c *C) { s.sys.InsertFault(`lstat "/target"`, errTesting) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, `cannot inspect "/target": testing`) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, E: errTesting}, }) } // Change.Perform wants to mount a filesystem. func (s *changeSuite) TestPerformFilesystemMount(c *C) { s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} synth, err := chg.Perform(s.as) c.Assert(err, IsNil) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, R: testutil.FileInfoDir}, {C: `mount "device" "/target" "type" 0 ""`}, }) } // Change.Perform wants to mount a filesystem but it fails. func (s *changeSuite) TestPerformFilesystemMountWithError(c *C) { s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) s.sys.InsertFault(`mount "device" "/target" "type" 0 ""`, errTesting) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} synth, err := chg.Perform(s.as) c.Assert(err, Equals, errTesting) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, R: testutil.FileInfoDir}, {C: `mount "device" "/target" "type" 0 ""`, E: errTesting}, }) } // Change.Perform wants to mount a filesystem but the mount point isn't there. func (s *changeSuite) TestPerformFilesystemMountWithoutMountPoint(c *C) { defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. s.sys.InsertFault(`lstat "/target"`, syscall.ENOENT) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} synth, err := chg.Perform(s.as) c.Assert(err, IsNil) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, E: syscall.ENOENT}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "target" 0755`}, {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `fchown 4 0 0`}, {C: `close 4`}, {C: `close 3`}, {C: `mount "device" "/target" "type" 0 ""`}, }) } // Change.Perform wants to create a filesystem but the mount point isn't there and cannot be created. func (s *changeSuite) TestPerformFilesystemMountWithoutMountPointWithErrors(c *C) { defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. s.sys.InsertFault(`lstat "/target"`, syscall.ENOENT) s.sys.InsertFault(`mkdirat 3 "target" 0755`, errTesting) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, `cannot create directory "/target": testing`) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, E: syscall.ENOENT}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "target" 0755`, E: errTesting}, {C: `close 3`}, }) } // Change.Perform wants to mount a filesystem but the mount point isn't there and the parent is read-only. func (s *changeSuite) TestPerformFilesystemMountWithoutMountPointAndReadOnlyBase(c *C) { defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. s.sys.InsertFault(`lstat "/rofs/target"`, syscall.ENOENT) s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) s.sys.InsertFault(`openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, syscall.ENOENT, nil) // works on 2nd try s.sys.InsertFault(`mkdirat 4 "target" 0755`, syscall.EROFS, nil) // works on 2nd try s.sys.InsertSysLstatResult(`lstat "/rofs" `, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}) s.sys.InsertReadDirResult(`readdir "/rofs"`, nil) // pretend /rofs is empty. s.sys.InsertFault(`lstat "/tmp/.snap/rofs"`, syscall.ENOENT) s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) s.sys.InsertFstatResult(`fstat 7 `, syscall.Stat_t{}) s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{}) s.sys.InsertFstatfsResult(`fstatfs 6 `, syscall.Statfs_t{}) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/rofs/target", Type: "type"}} synth, err := chg.Perform(s.as) c.Assert(err, IsNil) c.Assert(synth, DeepEquals, []*update.Change{ {Action: update.Mount, Entry: osutil.MountEntry{ Name: "tmpfs", Dir: "/rofs", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/rofs/target", "mode=0755", "uid=0", "gid=0"}}, }, }) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ // sniff mount target {C: `lstat "/rofs/target"`, E: syscall.ENOENT}, // /rofs/target is missing, create it {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `close 3`}, {C: `mkdirat 4 "target" 0755`, E: syscall.EROFS}, {C: `close 4`}, // error, read only filesystem, create a mimic {C: `lstat "/rofs" `, R: syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}}, {C: `readdir "/rofs"`, R: []os.FileInfo(nil)}, {C: `lstat "/tmp/.snap/rofs"`, E: syscall.ENOENT}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "tmp" 0755`}, {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `fchown 4 0 0`}, {C: `mkdirat 4 ".snap" 0755`}, {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5}, {C: `fchown 5 0 0`}, {C: `close 4`}, {C: `close 3`}, {C: `mkdirat 5 "rofs" 0755`}, {C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `fchown 3 0 0`}, {C: `close 3`}, {C: `close 5`}, {C: `lstat "/rofs"`, R: testutil.FileInfoDir}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, {C: `fstat 4 `, R: syscall.Stat_t{}}, {C: `close 3`}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, {C: `openat 5 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 6}, {C: `openat 6 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7}, {C: `fstat 7 `, R: syscall.Stat_t{}}, {C: `close 6`}, {C: `close 5`}, {C: `close 3`}, {C: `mount "/proc/self/fd/4" "/proc/self/fd/7" "" MS_BIND|MS_REC ""`}, {C: `close 7`}, {C: `close 4`}, {C: `lstat "/rofs"`, R: testutil.FileInfoDir}, {C: `mount "tmpfs" "/rofs" "tmpfs" 0 "mode=0755,uid=0,gid=0"`}, {C: `unmount "/tmp/.snap/rofs" UMOUNT_NOFOLLOW|MNT_DETACH`}, // Perform clean up after the unmount operation. {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, {C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, {C: `fstat 6 `, R: syscall.Stat_t{}}, {C: `close 5`}, {C: `close 4`}, {C: `close 3`}, {C: `fstatfs 6 `, R: syscall.Statfs_t{}}, {C: `remove "/tmp/.snap/rofs"`}, {C: `close 6`}, // mimic ready, re-try initial mkdir {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `close 3`}, {C: `mkdirat 4 "target" 0755`}, {C: `openat 4 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `fchown 3 0 0`}, {C: `close 3`}, {C: `close 4`}, // mount the filesystem {C: `mount "device" "/rofs/target" "type" 0 ""`}, }) } // Change.Perform wants to mount a filesystem but the mount point isn't there and the parent is read-only and mimic fails during planning. func (s *changeSuite) TestPerformFilesystemMountWithoutMountPointAndReadOnlyBaseErrorWhilePlanning(c *C) { defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. s.sys.InsertFault(`lstat "/rofs/target"`, syscall.ENOENT) s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) s.sys.InsertFault(`openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, syscall.ENOENT) s.sys.InsertFault(`mkdirat 4 "target" 0755`, syscall.EROFS) s.sys.InsertFault(`lstat "/tmp/.snap/rofs"`, syscall.ENOENT) s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir) s.sys.InsertSysLstatResult(`lstat "/rofs" `, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}) s.sys.InsertReadDirResult(`readdir "/rofs"`, nil) s.sys.InsertFault(`readdir "/rofs"`, errTesting) // make the writable mimic fail chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/rofs/target", Type: "type"}} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, `cannot create writable mimic over "/rofs": testing`) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ // sniff mount target {C: `lstat "/rofs/target"`, E: syscall.ENOENT}, // /rofs/target is missing, create it {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `close 3`}, {C: `mkdirat 4 "target" 0755`, E: syscall.EROFS}, {C: `close 4`}, // error, read only filesystem, create a mimic {C: `lstat "/rofs" `, R: syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}}, {C: `readdir "/rofs"`, E: errTesting}, // cannot create mimic, that's it }) } // Change.Perform wants to mount a filesystem but the mount point isn't there and the parent is read-only and mimic fails during execution. func (s *changeSuite) TestPerformFilesystemMountWithoutMountPointAndReadOnlyBaseErrorWhileExecuting(c *C) { defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. s.sys.InsertFault(`lstat "/rofs/target"`, syscall.ENOENT) s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) s.sys.InsertFault(`openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, syscall.ENOENT) s.sys.InsertFault(`mkdirat 4 "target" 0755`, syscall.EROFS) s.sys.InsertFault(`lstat "/tmp/.snap/rofs"`, syscall.ENOENT) s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir) s.sys.InsertSysLstatResult(`lstat "/rofs" `, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}) s.sys.InsertReadDirResult(`readdir "/rofs"`, nil) s.sys.InsertFault(`mkdirat 4 ".snap" 0755`, errTesting) // make the writable mimic fail chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/rofs/target", Type: "type"}} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, `cannot create writable mimic over "/rofs": cannot create directory "/tmp/.snap": testing`) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ // sniff mount target {C: `lstat "/rofs/target"`, E: syscall.ENOENT}, // /rofs/target is missing, create it {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `close 3`}, {C: `mkdirat 4 "target" 0755`, E: syscall.EROFS}, {C: `close 4`}, // error, read only filesystem, create a mimic {C: `lstat "/rofs" `, R: syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}}, {C: `readdir "/rofs"`, R: []os.FileInfo(nil)}, {C: `lstat "/tmp/.snap/rofs"`, E: syscall.ENOENT}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "tmp" 0755`}, {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `fchown 4 0 0`}, {C: `mkdirat 4 ".snap" 0755`, E: errTesting}, {C: `close 4`}, {C: `close 3`}, // cannot create mimic, that's it }) } // Change.Perform wants to mount a filesystem but there's a symlink in mount point. func (s *changeSuite) TestPerformFilesystemMountWithSymlinkInMountPoint(c *C) { s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoSymlink) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, `cannot use "/target" as mount point: not a directory`) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, R: testutil.FileInfoSymlink}, }) } // Change.Perform wants to mount a filesystem but there's a file in mount point. func (s *changeSuite) TestPerformFilesystemMountWithFileInMountPoint(c *C) { s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, `cannot use "/target" as mount point: not a directory`) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, R: testutil.FileInfoFile}, }) } // Change.Perform wants to unmount a filesystem. func (s *changeSuite) TestPerformFilesystemUnmount(c *C) { s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{}) chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} synth, err := chg.Perform(s.as) c.Assert(err, IsNil) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `unmount "/target" UMOUNT_NOFOLLOW`}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, {C: `fstat 4 `, R: syscall.Stat_t{}}, {C: `close 3`}, {C: `fstatfs 4 `, R: syscall.Statfs_t{}}, {C: `remove "/target"`}, {C: `close 4`}, }) c.Assert(synth, HasLen, 0) } // Change.Perform wants to detach a bind mount. func (s *changeSuite) TestPerformFilesystemDetch(c *C) { s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{}) chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/something", Dir: "/target", Options: []string{"x-snapd.detach"}}} synth, err := chg.Perform(s.as) c.Assert(err, IsNil) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `unmount "/target" UMOUNT_NOFOLLOW|MNT_DETACH`}, // Perform clean up after the unmount operation. {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, {C: `fstat 4 `, R: syscall.Stat_t{}}, {C: `close 3`}, {C: `fstatfs 4 `, R: syscall.Statfs_t{}}, {C: `remove "/target"`}, {C: `close 4`}, }) c.Assert(synth, HasLen, 0) } // Change.Perform wants to unmount a filesystem but it fails. func (s *changeSuite) TestPerformFilesystemUnmountError(c *C) { s.sys.InsertFault(`unmount "/target" UMOUNT_NOFOLLOW`, errTesting) chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} synth, err := chg.Perform(s.as) c.Assert(err, Equals, errTesting) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `unmount "/target" UMOUNT_NOFOLLOW`, E: errTesting}, }) c.Assert(synth, HasLen, 0) } // Change.Perform passes non-flag options to the kernel. func (s *changeSuite) TestPerformFilesystemMountWithOptions(c *C) { s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type", Options: []string{"ro", "funky"}}} synth, err := chg.Perform(s.as) c.Assert(err, IsNil) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, R: testutil.FileInfoDir}, {C: `mount "device" "/target" "type" MS_RDONLY "funky"`}, }) } // Change.Perform doesn't pass snapd-specific options to the kernel. func (s *changeSuite) TestPerformFilesystemMountWithSnapdSpecificOptions(c *C) { s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type", Options: []string{"ro", "x-snapd.funky"}}} synth, err := chg.Perform(s.as) c.Assert(err, IsNil) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, R: testutil.FileInfoDir}, {C: `mount "device" "/target" "type" MS_RDONLY ""`}, }) } // ############################################### // Topic: bind-mounting and unmounting directories // ############################################### // Change.Perform wants to bind mount a directory but the target cannot be stat'ed. func (s *changeSuite) TestPerformDirectoryBindMountTargetLstatError(c *C) { s.sys.InsertFault(`lstat "/target"`, errTesting) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, `cannot inspect "/target": testing`) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, E: errTesting}, }) } // Change.Perform wants to bind mount a directory but the source cannot be stat'ed. func (s *changeSuite) TestPerformDirectoryBindMountSourceLstatError(c *C) { s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) s.sys.InsertFault(`lstat "/source"`, errTesting) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, `cannot inspect "/source": testing`) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, R: testutil.FileInfoDir}, {C: `lstat "/source"`, E: errTesting}, }) } // Change.Perform wants to bind mount a directory. func (s *changeSuite) TestPerformDirectoryBindMount(c *C) { s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoDir) s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{}) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} synth, err := chg.Perform(s.as) c.Assert(err, IsNil) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, R: testutil.FileInfoDir}, {C: `lstat "/source"`, R: testutil.FileInfoDir}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, {C: `fstat 4 `, R: syscall.Stat_t{}}, {C: `close 3`}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, {C: `fstat 5 `, R: syscall.Stat_t{}}, {C: `close 3`}, {C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`}, {C: `close 5`}, {C: `close 4`}, }) } // Change.Perform wants to bind mount a directory but it fails. func (s *changeSuite) TestPerformDirectoryBindMountWithError(c *C) { s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoDir) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{}) s.sys.InsertFault(`mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`, errTesting) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} synth, err := chg.Perform(s.as) c.Assert(err, Equals, errTesting) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, R: testutil.FileInfoDir}, {C: `lstat "/source"`, R: testutil.FileInfoDir}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, {C: `fstat 4 `, R: syscall.Stat_t{}}, {C: `close 3`}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, {C: `fstat 5 `, R: syscall.Stat_t{}}, {C: `close 3`}, {C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`, E: errTesting}, {C: `close 5`}, {C: `close 4`}, }) } // Change.Perform wants to bind mount a directory but the mount point isn't there. func (s *changeSuite) TestPerformDirectoryBindMountWithoutMountPoint(c *C) { defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoDir) s.sys.InsertFault(`lstat "/target"`, syscall.ENOENT) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{}) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} synth, err := chg.Perform(s.as) c.Assert(err, IsNil) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, E: syscall.ENOENT}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "target" 0755`}, {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `fchown 4 0 0`}, {C: `close 4`}, {C: `close 3`}, {C: `lstat "/source"`, R: testutil.FileInfoDir}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, {C: `fstat 4 `, R: syscall.Stat_t{}}, {C: `close 3`}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, {C: `fstat 5 `, R: syscall.Stat_t{}}, {C: `close 3`}, {C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`}, {C: `close 5`}, {C: `close 4`}, }) } // Change.Perform wants to bind mount a directory but the mount source isn't there. func (s *changeSuite) TestPerformDirectoryBindMountWithoutMountSource(c *C) { defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. s.sys.InsertFault(`lstat "/source"`, syscall.ENOENT) s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{}) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} synth, err := chg.Perform(s.as) c.Assert(err, IsNil) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, R: testutil.FileInfoDir}, {C: `lstat "/source"`, E: syscall.ENOENT}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "source" 0755`}, {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `fchown 4 0 0`}, {C: `close 4`}, {C: `close 3`}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, {C: `fstat 4 `, R: syscall.Stat_t{}}, {C: `close 3`}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, {C: `fstat 5 `, R: syscall.Stat_t{}}, {C: `close 3`}, {C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`}, {C: `close 5`}, {C: `close 4`}, }) } // Change.Perform wants to create a directory bind mount but the mount point isn't there and cannot be created. func (s *changeSuite) TestPerformDirectoryBindMountWithoutMountPointWithErrors(c *C) { defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. s.sys.InsertFault(`lstat "/target"`, syscall.ENOENT) s.sys.InsertFault(`mkdirat 3 "target" 0755`, errTesting) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, `cannot create directory "/target": testing`) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, E: syscall.ENOENT}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "target" 0755`, E: errTesting}, {C: `close 3`}, }) } // Change.Perform wants to create a directory bind mount but the mount source isn't there and cannot be created. func (s *changeSuite) TestPerformDirectoryBindMountWithoutMountSourceWithErrors(c *C) { defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. s.sys.InsertFault(`lstat "/source"`, syscall.ENOENT) s.sys.InsertFault(`mkdirat 3 "source" 0755`, errTesting) s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, `cannot create directory "/source": testing`) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, R: testutil.FileInfoDir}, {C: `lstat "/source"`, E: syscall.ENOENT}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "source" 0755`, E: errTesting}, {C: `close 3`}, }) } // Change.Perform wants to bind mount a directory but the mount point isn't there and the parent is read-only. func (s *changeSuite) TestPerformDirectoryBindMountWithoutMountPointAndReadOnlyBase(c *C) { defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. s.sys.InsertFault(`lstat "/rofs/target"`, syscall.ENOENT) s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) s.sys.InsertFault(`openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, syscall.ENOENT, nil) // works on 2nd try s.sys.InsertFault(`mkdirat 4 "target" 0755`, syscall.EROFS, nil) // works on 2nd try s.sys.InsertSysLstatResult(`lstat "/rofs" `, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}) s.sys.InsertReadDirResult(`readdir "/rofs"`, nil) // pretend /rofs is empty. s.sys.InsertFault(`lstat "/tmp/.snap/rofs"`, syscall.ENOENT) s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir) s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoDir) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) s.sys.InsertFstatResult(`fstat 7 `, syscall.Stat_t{}) s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{}) s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{}) s.sys.InsertFstatfsResult(`fstatfs 6 `, syscall.Statfs_t{}) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/rofs/target", Options: []string{"bind"}}} synth, err := chg.Perform(s.as) c.Assert(err, IsNil) c.Assert(synth, DeepEquals, []*update.Change{ {Action: update.Mount, Entry: osutil.MountEntry{ Name: "tmpfs", Dir: "/rofs", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/rofs/target", "mode=0755", "uid=0", "gid=0"}}, }, }) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ // sniff mount target {C: `lstat "/rofs/target"`, E: syscall.ENOENT}, // /rofs/target is missing, create it {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `close 3`}, {C: `mkdirat 4 "target" 0755`, E: syscall.EROFS}, {C: `close 4`}, // error, read only filesystem, create a mimic {C: `lstat "/rofs" `, R: syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}}, {C: `readdir "/rofs"`, R: []os.FileInfo(nil)}, {C: `lstat "/tmp/.snap/rofs"`, E: syscall.ENOENT}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "tmp" 0755`}, {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `fchown 4 0 0`}, {C: `mkdirat 4 ".snap" 0755`}, {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5}, {C: `fchown 5 0 0`}, {C: `close 4`}, {C: `close 3`}, {C: `mkdirat 5 "rofs" 0755`}, {C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `fchown 3 0 0`}, {C: `close 3`}, {C: `close 5`}, {C: `lstat "/rofs"`, R: testutil.FileInfoDir}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, {C: `fstat 4 `, R: syscall.Stat_t{}}, {C: `close 3`}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, {C: `openat 5 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 6}, {C: `openat 6 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7}, {C: `fstat 7 `, R: syscall.Stat_t{}}, {C: `close 6`}, {C: `close 5`}, {C: `close 3`}, {C: `mount "/proc/self/fd/4" "/proc/self/fd/7" "" MS_BIND|MS_REC ""`}, {C: `close 7`}, {C: `close 4`}, {C: `lstat "/rofs"`, R: testutil.FileInfoDir}, {C: `mount "tmpfs" "/rofs" "tmpfs" 0 "mode=0755,uid=0,gid=0"`}, {C: `unmount "/tmp/.snap/rofs" UMOUNT_NOFOLLOW|MNT_DETACH`}, // Perform clean up after the unmount operation. {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, {C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, {C: `fstat 6 `, R: syscall.Stat_t{}}, {C: `close 5`}, {C: `close 4`}, {C: `close 3`}, {C: `fstatfs 6 `, R: syscall.Statfs_t{}}, {C: `remove "/tmp/.snap/rofs"`}, {C: `close 6`}, // mimic ready, re-try initial mkdir {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `close 3`}, {C: `mkdirat 4 "target" 0755`}, {C: `openat 4 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `fchown 3 0 0`}, {C: `close 3`}, {C: `close 4`}, // sniff mount source {C: `lstat "/source"`, R: testutil.FileInfoDir}, // mount the filesystem {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, {C: `fstat 4 `, R: syscall.Stat_t{}}, {C: `close 3`}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, {C: `openat 5 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, {C: `fstat 6 `, R: syscall.Stat_t{}}, {C: `close 5`}, {C: `close 3`}, {C: `mount "/proc/self/fd/4" "/proc/self/fd/6" "" MS_BIND ""`}, {C: `close 6`}, {C: `close 4`}, }) } // Change.Perform wants to bind mount a directory but the mount source isn't there and the parent is read-only. func (s *changeSuite) TestPerformDirectoryBindMountWithoutMountSourceAndReadOnlyBase(c *C) { defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir) s.sys.InsertFault(`lstat "/rofs/source"`, syscall.ENOENT) s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) s.sys.InsertFault(`mkdirat 4 "source" 0755`, syscall.EROFS) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/rofs/source", Dir: "/target", Options: []string{"bind"}}} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, `cannot operate on read-only filesystem at /rofs`) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, R: testutil.FileInfoDir}, {C: `lstat "/rofs/source"`, E: syscall.ENOENT}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `close 3`}, {C: `mkdirat 4 "source" 0755`, E: syscall.EROFS}, {C: `close 4`}, }) } // Change.Perform wants to bind mount a directory but the mount source isn't there and the parent is read-only but this is for a layout. func (s *changeSuite) TestPerformDirectoryBindMountWithoutMountSourceAndReadOnlyBaseForLayout(c *C) { defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) s.sys.InsertFault(`lstat "/rofs/source"`, syscall.ENOENT) s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) s.sys.InsertFault(`openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, syscall.ENOENT, nil) // works on 2nd try s.sys.InsertFault(`mkdirat 4 "source" 0755`, syscall.EROFS, nil) // works on 2nd try s.sys.InsertReadDirResult(`readdir "/rofs"`, nil) // pretend /rofs is empty. s.sys.InsertFault(`lstat "/tmp/.snap/rofs"`, syscall.ENOENT) s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir) s.sys.InsertSysLstatResult(`lstat "/rofs" `, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{}) s.sys.InsertFstatResult(`fstat 7 `, syscall.Stat_t{}) s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{}) s.sys.InsertFstatfsResult(`fstatfs 6 `, syscall.Statfs_t{}) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/rofs/source", Dir: "/target", Options: []string{"bind", "x-snapd.origin=layout"}}} synth, err := chg.Perform(s.as) c.Assert(err, IsNil) c.Check(synth, DeepEquals, []*update.Change{ {Action: update.Mount, Entry: osutil.MountEntry{ Name: "tmpfs", Dir: "/rofs", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/rofs/source", "mode=0755", "uid=0", "gid=0"}}, }, }) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ // sniff mount target and source {C: `lstat "/target"`, R: testutil.FileInfoDir}, {C: `lstat "/rofs/source"`, E: syscall.ENOENT}, // /rofs/source is missing, create it {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `close 3`}, {C: `mkdirat 4 "source" 0755`, E: syscall.EROFS}, {C: `close 4`}, // error /rofs is a read-only filesystem, create a mimic {C: `lstat "/rofs" `, R: syscall.Stat_t{Mode: 0755}}, {C: `readdir "/rofs"`, R: []os.FileInfo(nil)}, {C: `lstat "/tmp/.snap/rofs"`, E: syscall.ENOENT}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "tmp" 0755`}, {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `fchown 4 0 0`}, {C: `mkdirat 4 ".snap" 0755`}, {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5}, {C: `fchown 5 0 0`}, {C: `close 4`}, {C: `close 3`}, {C: `mkdirat 5 "rofs" 0755`}, {C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `fchown 3 0 0`}, {C: `close 3`}, {C: `close 5`}, {C: `lstat "/rofs"`, R: testutil.FileInfoDir}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, {C: `fstat 4 `, R: syscall.Stat_t{}}, {C: `close 3`}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, {C: `openat 5 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 6}, {C: `openat 6 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7}, {C: `fstat 7 `, R: syscall.Stat_t{}}, {C: `close 6`}, {C: `close 5`}, {C: `close 3`}, {C: `mount "/proc/self/fd/4" "/proc/self/fd/7" "" MS_BIND|MS_REC ""`}, {C: `close 7`}, {C: `close 4`}, {C: `lstat "/rofs"`, R: testutil.FileInfoDir}, {C: `mount "tmpfs" "/rofs" "tmpfs" 0 "mode=0755,uid=0,gid=0"`}, {C: `unmount "/tmp/.snap/rofs" UMOUNT_NOFOLLOW|MNT_DETACH`}, // Perform clean up after the unmount operation. {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, {C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, {C: `fstat 6 `, R: syscall.Stat_t{}}, {C: `close 5`}, {C: `close 4`}, {C: `close 3`}, {C: `fstatfs 6 `, R: syscall.Statfs_t{}}, {C: `remove "/tmp/.snap/rofs"`}, {C: `close 6`}, // /rofs/source was missing (we checked earlier), create it {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `close 3`}, {C: `mkdirat 4 "source" 0755`}, {C: `openat 4 "source" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `fchown 3 0 0`}, {C: `close 3`}, {C: `close 4`}, // bind mount /rofs/source -> /target {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, {C: `openat 4 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, {C: `fstat 5 `, R: syscall.Stat_t{}}, {C: `close 4`}, {C: `close 3`}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, {C: `fstat 4 `, R: syscall.Stat_t{}}, {C: `close 3`}, {C: `mount "/proc/self/fd/5" "/proc/self/fd/4" "" MS_BIND ""`}, {C: `close 4`}, {C: `close 5`}, }) } // Change.Perform wants to bind mount a directory but there's a symlink in mount point. func (s *changeSuite) TestPerformDirectoryBindMountWithSymlinkInMountPoint(c *C) { s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoSymlink) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, `cannot use "/target" as mount point: not a directory`) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, R: testutil.FileInfoSymlink}, }) } // Change.Perform wants to bind mount a directory but there's a file in mount mount. func (s *changeSuite) TestPerformDirectoryBindMountWithFileInMountPoint(c *C) { s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, `cannot use "/target" as mount point: not a directory`) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, R: testutil.FileInfoFile}, }) } // Change.Perform wants to bind mount a directory but there's a symlink in source. func (s *changeSuite) TestPerformDirectoryBindMountWithSymlinkInMountSource(c *C) { s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoSymlink) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, `cannot use "/source" as bind-mount source: not a directory`) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, R: testutil.FileInfoDir}, {C: `lstat "/source"`, R: testutil.FileInfoSymlink}, }) } // Change.Perform wants to bind mount a directory but there's a file in source. func (s *changeSuite) TestPerformDirectoryBindMountWithFileInMountSource(c *C) { s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoFile) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, `cannot use "/source" as bind-mount source: not a directory`) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, R: testutil.FileInfoDir}, {C: `lstat "/source"`, R: testutil.FileInfoFile}, }) } // Change.Perform wants to unmount a directory bind mount. func (s *changeSuite) TestPerformDirectoryBindUnmount(c *C) { s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{}) chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} synth, err := chg.Perform(s.as) c.Assert(err, IsNil) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `unmount "/target" UMOUNT_NOFOLLOW`}, // Perform clean up after the unmount operation. {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, {C: `fstat 4 `, R: syscall.Stat_t{}}, {C: `close 3`}, {C: `fstatfs 4 `, R: syscall.Statfs_t{}}, {C: `remove "/target"`}, {C: `close 4`}, }) c.Assert(synth, HasLen, 0) } // Change.Perform wants to unmount a directory bind mount but it fails. func (s *changeSuite) TestPerformDirectoryBindUnmountError(c *C) { s.sys.InsertFault(`unmount "/target" UMOUNT_NOFOLLOW`, errTesting) chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} synth, err := chg.Perform(s.as) c.Assert(err, Equals, errTesting) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `unmount "/target" UMOUNT_NOFOLLOW`, E: errTesting}, }) c.Assert(synth, HasLen, 0) } // ######################################### // Topic: bind-mounting and unmounting files // ######################################### // Change.Perform wants to bind mount a file but the target cannot be stat'ed. func (s *changeSuite) TestPerformFileBindMountTargetLstatError(c *C) { s.sys.InsertFault(`lstat "/target"`, errTesting) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, `cannot inspect "/target": testing`) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, E: errTesting}, }) } // Change.Perform wants to bind mount a file but the source cannot be stat'ed. func (s *changeSuite) TestPerformFileBindMountSourceLstatError(c *C) { s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile) s.sys.InsertFault(`lstat "/source"`, errTesting) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, `cannot inspect "/source": testing`) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, R: testutil.FileInfoFile}, {C: `lstat "/source"`, E: errTesting}, }) } // Change.Perform wants to bind mount a file. func (s *changeSuite) TestPerformFileBindMount(c *C) { s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoFile) s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{}) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} synth, err := chg.Perform(s.as) c.Assert(err, IsNil) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, R: testutil.FileInfoFile}, {C: `lstat "/source"`, R: testutil.FileInfoFile}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, {C: `fstat 4 `, R: syscall.Stat_t{}}, {C: `close 3`}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, {C: `fstat 5 `, R: syscall.Stat_t{}}, {C: `close 3`}, {C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`}, {C: `close 5`}, {C: `close 4`}, }) } // Change.Perform wants to bind mount a file but it fails. func (s *changeSuite) TestPerformFileBindMountWithError(c *C) { s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile) s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoFile) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{}) s.sys.InsertFault(`mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`, errTesting) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} synth, err := chg.Perform(s.as) c.Assert(err, Equals, errTesting) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, R: testutil.FileInfoFile}, {C: `lstat "/source"`, R: testutil.FileInfoFile}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, {C: `fstat 4 `, R: syscall.Stat_t{}}, {C: `close 3`}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, {C: `fstat 5 `, R: syscall.Stat_t{}}, {C: `close 3`}, {C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`, E: errTesting}, {C: `close 5`}, {C: `close 4`}, }) } // Change.Perform wants to bind mount a file but the mount point isn't there. func (s *changeSuite) TestPerformFileBindMountWithoutMountPoint(c *C) { defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoFile) s.sys.InsertFault(`lstat "/target"`, syscall.ENOENT) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{}) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} synth, err := chg.Perform(s.as) c.Assert(err, IsNil) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, E: syscall.ENOENT}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, R: 4}, {C: `fchown 4 0 0`}, {C: `close 4`}, {C: `close 3`}, {C: `lstat "/source"`, R: testutil.FileInfoFile}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, {C: `fstat 4 `, R: syscall.Stat_t{}}, {C: `close 3`}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, {C: `fstat 5 `, R: syscall.Stat_t{}}, {C: `close 3`}, {C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`}, {C: `close 5`}, {C: `close 4`}, }) } // Change.Perform wants to create a directory bind mount but the mount point isn't there and cannot be created. func (s *changeSuite) TestPerformFileBindMountWithoutMountPointWithErrors(c *C) { defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. s.sys.InsertFault(`lstat "/target"`, syscall.ENOENT) s.sys.InsertFault(`openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, errTesting) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, `cannot open file "/target": testing`) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, E: syscall.ENOENT}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, E: errTesting}, {C: `close 3`}, }) } // Change.Perform wants to bind mount a file but the mount source isn't there. func (s *changeSuite) TestPerformFileBindMountWithoutMountSource(c *C) { defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. s.sys.InsertFault(`lstat "/source"`, syscall.ENOENT) s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{}) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} synth, err := chg.Perform(s.as) c.Assert(err, IsNil) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, R: testutil.FileInfoFile}, {C: `lstat "/source"`, E: syscall.ENOENT}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, R: 4}, {C: `fchown 4 0 0`}, {C: `close 4`}, {C: `close 3`}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, {C: `fstat 4 `, R: syscall.Stat_t{}}, {C: `close 3`}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, {C: `fstat 5 `, R: syscall.Stat_t{}}, {C: `close 3`}, {C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`}, {C: `close 5`}, {C: `close 4`}, }) } // Change.Perform wants to create a file bind mount but the mount source isn't there and cannot be created. func (s *changeSuite) TestPerformFileBindMountWithoutMountSourceWithErrors(c *C) { defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. s.sys.InsertFault(`lstat "/source"`, syscall.ENOENT) s.sys.InsertFault(`openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, errTesting) s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, `cannot open file "/source": testing`) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, R: testutil.FileInfoFile}, {C: `lstat "/source"`, E: syscall.ENOENT}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, E: errTesting}, {C: `close 3`}, }) } // Change.Perform wants to bind mount a file but the mount point isn't there and the parent is read-only. func (s *changeSuite) TestPerformFileBindMountWithoutMountPointAndReadOnlyBase(c *C) { defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. s.sys.InsertFault(`lstat "/rofs/target"`, syscall.ENOENT) s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) s.sys.InsertFault(`openat 4 "target" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, syscall.EROFS, nil) // works on 2nd try s.sys.InsertSysLstatResult(`lstat "/rofs" `, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}) s.sys.InsertReadDirResult(`readdir "/rofs"`, nil) // pretend /rofs is empty. s.sys.InsertFault(`lstat "/tmp/.snap/rofs"`, syscall.ENOENT) s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir) s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoFile) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{}) s.sys.InsertFstatResult(`fstat 7 `, syscall.Stat_t{}) s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{}) s.sys.InsertFstatfsResult(`fstatfs 6 `, syscall.Statfs_t{}) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/rofs/target", Options: []string{"bind", "x-snapd.kind=file"}}} synth, err := chg.Perform(s.as) c.Assert(err, IsNil) c.Assert(synth, DeepEquals, []*update.Change{ {Action: update.Mount, Entry: osutil.MountEntry{ Name: "tmpfs", Dir: "/rofs", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/rofs/target", "mode=0755", "uid=0", "gid=0"}}, }, }) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ // sniff mount target {C: `lstat "/rofs/target"`, E: syscall.ENOENT}, // /rofs/target is missing, create it {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `close 3`}, {C: `openat 4 "target" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, E: syscall.EROFS}, {C: `close 4`}, // error, read only filesystem, create a mimic {C: `lstat "/rofs" `, R: syscall.Stat_t{Mode: 0755}}, {C: `readdir "/rofs"`, R: []os.FileInfo(nil)}, {C: `lstat "/tmp/.snap/rofs"`, E: syscall.ENOENT}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "tmp" 0755`}, {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `fchown 4 0 0`}, {C: `mkdirat 4 ".snap" 0755`}, {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5}, {C: `fchown 5 0 0`}, {C: `close 4`}, {C: `close 3`}, {C: `mkdirat 5 "rofs" 0755`}, {C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `fchown 3 0 0`}, {C: `close 3`}, {C: `close 5`}, {C: `lstat "/rofs"`, R: testutil.FileInfoDir}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, {C: `fstat 4 `, R: syscall.Stat_t{}}, {C: `close 3`}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, {C: `openat 5 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 6}, {C: `openat 6 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7}, {C: `fstat 7 `, R: syscall.Stat_t{}}, {C: `close 6`}, {C: `close 5`}, {C: `close 3`}, {C: `mount "/proc/self/fd/4" "/proc/self/fd/7" "" MS_BIND|MS_REC ""`}, {C: `close 7`}, {C: `close 4`}, {C: `lstat "/rofs"`, R: testutil.FileInfoDir}, {C: `mount "tmpfs" "/rofs" "tmpfs" 0 "mode=0755,uid=0,gid=0"`}, {C: `unmount "/tmp/.snap/rofs" UMOUNT_NOFOLLOW|MNT_DETACH`}, // Perform clean up after the unmount operation. {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, {C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, {C: `fstat 6 `, R: syscall.Stat_t{}}, {C: `close 5`}, {C: `close 4`}, {C: `close 3`}, {C: `fstatfs 6 `, R: syscall.Statfs_t{}}, {C: `remove "/tmp/.snap/rofs"`}, {C: `close 6`}, // mimic ready, re-try initial mkdir {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `close 3`}, {C: `openat 4 "target" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, R: 3}, {C: `fchown 3 0 0`}, {C: `close 3`}, {C: `close 4`}, // sniff mount source {C: `lstat "/source"`, R: testutil.FileInfoFile}, // mount the filesystem {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, {C: `fstat 4 `, R: syscall.Stat_t{}}, {C: `close 3`}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, {C: `openat 5 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, {C: `fstat 6 `, R: syscall.Stat_t{}}, {C: `close 5`}, {C: `close 3`}, {C: `mount "/proc/self/fd/4" "/proc/self/fd/6" "" MS_BIND ""`}, {C: `close 6`}, {C: `close 4`}, }) } // Change.Perform wants to bind mount a file but there's a symlink in mount point. func (s *changeSuite) TestPerformFileBindMountWithSymlinkInMountPoint(c *C) { s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoSymlink) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, `cannot use "/target" as mount point: not a regular file`) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, R: testutil.FileInfoSymlink}, }) } // Change.Perform wants to bind mount a file but there's a directory in mount point. func (s *changeSuite) TestPerformBindMountFileWithDirectoryInMountPoint(c *C) { s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, `cannot use "/target" as mount point: not a regular file`) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, R: testutil.FileInfoDir}, }) } // Change.Perform wants to bind mount a file but there's a symlink in source. func (s *changeSuite) TestPerformFileBindMountWithSymlinkInMountSource(c *C) { s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile) s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoSymlink) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, `cannot use "/source" as bind-mount source: not a regular file`) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, R: testutil.FileInfoFile}, {C: `lstat "/source"`, R: testutil.FileInfoSymlink}, }) } // Change.Perform wants to bind mount a file but there's a directory in source. func (s *changeSuite) TestPerformFileBindMountWithDirectoryInMountSource(c *C) { s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile) s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoDir) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, `cannot use "/source" as bind-mount source: not a regular file`) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, R: testutil.FileInfoFile}, {C: `lstat "/source"`, R: testutil.FileInfoDir}, }) } // Change.Perform wants to unmount a file bind mount made on empty squashfs placeholder. func (s *changeSuite) TestPerformFileBindUnmountOnSquashfs(c *C) { s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.SquashfsMagic}) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{Size: 0}) chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} synth, err := chg.Perform(s.as) c.Assert(err, IsNil) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `unmount "/target" UMOUNT_NOFOLLOW`}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, {C: `fstat 4 `, R: syscall.Stat_t{}}, {C: `close 3`}, {C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.SquashfsMagic}}, {C: `close 4`}, }) c.Assert(synth, HasLen, 0) } // Change.Perform wants to unmount a file bind mount made on non-empty ext4 placeholder. func (s *changeSuite) TestPerformFileBindUnmountOnExt4NonEmpty(c *C) { s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.Ext4Magic}) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{Size: 1}) chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} synth, err := chg.Perform(s.as) c.Assert(err, IsNil) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `unmount "/target" UMOUNT_NOFOLLOW`}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, {C: `fstat 4 `, R: syscall.Stat_t{Size: 1}}, {C: `close 3`}, {C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.Ext4Magic}}, {C: `fstat 4 `, R: syscall.Stat_t{Size: 1}}, {C: `close 4`}, }) c.Assert(synth, HasLen, 0) } // Change.Perform wants to unmount a file bind mount made on empty tmpfs placeholder. func (s *changeSuite) TestPerformFileBindUnmountOnTmpfsEmpty(c *C) { s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.TmpfsMagic}) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{Size: 0}) chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} synth, err := chg.Perform(s.as) c.Assert(err, IsNil) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `unmount "/target" UMOUNT_NOFOLLOW`}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, {C: `fstat 4 `, R: syscall.Stat_t{Size: 0}}, {C: `close 3`}, {C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.TmpfsMagic}}, {C: `fstat 4 `, R: syscall.Stat_t{Size: 0}}, {C: `remove "/target"`}, {C: `close 4`}, }) c.Assert(synth, HasLen, 0) } // Change.Perform wants to unmount a file bind mount made on empty tmpfs placeholder but it is busy!. func (s *changeSuite) TestPerformFileBindUnmountOnTmpfsEmptyButBusy(c *C) { s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.TmpfsMagic}) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{Size: 0}) s.sys.InsertFault(`remove "/target"`, syscall.EBUSY) chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, "device or resource busy") c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `unmount "/target" UMOUNT_NOFOLLOW`}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, {C: `fstat 4 `, R: syscall.Stat_t{Size: 0}}, {C: `close 3`}, {C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.TmpfsMagic}}, {C: `fstat 4 `, R: syscall.Stat_t{Size: 0}}, {C: `remove "/target"`, E: syscall.EBUSY}, {C: `close 4`}, }) c.Assert(synth, HasLen, 0) } // Change.Perform wants to unmount a file bind mount but it fails. func (s *changeSuite) TestPerformFileBindUnmountError(c *C) { s.sys.InsertFault(`unmount "/target" UMOUNT_NOFOLLOW`, errTesting) chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} synth, err := chg.Perform(s.as) c.Assert(err, Equals, errTesting) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `unmount "/target" UMOUNT_NOFOLLOW`, E: errTesting}, }) c.Assert(synth, HasLen, 0) } // ############################################################# // Topic: handling mounts with the x-snapd.ignore-missing option // ############################################################# func (s *changeSuite) TestPerformMountWithIgnoredMissingMountSource(c *C) { s.sys.InsertFault(`lstat "/source"`, syscall.ENOENT) s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.ignore-missing"}}} synth, err := chg.Perform(s.as) c.Assert(err, Equals, update.ErrIgnoredMissingMount) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, R: testutil.FileInfoDir}, {C: `lstat "/source"`, E: syscall.ENOENT}, }) } func (s *changeSuite) TestPerformMountWithIgnoredMissingMountPoint(c *C) { s.sys.InsertFault(`lstat "/target"`, syscall.ENOENT) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.ignore-missing"}}} synth, err := chg.Perform(s.as) c.Assert(err, Equals, update.ErrIgnoredMissingMount) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/target"`, E: syscall.ENOENT}, }) } // ######################## // Topic: creating symlinks // ######################## // Change.Perform wants to create a symlink but name cannot be stat'ed. func (s *changeSuite) TestPerformCreateSymlinkNameLstatError(c *C) { s.sys.InsertFault(`lstat "/name"`, errTesting) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, `cannot inspect "/name": testing`) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/name"`, E: errTesting}, }) } // Change.Perform wants to create a symlink. func (s *changeSuite) TestPerformCreateSymlink(c *C) { defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. s.sys.InsertFault(`lstat "/name"`, syscall.ENOENT) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} synth, err := chg.Perform(s.as) c.Assert(err, IsNil) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/name"`, E: syscall.ENOENT}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `symlinkat "/oldname" 3 "name"`}, {C: `close 3`}, }) } // Change.Perform wants to create a symlink but it fails. func (s *changeSuite) TestPerformCreateSymlinkWithError(c *C) { defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. s.sys.InsertFault(`lstat "/name"`, syscall.ENOENT) s.sys.InsertFault(`symlinkat "/oldname" 3 "name"`, errTesting) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, `cannot create symlink "/name": testing`) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/name"`, E: syscall.ENOENT}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `symlinkat "/oldname" 3 "name"`, E: errTesting}, {C: `close 3`}, }) } // Change.Perform wants to create a symlink but the target is empty. func (s *changeSuite) TestPerformCreateSymlinkWithNoTargetError(c *C) { s.sys.InsertFault(`lstat "/name"`, syscall.ENOENT) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink="}}} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, `cannot create symlink with empty target: "/name"`) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/name"`, E: syscall.ENOENT}, }) } // Change.Perform wants to create a symlink but the base directory isn't there. func (s *changeSuite) TestPerformCreateSymlinkWithoutBaseDir(c *C) { defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. s.sys.InsertFault(`lstat "/base/name"`, syscall.ENOENT) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/base/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} synth, err := chg.Perform(s.as) c.Assert(err, IsNil) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/base/name"`, E: syscall.ENOENT}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "base" 0755`}, {C: `openat 3 "base" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `fchown 4 0 0`}, {C: `close 3`}, {C: `symlinkat "/oldname" 4 "name"`}, {C: `close 4`}, }) } // Change.Perform wants to create a symlink but the base directory isn't there and cannot be created. func (s *changeSuite) TestPerformCreateSymlinkWithoutBaseDirWithErrors(c *C) { defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. s.sys.InsertFault(`lstat "/base/name"`, syscall.ENOENT) s.sys.InsertFault(`mkdirat 3 "base" 0755`, errTesting) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/base/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, `cannot create directory "/base": testing`) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/base/name"`, E: syscall.ENOENT}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "base" 0755`, E: errTesting}, {C: `close 3`}, }) } // Change.Perform wants to create a symlink but the base directory isn't there and the parent is read-only. func (s *changeSuite) TestPerformCreateSymlinkWithoutBaseDirAndReadOnlyBase(c *C) { defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. s.sys.InsertFault(`lstat "/rofs/name"`, syscall.ENOENT) s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) s.sys.InsertFault(`symlinkat "/oldname" 4 "name"`, syscall.EROFS, nil) // works on 2nd try s.sys.InsertSysLstatResult(`lstat "/rofs" `, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}) s.sys.InsertReadDirResult(`readdir "/rofs"`, nil) // pretend /rofs is empty. s.sys.InsertFault(`lstat "/tmp/.snap/rofs"`, syscall.ENOENT) s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) s.sys.InsertFstatResult(`fstat 7 `, syscall.Stat_t{}) s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{}) s.sys.InsertFstatfsResult(`fstatfs 6 `, syscall.Statfs_t{}) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/rofs/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} synth, err := chg.Perform(s.as) c.Assert(err, IsNil) c.Assert(synth, DeepEquals, []*update.Change{ {Action: update.Mount, Entry: osutil.MountEntry{ Name: "tmpfs", Dir: "/rofs", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/rofs/name", "mode=0755", "uid=0", "gid=0"}}, }, }) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ // sniff symlink name {C: `lstat "/rofs/name"`, E: syscall.ENOENT}, // create base name (/rofs) {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `close 3`}, // create symlink {C: `symlinkat "/oldname" 4 "name"`, E: syscall.EROFS}, {C: `close 4`}, // error, read only filesystem, create a mimic {C: `lstat "/rofs" `, R: syscall.Stat_t{Mode: 0755}}, {C: `readdir "/rofs"`, R: []os.FileInfo(nil)}, {C: `lstat "/tmp/.snap/rofs"`, E: syscall.ENOENT}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "tmp" 0755`}, {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `fchown 4 0 0`}, {C: `mkdirat 4 ".snap" 0755`}, {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5}, {C: `fchown 5 0 0`}, {C: `close 4`}, {C: `close 3`}, {C: `mkdirat 5 "rofs" 0755`}, {C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `fchown 3 0 0`}, {C: `close 3`}, {C: `close 5`}, {C: `lstat "/rofs"`, R: testutil.FileInfoDir}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, {C: `fstat 4 `, R: syscall.Stat_t{}}, {C: `close 3`}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, {C: `openat 5 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 6}, {C: `openat 6 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7}, {C: `fstat 7 `, R: syscall.Stat_t{}}, {C: `close 6`}, {C: `close 5`}, {C: `close 3`}, {C: `mount "/proc/self/fd/4" "/proc/self/fd/7" "" MS_BIND|MS_REC ""`}, {C: `close 7`}, {C: `close 4`}, {C: `lstat "/rofs"`, R: testutil.FileInfoDir}, {C: `mount "tmpfs" "/rofs" "tmpfs" 0 "mode=0755,uid=0,gid=0"`}, {C: `unmount "/tmp/.snap/rofs" UMOUNT_NOFOLLOW|MNT_DETACH`}, // Perform clean up after the unmount operation. {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, {C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, {C: `fstat 6 `, R: syscall.Stat_t{}}, {C: `close 5`}, {C: `close 4`}, {C: `close 3`}, {C: `fstatfs 6 `, R: syscall.Statfs_t{}}, {C: `remove "/tmp/.snap/rofs"`}, {C: `close 6`}, // mimic ready, re-try initial base mkdir {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `close 3`}, // create symlink {C: `symlinkat "/oldname" 4 "name"`}, {C: `close 4`}, }) } // Change.Perform wants to create a symlink but there's a file in the way. func (s *changeSuite) TestPerformCreateSymlinkWithFileInTheWay(c *C) { s.sys.InsertOsLstatResult(`lstat "/name"`, testutil.FileInfoFile) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, `cannot create symlink in "/name": existing file in the way`) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/name"`, R: testutil.FileInfoFile}, }) } // Change.Perform wants to create a symlink but a correct symlink is already present. func (s *changeSuite) TestPerformCreateSymlinkWithGoodSymlinkPresent(c *C) { defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. s.sys.InsertOsLstatResult(`lstat "/name"`, testutil.FileInfoSymlink) s.sys.InsertFault(`symlinkat "/oldname" 3 "name"`, syscall.EEXIST) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{Mode: syscall.S_IFLNK}) s.sys.InsertReadlinkatResult(`readlinkat 4 "" `, "/oldname") chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} synth, err := chg.Perform(s.as) c.Assert(err, IsNil) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/name"`, R: testutil.FileInfoSymlink}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `symlinkat "/oldname" 3 "name"`, E: syscall.EEXIST}, {C: `openat 3 "name" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, {C: `fstat 4 `, R: syscall.Stat_t{Mode: syscall.S_IFLNK}}, {C: `readlinkat 4 "" `, R: "/oldname"}, {C: `close 4`}, {C: `close 3`}, }) } // Change.Perform wants to create a symlink but a incorrect symlink is already present. func (s *changeSuite) TestPerformCreateSymlinkWithBadSymlinkPresent(c *C) { defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. s.sys.InsertOsLstatResult(`lstat "/name"`, testutil.FileInfoSymlink) s.sys.InsertFault(`symlinkat "/oldname" 3 "name"`, syscall.EEXIST) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{Mode: syscall.S_IFLNK}) s.sys.InsertReadlinkatResult(`readlinkat 4 "" `, "/evil") chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, `cannot create symbolic link "/name": existing symbolic link in the way`) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `lstat "/name"`, R: testutil.FileInfoSymlink}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `symlinkat "/oldname" 3 "name"`, E: syscall.EEXIST}, {C: `openat 3 "name" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, {C: `fstat 4 `, R: syscall.Stat_t{Mode: syscall.S_IFLNK}}, {C: `readlinkat 4 "" `, R: "/evil"}, {C: `close 4`}, {C: `close 3`}, }) } func (s *changeSuite) TestPerformRemoveSymlink(c *C) { chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "unused", Dir: "/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} synth, err := chg.Perform(s.as) c.Assert(err, IsNil) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `remove "/name"`}, }) } // Change.Perform wants to create a symlink in /etc and the write is made private. func (s *changeSuite) TestPerformCreateSymlinkWithAvoidedTrespassing(c *C) { defer s.as.MockUnrestrictedPaths("/tmp/")() // Allow writing to /tmp s.sys.InsertFault(`lstat "/etc/demo.conf"`, syscall.ENOENT) s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.SquashfsMagic}) s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST) s.sys.InsertFstatfsResult(`fstatfs 4 `, // On 1st call ext4, on 2nd call tmpfs syscall.Statfs_t{Type: update.Ext4Magic}, syscall.Statfs_t{Type: update.TmpfsMagic}) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) s.sys.InsertSysLstatResult(`lstat "/etc" `, syscall.Stat_t{Mode: 0755}) otherConf := testutil.FakeFileInfo("other.conf", 0755) s.sys.InsertReadDirResult(`readdir "/etc"`, []os.FileInfo{otherConf}) s.sys.InsertFault(`lstat "/tmp/.snap/etc"`, syscall.ENOENT) s.sys.InsertFault(`lstat "/tmp/.snap/etc/other.conf"`, syscall.ENOENT) s.sys.InsertOsLstatResult(`lstat "/etc"`, testutil.FileInfoDir) s.sys.InsertOsLstatResult(`lstat "/etc/other.conf"`, otherConf) s.sys.InsertFault(`mkdirat 3 "tmp" 0755`, syscall.EEXIST) s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{Mode: syscall.S_IFREG}) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{Mode: syscall.S_IFDIR}) s.sys.InsertFstatResult(`fstat 7 `, syscall.Stat_t{Mode: syscall.S_IFDIR}) s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{}) s.sys.InsertFstatfsResult(`fstatfs 6 `, syscall.Statfs_t{}) // This is the change we want to perform: // put a layout symlink at /etc/demo.conf -> /oldname chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/etc/demo.conf", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} synth, err := chg.Perform(s.as) c.Check(err, IsNil) c.Check(synth, HasLen, 2) // We have created some synthetic change (made /etc a new tmpfs and re-populate it) c.Assert(synth[0], DeepEquals, &update.Change{ Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/etc", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/etc/demo.conf", "mode=0755", "uid=0", "gid=0"}}, Action: "mount"}) c.Assert(synth[1], DeepEquals, &update.Change{ Entry: osutil.MountEntry{Name: "/etc/other.conf", Dir: "/etc/other.conf", Options: []string{"bind", "x-snapd.kind=file", "x-snapd.synthetic", "x-snapd.needed-by=/etc/demo.conf"}}, Action: "mount"}) // And this is exactly how we made that happen: c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ // Attempt to construct a symlink /etc/demo.conf -> /oldname. // This stops as soon as we notice that /etc is an ext4 filesystem. // To avoid writing to it directly we need a writable mimic. {C: `lstat "/etc/demo.conf"`, E: syscall.ENOENT}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `fstatfs 3 `, R: syscall.Statfs_t{Type: update.SquashfsMagic}}, {C: `fstat 3 `, R: syscall.Stat_t{}}, {C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST}, {C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `close 3`}, {C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.Ext4Magic}}, {C: `fstat 4 `, R: syscall.Stat_t{Mode: 0x4000}}, {C: `close 4`}, // Create a writable mimic over /etc, scan the contents of /etc first. // For convenience we pretend that /etc is empty. The mimic // replicates /etc in /tmp/.snap/etc for subsequent re-construction. {C: `lstat "/etc" `, R: syscall.Stat_t{Mode: 0755}}, {C: `readdir "/etc"`, R: []os.FileInfo{otherConf}}, {C: `lstat "/tmp/.snap/etc"`, E: syscall.ENOENT}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "tmp" 0755`, E: syscall.EEXIST}, {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `mkdirat 4 ".snap" 0755`}, {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5}, {C: `fchown 5 0 0`}, {C: `close 4`}, {C: `close 3`}, {C: `mkdirat 5 "etc" 0755`}, {C: `openat 5 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `fchown 3 0 0`}, {C: `close 3`}, {C: `close 5`}, // Prepare a secure bind mount operation /etc -> /tmp/.snap/etc {C: `lstat "/etc"`, R: testutil.FileInfoDir}, // Open an O_PATH descriptor to /etc. We need this as a source of a // secure bind mount operation. We also ensure that the descriptor // refers to a directory. // NOTE: we keep fd 4 open for subsequent use. {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, {C: `fstat 4 `, R: syscall.Stat_t{Mode: syscall.S_IFDIR}}, {C: `close 3`}, // Open an O_PATH descriptor to /tmp/.snap/etc. We need this as a // target of a secure bind mount operation. We also ensure that the // descriptor refers to a directory. // NOTE: we keep fd 7 open for subsequent use. {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, {C: `openat 5 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 6}, {C: `openat 6 "etc" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7}, {C: `fstat 7 `, R: syscall.Stat_t{Mode: syscall.S_IFDIR}}, {C: `close 6`}, {C: `close 5`}, {C: `close 3`}, // Perform the secure bind mount operation /etc -> /tmp/.snap/etc // and release the two associated file descriptors. {C: `mount "/proc/self/fd/4" "/proc/self/fd/7" "" MS_BIND|MS_REC ""`}, {C: `close 7`}, {C: `close 4`}, // Mount a tmpfs over /etc, re-constructing the original mode and // ownership. Bind mount each original file over and detach the copy // of /etc we had in /tmp/.snap/etc. {C: `lstat "/etc"`, R: testutil.FileInfoDir}, {C: `mount "tmpfs" "/etc" "tmpfs" 0 "mode=0755,uid=0,gid=0"`}, // Here we restore the contents of /etc: here it's just one file - other.conf {C: `lstat "/etc/other.conf"`, R: otherConf}, {C: `lstat "/tmp/.snap/etc/other.conf"`, E: syscall.ENOENT}, // Create /tmp/.snap/etc/other.conf as an empty file. {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `mkdirat 3 "tmp" 0755`, E: syscall.EEXIST}, {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `mkdirat 4 ".snap" 0755`}, {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5}, {C: `fchown 5 0 0`}, {C: `mkdirat 5 "etc" 0755`}, {C: `openat 5 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 6}, {C: `fchown 6 0 0`}, {C: `close 5`}, {C: `close 4`}, {C: `close 3`}, // NOTE: This is without O_DIRECTORY and with O_CREAT|O_EXCL, // we are creating an empty file for the subsequent bind mount. {C: `openat 6 "other.conf" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, R: 3}, {C: `fchown 3 0 0`}, {C: `close 3`}, {C: `close 6`}, // Open O_PATH to /tmp/.snap/etc/other.conf {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, {C: `openat 5 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 6}, {C: `openat 6 "other.conf" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7}, {C: `fstat 7 `, R: syscall.Stat_t{Mode: syscall.S_IFDIR}}, {C: `close 6`}, {C: `close 5`}, {C: `close 4`}, {C: `close 3`}, // Open O_PATH to /etc/other.conf {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, {C: `openat 4 "other.conf" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, {C: `fstat 5 `, R: syscall.Stat_t{Mode: syscall.S_IFREG}}, {C: `close 4`}, {C: `close 3`}, // Restore the /etc/other.conf file with a secure bind mount. {C: `mount "/proc/self/fd/7" "/proc/self/fd/5" "" MS_BIND ""`}, {C: `close 5`}, {C: `close 7`}, // We're done restoring now. {C: `unmount "/tmp/.snap/etc" UMOUNT_NOFOLLOW|MNT_DETACH`}, // Perform clean up after the unmount operation. {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, {C: `openat 5 "etc" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, {C: `fstat 6 `, R: syscall.Stat_t{}}, {C: `close 5`}, {C: `close 4`}, {C: `close 3`}, {C: `fstatfs 6 `, R: syscall.Statfs_t{}}, {C: `remove "/tmp/.snap/etc"`}, {C: `close 6`}, // The mimic is now complete and subsequent writes to /etc are private // to the mount namespace of the process. {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `fstatfs 3 `, R: syscall.Statfs_t{Type: update.SquashfsMagic}}, {C: `fstat 3 `, R: syscall.Stat_t{}}, {C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST}, {C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, {C: `close 3`}, {C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.TmpfsMagic}}, {C: `fstat 4 `, R: syscall.Stat_t{Mode: 0x4000}}, {C: `symlinkat "/oldname" 4 "demo.conf"`}, {C: `close 4`}, }) } // ########### // Topic: misc // ########### // Change.Perform handles unknown actions. func (s *changeSuite) TestPerformUnknownAction(c *C) { chg := &update.Change{Action: update.Action(42)} synth, err := chg.Perform(s.as) c.Assert(err, ErrorMatches, `cannot process mount change: unknown action: .*`) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), HasLen, 0) } // Change.Perform wants to keep a mount entry unchanged. func (s *changeSuite) TestPerformKeep(c *C) { chg := &update.Change{Action: update.Keep} synth, err := chg.Perform(s.as) c.Assert(err, IsNil) c.Assert(synth, HasLen, 0) c.Assert(s.sys.RCalls(), HasLen, 0) } // ############################################ // Topic: change history tracked in Assumptions // ############################################ func (s *changeSuite) TestPerformedChangesAreTracked(c *C) { s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) c.Assert(s.as.PastChanges(), HasLen, 0) chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} _, err := chg.Perform(s.as) c.Assert(err, IsNil) c.Assert(s.as.PastChanges(), DeepEquals, []*update.Change{ {Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}}, }) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{}) chg = &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} _, err = chg.Perform(s.as) c.Assert(err, IsNil) c.Assert(s.as.PastChanges(), DeepEquals, []*update.Change{ // past changes stack in order. {Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}}, {Action: update.Unmount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}}, }) } snapd-2.37.4~14.04.1/cmd/snap-update-ns/utils.go0000664000000000000000000005711413435556260015642 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main import ( "fmt" "io/ioutil" "os" "path/filepath" "strings" "syscall" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/osutil/sys" "github.com/snapcore/snapd/strutil" ) // not available through syscall const ( umountNoFollow = 8 // StReadOnly is the equivalent of ST_RDONLY StReadOnly = 1 // SquashfsMagic is the equivalent of SQUASHFS_MAGIC SquashfsMagic = 0x73717368 // Ext4Magic is the equivalent of EXT4_SUPER_MAGIC Ext4Magic = 0xef53 // TmpfsMagic is the equivalent of TMPFS_MAGIC TmpfsMagic = 0x01021994 ) // For mocking everything during testing. var ( osLstat = os.Lstat osReadlink = os.Readlink osRemove = os.Remove sysClose = syscall.Close sysMkdirat = syscall.Mkdirat sysMount = syscall.Mount sysOpen = syscall.Open sysOpenat = syscall.Openat sysUnmount = syscall.Unmount sysFchown = sys.Fchown sysFstat = syscall.Fstat sysFstatfs = syscall.Fstatfs sysSymlinkat = osutil.Symlinkat sysReadlinkat = osutil.Readlinkat sysFchdir = syscall.Fchdir sysLstat = syscall.Lstat ioutilReadDir = ioutil.ReadDir ) // ReadOnlyFsError is an error encapsulating encountered EROFS. type ReadOnlyFsError struct { Path string } func (e *ReadOnlyFsError) Error() string { return fmt.Sprintf("cannot operate on read-only filesystem at %s", e.Path) } // OpenPath creates a path file descriptor for the given // path, making sure no components are symbolic links. // // The file descriptor is opened using the O_PATH, O_NOFOLLOW, // and O_CLOEXEC flags. func OpenPath(path string) (int, error) { iter, err := strutil.NewPathIterator(path) if err != nil { return -1, fmt.Errorf("cannot open path: %s", err) } if !filepath.IsAbs(iter.Path()) { return -1, fmt.Errorf("path %v is not absolute", iter.Path()) } iter.Next() // Advance iterator to '/' // We use the following flags to open: // O_PATH: we don't intend to use the fd for IO // O_NOFOLLOW: don't follow symlinks // O_DIRECTORY: we expect to find directories (except for the leaf) // O_CLOEXEC: don't leak file descriptors over exec() boundaries openFlags := sys.O_PATH | syscall.O_NOFOLLOW | syscall.O_DIRECTORY | syscall.O_CLOEXEC fd, err := sysOpen("/", openFlags, 0) if err != nil { return -1, err } for iter.Next() { // Ensure the parent file descriptor is closed defer sysClose(fd) if !strings.HasSuffix(iter.CurrentName(), "/") { openFlags &^= syscall.O_DIRECTORY } fd, err = sysOpenat(fd, iter.CurrentCleanName(), openFlags, 0) if err != nil { return -1, err } } var statBuf syscall.Stat_t err = sysFstat(fd, &statBuf) if err != nil { sysClose(fd) return -1, err } if statBuf.Mode&syscall.S_IFMT == syscall.S_IFLNK { sysClose(fd) return -1, fmt.Errorf("%q is a symbolic link", path) } return fd, nil } // MkPrefix creates all the missing directories in a given base path and // returns the file descriptor to the leaf directory as well as the restricted // flag. This function is a base for secure variants of mkdir, touch and // symlink. None of the traversed directories can be symbolic links. func MkPrefix(base string, perm os.FileMode, uid sys.UserID, gid sys.GroupID, rs *Restrictions) (int, error) { iter, err := strutil.NewPathIterator(base) if err != nil { // TODO: Reword the error and adjust the tests. return -1, fmt.Errorf("cannot split unclean path %q", base) } if !filepath.IsAbs(iter.Path()) { return -1, fmt.Errorf("path %v is not absolute", iter.Path()) } iter.Next() // Advance iterator to '/' const openFlags = syscall.O_NOFOLLOW | syscall.O_CLOEXEC | syscall.O_DIRECTORY // Open the root directory and start there. // // We don't have to check for possible trespassing on / here because we are // going to check for it in sec.MkDir call below which verifies that // trespassing restrictions are not violated. fd, err := sysOpen("/", openFlags, 0) if err != nil { return -1, fmt.Errorf("cannot open root directory: %v", err) } for iter.Next() { // Keep closing the previous descriptor as we go, so that we have the // last one handy from the MkDir below. defer sysClose(fd) fd, err = MkDir(fd, iter.CurrentBase(), iter.CurrentCleanName(), perm, uid, gid, rs) if err != nil { return -1, err } } return fd, nil } // MkDir creates a directory with a given name. // // The directory is represented with a file descriptor and its name (for // convenience). This function is meant to be used to construct subsequent // elements of some path. The return value contains the newly created file // descriptor for the new directory or -1 on error. func MkDir(dirFd int, dirName string, name string, perm os.FileMode, uid sys.UserID, gid sys.GroupID, rs *Restrictions) (int, error) { if err := rs.Check(dirFd, dirName); err != nil { return -1, err } made := true const openFlags = syscall.O_NOFOLLOW | syscall.O_CLOEXEC | syscall.O_DIRECTORY if err := sysMkdirat(dirFd, name, uint32(perm.Perm())); err != nil { switch err { case syscall.EEXIST: made = false case syscall.EROFS: // Treat EROFS specially: this is a hint that we have to poke a // hole using tmpfs. The path below is the location where we // need to poke the hole. return -1, &ReadOnlyFsError{Path: dirName} default: return -1, fmt.Errorf("cannot create directory %q: %v", filepath.Join(dirName, name), err) } } newFd, err := sysOpenat(dirFd, name, openFlags, 0) if err != nil { return -1, fmt.Errorf("cannot open directory %q: %v", filepath.Join(dirName, name), err) } if made { // Chown each segment that we made. if err := sysFchown(newFd, uid, gid); err != nil { // Close the FD we opened if we fail here since the caller will get // an error and won't assume responsibility for the FD. sysClose(newFd) return -1, fmt.Errorf("cannot chown directory %q to %d.%d: %v", filepath.Join(dirName, name), uid, gid, err) } // As soon as we find a place that is safe to write we can switch off // the restricted mode (and thus any subsequent checks). This is // because we only allow "writing" to read-only filesystems where // writes fail with EROFS or to a tmpfs that snapd has privately // mounted inside the per-snap mount namespace. As soon as we start // walking over such tmpfs any subsequent children are either read- // only bind mounts from $SNAP, other tmpfs'es (e.g. one explicitly // constructed for a layout) or writable places that are bind-mounted // from $SNAP_DATA or similar. rs.Lift() } return newFd, err } // MkFile creates a file with a given name. // // The directory is represented with a file descriptor and its name (for // convenience). This function is meant to be used to create the leaf file as // a preparation for a mount point. Existing files are reused without errors. // Newly created files have the specified mode and ownership. func MkFile(dirFd int, dirName string, name string, perm os.FileMode, uid sys.UserID, gid sys.GroupID, rs *Restrictions) error { if err := rs.Check(dirFd, dirName); err != nil { return err } made := true // NOTE: Tests don't show O_RDONLY as has a value of 0 and is not // translated to textual form. It is added here for explicitness. const openFlags = syscall.O_NOFOLLOW | syscall.O_CLOEXEC | syscall.O_RDONLY // Open the final path segment as a file. Try to create the file (so that // we know if we need to chown it) but fall back to just opening an // existing one. newFd, err := sysOpenat(dirFd, name, openFlags|syscall.O_CREAT|syscall.O_EXCL, uint32(perm.Perm())) if err != nil { switch err { case syscall.EEXIST: // If the file exists then just open it without O_CREAT and O_EXCL newFd, err = sysOpenat(dirFd, name, openFlags, 0) if err != nil { return fmt.Errorf("cannot open file %q: %v", filepath.Join(dirName, name), err) } made = false case syscall.EROFS: // Treat EROFS specially: this is a hint that we have to poke a // hole using tmpfs. The path below is the location where we // need to poke the hole. return &ReadOnlyFsError{Path: dirName} default: return fmt.Errorf("cannot open file %q: %v", filepath.Join(dirName, name), err) } } defer sysClose(newFd) if made { // Chown the file if we made it. if err := sysFchown(newFd, uid, gid); err != nil { return fmt.Errorf("cannot chown file %q to %d.%d: %v", filepath.Join(dirName, name), uid, gid, err) } } return nil } // MkSymlink creates a symlink with a given name. // // The directory is represented with a file descriptor and its name (for // convenience). This function is meant to be used to create the leaf symlink. // Existing and identical symlinks are reused without errors. func MkSymlink(dirFd int, dirName string, name string, oldname string, rs *Restrictions) error { if err := rs.Check(dirFd, dirName); err != nil { return err } // Create the final path segment as a symlink. if err := sysSymlinkat(oldname, dirFd, name); err != nil { switch err { case syscall.EEXIST: var objFd int // If the file exists then just open it for examination. // Maybe it's the symlink we were hoping to create. objFd, err = sysOpenat(dirFd, name, syscall.O_CLOEXEC|sys.O_PATH|syscall.O_NOFOLLOW, 0) if err != nil { return fmt.Errorf("cannot open existing file %q: %v", filepath.Join(dirName, name), err) } defer sysClose(objFd) var statBuf syscall.Stat_t err = sysFstat(objFd, &statBuf) if err != nil { return fmt.Errorf("cannot inspect existing file %q: %v", filepath.Join(dirName, name), err) } if statBuf.Mode&syscall.S_IFMT != syscall.S_IFLNK { return fmt.Errorf("cannot create symbolic link %q: existing file in the way", filepath.Join(dirName, name)) } var n int buf := make([]byte, len(oldname)+2) n, err = sysReadlinkat(objFd, "", buf) if err != nil { return fmt.Errorf("cannot read symbolic link %q: %v", filepath.Join(dirName, name), err) } if string(buf[:n]) != oldname { return fmt.Errorf("cannot create symbolic link %q: existing symbolic link in the way", filepath.Join(dirName, name)) } return nil case syscall.EROFS: // Treat EROFS specially: this is a hint that we have to poke a // hole using tmpfs. The path below is the location where we // need to poke the hole. return &ReadOnlyFsError{Path: dirName} default: return fmt.Errorf("cannot create symlink %q: %v", filepath.Join(dirName, name), err) } } return nil } // MkdirAll is the secure variant of os.MkdirAll. // // Unlike the regular version this implementation does not follow any symbolic // links. At all times the new directory segment is created using mkdirat(2) // while holding an open file descriptor to the parent directory. // // The only handled error is mkdirat(2) that fails with EEXIST. All other // errors are fatal but there is no attempt to undo anything that was created. // // The uid and gid are used for the fchown(2) system call which is performed // after each segment is created and opened. The special value -1 may be used // to request that ownership is not changed. func MkdirAll(path string, perm os.FileMode, uid sys.UserID, gid sys.GroupID, rs *Restrictions) error { if path != filepath.Clean(path) { // TODO: Reword the error and adjust the tests. return fmt.Errorf("cannot split unclean path %q", path) } // Only support absolute paths to avoid bugs in snap-confine when // called from anywhere. if !filepath.IsAbs(path) { return fmt.Errorf("cannot create directory with relative path: %q", path) } base, name := filepath.Split(path) base = filepath.Clean(base) // Needed to chomp the trailing slash. // Create the prefix. dirFd, err := MkPrefix(base, perm, uid, gid, rs) if err != nil { return err } defer sysClose(dirFd) if name != "" { // Create the leaf as a directory. leafFd, err := MkDir(dirFd, base, name, perm, uid, gid, rs) if err != nil { return err } defer sysClose(leafFd) } return nil } // MkfileAll is a secure implementation of "mkdir -p $(dirname $1) && touch $1". // // This function is like MkdirAll but it creates an empty file instead of // a directory for the final path component. Each created directory component // is chowned to the desired user and group. func MkfileAll(path string, perm os.FileMode, uid sys.UserID, gid sys.GroupID, rs *Restrictions) error { if path != filepath.Clean(path) { // TODO: Reword the error and adjust the tests. return fmt.Errorf("cannot split unclean path %q", path) } // Only support absolute paths to avoid bugs in snap-confine when // called from anywhere. if !filepath.IsAbs(path) { return fmt.Errorf("cannot create file with relative path: %q", path) } // Only support file names, not directory names. if strings.HasSuffix(path, "/") { return fmt.Errorf("cannot create non-file path: %q", path) } base, name := filepath.Split(path) base = filepath.Clean(base) // Needed to chomp the trailing slash. // Create the prefix. dirFd, err := MkPrefix(base, perm, uid, gid, rs) if err != nil { return err } defer sysClose(dirFd) if name != "" { // Create the leaf as a file. err = MkFile(dirFd, base, name, perm, uid, gid, rs) } return err } // MksymlinkAll is a secure implementation of "ln -s". func MksymlinkAll(path string, perm os.FileMode, uid sys.UserID, gid sys.GroupID, oldname string, rs *Restrictions) error { if path != filepath.Clean(path) { // TODO: Reword the error and adjust the tests. return fmt.Errorf("cannot split unclean path %q", path) } // Only support absolute paths to avoid bugs in snap-confine when // called from anywhere. if !filepath.IsAbs(path) { return fmt.Errorf("cannot create symlink with relative path: %q", path) } // Only support file names, not directory names. if strings.HasSuffix(path, "/") { return fmt.Errorf("cannot create non-file path: %q", path) } if oldname == "" { return fmt.Errorf("cannot create symlink with empty target: %q", path) } base, name := filepath.Split(path) base = filepath.Clean(base) // Needed to chomp the trailing slash. // Create the prefix. dirFd, err := MkPrefix(base, perm, uid, gid, rs) if err != nil { return err } defer sysClose(dirFd) if name != "" { // Create the leaf as a symlink. err = MkSymlink(dirFd, base, name, oldname, rs) } return err } // planWritableMimic plans how to transform a given directory from read-only to writable. // // The algorithm is designed to be universally reversible so that it can be // always de-constructed back to the original directory. The original directory // is hidden by tmpfs and a subset of things that were present there originally // is bind mounted back on top of empty directories or empty files. Symlinks // are re-created directly. Devices and all other elements are not supported // because they are forbidden in snaps for which this function is designed to // be used with. Since the original directory is hidden the algorithm relies on // a temporary directory where the original is bind-mounted during the // progression of the algorithm. func planWritableMimic(dir, neededBy string) ([]*Change, error) { // We need a place for "safe keeping" of what is present in the original // directory as we are about to attach a tmpfs there, which will hide // everything inside. logger.Debugf("create-writable-mimic %q", dir) safeKeepingDir := filepath.Join("/tmp/.snap/", dir) var changes []*Change // Stat the original directory to know which mode and ownership to // replicate on top of the tmpfs we are about to create below. var sb syscall.Stat_t if err := sysLstat(dir, &sb); err != nil { return nil, err } // Bind mount the original directory elsewhere for safe-keeping. changes = append(changes, &Change{ Action: Mount, Entry: osutil.MountEntry{ // NOTE: Here we recursively bind because we realized that not // doing so doesn't work on core devices which use bind mounts // extensively to construct writable spaces in /etc and /var and // elsewhere. // // All directories present in the original are also recursively // bind mounted back to their original location. To unmount this // contraption we use MNT_DETACH which frees us from having to // enumerate the mount table, unmount all the things (starting // with most nested). // // The undo logic handles rbind mounts and adds x-snapd.unbind // flag to them, which in turns translates to MNT_DETACH on // umount2(2) system call. Name: dir, Dir: safeKeepingDir, Options: []string{"rbind"}}, }) // Mount tmpfs over the original directory, hiding its contents. // The mounted tmpfs will mimic the mode and ownership of the original // directory. changes = append(changes, &Change{ Action: Mount, Entry: osutil.MountEntry{ Name: "tmpfs", Dir: dir, Type: "tmpfs", Options: []string{ osutil.XSnapdSynthetic(), osutil.XSnapdNeededBy(neededBy), fmt.Sprintf("mode=%#o", sb.Mode&07777), fmt.Sprintf("uid=%d", sb.Uid), fmt.Sprintf("gid=%d", sb.Gid), }, }, }) // Iterate over the items in the original directory (nothing is mounted _yet_). entries, err := ioutilReadDir(dir) if err != nil { return nil, err } for _, fi := range entries { ch := &Change{Action: Mount, Entry: osutil.MountEntry{ Name: filepath.Join(safeKeepingDir, fi.Name()), Dir: filepath.Join(dir, fi.Name()), }} // Bind mount each element from the safe-keeping directory into the // tmpfs. Our Change.Perform() engine can create the missing // directories automatically so we don't bother creating those. m := fi.Mode() switch { case m.IsDir(): ch.Entry.Options = []string{"rbind"} case m.IsRegular(): ch.Entry.Options = []string{"bind", osutil.XSnapdKindFile()} case m&os.ModeSymlink != 0: if target, err := osReadlink(filepath.Join(dir, fi.Name())); err == nil { ch.Entry.Options = []string{osutil.XSnapdKindSymlink(), osutil.XSnapdSymlink(target)} } else { continue } default: logger.Noticef("skipping unsupported file %s", fi) continue } ch.Entry.Options = append(ch.Entry.Options, osutil.XSnapdSynthetic()) ch.Entry.Options = append(ch.Entry.Options, osutil.XSnapdNeededBy(neededBy)) changes = append(changes, ch) } // Finally unbind the safe-keeping directory as we don't need it anymore. changes = append(changes, &Change{ Action: Unmount, Entry: osutil.MountEntry{Name: "none", Dir: safeKeepingDir, Options: []string{osutil.XSnapdDetach()}}, }) return changes, nil } // FatalError is an error that we cannot correct. type FatalError struct { error } // execWritableMimic executes the plan for a writable mimic. // The result is a transformed mount namespace and a set of fake mount changes // that only exist in order to undo the plan. // // Certain assumptions are made about the plan, it must closely resemble that // created by planWritableMimic, in particular the sequence must look like this: // // - bind a directory aside into safekeeping location // - cover the original with tmpfs // - bind mount something from safekeeping location to an empty file or // directory in the tmpfs; this step can repeat any number of times // - unbind the safekeeping location // // Apart from merely executing the plan a fake plan is returned for undo. The // undo plan skips the following elements as compared to the original plan: // // - the initial bind mount that constructs the safekeeping directory is gone // - the final unmount that removes the safekeeping directory // - the source of each of the bind mounts that re-populate tmpfs. // // In the event of a failure the undo plan is executed and an error is // returned. If the undo plan fails the function returns a FatalError as it // cannot fix the system from an inconsistent state. func execWritableMimic(plan []*Change, as *Assumptions) ([]*Change, error) { undoChanges := make([]*Change, 0, len(plan)-2) for i, change := range plan { if _, err := changePerform(change, as); err != nil { // Drat, we failed! Let's undo everything according to our own undo // plan, by following it in reverse order. recoveryUndoChanges := make([]*Change, 0, len(undoChanges)+1) if i > 0 { // The undo plan doesn't contain the entry for the initial bind // mount of the safe keeping directory but we have already // performed it. For this recovery phase we need to insert that // in front of the undo plan manually. recoveryUndoChanges = append(recoveryUndoChanges, plan[0]) } recoveryUndoChanges = append(recoveryUndoChanges, undoChanges...) for j := len(recoveryUndoChanges) - 1; j >= 0; j-- { recoveryUndoChange := recoveryUndoChanges[j] // All the changes mount something, we need to reverse that. // The "undo plan" is "a plan that can be undone" not "the plan // for how to undo" so we need to flip the actions. recoveryUndoChange.Action = Unmount if recoveryUndoChange.Entry.OptBool("rbind") { recoveryUndoChange.Entry.Options = append(recoveryUndoChange.Entry.Options, osutil.XSnapdDetach()) } if _, err2 := changePerform(recoveryUndoChange, as); err2 != nil { // Drat, we failed when trying to recover from an error. // We cannot do anything at this stage. return nil, &FatalError{error: fmt.Errorf("cannot undo change %q while recovering from earlier error %v: %v", recoveryUndoChange, err, err2)} } } return nil, err } if i == 0 || i == len(plan)-1 { // Don't represent the initial and final changes in the undo plan. // The initial change is the safe-keeping bind mount, the final // change is the safe-keeping unmount. continue } if change.Entry.XSnapdKind() == "symlink" { // Don't represent symlinks in the undo plan. They are removed when // the tmpfs is unmounted. continue } // Store an undo change for the change we just performed. undoOpts := change.Entry.Options if change.Entry.OptBool("rbind") { undoOpts = make([]string, 0, len(change.Entry.Options)+1) undoOpts = append(undoOpts, change.Entry.Options...) undoOpts = append(undoOpts, "x-snapd.detach") } undoChange := &Change{ Action: Mount, Entry: osutil.MountEntry{Dir: change.Entry.Dir, Name: change.Entry.Name, Type: change.Entry.Type, Options: undoOpts}, } // Because of the use of a temporary bind mount (aka the safe-keeping // directory) we cannot represent bind mounts fully (the temporary bind // mount is unmounted as the last stage of this process). For that // reason let's hide the original location and overwrite it so to // appear as if the directory was a bind mount over itself. This is not // fully true (it is a bind mount from the old self to the new empty // directory or file in the same path, with the tmpfs in place already) // but this is closer to the truth and more in line with the idea that // this is just a plan for undoing the operation. if undoChange.Entry.OptBool("bind") || undoChange.Entry.OptBool("rbind") { undoChange.Entry.Name = undoChange.Entry.Dir } undoChanges = append(undoChanges, undoChange) } return undoChanges, nil } func createWritableMimic(dir, neededBy string, as *Assumptions) ([]*Change, error) { plan, err := planWritableMimic(dir, neededBy) if err != nil { return nil, err } changes, err := execWritableMimic(plan, as) if err != nil { return nil, err } return changes, nil } snapd-2.37.4~14.04.1/cmd/snap-update-ns/bootstrap.go0000664000000000000000000000777013435556260016522 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main // Use a pre-main helper to switch the mount namespace. This is required as // golang creates threads at will and setns(..., CLONE_NEWNS) fails if any // threads apart from the main thread exist. /* #include #include "bootstrap.h" // The bootstrap function is called by the loader before passing // control to main. We are using `preinit_array` rather than // `init_array` because the Go linker adds its own initialisation // function to `init_array`, and having ours run second would defeat // the purpose of the C bootstrap code. // // The `used` attribute ensures that the compiler doesn't oprimise out // the variable on the mistaken belief that it isn't used. __attribute__((section(".preinit_array"), used)) static typeof(&bootstrap) init = &bootstrap; // NOTE: do not add anything before the following `import "C"' */ import "C" import ( "errors" "fmt" "syscall" "unsafe" ) var ( // ErrNoNamespace is returned when a snap namespace does not exist. ErrNoNamespace = errors.New("cannot update mount namespace that was not created yet") ) // IMPORTANT: all the code in this section may be run with elevated privileges // when invoking snap-update-ns from the setuid snap-confine. // BootstrapError returns error (if any) encountered in pre-main C code. func BootstrapError() error { if C.bootstrap_msg == nil { return nil } errno := syscall.Errno(C.bootstrap_errno) // Translate EINVAL from setns or ENOENT from open into a dedicated error. if errno == syscall.EINVAL || errno == syscall.ENOENT { return ErrNoNamespace } if errno != 0 { return fmt.Errorf("%s: %s", C.GoString(C.bootstrap_msg), errno) } return fmt.Errorf("%s", C.GoString(C.bootstrap_msg)) } // This function is here to make clearing the boostrap errors accessible // from the tests. func clearBootstrapError() { C.bootstrap_msg = nil C.bootstrap_errno = 0 } // END IMPORTANT func makeArgv(args []string) []*C.char { // Create argv array with terminating NULL element argv := make([]*C.char, len(args)+1) for i, arg := range args { argv[i] = C.CString(arg) } return argv } func freeArgv(argv []*C.char) { for _, arg := range argv { C.free(unsafe.Pointer(arg)) } } // validateInstanceName checks if snap instance name is valid. // This also sets bootstrap_msg on failure. // // This function is here only to make the C.validate_instance_name // code testable from go. func validateInstanceName(instanceName string) int { cStr := C.CString(instanceName) defer C.free(unsafe.Pointer(cStr)) return int(C.validate_instance_name(cStr)) } // processArguments parses commnad line arguments. // The argument cmdline is a string with embedded // NUL bytes, separating particular arguments. // // This function is here only to make the C.validate_instance_name // code testable from go. func processArguments(args []string) (snapName string, shouldSetNs bool, processUserFstab bool, uid uint) { argv := makeArgv(args) defer freeArgv(argv) var snapNameOut *C.char var shouldSetNsOut C.bool var processUserFstabOut C.bool var uidOut C.ulong C.process_arguments(C.int(len(args)), &argv[0], &snapNameOut, &shouldSetNsOut, &processUserFstabOut, &uidOut) if snapNameOut != nil { snapName = C.GoString(snapNameOut) } shouldSetNs = bool(shouldSetNsOut) processUserFstab = bool(processUserFstabOut) uid = uint(uidOut) return snapName, shouldSetNs, processUserFstab, uid } snapd-2.37.4~14.04.1/cmd/snap-update-ns/bootstrap.h0000664000000000000000000000212613435556260016332 0ustar /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #ifndef SNAPD_CMD_SNAP_UPDATE_NS_H #define SNAPD_CMD_SNAP_UPDATE_NS_H #define _GNU_SOURCE #include #include extern int bootstrap_errno; 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, unsigned long * uid_out); int validate_instance_name(const char *instance_name); #endif snapd-2.37.4~14.04.1/cmd/snap-update-ns/main.go0000664000000000000000000002436413435556260015427 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main import ( "fmt" "os" "strings" "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/interfaces/mount" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/snap" ) var opts struct { FromSnapConfine bool `long:"from-snap-confine"` UserMounts bool `long:"user-mounts"` Positionals struct { SnapName string `positional-arg-name:"SNAP_NAME" required:"yes"` } `positional-args:"true"` } // IMPORTANT: all the code in main() until bootstrap is finished may be run // with elevated privileges when invoking snap-update-ns from the setuid // snap-confine. func main() { logger.SimpleSetup() if err := run(); err != nil { fmt.Printf("cannot update snap namespace: %s\n", err) os.Exit(1) } // END IMPORTANT } func parseArgs(args []string) error { parser := flags.NewParser(&opts, flags.HelpFlag|flags.PassDoubleDash|flags.PassAfterNonOption) _, err := parser.ParseArgs(args) return err } // IMPORTANT: all the code in run() until BootStrapError() is finished may // be run with elevated privileges when invoking snap-update-ns from // the setuid snap-confine. func run() error { // There is some C code that runs before main() is started. // That code always runs and sets an error condition if it fails. // Here we just check for the error. if err := BootstrapError(); err != nil { // If there is no mount namespace to transition to let's just quit // instantly without any errors as there is nothing to do anymore. if err == ErrNoNamespace { logger.Debugf("no preserved mount namespace, nothing to update") return nil } return err } // END IMPORTANT if err := parseArgs(os.Args[1:]); err != nil { return err } if opts.UserMounts { return applyUserFstab(opts.Positionals.SnapName) } return applyFstab(opts.Positionals.SnapName, opts.FromSnapConfine) } func applyFstab(instanceName string, fromSnapConfine bool) error { // Lock the mount namespace so that any concurrently attempted invocations // of snap-confine are synchronized and will see consistent state. lock, err := mount.OpenLock(instanceName) if err != nil { return fmt.Errorf("cannot open lock file for mount namespace of snap %q: %s", instanceName, err) } defer func() { logger.Debugf("unlocking mount namespace of snap %q", instanceName) lock.Close() }() logger.Debugf("locking mount namespace of snap %q", instanceName) if fromSnapConfine { // When --from-snap-confine is passed then we just ensure that the // namespace is locked. This is used by snap-confine to use // snap-update-ns to apply mount profiles. if err := lock.TryLock(); err != osutil.ErrAlreadyLocked { return fmt.Errorf("mount namespace of snap %q is not locked but --from-snap-confine was used", instanceName) } } else { if err := lock.Lock(); err != nil { return fmt.Errorf("cannot lock mount namespace of snap %q: %s", instanceName, err) } } // Freeze the mount namespace and unfreeze it later. This lets us perform // modifications without snap processes attempting to construct // symlinks or perform other malicious activity (such as attempting to // introduce a symlink that would cause us to mount something other // than what we expected). logger.Debugf("freezing processes of snap %q", instanceName) if err := freezeSnapProcesses(instanceName); err != nil { return err } defer func() { logger.Debugf("thawing processes of snap %q", instanceName) thawSnapProcesses(instanceName) }() // Allow creating directories related to this snap name. // // Note that we allow /var/snap instead of /var/snap/$SNAP_NAME because // content interface connections can readily create missing mount points on // both sides of the interface connection. // // We scope /snap/$SNAP_NAME because only one side of the connection can be // created, as snaps are read-only, the mimic construction will kick-in and // create the missing directory but this directory is only visible from the // snap that we are operating on (either plug or slot side, the point is, // the mount point is not universally visible). // // /snap/$SNAP_NAME needs to be there as the code that creates such mount // points must traverse writable host filesystem that contains /snap/*/ and // normally such access is off-limits. This approach allows /snap/foo // without allowing /snap/bin, for example. // // /snap/$SNAP_INSTANCE_NAME and /snap/$SNAP_NAME are added to allow // remapping for parallel installs only when the snap has an instance key // // TODO: Handle /home/*/snap/* when we do per-user mount namespaces and // allow defining layout items that refer to SNAP_USER_DATA and // SNAP_USER_COMMON. as := &Assumptions{} as.AddUnrestrictedPaths("/tmp", "/var/snap", "/snap/"+instanceName) if snapName := snap.InstanceSnap(instanceName); snapName != instanceName { as.AddUnrestrictedPaths("/snap/" + snapName) } return computeAndSaveChanges(instanceName, as) } func computeAndSaveChanges(snapName string, as *Assumptions) error { // Read the desired and current mount profiles. Note that missing files // count as empty profiles so that we can gracefully handle a mount // interface connection/disconnection. desiredProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapMountPolicyDir, snapName) desired, err := osutil.LoadMountProfile(desiredProfilePath) if err != nil { return fmt.Errorf("cannot load desired mount profile of snap %q: %s", snapName, err) } debugShowProfile(desired, "desired mount profile") currentProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapRunNsDir, snapName) currentBefore, err := osutil.LoadMountProfile(currentProfilePath) if err != nil { return fmt.Errorf("cannot load current mount profile of snap %q: %s", snapName, err) } debugShowProfile(currentBefore, "current mount profile (before applying changes)") // Synthesize mount changes that were applied before for the purpose of the tmpfs detector. for _, entry := range currentBefore.Entries { as.AddChange(&Change{Action: Mount, Entry: entry}) } currentAfter, err := applyProfile(snapName, currentBefore, desired, as) if err != nil { return err } logger.Debugf("saving current mount profile of snap %q", snapName) if err := currentAfter.Save(currentProfilePath); err != nil { return fmt.Errorf("cannot save current mount profile of snap %q: %s", snapName, err) } return nil } func applyProfile(snapName string, currentBefore, desired *osutil.MountProfile, as *Assumptions) (*osutil.MountProfile, error) { // Compute the needed changes and perform each change if // needed, collecting those that we managed to perform or that // were performed already. changesNeeded := NeededChanges(currentBefore, desired) debugShowChanges(changesNeeded, "mount changes needed") logger.Debugf("performing mount changes:") var changesMade []*Change for _, change := range changesNeeded { logger.Debugf("\t * %s", change) synthesised, err := changePerform(change, as) changesMade = append(changesMade, synthesised...) if len(synthesised) > 0 { logger.Debugf("\tsynthesised additional mount changes:") for _, synth := range synthesised { logger.Debugf(" * \t\t%s", synth) } } if err != nil { // We may have done something even if Perform itself has // failed. We need to collect synthesized changes and // store them. origin := change.Entry.XSnapdOrigin() if origin == "layout" || origin == "overname" { return nil, err } else if err != ErrIgnoredMissingMount { logger.Noticef("cannot change mount namespace of snap %q according to change %s: %s", snapName, change, err) } continue } changesMade = append(changesMade, change) } // Compute the new current profile so that it contains only changes that were made // and save it back for next runs. var currentAfter osutil.MountProfile for _, change := range changesMade { if change.Action == Mount || change.Action == Keep { currentAfter.Entries = append(currentAfter.Entries, change.Entry) } } debugShowProfile(¤tAfter, "current mount profile (after applying changes)") return ¤tAfter, nil } func debugShowProfile(profile *osutil.MountProfile, header string) { if len(profile.Entries) > 0 { logger.Debugf("%s:", header) for _, entry := range profile.Entries { logger.Debugf("\t%s", entry) } } else { logger.Debugf("%s: (none)", header) } } func debugShowChanges(changes []*Change, header string) { if len(changes) > 0 { logger.Debugf("%s:", header) for _, change := range changes { logger.Debugf("\t%s", change) } } else { logger.Debugf("%s: (none)", header) } } func applyUserFstab(snapName string) error { desiredProfilePath := fmt.Sprintf("%s/snap.%s.user-fstab", dirs.SnapMountPolicyDir, snapName) desired, err := osutil.LoadMountProfile(desiredProfilePath) if err != nil { return fmt.Errorf("cannot load desired user mount profile of snap %q: %s", snapName, err) } // Replace XDG_RUNTIME_DIR in mount profile xdgRuntimeDir := fmt.Sprintf("%s/%d", dirs.XdgRuntimeDirBase, os.Getuid()) for i := range desired.Entries { if strings.HasPrefix(desired.Entries[i].Name, "$XDG_RUNTIME_DIR/") { desired.Entries[i].Name = strings.Replace(desired.Entries[i].Name, "$XDG_RUNTIME_DIR", xdgRuntimeDir, 1) } if strings.HasPrefix(desired.Entries[i].Dir, "$XDG_RUNTIME_DIR/") { desired.Entries[i].Dir = strings.Replace(desired.Entries[i].Dir, "$XDG_RUNTIME_DIR", xdgRuntimeDir, 1) } } debugShowProfile(desired, "desired mount profile") // TODO: configure the secure helper and inform it about directories that // can be created without trespassing. as := &Assumptions{} _, err = applyProfile(snapName, &osutil.MountProfile{}, desired, as) return err } snapd-2.37.4~14.04.1/cmd/snap-update-ns/trespassing_test.go0000664000000000000000000003401113435556260020072 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017-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 . * */ package main_test import ( "syscall" . "gopkg.in/check.v1" update "github.com/snapcore/snapd/cmd/snap-update-ns" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/testutil" ) type trespassingSuite struct { testutil.BaseTest sys *testutil.SyscallRecorder } var _ = Suite(&trespassingSuite{}) func (s *trespassingSuite) SetUpTest(c *C) { s.BaseTest.SetUpTest(c) s.sys = &testutil.SyscallRecorder{} s.BaseTest.AddCleanup(update.MockSystemCalls(s.sys)) } func (s *trespassingSuite) TearDownTest(c *C) { s.BaseTest.TearDownTest(c) s.sys.CheckForStrayDescriptors(c) } // AddUnrestrictedPaths and IsRestricted func (s *trespassingSuite) TestAddUnrestrictedPaths(c *C) { a := &update.Assumptions{} c.Assert(a.IsRestricted("/etc/test.conf"), Equals, true) a.AddUnrestrictedPaths("/etc") c.Assert(a.IsRestricted("/etc/test.conf"), Equals, false) c.Assert(a.IsRestricted("/etc/"), Equals, false) c.Assert(a.IsRestricted("/etc"), Equals, false) c.Assert(a.IsRestricted("/etc2"), Equals, true) a.AddUnrestrictedPaths("/") c.Assert(a.IsRestricted("/foo"), Equals, false) } func (s *trespassingSuite) TestMockUnrestrictedPaths(c *C) { a := &update.Assumptions{} c.Assert(a.IsRestricted("/etc/test.conf"), Equals, true) restore := a.MockUnrestrictedPaths("/etc/") c.Assert(a.IsRestricted("/etc/test.conf"), Equals, false) restore() c.Assert(a.IsRestricted("/etc/test.conf"), Equals, true) } // canWriteToDirectory and AddChange // We are not allowed to write to ext4. func (s *trespassingSuite) TestCanWriteToDirectoryWritableExt4(c *C) { a := &update.Assumptions{} path := "/etc" fd, err := s.sys.Open(path, syscall.O_DIRECTORY, 0) c.Assert(err, IsNil) defer s.sys.Close(fd) s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.Ext4Magic}) s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) ok, err := a.CanWriteToDirectory(fd, path) c.Assert(err, IsNil) c.Assert(ok, Equals, false) } // We are allowed to write to ext4 that was mounted read-only. func (s *trespassingSuite) TestCanWriteToDirectoryReadOnlyExt4(c *C) { a := &update.Assumptions{} path := "/etc" fd, err := s.sys.Open(path, syscall.O_DIRECTORY, 0) c.Assert(err, IsNil) defer s.sys.Close(fd) s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.Ext4Magic, Flags: update.StReadOnly}) s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) ok, err := a.CanWriteToDirectory(fd, path) c.Assert(err, IsNil) c.Assert(ok, Equals, true) } // We are not allowed to write to tmpfs. func (s *trespassingSuite) TestCanWriteToDirectoryTmpfs(c *C) { a := &update.Assumptions{} path := "/etc" fd, err := s.sys.Open(path, syscall.O_DIRECTORY, 0) c.Assert(err, IsNil) defer s.sys.Close(fd) s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.TmpfsMagic}) s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) ok, err := a.CanWriteToDirectory(fd, path) c.Assert(err, IsNil) c.Assert(ok, Equals, false) } // We are allowed to write to tmpfs that was mounted by snapd. func (s *trespassingSuite) TestCanWriteToDirectoryTmpfsMountedBySnapd(c *C) { a := &update.Assumptions{} path := "/etc" fd, err := s.sys.Open(path, syscall.O_DIRECTORY, 0) c.Assert(err, IsNil) defer s.sys.Close(fd) s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.TmpfsMagic}) s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) a.AddChange(&update.Change{ Action: update.Mount, Entry: osutil.MountEntry{Type: "tmpfs", Dir: path}}) ok, err := a.CanWriteToDirectory(fd, path) c.Assert(err, IsNil) c.Assert(ok, Equals, true) } // We are allowed to write to directory beneath a tmpfs that was mounted by snapd. func (s *trespassingSuite) TestCanWriteToDirectoryUnderTmpfsMountedBySnapd(c *C) { a := &update.Assumptions{} fd, err := s.sys.Open("/etc", syscall.O_DIRECTORY, 0) c.Assert(err, IsNil) defer s.sys.Close(fd) s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.TmpfsMagic}) s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{Dev: 0x42}) a.AddChange(&update.Change{ Action: update.Mount, Entry: osutil.MountEntry{Type: "tmpfs", Dir: "/etc"}}) ok, err := a.CanWriteToDirectory(fd, "/etc") c.Assert(err, IsNil) c.Assert(ok, Equals, true) // Now we have primed the assumption state with knowledge of 0x42 device as // a verified tmpfs. We can now exploit it by trying to write to // /etc/conf.d and seeing that is allowed even though /etc/conf.d itself is // not a mount point representing tmpfs. fd2, err := s.sys.Open("/etc/conf.d", syscall.O_DIRECTORY, 0) c.Assert(err, IsNil) defer s.sys.Close(fd2) s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.TmpfsMagic}) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{Dev: 0x42}) ok, err = a.CanWriteToDirectory(fd2, "/etc/conf.d") c.Assert(err, IsNil) c.Assert(ok, Equals, true) } // We are allowed to write to directory which is a bind mount of something, beneath a tmpfs that was mounted by snapd. func (s *trespassingSuite) TestCanWriteToDirectoryUnderReboundTmpfsMountedBySnapd(c *C) { a := &update.Assumptions{} fd, err := s.sys.Open("/etc", syscall.O_DIRECTORY, 0) c.Assert(err, IsNil) c.Assert(fd, Equals, 3) defer s.sys.Close(fd) s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.TmpfsMagic}) s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{Dev: 0x42}) a.AddChange(&update.Change{ Action: update.Mount, Entry: osutil.MountEntry{Type: "tmpfs", Dir: "/etc"}}) ok, err := a.CanWriteToDirectory(fd, "/etc") c.Assert(err, IsNil) c.Assert(ok, Equals, true) // Now we have primed the assumption state with knowledge of 0x42 device as // a verified tmpfs. Unlike in the test above though the directory // /etc/conf.d is a bind mount from another tmpfs that we know nothing // about. fd2, err := s.sys.Open("/etc/conf.d", syscall.O_DIRECTORY, 0) c.Assert(err, IsNil) c.Assert(fd2, Equals, 4) defer s.sys.Close(fd2) s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.TmpfsMagic}) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{Dev: 0xdeadbeef}) ok, err = a.CanWriteToDirectory(fd2, "/etc/conf.d") c.Assert(err, IsNil) c.Assert(ok, Equals, false) } // We are allowed to write to an unrestricted path. func (s *trespassingSuite) TestCanWriteToDirectoryUnrestricted(c *C) { a := &update.Assumptions{} path := "/var/snap/foo/common" fd, err := s.sys.Open(path, syscall.O_DIRECTORY, 0) c.Assert(err, IsNil) defer s.sys.Close(fd) s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.Ext4Magic}) s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) a.AddUnrestrictedPaths(path) ok, err := a.CanWriteToDirectory(fd, path) c.Assert(err, IsNil) c.Assert(ok, Equals, true) } // Errors from fstatfs are propagated to the caller. func (s *trespassingSuite) TestCanWriteToDirectoryErrorsFstatfs(c *C) { a := &update.Assumptions{} path := "/etc" fd, err := s.sys.Open(path, syscall.O_DIRECTORY, 0) c.Assert(err, IsNil) defer s.sys.Close(fd) s.sys.InsertFault(`fstatfs 3 `, errTesting) ok, err := a.CanWriteToDirectory(fd, path) c.Assert(err, ErrorMatches, `cannot fstatfs "/etc": testing`) c.Assert(ok, Equals, false) } // Errors from fstat are propagated to the caller. func (s *trespassingSuite) TestCanWriteToDirectoryErrorsFstat(c *C) { a := &update.Assumptions{} path := "/etc" fd, err := s.sys.Open(path, syscall.O_DIRECTORY, 0) c.Assert(err, IsNil) defer s.sys.Close(fd) s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{}) s.sys.InsertFault(`fstat 3 `, errTesting) ok, err := a.CanWriteToDirectory(fd, path) c.Assert(err, ErrorMatches, `cannot fstat "/etc": testing`) c.Assert(ok, Equals, false) } // RestrictionsFor, Check and LiftRestrictions func (s *trespassingSuite) TestRestrictionsForEtc(c *C) { a := &update.Assumptions{} // There are restrictions for writing in /etc. rs := a.RestrictionsFor("/etc/test.conf") c.Assert(rs, NotNil) fd, err := s.sys.Open("/etc", syscall.O_DIRECTORY, 0) c.Assert(err, IsNil) defer s.sys.Close(fd) s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.Ext4Magic}) s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) // Check reports trespassing error, restrictions may be lifted though. err = rs.Check(fd, "/etc") c.Assert(err, ErrorMatches, `cannot write to "/etc/test.conf" because it would affect the host in "/etc"`) c.Assert(err.(*update.TrespassingError).ViolatedPath, Equals, "/etc") c.Assert(err.(*update.TrespassingError).DesiredPath, Equals, "/etc/test.conf") rs.Lift() c.Assert(rs.Check(fd, "/etc"), IsNil) } // Check returns errors from lower layers. func (s *trespassingSuite) TestRestrictionsForErrors(c *C) { a := &update.Assumptions{} rs := a.RestrictionsFor("/etc/test.conf") c.Assert(rs, NotNil) fd, err := s.sys.Open("/etc", syscall.O_DIRECTORY, 0) c.Assert(err, IsNil) defer s.sys.Close(fd) s.sys.InsertFault(`fstatfs 3 `, errTesting) err = rs.Check(fd, "/etc") c.Assert(err, ErrorMatches, `cannot fstatfs "/etc": testing`) } func (s *trespassingSuite) TestRestrictionsForVarSnap(c *C) { a := &update.Assumptions{} a.AddUnrestrictedPaths("/var/snap") // There are no restrictions in $SNAP_COMMON. rs := a.RestrictionsFor("/var/snap/foo/common/test.conf") c.Assert(rs, IsNil) // Nil restrictions have working Check and Lift methods. c.Assert(rs.Check(3, "unused"), IsNil) rs.Lift() } func (s *trespassingSuite) TestRestrictionsForRootfsEntries(c *C) { a := &update.Assumptions{} // The root directory is special, it's not a trespassing error we can // recover from because we cannot construct a writable mimic for the root // directory today. rs := a.RestrictionsFor("/foo.conf") fd, err := s.sys.Open("/", syscall.O_DIRECTORY, 0) c.Assert(err, IsNil) defer s.sys.Close(fd) s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.Ext4Magic}) s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) // Nil restrictions have working Check and Lift methods. c.Assert(rs.Check(fd, "/"), ErrorMatches, `cannot recover from trespassing over /`) } // isReadOnly func (s *trespassingSuite) TestIsReadOnlySquashfsMountedRo(c *C) { path := "/some/path" statfs := &syscall.Statfs_t{Type: update.SquashfsMagic, Flags: update.StReadOnly} result := update.IsReadOnly(path, statfs) c.Assert(result, Equals, true) } func (s *trespassingSuite) TestIsReadOnlySquashfsMountedRw(c *C) { path := "/some/path" statfs := &syscall.Statfs_t{Type: update.SquashfsMagic} result := update.IsReadOnly(path, statfs) c.Assert(result, Equals, true) } func (s *trespassingSuite) TestIsReadOnlyExt4MountedRw(c *C) { path := "/some/path" statfs := &syscall.Statfs_t{Type: update.Ext4Magic} result := update.IsReadOnly(path, statfs) c.Assert(result, Equals, false) } // isSnapdCreatedPrivateTmpfs func (s *trespassingSuite) TestIsPrivateTmpfsCreatedBySnapdNotATmpfs(c *C) { path := "/some/path" // An ext4 (which is not a tmpfs) is not a private tmpfs. statfs := &syscall.Statfs_t{Type: update.Ext4Magic} stat := &syscall.Stat_t{} result := update.IsPrivateTmpfsCreatedBySnapd(path, statfs, stat, nil) c.Assert(result, Equals, false) } func (s *trespassingSuite) TestIsPrivateTmpfsCreatedBySnapdNotTrusted(c *C) { path := "/some/path" // A tmpfs is not private if it doesn't come from a change we made. statfs := &syscall.Statfs_t{Type: update.TmpfsMagic} stat := &syscall.Stat_t{} result := update.IsPrivateTmpfsCreatedBySnapd(path, statfs, stat, nil) c.Assert(result, Equals, false) } func (s *trespassingSuite) TestIsPrivateTmpfsCreatedBySnapdViaChanges(c *C) { path := "/some/path" // A tmpfs is private because it was mounted by snap-update-ns. statfs := &syscall.Statfs_t{Type: update.TmpfsMagic} stat := &syscall.Stat_t{} // A tmpfs was mounted in the past so it is private. result := update.IsPrivateTmpfsCreatedBySnapd(path, statfs, stat, []*update.Change{ {Action: update.Mount, Entry: osutil.MountEntry{Name: "tmpfs", Dir: path, Type: "tmpfs"}}, }) c.Assert(result, Equals, true) // A tmpfs was mounted but then it was unmounted so it is not private anymore. result = update.IsPrivateTmpfsCreatedBySnapd(path, statfs, stat, []*update.Change{ {Action: update.Mount, Entry: osutil.MountEntry{Name: "tmpfs", Dir: path, Type: "tmpfs"}}, {Action: update.Unmount, Entry: osutil.MountEntry{Name: "tmpfs", Dir: path, Type: "tmpfs"}}, }) c.Assert(result, Equals, false) // Finally, after the mounting and unmounting the tmpfs was mounted again. result = update.IsPrivateTmpfsCreatedBySnapd(path, statfs, stat, []*update.Change{ {Action: update.Mount, Entry: osutil.MountEntry{Name: "tmpfs", Dir: path, Type: "tmpfs"}}, {Action: update.Unmount, Entry: osutil.MountEntry{Name: "tmpfs", Dir: path, Type: "tmpfs"}}, {Action: update.Mount, Entry: osutil.MountEntry{Name: "tmpfs", Dir: path, Type: "tmpfs"}}, }) c.Assert(result, Equals, true) } func (s *trespassingSuite) TestIsPrivateTmpfsCreatedBySnapdDeeper(c *C) { path := "/some/path/below" // A tmpfs is not private beyond the exact mount point from a change. // That is, sub-directories of a private tmpfs are not recognized as private. statfs := &syscall.Statfs_t{Type: update.TmpfsMagic} stat := &syscall.Stat_t{} result := update.IsPrivateTmpfsCreatedBySnapd(path, statfs, stat, []*update.Change{ {Action: update.Mount, Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/some/path", Type: "tmpfs"}}, }) c.Assert(result, Equals, false) } snapd-2.37.4~14.04.1/cmd/snap-update-ns/main_test.go0000664000000000000000000003441513435556260016464 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main_test import ( "bytes" "fmt" "io/ioutil" "os" "path/filepath" "testing" . "gopkg.in/check.v1" update "github.com/snapcore/snapd/cmd/snap-update-ns" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/testutil" ) func Test(t *testing.T) { TestingT(t) } type mainSuite struct { testutil.BaseTest as *update.Assumptions log *bytes.Buffer } var _ = Suite(&mainSuite{}) func (s *mainSuite) SetUpTest(c *C) { s.BaseTest.SetUpTest(c) s.as = &update.Assumptions{} buf, restore := logger.MockLogger() s.BaseTest.AddCleanup(restore) s.log = buf } func (s *mainSuite) TestComputeAndSaveChanges(c *C) { dirs.SetRootDir(c.MkDir()) defer dirs.SetRootDir("/") restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) { return nil, nil }) defer restore() snapName := "foo" desiredProfileContent := `/var/lib/snapd/hostfs/usr/share/fonts /usr/share/fonts none bind,ro 0 0 /var/lib/snapd/hostfs/usr/local/share/fonts /usr/local/share/fonts none bind,ro 0 0` desiredProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapMountPolicyDir, snapName) err := os.MkdirAll(filepath.Dir(desiredProfilePath), 0755) c.Assert(err, IsNil) err = ioutil.WriteFile(desiredProfilePath, []byte(desiredProfileContent), 0644) c.Assert(err, IsNil) currentProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapRunNsDir, snapName) err = os.MkdirAll(filepath.Dir(currentProfilePath), 0755) c.Assert(err, IsNil) err = ioutil.WriteFile(currentProfilePath, nil, 0644) c.Assert(err, IsNil) err = update.ComputeAndSaveChanges(snapName, s.as) c.Assert(err, IsNil) c.Check(currentProfilePath, testutil.FileEquals, `/var/lib/snapd/hostfs/usr/local/share/fonts /usr/local/share/fonts none bind,ro 0 0 /var/lib/snapd/hostfs/usr/share/fonts /usr/share/fonts none bind,ro 0 0 `) } func (s *mainSuite) TestAddingSyntheticChanges(c *C) { dirs.SetRootDir(c.MkDir()) defer dirs.SetRootDir("/") // The snap `mysnap` wishes to export it's usr/share/mysnap directory and // make it appear as if it was in /usr/share/mysnap directly. const snapName = "mysnap" const currentProfileContent = "" const desiredProfileContent = "/snap/mysnap/42/usr/share/mysnap /usr/share/mysnap none bind,ro 0 0" currentProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapRunNsDir, snapName) desiredProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapMountPolicyDir, snapName) c.Assert(os.MkdirAll(filepath.Dir(currentProfilePath), 0755), IsNil) c.Assert(os.MkdirAll(filepath.Dir(desiredProfilePath), 0755), IsNil) c.Assert(ioutil.WriteFile(currentProfilePath, []byte(currentProfileContent), 0644), IsNil) c.Assert(ioutil.WriteFile(desiredProfilePath, []byte(desiredProfileContent), 0644), IsNil) // In order to make that work, /usr/share had to be converted to a writable // mimic. Some actions were performed under the hood and now we see a // subset of them as synthetic changes here. // // Note that if you compare this to the code that plans a writable mimic // you will see that there are additional changes that are _not_ // represented here. The changes have only one goal: tell // snap-update-ns how the mimic can be undone in case it is no longer // needed. restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) { // The change that we were asked to perform is to create a bind mount // from within the snap to /usr/share/mysnap. c.Assert(chg, DeepEquals, &update.Change{ Action: update.Mount, Entry: osutil.MountEntry{ Name: "/snap/mysnap/42/usr/share/mysnap", Dir: "/usr/share/mysnap", Type: "none", Options: []string{"bind", "ro"}}}) synthetic := []*update.Change{ // The original directory (which was a part of the core snap and is // read only) was hidden with a tmpfs. {Action: update.Mount, Entry: osutil.MountEntry{ Dir: "/usr/share", Name: "tmpfs", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/usr/share/mysnap"}}}, // For the sake of brevity we will only represent a few of the // entries typically there. Normally this list can get quite long. // Also note that the entry is a little fake. In reality it was // constructed using a temporary bind mount that contained the // original mount entries of /usr/share but this fact was lost. // Again, the only point of this entry is to correctly perform an // undo operation when /usr/share/mysnap is no longer needed. {Action: update.Mount, Entry: osutil.MountEntry{ Dir: "/usr/share/adduser", Name: "/usr/share/adduser", Options: []string{"bind", "ro", "x-snapd.synthetic", "x-snapd.needed-by=/usr/share/mysnap"}}}, {Action: update.Mount, Entry: osutil.MountEntry{ Dir: "/usr/share/awk", Name: "/usr/share/awk", Options: []string{"bind", "ro", "x-snapd.synthetic", "x-snapd.needed-by=/usr/share/mysnap"}}}, } return synthetic, nil }) defer restore() c.Assert(update.ComputeAndSaveChanges(snapName, s.as), IsNil) c.Check(currentProfilePath, testutil.FileEquals, `tmpfs /usr/share tmpfs x-snapd.synthetic,x-snapd.needed-by=/usr/share/mysnap 0 0 /usr/share/adduser /usr/share/adduser none bind,ro,x-snapd.synthetic,x-snapd.needed-by=/usr/share/mysnap 0 0 /usr/share/awk /usr/share/awk none bind,ro,x-snapd.synthetic,x-snapd.needed-by=/usr/share/mysnap 0 0 /snap/mysnap/42/usr/share/mysnap /usr/share/mysnap none bind,ro 0 0 `) } func (s *mainSuite) TestRemovingSyntheticChanges(c *C) { dirs.SetRootDir(c.MkDir()) defer dirs.SetRootDir("/") // The snap `mysnap` no longer wishes to export it's usr/share/mysnap // directory. All the synthetic changes that were associated with that mount // entry can be discarded. const snapName = "mysnap" const currentProfileContent = `tmpfs /usr/share tmpfs x-snapd.synthetic,x-snapd.needed-by=/usr/share/mysnap 0 0 /usr/share/adduser /usr/share/adduser none bind,ro,x-snapd.synthetic,x-snapd.needed-by=/usr/share/mysnap 0 0 /usr/share/awk /usr/share/awk none bind,ro,x-snapd.synthetic,x-snapd.needed-by=/usr/share/mysnap 0 0 /snap/mysnap/42/usr/share/mysnap /usr/share/mysnap none bind,ro 0 0 ` const desiredProfileContent = "" currentProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapRunNsDir, snapName) desiredProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapMountPolicyDir, snapName) c.Assert(os.MkdirAll(filepath.Dir(currentProfilePath), 0755), IsNil) c.Assert(os.MkdirAll(filepath.Dir(desiredProfilePath), 0755), IsNil) c.Assert(ioutil.WriteFile(currentProfilePath, []byte(currentProfileContent), 0644), IsNil) c.Assert(ioutil.WriteFile(desiredProfilePath, []byte(desiredProfileContent), 0644), IsNil) n := -1 restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) { n++ switch n { case 0: c.Assert(chg, DeepEquals, &update.Change{ Action: update.Unmount, Entry: osutil.MountEntry{ Name: "/snap/mysnap/42/usr/share/mysnap", Dir: "/usr/share/mysnap", Type: "none", Options: []string{"bind", "ro"}, }, }) case 1: c.Assert(chg, DeepEquals, &update.Change{ Action: update.Unmount, Entry: osutil.MountEntry{ Name: "/usr/share/awk", Dir: "/usr/share/awk", Type: "none", Options: []string{"bind", "ro", "x-snapd.synthetic", "x-snapd.needed-by=/usr/share/mysnap"}, }, }) case 2: c.Assert(chg, DeepEquals, &update.Change{ Action: update.Unmount, Entry: osutil.MountEntry{ Name: "/usr/share/adduser", Dir: "/usr/share/adduser", Type: "none", Options: []string{"bind", "ro", "x-snapd.synthetic", "x-snapd.needed-by=/usr/share/mysnap"}, }, }) case 3: c.Assert(chg, DeepEquals, &update.Change{ Action: update.Unmount, Entry: osutil.MountEntry{ Name: "tmpfs", Dir: "/usr/share", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/usr/share/mysnap"}, }, }) default: panic(fmt.Sprintf("unexpected call n=%d, chg: %v", n, *chg)) } return nil, nil }) defer restore() c.Assert(update.ComputeAndSaveChanges(snapName, s.as), IsNil) c.Check(currentProfilePath, testutil.FileEquals, "") } func (s *mainSuite) TestApplyingLayoutChanges(c *C) { dirs.SetRootDir(c.MkDir()) defer dirs.SetRootDir("/") const snapName = "mysnap" const currentProfileContent = "" const desiredProfileContent = "/snap/mysnap/42/usr/share/mysnap /usr/share/mysnap none bind,ro,x-snapd.origin=layout 0 0" currentProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapRunNsDir, snapName) desiredProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapMountPolicyDir, snapName) c.Assert(os.MkdirAll(filepath.Dir(currentProfilePath), 0755), IsNil) c.Assert(os.MkdirAll(filepath.Dir(desiredProfilePath), 0755), IsNil) c.Assert(ioutil.WriteFile(currentProfilePath, []byte(currentProfileContent), 0644), IsNil) c.Assert(ioutil.WriteFile(desiredProfilePath, []byte(desiredProfileContent), 0644), IsNil) n := -1 restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) { n++ switch n { case 0: c.Assert(chg, DeepEquals, &update.Change{ Action: update.Mount, Entry: osutil.MountEntry{ Name: "/snap/mysnap/42/usr/share/mysnap", Dir: "/usr/share/mysnap", Type: "none", Options: []string{"bind", "ro", "x-snapd.origin=layout"}, }, }) return nil, fmt.Errorf("testing") default: panic(fmt.Sprintf("unexpected call n=%d, chg: %v", n, *chg)) } }) defer restore() // The error was not ignored, we bailed out. c.Assert(update.ComputeAndSaveChanges(snapName, s.as), ErrorMatches, "testing") c.Check(currentProfilePath, testutil.FileEquals, "") } func (s *mainSuite) TestApplyingParallelInstanceChanges(c *C) { dirs.SetRootDir(c.MkDir()) defer dirs.SetRootDir("/") const snapName = "mysnap" const currentProfileContent = "" const desiredProfileContent = "/snap/mysnap_foo /snap/mysnap none rbind,x-snapd.origin=overname 0 0" currentProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapRunNsDir, snapName) desiredProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapMountPolicyDir, snapName) c.Assert(os.MkdirAll(filepath.Dir(currentProfilePath), 0755), IsNil) c.Assert(os.MkdirAll(filepath.Dir(desiredProfilePath), 0755), IsNil) c.Assert(ioutil.WriteFile(currentProfilePath, []byte(currentProfileContent), 0644), IsNil) c.Assert(ioutil.WriteFile(desiredProfilePath, []byte(desiredProfileContent), 0644), IsNil) n := -1 restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) { n++ switch n { case 0: c.Assert(chg, DeepEquals, &update.Change{ Action: update.Mount, Entry: osutil.MountEntry{ Name: "/snap/mysnap_foo", Dir: "/snap/mysnap", Type: "none", Options: []string{"rbind", "x-snapd.origin=overname"}, }, }) return nil, fmt.Errorf("testing") default: panic(fmt.Sprintf("unexpected call n=%d, chg: %v", n, *chg)) } }) defer restore() // The error was not ignored, we bailed out. c.Assert(update.ComputeAndSaveChanges(snapName, nil), ErrorMatches, "testing") c.Check(currentProfilePath, testutil.FileEquals, "") } func (s *mainSuite) TestApplyIgnoredMissingMount(c *C) { dirs.SetRootDir(c.MkDir()) defer dirs.SetRootDir("/") const snapName = "mysnap" const currentProfileContent = "" const desiredProfileContent = "/source /target none bind,x-snapd.ignore-missing 0 0" currentProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapRunNsDir, snapName) desiredProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapMountPolicyDir, snapName) c.Assert(os.MkdirAll(filepath.Dir(currentProfilePath), 0755), IsNil) c.Assert(os.MkdirAll(filepath.Dir(desiredProfilePath), 0755), IsNil) c.Assert(ioutil.WriteFile(currentProfilePath, []byte(currentProfileContent), 0644), IsNil) c.Assert(ioutil.WriteFile(desiredProfilePath, []byte(desiredProfileContent), 0644), IsNil) n := -1 restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) { n++ switch n { case 0: c.Assert(chg, DeepEquals, &update.Change{ Action: update.Mount, Entry: osutil.MountEntry{ Name: "/source", Dir: "/target", Type: "none", Options: []string{"bind", "x-snapd.ignore-missing"}, }, }) return nil, update.ErrIgnoredMissingMount default: panic(fmt.Sprintf("unexpected call n=%d, chg: %v", n, *chg)) } }) defer restore() // The error was ignored, and no mount was recorded in the profile c.Assert(update.ComputeAndSaveChanges(snapName, s.as), IsNil) c.Check(s.log.String(), Equals, "") c.Check(currentProfilePath, testutil.FileEquals, "") } func (s *mainSuite) TestApplyUserFstab(c *C) { dirs.SetRootDir(c.MkDir()) defer dirs.SetRootDir("/") var changes []update.Change restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) { changes = append(changes, *chg) return nil, nil }) defer restore() snapName := "foo" desiredProfileContent := `$XDG_RUNTIME_DIR/doc/by-app/snap.foo $XDG_RUNTIME_DIR/doc none bind,rw 0 0` desiredProfilePath := fmt.Sprintf("%s/snap.%s.user-fstab", dirs.SnapMountPolicyDir, snapName) err := os.MkdirAll(filepath.Dir(desiredProfilePath), 0755) c.Assert(err, IsNil) err = ioutil.WriteFile(desiredProfilePath, []byte(desiredProfileContent), 0644) c.Assert(err, IsNil) err = update.ApplyUserFstab("foo") c.Assert(err, IsNil) xdgRuntimeDir := fmt.Sprintf("%s/%d", dirs.XdgRuntimeDirBase, os.Getuid()) c.Assert(changes, HasLen, 1) c.Assert(changes[0].Action, Equals, update.Mount) c.Assert(changes[0].Entry.Name, Equals, xdgRuntimeDir+"/doc/by-app/snap.foo") c.Assert(changes[0].Entry.Dir, Matches, xdgRuntimeDir+"/doc") } snapd-2.37.4~14.04.1/cmd/snap-update-ns/sorting_test.go0000664000000000000000000000365013435556260017222 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main import ( "sort" . "gopkg.in/check.v1" "github.com/snapcore/snapd/osutil" ) type sortSuite struct{} var _ = Suite(&sortSuite{}) func (s *sortSuite) TestTrailingSlashesComparison(c *C) { // Naively sorted entries. entries := []osutil.MountEntry{ {Dir: "/a/b"}, {Dir: "/a/b-1"}, {Dir: "/a/b-1/3"}, {Dir: "/a/b/c"}, } sort.Sort(byOriginAndMagicDir(entries)) // Entries sorted as if they had a trailing slash. c.Assert(entries, DeepEquals, []osutil.MountEntry{ {Dir: "/a/b-1"}, {Dir: "/a/b-1/3"}, {Dir: "/a/b"}, {Dir: "/a/b/c"}, }) } func (s *sortSuite) TestParallelInstancesAndSimple(c *C) { // Naively sorted entries. entries := []osutil.MountEntry{ {Dir: "/a/b"}, {Dir: "/a/b-1"}, {Dir: "/snap/bar", Options: []string{osutil.XSnapdOriginOvername()}}, {Dir: "/a/b-1/3"}, {Dir: "/foo/bar", Options: []string{osutil.XSnapdOriginOvername()}}, {Dir: "/a/b/c"}, } sort.Sort(byOriginAndMagicDir(entries)) // Entries sorted as if they had a trailing slash. c.Assert(entries, DeepEquals, []osutil.MountEntry{ {Dir: "/foo/bar", Options: []string{osutil.XSnapdOriginOvername()}}, {Dir: "/snap/bar", Options: []string{osutil.XSnapdOriginOvername()}}, {Dir: "/a/b-1"}, {Dir: "/a/b-1/3"}, {Dir: "/a/b"}, {Dir: "/a/b/c"}, }) } snapd-2.37.4~14.04.1/cmd/snap-update-ns/bootstrap_ppc64le.go0000664000000000000000000000171713435556260020052 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- // // +build ppc64le,go1.7,!go1.8 /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main /* #cgo LDFLAGS: -no-pie // we need "-no-pie" for ppc64le,go1.7 to work around build failure on // ppc64el with go1.7, see // https://forum.snapcraft.io/t/snapd-master-fails-on-zesty-ppc64el-with-r-ppc64-addr16-ha-for-symbol-out-of-range/ */ import "C" snapd-2.37.4~14.04.1/cmd/snap-update-ns/secure_bindmount.go0000664000000000000000000000737213435556260020050 0ustar // -*- 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 . * */ package main import ( "fmt" "syscall" ) // BindMount performs a bind mount between two absolute paths containing no // symlinks. func BindMount(sourceDir, targetDir string, flags uint) error { // This function only attempts to handle bind mounts. Expanding to other // mounts will require examining do_mount() from fs/namespace.c of the // kernel that called functions (eventually) verify `DCACHE_CANT_MOUNT` is // not set (eg, by calling lock_mount()). if flags&syscall.MS_BIND == 0 { return fmt.Errorf("cannot perform non-bind mount operation") } // The kernel doesn't support recursively switching a tree of bind mounts // to read only, and we haven't written a work around. if flags&syscall.MS_RDONLY != 0 && flags&syscall.MS_REC != 0 { return fmt.Errorf("cannot use MS_RDONLY and MS_REC together") } // Step 1: acquire file descriptors representing the source and destination // directories, ensuring no symlinks are followed. sourceFd, err := OpenPath(sourceDir) if err != nil { return err } defer sysClose(sourceFd) targetFd, err := OpenPath(targetDir) if err != nil { return err } defer sysClose(targetFd) // Step 2: perform a bind mount between the paths identified by the two // file descriptors. We primarily care about privilege escalation here and // trying to race the sysMount() by removing any part of the dir (sourceDir // or targetDir) after we have an open file descriptor to it (sourceFd or // targetFd) to then replace an element of the dir's path with a symlink // will cause the fd path (ie, sourceFdPath or targetFdPath) to be marked // as unmountable within the kernel (this path is also changed to show as // '(deleted)'). Alternatively, simply renaming the dir (sourceDir or // targetDir) after we have an open file descriptor to it (sourceFd or // targetFd) causes the mount to happen with the newly renamed path, but // this rename is controlled by DAC so while the user could race the mount // source or target, this rename can't be used to gain privileged access to // files. For systems with AppArmor enabled, this raced rename would be // denied by the per-snap snap-update-ns AppArmor profle. sourceFdPath := fmt.Sprintf("/proc/self/fd/%d", sourceFd) targetFdPath := fmt.Sprintf("/proc/self/fd/%d", targetFd) bindFlags := syscall.MS_BIND | (flags & syscall.MS_REC) if err := sysMount(sourceFdPath, targetFdPath, "", uintptr(bindFlags), ""); err != nil { return err } // Step 3: optionally change to readonly if flags&syscall.MS_RDONLY != 0 { // We need to look up the target directory a second time, because // targetFd refers to the path shadowed by the mount point. mountFd, err := OpenPath(targetDir) if err != nil { // FIXME: the mount occurred, but the user moved the target // somewhere return err } defer sysClose(mountFd) mountFdPath := fmt.Sprintf("/proc/self/fd/%d", mountFd) remountFlags := syscall.MS_REMOUNT | syscall.MS_BIND | syscall.MS_RDONLY if err := sysMount("none", mountFdPath, "", uintptr(remountFlags), ""); err != nil { sysUnmount(mountFdPath, syscall.MNT_DETACH|umountNoFollow) return err } } return nil } snapd-2.37.4~14.04.1/cmd/snap-update-ns/bootstrap_test.go0000664000000000000000000001606713435556260017560 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main_test import ( . "gopkg.in/check.v1" update "github.com/snapcore/snapd/cmd/snap-update-ns" ) type bootstrapSuite struct{} var _ = Suite(&bootstrapSuite{}) // Check that ValidateSnapName rejects "/" and "..". func (s *bootstrapSuite) TestValidateInstanceName(c *C) { validNames := []string{ "aa", "aa_a", "hello-world", "a123456789012345678901234567890123456789", "a123456789012345678901234567890123456789_0123456789", "hello-world_foo", "foo_0123456789", "foo_1234abcd", "a123456789012345678901234567890123456789", "a123456789012345678901234567890123456789_0123456789", } for _, name := range validNames { c.Check(update.ValidateInstanceName(name), Equals, 0, Commentf("name %q should be valid but is not", name)) } invalidNames := []string{ "", "a", "a_a", "a123456789012345678901234567890123456789_01234567890", "hello/world", "hello..world", "INVALID", "-invalid", "hello-world_", "_foo", "foo_01234567890", "foo_123_456", "foo__456", "foo_", "hello-world_foo_foo", "foo01234567890012345678900123456789001234567890", "foo01234567890012345678900123456789001234567890_foo", "a123456789012345678901234567890123456789_0123456789_", } for _, name := range invalidNames { c.Check(update.ValidateInstanceName(name), Equals, -1, Commentf("name %q should be invalid but is valid", name)) } } // Test various cases of command line handling. func (s *bootstrapSuite) TestProcessArguments(c *C) { cases := []struct { cmdline []string snapName string shouldSetNs bool userFstab bool uid uint errPattern string }{ // Corrupted buffer is dealt with. {[]string{}, "", false, false, 0, "argv0 is corrupted"}, // When testing real bootstrap is identified and disabled. {[]string{"argv0.test"}, "", false, false, 0, "bootstrap is not enabled while testing"}, // Snap name is mandatory. {[]string{"argv0"}, "", false, false, 0, "snap name not provided"}, // Snap name is parsed correctly. {[]string{"argv0", "snapname"}, "snapname", true, false, 0, ""}, {[]string{"argv0", "snapname_instance"}, "snapname_instance", true, false, 0, ""}, // Onlye one snap name is allowed. {[]string{"argv0", "snapone", "snaptwo"}, "", false, false, 0, "too many positional arguments"}, // Snap name is validated correctly. {[]string{"argv0", ""}, "", false, false, 0, "snap name must contain at least one letter"}, {[]string{"argv0", "in--valid"}, "", false, false, 0, "snap name cannot contain two consecutive dashes"}, {[]string{"argv0", "invalid-"}, "", false, false, 0, "snap name cannot end with a dash"}, {[]string{"argv0", "@invalid"}, "", false, false, 0, "snap name must use lower case letters, digits or dashes"}, {[]string{"argv0", "INVALID"}, "", false, false, 0, "snap name must use lower case letters, digits or dashes"}, {[]string{"argv0", "foo_01234567890"}, "", false, false, 0, "instance key must be shorter than 10 characters"}, {[]string{"argv0", "foo_0123456_2"}, "", false, false, 0, "snap instance name can contain only one underscore"}, // The option --from-snap-confine disables setns. {[]string{"argv0", "--from-snap-confine", "snapname"}, "snapname", false, false, 0, ""}, {[]string{"argv0", "snapname", "--from-snap-confine"}, "snapname", false, false, 0, ""}, // The option --user-mounts switches to the real uid {[]string{"argv0", "--user-mounts", "snapname"}, "snapname", false, true, 0, ""}, // Unknown options are reported. {[]string{"argv0", "-invalid"}, "", false, false, 0, "unsupported option"}, {[]string{"argv0", "--option"}, "", false, false, 0, "unsupported option"}, {[]string{"argv0", "--from-snap-confine", "-invalid", "snapname"}, "", false, false, 0, "unsupported option"}, // The -u option can be used to specify the user id. {[]string{"argv0", "snapname", "-u", "1234"}, "snapname", true, true, 1234, ""}, {[]string{"argv0", "-u", "1234", "snapname"}, "snapname", true, true, 1234, ""}, /* Empty user id is rejected. */ {[]string{"argv0", "-u", "", "snapname"}, "", false, false, 0, "cannot parse user id"}, /* Partially parsed values are rejected. */ {[]string{"argv0", "-u", "1foo", "snapname"}, "", false, false, 0, "cannot parse user id"}, /* Hexadecimal values are rejected. */ {[]string{"argv0", "-u", "0x16", "snapname"}, "", false, false, 0, "cannot parse user id"}, {[]string{"argv0", "-u", " 0x16", "snapname"}, "", false, false, 0, "cannot parse user id"}, {[]string{"argv0", "-u", "0x16 ", "snapname"}, "", false, false, 0, "cannot parse user id"}, {[]string{"argv0", "-u", " 0x16 ", "snapname"}, "", false, false, 0, "cannot parse user id"}, /* Octal-looking values are parsed as decimal. */ {[]string{"argv0", "-u", "042", "snapname"}, "snapname", true, true, 42, ""}, /* Spaces around octal values is rejected. */ {[]string{"argv0", "-u", " 042", "snapname"}, "", false, false, 0, "cannot parse user id"}, {[]string{"argv0", "-u", "042 ", "snapname"}, "", false, false, 0, "cannot parse user id"}, {[]string{"argv0", "-u", " 042 ", "snapname"}, "", false, false, 0, "cannot parse user id"}, /* Space around the value is rejected. */ {[]string{"argv0", "-u", "42 ", "snapname"}, "", false, false, 0, "cannot parse user id"}, {[]string{"argv0", "-u", " 42", "snapname"}, "", false, false, 0, "cannot parse user id"}, {[]string{"argv0", "-u", " 42 ", "snapname"}, "", false, false, 0, "cannot parse user id"}, {[]string{"argv0", "-u", "\n42 ", "snapname"}, "", false, false, 0, "cannot parse user id"}, {[]string{"argv0", "-u", "42\t", "snapname"}, "", false, false, 0, "cannot parse user id"}, /* Negative values are rejected. */ {[]string{"argv0", "-u", "-1", "snapname"}, "", false, false, 0, "user id cannot be negative"}, /* The option -u requires an argument. */ {[]string{"argv0", "snapname", "-u"}, "", false, false, 0, "-u requires an argument"}, } for _, tc := range cases { update.ClearBootstrapError() snapName, shouldSetNs, userFstab, uid := update.ProcessArguments(tc.cmdline) err := update.BootstrapError() comment := Commentf("failed with cmdline %q, expected error pattern %q, actual error %q", tc.cmdline, tc.errPattern, err) if tc.errPattern != "" { c.Assert(err, ErrorMatches, tc.errPattern, comment) } else { c.Assert(err, IsNil, comment) } c.Check(snapName, Equals, tc.snapName, comment) c.Check(shouldSetNs, Equals, tc.shouldSetNs, comment) c.Check(userFstab, Equals, tc.userFstab, comment) c.Check(uid, Equals, tc.uid, comment) } } snapd-2.37.4~14.04.1/cmd/snap-update-ns/freezer_test.go0000664000000000000000000000614513435556260017201 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main_test import ( "fmt" "os" "path/filepath" . "gopkg.in/check.v1" update "github.com/snapcore/snapd/cmd/snap-update-ns" "github.com/snapcore/snapd/testutil" ) type freezerSuite struct{} var _ = Suite(&freezerSuite{}) func (s *freezerSuite) TestFreezeSnapProcesses(c *C) { restore := update.MockFreezerCgroupDir(c) defer restore() n := "foo" // snap name p := filepath.Join(update.FreezerCgroupDir(), fmt.Sprintf("snap.%s", n)) // snap freezer cgroup f := filepath.Join(p, "freezer.state") // freezer.state file of the cgroup // When the freezer cgroup filesystem doesn't exist we do nothing at all. c.Assert(update.FreezeSnapProcesses(n), IsNil) _, err := os.Stat(f) c.Assert(os.IsNotExist(err), Equals, true) // When the freezer cgroup filesystem exists but the particular cgroup // doesn't exist we don nothing at all. c.Assert(os.MkdirAll(update.FreezerCgroupDir(), 0755), IsNil) c.Assert(update.FreezeSnapProcesses(n), IsNil) _, err = os.Stat(f) c.Assert(os.IsNotExist(err), Equals, true) // When the cgroup exists we write FROZEN the freezer.state file. c.Assert(os.MkdirAll(p, 0755), IsNil) c.Assert(update.FreezeSnapProcesses(n), IsNil) _, err = os.Stat(f) c.Assert(err, IsNil) c.Assert(f, testutil.FileEquals, `FROZEN`) } func (s *freezerSuite) TestThawSnapProcesses(c *C) { restore := update.MockFreezerCgroupDir(c) defer restore() n := "foo" // snap name p := filepath.Join(update.FreezerCgroupDir(), fmt.Sprintf("snap.%s", n)) // snap freezer cgroup f := filepath.Join(p, "freezer.state") // freezer.state file of the cgroup // When the freezer cgroup filesystem doesn't exist we do nothing at all. c.Assert(update.ThawSnapProcesses(n), IsNil) _, err := os.Stat(f) c.Assert(os.IsNotExist(err), Equals, true) // When the freezer cgroup filesystem exists but the particular cgroup // doesn't exist we don nothing at all. c.Assert(os.MkdirAll(update.FreezerCgroupDir(), 0755), IsNil) c.Assert(update.ThawSnapProcesses(n), IsNil) _, err = os.Stat(f) c.Assert(os.IsNotExist(err), Equals, true) // When the cgroup exists we write THAWED the freezer.state file. c.Assert(os.MkdirAll(p, 0755), IsNil) c.Assert(update.ThawSnapProcesses(n), IsNil) _, err = os.Stat(f) c.Assert(err, IsNil) c.Assert(f, testutil.FileEquals, `THAWED`) } snapd-2.37.4~14.04.1/cmd/snap-update-ns/sorting.go0000664000000000000000000000321113435556260016154 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main import ( "strings" "github.com/snapcore/snapd/osutil" ) // byOriginAndMagicDir allows sorting an array of entries by the source of mount // entry (overname, layout, content) and lexically by mount point name. // Automagically adds a trailing slash to paths. type byOriginAndMagicDir []osutil.MountEntry func (c byOriginAndMagicDir) Len() int { return len(c) } func (c byOriginAndMagicDir) Swap(i, j int) { c[i], c[j] = c[j], c[i] } func (c byOriginAndMagicDir) Less(i, j int) bool { iMe := c[i] jMe := c[j] iOrigin := iMe.XSnapdOrigin() jOrigin := jMe.XSnapdOrigin() if iOrigin == "overname" && iOrigin != jOrigin { // should ith element be created by 'overname' mapping, it is // always sorted before jth element, if that one comes from // layouts or content interface return true } iDir := c[i].Dir jDir := c[j].Dir if !strings.HasSuffix(iDir, "/") { iDir = iDir + "/" } if !strings.HasSuffix(jDir, "/") { jDir = jDir + "/" } return iDir < jDir } snapd-2.37.4~14.04.1/cmd/snap-update-ns/export_test.go0000664000000000000000000001116613435556260017057 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main import ( "os" "syscall" . "gopkg.in/check.v1" "github.com/snapcore/snapd/osutil/sys" ) var ( // change ValidateInstanceName = validateInstanceName ProcessArguments = processArguments // freezer FreezeSnapProcesses = freezeSnapProcesses ThawSnapProcesses = thawSnapProcesses // utils PlanWritableMimic = planWritableMimic ExecWritableMimic = execWritableMimic // main ComputeAndSaveChanges = computeAndSaveChanges ApplyUserFstab = applyUserFstab // bootstrap ClearBootstrapError = clearBootstrapError // trespassing IsReadOnly = isReadOnly IsPrivateTmpfsCreatedBySnapd = isPrivateTmpfsCreatedBySnapd ) // SystemCalls encapsulates various system interactions performed by this module. type SystemCalls interface { OsLstat(name string) (os.FileInfo, error) SysLstat(name string, buf *syscall.Stat_t) error ReadDir(dirname string) ([]os.FileInfo, error) Symlinkat(oldname string, dirfd int, newname string) error Readlinkat(dirfd int, path string, buf []byte) (int, error) Remove(name string) error Close(fd int) error Fchdir(fd int) error Fchown(fd int, uid sys.UserID, gid sys.GroupID) error Mkdirat(dirfd int, path string, mode uint32) error Mount(source string, target string, fstype string, flags uintptr, data string) (err error) Open(path string, flags int, mode uint32) (fd int, err error) Openat(dirfd int, path string, flags int, mode uint32) (fd int, err error) Unmount(target string, flags int) error Fstat(fd int, buf *syscall.Stat_t) error Fstatfs(fd int, buf *syscall.Statfs_t) error } // MockSystemCalls replaces real system calls with those of the argument. func MockSystemCalls(sc SystemCalls) (restore func()) { // save oldOsLstat := osLstat oldRemove := osRemove oldIoutilReadDir := ioutilReadDir oldSysClose := sysClose oldSysFchown := sysFchown oldSysMkdirat := sysMkdirat oldSysMount := sysMount oldSysOpen := sysOpen oldSysOpenat := sysOpenat oldSysUnmount := sysUnmount oldSysSymlinkat := sysSymlinkat oldReadlinkat := sysReadlinkat oldFstat := sysFstat oldFstatfs := sysFstatfs oldSysFchdir := sysFchdir oldSysLstat := sysLstat // override osLstat = sc.OsLstat osRemove = sc.Remove ioutilReadDir = sc.ReadDir sysClose = sc.Close sysFchown = sc.Fchown sysMkdirat = sc.Mkdirat sysMount = sc.Mount sysOpen = sc.Open sysOpenat = sc.Openat sysUnmount = sc.Unmount sysSymlinkat = sc.Symlinkat sysReadlinkat = sc.Readlinkat sysFstat = sc.Fstat sysFstatfs = sc.Fstatfs sysFchdir = sc.Fchdir sysLstat = sc.SysLstat return func() { // restore osLstat = oldOsLstat osRemove = oldRemove ioutilReadDir = oldIoutilReadDir sysClose = oldSysClose sysFchown = oldSysFchown sysMkdirat = oldSysMkdirat sysMount = oldSysMount sysOpen = oldSysOpen sysOpenat = oldSysOpenat sysUnmount = oldSysUnmount sysSymlinkat = oldSysSymlinkat sysReadlinkat = oldReadlinkat sysFstat = oldFstat sysFstatfs = oldFstatfs sysFchdir = oldSysFchdir sysLstat = oldSysLstat } } func MockFreezerCgroupDir(c *C) (restore func()) { old := freezerCgroupDir freezerCgroupDir = c.MkDir() return func() { freezerCgroupDir = old } } func FreezerCgroupDir() string { return freezerCgroupDir } func MockChangePerform(f func(chg *Change, as *Assumptions) ([]*Change, error)) func() { origChangePerform := changePerform changePerform = f return func() { changePerform = origChangePerform } } func MockReadDir(fn func(string) ([]os.FileInfo, error)) (restore func()) { old := ioutilReadDir ioutilReadDir = fn return func() { ioutilReadDir = old } } func MockReadlink(fn func(string) (string, error)) (restore func()) { old := osReadlink osReadlink = fn return func() { osReadlink = old } } func (as *Assumptions) IsRestricted(path string) bool { return as.isRestricted(path) } func (as *Assumptions) PastChanges() []*Change { return as.pastChanges } func (as *Assumptions) CanWriteToDirectory(dirFd int, dirName string) (bool, error) { return as.canWriteToDirectory(dirFd, dirName) } snapd-2.37.4~14.04.1/cmd/snap-repair/0000775000000000000000000000000013435556260013525 5ustar snapd-2.37.4~14.04.1/cmd/snap-repair/cmd_show.go0000664000000000000000000000423113435556260015657 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main import ( "fmt" "io" "strings" ) func init() { const ( short = "Shows specific repairs run on this device" long = "" ) if _, err := parser.AddCommand("show", short, long, &cmdShow{}); err != nil { panic(err) } } type cmdShow struct { Positional struct { Repair []string `positional-arg-name:""` } `positional-args:"yes"` } func showRepairDetails(w io.Writer, repair string) error { i := strings.LastIndex(repair, "-") if i < 0 { return fmt.Errorf("cannot parse repair %q", repair) } brand := repair[:i] seq := repair[i+1:] repairTraces, err := newRepairTraces(brand, seq) if err != nil { return err } if len(repairTraces) == 0 { return fmt.Errorf("cannot find repair \"%s-%s\"", brand, seq) } for _, trace := range repairTraces { fmt.Fprintf(w, "repair: %s\n", trace.Repair()) fmt.Fprintf(w, "revision: %s\n", trace.Revision()) fmt.Fprintf(w, "status: %s\n", trace.Status()) fmt.Fprintf(w, "summary: %s\n", trace.Summary()) fmt.Fprintf(w, "script:\n") if err := trace.WriteScriptIndented(w, 2); err != nil { fmt.Fprintf(w, "%serror: %s\n", indentPrefix(2), err) } fmt.Fprintf(w, "output:\n") if err := trace.WriteOutputIndented(w, 2); err != nil { fmt.Fprintf(w, "%serror: %s\n", indentPrefix(2), err) } } return nil } func (c *cmdShow) Execute([]string) error { for _, repair := range c.Positional.Repair { if err := showRepairDetails(Stdout, repair); err != nil { return err } fmt.Fprintf(Stdout, "\n") } return nil } snapd-2.37.4~14.04.1/cmd/snap-repair/trace.go0000664000000000000000000001106313435556260015153 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main import ( "bufio" "fmt" "io" "os" "path/filepath" "regexp" "strconv" "strings" "github.com/snapcore/snapd/dirs" ) // newRepairTraces returns all repairTrace about the given "brand" and "seq" // that can be found. brand, seq can be filepath.Glob expressions. func newRepairTraces(brand, seq string) ([]*repairTrace, error) { matches, err := filepath.Glob(filepath.Join(dirs.SnapRepairRunDir, brand, seq, "*")) if err != nil { return nil, err } var repairTraces []*repairTrace for _, match := range matches { if trace := newRepairTraceFromPath(match); trace != nil { repairTraces = append(repairTraces, trace) } } return repairTraces, nil } // repairTrace holds information about a repair that was run. type repairTrace struct { path string } // validRepairTraceName checks that the given name looks like a valid repair // trace var validRepairTraceName = regexp.MustCompile(`^r[0-9]+\.(done|skip|retry|running)$`) // newRepairTraceFromPath takes a repair log path like // the path /var/lib/snapd/repair/run/my-brand/1/r2.done // and contructs a repair log from that. func newRepairTraceFromPath(path string) *repairTrace { rt := &repairTrace{path: path} if !validRepairTraceName.MatchString(filepath.Base(path)) { return nil } return rt } // Repair returns the repair human readable string in the form $brand-$id func (rt *repairTrace) Repair() string { seq := filepath.Base(filepath.Dir(rt.path)) brand := filepath.Base(filepath.Dir(filepath.Dir(rt.path))) return fmt.Sprintf("%s-%s", brand, seq) } // Revision returns the revision of the repair func (rt *repairTrace) Revision() string { rev, err := revFromFilepath(rt.path) if err != nil { // this can never happen because we check that path starts // with the right prefix. However handle the case just in // case. return "-" } return rev } // Summary returns the summary of the repair that was run func (rt *repairTrace) Summary() string { f, err := os.Open(rt.path) if err != nil { return "-" } defer f.Close() needle := "summary: " scanner := bufio.NewScanner(f) for scanner.Scan() { s := scanner.Text() if strings.HasPrefix(s, needle) { return s[len(needle):] } } return "-" } // Status returns the status of the given repair {done,skip,retry,running} func (rt *repairTrace) Status() string { return filepath.Ext(rt.path)[1:] } func indentPrefix(level int) string { return strings.Repeat(" ", level) } // WriteScriptIndented outputs the script that produced this repair output // to the given writer w with the indent level given by indent. func (rt *repairTrace) WriteScriptIndented(w io.Writer, indent int) error { scriptPath := rt.path[:strings.LastIndex(rt.path, ".")] + ".script" f, err := os.Open(scriptPath) if err != nil { return err } defer f.Close() scanner := bufio.NewScanner(f) for scanner.Scan() { fmt.Fprintf(w, "%s%s\n", indentPrefix(indent), scanner.Text()) } if scanner.Err() != nil { return scanner.Err() } return nil } // WriteOutputIndented outputs the repair output to the given writer w // with the indent level given by indent. func (rt *repairTrace) WriteOutputIndented(w io.Writer, indent int) error { f, err := os.Open(rt.path) if err != nil { return err } defer f.Close() scanner := bufio.NewScanner(f) // move forward in the log to where the actual script output starts for scanner.Scan() { if scanner.Text() == "output:" { break } } // write the script output to w for scanner.Scan() { fmt.Fprintf(w, "%s%s\n", indentPrefix(indent), scanner.Text()) } if scanner.Err() != nil { return scanner.Err() } return nil } // revFromFilepath is a helper that extracts the revision number from the // filename of the repairTrace func revFromFilepath(name string) (string, error) { var rev int if _, err := fmt.Sscanf(filepath.Base(name), "r%d.", &rev); err == nil { return strconv.Itoa(rev), nil } return "", fmt.Errorf("cannot find revision in %q", name) } snapd-2.37.4~14.04.1/cmd/snap-repair/cmd_done_retry_skip_test.go0000664000000000000000000000435513435556260021145 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main_test import ( "io/ioutil" "os" "strconv" "syscall" . "gopkg.in/check.v1" repair "github.com/snapcore/snapd/cmd/snap-repair" ) func (r *repairSuite) TestStatusNoStatusFdEnv(c *C) { for _, s := range []string{"done", "skip", "retry"} { err := repair.ParseArgs([]string{s}) c.Check(err, ErrorMatches, "cannot find SNAP_REPAIR_STATUS_FD environment") } } func (r *repairSuite) TestStatusBadStatusFD(c *C) { for _, s := range []string{"done", "skip", "retry"} { os.Setenv("SNAP_REPAIR_STATUS_FD", "123456789") defer os.Unsetenv("SNAP_REPAIR_STATUS_FD") err := repair.ParseArgs([]string{s}) c.Check(err, ErrorMatches, `write : bad file descriptor`) } } func (r *repairSuite) TestStatusUnparsableStatusFD(c *C) { for _, s := range []string{"done", "skip", "retry"} { os.Setenv("SNAP_REPAIR_STATUS_FD", "xxx") defer os.Unsetenv("SNAP_REPAIR_STATUS_FD") err := repair.ParseArgs([]string{s}) c.Check(err, ErrorMatches, `cannot parse SNAP_REPAIR_STATUS_FD environment: strconv.*: parsing "xxx": invalid syntax`) } } func (r *repairSuite) TestStatusHappy(c *C) { for _, s := range []string{"done", "skip", "retry"} { rp, wp, err := os.Pipe() c.Assert(err, IsNil) defer rp.Close() defer wp.Close() fd, e := syscall.Dup(int(wp.Fd())) c.Assert(e, IsNil) wp.Close() os.Setenv("SNAP_REPAIR_STATUS_FD", strconv.Itoa(fd)) defer os.Unsetenv("SNAP_REPAIR_STATUS_FD") err = repair.ParseArgs([]string{s}) c.Check(err, IsNil) status, err := ioutil.ReadAll(rp) c.Assert(err, IsNil) c.Check(string(status), Equals, s+"\n") } } snapd-2.37.4~14.04.1/cmd/snap-repair/cmd_run_test.go0000664000000000000000000000426713435556260016553 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main_test import ( "os" "path/filepath" . "gopkg.in/check.v1" "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/asserts/sysdb" repair "github.com/snapcore/snapd/cmd/snap-repair" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/release" ) func (r *repairSuite) TestRun(c *C) { defer release.MockOnClassic(false)() r1 := sysdb.InjectTrusted(r.storeSigning.Trusted) defer r1() r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{r.repairRootAcctKey}) defer r2() r.freshState(c) const script = `#!/bin/sh echo "happy output" echo "done" >&$SNAP_REPAIR_STATUS_FD exit 0 ` seqRepairs := r.signSeqRepairs(c, []string{makeMockRepair(script)}) mockServer := makeMockServer(c, &seqRepairs, false) defer mockServer.Close() repair.MockBaseURL(mockServer.URL) origArgs := os.Args defer func() { os.Args = origArgs }() os.Args = []string{"snap-repair", "run"} err := repair.Run() c.Check(err, IsNil) c.Check(r.Stdout(), HasLen, 0) c.Check(osutil.FileExists(filepath.Join(dirs.SnapRepairRunDir, "canonical", "1", "r0.done")), Equals, true) } func (r *repairSuite) TestRunAlreadyLocked(c *C) { err := os.MkdirAll(dirs.SnapRunRepairDir, 0700) c.Assert(err, IsNil) flock, err := osutil.NewFileLock(filepath.Join(dirs.SnapRunRepairDir, "lock")) c.Assert(err, IsNil) err = flock.Lock() c.Assert(err, IsNil) defer flock.Unlock() err = repair.ParseArgs([]string{"run"}) c.Check(err, ErrorMatches, `cannot run, another snap-repair run already executing`) } snapd-2.37.4~14.04.1/cmd/snap-repair/cmd_list.go0000664000000000000000000000327313435556260015657 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main import ( "fmt" "text/tabwriter" ) func init() { const ( short = "Lists repairs run on this device" long = "" ) if _, err := parser.AddCommand("list", short, long, &cmdList{}); err != nil { panic(err) } } type cmdList struct{} func (c *cmdList) Execute([]string) error { w := tabwriter.NewWriter(Stdout, 5, 3, 2, ' ', 0) defer w.Flush() // FIXME: this will not currently list the repairs that are // skipped because of e.g. wrong architecture // directory structure is: // var/lib/snapd/run/repairs/ // canonical/ // 1/ // r0.retry // r0.script // r1.done // r1.script // 2/ // r3.done // r3.script repairTraces, err := newRepairTraces("*", "*") if err != nil { return err } if len(repairTraces) == 0 { fmt.Fprintf(Stderr, "no repairs yet\n") return nil } fmt.Fprintf(w, "Repair\tRev\tStatus\tSummary\n") for _, t := range repairTraces { fmt.Fprintf(w, "%s\t%v\t%s\t%s\n", t.Repair(), t.Revision(), t.Status(), t.Summary()) } return nil } snapd-2.37.4~14.04.1/cmd/snap-repair/cmd_run.go0000664000000000000000000000414513435556260015507 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main import ( "fmt" "net/url" "os" "path/filepath" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil" ) func init() { const ( short = "Fetch and run repair assertions as necessary for the device" long = "" ) if _, err := parser.AddCommand("run", short, long, &cmdRun{}); err != nil { panic(err) } } type cmdRun struct{} var baseURL *url.URL func init() { var baseurl string if osutil.GetenvBool("SNAPPY_USE_STAGING_STORE") { baseurl = "https://api.staging.snapcraft.io/v2/" } else { baseurl = "https://api.snapcraft.io/v2/" } var err error baseURL, err = url.Parse(baseurl) if err != nil { panic(fmt.Sprintf("cannot setup base url: %v", err)) } } func (c *cmdRun) Execute(args []string) error { if err := os.MkdirAll(dirs.SnapRunRepairDir, 0755); err != nil { return err } flock, err := osutil.NewFileLock(filepath.Join(dirs.SnapRunRepairDir, "lock")) if err != nil { return err } err = flock.TryLock() if err == osutil.ErrAlreadyLocked { return fmt.Errorf("cannot run, another snap-repair run already executing") } if err != nil { return err } defer flock.Unlock() run := NewRunner() run.BaseURL = baseURL err = run.LoadState() if err != nil { return err } for { repair, err := run.Next("canonical") if err == ErrRepairNotFound { // no more repairs break } if err != nil { return err } if err := repair.Run(); err != nil { return err } } return nil } snapd-2.37.4~14.04.1/cmd/snap-repair/trusted.go0000664000000000000000000001031613435556260015547 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main import ( "fmt" "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/osutil" ) const ( encodedRepairRootAccountKey = `type: account-key authority-id: canonical public-key-sha3-384: nttW6NfBXI_E-00u38W-KH6eiksfQNXuI7IiumoV49_zkbhM0sYTzSnFlwZC-W4t account-id: canonical name: repair-root since: 2017-07-07T00:00:00.0Z body-length: 1406 sign-key-sha3-384: -CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswHNiEB9Lxk AcbDTQRWhcGAASAAtlCIEilQCQh9Ffjn9IxD+5FtWfdKdJHrQdtFPy/2Q1kOvC++ef/3bG1xMwao tue9K0HCMtv3apHS1C32Y8JBMD8oykRpAd5H05+sgzZr3kCHIvgogKFsXfdd5+W5Q1+59Vy/81UH tJCs99wBwboNh/pMCXBGI3jDRN1f7hOxcHUIW+KTaHCVZnrXXmCn6Oe6brR9qiUXgEB2I6rBT/Fe cumdfvFN/zSsJ3Vvv9IbTfHYAZD82NrSqz4UZ3WJarIaxlykgLJaZN4bqSQYPYsc8lLlwjQeGloW +r8dIypKOzPnUYurzWcNzcCnNCT1zhpY/IK2rFcZbN5/mP2/5PtjFlbX88aPGPbOTqYANmxfboCx wo4D4aS7PD6gLC7XM8bgh8BACpmG3BnskL7F/9IHMl85SUHFIya2fDu7A7HqNUn7cpENGbHojj7G J2s2965FSRuIvp69wEmknYD/kahjT1+Vy94D2rVB7mjtTruPueF2KTpo2jRXFM+ABq+T9ybjXD6f UuSXu5xeg0Cv1sxOh4O4b45uaCXb8B74chEUW+cb3cV0NGE/QgBJUBeS68vUI8lqQFmPInci6Md4 oiKFVbloL0ZmOGj73Xv2uAcexAK9bEiI+adVS2x9r4eFwtkST3XG0t/kw7eLgAVjtRcpmD6EuZ0Q ulAJHEsl7Sazm8GRU4GtZWaCajVb4n5TS1lin2nqUXwqxRUA3smLqkQoYXQni9vmhez4dEP4MVvq /0IdI50UGME5Fzc8hUhYzvbNS8g+VOeAK/qj3dzcdF9+n940AQI16Fcxi1/xFk8A4dw3AaDl4XnJ piyStE+xi95ne5HJW8r/f/JQos8I6QR5w7pe2URbgUdVPgQLv3r/4dS/X3aP+oakrPR7JuAVdP62 vsjF4jK8Bl69mcF434xpshnbnW/f7XHomPY4gp8y7kD2/DdEs5hvaTHIPp25DEYhqjt3gfmMuUXi Mb5oy9KZp3ff8Squ+XNWSGQSyhX14xcQwM8QjNQnAisNg2hYwSM2n8q5IDWiwJQkFSriP5tMsa8E DMGI3LXUZKRJll9dQBjs6VzApT4/Ee0ZvSni0d2cWm3wkqFQudRpqz3JSwQ7jal0W5e0UhNeHh/W 7nACD5hvcwF7UgUz0r8adlOy+nyfvWte65nbcRrIH7GS1xdgS0e9eW4znsplp7s/Z3gMhi8CN5TY 0nZW82TTl69Wvn13SGJye0yJSjiy4KS0iRE6BwAt7dGAMs5c62IlBsWEHLmCW1/lixWA9YXT9iji G7DKSoofnsvqVP2wIQZxxt4xHMjUGXecyx8QX4BznwsV1vbzHOIG4a3Z9A1X1L3yh3ZbazFVeEE9 7Dhz9hGYfd3PvwARAQAB AcLDXAQAAQoABgUCWbuO2gAKCRDUpVvql9g3IOPcIADZWObdYMKh2SblWXxUchnc3S4LDRtL8v+Q HdnXO5+dJmsj5LWhdsB7lz373aaylFTwHpNDWcDdAu7ulP0vr7zJURo1jGOo7VojSEeuAAu3YhwL 2pR0p5Me0wuxl/pCX0x0nfDSeeTw11kproyN0GwJaErKEmyQyfOgVr2jN5sl1gBqQtKgG5gqZzC3 oFH1HYGPl2kfAorxFw7MoPy4aRFaxUJfx4x6bEktgkkFT7AWGmawVwcpiiUbbpe9CPLEsn6yqJI9 5XmQ3dJjp/6Y5D7x04LRH3Q5fajRcpdBrC0tDsT9UDbSRtIyo0KDNVHwQalLa2Sv51DV+Fy4eneM Lgu+oCUOnBecXIWnX+k0uyDW8aLHYapx8etpW3pln/hMRd8JxYVYAqDn7G2AYeSGS/4lzCJzysW2 2/4RhH9Ql8ea0nSWVTJr3pmXKlPSH/OOy9IADEDUuEdvyMcq3YOXA9E4L3g9vR31JH+++swcTQPz rnGx0mE+TCQRWok/NZ1QNv4eNZlnLXdNS1DoV/kRqU04cblYYUoSO34mkjPEJ8ti+VzKh/PTA6/7 1feFX276Zam/6b2rBLWCWYdblDM9oLAR4PfzntBZW4LzzOIb95IwiK4JoDoBr3x4+RxvxgVQLvHt 8990uQ0se9+8BtLVFtd07NbldHXRBlZkq22a8CeVYrU3YZEtuEBvlGDpkdegw/vcvgHUUK1f8dXJ 0+9oW2yQOLAguuPZ67GFSgdTnvR5dQYZZR2EbQJlPMOFA3loKeUxHSR9w7E3SFqXGqN1v6APDK0V lpVFq+rYlprvbf4GB0oR8yQOGtlxf+Ag3Nnx+90nlmtTK4k7tQpLzuAXGGREDCtn0y/YvWvGt6kN EV5Q/mAVe2/CtAUvfyX7V3xlzYCrJT9DBcCBMaUUekFrwvZi13WYJIn7YE2Qmam7ZsXdb991PoFv +c6Pmeg6w3y7D+Vj4Yfi8IrjPrc6765DaaZxFyMia9GEQKHChZDkiEiAM6RfwlC5YXGzCroaZi0Y Knf/UkUWoa/jKZgQNiqrZ9oGmbURLeXkkHzpcFitwjzWr6tNScCzNIqs/uxTxbFM8fJu1gSmauEY TE1rn62SiuHNRKJqfLcCHucStK10knHkHTAJ3avS7rBz0Dy8UOa77bOjyei5n2rkyXztL2YjjGYh 8jEt00xcvwJGePBfH10gCgTFWdfhfcP9/muKgiOSErQlHPypnr4vqO0PU9XDp106FFWyyNPd95kC l5IF9WMfl7YHpT0Ph7kBYwg9sKF/7oCVdbT5CoImxkE5DTkWB8xX6W/BhuMrp1rzTHFFGVd1ppb7 EMUll4dd78OWonMlIgsMRuTSn93awb4X8xSJhRi9 ` ) func init() { repairRootAccountKey, err := asserts.Decode([]byte(encodedRepairRootAccountKey)) if err != nil { panic(fmt.Sprintf("cannot decode trusted account-key: %v", err)) } if !osutil.GetenvBool("SNAPPY_USE_STAGING_STORE") { trustedRepairRootKeys = append(trustedRepairRootKeys, repairRootAccountKey.(*asserts.AccountKey)) } } snapd-2.37.4~14.04.1/cmd/snap-repair/cmd_done_retry_skip.go0000664000000000000000000000365613435556260020111 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main import ( "fmt" "os" "strconv" ) func init() { cmd, err := parser.AddCommand("done", "Signal repair is done", "", &cmdDone{}) if err != nil { panic(err) } cmd.Hidden = true cmd, err = parser.AddCommand("skip", "Signal repair should be skipped", "", &cmdSkip{}) if err != nil { panic(err) } cmd.Hidden = true cmd, err = parser.AddCommand("retry", "Signal repair must be retried next time", "", &cmdRetry{}) if err != nil { panic(err) } cmd.Hidden = true } func writeToStatusFD(msg string) error { statusFdStr := os.Getenv("SNAP_REPAIR_STATUS_FD") if statusFdStr == "" { return fmt.Errorf("cannot find SNAP_REPAIR_STATUS_FD environment") } fd, err := strconv.Atoi(statusFdStr) if err != nil { return fmt.Errorf("cannot parse SNAP_REPAIR_STATUS_FD environment: %s", err) } f := os.NewFile(uintptr(fd), "") defer f.Close() if _, err := f.Write([]byte(msg + "\n")); err != nil { return err } return nil } type cmdDone struct{} func (c *cmdDone) Execute(args []string) error { return writeToStatusFD("done") } type cmdSkip struct{} func (c *cmdSkip) Execute([]string) error { return writeToStatusFD("skip") } type cmdRetry struct{} func (c *cmdRetry) Execute([]string) error { return writeToStatusFD("retry") } snapd-2.37.4~14.04.1/cmd/snap-repair/staging.go0000664000000000000000000000712113435556260015511 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- // +build withtestkeys withstagingkeys /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main import ( "fmt" "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/osutil" ) const ( encodedStagingRepairRootAccountKey = `type: account-key authority-id: canonical public-key-sha3-384: 0GgXgD-RtfU0HJFBmaaiUQaNUFATl1oOlzJ44Bi4MbQAwcu8ektQLGBkKQ4JuA_O account-id: canonical name: repair-root since: 2017-07-07T00:00:00.0Z body-length: 1406 sign-key-sha3-384: e2r8on4LgdxQSaW5T8mBD5oC2fSTktTVxOYa5w3kIP4_nNF6L7mt6fOJShdOGkKu AcbDTQRWhcGAASAA0D+AqdqBtgiaABSZ++NdDKcvFtpmEphDfpEAAo4MAWucmE5yVN9mHFRXvwU0 ZkLQwPuiF5Vp5EP/kHyKgmmF9nKUBXnuZXfuH4vzH9ZuEfdxGc0On+XK4KwyPUzOXj2n1Rsxj0P5 06wJ6QFghi6nORx3Hz4pxZH7SRANgZudwWE53+whbkJyU/psv4RXfxPnu3YGo0qPk/wGCfV8kkkH UFmeqJJEk14EGI+Kv9WlIVAqctryqf9mSXkgnhp4lzdGpsRCmRcw7boVjdOieFCJv+gVs7i52Yxu YyiA09XF8j85DSMl4s/TyN4bm9649beBCpTJNyb3Ep6lydGiZck8ChJRLDXGP2XZtRKXsBMNIfwP 5qnLtX1/Sm1znag0grGbd3AUqL6ySAr42x8QIxZfqzk5DvQbF3xOiu2xzxTt1eB3069MlnFw99ui fLwlec7imbCiX3bryutCRhKgkJz4MbNsyHiW51k1l1IDbABey+gfVHpeBq/2aHK5qSy98dRBSYSj Ki++j8zR1ODequWy+OrF4cu6IQ35eunHQ2mRsJIE0xFGAjG3vCPJwzVoNS5m5R0ffncIUYdKxt/R W2mLo43qX0fquW5LvHyI14d3B3LYfKz05FmASJaE4A+/GQhM7kMCnmykro0MM6MU0sd2OruOZVVo z6GQ37Hyo/TGToyCr7qD/+tSq3dKYGyezl/Y4I589eqnc1DaMHL2ssiXDsbpSRHpnNqMUq6UNg4M NsUiDLaGsNJj1ft6A7jz+yoqJ74m3hlaQK1Rot8FXBkuJGoRKBahHbh/bfkGWChDvzY9ZpoUExY2 rp7tNYEP/LEAI/RQd03sBnqd3V8YhggT2n6QCC4ikLKvUTE3RY0qn5aAa7KC1wVi+7SfdeVl3RBF Jyb9GCYfRUv/bfFH/TCZ9WVN1v/GIMcjBFGJf7H3cz/deela53XSaecYHuvFRpVfzmx28UcR8UY4 5WqHfxnVQlY6DPv+kjzMzIEJGwgSAFc0d4wlSwS/Y1T0ednFRUyjMAxEUvE8tOLibtXw4q/srIFt OIgpd/xErcyi5Ddgt7EQoYo+rtVZ8x5EwR0+i7VAV+a3bnGSJW2LFEjt2RZUiMjohVZ4oOVuoDd2 VQzMFv41flbyqjgHhtJSCIOKDg9uI2FHbQ5vrX9qBooS68YkBALwCq+P7nSxDxFuS0CgrzSH35FX VneOl68U74pxRgdlPJ0HI92oilrbTH8Ft0m5SzNsy+9ZZZtIDFQW+lx/ApixyifARFnZ3C3Gdx59 FlFNbE75+X28joGtul2mPjJ1eI1dCwiFCF3R/rwfRmw3Wpv76re+EzVR1MJVCcTgC1lUoCJpKl1J n3PQLcR8J0iqswARAQAB AcLBXAQAAQoABgUCWYM7bQAKCRAHKljtl9kuLtCFD/4miBm0HyLE8GdboeUtWw+oOlH0AgabRqYi a1TpEJeYQIjnwDuCCPYtJxL1Rc+5dSNnbY9L+34NuaSyYMJY/FMuSS5iaNomGnj7YiAOds1+1/6h Z1bTm3ttZnphg5DxckYZLaKoYgRaOzAbiRM8l+2bDbXlq3KRxZ7o7D1V/xpPis8SWK57gQ7VppHI fcw5jnzWokWSowaKShimjJNCXMaeGdGJBLU1wcJC/XRf3tXSZecwMfL9CN/G8b17HvIFN/Pe3oS9 QxYMQ0p3J3PF3F19Iow0VHi78hPKtVmJb5igwzBlGYFW7zZ3R35nJ7Iv6VW58G2HDDGMdBfZp930 FbLb3mj8Yw3S5fcMZ09vpT7PK0tjFoVJtDFBOkrjvxVMEPRa0IJNcfl/hgPdp1/IFXWpZhfvk8a8 qgzffxN+Ro/J4Jt9QrHM4sNwiEOjVvHY4cQ9GOfns9UqocmxYPDxElBNraCFOCSudZgXiyF7zUYF OnYqTDR4ChiZtmUqIiZr6rXgZTm1raGlqR7nsbDlkJtru7tzkgMRw8xFRolaQIKiyAwTewF7vLho imwYTRuYRMzft1q5EeRWR4XwtlIuqsXg3FCGTNIG4HiAFKrrNV7AOvVjIUSgpOcWv2leSiRQjgpY I9oD82ii+5rKvebnGIa0o+sWhYNFoviP/49DnDNJWA== ` ) func init() { repairRootAccountKey, err := asserts.Decode([]byte(encodedStagingRepairRootAccountKey)) if err != nil { panic(fmt.Sprintf("cannot decode trusted account-key: %v", err)) } if osutil.GetenvBool("SNAPPY_USE_STAGING_STORE") { trustedRepairRootKeys = append(trustedRepairRootKeys, repairRootAccountKey.(*asserts.AccountKey)) } } snapd-2.37.4~14.04.1/cmd/snap-repair/main.go0000664000000000000000000000377313435556260015012 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main import ( "fmt" "io" "os" // TODO: consider not using go-flags at all "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/cmd" "github.com/snapcore/snapd/httputil" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/release" ) var ( Stdout io.Writer = os.Stdout Stderr io.Writer = os.Stderr opts struct{} parser *flags.Parser = flags.NewParser(&opts, flags.HelpFlag|flags.PassDoubleDash|flags.PassAfterNonOption) ) const ( shortHelp = "Repair an Ubuntu Core system" longHelp = ` snap-repair is a tool to fetch and run repair assertions which are used to do emergency repairs on the device. ` ) func init() { err := logger.SimpleSetup() if err != nil { fmt.Fprintf(Stderr, "WARNING: failed to activate logging: %v\n", err) } } var errOnClassic = fmt.Errorf("cannot use snap-repair on a classic system") func main() { if err := run(); err != nil { fmt.Fprintf(Stderr, "error: %v\n", err) if err != errOnClassic { os.Exit(1) } } } func run() error { if release.OnClassic { return errOnClassic } httputil.SetUserAgentFromVersion(cmd.Version, "snap-repair") if err := parseArgs(os.Args[1:]); err != nil { return err } return nil } func parseArgs(args []string) error { parser.ShortDescription = shortHelp parser.LongDescription = longHelp _, err := parser.ParseArgs(args) return err } snapd-2.37.4~14.04.1/cmd/snap-repair/runner_test.go0000664000000000000000000013554313435556260016437 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main_test import ( "bytes" "encoding/json" "fmt" "io" "io/ioutil" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "strconv" "strings" "time" . "gopkg.in/check.v1" "gopkg.in/retry.v1" "github.com/snapcore/snapd/arch" "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/asserts/assertstest" "github.com/snapcore/snapd/asserts/sysdb" repair "github.com/snapcore/snapd/cmd/snap-repair" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/httputil" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/testutil" ) type baseRunnerSuite struct { tmpdir string seedTime time.Time t0 time.Time storeSigning *assertstest.StoreStack brandSigning *assertstest.SigningDB brandAcct *asserts.Account brandAcctKey *asserts.AccountKey modelAs *asserts.Model seedAssertsDir string repairRootAcctKey *asserts.AccountKey repairsAcctKey *asserts.AccountKey repairsSigning *assertstest.SigningDB restoreLogger func() } func (s *baseRunnerSuite) SetUpSuite(c *C) { s.storeSigning = assertstest.NewStoreStack("canonical", nil) brandPrivKey, _ := assertstest.GenerateKey(752) s.brandAcct = assertstest.NewAccount(s.storeSigning, "my-brand", map[string]interface{}{ "account-id": "my-brand", }, "") s.brandAcctKey = assertstest.NewAccountKey(s.storeSigning, s.brandAcct, nil, brandPrivKey.PublicKey(), "") s.brandSigning = assertstest.NewSigningDB("my-brand", brandPrivKey) modelAs, err := s.brandSigning.Sign(asserts.ModelType, map[string]interface{}{ "series": "16", "brand-id": "my-brand", "model": "my-model-2", "architecture": "armhf", "gadget": "gadget", "kernel": "kernel", "timestamp": time.Now().UTC().Format(time.RFC3339), }, nil, "") c.Assert(err, IsNil) s.modelAs = modelAs.(*asserts.Model) repairRootKey, _ := assertstest.GenerateKey(1024) s.repairRootAcctKey = assertstest.NewAccountKey(s.storeSigning.RootSigning, s.storeSigning.TrustedAccount, nil, repairRootKey.PublicKey(), "") repairsKey, _ := assertstest.GenerateKey(752) repairRootSigning := assertstest.NewSigningDB("canonical", repairRootKey) s.repairsAcctKey = assertstest.NewAccountKey(repairRootSigning, s.storeSigning.TrustedAccount, nil, repairsKey.PublicKey(), "") s.repairsSigning = assertstest.NewSigningDB("canonical", repairsKey) } func (s *baseRunnerSuite) SetUpTest(c *C) { _, s.restoreLogger = logger.MockLogger() s.tmpdir = c.MkDir() dirs.SetRootDir(s.tmpdir) s.seedAssertsDir = filepath.Join(dirs.SnapSeedDir, "assertions") // dummy seed yaml err := os.MkdirAll(dirs.SnapSeedDir, 0755) c.Assert(err, IsNil) seedYamlFn := filepath.Join(dirs.SnapSeedDir, "seed.yaml") err = ioutil.WriteFile(seedYamlFn, nil, 0644) c.Assert(err, IsNil) seedTime, err := time.Parse(time.RFC3339, "2017-08-11T15:49:49Z") c.Assert(err, IsNil) err = os.Chtimes(filepath.Join(dirs.SnapSeedDir, "seed.yaml"), seedTime, seedTime) c.Assert(err, IsNil) s.seedTime = seedTime s.t0 = time.Now().UTC().Truncate(time.Minute) } func (s *baseRunnerSuite) TearDownTest(c *C) { dirs.SetRootDir("/") s.restoreLogger() } func (s *baseRunnerSuite) signSeqRepairs(c *C, repairs []string) []string { var seq []string for _, rpr := range repairs { decoded, err := asserts.Decode([]byte(rpr)) c.Assert(err, IsNil) signed, err := s.repairsSigning.Sign(asserts.RepairType, decoded.Headers(), decoded.Body(), "") c.Assert(err, IsNil) buf := &bytes.Buffer{} enc := asserts.NewEncoder(buf) enc.Encode(signed) enc.Encode(s.repairsAcctKey) seq = append(seq, buf.String()) } return seq } const freshStateJSON = `{"device":{"brand":"my-brand","model":"my-model"},"time-lower-bound":"2017-08-11T15:49:49Z"}` func (s *baseRunnerSuite) freshState(c *C) { err := os.MkdirAll(dirs.SnapRepairDir, 0775) c.Assert(err, IsNil) err = ioutil.WriteFile(dirs.SnapRepairStateFile, []byte(freshStateJSON), 0600) c.Assert(err, IsNil) } type runnerSuite struct { baseRunnerSuite restore func() } func (s *runnerSuite) SetUpSuite(c *C) { s.baseRunnerSuite.SetUpSuite(c) s.restore = httputil.SetUserAgentFromVersion("1", "snap-repair") } func (s *runnerSuite) TearDownSuite(c *C) { s.restore() } var _ = Suite(&runnerSuite{}) var ( testKey = `type: account-key authority-id: canonical account-id: canonical name: repair public-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj since: 2015-11-16T15:04:00Z body-length: 149 sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij AcZrBFaFwYABAvCX5A8dTcdLdhdiuy2YRHO5CAfM5InQefkKOhNMUq2yfi3Sk6trUHxskhZkPnm4 NKx2yRr332q7AJXQHLX+DrZ29ycyoQ2NQGO3eAfQ0hjAAQFYBF8SSh5SutPu5XCVABEBAAE= AXNpZw== ` testRepair = `type: repair authority-id: canonical brand-id: canonical repair-id: 2 summary: repair two architectures: - amd64 - arm64 series: - 16 models: - xyz/frobinator timestamp: 2017-03-30T12:22:16Z body-length: 7 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj script AXNpZw== ` testHeadersResp = `{"headers": {"architectures":["amd64","arm64"],"authority-id":"canonical","body-length":"7","brand-id":"canonical","models":["xyz/frobinator"],"repair-id":"2","series":["16"],"sign-key-sha3-384":"KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj","timestamp":"2017-03-30T12:22:16Z","type":"repair"}}` ) func mustParseURL(s string) *url.URL { u, err := url.Parse(s) if err != nil { panic(err) } return u } func (s *runnerSuite) mockBrokenTimeNowSetToEpoch(c *C, runner *repair.Runner) (restore func()) { epoch := time.Unix(0, 0) r := repair.MockTimeNow(func() time.Time { return epoch }) c.Check(runner.TLSTime().Equal(epoch), Equals, true) return r } func (s *runnerSuite) checkBrokenTimeNowMitigated(c *C, runner *repair.Runner) { c.Check(runner.TLSTime().Before(s.t0), Equals, false) } func (s *runnerSuite) TestFetchJustRepair(c *C) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ua := r.Header.Get("User-Agent") c.Check(strings.Contains(ua, "snap-repair"), Equals, true) c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion") c.Check(r.URL.Path, Equals, "/repairs/canonical/2") io.WriteString(w, testRepair) })) c.Assert(mockServer, NotNil) defer mockServer.Close() runner := repair.NewRunner() runner.BaseURL = mustParseURL(mockServer.URL) r := s.mockBrokenTimeNowSetToEpoch(c, runner) defer r() repair, aux, err := runner.Fetch("canonical", 2, -1) c.Assert(err, IsNil) c.Check(repair, NotNil) c.Check(aux, HasLen, 0) c.Check(repair.BrandID(), Equals, "canonical") c.Check(repair.RepairID(), Equals, 2) c.Check(repair.Body(), DeepEquals, []byte("script\n")) s.checkBrokenTimeNowMitigated(c, runner) } func (s *runnerSuite) TestFetchScriptTooBig(c *C) { restore := repair.MockMaxRepairScriptSize(4) defer restore() n := 0 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { n++ c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion") c.Check(r.URL.Path, Equals, "/repairs/canonical/2") io.WriteString(w, testRepair) })) c.Assert(mockServer, NotNil) defer mockServer.Close() runner := repair.NewRunner() runner.BaseURL = mustParseURL(mockServer.URL) _, _, err := runner.Fetch("canonical", 2, -1) c.Assert(err, ErrorMatches, `assertion body length 7 exceeds maximum body size 4 for "repair".*`) c.Assert(n, Equals, 1) } var ( testRetryStrategy = retry.LimitCount(5, retry.LimitTime(1*time.Second, retry.Exponential{ Initial: 1 * time.Millisecond, Factor: 1, }, )) ) func (s *runnerSuite) TestFetch500(c *C) { restore := repair.MockFetchRetryStrategy(testRetryStrategy) defer restore() n := 0 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { n++ w.WriteHeader(500) })) c.Assert(mockServer, NotNil) defer mockServer.Close() runner := repair.NewRunner() runner.BaseURL = mustParseURL(mockServer.URL) _, _, err := runner.Fetch("canonical", 2, -1) c.Assert(err, ErrorMatches, "cannot fetch repair, unexpected status 500") c.Assert(n, Equals, 5) } func (s *runnerSuite) TestFetchEmpty(c *C) { restore := repair.MockFetchRetryStrategy(testRetryStrategy) defer restore() n := 0 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { n++ w.WriteHeader(200) })) c.Assert(mockServer, NotNil) defer mockServer.Close() runner := repair.NewRunner() runner.BaseURL = mustParseURL(mockServer.URL) _, _, err := runner.Fetch("canonical", 2, -1) c.Assert(err, Equals, io.ErrUnexpectedEOF) c.Assert(n, Equals, 5) } func (s *runnerSuite) TestFetchBroken(c *C) { restore := repair.MockFetchRetryStrategy(testRetryStrategy) defer restore() n := 0 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { n++ w.WriteHeader(200) io.WriteString(w, "xyz:") })) c.Assert(mockServer, NotNil) defer mockServer.Close() runner := repair.NewRunner() runner.BaseURL = mustParseURL(mockServer.URL) _, _, err := runner.Fetch("canonical", 2, -1) c.Assert(err, Equals, io.ErrUnexpectedEOF) c.Assert(n, Equals, 5) } func (s *runnerSuite) TestFetchNotFound(c *C) { restore := repair.MockFetchRetryStrategy(testRetryStrategy) defer restore() n := 0 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { n++ w.WriteHeader(404) })) c.Assert(mockServer, NotNil) defer mockServer.Close() runner := repair.NewRunner() runner.BaseURL = mustParseURL(mockServer.URL) r := s.mockBrokenTimeNowSetToEpoch(c, runner) defer r() _, _, err := runner.Fetch("canonical", 2, -1) c.Assert(err, Equals, repair.ErrRepairNotFound) c.Assert(n, Equals, 1) s.checkBrokenTimeNowMitigated(c, runner) } func (s *runnerSuite) TestFetchIfNoneMatchNotModified(c *C) { restore := repair.MockFetchRetryStrategy(testRetryStrategy) defer restore() n := 0 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { n++ c.Check(r.Header.Get("If-None-Match"), Equals, `"0"`) w.WriteHeader(304) })) c.Assert(mockServer, NotNil) defer mockServer.Close() runner := repair.NewRunner() runner.BaseURL = mustParseURL(mockServer.URL) r := s.mockBrokenTimeNowSetToEpoch(c, runner) defer r() _, _, err := runner.Fetch("canonical", 2, 0) c.Assert(err, Equals, repair.ErrRepairNotModified) c.Assert(n, Equals, 1) s.checkBrokenTimeNowMitigated(c, runner) } func (s *runnerSuite) TestFetchIgnoreSupersededRevision(c *C) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, testRepair) })) c.Assert(mockServer, NotNil) defer mockServer.Close() runner := repair.NewRunner() runner.BaseURL = mustParseURL(mockServer.URL) _, _, err := runner.Fetch("canonical", 2, 2) c.Assert(err, Equals, repair.ErrRepairNotModified) } func (s *runnerSuite) TestFetchIdMismatch(c *C) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion") io.WriteString(w, testRepair) })) c.Assert(mockServer, NotNil) defer mockServer.Close() runner := repair.NewRunner() runner.BaseURL = mustParseURL(mockServer.URL) _, _, err := runner.Fetch("canonical", 4, -1) c.Assert(err, ErrorMatches, `cannot fetch repair, repair id mismatch canonical/2 != canonical/4`) } func (s *runnerSuite) TestFetchWrongFirstType(c *C) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion") c.Check(r.URL.Path, Equals, "/repairs/canonical/2") io.WriteString(w, testKey) })) c.Assert(mockServer, NotNil) defer mockServer.Close() runner := repair.NewRunner() runner.BaseURL = mustParseURL(mockServer.URL) _, _, err := runner.Fetch("canonical", 2, -1) c.Assert(err, ErrorMatches, `cannot fetch repair, unexpected first assertion "account-key"`) } func (s *runnerSuite) TestFetchRepairPlusKey(c *C) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion") c.Check(r.URL.Path, Equals, "/repairs/canonical/2") io.WriteString(w, testRepair) io.WriteString(w, "\n") io.WriteString(w, testKey) })) c.Assert(mockServer, NotNil) defer mockServer.Close() runner := repair.NewRunner() runner.BaseURL = mustParseURL(mockServer.URL) repair, aux, err := runner.Fetch("canonical", 2, -1) c.Assert(err, IsNil) c.Check(repair, NotNil) c.Check(aux, HasLen, 1) _, ok := aux[0].(*asserts.AccountKey) c.Check(ok, Equals, true) } func (s *runnerSuite) TestPeek(c *C) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ua := r.Header.Get("User-Agent") c.Check(strings.Contains(ua, "snap-repair"), Equals, true) c.Check(r.Header.Get("Accept"), Equals, "application/json") c.Check(r.URL.Path, Equals, "/repairs/canonical/2") io.WriteString(w, testHeadersResp) })) c.Assert(mockServer, NotNil) defer mockServer.Close() runner := repair.NewRunner() runner.BaseURL = mustParseURL(mockServer.URL) r := s.mockBrokenTimeNowSetToEpoch(c, runner) defer r() h, err := runner.Peek("canonical", 2) c.Assert(err, IsNil) c.Check(h["series"], DeepEquals, []interface{}{"16"}) c.Check(h["architectures"], DeepEquals, []interface{}{"amd64", "arm64"}) c.Check(h["models"], DeepEquals, []interface{}{"xyz/frobinator"}) s.checkBrokenTimeNowMitigated(c, runner) } func (s *runnerSuite) TestPeek500(c *C) { restore := repair.MockPeekRetryStrategy(testRetryStrategy) defer restore() n := 0 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { n++ w.WriteHeader(500) })) c.Assert(mockServer, NotNil) defer mockServer.Close() runner := repair.NewRunner() runner.BaseURL = mustParseURL(mockServer.URL) _, err := runner.Peek("canonical", 2) c.Assert(err, ErrorMatches, "cannot peek repair headers, unexpected status 500") c.Assert(n, Equals, 5) } func (s *runnerSuite) TestPeekInvalid(c *C) { restore := repair.MockPeekRetryStrategy(testRetryStrategy) defer restore() n := 0 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { n++ w.WriteHeader(200) io.WriteString(w, "{") })) c.Assert(mockServer, NotNil) defer mockServer.Close() runner := repair.NewRunner() runner.BaseURL = mustParseURL(mockServer.URL) _, err := runner.Peek("canonical", 2) c.Assert(err, Equals, io.ErrUnexpectedEOF) c.Assert(n, Equals, 5) } func (s *runnerSuite) TestPeekNotFound(c *C) { n := 0 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { n++ w.WriteHeader(404) })) c.Assert(mockServer, NotNil) defer mockServer.Close() runner := repair.NewRunner() runner.BaseURL = mustParseURL(mockServer.URL) r := s.mockBrokenTimeNowSetToEpoch(c, runner) defer r() _, err := runner.Peek("canonical", 2) c.Assert(err, Equals, repair.ErrRepairNotFound) c.Assert(n, Equals, 1) s.checkBrokenTimeNowMitigated(c, runner) } func (s *runnerSuite) TestPeekIdMismatch(c *C) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Header.Get("Accept"), Equals, "application/json") io.WriteString(w, testHeadersResp) })) c.Assert(mockServer, NotNil) defer mockServer.Close() runner := repair.NewRunner() runner.BaseURL = mustParseURL(mockServer.URL) _, err := runner.Peek("canonical", 4) c.Assert(err, ErrorMatches, `cannot peek repair headers, repair id mismatch canonical/2 != canonical/4`) } func (s *runnerSuite) TestLoadState(c *C) { s.freshState(c) runner := repair.NewRunner() err := runner.LoadState() c.Assert(err, IsNil) brand, model := runner.BrandModel() c.Check(brand, Equals, "my-brand") c.Check(model, Equals, "my-model") } func (s *runnerSuite) initSeed(c *C) { err := os.MkdirAll(s.seedAssertsDir, 0775) c.Assert(err, IsNil) } func (s *runnerSuite) writeSeedAssert(c *C, fname string, a asserts.Assertion) { err := ioutil.WriteFile(filepath.Join(s.seedAssertsDir, fname), asserts.Encode(a), 0644) c.Assert(err, IsNil) } func (s *runnerSuite) rmSeedAssert(c *C, fname string) { err := os.Remove(filepath.Join(s.seedAssertsDir, fname)) c.Assert(err, IsNil) } func (s *runnerSuite) TestLoadStateInitState(c *C) { // sanity c.Check(osutil.IsDirectory(dirs.SnapRepairDir), Equals, false) c.Check(osutil.FileExists(dirs.SnapRepairStateFile), Equals, false) // setup realistic seed/assertions r := sysdb.InjectTrusted(s.storeSigning.Trusted) defer r() s.initSeed(c) s.writeSeedAssert(c, "store.account-key", s.storeSigning.StoreAccountKey("")) s.writeSeedAssert(c, "brand.account", s.brandAcct) s.writeSeedAssert(c, "brand.account-key", s.brandAcctKey) s.writeSeedAssert(c, "model", s.modelAs) runner := repair.NewRunner() err := runner.LoadState() c.Assert(err, IsNil) c.Check(osutil.FileExists(dirs.SnapRepairStateFile), Equals, true) brand, model := runner.BrandModel() c.Check(brand, Equals, "my-brand") c.Check(model, Equals, "my-model-2") c.Check(runner.TimeLowerBound().Equal(s.seedTime), Equals, true) } func (s *runnerSuite) TestLoadStateInitDeviceInfoFail(c *C) { // sanity c.Check(osutil.IsDirectory(dirs.SnapRepairDir), Equals, false) c.Check(osutil.FileExists(dirs.SnapRepairStateFile), Equals, false) // setup realistic seed/assertions r := sysdb.InjectTrusted(s.storeSigning.Trusted) defer r() s.initSeed(c) const errPrefix = "cannot set device information: " tests := []struct { breakFunc func() expectedErr string }{ {func() { s.rmSeedAssert(c, "model") }, errPrefix + "no model assertion in seed data"}, {func() { s.rmSeedAssert(c, "brand.account") }, errPrefix + "no brand account assertion in seed data"}, {func() { s.rmSeedAssert(c, "brand.account-key") }, errPrefix + `cannot find public key.*`}, {func() { // broken signature blob := asserts.Encode(s.brandAcct) err := ioutil.WriteFile(filepath.Join(s.seedAssertsDir, "brand.account"), blob[:len(blob)-3], 0644) c.Assert(err, IsNil) }, errPrefix + "cannot decode signature:.*"}, {func() { s.writeSeedAssert(c, "model2", s.modelAs) }, errPrefix + "multiple models in seed assertions"}, } for _, test := range tests { s.writeSeedAssert(c, "store.account-key", s.storeSigning.StoreAccountKey("")) s.writeSeedAssert(c, "brand.account", s.brandAcct) s.writeSeedAssert(c, "brand.account-key", s.brandAcctKey) s.writeSeedAssert(c, "model", s.modelAs) test.breakFunc() runner := repair.NewRunner() err := runner.LoadState() c.Check(err, ErrorMatches, test.expectedErr) } } func (s *runnerSuite) TestTLSTime(c *C) { s.freshState(c) runner := repair.NewRunner() err := runner.LoadState() c.Assert(err, IsNil) epoch := time.Unix(0, 0) r := repair.MockTimeNow(func() time.Time { return epoch }) defer r() c.Check(runner.TLSTime().Equal(s.seedTime), Equals, true) } func makeReadOnly(c *C, dir string) (restore func()) { // skip tests that need this because uid==0 does not honor // write permissions in directories (yay, unix) if os.Getuid() == 0 { // FIXME: we could use osutil.Chattr() here c.Skip("too lazy to make path readonly as root") } err := os.Chmod(dir, 0555) c.Assert(err, IsNil) return func() { err := os.Chmod(dir, 0755) c.Assert(err, IsNil) } } func (s *runnerSuite) TestLoadStateInitStateFail(c *C) { restore := makeReadOnly(c, filepath.Dir(dirs.SnapSeedDir)) defer restore() runner := repair.NewRunner() err := runner.LoadState() c.Check(err, ErrorMatches, `cannot create repair state directory:.*`) } func (s *runnerSuite) TestSaveStateFail(c *C) { s.freshState(c) runner := repair.NewRunner() err := runner.LoadState() c.Assert(err, IsNil) restore := makeReadOnly(c, dirs.SnapRepairDir) defer restore() // no error because this is a no-op err = runner.SaveState() c.Check(err, IsNil) // mark as modified runner.SetStateModified(true) err = runner.SaveState() c.Check(err, ErrorMatches, `cannot save repair state:.*`) } func (s *runnerSuite) TestSaveState(c *C) { s.freshState(c) runner := repair.NewRunner() err := runner.LoadState() c.Assert(err, IsNil) runner.SetSequence("canonical", []*repair.RepairState{ {Sequence: 1, Revision: 3}, }) // mark as modified runner.SetStateModified(true) err = runner.SaveState() c.Assert(err, IsNil) c.Check(dirs.SnapRepairStateFile, testutil.FileEquals, `{"device":{"brand":"my-brand","model":"my-model"},"sequences":{"canonical":[{"sequence":1,"revision":3,"status":0}]},"time-lower-bound":"2017-08-11T15:49:49Z"}`) } func (s *runnerSuite) TestApplicable(c *C) { s.freshState(c) runner := repair.NewRunner() err := runner.LoadState() c.Assert(err, IsNil) scenarios := []struct { 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.UbuntuArchitecture()}}, true}, {map[string]interface{}{"architectures": []interface{}{"other-arch"}}, false}, {map[string]interface{}{"architectures": []interface{}{"other-arch", arch.UbuntuArchitecture()}}, true}, {map[string]interface{}{"architectures": arch.UbuntuArchitecture()}, 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}, // 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}, } for _, scen := range scenarios { ok := runner.Applicable(scen.headers) c.Check(ok, Equals, scen.applicable, Commentf("%v", scen)) } } var ( nextRepairs = []string{`type: repair authority-id: canonical brand-id: canonical repair-id: 1 summary: repair one timestamp: 2017-07-01T12:00:00Z body-length: 8 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj scriptA AXNpZw==`, `type: repair authority-id: canonical brand-id: canonical repair-id: 2 summary: repair two series: - 33 timestamp: 2017-07-02T12:00:00Z body-length: 8 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj scriptB AXNpZw==`, `type: repair revision: 2 authority-id: canonical brand-id: canonical repair-id: 3 summary: repair three rev2 series: - 16 timestamp: 2017-07-03T12:00:00Z body-length: 8 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj scriptC AXNpZw== `} repair3Rev4 = `type: repair revision: 4 authority-id: canonical brand-id: canonical repair-id: 3 summary: repair three rev4 series: - 16 timestamp: 2017-07-03T12:00:00Z body-length: 9 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj scriptC2 AXNpZw== ` repair4 = `type: repair authority-id: canonical brand-id: canonical repair-id: 4 summary: repair four timestamp: 2017-07-03T12:00:00Z body-length: 8 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj scriptD AXNpZw== ` ) func makeMockServer(c *C, seqRepairs *[]string, redirectFirst bool) *httptest.Server { var mockServer *httptest.Server mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ua := r.Header.Get("User-Agent") c.Check(strings.Contains(ua, "snap-repair"), Equals, true) urlPath := r.URL.Path if redirectFirst && r.Header.Get("Accept") == asserts.MediaType { if !strings.HasPrefix(urlPath, "/final/") { // redirect finalURL := mockServer.URL + "/final" + r.URL.Path w.Header().Set("Location", finalURL) w.WriteHeader(302) return } urlPath = strings.TrimPrefix(urlPath, "/final") } c.Check(strings.HasPrefix(urlPath, "/repairs/canonical/"), Equals, true) seq, err := strconv.Atoi(strings.TrimPrefix(urlPath, "/repairs/canonical/")) c.Assert(err, IsNil) if seq > len(*seqRepairs) { w.WriteHeader(404) return } rpr := []byte((*seqRepairs)[seq-1]) dec := asserts.NewDecoder(bytes.NewBuffer(rpr)) repair, err := dec.Decode() c.Assert(err, IsNil) switch r.Header.Get("Accept") { case "application/json": b, err := json.Marshal(map[string]interface{}{ "headers": repair.Headers(), }) c.Assert(err, IsNil) w.Write(b) case asserts.MediaType: etag := fmt.Sprintf(`"%d"`, repair.Revision()) if strings.Contains(r.Header.Get("If-None-Match"), etag) { w.WriteHeader(304) return } w.Write(rpr) } })) c.Assert(mockServer, NotNil) return mockServer } func (s *runnerSuite) TestTrustedRepairRootKeys(c *C) { acctKeys := repair.TrustedRepairRootKeys() c.Check(acctKeys, HasLen, 1) c.Check(acctKeys[0].AccountID(), Equals, "canonical") c.Check(acctKeys[0].PublicKeyID(), Equals, "nttW6NfBXI_E-00u38W-KH6eiksfQNXuI7IiumoV49_zkbhM0sYTzSnFlwZC-W4t") } func (s *runnerSuite) TestVerify(c *C) { r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) defer r1() r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) defer r2() runner := repair.NewRunner() a, err := s.repairsSigning.Sign(asserts.RepairType, map[string]interface{}{ "brand-id": "canonical", "repair-id": "2", "summary": "repair two", "timestamp": time.Now().UTC().Format(time.RFC3339), }, []byte("#script"), "") c.Assert(err, IsNil) rpr := a.(*asserts.Repair) err = runner.Verify(rpr, []asserts.Assertion{s.repairsAcctKey}) c.Check(err, IsNil) } func (s *runnerSuite) signSeqRepairs(c *C, repairs []string) []string { var seq []string for _, rpr := range repairs { decoded, err := asserts.Decode([]byte(rpr)) c.Assert(err, IsNil) signed, err := s.repairsSigning.Sign(asserts.RepairType, decoded.Headers(), decoded.Body(), "") c.Assert(err, IsNil) buf := &bytes.Buffer{} enc := asserts.NewEncoder(buf) enc.Encode(signed) enc.Encode(s.repairsAcctKey) seq = append(seq, buf.String()) } return seq } func (s *runnerSuite) loadSequences(c *C) map[string][]*repair.RepairState { data, err := ioutil.ReadFile(dirs.SnapRepairStateFile) c.Assert(err, IsNil) var x struct { Sequences map[string][]*repair.RepairState `json:"sequences"` } err = json.Unmarshal(data, &x) c.Assert(err, IsNil) return x.Sequences } func (s *runnerSuite) testNext(c *C, redirectFirst bool) { r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) defer r1() r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) defer r2() seqRepairs := s.signSeqRepairs(c, nextRepairs) mockServer := makeMockServer(c, &seqRepairs, redirectFirst) defer mockServer.Close() runner := repair.NewRunner() runner.BaseURL = mustParseURL(mockServer.URL) runner.LoadState() rpr, err := runner.Next("canonical") c.Assert(err, IsNil) c.Check(rpr.RepairID(), Equals, 1) c.Check(osutil.FileExists(filepath.Join(dirs.SnapRepairAssertsDir, "canonical", "1", "r0.repair")), Equals, true) rpr, err = runner.Next("canonical") c.Assert(err, IsNil) c.Check(rpr.RepairID(), Equals, 3) c.Check(filepath.Join(dirs.SnapRepairAssertsDir, "canonical", "3", "r2.repair"), testutil.FileEquals, seqRepairs[2]) // no more rpr, err = runner.Next("canonical") c.Check(err, Equals, repair.ErrRepairNotFound) expectedSeq := []*repair.RepairState{ {Sequence: 1}, {Sequence: 2, Status: repair.SkipStatus}, {Sequence: 3, Revision: 2}, } c.Check(runner.Sequence("canonical"), DeepEquals, expectedSeq) // on disk seqs := s.loadSequences(c) c.Check(seqs["canonical"], DeepEquals, expectedSeq) // start fresh run with new runner // will refetch repair 3 signed := s.signSeqRepairs(c, []string{repair3Rev4, repair4}) seqRepairs[2] = signed[0] seqRepairs = append(seqRepairs, signed[1]) runner = repair.NewRunner() runner.BaseURL = mustParseURL(mockServer.URL) runner.LoadState() rpr, err = runner.Next("canonical") c.Assert(err, IsNil) c.Check(rpr.RepairID(), Equals, 1) rpr, err = runner.Next("canonical") c.Assert(err, IsNil) c.Check(rpr.RepairID(), Equals, 3) // refetched new revision! c.Check(rpr.Revision(), Equals, 4) c.Check(rpr.Body(), DeepEquals, []byte("scriptC2\n")) // new repair rpr, err = runner.Next("canonical") c.Assert(err, IsNil) c.Check(rpr.RepairID(), Equals, 4) c.Check(rpr.Body(), DeepEquals, []byte("scriptD\n")) // no more rpr, err = runner.Next("canonical") c.Check(err, Equals, repair.ErrRepairNotFound) c.Check(runner.Sequence("canonical"), DeepEquals, []*repair.RepairState{ {Sequence: 1}, {Sequence: 2, Status: repair.SkipStatus}, {Sequence: 3, Revision: 4}, {Sequence: 4}, }) } func (s *runnerSuite) TestNext(c *C) { redirectFirst := false s.testNext(c, redirectFirst) } func (s *runnerSuite) TestNextRedirect(c *C) { redirectFirst := true s.testNext(c, redirectFirst) } func (s *runnerSuite) TestNextImmediateSkip(c *C) { seqRepairs := []string{`type: repair authority-id: canonical brand-id: canonical repair-id: 1 summary: repair one series: - 33 timestamp: 2017-07-02T12:00:00Z body-length: 8 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj scriptB AXNpZw==`} mockServer := makeMockServer(c, &seqRepairs, false) defer mockServer.Close() runner := repair.NewRunner() runner.BaseURL = mustParseURL(mockServer.URL) runner.LoadState() // not applicable => not returned _, err := runner.Next("canonical") c.Check(err, Equals, repair.ErrRepairNotFound) expectedSeq := []*repair.RepairState{ {Sequence: 1, Status: repair.SkipStatus}, } c.Check(runner.Sequence("canonical"), DeepEquals, expectedSeq) // on disk seqs := s.loadSequences(c) c.Check(seqs["canonical"], DeepEquals, expectedSeq) } func (s *runnerSuite) TestNextRefetchSkip(c *C) { seqRepairs := []string{`type: repair authority-id: canonical brand-id: canonical repair-id: 1 summary: repair one series: - 16 timestamp: 2017-07-02T12:00:00Z body-length: 8 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj scriptB AXNpZw==`} r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) defer r1() r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) defer r2() seqRepairs = s.signSeqRepairs(c, seqRepairs) mockServer := makeMockServer(c, &seqRepairs, false) defer mockServer.Close() runner := repair.NewRunner() runner.BaseURL = mustParseURL(mockServer.URL) runner.LoadState() _, err := runner.Next("canonical") c.Assert(err, IsNil) expectedSeq := []*repair.RepairState{ {Sequence: 1}, } c.Check(runner.Sequence("canonical"), DeepEquals, expectedSeq) // on disk seqs := s.loadSequences(c) c.Check(seqs["canonical"], DeepEquals, expectedSeq) // new fresh run, repair becomes now unapplicable seqRepairs[0] = `type: repair authority-id: canonical revision: 1 brand-id: canonical repair-id: 1 summary: repair one rev1 series: - 16 disabled: true timestamp: 2017-07-02T12:00:00Z body-length: 7 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj scriptX AXNpZw==` seqRepairs = s.signSeqRepairs(c, seqRepairs) runner = repair.NewRunner() runner.BaseURL = mustParseURL(mockServer.URL) runner.LoadState() _, err = runner.Next("canonical") c.Check(err, Equals, repair.ErrRepairNotFound) expectedSeq = []*repair.RepairState{ {Sequence: 1, Revision: 1, Status: repair.SkipStatus}, } c.Check(runner.Sequence("canonical"), DeepEquals, expectedSeq) // on disk seqs = s.loadSequences(c) c.Check(seqs["canonical"], DeepEquals, expectedSeq) } func (s *runnerSuite) TestNext500(c *C) { restore := repair.MockPeekRetryStrategy(testRetryStrategy) defer restore() mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) })) c.Assert(mockServer, NotNil) defer mockServer.Close() runner := repair.NewRunner() runner.BaseURL = mustParseURL(mockServer.URL) runner.LoadState() _, err := runner.Next("canonical") c.Assert(err, ErrorMatches, "cannot peek repair headers, unexpected status 500") } func (s *runnerSuite) TestNextNotFound(c *C) { s.freshState(c) restore := repair.MockPeekRetryStrategy(testRetryStrategy) defer restore() mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(404) })) c.Assert(mockServer, NotNil) defer mockServer.Close() runner := repair.NewRunner() 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) } func (s *runnerSuite) TestNextSaveStateError(c *C) { seqRepairs := []string{`type: repair authority-id: canonical brand-id: canonical repair-id: 1 summary: repair one series: - 33 timestamp: 2017-07-02T12:00:00Z body-length: 8 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj scriptB AXNpZw==`} mockServer := makeMockServer(c, &seqRepairs, false) defer mockServer.Close() runner := repair.NewRunner() runner.BaseURL = mustParseURL(mockServer.URL) runner.LoadState() // break SaveState restore := makeReadOnly(c, dirs.SnapRepairDir) defer restore() _, err := runner.Next("canonical") c.Check(err, ErrorMatches, `cannot save repair state:.*`) } func (s *runnerSuite) TestNextVerifyNoKey(c *C) { seqRepairs := []string{`type: repair authority-id: canonical brand-id: canonical repair-id: 1 summary: repair one timestamp: 2017-07-02T12:00:00Z body-length: 8 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj scriptB AXNpZw==`} mockServer := makeMockServer(c, &seqRepairs, false) defer mockServer.Close() runner := repair.NewRunner() runner.BaseURL = mustParseURL(mockServer.URL) runner.LoadState() _, err := runner.Next("canonical") c.Check(err, ErrorMatches, `cannot verify repair canonical-1: cannot find public key.*`) c.Check(runner.Sequence("canonical"), HasLen, 0) } func (s *runnerSuite) TestNextVerifySelfSigned(c *C) { randoKey, _ := assertstest.GenerateKey(752) randomSigning := assertstest.NewSigningDB("canonical", randoKey) randoKeyEncoded, err := asserts.EncodePublicKey(randoKey.PublicKey()) c.Assert(err, IsNil) acctKey, err := randomSigning.Sign(asserts.AccountKeyType, map[string]interface{}{ "authority-id": "canonical", "account-id": "canonical", "public-key-sha3-384": randoKey.PublicKey().ID(), "name": "repairs", "since": time.Now().UTC().Format(time.RFC3339), }, randoKeyEncoded, "") c.Assert(err, IsNil) rpr, err := randomSigning.Sign(asserts.RepairType, map[string]interface{}{ "brand-id": "canonical", "repair-id": "1", "summary": "repair one", "timestamp": time.Now().UTC().Format(time.RFC3339), }, []byte("scriptB\n"), "") c.Assert(err, IsNil) buf := &bytes.Buffer{} enc := asserts.NewEncoder(buf) enc.Encode(rpr) enc.Encode(acctKey) seqRepairs := []string{buf.String()} mockServer := makeMockServer(c, &seqRepairs, false) defer mockServer.Close() runner := repair.NewRunner() runner.BaseURL = mustParseURL(mockServer.URL) runner.LoadState() _, err = runner.Next("canonical") c.Check(err, ErrorMatches, `cannot verify repair canonical-1: circular assertions`) c.Check(runner.Sequence("canonical"), HasLen, 0) } func (s *runnerSuite) TestNextVerifyAllKeysOK(c *C) { r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) defer r1() r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) defer r2() decoded, err := asserts.Decode([]byte(nextRepairs[0])) c.Assert(err, IsNil) signed, err := s.repairsSigning.Sign(asserts.RepairType, decoded.Headers(), decoded.Body(), "") c.Assert(err, IsNil) // stream with all keys (any order) works as well buf := &bytes.Buffer{} enc := asserts.NewEncoder(buf) enc.Encode(signed) enc.Encode(s.storeSigning.TrustedKey) enc.Encode(s.repairRootAcctKey) enc.Encode(s.repairsAcctKey) seqRepairs := []string{buf.String()} mockServer := makeMockServer(c, &seqRepairs, false) defer mockServer.Close() runner := repair.NewRunner() runner.BaseURL = mustParseURL(mockServer.URL) runner.LoadState() rpr, err := runner.Next("canonical") c.Assert(err, IsNil) c.Check(rpr.RepairID(), Equals, 1) } func (s *runnerSuite) TestRepairSetStatus(c *C) { seqRepairs := []string{`type: repair authority-id: canonical brand-id: canonical repair-id: 1 summary: repair one timestamp: 2017-07-02T12:00:00Z body-length: 8 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj scriptB AXNpZw==`} r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) defer r1() r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) defer r2() seqRepairs = s.signSeqRepairs(c, seqRepairs) mockServer := makeMockServer(c, &seqRepairs, false) defer mockServer.Close() runner := repair.NewRunner() runner.BaseURL = mustParseURL(mockServer.URL) runner.LoadState() rpr, err := runner.Next("canonical") c.Assert(err, IsNil) rpr.SetStatus(repair.DoneStatus) expectedSeq := []*repair.RepairState{ {Sequence: 1, Status: repair.DoneStatus}, } c.Check(runner.Sequence("canonical"), DeepEquals, expectedSeq) // on disk seqs := s.loadSequences(c) c.Check(seqs["canonical"], DeepEquals, expectedSeq) } func (s *runnerSuite) TestRepairBasicRun(c *C) { seqRepairs := []string{`type: repair authority-id: canonical brand-id: canonical repair-id: 1 summary: repair one series: - 16 timestamp: 2017-07-02T12:00:00Z body-length: 7 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj exit 0 AXNpZw==`} r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) defer r1() r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) defer r2() seqRepairs = s.signSeqRepairs(c, seqRepairs) mockServer := makeMockServer(c, &seqRepairs, false) defer mockServer.Close() runner := repair.NewRunner() runner.BaseURL = mustParseURL(mockServer.URL) runner.LoadState() rpr, err := runner.Next("canonical") c.Assert(err, IsNil) rpr.Run() c.Check(filepath.Join(dirs.SnapRepairRunDir, "canonical", "1", "r0.script"), testutil.FileEquals, "exit 0\n") } func makeMockRepair(script string) string { return fmt.Sprintf(`type: repair authority-id: canonical brand-id: canonical repair-id: 1 summary: repair one series: - 16 timestamp: 2017-07-02T12:00:00Z body-length: %d sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj %s AXNpZw==`, len(script), script) } func verifyRepairStatus(c *C, status repair.RepairStatus) { c.Check(dirs.SnapRepairStateFile, testutil.FileContains, fmt.Sprintf(`{"device":{"brand":"","model":""},"sequences":{"canonical":[{"sequence":1,"revision":0,"status":%d}`, status)) } // tests related to correct execution of script type runScriptSuite struct { baseRunnerSuite seqRepairs []string mockServer *httptest.Server runner *repair.Runner runDir string restoreErrTrackerReportRepair func() errReport struct { repair string errMsg string dupSig string extra map[string]string } } var _ = Suite(&runScriptSuite{}) func (s *runScriptSuite) SetUpTest(c *C) { s.baseRunnerSuite.SetUpTest(c) s.mockServer = makeMockServer(c, &s.seqRepairs, false) s.runner = repair.NewRunner() s.runner.BaseURL = mustParseURL(s.mockServer.URL) s.runner.LoadState() s.runDir = filepath.Join(dirs.SnapRepairRunDir, "canonical", "1") s.restoreErrTrackerReportRepair = repair.MockErrtrackerReportRepair(s.errtrackerReportRepair) } func (s *runScriptSuite) TearDownTest(c *C) { s.baseRunnerSuite.TearDownTest(c) s.restoreErrTrackerReportRepair() s.mockServer.Close() } func (s *runScriptSuite) errtrackerReportRepair(repair, errMsg, dupSig string, extra map[string]string) (string, error) { s.errReport.repair = repair s.errReport.errMsg = errMsg s.errReport.dupSig = dupSig s.errReport.extra = extra return "some-oops-id", nil } func (s *runScriptSuite) testScriptRun(c *C, mockScript string) *repair.Repair { r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) defer r1() r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) defer r2() s.seqRepairs = s.signSeqRepairs(c, s.seqRepairs) rpr, err := s.runner.Next("canonical") c.Assert(err, IsNil) err = rpr.Run() c.Assert(err, IsNil) c.Check(filepath.Join(s.runDir, "r0.script"), testutil.FileEquals, mockScript) return rpr } func (s *runScriptSuite) verifyRundir(c *C, names []string) { dirents, err := ioutil.ReadDir(s.runDir) c.Assert(err, IsNil) c.Assert(dirents, HasLen, len(names)) for i := range dirents { c.Check(dirents[i].Name(), Matches, names[i]) } } type byMtime []os.FileInfo func (m byMtime) Len() int { return len(m) } func (m byMtime) Less(i, j int) bool { return m[i].ModTime().Before(m[j].ModTime()) } func (m byMtime) Swap(i, j int) { m[i], m[j] = m[j], m[i] } func (s *runScriptSuite) verifyOutput(c *C, name, expectedOutput string) { c.Check(filepath.Join(s.runDir, name), testutil.FileEquals, expectedOutput) // ensure correct permissions fi, err := os.Stat(filepath.Join(s.runDir, name)) c.Assert(err, IsNil) c.Check(fi.Mode(), Equals, os.FileMode(0600)) } func (s *runScriptSuite) TestRepairBasicRunHappy(c *C) { script := `#!/bin/sh echo "happy output" echo "done" >&$SNAP_REPAIR_STATUS_FD exit 0 ` s.seqRepairs = []string{makeMockRepair(script)} s.testScriptRun(c, script) // verify s.verifyRundir(c, []string{ `^r0.done$`, `^r0.script$`, `^work$`, }) s.verifyOutput(c, "r0.done", `repair: canonical-1 revision: 0 summary: repair one output: happy output `) verifyRepairStatus(c, repair.DoneStatus) } func (s *runScriptSuite) TestRepairBasicRunUnhappy(c *C) { script := `#!/bin/sh echo "unhappy output" exit 1 ` s.seqRepairs = []string{makeMockRepair(script)} s.testScriptRun(c, script) // verify s.verifyRundir(c, []string{ `^r0.retry$`, `^r0.script$`, `^work$`, }) s.verifyOutput(c, "r0.retry", `repair: canonical-1 revision: 0 summary: repair one output: unhappy output repair canonical-1 revision 0 failed: exit status 1`) verifyRepairStatus(c, repair.RetryStatus) c.Check(s.errReport.repair, Equals, "canonical/1") c.Check(s.errReport.errMsg, Equals, `repair canonical-1 revision 0 failed: exit status 1`) c.Check(s.errReport.dupSig, Equals, `canonical/1 repair canonical-1 revision 0 failed: exit status 1 output: repair: canonical-1 revision: 0 summary: repair one output: unhappy output `) c.Check(s.errReport.extra, DeepEquals, map[string]string{ "Revision": "0", "RepairID": "1", "BrandID": "canonical", "Status": "retry", }) } func (s *runScriptSuite) TestRepairBasicSkip(c *C) { script := `#!/bin/sh echo "other output" echo "skip" >&$SNAP_REPAIR_STATUS_FD exit 0 ` s.seqRepairs = []string{makeMockRepair(script)} s.testScriptRun(c, script) // verify s.verifyRundir(c, []string{ `^r0.script$`, `^r0.skip$`, `^work$`, }) s.verifyOutput(c, "r0.skip", `repair: canonical-1 revision: 0 summary: repair one output: other output `) verifyRepairStatus(c, repair.SkipStatus) } func (s *runScriptSuite) TestRepairBasicRunUnhappyThenHappy(c *C) { script := `#!/bin/sh if [ -f zzz-ran-once ]; then echo "happy now" echo "done" >&$SNAP_REPAIR_STATUS_FD exit 0 fi echo "unhappy output" touch zzz-ran-once exit 1 ` s.seqRepairs = []string{makeMockRepair(script)} rpr := s.testScriptRun(c, script) s.verifyRundir(c, []string{ `^r0.retry$`, `^r0.script$`, `^work$`, }) s.verifyOutput(c, "r0.retry", `repair: canonical-1 revision: 0 summary: repair one output: unhappy output repair canonical-1 revision 0 failed: exit status 1`) verifyRepairStatus(c, repair.RetryStatus) // run again, it will be happy this time err := rpr.Run() c.Assert(err, IsNil) s.verifyRundir(c, []string{ `^r0.done$`, `^r0.retry$`, `^r0.script$`, `^work$`, }) s.verifyOutput(c, "r0.done", `repair: canonical-1 revision: 0 summary: repair one output: happy now `) verifyRepairStatus(c, repair.DoneStatus) } func (s *runScriptSuite) TestRepairHitsTimeout(c *C) { r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) defer r1() r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) defer r2() restore := repair.MockDefaultRepairTimeout(100 * time.Millisecond) defer restore() script := `#!/bin/sh echo "output before timeout" sleep 100 ` s.seqRepairs = []string{makeMockRepair(script)} s.seqRepairs = s.signSeqRepairs(c, s.seqRepairs) rpr, err := s.runner.Next("canonical") c.Assert(err, IsNil) err = rpr.Run() c.Assert(err, IsNil) s.verifyRundir(c, []string{ `^r0.retry$`, `^r0.script$`, `^work$`, }) s.verifyOutput(c, "r0.retry", `repair: canonical-1 revision: 0 summary: repair one output: output before timeout repair canonical-1 revision 0 failed: repair did not finish within 100ms`) verifyRepairStatus(c, repair.RetryStatus) } func (s *runScriptSuite) TestRepairHasCorrectPath(c *C) { r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) defer r1() r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) defer r2() script := `#!/bin/sh echo PATH=$PATH ls -l ${PATH##*:}/repair ` s.seqRepairs = []string{makeMockRepair(script)} s.seqRepairs = s.signSeqRepairs(c, s.seqRepairs) rpr, err := s.runner.Next("canonical") c.Assert(err, IsNil) err = rpr.Run() c.Assert(err, IsNil) c.Check(filepath.Join(s.runDir, "r0.retry"), testutil.FileMatches, fmt.Sprintf(`(?ms).*^PATH=.*:.*/run/snapd/repair/tools.*`)) c.Check(filepath.Join(s.runDir, "r0.retry"), testutil.FileContains, `/repair -> /usr/lib/snapd/snap-repair`) // run again and ensure no error happens err = rpr.Run() c.Assert(err, IsNil) } snapd-2.37.4~14.04.1/cmd/snap-repair/main_test.go0000664000000000000000000000447613435556260016052 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main_test import ( "bytes" "testing" . "gopkg.in/check.v1" repair "github.com/snapcore/snapd/cmd/snap-repair" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/httputil" "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/testutil" ) // Hook up check.v1 into the "go test" runner func Test(t *testing.T) { TestingT(t) } type repairSuite struct { testutil.BaseTest baseRunnerSuite rootdir string stdout *bytes.Buffer stderr *bytes.Buffer restore func() } func (r *repairSuite) SetUpSuite(c *C) { r.baseRunnerSuite.SetUpSuite(c) r.restore = httputil.SetUserAgentFromVersion("", "") } func (r *repairSuite) TearDownSuite(c *C) { r.restore() } func (r *repairSuite) SetUpTest(c *C) { r.BaseTest.SetUpTest(c) r.baseRunnerSuite.SetUpTest(c) r.stdout = bytes.NewBuffer(nil) r.stderr = bytes.NewBuffer(nil) oldStdout := repair.Stdout r.AddCleanup(func() { repair.Stdout = oldStdout }) repair.Stdout = r.stdout oldStderr := repair.Stderr r.AddCleanup(func() { repair.Stderr = oldStderr }) repair.Stderr = r.stderr r.rootdir = c.MkDir() dirs.SetRootDir(r.rootdir) r.AddCleanup(func() { dirs.SetRootDir("/") }) } func (r *repairSuite) Stdout() string { return r.stdout.String() } func (r *repairSuite) Stderr() string { return r.stderr.String() } var _ = Suite(&repairSuite{}) func (r *repairSuite) TestUnknownArg(c *C) { err := repair.ParseArgs([]string{}) c.Check(err, ErrorMatches, "Please specify one command of: list, run or show") } func (r *repairSuite) TestRunOnClassic(c *C) { defer release.MockOnClassic(true)() err := repair.Run() c.Check(err, ErrorMatches, "cannot use snap-repair on a classic system") } snapd-2.37.4~14.04.1/cmd/snap-repair/trace_test.go0000664000000000000000000000477413435556260016225 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main_test import ( "io/ioutil" "os" "path/filepath" . "gopkg.in/check.v1" "github.com/snapcore/snapd/dirs" ) func makeMockRepairState(c *C) { // the canonical script dir content basedir := filepath.Join(dirs.SnapRepairRunDir, "canonical/1") err := os.MkdirAll(basedir, 0700) c.Assert(err, IsNil) err = ioutil.WriteFile(filepath.Join(basedir, "r3.retry"), []byte("repair: canonical-1\nsummary: repair one\noutput:\nretry output"), 0600) c.Assert(err, IsNil) err = ioutil.WriteFile(filepath.Join(basedir, "r3.script"), []byte("#!/bin/sh\necho retry output"), 0700) c.Assert(err, IsNil) // my-brand basedir = filepath.Join(dirs.SnapRepairRunDir, "my-brand/1") err = os.MkdirAll(basedir, 0700) c.Assert(err, IsNil) err = ioutil.WriteFile(filepath.Join(basedir, "r1.done"), []byte("repair: my-brand-1\nsummary: my-brand repair one\noutput:\ndone output"), 0600) c.Assert(err, IsNil) err = ioutil.WriteFile(filepath.Join(basedir, "r1.script"), []byte("#!/bin/sh\necho done output"), 0700) c.Assert(err, IsNil) basedir = filepath.Join(dirs.SnapRepairRunDir, "my-brand/2") err = os.MkdirAll(basedir, 0700) c.Assert(err, IsNil) err = ioutil.WriteFile(filepath.Join(basedir, "r2.skip"), []byte("repair: my-brand-2\nsummary: my-brand repair two\noutput:\nskip output"), 0600) c.Assert(err, IsNil) err = ioutil.WriteFile(filepath.Join(basedir, "r2.script"), []byte("#!/bin/sh\necho skip output"), 0700) c.Assert(err, IsNil) basedir = filepath.Join(dirs.SnapRepairRunDir, "my-brand/3") err = os.MkdirAll(basedir, 0700) c.Assert(err, IsNil) err = ioutil.WriteFile(filepath.Join(basedir, "r0.running"), []byte("repair: my-brand-3\nsummary: my-brand repair three\noutput:\nrunning output"), 0600) c.Assert(err, IsNil) err = ioutil.WriteFile(filepath.Join(basedir, "r0.script"), []byte("#!/bin/sh\necho running output"), 0700) c.Assert(err, IsNil) } snapd-2.37.4~14.04.1/cmd/snap-repair/cmd_show_test.go0000664000000000000000000000615113435556260016721 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main_test import ( "fmt" "os" "path/filepath" . "gopkg.in/check.v1" repair "github.com/snapcore/snapd/cmd/snap-repair" "github.com/snapcore/snapd/dirs" ) func (r *repairSuite) TestShowRepairSingle(c *C) { makeMockRepairState(c) err := repair.ParseArgs([]string{"show", "canonical-1"}) c.Check(err, IsNil) c.Check(r.Stdout(), Equals, `repair: canonical-1 revision: 3 status: retry summary: repair one script: #!/bin/sh echo retry output output: retry output `) } func (r *repairSuite) TestShowRepairMultiple(c *C) { makeMockRepairState(c) // repair.ParseArgs() always appends to its internal slice: // cmdShow.Positional.Repair. To workaround this we create a // new cmdShow here err := repair.NewCmdShow("canonical-1", "my-brand-1", "my-brand-2").Execute(nil) c.Check(err, IsNil) c.Check(r.Stdout(), Equals, `repair: canonical-1 revision: 3 status: retry summary: repair one script: #!/bin/sh echo retry output output: retry output repair: my-brand-1 revision: 1 status: done summary: my-brand repair one script: #!/bin/sh echo done output output: done output repair: my-brand-2 revision: 2 status: skip summary: my-brand repair two script: #!/bin/sh echo skip output output: skip output `) } func (r *repairSuite) TestShowRepairErrorNoRepairDir(c *C) { dirs.SetRootDir(c.MkDir()) err := repair.NewCmdShow("canonical-1").Execute(nil) c.Check(err, ErrorMatches, `cannot find repair "canonical-1"`) } func (r *repairSuite) TestShowRepairSingleWithoutScript(c *C) { makeMockRepairState(c) scriptPath := filepath.Join(dirs.SnapRepairRunDir, "canonical/1", "r3.script") err := os.Remove(scriptPath) c.Assert(err, IsNil) err = repair.NewCmdShow("canonical-1").Execute(nil) c.Check(err, IsNil) c.Check(r.Stdout(), Equals, fmt.Sprintf(`repair: canonical-1 revision: 3 status: retry summary: repair one script: error: open %s: no such file or directory output: retry output `, scriptPath)) } func (r *repairSuite) TestShowRepairSingleUnreadableOutput(c *C) { makeMockRepairState(c) scriptPath := filepath.Join(dirs.SnapRepairRunDir, "canonical/1", "r3.retry") err := os.Chmod(scriptPath, 0000) c.Assert(err, IsNil) defer os.Chmod(scriptPath, 0644) err = repair.NewCmdShow("canonical-1").Execute(nil) c.Check(err, IsNil) c.Check(r.Stdout(), Equals, fmt.Sprintf(`repair: canonical-1 revision: 3 status: retry summary: - script: #!/bin/sh echo retry output output: error: open %s: permission denied `, scriptPath)) } snapd-2.37.4~14.04.1/cmd/snap-repair/runner.go0000664000000000000000000006470613435556260015402 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main import ( "bufio" "bytes" "crypto/tls" "encoding/json" "errors" "fmt" "io" "io/ioutil" "net/http" "net/url" "os" "os/exec" "path/filepath" "strconv" "strings" "syscall" "time" "gopkg.in/retry.v1" "github.com/snapcore/snapd/arch" "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/asserts/sysdb" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/errtracker" "github.com/snapcore/snapd/httputil" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/strutil" ) var ( // TODO: move inside the repairs themselves? defaultRepairTimeout = 30 * time.Minute ) var errtrackerReportRepair = errtracker.ReportRepair // Repair is a runnable repair. type Repair struct { *asserts.Repair run *Runner sequence int } func (r *Repair) RunDir() string { return filepath.Join(dirs.SnapRepairRunDir, r.BrandID(), strconv.Itoa(r.RepairID())) } func (r *Repair) String() string { return fmt.Sprintf("%s-%v", r.BrandID(), r.RepairID()) } // SetStatus sets the status of the repair in the state and saves the latter. func (r *Repair) SetStatus(status RepairStatus) { brandID := r.BrandID() cur := *r.run.state.Sequences[brandID][r.sequence-1] cur.Status = status r.run.setRepairState(brandID, cur) r.run.SaveState() } // makeRepairSymlink ensures $dir/repair exists and is a symlink to // /usr/lib/snapd/snap-repair func makeRepairSymlink(dir string) (err error) { // make "repair" binary available to the repair scripts via symlink // to the real snap-repair if err = os.MkdirAll(dir, 0755); err != nil { return err } old := filepath.Join(dirs.CoreLibExecDir, "snap-repair") new := filepath.Join(dir, "repair") if err := os.Symlink(old, new); err != nil && !os.IsExist(err) { return err } return nil } // Run executes the repair script leaving execution trail files on disk. func (r *Repair) Run() error { // write the script to disk rundir := r.RunDir() err := os.MkdirAll(rundir, 0775) if err != nil { return err } // ensure the script can use "repair done" repairToolsDir := filepath.Join(dirs.SnapRunRepairDir, "tools") if err := makeRepairSymlink(repairToolsDir); err != nil { return err } baseName := fmt.Sprintf("r%d", r.Revision()) script := filepath.Join(rundir, baseName+".script") err = osutil.AtomicWriteFile(script, r.Body(), 0700, 0) if err != nil { return err } logPath := filepath.Join(rundir, baseName+".running") logf, err := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return err } defer logf.Close() fmt.Fprintf(logf, "repair: %s\n", r) fmt.Fprintf(logf, "revision: %d\n", r.Revision()) fmt.Fprintf(logf, "summary: %s\n", r.Summary()) fmt.Fprintf(logf, "output:\n") statusR, statusW, err := os.Pipe() if err != nil { return err } defer statusR.Close() defer statusW.Close() logger.Debugf("executing %s", script) // run the script env := os.Environ() // we need to hardcode FD=3 because this is the FD after // exec.Command() forked. there is no way in go currently // to run something right after fork() in the child to // know the fd. However because go will close all fds // except the ones in "cmd.ExtraFiles" we are safe to set "3" env = append(env, "SNAP_REPAIR_STATUS_FD=3") env = append(env, "SNAP_REPAIR_RUN_DIR="+rundir) // inject repairToolDir into PATH so that the script can use // `repair {done,skip,retry}` var havePath bool for i, envStr := range env { if strings.HasPrefix(envStr, "PATH=") { newEnv := fmt.Sprintf("%s:%s", strings.TrimSuffix(envStr, ":"), repairToolsDir) env[i] = newEnv havePath = true } } if !havePath { env = append(env, "PATH=/usr/sbin:/usr/bin:/sbin:/bin:"+repairToolsDir) } workdir := filepath.Join(rundir, "work") if err := os.MkdirAll(workdir, 0700); err != nil { return err } cmd := exec.Command(script) cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} cmd.Env = env cmd.Dir = workdir cmd.ExtraFiles = []*os.File{statusW} cmd.Stdout = logf cmd.Stderr = logf if err = cmd.Start(); err != nil { return err } statusW.Close() // wait for repair to finish or timeout var scriptErr error killTimerCh := time.After(defaultRepairTimeout) doneCh := make(chan error) go func() { doneCh <- cmd.Wait() close(doneCh) }() select { case scriptErr = <-doneCh: // done case <-killTimerCh: if err := osutil.KillProcessGroup(cmd); err != nil { logger.Noticef("cannot kill timed out repair %s: %s", r, err) } scriptErr = fmt.Errorf("repair did not finish within %s", defaultRepairTimeout) } // read repair status pipe, use the last value status := readStatus(statusR) statusPath := filepath.Join(rundir, baseName+"."+status.String()) // if the script had an error exit status still honor what we // read from the status-pipe, however report the error if scriptErr != nil { scriptErr = fmt.Errorf("repair %s revision %d failed: %s", r, r.Revision(), scriptErr) if err := r.errtrackerReport(scriptErr, status, logPath); err != nil { logger.Noticef("cannot report error to errtracker: %s", err) } // ensure the error is present in the output log fmt.Fprintf(logf, "\n%s", scriptErr) } if err := os.Rename(logPath, statusPath); err != nil { return err } r.SetStatus(status) return nil } func readStatus(r io.Reader) RepairStatus { var status RepairStatus scanner := bufio.NewScanner(r) for scanner.Scan() { switch strings.TrimSpace(scanner.Text()) { case "done": status = DoneStatus // TODO: support having a script skip over many and up to a given repair-id # case "skip": status = SkipStatus } } if scanner.Err() != nil { return RetryStatus } return status } // errtrackerReport reports an repairErr with the given logPath to the // snap error tracker. func (r *Repair) errtrackerReport(repairErr error, status RepairStatus, logPath string) error { errMsg := repairErr.Error() scriptOutput, err := ioutil.ReadFile(logPath) if err != nil { logger.Noticef("cannot read %s", logPath) } s := fmt.Sprintf("%s/%d", r.BrandID(), r.RepairID()) dupSig := fmt.Sprintf("%s\n%s\noutput:\n%s", s, errMsg, scriptOutput) extra := map[string]string{ "Revision": strconv.Itoa(r.Revision()), "BrandID": r.BrandID(), "RepairID": strconv.Itoa(r.RepairID()), "Status": status.String(), } _, err = errtrackerReportRepair(s, errMsg, dupSig, extra) return err } // Runner implements fetching, tracking and running repairs. type Runner struct { BaseURL *url.URL cli *http.Client state state stateModified bool // sequenceNext keeps track of the next integer id in a brand sequence to considered in this run, see Next. sequenceNext map[string]int } // NewRunner returns a Runner. func NewRunner() *Runner { run := &Runner{ sequenceNext: make(map[string]int), } opts := httputil.ClientOptions{ MayLogBody: false, TLSConfig: &tls.Config{ Time: run.now, }, } run.cli = httputil.NewHTTPClient(&opts) return run } var ( fetchRetryStrategy = retry.LimitCount(7, retry.LimitTime(90*time.Second, retry.Exponential{ Initial: 500 * time.Millisecond, Factor: 2.5, }, )) peekRetryStrategy = retry.LimitCount(5, retry.LimitTime(44*time.Second, retry.Exponential{ Initial: 300 * time.Millisecond, Factor: 2.5, }, )) ) var ( ErrRepairNotFound = errors.New("repair not found") ErrRepairNotModified = errors.New("repair was not modified") ) var ( maxRepairScriptSize = 24 * 1024 * 1024 ) // Fetch retrieves a stream with the repair with the given ids and any // auxiliary assertions. If revision>=0 the request will include an // If-None-Match header with an ETag for the revision, and // ErrRepairNotModified is returned if the revision is still current. func (run *Runner) Fetch(brandID string, repairID int, revision int) (*asserts.Repair, []asserts.Assertion, error) { u, err := run.BaseURL.Parse(fmt.Sprintf("repairs/%s/%d", brandID, repairID)) if err != nil { return nil, nil, err } var r []asserts.Assertion resp, err := httputil.RetryRequest(u.String(), func() (*http.Response, error) { req, err := http.NewRequest("GET", u.String(), nil) if err != nil { return nil, err } req.Header.Set("User-Agent", httputil.UserAgent()) req.Header.Set("Accept", "application/x.ubuntu.assertion") if revision >= 0 { req.Header.Set("If-None-Match", fmt.Sprintf(`"%d"`, revision)) } return run.cli.Do(req) }, func(resp *http.Response) error { if resp.StatusCode == 200 { logger.Debugf("fetching repair %s-%d", brandID, repairID) // decode assertions dec := asserts.NewDecoderWithTypeMaxBodySize(resp.Body, map[*asserts.AssertionType]int{ asserts.RepairType: maxRepairScriptSize, }) for { a, err := dec.Decode() if err == io.EOF { break } if err != nil { return err } r = append(r, a) } if len(r) == 0 { return io.ErrUnexpectedEOF } } return nil }, fetchRetryStrategy) if err != nil { return nil, nil, err } moveTimeLowerBound := true defer func() { if moveTimeLowerBound { t, _ := http.ParseTime(resp.Header.Get("Date")) run.moveTimeLowerBound(t) } }() switch resp.StatusCode { case 200: // ok case 304: // not modified return nil, nil, ErrRepairNotModified case 404: return nil, nil, ErrRepairNotFound default: moveTimeLowerBound = false return nil, nil, fmt.Errorf("cannot fetch repair, unexpected status %d", resp.StatusCode) } repair, aux, err := checkStream(brandID, repairID, r) if err != nil { return nil, nil, fmt.Errorf("cannot fetch repair, %v", err) } if repair.Revision() <= revision { // this shouldn't happen but if it does we behave like // all the rest of assertion infrastructure and ignore // the now superseded revision return nil, nil, ErrRepairNotModified } return repair, aux, err } func checkStream(brandID string, repairID int, r []asserts.Assertion) (repair *asserts.Repair, aux []asserts.Assertion, err error) { if len(r) == 0 { return nil, nil, fmt.Errorf("empty repair assertions stream") } var ok bool repair, ok = r[0].(*asserts.Repair) if !ok { return nil, nil, fmt.Errorf("unexpected first assertion %q", r[0].Type().Name) } if repair.BrandID() != brandID || repair.RepairID() != repairID { return nil, nil, fmt.Errorf("repair id mismatch %s/%d != %s/%d", repair.BrandID(), repair.RepairID(), brandID, repairID) } return repair, r[1:], nil } type peekResp struct { Headers map[string]interface{} `json:"headers"` } // Peek retrieves the headers for the repair with the given ids. func (run *Runner) Peek(brandID string, repairID int) (headers map[string]interface{}, err error) { u, err := run.BaseURL.Parse(fmt.Sprintf("repairs/%s/%d", brandID, repairID)) if err != nil { return nil, err } var rsp peekResp resp, err := httputil.RetryRequest(u.String(), func() (*http.Response, error) { req, err := http.NewRequest("GET", u.String(), nil) if err != nil { return nil, err } req.Header.Set("User-Agent", httputil.UserAgent()) req.Header.Set("Accept", "application/json") return run.cli.Do(req) }, func(resp *http.Response) error { rsp.Headers = nil if resp.StatusCode == 200 { dec := json.NewDecoder(resp.Body) return dec.Decode(&rsp) } return nil }, peekRetryStrategy) if err != nil { return nil, err } moveTimeLowerBound := true defer func() { if moveTimeLowerBound { t, _ := http.ParseTime(resp.Header.Get("Date")) run.moveTimeLowerBound(t) } }() switch resp.StatusCode { case 200: // ok case 404: return nil, ErrRepairNotFound default: moveTimeLowerBound = false return nil, fmt.Errorf("cannot peek repair headers, unexpected status %d", resp.StatusCode) } headers = rsp.Headers if headers["brand-id"] != brandID || headers["repair-id"] != strconv.Itoa(repairID) { return nil, fmt.Errorf("cannot peek repair headers, repair id mismatch %s/%s != %s/%d", headers["brand-id"], headers["repair-id"], brandID, repairID) } return headers, nil } // deviceInfo captures information about the device. type deviceInfo struct { Brand string `json:"brand"` Model string `json:"model"` } // RepairStatus represents the possible statuses of a repair. type RepairStatus int const ( RetryStatus RepairStatus = iota SkipStatus DoneStatus ) func (rs RepairStatus) String() string { switch rs { case RetryStatus: return "retry" case SkipStatus: return "skip" case DoneStatus: return "done" default: return "unknown" } } // RepairState holds the current revision and status of a repair in a sequence of repairs. type RepairState struct { Sequence int `json:"sequence"` Revision int `json:"revision"` Status RepairStatus `json:"status"` } // state holds the atomically updated control state of the runner with sequences of repairs and their states. type state struct { Device deviceInfo `json:"device"` Sequences map[string][]*RepairState `json:"sequences,omitempty"` TimeLowerBound time.Time `json:"time-lower-bound"` } func (run *Runner) setRepairState(brandID string, state RepairState) { if run.state.Sequences == nil { run.state.Sequences = make(map[string][]*RepairState) } sequence := run.state.Sequences[brandID] if state.Sequence > len(sequence) { run.stateModified = true run.state.Sequences[brandID] = append(sequence, &state) } else if *sequence[state.Sequence-1] != state { run.stateModified = true sequence[state.Sequence-1] = &state } } func (run *Runner) readState() error { r, err := os.Open(dirs.SnapRepairStateFile) if err != nil { return err } defer r.Close() dec := json.NewDecoder(r) return dec.Decode(&run.state) } func (run *Runner) moveTimeLowerBound(t time.Time) { if t.After(run.state.TimeLowerBound) { run.stateModified = true run.state.TimeLowerBound = t.UTC() } } var timeNow = time.Now func (run *Runner) now() time.Time { now := timeNow().UTC() if now.Before(run.state.TimeLowerBound) { return run.state.TimeLowerBound } return now } func (run *Runner) initState() error { if err := os.MkdirAll(dirs.SnapRepairDir, 0775); err != nil { return fmt.Errorf("cannot create repair state directory: %v", err) } // best-effort remove old os.Remove(dirs.SnapRepairStateFile) run.state = state{} // initialize time lower bound with image built time/seed.yaml time info, err := os.Stat(filepath.Join(dirs.SnapSeedDir, "seed.yaml")) if err != nil { return err } run.moveTimeLowerBound(info.ModTime()) // initialize device info if err := run.initDeviceInfo(); err != nil { return err } run.stateModified = true return run.SaveState() } func trustedBackstore(trusted []asserts.Assertion) asserts.Backstore { trustedBS := asserts.NewMemoryBackstore() for _, t := range trusted { trustedBS.Put(t.Type(), t) } return trustedBS } func checkAuthorityID(a asserts.Assertion, trusted asserts.Backstore) error { assertType := a.Type() if assertType != asserts.AccountKeyType && assertType != asserts.AccountType { return nil } // check that account and account-key assertions are signed by // a trusted authority acctID := a.AuthorityID() _, err := trusted.Get(asserts.AccountType, []string{acctID}, asserts.AccountType.MaxSupportedFormat()) if err != nil && !asserts.IsNotFound(err) { return err } if asserts.IsNotFound(err) { return fmt.Errorf("%v not signed by trusted authority: %s", a.Ref(), acctID) } return nil } func verifySignatures(a asserts.Assertion, workBS asserts.Backstore, trusted asserts.Backstore) error { if err := checkAuthorityID(a, trusted); err != nil { return err } acctKeyMaxSuppFormat := asserts.AccountKeyType.MaxSupportedFormat() seen := make(map[string]bool) bottom := false for !bottom { u := a.Ref().Unique() if seen[u] { return fmt.Errorf("circular assertions") } seen[u] = true signKey := []string{a.SignKeyID()} key, err := trusted.Get(asserts.AccountKeyType, signKey, acctKeyMaxSuppFormat) if err != nil && !asserts.IsNotFound(err) { return err } if err == nil { bottom = true } else { key, err = workBS.Get(asserts.AccountKeyType, signKey, acctKeyMaxSuppFormat) if err != nil && !asserts.IsNotFound(err) { return err } if asserts.IsNotFound(err) { return fmt.Errorf("cannot find public key %q", signKey[0]) } if err := checkAuthorityID(key, trusted); err != nil { return err } } if err := asserts.CheckSignature(a, key.(*asserts.AccountKey), nil, time.Time{}); err != nil { return err } a = key } return nil } func (run *Runner) initDeviceInfo() error { const errPrefix = "cannot set device information: " workBS := asserts.NewMemoryBackstore() assertSeedDir := filepath.Join(dirs.SnapSeedDir, "assertions") dc, err := ioutil.ReadDir(assertSeedDir) if err != nil { return err } var model *asserts.Model for _, fi := range dc { fn := filepath.Join(assertSeedDir, fi.Name()) f, err := os.Open(fn) if err != nil { // best effort continue } dec := asserts.NewDecoder(f) for { a, err := dec.Decode() if err != nil { // best effort break } switch a.Type() { case asserts.ModelType: if model != nil { return fmt.Errorf(errPrefix + "multiple models in seed assertions") } model = a.(*asserts.Model) case asserts.AccountType, asserts.AccountKeyType: workBS.Put(a.Type(), a) } } } if model == nil { return fmt.Errorf(errPrefix + "no model assertion in seed data") } trustedBS := trustedBackstore(sysdb.Trusted()) if err := verifySignatures(model, workBS, trustedBS); err != nil { return fmt.Errorf(errPrefix+"%v", err) } acctPK := []string{model.BrandID()} acctMaxSupFormat := asserts.AccountType.MaxSupportedFormat() acct, err := trustedBS.Get(asserts.AccountType, acctPK, acctMaxSupFormat) if err != nil { var err error acct, err = workBS.Get(asserts.AccountType, acctPK, acctMaxSupFormat) if err != nil { return fmt.Errorf(errPrefix + "no brand account assertion in seed data") } } if err := verifySignatures(acct, workBS, trustedBS); err != nil { return fmt.Errorf(errPrefix+"%v", err) } run.state.Device.Brand = model.BrandID() run.state.Device.Model = model.Model() return nil } // LoadState loads the repairs' state from disk, and (re)initializes it if it's missing or corrupted. func (run *Runner) LoadState() error { err := run.readState() if err == nil { return nil } // error => initialize from scratch if !os.IsNotExist(err) { logger.Noticef("cannor read repair state: %v", err) } return run.initState() } // SaveState saves the repairs' state to disk. func (run *Runner) SaveState() error { if !run.stateModified { return nil } m, err := json.Marshal(&run.state) if err != nil { return fmt.Errorf("cannot marshal repair state: %v", err) } err = osutil.AtomicWriteFile(dirs.SnapRepairStateFile, m, 0600, 0) if err != nil { return fmt.Errorf("cannot save repair state: %v", err) } run.stateModified = false return nil } func stringList(headers map[string]interface{}, name string) ([]string, error) { v, ok := headers[name] if !ok { return nil, nil } l, ok := v.([]interface{}) if !ok { return nil, fmt.Errorf("header %q is not a list", name) } r := make([]string, len(l)) for i, v := range l { s, ok := v.(string) if !ok { return nil, fmt.Errorf("header %q contains non-string elements", name) } r[i] = s } return r, nil } // Applicable returns whether a repair with the given headers is applicable to the device. func (run *Runner) Applicable(headers map[string]interface{}) bool { if headers["disabled"] == "true" { return false } series, err := stringList(headers, "series") if err != nil { return false } if len(series) != 0 && !strutil.ListContains(series, release.Series) { return false } archs, err := stringList(headers, "architectures") if err != nil { return false } if len(archs) != 0 && !strutil.ListContains(archs, arch.UbuntuArchitecture()) { return false } brandModel := fmt.Sprintf("%s/%s", run.state.Device.Brand, run.state.Device.Model) models, err := stringList(headers, "models") if err != nil { return false } if len(models) != 0 && !strutil.ListContains(models, brandModel) { // model prefix matching: brand/prefix* hit := false for _, patt := range models { if strings.HasSuffix(patt, "*") && strings.ContainsRune(patt, '/') { if strings.HasPrefix(brandModel, strings.TrimSuffix(patt, "*")) { hit = true break } } } if !hit { return false } } return true } var errSkip = errors.New("repair unnecessary on this system") func (run *Runner) fetch(brandID string, repairID int) (repair *asserts.Repair, aux []asserts.Assertion, err error) { headers, err := run.Peek(brandID, repairID) if err != nil { return nil, nil, err } if !run.Applicable(headers) { return nil, nil, errSkip } return run.Fetch(brandID, repairID, -1) } func (run *Runner) refetch(brandID string, repairID, revision int) (repair *asserts.Repair, aux []asserts.Assertion, err error) { return run.Fetch(brandID, repairID, revision) } func (run *Runner) saveStream(brandID string, repairID int, repair *asserts.Repair, aux []asserts.Assertion) error { d := filepath.Join(dirs.SnapRepairAssertsDir, brandID, strconv.Itoa(repairID)) err := os.MkdirAll(d, 0775) if err != nil { return err } buf := &bytes.Buffer{} enc := asserts.NewEncoder(buf) r := append([]asserts.Assertion{repair}, aux...) for _, a := range r { if err := enc.Encode(a); err != nil { return fmt.Errorf("cannot encode repair assertions %s-%d for saving: %v", brandID, repairID, err) } } p := filepath.Join(d, fmt.Sprintf("r%d.repair", r[0].Revision())) return osutil.AtomicWriteFile(p, buf.Bytes(), 0600, 0) } func (run *Runner) readSavedStream(brandID string, repairID, revision int) (repair *asserts.Repair, aux []asserts.Assertion, err error) { d := filepath.Join(dirs.SnapRepairAssertsDir, brandID, strconv.Itoa(repairID)) p := filepath.Join(d, fmt.Sprintf("r%d.repair", revision)) f, err := os.Open(p) if err != nil { return nil, nil, err } defer f.Close() dec := asserts.NewDecoder(f) var r []asserts.Assertion for { a, err := dec.Decode() if err == io.EOF { break } if err != nil { return nil, nil, fmt.Errorf("cannot decode repair assertions %s-%d from disk: %v", brandID, repairID, err) } r = append(r, a) } return checkStream(brandID, repairID, r) } func (run *Runner) makeReady(brandID string, sequenceNext int) (repair *asserts.Repair, err error) { sequence := run.state.Sequences[brandID] var aux []asserts.Assertion var state RepairState if sequenceNext <= len(sequence) { // consider retries state = *sequence[sequenceNext-1] if state.Status != RetryStatus { return nil, errSkip } var err error repair, aux, err = run.refetch(brandID, state.Sequence, state.Revision) if err != nil { if err != ErrRepairNotModified { logger.Noticef("cannot refetch repair %s-%d, will retry what is on disk: %v", brandID, sequenceNext, err) } // try to use what we have already on disk repair, aux, err = run.readSavedStream(brandID, state.Sequence, state.Revision) if err != nil { return nil, err } } } else { // fetch the next repair in the sequence // assumes no gaps, each repair id is present so far, // possibly skipped var err error repair, aux, err = run.fetch(brandID, sequenceNext) if err != nil && err != errSkip { return nil, err } state = RepairState{ Sequence: sequenceNext, } if err == errSkip { // TODO: store headers to justify decision state.Status = SkipStatus run.setRepairState(brandID, state) return nil, errSkip } } // verify with signatures if err := run.Verify(repair, aux); err != nil { return nil, fmt.Errorf("cannot verify repair %s-%d: %v", brandID, state.Sequence, err) } if err := run.saveStream(brandID, state.Sequence, repair, aux); err != nil { return nil, err } state.Revision = repair.Revision() if !run.Applicable(repair.Headers()) { state.Status = SkipStatus run.setRepairState(brandID, state) return nil, errSkip } run.setRepairState(brandID, state) return repair, nil } // Next returns the next repair for the brand id sequence to run/retry or ErrRepairNotFound if there is none atm. It updates the state as required. func (run *Runner) Next(brandID string) (*Repair, error) { sequenceNext := run.sequenceNext[brandID] if sequenceNext == 0 { sequenceNext = 1 } for { repair, err := run.makeReady(brandID, sequenceNext) // SaveState is a no-op unless makeReady modified the state stateErr := run.SaveState() if err != nil && err != errSkip && err != ErrRepairNotFound { // err is a non trivial error, just log the SaveState error and report err if stateErr != nil { logger.Noticef("%v", stateErr) } return nil, err } if stateErr != nil { return nil, stateErr } if err == ErrRepairNotFound { return nil, ErrRepairNotFound } sequenceNext += 1 run.sequenceNext[brandID] = sequenceNext if err == errSkip { continue } return &Repair{ Repair: repair, run: run, sequence: sequenceNext - 1, }, nil } } // Limit trust to specific keys while there's no delegation or limited // keys support. The obtained assertion stream may also include // account keys that are directly or indirectly signed by a trusted // key. var ( trustedRepairRootKeys []*asserts.AccountKey ) // Verify verifies that the repair is properly signed by the specific // trusted root keys or by account keys in the stream (passed via aux) // directly or indirectly signed by a trusted key. func (run *Runner) Verify(repair *asserts.Repair, aux []asserts.Assertion) error { workBS := asserts.NewMemoryBackstore() for _, a := range aux { if a.Type() != asserts.AccountKeyType { continue } err := workBS.Put(asserts.AccountKeyType, a) if err != nil { return err } } trustedBS := asserts.NewMemoryBackstore() for _, t := range trustedRepairRootKeys { trustedBS.Put(asserts.AccountKeyType, t) } for _, t := range sysdb.Trusted() { if t.Type() == asserts.AccountType { trustedBS.Put(asserts.AccountType, t) } } return verifySignatures(repair, workBS, trustedBS) } snapd-2.37.4~14.04.1/cmd/snap-repair/export_test.go0000664000000000000000000000660513435556260016443 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main import ( "net/url" "time" "gopkg.in/retry.v1" "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/httputil" ) var ( ParseArgs = parseArgs Run = run ) func MockBaseURL(baseurl string) (restore func()) { orig := baseURL u, err := url.Parse(baseurl) if err != nil { panic(err) } baseURL = u return func() { baseURL = orig } } func MockFetchRetryStrategy(strategy retry.Strategy) (restore func()) { originalFetchRetryStrategy := fetchRetryStrategy fetchRetryStrategy = strategy return func() { fetchRetryStrategy = originalFetchRetryStrategy } } func MockPeekRetryStrategy(strategy retry.Strategy) (restore func()) { originalPeekRetryStrategy := peekRetryStrategy peekRetryStrategy = strategy return func() { peekRetryStrategy = originalPeekRetryStrategy } } func MockMaxRepairScriptSize(maxSize int) (restore func()) { originalMaxSize := maxRepairScriptSize maxRepairScriptSize = maxSize return func() { maxRepairScriptSize = originalMaxSize } } func MockTrustedRepairRootKeys(keys []*asserts.AccountKey) (restore func()) { original := trustedRepairRootKeys trustedRepairRootKeys = keys return func() { trustedRepairRootKeys = original } } func TrustedRepairRootKeys() []*asserts.AccountKey { return trustedRepairRootKeys } func (run *Runner) BrandModel() (brand, model string) { return run.state.Device.Brand, run.state.Device.Model } func (run *Runner) SetStateModified(modified bool) { run.stateModified = modified } func (run *Runner) SetBrandModel(brand, model string) { run.state.Device.Brand = brand run.state.Device.Model = model } func (run *Runner) TimeLowerBound() time.Time { return run.state.TimeLowerBound } func (run *Runner) TLSTime() time.Time { return httputil.BaseTransport(run.cli).TLSClientConfig.Time() } func (run *Runner) Sequence(brand string) []*RepairState { return run.state.Sequences[brand] } func (run *Runner) SetSequence(brand string, sequence []*RepairState) { if run.state.Sequences == nil { run.state.Sequences = make(map[string][]*RepairState) } run.state.Sequences[brand] = sequence } func MockDefaultRepairTimeout(d time.Duration) (restore func()) { orig := defaultRepairTimeout defaultRepairTimeout = d return func() { defaultRepairTimeout = orig } } func MockErrtrackerReportRepair(mock func(string, string, string, map[string]string) (string, error)) (restore func()) { prev := errtrackerReportRepair errtrackerReportRepair = mock return func() { errtrackerReportRepair = prev } } func MockTimeNow(f func() time.Time) (restore func()) { origTimeNow := timeNow timeNow = f return func() { timeNow = origTimeNow } } func NewCmdShow(args ...string) *cmdShow { cmdShow := &cmdShow{} cmdShow.Positional.Repair = args return cmdShow } snapd-2.37.4~14.04.1/cmd/snap-repair/cmd_list_test.go0000664000000000000000000000257513435556260016722 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main_test import ( . "gopkg.in/check.v1" repair "github.com/snapcore/snapd/cmd/snap-repair" ) func (r *repairSuite) TestListNoRepairsYet(c *C) { err := repair.ParseArgs([]string{"list"}) c.Check(err, IsNil) c.Check(r.Stdout(), Equals, "") c.Check(r.Stderr(), Equals, "no repairs yet\n") } func (r *repairSuite) TestListRepairsSimple(c *C) { makeMockRepairState(c) err := repair.ParseArgs([]string{"list"}) c.Check(err, IsNil) c.Check(r.Stdout(), Equals, `Repair Rev Status Summary canonical-1 3 retry repair one my-brand-1 1 done my-brand repair one my-brand-2 2 skip my-brand repair two my-brand-3 0 running my-brand repair three `) c.Check(r.Stderr(), Equals, "") } snapd-2.37.4~14.04.1/cmd/snapd-generator/0000775000000000000000000000000013435556260014375 5ustar snapd-2.37.4~14.04.1/cmd/snapd-generator/main.c0000664000000000000000000000605013435556260015466 0ustar /* * 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 . * */ #include #include #include #include "config.h" #include "../libsnap-confine-private/cleanup-funcs.h" #include "../libsnap-confine-private/mountinfo.h" #include "../libsnap-confine-private/string-utils.h" static struct sc_mountinfo_entry *find_root_mountinfo(struct sc_mountinfo *mounts) { struct sc_mountinfo_entry *cur, *root = NULL; for (cur = sc_first_mountinfo_entry(mounts); cur != NULL; cur = sc_next_mountinfo_entry(cur)) { // Look for the mount info entry for the root file-system. if (sc_streq("/", cur->mount_dir)) { root = cur; } } return root; } int main(int argc, char **argv) { if (argc != 4) { printf("usage: snapd-workaround-generator " "normal-dir early-dir late-dir\n"); return 1; } const char *normal_dir = argv[1]; // For reference, but we don't use those variables here. // const char *early_dir = argv[2]; // const char *late_dir = argv[3]; // Load /proc/self/mountinfo so that we can inspect the root filesystem. struct sc_mountinfo *mounts SC_CLEANUP(sc_cleanup_mountinfo) = NULL; mounts = sc_parse_mountinfo(NULL); if (!mounts) { fprintf(stderr, "cannot open or parse /proc/self/mountinfo\n"); return 1; } struct sc_mountinfo_entry *root = find_root_mountinfo(mounts); if (!root) { fprintf(stderr, "cannot find mountinfo entry of the root filesystem\n"); return 1; } // Check if the root file-system is mounted with shared option. if (strstr(root->optional_fields, "shared:") != NULL) { // The workaround is not needed, everything is good as-is. return 0; } // Construct the file name for a new systemd mount unit. char fname[PATH_MAX + 1] = { 0 }; sc_must_snprintf(fname, sizeof fname, "%s/" SNAP_MOUNT_DIR ".mount", normal_dir); // Open the mount unit and write the contents. FILE *f SC_CLEANUP(sc_cleanup_file) = NULL; f = fopen(fname, "wt"); if (!f) { fprintf(stderr, "cannot open %s: %m\n", fname); return 1; } fprintf(f, "# Ensure that snap mount directory is mounted \"shared\" " "so snaps can be refreshed correctly (LP: #1668759).\n"); fprintf(f, "[Unit]\n"); fprintf(f, "Description=Ensure that the snap directory " "shares mount events.\n"); fprintf(f, "[Mount]\n"); fprintf(f, "What=" SNAP_MOUNT_DIR "\n"); fprintf(f, "Where=" SNAP_MOUNT_DIR "\n"); fprintf(f, "Type=none\n"); fprintf(f, "Options=bind,shared\n"); fprintf(f, "[Install]\n"); fprintf(f, "WantedBy=local-fs.target\n"); return 0; } snapd-2.37.4~14.04.1/cmd/snap/0000775000000000000000000000000013435556260012245 5ustar snapd-2.37.4~14.04.1/cmd/snap/cmd_pack_test.go0000664000000000000000000000555413435556260015405 0ustar package main_test import ( "io/ioutil" "os" "path/filepath" "gopkg.in/check.v1" snaprun "github.com/snapcore/snapd/cmd/snap" "github.com/snapcore/snapd/logger" ) const packSnapYaml = `name: hello version: 1.0.1 apps: app: command: bin/hello ` func makeSnapDirForPack(c *check.C, snapYaml string) string { tempdir := c.MkDir() c.Assert(os.Chmod(tempdir, 0755), check.IsNil) // use meta/snap.yaml metaDir := filepath.Join(tempdir, "meta") err := os.Mkdir(metaDir, 0755) c.Assert(err, check.IsNil) err = ioutil.WriteFile(filepath.Join(metaDir, "snap.yaml"), []byte(snapYaml), 0644) c.Assert(err, check.IsNil) return tempdir } func (s *SnapSuite) TestPackCheckSkeletonNoAppFiles(c *check.C) { _, r := logger.MockLogger() defer r() snapDir := makeSnapDirForPack(c, packSnapYaml) // check-skeleton does not fail due to missing files _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"pack", "--check-skeleton", snapDir}) c.Assert(err, check.IsNil) } func (s *SnapSuite) TestPackCheckSkeletonBadMeta(c *check.C) { // no snap name snapYaml := ` version: foobar apps: ` snapDir := makeSnapDirForPack(c, snapYaml) _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"pack", "--check-skeleton", snapDir}) c.Assert(err, check.ErrorMatches, `cannot validate snap "": snap name cannot be empty`) } func (s *SnapSuite) TestPackCheckSkeletonConflictingCommonID(c *check.C) { // conflicting common-id snapYaml := `name: foo version: foobar apps: foo: common-id: org.foo.foo bar: common-id: org.foo.foo ` snapDir := makeSnapDirForPack(c, snapYaml) _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"pack", "--check-skeleton", snapDir}) c.Assert(err, check.ErrorMatches, `cannot validate snap "foo": application ("bar" common-id "org.foo.foo" must be unique, already used by application "foo"|"foo" common-id "org.foo.foo" must be unique, already used by application "bar")`) } func (s *SnapSuite) TestPackPacksFailsForMissingPaths(c *check.C) { _, r := logger.MockLogger() defer r() snapDir := makeSnapDirForPack(c, packSnapYaml) _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"pack", snapDir, snapDir}) c.Assert(err, check.ErrorMatches, ".* snap is unusable due to missing files") } func (s *SnapSuite) TestPackPacksASnap(c *check.C) { snapDir := makeSnapDirForPack(c, packSnapYaml) const helloBinContent = `#!/bin/sh printf "hello world" ` // an example binary binDir := filepath.Join(snapDir, "bin") err := os.Mkdir(binDir, 0755) c.Assert(err, check.IsNil) err = ioutil.WriteFile(filepath.Join(binDir, "hello"), []byte(helloBinContent), 0755) c.Assert(err, check.IsNil) _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"pack", snapDir, snapDir}) c.Assert(err, check.IsNil) matches, err := filepath.Glob(snapDir + "/hello*.snap") c.Assert(err, check.IsNil) c.Assert(matches, check.HasLen, 1) } snapd-2.37.4~14.04.1/cmd/snap/cmd_buy_test.go0000664000000000000000000002776313435556260015274 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main_test import ( "encoding/json" "fmt" "net/http" "gopkg.in/check.v1" snap "github.com/snapcore/snapd/cmd/snap" ) type BuySnapSuite struct { BaseSnapSuite } var _ = check.Suite(&BuySnapSuite{}) type expectedURL struct { Body string Checker func(r *http.Request) callCount int } type expectedMethod map[string]*expectedURL type expectedMethods map[string]*expectedMethod type buyTestMockSnapServer struct { ExpectedMethods expectedMethods Checker *check.C } func (s *buyTestMockSnapServer) serveHttp(w http.ResponseWriter, r *http.Request) { method := s.ExpectedMethods[r.Method] if method == nil || len(*method) == 0 { s.Checker.Fatalf("unexpected HTTP method %s", r.Method) } url := (*method)[r.URL.Path] if url == nil { s.Checker.Fatalf("unexpected URL %q", r.URL.Path) } if url.Checker != nil { url.Checker(r) } fmt.Fprintln(w, url.Body) url.callCount++ } func (s *buyTestMockSnapServer) checkCounts() { for _, method := range s.ExpectedMethods { for _, url := range *method { s.Checker.Check(url.callCount, check.Equals, 1) } } } func (s *BuySnapSuite) SetUpTest(c *check.C) { s.BaseSnapSuite.SetUpTest(c) s.Login(c) } func (s *BuySnapSuite) TearDownTest(c *check.C) { s.Logout(c) s.BaseSnapSuite.TearDownTest(c) } func (s *BuySnapSuite) TestBuyHelp(c *check.C) { _, err := snap.Parser(snap.Client()).ParseArgs([]string{"buy"}) c.Assert(err, check.NotNil) c.Check(err.Error(), check.Equals, "the required argument `` was not provided") c.Check(s.Stdout(), check.Equals, "") c.Check(s.Stderr(), check.Equals, "") } func (s *BuySnapSuite) TestBuyInvalidCharacters(c *check.C) { _, err := snap.Parser(snap.Client()).ParseArgs([]string{"buy", "a:b"}) c.Assert(err, check.NotNil) c.Check(err.Error(), check.Equals, "cannot buy snap: invalid characters in name") c.Check(s.Stdout(), check.Equals, "") c.Check(s.Stderr(), check.Equals, "") _, err = snap.Parser(snap.Client()).ParseArgs([]string{"buy", "c*d"}) c.Assert(err, check.NotNil) c.Check(err.Error(), check.Equals, "cannot buy snap: invalid characters in name") c.Check(s.Stdout(), check.Equals, "") c.Check(s.Stderr(), check.Equals, "") } const buyFreeSnapFailsFindJson = ` { "type": "sync", "status-code": 200, "status": "OK", "result": [ { "channel": "stable", "confinement": "strict", "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", "developer": "canonical", "publisher": { "id": "canonical", "username": "canonical", "display-name": "Canonical", "validation": "verified" }, "download-size": 65536, "icon": "", "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", "name": "hello", "private": false, "resource": "/v2/snaps/hello", "revision": "1", "status": "available", "summary": "GNU Hello, the \"hello world\" snap", "type": "app", "version": "2.10" } ], "sources": [ "store" ], "suggested-currency": "GBP" } ` func (s *BuySnapSuite) TestBuyFreeSnapFails(c *check.C) { mockServer := &buyTestMockSnapServer{ ExpectedMethods: expectedMethods{ "GET": &expectedMethod{ "/v2/find": &expectedURL{ Body: buyFreeSnapFailsFindJson, }, }, }, Checker: c, } defer mockServer.checkCounts() s.RedirectClientToTestServer(mockServer.serveHttp) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"buy", "hello"}) c.Assert(err, check.NotNil) c.Check(err.Error(), check.Equals, "cannot buy snap: snap is free") c.Assert(rest, check.DeepEquals, []string{"hello"}) c.Check(s.Stdout(), check.Equals, "") c.Check(s.Stderr(), check.Equals, "") } const buySnapFindJson = ` { "type": "sync", "status-code": 200, "status": "OK", "result": [ { "channel": "stable", "confinement": "strict", "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", "developer": "canonical", "publisher": { "id": "canonical", "username": "canonical", "display-name": "Canonical", "validation": "verified" }, "download-size": 65536, "icon": "", "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", "name": "hello", "private": false, "resource": "/v2/snaps/hello", "revision": "1", "status": "priced", "summary": "GNU Hello, the \"hello world\" snap", "type": "app", "version": "2.10", "prices": {"USD": 3.99, "GBP": 2.99} } ], "sources": [ "store" ], "suggested-currency": "GBP" } ` func buySnapFindURL(c *check.C) *expectedURL { return &expectedURL{ Body: buySnapFindJson, Checker: func(r *http.Request) { c.Check(r.URL.Query().Get("name"), check.Equals, "hello") }, } } const buyReadyJson = ` { "type": "sync", "status-code": 200, "status": "OK", "result": true, "sources": [ "store" ], "suggested-currency": "GBP" } ` func buyReady(c *check.C) *expectedURL { return &expectedURL{ Body: buyReadyJson, } } const buySnapJson = ` { "type": "sync", "status-code": 200, "status": "OK", "result": { "state": "Complete" }, "sources": [ "store" ], "suggested-currency": "GBP" } ` const loginJson = ` { "type": "sync", "status-code": 200, "status": "OK", "result": { "id": 1, "username": "username", "email": "hello@mail.com", "macaroon": "1234abcd", "discharges": ["a", "b", "c"] }, "sources": [ "store" ] } ` func (s *BuySnapSuite) TestBuySnapSuccess(c *check.C) { mockServer := &buyTestMockSnapServer{ ExpectedMethods: expectedMethods{ "GET": &expectedMethod{ "/v2/find": buySnapFindURL(c), "/v2/buy/ready": buyReady(c), }, "POST": &expectedMethod{ "/v2/login": &expectedURL{ Body: loginJson, }, "/v2/buy": &expectedURL{ Body: buySnapJson, Checker: func(r *http.Request) { var postData struct { SnapID string `json:"snap-id"` Price float64 `json:"price"` Currency string `json:"currency"` } decoder := json.NewDecoder(r.Body) err := decoder.Decode(&postData) c.Assert(err, check.IsNil) c.Check(postData.SnapID, check.Equals, "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6") c.Check(postData.Price, check.Equals, 2.99) c.Check(postData.Currency, check.Equals, "GBP") }, }, }, }, Checker: c, } defer mockServer.checkCounts() s.RedirectClientToTestServer(mockServer.serveHttp) // Confirm the purchase. s.password = "the password" rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"buy", "hello"}) c.Check(err, check.IsNil) c.Check(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, `Please re-enter your Ubuntu One password to purchase "hello" from "canonical" for 2.99GBP. Press ctrl-c to cancel. Password of "hello@mail.com": Thanks for purchasing "hello". You may now install it on any of your devices with 'snap install hello'. `) c.Check(s.Stderr(), check.Equals, "") } const buySnapPaymentDeclinedJson = ` { "type": "error", "result": { "message": "payment declined", "kind": "payment-declined" }, "status-code": 400 } ` func (s *BuySnapSuite) TestBuySnapPaymentDeclined(c *check.C) { mockServer := &buyTestMockSnapServer{ ExpectedMethods: expectedMethods{ "GET": &expectedMethod{ "/v2/find": buySnapFindURL(c), "/v2/buy/ready": buyReady(c), }, "POST": &expectedMethod{ "/v2/login": &expectedURL{ Body: loginJson, }, "/v2/buy": &expectedURL{ Body: buySnapPaymentDeclinedJson, Checker: func(r *http.Request) { var postData struct { SnapID string `json:"snap-id"` Price float64 `json:"price"` Currency string `json:"currency"` } decoder := json.NewDecoder(r.Body) err := decoder.Decode(&postData) c.Assert(err, check.IsNil) c.Check(postData.SnapID, check.Equals, "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6") c.Check(postData.Price, check.Equals, 2.99) c.Check(postData.Currency, check.Equals, "GBP") }, }, }, }, Checker: c, } defer mockServer.checkCounts() s.RedirectClientToTestServer(mockServer.serveHttp) // Confirm the purchase. s.password = "the password" rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"buy", "hello"}) c.Assert(err, check.NotNil) c.Check(err.Error(), check.Equals, `Sorry, your payment method has been declined by the issuer. Please review your payment details at https://my.ubuntu.com/payment/edit and try again.`) c.Check(rest, check.DeepEquals, []string{"hello"}) c.Check(s.Stdout(), check.Equals, `Please re-enter your Ubuntu One password to purchase "hello" from "canonical" for 2.99GBP. Press ctrl-c to cancel. Password of "hello@mail.com": `) c.Check(s.Stderr(), check.Equals, "") } const readyToBuyNoPaymentMethodJson = ` { "type": "error", "result": { "message": "no payment methods", "kind": "no-payment-methods" }, "status-code": 400 }` func (s *BuySnapSuite) TestBuySnapFailsNoPaymentMethod(c *check.C) { mockServer := &buyTestMockSnapServer{ ExpectedMethods: expectedMethods{ "GET": &expectedMethod{ "/v2/find": buySnapFindURL(c), "/v2/buy/ready": &expectedURL{ Body: readyToBuyNoPaymentMethodJson, }, }, }, Checker: c, } defer mockServer.checkCounts() s.RedirectClientToTestServer(mockServer.serveHttp) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"buy", "hello"}) c.Assert(err, check.NotNil) c.Check(err.Error(), check.Equals, `You need to have a payment method associated with your account in order to buy a snap, please visit https://my.ubuntu.com/payment/edit to add one. Once you’ve added your payment details, you just need to run 'snap buy hello' again.`) c.Check(rest, check.DeepEquals, []string{"hello"}) c.Check(s.Stdout(), check.Equals, "") c.Check(s.Stderr(), check.Equals, "") } const readyToBuyNotAcceptedTermsJson = ` { "type": "error", "result": { "message": "terms of service not accepted", "kind": "terms-not-accepted" }, "status-code": 400 }` func (s *BuySnapSuite) TestBuySnapFailsNotAcceptedTerms(c *check.C) { mockServer := &buyTestMockSnapServer{ ExpectedMethods: expectedMethods{ "GET": &expectedMethod{ "/v2/find": buySnapFindURL(c), "/v2/buy/ready": &expectedURL{ Body: readyToBuyNotAcceptedTermsJson, }, }, }, Checker: c, } defer mockServer.checkCounts() s.RedirectClientToTestServer(mockServer.serveHttp) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"buy", "hello"}) c.Assert(err, check.NotNil) c.Check(err.Error(), check.Equals, `In order to buy "hello", you need to agree to the latest terms and conditions. Please visit https://my.ubuntu.com/payment/edit to do this. Once completed, return here and run 'snap buy hello' again.`) c.Check(rest, check.DeepEquals, []string{"hello"}) c.Check(s.Stdout(), check.Equals, "") c.Check(s.Stderr(), check.Equals, "") } func (s *BuySnapSuite) TestBuyFailsWithoutLogin(c *check.C) { // We don't login here s.Logout(c) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"buy", "hello"}) c.Check(err, check.NotNil) c.Check(err.Error(), check.Equals, "You need to be logged in to purchase software. Please run 'snap login' and try again.") c.Check(rest, check.DeepEquals, []string{"hello"}) c.Check(s.Stdout(), check.Equals, "") c.Check(s.Stderr(), check.Equals, "") } snapd-2.37.4~14.04.1/cmd/snap/cmd_create_user_test.go0000664000000000000000000001130613435556260016760 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main_test import ( "encoding/json" "fmt" "net/http" "gopkg.in/check.v1" "github.com/snapcore/snapd/client" snap "github.com/snapcore/snapd/cmd/snap" ) func makeCreateUserChecker(c *check.C, n *int, email string, sudoer, known bool) func(w http.ResponseWriter, r *http.Request) { f := func(w http.ResponseWriter, r *http.Request) { switch *n { case 0: c.Check(r.Method, check.Equals, "POST") c.Check(r.URL.Path, check.Equals, "/v2/create-user") var gotBody map[string]interface{} dec := json.NewDecoder(r.Body) err := dec.Decode(&gotBody) c.Assert(err, check.IsNil) wantBody := make(map[string]interface{}) if email != "" { wantBody["email"] = "one@email.com" } if sudoer { wantBody["sudoer"] = true } if known { wantBody["known"] = true } c.Check(gotBody, check.DeepEquals, wantBody) if email == "" { fmt.Fprintln(w, `{"type": "sync", "result": [{"username": "karl", "ssh-keys": ["a","b"]}]}`) } else { fmt.Fprintln(w, `{"type": "sync", "result": {"username": "karl", "ssh-keys": ["a","b"]}}`) } default: c.Fatalf("got too many requests (now on %d)", *n+1) } *n++ } return f } func (s *SnapSuite) TestCreateUserNoSudoer(c *check.C) { n := 0 s.RedirectClientToTestServer(makeCreateUserChecker(c, &n, "one@email.com", false, false)) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"create-user", "one@email.com"}) c.Assert(err, check.IsNil) c.Check(rest, check.DeepEquals, []string{}) c.Check(n, check.Equals, 1) c.Assert(s.Stdout(), check.Equals, `created user "karl"`+"\n") c.Assert(s.Stderr(), check.Equals, "") } func (s *SnapSuite) TestCreateUserSudoer(c *check.C) { n := 0 s.RedirectClientToTestServer(makeCreateUserChecker(c, &n, "one@email.com", true, false)) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"create-user", "--sudoer", "one@email.com"}) c.Assert(err, check.IsNil) c.Check(rest, check.DeepEquals, []string{}) c.Check(n, check.Equals, 1) c.Assert(s.Stdout(), check.Equals, `created user "karl"`+"\n") c.Assert(s.Stderr(), check.Equals, "") } func (s *SnapSuite) TestCreateUserJSON(c *check.C) { n := 0 s.RedirectClientToTestServer(makeCreateUserChecker(c, &n, "one@email.com", false, false)) expectedResponse := &client.CreateUserResult{ Username: "karl", SSHKeys: []string{"a", "b"}, } actualResponse := &client.CreateUserResult{} rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"create-user", "--json", "one@email.com"}) c.Assert(err, check.IsNil) c.Check(rest, check.DeepEquals, []string{}) c.Check(n, check.Equals, 1) json.Unmarshal(s.stdout.Bytes(), actualResponse) c.Assert(actualResponse, check.DeepEquals, expectedResponse) c.Assert(s.Stderr(), check.Equals, "") } func (s *SnapSuite) TestCreateUserNoEmailJSON(c *check.C) { n := 0 s.RedirectClientToTestServer(makeCreateUserChecker(c, &n, "", false, true)) var expectedResponse = []*client.CreateUserResult{{ Username: "karl", SSHKeys: []string{"a", "b"}, }} var actualResponse []*client.CreateUserResult rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"create-user", "--json", "--known"}) c.Assert(err, check.IsNil) c.Check(rest, check.DeepEquals, []string{}) c.Check(n, check.Equals, 1) json.Unmarshal(s.stdout.Bytes(), &actualResponse) c.Assert(actualResponse, check.DeepEquals, expectedResponse) c.Assert(s.Stderr(), check.Equals, "") } func (s *SnapSuite) TestCreateUserKnown(c *check.C) { n := 0 s.RedirectClientToTestServer(makeCreateUserChecker(c, &n, "one@email.com", false, true)) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"create-user", "--known", "one@email.com"}) c.Assert(err, check.IsNil) c.Check(rest, check.DeepEquals, []string{}) c.Check(n, check.Equals, 1) } func (s *SnapSuite) TestCreateUserKnownNoEmail(c *check.C) { n := 0 s.RedirectClientToTestServer(makeCreateUserChecker(c, &n, "", false, true)) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"create-user", "--known"}) c.Assert(err, check.IsNil) c.Check(rest, check.DeepEquals, []string{}) c.Check(n, check.Equals, 1) } snapd-2.37.4~14.04.1/cmd/snap/cmd_managed_test.go0000664000000000000000000000245313435556260016056 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main_test import ( "fmt" "net/http" . "gopkg.in/check.v1" snap "github.com/snapcore/snapd/cmd/snap" ) func (s *SnapSuite) TestManaged(c *C) { for _, managed := range []bool{true, false} { s.stdout.Truncate(0) s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, Equals, "GET") c.Check(r.URL.Path, Equals, "/v2/system-info") fmt.Fprintf(w, `{"type":"sync", "status-code": 200, "result": {"managed":%v}}`, managed) }) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"managed"}) c.Assert(err, IsNil) c.Check(s.Stdout(), Equals, fmt.Sprintf("%v\n", managed)) } } snapd-2.37.4~14.04.1/cmd/snap/cmd_changes.go0000664000000000000000000001170713435556260015035 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main import ( "fmt" "regexp" "sort" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/i18n" "github.com/jessevdk/go-flags" ) var shortChangesHelp = i18n.G("List system changes") var shortTasksHelp = i18n.G("List a change's tasks") var longChangesHelp = i18n.G(` The changes command displays a summary of system changes performed recently. `) var longTasksHelp = i18n.G(` The tasks command displays a summary of tasks associated with an individual change. `) type cmdChanges struct { clientMixin timeMixin Positional struct { Snap string `positional-arg-name:""` } `positional-args:"yes"` } type cmdTasks struct { timeMixin changeIDMixin } func init() { addCommand("changes", shortChangesHelp, longChangesHelp, func() flags.Commander { return &cmdChanges{} }, timeDescs, nil) addCommand("tasks", shortTasksHelp, longTasksHelp, func() flags.Commander { return &cmdTasks{} }, changeIDMixinOptDesc.also(timeDescs), changeIDMixinArgDesc).alias = "change" } type changesByTime []*client.Change func (s changesByTime) Len() int { return len(s) } func (s changesByTime) Less(i, j int) bool { return s[i].SpawnTime.Before(s[j].SpawnTime) } func (s changesByTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] } var allDigits = regexp.MustCompile(`^[0-9]+$`).MatchString func queryChanges(cli *client.Client, opts *client.ChangesOptions) ([]*client.Change, error) { chgs, err := cli.Changes(opts) if err != nil { return nil, err } if err := warnMaintenance(cli); err != nil { return nil, err } return chgs, nil } func (c *cmdChanges) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } if allDigits(c.Positional.Snap) { // TRANSLATORS: the %s is the argument given by the user to 'snap changes' return fmt.Errorf(i18n.G(`'snap changes' command expects a snap name, try 'snap tasks %s'`), c.Positional.Snap) } if c.Positional.Snap == "everything" { fmt.Fprintln(Stdout, i18n.G("Yes, yes it does.")) return nil } opts := client.ChangesOptions{ SnapName: c.Positional.Snap, Selector: client.ChangesAll, } changes, err := queryChanges(c.client, &opts) if err != nil { return err } if len(changes) == 0 { return fmt.Errorf(i18n.G("no changes found")) } sort.Sort(changesByTime(changes)) w := tabWriter() fmt.Fprintf(w, i18n.G("ID\tStatus\tSpawn\tReady\tSummary\n")) for _, chg := range changes { spawnTime := c.fmtTime(chg.SpawnTime) readyTime := c.fmtTime(chg.ReadyTime) if chg.ReadyTime.IsZero() { readyTime = "-" } fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", chg.ID, chg.Status, spawnTime, readyTime, chg.Summary) } w.Flush() fmt.Fprintln(Stdout) return nil } func (c *cmdTasks) Execute([]string) error { chid, err := c.GetChangeID() if err != nil { if err == noChangeFoundOK { return nil } return err } return c.showChange(chid) } func queryChange(cli *client.Client, chid string) (*client.Change, error) { chg, err := cli.Change(chid) if err != nil { return nil, err } if err := warnMaintenance(cli); err != nil { return nil, err } return chg, nil } func (c *cmdTasks) showChange(chid string) error { chg, err := queryChange(c.client, chid) if err != nil { return err } w := tabWriter() fmt.Fprintf(w, i18n.G("Status\tSpawn\tReady\tSummary\n")) for _, t := range chg.Tasks { spawnTime := c.fmtTime(t.SpawnTime) readyTime := c.fmtTime(t.ReadyTime) if t.ReadyTime.IsZero() { readyTime = "-" } summary := t.Summary if t.Status == "Doing" && t.Progress.Total > 1 { summary = fmt.Sprintf("%s (%.2f%%)", summary, float64(t.Progress.Done)/float64(t.Progress.Total)*100.0) } fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", t.Status, spawnTime, readyTime, summary) } w.Flush() for _, t := range chg.Tasks { if len(t.Log) == 0 { continue } fmt.Fprintln(Stdout) fmt.Fprintln(Stdout, line) fmt.Fprintln(Stdout, t.Summary) fmt.Fprintln(Stdout) for _, line := range t.Log { fmt.Fprintln(Stdout, line) } } fmt.Fprintln(Stdout) return nil } const line = "......................................................................" func warnMaintenance(cli *client.Client) error { if maintErr := cli.Maintenance(); maintErr != nil { msg, err := errorToCmdMessage("", maintErr, nil) if err != nil { return err } fmt.Fprintf(Stderr, "WARNING: %s\n", msg) } return nil } snapd-2.37.4~14.04.1/cmd/snap/cmd_create_key.go0000664000000000000000000000451013435556260015532 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main import ( "errors" "fmt" "github.com/jessevdk/go-flags" "golang.org/x/crypto/ssh/terminal" "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/i18n" ) type cmdCreateKey struct { Positional struct { KeyName string } `positional-args:"true"` } func init() { cmd := addCommand("create-key", i18n.G("Create cryptographic key pair"), i18n.G(` The create-key command creates a cryptographic key pair that can be used for signing assertions. `), func() flags.Commander { return &cmdCreateKey{} }, nil, []argDesc{{ // TRANSLATORS: This needs to begin with < and end with > name: i18n.G(""), // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("Name of key to create; defaults to 'default'"), }}) cmd.hidden = true } func (x *cmdCreateKey) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } keyName := x.Positional.KeyName if keyName == "" { keyName = "default" } if !asserts.IsValidAccountKeyName(keyName) { return fmt.Errorf(i18n.G("key name %q is not valid; only ASCII letters, digits, and hyphens are allowed"), keyName) } fmt.Fprint(Stdout, i18n.G("Passphrase: ")) passphrase, err := terminal.ReadPassword(0) fmt.Fprint(Stdout, "\n") if err != nil { return err } fmt.Fprint(Stdout, i18n.G("Confirm passphrase: ")) confirmPassphrase, err := terminal.ReadPassword(0) fmt.Fprint(Stdout, "\n") if err != nil { return err } if string(passphrase) != string(confirmPassphrase) { return errors.New("passphrases do not match") } if err != nil { return err } manager := asserts.NewGPGKeypairManager() return manager.Generate(string(passphrase), keyName) } snapd-2.37.4~14.04.1/cmd/snap/cmd_services_test.go0000664000000000000000000001707013435556260016306 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main_test import ( "encoding/json" "fmt" "net/http" "time" "gopkg.in/check.v1" "github.com/snapcore/snapd/client" snap "github.com/snapcore/snapd/cmd/snap" ) type appOpSuite struct { BaseSnapSuite restoreAll func() } var _ = check.Suite(&appOpSuite{}) func (s *appOpSuite) SetUpTest(c *check.C) { s.BaseSnapSuite.SetUpTest(c) restoreClientRetry := client.MockDoRetry(time.Millisecond, 10*time.Millisecond) restorePollTime := snap.MockPollTime(time.Millisecond) s.restoreAll = func() { restoreClientRetry() restorePollTime() } } func (s *appOpSuite) TearDownTest(c *check.C) { s.restoreAll() s.BaseSnapSuite.TearDownTest(c) } func (s *appOpSuite) expectedBody(op string, names []string, extra []string) map[string]interface{} { inames := make([]interface{}, len(names)) for i, name := range names { inames[i] = name } expectedBody := map[string]interface{}{ "action": op, "names": inames, } for _, x := range extra { expectedBody[x] = true } return expectedBody } func (s *appOpSuite) args(op string, names []string, extra []string, noWait bool) []string { args := []string{op} if noWait { args = append(args, "--no-wait") } for _, x := range extra { args = append(args, "--"+x) } args = append(args, names...) return args } func (s *appOpSuite) testOpNoArgs(c *check.C, op string) { s.RedirectClientToTestServer(nil) _, err := snap.Parser(snap.Client()).ParseArgs([]string{op}) c.Assert(err, check.ErrorMatches, `.* required argument .* not provided`) } func (s *appOpSuite) testOpErrorResponse(c *check.C, op string, names []string, extra []string, noWait bool) { n := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch n { case 0: c.Check(r.Method, check.Equals, "POST") c.Check(r.URL.Path, check.Equals, "/v2/apps") c.Check(r.URL.Query(), check.HasLen, 0) c.Check(DecodedRequestBody(c, r), check.DeepEquals, s.expectedBody(op, names, extra)) w.WriteHeader(400) fmt.Fprintln(w, `{"type": "error", "result": {"message": "error"}, "status-code": 400}`) default: c.Fatalf("expected to get 1 requests, now on %d", n+1) } n++ }) _, err := snap.Parser(snap.Client()).ParseArgs(s.args(op, names, extra, noWait)) c.Assert(err, check.ErrorMatches, "error") c.Check(n, check.Equals, 1) } func (s *appOpSuite) testOp(c *check.C, op, summary string, names []string, extra []string, noWait bool) { n := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch n { case 0: c.Check(r.URL.Path, check.Equals, "/v2/apps") c.Check(r.URL.Query(), check.HasLen, 0) c.Check(DecodedRequestBody(c, r), check.DeepEquals, s.expectedBody(op, names, extra)) c.Check(r.Method, check.Equals, "POST") w.WriteHeader(202) fmt.Fprintln(w, `{"type":"async", "change": "42", "status-code": 202}`) case 1: c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/changes/42") fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}}`) case 2: c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/changes/42") fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done"}}`) default: c.Fatalf("expected to get 2 requests, now on %d", n+1) } n++ }) rest, err := snap.Parser(snap.Client()).ParseArgs(s.args(op, names, extra, noWait)) c.Assert(err, check.IsNil) c.Assert(rest, check.HasLen, 0) c.Check(s.Stderr(), check.Equals, "") expectedN := 3 if noWait { summary = "42" expectedN = 1 } c.Check(s.Stdout(), check.Equals, summary+"\n") // ensure that the fake server api was actually hit c.Check(n, check.Equals, expectedN) } func (s *appOpSuite) TestAppOps(c *check.C) { extras := []string{"enable", "disable", "reload"} summaries := []string{"Started.", "Stopped.", "Restarted."} for i, op := range []string{"start", "stop", "restart"} { s.testOpNoArgs(c, op) for _, extra := range [][]string{nil, {extras[i]}} { for _, noWait := range []bool{false, true} { for _, names := range [][]string{ {"foo"}, {"foo", "bar"}, {"foo", "bar.baz"}, } { s.testOpErrorResponse(c, op, names, extra, noWait) s.testOp(c, op, summaries[i], names, extra, noWait) s.stdout.Reset() } } } } } func (s *appOpSuite) TestAppStatus(c *check.C) { n := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch n { case 0: c.Check(r.URL.Path, check.Equals, "/v2/apps") c.Check(r.URL.Query(), check.HasLen, 1) c.Check(r.URL.Query().Get("select"), check.Equals, "service") c.Check(r.Method, check.Equals, "GET") w.WriteHeader(200) enc := json.NewEncoder(w) enc.Encode(map[string]interface{}{ "type": "sync", "result": []map[string]interface{}{ {"snap": "foo", "name": "bar", "daemon": "oneshot", "active": false, "enabled": true, "activators": []map[string]interface{}{ {"name": "bar", "type": "timer", "active": true, "enabled": true}, }, }, {"snap": "foo", "name": "baz", "daemon": "oneshot", "active": false, "enabled": true, "activators": []map[string]interface{}{ {"name": "baz-sock1", "type": "socket", "active": true, "enabled": true}, {"name": "baz-sock2", "type": "socket", "active": false, "enabled": true}, }, }, {"snap": "foo", "name": "zed", "active": true, "enabled": true, }, }, "status": "OK", "status-code": 200, }) default: c.Fatalf("expected to get 1 requests, now on %d", n+1) } n++ }) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"services"}) c.Assert(err, check.IsNil) c.Assert(rest, check.HasLen, 0) c.Check(s.Stderr(), check.Equals, "") c.Check(s.Stdout(), check.Equals, `Service Startup Current Notes foo.bar enabled inactive timer-activated foo.baz enabled inactive socket-activated foo.zed enabled active - `) // ensure that the fake server api was actually hit c.Check(n, check.Equals, 1) } func (s *appOpSuite) TestAppStatusNoServices(c *check.C) { n := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch n { case 0: c.Check(r.URL.Path, check.Equals, "/v2/apps") c.Check(r.URL.Query(), check.HasLen, 1) c.Check(r.URL.Query().Get("select"), check.Equals, "service") c.Check(r.Method, check.Equals, "GET") w.WriteHeader(200) enc := json.NewEncoder(w) enc.Encode(map[string]interface{}{ "type": "sync", "result": []map[string]interface{}{}, "status": "OK", "status-code": 200, }) default: c.Fatalf("expected to get 1 requests, now on %d", n+1) } n++ }) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"services"}) c.Assert(err, check.IsNil) c.Assert(rest, check.HasLen, 0) c.Check(s.Stdout(), check.Equals, "") c.Check(s.Stderr(), check.Equals, "There are no services provided by installed snaps.\n") // ensure that the fake server api was actually hit c.Check(n, check.Equals, 1) } snapd-2.37.4~14.04.1/cmd/snap/cmd_help_test.go0000664000000000000000000001114213435556260015405 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main_test import ( "bytes" "fmt" "os" "regexp" "strings" "github.com/jessevdk/go-flags" "gopkg.in/check.v1" snap "github.com/snapcore/snapd/cmd/snap" ) func (s *SnapSuite) TestHelpPrintsHelp(c *check.C) { origArgs := os.Args defer func() { os.Args = origArgs }() for _, cmdLine := range [][]string{ {"snap"}, {"snap", "help"}, {"snap", "--help"}, {"snap", "-h"}, } { s.ResetStdStreams() os.Args = cmdLine comment := check.Commentf("%q", cmdLine) err := snap.RunMain() c.Assert(err, check.IsNil, comment) c.Check(s.Stdout(), check.Matches, "(?s)"+strings.Join([]string{ snap.LongSnapDescription, "", regexp.QuoteMeta(snap.SnapUsage), "", ".*", "", snap.SnapHelpAllFooter, snap.SnapHelpFooter, }, "\n")+`\s*`, comment) c.Check(s.Stderr(), check.Equals, "", comment) } } func (s *SnapSuite) TestHelpAllPrintsLongHelp(c *check.C) { origArgs := os.Args defer func() { os.Args = origArgs }() os.Args = []string{"snap", "help", "--all"} err := snap.RunMain() c.Assert(err, check.IsNil) c.Check(s.Stdout(), check.Matches, "(?sm)"+strings.Join([]string{ snap.LongSnapDescription, "", regexp.QuoteMeta(snap.SnapUsage), "", snap.SnapHelpCategoriesIntro, "", ".*", "", snap.SnapHelpAllFooter, }, "\n")+`\s*`) c.Check(s.Stderr(), check.Equals, "") } func nonHiddenCommands() map[string]bool { parser := snap.Parser(snap.Client()) commands := parser.Commands() names := make(map[string]bool, len(commands)) for _, cmd := range commands { if cmd.Hidden { continue } names[cmd.Name] = true } return names } func (s *SnapSuite) testSubCommandHelp(c *check.C, sub, expected string) { parser := snap.Parser(snap.Client()) rest, err := parser.ParseArgs([]string{sub, "--help"}) c.Assert(err, check.DeepEquals, &flags.Error{Type: flags.ErrHelp}) c.Assert(rest, check.HasLen, 0) var buf bytes.Buffer parser.WriteHelp(&buf) c.Check(buf.String(), check.Equals, expected) } func (s *SnapSuite) TestSubCommandHelpPrintsHelp(c *check.C) { origArgs := os.Args defer func() { os.Args = origArgs }() for cmd := range nonHiddenCommands() { s.ResetStdStreams() os.Args = []string{"snap", cmd, "--help"} err := snap.RunMain() comment := check.Commentf("%q", cmd) c.Assert(err, check.IsNil, comment) // regexp matches "Usage: snap " plus an arbitrary // number of [] plus an arbitrary number of // <> optionally ending in ellipsis c.Check(s.Stdout(), check.Matches, fmt.Sprintf(`(?sm)Usage:\s+snap %s(?: \[[^][]+\])*(?:(?: <[^<>]+>)+(?:\.\.\.)?)?$.*`, cmd), comment) c.Check(s.Stderr(), check.Equals, "", comment) } } func (s *SnapSuite) TestHelpCategories(c *check.C) { // non-hidden commands that are not expected to appear in the help summary excluded := []string{ "help", } all := nonHiddenCommands() categorised := make(map[string]bool, len(all)+len(excluded)) for _, cmd := range excluded { categorised[cmd] = true } seen := make(map[string]string, len(all)) for _, categ := range snap.HelpCategories { for _, cmd := range categ.Commands { categorised[cmd] = true if seen[cmd] != "" { c.Errorf("duplicated: %q in %q and %q", cmd, seen[cmd], categ.Label) } seen[cmd] = categ.Label } } for cmd := range all { if !categorised[cmd] { c.Errorf("uncategorised: %q", cmd) } } for cmd := range categorised { if !all[cmd] { c.Errorf("unknown (hidden?): %q", cmd) } } } func (s *SnapSuite) TestHelpCommandAllFails(c *check.C) { origArgs := os.Args defer func() { os.Args = origArgs }() os.Args = []string{"snap", "help", "interfaces", "--all"} err := snap.RunMain() c.Assert(err, check.ErrorMatches, "help accepts a command, or '--all', but not both.") } func (s *SnapSuite) TestManpageInSection8(c *check.C) { origArgs := os.Args defer func() { os.Args = origArgs }() os.Args = []string{"snap", "help", "--man"} err := snap.RunMain() c.Assert(err, check.IsNil) c.Check(s.Stdout(), check.Matches, `\.TH snap 8 (?s).*`) } snapd-2.37.4~14.04.1/cmd/snap/cmd_set.go0000664000000000000000000000523113435556260014213 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main import ( "fmt" "strings" "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/jsonutil" ) var shortSetHelp = i18n.G("Change configuration options") var longSetHelp = i18n.G(` The set command changes the provided configuration options as requested. $ snap set snap-name username=frank password=$PASSWORD All configuration changes are persisted at once, and only after the snap's configuration hook returns successfully. Nested values may be modified via a dotted path: $ snap set author.name=frank `) type cmdSet struct { waitMixin Positional struct { Snap installedSnapName ConfValues []string `required:"1"` } `positional-args:"yes" required:"yes"` } func init() { addCommand("set", shortSetHelp, longSetHelp, func() flags.Commander { return &cmdSet{} }, waitDescs, []argDesc{ { name: "", // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("The snap to configure (e.g. hello-world)"), }, { // TRANSLATORS: This needs to begin with < and end with > name: i18n.G(""), // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("Configuration value (key=value)"), }, }) } func (x *cmdSet) Execute(args []string) error { patchValues := make(map[string]interface{}) for _, patchValue := range x.Positional.ConfValues { parts := strings.SplitN(patchValue, "=", 2) if len(parts) != 2 { return fmt.Errorf(i18n.G("invalid configuration: %q (want key=value)"), patchValue) } var value interface{} if err := jsonutil.DecodeWithNumber(strings.NewReader(parts[1]), &value); err != nil { // Not valid JSON-- just save the string as-is. patchValues[parts[0]] = parts[1] } else { patchValues[parts[0]] = value } } snapName := string(x.Positional.Snap) id, err := x.client.SetConf(snapName, patchValues) if err != nil { return err } if _, err := x.wait(id); err != nil { if err == noWait { return nil } return err } return nil } snapd-2.37.4~14.04.1/cmd/snap/cmd_prefer.go0000664000000000000000000000321713435556260014705 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main import ( "github.com/snapcore/snapd/i18n" "github.com/jessevdk/go-flags" ) type cmdPrefer struct { waitMixin Positionals struct { Snap installedSnapName `required:"yes"` } `positional-args:"true"` } var shortPreferHelp = i18n.G("Enable aliases from a snap, disabling any conflicting aliases") var longPreferHelp = i18n.G(` The prefer command enables all aliases of the given snap in preference to conflicting aliases of other snaps whose aliases will be disabled (or removed, for manual ones). `) func init() { addCommand("prefer", shortPreferHelp, longPreferHelp, func() flags.Commander { return &cmdPrefer{} }, waitDescs, []argDesc{ {name: ""}, }) } func (x *cmdPrefer) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } id, err := x.client.Prefer(string(x.Positionals.Snap)) if err != nil { return err } chg, err := x.wait(id) if err != nil { if err == noWait { return nil } return err } return showAliasChanges(chg) } snapd-2.37.4~14.04.1/cmd/snap/cmd_connectivity_check_test.go0000664000000000000000000000506113435556260020333 0ustar // -*- 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 . * */ package main_test import ( "fmt" "io/ioutil" "net/http" "gopkg.in/check.v1" snap "github.com/snapcore/snapd/cmd/snap" ) func (s *SnapSuite) TestConnectivityHappy(c *check.C) { n := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch n { case 0: c.Check(r.Method, check.Equals, "POST") c.Check(r.URL.Path, check.Equals, "/v2/debug") c.Check(r.URL.RawQuery, check.Equals, "") data, err := ioutil.ReadAll(r.Body) c.Check(err, check.IsNil) c.Check(data, check.DeepEquals, []byte(`{"action":"connectivity"}`)) fmt.Fprintln(w, `{"type": "sync", "result": {}}`) default: c.Fatalf("expected to get 1 requests, now on %d", n+1) } n++ }) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "connectivity"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, `Connectivity status: * PASS `) c.Check(s.Stderr(), check.Equals, "") } func (s *SnapSuite) TestConnectivityUnhappy(c *check.C) { n := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch n { case 0: c.Check(r.Method, check.Equals, "POST") c.Check(r.URL.Path, check.Equals, "/v2/debug") c.Check(r.URL.RawQuery, check.Equals, "") data, err := ioutil.ReadAll(r.Body) c.Check(err, check.IsNil) c.Check(data, check.DeepEquals, []byte(`{"action":"connectivity"}`)) fmt.Fprintln(w, `{"type": "sync", "result": {"connectivity":false,"unreachable":["foo.bar.com"]}}`) default: c.Fatalf("expected to get 1 requests, now on %d", n+1) } n++ }) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "connectivity"}) c.Assert(err, check.ErrorMatches, "1 servers unreachable") // note that only the unreachable hosts are displayed c.Check(s.Stdout(), check.Equals, `Connectivity status: * foo.bar.com: unreachable `) c.Check(s.Stderr(), check.Equals, "") } snapd-2.37.4~14.04.1/cmd/snap/cmd_handle_link.go0000664000000000000000000000437013435556260015673 0ustar // -*- 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 . * */ package main import ( "fmt" "os" "syscall" "time" "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/userd/ui" ) type cmdHandleLink struct { waitMixin Positional struct { Uri string `positional-arg-name:""` } `positional-args:"yes" required:"yes"` } func init() { cmd := addCommand("handle-link", i18n.G("Handle a snap:// URI"), i18n.G("The handle-link command installs the snap-store snap and then invokes it."), func() flags.Commander { return &cmdHandleLink{} }, nil, nil) cmd.hidden = true } func (x *cmdHandleLink) ensureSnapStoreInstalled() error { // If the snap-store snap is installed, our work is done if _, _, err := x.client.Snap("snap-store"); err == nil { return nil } dialog, err := ui.New() if err != nil { return err } answeredYes := dialog.YesNo( i18n.G("Install snap-aware Snap Store snap?"), i18n.G("The Snap Store is required to open snaps from a web browser."), &ui.DialogOptions{ Timeout: 5 * time.Minute, Footer: i18n.G("This dialog will close automatically after 5 minutes of inactivity."), }) if !answeredYes { return fmt.Errorf(i18n.G("Snap Store required")) } changeID, err := x.client.Install("snap-store", nil) if err != nil { return err } _, err = x.wait(changeID) if err != nil && err != noWait { return err } return nil } func (x *cmdHandleLink) Execute([]string) error { if err := x.ensureSnapStoreInstalled(); err != nil { return err } argv := []string{"snap", "run", "snap-store", x.Positional.Uri} return syscall.Exec("/proc/self/exe", argv, os.Environ()) } snapd-2.37.4~14.04.1/cmd/snap/cmd_delete_key.go0000664000000000000000000000310713435556260015532 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main import ( "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/i18n" ) type cmdDeleteKey struct { Positional struct { KeyName keyName } `positional-args:"true" required:"true"` } func init() { cmd := addCommand("delete-key", i18n.G("Delete cryptographic key pair"), i18n.G(` The delete-key command deletes the local cryptographic key pair with the given name. `), func() flags.Commander { return &cmdDeleteKey{} }, nil, []argDesc{{ // TRANSLATORS: This needs to begin with < and end with > name: i18n.G(""), // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("Name of key to delete"), }}) cmd.hidden = true } func (x *cmdDeleteKey) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } manager := asserts.NewGPGKeypairManager() return manager.Delete(string(x.Positional.KeyName)) } snapd-2.37.4~14.04.1/cmd/snap/cmd_auto_import_test.go0000664000000000000000000002333013435556260017021 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main_test import ( "fmt" "io/ioutil" "net/http" "os" "path/filepath" . "gopkg.in/check.v1" snap "github.com/snapcore/snapd/cmd/snap" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/release" ) func makeMockMountInfo(c *C, content string) string { fn := filepath.Join(c.MkDir(), "mountinfo") err := ioutil.WriteFile(fn, []byte(content), 0644) c.Assert(err, IsNil) return fn } func (s *SnapSuite) TestAutoImportAssertsHappy(c *C) { restore := release.MockOnClassic(false) defer restore() fakeAssertData := []byte("my-assertion") n := 0 total := 2 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch n { case 0: c.Check(r.Method, Equals, "POST") c.Check(r.URL.Path, Equals, "/v2/assertions") postData, err := ioutil.ReadAll(r.Body) c.Assert(err, IsNil) c.Check(postData, DeepEquals, fakeAssertData) fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done"}}`) n++ case 1: c.Check(r.Method, Equals, "POST") c.Check(r.URL.Path, Equals, "/v2/create-user") postData, err := ioutil.ReadAll(r.Body) c.Assert(err, IsNil) c.Check(string(postData), Equals, `{"sudoer":true,"known":true}`) fmt.Fprintln(w, `{"type": "sync", "result": [{"username": "foo"}]}`) n++ default: c.Fatalf("unexpected request: %v (expected %d got %d)", r, total, n) } }) fakeAssertsFn := filepath.Join(c.MkDir(), "auto-import.assert") err := ioutil.WriteFile(fakeAssertsFn, fakeAssertData, 0644) c.Assert(err, IsNil) mockMountInfoFmt := ` 24 0 8:18 / %s rw,relatime shared:1 - ext4 /dev/sdb2 rw,errors=remount-ro,data=ordered` content := fmt.Sprintf(mockMountInfoFmt, filepath.Dir(fakeAssertsFn)) restore = snap.MockMountInfoPath(makeMockMountInfo(c, content)) defer restore() logbuf, restore := logger.MockLogger() defer restore() rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"auto-import"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Check(s.Stdout(), Equals, `created user "foo"`+"\n") // matches because we may get a: // "WARNING: cannot create syslog logger\n" // in the output c.Check(logbuf.String(), Matches, fmt.Sprintf("(?ms).*imported %s\n", fakeAssertsFn)) c.Check(n, Equals, total) } func (s *SnapSuite) TestAutoImportAssertsNotImportedFromLoop(c *C) { restore := release.MockOnClassic(false) defer restore() fakeAssertData := []byte("bad-assertion") s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { // assertion is ignored, nothing is posted to this endpoint panic("not reached") }) fakeAssertsFn := filepath.Join(c.MkDir(), "auto-import.assert") err := ioutil.WriteFile(fakeAssertsFn, fakeAssertData, 0644) c.Assert(err, IsNil) mockMountInfoFmtWithLoop := ` 24 0 8:18 / %s rw,relatime shared:1 - squashfs /dev/loop1 rw,errors=remount-ro,data=ordered` content := fmt.Sprintf(mockMountInfoFmtWithLoop, filepath.Dir(fakeAssertsFn)) restore = snap.MockMountInfoPath(makeMockMountInfo(c, content)) defer restore() rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"auto-import"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Check(s.Stdout(), Equals, "") c.Check(s.Stderr(), Equals, "") } func (s *SnapSuite) TestAutoImportCandidatesHappy(c *C) { dirs := make([]string, 4) args := make([]interface{}, len(dirs)) files := make([]string, len(dirs)) for i := range dirs { dirs[i] = c.MkDir() args[i] = dirs[i] files[i] = filepath.Join(dirs[i], "auto-import.assert") err := ioutil.WriteFile(files[i], nil, 0644) c.Assert(err, IsNil) } mockMountInfoFmtWithLoop := ` too short 24 0 8:18 / %[1]s rw,relatime foo ext3 /dev/meep2 no,separator 24 0 8:18 / %[2]s rw,relatime - ext3 /dev/meep2 rw,errors=remount-ro,data=ordered 24 0 8:18 / %[3]s rw,relatime opt:1 - ext4 /dev/meep3 rw,errors=remount-ro,data=ordered 24 0 8:18 / %[4]s rw,relatime opt:1 opt:2 - ext2 /dev/meep1 rw,errors=remount-ro,data=ordered ` content := fmt.Sprintf(mockMountInfoFmtWithLoop, args...) restore := snap.MockMountInfoPath(makeMockMountInfo(c, content)) defer restore() l, err := snap.AutoImportCandidates() c.Check(err, IsNil) c.Check(l, DeepEquals, files[1:]) } func (s *SnapSuite) TestAutoImportAssertsHappyNotOnClassic(c *C) { restore := release.MockOnClassic(true) defer restore() fakeAssertData := []byte("my-assertion") s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Errorf("auto-import on classic is disabled, but something tried to do a %q with %s", r.Method, r.URL.Path) }) fakeAssertsFn := filepath.Join(c.MkDir(), "auto-import.assert") err := ioutil.WriteFile(fakeAssertsFn, fakeAssertData, 0644) c.Assert(err, IsNil) mockMountInfoFmt := ` 24 0 8:18 / %s rw,relatime shared:1 - ext4 /dev/sdb2 rw,errors=remount-ro,data=ordered` content := fmt.Sprintf(mockMountInfoFmt, filepath.Dir(fakeAssertsFn)) restore = snap.MockMountInfoPath(makeMockMountInfo(c, content)) defer restore() rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"auto-import"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Check(s.Stdout(), Equals, "") c.Check(s.Stderr(), Equals, "auto-import is disabled on classic\n") } func (s *SnapSuite) TestAutoImportIntoSpool(c *C) { restore := release.MockOnClassic(false) defer restore() logbuf, restore := logger.MockLogger() defer restore() fakeAssertData := []byte("good-assertion") // ensure we can not connect snap.ClientConfig.BaseURL = "can-not-connect-to-this-url" fakeAssertsFn := filepath.Join(c.MkDir(), "auto-import.assert") err := ioutil.WriteFile(fakeAssertsFn, fakeAssertData, 0644) c.Assert(err, IsNil) mockMountInfoFmt := ` 24 0 8:18 / %s rw,relatime shared:1 - squashfs /dev/sc1 rw,errors=remount-ro,data=ordered` content := fmt.Sprintf(mockMountInfoFmt, filepath.Dir(fakeAssertsFn)) restore = snap.MockMountInfoPath(makeMockMountInfo(c, content)) defer restore() rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"auto-import"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Check(s.Stdout(), Equals, "") // matches because we may get a: // "WARNING: cannot create syslog logger\n" // in the output c.Check(logbuf.String(), Matches, "(?ms).*queuing for later.*\n") files, err := ioutil.ReadDir(dirs.SnapAssertsSpoolDir) c.Assert(err, IsNil) c.Check(files, HasLen, 1) c.Check(files[0].Name(), Equals, "iOkaeet50rajLvL-0Qsf2ELrTdn3XIXRIBlDewcK02zwRi3_TJlUOTl9AaiDXmDn.assert") } func (s *SnapSuite) TestAutoImportFromSpoolHappy(c *C) { restore := release.MockOnClassic(false) defer restore() fakeAssertData := []byte("my-assertion") n := 0 total := 2 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch n { case 0: c.Check(r.Method, Equals, "POST") c.Check(r.URL.Path, Equals, "/v2/assertions") postData, err := ioutil.ReadAll(r.Body) c.Assert(err, IsNil) c.Check(postData, DeepEquals, fakeAssertData) fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done"}}`) n++ case 1: c.Check(r.Method, Equals, "POST") c.Check(r.URL.Path, Equals, "/v2/create-user") postData, err := ioutil.ReadAll(r.Body) c.Assert(err, IsNil) c.Check(string(postData), Equals, `{"sudoer":true,"known":true}`) fmt.Fprintln(w, `{"type": "sync", "result": [{"username": "foo"}]}`) n++ default: c.Fatalf("unexpected request: %v (expected %d got %d)", r, total, n) } }) fakeAssertsFn := filepath.Join(dirs.SnapAssertsSpoolDir, "1234343") err := os.MkdirAll(filepath.Dir(fakeAssertsFn), 0755) c.Assert(err, IsNil) err = ioutil.WriteFile(fakeAssertsFn, fakeAssertData, 0644) c.Assert(err, IsNil) logbuf, restore := logger.MockLogger() defer restore() rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"auto-import"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Check(s.Stdout(), Equals, `created user "foo"`+"\n") // matches because we may get a: // "WARNING: cannot create syslog logger\n" // in the output c.Check(logbuf.String(), Matches, fmt.Sprintf("(?ms).*imported %s\n", fakeAssertsFn)) c.Check(n, Equals, total) c.Check(osutil.FileExists(fakeAssertsFn), Equals, false) } func (s *SnapSuite) TestAutoImportIntoSpoolUnhappyTooBig(c *C) { restore := release.MockOnClassic(false) defer restore() _, restoreLogger := logger.MockLogger() defer restoreLogger() // fake data is bigger than the default assertion limit fakeAssertData := make([]byte, 641*1024) // ensure we can not connect snap.ClientConfig.BaseURL = "can-not-connect-to-this-url" fakeAssertsFn := filepath.Join(c.MkDir(), "auto-import.assert") err := ioutil.WriteFile(fakeAssertsFn, fakeAssertData, 0644) c.Assert(err, IsNil) mockMountInfoFmt := ` 24 0 8:18 / %s rw,relatime shared:1 - squashfs /dev/sc1 rw,errors=remount-ro,data=ordered` content := fmt.Sprintf(mockMountInfoFmt, filepath.Dir(fakeAssertsFn)) restore = snap.MockMountInfoPath(makeMockMountInfo(c, content)) defer restore() _, err = snap.Parser(snap.Client()).ParseArgs([]string{"auto-import"}) c.Assert(err, ErrorMatches, "cannot queue .*, file size too big: 656384") } snapd-2.37.4~14.04.1/cmd/snap/cmd_disconnect_test.go0000664000000000000000000001613613435556260016616 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main_test import ( "fmt" "net/http" "os" "github.com/jessevdk/go-flags" . "gopkg.in/check.v1" . "github.com/snapcore/snapd/cmd/snap" ) func (s *SnapSuite) TestDisconnectHelp(c *C) { msg := `Usage: snap.test disconnect [disconnect-OPTIONS] [:] [:] The disconnect command disconnects a plug from a slot. It may be called in the following ways: $ snap disconnect : : Disconnects the specific plug from the specific slot. $ snap disconnect : Disconnects everything from the provided plug or slot. The snap name may be omitted for the core snap. [disconnect command options] --no-wait Do not wait for the operation to finish but just print the change id. ` s.testSubCommandHelp(c, "disconnect", msg) } func (s *SnapSuite) TestDisconnectExplicitEverything(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/v2/interfaces": c.Check(r.Method, Equals, "POST") c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{ "action": "disconnect", "plugs": []interface{}{ map[string]interface{}{ "snap": "producer", "plug": "plug", }, }, "slots": []interface{}{ map[string]interface{}{ "snap": "consumer", "slot": "slot", }, }, }) fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) case "/v2/changes/zzz": c.Check(r.Method, Equals, "GET") fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`) default: c.Fatalf("unexpected path %q", r.URL.Path) } }) rest, err := Parser(Client()).ParseArgs([]string{"disconnect", "producer:plug", "consumer:slot"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Assert(s.Stdout(), Equals, "") c.Assert(s.Stderr(), Equals, "") } func (s *SnapSuite) TestDisconnectEverythingFromSpecificSlot(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/v2/interfaces": c.Check(r.Method, Equals, "POST") c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{ "action": "disconnect", "plugs": []interface{}{ map[string]interface{}{ "snap": "", "plug": "", }, }, "slots": []interface{}{ map[string]interface{}{ "snap": "consumer", "slot": "slot", }, }, }) fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) case "/v2/changes/zzz": c.Check(r.Method, Equals, "GET") fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`) default: c.Fatalf("unexpected path %q", r.URL.Path) } }) rest, err := Parser(Client()).ParseArgs([]string{"disconnect", "consumer:slot"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Assert(s.Stdout(), Equals, "") c.Assert(s.Stderr(), Equals, "") } func (s *SnapSuite) TestDisconnectEverythingFromSpecificSnapPlugOrSlot(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/v2/interfaces": c.Check(r.Method, Equals, "POST") c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{ "action": "disconnect", "plugs": []interface{}{ map[string]interface{}{ "snap": "", "plug": "", }, }, "slots": []interface{}{ map[string]interface{}{ "snap": "consumer", "slot": "plug-or-slot", }, }, }) fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) case "/v2/changes/zzz": c.Check(r.Method, Equals, "GET") fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`) default: c.Fatalf("unexpected path %q", r.URL.Path) } }) rest, err := Parser(Client()).ParseArgs([]string{"disconnect", "consumer:plug-or-slot"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Assert(s.Stdout(), Equals, "") c.Assert(s.Stderr(), Equals, "") } func (s *SnapSuite) TestDisconnectEverythingFromSpecificSnap(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Fatalf("expected nothing to reach the server") }) rest, err := Parser(Client()).ParseArgs([]string{"disconnect", "consumer"}) c.Assert(err, ErrorMatches, `please provide the plug or slot name to disconnect from snap "consumer"`) c.Assert(rest, DeepEquals, []string{"consumer"}) c.Assert(s.Stdout(), Equals, "") c.Assert(s.Stderr(), Equals, "") } func (s *SnapSuite) TestDisconnectCompletion(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/v2/interfaces": c.Assert(r.Method, Equals, "GET") EncodeResponseBody(c, w, map[string]interface{}{ "type": "sync", "result": fortestingConnectionList, }) default: c.Fatalf("unexpected path %q", r.URL.Path) } }) os.Setenv("GO_FLAGS_COMPLETION", "verbose") defer os.Unsetenv("GO_FLAGS_COMPLETION") expected := []flags.Completion{} parser := Parser(Client()) parser.CompletionHandler = func(obtained []flags.Completion) { c.Check(obtained, DeepEquals, expected) } expected = []flags.Completion{{Item: "canonical-pi2:"}, {Item: "core:"}, {Item: "keyboard-lights:"}} _, err := parser.ParseArgs([]string{"disconnect", ""}) c.Assert(err, IsNil) expected = []flags.Completion{{Item: "canonical-pi2:pin-13", Description: "slot"}} _, err = parser.ParseArgs([]string{"disconnect", "ca"}) c.Assert(err, IsNil) expected = []flags.Completion{{Item: ":core-support", Description: "slot"}, {Item: ":core-support-plug", Description: "plug"}} _, err = parser.ParseArgs([]string{"disconnect", ":"}) c.Assert(err, IsNil) expected = []flags.Completion{{Item: "keyboard-lights:capslock-led", Description: "plug"}} _, err = parser.ParseArgs([]string{"disconnect", "k"}) c.Assert(err, IsNil) expected = []flags.Completion{{Item: "canonical-pi2:"}, {Item: "core:"}} _, err = parser.ParseArgs([]string{"disconnect", "keyboard-lights:capslock-led", ""}) c.Assert(err, IsNil) expected = []flags.Completion{{Item: "canonical-pi2:pin-13", Description: "slot"}} _, err = parser.ParseArgs([]string{"disconnect", "keyboard-lights:capslock-led", "ca"}) c.Assert(err, IsNil) expected = []flags.Completion{{Item: ":core-support", Description: "slot"}} _, err = parser.ParseArgs([]string{"disconnect", ":core-support-plug", ":"}) c.Assert(err, IsNil) c.Assert(s.Stdout(), Equals, "") c.Assert(s.Stderr(), Equals, "") } snapd-2.37.4~14.04.1/cmd/snap/times.go0000664000000000000000000000333013435556260013714 0ustar // -*- 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 . * */ package main import ( "strings" "time" "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/strutil/quantity" "github.com/snapcore/snapd/timeutil" ) var timeutilHuman = timeutil.Human type timeMixin struct { AbsTime bool `long:"abs-time"` } var timeDescs = mixinDescs{ // TRANSLATORS: This should not start with a lowercase letter. "abs-time": i18n.G("Display absolute times (in RFC 3339 format). Otherwise, display relative times up to 60 days, then YYYY-MM-DD."), } func (mx timeMixin) fmtTime(t time.Time) string { if mx.AbsTime { return t.Format(time.RFC3339) } return timeutilHuman(t) } type durationMixin struct { AbsTime bool `long:"abs-time"` } var durationDescs = mixinDescs{ // TRANSLATORS: This should not start with a lowercase letter. "abs-time": i18n.G("Display absolute times (in RFC 3339 format). Otherwise, display short relative times."), } func (mx durationMixin) fmtDuration(t time.Time) string { if mx.AbsTime { return t.Format(time.RFC3339) } return strings.TrimSpace(quantity.FormatDuration(time.Since(t).Seconds())) } snapd-2.37.4~14.04.1/cmd/snap/cmd_login.go0000664000000000000000000000710013435556260014525 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2016 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 . * */ package main import ( "bufio" "fmt" "strings" "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/i18n" ) type cmdLogin struct { clientMixin Positional struct { Email string } `positional-args:"yes"` } var shortLoginHelp = i18n.G("Authenticate to snapd and the store") var longLoginHelp = i18n.G(` The login command authenticates the user to snapd and the snap store, and saves credentials into the ~/.snap/auth.json file. Further communication with snapd will then be made using those credentials. It's not necessary to log in to interact with snapd. Doing so, however, enables purchasing of snaps using 'snap buy', as well as some some developer-oriented features as detailed in the help for the find, install and refresh commands. An account can be set up at https://login.ubuntu.com `) func init() { addCommand("login", shortLoginHelp, longLoginHelp, func() flags.Commander { return &cmdLogin{} }, nil, []argDesc{{ // TRANSLATORS: This is a noun, and it needs to begin with < and end with > name: i18n.G(""), // TRANSLATORS: This should not start with a lowercase letter (unless it's "login.ubuntu.com") desc: i18n.G("The login.ubuntu.com email to login as"), }}) } func requestLoginWith2faRetry(cli *client.Client, email, password string) error { var otp []byte var err error var msgs = [3]string{ i18n.G("Two-factor code: "), i18n.G("Bad code. Try again: "), i18n.G("Wrong again. Once more: "), } reader := bufio.NewReader(nil) for i := 0; ; i++ { // first try is without otp _, err = cli.Login(email, password, string(otp)) if i >= len(msgs) || !client.IsTwoFactorError(err) { return err } reader.Reset(Stdin) fmt.Fprint(Stdout, msgs[i]) // the browser shows it as well (and Sergio wants to see it ;) otp, _, err = reader.ReadLine() if err != nil { return err } } } func requestLogin(cli *client.Client, email string) error { fmt.Fprint(Stdout, fmt.Sprintf(i18n.G("Password of %q: "), email)) password, err := ReadPassword(0) fmt.Fprint(Stdout, "\n") if err != nil { return err } // strings.TrimSpace needed because we get \r from the pty in the tests return requestLoginWith2faRetry(cli, email, strings.TrimSpace(string(password))) } func (x *cmdLogin) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } //TRANSLATORS: after the "... at" follows a URL in the next line fmt.Fprint(Stdout, i18n.G("Personal information is handled as per our privacy notice at\n")) fmt.Fprint(Stdout, "https://www.ubuntu.com/legal/dataprivacy/snap-store\n\n") email := x.Positional.Email if email == "" { fmt.Fprint(Stdout, i18n.G("Email address: ")) in, _, err := bufio.NewReader(Stdin).ReadLine() if err != nil { return err } email = string(in) } err := requestLogin(x.client, email) if err != nil { return err } fmt.Fprintln(Stdout, i18n.G("Login successful")) return nil } snapd-2.37.4~14.04.1/cmd/snap/cmd_connect_test.go0000664000000000000000000002150113435556260016106 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main_test import ( "fmt" "net/http" "os" "github.com/jessevdk/go-flags" . "gopkg.in/check.v1" "github.com/snapcore/snapd/client" . "github.com/snapcore/snapd/cmd/snap" ) func (s *SnapSuite) TestConnectHelp(c *C) { msg := `Usage: snap.test connect [connect-OPTIONS] [:] [:] The connect command connects a plug to a slot. It may be called in the following ways: $ snap connect : : Connects the provided plug to the given slot. $ snap connect : Connects the specific plug to the only slot in the provided snap that matches the connected interface. If more than one potential slot exists, the command fails. $ snap connect : Connects the provided plug to the slot in the core snap with a name matching the plug name. [connect command options] --no-wait Do not wait for the operation to finish but just print the change id. ` s.testSubCommandHelp(c, "connect", msg) } func (s *SnapSuite) TestConnectExplicitEverything(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/v2/interfaces": c.Check(r.Method, Equals, "POST") c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{ "action": "connect", "plugs": []interface{}{ map[string]interface{}{ "snap": "producer", "plug": "plug", }, }, "slots": []interface{}{ map[string]interface{}{ "snap": "consumer", "slot": "slot", }, }, }) fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) case "/v2/changes/zzz": c.Check(r.Method, Equals, "GET") fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`) default: c.Fatalf("unexpected path %q", r.URL.Path) } }) rest, err := Parser(Client()).ParseArgs([]string{"connect", "producer:plug", "consumer:slot"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) } func (s *SnapSuite) TestConnectExplicitPlugImplicitSlot(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/v2/interfaces": c.Check(r.Method, Equals, "POST") c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{ "action": "connect", "plugs": []interface{}{ map[string]interface{}{ "snap": "producer", "plug": "plug", }, }, "slots": []interface{}{ map[string]interface{}{ "snap": "consumer", "slot": "", }, }, }) fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) case "/v2/changes/zzz": c.Check(r.Method, Equals, "GET") fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`) default: c.Fatalf("unexpected path %q", r.URL.Path) } }) rest, err := Parser(Client()).ParseArgs([]string{"connect", "producer:plug", "consumer"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) } func (s *SnapSuite) TestConnectImplicitPlugExplicitSlot(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/v2/interfaces": c.Check(r.Method, Equals, "POST") c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{ "action": "connect", "plugs": []interface{}{ map[string]interface{}{ "snap": "", "plug": "plug", }, }, "slots": []interface{}{ map[string]interface{}{ "snap": "consumer", "slot": "slot", }, }, }) fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) case "/v2/changes/zzz": c.Check(r.Method, Equals, "GET") fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`) default: c.Fatalf("unexpected path %q", r.URL.Path) } }) rest, err := Parser(Client()).ParseArgs([]string{"connect", "plug", "consumer:slot"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) } func (s *SnapSuite) TestConnectImplicitPlugImplicitSlot(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/v2/interfaces": c.Check(r.Method, Equals, "POST") c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{ "action": "connect", "plugs": []interface{}{ map[string]interface{}{ "snap": "", "plug": "plug", }, }, "slots": []interface{}{ map[string]interface{}{ "snap": "consumer", "slot": "", }, }, }) fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) case "/v2/changes/zzz": c.Check(r.Method, Equals, "GET") fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`) default: c.Fatalf("unexpected path %q", r.URL.Path) } }) rest, err := Parser(Client()).ParseArgs([]string{"connect", "plug", "consumer"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) } var fortestingConnectionList = client.Connections{ Slots: []client.Slot{ { Snap: "core", Name: "x11", Interface: "x11", }, { Snap: "core", Name: "core-support", Interface: "core-support", Connections: []client.PlugRef{ { Snap: "core", Name: "core-support-plug", }, }, }, { Snap: "wake-up-alarm", Name: "toggle", Interface: "bool-file", Label: "Alarm toggle", }, { Snap: "canonical-pi2", Name: "pin-13", Interface: "bool-file", Label: "Pin 13", Connections: []client.PlugRef{ { Snap: "keyboard-lights", Name: "capslock-led", }, }, }, }, Plugs: []client.Plug{ { Snap: "core", Name: "core-support-plug", Interface: "core-support", Connections: []client.SlotRef{ { Snap: "core", Name: "core-support", }, }, }, { Snap: "core", Name: "network-bind-plug", Interface: "network-bind", }, { Snap: "paste-daemon", Name: "network-listening", Interface: "network-listening", Label: "Ability to be a network service", }, { Snap: "potato", Name: "frying", Interface: "frying", Label: "Ability to fry a network service", }, { Snap: "keyboard-lights", Name: "capslock-led", Interface: "bool-file", Label: "Capslock indicator LED", Connections: []client.SlotRef{ { Snap: "canonical-pi2", Name: "pin-13", }, }, }, }, } func (s *SnapSuite) TestConnectCompletion(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/v2/interfaces": c.Assert(r.Method, Equals, "GET") EncodeResponseBody(c, w, map[string]interface{}{ "type": "sync", "result": fortestingConnectionList, }) default: c.Fatalf("unexpected path %q", r.URL.Path) } }) os.Setenv("GO_FLAGS_COMPLETION", "verbose") defer os.Unsetenv("GO_FLAGS_COMPLETION") expected := []flags.Completion{} parser := Parser(Client()) parser.CompletionHandler = func(obtained []flags.Completion) { c.Check(obtained, DeepEquals, expected) } expected = []flags.Completion{{Item: "core:"}, {Item: "paste-daemon:"}, {Item: "potato:"}} _, err := parser.ParseArgs([]string{"connect", ""}) c.Assert(err, IsNil) // connect's first argument can't start with : (only for the 2nd arg, the slot) expected = nil _, err = parser.ParseArgs([]string{"connect", ":"}) c.Assert(err, IsNil) expected = []flags.Completion{{Item: "paste-daemon:network-listening", Description: "plug"}} _, err = parser.ParseArgs([]string{"connect", "pa"}) c.Assert(err, IsNil) expected = []flags.Completion{{Item: "core:"}, {Item: "wake-up-alarm:"}} _, err = parser.ParseArgs([]string{"connect", "paste-daemon:network-listening", ""}) c.Assert(err, IsNil) expected = []flags.Completion{{Item: "wake-up-alarm:toggle", Description: "slot"}} _, err = parser.ParseArgs([]string{"connect", "paste-daemon:network-listening", "w"}) c.Assert(err, IsNil) expected = []flags.Completion{{Item: ":x11", Description: "slot"}} _, err = parser.ParseArgs([]string{"connect", "paste-daemon:network-listening", ":"}) c.Assert(err, IsNil) c.Assert(s.Stdout(), Equals, "") c.Assert(s.Stderr(), Equals, "") } snapd-2.37.4~14.04.1/cmd/snap/cmd_paths.go0000664000000000000000000000274713435556260014550 0ustar // -*- 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 . * */ package main import ( "fmt" "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/i18n" ) var pathsHelp = i18n.G("Print system paths") var longPathsHelp = i18n.G(` The paths command prints the list of paths detected and used by snapd. `) type cmdPaths struct{} func init() { addDebugCommand("paths", pathsHelp, longPathsHelp, func() flags.Commander { return &cmdPaths{} }, nil, nil) } func (cmd cmdPaths) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } // TODO: include paths reported by snap-confine for _, p := range []struct { name string path string }{ {"SNAPD_MOUNT", dirs.SnapMountDir}, {"SNAPD_BIN", dirs.SnapBinariesDir}, {"SNAPD_LIBEXEC", dirs.DistroLibExecDir}, } { fmt.Fprintf(Stdout, "%s=%s\n", p.name, p.path) } return nil } snapd-2.37.4~14.04.1/cmd/snap/test-data/0000775000000000000000000000000013435556260014133 5ustar snapd-2.37.4~14.04.1/cmd/snap/test-data/pubring.gpg0000664000000000000000000000422013435556260016276 0ustar  V=}`-8x`#= t Ңu,%]v5|C#j"ݭ5Ayw>x'ʾddS6qvRlY ЉkcHMSjUA#TIǡ:S? 'M1F&֮R1ʿ-ZN߾+”ƽF&daT7DRLi:V:0e.k lw&WZԡ;̿sf34%[i@wKG~>V_ uٕ5 E.pS3C_K[ a8wGp$X1,X  FN)mt\D-8GdYlPL" :]DM(4z԰ ,I2a²;F7 7$jd`7g;_'855@D }HKb݂z #Džx vs\<0 Ւ ]BwV9:І[m9Qg¿ VQ h&# uMWAlμͪwRդUF<Ѕt k)4xsVwÞa~d8,^ i-}ltcT8(W! H!e4K.a+"+ը(H(u0U(G"$,XZFyJ(t?@;GR`byN"=,yᄎ+M6LE "x #65hcA,:kŪ(%.)kjz {h![hKN|~DfaOQYC #X4(G _TRMgBP*¡1#fbKZ\aH 92mw?ԣ~7=>jzX.&CBXyAh:rݯB/lӤ:{r@"J T)& Ng4zݢ2&@v~4Z?'Wzv@;#Htev`3dMI janother(V/  >yeHyp~ 9:#D33lGwtfscF5Ĉsƹ s"'5JYr}<.dʮ)VU/ A }߈T T76Uvk8'y}& Qxl:K>5q3 )OՕ͏6n KAL*=$mScXT,G]~z,\n;.p0ƋK&&xfQ9Pd@DO๊8cG2&Gy+ ! q o]Umi0#Tӏ2"ODzI3(^|qݔ[RaC@yeHþش<;7ʓ௟EFϷ{YYkHx'ʾddS6qvRlY ЉkcHMSjUA#TIǡ:S? 'M1F&֮R1"ڇM65kS9l;>t8 2{"k^CR.\A4LzNz$x4"qNn'tJlir U Oy91:!{yW(4`;3L`<"n󃥮| [Wu N,'3^RnoucK i`xt=,4$_UI /$-xAafո`-v@Ѡaҵ(UvK&dz^ vzg(n)~㘛 xj'D^kZ0tͦn#7/nT$h83_#U΃{mJc+ԑ!3"”!aO<-Rp{ϓY^zv,en oh3gHAhʲ`Lb7c!)54Au4nF*oVUT90=6IVJF&^DXPR ӓV0:/s0U$A#FyaJ"+ A`~,.رD;oFIɛ lrg ]ֻsZюG~i=sS$<k)d9N"dyK5٩Yۻ9E=5Ĩ@ bj_͐F ^+R&r5CJ( Bnu"sѵ ->HwœC]2 Y>KhN\A~u1udefault(V/  uTUR'U|Ȗvt-v!16=2xzgpq{;$fgH?w%\kTO?Mu[ +v+ť>ʿ-ZN߾+”ƽF&daT7DRLi:V:0e.k lw&WZԡ;̿sf34%[i@wKG~>V_ uٕ5 E.pS3C_K[ a8wGp$X1,X  FN)mt\D-8GdYlPL" :]DM(4z԰ ,I2a²;F7 7$jd`7g;_'855@D }HKb݂z #Džx vs\<0 Ւ ]BwV9:І[m9Qg¿VQ h&# uMWAlμͪwRդUF<Ѕt k)4xsVwÞa~d8,^ i-}ltcT8(W! H!e4K.a+"+ը(H(u0U(G"$,XZFyJ(t?@;GR`byN"=,yᄎ+M6LE "x #65hcA,:kŪ(%.)kjz {h![hKN|~DfaOQYC #X4(G _TRMgBP*¡1#fbKZ\aH 92mw?ԣ~7=>jzX.&CBXyAh:rݯB/lӤ:{r@"J T)& Ng4zݢ2&@v~4Z?'Wzv@;#Htev`3dMI j*{0+.0#=Iͤ!y~0NѦq,VJ+(NӭX&۞j1W^ fobpkK0f~swcgp-#2O.Ӆ>sqҒUH,#Ar'_lg*Tugr{@VTˋw㠊{Y5Z9>t}  $Ɣ`esc-bL꿫G -YIQ[ݠ 2>eVY/:2|x=4s/.3, ޘEMvt`ꭽ&3*{c8%b1<7@*OKanW@}YA1KEH6F ČurfKZ 4JG5 yE`g`r>S#ۂmζ\Pn}t}cp55i !Oi '$#q k R3r+)$Τ\@>Kog<8|C4:pq@ ׼(zٺkk. ;ʍ[lQ) }CN|tg\m"p"1>ĨyO2 Ai关RUhGIeqY2{y2+=(g>ɛm-ˣU`q `3~C625 Up%E8z$ _oWAiﯹ@Nz%6 =OeHQvs*#Z2 1,59`#h'vTȉVBݩ'.?KY[ pDx|4K<2BE$ G?_5HF*AlKt^ɕ辚;RUi8xꥺ+Mm`X;L#I E7q9"fKd[b<-(5՟{Ed%A e;Q~Utpj Kϸ6ؑ6~/ ߚMA㱶k'.B՘h&B]Y?Yج^PS)-׺Bҍ< Sȹ &`յ yeHyp~ 9:#D33lGwtfscF5Ĉsƹ s"'5JYr}<.dʮ)VU/ A }߈T T76Uvk8'y}& Qxl:K>5q3 )OՕ͏6n KAL*=$mScXT,G]~z,\n;.p0ƋK&&xfQ9Pd@DO๊8cG2&Gy+ ! q o]Umi0#Tӏ2"ODzI3(^|qݔ[RaC@yeHþش<;7ʓ௟EFϷ{YYkHye ll3-7 P4 C1Ri MVv snapd-2.37.4~14.04.1/cmd/snap/cmd_keys_test.go0000664000000000000000000001014713435556260015434 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main_test import ( "encoding/json" "fmt" "io/ioutil" "os" "path/filepath" "testing" . "gopkg.in/check.v1" snap "github.com/snapcore/snapd/cmd/snap" ) type SnapKeysSuite struct { BaseSnapSuite GnupgCmd string tempdir string } // FIXME: Ideally we would just use gpg2 and remove the gnupg2_test.go file. // However currently there is LP: #1621839 which prevents us from // switching to gpg2 fully. Once this is resolved we should switch. var _ = Suite(&SnapKeysSuite{GnupgCmd: "/usr/bin/gpg"}) var fakePinentryData = []byte(`#!/bin/sh set -e echo "OK Pleased to meet you" while true; do read line case $line in BYE) exit 0 ;; *) echo "OK I agree to everything" ;; esac done `) func (s *SnapKeysSuite) SetUpTest(c *C) { if testing.Short() && s.GnupgCmd == "/usr/bin/gpg2" { c.Skip("gpg2 does not do short tests") } s.BaseSnapSuite.SetUpTest(c) s.tempdir = c.MkDir() for _, fileName := range []string{"pubring.gpg", "secring.gpg", "trustdb.gpg"} { data, err := ioutil.ReadFile(filepath.Join("test-data", fileName)) c.Assert(err, IsNil) err = ioutil.WriteFile(filepath.Join(s.tempdir, fileName), data, 0644) c.Assert(err, IsNil) } fakePinentryFn := filepath.Join(s.tempdir, "pinentry-fake") err := ioutil.WriteFile(fakePinentryFn, fakePinentryData, 0755) c.Assert(err, IsNil) gpgAgentConfFn := filepath.Join(s.tempdir, "gpg-agent.conf") err = ioutil.WriteFile(gpgAgentConfFn, []byte(fmt.Sprintf(`pinentry-program %s`, fakePinentryFn)), 0644) c.Assert(err, IsNil) os.Setenv("SNAP_GNUPG_HOME", s.tempdir) os.Setenv("SNAP_GNUPG_CMD", s.GnupgCmd) } func (s *SnapKeysSuite) TearDownTest(c *C) { os.Unsetenv("SNAP_GNUPG_HOME") os.Unsetenv("SNAP_GNUPG_CMD") s.BaseSnapSuite.TearDownTest(c) } func (s *SnapKeysSuite) TestKeys(c *C) { rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"keys"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Check(s.Stdout(), Matches, `Name +SHA3-384 default +g4Pks54W_US4pZuxhgG_RHNAf_UeZBBuZyGRLLmMj1Do3GkE_r_5A5BFjx24ZwVJ another +DVQf1U4mIsuzlQqAebjjTPYtYJ-GEhJy0REuj3zvpQYTZ7EJj7adBxIXLJ7Vmk3L `) c.Check(s.Stderr(), Equals, "") } func (s *SnapKeysSuite) TestKeysEmptyNoHeader(c *C) { // simulate empty keys err := os.RemoveAll(s.tempdir) c.Assert(err, IsNil) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"keys"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Check(s.Stdout(), Equals, "") c.Check(s.Stderr(), Equals, "No keys registered, see `snapcraft create-key`\n") } func (s *SnapKeysSuite) TestKeysJSON(c *C) { rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"keys", "--json"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) expectedResponse := []snap.Key{ { Name: "default", Sha3_384: "g4Pks54W_US4pZuxhgG_RHNAf_UeZBBuZyGRLLmMj1Do3GkE_r_5A5BFjx24ZwVJ", }, { Name: "another", Sha3_384: "DVQf1U4mIsuzlQqAebjjTPYtYJ-GEhJy0REuj3zvpQYTZ7EJj7adBxIXLJ7Vmk3L", }, } var obtainedResponse []snap.Key json.Unmarshal(s.stdout.Bytes(), &obtainedResponse) c.Check(obtainedResponse, DeepEquals, expectedResponse) c.Check(s.Stderr(), Equals, "") } func (s *SnapKeysSuite) TestKeysJSONEmpty(c *C) { err := os.RemoveAll(os.Getenv("SNAP_GNUPG_HOME")) c.Assert(err, IsNil) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"keys", "--json"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Check(s.Stdout(), Equals, "[]\n") c.Check(s.Stderr(), Equals, "") } snapd-2.37.4~14.04.1/cmd/snap/cmd_warnings.go0000664000000000000000000001340213435556260015247 0ustar // -*- 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 . * */ package main import ( "encoding/json" "fmt" "os" "path/filepath" "time" "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/strutil/quantity" ) type cmdWarnings struct { clientMixin timeMixin All bool `long:"all"` Verbose bool `long:"verbose"` } type cmdOkay struct{ clientMixin } var shortWarningsHelp = i18n.G("List warnings") var longWarningsHelp = i18n.G(` The warnings command lists the warnings that have been reported to the system. Once warnings have been listed with 'snap warnings', 'snap okay' may be used to silence them. A warning that's been silenced in this way will not be listed again unless it happens again, _and_ a cooldown time has passed. Warnings expire automatically, and once expired they are forgotten. `) var shortOkayHelp = i18n.G("Acknowledge warnings") var longOkayHelp = i18n.G(` The okay command acknowledges the warnings listed with 'snap warnings'. Once acknowledged a warning won't appear again unless it re-occurrs and sufficient time has passed. `) func init() { addCommand("warnings", shortWarningsHelp, longWarningsHelp, func() flags.Commander { return &cmdWarnings{} }, timeDescs.also(map[string]string{ // TRANSLATORS: This should not start with a lowercase letter. "all": i18n.G("Show all warnings"), // TRANSLATORS: This should not start with a lowercase letter. "verbose": i18n.G("Show more information"), }), nil) addCommand("okay", shortOkayHelp, longOkayHelp, func() flags.Commander { return &cmdOkay{} }, nil, nil) } func (cmd *cmdWarnings) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } now := time.Now() warnings, err := cmd.client.Warnings(client.WarningsOptions{All: cmd.All}) if err != nil { return err } if len(warnings) == 0 { if t, _ := lastWarningTimestamp(); t.IsZero() { fmt.Fprintln(Stdout, i18n.G("No warnings.")) } else { fmt.Fprintln(Stdout, i18n.G("No further warnings.")) } return nil } if err := writeWarningTimestamp(now); err != nil { return err } w := tabWriter() if cmd.Verbose { fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", i18n.G("First occurrence"), i18n.G("Last occurrence"), i18n.G("Expires after"), i18n.G("Acknowledged"), i18n.G("Repeats after"), i18n.G("Warning")) for _, warning := range warnings { lastShown := "-" if !warning.LastShown.IsZero() { lastShown = cmd.fmtTime(warning.LastShown) } fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", cmd.fmtTime(warning.FirstAdded), cmd.fmtTime(warning.LastAdded), quantity.FormatDuration(warning.ExpireAfter.Seconds()), lastShown, quantity.FormatDuration(warning.RepeatAfter.Seconds()), warning.Message) } } else { fmt.Fprintf(w, "%s\t%s\n", i18n.G("Last occurrence"), i18n.G("Warning")) for _, warning := range warnings { fmt.Fprintf(w, "%s\t%s\n", cmd.fmtTime(warning.LastAdded), warning.Message) } } w.Flush() return nil } func (cmd *cmdOkay) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } last, err := lastWarningTimestamp() if err != nil { return fmt.Errorf("no client-side warning timestamp found: %v", err) } return cmd.client.Okay(last) } const warnFileEnvKey = "SNAPD_LAST_WARNING_TIMESTAMP_FILENAME" func warnFilename(homedir string) string { if fn := os.Getenv(warnFileEnvKey); fn != "" { return fn } return filepath.Join(dirs.GlobalRootDir, homedir, ".snap", "warnings.json") } type clientWarningData struct { Timestamp time.Time `json:"timestamp"` } func writeWarningTimestamp(t time.Time) error { user, err := osutil.RealUser() if err != nil { return err } uid, gid, err := osutil.UidGid(user) if err != nil { return err } filename := warnFilename(user.HomeDir) if err := osutil.MkdirAllChown(filepath.Dir(filename), 0700, uid, gid); err != nil { return err } aw, err := osutil.NewAtomicFile(filename, 0600, 0, uid, gid) if err != nil { return err } // Cancel once Committed is a NOP :-) defer aw.Cancel() enc := json.NewEncoder(aw) if err := enc.Encode(clientWarningData{Timestamp: t}); err != nil { return err } return aw.Commit() } func lastWarningTimestamp() (time.Time, error) { user, err := osutil.RealUser() if err != nil { return time.Time{}, fmt.Errorf("cannot determine real user: %v", err) } f, err := os.Open(warnFilename(user.HomeDir)) if err != nil { return time.Time{}, fmt.Errorf("cannot open timestamp file: %v", err) } dec := json.NewDecoder(f) var d clientWarningData if err := dec.Decode(&d); err != nil { return time.Time{}, fmt.Errorf("cannot decode timestamp file: %v", err) } if dec.More() { return time.Time{}, fmt.Errorf("spurious extra data in timestamp file") } return d.Timestamp, nil } func maybePresentWarnings(count int, timestamp time.Time) { if count == 0 { return } if last, _ := lastWarningTimestamp(); !timestamp.After(last) { return } fmt.Fprintf(Stderr, i18n.NG("WARNING: There is %d new warning. See 'snap warnings'.\n", "WARNING: There are %d new warnings. See 'snap warnings'.\n", count), count) } snapd-2.37.4~14.04.1/cmd/snap/last.go0000664000000000000000000000635713435556260013552 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main import ( "errors" "fmt" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/i18n" ) type changeIDMixin struct { clientMixin LastChangeType string `long:"last"` Positional struct { ID changeID `positional-arg-name:""` } `positional-args:"yes"` } var changeIDMixinOptDesc = mixinDescs{ // TRANSLATORS: This should not start with a lowercase letter. "last": i18n.G("Select last change of given type (install, refresh, remove, try, auto-refresh, etc.). A question mark at the end of the type means to do nothing (instead of returning an error) if no change of the given type is found. Note the question mark could need protecting from the shell."), } var changeIDMixinArgDesc = []argDesc{{ // TRANSLATORS: This needs to begin with < and end with > name: i18n.G(""), // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("Change ID"), }} // should not be user-visible, but keep it clear and polite because mistakes happen var noChangeFoundOK = errors.New("no change found but that's ok") func (l *changeIDMixin) GetChangeID() (string, error) { if l.Positional.ID == "" && l.LastChangeType == "" { return "", fmt.Errorf(i18n.G("please provide change ID or type with --last=")) } if l.Positional.ID != "" { if l.LastChangeType != "" { return "", fmt.Errorf(i18n.G("cannot use change ID and type together")) } return string(l.Positional.ID), nil } cli := l.client // note that at this point we know l.LastChangeType != "" kind := l.LastChangeType optional := false if l := len(kind) - 1; kind[l] == '?' { optional = true kind = kind[:l] } // our internal change types use "-snap" postfix but let user skip it and use short form. if kind == "refresh" || kind == "install" || kind == "remove" || kind == "connect" || kind == "disconnect" || kind == "configure" || kind == "try" { kind += "-snap" } changes, err := queryChanges(cli, &client.ChangesOptions{Selector: client.ChangesAll}) if err != nil { return "", err } if len(changes) == 0 { if optional { return "", noChangeFoundOK } return "", fmt.Errorf(i18n.G("no changes found")) } chg := findLatestChangeByKind(changes, kind) if chg == nil { if optional { return "", noChangeFoundOK } return "", fmt.Errorf(i18n.G("no changes of type %q found"), l.LastChangeType) } return chg.ID, nil } func findLatestChangeByKind(changes []*client.Change, kind string) (latest *client.Change) { for _, chg := range changes { if chg.Kind == kind && (latest == nil || latest.SpawnTime.Before(chg.SpawnTime)) { latest = chg } } return latest } snapd-2.37.4~14.04.1/cmd/snap/color_test.go0000664000000000000000000001706613435556260014763 0ustar // -*- 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 . * */ package main_test import ( "os" "runtime" // "fmt" // "net/http" "gopkg.in/check.v1" cmdsnap "github.com/snapcore/snapd/cmd/snap" "github.com/snapcore/snapd/snap" ) func setEnviron(env map[string]string) func() { old := make(map[string]string, len(env)) ok := make(map[string]bool, len(env)) for k, v := range env { old[k], ok[k] = os.LookupEnv(k) if v != "" { os.Setenv(k, v) } else { os.Unsetenv(k) } } return func() { for k := range ok { if ok[k] { os.Setenv(k, old[k]) } else { os.Unsetenv(k) } } } } func (s *SnapSuite) TestCanUnicode(c *check.C) { // setenv is per thread runtime.LockOSThread() defer runtime.UnlockOSThread() type T struct { lang, lcAll, lcMsg string expected bool } for _, t := range []T{ {expected: false}, // all locale unset {lang: "C", expected: false}, {lang: "C", lcAll: "C", expected: false}, {lang: "C", lcAll: "C", lcMsg: "C", expected: false}, {lang: "C.UTF-8", lcAll: "C", lcMsg: "C", expected: false}, // LC_MESSAGES wins {lang: "C.UTF-8", lcAll: "C.UTF-8", lcMsg: "C", expected: false}, {lang: "C.UTF-8", lcAll: "C.UTF-8", lcMsg: "C.UTF-8", expected: true}, {lang: "C.UTF-8", lcAll: "C", lcMsg: "C.UTF-8", expected: true}, {lang: "C", lcAll: "C", lcMsg: "C.UTF-8", expected: true}, {lang: "C", lcAll: "C.UTF-8", expected: true}, {lang: "C.UTF-8", expected: true}, {lang: "C.utf8", expected: true}, // deals with a bit of rando weirdness } { restore := setEnviron(map[string]string{"LANG": t.lang, "LC_ALL": t.lcAll, "LC_MESSAGES": t.lcMsg}) c.Check(cmdsnap.CanUnicode("never"), check.Equals, false) c.Check(cmdsnap.CanUnicode("always"), check.Equals, true) restoreIsTTY := cmdsnap.MockIsStdoutTTY(true) c.Check(cmdsnap.CanUnicode("auto"), check.Equals, t.expected) cmdsnap.MockIsStdoutTTY(false) c.Check(cmdsnap.CanUnicode("auto"), check.Equals, false) restoreIsTTY() restore() } } func (s *SnapSuite) TestColorTable(c *check.C) { // setenv is per thread runtime.LockOSThread() defer runtime.UnlockOSThread() type T struct { isTTY bool noColor, term string expected interface{} desc string } for _, t := range []T{ {isTTY: false, expected: cmdsnap.NoEscColorTable, desc: "not a tty"}, {isTTY: false, noColor: "1", expected: cmdsnap.NoEscColorTable, desc: "no tty *and* NO_COLOR set"}, {isTTY: false, term: "linux-m", expected: cmdsnap.NoEscColorTable, desc: "no tty *and* mono term set"}, {isTTY: true, expected: cmdsnap.ColorColorTable, desc: "is a tty"}, {isTTY: true, noColor: "1", expected: cmdsnap.MonoColorTable, desc: "is a tty, but NO_COLOR set"}, {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.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)) c.Check(cmdsnap.ColorTable("auto"), check.DeepEquals, t.expected, check.Commentf(t.desc)) restoreEnv() restoreIsTTY() } } func (s *SnapSuite) TestPublisherEscapes(c *check.C) { // just check never/always; for auto checks look above type T struct { color, unicode bool username, display string verified bool short, long, fill string } for _, t := range []T{ // non-verified equal under fold: {color: false, unicode: false, username: "potato", display: "Potato", short: "potato", long: "Potato", fill: ""}, {color: false, unicode: true, username: "potato", display: "Potato", short: "potato", long: "Potato", fill: ""}, {color: true, unicode: false, username: "potato", display: "Potato", short: "potato\x1b[32m\x1b[0m", long: "Potato\x1b[32m\x1b[0m", fill: "\x1b[32m\x1b[0m"}, {color: true, unicode: true, username: "potato", display: "Potato", short: "potato\x1b[32m\x1b[0m", long: "Potato\x1b[32m\x1b[0m", fill: "\x1b[32m\x1b[0m"}, // verified equal under fold: {color: false, unicode: false, username: "potato", display: "Potato", verified: true, short: "potato*", long: "Potato*", fill: ""}, {color: false, unicode: true, username: "potato", display: "Potato", verified: true, short: "potato✓", long: "Potato✓", fill: ""}, {color: true, unicode: false, username: "potato", display: "Potato", verified: true, short: "potato\x1b[32m*\x1b[0m", long: "Potato\x1b[32m*\x1b[0m", fill: "\x1b[32m\x1b[0m"}, {color: true, unicode: true, username: "potato", display: "Potato", verified: true, short: "potato\x1b[32m✓\x1b[0m", long: "Potato\x1b[32m✓\x1b[0m", fill: "\x1b[32m\x1b[0m"}, // non-verified, different {color: false, unicode: false, username: "potato", display: "Carrot", short: "potato", long: "Carrot (potato)", fill: ""}, {color: false, unicode: true, username: "potato", display: "Carrot", short: "potato", long: "Carrot (potato)", fill: ""}, {color: true, unicode: false, username: "potato", display: "Carrot", short: "potato\x1b[32m\x1b[0m", long: "Carrot (potato\x1b[32m\x1b[0m)", fill: "\x1b[32m\x1b[0m"}, {color: true, unicode: true, username: "potato", display: "Carrot", short: "potato\x1b[32m\x1b[0m", long: "Carrot (potato\x1b[32m\x1b[0m)", fill: "\x1b[32m\x1b[0m"}, // verified, different {color: false, unicode: false, username: "potato", display: "Carrot", verified: true, short: "potato*", long: "Carrot (potato*)", fill: ""}, {color: false, unicode: true, username: "potato", display: "Carrot", verified: true, short: "potato✓", long: "Carrot (potato✓)", fill: ""}, {color: true, unicode: false, username: "potato", display: "Carrot", verified: true, short: "potato\x1b[32m*\x1b[0m", long: "Carrot (potato\x1b[32m*\x1b[0m)", fill: "\x1b[32m\x1b[0m"}, {color: true, unicode: true, username: "potato", display: "Carrot", verified: true, short: "potato\x1b[32m✓\x1b[0m", long: "Carrot (potato\x1b[32m✓\x1b[0m)", fill: "\x1b[32m\x1b[0m"}, // some interesting equal-under-folds: {color: false, unicode: false, username: "potato", display: "PoTaTo", short: "potato", long: "PoTaTo", fill: ""}, {color: false, unicode: false, username: "potato-team", display: "Potato Team", short: "potato-team", long: "Potato Team", fill: ""}, } { pub := &snap.StoreAccount{Username: t.username, DisplayName: t.display} if t.verified { pub.Validation = "verified" } color := "never" if t.color { color = "always" } unicode := "never" if t.unicode { unicode = "always" } mx := cmdsnap.ColorMixin(color, unicode) esc := cmdsnap.ColorMixinGetEscapes(mx) c.Check(cmdsnap.ShortPublisher(esc, pub), check.Equals, t.short) c.Check(cmdsnap.LongPublisher(esc, pub), check.Equals, t.long) c.Check(cmdsnap.FillerPublisher(esc), check.Equals, t.fill) } } snapd-2.37.4~14.04.1/cmd/snap/cmd_first_boot.go0000664000000000000000000000264613435556260015601 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2015 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 . * */ package main import ( "fmt" "github.com/jessevdk/go-flags" ) type cmdInternalFirstBoot struct{} func init() { cmd := addCommand("firstboot", "Internal", "The firstboot command is only retained for backwards compatibility.", func() flags.Commander { return &cmdInternalFirstBoot{} }, nil, nil) cmd.hidden = true } // WARNING: do not remove this command, older systems may still have // a systemd snapd.firstboot.service job in /etc/systemd/system // that we did not cleanup. so we need this dummy command or // those units will start failing. func (x *cmdInternalFirstBoot) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } fmt.Fprintf(Stderr, "firstboot command is deprecated\n") return nil } snapd-2.37.4~14.04.1/cmd/snap/cmd_run_test.go0000664000000000000000000011214513435556260015266 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main_test import ( "errors" "fmt" "os" "os/user" "path/filepath" "strings" "time" "gopkg.in/check.v1" snaprun "github.com/snapcore/snapd/cmd/snap" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/selinux" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snaptest" "github.com/snapcore/snapd/testutil" "github.com/snapcore/snapd/x11" ) var mockYaml = []byte(`name: snapname version: 1.0 apps: app: command: run-app hooks: configure: `) func (s *SnapSuite) TestInvalidParameters(c *check.C) { invalidParameters := []string{"run", "--hook=configure", "--command=command-name", "--", "snap-name"} _, err := snaprun.Parser(snaprun.Client()).ParseArgs(invalidParameters) c.Check(err, check.ErrorMatches, ".*you can only use one of --hook, --command, and --timer.*") invalidParameters = []string{"run", "--hook=configure", "--timer=10:00-12:00", "--", "snap-name"} _, err = snaprun.Parser(snaprun.Client()).ParseArgs(invalidParameters) c.Check(err, check.ErrorMatches, ".*you can only use one of --hook, --command, and --timer.*") invalidParameters = []string{"run", "--command=command-name", "--timer=10:00-12:00", "--", "snap-name"} _, err = snaprun.Parser(snaprun.Client()).ParseArgs(invalidParameters) c.Check(err, check.ErrorMatches, ".*you can only use one of --hook, --command, and --timer.*") invalidParameters = []string{"run", "-r=1", "--command=command-name", "--", "snap-name"} _, err = snaprun.Parser(snaprun.Client()).ParseArgs(invalidParameters) c.Check(err, check.ErrorMatches, ".*-r can only be used with --hook.*") invalidParameters = []string{"run", "-r=1", "--", "snap-name"} _, err = snaprun.Parser(snaprun.Client()).ParseArgs(invalidParameters) c.Check(err, check.ErrorMatches, ".*-r can only be used with --hook.*") invalidParameters = []string{"run", "--hook=configure", "--", "foo", "bar", "snap-name"} _, err = snaprun.Parser(snaprun.Client()).ParseArgs(invalidParameters) c.Check(err, check.ErrorMatches, ".*too many arguments for hook \"configure\": bar.*") } func (s *SnapSuite) TestSnapRunWhenMissingConfine(c *check.C) { _, r := logger.MockLogger() defer r() // mock installed snap snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R("x2"), }) // redirect exec var execs [][]string restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { execs = append(execs, args) return nil }) defer restorer() // and run it! // a regular run will fail _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app", "--arg1", "arg2"}) c.Assert(err, check.ErrorMatches, `.* your core/snapd package`) // a hook run will not fail _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=configure", "--", "snapname"}) c.Assert(err, check.IsNil) // but nothing is run ever c.Check(execs, check.IsNil) } func (s *SnapSuite) TestSnapRunAppIntegration(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(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app", "--arg1", "arg2"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) c.Check(execArgs, check.DeepEquals, []string{ filepath.Join(dirs.DistroLibExecDir, "snap-confine"), "snap.snapname.app", filepath.Join(dirs.CoreLibExecDir, "snap-exec"), "snapname.app", "--arg1", "arg2"}) c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2") } func (s *SnapSuite) TestSnapRunClassicAppIntegration(c *check.C) { defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap snaptest.MockSnapCurrent(c, string(mockYaml)+"confinement: classic\n", &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(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app", "--arg1", "arg2"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) c.Check(execArgs, check.DeepEquals, []string{ filepath.Join(dirs.DistroLibExecDir, "snap-confine"), "--classic", "snap.snapname.app", filepath.Join(dirs.DistroLibExecDir, "snap-exec"), "snapname.app", "--arg1", "arg2"}) c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2") } func (s *SnapSuite) TestSnapRunClassicAppIntegrationReexeced(c *check.C) { mountedCorePath := filepath.Join(dirs.SnapMountDir, "core/current") mountedCoreLibExecPath := filepath.Join(mountedCorePath, dirs.CoreLibExecDir) defer mockSnapConfine(mountedCoreLibExecPath)() // mock installed snap snaptest.MockSnapCurrent(c, string(mockYaml)+"confinement: classic\n", &snap.SideInfo{ Revision: snap.R("x2"), }) restore := snaprun.MockOsReadlink(func(name string) (string, error) { // pretend 'snap' is reexeced from 'core' return filepath.Join(mountedCorePath, "usr/bin/snap"), nil }) defer restore() execArgs := []string{} restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { execArgs = args return nil }) defer restorer() rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app", "--arg1", "arg2"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) c.Check(execArgs, check.DeepEquals, []string{ filepath.Join(mountedCoreLibExecPath, "snap-confine"), "--classic", "snap.snapname.app", filepath.Join(mountedCoreLibExecPath, "snap-exec"), "snapname.app", "--arg1", "arg2"}) } func (s *SnapSuite) TestSnapRunAppWithCommandIntegration(c *check.C) { defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R(42), }) // 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! _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--command=my-command", "--", "snapname.app", "arg1", "arg2"}) c.Assert(err, check.IsNil) 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"), "--command=my-command", "snapname.app", "arg1", "arg2"}) c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") } func (s *SnapSuite) TestSnapRunCreateDataDirs(c *check.C) { info, err := snap.InfoFromSnapYaml(mockYaml) c.Assert(err, check.IsNil) info.SideInfo.Revision = snap.R(42) fakeHome := c.MkDir() restorer := snaprun.MockUserCurrent(func() (*user.User, error) { return &user.User{HomeDir: fakeHome}, nil }) defer restorer() err = snaprun.CreateUserDataDirs(info) c.Assert(err, check.IsNil) c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname/42")), check.Equals, true) c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname/common")), check.Equals, true) } func (s *SnapSuite) TestParallelInstanceSnapRunCreateDataDirs(c *check.C) { info, err := snap.InfoFromSnapYaml(mockYaml) c.Assert(err, check.IsNil) info.SideInfo.Revision = snap.R(42) info.InstanceKey = "foo" fakeHome := c.MkDir() restorer := snaprun.MockUserCurrent(func() (*user.User, error) { return &user.User{HomeDir: fakeHome}, nil }) defer restorer() err = snaprun.CreateUserDataDirs(info) c.Assert(err, check.IsNil) c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname_foo/42")), check.Equals, true) c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname_foo/common")), check.Equals, true) // mount point for snap instance mapping has been created c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname")), check.Equals, true) // and it's empty inside m, err := filepath.Glob(filepath.Join(fakeHome, "/snap/snapname/*")) c.Assert(err, check.IsNil) c.Assert(m, check.HasLen, 0) } func (s *SnapSuite) TestSnapRunHookIntegration(c *check.C) { defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R(42), }) // 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() // Run a hook from the active revision _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=configure", "--", "snapname"}) c.Assert(err, check.IsNil) 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.hook.configure", filepath.Join(dirs.CoreLibExecDir, "snap-exec"), "--hook=configure", "snapname"}) c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") } func (s *SnapSuite) TestSnapRunHookUnsetRevisionIntegration(c *check.C) { defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R(42), }) // 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() // Specifically pass "unset" which would use the active version. _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=configure", "-r=unset", "--", "snapname"}) c.Assert(err, check.IsNil) 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.hook.configure", filepath.Join(dirs.CoreLibExecDir, "snap-exec"), "--hook=configure", "snapname"}) c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") } func (s *SnapSuite) TestSnapRunHookSpecificRevisionIntegration(c *check.C) { defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap // Create both revisions 41 and 42 snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R(41), }) snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R(42), }) // 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() // Run a hook on revision 41 _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=configure", "-r=41", "--", "snapname"}) c.Assert(err, check.IsNil) 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.hook.configure", filepath.Join(dirs.CoreLibExecDir, "snap-exec"), "--hook=configure", "snapname"}) c.Check(execEnv, testutil.Contains, "SNAP_REVISION=41") } func (s *SnapSuite) TestSnapRunHookMissingRevisionIntegration(c *check.C) { // Only create revision 42 snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R(42), }) // redirect exec restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { return nil }) defer restorer() // Attempt to run a hook on revision 41, which doesn't exist _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=configure", "-r=41", "--", "snapname"}) c.Assert(err, check.NotNil) c.Check(err, check.ErrorMatches, "cannot find .*") } func (s *SnapSuite) TestSnapRunHookInvalidRevisionIntegration(c *check.C) { _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=configure", "-r=invalid", "--", "snapname"}) c.Assert(err, check.NotNil) c.Check(err, check.ErrorMatches, "invalid snap revision: \"invalid\"") } func (s *SnapSuite) TestSnapRunHookMissingHookIntegration(c *check.C) { // Only create revision 42 snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R(42), }) // redirect exec called := false restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { called = true return nil }) defer restorer() _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=missing-hook", "--", "snapname"}) c.Assert(err, check.ErrorMatches, `cannot find hook "missing-hook" in "snapname"`) c.Check(called, check.Equals, false) } func (s *SnapSuite) TestSnapRunErorsForUnknownRunArg(c *check.C) { _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--unknown", "--", "snapname.app", "--arg1", "arg2"}) c.Assert(err, check.ErrorMatches, "unknown flag `unknown'") } func (s *SnapSuite) TestSnapRunErorsForMissingApp(c *check.C) { _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--command=shell"}) c.Assert(err, check.ErrorMatches, "need the application to run as argument") } func (s *SnapSuite) TestSnapRunErorrForUnavailableApp(c *check.C) { _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "not-there"}) c.Assert(err, check.ErrorMatches, fmt.Sprintf("cannot find current revision for snap not-there: readlink %s/not-there/current: no such file or directory", dirs.SnapMountDir)) } func (s *SnapSuite) TestSnapRunSaneEnvironmentHandling(c *check.C) { defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R(42), }) // redirect exec execEnv := []string{} restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { execEnv = envv return nil }) defer restorer() // set a SNAP{,_*} variable in the environment os.Setenv("SNAP_NAME", "something-else") os.Setenv("SNAP_ARCH", "PDP-7") defer os.Unsetenv("SNAP_NAME") defer os.Unsetenv("SNAP_ARCH") // but unrelated stuff is ok os.Setenv("SNAP_THE_WORLD", "YES") defer os.Unsetenv("SNAP_THE_WORLD") // and ensure those SNAP_ vars get overridden rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app", "--arg1", "arg2"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") c.Check(execEnv, check.Not(testutil.Contains), "SNAP_NAME=something-else") c.Check(execEnv, check.Not(testutil.Contains), "SNAP_ARCH=PDP-7") c.Check(execEnv, testutil.Contains, "SNAP_THE_WORLD=YES") } func (s *SnapSuite) TestSnapRunIsReexeced(c *check.C) { var osReadlinkResult string restore := snaprun.MockOsReadlink(func(name string) (string, error) { return osReadlinkResult, nil }) defer restore() for _, t := range []struct { readlink string expected bool }{ {filepath.Join(dirs.SnapMountDir, dirs.CoreLibExecDir, "snapd"), true}, {filepath.Join(dirs.DistroLibExecDir, "snapd"), false}, } { osReadlinkResult = t.readlink c.Check(snaprun.IsReexeced(), check.Equals, t.expected) } } func (s *SnapSuite) TestSnapRunAppIntegrationFromCore(c *check.C) { defer mockSnapConfine(filepath.Join(dirs.SnapMountDir, "core", "111", dirs.CoreLibExecDir))() // mock installed snap snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R("x2"), }) // pretend to be running from core restorer := snaprun.MockOsReadlink(func(string) (string, error) { return filepath.Join(dirs.SnapMountDir, "core/111/usr/bin/snap"), nil }) defer restorer() // redirect exec execArg0 := "" execArgs := []string{} execEnv := []string{} restorer = snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { execArg0 = arg0 execArgs = args execEnv = envv return nil }) defer restorer() // and run it! rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app", "--arg1", "arg2"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) c.Check(execArg0, check.Equals, filepath.Join(dirs.SnapMountDir, "/core/111", dirs.CoreLibExecDir, "snap-confine")) c.Check(execArgs, check.DeepEquals, []string{ filepath.Join(dirs.SnapMountDir, "/core/111", dirs.CoreLibExecDir, "snap-confine"), "snap.snapname.app", filepath.Join(dirs.CoreLibExecDir, "snap-exec"), "snapname.app", "--arg1", "arg2"}) c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2") } func (s *SnapSuite) TestSnapRunAppIntegrationFromSnapd(c *check.C) { defer mockSnapConfine(filepath.Join(dirs.SnapMountDir, "snapd", "222", dirs.CoreLibExecDir))() // mock installed snap snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R("x2"), }) // pretend to be running from snapd restorer := snaprun.MockOsReadlink(func(string) (string, error) { return filepath.Join(dirs.SnapMountDir, "snapd/222/usr/bin/snap"), nil }) defer restorer() // redirect exec execArg0 := "" execArgs := []string{} execEnv := []string{} restorer = snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { execArg0 = arg0 execArgs = args execEnv = envv return nil }) defer restorer() // and run it! rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app", "--arg1", "arg2"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) c.Check(execArg0, check.Equals, filepath.Join(dirs.SnapMountDir, "/snapd/222", dirs.CoreLibExecDir, "snap-confine")) c.Check(execArgs, check.DeepEquals, []string{ filepath.Join(dirs.SnapMountDir, "/snapd/222", dirs.CoreLibExecDir, "snap-confine"), "snap.snapname.app", filepath.Join(dirs.CoreLibExecDir, "snap-exec"), "snapname.app", "--arg1", "arg2"}) c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2") } func (s *SnapSuite) TestSnapRunXauthorityMigration(c *check.C) { defer mockSnapConfine(dirs.DistroLibExecDir)() u, err := user.Current() c.Assert(err, check.IsNil) // Ensure XDG_RUNTIME_DIR exists for the user we're testing with err = os.MkdirAll(filepath.Join(dirs.XdgRuntimeDirBase, u.Uid), 0700) c.Assert(err, check.IsNil) // mock installed snap; happily this also gives us a directory // below /tmp which the Xauthority migration expects. 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() xauthPath, err := x11.MockXauthority(2) c.Assert(err, check.IsNil) defer os.Remove(xauthPath) defer snaprun.MockGetEnv(func(name string) string { if name == "XAUTHORITY" { return xauthPath } return "" })() // and run it! rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{"snapname.app"}) 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"), "snapname.app"}) expectedXauthPath := filepath.Join(dirs.XdgRuntimeDirBase, u.Uid, ".Xauthority") c.Check(execEnv, testutil.Contains, fmt.Sprintf("XAUTHORITY=%s", expectedXauthPath)) info, err := os.Stat(expectedXauthPath) c.Assert(err, check.IsNil) c.Assert(info.Mode().Perm(), check.Equals, os.FileMode(0600)) err = x11.ValidateXauthorityFile(expectedXauthPath) c.Assert(err, check.IsNil) } // build the args for a hypothetical completer func mkCompArgs(compPoint string, argv ...string) []string { out := []string{ "99", // COMP_TYPE "99", // COMP_KEY "", // COMP_POINT "2", // COMP_CWORD " ", // COMP_WORDBREAKS } out[2] = compPoint out = append(out, strings.Join(argv, " ")) out = append(out, argv...) return out } func (s *SnapSuite) TestAntialiasHappy(c *check.C) { c.Assert(os.MkdirAll(dirs.SnapBinariesDir, 0755), check.IsNil) inArgs := mkCompArgs("10", "alias", "alias", "bo-alias") // first not so happy because no alias symlink app, outArgs := snaprun.Antialias("alias", inArgs) c.Check(app, check.Equals, "alias") c.Check(outArgs, check.DeepEquals, inArgs) c.Assert(os.Symlink("an-app", filepath.Join(dirs.SnapBinariesDir, "alias")), check.IsNil) // now really happy app, outArgs = snaprun.Antialias("alias", inArgs) c.Check(app, check.Equals, "an-app") c.Check(outArgs, check.DeepEquals, []string{ "99", // COMP_TYPE (no change) "99", // COMP_KEY (no change) "11", // COMP_POINT (+1 because "an-app" is one longer than "alias") "2", // COMP_CWORD (no change) " ", // COMP_WORDBREAKS (no change) "an-app alias bo-alias", // COMP_LINE (argv[0] changed) "an-app", // argv (arv[0] changed) "alias", "bo-alias", }) } func (s *SnapSuite) TestAntialiasBailsIfUnhappy(c *check.C) { // alias exists but args are somehow wonky c.Assert(os.MkdirAll(dirs.SnapBinariesDir, 0755), check.IsNil) c.Assert(os.Symlink("an-app", filepath.Join(dirs.SnapBinariesDir, "alias")), check.IsNil) // weird1 has COMP_LINE not start with COMP_WORDS[0], argv[0] equal to COMP_WORDS[0] weird1 := mkCompArgs("6", "alias", "") weird1[5] = "xxxxx " // weird2 has COMP_LINE not start with COMP_WORDS[0], argv[0] equal to the first word in COMP_LINE weird2 := mkCompArgs("6", "xxxxx", "") weird2[5] = "alias " for desc, inArgs := range map[string][]string{ "nil args": nil, "too-short args": {"alias"}, "COMP_POINT not a number": mkCompArgs("hello", "alias"), "COMP_POINT is inside argv[0]": mkCompArgs("2", "alias", ""), "COMP_POINT is outside argv": mkCompArgs("99", "alias", ""), "COMP_WORDS[0] is not argv[0]": mkCompArgs("10", "not-alias", ""), "mismatch between argv[0], COMP_LINE and COMP_WORDS, #1": weird1, "mismatch between argv[0], COMP_LINE and COMP_WORDS, #2": weird2, } { // antialias leaves args alone if it's too short app, outArgs := snaprun.Antialias("alias", inArgs) c.Check(app, check.Equals, "alias", check.Commentf(desc)) c.Check(outArgs, check.DeepEquals, inArgs, check.Commentf(desc)) } } func (s *SnapSuite) TestSnapRunAppWithStraceIntegration(c *check.C) { defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R("x2"), }) // pretend we have sudo and simulate some useful output that would // normally come from strace sudoCmd := testutil.MockCommand(c, "sudo", fmt.Sprintf(` echo "stdout output 1" >&2 echo 'execve("/path/to/snap-confine")' >&2 echo "snap-confine/snap-exec strace stuff" >&2 echo "getuid() = 1000" >&2 echo 'execve("%s/snapName/x2/bin/foo")' >&2 echo "interessting strace output" >&2 echo "and more" echo "stdout output 2" `, dirs.SnapMountDir)) defer sudoCmd.Restore() // pretend we have strace straceCmd := testutil.MockCommand(c, "strace", "") defer straceCmd.Restore() user, err := user.Current() c.Assert(err, check.IsNil) // and run it under strace rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--strace", "--", "snapname.app", "--arg1", "arg2"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) c.Check(sudoCmd.Calls(), check.DeepEquals, [][]string{ { "sudo", "-E", filepath.Join(straceCmd.BinDir(), "strace"), "-u", user.Username, "-f", "-e", "!select,pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday,nanosleep", filepath.Join(dirs.DistroLibExecDir, "snap-confine"), "snap.snapname.app", filepath.Join(dirs.CoreLibExecDir, "snap-exec"), "snapname.app", "--arg1", "arg2", }, }) c.Check(s.Stdout(), check.Equals, "stdout output 1\nstdout output 2\n") c.Check(s.Stderr(), check.Equals, fmt.Sprintf("execve(%q)\ninteressting strace output\nand more\n", filepath.Join(dirs.SnapMountDir, "snapName/x2/bin/foo"))) s.ResetStdStreams() sudoCmd.ForgetCalls() // try again without filtering rest, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--strace=--raw", "--", "snapname.app", "--arg1", "arg2"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) c.Check(sudoCmd.Calls(), check.DeepEquals, [][]string{ { "sudo", "-E", filepath.Join(straceCmd.BinDir(), "strace"), "-u", user.Username, "-f", "-e", "!select,pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday,nanosleep", filepath.Join(dirs.DistroLibExecDir, "snap-confine"), "snap.snapname.app", filepath.Join(dirs.CoreLibExecDir, "snap-exec"), "snapname.app", "--arg1", "arg2", }, }) c.Check(s.Stdout(), check.Equals, "stdout output 1\nstdout output 2\n") expectedFullFmt := `execve("/path/to/snap-confine") snap-confine/snap-exec strace stuff getuid() = 1000 execve("%s/snapName/x2/bin/foo") interessting strace output and more ` c.Check(s.Stderr(), check.Equals, fmt.Sprintf(expectedFullFmt, dirs.SnapMountDir)) } func (s *SnapSuite) TestSnapRunAppWithStraceOptions(c *check.C) { defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R("x2"), }) // pretend we have sudo sudoCmd := testutil.MockCommand(c, "sudo", "") defer sudoCmd.Restore() // pretend we have strace straceCmd := testutil.MockCommand(c, "strace", "") defer straceCmd.Restore() user, err := user.Current() c.Assert(err, check.IsNil) // and run it under strace rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", `--strace=-tt --raw -o "file with spaces"`, "--", "snapname.app", "--arg1", "arg2"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) c.Check(sudoCmd.Calls(), check.DeepEquals, [][]string{ { "sudo", "-E", filepath.Join(straceCmd.BinDir(), "strace"), "-u", user.Username, "-f", "-e", "!select,pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday,nanosleep", "-tt", "-o", "file with spaces", filepath.Join(dirs.DistroLibExecDir, "snap-confine"), "snap.snapname.app", filepath.Join(dirs.CoreLibExecDir, "snap-exec"), "snapname.app", "--arg1", "arg2", }, }) } func (s *SnapSuite) TestSnapRunShellIntegration(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(snaprun.Client()).ParseArgs([]string{"run", "--shell", "--", "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"), "--command=shell", "snapname.app", "--arg1", "arg2"}) c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2") } func (s *SnapSuite) TestSnapRunAppTimer(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{} execCalled := false restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { execArg0 = arg0 execArgs = args execCalled = true return nil }) defer restorer() fakeNow := time.Date(2018, 02, 12, 9, 55, 0, 0, time.Local) restorer = snaprun.MockTimeNow(func() time.Time { // Monday Feb 12, 9:55 return fakeNow }) defer restorer() // pretend we are outside of timer range rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", `--timer="mon,10:00~12:00,,fri,13:00"`, "--", "snapname.app", "--arg1", "arg2"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) c.Assert(execCalled, check.Equals, false) c.Check(s.Stderr(), check.Equals, fmt.Sprintf(`%s: attempted to run "snapname.app" timer outside of scheduled time "mon,10:00~12:00,,fri,13:00" `, fakeNow.Format(time.RFC3339))) s.ResetStdStreams() restorer = snaprun.MockTimeNow(func() time.Time { // Monday Feb 12, 10:20 return time.Date(2018, 02, 12, 10, 20, 0, 0, time.Local) }) defer restorer() // and run it under strace _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", `--timer="mon,10:00~12:00,,fri,13:00"`, "--", "snapname.app", "--arg1", "arg2"}) c.Assert(err, check.IsNil) c.Assert(execCalled, check.Equals, true) 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"), "snapname.app", "--arg1", "arg2"}) } func (s *SnapSuite) TestRunCmdWithTraceExecUnhappy(c *check.C) { defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R("1"), }) // pretend we have sudo sudoCmd := testutil.MockCommand(c, "sudo", "echo unhappy; exit 12") defer sudoCmd.Restore() // pretend we have strace straceCmd := testutil.MockCommand(c, "strace", "") defer straceCmd.Restore() rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--trace-exec", "--", "snapname.app", "--arg1", "arg2"}) c.Assert(err, check.ErrorMatches, "exit status 12") c.Assert(rest, check.DeepEquals, []string{"--", "snapname.app", "--arg1", "arg2"}) c.Check(s.Stdout(), check.Equals, "unhappy\n") c.Check(s.Stderr(), check.Equals, "") } func (s *SnapSuite) TestSnapRunRestoreSecurityContextHappy(c *check.C) { logbuf, restorer := logger.MockLogger() defer restorer() defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R("x2"), }) fakeHome := c.MkDir() restorer = snaprun.MockUserCurrent(func() (*user.User, error) { return &user.User{HomeDir: fakeHome}, nil }) defer restorer() // redirect exec execCalled := 0 restorer = snaprun.MockSyscallExec(func(_ string, args []string, envv []string) error { execCalled++ return nil }) defer restorer() verifyCalls := 0 restoreCalls := 0 isEnabledCalls := 0 enabled := false verify := true snapUserDir := filepath.Join(fakeHome, dirs.UserHomeSnapDir) restorer = snaprun.MockSELinuxVerifyPathContext(func(what string) (bool, error) { c.Check(what, check.Equals, snapUserDir) verifyCalls++ return verify, nil }) defer restorer() restorer = snaprun.MockSELinuxRestoreContext(func(what string, mode selinux.RestoreMode) error { c.Check(mode, check.Equals, selinux.RestoreMode{Recursive: true}) c.Check(what, check.Equals, snapUserDir) restoreCalls++ return nil }) defer restorer() restorer = snaprun.MockSELinuxIsEnabled(func() (bool, error) { isEnabledCalls++ return enabled, nil }) defer restorer() _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"}) c.Assert(err, check.IsNil) c.Check(execCalled, check.Equals, 1) c.Check(isEnabledCalls, check.Equals, 1) c.Check(verifyCalls, check.Equals, 0) c.Check(restoreCalls, check.Equals, 0) // pretend SELinux is on enabled = true _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"}) c.Assert(err, check.IsNil) c.Check(execCalled, check.Equals, 2) c.Check(isEnabledCalls, check.Equals, 2) c.Check(verifyCalls, check.Equals, 1) c.Check(restoreCalls, check.Equals, 0) // pretend the context does not match verify = false logbuf.Reset() _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"}) c.Assert(err, check.IsNil) c.Check(execCalled, check.Equals, 3) c.Check(isEnabledCalls, check.Equals, 3) c.Check(verifyCalls, check.Equals, 2) c.Check(restoreCalls, check.Equals, 1) // and we let the user know what we're doing c.Check(logbuf.String(), testutil.Contains, fmt.Sprintf("restoring default SELinux context of %s", snapUserDir)) } func (s *SnapSuite) TestSnapRunRestoreSecurityContextFail(c *check.C) { logbuf, restorer := logger.MockLogger() defer restorer() defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R("x2"), }) fakeHome := c.MkDir() restorer = snaprun.MockUserCurrent(func() (*user.User, error) { return &user.User{HomeDir: fakeHome}, nil }) defer restorer() // redirect exec execCalled := 0 restorer = snaprun.MockSyscallExec(func(_ string, args []string, envv []string) error { execCalled++ return nil }) defer restorer() verifyCalls := 0 restoreCalls := 0 isEnabledCalls := 0 enabledErr := errors.New("enabled failed") verifyErr := errors.New("verify failed") restoreErr := errors.New("restore failed") snapUserDir := filepath.Join(fakeHome, dirs.UserHomeSnapDir) restorer = snaprun.MockSELinuxVerifyPathContext(func(what string) (bool, error) { c.Check(what, check.Equals, snapUserDir) verifyCalls++ return false, verifyErr }) defer restorer() restorer = snaprun.MockSELinuxRestoreContext(func(what string, mode selinux.RestoreMode) error { c.Check(mode, check.Equals, selinux.RestoreMode{Recursive: true}) c.Check(what, check.Equals, snapUserDir) restoreCalls++ return restoreErr }) defer restorer() restorer = snaprun.MockSELinuxIsEnabled(func() (bool, error) { isEnabledCalls++ return enabledErr == nil, enabledErr }) defer restorer() _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"}) // these errors are only logged, but we still run the snap c.Assert(err, check.IsNil) c.Check(execCalled, check.Equals, 1) c.Check(logbuf.String(), testutil.Contains, "cannot determine SELinux status: enabled failed") c.Check(isEnabledCalls, check.Equals, 1) c.Check(verifyCalls, check.Equals, 0) c.Check(restoreCalls, check.Equals, 0) // pretend selinux is on enabledErr = nil logbuf.Reset() _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"}) c.Assert(err, check.IsNil) c.Check(execCalled, check.Equals, 2) c.Check(logbuf.String(), testutil.Contains, fmt.Sprintf("failed to verify SELinux context of %s: verify failed", snapUserDir)) c.Check(isEnabledCalls, check.Equals, 2) c.Check(verifyCalls, check.Equals, 1) c.Check(restoreCalls, check.Equals, 0) // pretend the context does not match verifyErr = nil logbuf.Reset() _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"}) c.Assert(err, check.IsNil) c.Check(execCalled, check.Equals, 3) c.Check(logbuf.String(), testutil.Contains, fmt.Sprintf("cannot restore SELinux context of %s: restore failed", snapUserDir)) c.Check(isEnabledCalls, check.Equals, 3) c.Check(verifyCalls, check.Equals, 2) c.Check(restoreCalls, check.Equals, 1) } snapd-2.37.4~14.04.1/cmd/snap/cmd_unalias.go0000664000000000000000000000337013435556260015056 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main import ( "github.com/snapcore/snapd/i18n" "github.com/jessevdk/go-flags" ) type cmdUnalias struct { waitMixin Positionals struct { AliasOrSnap aliasOrSnap `required:"yes"` } `positional-args:"true"` } var shortUnaliasHelp = i18n.G("Remove a manual alias, or the aliases for an entire snap") var longUnaliasHelp = i18n.G(` The unalias command removes a single alias if the provided argument is a manual alias, or disables all aliases of a snap, including manual ones, if the argument is a snap name. `) func init() { addCommand("unalias", shortUnaliasHelp, longUnaliasHelp, func() flags.Commander { return &cmdUnalias{} }, waitDescs.also(nil), []argDesc{ // TRANSLATORS: This needs to begin with < and end with > {name: i18n.G("")}, }) } func (x *cmdUnalias) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } id, err := x.client.Unalias(string(x.Positionals.AliasOrSnap)) if err != nil { return err } chg, err := x.wait(id) if err != nil { if err == noWait { return nil } return err } return showAliasChanges(chg) } snapd-2.37.4~14.04.1/cmd/snap/cmd_list.go0000664000000000000000000001006313435556260014372 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main import ( "errors" "fmt" "sort" "strings" "text/tabwriter" "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/strutil" ) var shortListHelp = i18n.G("List installed snaps") var longListHelp = i18n.G(` The list command displays a summary of snaps installed in the current system. A green check mark (given color and unicode support) after a publisher name indicates that the publisher has been verified. `) type cmdList struct { clientMixin Positional struct { Snaps []installedSnapName `positional-arg-name:""` } `positional-args:"yes"` All bool `long:"all"` colorMixin } func init() { addCommand("list", shortListHelp, longListHelp, func() flags.Commander { return &cmdList{} }, colorDescs.also(map[string]string{ // TRANSLATORS: This should not start with a lowercase letter. "all": i18n.G("Show all revisions"), }), nil) } type snapsByName []*client.Snap func (s snapsByName) Len() int { return len(s) } func (s snapsByName) Less(i, j int) bool { return s[i].Name < s[j].Name } func (s snapsByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] } var ErrNoMatchingSnaps = errors.New(i18n.G("no matching snaps installed")) // snapd will give us and we want // "" (local snap) "-" // risk risk // track track (not yet returned by snapd) // track/stable track // track/risk track/risk // risk/branch risk/… // track/risk/branch track/risk/… func fmtChannel(ch string) string { if ch == "" { // "" -> "-" (local snap) return "-" } idx := strings.IndexByte(ch, '/') if idx < 0 { // risk -> risk return ch } first, rest := ch[:idx], ch[idx+1:] if rest == "stable" && first != "" { // track/stable -> track return first } if idx2 := strings.IndexByte(rest, '/'); idx2 >= 0 { // track/risk/branch -> track/risk/… return ch[:idx2+idx+2] + "…" } // so it's foo/bar -> either risk/branch, or track/risk. if strutil.ListContains(channelRisks, first) { // risk/branch -> risk/… return first + "/…" } // track/risk -> track/risk return ch } func (x *cmdList) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } names := installedSnapNames(x.Positional.Snaps) snaps, err := x.client.List(names, &client.ListOptions{All: x.All}) if err != nil { if err == client.ErrNoSnapsInstalled { if len(names) == 0 { fmt.Fprintln(Stderr, i18n.G("No snaps are installed yet. Try 'snap install hello-world'.")) return nil } else { return ErrNoMatchingSnaps } } return err } else if len(snaps) == 0 { return ErrNoMatchingSnaps } sort.Sort(snapsByName(snaps)) esc := x.getEscapes() w := tabWriter() // TRANSLATORS: the %s is to insert a filler escape sequence (please keep it flush to the column header, with no extra spaces) fmt.Fprintf(w, i18n.G("Name\tVersion\tRev\tTracking\tPublisher%s\tNotes\n"), fillerPublisher(esc)) for _, snap := range snaps { // doing it this way because otherwise it's a sea of %s\t%s\t%s line := []string{ snap.Name, snap.Version, snap.Revision.String(), fmtChannel(snap.TrackingChannel), shortPublisher(esc, snap.Publisher), NotesFromLocal(snap).String(), } fmt.Fprintln(w, strings.Join(line, "\t")) } w.Flush() return nil } func tabWriter() *tabwriter.Writer { return tabwriter.NewWriter(Stdout, 5, 3, 2, ' ', 0) } snapd-2.37.4~14.04.1/cmd/snap/cmd_abort.go0000664000000000000000000000256713435556260014540 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2016 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 . * */ package main import ( "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/i18n" ) type cmdAbort struct{ changeIDMixin } var shortAbortHelp = i18n.G("Abort a pending change") var longAbortHelp = i18n.G(` The abort command attempts to abort a change that still has pending tasks. `) func init() { addCommand("abort", shortAbortHelp, longAbortHelp, func() flags.Commander { return &cmdAbort{} }, changeIDMixinOptDesc, changeIDMixinArgDesc, ) } func (x *cmdAbort) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } id, err := x.GetChangeID() if err != nil { if err == noChangeFoundOK { return nil } return err } _, err = x.client.Abort(id) return err } snapd-2.37.4~14.04.1/cmd/snap/cmd_aliases.go0000664000000000000000000000716113435556260015045 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main import ( "fmt" "sort" "strings" "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/i18n" ) type cmdAliases struct { clientMixin Positionals struct { Snap installedSnapName `positional-arg-name:""` } `positional-args:"true"` } var shortAliasesHelp = i18n.G("List aliases in the system") var longAliasesHelp = i18n.G(` The aliases command lists all aliases available in the system and their status. $ snap aliases Lists only the aliases defined by the specified snap. An alias noted as undefined means it was explicitly enabled or disabled but is not defined in the current revision of the snap, possibly temporarily (e.g. because of a revert). This can cleared with 'snap alias --reset'. `) func init() { addCommand("aliases", shortAliasesHelp, longAliasesHelp, func() flags.Commander { return &cmdAliases{} }, nil, nil) } type aliasInfo struct { Snap string Command string Alias string Status string Auto string } type aliasInfos []*aliasInfo func (infos aliasInfos) Len() int { return len(infos) } func (infos aliasInfos) Swap(i, j int) { infos[i], infos[j] = infos[j], infos[i] } func (infos aliasInfos) Less(i, j int) bool { if infos[i].Snap < infos[j].Snap { return true } if infos[i].Snap == infos[j].Snap { if infos[i].Command < infos[j].Command { return true } if infos[i].Command == infos[j].Command { if infos[i].Alias < infos[j].Alias { return true } } } return false } func (x *cmdAliases) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } allStatuses, err := x.client.Aliases() if err != nil { return err } var infos aliasInfos filterSnap := string(x.Positionals.Snap) if filterSnap != "" { allStatuses = map[string]map[string]client.AliasStatus{ filterSnap: allStatuses[filterSnap], } } for snapName, aliasStatuses := range allStatuses { for alias, aliasStatus := range aliasStatuses { infos = append(infos, &aliasInfo{ Snap: snapName, Command: aliasStatus.Command, Alias: alias, Status: aliasStatus.Status, Auto: aliasStatus.Auto, }) } } if len(infos) > 0 { w := tabWriter() fmt.Fprintln(w, i18n.G("Command\tAlias\tNotes")) defer w.Flush() sort.Sort(infos) for _, info := range infos { var notes []string if info.Status != "auto" { notes = append(notes, info.Status) if info.Status == "manual" && info.Auto != "" { notes = append(notes, "override") } } notesStr := strings.Join(notes, ",") if notesStr == "" { notesStr = "-" } fmt.Fprintf(w, "%s\t%s\t%s\n", info.Command, info.Alias, notesStr) } } else { if filterSnap != "" { fmt.Fprintf(Stderr, i18n.G("No aliases are currently defined for snap %q.\n"), filterSnap) } else { fmt.Fprintln(Stderr, i18n.G("No aliases are currently defined.")) } fmt.Fprintln(Stderr, i18n.G("\nUse 'snap help alias' to learn how to create aliases manually.")) } return nil } snapd-2.37.4~14.04.1/cmd/snap/cmd_snap_op_test.go0000664000000000000000000017353213435556260016130 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016-2018 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main_test import ( "fmt" "io/ioutil" "mime" "mime/multipart" "net/http" "net/http/httptest" "path/filepath" "regexp" "strings" "time" "gopkg.in/check.v1" "github.com/snapcore/snapd/client" snap "github.com/snapcore/snapd/cmd/snap" "github.com/snapcore/snapd/progress" "github.com/snapcore/snapd/progress/progresstest" "github.com/snapcore/snapd/testutil" "os" ) type snapOpTestServer struct { c *check.C checker func(r *http.Request) n int total int channel string confinement string rebooting bool snap string } var _ = check.Suite(&SnapOpSuite{}) func (t *snapOpTestServer) handle(w http.ResponseWriter, r *http.Request) { switch t.n { case 0: t.checker(r) method := "POST" if strings.HasSuffix(r.URL.Path, "/conf") { method = "PUT" } t.c.Check(r.Method, check.Equals, method) w.WriteHeader(202) fmt.Fprintln(w, `{"type":"async", "change": "42", "status-code": 202}`) case 1: t.c.Check(r.Method, check.Equals, "GET") t.c.Check(r.URL.Path, check.Equals, "/v2/changes/42") if !t.rebooting { fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}}`) } else { fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}, "maintenance": {"kind": "system-restart", "message": "system is restarting"}}}`) } case 2: t.c.Check(r.Method, check.Equals, "GET") t.c.Check(r.URL.Path, check.Equals, "/v2/changes/42") fmt.Fprintf(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {"snap-name": "%s"}}}\n`, t.snap) case 3: t.c.Check(r.Method, check.Equals, "GET") t.c.Check(r.URL.Path, check.Equals, "/v2/snaps") fmt.Fprintf(w, `{"type": "sync", "result": [{"name": "%s", "status": "active", "version": "1.0", "developer": "bar", "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"}, "revision":42, "channel":"%s", "confinement": "%s"}]}\n`, t.snap, t.channel, t.confinement) default: t.c.Fatalf("expected to get %d requests, now on %d", t.total, t.n+1) } t.n++ } type SnapOpSuite struct { BaseSnapSuite restoreAll func() srv snapOpTestServer } func (s *SnapOpSuite) SetUpTest(c *check.C) { s.BaseSnapSuite.SetUpTest(c) restoreClientRetry := client.MockDoRetry(time.Millisecond, 10*time.Millisecond) restorePollTime := snap.MockPollTime(time.Millisecond) s.restoreAll = func() { restoreClientRetry() restorePollTime() } s.srv = snapOpTestServer{ c: c, total: 4, snap: "foo", } } func (s *SnapOpSuite) TearDownTest(c *check.C) { s.restoreAll() s.BaseSnapSuite.TearDownTest(c) } func (s *SnapOpSuite) TestWait(c *check.C) { meter := &progresstest.Meter{} defer progress.MockMeter(meter)() restore := snap.MockMaxGoneTime(time.Millisecond) defer restore() // lazy way of getting a URL that won't work nor break stuff server := httptest.NewServer(nil) snap.ClientConfig.BaseURL = server.URL server.Close() cli := snap.Client() chg, err := snap.Wait(cli, "x") c.Assert(chg, check.IsNil) c.Assert(err, check.NotNil) c.Check(meter.Labels, testutil.Contains, "Waiting for server to restart") } func (s *SnapOpSuite) TestWaitRecovers(c *check.C) { meter := &progresstest.Meter{} defer progress.MockMeter(meter)() restore := snap.MockMaxGoneTime(time.Millisecond) defer restore() nah := true s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { if nah { nah = false return } fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done"}}`) }) cli := snap.Client() chg, err := snap.Wait(cli, "x") // we got the change c.Assert(chg, check.NotNil) c.Assert(err, check.IsNil) // but only after recovering c.Check(meter.Labels, testutil.Contains, "Waiting for server to restart") } func (s *SnapOpSuite) TestWaitRebooting(c *check.C) { meter := &progresstest.Meter{} defer progress.MockMeter(meter)() restore := snap.MockMaxGoneTime(time.Millisecond) defer restore() s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, `{"type": "sync", "result": { "ready": false, "status": "Doing", "tasks": [{"kind": "bar", "summary": "...", "status": "Doing", "progress": {"done": 1, "total": 1}, "log": ["INFO: info"]}] }, "maintenance": {"kind": "system-restart", "message": "system is restarting"}}`) }) cli := snap.Client() chg, err := snap.Wait(cli, "x") c.Assert(chg, check.IsNil) c.Assert(err, check.DeepEquals, &client.Error{Kind: client.ErrorKindSystemRestart, Message: "system is restarting"}) // last available info is still displayed c.Check(meter.Notices, testutil.Contains, "INFO: info") } func (s *SnapOpSuite) TestInstall(c *check.C) { s.srv.checker = func(r *http.Request) { c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ "action": "install", "channel": "candidate", }) s.srv.channel = "candidate" } s.RedirectClientToTestServer(s.srv.handle) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel", "candidate", "foo"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(candidate\) 1.0 from Bar installed`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) } func (s *SnapOpSuite) TestInstallFromTrack(c *check.C) { s.srv.checker = func(r *http.Request) { c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ "action": "install", "channel": "3.4/stable", }) s.srv.channel = "3.4/stable" } s.RedirectClientToTestServer(s.srv.handle) // snap install --channel=3.4 means 3.4/stable, this is what we test here rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel", "3.4", "foo"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(3.4/stable\) 1.0 from Bar installed`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) } func (s *SnapOpSuite) TestInstallFromBranch(c *check.C) { s.srv.checker = func(r *http.Request) { c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ "action": "install", "channel": "3.4/hotfix-1", }) s.srv.channel = "3.4/hotfix-1" } s.RedirectClientToTestServer(s.srv.handle) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel", "3.4/hotfix-1", "foo"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(3.4/hotfix-1\) 1.0 from Bar installed`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) } func (s *SnapOpSuite) TestInstallDevMode(c *check.C) { s.srv.checker = func(r *http.Request) { c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ "action": "install", "devmode": true, "channel": "beta", }) s.srv.channel = "beta" } s.RedirectClientToTestServer(s.srv.handle) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel", "beta", "--devmode", "foo"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(beta\) 1.0 from Bar installed`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) } func (s *SnapOpSuite) TestInstallClassic(c *check.C) { s.srv.checker = func(r *http.Request) { c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ "action": "install", "classic": true, }) s.srv.confinement = "classic" } s.RedirectClientToTestServer(s.srv.handle) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--classic", "foo"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from Bar installed`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) } func (s *SnapOpSuite) TestInstallStrictWithClassicFlag(c *check.C) { s.srv.checker = func(r *http.Request) { c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ "action": "install", "classic": true, }) s.srv.confinement = "strict" } s.RedirectClientToTestServer(s.srv.handle) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--classic", "foo"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from Bar installed`) c.Check(s.Stderr(), testutil.MatchesWrapped, `Warning:\s+flag --classic ignored for strictly confined snap foo.*`) // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) } func (s *SnapOpSuite) TestInstallUnaliased(c *check.C) { s.srv.checker = func(r *http.Request) { c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ "action": "install", "unaliased": true, }) } s.RedirectClientToTestServer(s.srv.handle) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--unaliased", "foo"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from Bar installed`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) } func (s *SnapOpSuite) TestInstallSnapNotFound(c *check.C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, `{"type": "error", "result": {"message": "snap not found", "value": "foo", "kind": "snap-not-found"}, "status-code": 404}`) }) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "foo"}) c.Assert(err, check.NotNil) c.Check(fmt.Sprintf("error: %v\n", err), check.Equals, `error: snap "foo" not found `) c.Check(s.Stdout(), check.Equals, "") c.Check(s.Stderr(), check.Equals, "") } func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailable(c *check.C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision available as specified", "value": "foo", "kind": "snap-revision-not-available"}, "status-code": 404}`) }) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "foo"}) c.Assert(err, check.NotNil) c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` error: snap "foo" not available as specified (see 'snap info foo') `) c.Check(s.Stdout(), check.Equals, "") c.Check(s.Stderr(), check.Equals, "") } func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableOnChannel(c *check.C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision available as specified", "value": "foo", "kind": "snap-revision-not-available"}, "status-code": 404}`) }) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel=mytrack", "foo"}) c.Assert(err, check.NotNil) c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` error: snap "foo" not available on channel "mytrack/stable" (see 'snap info foo') `) c.Check(s.Stdout(), check.Equals, "") c.Check(s.Stderr(), check.Equals, "") } func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableAtRevision(c *check.C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision available as specified", "value": "foo", "kind": "snap-revision-not-available"}, "status-code": 404}`) }) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--revision=2", "foo"}) c.Assert(err, check.NotNil) c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` error: snap "foo" revision 2 not available (see 'snap info foo') `) c.Check(s.Stdout(), check.Equals, "") c.Check(s.Stderr(), check.Equals, "") } func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableForChannelTrackOK(c *check.C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified channel", "value": { "snap-name": "foo", "action": "install", "architecture": "amd64", "channel": "stable", "releases": [{"architecture": "amd64", "channel": "beta"}, {"architecture": "amd64", "channel": "edge"}] }, "kind": "snap-channel-not-available"}, "status-code": 404}`) }) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "foo"}) c.Assert(err, check.NotNil) c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` error: snap "foo" is not available on stable but is available to install on the following channels: beta snap install --beta foo edge snap install --edge foo Please be mindful pre-release channels may include features not completely tested or implemented. Get more information with 'snap info foo'. `) c.Check(s.Stdout(), check.Equals, "") c.Check(s.Stderr(), check.Equals, "") } func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableForChannelTrackOKPrerelOK(c *check.C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified channel", "value": { "snap-name": "foo", "action": "install", "architecture": "amd64", "channel": "candidate", "releases": [{"architecture": "amd64", "channel": "beta"}, {"architecture": "amd64", "channel": "edge"}] }, "kind": "snap-channel-not-available"}, "status-code": 404}`) }) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--candidate", "foo"}) c.Assert(err, check.NotNil) c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` error: snap "foo" is not available on candidate but is available to install on the following channels: beta snap install --beta foo edge snap install --edge foo Get more information with 'snap info foo'. `) c.Check(s.Stdout(), check.Equals, "") c.Check(s.Stderr(), check.Equals, "") } func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableForChannelTrackOther(c *check.C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified channel", "value": { "snap-name": "foo", "action": "install", "architecture": "amd64", "channel": "stable", "releases": [{"architecture": "amd64", "channel": "1.0/stable"}, {"architecture": "amd64", "channel": "2.0/stable"}] }, "kind": "snap-channel-not-available"}, "status-code": 404}`) }) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "foo"}) c.Assert(err, check.NotNil) c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` error: snap "foo" is not available on latest/stable but is available to install on the following tracks: 1.0/stable snap install --channel=1.0 foo 2.0/stable snap install --channel=2.0 foo Please be mindful that different tracks may include different features. Get more information with 'snap info foo'. `) c.Check(s.Stdout(), check.Equals, "") c.Check(s.Stderr(), check.Equals, "") } func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableForChannelTrackLatestStable(c *check.C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified channel", "value": { "snap-name": "foo", "action": "install", "architecture": "amd64", "channel": "2.0/stable", "releases": [{"architecture": "amd64", "channel": "stable"}] }, "kind": "snap-channel-not-available"}, "status-code": 404}`) }) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel=2.0/stable", "foo"}) c.Assert(err, check.NotNil) c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` error: snap "foo" is not available on 2.0/stable but is available to install on the following tracks: latest/stable snap install --stable foo Please be mindful that different tracks may include different features. Get more information with 'snap info foo'. `) c.Check(s.Stdout(), check.Equals, "") c.Check(s.Stderr(), check.Equals, "") } func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableForChannelTrackAndRiskOther(c *check.C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified channel", "value": { "snap-name": "foo", "action": "install", "architecture": "amd64", "channel": "2.0/stable", "releases": [{"architecture": "amd64", "channel": "1.0/edge"}] }, "kind": "snap-channel-not-available"}, "status-code": 404}`) }) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel=2.0/stable", "foo"}) c.Assert(err, check.NotNil) c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` error: snap "foo" is not available on 2.0/stable but other tracks exist. Please be mindful that different tracks may include different features. Get more information with 'snap info foo'. `) c.Check(s.Stdout(), check.Equals, "") c.Check(s.Stderr(), check.Equals, "") } func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableForArchitectureTrackAndRiskOK(c *check.C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified architecture", "value": { "snap-name": "foo", "action": "install", "architecture": "arm64", "channel": "stable", "releases": [{"architecture": "amd64", "channel": "stable"}, {"architecture": "s390x", "channel": "stable"}] }, "kind": "snap-architecture-not-available"}, "status-code": 404}`) }) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "foo"}) c.Assert(err, check.NotNil) c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` error: snap "foo" is not available on stable for this architecture (arm64) but exists on other architectures (amd64, s390x). `) c.Check(s.Stdout(), check.Equals, "") c.Check(s.Stderr(), check.Equals, "") } func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableForArchitectureTrackAndRiskOther(c *check.C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified architecture", "value": { "snap-name": "foo", "action": "install", "architecture": "arm64", "channel": "1.0/stable", "releases": [{"architecture": "amd64", "channel": "stable"}, {"architecture": "s390x", "channel": "stable"}] }, "kind": "snap-architecture-not-available"}, "status-code": 404}`) }) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel=1.0/stable", "foo"}) c.Assert(err, check.NotNil) c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` error: snap "foo" is not available on this architecture (arm64) but exists on other architectures (amd64, s390x). `) c.Check(s.Stdout(), check.Equals, "") c.Check(s.Stderr(), check.Equals, "") } func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableInvalidChannel(c *check.C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified channel", "value": { "snap-name": "foo", "action": "install", "architecture": "amd64", "channel": "a/b/c/d", "releases": [{"architecture": "amd64", "channel": "stable"}] }, "kind": "snap-channel-not-available"}, "status-code": 404}`) }) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel=a/b/c/d", "foo"}) c.Assert(err, check.NotNil) c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` error: requested channel "a/b/c/d" is not valid (see 'snap info foo' for valid ones) `) c.Check(s.Stdout(), check.Equals, "") c.Check(s.Stderr(), check.Equals, "") } func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableForChannelNonExistingBranchOnMainChannel(c *check.C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified channel", "value": { "snap-name": "foo", "action": "install", "architecture": "amd64", "channel": "stable/baz", "releases": [{"architecture": "amd64", "channel": "stable"}] }, "kind": "snap-channel-not-available"}, "status-code": 404}`) }) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel=stable/baz", "foo"}) c.Assert(err, check.NotNil) c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` error: requested a non-existing branch on latest/stable for snap "foo": baz `) c.Check(s.Stdout(), check.Equals, "") c.Check(s.Stderr(), check.Equals, "") } func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableForChannelNonExistingBranch(c *check.C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified channel", "value": { "snap-name": "foo", "action": "install", "architecture": "amd64", "channel": "stable/baz", "releases": [{"architecture": "amd64", "channel": "edge"}] }, "kind": "snap-channel-not-available"}, "status-code": 404}`) }) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel=stable/baz", "foo"}) c.Assert(err, check.NotNil) c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` error: requested a non-existing branch for snap "foo": latest/stable/baz `) c.Check(s.Stdout(), check.Equals, "") c.Check(s.Stderr(), check.Equals, "") } func testForm(r *http.Request, c *check.C) *multipart.Form { contentType := r.Header.Get("Content-Type") mediaType, params, err := mime.ParseMediaType(contentType) c.Assert(err, check.IsNil) c.Assert(params["boundary"], check.Matches, ".{10,}") c.Check(mediaType, check.Equals, "multipart/form-data") form, err := multipart.NewReader(r.Body, params["boundary"]).ReadForm(1 << 20) c.Assert(err, check.IsNil) return form } func formFile(form *multipart.Form, c *check.C) (name, filename string, content []byte) { c.Assert(form.File, check.HasLen, 1) for name, fheaders := range form.File { c.Assert(fheaders, check.HasLen, 1) body, err := fheaders[0].Open() c.Assert(err, check.IsNil) defer body.Close() filename = fheaders[0].Filename content, err = ioutil.ReadAll(body) c.Assert(err, check.IsNil) return name, filename, content } return "", "", nil } func (s *SnapOpSuite) TestInstallPath(c *check.C) { s.srv.checker = func(r *http.Request) { c.Check(r.URL.Path, check.Equals, "/v2/snaps") form := testForm(r, c) defer form.RemoveAll() c.Check(form.Value["action"], check.DeepEquals, []string{"install"}) c.Check(form.Value["devmode"], check.IsNil) c.Check(form.Value["snap-path"], check.NotNil) c.Check(form.Value, check.HasLen, 2) name, _, body := formFile(form, c) c.Check(name, check.Equals, "snap") c.Check(string(body), check.Equals, "snap-data") } snapBody := []byte("snap-data") s.RedirectClientToTestServer(s.srv.handle) snapPath := filepath.Join(c.MkDir(), "foo.snap") err := ioutil.WriteFile(snapPath, snapBody, 0644) c.Assert(err, check.IsNil) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", snapPath}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from Bar installed`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) } func (s *SnapOpSuite) TestInstallPathDevMode(c *check.C) { s.srv.checker = func(r *http.Request) { c.Check(r.URL.Path, check.Equals, "/v2/snaps") form := testForm(r, c) defer form.RemoveAll() c.Check(form.Value["action"], check.DeepEquals, []string{"install"}) c.Check(form.Value["devmode"], check.DeepEquals, []string{"true"}) c.Check(form.Value["snap-path"], check.NotNil) c.Check(form.Value, check.HasLen, 3) name, _, body := formFile(form, c) c.Check(name, check.Equals, "snap") c.Check(string(body), check.Equals, "snap-data") } snapBody := []byte("snap-data") s.RedirectClientToTestServer(s.srv.handle) snapPath := filepath.Join(c.MkDir(), "foo.snap") err := ioutil.WriteFile(snapPath, snapBody, 0644) c.Assert(err, check.IsNil) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--devmode", snapPath}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from Bar installed`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) } func (s *SnapOpSuite) TestInstallPathClassic(c *check.C) { s.srv.checker = func(r *http.Request) { c.Check(r.URL.Path, check.Equals, "/v2/snaps") form := testForm(r, c) defer form.RemoveAll() c.Check(form.Value["action"], check.DeepEquals, []string{"install"}) c.Check(form.Value["classic"], check.DeepEquals, []string{"true"}) c.Check(form.Value["snap-path"], check.NotNil) c.Check(form.Value, check.HasLen, 3) name, _, body := formFile(form, c) c.Check(name, check.Equals, "snap") c.Check(string(body), check.Equals, "snap-data") s.srv.confinement = "classic" } snapBody := []byte("snap-data") s.RedirectClientToTestServer(s.srv.handle) snapPath := filepath.Join(c.MkDir(), "foo.snap") err := ioutil.WriteFile(snapPath, snapBody, 0644) c.Assert(err, check.IsNil) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--classic", snapPath}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from Bar installed`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) } func (s *SnapOpSuite) TestInstallPathDangerous(c *check.C) { s.srv.checker = func(r *http.Request) { c.Check(r.URL.Path, check.Equals, "/v2/snaps") form := testForm(r, c) defer form.RemoveAll() c.Check(form.Value["action"], check.DeepEquals, []string{"install"}) c.Check(form.Value["dangerous"], check.DeepEquals, []string{"true"}) c.Check(form.Value["snap-path"], check.NotNil) c.Check(form.Value, check.HasLen, 3) name, _, body := formFile(form, c) c.Check(name, check.Equals, "snap") c.Check(string(body), check.Equals, "snap-data") } snapBody := []byte("snap-data") s.RedirectClientToTestServer(s.srv.handle) snapPath := filepath.Join(c.MkDir(), "foo.snap") err := ioutil.WriteFile(snapPath, snapBody, 0644) c.Assert(err, check.IsNil) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--dangerous", snapPath}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from Bar installed`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) } func (s *SnapOpSuite) TestInstallPathInstance(c *check.C) { s.srv.checker = func(r *http.Request) { c.Check(r.URL.Path, check.Equals, "/v2/snaps") form := testForm(r, c) defer form.RemoveAll() c.Check(form.Value["action"], check.DeepEquals, []string{"install"}) c.Check(form.Value["name"], check.DeepEquals, []string{"foo_bar"}) c.Check(form.Value["devmode"], check.IsNil) c.Check(form.Value["snap-path"], check.NotNil) c.Check(form.Value, check.HasLen, 3) name, _, body := formFile(form, c) c.Check(name, check.Equals, "snap") c.Check(string(body), check.Equals, "snap-data") } snapBody := []byte("snap-data") s.RedirectClientToTestServer(s.srv.handle) // instance is named foo_bar s.srv.snap = "foo_bar" snapPath := filepath.Join(c.MkDir(), "foo.snap") err := ioutil.WriteFile(snapPath, snapBody, 0644) c.Assert(err, check.IsNil) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", snapPath, "--name", "foo_bar"}) c.Assert(rest, check.DeepEquals, []string{}) c.Assert(err, check.IsNil) c.Check(s.Stdout(), check.Matches, `(?sm).*foo_bar 1.0 from Bar installed`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) } func (s *SnapSuite) TestInstallWithInstanceNoPath(c *check.C) { _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--name", "foo_bar", "some-snap"}) c.Assert(err, check.ErrorMatches, "cannot use explicit name when installing from store") } func (s *SnapSuite) TestInstallManyWithInstance(c *check.C) { _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--name", "foo_bar", "some-snap-1", "some-snap-2"}) c.Assert(err, check.ErrorMatches, "cannot use instance name when installing multiple snaps") } func (s *SnapOpSuite) TestRevertRunthrough(c *check.C) { s.srv.total = 4 s.srv.channel = "potato" s.srv.checker = func(r *http.Request) { c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ "action": "revert", }) } s.RedirectClientToTestServer(s.srv.handle) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"revert", "foo"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) // tracking channel is "" in the test server c.Check(s.Stdout(), check.Equals, `foo reverted to 1.0 Channel for foo is closed; temporarily forwarding to potato. `) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) } func (s *SnapOpSuite) runRevertTest(c *check.C, opts *client.SnapOptions) { modes := []struct { enabled bool name string }{ {opts.DevMode, "devmode"}, {opts.JailMode, "jailmode"}, {opts.Classic, "classic"}, } s.srv.checker = func(r *http.Request) { c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") d := DecodedRequestBody(c, r) n := 1 c.Check(d["action"], check.Equals, "revert") for _, mode := range modes { if mode.enabled { n++ c.Check(d[mode.name], check.Equals, true) } else { c.Check(d[mode.name], check.IsNil) } } c.Check(d, check.HasLen, n) } s.RedirectClientToTestServer(s.srv.handle) cmd := []string{"revert", "foo"} for _, mode := range modes { if mode.enabled { cmd = append(cmd, "--"+mode.name) } } rest, err := snap.Parser(snap.Client()).ParseArgs(cmd) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, "foo reverted to 1.0\n") c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) } func (s *SnapOpSuite) TestRevertNoMode(c *check.C) { s.runRevertTest(c, &client.SnapOptions{}) } func (s *SnapOpSuite) TestRevertDevMode(c *check.C) { s.runRevertTest(c, &client.SnapOptions{DevMode: true}) } func (s *SnapOpSuite) TestRevertJailMode(c *check.C) { s.runRevertTest(c, &client.SnapOptions{JailMode: true}) } func (s *SnapOpSuite) TestRevertClassic(c *check.C) { s.runRevertTest(c, &client.SnapOptions{Classic: true}) } func (s *SnapOpSuite) TestRevertMissingName(c *check.C) { _, err := snap.Parser(snap.Client()).ParseArgs([]string{"revert"}) c.Assert(err, check.NotNil) c.Assert(err, check.ErrorMatches, "the required argument `` was not provided") } func (s *SnapSuite) TestRefreshListLessOptions(c *check.C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Fatal("expected to get 0 requests") }) for _, flag := range []string{"--beta", "--channel=potato", "--classic"} { _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--list", flag}) c.Assert(err, check.ErrorMatches, "--list does not accept additional arguments") _, err = snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--list", flag, "some-snap"}) c.Assert(err, check.ErrorMatches, "--list does not accept additional arguments") } } func (s *SnapSuite) TestRefreshList(c *check.C) { n := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch n { case 0: c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/find") c.Check(r.URL.Query().Get("select"), check.Equals, "refresh") fmt.Fprintln(w, `{"type": "sync", "result": [{"name": "foo", "status": "active", "version": "4.2update1", "developer": "bar", "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"}, "revision":17,"summary":"some summary"}]}`) default: c.Fatalf("expected to get 1 requests, now on %d", n+1) } n++ }) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--list"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `Name +Version +Rev +Publisher +Notes foo +4.2update1 +17 +bar +-.* `) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(n, check.Equals, 1) } func (s *SnapSuite) TestRefreshLegacyTime(c *check.C) { n := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch n { case 0: c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/system-info") fmt.Fprintln(w, `{"type": "sync", "status-code": 200, "result": {"refresh": {"schedule": "00:00-04:59/5:00-10:59/11:00-16:59/17:00-23:59", "last": "2017-04-25T17:35:00+02:00", "next": "2017-04-26T00:58:00+02:00"}}}`) default: c.Fatalf("expected to get 1 requests, now on %d", n+1) } n++ }) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--time", "--abs-time"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, `schedule: 00:00-04:59/5:00-10:59/11:00-16:59/17:00-23:59 last: 2017-04-25T17:35:00+02:00 next: 2017-04-26T00:58:00+02:00 `) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(n, check.Equals, 1) } func (s *SnapSuite) TestRefreshTimer(c *check.C) { n := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch n { case 0: c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/system-info") fmt.Fprintln(w, `{"type": "sync", "status-code": 200, "result": {"refresh": {"timer": "0:00-24:00/4", "last": "2017-04-25T17:35:00+02:00", "next": "2017-04-26T00:58:00+02:00"}}}`) default: c.Fatalf("expected to get 1 requests, now on %d", n+1) } n++ }) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--time", "--abs-time"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, `timer: 0:00-24:00/4 last: 2017-04-25T17:35:00+02:00 next: 2017-04-26T00:58:00+02:00 `) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(n, check.Equals, 1) } func (s *SnapSuite) TestRefreshHold(c *check.C) { n := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch n { case 0: c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/system-info") fmt.Fprintln(w, `{"type": "sync", "status-code": 200, "result": {"refresh": {"timer": "0:00-24:00/4", "last": "2017-04-25T17:35:00+02:00", "next": "2017-04-26T00:58:00+02:00", "hold": "2017-04-28T00:00:00+02:00"}}}`) default: c.Fatalf("expected to get 1 requests, now on %d", n+1) } n++ }) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--time", "--abs-time"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, `timer: 0:00-24:00/4 last: 2017-04-25T17:35:00+02:00 hold: 2017-04-28T00:00:00+02:00 next: 2017-04-26T00:58:00+02:00 (but held) `) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(n, check.Equals, 1) } func (s *SnapSuite) TestRefreshNoTimerNoSchedule(c *check.C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/system-info") fmt.Fprintln(w, `{"type": "sync", "status-code": 200, "result": {"refresh": {"last": "2017-04-25T17:35:00+0200", "next": "2017-04-26T00:58:00+0200"}}}`) }) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--time"}) c.Assert(err, check.ErrorMatches, `internal error: both refresh.timer and refresh.schedule are empty`) } func (s *SnapOpSuite) TestRefreshOne(c *check.C) { s.RedirectClientToTestServer(s.srv.handle) s.srv.checker = func(r *http.Request) { c.Check(r.Method, check.Equals, "POST") c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ "action": "refresh", }) } _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "foo"}) c.Assert(err, check.IsNil) c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from Bar refreshed`) } func (s *SnapOpSuite) TestRefreshOneSwitchChannel(c *check.C) { s.RedirectClientToTestServer(s.srv.handle) s.srv.checker = func(r *http.Request) { c.Check(r.Method, check.Equals, "POST") c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ "action": "refresh", "channel": "beta", }) s.srv.channel = "beta" } _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--beta", "foo"}) c.Assert(err, check.IsNil) c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(beta\) 1.0 from Bar refreshed`) } func (s *SnapOpSuite) TestRefreshOneClassic(c *check.C) { s.RedirectClientToTestServer(s.srv.handle) s.srv.checker = func(r *http.Request) { c.Check(r.Method, check.Equals, "POST") c.Check(r.URL.Path, check.Equals, "/v2/snaps/one") c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ "action": "refresh", "classic": true, }) } _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--classic", "one"}) c.Assert(err, check.IsNil) } func (s *SnapOpSuite) TestRefreshOneDevmode(c *check.C) { s.RedirectClientToTestServer(s.srv.handle) s.srv.checker = func(r *http.Request) { c.Check(r.Method, check.Equals, "POST") c.Check(r.URL.Path, check.Equals, "/v2/snaps/one") c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ "action": "refresh", "devmode": true, }) } _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--devmode", "one"}) c.Assert(err, check.IsNil) } func (s *SnapOpSuite) TestRefreshOneJailmode(c *check.C) { s.RedirectClientToTestServer(s.srv.handle) s.srv.checker = func(r *http.Request) { c.Check(r.Method, check.Equals, "POST") c.Check(r.URL.Path, check.Equals, "/v2/snaps/one") c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ "action": "refresh", "jailmode": true, }) } _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--jailmode", "one"}) c.Assert(err, check.IsNil) } func (s *SnapOpSuite) TestRefreshOneIgnoreValidation(c *check.C) { s.RedirectClientToTestServer(s.srv.handle) s.srv.checker = func(r *http.Request) { c.Check(r.Method, check.Equals, "POST") c.Check(r.URL.Path, check.Equals, "/v2/snaps/one") c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ "action": "refresh", "ignore-validation": true, }) } _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--ignore-validation", "one"}) c.Assert(err, check.IsNil) } func (s *SnapOpSuite) TestRefreshOneRebooting(c *check.C) { s.RedirectClientToTestServer(s.srv.handle) s.srv.checker = func(r *http.Request) { c.Check(r.Method, check.Equals, "POST") c.Check(r.URL.Path, check.Equals, "/v2/snaps/core") c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ "action": "refresh", }) } s.srv.rebooting = true restore := mockArgs("snap", "refresh", "core") defer restore() err := snap.RunMain() c.Check(err, check.IsNil) c.Check(s.Stderr(), check.Equals, "snapd is about to reboot the system\n") } func (s *SnapOpSuite) TestRefreshOneModeErr(c *check.C) { s.RedirectClientToTestServer(nil) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--jailmode", "--devmode", "one"}) c.Assert(err, check.ErrorMatches, `cannot use devmode and jailmode flags together`) } func (s *SnapOpSuite) TestRefreshOneChanErr(c *check.C) { s.RedirectClientToTestServer(nil) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--beta", "--channel=foo", "one"}) c.Assert(err, check.ErrorMatches, `Please specify a single channel`) } func (s *SnapOpSuite) TestRefreshAllChannel(c *check.C) { s.RedirectClientToTestServer(nil) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--beta"}) c.Assert(err, check.ErrorMatches, `a single snap name is needed to specify mode or channel flags`) } func (s *SnapOpSuite) TestRefreshManyChannel(c *check.C) { s.RedirectClientToTestServer(nil) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--beta", "one", "two"}) c.Assert(err, check.ErrorMatches, `a single snap name is needed to specify mode or channel flags`) } func (s *SnapOpSuite) TestRefreshManyIgnoreValidation(c *check.C) { s.RedirectClientToTestServer(nil) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--ignore-validation", "one", "two"}) c.Assert(err, check.ErrorMatches, `a single snap name must be specified when ignoring validation`) } func (s *SnapOpSuite) TestRefreshAllModeFlags(c *check.C) { s.RedirectClientToTestServer(nil) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--devmode"}) c.Assert(err, check.ErrorMatches, `a single snap name is needed to specify mode or channel flags`) } func (s *SnapOpSuite) TestRefreshOneAmend(c *check.C) { s.RedirectClientToTestServer(s.srv.handle) s.srv.checker = func(r *http.Request) { c.Check(r.Method, check.Equals, "POST") c.Check(r.URL.Path, check.Equals, "/v2/snaps/one") c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ "action": "refresh", "amend": true, }) } _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--amend", "one"}) c.Assert(err, check.IsNil) } func (s *SnapOpSuite) runTryTest(c *check.C, opts *client.SnapOptions) { // pass relative path to cmd tryDir := "some-dir" modes := []struct { enabled bool name string }{ {opts.DevMode, "devmode"}, {opts.JailMode, "jailmode"}, {opts.Classic, "classic"}, } s.srv.checker = func(r *http.Request) { // ensure the client always sends the absolute path fullTryDir, err := filepath.Abs(tryDir) c.Assert(err, check.IsNil) c.Check(r.URL.Path, check.Equals, "/v2/snaps") form := testForm(r, c) defer form.RemoveAll() c.Assert(form.Value["action"], check.HasLen, 1) c.Assert(form.Value["snap-path"], check.HasLen, 1) c.Check(form.File, check.HasLen, 0) c.Check(form.Value["action"][0], check.Equals, "try") c.Check(form.Value["snap-path"][0], check.Matches, regexp.QuoteMeta(fullTryDir)) for _, mode := range modes { if mode.enabled { c.Assert(form.Value[mode.name], check.HasLen, 1) c.Check(form.Value[mode.name][0], check.Equals, "true") } else { c.Check(form.Value[mode.name], check.IsNil) } } } s.RedirectClientToTestServer(s.srv.handle) cmd := []string{"try", tryDir} for _, mode := range modes { if mode.enabled { cmd = append(cmd, "--"+mode.name) } } rest, err := snap.Parser(snap.Client()).ParseArgs(cmd) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, fmt.Sprintf(`(?sm).*foo 1.0 mounted from .*%s`, tryDir)) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) } func (s *SnapOpSuite) TestTryNoMode(c *check.C) { s.runTryTest(c, &client.SnapOptions{}) } func (s *SnapOpSuite) TestTryDevMode(c *check.C) { s.runTryTest(c, &client.SnapOptions{DevMode: true}) } func (s *SnapOpSuite) TestTryJailMode(c *check.C) { s.runTryTest(c, &client.SnapOptions{JailMode: true}) } func (s *SnapOpSuite) TestTryClassic(c *check.C) { s.runTryTest(c, &client.SnapOptions{Classic: true}) } func (s *SnapOpSuite) TestTryNoSnapDirErrors(c *check.C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, check.Equals, "POST") w.WriteHeader(202) fmt.Fprintln(w, ` { "type": "error", "result": { "message":"error from server", "kind":"snap-not-a-snap" }, "status-code": 400 }`) }) cmd := []string{"try", "/"} _, err := snap.Parser(snap.Client()).ParseArgs(cmd) c.Assert(err, testutil.EqualsWrapped, ` "/" does not contain an unpacked snap. Try 'snapcraft prime' in your project directory, then 'snap try' again.`) } func (s *SnapOpSuite) TestTryMissingOpt(c *check.C) { oldArgs := os.Args defer func() { os.Args = oldArgs }() os.Args = []string{"snap", "try", "./"} var kind string s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, check.Equals, "POST", check.Commentf("%q", kind)) w.WriteHeader(400) fmt.Fprintf(w, ` { "type": "error", "result": { "message":"error from server", "value": "some-snap", "kind": %q }, "status-code": 400 }`, kind) }) type table struct { kind, expected string } tests := []table{ {"snap-needs-classic", "published using classic confinement"}, {"snap-needs-devmode", "only meant for development"}, } for _, test := range tests { kind = test.kind c.Check(snap.RunMain(), testutil.ContainsWrapped, test.expected, check.Commentf("%q", kind)) } } func (s *SnapOpSuite) TestInstallConfinedAsClassic(c *check.C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, check.Equals, "POST") w.WriteHeader(400) fmt.Fprintf(w, `{ "type": "error", "result": { "message":"error from server", "value": "some-snap", "kind": "snap-not-classic" }, "status-code": 400 }`) }) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--classic", "some-snap"}) c.Assert(err, check.ErrorMatches, `snap "some-snap" is not compatible with --classic`) } func (s *SnapSuite) TestInstallChannelDuplicationError(c *check.C) { _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--edge", "--beta", "some-snap"}) c.Assert(err, check.ErrorMatches, "Please specify a single channel") } func (s *SnapSuite) TestRefreshChannelDuplicationError(c *check.C) { _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--edge", "--beta", "some-snap"}) c.Assert(err, check.ErrorMatches, "Please specify a single channel") } func (s *SnapOpSuite) TestInstallFromChannel(c *check.C) { s.srv.checker = func(r *http.Request) { c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ "action": "install", "channel": "edge", }) s.srv.channel = "edge" } s.RedirectClientToTestServer(s.srv.handle) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--edge", "foo"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(edge\) 1.0 from Bar installed`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) } func (s *SnapOpSuite) TestEnable(c *check.C) { s.srv.total = 3 s.srv.checker = func(r *http.Request) { c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ "action": "enable", }) } s.RedirectClientToTestServer(s.srv.handle) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"enable", "foo"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `(?sm).*foo enabled`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) } func (s *SnapOpSuite) TestDisable(c *check.C) { s.srv.total = 3 s.srv.checker = func(r *http.Request) { c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ "action": "disable", }) } s.RedirectClientToTestServer(s.srv.handle) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"disable", "foo"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `(?sm).*foo disabled`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) } func (s *SnapOpSuite) TestRemove(c *check.C) { s.srv.total = 3 s.srv.checker = func(r *http.Request) { c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ "action": "remove", }) } s.RedirectClientToTestServer(s.srv.handle) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"remove", "foo"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `(?sm).*foo removed`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) } func (s *SnapOpSuite) TestRemoveRevision(c *check.C) { s.srv.total = 3 s.srv.checker = func(r *http.Request) { c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ "action": "remove", "revision": "17", }) } s.RedirectClientToTestServer(s.srv.handle) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"remove", "--revision=17", "foo"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(revision 17\) removed`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) } func (s *SnapOpSuite) TestRemoveManyRevision(c *check.C) { s.RedirectClientToTestServer(nil) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"remove", "--revision=17", "one", "two"}) c.Assert(err, check.ErrorMatches, `a single snap name is needed to specify the revision`) } func (s *SnapOpSuite) TestRemoveMany(c *check.C) { total := 3 n := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch n { case 0: c.Check(r.URL.Path, check.Equals, "/v2/snaps") c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ "action": "remove", "snaps": []interface{}{"one", "two"}, }) c.Check(r.Method, check.Equals, "POST") w.WriteHeader(202) fmt.Fprintln(w, `{"type":"async", "change": "42", "status-code": 202}`) case 1: c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/changes/42") fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}}`) case 2: c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/changes/42") fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {"snap-names": ["one","two"]}}}`) default: c.Fatalf("expected to get %d requests, now on %d", total, n+1) } n++ }) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"remove", "one", "two"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `(?sm).*one removed`) c.Check(s.Stdout(), check.Matches, `(?sm).*two removed`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(n, check.Equals, total) } func (s *SnapOpSuite) TestInstallManyChannel(c *check.C) { s.RedirectClientToTestServer(nil) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--beta", "one", "two"}) c.Assert(err, check.ErrorMatches, `a single snap name is needed to specify mode or channel flags`) } func (s *SnapOpSuite) TestInstallManyMixFileAndStore(c *check.C) { s.RedirectClientToTestServer(nil) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "store-snap", "./local.snap"}) c.Assert(err, check.ErrorMatches, `only one snap file can be installed at a time`) } func (s *SnapOpSuite) TestInstallMany(c *check.C) { total := 4 n := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch n { case 0: c.Check(r.URL.Path, check.Equals, "/v2/snaps") c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ "action": "install", "snaps": []interface{}{"one", "two"}, }) c.Check(r.Method, check.Equals, "POST") w.WriteHeader(202) fmt.Fprintln(w, `{"type":"async", "change": "42", "status-code": 202}`) case 1: c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/changes/42") fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}}`) case 2: c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/changes/42") fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {"snap-names": ["one","two"]}}}`) case 3: c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/snaps") fmt.Fprintf(w, `{"type": "sync", "result": [{"name": "one", "status": "active", "version": "1.0", "developer": "bar", "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"}, "revision":42, "channel":"stable"},{"name": "two", "status": "active", "version": "2.0", "developer": "baz", "publisher": {"id": "baz-id", "username": "baz", "display-name": "Baz", "validation": "unproven"}, "revision":42, "channel":"edge"}]}\n`) default: c.Fatalf("expected to get %d requests, now on %d", total, n+1) } n++ }) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "one", "two"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) // note that (stable) is omitted c.Check(s.Stdout(), check.Matches, `(?sm).*one 1.0 from Bar installed`) c.Check(s.Stdout(), check.Matches, `(?sm).*two \(edge\) 2.0 from Baz installed`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(n, check.Equals, total) } func (s *SnapOpSuite) TestInstallZeroEmpty(c *check.C) { _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install"}) c.Assert(err, check.ErrorMatches, "cannot install zero snaps") _, err = snap.Parser(snap.Client()).ParseArgs([]string{"install", ""}) c.Assert(err, check.ErrorMatches, "cannot install snap with empty name") _, err = snap.Parser(snap.Client()).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) {} cmds := [][]string{ {"remove", "--no-wait", "foo"}, {"remove", "--no-wait", "foo", "bar"}, {"install", "--no-wait", "foo"}, {"install", "--no-wait", "foo", "bar"}, {"revert", "--no-wait", "foo"}, {"refresh", "--no-wait", "foo"}, {"refresh", "--no-wait", "foo", "bar"}, {"refresh", "--no-wait"}, {"enable", "--no-wait", "foo"}, {"disable", "--no-wait", "foo"}, {"try", "--no-wait", "."}, {"switch", "--no-wait", "--channel=foo", "bar"}, // commands that use waitMixin from elsewhere {"start", "--no-wait", "foo"}, {"stop", "--no-wait", "foo"}, {"restart", "--no-wait", "foo"}, {"alias", "--no-wait", "foo", "bar"}, {"unalias", "--no-wait", "foo"}, {"prefer", "--no-wait", "foo"}, {"set", "--no-wait", "foo", "bar=baz"}, {"disconnect", "--no-wait", "foo:bar"}, {"connect", "--no-wait", "foo:bar"}, } s.RedirectClientToTestServer(s.srv.handle) for _, cmd := range cmds { rest, err := snap.Parser(snap.Client()).ParseArgs(cmd) c.Assert(err, check.IsNil, check.Commentf("%v", cmd)) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, "(?sm)42\n") c.Check(s.Stderr(), check.Equals, "") c.Check(s.srv.n, check.Equals, 1) // reset s.srv.n = 0 s.stdout.Reset() } } func (s *SnapOpSuite) TestNoWaitImmediateError(c *check.C) { cmds := [][]string{ {"remove", "--no-wait", "foo"}, {"remove", "--no-wait", "foo", "bar"}, {"install", "--no-wait", "foo"}, {"install", "--no-wait", "foo", "bar"}, {"revert", "--no-wait", "foo"}, {"refresh", "--no-wait", "foo"}, {"refresh", "--no-wait", "foo", "bar"}, {"refresh", "--no-wait"}, {"enable", "--no-wait", "foo"}, {"disable", "--no-wait", "foo"}, {"try", "--no-wait", "."}, {"switch", "--no-wait", "--channel=foo", "bar"}, // commands that use waitMixin from elsewhere {"start", "--no-wait", "foo"}, {"stop", "--no-wait", "foo"}, {"restart", "--no-wait", "foo"}, {"alias", "--no-wait", "foo", "bar"}, {"unalias", "--no-wait", "foo"}, {"prefer", "--no-wait", "foo"}, {"set", "--no-wait", "foo", "bar=baz"}, {"disconnect", "--no-wait", "foo:bar"}, {"connect", "--no-wait", "foo:bar"}, } s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, `{"type": "error", "result": {"message": "failure"}}`) }) for _, cmd := range cmds { _, err := snap.Parser(snap.Client()).ParseArgs(cmd) c.Assert(err, check.ErrorMatches, "failure", check.Commentf("%v", cmd)) } } func (s *SnapOpSuite) TestWaitServerError(c *check.C) { r := snap.MockMaxGoneTime(0) defer r() cmds := [][]string{ {"remove", "foo"}, {"remove", "foo", "bar"}, {"install", "foo"}, {"install", "foo", "bar"}, {"revert", "foo"}, {"refresh", "foo"}, {"refresh", "foo", "bar"}, {"refresh"}, {"enable", "foo"}, {"disable", "foo"}, {"try", "."}, {"switch", "--channel=foo", "bar"}, // commands that use waitMixin from elsewhere {"start", "foo"}, {"stop", "foo"}, {"restart", "foo"}, {"alias", "foo", "bar"}, {"unalias", "foo"}, {"prefer", "foo"}, {"set", "foo", "bar=baz"}, {"disconnect", "foo:bar"}, {"connect", "foo:bar"}, } n := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { n++ if n == 1 { w.WriteHeader(202) fmt.Fprintln(w, `{"type":"async", "change": "42", "status-code": 202}`) return } if n == 3 { fmt.Fprintln(w, `{"type": "error", "result": {"message": "unexpected request"}}`) return } fmt.Fprintln(w, `{"type": "error", "result": {"message": "server error"}}`) }) for _, cmd := range cmds { _, err := snap.Parser(snap.Client()).ParseArgs(cmd) c.Assert(err, check.ErrorMatches, "server error", check.Commentf("%v", cmd)) // reset n = 0 } } func (s *SnapOpSuite) TestSwitchHappy(c *check.C) { s.srv.total = 3 s.srv.checker = func(r *http.Request) { c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ "action": "switch", "channel": "beta", }) } s.RedirectClientToTestServer(s.srv.handle) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"switch", "--beta", "foo"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `(?sm).*"foo" switched to the "beta" channel`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) } func (s *SnapOpSuite) TestSwitchUnhappy(c *check.C) { _, err := snap.Parser(snap.Client()).ParseArgs([]string{"switch"}) c.Assert(err, check.ErrorMatches, "the required argument `` was not provided") } func (s *SnapOpSuite) TestSwitchAlsoUnhappy(c *check.C) { _, err := snap.Parser(snap.Client()).ParseArgs([]string{"switch", "foo"}) c.Assert(err, check.ErrorMatches, `missing --channel= parameter`) } func (s *SnapOpSuite) TestSnapOpNetworkTimeoutError(c *check.C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, check.Equals, "POST") w.WriteHeader(202) w.Write([]byte(` { "type": "error", "result": { "message":"Get https://api.snapcraft.io/api/v1/snaps/details/hello?channel=stable&fields=anon_download_url%2Carchitecture%2Cchannel%2Cdownload_sha3_384%2Csummary%2Cdescription%2Cdeltas%2Cbinary_filesize%2Cdownload_url%2Cepoch%2Cicon_url%2Clast_updated%2Cpackage_name%2Cprices%2Cpublisher%2Cratings_average%2Crevision%2Cscreenshot_urls%2Csnap_id%2Clicense%2Cbase%2Csupport_url%2Ccontact%2Ctitle%2Ccontent%2Cversion%2Corigin%2Cdeveloper_id%2Cprivate%2Cconfinement%2Cchannel_maps_list: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)", "kind":"network-timeout" }, "status-code": 400 } `)) }) cmd := []string{"install", "hello"} _, err := snap.Parser(snap.Client()).ParseArgs(cmd) c.Assert(err, check.ErrorMatches, `unable to contact snap store`) } snapd-2.37.4~14.04.1/cmd/snap/cmd_run.go0000664000000000000000000006732313435556260014236 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-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 . * */ package main import ( "bufio" "fmt" "io" "io/ioutil" "os" "os/exec" "os/user" "path/filepath" "regexp" "strconv" "strings" "syscall" "time" "github.com/godbus/dbus" "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/osutil/strace" "github.com/snapcore/snapd/selinux" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snapenv" "github.com/snapcore/snapd/strutil/shlex" "github.com/snapcore/snapd/timeutil" "github.com/snapcore/snapd/x11" ) var ( syscallExec = syscall.Exec userCurrent = user.Current osGetenv = os.Getenv timeNow = time.Now selinuxIsEnabled = selinux.IsEnabled selinuxVerifyPathContext = selinux.VerifyPathContext selinuxRestoreContext = selinux.RestoreContext ) type cmdRun struct { clientMixin Command string `long:"command" hidden:"yes"` HookName string `long:"hook" hidden:"yes"` Revision string `short:"r" default:"unset" hidden:"yes"` Shell bool `long:"shell" ` // 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. Strace string `long:"strace" optional:"true" optional-value:"with-strace" default:"no-strace" default-mask:"-"` Gdb bool `long:"gdb"` TraceExec bool `long:"trace-exec"` // not a real option, used to check if cmdRun is initialized by // the parser ParserRan int `long:"parser-ran" default:"1" hidden:"yes"` Timer string `long:"timer" hidden:"yes"` } func init() { addCommand("run", i18n.G("Run the given snap command"), i18n.G(` The run command executes the given snap command with the right confinement and environment. `), func() flags.Commander { return &cmdRun{} }, map[string]string{ // TRANSLATORS: This should not start with a lowercase letter. "command": i18n.G("Alternative command to run"), // TRANSLATORS: This should not start with a lowercase letter. "hook": i18n.G("Hook to run"), // TRANSLATORS: This should not start with a lowercase letter. "r": i18n.G("Use a specific snap revision when running hook"), // TRANSLATORS: This should not start with a lowercase letter. "shell": i18n.G("Run a shell instead of the command (useful for debugging)"), // TRANSLATORS: This should not start with a lowercase letter. "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."), // TRANSLATORS: This should not start with a lowercase letter. "gdb": i18n.G("Run the command with gdb"), // TRANSLATORS: This should not start with a lowercase letter. "timer": i18n.G("Run as a timer service with given schedule"), // TRANSLATORS: This should not start with a lowercase letter. "trace-exec": i18n.G("Display exec calls timing data"), "parser-ran": "", }, nil) } func maybeWaitForSecurityProfileRegeneration(cli *client.Client) error { // check if the security profiles key has changed, if so, we need // to wait for snapd to re-generate all profiles mismatch, err := interfaces.SystemKeyMismatch() if err == nil && !mismatch { return nil } // something went wrong with the system-key compare, try to // reach snapd before continuing if err != nil { logger.Debugf("SystemKeyMismatch returned an error: %v", err) } // We have a mismatch, try to connect to snapd, once we can // connect we just continue because that usually means that // a new snapd is ready and has generated profiles. // // There is a corner case if an upgrade leaves the old snapd // running and we connect to the old snapd. Handling this // correctly is tricky because our "snap run" pipeline may // depend on profiles written by the new snapd. So for now we // just continue and hope for the best. The real fix for this // is to fix the packaging so that snapd is stopped, upgraded // and started. // // connect timeout for client is 5s on each try, so 12*5s = 60s timeout := 12 if timeoutEnv := os.Getenv("SNAPD_DEBUG_SYSTEM_KEY_RETRY"); timeoutEnv != "" { if i, err := strconv.Atoi(timeoutEnv); err == nil { timeout = i } } for i := 0; i < timeout; i++ { if _, err := cli.SysInfo(); err == nil { return nil } // sleep a litte bit for good measure time.Sleep(1 * time.Second) } return fmt.Errorf("timeout waiting for snap system profiles to get updated") } func (x *cmdRun) Execute(args []string) error { if len(args) == 0 { return fmt.Errorf(i18n.G("need the application to run as argument")) } snapApp := args[0] args = args[1:] // Catch some invalid parameter combinations, provide helpful errors optionsSet := 0 for _, param := range []string{x.HookName, x.Command, x.Timer} { if param != "" { optionsSet++ } } if optionsSet > 1 { return fmt.Errorf("you can only use one of --hook, --command, and --timer") } if x.Revision != "unset" && x.Revision != "" && x.HookName == "" { return fmt.Errorf(i18n.G("-r can only be used with --hook")) } if x.HookName != "" && len(args) > 0 { // TRANSLATORS: %q is the hook name; %s a space-separated list of extra arguments return fmt.Errorf(i18n.G("too many arguments for hook %q: %s"), x.HookName, strings.Join(args, " ")) } if err := maybeWaitForSecurityProfileRegeneration(x.client); err != nil { return err } // Now actually handle the dispatching if x.HookName != "" { return x.snapRunHook(snapApp) } if x.Command == "complete" { snapApp, args = antialias(snapApp, args) } if x.Timer != "" { return x.snapRunTimer(snapApp, x.Timer, args) } return x.snapRunApp(snapApp, args) } // antialias changes snapApp and args if snapApp is actually an alias // for something else. If not, or if the args aren't what's expected // for completion, it returns them unchanged. func antialias(snapApp string, args []string) (string, []string) { if len(args) < 7 { // NOTE if len(args) < 7, Something is Wrong (at least WRT complete.sh and etelpmoc.sh) return snapApp, args } actualApp, err := resolveApp(snapApp) if err != nil || actualApp == snapApp { // no alias! woop. return snapApp, args } compPoint, err := strconv.Atoi(args[2]) if err != nil { // args[2] is not COMP_POINT return snapApp, args } if compPoint <= len(snapApp) { // COMP_POINT is inside $0 return snapApp, args } if compPoint > len(args[5]) { // COMP_POINT is bigger than $# return snapApp, args } if args[6] != snapApp { // args[6] is not COMP_WORDS[0] return snapApp, args } // it _should_ be COMP_LINE followed by one of // COMP_WORDBREAKS, but that's hard to do re, err := regexp.Compile(`^` + regexp.QuoteMeta(snapApp) + `\b`) if err != nil || !re.MatchString(args[5]) { // (weird regexp error, or) args[5] is not COMP_LINE return snapApp, args } argsOut := make([]string, len(args)) copy(argsOut, args) argsOut[2] = strconv.Itoa(compPoint - len(snapApp) + len(actualApp)) argsOut[5] = re.ReplaceAllLiteralString(args[5], actualApp) argsOut[6] = actualApp return actualApp, argsOut } func getSnapInfo(snapName string, revision snap.Revision) (info *snap.Info, err error) { if revision.Unset() { info, err = snap.ReadCurrentInfo(snapName) } else { info, err = snap.ReadInfo(snapName, &snap.SideInfo{ Revision: revision, }) } return info, err } func createOrUpdateUserDataSymlink(info *snap.Info, usr *user.User) error { // 'current' symlink for user data (SNAP_USER_DATA) userData := info.UserDataDir(usr.HomeDir) wantedSymlinkValue := filepath.Base(userData) currentActiveSymlink := filepath.Join(userData, "..", "current") var err error var currentSymlinkValue string for i := 0; i < 5; i++ { currentSymlinkValue, err = os.Readlink(currentActiveSymlink) // Failure other than non-existing symlink is fatal if err != nil && !os.IsNotExist(err) { // TRANSLATORS: %v the error message return fmt.Errorf(i18n.G("cannot read symlink: %v"), err) } if currentSymlinkValue == wantedSymlinkValue { break } if err == nil { // We may be racing with other instances of snap-run that try to do the same thing // If the symlink is already removed then we can ignore this error. err = os.Remove(currentActiveSymlink) if err != nil && !os.IsNotExist(err) { // abort with error break } } err = os.Symlink(wantedSymlinkValue, currentActiveSymlink) // Error other than symlink already exists will abort and be propagated if err == nil || !os.IsExist(err) { break } // If we arrived here it means the symlink couldn't be created because it got created // in the meantime by another instance, so we will try again. } if err != nil { return fmt.Errorf(i18n.G("cannot update the 'current' symlink of %q: %v"), currentActiveSymlink, err) } return nil } func createUserDataDirs(info *snap.Info) error { usr, err := userCurrent() if err != nil { return fmt.Errorf(i18n.G("cannot get the current user: %v"), err) } // see snapenv.User instanceUserData := info.UserDataDir(usr.HomeDir) instanceCommonUserData := info.UserCommonDataDir(usr.HomeDir) createDirs := []string{instanceUserData, instanceCommonUserData} if info.InstanceKey != "" { // parallel instance snaps get additional mapping in their mount // namespace, namely /home/joe/snap/foo_bar -> // /home/joe/snap/foo, make sure that the mount point exists and // is owned by the user snapUserDir := snap.UserSnapDir(usr.HomeDir, info.SnapName()) createDirs = append(createDirs, snapUserDir) } for _, d := range createDirs { if err := os.MkdirAll(d, 0755); err != nil { // TRANSLATORS: %q is the directory whose creation failed, %v the error message return fmt.Errorf(i18n.G("cannot create %q: %v"), d, err) } } if err := createOrUpdateUserDataSymlink(info, usr); err != nil { return err } return maybeRestoreSecurityContext(usr) } // maybeRestoreSecurityContext attempts to restore security context of ~/snap on // systems where it's applicable func maybeRestoreSecurityContext(usr *user.User) error { snapUserHome := filepath.Join(usr.HomeDir, dirs.UserHomeSnapDir) enabled, err := selinuxIsEnabled() if err != nil { return fmt.Errorf("cannot determine SELinux status: %v", err) } if !enabled { logger.Debugf("SELinux not enabled") return nil } match, err := selinuxVerifyPathContext(snapUserHome) if err != nil { return fmt.Errorf("failed to verify SELinux context of %v: %v", snapUserHome, err) } if match { return nil } logger.Noticef("restoring default SELinux context of %v", snapUserHome) if err := selinuxRestoreContext(snapUserHome, selinux.RestoreMode{Recursive: true}); err != nil { return fmt.Errorf("cannot restore SELinux context of %v: %v", snapUserHome, err) } return nil } func (x *cmdRun) useStrace() bool { return x.ParserRan == 1 && x.Strace != "no-strace" } func (x *cmdRun) straceOpts() (opts []string, raw bool, err error) { if x.Strace == "with-strace" { return nil, false, nil } split, err := shlex.Split(x.Strace) if err != nil { return nil, false, err } opts = make([]string, 0, len(split)) for _, opt := range split { if opt == "--raw" { raw = true continue } opts = append(opts, opt) } return opts, raw, nil } func (x *cmdRun) snapRunApp(snapApp string, args []string) error { snapName, appName := snap.SplitSnapApp(snapApp) info, err := getSnapInfo(snapName, snap.R(0)) if err != nil { return err } app := info.Apps[appName] if app == nil { return fmt.Errorf(i18n.G("cannot find app %q in %q"), appName, snapName) } return x.runSnapConfine(info, app.SecurityTag(), snapApp, "", args) } func (x *cmdRun) snapRunHook(snapName string) error { revision, err := snap.ParseRevision(x.Revision) if err != nil { return err } info, err := getSnapInfo(snapName, revision) if err != nil { return err } hook := info.Hooks[x.HookName] if hook == nil { return fmt.Errorf(i18n.G("cannot find hook %q in %q"), x.HookName, snapName) } return x.runSnapConfine(info, hook.SecurityTag(), snapName, hook.Name, nil) } func (x *cmdRun) snapRunTimer(snapApp, timer string, args []string) error { schedule, err := timeutil.ParseSchedule(timer) if err != nil { return fmt.Errorf("invalid timer format: %v", err) } now := timeNow() if !timeutil.Includes(schedule, now) { fmt.Fprintf(Stderr, "%s: attempted to run %q timer outside of scheduled time %q\n", now.Format(time.RFC3339), snapApp, timer) return nil } return x.snapRunApp(snapApp, args) } var osReadlink = os.Readlink func isReexeced() bool { exe, err := osReadlink("/proc/self/exe") if err != nil { logger.Noticef("cannot read /proc/self/exe: %v", err) return false } return strings.HasPrefix(exe, dirs.SnapMountDir) } func migrateXauthority(info *snap.Info) (string, error) { u, err := userCurrent() if err != nil { return "", fmt.Errorf(i18n.G("cannot get the current user: %s"), err) } // If our target directory (XDG_RUNTIME_DIR) doesn't exist we // don't attempt to create it. baseTargetDir := filepath.Join(dirs.XdgRuntimeDirBase, u.Uid) if !osutil.FileExists(baseTargetDir) { return "", nil } xauthPath := osGetenv("XAUTHORITY") if len(xauthPath) == 0 || !osutil.FileExists(xauthPath) { // Nothing to do for us. Most likely running outside of any // graphical X11 session. return "", nil } fin, err := os.Open(xauthPath) if err != nil { return "", err } defer fin.Close() // Abs() also calls Clean(); see https://golang.org/pkg/path/filepath/#Abs xauthPathAbs, err := filepath.Abs(fin.Name()) if err != nil { return "", nil } // Remove all symlinks from path xauthPathCan, err := filepath.EvalSymlinks(xauthPathAbs) if err != nil { return "", nil } // Ensure the XAUTHORITY env is not abused by checking that // it point to exactly the file we just opened (no symlinks, // no funny "../.." etc) if fin.Name() != xauthPathCan { logger.Noticef("WARNING: XAUTHORITY environment value is not a clean path: %q", xauthPathCan) return "", nil } // Only do the migration from /tmp since the real /tmp is not visible for snaps if !strings.HasPrefix(fin.Name(), "/tmp/") { return "", nil } // We are performing a Stat() here to make sure that the user can't // steal another user's Xauthority file. Note that while Stat() uses // fstat() on the file descriptor created during Open(), the file might // have changed ownership between the Open() and the Stat(). That's ok // because we aren't trying to block access that the user already has: // if the user has the privileges to chown another user's Xauthority // file, we won't block that since the user can just steal it without // having to use snap run. This code is just to ensure that a user who // doesn't have those privileges can't steal the file via snap run // (also note that the (potentially untrusted) snap isn't running yet). fi, err := fin.Stat() if err != nil { return "", err } sys := fi.Sys() if sys == nil { return "", fmt.Errorf(i18n.G("cannot validate owner of file %s"), fin.Name()) } // cheap comparison as the current uid is only available as a string // but it is better to convert the uid from the stat result to a // string than a string into a number. if fmt.Sprintf("%d", sys.(*syscall.Stat_t).Uid) != u.Uid { return "", fmt.Errorf(i18n.G("Xauthority file isn't owned by the current user %s"), u.Uid) } targetPath := filepath.Join(baseTargetDir, ".Xauthority") // Only validate Xauthority file again when both files don't match // otherwise we can continue using the existing Xauthority file. // This is ok to do here because we aren't trying to protect against // the user changing the Xauthority file in XDG_RUNTIME_DIR outside // of snapd. if osutil.FileExists(targetPath) { var fout *os.File if fout, err = os.Open(targetPath); err != nil { return "", err } if osutil.StreamsEqual(fin, fout) { fout.Close() return targetPath, nil } fout.Close() if err := os.Remove(targetPath); err != nil { return "", err } // Ensure we're validating the Xauthority file from the beginning if _, err := fin.Seek(int64(os.SEEK_SET), 0); err != nil { return "", err } } // To guard against setting XAUTHORITY to non-xauth files, check // that we have a valid Xauthority. Specifically, the file must be // parseable as an Xauthority file and not be empty. if err := x11.ValidateXauthority(fin); err != nil { return "", err } // Read data from the beginning of the file if _, err = fin.Seek(int64(os.SEEK_SET), 0); err != nil { return "", err } fout, err := os.OpenFile(targetPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) if err != nil { return "", err } defer fout.Close() // Read and write validated Xauthority file to its right location if _, err = io.Copy(fout, fin); err != nil { if err := os.Remove(targetPath); err != nil { logger.Noticef("WARNING: cannot remove file at %s: %s", targetPath, err) } return "", fmt.Errorf(i18n.G("cannot write new Xauthority file at %s: %s"), targetPath, err) } return targetPath, nil } func activateXdgDocumentPortal(info *snap.Info, snapApp, hook string) error { // Don't do anything for apps or hooks that don't plug the // desktop interface // // NOTE: This check is imperfect because we don't really know // if the interface is connected or not but this is an // acceptable compromise for not having to communicate with // snapd in snap run. In a typical desktop session the // document portal can be in use by many applications, not // just by snaps, so this is at most, pre-emptively using some // extra memory. var plugs map[string]*snap.PlugInfo if hook != "" { plugs = info.Hooks[hook].Plugs } else { _, appName := snap.SplitSnapApp(snapApp) plugs = info.Apps[appName].Plugs } plugsDesktop := false for _, plug := range plugs { if plug.Interface == "desktop" { plugsDesktop = true break } } if !plugsDesktop { return nil } u, err := userCurrent() if err != nil { return fmt.Errorf(i18n.G("cannot get the current user: %s"), err) } xdgRuntimeDir := filepath.Join(dirs.XdgRuntimeDirBase, u.Uid) // If $XDG_RUNTIME_DIR/doc appears to be a mount point, assume // that the document portal is up and running. expectedMountPoint := filepath.Join(xdgRuntimeDir, "doc") if mounted, err := osutil.IsMounted(expectedMountPoint); err != nil { logger.Noticef("Could not check document portal mount state: %s", err) } else if mounted { return nil } // If there is no session bus, our job is done. We check this // manually to avoid dbus.SessionBus() auto-launching a new // bus. busAddress := osGetenv("DBUS_SESSION_BUS_ADDRESS") if len(busAddress) == 0 { return nil } // We've previously tried to start the document portal and // were told the service is unknown: don't bother connecting // to the session bus again. // // As the file is in $XDG_RUNTIME_DIR, it will be cleared over // full logout/login or reboot cycles. portalsUnavailableFile := filepath.Join(xdgRuntimeDir, ".portals-unavailable") if osutil.FileExists(portalsUnavailableFile) { return nil } conn, err := dbus.SessionBus() if err != nil { return err } portal := conn.Object("org.freedesktop.portal.Documents", "/org/freedesktop/portal/documents") var mountPoint []byte if err := portal.Call("org.freedesktop.portal.Documents.GetMountPoint", 0).Store(&mountPoint); err != nil { // It is not considered an error if // xdg-document-portal is not available on the system. if dbusErr, ok := err.(dbus.Error); ok && dbusErr.Name == "org.freedesktop.DBus.Error.ServiceUnknown" { // We ignore errors here: if writing the file // fails, we'll just try connecting to D-Bus // again next time. if err = ioutil.WriteFile(portalsUnavailableFile, []byte(""), 0644); err != nil { logger.Noticef("WARNING: cannot write file at %s: %s", portalsUnavailableFile, err) } return nil } return err } // Sanity check to make sure the document portal is exposed // where we think it is. actualMountPoint := strings.TrimRight(string(mountPoint), "\x00") if actualMountPoint != expectedMountPoint { return fmt.Errorf(i18n.G("Expected portal at %#v, got %#v"), expectedMountPoint, actualMountPoint) } return nil } func (x *cmdRun) runCmdUnderGdb(origCmd, env []string) error { env = append(env, "SNAP_CONFINE_RUN_UNDER_GDB=1") cmd := []string{"sudo", "-E", "gdb", "-ex=run", "-ex=catch exec", "-ex=continue", "--args"} cmd = append(cmd, origCmd...) gcmd := exec.Command(cmd[0], cmd[1:]...) gcmd.Stdin = os.Stdin gcmd.Stdout = os.Stdout gcmd.Stderr = os.Stderr gcmd.Env = env return gcmd.Run() } func (x *cmdRun) runCmdWithTraceExec(origCmd, env []string) error { // setup private tmp dir with strace fifo straceTmp, err := ioutil.TempDir("", "exec-trace") if err != nil { return err } defer os.RemoveAll(straceTmp) straceLog := filepath.Join(straceTmp, "strace.fifo") if err := syscall.Mkfifo(straceLog, 0640); err != nil { return err } // ensure we have one writer on the fifo so that if strace fails // nothing blocks fw, err := os.OpenFile(straceLog, os.O_RDWR, 0640) if err != nil { return err } defer fw.Close() // read strace data from fifo async var slg *strace.ExecveTiming var straceErr error doneCh := make(chan bool, 1) go func() { // FIXME: make this configurable? nSlowest := 10 slg, straceErr = strace.TraceExecveTimings(straceLog, nSlowest) close(doneCh) }() cmd, err := strace.TraceExecCommand(straceLog, origCmd...) if err != nil { return err } // run cmd.Env = env cmd.Stdin = Stdin cmd.Stdout = Stdout cmd.Stderr = Stderr err = cmd.Run() // ensure we close the fifo here so that the strace.TraceExecCommand() // helper gets a EOF from the fifo (i.e. all writers must be closed // for this) fw.Close() // wait for strace reader <-doneCh if straceErr == nil { slg.Display(Stderr) } else { logger.Noticef("cannot extract runtime data: %v", straceErr) } return err } func (x *cmdRun) runCmdUnderStrace(origCmd, env []string) error { extraStraceOpts, raw, err := x.straceOpts() if err != nil { return err } cmd, err := strace.Command(extraStraceOpts, origCmd...) if err != nil { return err } // run with filter cmd.Env = env cmd.Stdin = Stdin cmd.Stdout = Stdout stderr, err := cmd.StderrPipe() if err != nil { return err } filterDone := make(chan bool, 1) go func() { defer func() { filterDone <- true }() if raw { // Passing --strace='--raw' disables the filtering of // early strace output. This is useful when tracking // down issues with snap helpers such as snap-confine, // snap-exec ... io.Copy(Stderr, stderr) return } r := bufio.NewReader(stderr) // The first thing from strace if things work is // "exeve(" - show everything until we see this to // not swallow real strace errors. for { s, err := r.ReadString('\n') if err != nil { break } if strings.Contains(s, "execve(") { break } fmt.Fprint(Stderr, s) } // The last thing that snap-exec does is to // execve() something inside the snap dir so // we know that from that point on the output // will be interessting to the user. // // We need check both /snap (which is where snaps // are located inside the mount namespace) and the // distro snap mount dir (which is different on e.g. // fedora/arch) to fully work with classic snaps. needle1 := fmt.Sprintf(`execve("%s`, dirs.SnapMountDir) needle2 := `execve("/snap` for { s, err := r.ReadString('\n') if err != nil { if err != io.EOF { fmt.Fprintf(Stderr, "cannot read strace output: %s\n", err) } break } // Ensure we catch the execve but *not* the // exec into // /snap/core/current/usr/lib/snapd/snap-confine // which is just `snap run` using the core version // snap-confine. if (strings.Contains(s, needle1) || strings.Contains(s, needle2)) && !strings.Contains(s, "usr/lib/snapd/snap-confine") { fmt.Fprint(Stderr, s) break } } io.Copy(Stderr, r) }() if err := cmd.Start(); err != nil { return err } <-filterDone err = cmd.Wait() return err } func (x *cmdRun) runSnapConfine(info *snap.Info, securityTag, snapApp, hook string, args []string) error { snapConfine := filepath.Join(dirs.DistroLibExecDir, "snap-confine") // if we re-exec, we must run the snap-confine from the core/snapd snap // as well, if they get out of sync, havoc will happen if isReexeced() { // exe is something like /snap/{snapd,core}/123/usr/bin/snap exe, err := osReadlink("/proc/self/exe") if err != nil { return err } // snapBase will be "/snap/{core,snapd}/$rev/" because // the snap binary is always at $root/usr/bin/snap snapBase := filepath.Clean(filepath.Join(filepath.Dir(exe), "..", "..")) // Run snap-confine from the core/snapd snap. That // will work because snap-confine on the core/snapd snap is // mostly statically linked (except libudev and libc) snapConfine = filepath.Join(snapBase, dirs.CoreLibExecDir, "snap-confine") } if !osutil.FileExists(snapConfine) { if hook != "" { logger.Noticef("WARNING: skipping running hook %q of snap %q: missing snap-confine", hook, info.InstanceName()) return nil } return fmt.Errorf(i18n.G("missing snap-confine: try updating your core/snapd package")) } if err := createUserDataDirs(info); err != nil { logger.Noticef("WARNING: cannot create user data directory: %s", err) } xauthPath, err := migrateXauthority(info) if err != nil { logger.Noticef("WARNING: cannot copy user Xauthority file: %s", err) } if err := activateXdgDocumentPortal(info, snapApp, hook); err != nil { logger.Noticef("WARNING: cannot start document portal: %s", err) } cmd := []string{snapConfine} if info.NeedsClassic() { cmd = append(cmd, "--classic") } if info.Base != "" { cmd = append(cmd, "--base", info.Base) } cmd = append(cmd, securityTag) // when under confinement, snap-exec is run from 'core' snap rootfs snapExecPath := filepath.Join(dirs.CoreLibExecDir, "snap-exec") if info.NeedsClassic() { // running with classic confinement, carefully pick snap-exec we // are going to use if isReexeced() { // same rule as when choosing the location of snap-confine snapExecPath = filepath.Join(dirs.SnapMountDir, "core/current", dirs.CoreLibExecDir, "snap-exec") } else { // there is no mount namespace where 'core' is the // rootfs, hence we need to use distro's snap-exec snapExecPath = filepath.Join(dirs.DistroLibExecDir, "snap-exec") } } cmd = append(cmd, snapExecPath) if x.Shell { cmd = append(cmd, "--command=shell") } if x.Gdb { cmd = append(cmd, "--command=gdb") } if x.Command != "" { cmd = append(cmd, "--command="+x.Command) } if hook != "" { cmd = append(cmd, "--hook="+hook) } // snap-exec is POSIXly-- options must come before positionals. cmd = append(cmd, snapApp) cmd = append(cmd, args...) extraEnv := make(map[string]string) if len(xauthPath) > 0 { extraEnv["XAUTHORITY"] = xauthPath } env := snapenv.ExecEnv(info, extraEnv) if x.TraceExec { return x.runCmdWithTraceExec(cmd, env) } else if x.Gdb { return x.runCmdUnderGdb(cmd, env) } else if x.useStrace() { return x.runCmdUnderStrace(cmd, env) } else { return syscallExec(cmd[0], cmd, env) } } snapd-2.37.4~14.04.1/cmd/snap/cmd_auto_import.go0000664000000000000000000001700613435556260015765 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2016 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 . * */ package main import ( "bufio" "crypto" "encoding/base64" "fmt" "io/ioutil" "os" "os/exec" "path/filepath" "strings" "syscall" "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/release" ) const autoImportsName = "auto-import.assert" var mountInfoPath = "/proc/self/mountinfo" func autoImportCandidates() ([]string, error) { var cands []string // see https://www.kernel.org/doc/Documentation/filesystems/proc.txt, // sec. 3.5 f, err := os.Open(mountInfoPath) if err != nil { return nil, err } defer f.Close() scanner := bufio.NewScanner(f) for scanner.Scan() { l := strings.Fields(scanner.Text()) // Per proc.txt:3.5, /proc//mountinfo looks like // // 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue // (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11) // // and (7) has zero or more elements, find the "-" separator. i := 6 for i < len(l) && l[i] != "-" { i++ } if i+2 >= len(l) { continue } mountSrc := l[i+2] // skip everything that is not a device (cgroups, debugfs etc) if !strings.HasPrefix(mountSrc, "/dev/") { continue } // skip all loop devices (snaps) if strings.HasPrefix(mountSrc, "/dev/loop") { continue } // skip all ram disks (unless in tests) if !osutil.GetenvBool("SNAPPY_TESTING") && strings.HasPrefix(mountSrc, "/dev/ram") { continue } mountPoint := l[4] cand := filepath.Join(mountPoint, autoImportsName) if osutil.FileExists(cand) { cands = append(cands, cand) } } return cands, scanner.Err() } func queueFile(src string) error { // refuse huge files, this is for assertions fi, err := os.Stat(src) if err != nil { return err } // 640kb ought be to enough for anyone if fi.Size() > 640*1024 { msg := fmt.Errorf("cannot queue %s, file size too big: %v", src, fi.Size()) logger.Noticef("error: %v", msg) return msg } // ensure name is predictable, weak hash is ok hash, _, err := osutil.FileDigest(src, crypto.SHA3_384) if err != nil { return err } dst := filepath.Join(dirs.SnapAssertsSpoolDir, fmt.Sprintf("%s.assert", base64.URLEncoding.EncodeToString(hash))) if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { return err } return osutil.CopyFile(src, dst, osutil.CopyFlagOverwrite) } func autoImportFromSpool(cli *client.Client) (added int, err error) { files, err := ioutil.ReadDir(dirs.SnapAssertsSpoolDir) if os.IsNotExist(err) { return 0, nil } if err != nil { return 0, err } for _, fi := range files { cand := filepath.Join(dirs.SnapAssertsSpoolDir, fi.Name()) if err := ackFile(cli, cand); err != nil { logger.Noticef("error: cannot import %s: %s", cand, err) continue } else { logger.Noticef("imported %s", cand) added++ } // FIXME: only remove stuff older than N days? if err := os.Remove(cand); err != nil { return 0, err } } return added, nil } func autoImportFromAllMounts(cli *client.Client) (int, error) { cands, err := autoImportCandidates() if err != nil { return 0, err } added := 0 for _, cand := range cands { err := ackFile(cli, cand) // the server is not ready yet if _, ok := err.(client.ConnectionError); ok { logger.Noticef("queuing for later %s", cand) if err := queueFile(cand); err != nil { return 0, err } continue } if err != nil { logger.Noticef("error: cannot import %s: %s", cand, err) continue } else { logger.Noticef("imported %s", cand) } added++ } return added, nil } func tryMount(deviceName string) (string, error) { tmpMountTarget, err := ioutil.TempDir("", "snapd-auto-import-mount-") if err != nil { err = fmt.Errorf("cannot create temporary mount point: %v", err) logger.Noticef("error: %v", err) return "", err } // udev does not provide much environment ;) if os.Getenv("PATH") == "" { os.Setenv("PATH", "/usr/sbin:/usr/bin:/sbin:/bin") } // not using syscall.Mount() because we don't know the fs type in advance cmd := exec.Command("mount", "-t", "ext4,vfat", "-o", "ro", "--make-private", deviceName, tmpMountTarget) if output, err := cmd.CombinedOutput(); err != nil { os.Remove(tmpMountTarget) err = fmt.Errorf("cannot mount %s: %s", deviceName, osutil.OutputErr(output, err)) logger.Noticef("error: %v", err) return "", err } return tmpMountTarget, nil } func doUmount(mp string) error { if err := syscall.Unmount(mp, 0); err != nil { return err } return os.Remove(mp) } type cmdAutoImport struct { clientMixin Mount []string `long:"mount" arg-name:""` ForceClassic bool `long:"force-classic"` } var shortAutoImportHelp = i18n.G("Inspect devices for actionable information") var longAutoImportHelp = i18n.G(` The auto-import command searches available mounted devices looking for assertions that are signed by trusted authorities, and potentially performs system changes based on them. If one or more device paths are provided via --mount, these are temporarily mounted to be inspected as well. Even in that case the command will still consider all available mounted devices for inspection. Assertions to be imported must be made available in the auto-import.assert file in the root of the filesystem. `) func init() { cmd := addCommand("auto-import", shortAutoImportHelp, longAutoImportHelp, func() flags.Commander { return &cmdAutoImport{} }, map[string]string{ // TRANSLATORS: This should not start with a lowercase letter. "mount": i18n.G("Temporarily mount device before inspecting"), // TRANSLATORS: This should not start with a lowercase letter. "force-classic": i18n.G("Force import on classic systems"), }, nil) cmd.hidden = true } func (x *cmdAutoImport) autoAddUsers() error { cmd := cmdCreateUser{ clientMixin: x.clientMixin, Known: true, Sudoer: true, } return cmd.Execute(nil) } func (x *cmdAutoImport) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } if release.OnClassic && !x.ForceClassic { fmt.Fprintf(Stderr, "auto-import is disabled on classic\n") return nil } for _, path := range x.Mount { // udev adds new /dev/loopX devices on the fly when a // loop mount happens and there is no loop device left. // // We need to ignore these events because otherwise both // our mount and the "mount -o loop" fight over the same // device and we get nasty errors if strings.HasPrefix(path, "/dev/loop") { continue } mp, err := tryMount(path) if err != nil { continue // Error was reported. Continue looking. } defer doUmount(mp) } added1, err := autoImportFromSpool(x.client) if err != nil { return err } added2, err := autoImportFromAllMounts(x.client) if err != nil { return err } if added1+added2 > 0 { return x.autoAddUsers() } return nil } snapd-2.37.4~14.04.1/cmd/snap/cmd_watch.go0000664000000000000000000000303113435556260014522 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main import ( "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/i18n" ) type cmdWatch struct{ changeIDMixin } var shortWatchHelp = i18n.G("Watch a change in progress") var longWatchHelp = i18n.G(` The watch command waits for the given change-id to finish and shows progress (if available). `) func init() { addCommand("watch", shortWatchHelp, longWatchHelp, func() flags.Commander { return &cmdWatch{} }, changeIDMixinOptDesc, changeIDMixinArgDesc) } func (x *cmdWatch) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } id, err := x.GetChangeID() if err != nil { if err == noChangeFoundOK { return nil } return err } // this is the only valid use of wait without a waitMixin (ie // without --no-wait), so we fake it here. wmx := &waitMixin{skipAbort: true} wmx.client = x.client _, err = wmx.wait(id) return err } snapd-2.37.4~14.04.1/cmd/snap/cmd_repair_repairs.go0000664000000000000000000000463113435556260016432 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main import ( "fmt" "os" "os/exec" "path/filepath" "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/release" ) func runSnapRepair(cmdStr string, args []string) error { // do not even try to run snap-repair on classic, some distros // may not even package it if release.OnClassic { return fmt.Errorf(i18n.G("repairs are not available on a classic system")) } snapRepairPath := filepath.Join(dirs.GlobalRootDir, dirs.CoreLibExecDir, "snap-repair") args = append([]string{cmdStr}, args...) cmd := exec.Command(snapRepairPath, args...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() } type cmdShowRepair struct { Positional struct { Repair []string `positional-arg-name:""` } `positional-args:"yes"` } var shortRepairHelp = i18n.G("Show specific repairs") var longRepairHelp = i18n.G(` The repair command shows the details about one or multiple repairs. `) func init() { cmd := addCommand("repair", shortRepairHelp, longRepairHelp, func() flags.Commander { return &cmdShowRepair{} }, nil, nil) if release.OnClassic { cmd.hidden = true } } func (x *cmdShowRepair) Execute(args []string) error { return runSnapRepair("show", x.Positional.Repair) } type cmdListRepairs struct{} var shortRepairsHelp = i18n.G("Lists all repairs") var longRepairsHelp = i18n.G(` The repairs command lists all processed repairs for this device. `) func init() { cmd := addCommand("repairs", shortRepairsHelp, longRepairsHelp, func() flags.Commander { return &cmdListRepairs{} }, nil, nil) if release.OnClassic { cmd.hidden = true } } func (x *cmdListRepairs) Execute(args []string) error { return runSnapRepair("list", args) } snapd-2.37.4~14.04.1/cmd/snap/cmd_get_test.go0000664000000000000000000001461513435556260015244 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main_test import ( "fmt" "net/http" "strings" . "gopkg.in/check.v1" snapset "github.com/snapcore/snapd/cmd/snap" ) type getCmdArgs struct { args, stdout, stderr, error string isTerminal bool } var getTests = []getCmdArgs{{ args: "get snap-name --foo", error: ".*unknown flag.*foo.*", }, { args: "get snapname test-key1", stdout: "test-value1\n", }, { args: "get snapname test-key2", stdout: "2\n", }, { args: "get snapname missing-key", stdout: "\n", }, { args: "get -t snapname test-key1", stdout: "\"test-value1\"\n", }, { args: "get -t snapname test-key2", stdout: "2\n", }, { args: "get -t snapname missing-key", stdout: "null\n", }, { args: "get -d snapname test-key1", stdout: "{\n\t\"test-key1\": \"test-value1\"\n}\n", }, { args: "get -l snapname test-key1", stdout: "Key Value\ntest-key1 test-value1\n", }, { args: "get snapname -l test-key1 test-key2", stdout: "Key Value\ntest-key1 test-value1\ntest-key2 2\n", }, { args: "get snapname document", stderr: `WARNING: The output of 'snap get' will become a list with columns - use -d or -l to force the output format.\n`, stdout: "{\n\t\"document\": {\n\t\t\"key1\": \"value1\",\n\t\t\"key2\": \"value2\"\n\t}\n}\n", }, { isTerminal: true, args: "get snapname document", stdout: "Key Value\ndocument.key1 value1\ndocument.key2 value2\n", }, { args: "get snapname -d test-key1 test-key2", stdout: "{\n\t\"test-key1\": \"test-value1\",\n\t\"test-key2\": 2\n}\n", }, { args: "get snapname -l document", stdout: "Key Value\ndocument.key1 value1\ndocument.key2 value2\n", }, { args: "get -d snapname document", stdout: "{\n\t\"document\": {\n\t\t\"key1\": \"value1\",\n\t\t\"key2\": \"value2\"\n\t}\n}\n", }, { args: "get -l snapname", stdout: "Key Value\nbar 100\nfoo {...}\n", }, { args: "get snapname -l test-key3 test-key4", stdout: "Key Value\ntest-key3.a 1\ntest-key3.b 2\ntest-key3-a 9\ntest-key4.a 3\ntest-key4.b 4\n", }, { args: "get -d snapname", stdout: "{\n\t\"bar\": 100,\n\t\"foo\": {\n\t\t\"key1\": \"value1\",\n\t\t\"key2\": \"value2\"\n\t}\n}\n", }, { isTerminal: true, args: "get snapname test-key1 test-key2", stdout: "Key Value\ntest-key1 test-value1\ntest-key2 2\n", }, { isTerminal: false, args: "get snapname test-key1 test-key2", stdout: "{\n\t\"test-key1\": \"test-value1\",\n\t\"test-key2\": 2\n}\n", stderr: `WARNING: The output of 'snap get' will become a list with columns - use -d or -l to force the output format.\n`, }, } func (s *SnapSuite) runTests(cmds []getCmdArgs, c *C) { for _, test := range cmds { s.stdout.Truncate(0) s.stderr.Truncate(0) c.Logf("Test: %s", test.args) restore := snapset.MockIsStdinTTY(test.isTerminal) defer restore() _, err := snapset.Parser(snapset.Client()).ParseArgs(strings.Fields(test.args)) if test.error != "" { c.Check(err, ErrorMatches, test.error) } else { c.Check(err, IsNil) c.Check(s.Stderr(), Equals, test.stderr) c.Check(s.Stdout(), Equals, test.stdout) } } } func (s *SnapSuite) TestSnapGetTests(c *C) { s.mockGetConfigServer(c) s.runTests(getTests, c) } var getNoConfigTests = []getCmdArgs{{ args: "get -l snapname", error: `snap "snapname" has no configuration`, }, { args: "get snapname", error: `snap "snapname" has no configuration`, }, { args: "get -d snapname", stdout: "{}\n", }} func (s *SnapSuite) TestSnapGetNoConfiguration(c *C) { s.mockGetEmptyConfigServer(c) s.runTests(getNoConfigTests, c) } func (s *SnapSuite) TestSortByPath(c *C) { values := []snapset.ConfigValue{ {Path: "test-key3.b"}, {Path: "a"}, {Path: "test-key3.a"}, {Path: "a.b.c"}, {Path: "test-key4.a"}, {Path: "test-key4.b"}, {Path: "a-b"}, {Path: "zzz"}, {Path: "aa"}, {Path: "test-key3-a"}, {Path: "a.b"}, } snapset.SortByPath(values) expected := []string{ "a", "a.b", "a.b.c", "a-b", "aa", "test-key3.a", "test-key3.b", "test-key3-a", "test-key4.a", "test-key4.b", "zzz", } c.Assert(values, HasLen, len(expected)) for i, e := range expected { c.Assert(values[i].Path, Equals, e) } } func (s *SnapSuite) mockGetConfigServer(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v2/snaps/snapname/conf" { c.Errorf("unexpected path %q", r.URL.Path) return } c.Check(r.Method, Equals, "GET") query := r.URL.Query() switch query.Get("keys") { case "test-key1": fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {"test-key1":"test-value1"}}`) case "test-key2": fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {"test-key2":2}}`) case "test-key1,test-key2": fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {"test-key1":"test-value1","test-key2":2}}`) case "test-key3,test-key4": fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {"test-key3":{"a":1,"b":2},"test-key3-a":9,"test-key4":{"a":3,"b":4}}}`) case "missing-key": fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {}}`) case "document": fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {"document":{"key1":"value1","key2":"value2"}}}`) case "": fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {"foo":{"key1":"value1","key2":"value2"},"bar":100}}`) default: c.Errorf("unexpected keys %q", query.Get("keys")) } }) } func (s *SnapSuite) mockGetEmptyConfigServer(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v2/snaps/snapname/conf" { c.Errorf("unexpected path %q", r.URL.Path) return } c.Check(r.Method, Equals, "GET") fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {}}`) }) } snapd-2.37.4~14.04.1/cmd/snap/cmd_sign.go0000664000000000000000000000420413435556260014357 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2015 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 . * */ package main import ( "fmt" "io/ioutil" "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/asserts/signtool" "github.com/snapcore/snapd/i18n" ) var shortSignHelp = i18n.G("Sign an assertion") var longSignHelp = i18n.G(` The sign command signs an assertion using the specified key, using the input for headers from a JSON mapping provided through stdin. The body of the assertion can be specified through a "body" pseudo-header. `) type cmdSign struct { KeyName keyName `short:"k" default:"default"` } func init() { cmd := addCommand("sign", shortSignHelp, longSignHelp, func() flags.Commander { return &cmdSign{} }, map[string]string{ // TRANSLATORS: This should not start with a lowercase letter. "k": i18n.G("Name of the key to use, otherwise use the default key"), }, nil) cmd.hidden = true } func (x *cmdSign) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } statement, err := ioutil.ReadAll(Stdin) if err != nil { return fmt.Errorf(i18n.G("cannot read assertion input: %v"), err) } keypairMgr := asserts.NewGPGKeypairManager() privKey, err := keypairMgr.GetByName(string(x.KeyName)) if err != nil { return err } signOpts := signtool.Options{ KeyID: privKey.PublicKey().ID(), Statement: statement, } encodedAssert, err := signtool.Sign(&signOpts, keypairMgr) if err != nil { return err } _, err = Stdout.Write(encodedAssert) if err != nil { return err } return nil } snapd-2.37.4~14.04.1/cmd/snap/cmd_delete_key_test.go0000664000000000000000000000406713435556260016577 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main_test import ( "encoding/json" . "gopkg.in/check.v1" snap "github.com/snapcore/snapd/cmd/snap" ) func (s *SnapKeysSuite) TestDeleteKeyRequiresName(c *C) { _, err := snap.Parser(snap.Client()).ParseArgs([]string{"delete-key"}) c.Assert(err, NotNil) c.Check(err.Error(), Equals, "the required argument `` was not provided") c.Check(s.Stdout(), Equals, "") c.Check(s.Stderr(), Equals, "") } func (s *SnapKeysSuite) TestDeleteKeyNonexistent(c *C) { _, err := snap.Parser(snap.Client()).ParseArgs([]string{"delete-key", "nonexistent"}) c.Assert(err, NotNil) c.Check(err.Error(), Equals, "cannot find key named \"nonexistent\" in GPG keyring") c.Check(s.Stdout(), Equals, "") c.Check(s.Stderr(), Equals, "") } func (s *SnapKeysSuite) TestDeleteKey(c *C) { rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"delete-key", "another"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Check(s.Stdout(), Equals, "") c.Check(s.Stderr(), Equals, "") _, err = snap.Parser(snap.Client()).ParseArgs([]string{"keys", "--json"}) c.Assert(err, IsNil) expectedResponse := []snap.Key{ { Name: "default", Sha3_384: "g4Pks54W_US4pZuxhgG_RHNAf_UeZBBuZyGRLLmMj1Do3GkE_r_5A5BFjx24ZwVJ", }, } var obtainedResponse []snap.Key json.Unmarshal(s.stdout.Bytes(), &obtainedResponse) c.Check(obtainedResponse, DeepEquals, expectedResponse) c.Check(s.Stderr(), Equals, "") } snapd-2.37.4~14.04.1/cmd/snap/cmd_download.go0000664000000000000000000000750713435556260015237 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main import ( "fmt" "os" "path/filepath" "strings" "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/asserts/sysdb" "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/image" "github.com/snapcore/snapd/snap" ) type cmdDownload struct { channelMixin Revision string `long:"revision"` Positional struct { Snap remoteSnapName } `positional-args:"true" required:"true"` } var shortDownloadHelp = i18n.G("Download the given snap") var longDownloadHelp = i18n.G(` The download command downloads the given snap and its supporting assertions to the current directory with .snap and .assert file extensions, respectively. `) func init() { addCommand("download", shortDownloadHelp, longDownloadHelp, func() flags.Commander { return &cmdDownload{} }, channelDescs.also(map[string]string{ "revision": i18n.G("Download the given revision of a snap, to which you must have developer access"), }), []argDesc{{ name: "", // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("Snap name"), }}) } func fetchSnapAssertions(tsto *image.ToolingStore, snapPath string, snapInfo *snap.Info) (string, error) { db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ Backstore: asserts.NewMemoryBackstore(), Trusted: sysdb.Trusted(), }) if err != nil { return "", err } assertPath := strings.TrimSuffix(snapPath, filepath.Ext(snapPath)) + ".assert" w, err := os.Create(assertPath) if err != nil { return "", fmt.Errorf(i18n.G("cannot create assertions file: %v"), err) } defer w.Close() encoder := asserts.NewEncoder(w) save := func(a asserts.Assertion) error { return encoder.Encode(a) } f := tsto.AssertionFetcher(db, save) _, err = image.FetchAndCheckSnapAssertions(snapPath, snapInfo, f, db) return assertPath, err } func (x *cmdDownload) Execute(args []string) error { if err := x.setChannelFromCommandline(); err != nil { return err } if len(args) > 0 { return ErrExtraArgs } var revision snap.Revision if x.Revision == "" { revision = snap.R(0) } else { if x.Channel != "" { return fmt.Errorf(i18n.G("cannot specify both channel and revision")) } var err error revision, err = snap.ParseRevision(x.Revision) if err != nil { return err } } snapName := string(x.Positional.Snap) tsto, err := image.NewToolingStore() if err != nil { return err } fmt.Fprintf(Stdout, i18n.G("Fetching snap %q\n"), snapName) dlOpts := image.DownloadOptions{ TargetDir: "", // cwd Channel: x.Channel, } snapPath, snapInfo, err := tsto.DownloadSnap(snapName, revision, &dlOpts) if err != nil { return err } fmt.Fprintf(Stdout, i18n.G("Fetching assertions for %q\n"), snapName) assertPath, err := fetchSnapAssertions(tsto, snapPath, snapInfo) if err != nil { return err } // simplify paths wd, _ := os.Getwd() if p, err := filepath.Rel(wd, assertPath); err == nil { assertPath = p } if p, err := filepath.Rel(wd, snapPath); err == nil { snapPath = p } // add a hint what to do with the downloaded snap (LP:1676707) fmt.Fprintf(Stdout, i18n.G(`Install the snap with: snap ack %s snap install %s `), assertPath, snapPath) return nil } snapd-2.37.4~14.04.1/cmd/snap/cmd_userd_test.go0000664000000000000000000000465113435556260015606 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main_test import ( "os" "strings" "syscall" "time" . "gopkg.in/check.v1" snap "github.com/snapcore/snapd/cmd/snap" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/testutil" ) type userdSuite struct { BaseSnapSuite testutil.DBusTest restoreLogger func() } var _ = Suite(&userdSuite{}) func (s *userdSuite) SetUpTest(c *C) { s.BaseSnapSuite.SetUpTest(c) s.DBusTest.SetUpTest(c) _, s.restoreLogger = logger.MockLogger() } func (s *userdSuite) TearDownTest(c *C) { s.BaseSnapSuite.TearDownTest(c) s.DBusTest.TearDownTest(c) s.restoreLogger() } func (s *userdSuite) TestUserdBadCommandline(c *C) { _, err := snap.Parser(snap.Client()).ParseArgs([]string{"userd", "extra-arg"}) c.Assert(err, ErrorMatches, "too many arguments for command") } func (s *userdSuite) TestUserdDBus(c *C) { go func() { myPid := os.Getpid() defer func() { me, err := os.FindProcess(myPid) c.Assert(err, IsNil) me.Signal(syscall.SIGUSR1) }() names := map[string]bool{ "io.snapcraft.Launcher": false, "io.snapcraft.Settings": false, } for i := 0; i < 1000; i++ { seenCount := 0 for name, seen := range names { if seen { seenCount++ continue } pid, err := testutil.DBusGetConnectionUnixProcessID(s.SessionBus, name) c.Logf("name: %v pid: %v err: %v", name, pid, err) if pid == myPid { names[name] = true seenCount++ } } if seenCount == len(names) { return } time.Sleep(10 * time.Millisecond) } c.Fatalf("not all names have appeared on the bus: %v", names) }() rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"userd"}) c.Assert(err, IsNil) c.Check(rest, DeepEquals, []string{}) c.Check(strings.ToLower(s.Stdout()), Equals, "exiting on user defined signal 1.\n") } snapd-2.37.4~14.04.1/cmd/snap/cmd_blame.go0000664000000000000000000000225513435556260014503 0ustar // -*- 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 . * */ package main //go:generate mkauthors.sh import ( "fmt" "math/rand" "github.com/jessevdk/go-flags" ) type cmdBlame struct{} var authors []string func init() { cmd := addCommand("blame", "", "", func() flags.Commander { return &cmdBlame{} }, nil, nil) cmd.hidden = true } func (x *cmdBlame) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } if len(authors) == 0 { return nil } fmt.Fprintf(Stdout, "It's all %s's fault.\n", authors[rand.Intn(len(authors))]) return nil } snapd-2.37.4~14.04.1/cmd/snap/cmd_create_key_test.go0000664000000000000000000000213413435556260016571 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main_test import ( . "gopkg.in/check.v1" snap "github.com/snapcore/snapd/cmd/snap" ) func (s *SnapSuite) TestCreateKeyInvalidCharacters(c *C) { _, err := snap.Parser(snap.Client()).ParseArgs([]string{"create-key", "a b"}) c.Assert(err, NotNil) c.Check(err.Error(), Equals, "key name \"a b\" is not valid; only ASCII letters, digits, and hyphens are allowed") c.Check(s.Stdout(), Equals, "") c.Check(s.Stderr(), Equals, "") } snapd-2.37.4~14.04.1/cmd/snap/cmd_find.go0000664000000000000000000001670013435556260014343 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016-2018 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main import ( "bufio" "errors" "fmt" "os" "sort" "strings" "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/strutil" ) var shortFindHelp = i18n.G("Find packages to install") var longFindHelp = i18n.G(` The find command queries the store for available packages in the stable channel. With the --private flag, which requires the user to be logged-in to the store (see 'snap help login'), it instead searches for private snaps that the user has developer access to, either directly or through the store's collaboration feature. A green check mark (given color and unicode support) after a publisher name indicates that the publisher has been verified. `) func getPrice(prices map[string]float64, currency string) (float64, string, error) { // If there are no prices, then the snap is free if len(prices) == 0 { // TRANSLATORS: free as in gratis return 0, "", errors.New(i18n.G("snap is free")) } // Look up the price by currency code val, ok := prices[currency] // Fall back to dollars if !ok { currency = "USD" val, ok = prices["USD"] } // If there aren't even dollars, grab the first currency, // ordered alphabetically by currency code if !ok { currency = "ZZZ" for c, v := range prices { if c < currency { currency, val = c, v } } } return val, currency, nil } type SectionName string func (s SectionName) Complete(match string) []flags.Completion { if ret, err := completeFromSortedFile(dirs.SnapSectionsFile, match); err == nil { return ret } cli := mkClient() sections, err := cli.Sections() if err != nil { return nil } ret := make([]flags.Completion, 0, len(sections)) for _, s := range sections { if strings.HasPrefix(s, match) { ret = append(ret, flags.Completion{Item: s}) } } return ret } func cachedSections() (sections []string, err error) { cachedSections, err := os.Open(dirs.SnapSectionsFile) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } defer cachedSections.Close() r := bufio.NewScanner(cachedSections) for r.Scan() { sections = append(sections, r.Text()) } if r.Err() != nil { return nil, r.Err() } return sections, nil } func getSections(cli *client.Client) (sections []string, err error) { // try loading from cached sections file sections, err = cachedSections() if err != nil { return nil, err } if sections != nil { return sections, nil } // fallback to listing from the daemon return cli.Sections() } func showSections(cli *client.Client) error { sections, err := getSections(cli) if err != nil { return err } sort.Strings(sections) fmt.Fprintf(Stdout, i18n.G("No section specified. Available sections:\n")) for _, sec := range sections { fmt.Fprintf(Stdout, " * %s\n", sec) } fmt.Fprintf(Stdout, i18n.G("Please try 'snap find --section='\n")) return nil } type cmdFind struct { clientMixin Private bool `long:"private"` Narrow bool `long:"narrow"` Section SectionName `long:"section" optional:"true" optional-value:"show-all-sections-please" default:"no-section-specified"` Positional struct { Query string } `positional-args:"yes"` colorMixin } func init() { addCommand("find", shortFindHelp, longFindHelp, func() flags.Commander { return &cmdFind{} }, colorDescs.also(map[string]string{ // TRANSLATORS: This should not start with a lowercase letter. "private": i18n.G("Search private snaps"), // TRANSLATORS: This should not start with a lowercase letter. "narrow": i18n.G("Only search for snaps in “stable”"), // TRANSLATORS: This should not start with a lowercase letter. "section": i18n.G("Restrict the search to a given section"), }), []argDesc{{ // TRANSLATORS: This needs to begin with < and end with > name: i18n.G(""), }}).alias = "search" } func (x *cmdFind) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } // LP: 1740605 if strings.TrimSpace(x.Positional.Query) == "" { x.Positional.Query = "" } // section will be: // - "show-all-sections-please" if the user specified --section // without any argument // - "no-section-specified" if "--section" was not specified on // the commandline at all switch x.Section { case "show-all-sections-please": return showSections(x.client) case "no-section-specified": x.Section = "" } // magic! `snap find` returns the featured snaps showFeatured := (x.Positional.Query == "" && x.Section == "") if showFeatured { x.Section = "featured" } if x.Section != "" && x.Section != "featured" { sections, err := cachedSections() if err != nil { return err } if !strutil.ListContains(sections, string(x.Section)) { // try the store just in case it was added in the last 24 hours sections, err = x.client.Sections() if err != nil { return err } if !strutil.ListContains(sections, string(x.Section)) { // TRANSLATORS: the %q is the (quoted) name of the section the user entered return fmt.Errorf(i18n.G("No matching section %q, use --section to list existing sections"), x.Section) } } } opts := &client.FindOptions{ Private: x.Private, Section: string(x.Section), Query: x.Positional.Query, } if !x.Narrow { opts.Scope = "wide" } snaps, resInfo, err := x.client.Find(opts) if e, ok := err.(*client.Error); ok && (e.Kind == client.ErrorKindNetworkTimeout || e.Kind == client.ErrorKindDNSFailure) { logger.Debugf("cannot list snaps: %v", e) return fmt.Errorf("unable to contact snap store") } if err != nil { return err } if len(snaps) == 0 { if x.Section == "" { // TRANSLATORS: the %q is the (quoted) query the user entered fmt.Fprintf(Stderr, i18n.G("No matching snaps for %q\n"), opts.Query) } else { // TRANSLATORS: the first %q is the (quoted) query, the // second %q is the (quoted) name of the section the // user entered fmt.Fprintf(Stderr, i18n.G("No matching snaps for %q in section %q\n"), opts.Query, x.Section) } return nil } // show featured header *after* we checked for errors from the find if showFeatured { fmt.Fprint(Stdout, i18n.G("No search term specified. Here are some interesting snaps:\n\n")) } esc := x.getEscapes() w := tabWriter() // TRANSLATORS: the %s is to insert a filler escape sequence (please keep it flush to the column header, with no extra spaces) fmt.Fprintf(w, i18n.G("Name\tVersion\tPublisher%s\tNotes\tSummary\n"), fillerPublisher(esc)) for _, snap := range snaps { fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", snap.Name, snap.Version, shortPublisher(esc, snap.Publisher), NotesFromRemote(snap, resInfo), snap.Summary) } w.Flush() if showFeatured { fmt.Fprint(Stdout, i18n.G("\nProvide a search term for more specific results.\n")) } return nil } snapd-2.37.4~14.04.1/cmd/snap/cmd_prepare_image.go0000664000000000000000000000456313435556260016227 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2016 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 . * */ package main import ( "path/filepath" "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/image" ) type cmdPrepareImage struct { Positional struct { ModelAssertionFn string Rootdir string } `positional-args:"yes" required:"yes"` ExtraSnaps []string `long:"extra-snaps"` Channel string `long:"channel" default:"stable"` } func init() { cmd := addCommand("prepare-image", i18n.G("Prepare a core device image"), i18n.G(` The prepare-image command performs some of the steps necessary for creating core device images. `), func() flags.Commander { return &cmdPrepareImage{} }, map[string]string{ // TRANSLATORS: This should not start with a lowercase letter. "extra-snaps": i18n.G("Extra snaps to be installed"), // TRANSLATORS: This should not start with a lowercase letter. "channel": i18n.G("The channel to use"), }, []argDesc{ { // TRANSLATORS: This needs to begin with < and end with > name: i18n.G(""), // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("The model assertion name"), }, { // TRANSLATORS: This needs to begin with < and end with > name: i18n.G(""), // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("The output directory"), }, }) cmd.hidden = true } func (x *cmdPrepareImage) Execute(args []string) error { opts := &image.Options{ ModelFile: x.Positional.ModelAssertionFn, RootDir: filepath.Join(x.Positional.Rootdir, "image"), GadgetUnpackDir: filepath.Join(x.Positional.Rootdir, "gadget"), Channel: x.Channel, Snaps: x.ExtraSnaps, } return image.Prepare(opts) } snapd-2.37.4~14.04.1/cmd/snap/notes_test.go0000664000000000000000000000551713435556260014773 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main_test import ( "gopkg.in/check.v1" "github.com/snapcore/snapd/client" snap "github.com/snapcore/snapd/cmd/snap" ) type notesSuite struct{} var _ = check.Suite(¬esSuite{}) func (notesSuite) TestNoNotes(c *check.C) { c.Check((&snap.Notes{}).String(), check.Equals, "-") } func (notesSuite) TestNotesPrice(c *check.C) { c.Check((&snap.Notes{ Price: "3.50GBP", }).String(), check.Equals, "3.50GBP") } func (notesSuite) TestNotesPrivate(c *check.C) { c.Check((&snap.Notes{ Private: true, }).String(), check.Equals, "private") } func (notesSuite) TestNotesDevMode(c *check.C) { c.Check((&snap.Notes{ DevMode: true, }).String(), check.Equals, "devmode") } func (notesSuite) TestNotesJailMode(c *check.C) { c.Check((&snap.Notes{ JailMode: true, }).String(), check.Equals, "jailmode") } func (notesSuite) TestNotesClassic(c *check.C) { c.Check((&snap.Notes{ Classic: true, }).String(), check.Equals, "classic") } func (notesSuite) TestNotesTryMode(c *check.C) { c.Check((&snap.Notes{ TryMode: true, }).String(), check.Equals, "try") } func (notesSuite) TestNotesDisabled(c *check.C) { c.Check((&snap.Notes{ Disabled: true, }).String(), check.Equals, "disabled") } func (notesSuite) TestNotesBroken(c *check.C) { c.Check((&snap.Notes{ Broken: true, }).String(), check.Equals, "broken") } func (notesSuite) TestNotesIgnoreValidation(c *check.C) { c.Check((&snap.Notes{ IgnoreValidation: true, }).String(), check.Equals, "ignore-validation") } func (notesSuite) TestNotesNothing(c *check.C) { c.Check((&snap.Notes{}).String(), check.Equals, "-") } func (notesSuite) TestNotesTwo(c *check.C) { c.Check((&snap.Notes{ DevMode: true, Broken: true, }).String(), check.Matches, "(devmode,broken|broken,devmode)") } func (notesSuite) TestNotesFromLocal(c *check.C) { // Check that DevMode note is derived from DevMode flag, not DevModeConfinement type. c.Check(snap.NotesFromLocal(&client.Snap{DevMode: true}).DevMode, check.Equals, true) c.Check(snap.NotesFromLocal(&client.Snap{Confinement: client.DevModeConfinement}).DevMode, check.Equals, false) c.Check(snap.NotesFromLocal(&client.Snap{IgnoreValidation: true}).IgnoreValidation, check.Equals, true) } snapd-2.37.4~14.04.1/cmd/snap/cmd_buy.go0000664000000000000000000000775213435556260014231 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main import ( "fmt" "strings" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/i18n" "github.com/jessevdk/go-flags" ) var shortBuyHelp = i18n.G("Buy a snap") var longBuyHelp = i18n.G(` The buy command buys a snap from the store. `) type cmdBuy struct { clientMixin Positional struct { SnapName remoteSnapName } `positional-args:"yes" required:"yes"` } func init() { cmd := addCommand("buy", shortBuyHelp, longBuyHelp, func() flags.Commander { return &cmdBuy{} }, map[string]string{}, []argDesc{{ name: "", // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("Snap name"), }}) cmd.hidden = true } func (x *cmdBuy) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } return buySnap(x.client, string(x.Positional.SnapName)) } func buySnap(cli *client.Client, snapName string) error { user := cli.LoggedInUser() if user == nil { return fmt.Errorf(i18n.G("You need to be logged in to purchase software. Please run 'snap login' and try again.")) } if strings.ContainsAny(snapName, ":*") { return fmt.Errorf(i18n.G("cannot buy snap: invalid characters in name")) } snap, resultInfo, err := cli.FindOne(snapName) if err != nil { return err } opts := &client.BuyOptions{ SnapID: snap.ID, Currency: resultInfo.SuggestedCurrency, } opts.Price, opts.Currency, err = getPrice(snap.Prices, opts.Currency) if err != nil { return fmt.Errorf(i18n.G("cannot buy snap: %v"), err) } if snap.Status == "available" { return fmt.Errorf(i18n.G("cannot buy snap: it has already been bought")) } err = cli.ReadyToBuy() if err != nil { if e, ok := err.(*client.Error); ok { switch e.Kind { case client.ErrorKindNoPaymentMethods: return fmt.Errorf(i18n.G(`You need to have a payment method associated with your account in order to buy a snap, please visit https://my.ubuntu.com/payment/edit to add one. Once you’ve added your payment details, you just need to run 'snap buy %s' again.`), snap.Name) case client.ErrorKindTermsNotAccepted: return fmt.Errorf(i18n.G(`In order to buy %q, you need to agree to the latest terms and conditions. Please visit https://my.ubuntu.com/payment/edit to do this. Once completed, return here and run 'snap buy %s' again.`), snap.Name, snap.Name) } } return err } // TRANSLATORS: %q, %q and %s are the snap name, developer, and price. Please wrap the translation at 80 characters. fmt.Fprintf(Stdout, i18n.G(`Please re-enter your Ubuntu One password to purchase %q from %q for %s. Press ctrl-c to cancel.`), snap.Name, snap.Publisher.Username, formatPrice(opts.Price, opts.Currency)) fmt.Fprint(Stdout, "\n") err = requestLogin(cli, user.Email) if err != nil { return err } _, err = cli.Buy(opts) if err != nil { if e, ok := err.(*client.Error); ok { switch e.Kind { case client.ErrorKindPaymentDeclined: return fmt.Errorf(i18n.G(`Sorry, your payment method has been declined by the issuer. Please review your payment details at https://my.ubuntu.com/payment/edit and try again.`)) } } return err } // TRANSLATORS: %q and %s are the same snap name. Please wrap the translation at 80 characters. fmt.Fprintf(Stdout, i18n.G(`Thanks for purchasing %q. You may now install it on any of your devices with 'snap install %s'.`), snapName, snapName) fmt.Fprint(Stdout, "\n") return nil } snapd-2.37.4~14.04.1/cmd/snap/cmd_help.go0000664000000000000000000002022313435556260014346 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main import ( "bytes" "fmt" "strings" "unicode/utf8" "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/i18n" ) var shortHelpHelp = i18n.G("Show help about a command") var longHelpHelp = i18n.G(` The help command displays information about snap commands. `) // addHelp adds --help like what go-flags would do for us, but hidden func addHelp(parser *flags.Parser) error { var help struct { ShowHelp func() error `short:"h" long:"help"` } help.ShowHelp = func() error { // this function is called via --help (or -h). In that // case, parser.Command.Active should be the command // on which help is being requested (like "snap foo // --help", active is foo), or nil in the toplevel. if parser.Command.Active == nil { // toplevel --help will get handled via ErrCommandRequired return nil } // not toplevel, so ask for regular help return &flags.Error{Type: flags.ErrHelp} } hlpgrp, err := parser.AddGroup("Help Options", "", &help) if err != nil { return err } hlpgrp.Hidden = true hlp := parser.FindOptionByLongName("help") hlp.Description = i18n.G("Show this help message") hlp.Hidden = true return nil } type cmdHelp struct { All bool `long:"all"` Manpage bool `long:"man" hidden:"true"` Positional struct { // TODO: find a way to make Command tab-complete Sub string `positional-arg-name:""` } `positional-args:"yes"` parser *flags.Parser } func init() { addCommand("help", shortHelpHelp, longHelpHelp, func() flags.Commander { return &cmdHelp{} }, map[string]string{ // TRANSLATORS: This should not start with a lowercase letter. "all": i18n.G("Show a short summary of all commands"), // TRANSLATORS: This should not start with a lowercase letter. "man": i18n.G("Generate the manpage"), }, nil) } func (cmd *cmdHelp) setParser(parser *flags.Parser) { cmd.parser = parser } // manfixer is a hackish way to get the generated manpage into section 8 // (go-flags doesn't have an option for this; I'll be proposing something // there soon, but still waiting on some other PRs to make it through) type manfixer struct { done bool } func (w *manfixer) Write(buf []byte) (int, error) { if !w.done { w.done = true if bytes.HasPrefix(buf, []byte(".TH snap 1 ")) { // io.Writer.Write must not modify the buffer, even temporarily n, _ := Stdout.Write(buf[:9]) Stdout.Write([]byte{'8'}) m, err := Stdout.Write(buf[10:]) return n + m + 1, err } } return Stdout.Write(buf) } func (cmd cmdHelp) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } if cmd.Manpage { // you shouldn't try to to combine --man with --all nor a // subcommand, but --man is hidden so no real need to check. cmd.parser.WriteManPage(&manfixer{}) return nil } if cmd.All { if cmd.Positional.Sub != "" { return fmt.Errorf(i18n.G("help accepts a command, or '--all', but not both.")) } printLongHelp(cmd.parser) return nil } if cmd.Positional.Sub != "" { subcmd := cmd.parser.Find(cmd.Positional.Sub) if subcmd == nil { return fmt.Errorf(i18n.G("Unknown command %q. Try 'snap help'."), cmd.Positional.Sub) } // this makes "snap help foo" work the same as "snap foo --help" cmd.parser.Command.Active = subcmd return &flags.Error{Type: flags.ErrHelp} } return &flags.Error{Type: flags.ErrCommandRequired} } type helpCategory struct { Label string Description string Commands []string } // helpCategories helps us by grouping commands var helpCategories = []helpCategory{ { Label: i18n.G("Basics"), Description: i18n.G("basic snap management"), Commands: []string{"find", "info", "install", "list", "remove"}, }, { Label: i18n.G("...more"), Description: i18n.G("slightly more advanced snap management"), Commands: []string{"refresh", "revert", "switch", "disable", "enable"}, }, { Label: i18n.G("History"), Description: i18n.G("manage system change transactions"), Commands: []string{"changes", "tasks", "abort", "watch"}, }, { Label: i18n.G("Daemons"), Description: i18n.G("manage services"), Commands: []string{"services", "start", "stop", "restart", "logs"}, }, { Label: i18n.G("Commands"), Description: i18n.G("manage aliases"), Commands: []string{"alias", "aliases", "unalias", "prefer"}, }, { Label: i18n.G("Configuration"), Description: i18n.G("system administration and configuration"), Commands: []string{"get", "set", "wait"}, }, { Label: i18n.G("Account"), Description: i18n.G("authentication to snapd and the snap store"), Commands: []string{"login", "logout", "whoami"}, }, { Label: i18n.G("Permissions"), Description: i18n.G("manage permissions"), Commands: []string{"interfaces", "interface", "connect", "disconnect"}, }, { Label: i18n.G("Snapshots"), Description: i18n.G("archives of snap data"), Commands: []string{"saved", "save", "check-snapshot", "restore", "forget"}, }, { Label: i18n.G("Other"), Description: i18n.G("miscellanea"), Commands: []string{"version", "warnings", "okay"}, }, { Label: i18n.G("Development"), Description: i18n.G("developer-oriented features"), Commands: []string{"run", "pack", "try", "ack", "known", "download"}, }, } var ( longSnapDescription = strings.TrimSpace(i18n.G(` The snap command lets you install, configure, refresh and remove snaps. Snaps are packages that work across many different Linux distributions, enabling secure delivery and operation of the latest apps and utilities. `)) snapUsage = i18n.G("Usage: snap [...]") snapHelpCategoriesIntro = i18n.G("Commands can be classified as follows:") snapHelpAllFooter = i18n.G("For more information about a command, run 'snap help '.") snapHelpFooter = i18n.G("For a short summary of all commands, run 'snap help --all'.") ) func printHelpHeader() { fmt.Fprintln(Stdout, longSnapDescription) fmt.Fprintln(Stdout) fmt.Fprintln(Stdout, snapUsage) fmt.Fprintln(Stdout) fmt.Fprintln(Stdout, snapHelpCategoriesIntro) fmt.Fprintln(Stdout) } func printHelpAllFooter() { fmt.Fprintln(Stdout) fmt.Fprintln(Stdout, snapHelpAllFooter) } func printHelpFooter() { printHelpAllFooter() fmt.Fprintln(Stdout, snapHelpFooter) } // this is called when the Execute returns a flags.Error with ErrCommandRequired func printShortHelp() { printHelpHeader() maxLen := 0 for _, categ := range helpCategories { if l := utf8.RuneCountInString(categ.Label); l > maxLen { maxLen = l } } for _, categ := range helpCategories { fmt.Fprintf(Stdout, "%*s: %s\n", maxLen+2, categ.Label, strings.Join(categ.Commands, ", ")) } printHelpFooter() } // this is "snap help --all" func printLongHelp(parser *flags.Parser) { printHelpHeader() maxLen := 0 for _, categ := range helpCategories { for _, command := range categ.Commands { if l := len(command); l > maxLen { maxLen = l } } } // flags doesn't have a LookupCommand? commands := parser.Commands() cmdLookup := make(map[string]*flags.Command, len(commands)) for _, cmd := range commands { cmdLookup[cmd.Name] = cmd } for _, categ := range helpCategories { fmt.Fprintln(Stdout) fmt.Fprintf(Stdout, " %s (%s):\n", categ.Label, categ.Description) for _, name := range categ.Commands { cmd := cmdLookup[name] if cmd == nil { fmt.Fprintf(Stderr, "??? Cannot find command %q mentioned in help categories, please report!\n", name) } else { fmt.Fprintf(Stdout, " %*s %s\n", -maxLen, name, cmd.ShortDescription) } } } printHelpAllFooter() } snapd-2.37.4~14.04.1/cmd/snap/cmd_info.go0000664000000000000000000003240313435556260014354 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016-2018 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main import ( "fmt" "io" "path/filepath" "strings" "text/tabwriter" "time" "unicode" "unicode/utf8" "github.com/jessevdk/go-flags" "gopkg.in/yaml.v2" "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/strutil" ) type infoCmd struct { clientMixin colorMixin timeMixin Verbose bool `long:"verbose"` Positional struct { Snaps []anySnapName `positional-arg-name:"" required:"1"` } `positional-args:"yes" required:"yes"` } var shortInfoHelp = i18n.G("Show detailed information about snaps") var longInfoHelp = i18n.G(` The info command shows detailed information about snaps. The snaps can be specified by name or by path; names are looked for both in the store and in the installed snaps; paths can refer to a .snap file, or to a directory that contains an unpacked snap suitable for 'snap try' (an example of this would be the 'prime' directory snapcraft produces). `) func init() { addCommand("info", shortInfoHelp, longInfoHelp, func() flags.Commander { return &infoCmd{} }, colorDescs.also(timeDescs).also(map[string]string{ // TRANSLATORS: This should not start with a lowercase letter. "verbose": i18n.G("Include more details on the snap (expanded notes, base, etc.)"), }), nil) } func norm(path string) string { path = filepath.Clean(path) if osutil.IsDirectory(path) { path = path + "/" } return path } func maybePrintPrice(w io.Writer, snap *client.Snap, resInfo *client.ResultInfo) { if resInfo == nil { return } price, currency, err := getPrice(snap.Prices, resInfo.SuggestedCurrency) if err != nil { return } fmt.Fprintf(w, "price:\t%s\n", formatPrice(price, currency)) } func maybePrintType(w io.Writer, t string) { // XXX: using literals here until we reshuffle snap & client properly // (and os->core rename happens, etc) switch t { case "", "app", "application": return case "os": t = "core" } fmt.Fprintf(w, "type:\t%s\n", t) } func maybePrintID(w io.Writer, snap *client.Snap) { if snap.ID != "" { fmt.Fprintf(w, "snap-id:\t%s\n", snap.ID) } } func maybePrintBase(w io.Writer, base string, verbose bool) { if verbose && base != "" { fmt.Fprintf(w, "base:\t%s\n", base) } } func tryDirect(w io.Writer, path string, verbose bool) bool { path = norm(path) snapf, err := snap.Open(path) if err != nil { return false } var sha3_384 string if verbose && !osutil.IsDirectory(path) { var err error sha3_384, _, err = asserts.SnapFileSHA3_384(path) if err != nil { return false } } info, err := snap.ReadInfoFromSnapFile(snapf, nil) if err != nil { return false } fmt.Fprintf(w, "path:\t%q\n", path) fmt.Fprintf(w, "name:\t%s\n", info.InstanceName()) fmt.Fprintf(w, "summary:\t%s\n", formatSummary(info.Summary())) var notes *Notes if verbose { fmt.Fprintln(w, "notes:\t") fmt.Fprintf(w, " confinement:\t%s\n", info.Confinement) if info.Broken == "" { fmt.Fprintln(w, " broken:\tfalse") } else { fmt.Fprintf(w, " broken:\ttrue (%s)\n", info.Broken) } } else { notes = NotesFromInfo(info) } fmt.Fprintf(w, "version:\t%s %s\n", info.Version, notes) maybePrintType(w, string(info.Type)) maybePrintBase(w, info.Base, verbose) if sha3_384 != "" { fmt.Fprintf(w, "sha3-384:\t%s\n", sha3_384) } return true } func coalesce(snaps ...*client.Snap) *client.Snap { for _, s := range snaps { if s != nil { return s } } return nil } // runesTrimRightSpace returns text, with any trailing whitespace dropped. func runesTrimRightSpace(text []rune) []rune { j := len(text) for j > 0 && unicode.IsSpace(text[j-1]) { j-- } return text[:j] } // runesLastIndexSpace returns the index of the last whitespace rune // in the text. If the text has no whitespace, returns -1. func runesLastIndexSpace(text []rune) int { for i := len(text) - 1; i >= 0; i-- { if unicode.IsSpace(text[i]) { return i } } return -1 } // wrapLine wraps a line to fit into width, preserving the line's indent, and // writes it out prepending padding to each line. func wrapLine(out io.Writer, text []rune, pad string, width int) error { // Note: this is _wrong_ for much of unicode (because the width of a rune on // the terminal is anything between 0 and 2, not always 1 as this code // assumes) but fixing that is Hard. Long story short, you can get close // using a couple of big unicode tables (which is what wcwidth // does). Getting it 100% requires a terminfo-alike of unicode behaviour. // However, before this we'd count bytes instead of runes, so we'd be // even more broken. Think of it as successive approximations... at least // with this work we share tabwriter's opinion on the width of things! // This (and possibly printDescr below) should move to strutil once // we're happy with it getting wider (heh heh) use. // discard any trailing whitespace text = runesTrimRightSpace(text) // establish the indent of the whole block idx := 0 for idx < len(text) && unicode.IsSpace(text[idx]) { idx++ } indent := pad + string(text[:idx]) text = text[idx:] width -= idx + utf8.RuneCountInString(pad) var err error for len(text) > width && err == nil { // find a good place to chop the text idx = runesLastIndexSpace(text[:width+1]) if idx < 0 { // there's no whitespace; just chop at line width idx = width } _, err = fmt.Fprint(out, indent, string(text[:idx]), "\n") // prune any remaining whitespace before the start of the next line for idx < len(text) && unicode.IsSpace(text[idx]) { idx++ } text = text[idx:] } if err != nil { return err } _, err = fmt.Fprint(out, indent, string(text), "\n") return err } // printDescr formats a given string (typically a snap description) // in a user friendly way. // // The rules are (intentionally) very simple: // - trim trailing whitespace // - word wrap at "max" chars preserving line indent // - keep \n intact and break there func printDescr(w io.Writer, descr string, max int) error { var err error descr = strings.TrimRightFunc(descr, unicode.IsSpace) for _, line := range strings.Split(descr, "\n") { err = wrapLine(w, []rune(line), " ", max) if err != nil { break } } return err } func maybePrintCommands(w io.Writer, snapName string, allApps []client.AppInfo, n int) { if len(allApps) == 0 { return } commands := make([]string, 0, len(allApps)) for _, app := range allApps { if app.IsService() { continue } cmdStr := snap.JoinSnapApp(snapName, app.Name) commands = append(commands, cmdStr) } if len(commands) == 0 { return } fmt.Fprintf(w, "commands:\n") for _, cmd := range commands { fmt.Fprintf(w, " - %s\n", cmd) } } func maybePrintServices(w io.Writer, snapName string, allApps []client.AppInfo, n int) { if len(allApps) == 0 { return } services := make([]string, 0, len(allApps)) for _, app := range allApps { if !app.IsService() { continue } var active, enabled string if app.Active { active = "active" } else { active = "inactive" } if app.Enabled { enabled = "enabled" } else { enabled = "disabled" } services = append(services, fmt.Sprintf(" %s:\t%s, %s, %s", snap.JoinSnapApp(snapName, app.Name), app.Daemon, enabled, active)) } if len(services) == 0 { return } fmt.Fprintf(w, "services:\n") for _, svc := range services { fmt.Fprintln(w, svc) } } var channelRisks = []string{"stable", "candidate", "beta", "edge"} // displayChannels displays channels and tracks in the right order func (x *infoCmd) displayChannels(w io.Writer, chantpl string, esc *escapes, remote *client.Snap, revLen, sizeLen int) (maxRevLen, maxSizeLen int) { fmt.Fprintln(w, "channels:") releasedfmt := "2006-01-02" if x.AbsTime { releasedfmt = time.RFC3339 } type chInfoT struct { name, version, released, revision, size, notes string } var chInfos []*chInfoT maxRevLen, maxSizeLen = revLen, sizeLen // order by tracks for _, tr := range remote.Tracks { trackHasOpenChannel := false for _, risk := range channelRisks { chName := fmt.Sprintf("%s/%s", tr, risk) ch, ok := remote.Channels[chName] if tr == "latest" { chName = risk } chInfo := chInfoT{name: chName} if ok { chInfo.version = ch.Version chInfo.revision = fmt.Sprintf("(%s)", ch.Revision) if len(chInfo.revision) > maxRevLen { maxRevLen = len(chInfo.revision) } chInfo.released = ch.ReleasedAt.Format(releasedfmt) chInfo.size = strutil.SizeToStr(ch.Size) if len(chInfo.size) > maxSizeLen { maxSizeLen = len(chInfo.size) } chInfo.notes = NotesFromChannelSnapInfo(ch).String() trackHasOpenChannel = true } else { if trackHasOpenChannel { chInfo.version = esc.uparrow } else { chInfo.version = esc.dash } } chInfos = append(chInfos, &chInfo) } } for _, chInfo := range chInfos { fmt.Fprintf(w, " "+chantpl, chInfo.name, chInfo.version, chInfo.released, maxRevLen, chInfo.revision, maxSizeLen, chInfo.size, chInfo.notes) } return maxRevLen, maxSizeLen } func formatSummary(raw string) string { s, err := yaml.Marshal(raw) if err != nil { return fmt.Sprintf("cannot marshal summary: %s", err) } return strings.TrimSpace(string(s)) } func (x *infoCmd) Execute([]string) error { termWidth, _ := termSize() termWidth -= 3 if termWidth > 100 { // any wider than this and it gets hard to read termWidth = 100 } esc := x.getEscapes() w := tabwriter.NewWriter(Stdout, 2, 2, 1, ' ', 0) noneOK := true for i, snapName := range x.Positional.Snaps { snapName := string(snapName) if i > 0 { fmt.Fprintln(w, "---") } if snapName == "system" { fmt.Fprintln(w, "system: You can't have it.") continue } if tryDirect(w, snapName, x.Verbose) { noneOK = false continue } remote, resInfo, _ := x.client.FindOne(snapName) local, _, _ := x.client.Snap(snapName) both := coalesce(local, remote) if both == nil { if len(x.Positional.Snaps) == 1 { return fmt.Errorf("no snap found for %q", snapName) } fmt.Fprintf(w, fmt.Sprintf(i18n.G("warning:\tno snap found for %q\n"), snapName)) continue } noneOK = false fmt.Fprintf(w, "name:\t%s\n", both.Name) fmt.Fprintf(w, "summary:\t%s\n", formatSummary(both.Summary)) fmt.Fprintf(w, "publisher:\t%s\n", longPublisher(esc, both.Publisher)) if both.Contact != "" { fmt.Fprintf(w, "contact:\t%s\n", strings.TrimPrefix(both.Contact, "mailto:")) } license := both.License if license == "" { license = "unset" } fmt.Fprintf(w, "license:\t%s\n", license) maybePrintPrice(w, remote, resInfo) fmt.Fprintln(w, "description: |") printDescr(w, both.Description, termWidth) maybePrintCommands(w, snapName, both.Apps, termWidth) maybePrintServices(w, snapName, both.Apps, termWidth) if x.Verbose { fmt.Fprintln(w, "notes:\t") fmt.Fprintf(w, " private:\t%t\n", both.Private) fmt.Fprintf(w, " confinement:\t%s\n", both.Confinement) } var notes *Notes if local != nil { if x.Verbose { jailMode := local.Confinement == client.DevModeConfinement && !local.DevMode fmt.Fprintf(w, " devmode:\t%t\n", local.DevMode) fmt.Fprintf(w, " jailmode:\t%t\n", jailMode) fmt.Fprintf(w, " trymode:\t%t\n", local.TryMode) fmt.Fprintf(w, " enabled:\t%t\n", local.Status == client.StatusActive) if local.Broken == "" { fmt.Fprintf(w, " broken:\t%t\n", false) } else { fmt.Fprintf(w, " broken:\t%t (%s)\n", true, local.Broken) } fmt.Fprintf(w, " ignore-validation:\t%t\n", local.IgnoreValidation) } else { notes = NotesFromLocal(local) } } // stops the notes etc trying to be aligned with channels w.Flush() maybePrintType(w, both.Type) maybePrintBase(w, both.Base, x.Verbose) maybePrintID(w, both) var localRev, localSize string var revLen, sizeLen int if local != nil { if local.TrackingChannel != "" { fmt.Fprintf(w, "tracking:\t%s\n", local.TrackingChannel) } if !local.InstallDate.IsZero() { fmt.Fprintf(w, "refresh-date:\t%s\n", x.fmtTime(local.InstallDate)) } localRev = fmt.Sprintf("(%s)", local.Revision) revLen = len(localRev) localSize = strutil.SizeToStr(local.InstalledSize) sizeLen = len(localSize) } chantpl := "%s:\t%s %s%*s %*s %s\n" if remote != nil && remote.Channels != nil && remote.Tracks != nil { chantpl = "%s:\t%s\t%s\t%*s\t%*s\t%s\n" w.Flush() revLen, sizeLen = x.displayChannels(w, chantpl, esc, remote, revLen, sizeLen) } if local != nil { fmt.Fprintf(w, chantpl, "installed", local.Version, "", revLen, localRev, sizeLen, localSize, notes) } } w.Flush() if noneOK { return fmt.Errorf(i18n.G("no valid snaps given")) } return nil } snapd-2.37.4~14.04.1/cmd/snap/cmd_version_other.go0000664000000000000000000000223513435556260016307 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- // +build !linux /* * Copyright (C) 2016 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 . * */ package main import ( "fmt" "runtime" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/release" ) func serverVersion(*client.Client) *client.ServerVersion { return &client.ServerVersion{ Version: i18n.G("unavailable"), Series: release.Series, OSID: runtime.GOOS, OnClassic: true, KernelVersion: fmt.Sprintf("%s (%s)", osutil.KernelVersion(), runtime.GOARCH), } } snapd-2.37.4~14.04.1/cmd/snap/cmd_interface_test.go0000664000000000000000000002246313435556260016425 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main_test import ( "io/ioutil" "net/http" "os" "github.com/jessevdk/go-flags" . "gopkg.in/check.v1" "github.com/snapcore/snapd/client" . "github.com/snapcore/snapd/cmd/snap" ) func (s *SnapSuite) TestInterfaceHelp(c *C) { msg := `Usage: snap.test interface [interface-OPTIONS] [] The interface command shows details of snap interfaces. If no interface name is provided, a list of interface names with at least one connection is shown, or a list of all interfaces if --all is provided. [interface command options] --attrs Show interface attributes --all Include unused interfaces [interface command arguments] : Show details of a specific interface ` s.testSubCommandHelp(c, "interface", msg) } func (s *SnapSuite) TestInterfaceListEmpty(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, Equals, "GET") c.Check(r.URL.Path, Equals, "/v2/interfaces") c.Check(r.URL.RawQuery, Equals, "select=connected") body, err := ioutil.ReadAll(r.Body) c.Check(err, IsNil) c.Check(body, DeepEquals, []byte{}) EncodeResponseBody(c, w, map[string]interface{}{ "type": "sync", "result": []*client.Interface{}, }) }) rest, err := Parser(Client()).ParseArgs([]string{"interface"}) c.Assert(err, ErrorMatches, "no interfaces currently connected") c.Assert(rest, DeepEquals, []string{"interface"}) c.Assert(s.Stdout(), Equals, "") c.Assert(s.Stderr(), Equals, "") } func (s *SnapSuite) TestInterfaceListAllEmpty(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, Equals, "GET") c.Check(r.URL.Path, Equals, "/v2/interfaces") c.Check(r.URL.RawQuery, Equals, "select=all") body, err := ioutil.ReadAll(r.Body) c.Check(err, IsNil) c.Check(body, DeepEquals, []byte{}) EncodeResponseBody(c, w, map[string]interface{}{ "type": "sync", "result": []*client.Interface{}, }) }) rest, err := Parser(Client()).ParseArgs([]string{"interface", "--all"}) c.Assert(err, ErrorMatches, "no interfaces found") c.Assert(rest, DeepEquals, []string{"--all"}) // XXX: feels like a bug in go-flags. c.Assert(s.Stdout(), Equals, "") c.Assert(s.Stderr(), Equals, "") } func (s *SnapSuite) TestInterfaceList(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, Equals, "GET") c.Check(r.URL.Path, Equals, "/v2/interfaces") c.Check(r.URL.RawQuery, Equals, "select=connected") body, err := ioutil.ReadAll(r.Body) c.Check(err, IsNil) c.Check(body, DeepEquals, []byte{}) EncodeResponseBody(c, w, map[string]interface{}{ "type": "sync", "result": []*client.Interface{{ Name: "network", Summary: "allows access to the network", }, { Name: "network-bind", Summary: "allows providing services on the network", }}, }) }) rest, err := Parser(Client()).ParseArgs([]string{"interface"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) expectedStdout := "" + "Name Summary\n" + "network allows access to the network\n" + "network-bind allows providing services on the network\n" c.Assert(s.Stdout(), Equals, expectedStdout) c.Assert(s.Stderr(), Equals, "") } func (s *SnapSuite) TestInterfaceListAll(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, Equals, "GET") c.Check(r.URL.Path, Equals, "/v2/interfaces") c.Check(r.URL.RawQuery, Equals, "select=all") body, err := ioutil.ReadAll(r.Body) c.Check(err, IsNil) c.Check(body, DeepEquals, []byte{}) EncodeResponseBody(c, w, map[string]interface{}{ "type": "sync", "result": []*client.Interface{{ Name: "network", Summary: "allows access to the network", }, { Name: "network-bind", Summary: "allows providing services on the network", }, { Name: "unused", Summary: "just an unused interface, nothing to see here", }}, }) }) rest, err := Parser(Client()).ParseArgs([]string{"interface", "--all"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) expectedStdout := "" + "Name Summary\n" + "network allows access to the network\n" + "network-bind allows providing services on the network\n" + "unused just an unused interface, nothing to see here\n" c.Assert(s.Stdout(), Equals, expectedStdout) c.Assert(s.Stderr(), Equals, "") } func (s *SnapSuite) TestInterfaceDetails(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, Equals, "GET") c.Check(r.URL.Path, Equals, "/v2/interfaces") c.Check(r.URL.RawQuery, Equals, "doc=true&names=network&plugs=true&select=all&slots=true") body, err := ioutil.ReadAll(r.Body) c.Check(err, IsNil) c.Check(body, DeepEquals, []byte{}) EncodeResponseBody(c, w, map[string]interface{}{ "type": "sync", "result": []*client.Interface{{ Name: "network", Summary: "allows access to the network", DocURL: "http://example.org/about-the-network-interface", Plugs: []client.Plug{ {Snap: "deepin-music", Name: "network"}, {Snap: "http", Name: "network"}, }, Slots: []client.Slot{{Snap: "system", Name: "network"}}, }}, }) }) rest, err := Parser(Client()).ParseArgs([]string{"interface", "network"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) expectedStdout := "" + "name: network\n" + "summary: allows access to the network\n" + "documentation: http://example.org/about-the-network-interface\n" + "plugs:\n" + " - deepin-music\n" + " - http\n" + "slots:\n" + " - system\n" c.Assert(s.Stdout(), Equals, expectedStdout) c.Assert(s.Stderr(), Equals, "") } func (s *SnapSuite) TestInterfaceDetailsAndAttrs(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, Equals, "GET") c.Check(r.URL.Path, Equals, "/v2/interfaces") c.Check(r.URL.RawQuery, Equals, "doc=true&names=serial-port&plugs=true&select=all&slots=true") body, err := ioutil.ReadAll(r.Body) c.Check(err, IsNil) c.Check(body, DeepEquals, []byte{}) EncodeResponseBody(c, w, map[string]interface{}{ "type": "sync", "result": []*client.Interface{{ Name: "serial-port", Summary: "allows providing or using a specific serial port", Plugs: []client.Plug{ {Snap: "minicom", Name: "serial-port"}, }, Slots: []client.Slot{{ Snap: "gizmo-gadget", Name: "debug-serial-port", Label: "serial port for debugging", Attrs: map[string]interface{}{ "header": "pin-array", "location": "internal", "path": "/dev/ttyS0", "number": 1, }, }}, }}, }) }) rest, err := Parser(Client()).ParseArgs([]string{"interface", "--attrs", "serial-port"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) expectedStdout := "" + "name: serial-port\n" + "summary: allows providing or using a specific serial port\n" + "plugs:\n" + " - minicom\n" + "slots:\n" + " - gizmo-gadget:debug-serial-port (serial port for debugging):\n" + " header: pin-array\n" + " location: internal\n" + " number: 1\n" + " path: /dev/ttyS0\n" c.Assert(s.Stdout(), Equals, expectedStdout) c.Assert(s.Stderr(), Equals, "") } func (s *SnapSuite) TestInterfaceCompletion(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Assert(r.Method, Equals, "GET") c.Check(r.URL.Path, Equals, "/v2/interfaces") c.Check(r.URL.RawQuery, Equals, "select=all") EncodeResponseBody(c, w, map[string]interface{}{ "type": "sync", "result": []*client.Interface{{ Name: "network", Summary: "allows access to the network", }, { Name: "network-bind", Summary: "allows providing services on the network", }}, }) }) os.Setenv("GO_FLAGS_COMPLETION", "verbose") defer os.Unsetenv("GO_FLAGS_COMPLETION") expected := []flags.Completion{} parser := Parser(Client()) parser.CompletionHandler = func(obtained []flags.Completion) { c.Check(obtained, DeepEquals, expected) } expected = []flags.Completion{ {Item: "network", Description: "allows access to the network"}, {Item: "network-bind", Description: "allows providing services on the network"}, } _, err := parser.ParseArgs([]string{"interface", ""}) c.Assert(err, IsNil) expected = []flags.Completion{ {Item: "network-bind", Description: "allows providing services on the network"}, } _, err = parser.ParseArgs([]string{"interface", "network-"}) c.Assert(err, IsNil) expected = []flags.Completion{} _, err = parser.ParseArgs([]string{"interface", "bogus"}) c.Assert(err, IsNil) c.Assert(s.Stdout(), Equals, "") c.Assert(s.Stderr(), Equals, "") } snapd-2.37.4~14.04.1/cmd/snap/cmd_sandbox_features.go0000664000000000000000000000447713435556260016767 0ustar // -*- 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 . * */ package main import ( "fmt" "sort" "strings" "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/i18n" ) var shortSandboxFeaturesHelp = i18n.G("Print sandbox features available on the system") var longSandboxFeaturesHelp = i18n.G(` The sandbox command prints tags describing features of individual sandbox components used by snapd on a given system. `) type cmdSandboxFeatures struct { clientMixin Required []string `long:"required" arg-name:""` } func init() { addDebugCommand("sandbox-features", shortSandboxFeaturesHelp, longSandboxFeaturesHelp, func() flags.Commander { return &cmdSandboxFeatures{} }, map[string]string{ "required": i18n.G("Ensure that given backend:feature is available"), }, nil) } func (cmd cmdSandboxFeatures) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } sysInfo, err := cmd.client.SysInfo() if err != nil { return err } sandboxFeatures := sysInfo.SandboxFeatures if len(cmd.Required) > 0 { avail := make(map[string]bool) for backend := range sandboxFeatures { for _, feature := range sandboxFeatures[backend] { avail[fmt.Sprintf("%s:%s", backend, feature)] = true } } for _, required := range cmd.Required { if !avail[required] { return fmt.Errorf("sandbox feature not available: %q", required) } } } else { backends := make([]string, 0, len(sandboxFeatures)) for backend := range sandboxFeatures { backends = append(backends, backend) } sort.Strings(backends) w := tabWriter() defer w.Flush() for _, backend := range backends { fmt.Fprintf(w, "%s:\t%s\n", backend, strings.Join(sandboxFeatures[backend], " ")) } } return nil } snapd-2.37.4~14.04.1/cmd/snap/cmd_aliases_test.go0000664000000000000000000001370113435556260016101 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main_test import ( "io/ioutil" "net/http" . "gopkg.in/check.v1" "github.com/snapcore/snapd/client" . "github.com/snapcore/snapd/cmd/snap" ) func (s *SnapSuite) TestAliasesHelp(c *C) { msg := `Usage: snap.test aliases [] The aliases command lists all aliases available in the system and their status. $ snap aliases Lists only the aliases defined by the specified snap. An alias noted as undefined means it was explicitly enabled or disabled but is not defined in the current revision of the snap, possibly temporarily (e.g. because of a revert). This can cleared with 'snap alias --reset'. ` s.testSubCommandHelp(c, "aliases", msg) } func (s *SnapSuite) TestAliases(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, Equals, "GET") c.Check(r.URL.Path, Equals, "/v2/aliases") body, err := ioutil.ReadAll(r.Body) c.Check(err, IsNil) c.Check(body, DeepEquals, []byte{}) EncodeResponseBody(c, w, map[string]interface{}{ "type": "sync", "result": map[string]map[string]client.AliasStatus{ "foo": { "foo0": {Command: "foo", Status: "auto", Auto: "foo"}, "foo_reset": {Command: "foo.reset", Manual: "reset", Status: "manual"}, }, "bar": { "bar_dump": {Command: "bar.dump", Status: "manual", Manual: "dump"}, "bar_dump.1": {Command: "bar.dump", Status: "disabled", Auto: "dump"}, "bar_restore": {Command: "bar.safe-restore", Status: "manual", Auto: "restore", Manual: "safe-restore"}, }, }, }) }) rest, err := Parser(Client()).ParseArgs([]string{"aliases"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) expectedStdout := "" + "Command Alias Notes\n" + "bar.dump bar_dump manual\n" + "bar.dump bar_dump.1 disabled\n" + "bar.safe-restore bar_restore manual,override\n" + "foo foo0 -\n" + "foo.reset foo_reset manual\n" c.Assert(s.Stdout(), Equals, expectedStdout) c.Assert(s.Stderr(), Equals, "") } func (s *SnapSuite) TestAliasesFilterSnap(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, Equals, "GET") c.Check(r.URL.Path, Equals, "/v2/aliases") body, err := ioutil.ReadAll(r.Body) c.Check(err, IsNil) c.Check(body, DeepEquals, []byte{}) EncodeResponseBody(c, w, map[string]interface{}{ "type": "sync", "result": map[string]map[string]client.AliasStatus{ "foo": { "foo0": {Command: "foo", Status: "auto", Auto: "foo"}, "foo_reset": {Command: "foo.reset", Manual: "reset", Status: "manual"}, }, "bar": { "bar_dump": {Command: "bar.dump", Status: "manual", Manual: "dump"}, "bar_dump.1": {Command: "bar.dump", Status: "disabled", Auto: "dump"}, }, }, }) }) rest, err := Parser(Client()).ParseArgs([]string{"aliases", "foo"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) expectedStdout := "" + "Command Alias Notes\n" + "foo foo0 -\n" + "foo.reset foo_reset manual\n" c.Assert(s.Stdout(), Equals, expectedStdout) c.Assert(s.Stderr(), Equals, "") } func (s *SnapSuite) TestAliasesNone(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, Equals, "GET") c.Check(r.URL.Path, Equals, "/v2/aliases") body, err := ioutil.ReadAll(r.Body) c.Check(err, IsNil) c.Check(body, DeepEquals, []byte{}) EncodeResponseBody(c, w, map[string]interface{}{ "type": "sync", "result": map[string]map[string]client.AliasStatus{}, }) }) _, err := Parser(Client()).ParseArgs([]string{"aliases"}) c.Assert(err, IsNil) c.Assert(s.Stdout(), Equals, "") c.Assert(s.Stderr(), Equals, "No aliases are currently defined.\n\nUse 'snap help alias' to learn how to create aliases manually.\n") } func (s *SnapSuite) TestAliasesNoneFilterSnap(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, Equals, "GET") c.Check(r.URL.Path, Equals, "/v2/aliases") body, err := ioutil.ReadAll(r.Body) c.Check(err, IsNil) c.Check(body, DeepEquals, []byte{}) EncodeResponseBody(c, w, map[string]interface{}{ "type": "sync", "result": map[string]map[string]client.AliasStatus{ "bar": { "bar0": {Command: "foo", Status: "auto", Auto: "foo"}, }}, }) }) _, err := Parser(Client()).ParseArgs([]string{"aliases", "not-bar"}) c.Assert(err, IsNil) c.Assert(s.Stdout(), Equals, "") c.Assert(s.Stderr(), Equals, "No aliases are currently defined for snap \"not-bar\".\n\nUse 'snap help alias' to learn how to create aliases manually.\n") } func (s *SnapSuite) TestAliasesSorting(c *C) { tests := []struct { snap1 string cmd1 string alias1 string snap2 string cmd2 string alias2 string }{ {"bar", "bar", "r", "baz", "baz", "z"}, {"bar", "bar", "bar0", "bar", "bar.app", "bapp"}, {"bar", "bar.app1", "bapp1", "bar", "bar.app2", "bapp2"}, {"bar", "bar.app1", "appx", "bar", "bar.app1", "appy"}, } for _, test := range tests { res := AliasInfoLess(test.snap1, test.alias1, test.cmd1, test.snap2, test.alias2, test.cmd2) c.Check(res, Equals, true, Commentf("%v", test)) rres := AliasInfoLess(test.snap2, test.alias2, test.cmd2, test.snap1, test.alias1, test.cmd1) c.Check(rres, Equals, false, Commentf("reversed %v", test)) } } snapd-2.37.4~14.04.1/cmd/snap/cmd_warnings_test.go0000664000000000000000000001423213435556260016310 0ustar // -*- 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 . * */ package main_test import ( "fmt" "io/ioutil" "net/http" "time" "gopkg.in/check.v1" snap "github.com/snapcore/snapd/cmd/snap" ) type warningSuite struct { BaseSnapSuite } var _ = check.Suite(&warningSuite{}) const twoWarnings = `{ "result": [ { "expire-after": "672h0m0s", "first-added": "2018-09-19T12:41:18.505007495Z", "last-added": "2018-09-19T12:41:18.505007495Z", "message": "hello world number one", "repeat-after": "24h0m0s" }, { "expire-after": "672h0m0s", "first-added": "2018-09-19T12:44:19.680362867Z", "last-added": "2018-09-19T12:44:19.680362867Z", "message": "hello world number two", "repeat-after": "24h0m0s" } ], "status": "OK", "status-code": 200, "type": "sync" }` func mkWarningsFakeHandler(c *check.C, body string) func(w http.ResponseWriter, r *http.Request) { var called bool return func(w http.ResponseWriter, r *http.Request) { if called { c.Fatalf("expected a single request") } called = true c.Check(r.URL.Path, check.Equals, "/v2/warnings") c.Check(r.URL.Query(), check.HasLen, 0) buf, err := ioutil.ReadAll(r.Body) c.Assert(err, check.IsNil) c.Check(string(buf), check.Equals, "") c.Check(r.Method, check.Equals, "GET") w.WriteHeader(200) fmt.Fprintln(w, body) } } func (s *warningSuite) TestNoWarningsEver(c *check.C) { s.RedirectClientToTestServer(mkWarningsFakeHandler(c, `{"type": "sync", "status-code": 200, "result": []}`)) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"warnings", "--abs-time"}) c.Assert(err, check.IsNil) c.Check(rest, check.HasLen, 0) c.Check(s.Stderr(), check.Equals, "") c.Check(s.Stdout(), check.Equals, "No warnings.\n") } func (s *warningSuite) TestNoFurtherWarnings(c *check.C) { snap.WriteWarningTimestamp(time.Now()) s.RedirectClientToTestServer(mkWarningsFakeHandler(c, `{"type": "sync", "status-code": 200, "result": []}`)) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"warnings", "--abs-time"}) c.Assert(err, check.IsNil) c.Check(rest, check.HasLen, 0) c.Check(s.Stderr(), check.Equals, "") c.Check(s.Stdout(), check.Equals, "No further warnings.\n") } func (s *warningSuite) TestWarnings(c *check.C) { s.RedirectClientToTestServer(mkWarningsFakeHandler(c, twoWarnings)) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"warnings", "--abs-time"}) c.Assert(err, check.IsNil) c.Check(rest, check.HasLen, 0) c.Check(s.Stderr(), check.Equals, "") c.Check(s.Stdout(), check.Equals, ` Last occurrence Warning 2018-09-19T12:41:18Z hello world number one 2018-09-19T12:44:19Z hello world number two `[1:]) } func (s *warningSuite) TestVerboseWarnings(c *check.C) { s.RedirectClientToTestServer(mkWarningsFakeHandler(c, twoWarnings)) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"warnings", "--abs-time", "--verbose"}) c.Assert(err, check.IsNil) c.Check(rest, check.HasLen, 0) c.Check(s.Stderr(), check.Equals, "") c.Check(s.Stdout(), check.Equals, ` First occurrence Last occurrence Expires after Acknowledged Repeats after Warning 2018-09-19T12:41:18Z 2018-09-19T12:41:18Z 28d0h - 1d00h hello world number one 2018-09-19T12:44:19Z 2018-09-19T12:44:19Z 28d0h - 1d00h hello world number two `[1:]) } func (s *warningSuite) TestOkay(c *check.C) { t0 := time.Now() snap.WriteWarningTimestamp(t0) var n int s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { n++ if n != 1 { c.Fatalf("expected 1 request, now on %d", n) } c.Check(r.URL.Path, check.Equals, "/v2/warnings") c.Check(r.URL.Query(), check.HasLen, 0) c.Assert(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{"action": "okay", "timestamp": t0.Format(time.RFC3339Nano)}) c.Check(r.Method, check.Equals, "POST") w.WriteHeader(200) fmt.Fprintln(w, `{ "status": "OK", "status-code": 200, "type": "sync" }`) }) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"okay"}) c.Assert(err, check.IsNil) c.Check(rest, check.HasLen, 0) c.Check(s.Stderr(), check.Equals, "") c.Check(s.Stdout(), check.Equals, "") } func (s *warningSuite) TestListWithWarnings(c *check.C) { var called bool s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { if called { c.Fatalf("expected a single request") } called = true c.Check(r.URL.Path, check.Equals, "/v2/snaps") c.Check(r.URL.Query(), check.HasLen, 0) buf, err := ioutil.ReadAll(r.Body) c.Assert(err, check.IsNil) c.Check(string(buf), check.Equals, "") c.Check(r.Method, check.Equals, "GET") w.WriteHeader(200) fmt.Fprintln(w, `{ "result": [{}], "status": "OK", "status-code": 200, "type": "sync", "warning-count": 2, "warning-timestamp": "2018-09-19T12:44:19.680362867Z" }`) }) cli := snap.Client() rest, err := snap.Parser(cli).ParseArgs([]string{"list"}) c.Assert(err, check.IsNil) { // TODO: I hope to get to refactor run() so we can // call it from tests and not have to do this (whole // block) by hand count, stamp := cli.WarningsSummary() c.Check(count, check.Equals, 2) c.Check(stamp, check.Equals, time.Date(2018, 9, 19, 12, 44, 19, 680362867, time.UTC)) snap.MaybePresentWarnings(count, stamp) } c.Check(rest, check.HasLen, 0) c.Check(s.Stdout(), check.Equals, ` Name Version Rev Tracking Publisher Notes unset - - disabled `[1:]) c.Check(s.Stderr(), check.Equals, "WARNING: There are 2 new warnings. See 'snap warnings'.\n") } snapd-2.37.4~14.04.1/cmd/snap/cmd_disconnect.go0000664000000000000000000000507413435556260015556 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main import ( "fmt" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/i18n" "github.com/jessevdk/go-flags" ) type cmdDisconnect struct { waitMixin Positionals struct { Offer disconnectSlotOrPlugSpec `required:"true"` Use disconnectSlotSpec } `positional-args:"true"` } var shortDisconnectHelp = i18n.G("Disconnect a plug from a slot") var longDisconnectHelp = i18n.G(` The disconnect command disconnects a plug from a slot. It may be called in the following ways: $ snap disconnect : : Disconnects the specific plug from the specific slot. $ snap disconnect : Disconnects everything from the provided plug or slot. The snap name may be omitted for the core snap. `) func init() { addCommand("disconnect", shortDisconnectHelp, longDisconnectHelp, func() flags.Commander { return &cmdDisconnect{} }, waitDescs, []argDesc{ // TRANSLATORS: This needs to begin with < and end with > {name: i18n.G(":")}, // TRANSLATORS: This needs to begin with < and end with > {name: i18n.G(":")}, }) } func (x *cmdDisconnect) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } offer := x.Positionals.Offer.SnapAndName use := x.Positionals.Use.SnapAndName // snap disconnect : // snap disconnect if use.Snap == "" && use.Name == "" { // Swap Offer and Use around offer, use = use, offer } if use.Name == "" { return fmt.Errorf("please provide the plug or slot name to disconnect from snap %q", use.Snap) } id, err := x.client.Disconnect(offer.Snap, offer.Name, use.Snap, use.Name) if err != nil { if client.IsInterfacesUnchangedError(err) { fmt.Fprintf(Stdout, i18n.G("No connections to disconnect")) fmt.Fprintf(Stdout, "\n") return nil } return err } if _, err := x.wait(id); err != nil { if err == noWait { return nil } return err } return nil } snapd-2.37.4~14.04.1/cmd/snap/cmd_sign_build.go0000664000000000000000000000717213435556260015545 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2015 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 . * */ package main import ( "fmt" "time" _ "golang.org/x/crypto/sha3" // expected for digests "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/i18n" ) type cmdSignBuild struct { Positional struct { Filename string } `positional-args:"yes" required:"yes"` // XXX complete DeveloperID and SnapID DeveloperID string `long:"developer-id" required:"yes"` SnapID string `long:"snap-id" required:"yes"` KeyName keyName `short:"k" default:"default" ` Grade string `long:"grade" choice:"devel" choice:"stable" default:"stable"` } var shortSignBuildHelp = i18n.G("Create a snap-build assertion") var longSignBuildHelp = i18n.G(` The sign-build command creates a snap-build assertion for the provided snap file. `) func init() { cmd := addCommand("sign-build", shortSignBuildHelp, longSignBuildHelp, func() flags.Commander { return &cmdSignBuild{} }, map[string]string{ // TRANSLATORS: This should not start with a lowercase letter. "developer-id": i18n.G("Identifier of the signer"), // TRANSLATORS: This should not start with a lowercase letter. "snap-id": i18n.G("Identifier of the snap package associated with the build"), // TRANSLATORS: This should not start with a lowercase letter. "k": i18n.G("Name of the GnuPG key to use (defaults to 'default' as key name)"), // TRANSLATORS: This should not start with a lowercase letter. "grade": i18n.G("Grade states the build quality of the snap (defaults to 'stable')"), }, []argDesc{{ // TRANSLATORS: This needs to begin with < and end with > name: i18n.G(""), // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("Filename of the snap you want to assert a build for"), }}) cmd.hidden = true } func (x *cmdSignBuild) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } snapDigest, snapSize, err := asserts.SnapFileSHA3_384(x.Positional.Filename) if err != nil { return err } gkm := asserts.NewGPGKeypairManager() privKey, err := gkm.GetByName(string(x.KeyName)) if err != nil { // TRANSLATORS: %q is the key name, %v the error message return fmt.Errorf(i18n.G("cannot use %q key: %v"), x.KeyName, err) } pubKey := privKey.PublicKey() timestamp := time.Now().Format(time.RFC3339) headers := map[string]interface{}{ "developer-id": x.DeveloperID, "authority-id": x.DeveloperID, "snap-sha3-384": snapDigest, "snap-id": x.SnapID, "snap-size": fmt.Sprintf("%d", snapSize), "grade": x.Grade, "timestamp": timestamp, } adb, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ KeypairManager: gkm, }) if err != nil { return fmt.Errorf(i18n.G("cannot open the assertions database: %v"), err) } a, err := adb.Sign(asserts.SnapBuildType, headers, nil, pubKey.ID()) if err != nil { return fmt.Errorf(i18n.G("cannot sign assertion: %v"), err) } _, err = Stdout.Write(asserts.Encode(a)) if err != nil { return err } return nil } snapd-2.37.4~14.04.1/cmd/snap/interfaces_common_test.go0000664000000000000000000000314013435556260017324 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main_test import ( . "gopkg.in/check.v1" . "github.com/snapcore/snapd/cmd/snap" ) type SnapAndNameSuite struct{} var _ = Suite(&SnapAndNameSuite{}) func (s *SnapAndNameSuite) TestUnmarshalFlag(c *C) { var sn SnapAndName // Typical err := sn.UnmarshalFlag("snap:name") c.Assert(err, IsNil) c.Check(sn.Snap, Equals, "snap") c.Check(sn.Name, Equals, "name") // Abbreviated err = sn.UnmarshalFlag("snap") c.Assert(err, IsNil) c.Check(sn.Snap, Equals, "snap") c.Check(sn.Name, Equals, "") // Invalid for _, input := range []string{ "snap:", // Empty name, should be spelled as "snap" ":", // Both snap and name empty, makes no sense "snap:name:more", // Name containing :, probably a typo "", // Empty input } { err = sn.UnmarshalFlag(input) c.Assert(err, ErrorMatches, `invalid value: ".*" \(want snap:name or snap\)`) c.Check(sn.Snap, Equals, "") c.Check(sn.Name, Equals, "") } } snapd-2.37.4~14.04.1/cmd/snap/cmd_abort_test.go0000664000000000000000000000514313435556260015570 0ustar // -*- 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 . * */ package main_test import ( "fmt" "net/http" "gopkg.in/check.v1" snap "github.com/snapcore/snapd/cmd/snap" ) func (s *SnapSuite) TestAbortLast(c *check.C) { n := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { n++ switch n { case 1: c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/changes") fmt.Fprintln(w, mockChangesJSON) case 2: c.Check(r.Method, check.Equals, "POST") c.Check(r.URL.Path, check.Equals, "/v2/changes/two") c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{"action": "abort"}) fmt.Fprintln(w, mockChangeJSON) default: c.Errorf("expected 2 queries, currently on %d", n) } }) rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"abort", "--last=install"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, "") c.Check(s.Stderr(), check.Equals, "") c.Assert(n, check.Equals, 2) } func (s *SnapSuite) TestAbortLastQuestionmark(c *check.C) { n := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { n++ c.Check(r.Method, check.Equals, "GET") c.Assert(r.URL.Path, check.Equals, "/v2/changes") switch n { case 1, 2: fmt.Fprintln(w, `{"type": "sync", "result": []}`) case 3, 4: fmt.Fprintln(w, mockChangesJSON) default: c.Errorf("expected 4 calls, now on %d", n) } }) for i := 0; i < 2; i++ { rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"abort", "--last=foobar?"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, "") c.Check(s.Stderr(), check.Equals, "") _, err = snap.Parser(snap.Client()).ParseArgs([]string{"abort", "--last=foobar"}) if i == 0 { c.Assert(err, check.ErrorMatches, `no changes found`) } else { c.Assert(err, check.ErrorMatches, `no changes of type "foobar" found`) } } c.Check(n, check.Equals, 4) } snapd-2.37.4~14.04.1/cmd/snap/cmd_wait_test.go0000664000000000000000000001070013435556260015420 0ustar // -*- 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 . * */ package main_test import ( "encoding/json" "fmt" "net/http" "time" . "gopkg.in/check.v1" snap "github.com/snapcore/snapd/cmd/snap" ) func (s *SnapSuite) TestCmdWaitHappy(c *C) { restore := snap.MockWaitConfTimeout(10 * time.Millisecond) defer restore() n := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, Equals, "GET") c.Check(r.URL.Path, Equals, "/v2/snaps/system/conf") fmt.Fprintln(w, fmt.Sprintf(`{"type":"sync", "status-code": 200, "result": {"seed.loaded":%v}}`, n > 1)) n++ }) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"wait", "system", "seed.loaded"}) c.Assert(err, IsNil) // ensure we retried a bit but make the check not overly precise // because this will run in super busy build hosts that where a // 10 millisecond sleep actually takes much longer until the kernel // hands control back to the process c.Check(n > 2, Equals, true) } func (s *SnapSuite) TestCmdWaitMissingConfKey(c *C) { n := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { n++ }) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"wait", "snapName"}) c.Assert(err, ErrorMatches, "the required argument `` was not provided") c.Check(n, Equals, 0) } func (s *SnapSuite) TestTrueishJSON(c *C) { tests := []struct { v interface{} b bool errStr string }{ // nil {nil, false, ""}, // bool {true, true, ""}, {false, false, ""}, // string {"a", true, ""}, {"", false, ""}, // json.Number {json.Number("1"), true, ""}, {json.Number("-1"), true, ""}, {json.Number("0"), false, ""}, {json.Number("1.0"), true, ""}, {json.Number("-1.0"), true, ""}, {json.Number("0.0"), false, ""}, // slices {[]interface{}{"a"}, true, ""}, {[]interface{}{}, false, ""}, {[]string{"a"}, true, ""}, {[]string{}, false, ""}, // arrays {[2]interface{}{"a", "b"}, true, ""}, {[0]interface{}{}, false, ""}, {[2]string{"a", "b"}, true, ""}, {[0]string{}, false, ""}, // maps {map[string]interface{}{"a": "a"}, true, ""}, {map[string]interface{}{}, false, ""}, {map[interface{}]interface{}{"a": "a"}, true, ""}, {map[interface{}]interface{}{}, false, ""}, // invalid {int(1), false, "cannot test type int for truth"}, } for _, t := range tests { res, err := snap.TrueishJSON(t.v) if t.errStr == "" { c.Check(err, IsNil) } else { c.Check(err, ErrorMatches, t.errStr) } c.Check(res, Equals, t.b, Commentf("unexpected result for %v (%T), did not get expected %v", t.v, t.v, t.b)) } } func (s *SnapSuite) TestCmdWaitIntegration(c *C) { restore := snap.MockWaitConfTimeout(2 * time.Millisecond) defer restore() var tests = []struct { v string willWait bool }{ // not-waiting {"1.0", false}, {"-1.0", false}, {"0.1", false}, {"-0.1", false}, {"1", false}, {"-1", false}, {`"a"`, false}, {`["a"]`, false}, {`{"a":"b"}`, false}, // waiting {"0", true}, {"0.0", true}, {"{}", true}, {"[]", true}, {`""`, true}, {"null", true}, } testValueCh := make(chan string, 2) n := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { testValue := <-testValueCh c.Check(r.Method, Equals, "GET") c.Check(r.URL.Path, Equals, "/v2/snaps/system/conf") fmt.Fprintln(w, fmt.Sprintf(`{"type":"sync", "status-code": 200, "result": {"test.value":%v}}`, testValue)) n++ }) for _, t := range tests { n = 0 testValueCh <- t.v if t.willWait { // a "trueish" value to ensure wait does not wait forever testValueCh <- "42" } _, err := snap.Parser(snap.Client()).ParseArgs([]string{"wait", "system", "test.value"}) c.Assert(err, IsNil) if t.willWait { // we waited once, then got a non-wait value c.Check(n, Equals, 2) } else { // no waiting happened c.Check(n, Equals, 1) } } } snapd-2.37.4~14.04.1/cmd/snap/cmd_interface.go0000664000000000000000000001211013435556260015352 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main import ( "encoding/json" "fmt" "io" "sort" "text/tabwriter" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/i18n" "github.com/jessevdk/go-flags" ) type cmdInterface struct { clientMixin ShowAttrs bool `long:"attrs"` ShowAll bool `long:"all"` Positionals struct { Interface interfaceName `skip-help:"true"` } `positional-args:"true"` } var shortInterfaceHelp = i18n.G("Show details of snap interfaces") var longInterfaceHelp = i18n.G(` The interface command shows details of snap interfaces. If no interface name is provided, a list of interface names with at least one connection is shown, or a list of all interfaces if --all is provided. `) func init() { addCommand("interface", shortInterfaceHelp, longInterfaceHelp, func() flags.Commander { return &cmdInterface{} }, map[string]string{ // TRANSLATORS: This should not start with a lowercase letter. "attrs": i18n.G("Show interface attributes"), // TRANSLATORS: This should not start with a lowercase letter. "all": i18n.G("Include unused interfaces"), }, []argDesc{{ // TRANSLATORS: This needs to begin with < and end with > name: i18n.G(""), // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("Show details of a specific interface"), }}) } func (x *cmdInterface) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } if x.Positionals.Interface != "" { // Show one interface in detail. name := string(x.Positionals.Interface) ifaces, err := x.client.Interfaces(&client.InterfaceOptions{ Names: []string{name}, Doc: true, Plugs: true, Slots: true, }) if err != nil { return err } if len(ifaces) == 0 { return fmt.Errorf(i18n.G("no such interface")) } x.showOneInterface(ifaces[0]) } else { // Show an overview of available interfaces. ifaces, err := x.client.Interfaces(&client.InterfaceOptions{ Connected: !x.ShowAll, }) if err != nil { return err } if len(ifaces) == 0 { if x.ShowAll { return fmt.Errorf(i18n.G("no interfaces found")) } return fmt.Errorf(i18n.G("no interfaces currently connected")) } x.showManyInterfaces(ifaces) } return nil } func (x *cmdInterface) showOneInterface(iface *client.Interface) { w := tabwriter.NewWriter(Stdout, 2, 2, 1, ' ', 0) defer w.Flush() fmt.Fprintf(w, "name:\t%s\n", iface.Name) if iface.Summary != "" { fmt.Fprintf(w, "summary:\t%s\n", iface.Summary) } if iface.DocURL != "" { fmt.Fprintf(w, "documentation:\t%s\n", iface.DocURL) } if len(iface.Plugs) > 0 { fmt.Fprintf(w, "plugs:\n") for _, plug := range iface.Plugs { var labelPart string if plug.Label != "" { labelPart = fmt.Sprintf(" (%s)", plug.Label) } if plug.Name == iface.Name { fmt.Fprintf(w, " - %s%s", plug.Snap, labelPart) } else { fmt.Fprintf(w, ` - %s:%s%s`, plug.Snap, plug.Name, labelPart) } // Print a colon which will make the snap:plug element a key-value // yaml object so that we can write the attributes. if len(plug.Attrs) > 0 && x.ShowAttrs { fmt.Fprintf(w, ":\n") x.showAttrs(w, plug.Attrs, " ") } else { fmt.Fprintf(w, "\n") } } } if len(iface.Slots) > 0 { fmt.Fprintf(w, "slots:\n") for _, slot := range iface.Slots { var labelPart string if slot.Label != "" { labelPart = fmt.Sprintf(" (%s)", slot.Label) } if slot.Name == iface.Name { fmt.Fprintf(w, " - %s%s", slot.Snap, labelPart) } else { fmt.Fprintf(w, ` - %s:%s%s`, slot.Snap, slot.Name, labelPart) } // Print a colon which will make the snap:slot element a key-value // yaml object so that we can write the attributes. if len(slot.Attrs) > 0 && x.ShowAttrs { fmt.Fprintf(w, ":\n") x.showAttrs(w, slot.Attrs, " ") } else { fmt.Fprintf(w, "\n") } } } } func (x *cmdInterface) showManyInterfaces(infos []*client.Interface) { w := tabWriter() defer w.Flush() fmt.Fprintln(w, i18n.G("Name\tSummary")) for _, iface := range infos { fmt.Fprintf(w, "%s\t%s\n", iface.Name, iface.Summary) } } func (x *cmdInterface) showAttrs(w io.Writer, attrs map[string]interface{}, indent string) { if len(attrs) == 0 { return } names := make([]string, 0, len(attrs)) for name := range attrs { names = append(names, name) } sort.Strings(names) for _, name := range names { value := attrs[name] switch value.(type) { case string, bool, json.Number: fmt.Fprintf(w, "%s %s:\t%v\n", indent, name, value) } } } snapd-2.37.4~14.04.1/cmd/snap/cmd_alias.go0000664000000000000000000000607213435556260014515 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main import ( "fmt" "io" "text/tabwriter" "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/snap" ) type cmdAlias struct { waitMixin Positionals struct { SnapApp appName `required:"yes"` Alias string `required:"yes"` } `positional-args:"true"` } // TODO: implement a completer for snapApp var shortAliasHelp = i18n.G("Set up a manual alias") var longAliasHelp = i18n.G(` The alias command aliases the given snap application to the given alias. Once this manual alias is setup the respective application command can be invoked just using the alias. `) func init() { addCommand("alias", shortAliasHelp, longAliasHelp, func() flags.Commander { return &cmdAlias{} }, waitDescs, []argDesc{ {name: ""}, // TRANSLATORS: This needs to begin with < and end with > {name: i18n.G("")}, }) } func (x *cmdAlias) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } snapName, appName := snap.SplitSnapApp(string(x.Positionals.SnapApp)) alias := x.Positionals.Alias id, err := x.client.Alias(snapName, appName, alias) if err != nil { return err } chg, err := x.wait(id) if err != nil { if err == noWait { return nil } return err } return showAliasChanges(chg) } type changedAlias struct { Snap string `json:"snap"` App string `json:"app"` Alias string `json:"alias"` } func showAliasChanges(chg *client.Change) error { var added, removed []*changedAlias if err := chg.Get("aliases-added", &added); err != nil && err != client.ErrNoData { return err } if err := chg.Get("aliases-removed", &removed); err != nil && err != client.ErrNoData { return err } w := tabwriter.NewWriter(Stdout, 2, 2, 1, ' ', 0) if len(added) != 0 { // TRANSLATORS: this is used to introduce a list of aliases that were added printChangedAliases(w, i18n.G("Added"), added) } if len(removed) != 0 { // TRANSLATORS: this is used to introduce a list of aliases that were removed printChangedAliases(w, i18n.G("Removed"), removed) } w.Flush() return nil } func printChangedAliases(w io.Writer, label string, changed []*changedAlias) { fmt.Fprintf(w, "%s:\n", label) for _, a := range changed { // TRANSLATORS: the first %s is a snap command (e.g. "hello-world.echo"), the second is the alias fmt.Fprintf(w, i18n.G("\t- %s as %s\n"), snap.JoinSnapApp(a.Snap, a.App), a.Alias) } } snapd-2.37.4~14.04.1/cmd/snap/cmd_export_key_test.go0000664000000000000000000000625313435556260016655 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main_test import ( "time" . "gopkg.in/check.v1" "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/asserts/assertstest" snap "github.com/snapcore/snapd/cmd/snap" ) func (s *SnapKeysSuite) TestExportKeyNonexistent(c *C) { _, err := snap.Parser(snap.Client()).ParseArgs([]string{"export-key", "nonexistent"}) c.Assert(err, NotNil) c.Check(err.Error(), Equals, "cannot find key named \"nonexistent\" in GPG keyring") c.Check(s.Stdout(), Equals, "") c.Check(s.Stderr(), Equals, "") } func (s *SnapKeysSuite) TestExportKeyDefault(c *C) { rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"export-key"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) pubKey, err := asserts.DecodePublicKey(s.stdout.Bytes()) c.Assert(err, IsNil) c.Check(pubKey.ID(), Equals, "g4Pks54W_US4pZuxhgG_RHNAf_UeZBBuZyGRLLmMj1Do3GkE_r_5A5BFjx24ZwVJ") c.Check(s.Stderr(), Equals, "") } func (s *SnapKeysSuite) TestExportKeyNonDefault(c *C) { rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"export-key", "another"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) pubKey, err := asserts.DecodePublicKey(s.stdout.Bytes()) c.Assert(err, IsNil) c.Check(pubKey.ID(), Equals, "DVQf1U4mIsuzlQqAebjjTPYtYJ-GEhJy0REuj3zvpQYTZ7EJj7adBxIXLJ7Vmk3L") c.Check(s.Stderr(), Equals, "") } func (s *SnapKeysSuite) TestExportKeyAccount(c *C) { storeSigning := assertstest.NewStoreStack("canonical", nil) manager := asserts.NewGPGKeypairManager() assertstest.NewAccount(storeSigning, "developer1", nil, "") rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"export-key", "another", "--account=developer1"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) assertion, err := asserts.Decode(s.stdout.Bytes()) c.Assert(err, IsNil) c.Check(assertion.Type(), Equals, asserts.AccountKeyRequestType) c.Check(assertion.Revision(), Equals, 0) c.Check(assertion.HeaderString("account-id"), Equals, "developer1") c.Check(assertion.HeaderString("name"), Equals, "another") c.Check(assertion.HeaderString("public-key-sha3-384"), Equals, "DVQf1U4mIsuzlQqAebjjTPYtYJ-GEhJy0REuj3zvpQYTZ7EJj7adBxIXLJ7Vmk3L") since, err := time.Parse(time.RFC3339, assertion.HeaderString("since")) c.Assert(err, IsNil) zone, offset := since.Zone() c.Check(zone, Equals, "UTC") c.Check(offset, Equals, 0) c.Check(s.Stderr(), Equals, "") privKey, err := manager.Get(assertion.HeaderString("public-key-sha3-384")) c.Assert(err, IsNil) err = asserts.SignatureCheck(assertion, privKey.PublicKey()) c.Assert(err, IsNil) } snapd-2.37.4~14.04.1/cmd/snap/cmd_logout.go0000664000000000000000000000234013435556260014727 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2015-2016 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 . * */ package main import ( "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/i18n" ) type cmdLogout struct { clientMixin } var shortLogoutHelp = i18n.G("Log out of snapd and the store") var longLogoutHelp = i18n.G(` The logout command logs the current user out of snapd and the store. `) func init() { addCommand("logout", shortLogoutHelp, longLogoutHelp, func() flags.Commander { return &cmdLogout{} }, nil, nil) } func (cmd *cmdLogout) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } return cmd.client.Logout() } snapd-2.37.4~14.04.1/cmd/snap/cmd_can_manage_refreshes.go0000664000000000000000000000247713435556260017550 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main import ( "fmt" "github.com/jessevdk/go-flags" ) type cmdCanManageRefreshes struct { clientMixin } func init() { cmd := addDebugCommand("can-manage-refreshes", "(internal) return if refresh.schedule=managed can be used", "(internal) return if refresh.schedule=managed can be used", func() flags.Commander { return &cmdCanManageRefreshes{} }, nil, nil) cmd.hidden = true } func (x *cmdCanManageRefreshes) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } var resp bool if err := x.client.Debug("can-manage-refreshes", nil, &resp); err != nil { return err } fmt.Fprintf(Stdout, "%v\n", resp) return nil } snapd-2.37.4~14.04.1/cmd/snap/cmd_sign_build_test.go0000664000000000000000000001152413435556260016600 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 . * */ package main_test import ( "fmt" "io/ioutil" "os" "path/filepath" . "gopkg.in/check.v1" "github.com/snapcore/snapd/asserts" snap "github.com/snapcore/snapd/cmd/snap" ) type SnapSignBuildSuite struct { BaseSnapSuite } var _ = Suite(&SnapSignBuildSuite{}) func (s *SnapSignBuildSuite) TestSignBuildMandatoryFlags(c *C) { _, err := snap.Parser(snap.Client()).ParseArgs([]string{"sign-build", "foo_1_amd64.snap"}) c.Assert(err, NotNil) c.Check(err.Error(), Equals, "the required flags `--developer-id' and `--snap-id' were not specified") c.Check(s.Stdout(), Equals, "") c.Check(s.Stderr(), Equals, "") } func (s *SnapSignBuildSuite) TestSignBuildMissingSnap(c *C) { _, err := snap.Parser(snap.Client()).ParseArgs([]string{"sign-build", "foo_1_amd64.snap", "--developer-id", "dev-id1", "--snap-id", "snap-id-1"}) c.Assert(err, NotNil) c.Check(err.Error(), Equals, "cannot compute snap \"foo_1_amd64.snap\" digest: open foo_1_amd64.snap: no such file or directory") c.Check(s.Stdout(), Equals, "") c.Check(s.Stderr(), Equals, "") } func (s *SnapSignBuildSuite) TestSignBuildMissingKey(c *C) { snapFilename := "foo_1_amd64.snap" _err := ioutil.WriteFile(snapFilename, []byte("sample"), 0644) c.Assert(_err, IsNil) defer os.Remove(snapFilename) tempdir := c.MkDir() os.Setenv("SNAP_GNUPG_HOME", tempdir) defer os.Unsetenv("SNAP_GNUPG_HOME") _, err := snap.Parser(snap.Client()).ParseArgs([]string{"sign-build", snapFilename, "--developer-id", "dev-id1", "--snap-id", "snap-id-1"}) c.Assert(err, NotNil) c.Check(err.Error(), Equals, "cannot use \"default\" key: cannot find key named \"default\" in GPG keyring") c.Check(s.Stdout(), Equals, "") c.Check(s.Stderr(), Equals, "") } func (s *SnapSignBuildSuite) TestSignBuildWorks(c *C) { snapFilename := "foo_1_amd64.snap" snapContent := []byte("sample") _err := ioutil.WriteFile(snapFilename, snapContent, 0644) c.Assert(_err, IsNil) defer os.Remove(snapFilename) tempdir := c.MkDir() for _, fileName := range []string{"pubring.gpg", "secring.gpg", "trustdb.gpg"} { data, err := ioutil.ReadFile(filepath.Join("test-data", fileName)) c.Assert(err, IsNil) err = ioutil.WriteFile(filepath.Join(tempdir, fileName), data, 0644) c.Assert(err, IsNil) } os.Setenv("SNAP_GNUPG_HOME", tempdir) defer os.Unsetenv("SNAP_GNUPG_HOME") _, err := snap.Parser(snap.Client()).ParseArgs([]string{"sign-build", snapFilename, "--developer-id", "dev-id1", "--snap-id", "snap-id-1"}) c.Assert(err, IsNil) assertion, err := asserts.Decode([]byte(s.Stdout())) c.Assert(err, IsNil) c.Check(assertion.Type(), Equals, asserts.SnapBuildType) c.Check(assertion.Revision(), Equals, 0) c.Check(assertion.HeaderString("authority-id"), Equals, "dev-id1") c.Check(assertion.HeaderString("developer-id"), Equals, "dev-id1") c.Check(assertion.HeaderString("grade"), Equals, "stable") c.Check(assertion.HeaderString("snap-id"), Equals, "snap-id-1") c.Check(assertion.HeaderString("snap-size"), Equals, fmt.Sprintf("%d", len(snapContent))) c.Check(assertion.HeaderString("snap-sha3-384"), Equals, "jyP7dUgb8HiRNd1SdYPp_il-YNrl6P6PgNAe-j6_7WytjKslENhMD3Of5XBU5bQK") // check for valid signature ?! c.Check(s.Stderr(), Equals, "") } func (s *SnapSignBuildSuite) TestSignBuildWorksDevelGrade(c *C) { snapFilename := "foo_1_amd64.snap" snapContent := []byte("sample") _err := ioutil.WriteFile(snapFilename, snapContent, 0644) c.Assert(_err, IsNil) defer os.Remove(snapFilename) tempdir := c.MkDir() for _, fileName := range []string{"pubring.gpg", "secring.gpg", "trustdb.gpg"} { data, err := ioutil.ReadFile(filepath.Join("test-data", fileName)) c.Assert(err, IsNil) err = ioutil.WriteFile(filepath.Join(tempdir, fileName), data, 0644) c.Assert(err, IsNil) } os.Setenv("SNAP_GNUPG_HOME", tempdir) defer os.Unsetenv("SNAP_GNUPG_HOME") _, err := snap.Parser(snap.Client()).ParseArgs([]string{"sign-build", snapFilename, "--developer-id", "dev-id1", "--snap-id", "snap-id-1", "--grade", "devel"}) c.Assert(err, IsNil) assertion, err := asserts.Decode([]byte(s.Stdout())) c.Assert(err, IsNil) c.Check(assertion.Type(), Equals, asserts.SnapBuildType) c.Check(assertion.HeaderString("grade"), Equals, "devel") // check for valid signature ?! c.Check(s.Stderr(), Equals, "") } snapd-2.37.4~14.04.1/cmd/snap/main.go0000664000000000000000000003454613435556260013534 0ustar // -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2014-2015 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 . * */ package main import ( "fmt" "io" "net/http" "os" "path/filepath" "runtime" "strings" "unicode" "unicode/utf8" "github.com/jessevdk/go-flags" "golang.org/x/crypto/ssh/terminal" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/cmd" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/httputil" "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap" ) func init() { // set User-Agent for when 'snap' talks to the store directly (snap download etc...) httputil.SetUserAgentFromVersion(cmd.Version, "snap") if osutil.GetenvBool("SNAPD_DEBUG") || osutil.GetenvBool("SNAPPY_TESTING") { // in tests or when debugging, enforce the "tidy" lint checks noticef = logger.Panicf } // plug/slot sanitization not used nor possible from snap command, make it no-op snap.SanitizePlugsSlots = func(snapInfo *snap.Info) {} } var ( // Standard streams, redirected for testing. Stdin io.Reader = os.Stdin Stdout io.Writer = os.Stdout Stderr io.Writer = os.Stderr // overridden for testing ReadPassword = terminal.ReadPassword // set to logger.Panicf in testing noticef = logger.Noticef ) type options struct { Version func() `long:"version"` } type argDesc struct { name string desc string } var optionsData options // ErrExtraArgs is returned if extra arguments to a command are found var ErrExtraArgs = fmt.Errorf(i18n.G("too many arguments for command")) // cmdInfo holds information needed to call parser.AddCommand(...). type cmdInfo struct { name, shortHelp, longHelp string builder func() flags.Commander hidden bool optDescs map[string]string argDescs []argDesc alias string extra func(*flags.Command) } // commands holds information about all non-debug commands. var commands []*cmdInfo // debugCommands holds information about all debug commands. var debugCommands []*cmdInfo // addCommand replaces parser.addCommand() in a way that is compatible with // re-constructing a pristine parser. func addCommand(name, shortHelp, longHelp string, builder func() flags.Commander, optDescs map[string]string, argDescs []argDesc) *cmdInfo { info := &cmdInfo{ name: name, shortHelp: shortHelp, longHelp: longHelp, builder: builder, optDescs: optDescs, argDescs: argDescs, } commands = append(commands, info) return info } // addDebugCommand replaces parser.addCommand() in a way that is // compatible with re-constructing a pristine parser. It is meant for // adding debug commands. func addDebugCommand(name, shortHelp, longHelp string, builder func() flags.Commander, optDescs map[string]string, argDescs []argDesc) *cmdInfo { info := &cmdInfo{ name: name, shortHelp: shortHelp, longHelp: longHelp, builder: builder, optDescs: optDescs, argDescs: argDescs, } debugCommands = append(debugCommands, info) return info } type parserSetter interface { setParser(*flags.Parser) } func lintDesc(cmdName, optName, desc, origDesc string) { if len(optName) == 0 { logger.Panicf("option on %q has no name", cmdName) } if len(origDesc) != 0 { logger.Panicf("description of %s's %q of %q set from tag (=> no i18n)", cmdName, optName, origDesc) } if len(desc) > 0 { // decode the first rune instead of converting all of desc into []rune r, _ := utf8.DecodeRuneInString(desc) // note IsLower != !IsUpper for runes with no upper/lower. // Also note that login.u.c. is the only exception we're allowing for // now, but the list of exceptions could grow -- if it does, we might // want to change it to check for urlish things instead of just // login.u.c. if unicode.IsLower(r) && !strings.HasPrefix(desc, "login.ubuntu.com") { noticef("description of %s's %q is lowercase: %q", cmdName, optName, desc) } } } func lintArg(cmdName, optName, desc, origDesc string) { lintDesc(cmdName, optName, desc, origDesc) if len(optName) > 0 && optName[0] == '<' && optName[len(optName)-1] == '>' { return } if len(optName) > 0 && optName[0] == '<' && strings.HasSuffix(optName, ">s") { // see comment in fixupArg about the >s case return } noticef("argument %q's %q should begin with < and end with >", cmdName, optName) } func fixupArg(optName string) string { // Due to misunderstanding some localized versions of option name are // literally "