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.
19 from __future__ import with_statement
29 from datetime import datetime
32 from mic import kickstart
33 from mic import msger, __version__ as VERSION
34 from mic.utils.errors import CreatorError, Abort
35 from mic.utils import misc, grabber, runner, fs_related as fs
36 from mic.chroot import kill_proc_inchroot
37 from mic.archive import get_archive_suffixes
38 from mic.conf import configmgr
39 from mic.utils.grabber import myurlgrab
40 #post script max run time
43 class BaseImageCreator(object):
44 """Installs a system to a chroot directory.
46 ImageCreator is the simplest creator class available; it will install and
47 configure a system image according to the supplied kickstart file.
51 import mic.imgcreate as imgcreate
52 ks = imgcreate.read_kickstart("foo.ks")
53 imgcreate.ImageCreator(ks, "foo").create()
62 def __init__(self, createopts = None, pkgmgr = None):
63 """Initialize an ImageCreator instance.
65 ks -- a pykickstart.KickstartParser instance; this instance will be
66 used to drive the install by e.g. providing the list of packages
67 to be installed, the system configuration and %post scripts
69 name -- a name for the image; used for e.g. image filenames or
76 self.__builddir = None
77 self.__bindmounts = []
81 self.tmpdir = "/var/tmp/mic"
82 self.cachedir = "/var/tmp/mic/cache"
83 self.workdir = "/var/tmp/mic/build"
85 self.installerfw_prefix = "INSTALLERFW_"
86 self.target_arch = "noarch"
87 self.strict_mode = False
88 self._local_pkgs_path = None
91 self.multiple_partitions = False
94 # If the kernel is save to the destdir when copy_kernel cmd is called.
95 self._need_copy_kernel = False
96 # setup tmpfs tmpdir when enabletmpfs is True
97 self.enabletmpfs = False
100 # Mapping table for variables that have different names.
101 optmap = {"pkgmgr" : "pkgmgr_name",
102 "arch" : "target_arch",
103 "local_pkgs_path" : "_local_pkgs_path",
104 "copy_kernel" : "_need_copy_kernel",
105 "strict_mode" : "strict_mode",
108 # update setting from createopts
109 for key in createopts.keys():
114 setattr(self, option, createopts[key])
116 self.destdir = os.path.abspath(os.path.expanduser(self.destdir))
119 if '@NAME@' in self.pack_to:
120 self.pack_to = self.pack_to.replace('@NAME@', self.name)
121 (tar, ext) = os.path.splitext(self.pack_to)
122 if ext in (".gz", ".bz2", ".lzo", ".bz") and tar.endswith(".tar"):
124 if ext not in get_archive_suffixes():
125 self.pack_to += ".tar"
127 self._dep_checks = ["ls", "bash", "cp", "echo", "modprobe"]
129 # Output image file names
131 # Output info related with manifest
132 self.image_files = {}
133 # A flag to generate checksum
134 self._genchecksum = False
136 self._alt_initrd_name = None
138 self._recording_pkgs = []
140 # available size in root fs, init to 0
141 self._root_fs_avail = 0
143 # Name of the disk image file that is created.
144 self._img_name = None
146 self.image_format = None
148 # Save qemu emulator file names in order to clean up it finally
149 self.qemu_emulators = []
151 # No ks provided when called by convertor, so skip the dependency check
153 # If we have btrfs partition we need to check necessary tools
154 for part in self.ks.handler.partition.partitions:
155 if part.fstype and part.fstype == "btrfs":
156 self._dep_checks.append("mkfs.btrfs")
158 if part.fstype == "cpio":
160 if len(self.ks.handler.partition.partitions) > 1:
161 self.multiple_partitions = True
164 if self.target_arch.startswith("arm"):
165 for dep in self._dep_checks:
166 if dep == "extlinux":
167 self._dep_checks.remove(dep)
169 if not os.path.exists("/usr/bin/qemu-arm") or \
170 not misc.is_statically_linked("/usr/bin/qemu-arm"):
171 self._dep_checks.append("qemu-arm-static")
173 if os.path.exists("/proc/sys/vm/vdso_enabled"):
174 vdso_fh = open("/proc/sys/vm/vdso_enabled","r")
175 vdso_value = vdso_fh.read().strip()
177 if (int)(vdso_value) == 1:
178 msger.warning("vdso is enabled on your host, which might "
179 "cause problems with arm emulations.\n"
180 "\tYou can disable vdso with following command before "
181 "starting image build:\n"
182 "\techo 0 | sudo tee /proc/sys/vm/vdso_enabled")
183 elif self.target_arch == "mipsel":
184 for dep in self._dep_checks:
185 if dep == "extlinux":
186 self._dep_checks.remove(dep)
188 if not os.path.exists("/usr/bin/qemu-mipsel") or \
189 not misc.is_statically_linked("/usr/bin/qemu-mipsel"):
190 self._dep_checks.append("qemu-mipsel-static")
192 if os.path.exists("/proc/sys/vm/vdso_enabled"):
193 vdso_fh = open("/proc/sys/vm/vdso_enabled","r")
194 vdso_value = vdso_fh.read().strip()
196 if (int)(vdso_value) == 1:
197 msger.warning("vdso is enabled on your host, which might "
198 "cause problems with mipsel emulations.\n"
199 "\tYou can disable vdso with following command before "
200 "starting image build:\n"
201 "\techo 0 | sudo tee /proc/sys/vm/vdso_enabled")
203 # make sure the specified tmpdir and cachedir exist
204 if not os.path.exists(self.tmpdir):
205 os.makedirs(self.tmpdir)
206 if not os.path.exists(self.cachedir):
207 os.makedirs(self.cachedir)
213 def __get_instroot(self):
214 if self.__builddir is None:
215 raise CreatorError("_instroot is not valid before calling mount()")
216 return self.__builddir + "/install_root"
217 _instroot = property(__get_instroot)
218 """The location of the install root directory.
220 This is the directory into which the system is installed. Subclasses may
221 mount a filesystem image here or copy files to/from here.
223 Note, this directory does not exist before ImageCreator.mount() is called.
225 Note also, this is a read-only attribute.
229 def __get_outdir(self):
230 if self.__builddir is None:
231 raise CreatorError("_outdir is not valid before calling mount()")
232 return self.__builddir + "/out"
233 _outdir = property(__get_outdir)
234 """The staging location for the final image.
236 This is where subclasses should stage any files that are part of the final
237 image. ImageCreator.package() will copy any files found here into the
238 requested destination directory.
240 Note, this directory does not exist before ImageCreator.mount() is called.
242 Note also, this is a read-only attribute.
248 # Hooks for subclasses
250 def _mount_instroot(self, base_on = None):
251 """Mount or prepare the install root directory.
253 This is the hook where subclasses may prepare the install root by e.g.
254 mounting creating and loopback mounting a filesystem image to
257 There is no default implementation.
259 base_on -- this is the value passed to mount() and can be interpreted
260 as the subclass wishes; it might e.g. be the location of
261 a previously created ISO containing a system image.
266 def _unmount_instroot(self):
267 """Undo anything performed in _mount_instroot().
269 This is the hook where subclasses must undo anything which was done
270 in _mount_instroot(). For example, if a filesystem image was mounted
271 onto _instroot, it should be unmounted here.
273 There is no default implementation.
278 def _create_bootconfig(self):
279 """Configure the image so that it's bootable.
281 This is the hook where subclasses may prepare the image for booting by
282 e.g. creating an initramfs and bootloader configuration.
284 This hook is called while the install root is still mounted, after the
285 packages have been installed and the kickstart configuration has been
286 applied, but before the %post scripts have been executed.
288 There is no default implementation.
293 def _stage_final_image(self):
294 """Stage the final system image in _outdir.
296 This is the hook where subclasses should place the image in _outdir
297 so that package() can copy it to the requested destination directory.
299 By default, this moves the install root into _outdir.
302 shutil.move(self._instroot, self._outdir + "/" + self.name)
304 def get_installed_packages(self):
305 return self._pkgs_content.keys()
307 def _save_recording_pkgs(self, destdir):
308 """Save the list or content of installed packages to file.
310 pkgs = self._pkgs_content.keys()
311 pkgs.sort() # inplace op
313 if not os.path.exists(destdir):
317 if 'vcs' in self._recording_pkgs:
318 vcslst = ["%s %s" % (k, v) for (k, v) in self._pkgs_vcsinfo.items()]
319 content = '\n'.join(sorted(vcslst))
320 elif 'name' in self._recording_pkgs:
321 content = '\n'.join(pkgs)
323 namefile = os.path.join(destdir, self.name + '.packages')
324 f = open(namefile, "w")
327 self.outimage.append(namefile);
329 # if 'content', save more details
330 if 'content' in self._recording_pkgs:
331 contfile = os.path.join(destdir, self.name + '.files')
332 f = open(contfile, "w")
337 pkgcont = self._pkgs_content[pkg]
339 content += '\n '.join(pkgcont)
345 self.outimage.append(contfile)
347 if 'license' in self._recording_pkgs:
348 licensefile = os.path.join(destdir, self.name + '.license')
349 f = open(licensefile, "w")
351 f.write('Summary:\n')
352 for license in reversed(sorted(self._pkgs_license, key=\
353 lambda license: len(self._pkgs_license[license]))):
354 f.write(" - %s: %s\n" \
355 % (license, len(self._pkgs_license[license])))
357 f.write('\nDetails:\n')
358 for license in reversed(sorted(self._pkgs_license, key=\
359 lambda license: len(self._pkgs_license[license]))):
360 f.write(" - %s:\n" % (license))
361 for pkg in sorted(self._pkgs_license[license]):
362 f.write(" - %s\n" % (pkg))
366 self.outimage.append(licensefile)
368 def _get_required_packages(self):
369 """Return a list of required packages.
371 This is the hook where subclasses may specify a set of packages which
372 it requires to be installed.
374 This returns an empty list by default.
376 Note, subclasses should usually chain up to the base class
377 implementation of this hook.
382 def _get_excluded_packages(self):
383 """Return a list of excluded packages.
385 This is the hook where subclasses may specify a set of packages which
386 it requires _not_ to be installed.
388 This returns an empty list by default.
390 Note, subclasses should usually chain up to the base class
391 implementation of this hook.
396 def _get_local_packages(self):
397 """Return a list of rpm path to be local installed.
399 This is the hook where subclasses may specify a set of rpms which
400 it requires to be installed locally.
402 This returns an empty list by default.
404 Note, subclasses should usually chain up to the base class
405 implementation of this hook.
408 if self._local_pkgs_path:
409 if os.path.isdir(self._local_pkgs_path):
411 os.path.join(self._local_pkgs_path, '*.rpm'))
412 elif os.path.splitext(self._local_pkgs_path)[-1] == '.rpm':
413 return [self._local_pkgs_path]
417 def _get_fstab(self):
418 """Return the desired contents of /etc/fstab.
420 This is the hook where subclasses may specify the contents of
421 /etc/fstab by returning a string containing the desired contents.
423 A sensible default implementation is provided.
426 s = "/dev/root / %s %s 0 0\n" \
428 "defaults,noatime" if not self._fsopts else self._fsopts)
429 s += self._get_fstab_special()
432 def _get_fstab_special(self):
433 s = "devpts /dev/pts devpts gid=5,mode=620 0 0\n"
434 s += "tmpfs /dev/shm tmpfs defaults 0 0\n"
435 s += "proc /proc proc defaults 0 0\n"
436 s += "sysfs /sys sysfs defaults 0 0\n"
439 def _set_part_env(self, pnum, prop, value):
440 """ This is a helper function which generates an environment variable
441 for a property "prop" with value "value" of a partition number "pnum".
443 The naming convention is:
444 * Variables start with INSTALLERFW_PART
445 * Then goes the partition number, the order is the same as
446 specified in the KS file
447 * Then goes the property name
455 name = self.installerfw_prefix + ("PART%d_" % pnum) + prop
456 return { name : value }
458 def _get_post_scripts_env(self, in_chroot):
459 """Return an environment dict for %post scripts.
461 This is the hook where subclasses may specify some environment
462 variables for %post scripts by return a dict containing the desired
465 in_chroot -- whether this %post script is to be executed chroot()ed
472 for p in kickstart.get_partitions(self.ks):
473 env.update(self._set_part_env(pnum, "SIZE", p.size))
474 env.update(self._set_part_env(pnum, "MOUNTPOINT", p.mountpoint))
475 env.update(self._set_part_env(pnum, "FSTYPE", p.fstype))
476 env.update(self._set_part_env(pnum, "LABEL", p.label))
477 env.update(self._set_part_env(pnum, "FSOPTS", p.fsopts))
478 env.update(self._set_part_env(pnum, "BOOTFLAG", p.active))
479 env.update(self._set_part_env(pnum, "ALIGN", p.align))
480 env.update(self._set_part_env(pnum, "TYPE_ID", p.part_type))
481 env.update(self._set_part_env(pnum, "UUID", p.uuid))
482 env.update(self._set_part_env(pnum, "DEVNODE",
483 "/dev/%s%d" % (p.disk, pnum + 1)))
484 env.update(self._set_part_env(pnum, "DISK_DEVNODE",
489 env[self.installerfw_prefix + "PART_COUNT"] = str(pnum)
491 # Partition table format
492 ptable_format = self.ks.handler.bootloader.ptable
493 env[self.installerfw_prefix + "PTABLE_FORMAT"] = ptable_format
495 # The kerned boot parameters
496 kernel_opts = self.ks.handler.bootloader.appendLine
497 env[self.installerfw_prefix + "KERNEL_OPTS"] = kernel_opts
499 # Name of the image creation tool
500 env[self.installerfw_prefix + "INSTALLER_NAME"] = "mic"
502 # The real current location of the mounted file-systems
506 mount_prefix = self._instroot
507 env[self.installerfw_prefix + "MOUNT_PREFIX"] = mount_prefix
509 # These are historical variables which lack the common name prefix
511 env["INSTALL_ROOT"] = self._instroot
512 env["IMG_NAME"] = self._name
516 def __get_imgname(self):
518 _name = property(__get_imgname)
519 """The name of the image file.
523 def _get_kernel_versions(self):
524 """Return a dict detailing the available kernel types/versions.
526 This is the hook where subclasses may override what kernel types and
527 versions should be available for e.g. creating the booloader
530 A dict should be returned mapping the available kernel types to a list
531 of the available versions for those kernels.
533 The default implementation uses rpm to iterate over everything
534 providing 'kernel', finds /boot/vmlinuz-* and returns the version
535 obtained from the vmlinuz filename. (This can differ from the kernel
536 RPM's n-v-r in the case of e.g. xen)
539 def get_kernel_versions(instroot):
542 files = glob.glob(instroot + "/boot/vmlinuz-*")
544 version = os.path.basename(file)[8:]
547 versions.add(version)
548 ret["kernel"] = list(versions)
551 def get_version(header):
553 for f in header['filenames']:
554 if f.startswith('/boot/vmlinuz-'):
559 return get_kernel_versions(self._instroot)
561 ts = rpm.TransactionSet(self._instroot)
564 for header in ts.dbMatch('provides', 'kernel'):
565 version = get_version(header)
569 name = header['name']
571 ret[name] = [version]
572 elif not version in ret[name]:
573 ret[name].append(version)
579 # Helpers for subclasses
581 def _do_bindmounts(self):
582 """Mount various system directories onto _instroot.
584 This method is called by mount(), but may also be used by subclasses
585 in order to re-mount the bindmounts after modifying the underlying
589 for b in self.__bindmounts:
592 def _undo_bindmounts(self):
593 """Unmount the bind-mounted system directories from _instroot.
595 This method is usually only called by unmount(), but may also be used
596 by subclasses in order to gain access to the filesystem obscured by
597 the bindmounts - e.g. in order to create device nodes on the image
601 self.__bindmounts.reverse()
602 for b in self.__bindmounts:
606 """Chroot into the install root.
608 This method may be used by subclasses when executing programs inside
609 the install root e.g.
611 subprocess.call(["/bin/ls"], preexec_fn = self.chroot)
614 os.chroot(self._instroot)
617 def _mkdtemp(self, prefix = "tmp-"):
618 """Create a temporary directory.
620 This method may be used by subclasses to create a temporary directory
621 for use in building the final image - e.g. a subclass might create
622 a temporary directory in order to bundle a set of files into a package.
624 The subclass may delete this directory if it wishes, but it will be
625 automatically deleted by cleanup().
627 The absolute path to the temporary directory is returned.
629 Note, this method should only be called after mount() has been called.
631 prefix -- a prefix which should be used when creating the directory;
635 self.__ensure_builddir()
636 return tempfile.mkdtemp(dir = self.__builddir, prefix = prefix)
638 def _mkstemp(self, prefix = "tmp-"):
639 """Create a temporary file.
641 This method may be used by subclasses to create a temporary file
642 for use in building the final image - e.g. a subclass might need
643 a temporary location to unpack a compressed file.
645 The subclass may delete this file if it wishes, but it will be
646 automatically deleted by cleanup().
648 A tuple containing a file descriptor (returned from os.open() and the
649 absolute path to the temporary directory is returned.
651 Note, this method should only be called after mount() has been called.
653 prefix -- a prefix which should be used when creating the file;
657 self.__ensure_builddir()
658 return tempfile.mkstemp(dir = self.__builddir, prefix = prefix)
660 def _mktemp(self, prefix = "tmp-"):
661 """Create a temporary file.
663 This method simply calls _mkstemp() and closes the returned file
666 The absolute path to the temporary file is returned.
668 Note, this method should only be called after mount() has been called.
670 prefix -- a prefix which should be used when creating the file;
675 (f, path) = self._mkstemp(prefix)
681 # Actual implementation
683 def __ensure_builddir(self):
684 if not self.__builddir is None:
688 self.workdir = os.path.join(self.tmpdir, "build")
689 if not os.path.exists(self.workdir):
690 os.makedirs(self.workdir)
691 self.__builddir = tempfile.mkdtemp(dir = self.workdir,
692 prefix = "imgcreate-")
693 except OSError, (err, msg):
694 raise CreatorError("Failed create build directory in %s: %s" %
697 def get_cachedir(self, cachedir = None):
701 self.__ensure_builddir()
703 self.cachedir = cachedir
705 self.cachedir = self.__builddir + "/mic-cache"
706 fs.makedirs(self.cachedir)
709 def __sanity_check(self):
710 """Ensure that the config we've been given is same."""
711 if not (kickstart.get_packages(self.ks) or
712 kickstart.get_groups(self.ks)):
713 raise CreatorError("No packages or groups specified")
715 kickstart.convert_method_to_repo(self.ks)
717 if not kickstart.get_repos(self.ks):
718 raise CreatorError("No repositories specified")
720 def __write_fstab(self):
721 if kickstart.use_installerfw(self.ks, "fstab"):
722 # The fstab file will be generated by installer framework scripts
725 fstab_contents = self._get_fstab()
727 fstab = open(self._instroot + "/etc/fstab", "w")
728 fstab.write(fstab_contents)
731 def __create_minimal_dev(self):
732 """Create a minimal /dev so that we don't corrupt the host /dev"""
733 origumask = os.umask(0000)
734 devices = (('null', 1, 3, 0666),
735 ('urandom',1, 9, 0666),
736 ('random', 1, 8, 0666),
737 ('full', 1, 7, 0666),
738 ('ptmx', 5, 2, 0666),
740 ('zero', 1, 5, 0666))
742 links = (("/proc/self/fd", "/dev/fd"),
743 ("/proc/self/fd/0", "/dev/stdin"),
744 ("/proc/self/fd/1", "/dev/stdout"),
745 ("/proc/self/fd/2", "/dev/stderr"))
747 for (node, major, minor, perm) in devices:
748 if not os.path.exists(self._instroot + "/dev/" + node):
749 os.mknod(self._instroot + "/dev/" + node,
751 os.makedev(major,minor))
753 for (src, dest) in links:
754 if not os.path.exists(self._instroot + dest):
755 os.symlink(src, self._instroot + dest)
759 def __setup_tmpdir(self):
760 if not self.enabletmpfs:
763 runner.show('mount -t tmpfs -o size=4G tmpfs %s' % self.workdir)
765 def __clean_tmpdir(self):
766 if not self.enabletmpfs:
769 runner.show('umount -l %s' % self.workdir)
772 #Add tpk-install option
773 createopts = configmgr.create
774 if createopts['tpk_install']:
775 path = createopts['tpk_install']
776 file_list = os.listdir(path)
778 sub = os.path.splitext(f)[1]
780 raise CreatorError("Not all files in the path: "+path +" is tpk")
782 tpk_dir = "/usr/apps/.preload-tpk"
783 fs.makedirs(self._instroot + "/usr/apps")
784 fs.makedirs(self._instroot + tpk_dir)
786 shutil.copy(path+"/"+f,self._instroot + tpk_dir)
788 def mount(self, base_on = None, cachedir = None):
789 """Setup the target filesystem in preparation for an install.
791 This function sets up the filesystem which the ImageCreator will
792 install into and configure. The ImageCreator class merely creates an
793 install root directory, bind mounts some system directories (e.g. /dev)
794 and writes out /etc/fstab. Other subclasses may also e.g. create a
795 sparse file, format it and loopback mount it to the install root.
797 base_on -- a previous install on which to base this install; defaults
798 to None, causing a new image to be created
800 cachedir -- a directory in which to store the Yum cache; defaults to
801 None, causing a new cache to be created; by setting this
802 to another directory, the same cache can be reused across
806 self.__setup_tmpdir()
807 self.__ensure_builddir()
809 # prevent popup dialog in Ubuntu(s)
810 misc.hide_loopdev_presentation()
812 fs.makedirs(self._instroot)
813 fs.makedirs(self._outdir)
815 self._mount_instroot(base_on)
817 for d in ("/dev/pts",
824 fs.makedirs(self._instroot + d)
826 if self.target_arch and self.target_arch.startswith("arm") or \
827 self.target_arch == "aarch64":
828 self.qemu_emulators = misc.setup_qemu_emulator(self._instroot,
831 self.get_cachedir(cachedir)
833 # bind mount system directories into _instroot
834 for (f, dest) in [("/sys", None),
836 ("/proc/sys/fs/binfmt_misc", None),
838 self.__bindmounts.append(
840 f, self._instroot, dest))
842 self._do_bindmounts()
844 self.__create_minimal_dev()
846 if os.path.exists(self._instroot + "/etc/mtab"):
847 os.unlink(self._instroot + "/etc/mtab")
848 os.symlink("../proc/mounts", self._instroot + "/etc/mtab")
852 # get size of available space in 'instroot' fs
853 self._root_fs_avail = misc.get_filesystem_avail(self._instroot)
857 """Unmounts the target filesystem.
859 The ImageCreator class detaches the system from the install root, but
860 other subclasses may also detach the loopback mounted filesystem image
861 from the install root.
865 mtab = self._instroot + "/etc/mtab"
866 if not os.path.islink(mtab):
867 os.unlink(self._instroot + "/etc/mtab")
869 for qemu_emulator in self.qemu_emulators:
870 os.unlink(self._instroot + qemu_emulator)
874 self._undo_bindmounts()
876 """ Clean up yum garbage """
878 instroot_pdir = os.path.dirname(self._instroot + self._instroot)
879 if os.path.exists(instroot_pdir):
880 shutil.rmtree(instroot_pdir, ignore_errors = True)
881 yumlibdir = self._instroot + "/var/lib/yum"
882 if os.path.exists(yumlibdir):
883 shutil.rmtree(yumlibdir, ignore_errors = True)
887 self._unmount_instroot()
889 # reset settings of popup dialog in Ubuntu(s)
890 misc.unhide_loopdev_presentation()
894 """Unmounts the target filesystem and deletes temporary files.
896 This method calls unmount() and then deletes any temporary files and
897 directories that were created on the host system while building the
900 Note, make sure to call this method once finished with the creator
901 instance in order to ensure no stale files are left on the host e.g.:
903 creator = ImageCreator(ks, name)
910 if not self.__builddir:
913 kill_proc_inchroot(self._instroot)
917 shutil.rmtree(self.__builddir, ignore_errors = True)
918 self.__builddir = None
920 self.__clean_tmpdir()
922 def __is_excluded_pkg(self, pkg):
923 if pkg in self._excluded_pkgs:
924 self._excluded_pkgs.remove(pkg)
927 for xpkg in self._excluded_pkgs:
928 if xpkg.endswith('*'):
929 if pkg.startswith(xpkg[:-1]):
931 elif xpkg.startswith('*'):
932 if pkg.endswith(xpkg[1:]):
937 def __select_packages(self, pkg_manager):
939 for pkg in self._required_pkgs:
940 e = pkg_manager.selectPackage(pkg)
942 if kickstart.ignore_missing(self.ks):
943 skipped_pkgs.append(pkg)
944 elif self.__is_excluded_pkg(pkg):
945 skipped_pkgs.append(pkg)
947 raise CreatorError("Failed to find package '%s' : %s" %
950 for pkg in skipped_pkgs:
951 msger.warning("Skipping missing package '%s'" % (pkg,))
953 def __select_groups(self, pkg_manager):
955 for group in self._required_groups:
956 e = pkg_manager.selectGroup(group.name, group.include)
958 if kickstart.ignore_missing(self.ks):
959 skipped_groups.append(group)
961 raise CreatorError("Failed to find group '%s' : %s" %
964 for group in skipped_groups:
965 msger.warning("Skipping missing group '%s'" % (group.name,))
967 def __deselect_packages(self, pkg_manager):
968 for pkg in self._excluded_pkgs:
969 pkg_manager.deselectPackage(pkg)
971 def __localinst_packages(self, pkg_manager):
972 for rpm_path in self._get_local_packages():
973 pkg_manager.installLocal(rpm_path)
975 def __preinstall_packages(self, pkg_manager):
979 self._preinstall_pkgs = kickstart.get_pre_packages(self.ks)
980 for pkg in self._preinstall_pkgs:
981 pkg_manager.preInstall(pkg)
983 def __check_packages(self, pkg_manager):
984 for pkg in self.check_pkgs:
985 pkg_manager.checkPackage(pkg)
987 def __attachment_packages(self, pkg_manager):
991 self._attachment = []
992 for item in kickstart.get_attachment(self.ks):
993 if item.startswith('/'):
994 fpaths = os.path.join(self._instroot, item.lstrip('/'))
995 for fpath in glob.glob(fpaths):
996 self._attachment.append(fpath)
999 filelist = pkg_manager.getFilelist(item)
1001 # found rpm in rootfs
1002 for pfile in pkg_manager.getFilelist(item):
1003 fpath = os.path.join(self._instroot, pfile.lstrip('/'))
1004 self._attachment.append(fpath)
1007 # try to retrieve rpm file
1008 (url, proxies) = pkg_manager.package_url(item)
1010 msger.warning("Can't get url from repo for %s" % item)
1012 fpath = os.path.join(self.cachedir, os.path.basename(url))
1013 if not os.path.exists(fpath):
1016 fpath = grabber.myurlgrab(url.full, fpath, proxies, None)
1017 except CreatorError:
1020 tmpdir = self._mkdtemp()
1021 misc.extract_rpm(fpath, tmpdir)
1022 for (root, dirs, files) in os.walk(tmpdir):
1024 fpath = os.path.join(root, fname)
1025 self._attachment.append(fpath)
1027 def install(self, repo_urls=None):
1028 """Install packages into the install root.
1030 This function installs the packages listed in the supplied kickstart
1031 into the install root. By default, the packages are installed from the
1032 repository URLs specified in the kickstart.
1034 repo_urls -- a dict which maps a repository name to a repository;
1035 if supplied, this causes any repository URLs specified in
1036 the kickstart to be overridden.
1039 def checkScriptletError(dirname, suffix):
1040 if os.path.exists(dirname):
1041 list = os.listdir(dirname)
1043 filepath = os.path.join(dirname, line)
1044 if os.path.isfile(filepath) and 0 < line.find(suffix):
1051 def showErrorInfo(filepath):
1052 if os.path.isfile(filepath):
1053 for line in open(filepath):
1054 msger.info("The error install package info: %s" % line)
1056 msger.info("%s is not found." % filepath)
1058 def get_ssl_verify(ssl_verify=None):
1059 if ssl_verify is not None:
1060 return not ssl_verify.lower().strip() == 'no'
1062 return not self.ssl_verify.lower().strip() == 'no'
1064 # initialize pkg list to install
1066 self.__sanity_check()
1068 self._required_pkgs = \
1069 kickstart.get_packages(self.ks, self._get_required_packages())
1070 self._excluded_pkgs = \
1071 kickstart.get_excluded(self.ks, self._get_excluded_packages())
1072 self._required_groups = kickstart.get_groups(self.ks)
1074 self._required_pkgs = None
1075 self._excluded_pkgs = None
1076 self._required_groups = None
1079 repo_urls = self.extrarepos
1081 pkg_manager = self.get_pkg_manager()
1084 if hasattr(self, 'install_pkgs') and self.install_pkgs:
1085 if 'debuginfo' in self.install_pkgs:
1086 pkg_manager.install_debuginfo = True
1089 for repo in kickstart.get_repos(self.ks, repo_urls, self.ignore_ksrepo):
1090 (name, baseurl, mirrorlist, inc, exc,
1091 proxy, proxy_username, proxy_password, debuginfo,
1092 source, gpgkey, disable, ssl_verify, nocache,
1093 cost, priority) = repo
1095 ssl_verify = get_ssl_verify(ssl_verify)
1096 yr = pkg_manager.addRepository(name, baseurl, mirrorlist, proxy,
1097 proxy_username, proxy_password, inc, exc, ssl_verify,
1098 nocache, cost, priority)
1100 if kickstart.exclude_docs(self.ks):
1101 rpm.addMacro("_excludedocs", "1")
1102 rpm.addMacro("_dbpath", "/var/lib/rpm")
1103 rpm.addMacro("__file_context_path", "%{nil}")
1104 if kickstart.inst_langs(self.ks) != None:
1105 rpm.addMacro("_install_langs", kickstart.inst_langs(self.ks))
1108 self.__preinstall_packages(pkg_manager)
1109 self.__select_packages(pkg_manager)
1110 self.__select_groups(pkg_manager)
1111 self.__deselect_packages(pkg_manager)
1112 self.__localinst_packages(pkg_manager)
1113 self.__check_packages(pkg_manager)
1115 BOOT_SAFEGUARD = 256L * 1024 * 1024 # 256M
1116 checksize = self._root_fs_avail
1118 checksize -= BOOT_SAFEGUARD
1119 if self.target_arch:
1120 pkg_manager._add_prob_flags(rpm.RPMPROB_FILTER_IGNOREARCH)
1122 # If we have multiple partitions, don't check diskspace when rpm run transaction
1123 # because rpm check '/' partition only.
1124 if self.multiple_partitions:
1125 pkg_manager._add_prob_flags(rpm.RPMPROB_FILTER_DISKSPACE)
1126 pkg_manager.runInstall(checksize)
1127 except CreatorError, e:
1129 except KeyboardInterrupt:
1132 self._pkgs_content = pkg_manager.getAllContent()
1133 self._pkgs_license = pkg_manager.getPkgsLicense()
1134 self._pkgs_vcsinfo = pkg_manager.getVcsInfo()
1135 self.__attachment_packages(pkg_manager)
1139 if checkScriptletError(self._instroot + "/tmp/.postscript/error/", "_error"):
1140 showErrorInfo(self._instroot + "/tmp/.preload_install_error")
1141 raise CreatorError('scriptlet errors occurred')
1146 # do some clean up to avoid lvm info leakage. this sucks.
1147 for subdir in ("cache", "backup", "archive"):
1148 lvmdir = self._instroot + "/etc/lvm/" + subdir
1150 for f in os.listdir(lvmdir):
1151 os.unlink(lvmdir + "/" + f)
1155 def tpkinstall(self):
1157 tpk_pkgs = kickstart.get_tpkpackages(self.ks)
1158 tpk_repoList = kickstart.get_tpkrepos(self.ks)
1159 if tpk_repoList and tpk_pkgs:
1160 tpk_dir = "/usr/apps/.preload-tpk"
1161 fs.makedirs(self._instroot + "/usr/apps")
1162 fs.makedirs(self._instroot + tpk_dir)
1163 for pkg in tpk_pkgs:
1165 for tpk_repo in tpk_repoList:
1166 if hasattr(tpk_repo,'baseurl') and tpk_repo.baseurl.startswith("file:"):
1167 tpk_repourl = tpk_repo.baseurl.replace('file:','')
1168 tpk_repourl = "/%s" % tpk_repourl.lstrip('/')
1169 tpk_pkgpath = tpk_repourl + "/"+ pkg
1170 if os.path.isfile(tpk_pkgpath):
1171 shutil.copy(tpk_pkgpath,self._instroot + tpk_dir)
1174 elif hasattr(tpk_repo,'baseurl'):
1175 url = tpk_repo.baseurl.join(pkg)
1176 filename = self._instroot+tpk_dir+"/"+pkg
1177 if tpk_repo.baseurl.startswith("http:"):
1179 status = urllib.urlopen(url).code
1181 filename = myurlgrab(url.full, filename, None)
1184 elif status == 404 or status == None:
1186 #url is ok, then download, url wrong, check other url.
1187 elif tpk_repo.baseurl.startswith("https:") :
1190 filename = myurlgrab(url.full, filename, None)
1191 except CreatorError:
1194 raise CreatorError("Tpk package missing.")
1196 def postinstall(self):
1197 self.copy_attachment()
1199 def _get_sign_scripts_env(self):
1200 """Return an environment dict for %post-umount scripts.
1202 This is the hook where subclasses may specify some environment
1203 variables for %post-umount scripts by return a dict containing the
1204 desired environment.
1209 # Directory path of images
1211 env['IMG_DIR_PATH'] = str(self._imgdir)
1215 for item in self._instloops:
1216 imgfiles.append(item['name'])
1218 imgpaths.append(os.path.join(self._imgdir, item['name']))
1221 env['IMG_FILES'] = ' '.join(imgfiles)
1223 # Absolute path of images
1224 env['IMG_PATHS'] = ' '.join(imgpaths)
1228 def run_sign_scripts(self):
1229 if kickstart.get_sign_scripts(self.ks)==[]:
1231 msger.info("Running sign scripts ...")
1232 if os.path.exists(self._instroot + "/tmp"):
1233 shutil.rmtree(self._instroot + "/tmp")
1234 os.mkdir (self._instroot + "/tmp", 0755)
1235 for s in kickstart.get_sign_scripts(self.ks):
1236 (fd, path) = tempfile.mkstemp(prefix = "ks-runscript-",
1237 dir = self._instroot + "/tmp")
1238 s.script = s.script.replace("\r", "")
1239 os.write(fd, s.script)
1240 if s.interp == '/bin/sh' or s.interp == '/bin/bash':
1242 os.write(fd, 'exit 0\n')
1244 os.chmod(path, 0700)
1246 for item in os.listdir(self._imgdir):
1247 sub = os.path.splitext(item)[1]
1249 shutil.move(os.path.join(self._imgdir, item),
1250 os.path.join(self._instroot + "/tmp", item))
1251 oldoutdir = os.getcwd()
1252 os.chdir(self._instroot + "/tmp")
1254 env = self._get_sign_scripts_env()
1255 #*.img files are moved to self._instroot + "/tmp" directory in running runscripts
1256 env['IMG_PATHS'] = env['IMG_PATHS'].replace(self._imgdir,self._instroot + "/tmp")
1259 p = subprocess.Popen([s.interp, path],
1261 stdout = subprocess.PIPE,
1262 stderr = subprocess.STDOUT)
1263 while p.poll() == None:
1264 msger.info(p.stdout.readline().strip())
1265 if p.returncode != 0:
1266 raise CreatorError("Failed to execute %%sign script "
1267 "with '%s'" % (s.interp))
1268 except OSError, (err, msg):
1269 raise CreatorError("Failed to execute %%sign script "
1270 "with '%s' : %s" % (s.interp, msg))
1274 for item in os.listdir(self._instroot + "/tmp"):
1275 shutil.move(os.path.join(self._instroot + "/tmp", item),
1276 os.path.join(self._imgdir, item))
1278 def __run_post_scripts(self):
1279 msger.info("Running post scripts ...")
1280 if os.path.exists(self._instroot + "/tmp"):
1281 shutil.rmtree(self._instroot + "/tmp")
1282 os.mkdir (self._instroot + "/tmp", 0755)
1283 for s in kickstart.get_post_scripts(self.ks):
1284 (fd, path) = tempfile.mkstemp(prefix = "ks-postscript-",
1285 dir = self._instroot + "/tmp")
1287 s.script = s.script.replace("\r", "")
1288 os.write(fd, s.script)
1289 if s.interp == '/bin/sh' or s.interp == '/bin/bash':
1291 os.write(fd, 'exit 0\n')
1293 os.chmod(path, 0700)
1295 env = self._get_post_scripts_env(s.inChroot)
1296 if 'PATH' not in env:
1297 env['PATH'] = '/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin'
1303 preexec = self._chroot
1304 script = "/tmp/" + os.path.basename(path)
1306 start_time = time.time()
1309 p = subprocess.Popen([s.interp, script],
1310 preexec_fn = preexec,
1312 stdout = subprocess.PIPE,
1313 stderr = subprocess.STDOUT)
1314 while p.poll() == None:
1315 msger.info(p.stdout.readline().strip())
1316 end_time = time.time()
1317 if (end_time - start_time)/60 > MAX_RUN_TIME:
1318 raise CreatorError("Your post script is executed more than "+MAX_RUN_TIME+"mins, please check it!")
1319 if p.returncode != 0:
1320 raise CreatorError("Failed to execute %%post script "
1321 "with '%s'" % (s.interp))
1322 except OSError, (err, msg):
1323 raise CreatorError("Failed to execute %%post script "
1324 "with '%s' : %s" % (s.interp, msg))
1328 def __save_repo_keys(self, repodata):
1332 gpgkeydir = "/etc/pki/rpm-gpg"
1333 fs.makedirs(self._instroot + gpgkeydir)
1334 for repo in repodata:
1336 repokey = gpgkeydir + "/RPM-GPG-KEY-%s" % repo["name"]
1337 shutil.copy(repo["repokey"], self._instroot + repokey)
1339 def configure(self, repodata = None):
1340 """Configure the system image according to the kickstart.
1342 This method applies the (e.g. keyboard or network) configuration
1343 specified in the kickstart and executes the kickstart %post scripts.
1345 If necessary, it also prepares the image to be bootable by e.g.
1346 creating an initrd and bootloader configuration.
1349 ksh = self.ks.handler
1351 msger.info('Applying configurations ...')
1353 kickstart.LanguageConfig(self._instroot).apply(ksh.lang)
1354 kickstart.KeyboardConfig(self._instroot).apply(ksh.keyboard)
1355 kickstart.TimezoneConfig(self._instroot).apply(ksh.timezone)
1356 #kickstart.AuthConfig(self._instroot).apply(ksh.authconfig)
1357 kickstart.FirewallConfig(self._instroot).apply(ksh.firewall)
1358 kickstart.RootPasswordConfig(self._instroot).apply(ksh.rootpw)
1359 kickstart.UserConfig(self._instroot).apply(ksh.user)
1360 kickstart.ServicesConfig(self._instroot).apply(ksh.services)
1361 kickstart.XConfig(self._instroot).apply(ksh.xconfig)
1362 kickstart.NetworkConfig(self._instroot).apply(ksh.network)
1363 kickstart.RPMMacroConfig(self._instroot).apply(self.ks)
1364 kickstart.DesktopConfig(self._instroot).apply(ksh.desktop)
1365 self.__save_repo_keys(repodata)
1366 kickstart.MoblinRepoConfig(self._instroot).apply(ksh.repo, repodata, self.repourl)
1368 msger.warning("Failed to apply configuration to image")
1371 self._create_bootconfig()
1372 self.__run_post_scripts()
1374 def launch_shell(self, launch):
1375 """Launch a shell in the install root.
1377 This method is launches a bash shell chroot()ed in the install root;
1378 this can be useful for debugging.
1382 msger.info("Launching shell. Exit to continue.")
1383 subprocess.call(["/bin/bash"], preexec_fn = self._chroot)
1385 def do_genchecksum(self, image_name):
1386 if not self._genchecksum:
1389 md5sum = misc.get_md5sum(image_name)
1390 with open(image_name + ".md5sum", "w") as f:
1391 f.write("%s %s" % (md5sum, os.path.basename(image_name)))
1392 self.outimage.append(image_name+".md5sum")
1394 def remove_exclude_image(self):
1395 for item in self._instloops[:]:
1396 if item['exclude_image']:
1397 msger.info("Removing %s in image." % item['name'])
1398 imgfile = os.path.join(self._imgdir, item['name'])
1401 except OSError as err:
1402 if err.errno == errno.ENOENT:
1404 self._instloops.remove(item)
1406 def create_cpio_image(self):
1407 for item in self._instloops:
1408 if item['cpioopts']:
1409 msger.info("Create image by cpio.")
1410 tmp_cpio = self.__builddir + "/tmp-cpio"
1411 if not os.path.exists(tmp_cpio):
1413 tmp_cpio_imgfile = os.path.join(tmp_cpio, item['name'])
1415 cpiocmd = fs.find_binary_path('cpio')
1417 oldoutdir = os.getcwd()
1418 os.chdir(os.path.join(self._instroot, item['mountpoint'].lstrip('/')))
1419 # find . | cpio --create --'format=newc' | gzip > ../ramdisk.img
1420 runner.show('find . | cpio --create %s | gzip > %s' % (item['cpioopts'], tmp_cpio_imgfile))
1422 except OSError, (errno, msg):
1423 raise errors.CreatorError("Create image by cpio error: %s" % msg)
1425 def copy_cpio_image(self):
1426 for item in self._instloops:
1427 if item['cpioopts']:
1428 tmp_cpio = self.__builddir + "/tmp-cpio"
1429 msger.info("Copy cpio image from %s to %s." %(tmp_cpio, self._imgdir))
1431 shutil.copyfile(os.path.join(tmp_cpio, item['name']),os.path.join(self._imgdir, item['name']))
1433 raise errors.CreatorError("Copy cpio image error")
1434 os.remove(os.path.join(tmp_cpio, item['name']))
1435 if not os.listdir(tmp_cpio):
1436 shutil.rmtree(tmp_cpio, ignore_errors=True)
1438 def package(self, destdir = "."):
1439 """Prepares the created image for final delivery.
1441 In its simplest form, this method merely copies the install root to the
1442 supplied destination directory; other subclasses may choose to package
1443 the image by e.g. creating a bootable ISO containing the image and
1444 bootloader configuration.
1446 destdir -- the directory into which the final image should be moved;
1447 this defaults to the current directory.
1450 self.remove_exclude_image()
1452 self._stage_final_image()
1454 if not os.path.exists(destdir):
1455 fs.makedirs(destdir)
1457 if self._recording_pkgs:
1458 self._save_recording_pkgs(destdir)
1460 # For image formats with two or multiple image files, it will be
1461 # better to put them under a directory
1462 if self.image_format in ("raw", "vmdk", "vdi", "nand", "mrstnand"):
1463 destdir = os.path.join(destdir, "%s-%s" \
1464 % (self.name, self.image_format))
1465 msger.debug("creating destination dir: %s" % destdir)
1466 fs.makedirs(destdir)
1468 # Ensure all data is flushed to _outdir
1469 runner.quiet('sync')
1471 misc.check_space_pre_cp(self._outdir, destdir)
1472 for f in os.listdir(self._outdir):
1473 shutil.move(os.path.join(self._outdir, f),
1474 os.path.join(destdir, f))
1475 self.outimage.append(os.path.join(destdir, f))
1476 self.do_genchecksum(os.path.join(destdir, f))
1478 def print_outimage_info(self):
1479 msg = "The new image can be found here:\n"
1480 self.outimage.sort()
1481 for file in self.outimage:
1482 msg += ' %s\n' % os.path.abspath(file)
1486 def check_depend_tools(self):
1487 for tool in self._dep_checks:
1488 fs.find_binary_path(tool)
1490 def package_output(self, image_format, destdir = ".", package="none"):
1491 if not package or package == "none":
1494 destdir = os.path.abspath(os.path.expanduser(destdir))
1495 (pkg, comp) = os.path.splitext(package)
1497 comp=comp.lstrip(".")
1501 dst = "%s/%s-%s.tar.%s" %\
1502 (destdir, self.name, image_format, comp)
1504 dst = "%s/%s-%s.tar" %\
1505 (destdir, self.name, image_format)
1507 msger.info("creating %s" % dst)
1508 tar = tarfile.open(dst, "w:" + comp)
1510 for file in self.outimage:
1511 msger.info("adding %s to %s" % (file, dst))
1513 arcname=os.path.join("%s-%s" \
1514 % (self.name, image_format),
1515 os.path.basename(file)))
1516 if os.path.isdir(file):
1517 shutil.rmtree(file, ignore_errors = True)
1523 '''All the file in outimage has been packaged into tar.* file'''
1524 self.outimage = [dst]
1526 def release_output(self, config, destdir, release):
1527 """ Create release directory and files
1531 """ release path """
1532 return os.path.join(destdir, fn)
1534 outimages = self.outimage
1537 new_kspath = _rpath(self.name+'.ks')
1538 with open(config) as fr:
1539 with open(new_kspath, "w") as wf:
1540 # When building a release we want to make sure the .ks
1541 # file generates the same build even when --release not used.
1542 wf.write(fr.read().replace("@BUILD_ID@", release))
1543 outimages.append(new_kspath)
1545 # save log file, logfile is only available in creator attrs
1546 if hasattr(self, 'releaselog') and self.releaselog:
1547 outimages.append(self.logfile)
1549 # rename iso and usbimg
1550 for f in os.listdir(destdir):
1551 if f.endswith(".iso"):
1552 newf = f[:-4] + '.img'
1553 elif f.endswith(".usbimg"):
1554 newf = f[:-7] + '.img'
1557 os.rename(_rpath(f), _rpath(newf))
1558 outimages.append(_rpath(newf))
1560 # generate MD5SUMS SHA1SUMS SHA256SUMS
1561 def generate_hashsum(hash_name, hash_method):
1562 with open(_rpath(hash_name), "w") as wf:
1563 for f in os.listdir(destdir):
1564 if f.endswith('SUMS'):
1567 if os.path.isdir(os.path.join(destdir, f)):
1570 hash_value = hash_method(_rpath(f))
1571 # There needs to be two spaces between the sum and
1572 # filepath to match the syntax with md5sum,sha1sum,
1573 # sha256sum. This way also *sum -c *SUMS can be used.
1574 wf.write("%s %s\n" % (hash_value, f))
1576 outimages.append("%s/%s" % (destdir, hash_name))
1579 'MD5SUMS' : misc.get_md5sum,
1580 'SHA1SUMS' : misc.get_sha1sum,
1581 'SHA256SUMS' : misc.get_sha256sum
1584 for k, v in hash_dict.items():
1585 generate_hashsum(k, v)
1587 # Filter out the nonexist file
1588 for fp in outimages[:]:
1589 if not os.path.exists("%s" % fp):
1590 outimages.remove(fp)
1592 def copy_kernel(self):
1593 """ Copy kernel files to the outimage directory.
1594 NOTE: This needs to be called before unmounting the instroot.
1597 if not self._need_copy_kernel:
1600 if not os.path.exists(self.destdir):
1601 os.makedirs(self.destdir)
1603 for kernel in glob.glob("%s/boot/vmlinuz-*" % self._instroot):
1604 kernelfilename = "%s/%s-%s" % (self.destdir,
1606 os.path.basename(kernel))
1607 msger.info('copy kernel file %s as %s' % (os.path.basename(kernel),
1609 shutil.copy(kernel, kernelfilename)
1610 self.outimage.append(kernelfilename)
1612 def copy_attachment(self):
1613 """ Subclass implement it to handle attachment files
1614 NOTE: This needs to be called before unmounting the instroot.
1618 def get_pkg_manager(self):
1619 return self.pkgmgr(target_arch = self.target_arch,
1620 instroot = self._instroot,
1621 cachedir = self.cachedir,
1622 strict_mode = self.strict_mode)
1624 def create_manifest(self):
1625 def get_pack_suffix():
1626 return '.' + self.pack_to.split('.', 1)[1]
1628 if not os.path.exists(self.destdir):
1629 os.makedirs(self.destdir)
1631 now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
1632 manifest_dict = {'version': VERSION,
1635 manifest_dict.update({'format': self.img_format})
1637 if hasattr(self, 'logfile') and self.logfile:
1638 manifest_dict.update({'log_file': self.logfile})
1640 if self.image_files:
1642 self.image_files.update({'pack': get_pack_suffix()})
1643 manifest_dict.update({self.img_format: self.image_files})
1645 msger.info('Creating manifest file...')
1646 manifest_file_path = os.path.join(self.destdir, 'manifest.json')
1647 with open(manifest_file_path, 'w') as fest_file:
1648 json.dump(manifest_dict, fest_file, indent=4)
1649 self.outimage.append(manifest_file_path)