/
installation.py
372 lines (294 loc) · 13 KB
/
installation.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
#
# Installation tasks
#
# Copyright (C) 2019 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.
#
import os
import parted
from datetime import timedelta
from time import sleep
from blivet import callbacks as blivet_callbacks, util as blivet_util, arch
from blivet.errors import FSResizeError, FormatResizeError, StorageError
from blivet.util import get_current_entropy
from pyanaconda.anaconda_loggers import get_module_logger
from pyanaconda.core.i18n import _
from pyanaconda.core.configuration.anaconda import conf
from pyanaconda.modules.common.constants.objects import ISCSI, FCOE, ZFCP
from pyanaconda.modules.common.constants.services import STORAGE
from pyanaconda.modules.common.errors.installation import StorageInstallationError
from pyanaconda.modules.common.task import Task
import gi
gi.require_version("BlockDev", "2.0")
from gi.repository import BlockDev as blockdev
log = get_module_logger(__name__)
__all__ = ["ActivateFilesystemsTask", "MountFilesystemsTask", "WriteConfigurationTask"]
class ActivateFilesystemsTask(Task):
"""Installation task for activation of the storage configuration."""
def __init__(self, storage, entropy_timeout=600):
"""Create a new task.
:param storage: the storage model
:param entropy_timeout: a number of seconds for entropy gathering
"""
super().__init__()
self._storage = storage
self._entropy_timeout = entropy_timeout
@property
def name(self):
return "Activate filesystems"
def run(self):
"""Do the activation.
:raise: StorageInstallationError if the activation fails
"""
if conf.target.is_directory:
log.debug("Don't activate file systems during "
"the installation to a directory.")
return
register = blivet_callbacks.create_new_callbacks_register(
create_format_pre=self._report_message,
resize_format_pre=self._report_message,
wait_for_entropy=self._wait_for_entropy
)
try:
self._turn_on_filesystems(
self._storage,
callbacks=register
)
except (FSResizeError, FormatResizeError) as e:
log.error("Failed to resize device %s: %s", e.details, str(e))
message = _("An error occurred while resizing the device {}: {}").format(
e.details, str(e)
)
raise StorageInstallationError(message) from None
except StorageError as e:
log.error("Failed to activate filesystems: %s", str(e))
raise StorageInstallationError(str(e)) from None
def _report_message(self, data):
"""Report a Blivet message.
:param data: Blivet's callback data
"""
self.report_progress(data.msg)
def _wait_for_entropy(self, data):
"""Wait for entropy.
:param data: Blivet's callback data
:return: True if we are out of time, otherwise False
"""
log.debug(data.msg)
required_entropy = data.min_entropy
total_time = self._entropy_timeout
current_time = 0
while True:
# Report the current status.
current_entropy = get_current_entropy()
current_percents = min(int(current_entropy / required_entropy * 100), 100)
remaining_time = max(total_time - current_time, 0)
self._report_entropy_message(current_percents, remaining_time)
sleep(5)
current_time += 5
# Enough entropy gathered.
if current_percents == 100:
return False
# Out of time.
if remaining_time == 0:
return True
def _report_entropy_message(self, percents, time):
"""Report an entropy message.
:param percents: the percentage of gathered entropy
:param time: a number of seconds of remaining time
"""
if percents == 100:
self.report_progress(_("Gathering entropy 100%"))
return
if time == 0:
self.report_progress(_("Gathering entropy (time ran out)"))
return
message = _("Gathering entropy {percents}% (remaining time {time})").format(
percents=percents,
time=timedelta(seconds=time)
)
self.report_progress(message)
def _turn_on_filesystems(self, storage, callbacks=None):
"""Perform installer-specific activation of storage configuration.
:param storage: the storage object
:type storage: an instance of InstallerStorage
:param callbacks: callbacks to be invoked when actions are executed
:type callbacks: return value of the :func:`blivet.callbacks.create_new_callbacks_register`
"""
storage.devicetree.teardown_all()
storage.do_it(callbacks)
self._setup_bootable_devices(storage)
storage.dump_state("final")
storage.turn_on_swap()
def _setup_bootable_devices(self, storage):
"""Set up the bootable devices.
Mark the boot devices as bootable.
:param storage: an instance of the storage
"""
if storage.bootloader.skip_bootloader:
return
if storage.bootloader.stage2_bootable:
boot = storage.boot_device
else:
boot = storage.bootloader.stage1_device
if boot.type == "mdarray":
boot_devs = boot.parents
else:
boot_devs = [boot]
for dev in boot_devs:
if not hasattr(dev, "bootable"):
log.info("Skipping %s, not bootable", dev)
continue
# Dos labels can only have one partition marked as active
# and unmarking ie the windows partition is not a good idea
skip = False
if dev.disk.format.parted_disk.type == "msdos":
for p in dev.disk.format.parted_disk.partitions:
if p.type == parted.PARTITION_NORMAL and \
p.getFlag(parted.PARTITION_BOOT):
skip = True
break
# GPT labeled disks should only have bootable set on the
# EFI system partition (parted sets the EFI System GUID on
# GPT partitions with the boot flag)
if dev.disk.format.label_type == "gpt" and \
dev.format.type not in ["efi", "macefi"]:
skip = True
if skip:
log.info("Skipping %s", dev.name)
continue
# hfs+ partitions on gpt can't be marked bootable via parted
if dev.disk.format.parted_disk.type != "gpt" or \
dev.format.type not in ["hfs+", "macefi"]:
log.info("setting boot flag on %s", dev.name)
dev.bootable = True
# Set the boot partition's name on disk labels that support it
if dev.parted_partition.disk.supportsFeature(parted.DISK_TYPE_PARTITION_NAME):
ped_partition = dev.parted_partition.getPedPartition()
ped_partition.set_name(dev.format.name)
log.info("Setting label on %s to '%s'", dev, dev.format.name)
dev.disk.setup()
dev.disk.format.commit_to_disk()
class MountFilesystemsTask(Task):
"""Installation task for mounting the filesystems."""
def __init__(self, storage):
"""Create a new task."""
super().__init__()
self._storage = storage
@property
def name(self):
return "Mount filesystems"
def run(self):
"""Mount the filesystems."""
self._storage.mount_filesystems()
class WriteConfigurationTask(Task):
"""Installation task for writing out the storage configuration."""
def __init__(self, storage):
"""Create a new task."""
super().__init__()
self._storage = storage
@property
def name(self):
return "Write the storage configuration"
def run(self):
"""Mount the filesystems."""
if conf.target.is_directory:
log.debug("Don't write the storage configuration "
"during the installation to a directory.")
return
self._write_storage_configuration(self._storage)
def _write_storage_configuration(self, storage, sysroot=None):
"""Write the storage configuration to sysroot.
:param storage: the storage object
:param sysroot: a path to the target OS installation
"""
if sysroot is None:
sysroot = conf.target.system_root
if not os.path.isdir("%s/etc" % sysroot):
os.mkdir("%s/etc" % sysroot)
self._write_escrow_packets(storage, sysroot)
storage.make_mtab()
storage.fsset.write()
iscsi_proxy = STORAGE.get_proxy(ISCSI)
iscsi_proxy.WriteConfiguration()
fcoe_proxy = STORAGE.get_proxy(FCOE)
fcoe_proxy.WriteConfiguration()
zfcp_proxy = STORAGE.get_proxy(ZFCP)
zfcp_proxy.WriteConfiguration()
self._write_dasd_conf(storage, sysroot)
def _write_escrow_packets(self, storage, sysroot):
"""Write the escrow packets.
:param storage: the storage object
:type storage: an instance of InstallerStorage
:param sysroot: a path to the target OS installation
:type sysroot: str
"""
escrow_devices = [
d for d in storage.devices
if d.format.type == 'luks' and d.format.escrow_cert
]
if not escrow_devices:
return
log.debug("escrow: write_escrow_packets start")
backup_passphrase = blockdev.crypto.generate_backup_passphrase()
try:
escrow_dir = sysroot + "/root"
log.debug("escrow: writing escrow packets to %s", escrow_dir)
blivet_util.makedirs(escrow_dir)
for device in escrow_devices:
log.debug("escrow: device %s: %s",
repr(device.path), repr(device.format.type))
device.format.escrow(escrow_dir,
backup_passphrase)
except (IOError, RuntimeError) as e:
# TODO: real error handling
log.error("failed to store encryption key: %s", e)
log.debug("escrow: write_escrow_packets done")
def _write_dasd_conf(self, storage, sysroot):
"""Write DASD configuration to sysroot.
Write /etc/dasd.conf to target system for all DASD devices
configured during installation.
:param storage: the storage object
:param sysroot: a path to the target OS installation
"""
dasds = [d for d in storage.devices if d.type == "dasd"]
dasds.sort(key=lambda d: d.name)
if not (arch.is_s390() and dasds):
return
with open(os.path.realpath(sysroot + "/etc/dasd.conf"), "w") as f:
for dasd in dasds:
fields = [dasd.busid] + dasd.get_opts()
f.write("%s\n" % " ".join(fields),)
# check for hyper PAV aliases; they need to get added to dasd.conf as well
sysfs = "/sys/bus/ccw/drivers/dasd-eckd"
# in the case that someone is installing with *only* FBA DASDs,the above
# sysfs path will not exist; so check for it and just bail out of here if
# that's the case
if not os.path.exists(sysfs):
return
# this does catch every DASD, even non-aliases, but we're only going to be
# checking for a very specific flag, so there won't be any duplicate entries
# in dasd.conf
devs = [d for d in os.listdir(sysfs) if d.startswith("0.0")]
with open(os.path.realpath(sysroot + "/etc/dasd.conf"), "a") as f:
for d in devs:
aliasfile = "%s/%s/alias" % (sysfs, d)
with open(aliasfile, "r") as falias:
alias = falias.read().strip()
# if alias == 1, then the device is an alias; otherwise it is a
# normal dasd (alias == 0) and we can skip it, since it will have
# been added to dasd.conf in the above block of code
if alias == "1":
f.write("%s\n" % d)