Merge lp:~barry/ubiquity/bug-959724 into lp:ubiquity

Proposed by Barry Warsaw on 2012-03-20
Status: Merged
Merged at revision: 5307
Proposed branch: lp:~barry/ubiquity/bug-959724
Merge into: lp:ubiquity
Diff against target: 511 lines (+372/-23) (has conflicts)
4 files modified
debian/changelog (+9/-0)
tests/test_ubi_partman.py (+284/-3)
ubiquity/frontend/kde_components/PartMan.py (+4/-2)
ubiquity/plugins/ubi-partman.py (+75/-18)
Text conflict in debian/changelog
To merge this branch: bzr merge lp:~barry/ubiquity/bug-959724
Reviewer Review Type Date Requested Status
Colin Watson Approve on 2012-03-21
Evan (community) 2012-03-20 Approve on 2012-03-21
Review via email: mp+98515@code.launchpad.net

Description of the Change

Not yet ready

To post a comment you must log in.
Barry Warsaw (barry) wrote :

This still needs tests and KDE support, but for now it should be useful as a proof of concept.

Barry Warsaw (barry) wrote :

This branch is ready to go. It has a test of the underlying (i.e. refactored) code deciding which file system to offer to install the boot loader to. I've tested the kde and gtk ui's in a live install and they seem to work. However, if you think I should add a real ui test for gtk, just mark this branch as Needs Fixing and I'll add that.

lp:~barry/ubiquity/bug-959724 updated on 2012-03-21
5287. By Barry Warsaw on 2012-03-21

Remove an unneeded additional realpath() call.

Evan (ev) wrote :

I'm somewhat weary of this breaking subtly as grub2 changes. Could we generate the reserved sector set as part of the build, using something like:

echo 'FS_RESERVED_FIRST_SECTOR = set(['; find . -name '*.c' | xargs grep "reserved_first_sector = 1" | sed 's,.*/\(.*\).c:.*,\1,' | while read x; do echo "'$x',"; done; echo '])'

With a check that if we didn't get any input at all, break the build as the format has likely changed.

Evan (ev) wrote :

That said, if others think this is overkill, ignore me :).

d-i/make-keyboard-names does something similar, if you'd like a reference.

Evan (ev) wrote :

+1

Evan (ev) :
review: Approve
Colin Watson (cjwatson) wrote :

That would require having access to the grub2 source at build time ...

This is more a property of the filesystems than of grub2, though.

lp:~barry/ubiquity/bug-959724 updated on 2012-03-21
5288. By Barry Warsaw on 2012-03-21

Make sure the UI offers to install to all disk devices.

Barry Warsaw (barry) wrote :

On Mar 21, 2012, at 07:41 PM, Evan Dandrea wrote:

>I'm somewhat weary of this breaking subtly as grub2 changes.

That's a legitimate concern. The problem is...

>Could we generate the reserved sector set as part of the build, using
>something like:
>
>echo 'FS_RESERVED_FIRST_SECTOR = set(['; find . -name '*.c' | xargs grep "reserved_first_sector = 1" | sed 's,.*/\(.*\).c:.*,\1,' | while read x; do echo "'$x',"; done; echo '])'

You'd have to do this as part of the grub2 build and make this list available
to ubiquity somehow. See Colin's suggestion in the bug; that information
(AFAICT) is only available in the grub2 source.

Is this list expected to change often?

Barry Warsaw (barry) wrote :

On Mar 21, 2012, at 08:19 PM, Barry Warsaw wrote:

>On Mar 21, 2012, at 07:41 PM, Evan Dandrea wrote:
>
>>I'm somewhat weary of this breaking subtly as grub2 changes.
>
>That's a legitimate concern. The problem is...
>
>>Could we generate the reserved sector set as part of the build, using
>>something like:
>>
>>echo 'FS_RESERVED_FIRST_SECTOR = set(['; find . -name '*.c' | xargs grep "reserved_first_sector = 1" | sed 's,.*/\(.*\).c:.*,\1,' | while read x; do echo "'$x',"; done; echo '])'
>
>You'd have to do this as part of the grub2 build and make this list available
>to ubiquity somehow. See Colin's suggestion in the bug; that information
>(AFAICT) is only available in the grub2 source.
>
>Is this list expected to change often?

We discussed this out-of-band and decided it wasn't worth it.

lp:~barry/ubiquity/bug-959724 updated on 2012-03-21
5289. By Barry Warsaw on 2012-03-21

Add a UI test and clean up some whitespace.

Colin Watson (cjwatson) wrote :

Awesome, thank you! This looks good to me now.

review: Approve
lp:~barry/ubiquity/bug-959724 updated on 2012-03-21
5290. By Barry Warsaw on 2012-03-21

Remove obsolete comment

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'debian/changelog'
2--- debian/changelog 2012-03-21 20:39:21 +0000
3+++ debian/changelog 2012-03-21 21:26:17 +0000
4@@ -24,6 +24,7 @@
5 * Refactor ubiquity.misc.grub_default to make it testable, and add tests
6 for it.
7
8+<<<<<<< TREE
9 [ Oliver Grawert ]
10 * bin/oem-config-firstboot: do not reset debconf user data in automatic mode
11 * ubiquity/plugins/ubi-tasks.py: run tasksel with --new-install option in
12@@ -35,6 +36,14 @@
13 attached to the bug, thanks to colin for all the fixes and the hard work.
14
15 -- Stéphane Graber <stgraber@ubuntu.com> Wed, 21 Mar 2012 13:08:44 -0400
16+=======
17+ [ Barry Warsaw ]
18+ * Do not offer to install the boot loader on device paths on which
19+ incompatible file systems will be installed (e.g. XFS).
20+ (LP: #959724)
21+
22+ -- Stéphane Graber <stgraber@ubuntu.com> Wed, 21 Mar 2012 11:26:21 -0400
23+>>>>>>> MERGE-SOURCE
24
25 ubiquity (2.9.31) precise; urgency=low
26
27
28=== modified file 'tests/test_ubi_partman.py'
29--- tests/test_ubi_partman.py 2012-03-13 23:24:14 +0000
30+++ tests/test_ubi_partman.py 2012-03-21 21:26:17 +0000
31@@ -1,6 +1,7 @@
32 #!/usr/bin/python
33
34
35+from itertools import izip, izip_longest
36 import os
37 from test import test_support
38 import unittest
39@@ -100,12 +101,27 @@
40 # self.assertNotEqual(method,
41 # self.page.method_description(method))
42
43+# A couple of mock helpers for some of the tests below.
44+def _fake_grub_options(*paths):
45+ # The interface expects a sequence-of-sequences, although the method
46+ # only cares about sub-sequences of length 1, where the path is
47+ # element zero.
48+ def grub_options():
49+ return [(path,) for path in paths]
50+ return grub_options
51+
52+def _fake_grub_default(default):
53+ def grub_default():
54+ return default
55+ return grub_default
56+
57+
58 @unittest.skipUnless('DEBCONF_SYSTEMRC' in os.environ, 'Need a database.')
59 class TestPage(unittest.TestCase):
60 def setUp(self):
61 # We could mock out the db for this, but we ultimately want to make
62 # sure that the debconf questions its getting exist.
63- self.page = ubi_partman.Page(None)
64+ self.page = ubi_partman.Page(None, ui=mock.Mock())
65 self.page.db = debconf.DebconfCommunicator('ubi-test', cloexec=True)
66 self.addCleanup(self.page.db.shutdown)
67
68@@ -166,6 +182,234 @@
69 no_detected = self.page.extended_description(q)
70 self.assertEqual(no_detected, head)
71
72+ def test_maybe_update_dont_install(self):
73+ self.page.install_bootloader = False
74+ self.page.maybe_update_grub()
75+ self.assertEqual(self.page.ui.set_grub_options.call_count, 0)
76+
77+ def test_maybe_update_install(self):
78+ self.page.install_bootloader = True
79+ self.page.disk_cache = {}
80+ self.page.partition_cache = {}
81+ self.page.maybe_update_grub()
82+ self.assertEqual(self.page.ui.set_grub_options.call_count, 1)
83+
84+ @mock.patch('ubiquity.misc.grub_options', _fake_grub_options('/dev/vda1'))
85+ @mock.patch('ubiquity.misc.grub_default', _fake_grub_default('/dev/vda'))
86+ def test_install_grub_to_valid_filesystem(self):
87+ # Return some fake grub options.
88+ self.page.install_bootloader = True
89+ self.page.disk_cache = {
90+ 'ignore-1': {
91+ 'device': '/dev/vda',
92+ },
93+ }
94+ self.page.partition_cache = {
95+ 'ignore': {
96+ 'parted': {
97+ 'path': '/dev/vda1',
98+ 'fs': 'ext4',
99+ }
100+ }
101+ }
102+ self.page.maybe_update_grub()
103+ self.assertEqual(self.page.ui.set_grub_options.call_count, 1)
104+ self.page.ui.set_grub_options.assert_called_once_with('/dev/vda', {
105+ '/dev/vda': True,
106+ '/dev/vda1': True,
107+ })
108+
109+ @mock.patch('ubiquity.misc.grub_options', _fake_grub_options('/dev/vda1'))
110+ @mock.patch('ubiquity.misc.grub_default', _fake_grub_default('/dev/vda'))
111+ def test_install_grub_to_invalid_filesystem(self):
112+ # Return some fake grub options.
113+ self.page.install_bootloader = True
114+ self.page.disk_cache = {
115+ 'ignore-1': {
116+ 'device': '/dev/vda',
117+ },
118+ }
119+ self.page.partition_cache = {
120+ 'ignore': {
121+ 'parted': {
122+ 'path': '/dev/vda1',
123+ 'fs': 'xfs',
124+ }
125+ }
126+ }
127+ self.page.maybe_update_grub()
128+ self.assertEqual(self.page.ui.set_grub_options.call_count, 1)
129+ self.page.ui.set_grub_options.assert_called_once_with('/dev/vda', {
130+ '/dev/vda': True,
131+ '/dev/vda1': False,
132+ })
133+
134+ @mock.patch('ubiquity.misc.grub_options',
135+ _fake_grub_options('/dev/vda1', '/dev/vda2'))
136+ @mock.patch('ubiquity.misc.grub_default', _fake_grub_default('/dev/vda'))
137+ def test_install_grub_to_mixed_filesystems(self):
138+ # Return some fake grub options.
139+ self.page.install_bootloader = True
140+ self.page.disk_cache = {
141+ 'ignore-1': {
142+ 'device': '/dev/vda',
143+ },
144+ }
145+ self.page.partition_cache = {
146+ 'ignore-1': {
147+ 'parted': {
148+ 'path': '/dev/vda1',
149+ 'fs': 'xfs',
150+ }
151+ },
152+ 'ignore-2': {
153+ 'parted': {
154+ 'path': '/dev/vda2',
155+ 'fs': 'ext2',
156+ }
157+ }
158+ }
159+ self.page.maybe_update_grub()
160+ self.assertEqual(self.page.ui.set_grub_options.call_count, 1)
161+ self.page.ui.set_grub_options.assert_called_once_with('/dev/vda', {
162+ '/dev/vda': True,
163+ '/dev/vda1': False,
164+ '/dev/vda2': True,
165+ })
166+
167+ @mock.patch('ubiquity.misc.grub_options',
168+ _fake_grub_options('/dev/vda1', '/dev/vda2', '/dev/vdb1'))
169+ @mock.patch('ubiquity.misc.grub_default', _fake_grub_default('/dev/vda'))
170+ def test_install_grub_offers_to_install_to_disk(self):
171+ # Return some fake grub options.
172+ self.page.install_bootloader = True
173+ self.page.disk_cache = {
174+ 'ignore-1': {
175+ 'device': '/dev/vda',
176+ },
177+ 'ignore-2': {
178+ 'device': '/dev/vdb',
179+ },
180+ }
181+ self.page.partition_cache = {
182+ 'ignore-1': {
183+ 'parted': {
184+ 'path': '/dev/vda1',
185+ 'fs': 'xfs',
186+ },
187+ },
188+ 'ignore-2': {
189+ 'parted': {
190+ 'path': '/dev/vda2',
191+ 'fs': 'ext2',
192+ },
193+ },
194+ 'ignore-3': {
195+ 'parted': {
196+ 'path': '/dev/vdb1',
197+ 'fs': 'xfs',
198+ },
199+ },
200+ }
201+ self.page.maybe_update_grub()
202+ self.assertEqual(self.page.ui.set_grub_options.call_count, 1)
203+ self.page.ui.set_grub_options.assert_called_once_with('/dev/vda', {
204+ '/dev/vda': True,
205+ '/dev/vdb': True,
206+ '/dev/vda1': False,
207+ '/dev/vda2': True,
208+ '/dev/vdb1': False,
209+ })
210+
211+ @mock.patch('ubiquity.misc.grub_options',
212+ _fake_grub_options('/dev/vda1', '/dev/vda2', '/dev/vdb1'))
213+ @mock.patch('ubiquity.misc.grub_default', _fake_grub_default('/dev/vda'))
214+ def test_install_grub_offers_to_install_to_all_but_jfs(self):
215+ # Return some fake grub options.
216+ self.page.install_bootloader = True
217+ self.page.disk_cache = {
218+ 'ignore-1': {
219+ 'device': '/dev/vda',
220+ },
221+ 'ignore-2': {
222+ 'device': '/dev/vdb',
223+ },
224+ }
225+ self.page.partition_cache = {
226+ 'ignore-1': {
227+ 'parted': {
228+ 'path': '/dev/vda1',
229+ 'fs': 'ext4',
230+ },
231+ },
232+ 'ignore-2': {
233+ 'parted': {
234+ 'path': '/dev/vda2',
235+ 'fs': 'ext2',
236+ },
237+ },
238+ 'ignore-3': {
239+ 'parted': {
240+ 'path': '/dev/vdb1',
241+ 'fs': 'jfs',
242+ },
243+ },
244+ }
245+ self.page.maybe_update_grub()
246+ self.assertEqual(self.page.ui.set_grub_options.call_count, 1)
247+ self.page.ui.set_grub_options.assert_called_once_with('/dev/vda', {
248+ '/dev/vda': True,
249+ '/dev/vdb': True,
250+ '/dev/vda1': True,
251+ '/dev/vda2': True,
252+ '/dev/vdb1': False,
253+ })
254+
255+ @mock.patch('ubiquity.misc.grub_options',
256+ _fake_grub_options('/dev/vda1', '/dev/vda2', '/dev/vdb1'))
257+ @mock.patch('ubiquity.misc.grub_default', _fake_grub_default('/dev/vda'))
258+ def test_install_grub_offers_to_install_to_all(self):
259+ # Return some fake grub options.
260+ self.page.install_bootloader = True
261+ self.page.disk_cache = {
262+ 'ignore-1': {
263+ 'device': '/dev/vda',
264+ },
265+ 'ignore-2': {
266+ 'device': '/dev/vdb',
267+ },
268+ }
269+ self.page.partition_cache = {
270+ 'ignore-1': {
271+ 'parted': {
272+ 'path': '/dev/vda1',
273+ 'fs': 'ext4',
274+ },
275+ },
276+ 'ignore-2': {
277+ 'parted': {
278+ 'path': '/dev/vda2',
279+ 'fs': 'ext2',
280+ },
281+ },
282+ 'ignore-3': {
283+ 'parted': {
284+ 'path': '/dev/vdb1',
285+ 'fs': 'fat16',
286+ },
287+ },
288+ }
289+ self.page.maybe_update_grub()
290+ self.assertEqual(self.page.ui.set_grub_options.call_count, 1)
291+ self.page.ui.set_grub_options.assert_called_once_with('/dev/vda', {
292+ '/dev/vda': True,
293+ '/dev/vdb': True,
294+ '/dev/vda1': True,
295+ '/dev/vda2': True,
296+ '/dev/vdb1': True,
297+ })
298+
299+
300 @unittest.skipUnless('DEBCONF_SYSTEMRC' in os.environ, 'Need a database.')
301 class TestCalculateAutopartitioningOptions(unittest.TestCase):
302 '''Test that the each expected autopartitioning option exists and is
303@@ -413,10 +657,19 @@
304 self.assertIn('manual', options)
305 self.assertItemsEqual(self.manual, options['manual'])
306
307+
308+def _fake_grub_options_pairs(paths, descriptions):
309+ # The interface expects a sequence-of-sequences, although the method
310+ # only cares about sub-sequences of length 1, where the path is
311+ # element zero.
312+ def grub_options():
313+ return [(path, description)
314+ for path, description
315+ in izip_longest(paths, descriptions, fillvalue='')]
316+ return grub_options
317+
318 class TestPageGtk(unittest.TestCase):
319 def setUp(self):
320- # FIXME Not sure why this is needed.
321- from ubiquity import gtkwidgets
322 controller = mock.Mock()
323 self.gtk = ubi_partman.PageGtk(controller)
324
325@@ -426,5 +679,33 @@
326 gtkwidgets.refresh()
327 self.gtk.controller.go_forward.assert_called_once_with()
328
329+ @mock.patch('ubiquity.misc.grub_options',
330+ _fake_grub_options_pairs(
331+ ('/dev/vda', '/dev/vdb',
332+ '/dev/vda1', '/dev/vda2', '/dev/vdb1'),
333+ ('Virtio Block Device (108 GB)',
334+ 'Virtio Block Device (801 GB)')))
335+ def test_boot_loader_installation_combobox(self):
336+ self.gtk.set_grub_options('/dev/vda', {
337+ '/dev/vda': True,
338+ '/dev/vda1': True,
339+ '/dev/vda2': False,
340+ '/dev/vdb': True,
341+ '/dev/vdb1': True,
342+ })
343+ # The combo box should have everything but vda2.
344+ expected = [
345+ '/dev/vda Virtio Block Device (108 GB)',
346+ '/dev/vdb Virtio Block Device (801 GB)',
347+ '/dev/vda1 ',
348+ '/dev/vdb1 ',
349+ ]
350+ row_text = []
351+ for row in self.gtk.grub_device_entry.get_model():
352+ row_text.append(' '.join(row))
353+ for want, got in izip(expected, row_text):
354+ self.assertEqual(want, got)
355+
356+
357 if __name__ == '__main__':
358 test_support.run_unittest(TestCalculateAutopartitioningOptions, TestPage, PartmanPageDirectoryTests)
359
360=== modified file 'ubiquity/frontend/kde_components/PartMan.py'
361--- ubiquity/frontend/kde_components/PartMan.py 2012-02-04 02:53:32 +0000
362+++ ubiquity/frontend/kde_components/PartMan.py 2012-03-21 21:26:17 +0000
363@@ -444,11 +444,13 @@
364 self.ctrlr.allow_change_step(False)
365 self.ctrlr.dbfilter.undo()
366
367- def setGrubOptions(self, options, default):
368+ def setGrubOptions(self, options, default, grub_installable):
369 self.part_advanced_bootloader_frame.setVisible(True)
370 self.grub_device_entry.clear()
371 for opt in options:
372- self.grub_device_entry.addItem(opt[0]);
373+ path = opt[0]
374+ if grub_installable.get(path, False):
375+ self.grub_device_entry.addItem(path)
376
377 index = self.grub_device_entry.findText(default)
378 if (index == -1):
379
380=== modified file 'ubiquity/plugins/ubi-partman.py'
381--- ubiquity/plugins/ubi-partman.py 2012-03-21 15:33:56 +0000
382+++ ubiquity/plugins/ubi-partman.py 2012-03-21 21:26:17 +0000
383@@ -40,6 +40,28 @@
384 PartitioningOption = namedtuple('PartitioningOption', ['title', 'desc'])
385 Partition = namedtuple('Partition', ['device', 'size', 'id', 'filesystem'])
386
387+# List of file system types that reserve extra space for grub. Found by
388+# grepping for "reserved_first_sector = 1" in the grub2 source as per
389+# https://bugs.launchpad.net/ubuntu/+source/ubiquity/+bug/959724
390+# Only those file systems that set this to 1 can have the boot loader
391+# installed on them. This is that list, with values taken from the .name
392+# entry in the matching structs.
393+FS_RESERVED_FIRST_SECTOR = set([
394+ 'btrfs',
395+ 'ext2',
396+ 'fat',
397+ 'hfsplus',
398+ 'nilfs2',
399+ 'ntfs',
400+ # Add a few ext variants that aren't explicitly described in grub2.
401+ 'ext3',
402+ 'ext4',
403+ # Add a few fat variants.
404+ 'fat16',
405+ 'fat32',
406+ # Others?
407+ ])
408+
409
410 class PageBase(plugin.PluginUI):
411 def __init__(self, *args, **kwargs):
412@@ -465,20 +487,20 @@
413 self.controller.go_forward()
414 return True
415
416- def set_grub_options(self, default):
417+ def set_grub_options(self, default, grub_installable):
418 from gi.repository import Gtk, GObject
419 self.bootloader_vbox.show()
420 options = misc.grub_options()
421- if default.startswith('/'):
422- default = os.path.realpath(default)
423 l = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_STRING)
424 self.grub_device_entry.set_model(l)
425 selected = False
426 for opt in options:
427- i = l.append(opt)
428- if opt[0] == default:
429- self.grub_device_entry.set_active_iter(i)
430- selected = True
431+ path = opt[0]
432+ if grub_installable.get(path, False):
433+ i = l.append(opt)
434+ if path == default:
435+ self.grub_device_entry.set_active_iter(i)
436+ selected = True
437 if not selected:
438 i = l.append([default, ''])
439 self.grub_device_entry.set_active_iter(i)
440@@ -1290,11 +1312,9 @@
441 self.disk_layout = layout
442 self.partAuto.setDiskLayout(layout)
443
444- def set_grub_options(self, default):
445+ def set_grub_options(self, default, grub_installable):
446 options = misc.grub_options()
447- if default.startswith('/'):
448- default = os.path.realpath(default)
449- self.partMan.setGrubOptions(options, default)
450+ self.partMan.setGrubOptions(options, default, grub_installable)
451
452 def get_grub_choice(self):
453 choice = self.partMan.getGrubChoice()
454@@ -2907,13 +2927,50 @@
455 self.local_progress = False
456
457 def maybe_update_grub(self):
458- if self.install_bootloader:
459- grub_bootdev=self.db.get("grub-installer/bootdev")
460- if grub_bootdev and grub_bootdev in (part[0] for part in misc.grub_options()):
461- default = grub_bootdev
462- else:
463- default = misc.grub_default()
464- self.ui.set_grub_options(default)
465+ if not self.install_bootloader:
466+ return
467+ paths = [part[0] for part in misc.grub_options()]
468+ # Get the default boot device.
469+ grub_bootdev = self.db.get("grub-installer/bootdev")
470+ if grub_bootdev and grub_bootdev in paths:
471+ default = grub_bootdev
472+ else:
473+ default = misc.grub_default()
474+ # Create a reverse mapping from grub options device path to the file
475+ # system type being installed on that option. Then later we'll make
476+ # sure that grub can actually be installed on that path's file system.
477+ fstype_by_path = {}
478+ for key, value in self.partition_cache.items():
479+ path = value.get('parted', {}).get('path')
480+ fstype = value.get('parted', {}).get('fs', 'free')
481+ if path is not None:
482+ # Duplicate paths should not be possible, but if we find one,
483+ # the first one (<wink>, i.e. random) wins.
484+ if path in fstype_by_path:
485+ self.debug('already found path %s with type %s',
486+ path, fstype_by_path[path])
487+ else:
488+ fstype_by_path[path] = fstype
489+ if default.startswith('/'):
490+ default = os.path.realpath(default)
491+ # Now, create a dictionary mapping boot device path to a flag
492+ # indicating whether grub can or cannot be installed on that path's
493+ # file system. Use 'free' as a default since grub can never be
494+ # installed on free space.
495+ grub_installable = {}
496+ for path in paths:
497+ fstype = fstype_by_path.get(path, 'free')
498+ can_install = fstype in FS_RESERVED_FIRST_SECTOR
499+ grub_installable[path] = can_install
500+ self.debug('device path: %s, fstype: %s, grub installable? %s',
501+ path, fstype, 'yes' if can_install else 'no')
502+ # Let grub offer to install to all the disk devices.
503+ for key, value in self.disk_cache.items():
504+ device = value.get('device')
505+ if device is not None:
506+ grub_installable[device] = True
507+ self.debug('device path: %s grub installable? yes', device)
508+ self.ui.set_grub_options(default, grub_installable)
509
510 # Notes:
511 #

Subscribers

People subscribed via source and target branches

to status/vote changes: