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 pykickstart.commands as kscommands
30 import pykickstart.constants as ksconstants
31 import pykickstart.errors as kserrors
32 import pykickstart.parser as ksparser
33 import pykickstart.version as ksversion
34 from pykickstart.handlers.control import commandMap
35 from pykickstart.handlers.control import dataMap
37 import errors as errors
38 import fs_related as fs
39 import kscommands.desktop as desktop
40 import kscommands.moblinrepo as moblinrepo
41 import kscommands.micboot as micboot
44 sys.path.append("~/0509/mic")
47 def read_kickstart(path):
48 """Parse a kickstart file and return a KickstartParser instance.
50 This is a simple utility function which takes a path to a kickstart file,
51 parses it and returns a pykickstart KickstartParser instance which can
52 be then passed to an ImageCreator constructor.
54 If an error occurs, a CreatorError exception is thrown.
57 #version = ksversion.makeVersion()
58 #ks = ksparser.KickstartParser(version)
60 using_version = ksversion.DEVEL
61 commandMap[using_version]["desktop"] = desktop.Moblin_Desktop
62 commandMap[using_version]["repo"] = moblinrepo.Moblin_Repo
63 commandMap[using_version]["bootloader"] = micboot.Moblin_Bootloader
64 dataMap[using_version]["RepoData"] = moblinrepo.Moblin_RepoData
65 superclass = ksversion.returnClassForVersion(version=using_version)
67 class KSHandlers(superclass):
68 def __init__(self, mapping={}):
69 superclass.__init__(self, mapping=commandMap[using_version])
71 ks = ksparser.KickstartParser(KSHandlers())
74 ks.readKickstart(path)
75 except IOError, (err, msg):
76 raise errors.KickstartError("Failed to read kickstart file "
77 "'%s' : %s" % (path, msg))
78 except kserrors.KickstartError, e:
79 raise errors.KickstartError("Failed to parse kickstart file "
80 "'%s' : %s" % (path, e))
83 def build_name(kscfg, prefix = None, suffix = None, maxlen = None):
84 """Construct and return an image name string.
86 This is a utility function to help create sensible name and fslabel
87 strings. The name is constructed using the sans-prefix-and-extension
88 kickstart filename and the supplied prefix and suffix.
90 If the name exceeds the maxlen length supplied, the prefix is first dropped
91 and then the kickstart filename portion is reduced until it fits. In other
92 words, the suffix takes precedence over the kickstart portion and the
93 kickstart portion takes precedence over the prefix.
95 kscfg -- a path to a kickstart file
96 prefix -- a prefix to prepend to the name; defaults to None, which causes
98 suffix -- a suffix to append to the name; defaults to None, which causes
99 a YYYYMMDDHHMM suffix to be used
100 maxlen -- the maximum length for the returned string; defaults to None,
101 which means there is no restriction on the name length
103 Note, if maxlen is less then the len(suffix), you get to keep both pieces.
106 name = os.path.basename(kscfg)
107 idx = name.rfind('.')
114 suffix = time.strftime("%Y%m%d%H%M")
116 if name.startswith(prefix):
117 name = name[len(prefix):]
119 ret = prefix + name + "-" + suffix
120 if not maxlen is None and len(ret) > maxlen:
121 ret = name[:maxlen - len(suffix) - 1] + "-" + suffix
125 class KickstartConfig(object):
126 """A base class for applying kickstart configurations to a system."""
127 def __init__(self, instroot):
128 self.instroot = instroot
130 def path(self, subpath):
131 return self.instroot + subpath
134 os.chroot(self.instroot)
137 def call(self, args):
138 if not os.path.exists("%s/%s" %(self.instroot, args[0])):
139 print "%s/%s" %(self.instroot, args[0])
140 raise errors.KickstartError("Unable to run %s!" %(args))
141 subprocess.call(args, preexec_fn = self.chroot)
146 class LanguageConfig(KickstartConfig):
147 """A class to apply a kickstart language configuration to a system."""
148 def apply(self, kslang):
149 lang = kslang.lang or "en_US.UTF-8"
151 f = open(self.path("/etc/sysconfig/i18n"), "w+")
152 f.write("LANG=\"" + lang + "\"\n")
155 class KeyboardConfig(KickstartConfig):
156 """A class to apply a kickstart keyboard configuration to a system."""
157 def apply(self, kskeyboard):
160 # should this impact the X keyboard config too?
161 # or do we want to make X be able to do this mapping?
163 #k = rhpl.keyboard.Keyboard()
164 #if kskeyboard.keyboard:
165 # k.set(kskeyboard.keyboard)
166 #k.write(self.instroot)
169 class TimezoneConfig(KickstartConfig):
170 """A class to apply a kickstart timezone configuration to a system."""
171 def apply(self, kstimezone):
172 tz = kstimezone.timezone or "America/New_York"
173 utc = str(kstimezone.isUtc)
175 f = open(self.path("/etc/sysconfig/clock"), "w+")
176 f.write("ZONE=\"" + tz + "\"\n")
177 f.write("UTC=" + utc + "\n")
180 shutil.copyfile(self.path("/usr/share/zoneinfo/%s" %(tz,)),
181 self.path("/etc/localtime"))
182 except (IOError, OSError), (errno, msg):
183 raise errors.KickstartError("Error copying timezone info: %s" %(msg,))
186 class AuthConfig(KickstartConfig):
187 """A class to apply a kickstart authconfig configuration to a system."""
188 def apply(self, ksauthconfig):
189 auth = ksauthconfig.authconfig or "--useshadow --enablemd5"
190 args = ["/usr/share/authconfig/authconfig.py", "--update", "--nostart"]
191 self.call(args + auth.split())
193 class FirewallConfig(KickstartConfig):
194 """A class to apply a kickstart firewall configuration to a system."""
195 def apply(self, ksfirewall):
197 # FIXME: should handle the rest of the options
199 if not os.path.exists(self.path("/usr/sbin/lokkit")):
201 if ksfirewall.enabled:
204 status = "--disabled"
206 self.call(["/usr/sbin/lokkit",
207 "-f", "--quiet", "--nostart", status])
209 class RootPasswordConfig(KickstartConfig):
210 """A class to apply a kickstart root password configuration to a system."""
212 self.call(["/usr/bin/passwd", "-d", "root"])
214 def set_encrypted(self, password):
215 self.call(["/usr/sbin/usermod", "-p", password, "root"])
217 def set_unencrypted(self, password):
218 for p in ("/bin/echo", "/usr/sbin/chpasswd"):
219 if not os.path.exists("%s/%s" %(self.instroot, p)):
220 raise errors.KickstartError("Unable to set unencrypted password due to lack of %s" % p)
222 p1 = subprocess.Popen(["/bin/echo", "root:%s" %password],
223 stdout = subprocess.PIPE,
224 preexec_fn = self.chroot)
225 p2 = subprocess.Popen(["/usr/sbin/chpasswd", "-m"],
227 stdout = subprocess.PIPE,
228 preexec_fn = self.chroot)
231 def apply(self, ksrootpw):
232 if ksrootpw.isCrypted:
233 self.set_encrypted(ksrootpw.password)
234 elif ksrootpw.password != "":
235 self.set_unencrypted(ksrootpw.password)
239 class UserConfig(KickstartConfig):
240 def set_empty_passwd(self, user):
241 self.call(["/usr/bin/passwd", "-d", user])
243 def set_encrypted_passwd(self, user, password):
244 self.call(["/usr/sbin/usermod", "-p", "%s" % password, user])
246 def set_unencrypted_passwd(self, user, password):
247 for p in ("/bin/echo", "/usr/sbin/chpasswd"):
248 if not os.path.exists("%s/%s" %(self.instroot, p)):
249 raise errors.KickstartError("Unable to set unencrypted password due to lack of %s" % p)
251 p1 = subprocess.Popen(["/bin/echo", "%s:%s" %(user, password)],
252 stdout = subprocess.PIPE,
253 preexec_fn = self.chroot)
254 p2 = subprocess.Popen(["/usr/sbin/chpasswd", "-m"],
256 stdout = subprocess.PIPE,
257 preexec_fn = self.chroot)
260 def addUser(self, userconfig):
261 args = [ "/usr/sbin/useradd" ]
262 if userconfig.groups:
263 args += [ "--groups", string.join(userconfig.groups, ",") ]
265 args.append(userconfig.name)
266 dev_null = os.open("/dev/null", os.O_WRONLY)
267 subprocess.call(args,
270 preexec_fn = self.chroot)
272 if userconfig.password not in (None, ""):
273 if userconfig.isCrypted:
274 self.set_encrypted_passwd(userconfig.name, userconfig.password)
276 self.set_unencrypted_passwd(userconfig.name, userconfig.password)
278 self.set_empty_passwd(userconfig.name)
280 raise errors.KickstartError("Invalid kickstart command: %s" % userconfig.__str__())
282 def apply(self, user):
283 for userconfig in user.userList:
285 self.addUser(userconfig)
289 class ServicesConfig(KickstartConfig):
290 """A class to apply a kickstart services configuration to a system."""
291 def apply(self, ksservices):
292 if not os.path.exists(self.path("/sbin/chkconfig")):
294 for s in ksservices.enabled:
295 self.call(["/sbin/chkconfig", s, "on"])
296 for s in ksservices.disabled:
297 self.call(["/sbin/chkconfig", s, "off"])
299 class XConfig(KickstartConfig):
300 """A class to apply a kickstart X configuration to a system."""
301 def apply(self, ksxconfig):
303 f = open(self.path("/etc/inittab"), "rw+")
305 buf = buf.replace("id:3:initdefault", "id:5:initdefault")
309 if ksxconfig.defaultdesktop:
310 f = open(self.path("/etc/sysconfig/desktop"), "w")
311 f.write("DESKTOP="+ksxconfig.defaultdesktop+"\n")
314 class DesktopConfig(KickstartConfig):
315 """A class to apply a kickstart desktop configuration to a system."""
316 def apply(self, ksdesktop):
317 if ksdesktop.defaultdesktop:
318 f = open(self.path("/etc/sysconfig/desktop"), "w")
319 f.write("DESKTOP="+ksdesktop.defaultdesktop+"\n")
321 if os.path.exists(self.path("/etc/gdm/custom.conf")):
322 f = open(self.path("/etc/skel/.dmrc"), "w")
323 f.write("[Desktop]\n")
324 f.write("Session="+ksdesktop.defaultdesktop.lower()+"\n")
326 if ksdesktop.session:
327 if os.path.exists(self.path("/etc/sysconfig/uxlaunch")):
328 f = open(self.path("/etc/sysconfig/uxlaunch"), "a+")
329 f.write("session="+ksdesktop.session.lower()+"\n")
331 if ksdesktop.autologinuser:
332 f = open(self.path("/etc/sysconfig/desktop"), "a+")
333 f.write("AUTOLOGIN_USER=" + ksdesktop.autologinuser + "\n")
335 if ksdesktop.session:
336 if os.path.exists(self.path("/etc/sysconfig/uxlaunch")):
337 f = open(self.path("/etc/sysconfig/uxlaunch"), "a+")
338 f.write("user="+ksdesktop.autologinuser+"\n")
340 if os.path.exists(self.path("/etc/gdm/custom.conf")):
341 f = open(self.path("/etc/gdm/custom.conf"), "w")
342 f.write("[daemon]\n")
343 f.write("AutomaticLoginEnable=true\n")
344 f.write("AutomaticLogin=" + ksdesktop.autologinuser + "\n")
347 class MoblinRepoConfig(KickstartConfig):
348 """A class to apply a kickstart desktop configuration to a system."""
349 def __create_repo_section(self, repo, type, fd):
352 reposuffix = {"base":"", "debuginfo":"-debuginfo", "source":"-source"}
353 reponame = repo.name + reposuffix[type]
356 baseurl = repo.baseurl
358 mirrorlist = repo.mirrorlist
359 elif type == "debuginfo":
361 if repo.baseurl.endswith("/"):
362 baseurl = os.path.dirname(os.path.dirname(repo.baseurl))
364 baseurl = os.path.dirname(repo.baseurl)
367 variant = repo.mirrorlist[repo.mirrorlist.find("$"):]
368 mirrorlist = repo.mirrorlist[0:repo.mirrorlist.find("$")]
369 mirrorlist += "debug" + "-" + variant
370 elif type == "source":
372 if repo.baseurl.endswith("/"):
373 baseurl = os.path.dirname(os.path.dirname(os.path.dirname(repo.baseurl)))
375 baseurl = os.path.dirname(os.path.dirname(repo.baseurl))
378 variant = repo.mirrorlist[repo.mirrorlist.find("$"):]
379 mirrorlist = repo.mirrorlist[0:repo.mirrorlist.find("$")]
380 mirrorlist += "source" + "-" + variant
382 fd.write("[" + reponame + "]\n")
383 fd.write("name=" + reponame + "\n")
384 fd.write("failovermethod=priority\n")
386 fd.write("baseurl=" + baseurl + "\n")
388 fd.write("mirrorlist=" + mirrorlist + "\n")
389 """ Skip saving proxy settings """
391 # fd.write("proxy=" + repo.proxy + "\n")
392 #if repo.proxy_username:
393 # fd.write("proxy_username=" + repo.proxy_username + "\n")
394 #if repo.proxy_password:
395 # fd.write("proxy_password=" + repo.proxy_password + "\n")
397 fd.write("gpgkey=" + repo.gpgkey + "\n")
398 fd.write("gpgcheck=1\n")
400 fd.write("gpgcheck=0\n")
401 if type == "source" or type == "debuginfo" or repo.disable:
402 fd.write("enabled=0\n")
404 fd.write("enabled=1\n")
407 def __create_repo_file(self, repo, repodir):
408 if not os.path.exists(self.path(repodir)):
409 fs.makedirs(self.path(repodir))
410 f = open(self.path(repodir + "/" + repo.name + ".repo"), "w")
411 self.__create_repo_section(repo, "base", f)
413 self.__create_repo_section(repo, "debuginfo", f)
415 self.__create_repo_section(repo, "source", f)
418 def apply(self, ksrepo, repodata):
419 for repo in ksrepo.repoList:
421 #self.__create_repo_file(repo, "/etc/yum.repos.d")
422 self.__create_repo_file(repo, "/etc/zypp/repos.d")
423 """ Import repo gpg keys """
425 dev_null = os.open("/dev/null", os.O_WRONLY)
426 for repo in repodata:
428 subprocess.call([fs.find_binary_path("rpm"), "--root=%s" % self.instroot, "--import", repo['repokey']],
429 stdout = dev_null, stderr = dev_null)
432 class RPMMacroConfig(KickstartConfig):
433 """A class to apply the specified rpm macros to the filesystem"""
437 if not os.path.exists(self.path("/etc/rpm")):
438 os.mkdir(self.path("/etc/rpm"))
439 f = open(self.path("/etc/rpm/macros.imgcreate"), "w+")
441 f.write("%_excludedocs 1\n")
442 f.write("%__file_context_path %{nil}\n")
443 if inst_langs(ks) != None:
444 f.write("%_install_langs ")
445 f.write(inst_langs(ks))
449 class NetworkConfig(KickstartConfig):
450 """A class to apply a kickstart network configuration to a system."""
451 def write_ifcfg(self, network):
452 p = self.path("/etc/sysconfig/network-scripts/ifcfg-" + network.device)
457 f.write("DEVICE=%s\n" % network.device)
458 f.write("BOOTPROTO=%s\n" % network.bootProto)
460 if network.bootProto.lower() == "static":
462 f.write("IPADDR=%s\n" % network.ip)
464 f.write("NETMASK=%s\n" % network.netmask)
467 f.write("ONBOOT=on\n")
469 f.write("ONBOOT=off\n")
472 f.write("ESSID=%s\n" % network.essid)
475 if network.ethtool.find("autoneg") == -1:
476 network.ethtool = "autoneg off " + network.ethtool
477 f.write("ETHTOOL_OPTS=%s\n" % network.ethtool)
479 if network.bootProto.lower() == "dhcp":
481 f.write("DHCP_HOSTNAME=%s\n" % network.hostname)
482 if network.dhcpclass:
483 f.write("DHCP_CLASSID=%s\n" % network.dhcpclass)
486 f.write("MTU=%s\n" % network.mtu)
490 def write_wepkey(self, network):
491 if not network.wepkey:
494 p = self.path("/etc/sysconfig/network-scripts/keys-" + network.device)
497 f.write("KEY=%s\n" % network.wepkey)
500 def write_sysconfig(self, useipv6, hostname, gateway):
501 path = self.path("/etc/sysconfig/network")
505 f.write("NETWORKING=yes\n")
508 f.write("NETWORKING_IPV6=yes\n")
510 f.write("NETWORKING_IPV6=no\n")
513 f.write("HOSTNAME=%s\n" % hostname)
515 f.write("HOSTNAME=localhost.localdomain\n")
518 f.write("GATEWAY=%s\n" % gateway)
522 def write_hosts(self, hostname):
524 if hostname and hostname != "localhost.localdomain":
525 localline += hostname + " "
526 l = hostname.split(".")
528 localline += l[0] + " "
529 localline += "localhost.localdomain localhost"
531 path = self.path("/etc/hosts")
534 f.write("127.0.0.1\t\t%s\n" % localline)
535 f.write("::1\t\tlocalhost6.localdomain6 localhost6\n")
538 def write_resolv(self, nodns, nameservers):
539 if nodns or not nameservers:
542 path = self.path("/etc/resolv.conf")
546 for ns in (nameservers):
548 f.write("nameserver %s\n" % ns)
552 def apply(self, ksnet):
553 fs.makedirs(self.path("/etc/sysconfig/network-scripts"))
561 for network in ksnet.network:
562 if not network.device:
563 raise errors.KickstartError("No --device specified with "
564 "network kickstart command")
566 if (network.onboot and network.bootProto.lower() != "dhcp" and
567 not (network.ip and network.netmask)):
568 raise errors.KickstartError("No IP address and/or netmask "
569 "specified with static "
570 "configuration for '%s'" %
573 self.write_ifcfg(network)
574 self.write_wepkey(network)
582 hostname = network.hostname
584 gateway = network.gateway
586 if network.nameserver:
587 nameservers = network.nameserver.split(",")
589 self.write_sysconfig(useipv6, hostname, gateway)
590 self.write_hosts(hostname)
591 self.write_resolv(nodns, nameservers)
594 def get_image_size(ks, default = None):
596 for p in ks.handler.partition.partitions:
597 if p.mountpoint == "/" and p.size:
600 return int(__size) * 1024L * 1024L
604 def get_image_fstype(ks, default = None):
605 for p in ks.handler.partition.partitions:
606 if p.mountpoint == "/" and p.fstype:
610 def get_image_fsopts(ks, default = None):
611 for p in ks.handler.partition.partitions:
612 if p.mountpoint == "/" and p.fsopts:
618 if isinstance(ks.handler.device, kscommands.device.FC3_Device):
619 devices.append(ks.handler.device)
621 devices.extend(ks.handler.device.deviceList)
624 for device in devices:
625 if not device.moduleName:
627 modules.extend(device.moduleName.split(":"))
631 def get_timeout(ks, default = None):
632 if not hasattr(ks.handler.bootloader, "timeout"):
634 if ks.handler.bootloader.timeout is None:
636 return int(ks.handler.bootloader.timeout)
638 def get_kernel_args(ks, default = "ro liveimg"):
639 if not hasattr(ks.handler.bootloader, "appendLine"):
641 if ks.handler.bootloader.appendLine is None:
643 return "%s %s" %(default, ks.handler.bootloader.appendLine)
645 def get_menu_args(ks, default = "bootinstall"):
646 if not hasattr(ks.handler.bootloader, "menus"):
648 if ks.handler.bootloader.menus is None:
650 return "%s %s" %(default, ks.handler.bootloader.menus)
652 def get_default_kernel(ks, default = None):
653 if not hasattr(ks.handler.bootloader, "default"):
655 if not ks.handler.bootloader.default:
657 return ks.handler.bootloader.default
659 def get_repos(ks, repo_urls = {}):
661 for repo in ks.handler.repo.repoList:
663 if hasattr(repo, "includepkgs"):
664 inc.extend(repo.includepkgs)
667 if hasattr(repo, "excludepkgs"):
668 exc.extend(repo.excludepkgs)
670 baseurl = repo.baseurl
671 mirrorlist = repo.mirrorlist
673 if repo.name in repo_urls:
674 baseurl = repo_urls[repo.name]
677 if repos.has_key(repo.name):
678 logging.warn("Overriding already specified repo %s" %(repo.name,))
681 if hasattr(repo, "proxy"):
683 proxy_username = None
684 if hasattr(repo, "proxy_username"):
685 proxy_username = repo.proxy_username
686 proxy_password = None
687 if hasattr(repo, "proxy_password"):
688 proxy_password = repo.proxy_password
689 if hasattr(repo, "debuginfo"):
690 debuginfo = repo.debuginfo
691 if hasattr(repo, "source"):
693 if hasattr(repo, "gpgkey"):
695 if hasattr(repo, "disable"):
696 disable = repo.disable
698 repos[repo.name] = (repo.name, baseurl, mirrorlist, inc, exc, proxy, proxy_username, proxy_password, debuginfo, source, gpgkey, disable)
700 return repos.values()
702 def convert_method_to_repo(ks):
704 ks.handler.repo.methodToRepo()
705 except (AttributeError, kserrors.KickstartError):
708 def get_packages(ks, required = []):
709 return ks.handler.packages.packageList + required
711 def get_groups(ks, required = []):
712 return ks.handler.packages.groupList + required
714 def get_excluded(ks, required = []):
715 return ks.handler.packages.excludedList + required
717 def get_partitions(ks, required = []):
718 return ks.handler.partition.partitions
720 def ignore_missing(ks):
721 return ks.handler.packages.handleMissing == ksconstants.KS_MISSING_IGNORE
723 def exclude_docs(ks):
724 return ks.handler.packages.excludeDocs
727 if hasattr(ks.handler.packages, "instLange"):
728 return ks.handler.packages.instLange
729 elif hasattr(ks.handler.packages, "instLangs"):
730 return ks.handler.packages.instLangs
733 def get_post_scripts(ks):
735 for s in ks.handler.scripts:
736 if s.type != ksparser.KS_SCRIPT_POST:
741 def add_repo(ks, repostr):
742 args = repostr.split()
743 repoobj = ks.handler.repo.parse(args[1:])
744 if repoobj and repoobj not in ks.handler.repo.repoList:
745 ks.handler.repo.repoList.append(repoobj)
747 def remove_all_repos(ks):
748 while len(ks.handler.repo.repoList) != 0:
749 del ks.handler.repo.repoList[0]
751 def remove_duplicate_repos(ks):
755 if len(ks.handler.repo.repoList) < 2:
757 if i >= len(ks.handler.repo.repoList) - 1:
759 name = ks.handler.repo.repoList[i].name
760 baseurl = ks.handler.repo.repoList[i].baseurl
761 if j < len(ks.handler.repo.repoList):
762 if (ks.handler.repo.repoList[j].name == name or \
763 ks.handler.repo.repoList[j].baseurl == baseurl):
764 del ks.handler.repo.repoList[j]
767 if j >= len(ks.handler.repo.repoList):
774 def resolve_groups(creator, repometadata, use_comps = False):
775 pkgmgr = creator.pkgmgr.get_default_pkg_manager
777 if creator.pkgmgr.managers.has_key("zypp") and creator.pkgmgr.managers['zypp'] == pkgmgr:
781 for repo in repometadata:
782 """ Mustn't replace group with package list if repo is ready for the corresponding package manager """
783 if iszypp and repo["patterns"] and not use_comps:
785 if not iszypp and repo["comps"] and use_comps:
789 But we also must handle such cases, use zypp but repo only has comps,
790 use yum but repo only has patterns, use zypp but use_comps is true,
791 use yum but use_comps is false.
795 if (use_comps and repo["comps"]) or (not repo["patterns"] and repo["comps"]):
796 groupfile = repo["comps"]
797 get_pkglist_handler = misc.get_pkglist_in_comps
799 if (not use_comps and repo["patterns"]) or (not repo["comps"] and repo["patterns"]):
800 groupfile = repo["patterns"]
801 get_pkglist_handler = misc.get_pkglist_in_patterns
806 if i >= len(ks.handler.packages.groupList):
808 pkglist = get_pkglist_handler(ks.handler.packages.groupList[i].name, groupfile)
810 del ks.handler.packages.groupList[i]
812 if pkg not in ks.handler.packages.packageList:
813 ks.handler.packages.packageList.append(pkg)