3 # Copyright (c) 2007 Red Hat, Inc.
4 # Copyright (c) 2009, 2010, 2011 Intel, Inc.
6 # This program is free software; you can redistribute it and/or modify it
7 # under the terms of the GNU General Public License as published by the Free
8 # Software Foundation; version 2 of the License
10 # This program is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc., 59
17 # Temple Place - Suite 330, Boston, MA 02111-1307, USA.
26 from mic.utils import errors, misc, runner, fs_related as fs
28 import pykickstart.commands as kscommands
29 import pykickstart.constants as ksconstants
30 import pykickstart.errors as kserrors
31 import pykickstart.parser as ksparser
32 import pykickstart.version as ksversion
33 from pykickstart.handlers.control import commandMap
34 from pykickstart.handlers.control import dataMap
36 import custom_commands.desktop as desktop
37 import custom_commands.moblinrepo as moblinrepo
38 import custom_commands.micboot as micboot
40 def read_kickstart(path):
41 """Parse a kickstart file and return a KickstartParser instance.
43 This is a simple utility function which takes a path to a kickstart file,
44 parses it and returns a pykickstart KickstartParser instance which can
45 be then passed to an ImageCreator constructor.
47 If an error occurs, a CreatorError exception is thrown.
50 #version = ksversion.makeVersion()
51 #ks = ksparser.KickstartParser(version)
53 using_version = ksversion.DEVEL
54 commandMap[using_version]["desktop"] = desktop.Moblin_Desktop
55 commandMap[using_version]["repo"] = moblinrepo.Moblin_Repo
56 commandMap[using_version]["bootloader"] = micboot.Moblin_Bootloader
57 dataMap[using_version]["RepoData"] = moblinrepo.Moblin_RepoData
58 superclass = ksversion.returnClassForVersion(version=using_version)
60 class KSHandlers(superclass):
61 def __init__(self, mapping={}):
62 superclass.__init__(self, mapping=commandMap[using_version])
64 ks = ksparser.KickstartParser(KSHandlers())
67 ks.readKickstart(path)
68 except kserrors.KickstartParseError, e:
69 msgptn = re.compile("^\D*(\d+).*(Section does not end with.*)$", re.S)
70 m = msgptn.match(str(e))
74 msger.warning("'%s:%s': %s" % (path, lineno, wrnmsg))
76 raise errors.KsError("'%s': %s" % (path, str(e)))
77 except kserrors.KickstartError, e:
78 raise errors.KsError("'%s': %s" % (path, str(e)))
81 def build_name(kscfg, prefix = None, suffix = None, maxlen = None):
82 """Construct and return an image name string.
84 This is a utility function to help create sensible name and fslabel
85 strings. The name is constructed using the sans-prefix-and-extension
86 kickstart filename and the supplied prefix and suffix.
88 If the name exceeds the maxlen length supplied, the prefix is first dropped
89 and then the kickstart filename portion is reduced until it fits. In other
90 words, the suffix takes precedence over the kickstart portion and the
91 kickstart portion takes precedence over the prefix.
93 kscfg -- a path to a kickstart file
94 prefix -- a prefix to prepend to the name; defaults to None, which causes
96 suffix -- a suffix to append to the name; defaults to None, which causes
97 a YYYYMMDDHHMM suffix to be used
98 maxlen -- the maximum length for the returned string; defaults to None,
99 which means there is no restriction on the name length
101 Note, if maxlen is less then the len(suffix), you get to keep both pieces.
104 name = os.path.basename(kscfg)
105 idx = name.rfind('.')
112 suffix = time.strftime("%Y%m%d%H%M")
114 if name.startswith(prefix):
115 name = name[len(prefix):]
117 ret = prefix + name + "-" + suffix
118 if not maxlen is None and len(ret) > maxlen:
119 ret = name[:maxlen - len(suffix) - 1] + "-" + suffix
123 class KickstartConfig(object):
124 """A base class for applying kickstart configurations to a system."""
125 def __init__(self, instroot):
126 self.instroot = instroot
128 def path(self, subpath):
129 return self.instroot + subpath
131 def _check_sysconfig(self):
132 if not os.path.exists(self.path("/etc/sysconfig")):
133 fs.makedirs(self.path("/etc/sysconfig"))
136 os.chroot(self.instroot)
139 def call(self, args):
140 if not os.path.exists("%s/%s" %(self.instroot, args[0])):
141 msger.warning("%s/%s" %(self.instroot, args[0]))
142 raise errors.KsError("Unable to run %s!" %(args))
143 subprocess.call(args, preexec_fn = self.chroot)
148 class LanguageConfig(KickstartConfig):
149 """A class to apply a kickstart language configuration to a system."""
150 def apply(self, kslang):
151 self._check_sysconfig()
153 f = open(self.path("/etc/sysconfig/i18n"), "w+")
154 f.write("LANG=\"" + kslang.lang + "\"\n")
157 class KeyboardConfig(KickstartConfig):
158 """A class to apply a kickstart keyboard configuration to a system."""
159 def apply(self, kskeyboard):
162 # should this impact the X keyboard config too?
163 # or do we want to make X be able to do this mapping?
165 #k = rhpl.keyboard.Keyboard()
166 #if kskeyboard.keyboard:
167 # k.set(kskeyboard.keyboard)
168 #k.write(self.instroot)
171 class TimezoneConfig(KickstartConfig):
172 """A class to apply a kickstart timezone configuration to a system."""
173 def apply(self, kstimezone):
174 self._check_sysconfig()
175 tz = kstimezone.timezone or "America/New_York"
176 utc = str(kstimezone.isUtc)
178 f = open(self.path("/etc/sysconfig/clock"), "w+")
179 f.write("ZONE=\"" + tz + "\"\n")
180 f.write("UTC=" + utc + "\n")
182 tz_source = self.path("/usr/share/zoneinfo/%s" % (tz))
183 tz_dest = self.path("/etc/localtime")
185 shutil.copyfile(tz_source, tz_dest)
186 except (IOError, OSError), (errno, msg):
187 raise errors.KsError("Error copying timezone info from "
189 % (tz_source, tz_dest, msg))
192 class AuthConfig(KickstartConfig):
193 """A class to apply a kickstart authconfig configuration to a system."""
194 def apply(self, ksauthconfig):
195 auth = ksauthconfig.authconfig or "--useshadow --enablemd5"
196 args = ["/usr/share/authconfig/authconfig.py", "--update", "--nostart"]
197 self.call(args + auth.split())
199 class FirewallConfig(KickstartConfig):
200 """A class to apply a kickstart firewall configuration to a system."""
201 def apply(self, ksfirewall):
203 # FIXME: should handle the rest of the options
205 if not os.path.exists(self.path("/usr/sbin/lokkit")):
207 if ksfirewall.enabled:
210 status = "--disabled"
212 self.call(["/usr/sbin/lokkit",
213 "-f", "--quiet", "--nostart", status])
215 class RootPasswordConfig(KickstartConfig):
216 """A class to apply a kickstart root password configuration to a system."""
218 self.call(["/usr/bin/passwd", "-d", "root"])
220 def set_encrypted(self, password):
221 self.call(["/usr/sbin/usermod", "-p", password, "root"])
223 def set_unencrypted(self, password):
224 for p in ("/bin/echo", "/usr/sbin/chpasswd"):
225 if not os.path.exists("%s/%s" %(self.instroot, p)):
226 raise errors.KsError("Unable to set unencrypted password due "
229 p1 = subprocess.Popen(["/bin/echo", "root:%s" %password],
230 stdout = subprocess.PIPE,
231 preexec_fn = self.chroot)
232 p2 = subprocess.Popen(["/usr/sbin/chpasswd", "-m"],
234 stdout = subprocess.PIPE,
235 preexec_fn = self.chroot)
238 def apply(self, ksrootpw):
239 if ksrootpw.isCrypted:
240 self.set_encrypted(ksrootpw.password)
241 elif ksrootpw.password != "":
242 self.set_unencrypted(ksrootpw.password)
246 class UserConfig(KickstartConfig):
247 def set_empty_passwd(self, user):
248 self.call(["/usr/bin/passwd", "-d", user])
250 def set_encrypted_passwd(self, user, password):
251 self.call(["/usr/sbin/usermod", "-p", "%s" % password, user])
253 def set_unencrypted_passwd(self, user, password):
254 for p in ("/bin/echo", "/usr/sbin/chpasswd"):
255 if not os.path.exists("%s/%s" %(self.instroot, p)):
256 raise errors.KsError("Unable to set unencrypted password due "
259 p1 = subprocess.Popen(["/bin/echo", "%s:%s" %(user, password)],
260 stdout = subprocess.PIPE,
261 preexec_fn = self.chroot)
262 p2 = subprocess.Popen(["/usr/sbin/chpasswd", "-m"],
264 stdout = subprocess.PIPE,
265 preexec_fn = self.chroot)
268 def addUser(self, userconfig):
269 args = [ "/usr/sbin/useradd" ]
270 if userconfig.groups:
271 args += [ "--groups", string.join(userconfig.groups, ",") ]
273 args.append(userconfig.name)
274 dev_null = os.open("/dev/null", os.O_WRONLY)
275 subprocess.call(args,
278 preexec_fn = self.chroot)
280 if userconfig.password not in (None, ""):
281 if userconfig.isCrypted:
282 self.set_encrypted_passwd(userconfig.name,
285 self.set_unencrypted_passwd(userconfig.name,
288 self.set_empty_passwd(userconfig.name)
290 raise errors.KsError("Invalid kickstart command: %s" \
291 % userconfig.__str__())
293 def apply(self, user):
294 for userconfig in user.userList:
296 self.addUser(userconfig)
300 class ServicesConfig(KickstartConfig):
301 """A class to apply a kickstart services configuration to a system."""
302 def apply(self, ksservices):
303 if not os.path.exists(self.path("/sbin/chkconfig")):
305 for s in ksservices.enabled:
306 self.call(["/sbin/chkconfig", s, "on"])
307 for s in ksservices.disabled:
308 self.call(["/sbin/chkconfig", s, "off"])
310 class XConfig(KickstartConfig):
311 """A class to apply a kickstart X configuration to a system."""
312 def apply(self, ksxconfig):
313 if ksxconfig.startX and os.path.exists(self.path("/etc/inittab")):
314 f = open(self.path("/etc/inittab"), "rw+")
316 buf = buf.replace("id:3:initdefault", "id:5:initdefault")
320 if ksxconfig.defaultdesktop:
321 self._check_sysconfig()
322 f = open(self.path("/etc/sysconfig/desktop"), "w")
323 f.write("DESKTOP="+ksxconfig.defaultdesktop+"\n")
326 class DesktopConfig(KickstartConfig):
327 """A class to apply a kickstart desktop configuration to a system."""
328 def apply(self, ksdesktop):
329 if ksdesktop.defaultdesktop:
330 self._check_sysconfig()
331 f = open(self.path("/etc/sysconfig/desktop"), "w")
332 f.write("DESKTOP="+ksdesktop.defaultdesktop+"\n")
334 if os.path.exists(self.path("/etc/gdm/custom.conf")):
335 f = open(self.path("/etc/skel/.dmrc"), "w")
336 f.write("[Desktop]\n")
337 f.write("Session="+ksdesktop.defaultdesktop.lower()+"\n")
339 if ksdesktop.session:
340 if os.path.exists(self.path("/etc/sysconfig/uxlaunch")):
341 f = open(self.path("/etc/sysconfig/uxlaunch"), "a+")
342 f.write("session="+ksdesktop.session.lower()+"\n")
344 if ksdesktop.autologinuser:
345 self._check_sysconfig()
346 f = open(self.path("/etc/sysconfig/desktop"), "a+")
347 f.write("AUTOLOGIN_USER=" + ksdesktop.autologinuser + "\n")
349 if os.path.exists(self.path("/etc/gdm/custom.conf")):
350 f = open(self.path("/etc/gdm/custom.conf"), "w")
351 f.write("[daemon]\n")
352 f.write("AutomaticLoginEnable=true\n")
353 f.write("AutomaticLogin=" + ksdesktop.autologinuser + "\n")
356 class MoblinRepoConfig(KickstartConfig):
357 """A class to apply a kickstart desktop configuration to a system."""
358 def __create_repo_section(self, repo, type, fd):
361 reposuffix = {"base":"", "debuginfo":"-debuginfo", "source":"-source"}
362 reponame = repo.name + reposuffix[type]
365 baseurl = repo.baseurl
367 mirrorlist = repo.mirrorlist
369 elif type == "debuginfo":
371 if repo.baseurl.endswith("/"):
372 baseurl = os.path.dirname(os.path.dirname(repo.baseurl))
374 baseurl = os.path.dirname(repo.baseurl)
378 variant = repo.mirrorlist[repo.mirrorlist.find("$"):]
379 mirrorlist = repo.mirrorlist[0:repo.mirrorlist.find("$")]
380 mirrorlist += "debug" + "-" + variant
382 elif type == "source":
384 if repo.baseurl.endswith("/"):
385 baseurl = os.path.dirname(
387 os.path.dirname(repo.baseurl)))
389 baseurl = os.path.dirname(os.path.dirname(repo.baseurl))
393 variant = repo.mirrorlist[repo.mirrorlist.find("$"):]
394 mirrorlist = repo.mirrorlist[0:repo.mirrorlist.find("$")]
395 mirrorlist += "source" + "-" + variant
397 fd.write("[" + reponame + "]\n")
398 fd.write("name=" + reponame + "\n")
399 fd.write("failovermethod=priority\n")
401 fd.write("baseurl=" + baseurl + "\n")
403 fd.write("mirrorlist=" + mirrorlist + "\n")
404 """ Skip saving proxy settings """
406 # fd.write("proxy=" + repo.proxy + "\n")
407 #if repo.proxy_username:
408 # fd.write("proxy_username=" + repo.proxy_username + "\n")
409 #if repo.proxy_password:
410 # fd.write("proxy_password=" + repo.proxy_password + "\n")
412 fd.write("gpgkey=" + repo.gpgkey + "\n")
413 fd.write("gpgcheck=1\n")
415 fd.write("gpgcheck=0\n")
416 if type == "source" or type == "debuginfo" or repo.disable:
417 fd.write("enabled=0\n")
419 fd.write("enabled=1\n")
422 def __create_repo_file(self, repo, repodir):
423 fs.makedirs(self.path(repodir))
424 f = open(self.path(repodir + "/" + repo.name + ".repo"), "w")
425 self.__create_repo_section(repo, "base", f)
427 self.__create_repo_section(repo, "debuginfo", f)
429 self.__create_repo_section(repo, "source", f)
432 def apply(self, ksrepo, repodata):
433 for repo in ksrepo.repoList:
435 #self.__create_repo_file(repo, "/etc/yum.repos.d")
436 self.__create_repo_file(repo, "/etc/zypp/repos.d")
437 """ Import repo gpg keys """
439 for repo in repodata:
442 "--root=%s" % self.instroot,
446 class RPMMacroConfig(KickstartConfig):
447 """A class to apply the specified rpm macros to the filesystem"""
451 if not os.path.exists(self.path("/etc/rpm")):
452 os.mkdir(self.path("/etc/rpm"))
453 f = open(self.path("/etc/rpm/macros.imgcreate"), "w+")
455 f.write("%_excludedocs 1\n")
456 f.write("%__file_context_path %{nil}\n")
457 if inst_langs(ks) != None:
458 f.write("%_install_langs ")
459 f.write(inst_langs(ks))
463 class NetworkConfig(KickstartConfig):
464 """A class to apply a kickstart network configuration to a system."""
465 def write_ifcfg(self, network):
466 p = self.path("/etc/sysconfig/network-scripts/ifcfg-" + network.device)
471 f.write("DEVICE=%s\n" % network.device)
472 f.write("BOOTPROTO=%s\n" % network.bootProto)
474 if network.bootProto.lower() == "static":
476 f.write("IPADDR=%s\n" % network.ip)
478 f.write("NETMASK=%s\n" % network.netmask)
481 f.write("ONBOOT=on\n")
483 f.write("ONBOOT=off\n")
486 f.write("ESSID=%s\n" % network.essid)
489 if network.ethtool.find("autoneg") == -1:
490 network.ethtool = "autoneg off " + network.ethtool
491 f.write("ETHTOOL_OPTS=%s\n" % network.ethtool)
493 if network.bootProto.lower() == "dhcp":
495 f.write("DHCP_HOSTNAME=%s\n" % network.hostname)
496 if network.dhcpclass:
497 f.write("DHCP_CLASSID=%s\n" % network.dhcpclass)
500 f.write("MTU=%s\n" % network.mtu)
504 def write_wepkey(self, network):
505 if not network.wepkey:
508 p = self.path("/etc/sysconfig/network-scripts/keys-" + network.device)
511 f.write("KEY=%s\n" % network.wepkey)
514 def write_sysconfig(self, useipv6, hostname, gateway):
515 path = self.path("/etc/sysconfig/network")
519 f.write("NETWORKING=yes\n")
522 f.write("NETWORKING_IPV6=yes\n")
524 f.write("NETWORKING_IPV6=no\n")
527 f.write("HOSTNAME=%s\n" % hostname)
529 f.write("HOSTNAME=localhost.localdomain\n")
532 f.write("GATEWAY=%s\n" % gateway)
536 def write_hosts(self, hostname):
538 if hostname and hostname != "localhost.localdomain":
539 localline += hostname + " "
540 l = hostname.split(".")
542 localline += l[0] + " "
543 localline += "localhost.localdomain localhost"
545 path = self.path("/etc/hosts")
548 f.write("127.0.0.1\t\t%s\n" % localline)
549 f.write("::1\t\tlocalhost6.localdomain6 localhost6\n")
552 def write_resolv(self, nodns, nameservers):
553 if nodns or not nameservers:
556 path = self.path("/etc/resolv.conf")
560 for ns in (nameservers):
562 f.write("nameserver %s\n" % ns)
566 def apply(self, ksnet):
567 fs.makedirs(self.path("/etc/sysconfig/network-scripts"))
575 for network in ksnet.network:
576 if not network.device:
577 raise errors.KsError("No --device specified with "
578 "network kickstart command")
580 if (network.onboot and network.bootProto.lower() != "dhcp" and
581 not (network.ip and network.netmask)):
582 raise errors.KsError("No IP address and/or netmask "
583 "specified with static "
584 "configuration for '%s'" %
587 self.write_ifcfg(network)
588 self.write_wepkey(network)
596 hostname = network.hostname
598 gateway = network.gateway
600 if network.nameserver:
601 nameservers = network.nameserver.split(",")
603 self.write_sysconfig(useipv6, hostname, gateway)
604 self.write_hosts(hostname)
605 self.write_resolv(nodns, nameservers)
608 def get_image_size(ks, default = None):
610 for p in ks.handler.partition.partitions:
611 if p.mountpoint == "/" and p.size:
614 return int(__size) * 1024L * 1024L
618 def get_image_fstype(ks, default = None):
619 for p in ks.handler.partition.partitions:
620 if p.mountpoint == "/" and p.fstype:
624 def get_image_fsopts(ks, default = None):
625 for p in ks.handler.partition.partitions:
626 if p.mountpoint == "/" and p.fsopts:
632 if isinstance(ks.handler.device, kscommands.device.FC3_Device):
633 devices.append(ks.handler.device)
635 devices.extend(ks.handler.device.deviceList)
638 for device in devices:
639 if not device.moduleName:
641 modules.extend(device.moduleName.split(":"))
645 def get_timeout(ks, default = None):
646 if not hasattr(ks.handler.bootloader, "timeout"):
648 if ks.handler.bootloader.timeout is None:
650 return int(ks.handler.bootloader.timeout)
652 def get_kernel_args(ks, default = "ro liveimg"):
653 if not hasattr(ks.handler.bootloader, "appendLine"):
655 if ks.handler.bootloader.appendLine is None:
657 return "%s %s" %(default, ks.handler.bootloader.appendLine)
659 def get_menu_args(ks, default = ""):
660 if not hasattr(ks.handler.bootloader, "menus"):
662 if ks.handler.bootloader.menus in (None, ""):
664 return "%s" % ks.handler.bootloader.menus
666 def get_default_kernel(ks, default = None):
667 if not hasattr(ks.handler.bootloader, "default"):
669 if not ks.handler.bootloader.default:
671 return ks.handler.bootloader.default
673 def get_repos(ks, repo_urls = {}):
675 for repo in ks.handler.repo.repoList:
677 if hasattr(repo, "includepkgs"):
678 inc.extend(repo.includepkgs)
681 if hasattr(repo, "excludepkgs"):
682 exc.extend(repo.excludepkgs)
684 baseurl = repo.baseurl
685 mirrorlist = repo.mirrorlist
687 if repo.name in repo_urls:
688 baseurl = repo_urls[repo.name]
691 if repos.has_key(repo.name):
692 msger.warning("Overriding already specified repo %s" %(repo.name,))
695 if hasattr(repo, "proxy"):
697 proxy_username = None
698 if hasattr(repo, "proxy_username"):
699 proxy_username = repo.proxy_username
700 proxy_password = None
701 if hasattr(repo, "proxy_password"):
702 proxy_password = repo.proxy_password
703 if hasattr(repo, "debuginfo"):
704 debuginfo = repo.debuginfo
705 if hasattr(repo, "source"):
707 if hasattr(repo, "gpgkey"):
709 if hasattr(repo, "disable"):
710 disable = repo.disable
712 if hasattr(repo, "ssl_verify"):
713 ssl_verify = repo.ssl_verify == "yes"
715 if hasattr(repo, "cost"):
718 if hasattr(repo, "priority"):
719 priority = repo.priority
721 repos[repo.name] = (repo.name, baseurl, mirrorlist, inc, exc,
722 proxy, proxy_username, proxy_password, debuginfo,
723 source, gpgkey, disable, ssl_verify, cost, priority)
725 return repos.values()
727 def convert_method_to_repo(ks):
729 ks.handler.repo.methodToRepo()
730 except (AttributeError, kserrors.KickstartError):
733 def get_packages(ks, required = []):
734 return ks.handler.packages.packageList + required
736 def get_groups(ks, required = []):
737 return ks.handler.packages.groupList + required
739 def get_excluded(ks, required = []):
740 return ks.handler.packages.excludedList + required
742 def get_partitions(ks, required = []):
743 return ks.handler.partition.partitions
745 def ignore_missing(ks):
746 return ks.handler.packages.handleMissing == ksconstants.KS_MISSING_IGNORE
748 def exclude_docs(ks):
749 return ks.handler.packages.excludeDocs
752 if hasattr(ks.handler.packages, "instLange"):
753 return ks.handler.packages.instLange
754 elif hasattr(ks.handler.packages, "instLangs"):
755 return ks.handler.packages.instLangs
758 def get_post_scripts(ks):
760 for s in ks.handler.scripts:
761 if s.type != ksparser.KS_SCRIPT_POST:
766 def add_repo(ks, repostr):
767 args = repostr.split()
768 repoobj = ks.handler.repo.parse(args[1:])
769 if repoobj and repoobj not in ks.handler.repo.repoList:
770 ks.handler.repo.repoList.append(repoobj)
772 def remove_all_repos(ks):
773 while len(ks.handler.repo.repoList) != 0:
774 del ks.handler.repo.repoList[0]
776 def remove_duplicate_repos(ks):
780 if len(ks.handler.repo.repoList) < 2:
782 if i >= len(ks.handler.repo.repoList) - 1:
784 name = ks.handler.repo.repoList[i].name
785 baseurl = ks.handler.repo.repoList[i].baseurl
786 if j < len(ks.handler.repo.repoList):
787 if (ks.handler.repo.repoList[j].name == name or \
788 ks.handler.repo.repoList[j].baseurl == baseurl):
789 del ks.handler.repo.repoList[j]
792 if j >= len(ks.handler.repo.repoList):
799 def resolve_groups(creatoropts, repometadata):
801 if 'zypp' == creatoropts['pkgmgr']:
803 ks = creatoropts['ks']
805 for repo in repometadata:
806 """ Mustn't replace group with package list if repo is ready for the
807 corresponding package manager.
810 if iszypp and repo["patterns"]:
812 if not iszypp and repo["comps"]:
815 # But we also must handle such cases, use zypp but repo only has comps,
816 # use yum but repo only has patterns, use zypp but use_comps is true,
817 # use yum but use_comps is false.
819 if iszypp and repo["comps"]:
820 groupfile = repo["comps"]
821 get_pkglist_handler = misc.get_pkglist_in_comps
822 if not iszypp and repo["patterns"]:
823 groupfile = repo["patterns"]
824 get_pkglist_handler = misc.get_pkglist_in_patterns
829 if i >= len(ks.handler.packages.groupList):
831 pkglist = get_pkglist_handler(
832 ks.handler.packages.groupList[i].name,
835 del ks.handler.packages.groupList[i]
837 if pkg not in ks.handler.packages.packageList:
838 ks.handler.packages.packageList.append(pkg)