program="virt-sysprep"
version="@PACKAGE_VERSION@"
+# Uncomment this to see every shell command that is executed.
+#set -x
+
TEMP=`getopt \
-o a:c:d:vVx \
- --long help,add:,connect:,domain:,enable:,format::,hostname:,list-operations,verbose,version \
+ --long help,add:,connect:,domain:,enable:,format::,hostname:,list-operations,selinux-relabel,no-selinux-relabel,verbose,version \
-n $program -- "$@"`
if [ $? != 0 ]; then
echo "$program: problem parsing the command line arguments"
fi
eval set -- "$TEMP"
-# This array accumulates the arguments we pass through to guestfish.
-declare -a guestfish
-guestfish[0]="guestfish"
-guestfish[1]="--rw"
-guestfish[2]="--listen"
-guestfish[3]="-i"
-i=4
+# This array accumulates the arguments we pass through to guestmount.
+declare -a params
+i=0
verbose=
add_params=0
enable=
hostname_param=localhost.localdomain
+selinux_relabel=auto
usage ()
{
while true; do
case "$1" in
-a|--add)
- guestfish[i++]="-a"
- guestfish[i++]="$2"
+ params[i++]="-a"
+ params[i++]="$2"
((add_params++))
shift 2;;
-c|--connect)
- guestfish[i++]="-c"
- guestfish[i++]="$2"
+ params[i++]="-c"
+ params[i++]="$2"
shift 2;;
-d|--domain)
- guestfish[i++]="-d"
- guestfish[i++]="$2"
+ params[i++]="-d"
+ params[i++]="$2"
((add_params++))
shift 2;;
--enable)
shift 2;;
--format)
if [ -n "$2" ]; then
- guestfish[i++]="--format=$2"
+ params[i++]="--format=$2"
else
- guestfish[i++]="--format"
+ params[i++]="--format"
fi
shift 2;;
--help)
--list-operations)
enable=list
shift;;
+ --selinux-relabel)
+ selinux_relabel=yes
+ shift;;
+ --no-selinux-relabel)
+ selinux_relabel=no
+ shift;;
-v|--verbose)
- guestfish[i++]="-v"
+ params[i++]="-v"
verbose=yes
shift;;
-V|--version)
echo "$program $version"
exit 0;;
-x)
- guestfish[i++]="-x"
+ # Can't pass the -x option directly to guestmount because
+ # that stops guestmount from forking, which means we can't
+ # coordinate with guestmount when it has finished
+ # initializing. So instead set just the underlying option
+ # in libguestfs by exporting LIBGUESTFS_TRACE.
+ # Unfortunately (a) this omits FUSE calls, but don't worry
+ # about that for now, and more importantly (b) trace
+ # messages disappear into never-never land after the fork.
+ export LIBGUESTFS_TRACE=1
shift;;
--)
shift
# enable all of these, although some of them are only done on certain
# guest types (see details below).
if [ -z "$enable" ]; then
+ dhcp_client_state=yes
+ dhcp_server_state=yes
hostname=yes
+ logfiles=yes
net_hwaddr=yes
+ random_seed=yes
+ smolt_uuid=yes
ssh_hostkeys=yes
udev_persistent_net=yes
+ yum_uuid=yes
elif [ "$enable" = "list" ]; then
+ echo "dhcp-client-state"
+ echo "dhcp-server-state"
echo "hostname"
+ echo "logfiles"
echo "net-hwaddr"
+ echo "random-seed"
+ echo "smolt-uuid"
echo "ssh-hostkeys"
echo "udev-persistent-net"
+ echo "yum-uuid"
exit 0
else
for opt in $(echo "$enable" | sed 's/,/ /g'); do
case "$opt" in
+ dhcp-client-state) dhcp_client_state=yes ;;
+ dhcp-server-state) dhcp_server_state=yes ;;
hostname) hostname=yes ;;
+ logfiles) logfiles=yes ;;
net-hwaddr) net_hwaddr=yes ;;
+ random-seed) random_seed=yes ;;
+ smolt-uuid) smolt_uuid=yes ;;
ssh-hostkeys) ssh_hostkeys=yes ;;
udev-persistent-net) udev_persistent_net=yes ;;
+ yum-uuid) yum_uuid=yes ;;
*)
echo "error: unknown --enable feature: $opt"
exit 1
set -e
if [ "$verbose" = "yes" ]; then
- echo command: "${guestfish[@]}"
+ echo params: "${params[@]}"
fi
# Create a temporary directory for general purpose use during operations.
tmpdir="$(mktemp -d)"
-# Increase the amount of memory allocated to the appliance because
-# we're using augeas. The user can override this by setting
-# $LIBGUESTFS_MEMSIZE before running the script.
-export LIBGUESTFS_MEMSIZE=${LIBGUESTFS_MEMSIZE:-2048}
-
-# Call guestfish.
-GUESTFISH_PID=
-eval $("${guestfish[@]}")
-if [ -z "$GUESTFISH_PID" ]; then
- echo "$program: guestfish didn't start up, see error messages above"
- exit 1
-fi
-
-# Helper.
-gf="guestfish --remote --"
-
cleanup ()
{
- $gf exit >/dev/null 2>&1 ||:
- rm -rf "$tmpdir" ||:
+ if [ -d $tmpdir/mnt ]; then
+ fusermount -u $tmpdir/mnt >/dev/null 2>&1 ||:
+ fi
+ rm -rf $tmpdir ||:
}
trap cleanup EXIT ERR
-# Launch back-end, inspect for operating systems, and get the guest
-# root disk.
-root=$($gf inspect-get-roots)
-
-if [ "$root" = "" ]; then
- echo "$program: no operating system was found on this disk"
- exit 1
-fi
-
-if [ "$verbose" = "yes" ]; then
- echo root: "$root"
-fi
-
-# Get the guest type.
-type="$($gf -inspect-get-type $root)"
+# Run virt-inspector and grab inspection information about this guest.
+virt-inspector "${params[@]}" > $tmpdir/xml
+xmlstarlet sel -t -c \
+ "string(/operatingsystems/operatingsystem[position()=1]/name)" \
+ $tmpdir/xml > $tmpdir/type
+xmlstarlet sel -t -c \
+ "string(/operatingsystems/operatingsystem[position()=1]/distro)" \
+ $tmpdir/xml > $tmpdir/distro ||:
+xmlstarlet sel -t -c \
+ "string(/operatingsystems/operatingsystem[position()=1]/package_format)" \
+ $tmpdir/xml > $tmpdir/package_format ||:
+xmlstarlet sel -t -c \
+ "string(/operatingsystems/operatingsystem[position()=1]/package_management)" \
+ $tmpdir/xml > $tmpdir/package_management ||:
+
+type="$(cat $tmpdir/type)"
+distro="$(cat $tmpdir/distro)"
+package_format="$(cat $tmpdir/package_format)"
+package_management="$(cat $tmpdir/package_management)"
+
+# Mount the disk.
+mkdir $tmpdir/mnt
+guestmount --rw -i "${params[@]}" $tmpdir/mnt
+
+mnt="$tmpdir/mnt"
-if [ "$type" = "linux" ]; then
- distro="$($gf -inspect-get-distro $root)"
-fi
+#----------------------------------------------------------------------
+# The sysprep operations.
-if [ "$type" = "windows" ]; then
- systemroot="$($gf -inspect-get-windows-systemroot $root)"
+if [ "$dhcp_client_state" = "yes" ]; then
+ case "$type" in
+ linux)
+ rm -rf $mnt/var/lib/dhclient/*
+ # RHEL 3:
+ rm -rf $mnt/var/lib/dhcp/*
+ ;;
+ esac
fi
-# Start Augeas if it's a Linux guest.
-if [ "$type" = "linux" ]; then
- $gf aug-init / 0
- using_augeas=yes
+if [ "$dhcp_server_state" = "yes" ]; then
+ case "$type" in
+ linux)
+ rm -rf $mnt/var/lib/dhcpd/*
+ ;;
+ esac
fi
-#----------------------------------------------------------------------
-# Useful functions.
-
-# erase_line filename regex
-#
-# Erase line(s) in a file that match the given regex.
-erase_line ()
-{
- $gf download "$1" "$tmpdir/file"
- sed "/$2/d" < "$tmpdir/file" > "$tmpdir/file.1"
- $gf upload "$tmpdir/file.1" "$1"
-}
-
-# prepend_line filename line
-#
-# Prepend a line to a file (this is better than appending, because it
-# works even when the original file isn't terminated with a newline).
-prepend_line ()
-{
- $gf download "$1" "$tmpdir/file"
- echo "$2" > "$tmpdir/file.1"
- cat "$tmpdir/file.1" "$tmpdir/file" >> "$tmpdir/file.2"
- $gf upload "$tmpdir/file.2" "$1"
-}
-
-# rm_files wildcard
-#
-# Remove files. Doesn't fail if no files exist. Note the wildcard
-# parameter cannot contain spaces or characters that need special
-# quoting.
-rm_files ()
-{
- files=$($gf glob-expand "$1")
- for f in $files; do
- $gf rm "$f"
- done
-}
-
-# rm_file filename
-#
-# Remove a single file. No error if the file doesn't exist or is not
-# a file.
-rm_file ()
-{
- t=$($gf is-file "$1")
- if [ "$t" = "true" ]; then
- $gf rm "$1"
- fi
-}
-
-#----------------------------------------------------------------------
-# The sysprep operations.
-
if [ "$hostname" = "yes" ]; then
case "$type/$distro" in
linux/fedora)
- $gf aug-set /files/etc/sysconfig/network/HOSTNAME "$hostname_param"
- augeas_save_needed=yes
+ echo "HOSTNAME=$hostname_param" > $mnt/etc/sysconfig/network.new
+ sed '/^HOSTNAME=/d' < $mnt/etc/sysconfig/network >> $mnt/etc/sysconfig/network.new
+ mv -f $mnt/etc/sysconfig/network.new $mnt/etc/sysconfig/network
+ created_files=yes
;;
linux/debian|linux/ubuntu)
- $gf write /etc/hostname "$hostname_param"
+ echo "$hostname_param" > $mnt/etc/hostname
+ created_files=yes
+ ;;
+ esac
+fi
+
+if [ "$logfiles" = "yes" ]; then
+ case "$type" in
+ linux)
+ rm -rf $mnt/var/log/*.log*
+ rm -rf $mnt/var/log/audit/*
+ rm -rf $mnt/var/log/btmp*
+ rm -rf $mnt/var/log/cron*
+ rm -rf $mnt/var/log/dmesg*
+ rm -rf $mnt/var/log/lastlog*
+ rm -rf $mnt/var/log/maillog*
+ rm -rf $mnt/var/log/mail/*
+ rm -rf $mnt/var/log/messages*
+ rm -rf $mnt/var/log/secure*
+ rm -rf $mnt/var/log/spooler*
+ rm -rf $mnt/var/log/tallylog*
+ rm -rf $mnt/var/log/wtmp*
;;
esac
fi
if [ "$net_hwaddr" = "yes" ]; then
case "$type/$distro" in
linux/fedora)
- # XXX these filenames can have spaces and untrusted chars in them!
- nodes=$( $gf aug-ls /files/etc/sysconfig/network-scripts |
- grep /files/etc/sysconfig/network-scripts/ifcfg- )
- for node in $nodes; do
- $gf -aug-rm "$node/HWADDR" >/dev/null
- augeas_save_needed=yes
- done
+ if [ -d $mnt/etc/sysconfig/network-scripts ]; then
+ rm_hwaddr ()
+ {
+ sed '/^HWADDR=/d' < "$1" > "$1.new"
+ mv -f "$1.new" "$1"
+ }
+ export -f rm_hwaddr
+ find $mnt/etc/sysconfig/network-scripts \
+ -name 'ifcfg-*' -type f \
+ -exec bash -c 'rm_hwaddr "$0"' {} \;
+ created_files=yes
+ fi
;;
esac
fi
+if [ "$random_seed" = "yes" -a "$type" = "linux" ]; then
+ f=
+ if [ -f $mnt/var/lib/random-seed ]; then
+ # Fedora
+ f=$mnt/var/lib/random-seed
+ elif [ -f $mnt/var/lib/urandom/random-seed ]; then
+ # Debian
+ f=$mnt/var/lib/urandom/random-seed
+ fi
+ if [ -n "$f" ]; then
+ dd if=/dev/random of="$f" bs=8 count=1 conv=nocreat,notrunc 2>/dev/null
+ fi
+fi
+
+if [ "$smolt_uuid" = "yes" -a "$type" = "linux" ]; then
+ rm -f $mnt/etc/sysconfig/hw-uuid
+ rm -f $mnt/etc/smolt/uuid
+ rm -f $mnt/etc/smolt/hw-uuid
+fi
+
if [ "$ssh_hostkeys" = "yes" -a "$type" != "windows" ]; then
- rm_files "/etc/ssh/*_host_*"
+ rm -rf $mnt/etc/ssh/*_host_*
fi
if [ "$udev_persistent_net" = "yes" -a "$type" = "linux" ]; then
- rm_file /etc/udev/rules.d/70-persistent-net.rules
+ rm -f $mnt/etc/udev/rules.d/70-persistent-net.rules
+fi
+
+if [ "$yum_uuid" = "yes" -a "$package_management" = "yum" ]; then
+ rm -f $mnt/var/lib/yum/uuid
fi
#----------------------------------------------------------------------
# Clean up and close down.
-if [ "$using_augeas" = "yes" -a "$augeas_save_needed" = "yes" ]; then
- $gf aug-save
- $gf aug-close
-fi
+# If we created any new files and the guest uses SELinux, then we have
+# to relabel the filesystem on boot. Could do with a better way to
+# test "guest uses SELinux" (XXX).
+case "$selinux_relabel/$created_files" in
+ yes/*)
+ touch $mnt/.autorelabel;;
+ auto/yes)
+ case "$type/$distro" in
+ linux/fedora|linux/rhel|linux/centos|linux/scientificlinux|linux/redhat-based)
+ touch $mnt/.autorelabel
+ ;;
+ esac
+ ;;
+esac
+
+sync
+
+fusermount -u $tmpdir/mnt
+rm -rf $tmpdir
-$gf umount-all
-$gf sync
-$gf exit
+trap - EXIT ERR
exit 0
the guest, you I<must copy or clone the disk first>.
See L</COPYING AND CLONING> below.
+You do I<not> need to run virt-sysprep as root. In fact we'd
+generally recommend that you don't. The time you might want to run it
+as root is when you need root in order to access the disk image, but
+even in this case it would be better to change the permissions on the
+disk image to be writable as the non-root user running virt-sysprep.
+
"Sysprep" stands for "system preparation" tool. The name comes from
the Microsoft program C<sysprep.exe> which is used to unconfigure
Windows machines in preparation for cloning them. Having said that,
List the operations supported by the virt-sysprep program.
+=item B<--selinux-relabel>
+
+=item B<--no-selinux-relabel>
+
+I<--selinux-relabel> forces SELinux relabelling next time the guest
+boots. I<--no-selinux-relabel> disables relabelling.
+
+The default is to try to detect if SELinux relabelling is required.
+See L</SELINUX RELABELLING> below for more details.
+
=item B<-v>
=item B<--verbose>
To list the operations supported by the current version of
virt-sysprep, use I<--list-operations>.
+=head2 dhcp-client-state
+
+Remove DHCP client leases.
+
+=head2 dhcp-server-state
+
+Remove DHCP server leases.
+
=head2 hostname
-This changes the hostname of the guest to the value given in the
+Changes the hostname of the guest to the value given in the
I<--hostname> parameter.
If the I<--hostname> parameter is not given, then the hostname is
changed to C<localhost.localdomain>.
+=head2 logfiles
+
+Remove many log files.
+
=head2 net-hwaddr
Remove HWADDR (hard-coded MAC address) configuration. For Fedora and
Red Hat Enterprise Linux, this is removed from C<ifcfg-*> files.
+=head2 random-seed
+
+Write some random bytes from the host into the random seed file of
+the guest.
+
+See C</RANDOM SEED> below.
+
+=head2 smolt-uuid
+
+Remove the Smolt hardware UUID.
+
=head2 ssh-hostkeys
-This erases the SSH host keys in the guest.
+Remove the SSH host keys in the guest.
The SSH host keys are regenerated (differently) next time the guest is
booted.
=head2 udev-persistent-net
-This erases udev persistent net rules which map the guest's existing
-MAC address to a fixed ethernet device (eg. eth0).
+Remove udev persistent net rules which map the guest's existing MAC
+address to a fixed ethernet device (eg. eth0).
After a guest is cloned, the MAC address usually changes. Since the
old MAC address occupies the old name (eg. eth0), this means the fresh
MAC address is assigned to a new name (eg. eth1) and this is usually
undesirable. Erasing the udev persistent net rules avoids this.
+=head2 yum-uuid
+
+Remove the yum UUID.
+
+yum creates a fresh UUID the next time it runs when it notices that
+the original UUID has been erased.
+
=head1 COPYING AND CLONING
Virt-sysprep can be used as part of a process of cloning guests, or to
run L<virt-resize(1)>. Virt-resize performs a copy and resize, and
thus is ideal for cloning guests from a template.
+=head1 SECURITY
+
+Although virt-sysprep removes some sensitive information from
+the guest, it does not pretend to remove all of it. You should
+examine the L</OPERATIONS> above, and the implementation of
+the operations in the shell script.
+
+You should also examine the guest afterwards.
+
+Sensitive files are simply removed. The data they contained may still
+exist on the disk, easily recovered with a hex editor or undelete
+tool. Use L<virt-sparsify(1)> as one way to remove this content. See
+also the L<scrub(1)> command to get rid of deleted content in
+directory entries and inodes.
+
+=head2 RANDOM SEED
+
+I<(This section applies to Linux guests only)>
+
+The virt-sysprep C<random-seed> operation writes a few bytes of
+randomness from the host into the guest's random seed file.
+
+If this is just done once and the guest is cloned from the same
+template, then each guest will start with the same entropy, and things
+like SSH host keys and TCP sequence numbers may be predictable.
+
+Therefore you should arrange to add more randomness I<after> cloning
+from a template too, which can be done by just enabling the
+C<random-seed> operation:
+
+ cp template.img newguest.img
+ virt-sysprep --enable=random-seed -a newguest.img
+
+=head2 SELINUX RELABELLING
+
+I<(This section applies to Linux guests using SELinux only)>
+
+If any new files are created by virt-sysprep, then virt-sysprep
+touches C</.autorelabel> so that these will be correctly labelled by
+SELinux the next time the guest is booted. This process interrupts
+boot and can take some time.
+
+You can force relabelling for all guests by supplying the
+I<--selinux-relabel> option.
+
+You can disable relabelling entirely by supplying the
+I<--no-selinux-relabel> option.
+
=head1 SHELL QUOTING
Libvirt guest names can contain arbitrary characters, some of which
L<virt-resize(1)>,
L<virt-sparsify(1)>,
L<virsh(1)>,
-L<qemu-img(1)>,
L<lvcreate(8)>,
+L<qemu-img(1)>,
+L<scrub(1)>,
L<http://libguestfs.org/>,
L<http://libvirt.org/>.