2 # kickstart.py : Apply kickstart configuration to a system
4 # Copyright 2007, Red Hat Inc.
5 # Copyright 2009, 2010, 2011 Intel, Inc.
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; version 2 of the License.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU Library General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # 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 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 for repo in repodata:
422 msger.run(['rpm', "--root=%s" % self.instroot, "--import", repo['repokey']], True)
424 class RPMMacroConfig(KickstartConfig):
425 """A class to apply the specified rpm macros to the filesystem"""
429 if not os.path.exists(self.path("/etc/rpm")):
430 os.mkdir(self.path("/etc/rpm"))
431 f = open(self.path("/etc/rpm/macros.imgcreate"), "w+")
433 f.write("%_excludedocs 1\n")
434 f.write("%__file_context_path %{nil}\n")
435 if inst_langs(ks) != None:
436 f.write("%_install_langs ")
437 f.write(inst_langs(ks))
441 class NetworkConfig(KickstartConfig):
442 """A class to apply a kickstart network configuration to a system."""
443 def write_ifcfg(self, network):
444 p = self.path("/etc/sysconfig/network-scripts/ifcfg-" + network.device)
449 f.write("DEVICE=%s\n" % network.device)
450 f.write("BOOTPROTO=%s\n" % network.bootProto)
452 if network.bootProto.lower() == "static":
454 f.write("IPADDR=%s\n" % network.ip)
456 f.write("NETMASK=%s\n" % network.netmask)
459 f.write("ONBOOT=on\n")
461 f.write("ONBOOT=off\n")
464 f.write("ESSID=%s\n" % network.essid)
467 if network.ethtool.find("autoneg") == -1:
468 network.ethtool = "autoneg off " + network.ethtool
469 f.write("ETHTOOL_OPTS=%s\n" % network.ethtool)
471 if network.bootProto.lower() == "dhcp":
473 f.write("DHCP_HOSTNAME=%s\n" % network.hostname)
474 if network.dhcpclass:
475 f.write("DHCP_CLASSID=%s\n" % network.dhcpclass)
478 f.write("MTU=%s\n" % network.mtu)
482 def write_wepkey(self, network):
483 if not network.wepkey:
486 p = self.path("/etc/sysconfig/network-scripts/keys-" + network.device)
489 f.write("KEY=%s\n" % network.wepkey)
492 def write_sysconfig(self, useipv6, hostname, gateway):
493 path = self.path("/etc/sysconfig/network")
497 f.write("NETWORKING=yes\n")
500 f.write("NETWORKING_IPV6=yes\n")
502 f.write("NETWORKING_IPV6=no\n")
505 f.write("HOSTNAME=%s\n" % hostname)
507 f.write("HOSTNAME=localhost.localdomain\n")
510 f.write("GATEWAY=%s\n" % gateway)
514 def write_hosts(self, hostname):
516 if hostname and hostname != "localhost.localdomain":
517 localline += hostname + " "
518 l = hostname.split(".")
520 localline += l[0] + " "
521 localline += "localhost.localdomain localhost"
523 path = self.path("/etc/hosts")
526 f.write("127.0.0.1\t\t%s\n" % localline)
527 f.write("::1\t\tlocalhost6.localdomain6 localhost6\n")
530 def write_resolv(self, nodns, nameservers):
531 if nodns or not nameservers:
534 path = self.path("/etc/resolv.conf")
538 for ns in (nameservers):
540 f.write("nameserver %s\n" % ns)
544 def apply(self, ksnet):
545 fs.makedirs(self.path("/etc/sysconfig/network-scripts"))
553 for network in ksnet.network:
554 if not network.device:
555 raise errors.KsError("No --device specified with "
556 "network kickstart command")
558 if (network.onboot and network.bootProto.lower() != "dhcp" and
559 not (network.ip and network.netmask)):
560 raise errors.KsError("No IP address and/or netmask "
561 "specified with static "
562 "configuration for '%s'" %
565 self.write_ifcfg(network)
566 self.write_wepkey(network)
574 hostname = network.hostname
576 gateway = network.gateway
578 if network.nameserver:
579 nameservers = network.nameserver.split(",")
581 self.write_sysconfig(useipv6, hostname, gateway)
582 self.write_hosts(hostname)
583 self.write_resolv(nodns, nameservers)
586 def get_image_size(ks, default = None):
588 for p in ks.handler.partition.partitions:
589 if p.mountpoint == "/" and p.size:
592 return int(__size) * 1024L * 1024L
596 def get_image_fstype(ks, default = None):
597 for p in ks.handler.partition.partitions:
598 if p.mountpoint == "/" and p.fstype:
602 def get_image_fsopts(ks, default = None):
603 for p in ks.handler.partition.partitions:
604 if p.mountpoint == "/" and p.fsopts:
610 if isinstance(ks.handler.device, kscommands.device.FC3_Device):
611 devices.append(ks.handler.device)
613 devices.extend(ks.handler.device.deviceList)
616 for device in devices:
617 if not device.moduleName:
619 modules.extend(device.moduleName.split(":"))
623 def get_timeout(ks, default = None):
624 if not hasattr(ks.handler.bootloader, "timeout"):
626 if ks.handler.bootloader.timeout is None:
628 return int(ks.handler.bootloader.timeout)
630 def get_kernel_args(ks, default = "ro liveimg"):
631 if not hasattr(ks.handler.bootloader, "appendLine"):
633 if ks.handler.bootloader.appendLine is None:
635 return "%s %s" %(default, ks.handler.bootloader.appendLine)
637 def get_menu_args(ks, default = "liveinst"):
638 if not hasattr(ks.handler.bootloader, "menus"):
640 if ks.handler.bootloader.menus in (None, ""):
642 return "%s" % ks.handler.bootloader.menus
644 def get_default_kernel(ks, default = None):
645 if not hasattr(ks.handler.bootloader, "default"):
647 if not ks.handler.bootloader.default:
649 return ks.handler.bootloader.default
651 def get_repos(ks, repo_urls = {}):
653 for repo in ks.handler.repo.repoList:
655 if hasattr(repo, "includepkgs"):
656 inc.extend(repo.includepkgs)
659 if hasattr(repo, "excludepkgs"):
660 exc.extend(repo.excludepkgs)
662 baseurl = repo.baseurl
663 mirrorlist = repo.mirrorlist
665 if repo.name in repo_urls:
666 baseurl = repo_urls[repo.name]
669 if repos.has_key(repo.name):
670 msger.warning("Overriding already specified repo %s" %(repo.name,))
673 if hasattr(repo, "proxy"):
675 proxy_username = None
676 if hasattr(repo, "proxy_username"):
677 proxy_username = repo.proxy_username
678 proxy_password = None
679 if hasattr(repo, "proxy_password"):
680 proxy_password = repo.proxy_password
681 if hasattr(repo, "debuginfo"):
682 debuginfo = repo.debuginfo
683 if hasattr(repo, "source"):
685 if hasattr(repo, "gpgkey"):
687 if hasattr(repo, "disable"):
688 disable = repo.disable
690 repos[repo.name] = (repo.name, baseurl, mirrorlist, inc, exc, proxy, proxy_username, proxy_password, debuginfo, source, gpgkey, disable)
692 return repos.values()
694 def convert_method_to_repo(ks):
696 ks.handler.repo.methodToRepo()
697 except (AttributeError, kserrors.KickstartError):
700 def get_packages(ks, required = []):
701 return ks.handler.packages.packageList + required
703 def get_groups(ks, required = []):
704 return ks.handler.packages.groupList + required
706 def get_excluded(ks, required = []):
707 return ks.handler.packages.excludedList + required
709 def get_partitions(ks, required = []):
710 return ks.handler.partition.partitions
712 def ignore_missing(ks):
713 return ks.handler.packages.handleMissing == ksconstants.KS_MISSING_IGNORE
715 def exclude_docs(ks):
716 return ks.handler.packages.excludeDocs
719 if hasattr(ks.handler.packages, "instLange"):
720 return ks.handler.packages.instLange
721 elif hasattr(ks.handler.packages, "instLangs"):
722 return ks.handler.packages.instLangs
725 def get_post_scripts(ks):
727 for s in ks.handler.scripts:
728 if s.type != ksparser.KS_SCRIPT_POST:
733 def add_repo(ks, repostr):
734 args = repostr.split()
735 repoobj = ks.handler.repo.parse(args[1:])
736 if repoobj and repoobj not in ks.handler.repo.repoList:
737 ks.handler.repo.repoList.append(repoobj)
739 def remove_all_repos(ks):
740 while len(ks.handler.repo.repoList) != 0:
741 del ks.handler.repo.repoList[0]
743 def remove_duplicate_repos(ks):
747 if len(ks.handler.repo.repoList) < 2:
749 if i >= len(ks.handler.repo.repoList) - 1:
751 name = ks.handler.repo.repoList[i].name
752 baseurl = ks.handler.repo.repoList[i].baseurl
753 if j < len(ks.handler.repo.repoList):
754 if (ks.handler.repo.repoList[j].name == name or \
755 ks.handler.repo.repoList[j].baseurl == baseurl):
756 del ks.handler.repo.repoList[j]
759 if j >= len(ks.handler.repo.repoList):
766 def resolve_groups(creator, repometadata, use_comps = False):
767 pkgmgr = creator.pkgmgr.get_default_pkg_manager
769 if creator.pkgmgr.managers.has_key("zypp") and creator.pkgmgr.managers['zypp'] == pkgmgr:
773 for repo in repometadata:
774 """ Mustn't replace group with package list if repo is ready for the corresponding package manager """
775 if iszypp and repo["patterns"] and not use_comps:
777 if not iszypp and repo["comps"] and use_comps:
781 But we also must handle such cases, use zypp but repo only has comps,
782 use yum but repo only has patterns, use zypp but use_comps is true,
783 use yum but use_comps is false.
787 if (use_comps and repo["comps"]) or (not repo["patterns"] and repo["comps"]):
788 groupfile = repo["comps"]
789 get_pkglist_handler = misc.get_pkglist_in_comps
791 if (not use_comps and repo["patterns"]) or (not repo["comps"] and repo["patterns"]):
792 groupfile = repo["patterns"]
793 get_pkglist_handler = misc.get_pkglist_in_patterns
798 if i >= len(ks.handler.packages.groupList):
800 pkglist = get_pkglist_handler(ks.handler.packages.groupList[i].name, groupfile)
802 del ks.handler.packages.groupList[i]
804 if pkg not in ks.handler.packages.packageList:
805 ks.handler.packages.packageList.append(pkg)