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):
146 f = open(self.path("/etc/sysconfig/i18n"), "w+")
147 f.write("LANG=\"" + kslang.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 fs.makedirs(self.path(repodir))
404 f = open(self.path(repodir + "/" + repo.name + ".repo"), "w")
405 self.__create_repo_section(repo, "base", f)
407 self.__create_repo_section(repo, "debuginfo", f)
409 self.__create_repo_section(repo, "source", f)
412 def apply(self, ksrepo, repodata):
413 for repo in ksrepo.repoList:
415 #self.__create_repo_file(repo, "/etc/yum.repos.d")
416 self.__create_repo_file(repo, "/etc/zypp/repos.d")
417 """ Import repo gpg keys """
419 for repo in repodata:
421 msger.run(['rpm', "--root=%s" % self.instroot, "--import", repo['repokey']], True)
423 class RPMMacroConfig(KickstartConfig):
424 """A class to apply the specified rpm macros to the filesystem"""
428 if not os.path.exists(self.path("/etc/rpm")):
429 os.mkdir(self.path("/etc/rpm"))
430 f = open(self.path("/etc/rpm/macros.imgcreate"), "w+")
432 f.write("%_excludedocs 1\n")
433 f.write("%__file_context_path %{nil}\n")
434 if inst_langs(ks) != None:
435 f.write("%_install_langs ")
436 f.write(inst_langs(ks))
440 class NetworkConfig(KickstartConfig):
441 """A class to apply a kickstart network configuration to a system."""
442 def write_ifcfg(self, network):
443 p = self.path("/etc/sysconfig/network-scripts/ifcfg-" + network.device)
448 f.write("DEVICE=%s\n" % network.device)
449 f.write("BOOTPROTO=%s\n" % network.bootProto)
451 if network.bootProto.lower() == "static":
453 f.write("IPADDR=%s\n" % network.ip)
455 f.write("NETMASK=%s\n" % network.netmask)
458 f.write("ONBOOT=on\n")
460 f.write("ONBOOT=off\n")
463 f.write("ESSID=%s\n" % network.essid)
466 if network.ethtool.find("autoneg") == -1:
467 network.ethtool = "autoneg off " + network.ethtool
468 f.write("ETHTOOL_OPTS=%s\n" % network.ethtool)
470 if network.bootProto.lower() == "dhcp":
472 f.write("DHCP_HOSTNAME=%s\n" % network.hostname)
473 if network.dhcpclass:
474 f.write("DHCP_CLASSID=%s\n" % network.dhcpclass)
477 f.write("MTU=%s\n" % network.mtu)
481 def write_wepkey(self, network):
482 if not network.wepkey:
485 p = self.path("/etc/sysconfig/network-scripts/keys-" + network.device)
488 f.write("KEY=%s\n" % network.wepkey)
491 def write_sysconfig(self, useipv6, hostname, gateway):
492 path = self.path("/etc/sysconfig/network")
496 f.write("NETWORKING=yes\n")
499 f.write("NETWORKING_IPV6=yes\n")
501 f.write("NETWORKING_IPV6=no\n")
504 f.write("HOSTNAME=%s\n" % hostname)
506 f.write("HOSTNAME=localhost.localdomain\n")
509 f.write("GATEWAY=%s\n" % gateway)
513 def write_hosts(self, hostname):
515 if hostname and hostname != "localhost.localdomain":
516 localline += hostname + " "
517 l = hostname.split(".")
519 localline += l[0] + " "
520 localline += "localhost.localdomain localhost"
522 path = self.path("/etc/hosts")
525 f.write("127.0.0.1\t\t%s\n" % localline)
526 f.write("::1\t\tlocalhost6.localdomain6 localhost6\n")
529 def write_resolv(self, nodns, nameservers):
530 if nodns or not nameservers:
533 path = self.path("/etc/resolv.conf")
537 for ns in (nameservers):
539 f.write("nameserver %s\n" % ns)
543 def apply(self, ksnet):
544 fs.makedirs(self.path("/etc/sysconfig/network-scripts"))
552 for network in ksnet.network:
553 if not network.device:
554 raise errors.KsError("No --device specified with "
555 "network kickstart command")
557 if (network.onboot and network.bootProto.lower() != "dhcp" and
558 not (network.ip and network.netmask)):
559 raise errors.KsError("No IP address and/or netmask "
560 "specified with static "
561 "configuration for '%s'" %
564 self.write_ifcfg(network)
565 self.write_wepkey(network)
573 hostname = network.hostname
575 gateway = network.gateway
577 if network.nameserver:
578 nameservers = network.nameserver.split(",")
580 self.write_sysconfig(useipv6, hostname, gateway)
581 self.write_hosts(hostname)
582 self.write_resolv(nodns, nameservers)
585 def get_image_size(ks, default = None):
587 for p in ks.handler.partition.partitions:
588 if p.mountpoint == "/" and p.size:
591 return int(__size) * 1024L * 1024L
595 def get_image_fstype(ks, default = None):
596 for p in ks.handler.partition.partitions:
597 if p.mountpoint == "/" and p.fstype:
601 def get_image_fsopts(ks, default = None):
602 for p in ks.handler.partition.partitions:
603 if p.mountpoint == "/" and p.fsopts:
609 if isinstance(ks.handler.device, kscommands.device.FC3_Device):
610 devices.append(ks.handler.device)
612 devices.extend(ks.handler.device.deviceList)
615 for device in devices:
616 if not device.moduleName:
618 modules.extend(device.moduleName.split(":"))
622 def get_timeout(ks, default = None):
623 if not hasattr(ks.handler.bootloader, "timeout"):
625 if ks.handler.bootloader.timeout is None:
627 return int(ks.handler.bootloader.timeout)
629 def get_kernel_args(ks, default = "ro liveimg"):
630 if not hasattr(ks.handler.bootloader, "appendLine"):
632 if ks.handler.bootloader.appendLine is None:
634 return "%s %s" %(default, ks.handler.bootloader.appendLine)
636 def get_menu_args(ks, default = "liveinst"):
637 if not hasattr(ks.handler.bootloader, "menus"):
639 if ks.handler.bootloader.menus in (None, ""):
641 return "%s" % ks.handler.bootloader.menus
643 def get_default_kernel(ks, default = None):
644 if not hasattr(ks.handler.bootloader, "default"):
646 if not ks.handler.bootloader.default:
648 return ks.handler.bootloader.default
650 def get_repos(ks, repo_urls = {}):
652 for repo in ks.handler.repo.repoList:
654 if hasattr(repo, "includepkgs"):
655 inc.extend(repo.includepkgs)
658 if hasattr(repo, "excludepkgs"):
659 exc.extend(repo.excludepkgs)
661 baseurl = repo.baseurl
662 mirrorlist = repo.mirrorlist
664 if repo.name in repo_urls:
665 baseurl = repo_urls[repo.name]
668 if repos.has_key(repo.name):
669 msger.warning("Overriding already specified repo %s" %(repo.name,))
672 if hasattr(repo, "proxy"):
674 proxy_username = None
675 if hasattr(repo, "proxy_username"):
676 proxy_username = repo.proxy_username
677 proxy_password = None
678 if hasattr(repo, "proxy_password"):
679 proxy_password = repo.proxy_password
680 if hasattr(repo, "debuginfo"):
681 debuginfo = repo.debuginfo
682 if hasattr(repo, "source"):
684 if hasattr(repo, "gpgkey"):
686 if hasattr(repo, "disable"):
687 disable = repo.disable
689 repos[repo.name] = (repo.name, baseurl, mirrorlist, inc, exc, proxy, proxy_username, proxy_password, debuginfo, source, gpgkey, disable)
691 return repos.values()
693 def convert_method_to_repo(ks):
695 ks.handler.repo.methodToRepo()
696 except (AttributeError, kserrors.KickstartError):
699 def get_packages(ks, required = []):
700 return ks.handler.packages.packageList + required
702 def get_groups(ks, required = []):
703 return ks.handler.packages.groupList + required
705 def get_excluded(ks, required = []):
706 return ks.handler.packages.excludedList + required
708 def get_partitions(ks, required = []):
709 return ks.handler.partition.partitions
711 def ignore_missing(ks):
712 return ks.handler.packages.handleMissing == ksconstants.KS_MISSING_IGNORE
714 def exclude_docs(ks):
715 return ks.handler.packages.excludeDocs
718 if hasattr(ks.handler.packages, "instLange"):
719 return ks.handler.packages.instLange
720 elif hasattr(ks.handler.packages, "instLangs"):
721 return ks.handler.packages.instLangs
724 def get_post_scripts(ks):
726 for s in ks.handler.scripts:
727 if s.type != ksparser.KS_SCRIPT_POST:
732 def add_repo(ks, repostr):
733 args = repostr.split()
734 repoobj = ks.handler.repo.parse(args[1:])
735 if repoobj and repoobj not in ks.handler.repo.repoList:
736 ks.handler.repo.repoList.append(repoobj)
738 def remove_all_repos(ks):
739 while len(ks.handler.repo.repoList) != 0:
740 del ks.handler.repo.repoList[0]
742 def remove_duplicate_repos(ks):
746 if len(ks.handler.repo.repoList) < 2:
748 if i >= len(ks.handler.repo.repoList) - 1:
750 name = ks.handler.repo.repoList[i].name
751 baseurl = ks.handler.repo.repoList[i].baseurl
752 if j < len(ks.handler.repo.repoList):
753 if (ks.handler.repo.repoList[j].name == name or \
754 ks.handler.repo.repoList[j].baseurl == baseurl):
755 del ks.handler.repo.repoList[j]
758 if j >= len(ks.handler.repo.repoList):
765 def resolve_groups(creator, repometadata, use_comps = False):
766 pkgmgr = creator.pkgmgr.get_default_pkg_manager
768 if creator.pkgmgr.managers.has_key("zypp") and creator.pkgmgr.managers['zypp'] == pkgmgr:
772 for repo in repometadata:
773 """ Mustn't replace group with package list if repo is ready for the corresponding package manager """
774 if iszypp and repo["patterns"] and not use_comps:
776 if not iszypp and repo["comps"] and use_comps:
780 But we also must handle such cases, use zypp but repo only has comps,
781 use yum but repo only has patterns, use zypp but use_comps is true,
782 use yum but use_comps is false.
786 if (use_comps and repo["comps"]) or (not repo["patterns"] and repo["comps"]):
787 groupfile = repo["comps"]
788 get_pkglist_handler = misc.get_pkglist_in_comps
790 if (not use_comps and repo["patterns"]) or (not repo["comps"] and repo["patterns"]):
791 groupfile = repo["patterns"]
792 get_pkglist_handler = misc.get_pkglist_in_patterns
797 if i >= len(ks.handler.packages.groupList):
799 pkglist = get_pkglist_handler(ks.handler.packages.groupList[i].name, groupfile)
801 del ks.handler.packages.groupList[i]
803 if pkg not in ks.handler.packages.packageList:
804 ks.handler.packages.packageList.append(pkg)