-
Notifications
You must be signed in to change notification settings - Fork 345
/
kickstart.py
426 lines (330 loc) · 14.6 KB
/
kickstart.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
#
# kickstart.py: kickstart install support
#
# Copyright (C) 1999-2016
# Red Hat, Inc. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty 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, see <http://www.gnu.org/licenses/>.
#
import glob
import os
import os.path
import sys
import tempfile
import time
import warnings
from contextlib import contextmanager
from pyanaconda.anaconda_loggers import get_module_logger, get_stdout_logger
from pyanaconda.core import util
from pyanaconda.core.path import open_with_perm
from pyanaconda.core.configuration.anaconda import conf
from pyanaconda.core.kickstart import VERSION, commands as COMMANDS
from pyanaconda.core.kickstart.specification import KickstartSpecification
from pyanaconda.core.constants import IPMI_ABORTED
from pyanaconda.errors import ScriptError, errorHandler
from pyanaconda.flags import flags
from pyanaconda.core.i18n import _
from pyanaconda.modules.common.constants.objects import SCRIPTS
from pyanaconda.modules.common.constants.services import BOSS, RUNTIME
from pyanaconda.modules.common.structures.kickstart import KickstartReport
from pykickstart.base import KickstartCommand, RemovedCommand
from pykickstart.constants import KS_SCRIPT_POST, KS_SCRIPT_TRACEBACK
from pykickstart.errors import KickstartError, KickstartParseWarning
from pykickstart.ko import KickstartObject
from pykickstart.parser import KickstartParser
from pykickstart.parser import Script as KSScript
from pykickstart.sections import NullSection, PreScriptSection, PostScriptSection, OnErrorScriptSection, TracebackScriptSection, Section
from pykickstart.version import returnClassForVersion
log = get_module_logger(__name__)
stdoutLog = get_stdout_logger()
# kickstart parsing and kickstart script
script_log = log.getChild("script")
parsing_log = log.getChild("parsing")
@contextmanager
def check_kickstart_error():
try:
yield
except KickstartError as e:
# We do not have an interface here yet, so we cannot use our error
# handling callback.
print(e)
util.ipmi_report(IPMI_ABORTED)
sys.exit(1)
class AnacondaKSScript(KSScript):
""" Execute a kickstart script
This will write the script to a file named /tmp/ks-script- before
execution.
Output is logged by the program logger, the path specified by --log
or to /tmp/ks-script-\\*.log
"""
def run(self, chroot):
""" Run the kickstart script
@param chroot directory path to chroot into before execution
"""
if self.inChroot:
scriptRoot = chroot
else:
scriptRoot = "/"
(fd, path) = tempfile.mkstemp("", "ks-script-", scriptRoot + "/tmp")
os.write(fd, self.script.encode("utf-8"))
os.close(fd)
os.chmod(path, 0o700)
# Always log stdout/stderr from scripts. Using --log just lets you
# pick where it goes. The script will also be logged to program.log
# because of execWithRedirect.
if self.logfile:
if self.inChroot:
messages = "%s/%s" % (scriptRoot, self.logfile)
else:
messages = self.logfile
d = os.path.dirname(messages)
if not os.path.exists(d):
os.makedirs(d)
else:
# Always log outside the chroot, we copy those logs into the
# chroot later.
messages = "/tmp/%s.log" % os.path.basename(path)
with open_with_perm(messages, "w", 0o600) as fp:
rc = util.execWithRedirect(self.interp, ["/tmp/%s" % os.path.basename(path)],
stdout=fp,
root=scriptRoot)
if rc != 0:
script_log.error("Error code %s running the kickstart script at line %s", rc, self.lineno)
if self.errorOnFail:
err = ""
with open(messages, "r") as fp:
err = "".join(fp.readlines())
# Show error dialog even for non-interactive
flags.ksprompt = True
errorHandler.cb(ScriptError(self.lineno, err))
util.ipmi_report(IPMI_ABORTED)
sys.exit(0)
class AnacondaInternalScript(AnacondaKSScript):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._hidden = True
def __str__(self):
# Scripts that implement portions of anaconda (copying screenshots and
# log files, setfilecons, etc.) should not be written to the output
# kickstart file.
return ""
###
### SUBCLASSES OF PYKICKSTART COMMAND HANDLERS
###
class UselessSection(Section):
"""Kickstart section that was moved on DBus and doesn't do anything."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.sectionOpen = kwargs.get("sectionOpen")
class UselessCommand(KickstartCommand):
"""Kickstart command that was moved on DBus and doesn't do anything.
Use this class to override the pykickstart command in our command map,
when we don't want the command to do anything.
"""
def __str__(self):
"""Generate this part of a kickstart file from the DBus module."""
return ""
def parse(self, args):
"""Do not parse anything.
We can keep this method for the checks if it is possible, but
it shouldn't parse anything.
"""
log.warning("Command %s will be parsed in DBus module.", self.currentCmd)
class UselessObject(KickstartObject):
"""Kickstart object that was moved on DBus and doesn't do anything."""
def __str__(self):
"""Generate this part of a kickstart file from the DBus module."""
return ""
###
### HANDLERS
###
# This is just the latest entry from pykickstart.handlers.control with all the
# classes we're overriding in place of the defaults.
class AnacondaKickstartSpecification(KickstartSpecification):
"""The kickstart specification of the main process."""
commands = {
"cmdline": COMMANDS.DisplayMode,
"eula": COMMANDS.Eula,
"graphical": COMMANDS.DisplayMode,
"halt": COMMANDS.Reboot,
"logging": COMMANDS.Logging,
"poweroff": COMMANDS.Reboot,
"reboot": COMMANDS.Reboot,
"rescue": COMMANDS.Rescue,
"shutdown": COMMANDS.Reboot,
"text": COMMANDS.DisplayMode,
"vnc": COMMANDS.Vnc,
}
@classmethod
def generate_command_map(cls, handler):
"""Generate a command map.
:param handler: a kickstart handler
:return: a map of command overrides
"""
command_map = dict(cls.commands)
for name, command in handler.commandMap.items():
# Ignore removed commands.
if issubclass(command, RemovedCommand):
continue
# Mark unspecified commands as useless.
if name not in command_map:
command_map[name] = UselessCommand
return command_map
@classmethod
def generate_data_map(cls, handler):
"""Generate a data map.
:param handler: a kickstart handler
:return: a map of data overrides
"""
return dict(cls.commands_data)
# Get the kickstart handler for the specified version.
superclass = returnClassForVersion(VERSION)
# Generate the command and data overrides.
specification = AnacondaKickstartSpecification
commandMap = specification.generate_command_map(superclass)
dataMap = specification.generate_data_map(superclass)
class AnacondaKSHandler(superclass):
def __init__(self, commandUpdates=None, dataUpdates=None):
if commandUpdates is None:
commandUpdates = commandMap
if dataUpdates is None:
dataUpdates = dataMap
super().__init__(commandUpdates=commandUpdates, dataUpdates=dataUpdates)
self.onPart = {}
# The %packages section is handled by the DBus module.
self.packages = UselessObject()
# The %pre, %pre-install sections are handled by the DBus module.
self.scripts = UselessObject()
def __str__(self):
proxy = BOSS.get_proxy()
modules = proxy.GenerateKickstart().strip()
return super().__str__() + "\n" + modules
class AnacondaPreParser(KickstartParser):
# A subclass of KickstartParser that only looks for %pre scripts and
# sets them up to be run. All other scripts and commands are ignored.
def __init__(self, handler):
super().__init__(handler, missingIncludeIsFatal=False)
def handleCommand(self, lineno, args):
pass
def setupSections(self):
self.registerSection(PreScriptSection(self.handler, dataObj=AnacondaKSScript))
self.registerSection(NullSection(self.handler, sectionOpen="%pre-install"))
self.registerSection(NullSection(self.handler, sectionOpen="%post"))
self.registerSection(NullSection(self.handler, sectionOpen="%onerror"))
self.registerSection(NullSection(self.handler, sectionOpen="%traceback"))
self.registerSection(NullSection(self.handler, sectionOpen="%packages"))
self.registerSection(NullSection(self.handler, sectionOpen="%addon"))
class AnacondaKSParser(KickstartParser):
def __init__(self, handler, scriptClass=AnacondaKSScript):
self.scriptClass = scriptClass
super().__init__(handler)
def handleCommand(self, lineno, args):
if not self.handler:
return
return KickstartParser.handleCommand(self, lineno, args)
def setupSections(self):
self.registerSection(PostScriptSection(self.handler, dataObj=self.scriptClass))
self.registerSection(TracebackScriptSection(self.handler, dataObj=self.scriptClass))
self.registerSection(OnErrorScriptSection(self.handler, dataObj=self.scriptClass))
self.registerSection(UselessSection(self.handler, sectionOpen="%pre"))
self.registerSection(UselessSection(self.handler, sectionOpen="%pre-install"))
self.registerSection(UselessSection(self.handler, sectionOpen="%packages"))
self.registerSection(UselessSection(self.handler, sectionOpen="%addon"))
def preScriptPass(f):
# The first pass through kickstart file processing - look for %pre scripts
# and run them. This must come in a separate pass in case a script
# generates an included file that has commands for later.
# run %pre scripts
runPreScripts()
def parseKickstart(handler, f, strict_mode=False):
# preprocessing the kickstart file has already been handled in initramfs.
ksparser = AnacondaKSParser(handler)
kswarnings = []
showwarning = warnings.showwarning
def ksshowwarning(message, category, filename, lineno, file=None, line=None):
# Print the warning with default function.
showwarning(message, category, filename, lineno, file, line)
# Collect pykickstart warnings.
if issubclass(category, KickstartParseWarning):
kswarnings.append(message)
try:
# Process warnings differently in this part.
with warnings.catch_warnings():
# Set up the warnings module.
warnings.showwarning = ksshowwarning
warnings.simplefilter("always", category=KickstartParseWarning)
# Parse the kickstart file in DBus modules.
boss = BOSS.get_proxy()
report = KickstartReport.from_structure(
boss.ReadKickstartFile(f)
)
for warn in report.warning_messages:
warnings.warn(warn.message, KickstartParseWarning)
if not report.is_valid():
message = "\n\n".join(map(str, report.error_messages))
raise KickstartError(message)
# Parse the kickstart file in anaconda.
ksparser.readKickstart(f)
# Print kickstart warnings and error out if in strict mode
if kswarnings:
print(_("\nSome warnings occurred during reading the kickstart file:"))
for w in kswarnings:
print(str(w).strip())
if strict_mode:
raise KickstartError("Please modify your kickstart file to fix the warnings "
"or remove the `ksstrict` option.")
except KickstartError as e:
# We do not have an interface here yet, so we cannot use our error
# handling callback.
parsing_log.error(e)
# Print an error and terminate.
print(_("\nAn error occurred during reading the kickstart file:"
"\n%s\n\nThe installer will now terminate.") % str(e).strip())
util.ipmi_report(IPMI_ABORTED)
time.sleep(10)
sys.exit(1)
def appendPostScripts(ksdata):
scripts = ""
# Read in all the post script snippets to a single big string.
for fn in sorted(glob.glob("/usr/share/anaconda/post-scripts/*ks")):
f = open(fn, "r")
scripts += f.read()
f.close()
# Then parse the snippets against the existing ksdata. We can do this
# because pykickstart allows multiple parses to save their data into a
# single data object. Errors parsing the scripts are a bug in anaconda,
# so just raise an exception.
ksparser = AnacondaKSParser(ksdata, scriptClass=AnacondaInternalScript)
ksparser.readKickstartFromString(scripts, reset=False)
def runPostScripts(scripts):
postScripts = [s for s in scripts if s.type == KS_SCRIPT_POST]
if len(postScripts) == 0:
return
script_log.info("Running kickstart %%post script(s)")
for script in postScripts:
script.run(conf.target.system_root)
script_log.info("All kickstart %%post script(s) have been run")
def runPreScripts():
runtime_proxy = RUNTIME.get_proxy(SCRIPTS)
task = runtime_proxy.RunPreScriptsWithTask()
return task
def runPreInstallScripts():
runtime_proxy = RUNTIME.get_proxy(SCRIPTS)
task = runtime_proxy.RunPreInstallScriptsWithTask()
return task
def runTracebackScripts(scripts):
script_log.info("Running kickstart %%traceback script(s)")
for script in filter(lambda s: s.type == KS_SCRIPT_TRACEBACK, scripts):
script.run("/")
script_log.info("All kickstart %%traceback script(s) have been run")