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.
26 from mic.utils import errors
27 from mic.utils import misc
28 from mic.utils import fs_related as fs
31 import pykickstart.commands as kscommands
32 import pykickstart.constants as ksconstants
33 import pykickstart.errors as kserrors
34 import pykickstart.parser as ksparser
35 import pykickstart.version as ksversion
36 from pykickstart.handlers.control import commandMap
37 from pykickstart.handlers.control import dataMap
39 import custom_commands.desktop as desktop
40 import custom_commands.moblinrepo as moblinrepo
41 import custom_commands.micboot as micboot
43 def read_kickstart(path):
44 """Parse a kickstart file and return a KickstartParser instance.
46 This is a simple utility function which takes a path to a kickstart file,
47 parses it and returns a pykickstart KickstartParser instance which can
48 be then passed to an ImageCreator constructor.
50 If an error occurs, a CreatorError exception is thrown.
53 #version = ksversion.makeVersion()
54 #ks = ksparser.KickstartParser(version)
56 using_version = ksversion.DEVEL
57 commandMap[using_version]["desktop"] = desktop.Moblin_Desktop
58 commandMap[using_version]["repo"] = moblinrepo.Moblin_Repo
59 commandMap[using_version]["bootloader"] = micboot.Moblin_Bootloader
60 dataMap[using_version]["RepoData"] = moblinrepo.Moblin_RepoData
61 superclass = ksversion.returnClassForVersion(version=using_version)
63 class KSHandlers(superclass):
64 def __init__(self, mapping={}):
65 superclass.__init__(self, mapping=commandMap[using_version])
67 ks = ksparser.KickstartParser(KSHandlers())
70 ks.readKickstart(path)
71 except IOError, (err, msg):
72 raise errors.KsError("Failed to read kickstart file "
73 "'%s' : %s" % (path, msg))
74 except kserrors.KickstartParseError, e:
75 raise errors.KsError("Failed to parse kickstart file "
76 "'%s' : %s" % (path, e))
79 def build_name(kscfg, prefix = None, suffix = None, maxlen = None):
80 """Construct and return an image name string.
82 This is a utility function to help create sensible name and fslabel
83 strings. The name is constructed using the sans-prefix-and-extension
84 kickstart filename and the supplied prefix and suffix.
86 If the name exceeds the maxlen length supplied, the prefix is first dropped
87 and then the kickstart filename portion is reduced until it fits. In other
88 words, the suffix takes precedence over the kickstart portion and the
89 kickstart portion takes precedence over the prefix.
91 kscfg -- a path to a kickstart file
92 prefix -- a prefix to prepend to the name; defaults to None, which causes
94 suffix -- a suffix to append to the name; defaults to None, which causes
95 a YYYYMMDDHHMM suffix to be used
96 maxlen -- the maximum length for the returned string; defaults to None,
97 which means there is no restriction on the name length
99 Note, if maxlen is less then the len(suffix), you get to keep both pieces.
102 name = os.path.basename(kscfg)
103 idx = name.rfind('.')
110 suffix = time.strftime("%Y%m%d%H%M")
112 if name.startswith(prefix):
113 name = name[len(prefix):]
115 ret = prefix + name + "-" + suffix
116 if not maxlen is None and len(ret) > maxlen:
117 ret = name[:maxlen - len(suffix) - 1] + "-" + suffix
121 class KickstartConfig(object):
122 """A base class for applying kickstart configurations to a system."""
123 def __init__(self, instroot):
124 self.instroot = instroot
126 def path(self, subpath):
127 return self.instroot + subpath
130 os.chroot(self.instroot)
133 def call(self, args):
134 if not os.path.exists("%s/%s" %(self.instroot, args[0])):
135 msger.warning("%s/%s" %(self.instroot, args[0]))
136 raise errors.KsError("Unable to run %s!" %(args))
137 subprocess.call(args, preexec_fn = self.chroot)
142 class LanguageConfig(KickstartConfig):
143 """A class to apply a kickstart language configuration to a system."""
144 def apply(self, kslang):
145 lang = kslang.lang or "en_US.UTF-8"
147 f = open(self.path("/etc/sysconfig/i18n"), "w+")
148 f.write("LANG=\"" + lang + "\"\n")
151 class KeyboardConfig(KickstartConfig):
152 """A class to apply a kickstart keyboard configuration to a system."""
153 def apply(self, kskeyboard):
156 # should this impact the X keyboard config too?
157 # or do we want to make X be able to do this mapping?
159 #k = rhpl.keyboard.Keyboard()
160 #if kskeyboard.keyboard:
161 # k.set(kskeyboard.keyboard)
162 #k.write(self.instroot)
165 class TimezoneConfig(KickstartConfig):
166 """A class to apply a kickstart timezone configuration to a system."""
167 def apply(self, kstimezone):
168 tz = kstimezone.timezone or "America/New_York"
169 utc = str(kstimezone.isUtc)
171 f = open(self.path("/etc/sysconfig/clock"), "w+")
172 f.write("ZONE=\"" + tz + "\"\n")
173 f.write("UTC=" + utc + "\n")
176 shutil.copyfile(self.path("/usr/share/zoneinfo/%s" %(tz,)),
177 self.path("/etc/localtime"))
178 except (IOError, OSError), (errno, msg):
179 raise errors.KsError("Error copying timezone info: %s" %(msg,))
182 class AuthConfig(KickstartConfig):
183 """A class to apply a kickstart authconfig configuration to a system."""
184 def apply(self, ksauthconfig):
185 auth = ksauthconfig.authconfig or "--useshadow --enablemd5"
186 args = ["/usr/share/authconfig/authconfig.py", "--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):
193 # FIXME: should handle the rest of the options
195 if not os.path.exists(self.path("/usr/sbin/lokkit")):
197 if ksfirewall.enabled:
200 status = "--disabled"
202 self.call(["/usr/sbin/lokkit",
203 "-f", "--quiet", "--nostart", status])
205 class RootPasswordConfig(KickstartConfig):
206 """A class to apply a kickstart root password configuration to a system."""
208 self.call(["/usr/bin/passwd", "-d", "root"])
210 def set_encrypted(self, password):
211 self.call(["/usr/sbin/usermod", "-p", password, "root"])
213 def set_unencrypted(self, password):
214 for p in ("/bin/echo", "/usr/sbin/chpasswd"):
215 if not os.path.exists("%s/%s" %(self.instroot, p)):
216 raise errors.KsError("Unable to set unencrypted password due to lack of %s" % p)
218 p1 = subprocess.Popen(["/bin/echo", "root:%s" %password],
219 stdout = subprocess.PIPE,
220 preexec_fn = self.chroot)
221 p2 = subprocess.Popen(["/usr/sbin/chpasswd", "-m"],
223 stdout = subprocess.PIPE,
224 preexec_fn = self.chroot)
227 def apply(self, ksrootpw):
228 if ksrootpw.isCrypted:
229 self.set_encrypted(ksrootpw.password)
230 elif ksrootpw.password != "":
231 self.set_unencrypted(ksrootpw.password)
235 class UserConfig(KickstartConfig):
236 def set_empty_passwd(self, user):
237 self.call(["/usr/bin/passwd", "-d", user])
239 def set_encrypted_passwd(self, user, password):
240 self.call(["/usr/sbin/usermod", "-p", "%s" % password, user])
242 def set_unencrypted_passwd(self, user, password):
243 for p in ("/bin/echo", "/usr/sbin/chpasswd"):
244 if not os.path.exists("%s/%s" %(self.instroot, p)):
245 raise errors.KsError("Unable to set unencrypted password due to lack of %s" % p)
247 p1 = subprocess.Popen(["/bin/echo", "%s:%s" %(user, password)],
248 stdout = subprocess.PIPE,
249 preexec_fn = self.chroot)
250 p2 = subprocess.Popen(["/usr/sbin/chpasswd", "-m"],
252 stdout = subprocess.PIPE,
253 preexec_fn = self.chroot)
256 def addUser(self, userconfig):
257 args = [ "/usr/sbin/useradd" ]
258 if userconfig.groups:
259 args += [ "--groups", string.join(userconfig.groups, ",") ]
261 args.append(userconfig.name)
262 dev_null = os.open("/dev/null", os.O_WRONLY)
263 subprocess.call(args,
266 preexec_fn = self.chroot)
268 if userconfig.password not in (None, ""):
269 if userconfig.isCrypted:
270 self.set_encrypted_passwd(userconfig.name, userconfig.password)
272 self.set_unencrypted_passwd(userconfig.name, userconfig.password)
274 self.set_empty_passwd(userconfig.name)
276 raise errors.KsError("Invalid kickstart command: %s" % userconfig.__str__())
278 def apply(self, user):
279 for userconfig in user.userList:
281 self.addUser(userconfig)
285 class ServicesConfig(KickstartConfig):
286 """A class to apply a kickstart services configuration to a system."""
287 def apply(self, ksservices):
288 if not os.path.exists(self.path("/sbin/chkconfig")):
290 for s in ksservices.enabled:
291 self.call(["/sbin/chkconfig", s, "on"])
292 for s in ksservices.disabled:
293 self.call(["/sbin/chkconfig", s, "off"])
295 class XConfig(KickstartConfig):
296 """A class to apply a kickstart X configuration to a system."""
297 def apply(self, ksxconfig):
299 f = open(self.path("/etc/inittab"), "rw+")
301 buf = buf.replace("id:3:initdefault", "id:5:initdefault")
305 if ksxconfig.defaultdesktop:
306 f = open(self.path("/etc/sysconfig/desktop"), "w")
307 f.write("DESKTOP="+ksxconfig.defaultdesktop+"\n")
310 class DesktopConfig(KickstartConfig):
311 """A class to apply a kickstart desktop configuration to a system."""
312 def apply(self, ksdesktop):
313 if ksdesktop.defaultdesktop:
314 f = open(self.path("/etc/sysconfig/desktop"), "w")
315 f.write("DESKTOP="+ksdesktop.defaultdesktop+"\n")
317 if os.path.exists(self.path("/etc/gdm/custom.conf")):
318 f = open(self.path("/etc/skel/.dmrc"), "w")
319 f.write("[Desktop]\n")
320 f.write("Session="+ksdesktop.defaultdesktop.lower()+"\n")
322 if ksdesktop.session:
323 if os.path.exists(self.path("/etc/sysconfig/uxlaunch")):
324 f = open(self.path("/etc/sysconfig/uxlaunch"), "a+")
325 f.write("session="+ksdesktop.session.lower()+"\n")
327 if ksdesktop.autologinuser:
328 f = open(self.path("/etc/sysconfig/desktop"), "a+")
329 f.write("AUTOLOGIN_USER=" + ksdesktop.autologinuser + "\n")
331 if ksdesktop.session:
332 if os.path.exists(self.path("/etc/sysconfig/uxlaunch")):
333 f = open(self.path("/etc/sysconfig/uxlaunch"), "a+")
334 f.write("user="+ksdesktop.autologinuser+"\n")
336 if os.path.exists(self.path("/etc/gdm/custom.conf")):
337 f = open(self.path("/etc/gdm/custom.conf"), "w")
338 f.write("[daemon]\n")
339 f.write("AutomaticLoginEnable=true\n")
340 f.write("AutomaticLogin=" + ksdesktop.autologinuser + "\n")
343 class MoblinRepoConfig(KickstartConfig):
344 """A class to apply a kickstart desktop configuration to a system."""
345 def __create_repo_section(self, repo, type, fd):
348 reposuffix = {"base":"", "debuginfo":"-debuginfo", "source":"-source"}
349 reponame = repo.name + reposuffix[type]
352 baseurl = repo.baseurl
354 mirrorlist = repo.mirrorlist
355 elif type == "debuginfo":
357 if repo.baseurl.endswith("/"):
358 baseurl = os.path.dirname(os.path.dirname(repo.baseurl))
360 baseurl = os.path.dirname(repo.baseurl)
363 variant = repo.mirrorlist[repo.mirrorlist.find("$"):]
364 mirrorlist = repo.mirrorlist[0:repo.mirrorlist.find("$")]
365 mirrorlist += "debug" + "-" + variant
366 elif type == "source":
368 if repo.baseurl.endswith("/"):
369 baseurl = os.path.dirname(os.path.dirname(os.path.dirname(repo.baseurl)))
371 baseurl = os.path.dirname(os.path.dirname(repo.baseurl))
374 variant = repo.mirrorlist[repo.mirrorlist.find("$"):]
375 mirrorlist = repo.mirrorlist[0:repo.mirrorlist.find("$")]
376 mirrorlist += "source" + "-" + variant
378 fd.write("[" + reponame + "]\n")
379 fd.write("name=" + reponame + "\n")
380 fd.write("failovermethod=priority\n")
382 fd.write("baseurl=" + baseurl + "\n")
384 fd.write("mirrorlist=" + mirrorlist + "\n")
385 """ Skip saving proxy settings """
387 # fd.write("proxy=" + repo.proxy + "\n")
388 #if repo.proxy_username:
389 # fd.write("proxy_username=" + repo.proxy_username + "\n")
390 #if repo.proxy_password:
391 # fd.write("proxy_password=" + repo.proxy_password + "\n")
393 fd.write("gpgkey=" + repo.gpgkey + "\n")
394 fd.write("gpgcheck=1\n")
396 fd.write("gpgcheck=0\n")
397 if type == "source" or type == "debuginfo" or repo.disable:
398 fd.write("enabled=0\n")
400 fd.write("enabled=1\n")
403 def __create_repo_file(self, repo, repodir):
404 if not os.path.exists(self.path(repodir)):
405 fs.makedirs(self.path(repodir))
406 f = open(self.path(repodir + "/" + repo.name + ".repo"), "w")
407 self.__create_repo_section(repo, "base", f)
409 self.__create_repo_section(repo, "debuginfo", f)
411 self.__create_repo_section(repo, "source", f)
414 def apply(self, ksrepo, repodata):
415 for repo in ksrepo.repoList:
417 #self.__create_repo_file(repo, "/etc/yum.repos.d")
418 self.__create_repo_file(repo, "/etc/zypp/repos.d")
419 """ Import repo gpg keys """
421 dev_null = os.open("/dev/null", os.O_WRONLY)
422 for repo in repodata:
424 subprocess.call([fs.find_binary_path("rpm"), "--root=%s" % self.instroot, "--import", repo['repokey']],
425 stdout = dev_null, stderr = dev_null)
428 class RPMMacroConfig(KickstartConfig):
429 """A class to apply the specified rpm macros to the filesystem"""
433 if not os.path.exists(self.path("/etc/rpm")):
434 os.mkdir(self.path("/etc/rpm"))
435 f = open(self.path("/etc/rpm/macros.imgcreate"), "w+")
437 f.write("%_excludedocs 1\n")
438 f.write("%__file_context_path %{nil}\n")
439 if inst_langs(ks) != None:
440 f.write("%_install_langs ")
441 f.write(inst_langs(ks))
445 class NetworkConfig(KickstartConfig):
446 """A class to apply a kickstart network configuration to a system."""
447 def write_ifcfg(self, network):
448 p = self.path("/etc/sysconfig/network-scripts/ifcfg-" + network.device)
453 f.write("DEVICE=%s\n" % network.device)
454 f.write("BOOTPROTO=%s\n" % network.bootProto)
456 if network.bootProto.lower() == "static":
458 f.write("IPADDR=%s\n" % network.ip)
460 f.write("NETMASK=%s\n" % network.netmask)
463 f.write("ONBOOT=on\n")
465 f.write("ONBOOT=off\n")
468 f.write("ESSID=%s\n" % network.essid)
471 if network.ethtool.find("autoneg") == -1:
472 network.ethtool = "autoneg off " + network.ethtool
473 f.write("ETHTOOL_OPTS=%s\n" % network.ethtool)
475 if network.bootProto.lower() == "dhcp":
477 f.write("DHCP_HOSTNAME=%s\n" % network.hostname)
478 if network.dhcpclass:
479 f.write("DHCP_CLASSID=%s\n" % network.dhcpclass)
482 f.write("MTU=%s\n" % network.mtu)
486 def write_wepkey(self, network):
487 if not network.wepkey:
490 p = self.path("/etc/sysconfig/network-scripts/keys-" + network.device)
493 f.write("KEY=%s\n" % network.wepkey)
496 def write_sysconfig(self, useipv6, hostname, gateway):
497 path = self.path("/etc/sysconfig/network")
501 f.write("NETWORKING=yes\n")
504 f.write("NETWORKING_IPV6=yes\n")
506 f.write("NETWORKING_IPV6=no\n")
509 f.write("HOSTNAME=%s\n" % hostname)
511 f.write("HOSTNAME=localhost.localdomain\n")
514 f.write("GATEWAY=%s\n" % gateway)
518 def write_hosts(self, hostname):
520 if hostname and hostname != "localhost.localdomain":
521 localline += hostname + " "
522 l = hostname.split(".")
524 localline += l[0] + " "
525 localline += "localhost.localdomain localhost"
527 path = self.path("/etc/hosts")
530 f.write("127.0.0.1\t\t%s\n" % localline)
531 f.write("::1\t\tlocalhost6.localdomain6 localhost6\n")
534 def write_resolv(self, nodns, nameservers):
535 if nodns or not nameservers:
538 path = self.path("/etc/resolv.conf")
542 for ns in (nameservers):
544 f.write("nameserver %s\n" % ns)
548 def apply(self, ksnet):
549 fs.makedirs(self.path("/etc/sysconfig/network-scripts"))
557 for network in ksnet.network:
558 if not network.device:
559 raise errors.KsError("No --device specified with "
560 "network kickstart command")
562 if (network.onboot and network.bootProto.lower() != "dhcp" and
563 not (network.ip and network.netmask)):
564 raise errors.KsError("No IP address and/or netmask "
565 "specified with static "
566 "configuration for '%s'" %
569 self.write_ifcfg(network)
570 self.write_wepkey(network)
578 hostname = network.hostname
580 gateway = network.gateway
582 if network.nameserver:
583 nameservers = network.nameserver.split(",")
585 self.write_sysconfig(useipv6, hostname, gateway)
586 self.write_hosts(hostname)
587 self.write_resolv(nodns, nameservers)
590 def get_image_size(ks, default = None):
592 for p in ks.handler.partition.partitions:
593 if p.mountpoint == "/" and p.size:
596 return int(__size) * 1024L * 1024L
600 def get_image_fstype(ks, default = None):
601 for p in ks.handler.partition.partitions:
602 if p.mountpoint == "/" and p.fstype:
606 def get_image_fsopts(ks, default = None):
607 for p in ks.handler.partition.partitions:
608 if p.mountpoint == "/" and p.fsopts:
614 if isinstance(ks.handler.device, kscommands.device.FC3_Device):
615 devices.append(ks.handler.device)
617 devices.extend(ks.handler.device.deviceList)
620 for device in devices:
621 if not device.moduleName:
623 modules.extend(device.moduleName.split(":"))
627 def get_timeout(ks, default = None):
628 if not hasattr(ks.handler.bootloader, "timeout"):
630 if ks.handler.bootloader.timeout is None:
632 return int(ks.handler.bootloader.timeout)
634 def get_kernel_args(ks, default = "ro liveimg"):
635 if not hasattr(ks.handler.bootloader, "appendLine"):
637 if ks.handler.bootloader.appendLine is None:
639 return "%s %s" %(default, ks.handler.bootloader.appendLine)
641 def get_menu_args(ks, default = "liveinst"):
642 if not hasattr(ks.handler.bootloader, "menus"):
644 if ks.handler.bootloader.menus in (None, ""):
646 return "%s" % ks.handler.bootloader.menus
648 def get_default_kernel(ks, default = None):
649 if not hasattr(ks.handler.bootloader, "default"):
651 if not ks.handler.bootloader.default:
653 return ks.handler.bootloader.default
655 def get_repos(ks, repo_urls = {}):
657 for repo in ks.handler.repo.repoList:
659 if hasattr(repo, "includepkgs"):
660 inc.extend(repo.includepkgs)
663 if hasattr(repo, "excludepkgs"):
664 exc.extend(repo.excludepkgs)
666 baseurl = repo.baseurl
667 mirrorlist = repo.mirrorlist
669 if repo.name in repo_urls:
670 baseurl = repo_urls[repo.name]
673 if repos.has_key(repo.name):
674 logging.warn("Overriding already specified repo %s" %(repo.name,))
677 if hasattr(repo, "proxy"):
679 proxy_username = None
680 if hasattr(repo, "proxy_username"):
681 proxy_username = repo.proxy_username
682 proxy_password = None
683 if hasattr(repo, "proxy_password"):
684 proxy_password = repo.proxy_password
685 if hasattr(repo, "debuginfo"):
686 debuginfo = repo.debuginfo
687 if hasattr(repo, "source"):
689 if hasattr(repo, "gpgkey"):
691 if hasattr(repo, "disable"):
692 disable = repo.disable
694 repos[repo.name] = (repo.name, baseurl, mirrorlist, inc, exc, proxy, proxy_username, proxy_password, debuginfo, source, gpgkey, disable)
696 return repos.values()
698 def convert_method_to_repo(ks):
700 ks.handler.repo.methodToRepo()
701 except (AttributeError, kserrors.KickstartError):
704 def get_packages(ks, required = []):
705 return ks.handler.packages.packageList + required
707 def get_groups(ks, required = []):
708 return ks.handler.packages.groupList + required
710 def get_excluded(ks, required = []):
711 return ks.handler.packages.excludedList + required
713 def get_partitions(ks, required = []):
714 return ks.handler.partition.partitions
716 def ignore_missing(ks):
717 return ks.handler.packages.handleMissing == ksconstants.KS_MISSING_IGNORE
719 def exclude_docs(ks):
720 return ks.handler.packages.excludeDocs
723 if hasattr(ks.handler.packages, "instLange"):
724 return ks.handler.packages.instLange
725 elif hasattr(ks.handler.packages, "instLangs"):
726 return ks.handler.packages.instLangs
729 def get_post_scripts(ks):
731 for s in ks.handler.scripts:
732 if s.type != ksparser.KS_SCRIPT_POST:
737 def add_repo(ks, repostr):
738 args = repostr.split()
739 repoobj = ks.handler.repo.parse(args[1:])
740 if repoobj and repoobj not in ks.handler.repo.repoList:
741 ks.handler.repo.repoList.append(repoobj)
743 def remove_all_repos(ks):
744 while len(ks.handler.repo.repoList) != 0:
745 del ks.handler.repo.repoList[0]
747 def remove_duplicate_repos(ks):
751 if len(ks.handler.repo.repoList) < 2:
753 if i >= len(ks.handler.repo.repoList) - 1:
755 name = ks.handler.repo.repoList[i].name
756 baseurl = ks.handler.repo.repoList[i].baseurl
757 if j < len(ks.handler.repo.repoList):
758 if (ks.handler.repo.repoList[j].name == name or \
759 ks.handler.repo.repoList[j].baseurl == baseurl):
760 del ks.handler.repo.repoList[j]
763 if j >= len(ks.handler.repo.repoList):
770 def resolve_groups(creator, repometadata, use_comps = False):
771 pkgmgr = creator.pkgmgr.get_default_pkg_manager
773 if creator.pkgmgr.managers.has_key("zypp") and creator.pkgmgr.managers['zypp'] == pkgmgr:
777 for repo in repometadata:
778 """ Mustn't replace group with package list if repo is ready for the corresponding package manager """
779 if iszypp and repo["patterns"] and not use_comps:
781 if not iszypp and repo["comps"] and use_comps:
785 But we also must handle such cases, use zypp but repo only has comps,
786 use yum but repo only has patterns, use zypp but use_comps is true,
787 use yum but use_comps is false.
791 if (use_comps and repo["comps"]) or (not repo["patterns"] and repo["comps"]):
792 groupfile = repo["comps"]
793 get_pkglist_handler = misc.get_pkglist_in_comps
795 if (not use_comps and repo["patterns"]) or (not repo["comps"] and repo["patterns"]):
796 groupfile = repo["patterns"]
797 get_pkglist_handler = misc.get_pkglist_in_patterns
802 if i >= len(ks.handler.packages.groupList):
804 pkglist = get_pkglist_handler(ks.handler.packages.groupList[i].name, groupfile)
806 del ks.handler.packages.groupList[i]
808 if pkg not in ks.handler.packages.packageList:
809 ks.handler.packages.packageList.append(pkg)