Merge lp:~mvo/snappy/snappy-snapfs-mount into lp:~snappy-dev/snappy/snappy-moved-to-github

Proposed by Michael Vogt
Status: Needs review
Proposed branch: lp:~mvo/snappy/snappy-snapfs-mount
Merge into: lp:~snappy-dev/snappy/snappy-moved-to-github
Prerequisite: lp:~mvo/snappy/snappy-snapfs-install-via-unpack
Diff against target: 948 lines (+411/-134)
11 files modified
debian/ubuntu-snappy-cli.dirs (+1/-0)
dirs/dirs.go (+2/-0)
pkg/clickdeb/deb.go (+29/-23)
pkg/file.go (+81/-0)
pkg/snapfs/snapfs.go (+47/-22)
pkg/snapfs/snapfs_test.go (+0/-9)
snappy/pkgformat.go (+0/-64)
snappy/snapp.go (+73/-5)
snappy/snapp_snapfs_test.go (+93/-11)
systemd/systemd.go (+46/-0)
systemd/systemd_test.go (+39/-0)
To merge this branch: bzr merge lp:~mvo/snappy/snappy-snapfs-mount
Reviewer Review Type Date Requested Status
Gustavo Niemeyer Approve
John Lenton (community) Approve
Review via email: mp+273883@code.launchpad.net

Description of the change

Add support to (auto)mount the snapfs based snaps. It will unpack the meta-data to disk (to speedup snappy list etc) but keep the rest in the squashfs blob and (auto)mount at runtime.

This is a bit of a RFC branch, I'm not yet super happy about it. It will put the blob into
  /apps/$pkg/$version/blob.snap
and will mount that to:
  /apps/$pkg/$version/run

The downside of this approach is of course that the binary/service paths of the deb backend and the squashfs backend are no longer identical which makes the code that writes that a bit ugly.

I think we have three option and would love to get feedback which one is best:
 1. keep it like its done in this branch given and cleanup once the clickdeb backend is killed
 2. change deb backend to write the actual (non-meta) data to "run/" as well
 3. put the blob.snap as "/apps/$pkg/$pkg_$version.snap" and auto-mount it to /apps/$pkg/$version
    (which complicates the removal a bit because its no longer enough to just rm -rf the /apps/$pkg/$version dir)

To post a comment you must log in.
Revision history for this message
John Lenton (chipaca) wrote :

4. put the .snap in /var/snappy/blobs?

Revision history for this message
John Lenton (chipaca) wrote :

*/var/lib/snappy/blobs i meant. But /var/lib/snappy/snaps is probably better.

A small complication of remove and purge, but other than that it'd work, right?

Revision history for this message
John Lenton (chipaca) :
Revision history for this message
Michael Vogt (mvo) wrote :

Thanks John! So with /var/lib/snappy/snaps we would setup the automount unit to mount /apps/$name/$ver - this would mean that a "snappy list" would always automount the snaps. My current approach was to only mount them when they are used and unpack the meta/* only. But maybe thats premature optimization(?).

Revision history for this message
John Lenton (chipaca) wrote :

I think it might be. Let me tinker with list a bit tomorrow -- i think it can be made to work without loading package.yaml's.

Revision history for this message
John Lenton (chipaca) wrote :

Tinkered now instead.

To make list not load package.yaml's, you'd need to

* move it to husks, or to the rest api. Latter needs doing anyway; former would be short-term quick if needed.

* add a concreter that knew about squashfs:
 - `IsInstalled` checks for presence of blob instead of package.yaml
 - `Load` loaded a RemoteSnapPart from the locally-stored remote manifest. Would be missing InstalledSize, which list doesn't output. Would probably mean moving RemoteSnapPart to its own package (\o/)

If doing it via the REST API, /1.0/packages would need to recover the ?sources=local option, and an additional flag to tell it to do things this way. Piece of cake.

So, I'd suggest do it the clean way, mounting to /$type/$name/$ver (i think we agree it's the clean way?). Then we benchmark and decide whether to do the above now, or later.

HTH,

lp:~mvo/snappy/snappy-snapfs-mount updated
754. By Michael Vogt

merged lp:snappy

755. By Michael Vogt

refactor and get rid of runPrefix

Revision history for this message
Michael Vogt (mvo) wrote :

Thanks, I pondered over this too and I like the approach of /var/lib/snappy/snaps and that we can use husks^Wleightweight to introspect without the full mount dance. I will update the MP accordingly.

lp:~mvo/snappy/snappy-snapfs-mount updated
756. By Michael Vogt

ensure removal removes the snapfs and refactor

757. By Michael Vogt

update comments

758. By Michael Vogt

unmount after deactivate

Revision history for this message
Michael Vogt (mvo) wrote :

A new look would be welcome :)

lp:~mvo/snappy/snappy-snapfs-mount updated
759. By Michael Vogt

improve comments

Revision history for this message
John Lenton (chipaca) wrote :

Some comments.

Need to iterate the review a little.

Revision history for this message
Sergio Schvezov (sergiusens) wrote :

I can't beat Chipaca on reviews but I did manage to add a bite of a comment that is not a problem today but will avoid headaches later. It is an 'if you want to do this' type of comment.

Good work

lp:~mvo/snappy/snappy-snapfs-mount updated
760. By Michael Vogt

address review comments

761. By Michael Vogt

refactor and ove MountUnit handling to systemd package

762. By Michael Vogt

add more tests

763. By Michael Vogt

add comment about little endian squashfs

Revision history for this message
Michael Vogt (mvo) wrote :

Thanks for the review! I addressed the comemnts, please let me know if its sufficient.

Revision history for this message
John Lenton (chipaca) :
lp:~mvo/snappy/snappy-snapfs-mount updated
764. By Michael Vogt

pkg/snapfs/snapfs.go: use filepath.Clean() in BlobPath

Revision history for this message
Michael Vogt (mvo) wrote :

Thanks! Adressing more stuff.

Revision history for this message
John Lenton (chipaca) wrote :

This looks good enough, now :)

review: Approve
Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

I'm adding a few comments below. Please feel free to merge this once you're personally happy about it.

review: Approve
Revision history for this message
John Lenton (chipaca) :
lp:~mvo/snappy/snappy-snapfs-mount updated
765. By Michael Vogt

merged lp:snappy

766. By Michael Vogt

use properly terminated sentences (thanks gustavo)

767. By Michael Vogt

more documenation and improve error reporting (thanks Gustavo)

Revision history for this message
Michael Vogt (mvo) wrote :

Thanks Gustavo for this excellent review. I started fixing some of the outlined issues and it looks like I'm running out of time. I will continue later but be assured that I will address every point in the review :)

Unmerged revisions

767. By Michael Vogt

more documenation and improve error reporting (thanks Gustavo)

766. By Michael Vogt

use properly terminated sentences (thanks gustavo)

765. By Michael Vogt

merged lp:snappy

764. By Michael Vogt

pkg/snapfs/snapfs.go: use filepath.Clean() in BlobPath

763. By Michael Vogt

add comment about little endian squashfs

762. By Michael Vogt

add more tests

761. By Michael Vogt

refactor and ove MountUnit handling to systemd package

760. By Michael Vogt

address review comments

759. By Michael Vogt

improve comments

758. By Michael Vogt

unmount after deactivate

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'debian/ubuntu-snappy-cli.dirs'
2--- debian/ubuntu-snappy-cli.dirs 1970-01-01 00:00:00 +0000
3+++ debian/ubuntu-snappy-cli.dirs 2015-10-20 15:38:57 +0000
4@@ -0,0 +1,1 @@
5+/var/lib/snappy/snaps
6
7=== modified file 'dirs/dirs.go'
8--- dirs/dirs.go 2015-09-25 15:27:11 +0000
9+++ dirs/dirs.go 2015-10-20 15:38:57 +0000
10@@ -35,6 +35,7 @@
11 LocaleDir string
12 SnapIconsDir string
13 SnapMetaDir string
14+ SnapBlobDir string
15
16 SnapBinariesDir string
17 SnapServicesDir string
18@@ -59,6 +60,7 @@
19 SnapSeccompDir = filepath.Join(rootdir, SnappyDir, "seccomp", "profiles")
20 SnapIconsDir = filepath.Join(rootdir, SnappyDir, "icons")
21 SnapMetaDir = filepath.Join(rootdir, SnappyDir, "meta")
22+ SnapBlobDir = filepath.Join(rootdir, SnappyDir, "snaps")
23
24 SnapBinariesDir = filepath.Join(SnapAppsDir, "bin")
25 SnapServicesDir = filepath.Join(rootdir, "/etc/systemd/system")
26
27=== modified file 'pkg/clickdeb/deb.go'
28--- pkg/clickdeb/deb.go 2015-10-08 08:29:16 +0000
29+++ pkg/clickdeb/deb.go 2015-10-20 15:38:57 +0000
30@@ -40,33 +40,34 @@
31
32 var (
33 // ErrSnapInvalidContent is returned if a snap package contains
34- // invalid content
35+ // invalid content.
36 ErrSnapInvalidContent = errors.New("snap contains invalid content")
37
38- // ErrMemberNotFound is returned when a tar member is not found in the archive
39+ // ErrMemberNotFound is returned when a tar member is not found in
40+ // the archive.
41 ErrMemberNotFound = errors.New("member not found")
42 )
43
44-// ErrUnpackFailed is the error type for a snap unpack problem
45+// ErrUnpackFailed is the error type for a snap unpack problem.
46 type ErrUnpackFailed struct {
47 snapFile string
48 instDir string
49 origErr error
50 }
51
52-// ErrUnpackFailed is returned if unpacking a snap fails
53+// ErrUnpackFailed is returned if unpacking a snap fails.
54 func (e *ErrUnpackFailed) Error() string {
55 return fmt.Sprintf("unpack %s to %s failed with %s", e.snapFile, e.instDir, e.origErr)
56 }
57
58-// simple pipe based xz reader
59+// Simple pipe based xz reader.
60 func xzPipeReader(r io.Reader) io.Reader {
61 pr, pw := io.Pipe()
62 cmd := exec.Command("xz", "--decompress", "--stdout")
63 cmd.Stdin = r
64 cmd.Stdout = pw
65
66- // run xz in its own go-routine
67+ // run xz in its own go-routine.
68 go func() {
69 pw.CloseWithError(cmd.Run())
70 }()
71@@ -74,7 +75,7 @@
72 return pr
73 }
74
75-// simple pipe based xz writer
76+// Simple pipe based xz writer.
77 type xzPipeWriter struct {
78 cmd *exec.Cmd
79 w io.Writer
80@@ -93,7 +94,7 @@
81 x.cmd.Stdout = x.w
82 x.cmd.Stderr = os.Stderr
83
84- // Start is async
85+ // Start is async.
86 x.cmd.Start()
87
88 return x
89@@ -108,7 +109,7 @@
90 return x.cmd.Wait()
91 }
92
93-// ensure that the content of our data is valid:
94+// Ensure that the content of our data is valid:
95 // - no relative path allowed to prevent writing outside of the parent dir
96 func clickVerifyContentFn(path string) (string, error) {
97 path = filepath.Clean(path)
98@@ -122,7 +123,7 @@
99 }
100
101 // ClickDeb provides support for the "click" containers (a special kind of
102-// deb package)
103+// deb package).
104 type ClickDeb struct {
105 file *os.File
106 }
107@@ -145,29 +146,34 @@
108 return &ClickDeb{f}, nil
109 }
110
111-// Name returns the Name of the backing file
112+// Name returns the Name of the backing file.
113 func (d *ClickDeb) Name() string {
114 return d.file.Name()
115 }
116
117-// Close closes the backing file
118+// NeedsAutoMountUnit returns false.
119+func (d *ClickDeb) NeedsAutoMountUnit() bool {
120+ return false
121+}
122+
123+// Close closes the backing file.
124 func (d *ClickDeb) Close() error {
125 return d.file.Close()
126 }
127
128-// Verify checks that the clickdeb is signed
129+// Verify checks that the clickdeb is signed.
130 func (d *ClickDeb) Verify(allowUnauthenticated bool) error {
131 return Verify(d.Name(), allowUnauthenticated)
132 }
133
134 // ControlMember returns the content of the given control member file
135-// (e.g. the content of the "manifest" file in the control.tar.gz ar member)
136+// (e.g. the content of the "manifest" file in the control.tar.gz ar member).
137 func (d *ClickDeb) ControlMember(controlMember string) (content []byte, err error) {
138 return d.member("control.tar", controlMember)
139 }
140
141 // MetaMember returns the content of the given meta file (e.g. the content of
142-// the "package.yaml" file) from the data.tar.gz ar member's meta/ directory
143+// the "package.yaml" file) from the data.tar.gz ar member's meta/ directory.
144 func (d *ClickDeb) MetaMember(metaMember string) (content []byte, err error) {
145 return d.member("data.tar", filepath.Join("meta", metaMember))
146 }
147@@ -213,7 +219,7 @@
148 }
149
150 // ExtractHashes reads "hashes.yaml" from the clickdeb and writes it to
151-// the given directory
152+// the given directory.
153 func (d *ClickDeb) ExtractHashes(dir string) error {
154 hashesFile := filepath.Join(dir, "hashes.yaml")
155 hashesData, err := d.ControlMember("hashes.yaml")
156@@ -226,7 +232,7 @@
157
158 // Unpack unpacks the data.tar.{gz,bz2,xz} into the given target directory
159 // with click specific verification, i.e. no files will be extracted outside
160-// of the targetdir (no ".." inside the data.tar is allowed)
161+// of the targetdir (no ".." inside the data.tar is allowed).
162 func (d *ClickDeb) Unpack(targetDir string) error {
163 var err error
164
165@@ -244,7 +250,7 @@
166 return helpers.UnpackTar(dataReader, targetDir, clickVerifyContentFn)
167 }
168
169-// FIXME: this should move into the "ar" library itself
170+// FIXME: this should move into the "ar" library itself.
171 func addFileToAr(arWriter *ar.Writer, filename string) error {
172 dataF, err := os.Open(filename)
173 if err != nil {
174@@ -276,7 +282,7 @@
175 return nil
176 }
177
178-// FIXME: this should move into the "ar" library itself
179+// FIXME: this should move into the "ar" library itself.
180 func addDataToAr(arWriter *ar.Writer, filename string, data []byte) error {
181 size := int64(len(data))
182 hdr := &ar.Header{
183@@ -295,11 +301,11 @@
184 }
185
186 // tarExcludeFunc is a helper for tarCreate that is called for each file
187-// that is about to be added. If it returns "false" the file is skipped
188+// that is about to be added. If it returns "false" the file is skipped.
189 type tarExcludeFunc func(path string) bool
190
191 // tarCreate creates a tarfile for a clickdeb, all files in the archive
192-// belong to root (same as dpkg-deb)
193+// belong to root (same as dpkg-deb).
194 func tarCreate(tarname string, sourceDir string, fn tarExcludeFunc) error {
195 w, err := os.Create(tarname)
196 if err != nil {
197@@ -397,7 +403,7 @@
198 }
199
200 // Build takes a build debian directory with DEBIAN/ dir and creates a
201-// clickdeb from it
202+// clickdeb from it.
203 func (d *ClickDeb) Build(sourceDir string, dataTarFinishedCallback func(dataName string) error) error {
204 var err error
205
206@@ -496,7 +502,7 @@
207 // target dir and drop privs when doing this.
208 //
209 // To do this reliably in go we need to exec a helper as we can not
210-// just fork() and drop privs in the child (no support for stock fork in go)
211+// just fork() and drop privs in the child (no support for stock fork in go).
212 func (d *ClickDeb) UnpackWithDropPrivs(instDir, rootdir string) error {
213 // no need to drop privs, we are not root
214 if !helpers.ShouldDropPrivs() {
215
216=== added file 'pkg/file.go'
217--- pkg/file.go 1970-01-01 00:00:00 +0000
218+++ pkg/file.go 2015-10-20 15:38:57 +0000
219@@ -0,0 +1,81 @@
220+// -*- Mode: Go; indent-tabs-mode: t -*-
221+
222+/*
223+ * Copyright (C) 2015 Canonical Ltd
224+ *
225+ * This program is free software: you can redistribute it and/or modify
226+ * it under the terms of the GNU General Public License version 3 as
227+ * published by the Free Software Foundation.
228+ *
229+ * This program is distributed in the hope that it will be useful,
230+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
231+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
232+ * GNU General Public License for more details.
233+ *
234+ * You should have received a copy of the GNU General Public License
235+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
236+ *
237+ */
238+
239+package pkg
240+
241+import (
242+ "bytes"
243+ "fmt"
244+ "os"
245+ "strings"
246+
247+ "launchpad.net/snappy/pkg/clickdeb"
248+ "launchpad.net/snappy/pkg/snapfs"
249+)
250+
251+// File is the interface to interact with the low-level snap files.
252+type File interface {
253+ // Verify verfies the integrity of the file.
254+ // FIXME: use flags here instead of a boolean
255+ Verify(allowUnauthenticated bool) error
256+ // Close closes the snapfile.
257+ // FIXME: this can go away once we no longer support clickdebs.
258+ Close() error
259+ // UnpackWithDropPrivs unpacks the given the snap to the given
260+ // targetdir relative to the given rootDir.
261+ // FIXME: name leaks implementation details, should be Unpack()
262+ UnpackWithDropPrivs(targetDir, rootDir string) error
263+ // ControlMember returns the content of snap meta data files.
264+ ControlMember(name string) ([]byte, error)
265+ // MetaMember returns the content of snap meta data files.
266+ // FIXME: redundant
267+ MetaMember(name string) ([]byte, error)
268+ // ExtractHashes extracs the hashes from the snap and puts
269+ // them into the filesystem for verification.
270+ ExtractHashes(targetDir string) error
271+
272+ // NeedsAutoMountUnit determines if it's required to setup
273+ // an automount unit for the snap when the snap is activated
274+ NeedsAutoMountUnit() bool
275+}
276+
277+// Open opens a given snap file with the right backend.
278+func Open(path string) (File, error) {
279+ f, err := os.Open(path)
280+ if err != nil {
281+ return nil, fmt.Errorf("cannot open snap: %v", err)
282+ }
283+ defer f.Close()
284+
285+ // look, libmagic!
286+ header := make([]byte, 20)
287+ if _, err := f.ReadAt(header, 0); err != nil {
288+ return nil, fmt.Errorf("cannot read snap: %v", err)
289+ }
290+ // Note that we only support little endian squashfs. There
291+ // is nothing else with squashfs 4.0.
292+ if bytes.HasPrefix(header, []byte{'h', 's', 'q', 's'}) {
293+ return snapfs.New(path), nil
294+ }
295+ if strings.HasPrefix(string(header), "!<arch>\ndebian") {
296+ return clickdeb.Open(path)
297+ }
298+
299+ return nil, fmt.Errorf("cannot open snap: unknown header: %q", header)
300+}
301
302=== renamed file 'pkg/snapfs/pkg.go' => 'pkg/snapfs/snapfs.go'
303--- pkg/snapfs/pkg.go 2015-10-08 17:38:48 +0000
304+++ pkg/snapfs/snapfs.go 2015-10-20 15:38:57 +0000
305@@ -27,51 +27,82 @@
306 "path/filepath"
307 "strings"
308
309+ "launchpad.net/snappy/dirs"
310 "launchpad.net/snappy/helpers"
311 )
312
313-// Snap is the squashfs based snap
314+// BlobPath is a helper that calculates the blob path from the baseDir.
315+// FIXME: feels wrong (both location and approach). need something better.
316+func BlobPath(instDir string) string {
317+ l := strings.Split(filepath.Clean(instDir), string(filepath.Separator))
318+ if len(l) < 2 {
319+ panic(fmt.Sprintf("invalid path for BlobPath: %q", instDir))
320+ }
321+
322+ return filepath.Join(dirs.SnapBlobDir, fmt.Sprintf("%s_%s.snap", l[len(l)-2], l[len(l)-1]))
323+}
324+
325+// Snap is the squashfs based snap.
326 type Snap struct {
327 path string
328 }
329
330-// Name returns the Name of the backing file
331+// Name returns the Name of the backing file.
332 func (s *Snap) Name() string {
333 return filepath.Base(s.path)
334 }
335
336-// New returns a new Snapfs snap
337+// NeedsAutoMountUnit returns true.
338+func (s *Snap) NeedsAutoMountUnit() bool {
339+ return true
340+}
341+
342+// New returns a new Snapfs snap.
343 func New(path string) *Snap {
344 return &Snap{path: path}
345 }
346
347-// Close is not doing anything for snapfs - COMPAT
348+// Close is not doing anything for snapfs. - COMPAT
349 func (s *Snap) Close() error {
350 return nil
351 }
352
353-// ControlMember extracts from meta/ - COMPAT
354+// ControlMember extracts from meta/. - COMPAT
355 func (s *Snap) ControlMember(controlMember string) ([]byte, error) {
356 return s.ReadFile(filepath.Join("DEBIAN", controlMember))
357 }
358
359-// MetaMember extracts from meta/ - COMPAT
360+// MetaMember extracts from meta/. - COMPAT
361 func (s *Snap) MetaMember(metaMember string) ([]byte, error) {
362 return s.ReadFile(filepath.Join("meta", metaMember))
363 }
364
365-// ExtractHashes does notthing for snapfs snaps - COMAPT
366+// ExtractHashes does notthing for snapfs snaps. - COMAPT
367 func (s *Snap) ExtractHashes(dir string) error {
368 return nil
369 }
370
371-// UnpackWithDropPrivs unpacks the meta and puts stuff in place - COMAPT
372+// UnpackWithDropPrivs just copies the blob into place. - COMPAT
373 func (s *Snap) UnpackWithDropPrivs(instDir, rootdir string) error {
374- // FIXME: actually drop privs
375- return s.Unpack("*", instDir)
376+ // FIXME: we need to unpack "meta/*" here because otherwise there
377+ // is no meta/package.yaml for "snappy list -v" for
378+ // inactive versions.
379+ if err := s.UnpackMeta(instDir); err != nil {
380+ return err
381+ }
382+
383+ // ensure mount-point and blob dir.
384+ for _, dir := range []string{instDir, dirs.SnapBlobDir} {
385+ if err := os.MkdirAll(dir, 0755); err != nil {
386+ return err
387+ }
388+ }
389+
390+ // FIXME: helpers.CopyFile() has no preserve attribute flag yet
391+ return runCommand("cp", "-a", s.path, BlobPath(instDir))
392 }
393
394-// UnpackMeta unpacks just the meta/* directory of the given snap
395+// UnpackMeta unpacks just the meta/* directory of the given snap.
396 func (s *Snap) UnpackMeta(dst string) error {
397 if err := s.Unpack("meta/*", dst); err != nil {
398 return err
399@@ -89,12 +120,12 @@
400 return nil
401 }
402
403-// Unpack unpacks the src (which may be a glob into the given target dir
404+// Unpack unpacks the src (which may be a glob into the given target dir.
405 func (s *Snap) Unpack(src, dstDir string) error {
406 return runCommand("unsquashfs", "-f", "-i", "-d", dstDir, s.path, src)
407 }
408
409-// ReadFile returns the content of a single file inside a snapfs snap
410+// ReadFile returns the content of a single file inside a snapfs snap.
411 func (s *Snap) ReadFile(path string) (content []byte, err error) {
412 tmpdir, err := ioutil.TempDir("", "read-file")
413 if err != nil {
414@@ -110,21 +141,15 @@
415 return ioutil.ReadFile(filepath.Join(unpackDir, path))
416 }
417
418-// CopyBlob copies the snap to a new place
419-func (s *Snap) CopyBlob(targetFile string) error {
420- // FIXME: helpers.CopyFile() has no preserve attribute flag yet
421- return runCommand("cp", "-a", s.path, targetFile)
422-}
423-
424-// Verify verifies the snap
425+// Verify verifies the snap.
426 func (s *Snap) Verify(unauthOk bool) error {
427 // FIXME: there is no verification yet for snapfs packages, this
428 // will be done via assertions later for now we rely on
429- // the https security
430+ // the https security.
431 return nil
432 }
433
434-// Build builds the snap
435+// Build builds the snap.
436 func (s *Snap) Build(buildDir string) error {
437 fullSnapPath, err := filepath.Abs(s.path)
438 if err != nil {
439
440=== renamed file 'pkg/snapfs/pkg_test.go' => 'pkg/snapfs/snapfs_test.go'
441--- pkg/snapfs/pkg_test.go 2015-10-08 14:12:43 +0000
442+++ pkg/snapfs/snapfs_test.go 2015-10-20 15:38:57 +0000
443@@ -93,15 +93,6 @@
444 c.Assert(string(content), Equals, "name: foo")
445 }
446
447-func (s *SquashfsTestSuite) TestCopyBlob(c *C) {
448- snap := makeSnap(c, "name: foo", "")
449- dst := filepath.Join(c.MkDir(), "blob.snap")
450-
451- err := snap.CopyBlob(dst)
452- c.Assert(err, IsNil)
453- c.Assert(helpers.FilesAreEqual(snap.Name(), dst), Equals, true)
454-}
455-
456 func (s *SquashfsTestSuite) TestUnpackGlob(c *C) {
457 data := "some random data"
458 snap := makeSnap(c, "", data)
459
460=== removed file 'snappy/pkgformat.go'
461--- snappy/pkgformat.go 2015-10-08 17:42:22 +0000
462+++ snappy/pkgformat.go 1970-01-01 00:00:00 +0000
463@@ -1,64 +0,0 @@
464-// -*- Mode: Go; indent-tabs-mode: t -*-
465-
466-/*
467- * Copyright (C) 2014-2015 Canonical Ltd
468- *
469- * This program is free software: you can redistribute it and/or modify
470- * it under the terms of the GNU General Public License version 3 as
471- * published by the Free Software Foundation.
472- *
473- * This program is distributed in the hope that it will be useful,
474- * but WITHOUT ANY WARRANTY; without even the implied warranty of
475- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
476- * GNU General Public License for more details.
477- *
478- * You should have received a copy of the GNU General Public License
479- * along with this program. If not, see <http://www.gnu.org/licenses/>.
480- *
481- */
482-
483-package snappy
484-
485-import (
486- "bytes"
487- "fmt"
488- "os"
489- "strings"
490-
491- "launchpad.net/snappy/pkg/clickdeb"
492- "launchpad.net/snappy/pkg/snapfs"
493-)
494-
495-// PackageFile is the interface to interact with the low-level snap files
496-type PackageFile interface {
497- Verify(allowUnauthenticated bool) error
498- Close() error
499- UnpackWithDropPrivs(targetDir, rootDir string) error
500- ControlMember(name string) ([]byte, error)
501- MetaMember(name string) ([]byte, error)
502- ExtractHashes(targetDir string) error
503-}
504-
505-// OpenPackageFile opens a given snap file with the right backend
506-func OpenPackageFile(path string) (PackageFile, error) {
507- f, err := os.Open(path)
508- if err != nil {
509- return nil, err
510- }
511- defer f.Close()
512-
513- // look, libmagic!
514- header := make([]byte, 20)
515- if _, err := f.Read(header); err != nil {
516- return nil, err
517- }
518- // note that we only support little endian squashfs for now
519- if bytes.HasPrefix(header, []byte{'h', 's', 'q', 's'}) {
520- return snapfs.New(path), nil
521- }
522- if strings.HasPrefix(string(header), "!<arch>\ndebian") {
523- return clickdeb.Open(path)
524- }
525-
526- return nil, fmt.Errorf("unknown header %v", header)
527-}
528
529=== modified file 'snappy/snapp.go'
530--- snappy/snapp.go 2015-10-15 08:26:11 +0000
531+++ snappy/snapp.go 2015-10-20 15:38:57 +0000
532@@ -44,6 +44,7 @@
533 "launchpad.net/snappy/oauth"
534 "launchpad.net/snappy/pkg"
535 "launchpad.net/snappy/pkg/remote"
536+ "launchpad.net/snappy/pkg/snapfs"
537 "launchpad.net/snappy/policy"
538 "launchpad.net/snappy/progress"
539 "launchpad.net/snappy/release"
540@@ -181,7 +182,7 @@
541 isActive bool
542 isInstalled bool
543 description string
544- deb PackageFile
545+ deb pkg.File
546 basedir string
547 }
548
549@@ -430,7 +431,7 @@
550 // package, as deduced from the license agreement (which might involve asking
551 // the user), or an error that explains the reason why installation should not
552 // proceed.
553-func (m *packageYaml) checkLicenseAgreement(ag agreer, d PackageFile, currentActiveDir string) error {
554+func (m *packageYaml) checkLicenseAgreement(ag agreer, d pkg.File, currentActiveDir string) error {
555 if !m.ExplicitLicenseAgreement {
556 return nil
557 }
558@@ -541,7 +542,7 @@
559 // Caller should call Close on the pkg.
560 // TODO: expose that Close.
561 func NewSnapPartFromSnapFile(snapFile string, origin string, unauthOk bool) (*SnapPart, error) {
562- d, err := OpenPackageFile(snapFile)
563+ d, err := pkg.Open(snapFile)
564 if err != nil {
565 return nil, err
566 }
567@@ -620,7 +621,7 @@
568 // read hash, its ok if its not there, some older versions of
569 // snappy did not write this file
570 hashesData, err := ioutil.ReadFile(filepath.Join(part.basedir, "meta", "hashes.yaml"))
571- if err != nil {
572+ if err != nil && !os.IsNotExist(err) {
573 return nil, err
574 }
575
576@@ -999,12 +1000,26 @@
577 }
578 }
579
580+ // the hooks must run before the snap is (auto)mounted
581+ // the reason is that the click hooks use:
582+ // ".click/$name.$origin.manifest"
583+ // but because the origin is not known at build time it needs
584+ // to be generated at runtime but will be mounted over once
585+ // the snap is mounted
586 if err := installClickHooks(s.basedir, s.m, s.origin, inhibitHooks); err != nil {
587 // cleanup the failed hooks
588 removeClickHooks(s.m, s.origin, inhibitHooks)
589 return err
590 }
591
592+ // generate the automount unit for the squashfs
593+ // FIXME: this is ugly
594+ if s.deb != nil && s.deb.NeedsAutoMountUnit() {
595+ if err := s.m.addSnapfsAutomount(s.basedir, inhibitHooks, inter); err != nil {
596+ return err
597+ }
598+ }
599+
600 // generate the security policy from the package.yaml
601 if err := s.m.addSecurityPolicy(s.basedir); err != nil {
602 return err
603@@ -1018,7 +1033,6 @@
604 if err := s.m.addPackageServices(s.basedir, inhibitHooks, inter); err != nil {
605 return err
606 }
607-
608 if err := os.Remove(currentActiveSymlink); err != nil && !os.IsNotExist(err) {
609 logger.Noticef("Failed to remove %q: %v", currentActiveSymlink, err)
610 }
611@@ -1068,6 +1082,9 @@
612 if err := s.m.removeSecurityPolicy(s.basedir); err != nil {
613 return err
614 }
615+ if err := s.m.removeSnapfsAutomount(s.basedir, inter); err != nil {
616+ return err
617+ }
618
619 if s.Type() == pkg.TypeFramework {
620 if err := policy.Remove(s.Name(), s.basedir, dirs.GlobalRootDir); err != nil {
621@@ -1132,11 +1149,19 @@
622 return err
623 }
624
625+ // unmount squashfs but ignore errors as its ok if the fs is not mounted
626+ exec.Command("unmount", "--lazy", filepath.Join(s.basedir)).CombinedOutput()
627+
628 err = os.RemoveAll(s.basedir)
629 if err != nil {
630 return err
631 }
632
633+ err = os.RemoveAll(snapfs.BlobPath(s.basedir))
634+ if err != nil {
635+ return err
636+ }
637+
638 // best effort(?)
639 os.Remove(filepath.Dir(s.basedir))
640
641@@ -1996,3 +2021,46 @@
642
643 return env
644 }
645+
646+func (m *packageYaml) addSnapfsAutomount(baseDir string, inhibitHooks bool, inter interacter) error {
647+ squashfsPath := stripGlobalRootDir(snapfs.BlobPath(baseDir))
648+ whereDir := stripGlobalRootDir(baseDir)
649+
650+ sysd := systemd.New(dirs.GlobalRootDir, inter)
651+ _, err := sysd.WriteMountUnitFile(m.Name, squashfsPath, whereDir)
652+ if err != nil {
653+ return err
654+ }
655+ autoMountUnitName, err := sysd.WriteAutoMountUnitFile(m.Name, whereDir)
656+ if err != nil {
657+ return err
658+ }
659+
660+ // we always enable the mount unit even in inhibit hooks
661+ if err := sysd.Enable(autoMountUnitName); err != nil {
662+ return err
663+ }
664+
665+ if !inhibitHooks {
666+ return sysd.Start(autoMountUnitName)
667+ }
668+
669+ return nil
670+}
671+
672+func (m *packageYaml) removeSnapfsAutomount(baseDir string, inter interacter) error {
673+ sysd := systemd.New(dirs.GlobalRootDir, inter)
674+ for _, s := range []string{"mount", "automount"} {
675+ unit := systemd.MountUnitPath(stripGlobalRootDir(baseDir), s)
676+ if helpers.FileExists(unit) {
677+ // we ignore errors, nothing should stop removals
678+ _ = sysd.Disable(filepath.Base(unit))
679+ _ = sysd.Stop(filepath.Base(unit), time.Duration(1*time.Second))
680+ if err := os.Remove(unit); err != nil {
681+ return err
682+ }
683+ }
684+ }
685+
686+ return nil
687+}
688
689=== modified file 'snappy/snapp_snapfs_test.go'
690--- snappy/snapp_snapfs_test.go 2015-10-08 10:33:37 +0000
691+++ snappy/snapp_snapfs_test.go 2015-10-20 15:38:57 +0000
692@@ -20,11 +20,14 @@
693 package snappy
694
695 import (
696+ "io/ioutil"
697+ "os"
698 "path/filepath"
699
700 "launchpad.net/snappy/dirs"
701 "launchpad.net/snappy/helpers"
702 "launchpad.net/snappy/pkg/snapfs"
703+ "launchpad.net/snappy/systemd"
704
705 . "gopkg.in/check.v1"
706 )
707@@ -36,6 +39,12 @@
708 // mocks
709 aaClickHookCmd = "/bin/true"
710 dirs.SetRootDir(c.MkDir())
711+ os.MkdirAll(filepath.Join(dirs.SnapServicesDir, "multi-user.target.wants"), 0755)
712+
713+ // ensure we do not run a real systemd (slows down tests)
714+ systemd.SystemctlCmd = func(cmd ...string) ([]byte, error) {
715+ return []byte("ActiveState=inactive\n"), nil
716+ }
717
718 // ensure we use the right builder func (snapfs)
719 snapBuilderFunc = BuildSnapfsSnap
720@@ -70,15 +79,88 @@
721 _, err = part.Install(&MockProgressMeter{}, 0)
722 c.Assert(err, IsNil)
723
724- // after install its just on disk for now, note that this will
725- // change once the mounting gets added
726- base := filepath.Join(dirs.SnapAppsDir, "hello-app.origin", "1.10")
727- for _, needle := range []string{
728- "bin/foo",
729- "meta/package.yaml",
730- ".click/info/hello-app.origin.manifest",
731- } {
732- println(needle)
733- c.Assert(helpers.FileExists(filepath.Join(base, needle)), Equals, true)
734- }
735+ // after install the blob is in the right dir
736+ c.Assert(helpers.FileExists(filepath.Join(dirs.SnapBlobDir, "hello-app.origin_1.10.snap")), Equals, true)
737+
738+ // ensure the right unit is created
739+ mup := systemd.MountUnitPath("/apps/hello-app.origin/1.10", "mount")
740+ content, err := ioutil.ReadFile(mup)
741+ c.Assert(err, IsNil)
742+ c.Assert(string(content), Matches, "(?ms).*^Where=/apps/hello-app.origin/1.10")
743+ c.Assert(string(content), Matches, "(?ms).*^What=/var/lib/snappy/snaps/hello-app.origin_1.10.snap")
744+}
745+
746+func (s *SnapfsTestSuite) TestAddSnapfsAutomount(c *C) {
747+ m := packageYaml{
748+ Name: "foo.origin",
749+ Version: "1.0",
750+ Architectures: []string{"all"},
751+ }
752+ inter := &MockProgressMeter{}
753+ err := m.addSnapfsAutomount(filepath.Join(dirs.SnapAppsDir, "foo.origin/1.0"), true, inter)
754+ c.Assert(err, IsNil)
755+
756+ // ensure correct mount unit
757+ mount, err := ioutil.ReadFile(filepath.Join(dirs.SnapServicesDir, "apps-foo.origin-1.0.mount"))
758+ c.Assert(err, IsNil)
759+ c.Assert(string(mount), Equals, `[Unit]
760+Description=Snapfs mount unit for foo.origin
761+
762+[Mount]
763+What=/var/lib/snappy/snaps/foo.origin_1.0.snap
764+Where=/apps/foo.origin/1.0
765+`)
766+
767+ // and correct automount unit
768+ automount, err := ioutil.ReadFile(filepath.Join(dirs.SnapServicesDir, "apps-foo.origin-1.0.automount"))
769+ c.Assert(err, IsNil)
770+ c.Assert(string(automount), Equals, `[Unit]
771+Description=Snapfs automount unit for foo.origin
772+
773+[Automount]
774+Where=/apps/foo.origin/1.0
775+TimeoutIdleSec=30
776+
777+[Install]
778+WantedBy=multi-user.target
779+`)
780+}
781+
782+func (s *SnapfsTestSuite) TestRemoveSnapfsAutomount(c *C) {
783+ m := packageYaml{}
784+ inter := &MockProgressMeter{}
785+ err := m.addSnapfsAutomount(filepath.Join(dirs.SnapAppsDir, "foo.origin/1.0"), true, inter)
786+ c.Assert(err, IsNil)
787+
788+ // ensure we have the files
789+ for _, ext := range []string{"mount", "automount"} {
790+ p := filepath.Join(dirs.SnapServicesDir, "apps-foo.origin-1.0.") + ext
791+ c.Assert(helpers.FileExists(p), Equals, true)
792+ }
793+
794+ // now call remove and ensure they are gone
795+ err = m.removeSnapfsAutomount(filepath.Join(dirs.SnapAppsDir, "foo.origin/1.0"), inter)
796+ c.Assert(err, IsNil)
797+ for _, ext := range []string{"mount", "automount"} {
798+ p := filepath.Join(dirs.SnapServicesDir, "apps-foo.origin-1.0.") + ext
799+ c.Assert(helpers.FileExists(p), Equals, false)
800+ }
801+}
802+
803+func (s *SnapfsTestSuite) TestRemoveViaSnapfsWorks(c *C) {
804+ snapPkg := makeTestSnapPackage(c, packageHello)
805+ part, err := NewSnapPartFromSnapFile(snapPkg, "origin", true)
806+ c.Assert(err, IsNil)
807+
808+ _, err = part.Install(&MockProgressMeter{}, 0)
809+ c.Assert(err, IsNil)
810+
811+ // after install the blob is in the right dir
812+ c.Assert(helpers.FileExists(filepath.Join(dirs.SnapBlobDir, "hello-app.origin_1.10.snap")), Equals, true)
813+
814+ // now remove and ensure its gone
815+ err = part.Uninstall(&MockProgressMeter{})
816+ c.Assert(err, IsNil)
817+ c.Assert(helpers.FileExists(filepath.Join(dirs.SnapBlobDir, "hello-app.origin_1.10.snap")), Equals, false)
818+
819 }
820
821=== modified file 'systemd/systemd.go'
822--- systemd/systemd.go 2015-09-15 14:53:38 +0000
823+++ systemd/systemd.go 2015-10-20 15:38:57 +0000
824@@ -33,6 +33,7 @@
825 "text/template"
826 "time"
827
828+ "launchpad.net/snappy/dirs"
829 "launchpad.net/snappy/helpers"
830 "launchpad.net/snappy/logger"
831 )
832@@ -95,6 +96,8 @@
833 Status(service string) (string, error)
834 ServiceStatus(service string) (*ServiceStatus, error)
835 Logs(services []string) ([]Log, error)
836+ WriteMountUnitFile(name, what, where string) (string, error)
837+ WriteAutoMountUnitFile(name, where string) (string, error)
838 }
839
840 // A Log is a single entry in the systemd journal
841@@ -510,3 +513,46 @@
842 func (l Log) String() string {
843 return fmt.Sprintf("%s %s %s", l.Timestamp(), l.SID(), l.Message())
844 }
845+
846+// MountUnitPath returns the path of a {,auto}mount unit
847+func MountUnitPath(baseDir, ext string) string {
848+ // FIXME: use github.com/coreos/go-systemd/unit/escape.go
849+ // once we can update go-systemd. we can not right now
850+ // because versions >= 2 are not compatible with go1.3 from
851+ // 15.04
852+ p, err := exec.Command("systemd-escape", "--path", baseDir).CombinedOutput()
853+ if err != nil {
854+ panic(fmt.Sprintf("systemd-escape failed for %s with %q", baseDir, p))
855+ }
856+ escapedPath := strings.TrimSpace(string(p))
857+ return filepath.Join(dirs.SnapServicesDir, fmt.Sprintf("%s.%s", escapedPath, ext))
858+}
859+
860+func (s *systemd) WriteMountUnitFile(name, what, where string) (string, error) {
861+ c := fmt.Sprintf(`[Unit]
862+Description=Snapfs mount unit for %s
863+
864+[Mount]
865+What=%s
866+Where=%s
867+`, name, what, where)
868+
869+ mu := MountUnitPath(where, "mount")
870+ return filepath.Base(mu), helpers.AtomicWriteFile(mu, []byte(c), 0644)
871+}
872+
873+func (s *systemd) WriteAutoMountUnitFile(name, where string) (string, error) {
874+ c := fmt.Sprintf(`[Unit]
875+Description=Snapfs automount unit for %s
876+
877+[Automount]
878+Where=%s
879+TimeoutIdleSec=30
880+
881+[Install]
882+WantedBy=multi-user.target
883+`, name, where)
884+
885+ mu := MountUnitPath(where, "automount")
886+ return filepath.Base(mu), helpers.AtomicWriteFile(MountUnitPath(where, "automount"), []byte(c), 0644)
887+}
888
889=== modified file 'systemd/systemd_test.go'
890--- systemd/systemd_test.go 2015-09-14 12:29:15 +0000
891+++ systemd/systemd_test.go 2015-10-20 15:38:57 +0000
892@@ -21,6 +21,7 @@
893
894 import (
895 "fmt"
896+ "io/ioutil"
897 "os"
898 "path/filepath"
899 "testing"
900@@ -28,6 +29,7 @@
901
902 . "gopkg.in/check.v1"
903
904+ "launchpad.net/snappy/dirs"
905 "launchpad.net/snappy/helpers"
906 )
907
908@@ -411,3 +413,40 @@
909 }.String(), Equals, "1970-01-01T00:00:00.000042Z me hi")
910
911 }
912+
913+func (s *SystemdTestSuite) TestMountUnitPath(c *C) {
914+ c.Assert(MountUnitPath("/apps/hello.origin/1.1", "mount"), Equals, filepath.Join(dirs.SnapServicesDir, "apps-hello.origin-1.1.mount"))
915+}
916+
917+func (s *SystemdTestSuite) TestWriteMountUnit(c *C) {
918+ mountUnitName, err := New("", nil).WriteMountUnitFile("foo.origin", "/var/lib/snappy/snaps/foo.origin_1.0.snap", "/apps/foo.origin/1.0")
919+ c.Assert(err, IsNil)
920+
921+ mount, err := ioutil.ReadFile(filepath.Join(dirs.SnapServicesDir, mountUnitName))
922+ c.Assert(err, IsNil)
923+ c.Assert(string(mount), Equals, `[Unit]
924+Description=Snapfs mount unit for foo.origin
925+
926+[Mount]
927+What=/var/lib/snappy/snaps/foo.origin_1.0.snap
928+Where=/apps/foo.origin/1.0
929+`)
930+}
931+
932+func (s *SystemdTestSuite) TestWriteAutoMountUnit(c *C) {
933+ mountUnitName, err := New("", nil).WriteAutoMountUnitFile("foo.origin", "/apps/foo.origin/1.0")
934+ c.Assert(err, IsNil)
935+
936+ automount, err := ioutil.ReadFile(filepath.Join(dirs.SnapServicesDir, mountUnitName))
937+ c.Assert(err, IsNil)
938+ c.Assert(string(automount), Equals, `[Unit]
939+Description=Snapfs automount unit for foo.origin
940+
941+[Automount]
942+Where=/apps/foo.origin/1.0
943+TimeoutIdleSec=30
944+
945+[Install]
946+WantedBy=multi-user.target
947+`)
948+}

Subscribers

People subscribed via source and target branches