diff --git a/docs/fedora-minimized.ks b/docs/fedora-minimized.ks new file mode 100644 index 00000000..ac970179 --- /dev/null +++ b/docs/fedora-minimized.ks @@ -0,0 +1,104 @@ +# Minimal Disk Image -- Example of image-minimizer usage in %post +# +sshpw --username=root --plaintext randOmStrinGhERE +# Firewall configuration +firewall --enabled +# Use network installation +url --url="http://dl.fedoraproject.org/pub/fedora/linux/development/20/x86_64/os/" + +# Root password +rootpw --plaintext removethispw +# Network information +network --bootproto=dhcp --onboot=on --activate +# System authorization information +auth --useshadow --enablemd5 +# System keyboard +keyboard --xlayouts=us --vckeymap=us +# System language +lang en_US.UTF-8 +# SELinux configuration +selinux --enforcing +# Installation logging level +logging --level=info +# Shutdown after installation +shutdown +# System timezone +timezone US/Eastern +# System bootloader configuration +bootloader --location=mbr +# Clear the Master Boot Record +zerombr +# Partition clearing information +clearpart --all +# Disk partitioning information +part / --fstype="ext4" --size=4000 +part swap --size=1000 + +%post +# Remove root password +passwd -d root > /dev/null +%end + +%packages +@core +kernel +memtest86+ +grub2-efi +grub2 +shim +syslinux +-dracut-config-rescue +%end + +# +# Use the image-minimizer to remove some packages and dirs +# +%post --interpreter=image-minimizer --nochroot + + +# Kernel modules minimization +# Drop many filesystems +drop /lib/modules/*/kernel/fs +keep /lib/modules/*/kernel/fs/ext* +keep /lib/modules/*/kernel/fs/mbcache* +keep /lib/modules/*/kernel/fs/squashfs +keep /lib/modules/*/kernel/fs/jbd* +keep /lib/modules/*/kernel/fs/btrfs +keep /lib/modules/*/kernel/fs/cifs* +keep /lib/modules/*/kernel/fs/fat +keep /lib/modules/*/kernel/fs/nfs +keep /lib/modules/*/kernel/fs/nfs_common +keep /lib/modules/*/kernel/fs/fscache +keep /lib/modules/*/kernel/fs/lockd +keep /lib/modules/*/kernel/fs/nls/nls_utf8.ko +keep /lib/modules/*/kernel/fs/configfs/configfs.ko +keep /lib/modules/*/kernel/fs/fuse +keep /lib/modules/*/kernel/fs/isofs +# No sound +drop /lib/modules/*/kernel/sound + + +# Drop some unused rpms, without dropping dependencies +droprpm checkpolicy +droprpm dmraid-events +droprpm gamin +droprpm gnupg2 +droprpm linux-atm-libs +droprpm make +droprpm mtools +droprpm mysql-libs +droprpm perl +droprpm perl-Module-Pluggable +droprpm perl-Net-Telnet +droprpm perl-PathTools +droprpm perl-Pod-Escapes +droprpm perl-Pod-Simple +droprpm perl-Scalar-List-Utils +droprpm perl-hivex +droprpm perl-macros +droprpm sgpio +droprpm syslinux +droprpm system-config-firewall-base +droprpm usermode + +%end diff --git a/lorax.spec b/lorax.spec index 1adfd637..7b7e6657 100644 --- a/lorax.spec +++ b/lorax.spec @@ -60,6 +60,9 @@ Requires: grub2-tools Requires: openssh %endif +# Moved image-minimizer tool to lorax +Provides: appliance-tools-minimizer + %description Lorax is a tool for creating the anaconda install images. @@ -85,6 +88,7 @@ make DESTDIR=$RPM_BUILD_ROOT mandir=%{_mandir} install %{_sbindir}/lorax %{_sbindir}/mkefiboot %{_sbindir}/livemedia-creator +%{_bindir}/image-minimizer %dir %{_sysconfdir}/lorax %config(noreplace) %{_sysconfdir}/lorax/lorax.conf %dir %{_datadir}/lorax diff --git a/setup.py b/setup.py index 4d5b543c..5d38b5b4 100644 --- a/setup.py +++ b/setup.py @@ -18,6 +18,7 @@ for root, dnames, fnames in os.walk("share"): # executable data_files.append(("/usr/sbin", ["src/sbin/lorax", "src/sbin/mkefiboot", "src/sbin/livemedia-creator"])) +data_files.append(("/usr/bin", ["src/bin/image-minimizer"])) # get the version sys.path.insert(0, "src") diff --git a/src/bin/image-minimizer b/src/bin/image-minimizer new file mode 100755 index 00000000..e6135564 --- /dev/null +++ b/src/bin/image-minimizer @@ -0,0 +1,198 @@ +#!/usr/bin/python +# +# image-minimizer: removes files and packages on the filesystem +# +# Copyright 2007-2010 Red Hat Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Library General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +import glob +import optparse +import os +import shutil +import sys +import rpm + +class ImageMinimizer: + filename = '' + dryrun = False + verbose = False + prefix = None + drops = set() + visited = set() + drops_rpm = set() + ts = None + + def __init__(self, filename, root, dryrun, verbose): + self.filename = filename + self.prefix = root + self.dryrun = dryrun + self.verbose = verbose + self.ts = None + + # Recursively adds all files and directories. + # This is done becuase globbing does not allow + # ** for arbitrary nesting. + def add_directory(self, files, dirname): + self.visited.add(dirname) + for root, dirs, items in os.walk(dirname): + for dir in dirs: + self.visited.add(os.path.join(root, dir)) + for name in items: + files.add(os.path.join(root, name)) + + def add_pattern(self, files, pattern): + globs = glob.glob(pattern) + if self.verbose and len(globs) == 0: + print "%s file not found" % pattern + for g in globs: + if os.path.isdir(g): + self.add_directory(files, g) + else: + files.add(g) + + def add_pattern_rpm(self, rpms, pattern): + if self.ts is None: + if self.prefix is None: + raise Exception ('Must specify installation root for droprpm/keeprpm') + self.ts = rpm.TransactionSet(self.prefix) + mi = self.ts.dbMatch() + mi.pattern('name', rpm.RPMMIRE_GLOB, pattern) + not_found = True + for hdr in mi: + not_found = False + rpms.add(hdr['name']) + if self.verbose and not_found: + print "%s package not found" % pattern + + # Parses each line in the ifle + def parse_line(self, line): + command = "" + pattern = "" + tok = line.split(None,1) + if len(tok) > 0: + command = tok[0].lower() + if len(tok) > 1: + pattern = tok[1].strip() + + # Strip out all the comments and blank lines + if not (command.startswith('#') or command==''): + if command == 'keep': + if self.prefix is not None : + pattern = pattern.lstrip('/') + pattern = os.path.join(self.prefix, pattern) + keeps = set() + self.add_pattern(keeps, pattern) + self.drops.difference_update(keeps) + keeps = None + elif command == 'drop': + if self.prefix is not None : + pattern = pattern.lstrip('/') + pattern = os.path.join(self.prefix, pattern) + self.add_pattern(self.drops, pattern) + elif command == 'keeprpm': + keeps_rpm = set() + self.add_pattern_rpm(keeps_rpm, pattern) + self.drops_rpm.difference_update(keeps_rpm) + keeps_rpm = None + elif command == 'droprpm': + self.add_pattern_rpm(self.drops_rpm, pattern) + else: + raise Exception ('Unknown Command: ' + command) + + def remove(self): + for tag in sorted(self.drops, reverse=True): + self.visited.add(os.path.split(tag)[0]) + if os.path.isdir(tag): + self.visited.add(tag) + else: + if self.dryrun: + print 'rm ' + tag + else: + if self.verbose: + print 'rm ' + tag + os.remove(tag) + + #remove all empty directory. Every 8k counts! + for dir in sorted(self.visited, reverse=True): + if len(os.listdir(dir)) == 0: + if self.dryrun: + print 'rm -rf ' + dir + else: + if self.verbose: + print 'rm -rf ' + dir + os.rmdir(dir) + + def remove_rpm(self): + + def runCallback(reason, amount, total, key, client_data): + if self.verbose and reason == rpm.RPMCALLBACK_UNINST_STOP: + print key, "erased" + + if len(self.drops_rpm) == 0: + return + + for pkg in self.drops_rpm: + if self.dryrun: + print "erasing ", pkg + else: + self.ts.addErase(pkg) + if not self.dryrun: + # skip ts.check(), equivalent to --nodeps + self.ts.run(runCallback, "erase") + + def filter(self): + for line in (open(self.filename).readlines()): + self.parse_line(line.strip()) + self.remove() + self.remove_rpm() + + +def parse_options(): + usage = "usage: %prog [options] filename" + parser = optparse.OptionParser(usage=usage) + + parser.set_defaults(root=os.environ.get('INSTALL_ROOT', '/mnt/sysimage/'), dry_run=False) + + parser.add_option("-i", "--installroot", type="string", dest="root", + help="Root path to prepend to all file patterns and installation root for RPM " + "operations. Defaults to INSTALL_ROOT or /mnt/sysimage/") + + parser.add_option("--dryrun", action="store_true", dest="dryrun", + help="If set, no filesystem changes are made.") + + parser.add_option("-v", "--verbose", action="store_true", dest="verbose", + help="Display every action as it is performed.") + + (options, args) = parser.parse_args() + if len(args) == 0: + parser.print_help() + sys.exit(1) + + return (options, args) + + +if __name__ == "__main__": + try: + (options, args) = parse_options() + filename = args[0] + minimizer = ImageMinimizer(filename, options.root, options.dryrun, + options.verbose) + minimizer.filter() + except SystemExit, e: + sys.exit(e.code) + except KeyboardInterrupt, e: + print >> sys.stderr, _("Aborted at user request") + except Exception, e: + print e + sys.exit(1)