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.KickstartError, e:
69 raise errors.KsError("'%s': %s" % (path, str(e)))
70 except kserrors.KickstartParseError, e:
71 raise errors.KsError("'%s': %s" % (path, str(e)))
74 def build_name(kscfg, prefix = None, suffix = None, maxlen = None):
75 """Construct and return an image name string.
77 This is a utility function to help create sensible name and fslabel
78 strings. The name is constructed using the sans-prefix-and-extension
79 kickstart filename and the supplied prefix and suffix.
81 If the name exceeds the maxlen length supplied, the prefix is first dropped
82 and then the kickstart filename portion is reduced until it fits. In other
83 words, the suffix takes precedence over the kickstart portion and the
84 kickstart portion takes precedence over the prefix.
86 kscfg -- a path to a kickstart file
87 prefix -- a prefix to prepend to the name; defaults to None, which causes
89 suffix -- a suffix to append to the name; defaults to None, which causes
90 a YYYYMMDDHHMM suffix to be used
91 maxlen -- the maximum length for the returned string; defaults to None,
92 which means there is no restriction on the name length
94 Note, if maxlen is less then the len(suffix), you get to keep both pieces.
97 name = os.path.basename(kscfg)
105 suffix = time.strftime("%Y%m%d%H%M")
107 if name.startswith(prefix):
108 name = name[len(prefix):]
110 ret = prefix + name + "-" + suffix
111 if not maxlen is None and len(ret) > maxlen:
112 ret = name[:maxlen - len(suffix) - 1] + "-" + suffix
116 class KickstartConfig(object):
117 """A base class for applying kickstart configurations to a system."""
118 def __init__(self, instroot):
119 self.instroot = instroot
121 def path(self, subpath):
122 return self.instroot + subpath
124 def _check_sysconfig(self):
125 if not os.path.exists(self.path("/etc/sysconfig")):
126 fs.makedirs(self.path("/etc/sysconfig"))
129 os.chroot(self.instroot)
132 def call(self, args):
133 if not os.path.exists("%s/%s" %(self.instroot, args[0])):
134 msger.warning("%s/%s" %(self.instroot, args[0]))
135 raise errors.KsError("Unable to run %s!" %(args))
136 subprocess.call(args, preexec_fn = self.chroot)
141 class LanguageConfig(KickstartConfig):
142 """A class to apply a kickstart language configuration to a system."""
143 def apply(self, kslang):
144 self._check_sysconfig()
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 self._check_sysconfig()
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")
175 tz_source = self.path("/usr/share/zoneinfo/%s" % (tz))
176 tz_dest = self.path("/etc/localtime")
178 shutil.copyfile(tz_source, tz_dest)
179 except (IOError, OSError), (errno, msg):
180 raise errors.KickstartError("Error copying timezone info from '%s' to '%s': %s" %(tz_source, tz_dest, msg))
183 class AuthConfig(KickstartConfig):
184 """A class to apply a kickstart authconfig configuration to a system."""
185 def apply(self, ksauthconfig):
186 auth = ksauthconfig.authconfig or "--useshadow --enablemd5"
187 args = ["/usr/share/authconfig/authconfig.py", "--update", "--nostart"]
188 self.call(args + auth.split())
190 class FirewallConfig(KickstartConfig):
191 """A class to apply a kickstart firewall configuration to a system."""
192 def apply(self, ksfirewall):
194 # FIXME: should handle the rest of the options
196 if not os.path.exists(self.path("/usr/sbin/lokkit")):
198 if ksfirewall.enabled:
201 status = "--disabled"
203 self.call(["/usr/sbin/lokkit",
204 "-f", "--quiet", "--nostart", status])
206 class RootPasswordConfig(KickstartConfig):
207 """A class to apply a kickstart root password configuration to a system."""
209 self.call(["/usr/bin/passwd", "-d", "root"])
211 def set_encrypted(self, password):
212 self.call(["/usr/sbin/usermod", "-p", password, "root"])
214 def set_unencrypted(self, password):
215 for p in ("/bin/echo", "/usr/sbin/chpasswd"):
216 if not os.path.exists("%s/%s" %(self.instroot, p)):
217 raise errors.KsError("Unable to set unencrypted password due to lack of %s" % p)
219 p1 = subprocess.Popen(["/bin/echo", "root:%s" %password],
220 stdout = subprocess.PIPE,
221 preexec_fn = self.chroot)
222 p2 = subprocess.Popen(["/usr/sbin/chpasswd", "-m"],
224 stdout = subprocess.PIPE,
225 preexec_fn = self.chroot)
228 def apply(self, ksrootpw):
229 if ksrootpw.isCrypted:
230 self.set_encrypted(ksrootpw.password)
231 elif ksrootpw.password != "":
232 self.set_unencrypted(ksrootpw.password)
236 class UserConfig(KickstartConfig):
237 def set_empty_passwd(self, user):
238 self.call(["/usr/bin/passwd", "-d", user])
240 def set_encrypted_passwd(self, user, password):
241 self.call(["/usr/sbin/usermod", "-p", "%s" % password, user])
243 def set_unencrypted_passwd(self, user, password):
244 for p in ("/bin/echo", "/usr/sbin/chpasswd"):
245 if not os.path.exists("%s/%s" %(self.instroot, p)):
246 raise errors.KsError("Unable to set unencrypted password due to lack of %s" % p)
248 p1 = subprocess.Popen(["/bin/echo", "%s:%s" %(user, password)],
249 stdout = subprocess.PIPE,
250 preexec_fn = self.chroot)
251 p2 = subprocess.Popen(["/usr/sbin/chpasswd", "-m"],
253 stdout = subprocess.PIPE,
254 preexec_fn = self.chroot)
257 def addUser(self, userconfig):
258 args = [ "/usr/sbin/useradd" ]
259 if userconfig.groups:
260 args += [ "--groups", string.join(userconfig.groups, ",") ]
262 args.append(userconfig.name)
263 dev_null = os.open("/dev/null", os.O_WRONLY)
264 subprocess.call(args,
267 preexec_fn = self.chroot)
269 if userconfig.password not in (None, ""):
270 if userconfig.isCrypted:
271 self.set_encrypted_passwd(userconfig.name, userconfig.password)
273 self.set_unencrypted_passwd(userconfig.name, userconfig.password)
275 self.set_empty_passwd(userconfig.name)
277 raise errors.KsError("Invalid kickstart command: %s" % userconfig.__str__())
279 def apply(self, user):
280 for userconfig in user.userList:
282 self.addUser(userconfig)
286 class ServicesConfig(KickstartConfig):
287 """A class to apply a kickstart services configuration to a system."""
288 def apply(self, ksservices):
289 if not os.path.exists(self.path("/sbin/chkconfig")):
291 for s in ksservices.enabled:
292 self.call(["/sbin/chkconfig", s, "on"])
293 for s in ksservices.disabled:
294 self.call(["/sbin/chkconfig", s, "off"])
296 class XConfig(KickstartConfig):
297 """A class to apply a kickstart X configuration to a system."""
298 def apply(self, ksxconfig):
299 if ksxconfig.startX and os.path.exists(self.path("/etc/inittab")):
300 f = open(self.path("/etc/inittab"), "rw+")
302 buf = buf.replace("id:3:initdefault", "id:5:initdefault")
306 if ksxconfig.defaultdesktop:
307 self._check_sysconfig()
308 f = open(self.path("/etc/sysconfig/desktop"), "w")
309 f.write("DESKTOP="+ksxconfig.defaultdesktop+"\n")
312 class DesktopConfig(KickstartConfig):
313 """A class to apply a kickstart desktop configuration to a system."""
314 def apply(self, ksdesktop):
315 if ksdesktop.defaultdesktop:
316 self._check_sysconfig()
317 f = open(self.path("/etc/sysconfig/desktop"), "w")
318 f.write("DESKTOP="+ksdesktop.defaultdesktop+"\n")
320 if os.path.exists(self.path("/etc/gdm/custom.conf")):
321 f = open(self.path("/etc/skel/.dmrc"), "w")
322 f.write("[Desktop]\n")
323 f.write("Session="+ksdesktop.defaultdesktop.lower()+"\n")
325 if ksdesktop.session:
326 if os.path.exists(self.path("/etc/sysconfig/uxlaunch")):
327 f = open(self.path("/etc/sysconfig/uxlaunch"), "a+")
328 f.write("session="+ksdesktop.session.lower()+"\n")
330 if ksdesktop.autologinuser:
331 self._check_sysconfig()
332 f = open(self.path("/etc/sysconfig/desktop"), "a+")
333 f.write("AUTOLOGIN_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 runner.quiet(['rpm', "--root=%s" % self.instroot, "--import", repo['repokey']])
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 if hasattr(repo, "ssl_verify"):
690 ssl_verify = repo.ssl_verify == "yes"
692 if hasattr(repo, "cost"):
695 if hasattr(repo, "priority"):
696 priority = repo.priority
698 repos[repo.name] = (repo.name, baseurl, mirrorlist, inc, exc,
699 proxy, proxy_username, proxy_password, debuginfo,
700 source, gpgkey, disable, ssl_verify, cost, priority)
702 return repos.values()
704 def convert_method_to_repo(ks):
706 ks.handler.repo.methodToRepo()
707 except (AttributeError, kserrors.KickstartError):
710 def get_packages(ks, required = []):
711 return ks.handler.packages.packageList + required
713 def get_groups(ks, required = []):
714 return ks.handler.packages.groupList + required
716 def get_excluded(ks, required = []):
717 return ks.handler.packages.excludedList + required
719 def get_partitions(ks, required = []):
720 return ks.handler.partition.partitions
722 def ignore_missing(ks):
723 return ks.handler.packages.handleMissing == ksconstants.KS_MISSING_IGNORE
725 def exclude_docs(ks):
726 return ks.handler.packages.excludeDocs
729 if hasattr(ks.handler.packages, "instLange"):
730 return ks.handler.packages.instLange
731 elif hasattr(ks.handler.packages, "instLangs"):
732 return ks.handler.packages.instLangs
735 def get_post_scripts(ks):
737 for s in ks.handler.scripts:
738 if s.type != ksparser.KS_SCRIPT_POST:
743 def add_repo(ks, repostr):
744 args = repostr.split()
745 repoobj = ks.handler.repo.parse(args[1:])
746 if repoobj and repoobj not in ks.handler.repo.repoList:
747 ks.handler.repo.repoList.append(repoobj)
749 def remove_all_repos(ks):
750 while len(ks.handler.repo.repoList) != 0:
751 del ks.handler.repo.repoList[0]
753 def remove_duplicate_repos(ks):
757 if len(ks.handler.repo.repoList) < 2:
759 if i >= len(ks.handler.repo.repoList) - 1:
761 name = ks.handler.repo.repoList[i].name
762 baseurl = ks.handler.repo.repoList[i].baseurl
763 if j < len(ks.handler.repo.repoList):
764 if (ks.handler.repo.repoList[j].name == name or \
765 ks.handler.repo.repoList[j].baseurl == baseurl):
766 del ks.handler.repo.repoList[j]
769 if j >= len(ks.handler.repo.repoList):
776 def resolve_groups(creatoropts, repometadata):
778 if 'zypp' == creatoropts['pkgmgr']:
780 ks = creatoropts['ks']
782 for repo in repometadata:
783 """ Mustn't replace group with package list if repo is ready for the corresponding package manager """
784 if iszypp and repo["patterns"]:
786 if not iszypp and repo["comps"]:
790 But we also must handle such cases, use zypp but repo only has comps,
791 use yum but repo only has patterns, use zypp but use_comps is true,
792 use yum but use_comps is false.
795 if iszypp and repo["comps"]:
796 groupfile = repo["comps"]
797 get_pkglist_handler = misc.get_pkglist_in_comps
798 if not iszypp and repo["patterns"]:
799 groupfile = repo["patterns"]
800 get_pkglist_handler = misc.get_pkglist_in_patterns
805 if i >= len(ks.handler.packages.groupList):
807 pkglist = get_pkglist_handler(ks.handler.packages.groupList[i].name, groupfile)
809 del ks.handler.packages.groupList[i]
811 if pkg not in ks.handler.packages.packageList:
812 ks.handler.packages.packageList.append(pkg)