/
custom.py
2702 lines (2240 loc) · 115 KB
/
custom.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#
# Custom partitioning classes.
#
# Copyright (C) 2012-2014 Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions of
# the GNU General Public License v.2, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY expressed or implied, including the implied warranties of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details. You should have received a copy of the
# GNU General Public License along with this program; if not, write to the
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the
# source code or documentation are not subject to the GNU General Public
# License and may only be used or replicated with the express permission of
# Red Hat, Inc.
#
# Red Hat Author(s): Chris Lumens <clumens@redhat.com>
# David Lehman <dlehman@redhat.com>
#
# TODO:
# - Deleting an LV is not reflected in available space in the bottom left.
# - this is only true for preexisting LVs
# - Device descriptions, suggested sizes, etc. should be moved out into a support file.
# - Tabbing behavior in the accordion is weird.
# - Implement striping and mirroring for LVM.
# - Activating reformat should always enable resize for existing devices.
from pykickstart.constants import CLEARPART_TYPE_NONE
from pyanaconda.i18n import _, N_, CP_
from pyanaconda.product import productName, productVersion, translated_new_install_name
from pyanaconda.threads import AnacondaThread, threadMgr
from pyanaconda.constants import THREAD_EXECUTE_STORAGE, THREAD_STORAGE, THREAD_CUSTOM_STORAGE_INIT
from pyanaconda.iutil import lowerASCII, firstNotNone
from pyanaconda.bootloader import BootLoaderError
from pyanaconda.kickstart import refreshAutoSwapSize
from pyanaconda import network
from blivet import devicefactory
from blivet.formats import device_formats
from blivet.formats import getFormat
from blivet.formats.fs import FS
from blivet.size import Size
from blivet import Root
from blivet.devicefactory import DEVICE_TYPE_LVM
from blivet.devicefactory import DEVICE_TYPE_BTRFS
from blivet.devicefactory import DEVICE_TYPE_PARTITION
from blivet.devicefactory import DEVICE_TYPE_MD
from blivet.devicefactory import DEVICE_TYPE_DISK
from blivet.devicefactory import DEVICE_TYPE_LVM_THINP
from blivet.devicefactory import SIZE_POLICY_AUTO
from blivet import findExistingInstallations
from blivet.partitioning import doAutoPartition
from blivet.errors import StorageError
from blivet.errors import NoDisksError
from blivet.errors import NotEnoughFreeSpaceError
from blivet.devicelibs import raid, crypto
from blivet.devices import LUKSDevice
from blivet.devices.md import MDRaidArrayDevice
from pyanaconda.storage_utils import ui_storage_logger, device_type_from_autopart, storage_checker, \
verify_luks_devices_have_key
from pyanaconda.storage_utils import DEVICE_TEXT_PARTITION, DEVICE_TEXT_MAP, DEVICE_TEXT_MD
from pyanaconda.storage_utils import PARTITION_ONLY_FORMAT_TYPES, MOUNTPOINT_DESCRIPTIONS
from pyanaconda.storage_utils import NAMED_DEVICE_TYPES, CONTAINER_DEVICE_TYPES
from pyanaconda.storage_utils import try_populate_devicetree
from pyanaconda.storage_utils import filter_unsupported_disklabel_devices
from pyanaconda.ui.communication import hubQ
from pyanaconda.ui.gui.spokes import NormalSpoke
from pyanaconda.ui.helpers import StorageCheckHandler
from pyanaconda.ui.lib.disks import getDiskDescription
from pyanaconda.ui.gui.spokes.lib.cart import SelectedDisksDialog
from pyanaconda.ui.gui.spokes.lib.passphrase import PassphraseDialog
from pyanaconda.ui.gui.spokes.lib.accordion import updateSelectorFromDevice, Accordion, Page, CreateNewPage, UnknownPage
from pyanaconda.ui.gui.spokes.lib.refresh import RefreshDialog
from pyanaconda.ui.gui.spokes.lib.summary import ActionSummaryDialog
from pyanaconda.ui.gui.spokes.lib.custom_storage_helpers import size_from_entry
from pyanaconda.ui.gui.spokes.lib.custom_storage_helpers import validate_label, validate_mountpoint, get_raid_level
from pyanaconda.ui.gui.spokes.lib.custom_storage_helpers import selectedRaidLevel, raidLevelSelection, defaultRaidLevel, requiresRaidSelection, containerRaidLevelsSupported, raidLevelsSupported, defaultContainerRaidLevel
from pyanaconda.ui.gui.spokes.lib.custom_storage_helpers import get_container_type_name, RAID_NOT_ENOUGH_DISKS
from pyanaconda.ui.gui.spokes.lib.custom_storage_helpers import AddDialog, ConfirmDeleteDialog, DisksDialog, ContainerDialog
from pyanaconda.ui.gui.utils import setViewportBackground, fancy_set_sensitive, ignoreEscape
from pyanaconda.ui.gui.utils import really_hide, really_show, GtkActionList, timed_action
from pyanaconda.ui.categories.system import SystemCategory
import gi
gi.require_version("Gtk", "3.0")
gi.require_version("Gdk", "3.0")
gi.require_version("AnacondaWidgets", "3.0")
from gi.repository import Gdk, Gtk
from gi.repository.AnacondaWidgets import MountpointSelector
from functools import wraps
import logging
log = logging.getLogger("anaconda")
__all__ = ["CustomPartitioningSpoke"]
NOTEBOOK_LABEL_PAGE = 0
NOTEBOOK_DETAILS_PAGE = 1
NOTEBOOK_LUKS_PAGE = 2
NOTEBOOK_UNEDITABLE_PAGE = 3
NOTEBOOK_INCOMPLETE_PAGE = 4
NEW_CONTAINER_TEXT = N_("Create a new %(container_type)s ...")
CONTAINER_TOOLTIP = N_("Create or select %(container_type)s")
DEVICE_CONFIGURATION_ERROR_MSG = N_("Device reconfiguration failed. <a href=\"\">Click for "
"details.</a>")
UNRECOVERABLE_ERROR_MSG = N_("Storage configuration reset due to unrecoverable "
"error. <a href=\"\">Click for details.</a>")
def dev_type_from_const(dev_type_const):
return getattr(devicefactory, dev_type_const, None)
def ui_storage_logged(func):
@wraps(func)
def decorated(*args, **kwargs):
with ui_storage_logger():
return func(*args, **kwargs)
return decorated
class CustomPartitioningSpoke(NormalSpoke, StorageCheckHandler):
builderObjects = ["customStorageWindow", "containerStore", "deviceTypeStore",
"partitionStore", "raidStoreFiltered", "raidLevelStore",
"addImage", "removeImage", "settingsImage",
"mountPointCompletion", "mountPointStore", "fileSystemStore"]
mainWidgetName = "customStorageWindow"
uiFile = "spokes/custom.glade"
helpFile = "CustomSpoke.xml"
category = SystemCategory
title = N_("MANUAL PARTITIONING")
def __init__(self, data, storage, payload, instclass):
StorageCheckHandler.__init__(self)
NormalSpoke.__init__(self, data, storage, payload, instclass)
self._back_already_clicked = False
self._storage_playground = None
self.passphrase = ""
self._current_selector = None
self._devices = []
self._error = None
self._hidden_disks = []
self._fs_types = [] # list of supported fstypes
self._free_space = Size(0)
self._default_autopart_type = None
self._device_size_text = None
self._device_disks = []
self._device_container_name = None
self._device_container_raid_level = None
self._device_container_encrypted = False
self._device_container_size = SIZE_POLICY_AUTO
self._device_name_dict = {DEVICE_TYPE_LVM: None,
DEVICE_TYPE_MD: None,
DEVICE_TYPE_LVM_THINP: None,
DEVICE_TYPE_PARTITION: "",
DEVICE_TYPE_BTRFS: "",
DEVICE_TYPE_DISK: ""}
self._initialized = False
def apply(self):
self.clear_errors()
self._unhide_unusable_disks()
new_swaps = (dev for dev in self.get_new_devices() if dev.format.type == "swap")
self.storage.setFstabSwaps(new_swaps)
# update the global passphrase
self.data.autopart.passphrase = self.passphrase
# make sure any device/passphrase pairs we've obtained are remembered
for device in self.storage.devices:
if device.format.type == "luks" and not device.format.exists:
if not device.format.hasKey:
device.format.passphrase = self.passphrase
self.storage.savePassphrase(device)
hubQ.send_ready("StorageSpoke", True)
@property
def indirect(self):
return True
# This spoke has no status since it's not in a hub
@property
def status(self):
return None
def _grabObjects(self):
self._configureBox = self.builder.get_object("configureBox")
self._partitionsViewport = self.builder.get_object("partitionsViewport")
self._partitionsNotebook = self.builder.get_object("partitionsNotebook")
# Connect partitionsNotebook focus events to scrolling in the parent viewport
partitionsNotebookViewport = self.builder.get_object("partitionsNotebookViewport")
self._partitionsNotebook.set_focus_vadjustment(partitionsNotebookViewport.get_vadjustment())
self._whenCreateLabel = self.builder.get_object("whenCreateLabel")
self._availableSpaceLabel = self.builder.get_object("availableSpaceLabel")
self._totalSpaceLabel = self.builder.get_object("totalSpaceLabel")
self._summaryLabel = self.builder.get_object("summary_label")
# Buttons
self._addButton = self.builder.get_object("addButton")
self._applyButton = self.builder.get_object("applyButton")
self._configButton = self.builder.get_object("configureButton")
self._removeButton = self.builder.get_object("removeButton")
self._resetButton = self.builder.get_object("resetButton")
# Detailed configuration stuff
self._encryptCheckbox = self.builder.get_object("encryptCheckbox")
self._fsCombo = self.builder.get_object("fileSystemTypeCombo")
self._fsStore = self.builder.get_object("fileSystemStore")
self._labelEntry = self.builder.get_object("labelEntry")
self._mountPointEntry = self.builder.get_object("mountPointEntry")
self._nameEntry = self.builder.get_object("nameEntry")
self._raidLevelCombo = self.builder.get_object("raidLevelCombo")
self._raidLevelLabel = self.builder.get_object("raidLevelLabel")
self._reformatCheckbox = self.builder.get_object("reformatCheckbox")
self._sizeEntry = self.builder.get_object("sizeEntry")
self._typeStore = self.builder.get_object("deviceTypeStore")
self._typeCombo = self.builder.get_object("deviceTypeCombo")
self._modifyContainerButton = self.builder.get_object("modifyContainerButton")
self._containerCombo = self.builder.get_object("containerCombo")
self._containerStore = self.builder.get_object("containerStore")
self._deviceDescLabel = self.builder.get_object("deviceDescLabel")
self._passphraseEntry = self.builder.get_object("passphraseEntry")
# Stores
self._raidStoreFilter = self.builder.get_object("raidStoreFiltered")
# Labels
self._selectedDeviceLabel = self.builder.get_object("selectedDeviceLabel")
self._selectedDeviceDescLabel = self.builder.get_object("selectedDeviceDescLabel")
self._encryptedDeviceLabel = self.builder.get_object("encryptedDeviceLabel")
self._encryptedDeviceDescLabel = self.builder.get_object("encryptedDeviceDescriptionLabel")
self._incompleteDeviceLabel = self.builder.get_object("incompleteDeviceLabel")
self._incompleteDeviceDescLabel = self.builder.get_object("incompleteDeviceDescriptionLabel")
self._incompleteDeviceOptionsLabel = self.builder.get_object("incompleteDeviceOptionsLabel")
self._uneditableDeviceLabel = self.builder.get_object("uneditableDeviceLabel")
self._uneditableDeviceDescLabel = self.builder.get_object("uneditableDeviceDescriptionLabel")
self._containerLabel = self.builder.get_object("containerLabel")
def initialize(self):
NormalSpoke.initialize(self)
self.initialize_start()
self._grabObjects()
setViewportBackground(self.builder.get_object("availableSpaceViewport"), "#db3279")
setViewportBackground(self.builder.get_object("totalSpaceViewport"), "#60605b")
self._raidStoreFilter.set_visible_func(self._raid_level_visible)
self._accordion = Accordion()
self._partitionsViewport.add(self._accordion)
# Populate the list of valid filesystem types from the format classes.
# Unfortunately, we have to narrow them down a little bit more because
# this list will include things like PVs and RAID members.
self._fsStore.clear()
# Set the default partitioning scheme.
self._default_autopart_type = firstNotNone((self.data.autopart.type,
self.instclass.default_autopart_type))
# Connect viewport scrolling with accordion focus events
self._accordion.set_focus_hadjustment(self._partitionsViewport.get_hadjustment())
self._accordion.set_focus_vadjustment(self._partitionsViewport.get_vadjustment())
threadMgr.add(AnacondaThread(name=THREAD_CUSTOM_STORAGE_INIT, target=self._initialize))
def _initialize(self):
self._fs_types = []
actions = GtkActionList()
for cls in device_formats.itervalues():
obj = cls()
# btrfs is always handled by on_device_type_changed
supported_fs = (obj.type != "btrfs" and
obj.type != "tmpfs" and
obj.supported and obj.formattable and
(isinstance(obj, FS) or
obj.type in ["biosboot", "prepboot", "swap"]))
if supported_fs:
actions.add_action(self._fsStore.append, [obj.name])
self._fs_types.append(obj.name)
actions.fire()
# report that the custom spoke has been initialized
self.initialize_done()
@property
def _clearpartDevices(self):
return [d for d in self._devices if d.name in self.data.clearpart.drives and d.partitioned]
@property
def unusedDevices(self):
unused_devices = [d for d in self._storage_playground.unusedDevices
if d.disks and d.mediaPresent and
not d.partitioned and (d.direct or d.isleaf)]
# add incomplete VGs and MDs
incomplete = [d for d in self._storage_playground.devicetree._devices
if not getattr(d, "complete", True)]
unused_devices.extend(incomplete)
unused_devices.extend(d for d in self._storage_playground.partitioned if not d.format.supported)
return unused_devices
@property
def bootLoaderDevices(self):
devices = []
format_types = ["biosboot", "prepboot"]
for device in self._devices:
if device.format.type not in format_types:
continue
disk_names = (d.name for d in device.disks)
# bootDrive may not be setup because it IS one of these.
if not self.data.bootloader.bootDrive or \
self.data.bootloader.bootDrive in disk_names:
devices.append(device)
return devices
@property
def _currentFreeInfo(self):
return self._storage_playground.getFreeSpace(clearPartType=CLEARPART_TYPE_NONE)
def _setCurrentFreeSpace(self):
"""Add up all the free space on selected disks and return it as a Size."""
self._free_space = sum(f[0] for f in self._currentFreeInfo.values())
def _currentTotalSpace(self):
"""Add up the sizes of all selected disks and return it as a Size."""
totalSpace = sum((disk.size for disk in self._clearpartDevices),
Size(0))
return totalSpace
def _updateSpaceDisplay(self):
# Set up the free space/available space displays in the bottom left.
self._setCurrentFreeSpace()
self._availableSpaceLabel.set_text(str(self._free_space))
self._totalSpaceLabel.set_text(str(self._currentTotalSpace()))
count = len(self.data.clearpart.drives)
summary = CP_("GUI|Custom Partitioning",
"%d _storage device selected",
"%d _storage devices selected",
count) % count
self._summaryLabel.set_text(summary)
self._summaryLabel.set_use_underline(True)
@ui_storage_logged
def _hide_unusable_disks(self):
self._hidden_disks = []
for disk in self._storage_playground.disks:
if disk.protected or not disk.mediaPresent:
# hide removable disks containing install media
self._hidden_disks.append(disk)
self._storage_playground.devicetree.hide(disk)
def _unhide_unusable_disks(self):
for disk in reversed(self._hidden_disks):
self._storage_playground.devicetree.unhide(disk)
def _reset_storage(self):
self._storage_playground = self.storage.copy()
self._hide_unusable_disks()
self._devices = self._storage_playground.devices
def refresh(self):
self.clear_errors()
NormalSpoke.refresh(self)
# Make sure the storage spoke execute method has finished before we
# copy the storage instance.
for thread_name in [THREAD_EXECUTE_STORAGE, THREAD_STORAGE]:
threadMgr.wait(thread_name)
self._back_already_clicked = False
self.passphrase = self.data.autopart.passphrase
self._reset_storage()
self._do_refresh()
self._updateSpaceDisplay()
self._applyButton.set_sensitive(False)
def _get_container_names(self):
for data in self._containerStore:
yield data[0]
@property
def _current_page(self):
# The current page is really a function of the current selector.
# Whatever selector on the LHS is selected, the current page is the
# page containing that selector.
if not self._current_selector:
return None
for page in self._accordion.allPages:
if self._current_selector in page.members:
return page
return None
def _clear_current_selector(self):
""" If something is selected, deselect it
"""
if self._current_selector:
self._current_selector.set_chosen(False)
self._current_selector = None
def _get_fstype(self, fstypeCombo):
itr = fstypeCombo.get_active_iter()
if not itr:
return None
model = fstypeCombo.get_model()
return model[itr][0]
def _get_autopart_type(self, autopartTypeCombo):
itr = autopartTypeCombo.get_active_iter()
if not itr:
return None
model = autopartTypeCombo.get_model()
return model[itr][1]
def _change_autopart_type(self, autopartTypeCombo):
"""
This is called when the autopart type combo on the left hand side of
custom partitioning is changed. We already know how to handle the case
where the user changes the type and then clicks the autopart link
button. This handles the case where the user changes the type and then
clicks the '+' button.
"""
self.data.autopart.type = self._get_autopart_type(autopartTypeCombo)
def get_new_devices(self):
# A device scheduled for formatting only belongs in the new root.
new_devices = [d for d in self._devices if d.direct and
not d.format.exists and
not d.partitioned]
# If mountpoints have been assigned to any existing devices, go ahead
# and pull those in along with any existing swap devices. It doesn't
# matter if the formats being mounted exist or not.
new_mounts = [d for d in self._storage_playground.mountpoints.values() if d.exists]
if new_mounts or new_devices:
new_devices.extend(self._storage_playground.mountpoints.values())
new_devices.extend(self.bootLoaderDevices)
new_devices = list(set(new_devices))
return new_devices
def _populate_accordion(self):
# Make sure we start with a clean state.
self._accordion.removeAllPages()
new_devices = filter_unsupported_disklabel_devices(self.get_new_devices())
all_devices = filter_unsupported_disklabel_devices(self._devices)
unused_devices = filter_unsupported_disklabel_devices(self.unusedDevices)
# Now it's time to populate the accordion.
log.debug("ui: devices=%s", [d.name for d in all_devices])
log.debug("ui: unused=%s", [d.name for d in unused_devices])
log.debug("ui: new_devices=%s", [d.name for d in new_devices])
ui_roots = []
for root in self._storage_playground.roots:
root_devices = root.swaps + root.mounts.values()
# Don't make a page if none of the root's devices are left.
# Also, only include devices in an old page if the format is intact.
if not any(d for d in root_devices if d in all_devices and d.disks and
(root.name == translated_new_install_name() or d.format.exists)):
continue
if not filter_unsupported_disklabel_devices(root_devices):
continue
ui_roots.append(root)
# If we've not yet run autopart, add an instance of CreateNewPage. This
# ensures it's only added once.
if not new_devices:
page = CreateNewPage(translated_new_install_name(),
self.on_create_clicked,
self._change_autopart_type,
self._default_autopart_type,
partitionsToReuse=bool(ui_roots) or bool(unused_devices))
self._accordion.addPage(page, cb=self.on_page_clicked)
self._partitionsNotebook.set_current_page(NOTEBOOK_LABEL_PAGE)
self._whenCreateLabel.set_text(_("When you create mount points for "
"your %(name)s %(version)s installation, you'll be able to "
"view their details here.") % {"name" : productName,
"version" : productVersion})
else:
swaps = [d for d in new_devices if d.format.type == "swap"]
mounts = dict((d.format.mountpoint, d) for d in new_devices
if getattr(d.format, "mountpoint", None))
for device in new_devices:
if device in self.bootLoaderDevices:
mounts[device.format.name] = device
new_root = Root(mounts=mounts, swaps=swaps, name=translated_new_install_name())
ui_roots.insert(0, new_root)
# Add in all the existing (or autopart-created) operating systems.
for root in ui_roots:
page = Page(root.name)
for (mountpoint, device) in root.mounts.iteritems():
if device not in all_devices or \
not device.disks or \
(root.name != translated_new_install_name() and not device.format.exists):
continue
selector = page.addSelector(device, self.on_selector_clicked,
mountpoint=mountpoint)
selector.root = root
for device in root.swaps:
# by using all_devices we've already accounted for devices on unsupported disklabels
if device not in all_devices or \
(root.name != translated_new_install_name() and not device.format.exists):
continue
selector = page.addSelector(device, self.on_selector_clicked)
selector.root = root
page.show_all()
self._accordion.addPage(page, cb=self.on_page_clicked)
# Anything that doesn't go with an OS we understand? Put it in the Other box.
if unused_devices:
page = UnknownPage(_("Unknown"))
for u in sorted(unused_devices, key=lambda d: d.name):
page.addSelector(u, self.on_selector_clicked)
page.show_all()
self._accordion.addPage(page, cb=self.on_page_clicked)
def _do_refresh(self, mountpointToShow=None):
# block mountpoint selector signal handler for now
self._initialized = False
self._clear_current_selector()
# Start with buttons disabled, since nothing is selected.
self._removeButton.set_sensitive(False)
self._configButton.set_sensitive(False)
# populate the accorion with roots and mount points
self._populate_accordion()
# And then open the first page by default. Most of the time, this will
# be fine since it'll be the new installation page.
self._initialized = True
firstPage = self._accordion.allPages[0]
self._accordion.expandPage(firstPage.pageTitle)
self._show_mountpoint(page=firstPage, mountpoint=mountpointToShow)
self._applyButton.set_sensitive(False)
self._resetButton.set_sensitive(len(self._storage_playground.devicetree.findActions()) > 0)
###
### RIGHT HAND SIDE METHODS
###
def add_new_selector(self, device):
""" Add an entry for device to the new install Page. """
page = self._accordion._find_by_title(translated_new_install_name()).get_child()
devices = [device]
if not page.members:
# remove the CreateNewPage and replace it with a regular Page
expander = self._accordion._find_by_title(translated_new_install_name())
expander.remove(expander.get_child())
page = Page(translated_new_install_name())
expander.add(page)
# also pull in biosboot and prepboot that are on our boot disk
devices.extend(self.bootLoaderDevices)
devices = list(set(devices))
for _device in devices:
page.addSelector(_device, self.on_selector_clicked)
page.show_all()
def _update_selectors(self):
""" Update all btrfs selectors' size properties. """
# we're only updating selectors in the new root. problem?
page = self._accordion._find_by_title(translated_new_install_name()).get_child()
for selector in page.members:
updateSelectorFromDevice(selector, selector.device)
def _replace_device(self, **kwargs):
""" Create a replacement device and update the device selector. """
selector = kwargs.pop("selector", None)
dev_type = kwargs.pop("device_type")
size = kwargs.pop("size")
new_device = self._storage_playground.factoryDevice(dev_type, size, **kwargs)
self._devices = self._storage_playground.devices
if selector:
# update the selector with the new device and its size
updateSelectorFromDevice(selector, new_device)
def _update_device_in_selectors(self, old_device, new_device):
for s in self._accordion.allSelectors:
if s._device == old_device:
updateSelectorFromDevice(s, new_device)
def _update_all_devices_in_selectors(self):
for s in self._accordion.allSelectors:
for new_device in self._storage_playground.devices:
if ((s._device.name == new_device.name) or
(getattr(s._device, "req_name", 1) == getattr(new_device, "req_name", 2)) and
s._device.type == new_device.type and
s._device.format.type == new_device.format.type):
updateSelectorFromDevice(s, new_device)
break
else:
log.warning("failed to replace device: %s", s._device)
def _add_device_type(self, dev_type_const):
self._typeStore.append([_(DEVICE_TEXT_MAP[dev_type_from_const(dev_type_const)]),
dev_type_const])
def _validate_mountpoint(self, mountpoint, device, device_type, new_fs_type,
reformat, encrypted, raid_level):
""" Validate various aspects of a mountpoint.
:param str mountpoint: the mountpoint
:param device: blivet.devices.Device instance
:param int device_type: one of an enumeration of device types
:param str new_fs_type: string representing the new filesystem type
:param bool reformat: whether the device is to be reformatted
:param bool encrypted: whether the device is to be encrypted
:param raid_level: instance of blivet.devicelibs.raid.RAIDLevel or None
"""
error = None
if device_type not in (DEVICE_TYPE_PARTITION, DEVICE_TYPE_MD) and \
mountpoint == "/boot/efi":
error = (_("/boot/efi must be on a device of type %(oneFsType)s or %(anotherFsType)s")
% {"oneFsType": _(DEVICE_TEXT_PARTITION), "anotherFsType": _(DEVICE_TEXT_MD)})
elif device_type != DEVICE_TYPE_PARTITION and \
new_fs_type in PARTITION_ONLY_FORMAT_TYPES:
error = (_("%(fs)s must be on a device of type %(type)s")
% {"fs" : new_fs_type, "type" : _(DEVICE_TEXT_PARTITION)})
elif mountpoint and encrypted and mountpoint.startswith("/boot"):
error = _("%s cannot be encrypted") % mountpoint
elif encrypted and new_fs_type in PARTITION_ONLY_FORMAT_TYPES:
error = _("%s cannot be encrypted") % new_fs_type
elif mountpoint == "/" and device.format.exists and not reformat:
error = _("You must create a new file system on the root device.")
if not error and \
(raid_level is not None or requiresRaidSelection(device_type)) and \
raid_level not in raidLevelsSupported(device_type):
error = _("Device does not support RAID level selection %s.") % raid_level
if not error and raid_level is not None:
min_disks = raid_level.min_members
if len(self._device_disks) < min_disks:
error = _(RAID_NOT_ENOUGH_DISKS) % {"level": raid_level,
"min" : min_disks,
"count": len(self._device_disks)}
return error
def _update_size_props(self):
self._update_selectors()
self._updateSpaceDisplay()
def _try_replace_device(self, selector, removed_device, new_device_info,
old_device_info):
if removed_device:
# we don't want to pass the device if we removed it
new_device_info["device"] = None
try:
self._replace_device(selector=selector, **new_device_info)
return True
except StorageError as e:
log.error("factoryDevice failed: %s", e)
# the factory's error handling has replaced all of the
# devices with copies, so update the selectors' devices
# accordingly
self._update_all_devices_in_selectors()
self._error = e
self.set_warning(_(DEVICE_CONFIGURATION_ERROR_MSG))
if not removed_device:
# nothing more to do
return True
else:
# we have removed the old device so we now have to re-create it
# the disks need to be updated since we've replaced all
# of the devices with copies in the devicefactory error
# handler
old_disk_names = (d.name for d in old_device_info["disks"])
old_device_info["disks"] = [self._storage_playground.devicetree.getDeviceByName(n) for n in old_disk_names]
try:
self._replace_device(selector=selector, **old_device_info)
return True
except StorageError as e:
# failed to recover.
self.refresh() # this calls self.clear_errors
self._error = e
self.set_warning(_(UNRECOVERABLE_ERROR_MSG))
return False
@ui_storage_logged
def _revert_reformat(self, device, use_dev):
# figure out the existing device and reset it
if not use_dev.format.exists:
original_device = use_dev
else:
original_device = device
log.debug("resetting device %s", original_device.name)
self._storage_playground.resetDevice(original_device)
def _bound_size(self, size, device):
# If no size was specified, we just want to grow to the maximum.
# But resizeDevice doesn't take None for a value.
if not size:
size = device.maxSize
elif size < device.minSize:
size = device.minSize
elif size > device.maxSize:
size = device.maxSize
return size
@ui_storage_logged
def _handle_size_change(self, size, old_size, device, use_dev):
# If a LUKS device is being displayed, adjust the size
# to the appropriate size for the raw device.
use_size = size
use_old_size = old_size
if use_dev is not device:
use_size = size + crypto.LUKS_METADATA_SIZE
use_old_size = use_dev.size
# bound size to boundaries given by the device
use_size = self._bound_size(use_size, use_dev)
use_size = use_dev.alignTargetSize(use_size)
# And then we need to re-check that the max size is actually
# different from the current size.
_changed_size = False
if use_size != use_dev.size and use_size == use_dev.currentSize:
# size has been set back to its original value
actions = self._storage_playground.devicetree.findActions(action_type="resize",
devid=use_dev.id)
for action in reversed(actions):
self._storage_playground.devicetree.cancelAction(action)
_changed_size = True
elif use_size != use_dev.size:
log.debug("scheduling resize of device %s to %s", use_dev.name, use_size)
try:
self._storage_playground.resizeDevice(use_dev, use_size)
except StorageError as e:
log.error("failed to schedule device resize: %s", e)
use_dev.size = use_old_size
self._error = e
self.set_warning(_("Device resize request failed. "
"<a href=\"\">Click for details.</a>"))
else:
_changed_size = True
if _changed_size:
log.debug("new size: %s", use_dev.size)
log.debug("target size: %s", use_dev.targetSize)
# update the selector's size property
# The selector shows the visible disk, so it is necessary
# to use device and size, which are the values visible to
# the user.
for s in self._accordion.allSelectors:
if s._device == device:
s.size = str(device.size)
# update size props of all btrfs devices' selectors
self._update_size_props()
@ui_storage_logged
def _handle_encryption_change(self, encrypted, device, old_device, selector):
if not encrypted:
log.info("removing encryption from %s", device.name)
self._storage_playground.destroyDevice(device)
self._devices.remove(device)
old_device = device
device = device.slave
selector.device = device
self._update_device_in_selectors(old_device, device)
else:
log.info("applying encryption to %s", device.name)
old_device = device
new_fmt = getFormat("luks", device=device.path)
self._storage_playground.formatDevice(device, new_fmt)
luks_dev = LUKSDevice("luks-" + device.name,
parents=[device])
self._storage_playground.createDevice(luks_dev)
self._devices.append(luks_dev)
device = luks_dev
selector.device = device
self._update_device_in_selectors(old_device, device)
self._devices = self._storage_playground.devices
# possibly changed device and old_device, need to return the new ones
return (device, old_device)
@ui_storage_logged
def _do_reformat(self, device, mountpoint, label, changed_encryption,
encrypted, selector, fs_type):
self.clear_errors()
#
# ENCRYPTION
#
old_device = None
if changed_encryption:
device, old_device = self._handle_encryption_change(encrypted,
device, old_device, selector)
#
# FORMATTING
#
log.info("scheduling reformat of %s as %s", device.name, fs_type)
old_format = device.format
new_format = getFormat(fs_type,
mountpoint=mountpoint, label=label,
device=device.path)
try:
self._storage_playground.formatDevice(device, new_format)
except StorageError as e:
log.error("failed to register device format action: %s", e)
device.format = old_format
self._error = e
self.set_warning(_("Device reformat request failed. "
"<a href=\"\">Click for details.</a>"))
else:
# first, remove this selector from any old install page(s)
new_selector = None
for (page, _selector) in self._accordion.allMembers:
if _selector.device in (device, old_device):
if page.pageTitle == translated_new_install_name():
new_selector = _selector
continue
page.removeSelector(_selector)
if not page.members:
log.debug("removing empty page %s", page.pageTitle)
self._accordion.removePage(page.pageTitle)
# either update the existing selector or add a new one
if new_selector:
updateSelectorFromDevice(new_selector, device)
else:
self.add_new_selector(device)
# possibly changed device, need to return the new one
return device
def _save_right_side(self, selector):
""" Save settings from RHS and apply changes to the device.
This method must never trigger a call to self._do_refresh.
"""
# check if initialized and have something to operate on
if not self._initialized or not selector:
return
# only call _save_right_side if on the right page and some changes need
# to be saved (sensitivity of the Update Settings button reflects that)
if self._partitionsNotebook.get_current_page() != NOTEBOOK_DETAILS_PAGE or \
not self._applyButton.get_sensitive():
return
device = selector.device
if device not in self._devices:
# just-removed device
return
self._back_already_clicked = False
# dictionaries for many, many pieces of information about the device and
# requested changes, minimum required entropy for LUKS creation is
# always the same
new_device_info = {"min_luks_entropy": crypto.MIN_CREATE_ENTROPY}
old_device_info = {"min_luks_entropy": crypto.MIN_CREATE_ENTROPY}
new_device_info["device"] = device
use_dev = device.raw_device
log.info("ui: saving changes to device %s", device.name)
# TODO: member type (as a device type?)
# NAME
old_name = getattr(use_dev, "lvname", use_dev.name)
name = old_name
changed_name = False
if self._nameEntry.get_sensitive():
name = self._nameEntry.get_text()
changed_name = (name != old_name)
else:
# name entry insensitive means we don't control the name
name = None
old_device_info["name"] = old_name
new_device_info["name"] = name
# SIZE
old_size = device.size
# we are interested in size human readable representation change because
# that's what the user sees
same_size = self._device_size_text == self._sizeEntry.get_text()
if same_size:
size = old_size
else:
size = size_from_entry(self._sizeEntry)
changed_size = ((use_dev.resizable or not use_dev.exists) and
not same_size)
old_device_info["size"] = old_size
new_device_info["size"] = size
# DEVICE TYPE
device_type = self._get_current_device_type()
old_device_type = devicefactory.get_device_type(device)
changed_device_type = (old_device_type != device_type)
old_device_info["device_type"] = old_device_type
new_device_info["device_type"] = device_type
# REFORMAT
reformat = self._reformatCheckbox.get_active()
log.debug("reformat: %s", reformat)
# FS TYPE
old_fs_type = device.format.type
fs_type_index = self._fsCombo.get_active()
fs_type_str = self._fsCombo.get_model()[fs_type_index][0]
new_fs = getFormat(fs_type_str)
fs_type = new_fs.type
changed_fs_type = (old_fs_type != fs_type)
old_device_info["fstype"] = old_fs_type
new_device_info["fstype"] = fs_type
# ENCRYPTION
old_encrypted = isinstance(device, LUKSDevice)
encrypted = self._encryptCheckbox.get_active() and self._encryptCheckbox.is_sensitive()
changed_encryption = (old_encrypted != encrypted)
old_device_info["encrypted"] = old_encrypted
new_device_info["encrypted"] = encrypted
# FS LABEL
label = self._labelEntry.get_text()
old_label = getattr(device.format, "label", "")
changed_label = (label != old_label)
old_device_info["label"] = old_label
new_device_info["label"] = label
if changed_label or changed_fs_type:
error = validate_label(label, new_fs)
if error:
self._error = error