2 # kickstart.py : Apply kickstart configuration to a system
4 # Copyright 2007, Red Hat Inc.
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; version 2 of the License.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU Library General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
29 import system_config_keyboard
.keyboard
as keyboard
31 import rhpl
.keyboard
as keyboard
33 import pykickstart
.commands
as kscommands
34 import pykickstart
.constants
as ksconstants
35 import pykickstart
.errors
as kserrors
36 import pykickstart
.parser
as ksparser
37 import pykickstart
.version
as ksversion
39 import imgcreate
.errors
as errors
40 import imgcreate
.fs
as fs
42 def read_kickstart(path
):
43 """Parse a kickstart file and return a KickstartParser instance.
45 This is a simple utility function which takes a path to a kickstart file,
46 parses it and returns a pykickstart KickstartParser instance which can
47 be then passed to an ImageCreator constructor.
49 If an error occurs, a CreatorError exception is thrown.
52 version
= ksversion
.makeVersion()
53 ks
= ksparser
.KickstartParser(version
)
55 ksfile
= urlgrabber
.urlgrab(path
)
56 ks
.readKickstart(ksfile
)
57 # Fallback to e.args[0] is a workaround for bugs in urlgragger and pykickstart.
59 raise errors
.KickstartError("Failed to read kickstart file "
60 "'%s' : %s" % (path
, e
.strerror
or
62 except kserrors
.KickstartError
, e
:
63 raise errors
.KickstartError("Failed to parse kickstart file "
64 "'%s' : %s" % (path
, e
))
67 def build_name(kscfg
, prefix
= None, suffix
= None, maxlen
= None):
68 """Construct and return an image name string.
70 This is a utility function to help create sensible name and fslabel
71 strings. The name is constructed using the sans-prefix-and-extension
72 kickstart filename and the supplied prefix and suffix.
74 If the name exceeds the maxlen length supplied, the prefix is first dropped
75 and then the kickstart filename portion is reduced until it fits. In other
76 words, the suffix takes precedence over the kickstart portion and the
77 kickstart portion takes precedence over the prefix.
79 kscfg -- a path to a kickstart file
80 prefix -- a prefix to prepend to the name; defaults to None, which causes
82 suffix -- a suffix to append to the name; defaults to None, which causes
83 a YYYYMMDDHHMM suffix to be used
84 maxlen -- the maximum length for the returned string; defaults to None,
85 which means there is no restriction on the name length
87 Note, if maxlen is less then the len(suffix), you get to keep both pieces.
90 name
= os
.path
.basename(kscfg
)
98 suffix
= time
.strftime("%Y%m%d%H%M")
100 if name
.startswith(prefix
):
101 name
= name
[len(prefix
):]
103 ret
= prefix
+ name
+ "-" + suffix
104 if not maxlen
is None and len(ret
) > maxlen
:
105 ret
= name
[:maxlen
- len(suffix
) - 1] + "-" + suffix
109 class KickstartConfig(object):
110 """A base class for applying kickstart configurations to a system."""
111 def __init__(self
, instroot
):
112 self
.instroot
= instroot
114 def path(self
, subpath
):
115 return self
.instroot
+ subpath
118 os
.chroot(self
.instroot
)
121 def call(self
, args
):
122 if not os
.path
.exists("%s/%s" %(self
.instroot
, args
[0])):
123 raise errors
.KickstartError("Unable to run %s!" %(args))
124 subprocess
.call(args
, preexec_fn
= self
.chroot
)
129 class LanguageConfig(KickstartConfig
):
130 """A class to apply a kickstart language configuration to a system."""
131 def apply(self
, kslang
):
132 lang
= kslang
.lang
or "en_US.UTF-8"
134 f
= open(self
.path("/etc/locale.conf"), "w+")
135 f
.write("LANG=\"" + lang
+ "\"\n")
138 class KeyboardConfig(KickstartConfig
):
139 """A class to apply a kickstart keyboard configuration to a system."""
140 def apply(self
, kskeyboard
):
141 vcconf_file
= self
.path("/etc/vconsole.conf")
142 DEFAULT_VC_FONT
= "latarcyrheb-sun16"
144 if not kskeyboard
.keyboard
:
145 kskeyboard
.keyboard
= "us"
148 with
open(vcconf_file
, "w") as f
:
149 f
.write('KEYMAP="%s"\n' % kskeyboard
.keyboard
)
151 # systemd now defaults to a font that cannot display non-ascii
152 # characters, so we have to tell it to use a better one
153 f
.write('FONT="%s"\n' % DEFAULT_VC_FONT
)
155 logging
.error("Cannot write vconsole configuration file: %s" % e
)
157 class TimezoneConfig(KickstartConfig
):
158 """A class to apply a kickstart timezone configuration to a system."""
159 def apply(self
, kstimezone
):
160 tz
= kstimezone
.timezone
or "America/New_York"
161 utc
= str(kstimezone
.isUtc
)
163 # /etc/localtime is a symlink with glibc > 2.15-41
164 # but if it exists as a file keep it as a file and fall back
166 localtime
= self
.path("/etc/localtime")
167 if os
.path
.isfile(localtime
) and \
168 not os
.path
.islink(localtime
):
170 shutil
.copy2(self
.path("/usr/share/zoneinfo/%s" %(tz
,)),
172 except (OSError, shutil
.Error
) as e
:
173 logging
.error("Error copying timezone: %s" %(e
.strerror
,))
175 if os
.path
.exists(localtime
):
177 os
.symlink("/usr/share/zoneinfo/%s" %(tz
,), localtime
)
179 class AuthConfig(KickstartConfig
):
180 """A class to apply a kickstart authconfig configuration to a system."""
181 def apply(self
, ksauthconfig
):
182 if not os
.path
.exists(self
.path("/usr/sbin/authconfig")):
185 auth
= ksauthconfig
.authconfig
or "--useshadow --enablemd5"
186 args
= ["/usr/sbin/authconfig", "--update", "--nostart"]
187 self
.call(args
+ auth
.split())
189 class FirewallConfig(KickstartConfig
):
190 """A class to apply a kickstart firewall configuration to a system."""
191 def apply(self
, ksfirewall
):
192 args
= ["/usr/bin/firewall-offline-cmd"]
193 # enabled is None if neither --enable or --disable is passed
194 # default to enabled if nothing has been set.
195 if ksfirewall
.enabled
== False:
196 args
+= ["--disabled"]
198 args
+= ["--enabled"]
200 for dev
in ksfirewall
.trusts
:
201 args
+= [ "--trust=%s" % (dev
,) ]
203 for port
in ksfirewall
.ports
:
204 args
+= [ "--port=%s" % (port
,) ]
206 for service
in ksfirewall
.services
:
207 args
+= [ "--service=%s" % (service
,) ]
211 class RootPasswordConfig(KickstartConfig
):
212 """A class to apply a kickstart root password configuration to a system."""
214 self
.call(["/usr/bin/passwd", "-l", "root"])
216 def set_encrypted(self
, password
):
217 self
.call(["/usr/sbin/usermod", "-p", password
, "root"])
219 def set_unencrypted(self
, password
):
220 for p
in ("/bin/echo", "/usr/bin/passwd"):
221 if not os
.path
.exists("%s/%s" %(self
.instroot
, p
)):
222 raise errors
.KickstartError("Unable to set unencrypted password due to lack of %s" % p
)
224 p1
= subprocess
.Popen(["/bin/echo", password
],
225 stdout
= subprocess
.PIPE
,
226 preexec_fn
= self
.chroot
)
227 p2
= subprocess
.Popen(["/usr/bin/passwd", "--stdin", "root"],
229 stdout
= subprocess
.PIPE
,
230 preexec_fn
= self
.chroot
)
233 def apply(self
, ksrootpw
):
234 if ksrootpw
.isCrypted
:
235 self
.set_encrypted(ksrootpw
.password
)
236 elif ksrootpw
.password
!= "":
237 self
.set_unencrypted(ksrootpw
.password
)
242 class ServicesConfig(KickstartConfig
):
243 """A class to apply a kickstart services configuration to a system."""
244 def apply(self
, ksservices
):
245 if not os
.path
.exists(self
.path("/sbin/chkconfig")):
247 for s
in ksservices
.enabled
:
248 self
.call(["/sbin/chkconfig", s
, "on"])
249 for s
in ksservices
.disabled
:
250 self
.call(["/sbin/chkconfig", s
, "off"])
252 class XConfig(KickstartConfig
):
253 """A class to apply a kickstart X configuration to a system."""
254 RUNLEVELS
= {3: 'multi-user.target', 5: 'graphical.target'}
256 def apply(self
, ksxconfig
):
257 if ksxconfig
.defaultdesktop
:
258 f
= open(self
.path("/etc/sysconfig/desktop"), "w")
259 f
.write("DESKTOP="+ksxconfig
.defaultdesktop
+"\n")
263 if not os
.path
.isdir(self
.path('/etc/systemd/system')):
264 logging
.warning("there is no /etc/systemd/system directory, cannot update default.target!")
266 default_target
= self
.path('/etc/systemd/system/default.target')
267 if os
.path
.islink(default_target
):
268 os
.unlink(default_target
)
269 os
.symlink('/lib/systemd/system/graphical.target', default_target
)
271 class RPMMacroConfig(KickstartConfig
):
272 """A class to apply the specified rpm macros to the filesystem"""
276 f
= open(self
.path("/etc/rpm/macros.imgcreate"), "w+")
278 f
.write("%_excludedocs 1\n")
279 if not selinux_enabled(ks
):
280 f
.write("%__file_context_path %{nil}\n")
281 if inst_langs(ks
) != None:
282 f
.write("%_install_langs ")
283 f
.write(inst_langs(ks
))
287 class NetworkConfig(KickstartConfig
):
288 """A class to apply a kickstart network configuration to a system."""
289 def write_ifcfg(self
, network
):
290 p
= self
.path("/etc/sysconfig/network-scripts/ifcfg-" + network
.device
)
295 f
.write("DEVICE=%s\n" % network
.device
)
296 f
.write("BOOTPROTO=%s\n" % network
.bootProto
)
298 if network
.bootProto
.lower() == "static":
300 f
.write("IPADDR=%s\n" % network
.ip
)
302 f
.write("NETMASK=%s\n" % network
.netmask
)
305 f
.write("ONBOOT=on\n")
307 f
.write("ONBOOT=off\n")
310 f
.write("ESSID=%s\n" % network
.essid
)
313 if network
.ethtool
.find("autoneg") == -1:
314 network
.ethtool
= "autoneg off " + network
.ethtool
315 f
.write("ETHTOOL_OPTS=%s\n" % network
.ethtool
)
317 if network
.bootProto
.lower() == "dhcp":
319 f
.write("DHCP_HOSTNAME=%s\n" % network
.hostname
)
320 if network
.dhcpclass
:
321 f
.write("DHCP_CLASSID=%s\n" % network
.dhcpclass
)
324 f
.write("MTU=%s\n" % network
.mtu
)
328 def write_wepkey(self
, network
):
329 if not network
.wepkey
:
332 p
= self
.path("/etc/sysconfig/network-scripts/keys-" + network
.device
)
335 f
.write("KEY=%s\n" % network
.wepkey
)
338 def write_sysconfig(self
, useipv6
, hostname
, gateway
):
339 path
= self
.path("/etc/sysconfig/network")
343 f
.write("NETWORKING=yes\n")
346 f
.write("NETWORKING_IPV6=yes\n")
348 f
.write("NETWORKING_IPV6=no\n")
351 f
.write("GATEWAY=%s\n" % gateway
)
355 def write_hosts(self
, hostname
):
357 if hostname
and hostname
!= "localhost.localdomain":
358 localline
+= hostname
+ " "
359 l
= hostname
.split(".")
361 localline
+= l
[0] + " "
362 localline
+= "localhost.localdomain localhost"
364 path
= self
.path("/etc/hosts")
367 f
.write("127.0.0.1\t\t%s\n" % localline
)
368 f
.write("::1\t\tlocalhost6.localdomain6 localhost6\n")
371 def write_hostname(self
, hostname
):
375 path
= self
.path("/etc/hostname")
378 f
.write("%s\n" % (hostname
,))
381 def write_resolv(self
, nodns
, nameservers
):
382 if nodns
or not nameservers
:
385 path
= self
.path("/etc/resolv.conf")
389 for ns
in (nameservers
):
391 f
.write("nameserver %s\n" % ns
)
395 def apply(self
, ksnet
):
396 fs
.makedirs(self
.path("/etc/sysconfig/network-scripts"))
404 for network
in ksnet
.network
:
405 if not network
.device
:
406 raise errors
.KickstartError("No --device specified with "
407 "network kickstart command")
409 if (network
.onboot
and network
.bootProto
.lower() != "dhcp" and
410 not (network
.ip
and network
.netmask
)):
411 raise errors
.KickstartError("No IP address and/or netmask "
412 "specified with static "
413 "configuration for '%s'" %
416 self
.write_ifcfg(network
)
417 self
.write_wepkey(network
)
425 hostname
= network
.hostname
427 gateway
= network
.gateway
429 if network
.nameserver
:
430 nameservers
= network
.nameserver
.split(",")
432 self
.write_sysconfig(useipv6
, hostname
, gateway
)
433 self
.write_hosts(hostname
)
434 self
.write_hostname(hostname
)
435 self
.write_resolv(nodns
, nameservers
)
437 class SelinuxConfig(KickstartConfig
):
438 """A class to apply a kickstart selinux configuration to a system."""
439 def relabel(self
, ksselinux
):
440 # touch some files which get unhappy if they're not labeled correctly
441 for fn
in ("/etc/resolv.conf",):
447 if ksselinux
.selinux
== ksconstants
.SELINUX_DISABLED
:
450 if not os
.path
.exists(self
.path("/sbin/setfiles")):
453 self
.call(["/sbin/setfiles", "-p", "-e", "/proc", "-e", "/sys", "-e", "/dev", selinux
.selinux_file_context_path(), "/"])
455 def apply(self
, ksselinux
):
456 selinux_config
= "/etc/selinux/config"
457 if not os
.path
.exists(self
.instroot
+selinux_config
):
460 if ksselinux
.selinux
== ksconstants
.SELINUX_ENFORCING
:
461 cmd
= "SELINUX=enforcing\n"
462 elif ksselinux
.selinux
== ksconstants
.SELINUX_PERMISSIVE
:
463 cmd
= "SELINUX=permissive\n"
464 elif ksselinux
.selinux
== ksconstants
.SELINUX_DISABLED
:
465 cmd
= "SELINUX=disabled\n"
469 # Replace the SELINUX line in the config
470 lines
= open(self
.instroot
+selinux_config
).readlines()
471 with
open(self
.instroot
+selinux_config
, "w") as f
:
473 if line
.startswith("SELINUX="):
478 self
.relabel(ksselinux
)
480 def get_image_size(ks
, default
= None):
482 for p
in ks
.handler
.partition
.partitions
:
483 if p
.mountpoint
== "/" and p
.size
:
486 return int(__size
) * 1024L * 1024L
490 def get_image_fstype(ks
, default
= None):
491 for p
in ks
.handler
.partition
.partitions
:
492 if p
.mountpoint
== "/" and p
.fstype
:
498 if not hasattr(ks
.handler
.device
, "deviceList"):
499 devices
.append(ks
.handler
.device
)
501 devices
.extend(ks
.handler
.device
.deviceList
)
504 for device
in devices
:
505 if not device
.moduleName
:
507 modules
.extend(device
.moduleName
.split(":"))
511 def get_timeout(ks
, default
= None):
512 if not hasattr(ks
.handler
.bootloader
, "timeout"):
514 if ks
.handler
.bootloader
.timeout
is None:
516 return int(ks
.handler
.bootloader
.timeout
)
518 def get_kernel_args(ks
, default
= "ro rd.live.image quiet"):
519 if not hasattr(ks
.handler
.bootloader
, "appendLine"):
521 if ks
.handler
.bootloader
.appendLine
is None:
523 return "%s %s" %(default
, ks
.handler
.bootloader
.appendLine
)
525 def get_default_kernel(ks
, default
= None):
526 if not hasattr(ks
.handler
.bootloader
, "default"):
528 if not ks
.handler
.bootloader
.default
:
530 return ks
.handler
.bootloader
.default
532 def get_repos(ks
, repo_urls
= {}):
534 for repo
in ks
.handler
.repo
.repoList
:
536 if hasattr(repo
, "includepkgs"):
537 inc
.extend(repo
.includepkgs
)
540 if hasattr(repo
, "excludepkgs"):
541 exc
.extend(repo
.excludepkgs
)
543 baseurl
= repo
.baseurl
544 mirrorlist
= repo
.mirrorlist
546 sslverify
= not repo
.noverifyssl
548 if repo
.name
in repo_urls
:
549 baseurl
= repo_urls
[repo
.name
]
552 if repos
.has_key(repo
.name
):
553 logging
.warn("Overriding already specified repo %s" %(repo
.name
,))
554 repos
[repo
.name
] = (repo
.name
, baseurl
, mirrorlist
, proxy
, inc
, exc
, repo
.cost
, sslverify
)
556 return repos
.values()
558 def convert_method_to_repo(ks
):
560 ks
.handler
.repo
.methodToRepo()
561 except (AttributeError, kserrors
.KickstartError
):
564 def get_packages(ks
, required
= []):
565 return ks
.handler
.packages
.packageList
+ required
567 def get_groups(ks
, required
= []):
568 return ks
.handler
.packages
.groupList
+ required
570 def get_excluded(ks
, required
= []):
571 return ks
.handler
.packages
.excludedList
+ required
573 def get_partitions(ks
, required
= []):
574 return ks
.handler
.partition
.partitions
576 def ignore_missing(ks
):
577 return ks
.handler
.packages
.handleMissing
== ksconstants
.KS_MISSING_IGNORE
579 def exclude_docs(ks
):
580 return ks
.handler
.packages
.excludeDocs
583 if hasattr(ks
.handler
.packages
, "instLange"):
584 return ks
.handler
.packages
.instLange
585 elif hasattr(ks
.handler
.packages
, "instLangs"):
586 return ks
.handler
.packages
.instLangs
589 def get_pre_scripts(ks
):
591 for s
in ks
.handler
.scripts
:
592 if s
.type != ksparser
.KS_SCRIPT_PRE
:
597 def get_post_scripts(ks
):
599 for s
in ks
.handler
.scripts
:
600 if s
.type != ksparser
.KS_SCRIPT_POST
:
605 def selinux_enabled(ks
):
606 return ks
.handler
.selinux
.selinux
in (ksconstants
.SELINUX_ENFORCING
,
607 ksconstants
.SELINUX_PERMISSIVE
)