Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
openSUSE:13.1:Update
virt-v2v
add_suse_converter.patch
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File add_suse_converter.patch of Package virt-v2v
This is a new converter specific to converting SUSE based distributions. (fate#315181) This should be pushed upstream after fully validated (and RHEL-specifics removed). --- MANIFEST | 1 lib/Sys/VirtConvert/Converter/SUSE.pm | 2476 ++++++++++++++++++++++++++++++++++ 2 files changed, 2477 insertions(+) diff -Nurp a/lib/Sys/VirtConvert/Converter/SUSE.pm b/lib/Sys/VirtConvert/Converter/SUSE.pm --- a/lib/Sys/VirtConvert/Converter/SUSE.pm 1969-12-31 17:00:00.000000000 -0700 +++ b/lib/Sys/VirtConvert/Converter/SUSE.pm 2013-09-14 12:53:52.113423914 -0600 @@ -0,0 +1,2476 @@ +# Sys::VirtConvert::Converter::SUSE +# Copyright (C) 2009-2012 SUSE Inc. +# +# Based on Sys::VirtConvert::Converter::RedHat +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +use strict; +use warnings; + + +# Functions common between grub legacy and grub2. All bootloader +# interactions should eventually go through Bootloader::Tools. +package Sys::VirtConvert::Converter::SUSE::Grub; + +use Sys::VirtConvert::Util; +use Locale::TextDomain 'virt-v2v'; + +sub get_initrd +{ + my $self = shift; + my ($path) = @_; + + my $g = $self->{g}; + + # The default name of the initrd is the same as the kernel name, with + # initrd replacing vmlinuz. This can be confirmed with grubby, or + # Bootloader::Tools->GetSection, but for now just do the replacement + # if the file exists. + my $initrd; + ($initrd = $path) =~ s/vmlinuz/initrd/; + if ($g->exists($initrd)) { + return $initrd; + } + + v2vdie __x('Didn\'t find initrd for kernel {path}', path => $path); +} + +sub get_default_image +{ + my $self = shift; + my ($path) = @_; + + my $g = $self->{g}; + + my $default = $g->command(['/usr/bin/perl', + '-MBootloader::Tools', + '-e', 'InitLibrary(); '. + 'my $default=Bootloader::Tools::GetDefaultSection(); '. + 'print $default->{image};']); + $default =~ s/^\(hd.*\)//; + + return $default; +} + +sub set_default_image +{ + my $self = shift; + my ($path) = @_; + + my $g = $self->{g}; + + # Using the image path to set a default image is not always reliable. + # To be safe, get the image name, then set that as the default image. + $g->command(['/usr/bin/perl', + '-MBootloader::Tools', + '-e', 'InitLibrary(); '. + 'my @sections = '. + 'GetSectionList(type=>image, image=>"'.$path.'"); '. + 'my $section = GetSection(@sections); '. + 'my $newdefault = $section->{name}; '. + 'SetGlobals(default, "$newdefault");']); +} + +sub check_efi +{ + my $self = shift; + my $g = $self->{g}; + + # Check the first partition of each device looking for an EFI boot + # partition. We can't be sure which device is the boot device, so we just + # check them all. + foreach my $device ($g->list_devices()) { + my $guid = eval { $g->part_get_gpt_type($device, 1) }; + next unless defined($guid); + + if ($guid eq 'C12A7328-F81F-11D2-BA4B-00A0C93EC93B') { + $self->convert_efi($device); + last; + } + } +} + + +# Methods for inspecting and manipulating grub legacy +package Sys::VirtConvert::Converter::SUSE::GrubLegacy; + +use Sys::VirtConvert::Util; + +use File::Basename; +use Locale::TextDomain 'virt-v2v'; + +@Sys::VirtConvert::Converter::SUSE::GrubLegacy::ISA = + qw(Sys::VirtConvert::Converter::SUSE::Grub); + +sub new +{ + my $class = shift; + my ($g, $root) = @_; + + my $self = {}; + bless($self, $class); + + $self->{g} = $g; + $self->{root} = $root; + + # Look for an EFI configuration + foreach my $cfg ($g->glob_expand('/boot/*efi/*/grub.conf')) { + $self->check_efi(); + } + + # Look for the grub configuration file + foreach my $path ('/boot/grub/menu.lst', '/boot/grub/grub.conf') + { + if ($g->exists($path)) { + $self->{grub_conf} = $path; + last; + } + } + + # We can't continue without a config file + die unless defined ($self->{grub_conf}); + + # Find the path which needs to be prepended to paths in grub.conf to + # make them absolute + # Look for the most specific mount point discovered + + # Default to / (no prefix required) + $self->{grub_fs} ||= ""; + + my %mounts = $g->inspect_get_mountpoints($root); + foreach my $path ('/boot/grub', '/boot') { + if (exists($mounts{$path})) { + $self->{grub_fs} = $path; + last; + } + } + + # Initialise augeas + eval { + # Check grub_conf is included by the Grub lens + my $found = 0; + foreach my $incl ($g->aug_match("/augeas/load/Grub/incl")) { + if ($g->aug_get($incl) eq $self->{grub_conf}) { + $found = 1; + last; + } + } + + # If it wasn't there, add it + unless ($found) { + $g->aug_set("/augeas/load/Grub/incl[last()+1]", $self->{grub_conf}); + + # Make augeas pick up the new configuration + $g->aug_load(); + } + }; + augeas_error($g, $@) if ($@); + + return $self; +} + +sub list_kernels +{ + my $self = shift; + + my $g = $self->{g}; + my $grub_conf = $self->{grub_conf}; + my $grub_fs = $self->{grub_fs}; + + # Look for a kernel, starting with the default + my @paths = eval { + # Get a list of all kernels + my @ret = $g->aug_match("/files$grub_conf/title/kernel"); + + # Get the default kernel from grub if it's set + # Doesn't matter if there isn't one (aug_get will fail) + my $default = eval { + my $idx = $g->aug_get("/files$grub_conf/default"); + + # Grub indices are zero-based, augeas is 1-based + "/files$grub_conf/title[".($idx + 1)."]/kernel"; + }; + + if (defined($default)) { + # Remove the default from the list and put it at the beginning + @ret = grep {!m{$default}} @ret; + unshift(@ret, $default); + } + + @ret; + }; + augeas_error($g, $@) if ($@); + + my @kernels; + my %checked; + foreach my $path (@paths) { + next if $checked{$path}; + $checked{$path} = 1; + + my $kernel = eval { $g->aug_get($path) }; + augeas_error($g, $@) if ($@); + + # Prepend the grub filesystem to the kernel path + $kernel = "$grub_fs$kernel" if defined $grub_fs; + + # Check the kernel exists + if ($g->exists($kernel)) { + push(@kernels, $kernel); + } + + else { + logmsg WARN, __x('grub refers to {path}, which doesn\'t exist.', + path => $kernel); + } + } + + return @kernels; +} + +sub update_console +{ + my $self = shift; + my ($remove) = @_; + + my $g = $self->{g}; + my $grub_conf = $self->{grub_conf}; + + eval { + # Update any kernel console lines + my $deleted = 0; + foreach my $augpath + ($g->aug_match("/files$grub_conf/title/kernel/console")) + { + my $console = $g->aug_get($augpath); + if ($console =~ /\b(x|h)vc0\b/) { + if ($remove) { + $g->aug_defvar("delconsole$deleted", $augpath); + $deleted++; + } else { + $console =~ s/\b(x|h)vc0\b/ttyS0/g; + $g->aug_set($augpath, $console); + } + } + + if ($remove && $console =~ /\bttyS0\b/) { + $g->aug_rm($augpath); + } + } + + for (my $i = 0; $i < $deleted; $i++) { + $g->aug_rm('$delconsole'.$i); + } + }; + augeas_error($g, $@) if ($@); +} + +sub check +{ + my $self = shift; + my ($path) = @_; + + my $g = $self->{g}; + my $grub_conf = $self->{grub_conf}; + my $grub_fs = $self->{grub_fs}; + $path =~ /^$grub_fs(.*)/ + or v2vdie __x('Kernel {kernel} is not under grub tree {grub}', + kernel => $path, grub => $grub_fs); + my $grub_path = $1; + + # Nothing to do if the kernel already has a grub entry + return if $g->aug_match("/files$grub_conf/title/kernel". + "[. = '$grub_path']") ne ''; + + my $kernel = + Sys::VirtConvert::Converter::SUSE::_inspect_linux_kernel($g, $path); + my $version = $kernel->{version}; + my $grub_initrd = dirname($path)."/initrd-$version"; + + # No point in dying if /etc/SuSE-release can't be read + my ($title) = eval { $g->read_lines('/etc/SuSE-release') }; + $title ||= 'Linux'; + + # This is how new-kernel-pkg does it + $title =~ s/ release.*//; + $title .= " ($version)"; + + # Doesn't matter if there's no default + my $default = eval { $g->aug_get("/files$grub_conf/default") }; + + if (defined($default)) { + $g->aug_defvar('template', + "/files$grub_conf/title[".($default + 1).']'); + } + + # If there's no default, take the first entry with a kernel + else { + my ($match) = $g->aug_match("/files$grub_conf/title/kernel"); + die("No template kernel found in grub.") unless defined($match); + + $match =~ s/\/kernel$//; + $g->aug_defvar('template', $match); + } + + # Add a new title node at the end + $g->aug_defnode('new', "/files$grub_conf/title[last()+1]", $title); + + # N.B. Don't change the order of root, kernel and initrd below, or the + # guest will not boot. + + # Copy root from the template + $g->aug_set('$new/root', $g->aug_get('$template/root')); + + # Set kernel and initrd to the new values + $g->aug_set('$new/kernel', $grub_path); + $g->aug_set('$new/initrd', $grub_initrd); + + # Copy all kernel command-line arguments + foreach my $arg ($g->aug_match('$template/kernel/*')) { + # kernel arguments don't necessarily have values + my $val = eval { $g->aug_get($arg) }; + + $arg =~ /([^\/]*)$/; + $arg = $1; + + if (defined($val)) { + $g->aug_set('$new/kernel/'.$arg, $val); + } else { + $g->aug_clear('$new/kernel/'.$arg); + } + } +} + +sub write +{ + my $self = shift; + my ($path) = @_; + + my $g = $self->{g}; + my $grub_conf = $self->{grub_conf}; + my $grub_fs = $self->{grub_fs}; + + $path =~ /^$grub_fs(.*)/ + or v2vdie __x('Kernel {kernel} is not under grub tree {grub}', + kernel => $path, grub => $grub_fs); + my $grub_path = $1; + + # Find the grub entry for the given kernel + eval { + my ($aug_path) = + $g->aug_match("/files$grub_conf/title/kernel[. = '$grub_path']"); + + v2vdie __x('Didn\'t find grub entry for kernel {kernel}', + kernel => $path) + unless defined($aug_path); + + $aug_path =~ m{/files$grub_conf/title(?:\[(\d+)\])?/kernel} + or die($aug_path); + my $grub_index = defined($1) ? $1 - 1 : 0; + + $g->aug_set("/files$grub_conf/default", $grub_index); + $g->aug_save(); + }; + augeas_error($g, $@) if ($@); +} + +# For Grub legacy, all we have to do is re-install grub in the correct place. +sub convert_efi +{ + my $self = shift; + my ($device) = @_; + + my $g = $self->{g}; + + $g->cp('/etc/grub.conf', '/boot/grub/grub.conf'); + $g->ln_sf('/boot/grub/grub.conf', '/etc/grub.conf'); + + # Reload to pick up grub.conf in its new location + eval { $g->aug_load() }; + augeas_error($g, $@) if ($@); + + $g->command(['grub-install.unsupported', $device]); +} + + +# Methods for inspecting and manipulating grub2. Note that we don't actually +# attempt to use grub2's configuration because it's utterly insane. Instead, +# we reverse engineer the way the config is automatically generated and use +# that instead. +package Sys::VirtConvert::Converter::SUSE::Grub2; + +use Sys::VirtConvert::Util qw(:DEFAULT augeas_error); +use Locale::TextDomain 'virt-v2v'; + +@Sys::VirtConvert::Converter::SUSE::Grub2::ISA = + qw(Sys::VirtConvert::Converter::SUSE::Grub); + +sub new +{ + my $class = shift; + my ($g, $root, $config) = @_; + + my $self = {}; + bless($self, $class); + + $self->{g} = $g; + $self->{root} = $root; + $self->{config} = $config; + + # Look for an EFI configuration + foreach my $cfg ($g->glob_expand('/boot/grub-efi/*/grub.cfg')) { + $self->check_efi(); + } + + # Check we have a grub2 configuration + if ($g->exists('/boot/grub2/grub.cfg')) { + $self->{cfg} = '/boot/grub2/grub.cfg'; + } + die unless exists $self->{cfg}; + + return $self; +} + +sub list_kernels +{ + my $self = shift; + my $g = $self->{g}; + + my @kernels; + + # Start by adding the default kernel + my $default = $self->get_default_image; + push(@kernels, $default) if length($default) > 0; + + # This is how the grub2 config generator enumerates kernels + foreach my $kernel ($g->glob_expand('/boot/kernel-*'), + $g->glob_expand('/boot/vmlinuz-*'), + $g->glob_expand('/vmlinuz-*')) + { + push(@kernels, $kernel) + unless $kernel =~ /\.(?:dpkg-.*|rpmsave|rpmnew)$/; + } + + return @kernels; +} + +sub update_console +{ + my $self = shift; + my ($remove) = @_; + + my $g = $self->{g}; + + my $cmdline = + eval { $g->aug_get('/files/etc/sysconfig/grub/GRUB_CMDLINE_LINUX') }; + + if (defined($cmdline) && $cmdline =~ /\bconsole=(?:x|h)vc0\b/) { + if ($remove) { + $cmdline =~ s/\bconsole=(?:x|h)vc0\b\s*//g; + } else { + $cmdline =~ s/\bconsole=(?:x|h)vc0\b/console=ttyS0/g; + } + + eval { + $g->aug_set('/files/etc/sysconfig/grub/GRUB_CMDLINE_LINUX', $cmdline); + $g->aug_save(); + }; + augeas_error($g, $@) if ($@); + + # We need to re-generate the grub config if we've updated this file + $g->command(['grub2-mkconfig', '-o', $self->{cfg}]); + } +} + +sub check +{ + # Nothing required for grub2 +} + +sub write +{ + my $self = shift; + my ($path) = @_; + + my $g = $self->{g}; + + my $default = $self->get_default_image; + + if ($default ne $path) { + eval { + $self->set_default_image($path); + }; + if ($@) { + logmsg WARN, __x('Unable to set default kernel to {path}', + path => $path); + } + } +} + +# For grub2, we : +# Turn the EFI partition into a BIOS Boot Partition +# Remove the former EFI partition from fstab +# Install the non-EFI version of grub +# Install grub2 in the BIOS Boot Partition +# Regenerate grub.cfg +sub convert_efi +{ + my $self = shift; + my ($device) = @_; + + my $g = $self->{g}; + + # EFI systems boot using grub2-efi, and probably don't have the base grub2 + # package installed. + Sys::VirtConvert::Convert::SUSE::_install_any + (undef, ['grub2'], undef, $g, $self->{root}, $self->{config}, $self) + or v2vdie __x('Failed to install non-EFI grub2'); + + # Relabel the EFI boot partition as a BIOS boot partition + $g->part_set_gpt_type($device, 1, '21686148-6449-6E6F-744E-656564454649'); + + # Delete the fstab entry for the EFI boot partition + eval { + foreach my $node ($g->aug_match("/files/etc/fstab/*[file = '/boot/efi']")) + { + $g->aug_rm($node); + } + }; + augeas_error($g, $@) if $@; + + # Install grub2 in the BIOS boot partition. This overwrites the previous + # contents of the EFI boot partition. + $g->command(['grub2-install', $device]); + + # Re-generate the grub2 config, and put it in the correct place + $g->command(['grub2-mkconfig', '-o', '/boot/grub2/grub.cfg']); +} + + +package Sys::VirtConvert::Converter::SUSE; + +use Sys::VirtConvert::Util qw(:DEFAULT augeas_error scsi_first_cmp); +use Locale::TextDomain 'virt-v2v'; + +=pod + +=head1 NAME + +Sys::VirtConvert::Converter::SUSE - Convert a SUSE based guest to run on KVM + +=head1 SYNOPSIS + + use Sys::VirtConvert::Converter; + + Sys::VirtConvert::Converter->convert($g, $root, $meta); + +=head1 DESCRIPTION + +Sys::VirtConvert::Converter::SUSE converts a SUSE based guest to run on KVM. + +=head1 METHODS + +=over + +=cut + +sub _is_sles_family +{ + my ($g, $root) = @_; + + return ($g->inspect_get_type($root) eq 'linux') && + ($g->inspect_get_distro($root) =~ /^(sles|suse-based)$/); +} + +=item Sys::VirtConvert::Converter::SUSE->can_handle(g, root) + +Return 1 if Sys::VirtConvert::Converter::SUSE can convert the given guest + +=cut + +sub can_handle +{ + my $class = shift; + + my ($g, $root) = @_; + + return ($g->inspect_get_type($root) eq 'linux' && + (_is_sles_family($g, $root) || + $g->inspect_get_distro($root) eq 'opensuse')); +} + +=item Sys::VirtConvert::Converter::SUSE->convert(g, root, config, meta, + options) + +Convert a SUSE based guest. Assume that can_handle has previously returned 1. + +=over + +=item g + +An initialised Sys::Guestfs handle + +=item root + +The root device of this operating system. + +=item config + +An initialised Sys::VirtConvert::Config + +=item meta + +Guest metadata. + +=item options + +A hashref of options which can influence the conversion + +=back + +=cut + +sub convert +{ + my $class = shift; + + my ($g, $root, $config, $meta, $options) = @_; + + _clean_rpmdb($g); + _init_selinux($g); + _init_augeas($g); + + _init_network($g); + + my $grub; + $grub = eval + { Sys::VirtConvert::Converter::SUSE::Grub2->new($g, $root, $config) }; + $grub = eval + { Sys::VirtConvert::Converter::SUSE::GrubLegacy->new($g, $root) } + unless defined($grub); + v2vdie __('No grub configuration found') unless defined($grub); + + # Un-configure HV specific attributes which don't require a direct + # replacement + _unconfigure_hv($g, $root); + + # Try to install the virtio capability + my $virtio = _install_capability('virtio', $g, $root, $config, $meta, $grub); + + # Get an appropriate kernel, or install one if none is available + my $kernel = _configure_kernel($virtio, $g, $root, $config, $meta, $grub); + + # Install user custom packages + if (! _install_capability('user-custom', $g, $root, $config, $meta, $grub)) { + logmsg WARN, __('Failed to install user-custom packages'); + } + + # Configure the rest of the system + my $remove_serial_console = exists($options->{NO_SERIAL_CONSOLE}); + _configure_console($g, $grub, $remove_serial_console); + + _configure_display_driver($g, $root, $config, $meta, $grub); + _remap_block_devices($meta, $virtio, $g, $root, $grub); + _configure_kernel_modules($g, $virtio); + _configure_boot($kernel, $virtio, $g, $root, $grub); + + my %guestcaps; + + $guestcaps{block} = $virtio == 1 ? 'virtio' : 'ide'; + $guestcaps{net} = $virtio == 1 ? 'virtio' : 'e1000'; + $guestcaps{arch} = _get_os_arch($g, $root, $grub); + $guestcaps{acpi} = _supports_acpi($g, $root, $guestcaps{arch}); + + return \%guestcaps; +} + +sub _init_selinux +{ + my ($g) = @_; + + # Assume SELinux isn't in use if load_policy isn't available + return if(!$g->exists('/usr/sbin/load_policy')); + + # Actually loading the policy has proven to be problematic. We make whatever + # changes are necessary, and make the guest relabel on the next boot. + $g->touch('/.autorelabel'); +} + +sub _init_augeas +{ + my ($g) = @_; + + # Initialise augeas + eval { $g->aug_init("/", 1) }; + augeas_error($g, $@) if ($@); +} + +sub _init_network +{ + my ($g) = @_; + + # Start dhcpcd to obtain a network address + eval { + $g->sh('dhcpcd --netconfig -p eth0'); + }; + + if ($@) { + logmsg WARN, __x('Failed to obtain an IP address. '. + 'Error message was: {error}', error => $@); + return 0; + } + + # Immediately exit dhcpcd as the daemon causes autosync to fail + $g->sh('dhcpcd -x eth0'); + + return 1; +} + +# Execute an augeas modprobe query against all possible modprobe locations +sub _aug_modprobe +{ + my ($g, $query) = @_; + + my @paths; + for my $pattern ('/files/etc/conf.modules/alias', + '/files/etc/modules.conf/alias', + '/files/etc/modprobe.conf/alias', + '/files/etc/modprobe.d/*/alias') { + push(@paths, $g->aug_match($pattern.'['.$query.']')); + } + + return @paths; +} + +# Check how new modules should be configured. Possibilities, in descending +# order of preference, are: +# modprobe.d/ +# modprobe.conf +# modules.conf +# conf.modules +sub _discover_modpath +{ + my ($g) = @_; + + my $modpath; + + # Note that we're checking in ascending order of preference so that the last + # discovered method will be chosen + + foreach my $file ('/etc/conf.modules', '/etc/modules.conf') { + if($g->exists($file)) { + $modpath = $file; + } + } + + if($g->exists("/etc/modprobe.conf")) { + $modpath = "modprobe.conf"; + } + + # If the modprobe.d directory exists, create new entries in + # modprobe.d/virtv2v-added.conf + if($g->exists("/etc/modprobe.d")) { + $modpath = "modprobe.d/virtv2v-added.conf"; + } + + v2vdie __('Unable to find any valid modprobe configuration') + unless defined($modpath); + + return $modpath; +} + +sub _configure_kernel_modules +{ + my ($g, $virtio, $modpath) = @_; + + # Make a note of whether we've added scsi_hostadapter + # For simplicity we always ensure this is set for virtio disks. + my $scsi_hostadapter = 0; + + eval { + foreach my $path (_aug_modprobe($g, ". =~ regexp('eth[0-9]+')")) + { + $g->aug_set($path.'/modulename', + $virtio == 1 ? 'virtio_net' : 'e1000'); + } + + my @paths = _aug_modprobe($g, ". =~ regexp('scsi_hostadapter.*')"); + if ($virtio) { + # There's only 1 scsi controller in the converted guest. + # Convert only the first scsi_hostadapter entry to virtio. + + # Note that we delete paths in reverse order. This means we don't + # have to worry about alias indices being changed. + while (@paths > 1) { + $g->aug_rm(pop(@paths)); + } + + if (@paths == 1) { + $g->aug_set(pop(@paths).'/modulename', 'virtio_blk'); + $scsi_hostadapter = 1; + } + } + + else { + # There's no scsi controller in an IDE guest + while (@paths) { + $g->aug_rm(pop(@paths)); + } + } + + # Display a warning about any leftover xen modules which we haven't + # converted + my @xen_modules = qw(xennet xen-vnif xenblk xen-vbd); + my $query = '('.join('|', @xen_modules).')'; + + foreach my $path (_aug_modprobe($g, "modulename =~ regexp('$query')")) { + my $device = $g->aug_get($path); + my $module = $g->aug_get($path.'/modulename'); + + logmsg WARN, __x("Don't know how to update ". + '{device}, which loads the {module} module.', + device => $device, module => $module); + } + + # Add an explicit scsi_hostadapter if it wasn't there before + if ($virtio && !$scsi_hostadapter) { + my $modpath = _discover_modpath($g); + + $g->aug_set("/files$modpath/alias[last()+1]", + 'scsi_hostadapter'); + $g->aug_set("/files$modpath/alias[last()]/modulename", + 'virtio_blk'); + } + + $g->aug_save(); + }; + augeas_error($g, $@) if $@; +} + +# Configure a console on ttyS0. Make sure existing console references use it. +# N.B. Note that the RHEL 6 xen guest kernel presents a console device called +# /dev/hvc0, where previous xen guest kernels presented /dev/xvc0. The regular +# kernel running under KVM also presents a virtio console device called +# /dev/hvc0, so ideally we would just leave it alone. However, RHEL 6 libvirt +# doesn't yet support this device so we can't attach to it. We therefore use +# /dev/ttyS0 for RHEL 6 anyway. +# +# If the target doesn't support a serial console, we want to remove all +# references to it instead. +sub _configure_console +{ + my ($g, $grub, $remove) = @_; + + # Look for gettys which use xvc0 or hvc0 + foreach my $augpath ($g->aug_match("/files/etc/inittab/*/process")) { + my $proc = $g->aug_get($augpath); + + # If the process mentions xvc0, change it to ttyS0 + if ($proc =~ /\b(x|h)vc0\b/) { + if ($remove) { + $g->aug_rm($augpath.'/..'); + } else { + $proc =~ s/\b(x|h)vc0\b/ttyS0/g; + $g->aug_set($augpath, $proc); + } + } + + if ($remove && $proc =~ /\bttyS0\b/) { + $g->aug_rm($augpath.'/..'); + } + } + + # Replace any mention of xvc0 or hvc0 in /etc/securetty with ttyS0 + foreach my $augpath ($g->aug_match('/files/etc/securetty/*')) { + my $tty = $g->aug_get($augpath); + + if($tty eq "xvc0" || $tty eq "hvc0") { + if ($remove) { + $g->aug_rm($augpath); + } else { + $g->aug_set($augpath, 'ttyS0'); + } + } + + if ($remove && $tty eq 'ttyS0') { + $g->aug_rm($augpath); + } + } + + $grub->update_console($remove); + + eval { $g->aug_save() }; + augeas_error($g, $@) if ($@); +} + +sub _configure_display_driver +{ + my ($g, $root, $config, $meta, $grub) = @_; + + # Update the display driver if it exists + my $updated = 0; + eval { + my $xorg; + + # Check which X configuration we have, and make augeas load it if + # necessary + if (! $g->exists('/etc/X11/xorg.conf') && + $g->exists('/etc/X11/XF86Config')) + { + $g->aug_set('/augeas/load/Xorg/incl[last()+1]', + '/etc/X11/XF86Config'); + + # Reload to pick up the new configuration + $g->aug_load(); + + $xorg = '/etc/X11/XF86Config'; + } else { + $xorg = '/etc/X11/xorg.conf'; + } + + foreach my $path ($g->aug_match('/files'.$xorg.'/Device/Driver')) { + $g->aug_set($path, 'cirrus'); + $updated = 1; + } + + # Remove VendorName and BoardName if present + foreach my $path + ($g->aug_match('/files'.$xorg.'/Device/VendorName'), + $g->aug_match('/files'.$xorg.'/Device/BoardName')) + { + $g->aug_rm($path); + } + + $g->aug_save(); + }; + + # Propagate augeas errors + augeas_error($g, $@) if ($@); + + # If we updated the X driver, check if X itself is actually installed. If it + # is, ensure the cirrus driver is installed. + if ($updated && + ($g->exists('/usr/bin/X') || $g->exists('/usr/bin/X11/X')) && + !_install_capability('cirrus', $g, $root, $config, $meta, $grub)) + { + logmsg WARN, __('Display driver was updated to cirrus, but unable to '. + 'install cirrus driver. X may not function correctly'); + } +} + +# If the guest was shutdown uncleanly, it's possible that transient state was +# left lying around in the rpm database. Given that nothing is using the +# rpmdb at this point, it's safe to delete these files. +sub _clean_rpmdb +{ + my $g = shift; + + foreach my $f ($g->glob_expand('/var/lib/rpm/__db.00?')) { + $g->rm($f); + } +} + +# Use various methods to try to work out what Linux kernel we've got. +# Returns a hashref containing: +# path => path to kernel (same as $path variable passed in) +# package => base package name (eg. "kernel", "kernel-PAE") +# version => version string +# modules => array ref list of modules (paths to *.ko files) +# arch => architecture of the kernel +sub _inspect_linux_kernel +{ + my ($g, $path) = @_; + + my %kernel = (); + + $kernel{path} = $path; + + # If this is a packaged kernel, try to work out the name of the package + # which installed it. This lets us know what to install to replace it with, + # e.g. kernel, kernel-pae, kernel-smp, kernel-bigsmp, kernel-default + my $package = eval { $g->command(['rpm', '-qf', '--qf', + '%{NAME}', $path]) }; + $kernel{package} = $package if defined($package);; + + # Try to get the kernel version by running file against it + my $version; + my $filedesc = $g->file($path); + if($filedesc =~ /^$path: Linux kernel .*\bversion\s+(\S+)\b/) { + $version = $1; + } + + # Sometimes file can't work out the kernel version, for example because it's + # a Xen PV kernel. In this case try to guess the version from the filename + else { + if($path =~ m{/boot/vmlinuz-(.*)}) { + $version = $1; + + # Check /lib/modules/$version exists + if(!$g->is_dir("/lib/modules/$version")) { + warn __x("Didn't find modules directory {modules} for kernel ". + "{path}\n", modules => "/lib/modules/$version", + path => $path); + + # Give up + return undef; + } + } else { + warn __x("Couldn't guess kernel version number from path for ". + "kernel {path}\n", path => $path); + + # Give up + return undef; + } + } + + $kernel{version} = $version; + + # List modules. + my @modules; + my $any_module; + my $prefix = "/lib/modules/$version"; + foreach my $module ($g->find ($prefix)) { + if ($module =~ m{/([^/]+)\.(?:ko|o)$}) { + $any_module = "$prefix$module" unless defined $any_module; + push @modules, $1; + } + } + + $kernel{modules} = \@modules; + + # Determine kernel architecture by looking at the arch + # of any kernel module. + $kernel{arch} = $g->file_architecture ($any_module); + + return \%kernel; +} + +sub _configure_kernel +{ + my ($virtio, $g, $root, $config, $meta, $grub) = @_; + + # Pick first appropriate kernel returned by list_kernels + my $boot_kernel; + foreach my $path ($grub->list_kernels()) { + my $kernel = _inspect_linux_kernel($g, $path); + my $version = $kernel->{version}; + + # Skip foreign kernels + next if _is_hv_kernel($g, $version); + + # If we're configuring virtio, check this kernel supports it + next if ($virtio && !_supports_virtio($version, $g)); + + $boot_kernel = $version; + last; + } + + # Warn if there is no installed virtio capable kernel + logmsg NOTICE, __x('virtio capable guest, but no virtio kernel found. '. + 'Trying to install one...') + if ($virtio && !defined($boot_kernel)); + + # If none of the installed kernels are appropriate, install a new one + if(!defined($boot_kernel)) { + my ($kernel_pkg, $kernel_arch, undef) = + _discover_kernel($g, $root, $grub); + + # If the guest is using a Xen PV kernel, choose an appropriate + # normal kernel replacement + if ($kernel_pkg eq "kernel-xen" || $kernel_pkg eq "kernel-xen-base") { + $kernel_pkg = _get_replacement_kernel_name($g, $root, + $kernel_arch, $meta); + + # Check there isn't already one installed + my ($kernel) = _get_installed("$kernel_pkg.$kernel_arch", $g); + $boot_kernel = $kernel->[1].'-'.$kernel->[2].'.'.$kernel_arch + if defined($kernel); + } + + if (!defined($boot_kernel)) { + # List of kernels before the new kernel installation + my @k_before = $g->glob_expand('/boot/vmlinuz-*'); + + if (_install_any([$kernel_pkg, $kernel_arch], undef, undef, + $g, $root, $config, $grub)) + { + # Figure out which kernel has just been installed + my $version; + foreach my $k ($g->glob_expand('/boot/vmlinuz-*')) { + if (!grep(/^$k$/, @k_before)) { + # Check which directory in /lib/modules the kernel rpm + # creates + foreach my $file + ($g->command_lines(['rpm', '-qlf', $k])) + { + next unless ($file =~ m{^/lib/modules/([^/]+)$}); + + if ($g->is_dir("/lib/modules/$1")) { + $boot_kernel = $1; + last; + } + } + + last if defined($boot_kernel); + } + } + + v2vdie __x('Couldn\'t determine version of installed kernel') + unless defined($boot_kernel); + } else { + v2vdie __x('Failed to find a {name} package to install', + name => "$kernel_pkg"); + } + } + } + + # Check we have a bootable kernel. + v2vdie __('No bootable kernels installed, and no replacement '. + "is available.\nUnable to continue.") + unless defined($boot_kernel); + + # Ensure DEFAULTKERNEL is set to boot kernel package name + # It's not fatal if this rpm command fails + my ($kernel_pkg) = + eval { $g->command_lines(['rpm', '-qf', "/lib/modules/$boot_kernel", + '--qf', '%{NAME}\n']) }; + if (defined($kernel_pkg) && $g->exists('/etc/sysconfig/kernel')) { + eval { + foreach my $path ($g->aug_match('/files/etc/sysconfig/kernel'. + '/DEFAULTKERNEL/value')) + { + $g->aug_set($path, $kernel_pkg); + } + + $g->aug_save(); + }; + # Propagate augeas errors + augeas_error($g, $@) if ($@); + } + + return $boot_kernel; +} + +sub _configure_boot +{ + my ($kernel, $virtio, $g, $root, $grub) = @_; + + if($virtio) { + # The order of modules here is deliberately the same as the order + # specified in the postinstall script of kmod-virtio in RHEL 3. The + # reason is that the probing order determines the major number of vdX + # block devices. If we change it, RHEL 3 KVM guests won't boot. + _prepare_bootable($g, $root, $grub, $kernel, "virtio", "virtio_ring", + "virtio_blk", "virtio_net", + "virtio_pci"); + } else { + _prepare_bootable($g, $root, $grub, $kernel, "sym53c8xx"); + } +} + +# Get the target architecture from the default boot kernel +sub _get_os_arch +{ + my ($g, $root, $grub) = @_; + + # Get the arch of the default kernel + my @kernels = $grub->list_kernels(); + my $path = $kernels[0] if @kernels > 0; + my $kernel = _inspect_linux_kernel($g, $path) if defined($path); + my $arch = $kernel->{arch} if defined($kernel); + + # Use the libguestfs-detected arch if the above failed + $arch = $g->inspect_get_arch($root) unless defined($arch); + + # Default to x86_64 if we still didn't find an architecture + return 'x86_64' unless defined($arch); + + # We want an i586 guest for i[346]86 + return 'i586' if($arch =~ /^i[346]86$/); + + return $arch; +} + +# Determine if a specific kernel is hypervisor-specific +sub _is_hv_kernel +{ + my ($g, $version) = @_; + + # Xen PV kernels can be distinguished from other kernels by their inclusion + # of the xennet driver + foreach my $entry ($g->find("/lib/modules/$version/")) { + return 1 if $entry =~ /(^|\/)xennet\.k?o$/; + } + + return 0; +} + +sub _remove_applications +{ + my ($g, @apps) = @_; + + # Nothing to do if we were given an empty list + return if scalar(@apps) == 0; + + $g->command(['rpm', '-e', @apps]); + + # Make augeas reload in case the removal changed anything + eval { $g->aug_load() }; + augeas_error($g, $@) if ($@); +} + +sub _get_application_owner +{ + my ($file, $g) = @_; + + return $g->command(['rpm', '-qf', $file]); +} + +sub _unconfigure_hv +{ + my ($g, $root) = @_; + + my @apps = $g->inspect_list_applications($root); + + _unconfigure_xen($g, \@apps); + _unconfigure_vbox($g, \@apps); + _unconfigure_vmware($g, \@apps); + _unconfigure_citrix($g, \@apps); +} + +# Unconfigure Xen specific guest modifications +sub _unconfigure_xen +{ + # FIXME Nothing to do for SUSE (possibly remove kernel-xen?) +} + +# Unconfigure VirtualBox specific guest modifications +sub _unconfigure_vbox +{ + my ($g, $apps) = @_; + + # Uninstall VirtualBox Guest Additions + my @remove; + foreach my $app (@$apps) { + my $name = $app->{app_name}; + + if ($name eq "virtualbox-guest-additions") { + push(@remove, $name); + } + } + _remove_applications($g, @remove); + + # VirtualBox Guest Additions may have been installed from tarball, in which + # case the above won't detect it. Look for the uninstall tool, and run it + # if it's present. + # + # Note that it's important we do this early in the conversion process, as + # this uninstallation script naively overwrites configuration files with + # versions it cached prior to installation. + my $vboxconfig = '/var/lib/VBoxGuestAdditions/config'; + if ($g->exists($vboxconfig)) { + my $vboxuninstall; + foreach (split /\n/, $g->cat($vboxconfig)) { + if ($_ =~ /^INSTALL_DIR=(.*$)/) { + $vboxuninstall = $1 . '/uninstall.sh'; + } + } + if ($g->exists($vboxuninstall)) { + eval { $g->command([$vboxuninstall]) }; + logmsg WARN, __x('VirtualBox Guest Additions were detected, but '. + 'uninstallation failed. The error message was: '. + '{error}', error => $@) if $@; + + # Reload augeas to detect changes made by vbox tools uninstallation + eval { $g->aug_load() }; + augeas_error($g, $@) if $@; + } + } +} + +# Unconfigure VMware specific guest modifications +sub _unconfigure_vmware +{ + my ($g, $apps) = @_; + + # FIXME YUM does not ship with SUSE, but is available through OSS repos + # Look for any configured vmware yum repos, and disable them + foreach my $repo ($g->aug_match( + '/files/etc/yum.repos.d/*/*'. + '[baseurl =~ regexp(\'https?://([^/]+\.)?vmware\.com/.*\')]')) + { + eval { + $g->aug_set($repo.'/enabled', 0); + $g->aug_save(); + }; + augeas_error($g, $@) if ($@); + } + + # Uninstall VMwareTools + my @remove; + my @libraries; + foreach my $app (@$apps) { + my $name = $app->{app_name}; + + if ($name =~ /^vmware-tools-/) { + if ($name =~ /^vmware-tools-libraries-/) { + push(@libraries, $name); + } else { + push (@remove, $name); + } + } + elsif ($name eq "VMwareTools" || $name =~ /^(?:kmod-)?vmware-tools-/) { + push(@remove, $name); + } + } + + # VMware tools includes 'libraries' packages which provide custom versions + # of core functionality. We need to install non-custom versions of + # everything provided by these packages before attempting to uninstall + # them, or we'll hit dependency issues + if (@libraries > 0) { + # We only support removal of these libraries packages on systems which + # use yum. + # FIXME zypper can't resolve dependencies (yet). Leave the yum check + # in place for now. + if ($g->exists('/usr/bin/yum')) { + foreach my $library (@libraries) { + eval { + my @provides = $g->command_lines + (['rpm', '-q', '--provides', $library]); + + # The packages also explicitly provide themselves. + # Filter this out. + @provides = grep {$_ !~ /$library/} + + # Trim whitespace + map { s/^\s*(\S+)\s*$/$1/; $_ } @provides; + + # Install the dependencies with yum. We use yum + # explicitly here, as up2date wouldn't work anyway and + # local install is impractical due to the large number + # of required dependencies out of our control. + my %alts; + foreach my $alt ($g->command_lines + (['yum', '-q', 'resolvedep', @provides])) + { + $alts{$alt} = 1; + } + + my @replacements = keys(%alts); + $g->command(['yum', 'install', '-y', @replacements]) + if @replacements > 0; + + push(@remove, $library); + }; + logmsg WARN, __x('Failed to install replacement '. + 'dependencies for {lib}. Package will '. + 'not be uninstalled. Error was: {error}', + lib => $library, error => $@) if $@; + } + } + } + + _remove_applications($g, @remove); + + # VMwareTools may have been installed from tarball, in which case the above + # won't detect it. Look for the uninstall tool, and run it if it's present. + # + # Note that it's important we do this early in the conversion process, as + # this uninstallation script naively overwrites configuration files with + # versions it cached prior to installation. + my $vmwaretools = '/usr/bin/vmware-uninstall-tools.pl'; + if ($g->exists($vmwaretools)) { + eval { $g->command([$vmwaretools]) }; + logmsg WARN, __x('VMware Tools was detected, but uninstallation '. + 'failed. The error message was: {error}', + error => $@) if $@; + + # Reload augeas to detect changes made by vmware tools uninstallation + eval { $g->aug_load() }; + augeas_error($g, $@) if $@; + } +} + +sub _unconfigure_citrix +{ + my ($g, $apps) = @_; + + # Look for xe-guest-utilities* + my @remove; + foreach my $app (@$apps) { + my $name = $app->{app_name}; + + if($name =~ /^xe-guest-utilities(-.*)?$/) { + push(@remove, $name); + } + } + _remove_applications($g, @remove); + + # Installing these guest utilities automatically unconfigures ttys in + # /etc/inittab if the system uses it. We need to put them back. + if (scalar(@remove) > 0) { + eval { + my $updated = 0; + for my $commentp ($g->aug_match('/files/etc/inittab/#comment')) { + my $comment = $g->aug_get($commentp); + + # The entries in question are named 1-6, and will normally be + # active in runlevels 2-5. They will be gettys. We could be + # extremely prescriptive here, but allow for a reasonable amount + # of variation just in case. + next unless $comment =~ /^([1-6]):([2-5]+):respawn:(.*)/; + + my $name = $1; + my $runlevels = $2; + my $process = $3; + + next unless $process =~ /getty/; + + # Create a new entry immediately after the comment + $g->aug_insert($commentp, $name, 0); + $g->aug_set("/files/etc/inittab/$name/runlevels", $runlevels); + $g->aug_set("/files/etc/inittab/$name/action", 'respawn'); + $g->aug_set("/files/etc/inittab/$name/process", $process); + + # Create a variable to point to the comment node so we can + # delete it later. If we deleted it here it would invalidate + # subsquent comment paths returned by aug_match. + $g->aug_defvar("delete$updated", $commentp); + + $updated++; + } + + # Delete all the comments + my $i = 0; + while ($i < $updated) { + $g->aug_rm("\$delete$i"); + $i++; + } + + $g->aug_save(); + }; + augeas_error($g, $@) if ($@); + } +} + +sub _install_capability +{ + my ($name, $g, $root, $config, $meta, $grub) = @_; + + my $cap = eval { $config->match_capability($g, $root, $name) }; + if ($@) { + warn($@); + return 0; + } + + if (!defined($cap)) { + logmsg WARN, __x('{name} capability not found in configuration', + name => $name); + return 0; + } + + my @install; + my @update; + my $kernel; + foreach my $name (keys(%$cap)) { + my $props = $cap->{$name}; + my $ifinstalled = $props->{ifinstalled}; + + # Parse epoch, version and release from minversion + my ($min_epoch, $min_version, $min_release); + if (exists($props->{minversion})) { + eval { + ($min_epoch, $min_version, $min_release) = + _parse_evr($props->{minversion}); + }; + v2vdie __x('Unrecognised format for {field} in config: '. + '{value}. {field} must be in the format '. + '[epoch:]version[-release].', + field => 'minversion', value => $props->{minversion}) + if $@; + } + + # Kernels are special + if ($name eq 'kernel') { + my ($kernel_pkg, $kernel_arch, $kernel_rpmver) = + _discover_kernel($g, $root, $grub); + + # If we didn't establish a kernel version, assume we have to update + # it. + if (!defined($kernel_rpmver)) { + $kernel = [$kernel_pkg, $kernel_arch]; + } + + else { + my ($kernel_epoch, $kernel_ver, $kernel_release) = + eval { _parse_evr($kernel_rpmver) }; + if ($@) { + # Don't die here, just make best effort to do a version + # comparison by directly comparing the full strings + $kernel_epoch = undef; + $kernel_ver = $kernel_rpmver; + $kernel_release = undef; + + $min_epoch = undef; + $min_version = $props->{minversion}; + $min_release = undef; + } + + # If the guest is using a Xen PV kernel, choose an appropriate + # normal kernel replacement + if ($kernel_pkg eq "kernel-xen" || $kernel_pkg eq "kernel-xen-base") + { + $kernel_pkg = + _get_replacement_kernel_name($g, $root, $kernel_arch, + $meta); + + # Check if we've already got an appropriate kernel + my ($inst) = + _get_installed("$kernel_pkg.$kernel_arch", $g); + + if (!defined($inst) || + (defined($min_version) && + _evr_cmp($inst->[0], $inst->[1], $inst->[2], + $min_epoch, $min_version, $min_release) < 0)) + { + # filter out xen from release field + if (defined($kernel_release) && + $kernel_release =~ /^(\S+?)(xen)?$/) + { + $kernel_release = $1; + } + + # If the guest kernel is new enough, but PV, try to + # replace it with an equivalent version FV kernel + if (!defined($min_version) || + _evr_cmp($kernel_epoch, $kernel_ver, + $kernel_release, + $min_epoch, $min_version, + $min_release) >= 0) + { + $kernel = [$kernel_pkg, $kernel_arch, + $kernel_epoch, $kernel_ver, + $kernel_release]; + } + + # Otherwise, just grab the latest + else { + $kernel = [$kernel_pkg, $kernel_arch]; + } + } + } + + # If the kernel is too old, grab the latest replacement + elsif (defined($min_version) && + _evr_cmp($kernel_epoch, $kernel_ver, $kernel_release, + $min_epoch, $min_version, $min_release) < 0) + { + $kernel = [$kernel_pkg, $kernel_arch]; + } + } + } + + else { + my @installed = _get_installed($name, $g); + + # Ignore an 'ifinstalled' dep if it's not currently installed + next if (@installed == 0 && $ifinstalled); + + # Ok if any version is installed and no minversion was specified + next if (@installed > 0 && !defined($min_version)); + + if (defined($min_version)) { + # Check if any installed version meets the minimum version + my $found = 0; + foreach my $app (@installed) { + my ($epoch, $version, $release) = @$app; + + if (_evr_cmp($app->[0], $app->[1], $app->[2], + $min_epoch, $min_version, $min_release) >= 0) { + $found = 1; + last; + } + } + + # Install the latest available version of the dep if it wasn't + # found. NOTE - No version requirements are enforced. + if (!$found) { + if (@installed == 0) { + push(@install, [$name]); + } else { + push(@update, [$name]); + } + } + } else { + push(@install, [$name]); + } + } + } + + # Capability is already installed + if (!defined($kernel) && @install == 0 && @update == 0) { + return 1; + } + + my $success = _install_any($kernel, \@install, \@update, + $g, $root, $config, $grub); + + return $success; +} + +sub _install_any +{ + my ($kernel, $install, $update, $g, $root, $config, $grub) = @_; + + # If we're installing a kernel, check which kernels are there first + my @k_before = $g->glob_expand('/boot/vmlinuz-*') if defined($kernel); + + # Due to bnc#836521, root can be set to (hd*), instead of (hd*,*) + # For the time being, workaround the problem + my $pbl_fix = _modify_perlBootloader($g) if defined($kernel); + + my $success = 0; + eval { + # Try to fetch these dependencies using the guest's native update + # tool + $success = _install_zypper($kernel, $install, $update, $g); + + # Fall back to local config if the above didn't work + $success = _install_config($kernel, $install, $update, + $g, $root, $config) + unless ($success); + }; + warn($@) if $@; + + # Restore the previous bootloader file if previously fixed (to ensure + # future updates don't complain + _restore_perlBootloader($g) if ($pbl_fix == 1); + + # Make augeas reload to pick up any altered configuration + eval { $g->aug_load() }; + augeas_error($g, $@) if ($@); + + # Check that the kernel we installed has a grub entry. + if (defined($kernel)) { + foreach my $k ($g->glob_expand('/boot/vmlinuz-*')) { + if (!grep(/^$k$/, @k_before)) { + $grub->check($k); + last; + } + } + } + + return $success; +} + +sub _install_zypper +{ + my ($kernel, $install, $update, $g) = @_; + + # Check this system has zypper + return 0 unless ($g->exists('/usr/bin/zypper')); + + # Install or update the kernel? + # If it isn't installed (because we're replacing a PV kernel), we need to + # install + # If we're installing a specific version, we need to install + # If the kernel package we're installing is already installed and we're + # just upgrading to the latest version, we need to update + if (defined($kernel)) { + my @installed = _get_installed($kernel->[0], $g); + + # Don't modify the contents of $install and $update in case we fall + # through and they're reused in another function + if (@installed == 0 || defined($kernel->[2])) { + my @tmp = defined($install) ? @$install : (); + push(@tmp, $kernel); + $install = \@tmp; + } else { + my @tmp = defined($update) ? @$update : (); + push(@tmp, $kernel); + $update = \@tmp; + } + } + + my $success = 1; + # Error when installing: "No provider of 'pkg' found." + # (Not an) Error when updating: "Package 'pkg' is not available in your + # repositories. Cannot reinstall, upgrade, or downgrade." + ZYPPER: foreach my $task ( + [ "install", $install, qr/(^No package|already installed)/ ], + [ "update", $update, qr/(^No Packages|not available)/ ] + ) { + my ($action, $list, $failure) = @$task; + + # Build a list of packages to install + my @pkgs; + foreach my $entry (@$list) { + next unless (defined($entry)); + + # zypper doesn't need arch or epoch + my ($name, undef, undef, $version, $release) = @$entry; + + # Construct n-v-r + my $pkg = $name; + $pkg .= "-$version" if (defined($version)); + $pkg .= "-$release" if (defined($release)); + + push(@pkgs, "$pkg"); + } + + if (@pkgs) { + my @output = + eval { $g->command(['/usr/bin/zypper', '-n', "$action", + "@pkgs"]) }; + if ($@) { + # Catch 'No provider' errors and ignore them, as a local + # install will then be attempted + if ($@ =~ /No provider/) { + logmsg NOTICE, __x('No provider of {package} found using '. + 'zypper. Checking virt-v2v repo.', + package => @pkgs); + } else { + logmsg WARN, __x('Failed to {action} {package} using '. + 'zypper: {error}', action => $action, + package => @pkgs, error => $@); + } + $success = 0; + last ZYPPER; + } + foreach my $line (@output) { + # Don't report an error or results if package is already installed + # or not found in a repo + if ($line =~ /$failure/) { + $success = 0; + last ZYPPER; + } + logmsg NOTICE, __x(($action eq 'install' ? 'Installed' : 'Updated'). + ' the following package(s) using zypper:'); + foreach my $pkg (@pkgs) { + logmsg NOTICE, __x(" $pkg"); + } + } + } + } + + return $success; +} + +sub _install_config +{ + my ($kernel_naevr, $install, $update, $g, $root, $config) = @_; + + my ($kernel, $user); + if (defined($kernel_naevr)) { + my ($kernel_pkg, $kernel_arch) = @$kernel_naevr; + + ($kernel, $user) = + $config->match_app($g, $root, $kernel_pkg, $kernel_arch); + } else { + $user = []; + } + + foreach my $pkg (@$install, @$update) { + push(@$user, $pkg->[0]); + } + + my @missing; + if (defined($kernel)) { + my $transfer_path = $config->get_transfer_path($kernel); + if (!defined($transfer_path) || !$g->exists($transfer_path)) { + push(@missing, $kernel); + } + } + + my @user_paths = _get_deppaths($g, $root, $config, + \@missing, $g->inspect_get_arch($root), @$user); + + # We can't proceed if there are any files missing + v2vdie __x('Installation failed because the following '. + 'files referenced in the configuration file are '. + 'required, but missing: {list}', + list => join(' ', @missing)) if scalar(@missing) > 0; + # Install any non-kernel requirements + _install_rpms($g, $config, 1, @user_paths); + + if (defined($kernel)) { + _install_rpms($g, $config, 0, ($kernel)); + } + + return 1; +} + +# Install a set of rpms +sub _install_rpms +{ + local $_; + my ($g, $config, $update, @rpms) = @_; + + # Nothing to do if we got an empty set + return if(scalar(@rpms) == 0); + + # All paths are relative to the transfer mount. Need to make them absolute. + # No need to check get_transfer_path here as all paths have been previously + # checked + @rpms = map { $_ = $config->get_transfer_path($_) } @rpms; + + eval {$g->command(['rpm', $update == 1 ? '-U' : '-i', @rpms]) }; + if($@) { + logmsg WARN, __x('Failed to install packages from virt-v2v repo: '. + '{error}', error => $@); + } + else { + logmsg NOTICE, __x('Installed the following package(s) from virt-v2v repo:'); + foreach my $pkg (@rpms) { + $pkg =~ s/(.*\/)//; + logmsg NOTICE, __x(" $pkg"); + } + } + + # Reload augeas in case the rpm installation changed anything + eval { $g->aug_load() }; + augeas_error($g, $@) if($@); +} + +# Return a list of dependency paths which need to be installed for the given +# apps +sub _get_deppaths +{ + my ($g, $root, $config, $missing, $arch, @apps) = @_; + + my %required; + foreach my $app (@apps) { + my ($path, $deps) = $config->match_app($g, $root, $app, $arch); + + my $transfer_path = $config->get_transfer_path($path); + my $exists = defined($transfer_path) && $g->exists($transfer_path); + + if (!$exists) { + push(@$missing, $path); + } + + if (!$exists || !_newer_installed($transfer_path, $g, $config)) { + $required{$path} = 1; + + foreach my $deppath (_get_deppaths($g, $root, $config, + $missing, $arch, @$deps)) + { + $required{$deppath} = 1; + } + } + + } + + return keys(%required); +} + +# Return 1 if the requested rpm, or a newer version, is installed +# Return 0 otherwise +sub _newer_installed +{ + my ($rpm, $g, $config) = @_; + + my ($name, $epoch, $version, $release, $arch) = + _get_nevra($rpm, $g, $config); + + my @installed = _get_installed("$name.$arch", $g); + + # Search installed rpms matching <name>.<arch> + foreach my $pkg (@installed) { + next if _evr_cmp($pkg->[0], $pkg->[1], $pkg->[2], + $epoch, $version, $release) < 0; + return 1; + } + + return 0; +} + +sub _get_nevra +{ + my ($rpm, $g, $config) = @_; + + # Get NEVRA for the rpm to be installed + my $nevra = $g->command(['rpm', '-qp', '--qf', + '%{NAME} %{EPOCH} %{VERSION} %{RELEASE} %{ARCH}', + $rpm]); + + $nevra =~ /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)$/ + or die("Unexpected return from rpm command: $nevra"); + my ($name, $epoch, $version, $release, $arch) = ($1, $2, $3, $4, $5); + + # Ensure epoch is always numeric + $epoch = 0 if('(none)' eq $epoch); + + return ($name, $epoch, $version, $release, $arch); +} + +# Inspect the guest to work out what kernel package is in use +# Returns ($kernel_pkg, $kernel_arch) +sub _discover_kernel +{ + my ($g, $root, $grub) = @_; + + # Get a current bootable kernel, preferrably the default + my $kernel_pkg; + my $kernel_arch; + my $kernel_ver; + + foreach my $path ($grub->list_kernels()) { + my $kernel = _inspect_linux_kernel($g, $path); + + # Check its architecture is known + $kernel_arch = $kernel->{arch}; + next unless (defined($kernel_arch)); + + # Get the kernel package name + $kernel_pkg = $kernel->{package}; + + # Get the kernel package version + $kernel_ver = $kernel->{version}; + + last; + } + + # Default to 'kernel' if package name wasn't discovered + $kernel_pkg = "kernel" if (!defined($kernel_pkg)); + + # Default the kernel architecture to the userspace architecture if it wasn't + # directly detected + $kernel_arch = $g->inspect_get_arch($root) unless defined($kernel_arch); + + # The supported 32 bit kernel architecture is i586. + $kernel_arch = 'i586' if ('i386' eq $kernel_arch); + + return ($kernel_pkg, $kernel_arch, $kernel_ver); +} + +sub _get_replacement_kernel_name +{ + my ($g, $root, $arch, $meta) = @_; + + # Make an informed choice about a replacement kernel for distros we know + # about + + # SLES 11 + if (_is_sles_family($g, $root) && + $g->inspect_get_major_version($root) eq '11') { + if ($arch eq 'i586') { + # If the guest has > 10G RAM, give it a pae kernel + if ($meta->{memory} > 10 * 1024 * 1024 * 1024) { + return 'kernel-pae'; + } + + else { + return 'kernel-default'; + } + } + + # There's only 1 kernel package on SLES 11 x86_64 + else { + return 'kernel-default'; + } + } + + # SLES 10 + elsif (_is_sles_family($g, $root) && + $g->inspect_get_major_version($root) eq '10') { + if ($arch eq 'i586') { + # If the guest has > 10G RAM, give it a bigsmp kernel + if ($meta->{memory} > 10 * 1024 * 1024 * 1024) { + return 'kernel-bigsmp'; + } + + # SMP kernel for guests with >1 CPU + elsif ($meta->{cpus} > 1) { + return 'kernel-smp'; + } + + else { + return 'kernel-default'; + } + } + + else { + if ($meta->{cpus} > 1) { + return 'kernel-smp'; + } + + else { + return 'kernel-default'; + } + } + } + + # For other distros, be conservative and just return 'kernel' + return 'kernel'; +} + +sub _get_installed +{ + my ($name, $g) = @_; + + my $rpmcmd = ['rpm', '-q', '--qf', '%{EPOCH} %{VERSION} %{RELEASE}\n', + $name]; + my @output = eval { $g->command_lines($rpmcmd) }; + if ($@) { + # RPM command returned non-zero. This might be because there was + # actually an error, or might just be because the package isn't + # installed. + # Unfortunately, rpm sent its error to stdout instead of stderr, and + # command_lines only gives us stderr in $@. To get round this we'll + # execute the command again, sending all output to stdout and ignoring + # failure. If the output contains 'not installed', we'll assume it's not + # a real error. + my $error = $g->sh("LANG=C '".join("' '", @$rpmcmd)."' 2>&1 ||:"); + + return () if ($error =~ /not installed/); + + v2vdie __x('Error running {command}: {error}', + command => join(' ', @$rpmcmd), error => $error); + } + + my @installed = (); + foreach my $installed (@output) { + $installed =~ /^(\S+)\s+(\S+)\s+(\S+)$/ + or die("Unexpected return from rpm command: $installed"); + my ($epoch, $version, $release) = ($1, $2, $3); + + # Ensure epoch is always numeric + $epoch = 0 if('(none)' eq $epoch); + + push(@installed, [$epoch, $version, $release]); + } + + return sort { _evr_cmp($a->[0], $a->[1], $a->[2], + $b->[0], $b->[1], $b->[2]) } @installed; +} + +sub _parse_evr +{ + my ($evr) = @_; + + $evr =~ /^(?:(\d+):)?([^-]+)(?:-(\S+))?$/ or die(); + + my $epoch = $1; + my $version = $2; + my $release = $3; + + return ($epoch, $version, $release); +} + +sub _evr_cmp +{ + my ($e1, $v1, $r1, $e2, $v2, $r2) = @_; + + # Treat epoch as zero if undefined + $e1 ||= 0; + $e2 ||= 0; + + return -1 if ($e1 < $e2); + return 1 if ($e1 > $e2); + + # version must be defined + my $cmp = _rpmvercmp($v1, $v2); + return $cmp if ($cmp != 0); + + # Treat release as the empty string if undefined + $r1 ||= ""; + $r2 ||= ""; + + return _rpmvercmp($r1, $r2); +} + +# An implementation of rpmvercmp. Compares two rpm version/release numbers and +# returns -1/0/1 as appropriate. +# Note that this is intended to have the exact same behaviour as the real +# rpmvercmp, not be in any way sane. +sub _rpmvercmp +{ + my ($a, $b) = @_; + + # Simple equality test + return 0 if($a eq $b); + + my @aparts; + my @bparts; + + # [t]ransformation + # [s]tring + # [l]ist + foreach my $t ([$a => \@aparts], + [$b => \@bparts]) { + my $s = $t->[0]; + my $l = $t->[1]; + + # We split not only on non-alphanumeric characters, but also on the + # boundary of digits and letters. This corresponds to the behaviour of + # rpmvercmp because it does 2 types of iteration over a string. The + # first iteration skips non-alphanumeric characters. The second skips + # over either digits or letters only, according to the first character + # of $a. + @$l = split(/(?<=[[:digit:]])(?=[[:alpha:]]) | # digit<>alpha + (?<=[[:alpha:]])(?=[[:digit:]]) | # alpha<>digit + [^[:alnum:]]+ # sequence of non-alphanumeric + /x, $s); + } + + # Find the minimun of the number of parts of $a and $b + my $parts = scalar(@aparts) < scalar(@bparts) ? + scalar(@aparts) : scalar(@bparts); + + for(my $i = 0; $i < $parts; $i++) { + my $acmp = $aparts[$i]; + my $bcmp = $bparts[$i]; + + # Return 1 if $a is numeric and $b is not + if($acmp =~ /^[[:digit:]]/) { + return 1 if($bcmp !~ /^[[:digit:]]/); + + # Drop leading zeroes + $acmp =~ /^0*(.*)$/; + $acmp = $1; + $bcmp =~ /^0*(.*)$/; + $bcmp = $1; + + # We do a string comparison of 2 numbers later. At this stage, if + # they're of differing lengths, one is larger. + return 1 if(length($acmp) > length($bcmp)); + return -1 if(length($bcmp) > length($acmp)); + } + + # Return -1 if $a is letters and $b is not + else { + return -1 if($bcmp !~ /^[[:alpha:]]/); + } + + # Return only if they differ + return -1 if($acmp lt $bcmp); + return 1 if($acmp gt $bcmp); + } + + # We got here because all the parts compared so far have been equal, and one + # or both have run out of parts. + + # Whichever has the greatest number of parts is the largest + return -1 if(scalar(@aparts) < scalar(@bparts)); + return 1 if(scalar(@aparts) > scalar(@bparts)); + + # We can get here if the 2 strings differ only in non-alphanumeric + # separators. + return 0; +} + +sub _remap_block_devices +{ + my ($meta, $virtio, $g, $root, $grub) = @_; + + my @devices = map { $_->{device} } @{$meta->{disks}}; + @devices = sort { scsi_first_cmp($a, $b) } @devices; + + # @devices contains an ordered list of libvirt device names. Because + # libvirt uses a similar naming scheme to Linux, these will mostly be the + # same names as used by the guest. They are ordered as they were passed to + # libguestfs, which means their device name in the appliance can be + # inferred. + + # If the guest is using libata, IDE drives could have different names in the + # guest from their libvirt device names. + + # Modern SUSE distros use libata, and IDE devices are presented as sdX + my $libata = 1; + + # Disable libata for SUSE7/8, although this platform is unsupported + if (_is_sles_family($g, $root)) { + my $major_version = $g->inspect_get_major_version($root); + if ($major_version eq '7' || + $major_version eq '8') + { + $libata = 0; + } + } + + # Create a hash to track any hd->sd conversions, as any references to + # hd devices (in fstab, menu.lst, etc) will need to be converted later. + my %idemap; + + if ($libata) { + # If there are any IDE devices, the guest will have named these sdX + # after any SCSI devices. i.e. If we have disks hda, hdb, sda and sdb, + # these will have been presented to the guest as sdc, sdd, sda and sdb + # respectively. + # + # N.B. I think the IDE/SCSI ordering situation may be somewhat more + # complicated than previously thought. This code originally put IDE + # drives first, but this hasn't worked for an OVA file. Given that + # this is a weird and somewhat unlikely situation I'm going with SCSI + # first until we have a more comprehensive solution. + + my $idedev; + my @newdevices; + my $suffix = 'a'; + foreach my $device (@devices) { + if ($device =~ /(?:h|s)d[a-z]+/) { + $idedev = $device; + $device = 'sd'.$suffix++; + $idemap{$device} = $idedev if ($device ne $idedev); + } + push(@newdevices, $device); + } + @devices = @newdevices; + } + + # We now assume that @devices contains an ordered list of device names, as + # used by the guest. Create a map of old guest device names to new guest + # device names. + my %map; + + # Everything will be converted to either vdX, sdX or hdX + my $prefix; + if ($virtio) { + $prefix = 'vd'; + } elsif ($libata) { + $prefix = 'sd'; + } else { + $prefix = 'hd' + } + + my $letter = 'a'; + foreach my $device (@devices) { + my $mapped = $prefix.$letter; + + + # If a Xen guest has non-PV devices, Xen also simultaneously presents + # these as xvd devices. i.e. hdX and xvdX both exist and are the same + # device. + # This mapping is also useful for P2V conversion of Citrix Xenserver + # guests done in HVM mode. Disks are detected as sdX, although the guest + # uses xvdX natively. + if ($device =~ /^(?:h|s)d([a-z]+)/) { + $map{'xvd'.$1} = $mapped; + } + $map{$device} = $mapped; + # Also add a mapping for previously saved hd->sd conversions + #if (defined($idemap{$device}) && (($idemap{$device}) ne "")){ + if (defined($idemap{$device})){ + my $idedevice; + $idedevice = $idemap{$device}; + $map{$idedevice} = $mapped; + } + + $letter++; + } + + eval { + # Update bare device references in fstab and grub's menu.lst and device.map + foreach my $spec ($g->aug_match('/files/etc/fstab/*/spec'), + $g->aug_match("/files$grub->{grub_conf}/*/kernel/root"), + $g->aug_match("/files$grub->{grub_conf}/*/kernel/resume"), + $g->aug_match('/files/boot/grub/device.map/*'. + '[label() != "#comment"]')) + { + my $device = $g->aug_get($spec); + + # Match device names and partition numbers + my $name; my $part; + foreach my $r (qr{^/dev/(cciss/c\d+d\d+)(?:p(\d+))?$}, + qr{^/dev/([a-z]+)(\d*)?$}) { + if ($device =~ $r) { + $name = $1; + $part = $2; + last; + } + } + + # Ignore this entry if it isn't a device name + next unless defined($name); + + # Ignore md and floppy devices, which don't need to be mapped + next if $name =~ /(md|fd)/; + + # Ignore this entry if it refers to a device we don't know anything + # about. The user will have to fix this post-conversion. + if (!exists($map{$name})) { + my $warned = 0; + for my $file ('/etc/fstab', '/boot/grub/device.map') { + if ($spec =~ m{^/files$file}) { + logmsg WARN, __x('{file} references unknown device '. + '{device}. This entry must be '. + 'manually fixed after conversion.', + file => $file, device => $device); + $warned = 1; + } + } + + # Shouldn't happen. Not fatal if it does, though. + if (!$warned) { + logmsg WARN, 'Please report this warning as a bug. '. + "augeas path $spec refers to unknown device ". + "$device. This entry must be manually fixed ". + 'after conversion.' + } + + next; + } + + my $mapped = '/dev/'.$map{$name}; + $mapped .= $part if defined($part); + $g->aug_set($spec, $mapped); + } + + $g->aug_save(); + }; + + augeas_error($g, $@) if ($@); + + # Delete cached (and now out of date) blkid info if it exists + foreach my $blkidtab ('/etc/blkid/blkid.tab', '/etc/blkid.tab') { + $g->rm($blkidtab) if ($g->exists($blkidtab)); + } +} + +sub _prepare_bootable +{ + my ($g, $root, $grub, $version, @modules) = @_; + + my $path = "/boot/vmlinuz-$version"; + $grub->write($path); + my $grub_initrd = $grub->get_initrd($path); + + # Backup the original initrd + $g->mv($grub_initrd, "$grub_initrd.pre-v2v") + if $g->exists($grub_initrd); + + # dracut does not exist in SUSE, but might in the future + if ($g->exists('/sbin/dracut')) { + $g->command(['/sbin/dracut', '--add-drivers', join(" ", @modules), + $grub_initrd, $version]); + } + + elsif ($g->exists('/sbin/mkinitrd')) { + # Create a new initrd which probes the required kernel modules + my @module_args = (); + push(@module_args, "-m \""); + foreach my $module (@modules) { + push(@module_args, "$module "); + } + push(@module_args, "\""); + + my @env; + + $g->sh(join(' ', @env).'/sbin/mkinitrd '.join(' ', @module_args). + " -i $grub_initrd -k /boot/vmlinuz-$version"); + } + + else { + v2vdie __('Didn\'t find mkinitrd or dracut. Unable to update initrd.'); + } + +} + +# Return 1 if the guest supports ACPI, 0 otherwise +sub _supports_acpi +{ + my ($g, $root, $arch) = @_; + + # Blacklist configurations which are known to fail + # possibly SLES 8, x86_64 (Similar to RHEL 3?) + if (_is_sles_family($g, $root) && $g->inspect_get_major_version($root) == 8 && + $arch eq 'x86_64') { + return 0; + } + + return 1; +} + +sub _supports_virtio +{ + my ($kernel, $g) = @_; + + my %checklist = ( + "virtio_net" => 0, + "virtio_blk" => 0 + ); + + # Search the installed kernel's modules for the virtio drivers + foreach my $module ($g->find("/lib/modules/$kernel")) { + foreach my $driver (keys(%checklist)) { + if($module =~ m{/$driver\.(?:o|ko)$}) { + $checklist{$driver} = 1; + } + } + } + + # Check we've got all the drivers in the checklist + foreach my $driver (keys(%checklist)) { + if(!$checklist{$driver}) { + return 0; + } + } + + return 1; +} + +# The next two functions create a temporary workaround to bnc#836521. The actual +# fix is in a new perl-Bootloader, which will likely not be on most guests. +sub _modify_perlBootloader +{ + my ($g) = @_; + my $module = $g->sh("rpm -ql perl-Bootloader | grep GRUB.pm"); + chomp($module); + my $module_bak = "$module.v2vtmp"; + + if ($g->grep('/dev/(?:vx', "$module")) { + $g->mv($module, $module_bak); + $g->sh("/usr/bin/sed -e's/vx/xv/' $module_bak > $module"); + + return 1; + } + + return 0; +} + +sub _restore_perlBootloader +{ + my ($g) = @_; + my $module = $g->sh("rpm -ql perl-Bootloader | grep GRUB.pm"); + chomp($module); + my $module_bak = "$module.v2vtmp"; + + if ($g->exists($module_bak)) { + $g->mv($module_bak, $module); + } +} + +=back + +=head1 COPYRIGHT + +Copyright (c) 2013 SUSE LINUX Products GmbH, Nuernberg, Germany. + +=head1 LICENSE + +Please see the file COPYING.LIB for the full license. + +=head1 SEE ALSO + +L<Sys::VirtConvert::Converter(3pm)>, +L<Sys::VirtConvert(3pm)>, +L<virt-v2v(1)>, +L<http://libguestfs.org/>. + +=cut + +1; diff -Nurp a/MANIFEST b/MANIFEST --- a/MANIFEST 2013-09-14 12:53:18.321119962 -0600 +++ b/MANIFEST 2013-09-14 12:53:34.471271204 -0600 @@ -17,6 +17,7 @@ lib/Sys/VirtConvert/Connection/VMwareOVA lib/Sys/VirtConvert/Connection/Volume.pm lib/Sys/VirtConvert/Converter.pm lib/Sys/VirtConvert/Converter/RedHat.pm +lib/Sys/VirtConvert/Converter/SUSE.pm lib/Sys/VirtConvert/Converter/Windows.pm lib/Sys/VirtConvert/ExecHelper.pm lib/Sys/VirtConvert/GuestfsHandle.pm
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor