Merge lp:~chipaca/snappy/clickety into lp:~snappy-dev/snappy/snappy-moved-to-github
- clickety
- Merge into snappy-moved-to-github
Proposed by
John Lenton
Status: | Superseded |
---|---|
Proposed branch: | lp:~chipaca/snappy/clickety |
Merge into: | lp:~snappy-dev/snappy/snappy-moved-to-github |
Diff against target: |
999 lines (+349/-374) 9 files modified
clickdeb/deb.go (+49/-0) snappy/build.go (+5/-3) snappy/click.go (+9/-292) snappy/click_test.go (+7/-36) snappy/common_test.go (+10/-3) snappy/errors.go (+0/-16) snappy/oem.go (+1/-1) snappy/snapp.go (+267/-23) snappy/snapp_test.go (+1/-0) |
To merge this branch: | bzr merge lp:~chipaca/snappy/clickety |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Snappy Developers | Pending | ||
Review via email: mp+260456@code.launchpad.net |
This proposal has been superseded by a proposal from 2015-05-28.
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
Sergio Schvezov (sergiusens) : | # |
Revision history for this message
John Lenton (chipaca) : | # |
lp:~chipaca/snappy/clickety
updated
- 477. By John Lenton
-
tweaks as per review comments
Unmerged revisions
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'clickdeb/deb.go' |
2 | --- clickdeb/deb.go 2015-05-19 14:09:19 +0000 |
3 | +++ clickdeb/deb.go 2015-05-28 12:02:18 +0000 |
4 | @@ -44,6 +44,18 @@ |
5 | ErrSnapInvalidContent = errors.New("snap contains invalid content") |
6 | ) |
7 | |
8 | +// ErrUnpackFailed is the error type for a snap unpack problem |
9 | +type ErrUnpackFailed struct { |
10 | + snapFile string |
11 | + instDir string |
12 | + origErr error |
13 | +} |
14 | + |
15 | +// ErrUnpackFailed is returned if unpacking a snap fails |
16 | +func (e *ErrUnpackFailed) Error() string { |
17 | + return fmt.Sprintf("unpack %s to %s failed with %s", e.snapFile, e.instDir, e.origErr) |
18 | +} |
19 | + |
20 | // simple pipe based xz reader |
21 | func xzPipeReader(r io.Reader) io.Reader { |
22 | pr, pw := io.Pipe() |
23 | @@ -186,6 +198,18 @@ |
24 | return content, nil |
25 | } |
26 | |
27 | +// ExtractHashes gets "hashes.yaml" from the clickdeb and writes it to |
28 | +// the given directory |
29 | +func (d *ClickDeb) ExtractHashes(dir string) error { |
30 | + hashesFile := filepath.Join(dir, "hashes.yaml") |
31 | + hashesData, err := d.ControlMember("hashes.yaml") |
32 | + if err != nil { |
33 | + return err |
34 | + } |
35 | + |
36 | + return ioutil.WriteFile(hashesFile, hashesData, 0644) |
37 | +} |
38 | + |
39 | // Unpack unpacks the data.tar.{gz,bz2,xz} into the given target directory |
40 | // with click specific verification, i.e. no files will be extracted outside |
41 | // of the targetdir (no ".." inside the data.tar is allowed) |
42 | @@ -453,3 +477,28 @@ |
43 | |
44 | return dataReader, nil |
45 | } |
46 | + |
47 | +// UnpackWithDropPrivs will unapck the ClickDeb content into the |
48 | +// target dir and drop privs when doing this. |
49 | +// |
50 | +// To do this reliably in go we need to exec a helper as we can not |
51 | +// just fork() and drop privs in the child (no support for stock fork in go) |
52 | +func (d *ClickDeb) UnpackWithDropPrivs(instDir, rootdir string) error { |
53 | + // no need to drop privs, we are not root |
54 | + if !helpers.ShouldDropPrivs() { |
55 | + return d.Unpack(instDir) |
56 | + } |
57 | + |
58 | + cmd := exec.Command("snappy", "internal-unpack", d.Name(), instDir, rootdir) |
59 | + cmd.Stdout = os.Stdout |
60 | + cmd.Stderr = os.Stderr |
61 | + if err := cmd.Run(); err != nil { |
62 | + return &ErrUnpackFailed{ |
63 | + snapFile: d.Name(), |
64 | + instDir: instDir, |
65 | + origErr: err, |
66 | + } |
67 | + } |
68 | + |
69 | + return nil |
70 | +} |
71 | |
72 | === modified file 'snappy/build.go' |
73 | --- snappy/build.go 2015-05-15 13:33:27 +0000 |
74 | +++ snappy/build.go 2015-05-28 12:02:18 +0000 |
75 | @@ -268,10 +268,12 @@ |
76 | hashes.ArchiveSha512 = sha512 |
77 | |
78 | err = filepath.Walk(buildDir, func(path string, info os.FileInfo, err error) error { |
79 | - if strings.HasPrefix(path[len(buildDir):], "/DEBIAN") { |
80 | - return nil |
81 | + // path will always start with buildDir... |
82 | + if path[len(buildDir):] == "/DEBIAN" { |
83 | + return filepath.SkipDir |
84 | } |
85 | - if path == buildDir { |
86 | + // ...so if path's length is == buildDir, it's buildDir |
87 | + if len(path) == len(buildDir) { |
88 | return nil |
89 | } |
90 | |
91 | |
92 | === modified file 'snappy/click.go' |
93 | --- snappy/click.go 2015-05-28 11:19:41 +0000 |
94 | +++ snappy/click.go 2015-05-28 12:02:18 +0000 |
95 | @@ -47,6 +47,7 @@ |
96 | "launchpad.net/snappy/logger" |
97 | "launchpad.net/snappy/pkg" |
98 | "launchpad.net/snappy/policy" |
99 | + "launchpad.net/snappy/progress" |
100 | "launchpad.net/snappy/systemd" |
101 | |
102 | "github.com/mvo5/goconfigparser" |
103 | @@ -290,16 +291,6 @@ |
104 | return nil |
105 | } |
106 | |
107 | -func writeHashesFile(d *clickdeb.ClickDeb, instDir string) error { |
108 | - hashesFile := filepath.Join(instDir, "meta", "hashes.yaml") |
109 | - hashesData, err := d.ControlMember("hashes.yaml") |
110 | - if err != nil { |
111 | - return err |
112 | - } |
113 | - |
114 | - return ioutil.WriteFile(hashesFile, hashesData, 0644) |
115 | -} |
116 | - |
117 | // generate the name |
118 | func generateBinaryName(m *packageYaml, binary Binary) string { |
119 | var binName string |
120 | @@ -724,58 +715,6 @@ |
121 | return nil |
122 | } |
123 | |
124 | -// takes a name and PATH (colon separated) and returns the full qualified path |
125 | -func findBinaryInPath(name, path string) string { |
126 | - for _, entry := range strings.Split(path, ":") { |
127 | - fname := filepath.Join(entry, name) |
128 | - if st, err := os.Stat(fname); err == nil { |
129 | - // check for any x bit |
130 | - if st.Mode()&0111 != 0 { |
131 | - return fname |
132 | - } |
133 | - } |
134 | - } |
135 | - |
136 | - return "" |
137 | -} |
138 | - |
139 | -// unpackWithDropPrivs is a helper that will unapck the ClickDeb content |
140 | -// into the target dir and drop privs when doing this. |
141 | -// |
142 | -// To do this reliably in go we need to exec a helper as we can not |
143 | -// just fork() and drop privs in the child (no support for stock fork in go) |
144 | -func unpackWithDropPrivs(d *clickdeb.ClickDeb, instDir string) error { |
145 | - // no need to drop privs, we are not root |
146 | - if !helpers.ShouldDropPrivs() { |
147 | - return d.Unpack(instDir) |
148 | - } |
149 | - |
150 | - // find priv helper executable |
151 | - privHelper := "" |
152 | - for _, path := range []string{"PATH", "GOPATH"} { |
153 | - privHelper = findBinaryInPath("snappy", os.Getenv(path)) |
154 | - if privHelper != "" { |
155 | - break |
156 | - } |
157 | - } |
158 | - if privHelper == "" { |
159 | - return ErrUnpackHelperNotFound |
160 | - } |
161 | - |
162 | - cmd := exec.Command(privHelper, "internal-unpack", d.Name(), instDir, globalRootDir) |
163 | - cmd.Stdout = os.Stdout |
164 | - cmd.Stderr = os.Stderr |
165 | - if err := cmd.Run(); err != nil { |
166 | - return &ErrUnpackFailed{ |
167 | - snapFile: d.Name(), |
168 | - instDir: instDir, |
169 | - origErr: err, |
170 | - } |
171 | - } |
172 | - |
173 | - return nil |
174 | -} |
175 | - |
176 | type agreer interface { |
177 | Agreed(intro, license string) bool |
178 | } |
179 | @@ -809,237 +748,15 @@ |
180 | return nil |
181 | } |
182 | |
183 | -func installClick(snapFile string, flags InstallFlags, inter interacter, origin string) (name string, err error) { |
184 | +func installClick(snapFile string, flags InstallFlags, inter progress.Meter, origin string) (name string, err error) { |
185 | allowUnauthenticated := (flags & AllowUnauthenticated) != 0 |
186 | - if err := auditClick(snapFile, allowUnauthenticated); err != nil { |
187 | - return "", err |
188 | - // ? |
189 | - //return SnapAuditError |
190 | - } |
191 | - |
192 | - d, err := clickdeb.Open(snapFile) |
193 | - if err != nil { |
194 | - return "", err |
195 | - } |
196 | - defer d.Close() |
197 | - |
198 | - manifestData, err := d.ControlMember("manifest") |
199 | - if err != nil { |
200 | - logger.Noticef("Snap inspect failed for %q: %v", snapFile, err) |
201 | - return "", err |
202 | - } |
203 | - |
204 | - yamlData, err := d.MetaMember("package.yaml") |
205 | - if err != nil { |
206 | - return "", err |
207 | - } |
208 | - |
209 | - m, err := parsePackageYamlData(yamlData) |
210 | - if err != nil { |
211 | - return "", err |
212 | - } |
213 | - |
214 | - if err := m.checkForPackageInstalled(origin); err != nil { |
215 | - return "", err |
216 | - } |
217 | - |
218 | - if err := m.checkForNameClashes(); err != nil { |
219 | - return "", err |
220 | - } |
221 | - |
222 | - if err := m.checkForFrameworks(); err != nil { |
223 | - return "", err |
224 | - } |
225 | - |
226 | - targetDir := snapAppsDir |
227 | - // the "oem" parts are special |
228 | - if m.Type == pkg.TypeOem { |
229 | - targetDir = snapOemDir |
230 | - |
231 | - // TODO do the following at a higher level once the store publishes snap types |
232 | - // this is horrible |
233 | - if allowOEM := (flags & AllowOEM) != 0; !allowOEM { |
234 | - if currentOEM, err := getOem(); err == nil { |
235 | - if currentOEM.Name != m.Name { |
236 | - return "", ErrOEMPackageInstall |
237 | - } |
238 | - } else { |
239 | - // there should always be an oem package now |
240 | - return "", ErrOEMPackageInstall |
241 | - } |
242 | - } |
243 | - |
244 | - if err := installOemHardwareUdevRules(m); err != nil { |
245 | - return "", err |
246 | - } |
247 | - } |
248 | - |
249 | - fullName := m.qualifiedName(origin) |
250 | - instDir := filepath.Join(targetDir, fullName, m.Version) |
251 | - currentActiveDir, _ := filepath.EvalSymlinks(filepath.Join(instDir, "..", "current")) |
252 | - |
253 | - if err := m.checkLicenseAgreement(inter, d, currentActiveDir); err != nil { |
254 | - return "", err |
255 | - } |
256 | - |
257 | - dataDir := filepath.Join(snapDataDir, fullName, m.Version) |
258 | - |
259 | - if err := os.MkdirAll(instDir, 0755); err != nil { |
260 | - logger.Noticef("Can not create %q: %v", instDir, err) |
261 | - return "", err |
262 | - } |
263 | - |
264 | - // if anything goes wrong here we cleanup |
265 | - defer func() { |
266 | - if err != nil { |
267 | - if e := os.RemoveAll(instDir); e != nil && !os.IsNotExist(e) { |
268 | - logger.Noticef("Failed to remove %q: %v", instDir, e) |
269 | - } |
270 | - } |
271 | - }() |
272 | - |
273 | - // we need to call the external helper so that we can reliable drop |
274 | - // privs |
275 | - if err := unpackWithDropPrivs(d, instDir); err != nil { |
276 | - return "", err |
277 | - } |
278 | - |
279 | - // legacy, the hooks (e.g. apparmor) need this. Once we converted |
280 | - // all hooks this can go away |
281 | - clickMetaDir := path.Join(instDir, ".click", "info") |
282 | - if err := os.MkdirAll(clickMetaDir, 0755); err != nil { |
283 | - return "", err |
284 | - } |
285 | - if err := writeCompatManifestJSON(clickMetaDir, manifestData, origin); err != nil { |
286 | - return "", err |
287 | - } |
288 | - |
289 | - // write the hashes now |
290 | - if err := writeHashesFile(d, instDir); err != nil { |
291 | - return "", err |
292 | - } |
293 | - |
294 | - inhibitHooks := (flags & InhibitHooks) != 0 |
295 | - |
296 | - // deal with the data: |
297 | - // |
298 | - // if there was a previous version, stop it |
299 | - // from being active so that it stops running and can no longer be |
300 | - // started then copy the data |
301 | - // |
302 | - // otherwise just create a empty data dir |
303 | - if currentActiveDir != "" { |
304 | - oldM, err := parsePackageYamlFile(filepath.Join(currentActiveDir, "meta", "package.yaml")) |
305 | - if err != nil { |
306 | - return "", err |
307 | - } |
308 | - |
309 | - // we need to stop making it active |
310 | - err = unsetActiveClick(currentActiveDir, inhibitHooks, inter) |
311 | - defer func() { |
312 | - if err != nil { |
313 | - if cerr := setActiveClick(currentActiveDir, inhibitHooks, inter); cerr != nil { |
314 | - logger.Noticef("Setting old version back to active failed: %v", cerr) |
315 | - } |
316 | - } |
317 | - }() |
318 | - if err != nil { |
319 | - return "", err |
320 | - } |
321 | - |
322 | - err = copySnapData(fullName, oldM.Version, m.Version) |
323 | - } else { |
324 | - err = os.MkdirAll(dataDir, 0755) |
325 | - } |
326 | - |
327 | - defer func() { |
328 | - if err != nil { |
329 | - if cerr := removeSnapData(fullName, m.Version); cerr != nil { |
330 | - logger.Noticef("When cleaning up data for %s %s: %v", m.Name, m.Version, cerr) |
331 | - } |
332 | - } |
333 | - }() |
334 | - |
335 | - if err != nil { |
336 | - return "", err |
337 | - } |
338 | - |
339 | - // and finally make active |
340 | - err = setActiveClick(instDir, inhibitHooks, inter) |
341 | - defer func() { |
342 | - if err != nil && currentActiveDir != "" { |
343 | - if cerr := setActiveClick(currentActiveDir, inhibitHooks, inter); cerr != nil { |
344 | - logger.Noticef("When setting old %s version back to active: %v", m.Name, cerr) |
345 | - } |
346 | - } |
347 | - }() |
348 | - if err != nil { |
349 | - return "", err |
350 | - } |
351 | - |
352 | - // oh, one more thing: refresh the security bits |
353 | - if !inhibitHooks { |
354 | - part, err := NewSnapPartFromYaml(filepath.Join(instDir, "meta", "package.yaml"), origin, m) |
355 | - if err != nil { |
356 | - return "", err |
357 | - } |
358 | - |
359 | - deps, err := part.Dependents() |
360 | - if err != nil { |
361 | - return "", err |
362 | - } |
363 | - |
364 | - sysd := systemd.New(globalRootDir, inter) |
365 | - stopped := make(map[string]time.Duration) |
366 | - defer func() { |
367 | - if err != nil { |
368 | - for serviceName := range stopped { |
369 | - if e := sysd.Start(serviceName); e != nil { |
370 | - inter.Notify(fmt.Sprintf("unable to restart %s with the old %s: %s", serviceName, part.Name(), e)) |
371 | - } |
372 | - } |
373 | - } |
374 | - }() |
375 | - |
376 | - for _, dep := range deps { |
377 | - if !dep.IsActive() { |
378 | - continue |
379 | - } |
380 | - for _, svc := range dep.Services() { |
381 | - serviceName := filepath.Base(generateServiceFileName(dep.m, svc)) |
382 | - timeout := time.Duration(svc.StopTimeout) |
383 | - if err = sysd.Stop(serviceName, timeout); err != nil { |
384 | - inter.Notify(fmt.Sprintf("unable to stop %s; aborting install: %s", serviceName, err)) |
385 | - return "", err |
386 | - } |
387 | - stopped[serviceName] = timeout |
388 | - } |
389 | - } |
390 | - |
391 | - if err := part.RefreshDependentsSecurity(currentActiveDir, inter); err != nil { |
392 | - return "", err |
393 | - } |
394 | - |
395 | - started := make(map[string]time.Duration) |
396 | - defer func() { |
397 | - if err != nil { |
398 | - for serviceName, timeout := range started { |
399 | - if e := sysd.Stop(serviceName, timeout); e != nil { |
400 | - inter.Notify(fmt.Sprintf("unable to stop %s with the old %s: %s", serviceName, part.Name(), e)) |
401 | - } |
402 | - } |
403 | - } |
404 | - }() |
405 | - for serviceName, timeout := range stopped { |
406 | - if err = sysd.Start(serviceName); err != nil { |
407 | - inter.Notify(fmt.Sprintf("unable to restart %s; aborting install: %s", serviceName, err)) |
408 | - return "", err |
409 | - } |
410 | - started[serviceName] = timeout |
411 | - } |
412 | - } |
413 | - |
414 | - return m.Name, nil |
415 | + part, err := NewSnapPartFromSnap(snapFile, origin, allowUnauthenticated) |
416 | + if err != nil { |
417 | + return "", err |
418 | + } |
419 | + defer part.deb.Close() |
420 | + |
421 | + return part.Install(inter, flags) |
422 | } |
423 | |
424 | // removeSnapData removes the data for the given version of the given snap |
425 | |
426 | === modified file 'snappy/click_test.go' |
427 | --- snappy/click_test.go 2015-05-28 11:17:44 +0000 |
428 | +++ snappy/click_test.go 2015-05-28 12:02:18 +0000 |
429 | @@ -246,19 +246,6 @@ |
430 | c.Assert(err, NotNil) |
431 | } |
432 | |
433 | -type agreerator struct { |
434 | - y bool |
435 | - intro string |
436 | - license string |
437 | -} |
438 | - |
439 | -func (a *agreerator) Agreed(intro, license string) bool { |
440 | - a.intro = intro |
441 | - a.license = license |
442 | - return a.y |
443 | -} |
444 | -func (a *agreerator) Notify(string) {} |
445 | - |
446 | // if the snap asks for accepting a license, and an agreer isn't provided, |
447 | // install fails |
448 | func (s *SnapTestSuite) TestLocalSnapInstallMissingAccepterFails(c *C) { |
449 | @@ -271,7 +258,7 @@ |
450 | // Agreed returns false, install fails |
451 | func (s *SnapTestSuite) TestLocalSnapInstallNegAccepterFails(c *C) { |
452 | pkg := makeTestSnapPackage(c, "explicit-license-agreement: Y") |
453 | - _, err := installClick(pkg, 0, &agreerator{y: false}, testOrigin) |
454 | + _, err := installClick(pkg, 0, &MockProgressMeter{y: false}, testOrigin) |
455 | c.Check(err, Equals, ErrLicenseNotAccepted) |
456 | } |
457 | |
458 | @@ -282,7 +269,7 @@ |
459 | defer func() { licenseChecker = checkLicenseExists }() |
460 | |
461 | pkg := makeTestSnapPackageFull(c, "explicit-license-agreement: Y", false) |
462 | - _, err := installClick(pkg, 0, &agreerator{y: true}, testOrigin) |
463 | + _, err := installClick(pkg, 0, &MockProgressMeter{y: true}, testOrigin) |
464 | c.Check(err, Equals, ErrLicenseNotProvided) |
465 | } |
466 | |
467 | @@ -290,14 +277,14 @@ |
468 | // Agreed returns true, install succeeds |
469 | func (s *SnapTestSuite) TestLocalSnapInstallPosAccepterWorks(c *C) { |
470 | pkg := makeTestSnapPackage(c, "explicit-license-agreement: Y") |
471 | - _, err := installClick(pkg, 0, &agreerator{y: true}, testOrigin) |
472 | + _, err := installClick(pkg, 0, &MockProgressMeter{y: true}, testOrigin) |
473 | c.Check(err, Equals, nil) |
474 | } |
475 | |
476 | // Agreed is given reasonable values for intro and license |
477 | func (s *SnapTestSuite) TestLocalSnapInstallAccepterReasonable(c *C) { |
478 | pkg := makeTestSnapPackage(c, "name: foobar\nexplicit-license-agreement: Y") |
479 | - ag := &agreerator{y: true} |
480 | + ag := &MockProgressMeter{y: true} |
481 | _, err := installClick(pkg, 0, ag, testOrigin) |
482 | c.Assert(err, Equals, nil) |
483 | c.Check(ag.intro, Matches, ".*foobar.*requires.*license.*") |
484 | @@ -307,7 +294,7 @@ |
485 | // If a previous version is installed with the same license version, the agreer |
486 | // isn't called |
487 | func (s *SnapTestSuite) TestPreviouslyAcceptedLicense(c *C) { |
488 | - ag := &agreerator{y: true} |
489 | + ag := &MockProgressMeter{y: true} |
490 | yaml := "name: foox\nexplicit-license-agreement: Y\nlicense-version: 2\n" |
491 | yamlFile, err := makeInstalledMockSnap(s.tempdir, yaml+"version: 1") |
492 | pkgdir := filepath.Dir(filepath.Dir(yamlFile)) |
493 | @@ -325,7 +312,7 @@ |
494 | // If a previous version is installed with the same license version, but without |
495 | // explicit license agreement set, the agreer *is* called |
496 | func (s *SnapTestSuite) TestSameLicenseVersionButNotRequired(c *C) { |
497 | - ag := &agreerator{y: true} |
498 | + ag := &MockProgressMeter{y: true} |
499 | yaml := "name: foox\nlicense-version: 2\n" |
500 | yamlFile, err := makeInstalledMockSnap(s.tempdir, yaml+"version: 1") |
501 | pkgdir := filepath.Dir(filepath.Dir(yamlFile)) |
502 | @@ -342,7 +329,7 @@ |
503 | // If a previous version is installed with a different license version, the |
504 | // agreer *is* called |
505 | func (s *SnapTestSuite) TestDifferentLicenseVersion(c *C) { |
506 | - ag := &agreerator{y: true} |
507 | + ag := &MockProgressMeter{y: true} |
508 | yaml := "name: foox\nexplicit-license-agreement: Y\n" |
509 | yamlFile, err := makeInstalledMockSnap(s.tempdir, yaml+"license-version: 2\nversion: 1") |
510 | pkgdir := filepath.Dir(filepath.Dir(yamlFile)) |
511 | @@ -1021,22 +1008,6 @@ |
512 | |
513 | } |
514 | |
515 | -func (s *SnapTestSuite) TestFindBinaryInPath(c *C) { |
516 | - fakeBinDir := c.MkDir() |
517 | - runMePath := filepath.Join(fakeBinDir, "runme") |
518 | - err := ioutil.WriteFile(runMePath, []byte(""), 0755) |
519 | - c.Assert(err, IsNil) |
520 | - |
521 | - p := filepath.Join(fakeBinDir, "not-executable") |
522 | - err = ioutil.WriteFile(p, []byte(""), 0644) |
523 | - c.Assert(err, IsNil) |
524 | - |
525 | - fakePATH := fmt.Sprintf("/some/dir:%s", fakeBinDir) |
526 | - c.Assert(findBinaryInPath("runme", fakePATH), Equals, runMePath) |
527 | - c.Assert(findBinaryInPath("no-such-binary-nowhere", fakePATH), Equals, "") |
528 | - c.Assert(findBinaryInPath("not-executable", fakePATH), Equals, "") |
529 | -} |
530 | - |
531 | func (s *SnapTestSuite) TestLocalSnapInstallRunHooks(c *C) { |
532 | // we can not strip the global rootdir for the hook tests |
533 | stripGlobalRootDir = func(s string) string { return s } |
534 | |
535 | === modified file 'snappy/common_test.go' |
536 | --- snappy/common_test.go 2015-05-20 17:24:29 +0000 |
537 | +++ snappy/common_test.go 2015-05-28 12:02:18 +0000 |
538 | @@ -64,7 +64,7 @@ |
539 | return "", err |
540 | } |
541 | |
542 | - dirName := fmt.Sprintf("%s.%s", m.Name, testOrigin) |
543 | + dirName := m.qualifiedName(testOrigin) |
544 | metaDir := filepath.Join(tempdir, "apps", dirName, m.Version, "meta") |
545 | if err := os.MkdirAll(metaDir, 0775); err != nil { |
546 | return "", err |
547 | @@ -191,7 +191,12 @@ |
548 | spin bool |
549 | spinMsg string |
550 | written int |
551 | + // Notifier: |
552 | notified []string |
553 | + // Agreer: |
554 | + intro string |
555 | + license string |
556 | + y bool |
557 | } |
558 | |
559 | func (m *MockProgressMeter) Start(pkg string, total float64) { |
560 | @@ -214,8 +219,10 @@ |
561 | func (m *MockProgressMeter) Finished() { |
562 | m.finished = true |
563 | } |
564 | -func (m *MockProgressMeter) Agreed(string, string) bool { |
565 | - return false |
566 | +func (m *MockProgressMeter) Agreed(intro, license string) bool { |
567 | + m.intro = intro |
568 | + m.license = license |
569 | + return m.y |
570 | } |
571 | func (m *MockProgressMeter) Notify(msg string) { |
572 | m.notified = append(m.notified, msg) |
573 | |
574 | === modified file 'snappy/errors.go' |
575 | --- snappy/errors.go 2015-05-20 17:24:29 +0000 |
576 | +++ snappy/errors.go 2015-05-28 12:02:18 +0000 |
577 | @@ -98,10 +98,6 @@ |
578 | // a not (yet) supported platform |
579 | ErrBuildPlatformNotSupported = errors.New("building on a not (yet) supported platform") |
580 | |
581 | - // ErrUnpackHelperNotFound is returned if the unpack helper |
582 | - // can not be found |
583 | - ErrUnpackHelperNotFound = errors.New("unpack helper not found, do you have snappy installed in your PATH or GOPATH?") |
584 | - |
585 | // ErrLicenseNotAccepted is returned when the user does not accept the |
586 | // license |
587 | ErrLicenseNotAccepted = errors.New("license not accepted") |
588 | @@ -155,18 +151,6 @@ |
589 | return fmt.Sprintf("%s failed to install: %s", e.snap, e.origErr) |
590 | } |
591 | |
592 | -// ErrUnpackFailed is the error type for a snap unpack problem |
593 | -type ErrUnpackFailed struct { |
594 | - snapFile string |
595 | - instDir string |
596 | - origErr error |
597 | -} |
598 | - |
599 | -// ErrUnpackFailed is returned if unpacking a snap fails |
600 | -func (e *ErrUnpackFailed) Error() string { |
601 | - return fmt.Sprintf("unpack %s to %s failed with %s", e.snapFile, e.instDir, e.origErr) |
602 | -} |
603 | - |
604 | // ErrHookFailed is returned if a hook command fails |
605 | type ErrHookFailed struct { |
606 | cmd string |
607 | |
608 | === modified file 'snappy/oem.go' |
609 | --- snappy/oem.go 2015-05-28 11:17:44 +0000 |
610 | +++ snappy/oem.go 2015-05-28 12:02:18 +0000 |
611 | @@ -102,7 +102,7 @@ |
612 | // logic for an oem package in every other function |
613 | var getOem = getOemImpl |
614 | |
615 | -var getOemImpl = func() (*packageYaml, error) { |
616 | +func getOemImpl() (*packageYaml, error) { |
617 | oems, _ := ActiveSnapsByType(pkg.TypeOem) |
618 | if len(oems) == 1 { |
619 | return oems[0].(*SnapPart).m, nil |
620 | |
621 | === modified file 'snappy/snapp.go' |
622 | --- snappy/snapp.go 2015-05-20 17:24:29 +0000 |
623 | +++ snappy/snapp.go 2015-05-28 12:02:18 +0000 |
624 | @@ -22,7 +22,6 @@ |
625 | import ( |
626 | "bytes" |
627 | "encoding/json" |
628 | - "errors" |
629 | "fmt" |
630 | "io" |
631 | "io/ioutil" |
632 | @@ -46,6 +45,7 @@ |
633 | "launchpad.net/snappy/policy" |
634 | "launchpad.net/snappy/progress" |
635 | "launchpad.net/snappy/release" |
636 | + "launchpad.net/snappy/systemd" |
637 | ) |
638 | |
639 | const ( |
640 | @@ -170,8 +170,8 @@ |
641 | isActive bool |
642 | isInstalled bool |
643 | description string |
644 | - |
645 | - basedir string |
646 | + deb *clickdeb.ClickDeb |
647 | + basedir string |
648 | } |
649 | |
650 | var commasplitter = regexp.MustCompile(`\s*,\s*`).Split |
651 | @@ -456,10 +456,50 @@ |
652 | if err != nil { |
653 | return nil, err |
654 | } |
655 | + part.isInstalled = true |
656 | |
657 | return part, nil |
658 | } |
659 | |
660 | +// NewSnapPartFromSnap loads a snap from the given (clickdeb) snap file. |
661 | +// Caller should call Close on the clickdeb. |
662 | +// TODO: expose that Close. |
663 | +func NewSnapPartFromSnap(snapFile string, origin string, unauthOk bool) (*SnapPart, error) { |
664 | + if err := clickdeb.Verify(snapFile, unauthOk); err != nil { |
665 | + return nil, err |
666 | + } |
667 | + |
668 | + d, err := clickdeb.Open(snapFile) |
669 | + if err != nil { |
670 | + return nil, err |
671 | + } |
672 | + |
673 | + yamlData, err := d.MetaMember("package.yaml") |
674 | + if err != nil { |
675 | + return nil, err |
676 | + } |
677 | + |
678 | + m, err := parsePackageYamlData(yamlData) |
679 | + if err != nil { |
680 | + return nil, err |
681 | + } |
682 | + |
683 | + targetDir := snapAppsDir |
684 | + // the "oem" parts are special |
685 | + if m.Type == pkg.TypeOem { |
686 | + targetDir = snapOemDir |
687 | + } |
688 | + fullName := m.qualifiedName(origin) |
689 | + instDir := filepath.Join(targetDir, fullName, m.Version) |
690 | + |
691 | + return &SnapPart{ |
692 | + basedir: instDir, |
693 | + origin: origin, |
694 | + m: m, |
695 | + deb: d, |
696 | + }, nil |
697 | +} |
698 | + |
699 | // NewSnapPartFromYaml returns a new SnapPart from the given *packageYaml at yamlPath |
700 | func NewSnapPartFromYaml(yamlPath, origin string, m *packageYaml) (*SnapPart, error) { |
701 | if _, err := os.Stat(yamlPath); err != nil { |
702 | @@ -467,10 +507,9 @@ |
703 | } |
704 | |
705 | part := &SnapPart{ |
706 | - basedir: filepath.Dir(filepath.Dir(yamlPath)), |
707 | - isInstalled: true, |
708 | - origin: origin, |
709 | - m: m, |
710 | + basedir: filepath.Dir(filepath.Dir(yamlPath)), |
711 | + origin: origin, |
712 | + m: m, |
713 | } |
714 | |
715 | // check if the part is active |
716 | @@ -610,8 +649,184 @@ |
717 | } |
718 | |
719 | // Install installs the snap |
720 | -func (s *SnapPart) Install(pb progress.Meter, flags InstallFlags) (name string, err error) { |
721 | - return "", errors.New("Install of a local part is not possible") |
722 | +func (s *SnapPart) Install(inter progress.Meter, flags InstallFlags) (name string, err error) { |
723 | + allowOEM := (flags & AllowOEM) != 0 |
724 | + inhibitHooks := (flags & InhibitHooks) != 0 |
725 | + |
726 | + if s.IsInstalled() { |
727 | + return "", ErrAlreadyInstalled |
728 | + } |
729 | + |
730 | + if err := s.CanInstall(allowOEM, inter); err != nil { |
731 | + return "", err |
732 | + } |
733 | + |
734 | + manifestData, err := s.deb.ControlMember("manifest") |
735 | + if err != nil { |
736 | + logger.Noticef("Snap inspect failed for %q: %v", s.Name(), err) |
737 | + return "", err |
738 | + } |
739 | + |
740 | + // the "oem" parts are special |
741 | + if s.Type() == pkg.TypeOem { |
742 | + if err := installOemHardwareUdevRules(s.m); err != nil { |
743 | + return "", err |
744 | + } |
745 | + } |
746 | + |
747 | + fullName := QualifiedName(s) |
748 | + currentActiveDir, _ := filepath.EvalSymlinks(filepath.Join(s.basedir, "..", "current")) |
749 | + dataDir := filepath.Join(snapDataDir, fullName, s.Version()) |
750 | + |
751 | + if err := os.MkdirAll(s.basedir, 0755); err != nil { |
752 | + logger.Noticef("Can not create %q: %v", s.basedir, err) |
753 | + return "", err |
754 | + } |
755 | + |
756 | + // if anything goes wrong here we cleanup |
757 | + defer func() { |
758 | + if err != nil { |
759 | + if e := os.RemoveAll(s.basedir); e != nil && !os.IsNotExist(e) { |
760 | + logger.Noticef("Failed to remove %q: %v", s.basedir, e) |
761 | + } |
762 | + } |
763 | + }() |
764 | + |
765 | + // we need to call the external helper so that we can reliable drop |
766 | + // privs |
767 | + if err := s.deb.UnpackWithDropPrivs(s.basedir, globalRootDir); err != nil { |
768 | + return "", err |
769 | + } |
770 | + |
771 | + // legacy, the hooks (e.g. apparmor) need this. Once we converted |
772 | + // all hooks this can go away |
773 | + clickMetaDir := filepath.Join(s.basedir, ".click", "info") |
774 | + if err := os.MkdirAll(clickMetaDir, 0755); err != nil { |
775 | + return "", err |
776 | + } |
777 | + if err := writeCompatManifestJSON(clickMetaDir, manifestData, s.origin); err != nil { |
778 | + return "", err |
779 | + } |
780 | + |
781 | + // write the hashes now |
782 | + if err := s.deb.ExtractHashes(filepath.Join(s.basedir, "meta")); err != nil { |
783 | + return "", err |
784 | + } |
785 | + |
786 | + // deal with the data: |
787 | + // |
788 | + // if there was a previous version, stop it |
789 | + // from being active so that it stops running and can no longer be |
790 | + // started then copy the data |
791 | + // |
792 | + // otherwise just create a empty data dir |
793 | + if currentActiveDir != "" { |
794 | + oldM, err := parsePackageYamlFile(filepath.Join(currentActiveDir, "meta", "package.yaml")) |
795 | + if err != nil { |
796 | + return "", err |
797 | + } |
798 | + |
799 | + // we need to stop making it active |
800 | + err = unsetActiveClick(currentActiveDir, inhibitHooks, inter) |
801 | + defer func() { |
802 | + if err != nil { |
803 | + if cerr := setActiveClick(currentActiveDir, inhibitHooks, inter); cerr != nil { |
804 | + logger.Noticef("Setting old version back to active failed: %v", cerr) |
805 | + } |
806 | + } |
807 | + }() |
808 | + if err != nil { |
809 | + return "", err |
810 | + } |
811 | + |
812 | + err = copySnapData(fullName, oldM.Version, s.Version()) |
813 | + } else { |
814 | + err = os.MkdirAll(dataDir, 0755) |
815 | + } |
816 | + |
817 | + defer func() { |
818 | + if err != nil { |
819 | + if cerr := removeSnapData(fullName, s.Version()); cerr != nil { |
820 | + logger.Noticef("When cleaning up data for %s %s: %v", s.Name(), s.Version(), cerr) |
821 | + } |
822 | + } |
823 | + }() |
824 | + |
825 | + if err != nil { |
826 | + return "", err |
827 | + } |
828 | + |
829 | + // and finally make active |
830 | + err = setActiveClick(s.basedir, inhibitHooks, inter) |
831 | + defer func() { |
832 | + if err != nil && currentActiveDir != "" { |
833 | + if cerr := setActiveClick(currentActiveDir, inhibitHooks, inter); cerr != nil { |
834 | + logger.Noticef("When setting old %s version back to active: %v", s.Name(), cerr) |
835 | + } |
836 | + } |
837 | + }() |
838 | + if err != nil { |
839 | + return "", err |
840 | + } |
841 | + |
842 | + // oh, one more thing: refresh the security bits |
843 | + if !inhibitHooks { |
844 | + deps, err := s.Dependents() |
845 | + if err != nil { |
846 | + return "", err |
847 | + } |
848 | + |
849 | + sysd := systemd.New(globalRootDir, inter) |
850 | + stopped := make(map[string]time.Duration) |
851 | + defer func() { |
852 | + if err != nil { |
853 | + for serviceName := range stopped { |
854 | + if e := sysd.Start(serviceName); e != nil { |
855 | + inter.Notify(fmt.Sprintf("unable to restart %s with the old %s: %s", serviceName, s.Name(), e)) |
856 | + } |
857 | + } |
858 | + } |
859 | + }() |
860 | + |
861 | + for _, dep := range deps { |
862 | + if !dep.IsActive() { |
863 | + continue |
864 | + } |
865 | + for _, svc := range dep.Services() { |
866 | + serviceName := filepath.Base(generateServiceFileName(dep.m, svc)) |
867 | + timeout := time.Duration(svc.StopTimeout) |
868 | + if err = sysd.Stop(serviceName, timeout); err != nil { |
869 | + inter.Notify(fmt.Sprintf("unable to stop %s; aborting install: %s", serviceName, err)) |
870 | + return "", err |
871 | + } |
872 | + stopped[serviceName] = timeout |
873 | + } |
874 | + } |
875 | + |
876 | + if err := s.RefreshDependentsSecurity(currentActiveDir, inter); err != nil { |
877 | + return "", err |
878 | + } |
879 | + |
880 | + started := make(map[string]time.Duration) |
881 | + defer func() { |
882 | + if err != nil { |
883 | + for serviceName, timeout := range started { |
884 | + if e := sysd.Stop(serviceName, timeout); e != nil { |
885 | + inter.Notify(fmt.Sprintf("unable to stop %s with the old %s: %s", serviceName, s.Name(), e)) |
886 | + } |
887 | + } |
888 | + } |
889 | + }() |
890 | + for serviceName, timeout := range stopped { |
891 | + if err = sysd.Start(serviceName); err != nil { |
892 | + inter.Notify(fmt.Sprintf("unable to restart %s; aborting install: %s", serviceName, err)) |
893 | + return "", err |
894 | + } |
895 | + started[serviceName] = timeout |
896 | + } |
897 | + } |
898 | + |
899 | + return s.Name(), nil |
900 | } |
901 | |
902 | // SetActive sets the snap active |
903 | @@ -716,6 +931,45 @@ |
904 | return needed, nil |
905 | } |
906 | |
907 | +// CanInstall checks whether the SnapPart passes a series of tests required for installation |
908 | +func (s *SnapPart) CanInstall(allowOEM bool, inter interacter) error { |
909 | + if s.IsInstalled() { |
910 | + return ErrAlreadyInstalled |
911 | + } |
912 | + |
913 | + if err := s.m.checkForPackageInstalled(s.Origin()); err != nil { |
914 | + return err |
915 | + } |
916 | + |
917 | + if err := s.m.checkForNameClashes(); err != nil { |
918 | + return err |
919 | + } |
920 | + |
921 | + if err := s.m.checkForFrameworks(); err != nil { |
922 | + return err |
923 | + } |
924 | + |
925 | + if s.Type() == pkg.TypeOem { |
926 | + if !allowOEM { |
927 | + if currentOEM, err := getOem(); err == nil { |
928 | + if currentOEM.Name != s.Name() { |
929 | + return ErrOEMPackageInstall |
930 | + } |
931 | + } else { |
932 | + // there should always be an oem package now |
933 | + return ErrOEMPackageInstall |
934 | + } |
935 | + } |
936 | + } |
937 | + |
938 | + curr, _ := filepath.EvalSymlinks(filepath.Join(s.basedir, "..", "current")) |
939 | + if err := s.m.checkLicenseAgreement(inter, s.deb, curr); err != nil { |
940 | + return err |
941 | + } |
942 | + |
943 | + return nil |
944 | +} |
945 | + |
946 | var timestampUpdater = helpers.UpdateTimestamp |
947 | |
948 | func updateAppArmorJSONTimestamp(fullName, thing, version string) error { |
949 | @@ -829,6 +1083,7 @@ |
950 | if err != nil { |
951 | return nil, err |
952 | } |
953 | + |
954 | for _, yamlfile := range matches { |
955 | |
956 | // skip "current" and similar symlinks |
957 | @@ -840,20 +1095,8 @@ |
958 | continue |
959 | } |
960 | |
961 | - m, err := parsePackageYamlFile(realpath) |
962 | - if err != nil { |
963 | - return nil, err |
964 | - } |
965 | - |
966 | - origin := "" |
967 | - if m.Type != pkg.TypeFramework && m.Type != pkg.TypeOem { |
968 | - origin, err = originFromYamlPath(realpath) |
969 | - if err != nil { |
970 | - return nil, err |
971 | - } |
972 | - } |
973 | - |
974 | - snap, err := NewSnapPartFromYaml(realpath, origin, m) |
975 | + origin, _ := originFromYamlPath(realpath) |
976 | + snap, err := NewInstalledSnapPart(realpath, origin) |
977 | if err != nil { |
978 | return nil, err |
979 | } |
980 | @@ -873,6 +1116,7 @@ |
981 | return ext[1:] |
982 | } |
983 | |
984 | +// originFromYamlPath *must* return "" if it's returning error. |
985 | func originFromYamlPath(path string) (string, error) { |
986 | origin := originFromBasedir(filepath.Join(path, "..", "..")) |
987 | |
988 | |
989 | === modified file 'snappy/snapp_test.go' |
990 | --- snappy/snapp_test.go 2015-05-20 17:24:29 +0000 |
991 | +++ snappy/snapp_test.go 2015-05-28 12:02:18 +0000 |
992 | @@ -145,6 +145,7 @@ |
993 | c.Check(snap.Version(), Equals, "1.10") |
994 | c.Check(snap.IsActive(), Equals, false) |
995 | c.Check(snap.Description(), Equals, "Hello") |
996 | + c.Check(snap.IsInstalled(), Equals, true) |
997 | |
998 | services := snap.Services() |
999 | c.Assert(services, HasLen, 1) |