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

Proposed by Michael Vogt on 2015-10-08
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 on 2015-10-20
John Lenton 2015-10-08 Approve on 2015-10-19
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.
John Lenton (chipaca) wrote :

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

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?

John Lenton (chipaca) :
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(?).

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.

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 on 2015-10-16
754. By Michael Vogt on 2015-10-16

merged lp:snappy

755. By Michael Vogt on 2015-10-16

refactor and get rid of runPrefix

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 on 2015-10-16
756. By Michael Vogt on 2015-10-16

ensure removal removes the snapfs and refactor

757. By Michael Vogt on 2015-10-16

update comments

758. By Michael Vogt on 2015-10-16

unmount after deactivate

Michael Vogt (mvo) wrote :

A new look would be welcome :)

lp:~mvo/snappy/snappy-snapfs-mount updated on 2015-10-16
759. By Michael Vogt on 2015-10-16

improve comments

John Lenton (chipaca) wrote :

Some comments.

Need to iterate the review a little.

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 on 2015-10-16
760. By Michael Vogt on 2015-10-16

address review comments

761. By Michael Vogt on 2015-10-16

refactor and ove MountUnit handling to systemd package

762. By Michael Vogt on 2015-10-16

add more tests

763. By Michael Vogt on 2015-10-16

add comment about little endian squashfs

Michael Vogt (mvo) wrote :

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

John Lenton (chipaca) :
lp:~mvo/snappy/snappy-snapfs-mount updated on 2015-10-16
764. By Michael Vogt on 2015-10-16

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

Michael Vogt (mvo) wrote :

Thanks! Adressing more stuff.

John Lenton (chipaca) wrote :

This looks good enough, now :)

review: Approve
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
John Lenton (chipaca) :
lp:~mvo/snappy/snappy-snapfs-mount updated on 2015-10-20
765. By Michael Vogt on 2015-10-20

merged lp:snappy

766. By Michael Vogt on 2015-10-20

use properly terminated sentences (thanks gustavo)

767. By Michael Vogt on 2015-10-20

more documenation and improve error reporting (thanks Gustavo)

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 on 2015-10-20

more documenation and improve error reporting (thanks Gustavo)

766. By Michael Vogt on 2015-10-20

use properly terminated sentences (thanks gustavo)

765. By Michael Vogt on 2015-10-20

merged lp:snappy

764. By Michael Vogt on 2015-10-16

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

763. By Michael Vogt on 2015-10-16

add comment about little endian squashfs

762. By Michael Vogt on 2015-10-16

add more tests

761. By Michael Vogt on 2015-10-16

refactor and ove MountUnit handling to systemd package

760. By Michael Vogt on 2015-10-16

address review comments

759. By Michael Vogt on 2015-10-16

improve comments

758. By Michael Vogt on 2015-10-16

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