Merge lp:~jamesodhunt/snappy/add-lsblk-cache into lp:~snappy-dev/snappy/snappy-moved-to-github

Proposed by James Hunt
Status: Rejected
Rejected by: James Hunt
Proposed branch: lp:~jamesodhunt/snappy/add-lsblk-cache
Merge into: lp:~snappy-dev/snappy/snappy-moved-to-github
Diff against target: 465 lines (+220/-115)
8 files modified
cmd/snappy/cmd_booted.go (+2/-2)
cmd/snappy/cmd_cache_lsblk.go (+24/-0)
partition/bootloader_uboot.go (+0/-106)
partition/partition.go (+60/-6)
partition/partition_test.go (+2/-0)
partition/utils.go (+122/-0)
snappy/systemimage.go (+5/-1)
snappy/systemimage_test.go (+5/-0)
To merge this branch: bzr merge lp:~jamesodhunt/snappy/add-lsblk-cache
Reviewer Review Type Date Requested Status
James Hunt (community) Disapprove
Review via email: mp+248245@code.launchpad.net

Description of the change

* Allow 'snappy list -a' to be run as a non-root user by reading from the lsblk cache.

Note that this branch doesn't add a systemd service to generate the cache since we cannot create a working packaging branch until we've packaged up all the godeps into the archive.

Meantime, to test this:

# Should fail with an error telling you to re-run as root.
$ ./snappy list -a

# Should work
$ sudo ./snappy list -a

# Generate cache to allow non-priv user to run 'snappy list -a'
$ sudo ./snappy cache-lsblk

# Should now work
$ ./snappy list -a

To post a comment you must log in.
Revision history for this message
Michael Vogt (mvo) wrote :

Thanks for this branch!

I think this looks good but there are some details in the inline comments. I also feel that the new functions in utils.go needs tests. It does not have to be done in this MP but I think we should add them soon.

Another note as suggested by Loic it might be worthwhile to investigate if we can get the information we need (NAME, LABEL, PKNAME, MOUNTPOINT) without the need to run as root as this would avoid having to have the extra systemd unit and the extra command in snappy. This can be done in a followup MP of course.

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

I looked into the lsblk issue this morning and it seems like thats the place we can fix the underlying issue.

The problem appears to be that we do not build util-linux with libudev-dev as a build-dependency which means that the partition label is not visible for non-root-users. When libudev-dev is added it will show partition labels even for non-root users (udevadm info -x /dev/sda3 confirms that the partition label info is available via udev).

I uploaded https://launchpadlibrarian.net/196449981/util-linux_2.25.2-4ubuntu1_2.25.2-4ubuntu1%2Bppa1.diff.gz into the PPA and will push this change to vivid once I got feedback from upstream.

Revision history for this message
James Hunt (jamesodhunt) wrote :

After discussions yesterday, I'm going to remove the lsblk code, so this branch shouldn't now be merged.

review: Disapprove

Unmerged revisions

136. By James Hunt

* Update tests to take account of new lsblk cache code.

135. By James Hunt

* Allow 'snappy list -a' to be run as a non-root user by reading from
  the lsblk cache.
* Show "booted" command as an internal one in usage.
* Add "cache-lsblk" command to generate the cache.
* Move more functions into utils.go.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cmd/snappy/cmd_booted.go'
2--- cmd/snappy/cmd_booted.go 2015-01-27 20:46:45 +0000
3+++ cmd/snappy/cmd_booted.go 2015-02-02 14:07:12 +0000
4@@ -9,8 +9,8 @@
5
6 func init() {
7 Parser.AddCommand("booted",
8- "Flag that rootfs booted successfully",
9- "Not necessary to run this command manually",
10+ "Flag that rootfs booted successfully (INTERNAL)",
11+ "Do not run this command manually",
12 &cmdBooted)
13 }
14
15
16=== added file 'cmd/snappy/cmd_cache_lsblk.go'
17--- cmd/snappy/cmd_cache_lsblk.go 1970-01-01 00:00:00 +0000
18+++ cmd/snappy/cmd_cache_lsblk.go 2015-02-02 14:07:12 +0000
19@@ -0,0 +1,24 @@
20+package main
21+
22+import "launchpad.net/snappy/snappy"
23+
24+type CmdCacheLsblk struct {
25+}
26+
27+var cmdCacheLsblk CmdCacheLsblk
28+
29+func init() {
30+ Parser.AddCommand("cache-lsblk",
31+ "Cache lsblk(8) data (INTERNAL)",
32+ "Do not run this command manually",
33+ &cmdCacheLsblk)
34+}
35+
36+func (x *CmdCacheLsblk) Execute(args []string) (err error) {
37+ parts, err := snappy.InstalledSnapsByType(snappy.SnapTypeCore)
38+ if err != nil {
39+ return err
40+ }
41+
42+ return parts[0].(*snappy.SystemImagePart).GenerateLsblkCache()
43+}
44
45=== modified file 'partition/bootloader_uboot.go'
46--- partition/bootloader_uboot.go 2015-01-27 08:25:19 +0000
47+++ partition/bootloader_uboot.go 2015-02-02 14:07:12 +0000
48@@ -5,7 +5,6 @@
49 package partition
50
51 import (
52- "bufio"
53 "fmt"
54 "os"
55 "path"
56@@ -172,46 +171,6 @@
57 return u.otherRootfs
58 }
59
60-// FIXME: put into utils package
61-func readLines(path string) (lines []string, err error) {
62-
63- file, err := os.Open(path)
64-
65- if err != nil {
66- return nil, err
67- }
68-
69- defer file.Close()
70-
71- scanner := bufio.NewScanner(file)
72- for scanner.Scan() {
73- lines = append(lines, scanner.Text())
74- }
75-
76- return lines, scanner.Err()
77-}
78-
79-// FIXME: put into utils package
80-func writeLines(lines []string, path string) (err error) {
81-
82- file, err := os.Create(path)
83-
84- if err != nil {
85- return err
86- }
87-
88- defer file.Close()
89-
90- writer := bufio.NewWriter(file)
91-
92- for _, line := range lines {
93- if _, err = fmt.Fprintln(writer, line); err != nil {
94- return err
95- }
96- }
97- return writer.Flush()
98-}
99-
100 // Returns name=value entries from the specified file, removing all
101 // blank lines and comments.
102 func getNameValuePairs(file string) (vars []string, err error) {
103@@ -379,68 +338,3 @@
104 return err
105 }
106
107-// Write lines to file atomically. File does not have to preexist.
108-// FIXME: put into utils package
109-func atomicFileUpdate(file string, lines []string) (err error) {
110- tmpFile := fmt.Sprintf("%s.NEW", file)
111-
112- if err := writeLines(lines, tmpFile); err != nil {
113- return err
114- }
115-
116- // atomic update
117- if err = os.Rename(tmpFile, file); err != nil {
118- return err
119- }
120-
121- return err
122-}
123-
124-// Rewrite the specified file, applying the specified set of changes.
125-// Lines not in the changes slice are left alone.
126-// If the original file does not contain any of the name entries (from
127-// the corresponding ConfigFileChange objects), those entries are
128-// appended to the file.
129-//
130-// FIXME: put into utils package
131-func modifyNameValueFile(file string, changes []ConfigFileChange) (err error) {
132- var lines []string
133- var updated []ConfigFileChange
134-
135- if lines, err = readLines(file); err != nil {
136- return err
137- }
138-
139- var new []string
140-
141- for _, line := range lines {
142- for _, change := range changes {
143- if strings.HasPrefix(line, fmt.Sprintf("%s=", change.Name)) {
144- line = fmt.Sprintf("%s=%s", change.Name, change.Value)
145- updated = append(updated, change)
146- }
147- }
148- new = append(new, line)
149- }
150-
151- lines = new
152-
153- for _, change := range changes {
154- var got bool = false
155- for _, update := range updated {
156- if update.Name == change.Name {
157- got = true
158- break
159- }
160- }
161-
162- if got == false {
163- // name/value pair did not exist in original
164- // file, so append
165- lines = append(lines, fmt.Sprintf("%s=%s",
166- change.Name, change.Value))
167- }
168- }
169-
170- return atomicFileUpdate(file, lines)
171-}
172
173=== modified file 'partition/partition.go'
174--- partition/partition.go 2015-01-30 08:52:10 +0000
175+++ partition/partition.go 2015-02-02 14:07:12 +0000
176@@ -36,6 +36,7 @@
177 "regexp"
178 "strings"
179 "syscall"
180+ "path/filepath"
181
182 "gopkg.in/yaml.v2"
183 )
184@@ -67,9 +68,12 @@
185 // diretory.
186 const MOUNT_TARGET = "system"
187
188-// File creation mode used when any directories are created
189+// Default file creation mode used when any directories are created
190 const DIR_MODE = 0750
191
192+// Mode used for directories that must allow non-priv users access
193+const DIR_MODE_PERMISSIVE = 0755
194+
195 var (
196 BootloaderError = errors.New("Unable to determine bootloader")
197
198@@ -96,6 +100,22 @@
199 // to the disk (such as uBoot, MLO)
200 const FLASH_ASSETS_DIR = "flashtool-assets"
201
202+// Full path to file containing lsblk(8) output, generated in early
203+// boot.
204+//
205+// lsblk(8) has rather unfortunate semantics - if run as a non-root
206+// user, it "works", but returns *incomplete* information. Specfically,
207+// the partition label information is missing.
208+//
209+// Since this information is critical, and since snappy should be
210+// runable as a non-root user for query-type operations we now run
211+// lsblk(8) as root in early boot, and save the output.
212+//
213+// As the partition layout is never expected to change on an
214+// operational system, this is a reasonable compromise (and avoids
215+// set-uid nastiness).
216+const lsblkCacheFile = "/run/snappy/lsblk.txt"
217+
218 //--------------------------------------------------------------------
219 // FIXME: Globals
220
221@@ -117,6 +137,8 @@
222 type PartitionInterface interface {
223 UpdateBootloader() (err error)
224 MarkBootSuccessful() (err error)
225+ GenerateLsblkCache() (err error)
226+
227 // FIXME: could we make SyncBootloaderFiles part of UpdateBootloader
228 // to expose even less implementation details?
229 SyncBootloaderFiles() (err error)
230@@ -304,11 +326,43 @@
231 }
232
233 var runLsblk = func() (output []string, err error) {
234- return runCommandWithStdout(
235- "/bin/lsblk",
236- "--ascii",
237- "--output=NAME,LABEL,PKNAME,MOUNTPOINT",
238- "--pairs")
239+ // always run the actual command as root since:
240+ //
241+ // - Doing so means we can potentially test that the code that
242+ // runs lsblk in early boot gives the same output as the command
243+ // below (by running tests as root and non-root).
244+ //
245+ // - The system remains upgradeable should the cache file be
246+ // removed inadvertently.
247+ if os.Getuid() == 0 {
248+ return getLsblkOutput()
249+ }
250+
251+ if !fileExists(lsblkCacheFile) {
252+ panic(fmt.Sprintf("ERROR: cannot find lsblk cache %s - " +
253+ "re-run as root user",
254+ lsblkCacheFile))
255+ }
256+
257+ return readLines(lsblkCacheFile)
258+}
259+
260+func (p *Partition) GenerateLsblkCache() (err error) {
261+ var lines []string
262+
263+ // ensure the directory exists.
264+
265+ dir := filepath.Dir(lsblkCacheFile)
266+ if err = os.MkdirAll(dir, DIR_MODE_PERMISSIVE); err != nil {
267+ return err
268+ }
269+
270+ lines, err = getLsblkOutput()
271+ if err != nil {
272+ return err
273+ }
274+
275+ return writeLines(lines, lsblkCacheFile)
276 }
277
278 // Determine details of the recognised disk partitions
279
280=== modified file 'partition/partition_test.go'
281--- partition/partition_test.go 2015-01-29 14:33:54 +0000
282+++ partition/partition_test.go 2015-02-02 14:07:12 +0000
283@@ -34,6 +34,8 @@
284 }
285
286 func (s *PartitionTestSuite) TestHardwareSpec(c *C) {
287+ runLsblk = mockRunLsblkDualSnappy
288+
289 p := New()
290 c.Assert(p, NotNil)
291
292
293=== modified file 'partition/utils.go'
294--- partition/utils.go 2015-01-27 07:59:45 +0000
295+++ partition/utils.go 2015-02-02 14:07:12 +0000
296@@ -6,6 +6,7 @@
297 "os"
298 "os/exec"
299 "strings"
300+ "bufio"
301 )
302
303 // Return true if given path exists.
304@@ -75,5 +76,126 @@
305
306 output = strings.Split(string(bytes), "\n")
307
308+ // remove last element if it's empty
309+ if len(output) > 1 {
310+ last := output[len(output)-1]
311+ if last == "" {
312+ output = output[:len(output)-1]
313+ }
314+ }
315+
316 return output, err
317 }
318+
319+// Return a string slice of all lines in file specified by path
320+func readLines(path string) (lines []string, err error) {
321+
322+ file, err := os.Open(path)
323+
324+ if err != nil {
325+ return nil, err
326+ }
327+
328+ defer file.Close()
329+
330+ scanner := bufio.NewScanner(file)
331+ for scanner.Scan() {
332+ lines = append(lines, scanner.Text())
333+ }
334+
335+ return lines, scanner.Err()
336+}
337+
338+// Write lines slice to file specified by path
339+func writeLines(lines []string, path string) (err error) {
340+
341+ file, err := os.Create(path)
342+
343+ if err != nil {
344+ return err
345+ }
346+
347+ defer file.Close()
348+
349+ writer := bufio.NewWriter(file)
350+
351+ for _, line := range lines {
352+ if _, err = fmt.Fprintln(writer, line); err != nil {
353+ return err
354+ }
355+ }
356+ return writer.Flush()
357+}
358+
359+// Write lines to file atomically. File does not have to preexist.
360+func atomicFileUpdate(file string, lines []string) (err error) {
361+ tmpFile := fmt.Sprintf("%s.NEW", file)
362+
363+ if err := writeLines(lines, tmpFile); err != nil {
364+ return err
365+ }
366+
367+ // atomic update
368+ if err = os.Rename(tmpFile, file); err != nil {
369+ return err
370+ }
371+
372+ return err
373+}
374+
375+// Rewrite the specified file, applying the specified set of changes.
376+// Lines not in the changes slice are left alone.
377+// If the original file does not contain any of the name entries (from
378+// the corresponding ConfigFileChange objects), those entries are
379+// appended to the file.
380+//
381+func modifyNameValueFile(file string, changes []ConfigFileChange) (err error) {
382+ var lines []string
383+ var updated []ConfigFileChange
384+
385+ if lines, err = readLines(file); err != nil {
386+ return err
387+ }
388+
389+ var new []string
390+
391+ for _, line := range lines {
392+ for _, change := range changes {
393+ if strings.HasPrefix(line, fmt.Sprintf("%s=", change.Name)) {
394+ line = fmt.Sprintf("%s=%s", change.Name, change.Value)
395+ updated = append(updated, change)
396+ }
397+ }
398+ new = append(new, line)
399+ }
400+
401+ lines = new
402+
403+ for _, change := range changes {
404+ var got bool = false
405+ for _, update := range updated {
406+ if update.Name == change.Name {
407+ got = true
408+ break
409+ }
410+ }
411+
412+ if got == false {
413+ // name/value pair did not exist in original
414+ // file, so append
415+ lines = append(lines, fmt.Sprintf("%s=%s",
416+ change.Name, change.Value))
417+ }
418+ }
419+
420+ return atomicFileUpdate(file, lines)
421+}
422+
423+func getLsblkOutput() ([]string, error) {
424+ return runCommandWithStdout(
425+ "/bin/lsblk",
426+ "--ascii",
427+ "--output=NAME,LABEL,PKNAME,MOUNTPOINT",
428+ "--pairs")
429+}
430+
431
432=== modified file 'snappy/systemimage.go'
433--- snappy/systemimage.go 2015-01-29 15:28:06 +0000
434+++ snappy/systemimage.go 2015-02-02 14:07:12 +0000
435@@ -121,9 +121,13 @@
436 // Mark the *currently* booted rootfs as "good" (it booted :)
437 // Note: Not part of the Part interface.
438 func (s *SystemImagePart) MarkBootSuccessful() (err error) {
439-
440 return s.partition.MarkBootSuccessful()
441 }
442+
443+func (s *SystemImagePart) GenerateLsblkCache() (err error) {
444+ return s.partition.GenerateLsblkCache()
445+}
446+
447 func (s *SystemImagePart) Channel() string {
448
449 return s.channelName
450
451=== modified file 'snappy/systemimage_test.go'
452--- snappy/systemimage_test.go 2015-01-28 10:31:55 +0000
453+++ snappy/systemimage_test.go 2015-02-02 14:07:12 +0000
454@@ -293,6 +293,11 @@
455 p.markBootSuccessfulCalled = true
456 return nil
457 }
458+
459+func (p *MockPartition) GenerateLsblkCache() (err error) {
460+ return err
461+}
462+
463 func (p *MockPartition) SyncBootloaderFiles() (err error) {
464 p.syncBootloaderFilesCalled = true
465 return nil

Subscribers

People subscribed via source and target branches