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.
27 from mic.utils import errors, misc, runner, fs_related as fs
29 import pykickstart.commands as kscommands
30 import pykickstart.constants as ksconstants
31 import pykickstart.errors as kserrors
32 import pykickstart.parser as ksparser
33 import pykickstart.version as ksversion
34 from pykickstart.handlers.control import commandMap
35 from pykickstart.handlers.control import dataMap
37 import custom_commands.desktop as desktop
38 import custom_commands.moblinrepo as moblinrepo
39 import custom_commands.micboot as micboot
41 def read_kickstart(path):
42 """Parse a kickstart file and return a KickstartParser instance.
44 This is a simple utility function which takes a path to a kickstart file,
45 parses it and returns a pykickstart KickstartParser instance which can
46 be then passed to an ImageCreator constructor.
48 If an error occurs, a CreatorError exception is thrown.
51 #version = ksversion.makeVersion()
52 #ks = ksparser.KickstartParser(version)
54 using_version = ksversion.DEVEL
55 commandMap[using_version]["desktop"] = desktop.Moblin_Desktop
56 commandMap[using_version]["repo"] = moblinrepo.Moblin_Repo
57 commandMap[using_version]["bootloader"] = micboot.Moblin_Bootloader
58 dataMap[using_version]["RepoData"] = moblinrepo.Moblin_RepoData
59 superclass = ksversion.returnClassForVersion(version=using_version)
61 class KSHandlers(superclass):
62 def __init__(self, mapping={}):
63 superclass.__init__(self, mapping=commandMap[using_version])
65 ks = ksparser.KickstartParser(KSHandlers())
68 ks.readKickstart(path)
69 except IOError, (err, msg):
70 raise errors.KsError("Failed to read kickstart file "
71 "'%s' : %s" % (path, msg))
72 except kserrors.KickstartParseError, e:
73 raise errors.KsError("Failed to parse kickstart file "
74 "'%s' : %s" % (path, e))
77 def build_name(kscfg, prefix = None, suffix = None, maxlen = None):
78 """Construct and return an image name string.
80 This is a utility function to help create sensible name and fslabel
81 strings. The name is constructed using the sans-prefix-and-extension
82 kickstart filename and the supplied prefix and suffix.
84 If the name exceeds the maxlen length supplied, the prefix is first dropped
85 and then the kickstart filename portion is reduced until it fits. In other
86 words, the suffix takes precedence over the kickstart portion and the
87 kickstart portion takes precedence over the prefix.
89 kscfg -- a path to a kickstart file
90 prefix -- a prefix to prepend to the name; defaults to None, which causes
92 suffix -- a suffix to append to the name; defaults to None, which causes
93 a YYYYMMDDHHMM suffix to be used
94 maxlen -- the maximum length for the returned string; defaults to None,
95 which means there is no restriction on the name length
97 Note, if maxlen is less then the len(suffix), you get to keep both pieces.
100 name = os.path.basename(kscfg)
101 idx = name.rfind('.')
108 suffix = time.strftime("%Y%m%d%H%M")
110 if name.startswith(prefix):
111 name = name[len(prefix):]
113 ret = prefix + name + "-" + suffix
114 if not maxlen is None and len(ret) > maxlen:
115 ret = name[:maxlen - len(suffix) - 1] + "-" + suffix
119 class KickstartConfig(object):
120 """A base class for applying kickstart configurations to a system."""
121 def __init__(self, instroot):
122 self.instroot = instroot
124 def path(self, subpath):
125 return self.instroot + subpath
128 os.chroot(self.instroot)
131 def call(self, args):
132 if not os.path.exists("%s/%s" %(self.instroot, args[0])):
133 msger.warning("%s/%s" %(self.instroot, args[0]))
134 raise errors.KsError("Unable to run %s!" %(args))
135 subprocess.call(args, preexec_fn = self.chroot)
140 class LanguageConfig(KickstartConfig):
141 """A class to apply a kickstart language configuration to a system."""
142 def apply(self, kslang):
144 f = open(self.path("/etc/sysconfig/i18n"), "w+")
145 f.write("LANG=\"" + kslang.lang + "\"\n")
148 class KeyboardConfig(KickstartConfig):
149 """A class to apply a kickstart keyboard configuration to a system."""
150 def apply(self, kskeyboard):
153 # should this impact the X keyboard config too?
154 # or do we want to make X be able to do this mapping?
156 #k = rhpl.keyboard.Keyboard()
157 #if kskeyboard.keyboard:
158 # k.set(kskeyboard.keyboard)
159 #k.write(self.instroot)
162 class TimezoneConfig(KickstartConfig):
163 """A class to apply a kickstart timezone configuration to a system."""
164 def apply(self, kstimezone):
165 tz = kstimezone.timezone or "America/New_York"
166 utc = str(kstimezone.isUtc)
168 f = open(self.path("/etc/sysconfig/clock"), "w+")
169 f.write("ZONE=\"" + tz + "\"\n")
170 f.write("UTC=" + utc + "\n")
173 shutil.copyfile(self.path("/usr/share/zoneinfo/%s" %(tz,)),
174 self.path("/etc/localtime"))
175 except (IOError, OSError), (errno, msg):
176 raise errors.KsError("Error copying timezone info: %s" %(msg,))
179 class AuthConfig(KickstartConfig):
180 """A class to apply a kickstart authconfig configuration to a system."""
181 def apply(self, ksauthconfig):
182 auth = ksauthconfig.authconfig or "--useshadow --enablemd5"
183 args = ["/usr/share/authconfig/authconfig.py", "--update", "--nostart"]
184 self.call(args + auth.split())
186 class FirewallConfig(KickstartConfig):
187 """A class to apply a kickstart firewall configuration to a system."""
188 def apply(self, ksfirewall):
190 # FIXME: should handle the rest of the options
192 if not os.path.exists(self.path("/usr/sbin/lokkit")):
194 if ksfirewall.enabled:
197 status = "--disabled"
199 self.call(["/usr/sbin/lokkit",
200 "-f", "--quiet", "--nostart", status])
202 class RootPasswordConfig(KickstartConfig):
203 """A class to apply a kickstart root password configuration to a system."""
205 self.call(["/usr/bin/passwd", "-d", "root"])
207 def set_encrypted(self, password):
208 self.call(["/usr/sbin/usermod", "-p", password, "root"])
210 def set_unencrypted(self, password):
211 for p in ("/bin/echo", "/usr/sbin/chpasswd"):
212 if not os.path.exists("%s/%s" %(self.instroot, p)):
213 raise errors.KsError("Unable to set unencrypted password due to lack of %s" % p)
215 p1 = subprocess.Popen(["/bin/echo", "root:%s" %password],
216 stdout = subprocess.PIPE,
217 preexec_fn = self.chroot)
218 p2 = subprocess.Popen(["/usr/sbin/chpasswd", "-m"],
220 stdout = subprocess.PIPE,
221 preexec_fn = self.chroot)
224 def apply(self, ksrootpw):
225 if ksrootpw.isCrypted:
226 self.set_encrypted(ksrootpw.password)
227 elif ksrootpw.password != "":
228 self.set_unencrypted(ksrootpw.password)
232 class UserConfig(KickstartConfig):
233 def set_empty_passwd(self, user):
234 self.call(["/usr/bin/passwd", "-d", user])
236 def set_encrypted_passwd(self, user, password):
237 self.call(["/usr/sbin/usermod", "-p", "%s" % password, user])
239 def set_unencrypted_passwd(self, user, password):
240 for p in ("/bin/echo", "/usr/sbin/chpasswd"):
241 if not os.path.exists("%s/%s" %(self.instroot, p)):
242 raise errors.KsError("Unable to set unencrypted password due to lack of %s" % p)
244 p1 = subprocess.Popen(["/bin/echo", "%s:%s" %(user, password)],
245 stdout = subprocess.PIPE,
246 preexec_fn = self.chroot)
247 p2 = subprocess.Popen(["/usr/sbin/chpasswd", "-m"],
249 stdout = subprocess.PIPE,
250 preexec_fn = self.chroot)
253 def addUser(self, userconfig):
254 args = [ "/usr/sbin/useradd" ]
255 if userconfig.groups:
256 args += [ "--groups", string.join(userconfig.groups, ",") ]
258 args.append(userconfig.name)
259 dev_null = os.open("/dev/null", os.O_WRONLY)
260 subprocess.call(args,
263 preexec_fn = self.chroot)
265 if userconfig.password not in (None, ""):
266 if userconfig.isCrypted:
267 self.set_encrypted_passwd(userconfig.name, userconfig.password)
269 self.set_unencrypted_passwd(userconfig.name, userconfig.password)
271 self.set_empty_passwd(userconfig.name)
273 raise errors.KsError("Invalid kickstart command: %s" % userconfig.__str__())
275 def apply(self, user):
276 for userconfig in user.userList:
278 self.addUser(userconfig)
282 class ServicesConfig(KickstartConfig):
283 """A class to apply a kickstart services configuration to a system."""
284 def apply(self, ksservices):
285 if not os.path.exists(self.path("/sbin/chkconfig")):
287 for s in ksservices.enabled:
288 self.call(["/sbin/chkconfig", s, "on"])
289 for s in ksservices.disabled:
290 self.call(["/sbin/chkconfig", s, "off"])
292 class XConfig(KickstartConfig):
293 """A class to apply a kickstart X configuration to a system."""
294 def apply(self, ksxconfig):
296 f = open(self.path("/etc/inittab"), "rw+")
298 buf = buf.replace("id:3:initdefault", "id:5:initdefault")
302 if ksxconfig.defaultdesktop:
303 f = open(self.path("/etc/sysconfig/desktop"), "w")
304 f.write("DESKTOP="+ksxconfig.defaultdesktop+"\n")
307 class DesktopConfig(KickstartConfig):
308 """A class to apply a kickstart desktop configuration to a system."""
309 def apply(self, ksdesktop):
310 if ksdesktop.defaultdesktop:
311 f = open(self.path("/etc/sysconfig/desktop"), "w")
312 f.write("DESKTOP="+ksdesktop.defaultdesktop+"\n")
314 if os.path.exists(self.path("/etc/gdm/custom.conf")):
315 f = open(self.path("/etc/skel/.dmrc"), "w")
316 f.write("[Desktop]\n")
317 f.write("Session="+ksdesktop.defaultdesktop.lower()+"\n")
319 if ksdesktop.session:
320 if os.path.exists(self.path("/etc/sysconfig/uxlaunch")):
321 f = open(self.path("/etc/sysconfig/uxlaunch"), "a+")
322 f.write("session="+ksdesktop.session.lower()+"\n")
324 if ksdesktop.autologinuser:
325 f = open(self.path("/etc/sysconfig/desktop"), "a+")
326 f.write("AUTOLOGIN_USER=" + ksdesktop.autologinuser + "\n")
328 if ksdesktop.session:
329 if os.path.exists(self.path("/etc/sysconfig/uxlaunch")):
330 f = open(self.path("/etc/sysconfig/uxlaunch"), "a+")
331 f.write("user="+ksdesktop.autologinuser+"\n")
333 if os.path.exists(self.path("/etc/gdm/custom.conf")):
334 f = open(self.path("/etc/gdm/custom.conf"), "w")
335 f.write("[daemon]\n")
336 f.write("AutomaticLoginEnable=true\n")
337 f.write("AutomaticLogin=" + ksdesktop.autologinuser + "\n")
340 class MoblinRepoConfig(KickstartConfig):
341 """A class to apply a kickstart desktop configuration to a system."""
342 def __create_repo_section(self, repo, type, fd):
345 reposuffix = {"base":"", "debuginfo":"-debuginfo", "source":"-source"}
346 reponame = repo.name + reposuffix[type]
349 baseurl = repo.baseurl
351 mirrorlist = repo.mirrorlist
352 elif type == "debuginfo":
354 if repo.baseurl.endswith("/"):
355 baseurl = os.path.dirname(os.path.dirname(repo.baseurl))
357 baseurl = os.path.dirname(repo.baseurl)
360 variant = repo.mirrorlist[repo.mirrorlist.find("$"):]
361 mirrorlist = repo.mirrorlist[0:repo.mirrorlist.find("$")]
362 mirrorlist += "debug" + "-" + variant
363 elif type == "source":
365 if repo.baseurl.endswith("/"):
366 baseurl = os.path.dirname(os.path.dirname(os.path.dirname(repo.baseurl)))
368 baseurl = os.path.dirname(os.path.dirname(repo.baseurl))
371 variant = repo.mirrorlist[repo.mirrorlist.find("$"):]
372 mirrorlist = repo.mirrorlist[0:repo.mirrorlist.find("$")]
373 mirrorlist += "source" + "-" + variant
375 fd.write("[" + reponame + "]\n")
376 fd.write("name=" + reponame + "\n")
377 fd.write("failovermethod=priority\n")
379 fd.write("baseurl=" + baseurl + "\n")
381 fd.write("mirrorlist=" + mirrorlist + "\n")
382 """ Skip saving proxy settings """
384 # fd.write("proxy=" + repo.proxy + "\n")
385 #if repo.proxy_username:
386 # fd.write("proxy_username=" + repo.proxy_username + "\n")
387 #if repo.proxy_password:
388 # fd.write("proxy_password=" + repo.proxy_password + "\n")
390 fd.write("gpgkey=" + repo.gpgkey + "\n")
391 fd.write("gpgcheck=1\n")
393 fd.write("gpgcheck=0\n")
394 if type == "source" or type == "debuginfo" or repo.disable:
395 fd.write("enabled=0\n")
397 fd.write("enabled=1\n")
400 def __create_repo_file(self, repo, repodir):
401 fs.makedirs(self.path(repodir))
402 f = open(self.path(repodir + "/" + repo.name + ".repo"), "w")
403 self.__create_repo_section(repo, "base", f)
405 self.__create_repo_section(repo, "debuginfo", f)
407 self.__create_repo_section(repo, "source", f)
410 def apply(self, ksrepo, repodata):
411 for repo in ksrepo.repoList:
413 #self.__create_repo_file(repo, "/etc/yum.repos.d")
414 self.__create_repo_file(repo, "/etc/zypp/repos.d")
415 """ Import repo gpg keys """
417 for repo in repodata:
419 runner.quiet(['rpm', "--root=%s" % self.instroot, "--import", repo['repokey']])
421 class RPMMacroConfig(KickstartConfig):
422 """A class to apply the specified rpm macros to the filesystem"""
426 if not os.path.exists(self.path("/etc/rpm")):
427 os.mkdir(self.path("/etc/rpm"))
428 f = open(self.path("/etc/rpm/macros.imgcreate"), "w+")
430 f.write("%_excludedocs 1\n")
431 f.write("%__file_context_path %{nil}\n")
432 if inst_langs(ks) != None:
433 f.write("%_install_langs ")
434 f.write(inst_langs(ks))
438 class NetworkConfig(KickstartConfig):
439 """A class to apply a kickstart network configuration to a system."""
440 def write_ifcfg(self, network):
441 p = self.path("/etc/sysconfig/network-scripts/ifcfg-" + network.device)
446 f.write("DEVICE=%s\n" % network.device)
447 f.write("BOOTPROTO=%s\n" % network.bootProto)
449 if network.bootProto.lower() == "static":
451 f.write("IPADDR=%s\n" % network.ip)
453 f.write("NETMASK=%s\n" % network.netmask)
456 f.write("ONBOOT=on\n")
458 f.write("ONBOOT=off\n")
461 f.write("ESSID=%s\n" % network.essid)
464 if network.ethtool.find("autoneg") == -1:
465 network.ethtool = "autoneg off " + network.ethtool
466 f.write("ETHTOOL_OPTS=%s\n" % network.ethtool)
468 if network.bootProto.lower() == "dhcp":
470 f.write("DHCP_HOSTNAME=%s\n" % network.hostname)
471 if network.dhcpclass:
472 f.write("DHCP_CLASSID=%s\n" % network.dhcpclass)
475 f.write("MTU=%s\n" % network.mtu)
479 def write_wepkey(self, network):
480 if not network.wepkey:
483 p = self.path("/etc/sysconfig/network-scripts/keys-" + network.device)
486 f.write("KEY=%s\n" % network.wepkey)
489 def write_sysconfig(self, useipv6, hostname, gateway):
490 path = self.path("/etc/sysconfig/network")
494 f.write("NETWORKING=yes\n")
497 f.write("NETWORKING_IPV6=yes\n")
499 f.write("NETWORKING_IPV6=no\n")
502 f.write("HOSTNAME=%s\n" % hostname)
504 f.write("HOSTNAME=localhost.localdomain\n")
507 f.write("GATEWAY=%s\n" % gateway)
511 def write_hosts(self, hostname):
513 if hostname and hostname != "localhost.localdomain":
514 localline += hostname + " "
515 l = hostname.split(".")
517 localline += l[0] + " "
518 localline += "localhost.localdomain localhost"
520 path = self.path("/etc/hosts")
523 f.write("127.0.0.1\t\t%s\n" % localline)
524 f.write("::1\t\tlocalhost6.localdomain6 localhost6\n")
527 def write_resolv(self, nodns, nameservers):
528 if nodns or not nameservers:
531 path = self.path("/etc/resolv.conf")
535 for ns in (nameservers):
537 f.write("nameserver %s\n" % ns)
541 def apply(self, ksnet):
542 fs.makedirs(self.path("/etc/sysconfig/network-scripts"))
550 for network in ksnet.network:
551 if not network.device:
552 raise errors.KsError("No --device specified with "
553 "network kickstart command")
555 if (network.onboot and network.bootProto.lower() != "dhcp" and
556 not (network.ip and network.netmask)):
557 raise errors.KsError("No IP address and/or netmask "
558 "specified with static "
559 "configuration for '%s'" %
562 self.write_ifcfg(network)
563 self.write_wepkey(network)
571 hostname = network.hostname
573 gateway = network.gateway
575 if network.nameserver:
576 nameservers = network.nameserver.split(",")
578 self.write_sysconfig(useipv6, hostname, gateway)
579 self.write_hosts(hostname)
580 self.write_resolv(nodns, nameservers)
583 def get_image_size(ks, default = None):
585 for p in ks.handler.partition.partitions:
586 if p.mountpoint == "/" and p.size:
589 return int(__size) * 1024L * 1024L
593 def get_image_fstype(ks, default = None):
594 for p in ks.handler.partition.partitions:
595 if p.mountpoint == "/" and p.fstype:
599 def get_image_fsopts(ks, default = None):
600 for p in ks.handler.partition.partitions:
601 if p.mountpoint == "/" and p.fsopts:
607 if isinstance(ks.handler.device, kscommands.device.FC3_Device):
608 devices.append(ks.handler.device)
610 devices.extend(ks.handler.device.deviceList)
613 for device in devices:
614 if not device.moduleName:
616 modules.extend(device.moduleName.split(":"))
620 def get_timeout(ks, default = None):
621 if not hasattr(ks.handler.bootloader, "timeout"):
623 if ks.handler.bootloader.timeout is None:
625 return int(ks.handler.bootloader.timeout)
627 def get_kernel_args(ks, default = "ro liveimg"):
628 if not hasattr(ks.handler.bootloader, "appendLine"):
630 if ks.handler.bootloader.appendLine is None:
632 return "%s %s" %(default, ks.handler.bootloader.appendLine)
634 def get_menu_args(ks, default = "liveinst"):
635 if not hasattr(ks.handler.bootloader, "menus"):
637 if ks.handler.bootloader.menus in (None, ""):
639 return "%s" % ks.handler.bootloader.menus
641 def get_default_kernel(ks, default = None):
642 if not hasattr(ks.handler.bootloader, "default"):
644 if not ks.handler.bootloader.default:
646 return ks.handler.bootloader.default
648 def get_repos(ks, repo_urls = {}):
650 for repo in ks.handler.repo.repoList:
652 if hasattr(repo, "includepkgs"):
653 inc.extend(repo.includepkgs)
656 if hasattr(repo, "excludepkgs"):
657 exc.extend(repo.excludepkgs)
659 baseurl = repo.baseurl
660 mirrorlist = repo.mirrorlist
662 if repo.name in repo_urls:
663 baseurl = repo_urls[repo.name]
666 if repos.has_key(repo.name):
667 msger.warning("Overriding already specified repo %s" %(repo.name,))
670 if hasattr(repo, "proxy"):
672 proxy_username = None
673 if hasattr(repo, "proxy_username"):
674 proxy_username = repo.proxy_username
675 proxy_password = None
676 if hasattr(repo, "proxy_password"):
677 proxy_password = repo.proxy_password
678 if hasattr(repo, "debuginfo"):
679 debuginfo = repo.debuginfo
680 if hasattr(repo, "source"):
682 if hasattr(repo, "gpgkey"):
684 if hasattr(repo, "disable"):
685 disable = repo.disable
687 repos[repo.name] = (repo.name, baseurl, mirrorlist, inc, exc, proxy, proxy_username, proxy_password, debuginfo, source, gpgkey, disable)
689 return repos.values()
691 def convert_method_to_repo(ks):
693 ks.handler.repo.methodToRepo()
694 except (AttributeError, kserrors.KickstartError):
697 def get_packages(ks, required = []):
698 return ks.handler.packages.packageList + required
700 def get_groups(ks, required = []):
701 return ks.handler.packages.groupList + required
703 def get_excluded(ks, required = []):
704 return ks.handler.packages.excludedList + required
706 def get_partitions(ks, required = []):
707 return ks.handler.partition.partitions
709 def ignore_missing(ks):
710 return ks.handler.packages.handleMissing == ksconstants.KS_MISSING_IGNORE
712 def exclude_docs(ks):
713 return ks.handler.packages.excludeDocs
716 if hasattr(ks.handler.packages, "instLange"):
717 return ks.handler.packages.instLange
718 elif hasattr(ks.handler.packages, "instLangs"):
719 return ks.handler.packages.instLangs
722 def get_post_scripts(ks):
724 for s in ks.handler.scripts:
725 if s.type != ksparser.KS_SCRIPT_POST:
730 def add_repo(ks, repostr):
731 args = repostr.split()
732 repoobj = ks.handler.repo.parse(args[1:])
733 if repoobj and repoobj not in ks.handler.repo.repoList:
734 ks.handler.repo.repoList.append(repoobj)
736 def remove_all_repos(ks):
737 while len(ks.handler.repo.repoList) != 0:
738 del ks.handler.repo.repoList[0]
740 def remove_duplicate_repos(ks):
744 if len(ks.handler.repo.repoList) < 2:
746 if i >= len(ks.handler.repo.repoList) - 1:
748 name = ks.handler.repo.repoList[i].name
749 baseurl = ks.handler.repo.repoList[i].baseurl
750 if j < len(ks.handler.repo.repoList):
751 if (ks.handler.repo.repoList[j].name == name or \
752 ks.handler.repo.repoList[j].baseurl == baseurl):
753 del ks.handler.repo.repoList[j]
756 if j >= len(ks.handler.repo.repoList):
763 def resolve_groups(creator, repometadata, use_comps = False):
764 pkgmgr = creator.pkgmgr.get_default_pkg_manager
766 if creator.pkgmgr.managers.has_key("zypp") and creator.pkgmgr.managers['zypp'] == pkgmgr:
770 for repo in repometadata:
771 """ Mustn't replace group with package list if repo is ready for the corresponding package manager """
772 if iszypp and repo["patterns"] and not use_comps:
774 if not iszypp and repo["comps"] and use_comps:
778 But we also must handle such cases, use zypp but repo only has comps,
779 use yum but repo only has patterns, use zypp but use_comps is true,
780 use yum but use_comps is false.
784 if (use_comps and repo["comps"]) or (not repo["patterns"] and repo["comps"]):
785 groupfile = repo["comps"]
786 get_pkglist_handler = misc.get_pkglist_in_comps
788 if (not use_comps and repo["patterns"]) or (not repo["comps"] and repo["patterns"]):
789 groupfile = repo["patterns"]
790 get_pkglist_handler = misc.get_pkglist_in_patterns
795 if i >= len(ks.handler.packages.groupList):
797 pkglist = get_pkglist_handler(ks.handler.packages.groupList[i].name, groupfile)
799 del ks.handler.packages.groupList[i]
801 if pkg not in ks.handler.packages.packageList:
802 ks.handler.packages.packageList.append(pkg)