3 # edit-liveos: Edit a LiveOS to insert files or to clone an instance onto a new
6 # Copyright 2009, Red Hat Inc.
7 # Written by Perry Myers <pmyers at redhat.com> & David Huff <dhuff at redhat.com>
8 # Cloning code added by Frederick Grose <fgrose at sugarlabs.org>
11 # This program is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; version 2 of the License.
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU Library General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with this program; if not, write to the Free Software
22 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
35 from imgcreate
.debug
import *
36 from imgcreate
.errors
import *
37 from imgcreate
.fs
import *
38 from imgcreate
.live
import *
39 from imgcreate
.creator
import *
40 import imgcreate
.kickstart
as kickstart
41 from imgcreate
import read_kickstart
43 class ExistingSparseLoopbackDisk(SparseLoopbackDisk
):
44 """don't want to expand the disk"""
45 def __init__(self
, lofile
, size
):
46 SparseLoopbackDisk
.__init
__(self
, lofile
, size
)
49 #self.expand(create = True)
50 LoopbackDisk
.create(self
)
52 class LiveImageEditor(LiveImageCreator
):
53 """class for editing LiveOS images.
55 We need an instance of LiveImageCreator, however, we do not have a kickstart
56 file and we may not need to create a new image. We just want to reuse some
57 of LiveImageCreators methods on an existing LiveOS image.
61 def __init__(self
, name
):
62 """Initialize a LiveImageEditor instance.
64 creates a dummy instance of LiveImageCreator
65 We do not initialize any sub classes b/c we have no ks file.
70 self
.tmpdir
= "/var/tmp"
71 """The directory in which all temporary files will be created."""
74 """Signals when to copy a running LiveOS image as base."""
77 """A string of file or directory paths to include in __copy_img_root."""
79 self
._builder
= os
.getlogin()
80 """The name of the Remix builder for _branding.
81 Default = os.getlogin()"""
83 self
.compress_type
= None
84 """mksquashfs compressor to use. Use 'None' to force reading of the
85 existing image, or enter a -p --compress_type value to override the
86 current compression or lack thereof. Compression type options vary with
87 the version of the kernel and SquashFS used."""
89 self
.skip_compression
= False
90 """Controls whether to use squashfs to compress the image."""
92 self
.skip_minimize
= False
93 """Controls whether an image minimizing snapshot should be created."""
95 self
._isofstype
= "iso9660"
98 self
._ImageCreator
__builddir
= None
99 """working directory"""
101 self
._ImageCreator
_outdir
= None
102 """where final iso gets written"""
104 self
._ImageCreator
__bindmounts
= []
106 self
._LoopImageCreator
__blocksize
= 4096
107 self
._LoopImageCreator
__fslabel
= None
108 self
._LoopImageCreator
__instloop
= None
109 self
._LoopImageCreator
__fstype
= None
110 self
._LoopImageCreator
__image
_size
= None
112 self
.__instroot
= None
114 self
._LiveImageCreatorBase
__isodir
= None
115 """directory where the iso is staged"""
118 """optional kickstart file as a recipe for editing the image"""
120 self
._ImageCreator
__selinux
_mountpoint
= "/sys/fs/selinux"
121 with
open("/proc/self/mountinfo", "r") as f
:
122 for line
in f
.readlines():
123 fields
= line
.split()
124 if fields
[-2] == "selinuxfs":
125 self
.__ImageCreator
__selinux
_mountpoint
= fields
[4]
129 def __get_image(self
):
130 if self
._LoopImageCreator
__imagedir
is None:
131 self
.__ensure
_builddir
()
132 self
._LoopImageCreator
__imagedir
= \
133 tempfile
.mkdtemp(dir = os
.path
.abspath(self
.tmpdir
),
134 prefix
= self
.name
+ "-")
135 rtn
= self
._LoopImageCreator
__imagedir
+ "/ext3fs.img"
137 _image
= property(__get_image
)
138 """The location of the filesystem image file."""
140 def _get_fslabel(self
):
141 dev_null
= os
.open("/dev/null", os
.O_WRONLY
)
143 out
= subprocess
.Popen(["/sbin/e2label", self
._image
],
144 stdout
= subprocess
.PIPE
,
145 stderr
= dev_null
).communicate()[0]
147 self
._LoopImageCreator
__fslabel
= out
.strip()
150 raise CreatorError("Failed to determine fsimage LABEL: %s" % e
)
154 def __ensure_builddir(self
):
155 if not self
._ImageCreator
__builddir
is None:
159 self
._ImageCreator
__builddir
= tempfile
.mkdtemp(dir = os
.path
.abspath(self
.tmpdir
),
160 prefix
= "edit-liveos-")
161 except OSError, (err
, msg
):
162 raise CreatorError("Failed create build directory in %s: %s" %
165 def _run_script(self
, script
):
167 (fd
, path
) = tempfile
.mkstemp(prefix
= "script-",
168 dir = self
._instroot
+ "/tmp")
170 logging
.debug("copying script to install root: %s" % path
)
171 shutil
.copy(os
.path
.abspath(script
), path
)
175 script
= "/tmp/" + os
.path
.basename(path
)
178 subprocess
.call([script
], preexec_fn
= self
._chroot
)
180 raise CreatorError("Failed to execute script %s, %s " % (script
, e
))
184 def mount(self
, base_on
, cachedir
= None):
185 """mount existing file system.
187 We have to override mount b/c we many not be creating an new install
188 root nor do we need to setup the file system, i.e., makedirs(/etc/,
189 /boot, ...), nor do we want to overwrite fstab, or create selinuxfs.
191 We also need to get some info about the image before we can mount it.
193 base_on -- the <LIVEIMG.src> a LiveOS.iso file or an attached LiveOS
194 device, such as, /dev/live for a currently running image.
196 cachedir -- a directory in which to store a Yum cache;
197 Not used in edit-liveos.
202 raise CreatorError("No base LiveOS image specified.")
204 self
.__ensure
_builddir
()
206 self
._ImageCreator
_instroot
= self
._ImageCreator
__builddir
+ "/install_root"
207 self
._LoopImageCreator
__imagedir
= self
._ImageCreator
__builddir
+ "/ex"
208 self
._ImageCreator
_outdir
= self
._ImageCreator
__builddir
+ "/out"
210 makedirs(self
._ImageCreator
_instroot
)
211 makedirs(self
._LoopImageCreator
__imagedir
)
212 makedirs(self
._ImageCreator
_outdir
)
215 # Need to clone base_on into ext3fs.img at this point
216 self
._LoopImageCreator
__fslabel
= self
.name
217 self
._base
_on
(base_on
)
219 LiveImageCreator
._base
_on
(self
, base_on
)
220 self
._LoopImageCreator
__fstype
= get_fsvalue(self
._image
, 'TYPE')
223 self
.fslabel
= self
._LoopImageCreator
__fslabel
224 if self
._LoopImageCreator
__image
_size
== None:
225 self
._LoopImageCreator
__image
_size
= os
.stat(self
._image
)[stat
.ST_SIZE
]
227 self
._LoopImageCreator
__instloop
= ExtDiskMount(
228 ExistingSparseLoopbackDisk(self
._image
,
229 self
._LoopImageCreator
__image
_size
),
230 self
._ImageCreator
_instroot
,
232 self
._LoopImageCreator
__blocksize
,
236 self
._LoopImageCreator
__instloop
.mount()
237 except MountError
, e
:
238 raise CreatorError("Failed to loopback mount '%s' : %s" %
241 cachesrc
= cachedir
or (self
._ImageCreator
__builddir
+ "/yum-cache")
244 for (f
, dest
) in [("/sys", None), ("/proc", None),
245 ("/dev/pts", None), ("/dev/shm", None),
246 (cachesrc
, "/var/cache/yum")]:
247 self
._ImageCreator
__bindmounts
.append(BindChrootMount(f
, self
._instroot
, dest
))
249 self
._do
_bindmounts
()
251 os
.symlink("../proc/mounts", self
._instroot
+ "/etc/mtab")
253 self
.__copy
_img
_root
(base_on
)
254 self
._brand
(self
._builder
)
256 def _base_on(self
, base_on
):
257 """Clone the running LiveOS image as the basis for the new image."""
259 self
.__fstype
= 'ext4'
260 self
.__image
_size
= 4096L * 1024 * 1024
261 self
.__blocksize
= 4096
263 self
.__instloop
= ExtDiskMount(SparseLoopbackDisk(self
._image
,
271 self
.__instloop
.mount()
272 except MountError
, e
:
273 raise CreatorError("Failed to loopback mount '%s' : %s" %
276 subprocess
.call(['rsync', '-ptgorlHASx', '--specials', '--progress',
278 '--exclude', '/etc/mtab',
279 '--exclude', '/etc/blkid/*',
280 '--exclude', '/dev/*',
281 '--exclude', '/proc/*',
282 '--exclude', '/home/*',
283 '--exclude', '/media/*',
284 '--exclude', '/mnt/live',
285 '--exclude', '/sys/*',
286 '--exclude', '/tmp/*',
287 '--exclude', '/.liveimg*',
288 '--exclude', '/.autofsck',
289 '/', self
._instroot
])
290 subprocess
.call(['sync'])
292 self
._ImageCreator
__create
_minimal
_dev
()
294 self
.__instloop
.cleanup()
297 def __copy_img_root(self
, base_on
):
298 """helper function to copy root content of the base LiveIMG to
301 ignore_list
= ['ext3fs.img', 'squashfs.img', 'osmin.img', 'home.img',
305 ignore_list
.remove('home.img')
306 includes
= 'boot, /EFI, /syslinux, /LiveOS'
308 includes
+= ", " + self
._include
310 imgmnt
= DiskMount(RawDisk(0, base_on
), self
._mkdtemp
())
312 imgmnt
= DiskMount(LoopbackDisk(base_on
, 0), self
._mkdtemp
())
314 self
._LiveImageCreatorBase
__isodir
= self
._ImageCreator
__builddir
+ "/iso"
318 except MountError
, e
:
319 raise CreatorError("Failed to mount '%s' : %s" % (base_on
, e
))
321 # include specified files or directories
323 baseimg
= os
.path
.join(imgmnt
.mountdir
, 'LiveOS',
325 # 'self.compress_type = None' will force reading it from
327 if self
.compress_type
is None:
328 self
.compress_type
= squashfs_compression_type(baseimg
)
329 if self
.compress_type
== 'undetermined':
330 # 'gzip' for compatibility with older versions.
331 self
.compress_type
= 'gzip'
333 dst
= self
._LiveImageCreatorBase
__isodir
335 for fd
in includes
.split(', /'):
336 src
= os
.path
.join(imgmnt
.mountdir
, fd
)
337 if os
.path
.isfile(src
):
338 shutil
.copy2(src
, os
.path
.join(dst
, fd
))
339 elif os
.path
.isdir(src
):
340 shutil
.copytree(src
, os
.path
.join(dst
, fd
),
342 ignore
=shutil
.ignore_patterns(
345 #copy over everything but squashfs.img or ext3fs.img
346 shutil
.copytree(imgmnt
.mountdir
,
347 self
._LiveImageCreatorBase
__isodir
,
348 ignore
=shutil
.ignore_patterns(*ignore_list
))
349 subprocess
.call(['sync'])
354 def _brand (self
, _builder
):
355 """Adjust the image branding to show its variation from original
356 source by builder and build date."""
358 self
.fslabel
= self
.name
359 dt
= time
.strftime('%d-%b-%Y')
361 lst
= ['isolinux/isolinux.cfg', 'syslinux/syslinux.cfg',
362 'syslinux/extlinux.conf']
364 fpath
= os
.path
.join(self
._LiveImageCreatorBase
__isodir
, f
)
365 if os
.path
.exists(fpath
):
368 # Get build name from boot configuration file.
370 cfgf
= open(fpath
, 'r')
372 raise CreatorError("Failed to open '%s' : %s" % (fpath
, e
))
376 i
= line
.find('Welcome to ')
378 release
= line
[i
+11:-2]
384 ntext
= dt
.translate(None, '-') + '-' + _builder
+ '-Remix-' + release
386 # Update fedora-release message with Remix details.
387 releasefiles
= '/etc/fedora-release, /etc/generic-release'
388 if self
._releasefile
:
389 releasefiles
+= ', ' + self
._releasefile
390 for fn
in releasefiles
.split(', '):
391 if os
.path
.exists(fn
):
393 with
open(self
._instroot
+ fn
, 'r') as f
:
394 text
= ntext
+ '\n' + f
.read()
395 open(f
.name
, 'w').write(text
)
397 raise CreatorError("Failed to open or write '%s' : %s" %
400 self
._releasefile
= ntext
401 self
.name
+= '-' + os
.uname()[4] + '-' + time
.strftime('%Y%m%d.%H%M')
404 def _configure_bootloader(self
, isodir
):
405 """Restore the boot configuration files for an iso image boot."""
407 bootfolder
= os
.path
.join(isodir
, 'isolinux')
408 oldpath
= os
.path
.join(isodir
, 'syslinux')
409 if os
.path
.exists(oldpath
):
410 os
.rename(oldpath
, bootfolder
)
412 cfgf
= os
.path
.join(bootfolder
, 'isolinux.cfg')
413 for f
in ['syslinux.cfg', 'extlinux.conf']:
414 src
= os
.path
.join(bootfolder
, f
)
415 if os
.path
.exists(src
):
418 args
= ['/bin/sed', '-i']
419 if self
._releasefile
:
421 args
.append('s/Welcome to .*/Welcome to ' + self
._releasefile
+ '!/')
424 args
.append('s/rootfstype=[^ ]* [^ ]*/rootfstype=auto ro/')
426 args
.append('s/\(r*d*.*live.*ima*ge*\) .* quiet/\1 quiet/')
428 args
.append('s/root=[^ ]*/root=live:CDLABEL=' + self
.name
+ '/')
430 # bootloader --append "!opt-to-remove opt-to-add"
431 for param
in kickstart
.get_kernel_args(self
.ks
,"").split():
432 if param
.startswith('!'):
434 # remove parameter prefixed with !
436 args
.append("/^ append/s/%s //" % param
)
437 # special case for last parameter
439 args
.append("/^ append/s/%s$//" % param
)
443 args
.append("/^ append/s/$/ %s/" % param
)
445 dev_null
= os
.open("/dev/null", os
.O_WRONLY
)
447 subprocess
.Popen(args
,
448 stdout
= subprocess
.PIPE
,
449 stderr
= dev_null
).communicate()[0]
453 raise CreatorError("Failed to configure bootloader file: %s" % e
)
458 def _run_pre_scripts(self
):
459 for s
in kickstart
.get_pre_scripts(self
.ks
):
460 (fd
, path
) = tempfile
.mkstemp(prefix
= "ks-script-",
461 dir = self
._instroot
+ "/tmp")
463 os
.write(fd
, s
.script
)
467 env
= self
._get
_post
_scripts
_env
(s
.inChroot
)
470 env
["INSTALL_ROOT"] = self
._instroot
474 preexec
= self
._chroot
475 script
= "/tmp/" + os
.path
.basename(path
)
478 subprocess
.check_call([s
.interp
, script
],
479 preexec_fn
= preexec
, env
= env
)
481 raise CreatorError("Failed to execute %%post script "
482 "with '%s' : %s" % (s
.interp
, e
.strerror
))
483 except subprocess
.CalledProcessError
, err
:
485 raise CreatorError("%%post script failed with code %d "
487 logging
.warning("ignoring %%post failure (code %d)"
492 class simpleCallback
:
496 def callback(self
, what
, amount
, total
, mydata
, wibble
):
497 if what
== rpm
.RPMCALLBACK_TRANS_START
:
500 elif what
== rpm
.RPMCALLBACK_INST_OPEN_FILE
:
502 print "Installing %s\r" % (hdr
["name"])
503 fd
= os
.open(path
, os
.O_RDONLY
)
504 nvr
= '%s-%s-%s' % ( hdr
['name'], hdr
['version'], hdr
['release'] )
508 elif what
== rpm
.RPMCALLBACK_INST_CLOSE_FILE
:
510 nvr
= '%s-%s-%s' % ( hdr
['name'], hdr
['version'], hdr
['release'] )
511 os
.close(self
.fdnos
[nvr
])
513 elif what
== rpm
.RPMCALLBACK_INST_PROGRESS
:
515 print "%s: %.5s%% done\r" % (hdr
["name"], (float(amount
) / total
) * 100),
517 def install_rpms(self
):
518 if kickstart
.exclude_docs(self
.ks
):
519 rpm
.addMacro("_excludedocs", "1")
520 if not kickstart
.selinux_enabled(self
.ks
):
521 rpm
.addMacro("__file_context_path", "%{nil}")
522 if kickstart
.inst_langs(self
.ks
) != None:
523 rpm
.addMacro("_install_langs", kickstart
.inst_langs(self
.ks
))
524 # start RPM transaction
525 ts
=rpm
.TransactionSet(self
._instroot
)
526 for repo
in kickstart
.get_repos(self
.ks
):
527 (name
, baseurl
, mirrorlist
, proxy
, inc
, exc
, cost
) = repo
528 if baseurl
.startswith("file://"):
530 elif not baseurl
.startswith("/"):
531 raise CreatorError("edit-livecd accepts only --baseurl pointing to a local folder with RPMs (not YUM repo)")
532 if not baseurl
.endswith("/"):
534 for pkg_from_list
in kickstart
.get_packages(self
.ks
):
535 # TODO report if package listed in ks is missing
536 for pkg
in glob
.glob(baseurl
+pkg_from_list
+"-[0-9]*.rpm"):
537 fdno
= os
.open(pkg
, os
.O_RDONLY
)
538 hdr
= ts
.hdrFromFdno(fdno
)
540 ts
.addInstall(hdr
,(hdr
,pkg
), "u")
541 ts
.run(self
.simpleCallback().callback
,'')
543 def parse_options(args
):
544 parser
= optparse
.OptionParser(usage
= """
547 [-k <kickstart-file>]
561 parser
.add_option("-n", "--name", type="string", dest
="name",
562 help="name of new LiveOS (don't include .iso, it will "
565 parser
.add_option("-o", "--output", type="string", dest
="output",
566 help="specify directory for new iso file.")
568 parser
.add_option("-k", "--kickstart", type="string", dest
="kscfg",
569 help="Path or url to kickstart config file")
571 parser
.add_option("-s", "--script", type="string", dest
="script",
572 help="specify script to run chrooted in the LiveOS "
575 parser
.add_option("-t", "--tmpdir", type="string",
576 dest
="tmpdir", default
="/var/tmp",
577 help="Temporary directory to use (default: /var/tmp)")
579 parser
.add_option("-e", "--exclude", type="string", dest
="exclude",
580 help="Specify directory or file patterns to be excluded "
581 "from the rsync copy of the filesystem.")
583 parser
.add_option("-f", "--exclude-file", type="string",
585 help="Specify a file containing file patterns to be "
586 "excluded from the rsync copy of the filesystem.")
588 parser
.add_option("-i", "--include", type="string", dest
="include",
589 help="Specify directory or file patterns to be included "
592 parser
.add_option("-r", "--releasefile", type="string", dest
="releasefile",
593 help="Specify release file/s for branding.")
595 parser
.add_option("-b", "--builder", type="string",
596 dest
="builder", default
=os
.getlogin(),
597 help="Specify the builder of a Remix.")
599 parser
.add_option("", "--clone", action
="store_true", dest
="clone",
600 help="Specify that source image is LiveOS block device.")
602 parser
.add_option("-c", "--compress_type", type="string",
603 dest
="compress_type",
604 help="Specify the compression type for SquashFS. Will "
605 "override the current compression or lack thereof.")
607 parser
.add_option("", "--skip-compression", action
="store_true",
608 dest
="skip_compression", default
=False,
609 help="Specify no compression of filesystem, ext3fs.img")
611 parser
.add_option("", "--skip-minimize", action
="store_true",
612 dest
="skip_minimize", default
=False,
613 help="Specify no osmin.img minimal snapshot.")
615 setup_logging(parser
)
617 (options
, args
) = parser
.parse_args()
625 return (args
[0], options
)
627 def get_fsvalue(filesystem
, tag
):
628 dev_null
= os
.open('/dev/null', os
.O_WRONLY
)
629 args
= ['/sbin/blkid', '-s', tag
, '-o', 'value', filesystem
]
631 fs_type
= subprocess
.Popen(args
,
632 stdout
=subprocess
.PIPE
,
633 stderr
=dev_null
).communicate()[0]
635 raise CreatorError("Failed to determine fs %s: %s" % value
, e
)
639 return fs_type
.rstrip()
641 def rebuild_iso_symlinks(isodir
):
642 # remove duplicate files and rebuild symlinks to reduce iso size
643 efi_vmlinuz
= "%s/EFI/BOOT/vmlinuz0" % isodir
644 isolinux_vmlinuz
= "%s/isolinux/vmlinuz0" % isodir
645 efi_initrd
= "%s/EFI/BOOT/initrd0.img" % isodir
646 isolinux_initrd
= "%s/isolinux/initrd0.img" % isodir
648 if os
.path
.exists(efi_vmlinuz
):
649 os
.remove(efi_vmlinuz
)
650 os
.remove(efi_initrd
)
651 os
.symlink(isolinux_vmlinuz
,efi_vmlinuz
)
652 os
.symlink(isolinux_initrd
,efi_initrd
)
655 # LiveOS set to <LIVEIMG.src>
656 (LiveOS
, options
) = parse_options(sys
.argv
[1:])
658 if os
.geteuid () != 0:
659 print >> sys
.stderr
, "You must run edit-liveos as root"
664 elif stat
.S_ISBLK(os
.stat(LiveOS
).st_mode
):
665 name
= get_fsvalue(LiveOS
, 'LABEL') + '.edited'
667 name
= os
.path
.basename(LiveOS
) + ".edited"
670 output
= options
.output
672 output
= os
.path
.dirname(LiveOS
)
674 output
= options
.tmpdir
676 editor
= LiveImageEditor(name
)
677 editor
._exclude
= options
.exclude
678 editor
._exclude
_file
= options
.exclude_file
679 editor
._include
= options
.include
680 editor
.clone
= options
.clone
681 editor
.tmpdir
= options
.tmpdir
682 editor
._builder
= options
.builder
683 editor
._releasefile
= options
.releasefile
684 editor
.compress_type
= options
.compress_type
685 editor
.skip_compression
= options
.skip_compression
686 editor
.skip_minimize
= options
.skip_minimize
690 editor
.ks
= read_kickstart(options
.kscfg
)
691 # part / --size <new rootfs size to be resized to>
692 editor
._LoopImageCreator
__image
_size
= kickstart
.get_image_size(editor
.ks
)
694 if not os
.path
.exists(options
.script
):
695 print "Invalid Script Path '%s'" % options
.script
697 editor
.mount(LiveOS
, cachedir
= None)
698 editor
._configure
_bootloader
(editor
._LiveImageCreatorBase
__isodir
)
700 editor
._run
_pre
_scripts
()
701 editor
.install_rpms()
702 editor
._run
_post
_scripts
()
704 print "Running edit script '%s'" % options
.script
705 editor
._run
_script
(options
.script
)
707 print "Launching shell. Exit to continue."
708 print "----------------------------------"
709 editor
.launch_shell()
710 rebuild_iso_symlinks(editor
._LiveImageCreatorBase
__isodir
)
712 editor
.package(output
)
713 logging
.info("%s.iso saved to %s" % (editor
.name
, output
))
714 except CreatorError
, e
:
715 logging
.error(u
"Error editing LiveOS : %s" % e
)
722 if __name__
== "__main__":
725 arch
= rpmUtils
.arch
.getBaseArch()
726 if arch
in ("i386", "x86_64"):
727 LiveImageCreator
= x86LiveImageCreator
728 elif arch
in ("ppc",):
729 LiveImageCreator
= ppcLiveImageCreator
730 elif arch
in ("ppc64",):
731 LiveImageCreator
= ppc64LiveImageCreator
733 raise CreatorError("Architecture not supported!")