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
, docleanup
=True):
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]
128 self
.docleanup
= docleanup
131 def __get_image(self
):
132 if self
._LoopImageCreator
__imagedir
is None:
133 self
.__ensure
_builddir
()
134 self
._LoopImageCreator
__imagedir
= \
135 tempfile
.mkdtemp(dir = os
.path
.abspath(self
.tmpdir
),
136 prefix
= self
.name
+ "-")
137 rtn
= self
._LoopImageCreator
__imagedir
+ "/ext3fs.img"
139 _image
= property(__get_image
)
140 """The location of the filesystem image file."""
142 def _get_fslabel(self
):
143 dev_null
= os
.open("/dev/null", os
.O_WRONLY
)
145 out
= subprocess
.Popen(["/sbin/e2label", self
._image
],
146 stdout
= subprocess
.PIPE
,
147 stderr
= dev_null
).communicate()[0]
149 self
._LoopImageCreator
__fslabel
= out
.strip()
152 raise CreatorError("Failed to determine fsimage LABEL: %s" % e
)
156 def __ensure_builddir(self
):
157 if not self
._ImageCreator
__builddir
is None:
161 self
._ImageCreator
__builddir
= tempfile
.mkdtemp(dir = os
.path
.abspath(self
.tmpdir
),
162 prefix
= "edit-liveos-")
163 except OSError, (err
, msg
):
164 raise CreatorError("Failed create build directory in %s: %s" %
167 def _run_script(self
, script
):
169 (fd
, path
) = tempfile
.mkstemp(prefix
= "script-",
170 dir = self
._instroot
+ "/tmp")
172 logging
.debug("copying script to install root: %s" % path
)
173 shutil
.copy(os
.path
.abspath(script
), path
)
177 script
= "/tmp/" + os
.path
.basename(path
)
180 subprocess
.call([script
], preexec_fn
= self
._chroot
)
182 raise CreatorError("Failed to execute script %s, %s " % (script
, e
))
186 def mount(self
, base_on
, cachedir
= None):
187 """mount existing file system.
189 We have to override mount b/c we many not be creating an new install
190 root nor do we need to setup the file system, i.e., makedirs(/etc/,
191 /boot, ...), nor do we want to overwrite fstab, or create selinuxfs.
193 We also need to get some info about the image before we can mount it.
195 base_on -- the <LIVEIMG.src> a LiveOS.iso file or an attached LiveOS
196 device, such as, /dev/live for a currently running image.
198 cachedir -- a directory in which to store a Yum cache;
199 Not used in edit-liveos.
204 raise CreatorError("No base LiveOS image specified.")
206 self
.__ensure
_builddir
()
208 self
._ImageCreator
_instroot
= self
._ImageCreator
__builddir
+ "/install_root"
209 self
._LoopImageCreator
__imagedir
= self
._ImageCreator
__builddir
+ "/ex"
210 self
._ImageCreator
_outdir
= self
._ImageCreator
__builddir
+ "/out"
212 makedirs(self
._ImageCreator
_instroot
)
213 makedirs(self
._LoopImageCreator
__imagedir
)
214 makedirs(self
._ImageCreator
_outdir
)
217 # Need to clone base_on into ext3fs.img at this point
218 self
._LoopImageCreator
__fslabel
= self
.name
219 self
._base
_on
(base_on
)
221 LiveImageCreator
._base
_on
(self
, base_on
)
222 self
._LoopImageCreator
__fstype
= get_fsvalue(self
._image
, 'TYPE')
225 self
.fslabel
= self
._LoopImageCreator
__fslabel
226 if self
._LoopImageCreator
__image
_size
== None:
227 self
._LoopImageCreator
__image
_size
= os
.stat(self
._image
)[stat
.ST_SIZE
]
229 self
._LoopImageCreator
__instloop
= ExtDiskMount(
230 ExistingSparseLoopbackDisk(self
._image
,
231 self
._LoopImageCreator
__image
_size
),
232 self
._ImageCreator
_instroot
,
234 self
._LoopImageCreator
__blocksize
,
238 self
._LoopImageCreator
__instloop
.mount()
239 except MountError
, e
:
240 raise CreatorError("Failed to loopback mount '%s' : %s" %
243 cachesrc
= cachedir
or (self
._ImageCreator
__builddir
+ "/yum-cache")
246 for (f
, dest
) in [("/sys", None), ("/proc", None),
247 ("/dev/pts", None), ("/dev/shm", None),
248 (cachesrc
, "/var/cache/yum")]:
249 self
._ImageCreator
__bindmounts
.append(BindChrootMount(f
, self
._instroot
, dest
))
251 self
._do
_bindmounts
()
253 os
.symlink("../proc/mounts", self
._instroot
+ "/etc/mtab")
255 self
.__copy
_img
_root
(base_on
)
256 self
._brand
(self
._builder
)
258 def _base_on(self
, base_on
):
259 """Clone the running LiveOS image as the basis for the new image."""
261 self
.__fstype
= 'ext4'
262 self
.__image
_size
= 4096L * 1024 * 1024
263 self
.__blocksize
= 4096
265 self
.__instloop
= ExtDiskMount(SparseLoopbackDisk(self
._image
,
273 self
.__instloop
.mount()
274 except MountError
, e
:
275 raise CreatorError("Failed to loopback mount '%s' : %s" %
278 subprocess
.call(['rsync', '-ptgorlHASx', '--specials', '--progress',
280 '--exclude', '/etc/mtab',
281 '--exclude', '/etc/blkid/*',
282 '--exclude', '/dev/*',
283 '--exclude', '/proc/*',
284 '--exclude', '/home/*',
285 '--exclude', '/media/*',
286 '--exclude', '/mnt/live',
287 '--exclude', '/sys/*',
288 '--exclude', '/tmp/*',
289 '--exclude', '/.liveimg*',
290 '--exclude', '/.autofsck',
291 '/', self
._instroot
])
292 subprocess
.call(['sync'])
294 self
._ImageCreator
__create
_minimal
_dev
()
296 self
.__instloop
.cleanup()
299 def __copy_img_root(self
, base_on
):
300 """helper function to copy root content of the base LiveIMG to
303 ignore_list
= ['ext3fs.img', 'squashfs.img', 'osmin.img', 'home.img',
307 ignore_list
.remove('home.img')
308 includes
= 'boot, /EFI, /syslinux, /LiveOS'
310 includes
+= ", " + self
._include
312 imgmnt
= DiskMount(RawDisk(0, base_on
), self
._mkdtemp
())
314 imgmnt
= DiskMount(LoopbackDisk(base_on
, 0), self
._mkdtemp
())
316 self
._LiveImageCreatorBase
__isodir
= self
._ImageCreator
__builddir
+ "/iso"
320 except MountError
, e
:
321 raise CreatorError("Failed to mount '%s' : %s" % (base_on
, e
))
323 # include specified files or directories
325 baseimg
= os
.path
.join(imgmnt
.mountdir
, 'LiveOS',
327 # 'self.compress_type = None' will force reading it from
329 if self
.compress_type
is None:
330 self
.compress_type
= squashfs_compression_type(baseimg
)
331 if self
.compress_type
== 'undetermined':
332 # 'gzip' for compatibility with older versions.
333 self
.compress_type
= 'gzip'
335 dst
= self
._LiveImageCreatorBase
__isodir
337 for fd
in includes
.split(', /'):
338 src
= os
.path
.join(imgmnt
.mountdir
, fd
)
339 if os
.path
.isfile(src
):
340 shutil
.copy2(src
, os
.path
.join(dst
, fd
))
341 elif os
.path
.isdir(src
):
342 shutil
.copytree(src
, os
.path
.join(dst
, fd
),
344 ignore
=shutil
.ignore_patterns(
347 #copy over everything but squashfs.img or ext3fs.img
348 shutil
.copytree(imgmnt
.mountdir
,
349 self
._LiveImageCreatorBase
__isodir
,
350 ignore
=shutil
.ignore_patterns(*ignore_list
))
351 subprocess
.call(['sync'])
356 def _brand (self
, _builder
):
357 """Adjust the image branding to show its variation from original
358 source by builder and build date."""
360 self
.fslabel
= self
.name
361 dt
= time
.strftime('%d-%b-%Y')
363 lst
= ['isolinux/isolinux.cfg', 'syslinux/syslinux.cfg',
364 'syslinux/extlinux.conf']
366 fpath
= os
.path
.join(self
._LiveImageCreatorBase
__isodir
, f
)
367 if os
.path
.exists(fpath
):
370 # Get build name from boot configuration file.
372 cfgf
= open(fpath
, 'r')
374 raise CreatorError("Failed to open '%s' : %s" % (fpath
, e
))
378 i
= line
.find('Welcome to ')
380 release
= line
[i
+11:-2]
386 ntext
= dt
.translate(None, '-') + '-' + _builder
+ '-Remix-' + release
388 # Update fedora-release message with Remix details.
389 releasefiles
= '/etc/fedora-release, /etc/generic-release'
390 if self
._releasefile
:
391 releasefiles
+= ', ' + self
._releasefile
392 for fn
in releasefiles
.split(', '):
393 if os
.path
.exists(fn
):
395 with
open(self
._instroot
+ fn
, 'r') as f
:
396 text
= ntext
+ '\n' + f
.read()
397 open(f
.name
, 'w').write(text
)
399 raise CreatorError("Failed to open or write '%s' : %s" %
402 self
._releasefile
= ntext
403 self
.name
+= '-' + os
.uname()[4] + '-' + time
.strftime('%Y%m%d.%H%M')
406 def _configure_bootloader(self
, isodir
):
407 """Restore the boot configuration files for an iso image boot."""
409 bootfolder
= os
.path
.join(isodir
, 'isolinux')
410 oldpath
= os
.path
.join(isodir
, 'syslinux')
411 if os
.path
.exists(oldpath
):
412 os
.rename(oldpath
, bootfolder
)
414 cfgf
= os
.path
.join(bootfolder
, 'isolinux.cfg')
415 for f
in ['syslinux.cfg', 'extlinux.conf']:
416 src
= os
.path
.join(bootfolder
, f
)
417 if os
.path
.exists(src
):
420 args
= ['/bin/sed', '-i']
421 if self
._releasefile
:
423 args
.append('s/Welcome to .*/Welcome to ' + self
._releasefile
+ '!/')
426 args
.append('s/rootfstype=[^ ]* [^ ]*/rootfstype=auto ro/')
428 args
.append('s/\(r*d*.*live.*ima*ge*\) .* quiet/\1 quiet/')
430 args
.append('s/root=[^ ]*/root=live:CDLABEL=' + self
.name
+ '/')
432 # bootloader --append "!opt-to-remove opt-to-add"
433 for param
in kickstart
.get_kernel_args(self
.ks
,"").split():
434 if param
.startswith('!'):
436 # remove parameter prefixed with !
438 args
.append("/^ append/s/%s //" % param
)
439 # special case for last parameter
441 args
.append("/^ append/s/%s$//" % param
)
445 args
.append("/^ append/s/$/ %s/" % param
)
447 dev_null
= os
.open("/dev/null", os
.O_WRONLY
)
449 subprocess
.Popen(args
,
450 stdout
= subprocess
.PIPE
,
451 stderr
= dev_null
).communicate()[0]
455 raise CreatorError("Failed to configure bootloader file: %s" % e
)
460 def _run_pre_scripts(self
):
461 for s
in kickstart
.get_pre_scripts(self
.ks
):
462 (fd
, path
) = tempfile
.mkstemp(prefix
= "ks-script-",
463 dir = self
._instroot
+ "/tmp")
465 os
.write(fd
, s
.script
)
469 env
= self
._get
_post
_scripts
_env
(s
.inChroot
)
472 env
["INSTALL_ROOT"] = self
._instroot
476 preexec
= self
._chroot
477 script
= "/tmp/" + os
.path
.basename(path
)
480 subprocess
.check_call([s
.interp
, script
],
481 preexec_fn
= preexec
, env
= env
)
483 raise CreatorError("Failed to execute %%post script "
484 "with '%s' : %s" % (s
.interp
, e
.strerror
))
485 except subprocess
.CalledProcessError
, err
:
487 raise CreatorError("%%post script failed with code %d "
489 logging
.warning("ignoring %%post failure (code %d)"
494 class simpleCallback
:
498 def callback(self
, what
, amount
, total
, mydata
, wibble
):
499 if what
== rpm
.RPMCALLBACK_TRANS_START
:
502 elif what
== rpm
.RPMCALLBACK_INST_OPEN_FILE
:
504 print "Installing %s\r" % (hdr
["name"])
505 fd
= os
.open(path
, os
.O_RDONLY
)
506 nvr
= '%s-%s-%s' % ( hdr
['name'], hdr
['version'], hdr
['release'] )
510 elif what
== rpm
.RPMCALLBACK_INST_CLOSE_FILE
:
512 nvr
= '%s-%s-%s' % ( hdr
['name'], hdr
['version'], hdr
['release'] )
513 os
.close(self
.fdnos
[nvr
])
515 elif what
== rpm
.RPMCALLBACK_INST_PROGRESS
:
517 print "%s: %.5s%% done\r" % (hdr
["name"], (float(amount
) / total
) * 100),
519 def install_rpms(self
):
520 if kickstart
.exclude_docs(self
.ks
):
521 rpm
.addMacro("_excludedocs", "1")
522 if not kickstart
.selinux_enabled(self
.ks
):
523 rpm
.addMacro("__file_context_path", "%{nil}")
524 if kickstart
.inst_langs(self
.ks
) != None:
525 rpm
.addMacro("_install_langs", kickstart
.inst_langs(self
.ks
))
526 # start RPM transaction
527 ts
=rpm
.TransactionSet(self
._instroot
)
528 for repo
in kickstart
.get_repos(self
.ks
):
529 (name
, baseurl
, mirrorlist
, proxy
, inc
, exc
, cost
) = repo
530 if baseurl
.startswith("file://"):
532 elif not baseurl
.startswith("/"):
533 raise CreatorError("edit-livecd accepts only --baseurl pointing to a local folder with RPMs (not YUM repo)")
534 if not baseurl
.endswith("/"):
536 for pkg_from_list
in kickstart
.get_packages(self
.ks
):
537 # TODO report if package listed in ks is missing
538 for pkg
in glob
.glob(baseurl
+pkg_from_list
+"-[0-9]*.rpm"):
539 fdno
= os
.open(pkg
, os
.O_RDONLY
)
540 hdr
= ts
.hdrFromFdno(fdno
)
542 ts
.addInstall(hdr
,(hdr
,pkg
), "u")
543 ts
.run(self
.simpleCallback().callback
,'')
545 def parse_options(args
):
546 parser
= optparse
.OptionParser(usage
= """
549 [-k <kickstart-file>]
563 parser
.add_option("-n", "--name", type="string", dest
="name",
564 help="name of new LiveOS (don't include .iso, it will "
567 parser
.add_option("-o", "--output", type="string", dest
="output",
568 help="specify directory for new iso file.")
570 parser
.add_option("-k", "--kickstart", type="string", dest
="kscfg",
571 help="Path or url to kickstart config file")
573 parser
.add_option("-s", "--script", type="string", dest
="script",
574 help="specify script to run chrooted in the LiveOS "
577 parser
.add_option("-t", "--tmpdir", type="string",
578 dest
="tmpdir", default
="/var/tmp",
579 help="Temporary directory to use (default: /var/tmp)")
581 parser
.add_option("-e", "--exclude", type="string", dest
="exclude",
582 help="Specify directory or file patterns to be excluded "
583 "from the rsync copy of the filesystem.")
585 parser
.add_option("-f", "--exclude-file", type="string",
587 help="Specify a file containing file patterns to be "
588 "excluded from the rsync copy of the filesystem.")
590 parser
.add_option("-i", "--include", type="string", dest
="include",
591 help="Specify directory or file patterns to be included "
594 parser
.add_option("-r", "--releasefile", type="string", dest
="releasefile",
595 help="Specify release file/s for branding.")
597 parser
.add_option("-b", "--builder", type="string",
598 dest
="builder", default
=os
.getlogin(),
599 help="Specify the builder of a Remix.")
601 parser
.add_option("", "--clone", action
="store_true", dest
="clone",
602 help="Specify that source image is LiveOS block device.")
604 parser
.add_option("-c", "--compress_type", type="string",
605 dest
="compress_type",
606 help="Specify the compression type for SquashFS. Will "
607 "override the current compression or lack thereof.")
609 parser
.add_option("", "--skip-compression", action
="store_true",
610 dest
="skip_compression", default
=False,
611 help="Specify no compression of filesystem, ext3fs.img")
613 parser
.add_option("", "--skip-minimize", action
="store_true",
614 dest
="skip_minimize", default
=False,
615 help="Specify no osmin.img minimal snapshot.")
616 parser
.add_option("", "--nocleanup", action
="store_true",
617 dest
="nocleanup", default
=False,
618 help="Skip cleanup of temporary files")
620 setup_logging(parser
)
622 (options
, args
) = parser
.parse_args()
630 return (args
[0], options
)
632 def get_fsvalue(filesystem
, tag
):
633 dev_null
= os
.open('/dev/null', os
.O_WRONLY
)
634 args
= ['/sbin/blkid', '-s', tag
, '-o', 'value', filesystem
]
636 fs_type
= subprocess
.Popen(args
,
637 stdout
=subprocess
.PIPE
,
638 stderr
=dev_null
).communicate()[0]
640 raise CreatorError("Failed to determine fs %s: %s" % value
, e
)
644 return fs_type
.rstrip()
646 def rebuild_iso_symlinks(isodir
):
647 # remove duplicate files and rebuild symlinks to reduce iso size
648 efi_vmlinuz
= "%s/EFI/BOOT/vmlinuz0" % isodir
649 isolinux_vmlinuz
= "%s/isolinux/vmlinuz0" % isodir
650 efi_initrd
= "%s/EFI/BOOT/initrd0.img" % isodir
651 isolinux_initrd
= "%s/isolinux/initrd0.img" % isodir
653 if os
.path
.exists(efi_vmlinuz
):
654 os
.remove(efi_vmlinuz
)
655 os
.remove(efi_initrd
)
656 os
.symlink(isolinux_vmlinuz
,efi_vmlinuz
)
657 os
.symlink(isolinux_initrd
,efi_initrd
)
660 # LiveOS set to <LIVEIMG.src>
661 (LiveOS
, options
) = parse_options(sys
.argv
[1:])
663 if os
.geteuid () != 0:
664 print >> sys
.stderr
, "You must run edit-liveos as root"
669 elif stat
.S_ISBLK(os
.stat(LiveOS
).st_mode
):
670 name
= get_fsvalue(LiveOS
, 'LABEL') + '.edited'
672 name
= os
.path
.basename(LiveOS
) + ".edited"
675 output
= options
.output
677 output
= os
.path
.dirname(LiveOS
)
679 output
= options
.tmpdir
681 editor
= LiveImageEditor(name
, docleanup
=not options
.nocleanup
)
682 editor
._exclude
= options
.exclude
683 editor
._exclude
_file
= options
.exclude_file
684 editor
._include
= options
.include
685 editor
.clone
= options
.clone
686 editor
.tmpdir
= options
.tmpdir
687 editor
._builder
= options
.builder
688 editor
._releasefile
= options
.releasefile
689 editor
.compress_type
= options
.compress_type
690 editor
.skip_compression
= options
.skip_compression
691 editor
.skip_minimize
= options
.skip_minimize
695 editor
.ks
= read_kickstart(options
.kscfg
)
696 # part / --size <new rootfs size to be resized to>
697 editor
._LoopImageCreator
__image
_size
= kickstart
.get_image_size(editor
.ks
)
699 if not os
.path
.exists(options
.script
):
700 print "Invalid Script Path '%s'" % options
.script
702 editor
.mount(LiveOS
, cachedir
= None)
703 editor
._configure
_bootloader
(editor
._LiveImageCreatorBase
__isodir
)
705 editor
._run
_pre
_scripts
()
706 editor
.install_rpms()
707 editor
._run
_post
_scripts
()
709 print "Running edit script '%s'" % options
.script
710 editor
._run
_script
(options
.script
)
712 print "Launching shell. Exit to continue."
713 print "----------------------------------"
714 editor
.launch_shell()
715 rebuild_iso_symlinks(editor
._LiveImageCreatorBase
__isodir
)
717 editor
.package(output
)
718 logging
.info("%s.iso saved to %s" % (editor
.name
, output
))
719 except CreatorError
, e
:
720 logging
.error(u
"Error editing LiveOS : %s" % e
)
727 if __name__
== "__main__":
730 arch
= rpmUtils
.arch
.getBaseArch()
731 if arch
in ("i386", "x86_64"):
732 LiveImageCreator
= x86LiveImageCreator
733 elif arch
in ("ppc",):
734 LiveImageCreator
= ppcLiveImageCreator
735 elif arch
in ("ppc64",):
736 LiveImageCreator
= ppc64LiveImageCreator
738 raise CreatorError("Architecture not supported!")