Daniel, thanks for the comments .. most of them are integrated, but I
have a problem with changing MAC addresses, because the domain xml is
regenerated every time, so I cannot rely on the interface name being
"eth0" as fedora during boot creates a new ethX for each new MAC
address .. any good advice?
On Thu, 2010-04-15 at 15:27 +0100, Daniel P. Berrange wrote:
On Thu, Apr 15, 2010 at 02:35:41PM +0200, Gerhard Stenzel wrote:
> The following patch mainly adds a set of test case to verify that
> several spoofing attacks are prevented by the nwfilter subsystem.
>
> In order to have a well defined test machine, the patch also includes
> test scripts to network install a virtual disk from scratch, to boot the
> virtual test machine prior to running the actual test scripts and to
> shut it down afterwards.
>
> While I have tried to remove as much dependency on my local setup as
> possible there is still some left, so I am currently more interested in
> feedback about the general approach, not necessarily actual inclusion
> into the libvirt-TCK git.
Your actual test cases look good, so I'll just put comments about
the setup/teardown stuff inline.
ok .. sounds good
>
> For example, I am currently trying to find a suitable location for the
> kickstart file, and also a suitable place for the common_functions.pl.
The 'lib' directory contains modules which provide common functions &
code for the test scripts. In this case I'd suggest creating a file
lib/Sys/Virt/TCK/NetworkHelpers.pm (use Sys::Virt::TCK::NetworkHelpers)
ok .. done
> Index: libvirt-tck/scripts/network/README
> ===================================================================
> --- /dev/null
> +++ libvirt-tck/scripts/network/README
> @@ -0,0 +1,14 @@
> +
> +Test cases:
> +
> +000-install-image.t creates and install a 2GB fedora virtual disk via
> kickstart file from the network
> +001-boot-image.t defines and boots a VM which uses the fedora virtual
> disk
> +100-ping-still-working.t verifies the VM is pingable
> +210-no-mac-spoofing.t verifies mac spoofing is prevented
> +220-no-ip-spoofing.t verifies ip spoofing is prevented
> +230-no-mac-broadcast.t verifies mac broadcasting is prevented
> +240-no-arp-spoofing.t verifies arp spoofing is prevented
> +999-shutdown-image.t shuts the VM down
One thing about the TCK test cases is that each one should be
self-contained, doing all setup & teardown it requires, not
reliant on any of the other tests cases or ordering of tests.
So instead of having the 000-install-image.t & 0001-boot-image.t
scripts that do setup, you'd want to create some library code
that can be used to install + boot the guest, and just call that
from each test case.
I am currently trying this .. however, what I am struggling with is that
the MAC address is different for every boot.
> Index: libvirt-tck/scripts/network/000-install-image.t
> ===================================================================
> --- /dev/null
> +++ libvirt-tck/scripts/network/000-install-image.t
> @@ -0,0 +1,181 @@
> +# -*- perl -*-
> +#
> +# Copyright (C) 2010 IBM Corp.
> +#
> +# This program is free software; You can redistribute it and/or modify
> +# it under the GNU General Public License as published by the Free
> +# Software Foundation; either version 2, or (at your option) any
> +# later version
> +#
> +# The file "LICENSE" distributed along with this file provides full
> +# details of the terms and conditions
> +#
> +
> +=pod
> +
> +=head1 NAME
> +
> +network/000-install-image.t - install network test image
> +
> +=head1 DESCRIPTION
> +
> +The test case creates and install a 2GB fedora virtual
> +disk via kickstart file from the network.
> +
> +=cut
> +
> +use strict;
> +use warnings;
> +
> +use Test::More tests => 1;
> +
> +use Sys::Virt::TCK;
> +
> +my $tck = Sys::Virt::TCK->new();
> +my $conn = eval { $tck->setup(); };
> +BAIL_OUT "failed to setup test harness: $@" if $@;
> +END { $tck->cleanup if $tck; }
> +
> +# variables which may need to be adapted
> +my $domain_name ="f12nwtest";
> +my $disk_name = "/var/lib/libvirt/images/${domain_name}.img";
> +my $disk_size = "2147483648";
You want to avoid hardcoding any disk paths in the test cases.
There's a convenient helper function if you want to create a
scratch disk for a guest
my $path = $tck->create_sparse_disk("nwfilter", "root.img",
2147483648);
create_sparse_disk does not work for me, anaconda does not recognize
installable disks .. anyway I got rid of the hardcoded names.
> +my $kickstart_file ="http://192.168.122.1/ks.cfg";
> +my $cmdline = "ip=dhcp gateway=192.168.122.1 ks=${kickstart_file}";
In the 'conf/default.cfg' file we list the kernel+initrd images for
various distros. We could likely also have an optional kickstart
file listed there, so we move this configuration info out of the test
cases.
sounds good .. but not done yet
> +# see if the domain already exits
> +my $already_defined = 0;
> +diag "searching if ${domain_name} is already defined";
> +my $nnames = $conn->num_of_defined_domains();
> +my @names = $conn->list_defined_domain_names($nnames);
> +foreach (@names){
> + if (/${domain_name}/) {
> + print "$_ already exists, no need to redefine\n";
> + $already_defined = 1;
> + }
> +}
> +diag $already_defined;
All guests created by the TCK test cases should have a name prefix
of 'tck' to avoid clashing with existing guests. Each test case
should destroy + undefine all guests it creates. The TCK setup
code will validate that there are no guests having a name starting
with a 'tck' prefix & optionally destroy them. So each of your
test cases should assume the guest doesn't already exist + clean
it up before exiting.
ok .. working on that one
NB, the disk files are not deleted between runs, so you will
still avoid the overhead of installation.
I'm talking with the libguestfs folks to see if we can re-use
their guest appliance + daemon so we can avoid the actual install
+ ssh stuff at some point in the future.
> +# check for installation disk and build it if not exists
> +my $already_installed = 0;
> +my $pool = $conn->get_storage_pool_by_name("default");
> +my $nnames = $pool->num_of_storage_volumes();
> +my @volNames = $pool->list_storage_vol_names($nnames);
> +foreach (@volNames){
> + if (/${domain_name}/) {
> + print "$_ already exists, no need to install\n";
> + $already_installed = 1;
> + }
> +}
> +
> +my $volumexml = "<volume>".
> +" <name>${domain_name}.img</name>".
> +" <key>${disk_name}</key>".
> +" <source>".
> +" </source>".
> +" <capacity>${disk_size}</capacity>".
> +" <allocation>4096</allocation>".
> +" <target>".
> +" <path>${disk_name}</path>".
> +" <format type='raw'/>".
> +" <permissions>".
> +" <mode>0644</mode>".
> +" <owner>0</owner>".
> +" <group>0</group>".
> +" </permissions>".
> +" </target>".
> +"</volume>";
If using the create_sparse_disk helper I mentioned, you can avoid this
chunk of code. Just do a check to see if $path exists or not.
ok . solved differently with generic_volume()
> +# prepare image
> +if ($already_installed == 0) {
> + diag "Creating ${disk_name}";
> + diag $volumexml;
> + my $vol = $pool->create_volume($volumexml)
> +# system("qemu-img create ${disk_name} ${disk_size}");
> +}
> +
> +my $topxml = " <name>${domain_name}</name>".
> +" <memory>524288</memory>".
> +" <currentMemory>524288</currentMemory>".
> +" <vcpu>1</vcpu>";
> +
> +my $osxml = " <os>".
> +" <type arch='x86_64'
machine='fedora-13'>hvm</type>".
> +"
<kernel>/var/cache/libvirt-tck/os-i686-hvm/vmlinuz</kernel>".
> +"
<initrd>/var/cache/libvirt-tck/os-i686-hvm/initrd</initrd>".
> +" <cmdline>${cmdline}</cmdline>".
> +" <boot dev='hd'/>".
> +" </os>";
> +
> +my $bottomxml = " <features>".
> +" <acpi/>".
> +" <apic/>".
> +" </features>".
> +" <clock offset='utc'/>".
> +" <on_poweroff>destroy</on_poweroff>".
> +" <on_reboot>restart</on_reboot>".
> +" <on_crash>restart</on_crash>".
> +" <devices>".
> +" <emulator>/usr/bin/qemu-kvm</emulator>".
> +" <disk type='file' device='disk'>".
> +" <driver name='qemu' type='raw'/>".
> +" <source file='${disk_name}'/>".
> +" <target dev='hda' bus='ide'/>".
> +" </disk>".
> +" <controller type='ide' index='0'>".
> +" </controller>".
> +" <interface type='network'>".
> +" <source network='default'/>".
> +" <target dev='vnet0'/>".
> +" <model type='virtio'/>".
> +" </interface>".
> +" <serial type='pty'>".
> +" <target port='0'/>".
> +" </serial>".
> +" <console type='pty'>".
> +" <target port='0'/>".
> +" </console>".
> +" <graphics type='vnc' port='-1' autoport='yes'
listen='127.0.0.1'
> keymap='de'/>".
> +" <video>".
> +" <model type='cirrus' vram='9216'
heads='1'/>".
> +" </video>".
> +" </devices>";
> +
> +my $xml = "<domain type='kvm'>" .
> +$topxml.
> +$osxml.
> +$bottomxml.
> +"</domain>";
There are a couple of helpers for building XML in a portable manner.
In the Sys::Virt::TCK class there's a method 'generic_domain' which
will find you a bare minimum VM matching the best kernel / arch
found in the TCK config file. It returns an instance of the the
class Sys::Virt::TCK::DomainBuilder which lets you then add
extra XML to the guest.
eg, so you could do
my $guest = $tck->generic_domain();
$guest->boot_cmdline($cmdline);
The 'boot_cmdline' method doesn't exist yet, but feel frree to extend
the shared helper modules as needed. Its much nicer to use them than
to build up XML manually.
ok .. done
> +
> +diag $xml;
> +diag "Defining an inactive domain config";
> +my $dom;
> +
> +# no need to start if already installed
> +if (($already_installed == 0) && ($already_defined == 0)) {
> + ok_domain(sub { $dom = $conn->define_domain($xml) }, "defined
> persistent domain config");
> + $xml = $dom->get_xml_description;
> + diag $xml;
> + diag "Starting inactive domain config";
> + $dom->create;
> +
> + # wait for completion of installation
> + diag "wait for installation to finish .. ";
> + while($dom->is_active()) {
> + sleep(10);
> + diag ".. to view progress connect to virtual machine ${domain_name} ..
> ";
> + }
> + sleep(10);
> + diag " .. done";
> + # cleanup
> + $dom->undefine;
> +} else {
> + ok_domain { $dom = $conn->get_domain_by_name($domain_name) } "the
> existing domain object";
> +}
> +
> +
> +
> +
> +
> Index: libvirt-tck/scripts/network/001-boot-image.t
> ===================================================================
> --- /dev/null
> +++ libvirt-tck/scripts/network/001-boot-image.t
> @@ -0,0 +1,133 @@
> +# -*- perl -*-
> +#
> +# Copyright (C) 2010 IBM Corp.
> +#
> +# This program is free software; You can redistribute it and/or modify
> +# it under the GNU General Public License as published by the Free
> +# Software Foundation; either version 2, or (at your option) any
> +# later version
> +#
> +# The file "LICENSE" distributed along with this file provides full
> +# details of the terms and conditions
> +#
> +
> +=pod
> +
> +=head1 NAME
> +
> +network/001-boot-image.t - boot installed test image
> +
> +=head1 DESCRIPTION
> +
> +The test case defines and boots a VM which uses the
> +fedora virtual disk create ny 000-install-image
> +
> +=cut
> +
> +use strict;
> +use warnings;
> +
> +use Test::More tests => 2;
> +
> +use Sys::Virt::TCK;
> +use XML::LibXML;
> +
> +require 'scripts/network/common_functions.pl';
> +
> +my $tck = Sys::Virt::TCK->new();
> +my $conn = eval { $tck->setup(); };
> +BAIL_OUT "failed to setup test harness: $@" if $@;
> +END { $tck->cleanup if $tck; }
> +
> +my $already_defined = 0;
> +my $domain_name ="f12nwtest";
> +
> +# see if the domain already exits
> +diag "searching if ${domain_name} is already defined";
> +my $nnames = $conn->num_of_defined_domains();
> +my @names = $conn->list_defined_domain_names($nnames);
> +foreach (@names){
> + if (/${domain_name}/) {
> + print "$_ already exists, no need to redefine\n";
> + $already_defined = 1;
> + }
> +}
> +diag $already_defined;
> +
> +my $dom;
> +my $xml;
> +
> +if ($already_defined == 1) {
> + ok_domain { $dom = $conn->get_domain_by_name($domain_name) } "the
> existing domain object";
> +} else {
> + my $topxml = " <name>${domain_name}</name>".
> + " <memory>524288</memory>".
> + " <currentMemory>524288</currentMemory>".
> + " <vcpu>1</vcpu>";
> +
> + my $osxml = " <os>".
> + " <type arch='x86_64'
machine='fedora-13'>hvm</type>".
> + " <boot dev='hd'/>".
> + " </os>";
> +
> + my $bottomxml = " <features>".
> + " <acpi/>".
> + " <apic/>".
> + " </features>".
> + " <clock offset='utc'/>".
> + " <on_poweroff>destroy</on_poweroff>".
> + " <on_reboot>restart</on_reboot>".
> + " <on_crash>restart</on_crash>".
> + " <devices>".
> + " <emulator>/usr/bin/qemu-kvm</emulator>".
> + " <disk type='file' device='disk'>".
> + " <driver name='qemu' type='raw'/>".
> + " <source
file='/var/lib/libvirt/images/${domain_name}.img'/>".
> + " <target dev='hda' bus='ide'/>".
> + " </disk>".
> + " <controller type='ide' index='0'>".
> + " </controller>".
> + " <interface type='network'>".
> + " <source network='default'/>".
> + " <filterref filter='no-spoofing'/>".
> + " <target dev='vnet0'/>".
> + " <model type='virtio'/>".
> + " </interface>".
> + " <serial type='pty'>".
> + " <target port='0'/>".
> + " </serial>".
> + " <console type='pty'>".
> + " <target port='0'/>".
> + " </console>".
> + " <graphics type='vnc' port='-1' autoport='yes'
listen='127.0.0.1'
> keymap='de'/>".
> + " <video>".
> + " <model type='cirrus' vram='9216'
heads='1'/>".
> + " </video>".
> + " </devices>";
> +
> + $xml = "<domain type='kvm'>" .
> + $topxml.
> + $osxml.
> + $bottomxml.
> + "</domain>";
Again, I think you'd be better using the '$tck->generic_domain' method,
and adding support to Sys::Virt::TCK::DomainBuilder for adding the
'<filterref filter='no-spoofing'/>' bit of the XML.
ok .. done
> Index: libvirt-tck/scripts/network/100-ping-still-working.t
> ===================================================================
> --- /dev/null
> +++ libvirt-tck/scripts/network/100-ping-still-working.t
> @@ -0,0 +1,71 @@
> +# -*- perl -*-
> +#
> +# Copyright (C) 2010 IBM Corp.
> +#
> +# This program is free software; You can redistribute it and/or modify
> +# it under the GNU General Public License as published by the Free
> +# Software Foundation; either version 2, or (at your option) any
> +# later version
> +#
> +# The file "LICENSE" distributed along with this file provides full
> +# details of the terms and conditions
> +#
> +
> +=pod
> +
> +=head1 NAME
> +
> +network/100-ping-still-working.t - verify machines can be pinged from
> host
> +
> +=head1 DESCRIPTION
> +
> +The test case validates that it is possible to ping a guest machine
> from
> +the host.
> +
> +=cut
> +
> +use strict;
> +use warnings;
> +
> +use Test::More tests => 4;
> +
> +use Sys::Virt::TCK;
> +use Test::Exception;
> +use Net::SSH::Perl;
> +use XML::LibXML;
> +
> +require 'scripts/network/common_functions.pl';
> +
> +my $tck = Sys::Virt::TCK->new();
> +my $conn = eval { $tck->setup(); };
> +BAIL_OUT "failed to setup test harness: $@" if $@;
> +END {
> + $tck->cleanup if $tck;
> +}
> +
> +# create first domain and start it
> +diag "Trying domain lookup by name";
> +my $dom1;
> +my $domain_name ="f12nwtest";
This is where you'd want to start the guest domain
> +
> +ok_domain { $dom1 = $conn->get_domain_by_name($domain_name) } "the
> running domain object";
> +my $xml = $dom1->get_xml_description;
> +diag $xml;
> +ok($dom1->get_id() > 0, "running domain has an ID > 0");
> +my $mac1 = get_macaddress($xml);
> +diag $mac1;
> +my $guestip1 = get_ip_from_leases($mac1);
> +diag "ip is $guestip1";
> +
> +# check ebtables entry
> +my $ebtable1 = `/sbin/ebtables -L;/sbin/ebtables -t nat -L`;
> +diag $ebtable1;
> +# fixme to include mac adress
> +ok($ebtable1 =~ "vnet0", "check ebtables entry");
> +
> +# ping guest1
> +my $ping1 = `ping -c 10 $guestip1`;
> +diag $ping1;
> +ok($ping1 =~ "10 received", "ping $guestip1 test");
And run 'destroy' here. If you use a transient guest when
initially booting ($conn->create_domain($xml)) then you'll
not have to worry about the undefine step.
> +
> +sub get_macaddress {
> + my $xmldesc = shift;
> +
> + my $mac;
> + my $parser = XML::LibXML->new();
> +
> + my $doc = $parser->parse_string($xmldesc);
> +
> + my $rootel = $doc -> getDocumentElement();
> +
> + my @devices = $rootel->getChildrenByTagName("devices");
> + foreach my $device(@devices) {
> + my @interfaces = $device->getChildrenByTagName("interface");
> + foreach my $interface(@interfaces) {
> + my @targets = $interface->getChildrenByTagName("mac");
> + foreach my $target(@targets) {
> + $mac = $target->getAttribute("address");
> + }
> + }
> + }
> + utf8::decode($mac);
> + return $mac;
> +}
You can simplify this using XPath - the $tck object provides a
convenient method for performing an XPath query on a $dom object.
eg that whole method can just be
my $resultt = xpath($dom, "/domain/devices/interface/mac/\@address")
my @macaddrs = map { $_->getNodeValue} $result->get_nodelist;
ok .. but not done yet
Daniel
--
Best regards,
Gerhard Stenzel,
-----------------------------------------------------------------------------------------------------------------------------------
IBM Deutschland Research & Development GmbH
Vorsitzender des Aufsichtsrats: Martin Jetter
Geschäftsführung: Dirk Wittkopp
Sitz der Gesellschaft: Böblingen
Registergericht: Amtsgericht Stuttgart, HRB 243294