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.
25 from mic.utils import errors
26 from mic.utils import misc
27 from mic.utils import fs_related as fs
30 import pykickstart.commands as kscommands
31 import pykickstart.constants as ksconstants
32 import pykickstart.errors as kserrors
33 import pykickstart.parser as ksparser
34 import pykickstart.version as ksversion
35 from pykickstart.handlers.control import commandMap
36 from pykickstart.handlers.control import dataMap
38 import custom_commands.desktop as desktop
39 import custom_commands.moblinrepo as moblinrepo
40 import custom_commands.micboot as micboot
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 using_version = ksversion.DEVEL
56 commandMap[using_version]["desktop"] = desktop.Moblin_Desktop
57 commandMap[using_version]["repo"] = moblinrepo.Moblin_Repo
58 commandMap[using_version]["bootloader"] = micboot.Moblin_Bootloader
59 dataMap[using_version]["RepoData"] = moblinrepo.Moblin_RepoData
60 superclass = ksversion.returnClassForVersion(version=using_version)
62 class KSHandlers(superclass):
63 def __init__(self, mapping={}):
64 superclass.__init__(self, mapping=commandMap[using_version])
66 ks = ksparser.KickstartParser(KSHandlers())
69 ks.readKickstart(path)
70 except IOError, (err, msg):
71 raise errors.KsError("Failed to read kickstart file "
72 "'%s' : %s" % (path, msg))
73 except kserrors.KickstartParseError, e:
74 raise errors.KsError("Failed to parse kickstart file "
75 "'%s' : %s" % (path, e))
78 def build_name(kscfg, prefix = None, suffix = None, maxlen = None):
79 """Construct and return an image name string.
81 This is a utility function to help create sensible name and fslabel
82 strings. The name is constructed using the sans-prefix-and-extension
83 kickstart filename and the supplied prefix and suffix.
85 If the name exceeds the maxlen length supplied, the prefix is first dropped
86 and then the kickstart filename portion is reduced until it fits. In other
87 words, the suffix takes precedence over the kickstart portion and the
88 kickstart portion takes precedence over the prefix.
90 kscfg -- a path to a kickstart file
91 prefix -- a prefix to prepend to the name; defaults to None, which causes
93 suffix -- a suffix to append to the name; defaults to None, which causes
94 a YYYYMMDDHHMM suffix to be used
95 maxlen -- the maximum length for the returned string; defaults to None,
96 which means there is no restriction on the name length
98 Note, if maxlen is less then the len(suffix), you get to keep both pieces.
101 name = os.path.basename(kscfg)
102 idx = name.rfind('.')
109 suffix = time.strftime("%Y%m%d%H%M")
111 if name.startswith(prefix):
112 name = name[len(prefix):]
114 ret = prefix + name + "-" + suffix
115 if not maxlen is None and len(ret) > maxlen:
116 ret = name[:maxlen - len(suffix) - 1] + "-" + suffix
120 class KickstartConfig(object):
121 """A base class for applying kickstart configurations to a system."""
122 def __init__(self, instroot):
123 self.instroot = instroot
125 def path(self, subpath):
126 return self.instroot + subpath
129 os.chroot(self.instroot)
132 def call(self, args):
133 if not os.path.exists("%s/%s" %(self.instroot, args[0])):
134 msger.warning("%s/%s" %(self.instroot, args[0]))
135 raise errors.KsError("Unable to run %s!" %(args))
136 subprocess.call(args, preexec_fn = self.chroot)
141 class LanguageConfig(KickstartConfig):
142 """A class to apply a kickstart language configuration to a system."""
143 def apply(self, kslang):
144 lang = kslang.lang or "en_US.UTF-8"
146 f = open(self.path("/etc/sysconfig/i18n"), "w+")
147 f.write("LANG=\"" + lang + "\"\n")
150 class KeyboardConfig(KickstartConfig):
151 """A class to apply a kickstart keyboard configuration to a system."""
152 def apply(self, kskeyboard):
155 # should this impact the X keyboard config too?
156 # or do we want to make X be able to do this mapping?
158 #k = rhpl.keyboard.Keyboard()
159 #if kskeyboard.keyboard:
160 # k.set(kskeyboard.keyboard)
161 #k.write(self.instroot)
164 class TimezoneConfig(KickstartConfig):
165 """A class to apply a kickstart timezone configuration to a system."""
166 def apply(self, kstimezone):
167 tz = kstimezone.timezone or "America/New_York"
168 utc = str(kstimezone.isUtc)
170 f = open(self.path("/etc/sysconfig/clock"), "w+")
171 f.write("ZONE=\"" + tz + "\"\n")
172 f.write("UTC=" + utc + "\n")
175 shutil.copyfile(self.path("/usr/share/zoneinfo/%s" %(tz,)),
176 self.path("/etc/localtime"))
177 except (IOError, OSError), (errno, msg):
178 raise errors.KsError("Error copying timezone info: %s" %(msg,))
181 class AuthConfig(KickstartConfig):
182 """A class to apply a kickstart authconfig configuration to a system."""
183 def apply(self, ksauthconfig):
184 auth = ksauthconfig.authconfig or "--useshadow --enablemd5"
185 args = ["/usr/share/authconfig/authconfig.py", "--update", "--nostart"]
186 self.call(args + auth.split())
188 class FirewallConfig(KickstartConfig):
189 """A class to apply a kickstart firewall configuration to a system."""
190 def apply(self, ksfirewall):
192 # FIXME: should handle the rest of the options
194 if not os.path.exists(self.path("/usr/sbin/lokkit")):
196 if ksfirewall.enabled:
199 status = "--disabled"
201 self.call(["/usr/sbin/lokkit",
202 "-f", "--quiet", "--nostart", status])
204 class RootPasswordConfig(KickstartConfig):
205 """A class to apply a kickstart root password configuration to a system."""
207 self.call(["/usr/bin/passwd", "-d", "root"])
209 def set_encrypted(self, password):
210 self.call(["/usr/sbin/usermod", "-p", password, "root"])
212 def set_unencrypted(self, password):
213 for p in ("/bin/echo", "/usr/sbin/chpasswd"):
214 if not os.path.exists("%s/%s" %(self.instroot, p)):
215 raise errors.KsError("Unable to set unencrypted password due to lack of %s" % p)
217 p1 = subprocess.Popen(["/bin/echo", "root:%s" %password],
218 stdout = subprocess.PIPE,
219 preexec_fn = self.chroot)
220 p2 = subprocess.Popen(["/usr/sbin/chpasswd", "-m"],
222 stdout = subprocess.PIPE,
223 preexec_fn = self.chroot)
226 def apply(self, ksrootpw):
227 if ksrootpw.isCrypted:
228 self.set_encrypted(ksrootpw.password)
229 elif ksrootpw.password != "":
230 self.set_unencrypted(ksrootpw.password)
234 class UserConfig(KickstartConfig):
235 def set_empty_passwd(self, user):
236 self.call(["/usr/bin/passwd", "-d", user])
238 def set_encrypted_passwd(self, user, password):
239 self.call(["/usr/sbin/usermod", "-p", "%s" % password, user])
241 def set_unencrypted_passwd(self, user, password):
242 for p in ("/bin/echo", "/usr/sbin/chpasswd"):
243 if not os.path.exists("%s/%s" %(self.instroot, p)):
244 raise errors.KsError("Unable to set unencrypted password due to lack of %s" % p)
246 p1 = subprocess.Popen(["/bin/echo", "%s:%s" %(user, password)],
247 stdout = subprocess.PIPE,
248 preexec_fn = self.chroot)
249 p2 = subprocess.Popen(["/usr/sbin/chpasswd", "-m"],
251 stdout = subprocess.PIPE,
252 preexec_fn = self.chroot)
255 def addUser(self, userconfig):
256 args = [ "/usr/sbin/useradd" ]
257 if userconfig.groups:
258 args += [ "--groups", string.join(userconfig.groups, ",") ]
260 args.append(userconfig.name)
261 dev_null = os.open("/dev/null", os.O_WRONLY)
262 subprocess.call(args,
265 preexec_fn = self.chroot)
267 if userconfig.password not in (None, ""):
268 if userconfig.isCrypted:
269 self.set_encrypted_passwd(userconfig.name, userconfig.password)
271 self.set_unencrypted_passwd(userconfig.name, userconfig.password)
273 self.set_empty_passwd(userconfig.name)
275 raise errors.KsError("Invalid kickstart command: %s" % userconfig.__str__())
277 def apply(self, user):
278 for userconfig in user.userList:
280 self.addUser(userconfig)
284 class ServicesConfig(KickstartConfig):
285 """A class to apply a kickstart services configuration to a system."""
286 def apply(self, ksservices):
287 if not os.path.exists(self.path("/sbin/chkconfig")):
289 for s in ksservices.enabled:
290 self.call(["/sbin/chkconfig", s, "on"])
291 for s in ksservices.disabled:
292 self.call(["/sbin/chkconfig", s, "off"])
294 class XConfig(KickstartConfig):
295 """A class to apply a kickstart X configuration to a system."""
296 def apply(self, ksxconfig):
298 f = open(self.path("/etc/inittab"), "rw+")
300 buf = buf.replace("id:3:initdefault", "id:5:initdefault")
304 if ksxconfig.defaultdesktop:
305 f = open(self.path("/etc/sysconfig/desktop"), "w")
306 f.write("DESKTOP="+ksxconfig.defaultdesktop+"\n")
309 class DesktopConfig(KickstartConfig):
310 """A class to apply a kickstart desktop configuration to a system."""
311 def apply(self, ksdesktop):
312 if ksdesktop.defaultdesktop:
313 f = open(self.path("/etc/sysconfig/desktop"), "w")
314 f.write("DESKTOP="+ksdesktop.defaultdesktop+"\n")
316 if os.path.exists(self.path("/etc/gdm/custom.conf")):
317 f = open(self.path("/etc/skel/.dmrc"), "w")
318 f.write("[Desktop]\n")
319 f.write("Session="+ksdesktop.defaultdesktop.lower()+"\n")
321 if ksdesktop.session:
322 if os.path.exists(self.path("/etc/sysconfig/uxlaunch")):
323 f = open(self.path("/etc/sysconfig/uxlaunch"), "a+")
324 f.write("session="+ksdesktop.session.lower()+"\n")
326 if ksdesktop.autologinuser:
327 f = open(self.path("/etc/sysconfig/desktop"), "a+")
328 f.write("AUTOLOGIN_USER=" + ksdesktop.autologinuser + "\n")
330 if ksdesktop.session:
331 if os.path.exists(self.path("/etc/sysconfig/uxlaunch")):
332 f = open(self.path("/etc/sysconfig/uxlaunch"), "a+")
333 f.write("user="+ksdesktop.autologinuser+"\n")
335 if os.path.exists(self.path("/etc/gdm/custom.conf")):
336 f = open(self.path("/etc/gdm/custom.conf"), "w")
337 f.write("[daemon]\n")
338 f.write("AutomaticLoginEnable=true\n")
339 f.write("AutomaticLogin=" + ksdesktop.autologinuser + "\n")
342 class MoblinRepoConfig(KickstartConfig):
343 """A class to apply a kickstart desktop configuration to a system."""
344 def __create_repo_section(self, repo, type, fd):
347 reposuffix = {"base":"", "debuginfo":"-debuginfo", "source":"-source"}
348 reponame = repo.name + reposuffix[type]
351 baseurl = repo.baseurl
353 mirrorlist = repo.mirrorlist
354 elif type == "debuginfo":
356 if repo.baseurl.endswith("/"):
357 baseurl = os.path.dirname(os.path.dirname(repo.baseurl))
359 baseurl = os.path.dirname(repo.baseurl)
362 variant = repo.mirrorlist[repo.mirrorlist.find("$"):]
363 mirrorlist = repo.mirrorlist[0:repo.mirrorlist.find("$")]
364 mirrorlist += "debug" + "-" + variant
365 elif type == "source":
367 if repo.baseurl.endswith("/"):
368 baseurl = os.path.dirname(os.path.dirname(os.path.dirname(repo.baseurl)))
370 baseurl = os.path.dirname(os.path.dirname(repo.baseurl))
373 variant = repo.mirrorlist[repo.mirrorlist.find("$"):]
374 mirrorlist = repo.mirrorlist[0:repo.mirrorlist.find("$")]
375 mirrorlist += "source" + "-" + variant
377 fd.write("[" + reponame + "]\n")
378 fd.write("name=" + reponame + "\n")
379 fd.write("failovermethod=priority\n")
381 fd.write("baseurl=" + baseurl + "\n")
383 fd.write("mirrorlist=" + mirrorlist + "\n")
384 """ Skip saving proxy settings """
386 # fd.write("proxy=" + repo.proxy + "\n")
387 #if repo.proxy_username:
388 # fd.write("proxy_username=" + repo.proxy_username + "\n")
389 #if repo.proxy_password:
390 # fd.write("proxy_password=" + repo.proxy_password + "\n")
392 fd.write("gpgkey=" + repo.gpgkey + "\n")
393 fd.write("gpgcheck=1\n")
395 fd.write("gpgcheck=0\n")
396 if type == "source" or type == "debuginfo" or repo.disable:
397 fd.write("enabled=0\n")
399 fd.write("enabled=1\n")
402 def __create_repo_file(self, repo, repodir):
403 if not os.path.exists(self.path(repodir)):
404 fs.makedirs(self.path(repodir))
405 f = open(self.path(repodir + "/" + repo.name + ".repo"), "w")
406 self.__create_repo_section(repo, "base", f)
408 self.__create_repo_section(repo, "debuginfo", f)
410 self.__create_repo_section(repo, "source", f)
413 def apply(self, ksrepo, repodata):
414 for repo in ksrepo.repoList:
416 #self.__create_repo_file(repo, "/etc/yum.repos.d")
417 self.__create_repo_file(repo, "/etc/zypp/repos.d")
418 """ Import repo gpg keys """
420 dev_null = os.open("/dev/null", os.O_WRONLY)
421 for repo in repodata:
423 subprocess.call([fs.find_binary_path("rpm"), "--root=%s" % self.instroot, "--import", repo['repokey']],
424 stdout = dev_null, stderr = dev_null)
427 class RPMMacroConfig(KickstartConfig):
428 """A class to apply the specified rpm macros to the filesystem"""
432 if not os.path.exists(self.path("/etc/rpm")):
433 os.mkdir(self.path("/etc/rpm"))
434 f = open(self.path("/etc/rpm/macros.imgcreate"), "w+")
436 f.write("%_excludedocs 1\n")
437 f.write("%__file_context_path %{nil}\n")
438 if inst_langs(ks) != None:
439 f.write("%_install_langs ")
440 f.write(inst_langs(ks))
444 class NetworkConfig(KickstartConfig):
445 """A class to apply a kickstart network configuration to a system."""
446 def write_ifcfg(self, network):
447 p = self.path("/etc/sysconfig/network-scripts/ifcfg-" + network.device)
452 f.write("DEVICE=%s\n" % network.device)
453 f.write("BOOTPROTO=%s\n" % network.bootProto)
455 if network.bootProto.lower() == "static":
457 f.write("IPADDR=%s\n" % network.ip)
459 f.write("NETMASK=%s\n" % network.netmask)
462 f.write("ONBOOT=on\n")
464 f.write("ONBOOT=off\n")
467 f.write("ESSID=%s\n" % network.essid)
470 if network.ethtool.find("autoneg") == -1:
471 network.ethtool = "autoneg off " + network.ethtool
472 f.write("ETHTOOL_OPTS=%s\n" % network.ethtool)
474 if network.bootProto.lower() == "dhcp":
476 f.write("DHCP_HOSTNAME=%s\n" % network.hostname)
477 if network.dhcpclass:
478 f.write("DHCP_CLASSID=%s\n" % network.dhcpclass)
481 f.write("MTU=%s\n" % network.mtu)
485 def write_wepkey(self, network):
486 if not network.wepkey:
489 p = self.path("/etc/sysconfig/network-scripts/keys-" + network.device)
492 f.write("KEY=%s\n" % network.wepkey)
495 def write_sysconfig(self, useipv6, hostname, gateway):
496 path = self.path("/etc/sysconfig/network")
500 f.write("NETWORKING=yes\n")
503 f.write("NETWORKING_IPV6=yes\n")
505 f.write("NETWORKING_IPV6=no\n")
508 f.write("HOSTNAME=%s\n" % hostname)
510 f.write("HOSTNAME=localhost.localdomain\n")
513 f.write("GATEWAY=%s\n" % gateway)
517 def write_hosts(self, hostname):
519 if hostname and hostname != "localhost.localdomain":
520 localline += hostname + " "
521 l = hostname.split(".")
523 localline += l[0] + " "
524 localline += "localhost.localdomain localhost"
526 path = self.path("/etc/hosts")
529 f.write("127.0.0.1\t\t%s\n" % localline)
530 f.write("::1\t\tlocalhost6.localdomain6 localhost6\n")
533 def write_resolv(self, nodns, nameservers):
534 if nodns or not nameservers:
537 path = self.path("/etc/resolv.conf")
541 for ns in (nameservers):
543 f.write("nameserver %s\n" % ns)
547 def apply(self, ksnet):
548 fs.makedirs(self.path("/etc/sysconfig/network-scripts"))
556 for network in ksnet.network:
557 if not network.device:
558 raise errors.KsError("No --device specified with "
559 "network kickstart command")
561 if (network.onboot and network.bootProto.lower() != "dhcp" and
562 not (network.ip and network.netmask)):
563 raise errors.KsError("No IP address and/or netmask "
564 "specified with static "
565 "configuration for '%s'" %
568 self.write_ifcfg(network)
569 self.write_wepkey(network)
577 hostname = network.hostname
579 gateway = network.gateway
581 if network.nameserver:
582 nameservers = network.nameserver.split(",")
584 self.write_sysconfig(useipv6, hostname, gateway)
585 self.write_hosts(hostname)
586 self.write_resolv(nodns, nameservers)
589 def get_image_size(ks, default = None):
591 for p in ks.handler.partition.partitions:
592 if p.mountpoint == "/" and p.size:
595 return int(__size) * 1024L * 1024L
599 def get_image_fstype(ks, default = None):
600 for p in ks.handler.partition.partitions:
601 if p.mountpoint == "/" and p.fstype:
605 def get_image_fsopts(ks, default = None):
606 for p in ks.handler.partition.partitions:
607 if p.mountpoint == "/" and p.fsopts:
613 if isinstance(ks.handler.device, kscommands.device.FC3_Device):
614 devices.append(ks.handler.device)
616 devices.extend(ks.handler.device.deviceList)
619 for device in devices:
620 if not device.moduleName:
622 modules.extend(device.moduleName.split(":"))
626 def get_timeout(ks, default = None):
627 if not hasattr(ks.handler.bootloader, "timeout"):
629 if ks.handler.bootloader.timeout is None:
631 return int(ks.handler.bootloader.timeout)
633 def get_kernel_args(ks, default = "ro liveimg"):
634 if not hasattr(ks.handler.bootloader, "appendLine"):
636 if ks.handler.bootloader.appendLine is None:
638 return "%s %s" %(default, ks.handler.bootloader.appendLine)
640 def get_menu_args(ks, default = "liveinst"):
641 if not hasattr(ks.handler.bootloader, "menus"):
643 if ks.handler.bootloader.menus in (None, ""):
645 return "%s" % ks.handler.bootloader.menus
647 def get_default_kernel(ks, default = None):
648 if not hasattr(ks.handler.bootloader, "default"):
650 if not ks.handler.bootloader.default:
652 return ks.handler.bootloader.default
654 def get_repos(ks, repo_urls = {}):
656 for repo in ks.handler.repo.repoList:
658 if hasattr(repo, "includepkgs"):
659 inc.extend(repo.includepkgs)
662 if hasattr(repo, "excludepkgs"):
663 exc.extend(repo.excludepkgs)
665 baseurl = repo.baseurl
666 mirrorlist = repo.mirrorlist
668 if repo.name in repo_urls:
669 baseurl = repo_urls[repo.name]
672 if repos.has_key(repo.name):
673 msger.warning("Overriding already specified repo %s" %(repo.name,))
676 if hasattr(repo, "proxy"):
678 proxy_username = None
679 if hasattr(repo, "proxy_username"):
680 proxy_username = repo.proxy_username
681 proxy_password = None
682 if hasattr(repo, "proxy_password"):
683 proxy_password = repo.proxy_password
684 if hasattr(repo, "debuginfo"):
685 debuginfo = repo.debuginfo
686 if hasattr(repo, "source"):
688 if hasattr(repo, "gpgkey"):
690 if hasattr(repo, "disable"):
691 disable = repo.disable
693 repos[repo.name] = (repo.name, baseurl, mirrorlist, inc, exc, proxy, proxy_username, proxy_password, debuginfo, source, gpgkey, disable)
695 return repos.values()
697 def convert_method_to_repo(ks):
699 ks.handler.repo.methodToRepo()
700 except (AttributeError, kserrors.KickstartError):
703 def get_packages(ks, required = []):
704 return ks.handler.packages.packageList + required
706 def get_groups(ks, required = []):
707 return ks.handler.packages.groupList + required
709 def get_excluded(ks, required = []):
710 return ks.handler.packages.excludedList + required
712 def get_partitions(ks, required = []):
713 return ks.handler.partition.partitions
715 def ignore_missing(ks):
716 return ks.handler.packages.handleMissing == ksconstants.KS_MISSING_IGNORE
718 def exclude_docs(ks):
719 return ks.handler.packages.excludeDocs
722 if hasattr(ks.handler.packages, "instLange"):
723 return ks.handler.packages.instLange
724 elif hasattr(ks.handler.packages, "instLangs"):
725 return ks.handler.packages.instLangs
728 def get_post_scripts(ks):
730 for s in ks.handler.scripts:
731 if s.type != ksparser.KS_SCRIPT_POST:
736 def add_repo(ks, repostr):
737 args = repostr.split()
738 repoobj = ks.handler.repo.parse(args[1:])
739 if repoobj and repoobj not in ks.handler.repo.repoList:
740 ks.handler.repo.repoList.append(repoobj)
742 def remove_all_repos(ks):
743 while len(ks.handler.repo.repoList) != 0:
744 del ks.handler.repo.repoList[0]
746 def remove_duplicate_repos(ks):
750 if len(ks.handler.repo.repoList) < 2:
752 if i >= len(ks.handler.repo.repoList) - 1:
754 name = ks.handler.repo.repoList[i].name
755 baseurl = ks.handler.repo.repoList[i].baseurl
756 if j < len(ks.handler.repo.repoList):
757 if (ks.handler.repo.repoList[j].name == name or \
758 ks.handler.repo.repoList[j].baseurl == baseurl):
759 del ks.handler.repo.repoList[j]
762 if j >= len(ks.handler.repo.repoList):
769 def resolve_groups(creator, repometadata, use_comps = False):
770 pkgmgr = creator.pkgmgr.get_default_pkg_manager
772 if creator.pkgmgr.managers.has_key("zypp") and creator.pkgmgr.managers['zypp'] == pkgmgr:
776 for repo in repometadata:
777 """ Mustn't replace group with package list if repo is ready for the corresponding package manager """
778 if iszypp and repo["patterns"] and not use_comps:
780 if not iszypp and repo["comps"] and use_comps:
784 But we also must handle such cases, use zypp but repo only has comps,
785 use yum but repo only has patterns, use zypp but use_comps is true,
786 use yum but use_comps is false.
790 if (use_comps and repo["comps"]) or (not repo["patterns"] and repo["comps"]):
791 groupfile = repo["comps"]
792 get_pkglist_handler = misc.get_pkglist_in_comps
794 if (not use_comps and repo["patterns"]) or (not repo["comps"] and repo["patterns"]):
795 groupfile = repo["patterns"]
796 get_pkglist_handler = misc.get_pkglist_in_patterns
801 if i >= len(ks.handler.packages.groupList):
803 pkglist = get_pkglist_handler(ks.handler.packages.groupList[i].name, groupfile)
805 del ks.handler.packages.groupList[i]
807 if pkg not in ks.handler.packages.packageList:
808 ks.handler.packages.packageList.append(pkg)