Merge lp:~mvo/snappy/snappy-snapfs-mount into lp:~snappy-dev/snappy/snappy-moved-to-github
- snappy-snapfs-mount
- Merge into snappy-moved-to-github
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gustavo Niemeyer | Approve | ||
John Lenton (community) | Approve | ||
Review via email: mp+273883@code.launchpad.net |
Commit message
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/
and will mount that to:
/apps/
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/
(which complicates the removal a bit because its no longer enough to just rm -rf the /apps/$pkg/$version dir)
John Lenton (chipaca) wrote : | # |
*/var/lib/
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/
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,
- 754. By Michael Vogt
-
merged lp:snappy
- 755. By Michael Vogt
-
refactor and get rid of runPrefix
Michael Vogt (mvo) wrote : | # |
Thanks, I pondered over this too and I like the approach of /var/lib/
- 756. By Michael Vogt
-
ensure removal removes the snapfs and refactor
- 757. By Michael Vogt
-
update comments
- 758. By Michael Vogt
-
unmount after deactivate
- 759. By Michael Vogt
-
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
- 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
Michael Vogt (mvo) wrote : | # |
Thanks for the review! I addressed the comemnts, please let me know if its sufficient.
John Lenton (chipaca) : | # |
- 764. By Michael Vogt
-
pkg/snapfs/
snapfs. go: use filepath.Clean() in BlobPath
John Lenton (chipaca) wrote : | # |
This looks good enough, now :)
Gustavo Niemeyer (niemeyer) wrote : | # |
I'm adding a few comments below. Please feel free to merge this once you're personally happy about it.
John Lenton (chipaca) : | # |
- 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)
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
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 | +} |
4. put the .snap in /var/snappy/blobs?