diff --git a/lib/Ravada.pm b/lib/Ravada.pm index 54c6b8b57..35074405f 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -2952,6 +2952,7 @@ sub _upgrade_tables { $self->_upgrade_table('domains','auto_compact','int default NULL'); $self->_upgrade_table('domains','date_status_change' , 'datetime'); $self->_upgrade_table('domains','show_clones' , 'int not null default 1'); + $self->_upgrade_table('domains','config_no_hd' , 'text'); $self->_upgrade_table('domains_network','allowed','int not null default 1'); @@ -5647,7 +5648,7 @@ sub _cmd_refresh_machine($self, $request) { ,timeout => 60, retry => 10) if $is_active && $domain->ip && $domain->list_ports; - $domain->_unlock_host_devices() if !$is_active; + $domain->_dettach_host_devices() if !$is_active; } sub _cmd_refresh_machine_ports($self, $request) { diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 670b6f57b..57266fb8b 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -300,6 +300,7 @@ sub _vm_disconnect { sub _around_start($orig, $self, @arg) { $self->_post_hibernate() if $self->is_hibernated && !$self->_data('post_hibernated'); + $self->_dettach_host_devices() if !$self->is_active; $self->_start_preconditions(@arg); @@ -355,7 +356,7 @@ sub _around_start($orig, $self, @arg) { $arg{listen_ip} = $display_ip; } if ($enable_host_devices) { - $self->_add_host_devices(@arg); + $self->_attach_host_devices(@arg); } else { $self->_dettach_host_devices(); } @@ -913,7 +914,7 @@ sub _pre_prepare_base($self, $user, $request = undef ) { sleep 1; } } - $self->_unlock_host_devices() if !$self->is_active; + $self->_dettach_host_devices() if !$self->is_active; # $self->_post_remove_base(); if (!$self->is_local) { @@ -3126,7 +3127,7 @@ sub _post_shutdown { my $is_active = $self->is_active; if ( $self->is_known && !$self->is_volatile && !$is_active ) { - $self->_unlock_host_devices(); + $self->_dettach_host_devices(); if ($self->is_hibernated) { $self->_data(status => 'hibernated'); } else { @@ -7125,9 +7126,7 @@ sub add_host_device($self, $host_device) { sub remove_host_device($self, $host_device) { confess if !ref($host_device); - confess if $self->readonly; - $self->_dettach_host_device($host_device); my $id_hd = $host_device->id; @@ -7185,8 +7184,8 @@ sub list_host_devices_attached($self) { $sth_locked->execute($self->id, $row->{name}); my ($is_locked) = $sth_locked->fetchrow(); $row->{is_locked} = 1 if $is_locked; + push @found,($row); } - push @found,($row); } return @found; @@ -7195,6 +7194,21 @@ sub list_host_devices_attached($self) { # adds host devices to domain instance # usually run right before startup sub _add_host_devices($self, @args) { + $self->_attach_host_devices(@args); +} + +sub _backup_config_no_hd($self) { + $self->_dettach_host_devices(); + $self->_data('config_no_hd' => $self->get_config_txt); +} + +sub _restore_config_no_hd($self) { + my $config_no_hd = $self->_data('config_no_hd'); + return if !$config_no_hd; + $self->reload_config($config_no_hd); +} + +sub _attach_host_devices($self, @args) { my @host_devices = $self->list_host_devices(); return if !@host_devices; return if $self->is_active(); @@ -7208,28 +7222,21 @@ sub _add_host_devices($self, @args) { $request = delete $args{request} if exists $args{request}; } + $self->_backup_config_no_hd(); my $doc = $self->get_config(); for my $host_device ( @host_devices ) { + next if !$host_device->enabled(); my $device_configured = $self->_device_already_configured($host_device); + my $device; if ( $device_configured ) { if ( $host_device->enabled() && $host_device->is_device($device_configured) && $self->_lock_host_device($host_device) ) { - next; + $device = $device_configured; } else { $self->_dettach_host_device($host_device, $doc, $device_configured); } } - next if !$host_device->enabled(); - - my ($device) = $host_device->list_available_devices(); - if ( !$device ) { - $device = _refresh_domains_with_locked_devices($host_device); - if (!$device) { - $self->_data(status => 'down'); - $self->_unlock_host_devices(); - die "Error: No available devices in ".$host_device->name."\n"; - } - } + $device = $self->_search_free_device($host_device) if !$device; $self->_lock_host_device($host_device, $device); @@ -7253,12 +7260,25 @@ sub _add_host_devices($self, @args) { } +sub _search_free_device($self, $host_device) { + my ($device) = $host_device->list_available_devices(); + if ( !$device ) { + $device = _refresh_domains_with_locked_devices($host_device); + if (!$device) { + $self->_data(status => 'down'); + $self->_unlock_host_devices(); + die "Error: No available devices in ".$host_device->name."\n"; + } + } + return $device; +} + sub _dettach_host_devices($self) { my @host_devices = $self->list_host_devices(); for my $host_device ( @host_devices ) { $self->_dettach_host_device($host_device); } - $self->remove_host_devices(); + $self->_restore_config_no_hd(); } sub _dettach_host_device($self, $host_device, $doc=$self->get_config @@ -7268,6 +7288,7 @@ sub _dettach_host_device($self, $host_device, $doc=$self->get_config return if !defined $device or !length($device); for my $entry( $host_device->render_template($device) ) { + if ($entry->{type} eq 'node') { $self->remove_config_node($entry->{path}, $entry->{content}, $doc); } elsif ($entry->{type} eq 'unique_node') { @@ -7285,12 +7306,6 @@ sub _dettach_host_device($self, $host_device, $doc=$self->get_config $self->reload_config($doc); $self->_unlock_host_device($device); - my $sth = $$CONNECTOR->dbh->prepare( - "UPDATE host_devices_domain SET name=NULL " - ." WHERE id_domain=? AND id_host_device=?" - ); - $sth->execute($self->id, $host_device->id); - } # marks a host device as being used by a domain @@ -7314,7 +7329,10 @@ sub _lock_host_device($self, $host_device, $device=undef) { my $query = "INSERT INTO host_devices_domain_locked (id_domain,id_vm,name) VALUES(?,?,?)"; my $sth = $$CONNECTOR->dbh->prepare($query); - eval { $sth->execute($self->id,$self->_vm->id, $device) }; + my $id_vm = $self->_data('id_vm'); + $id_vm = $self->_vm->id if !$id_vm; + cluck if !$id_vm; + eval { $sth->execute($self->id,$id_vm, $device) }; if ($@) { warn $@; $self->_data(status => 'shutdown'); @@ -7350,7 +7368,7 @@ sub _check_host_device_already_used($self, $device) { ." WHERE id_vm=? AND name=?" ; my $sth = $$CONNECTOR->dbh->prepare($query); - $sth->execute($self->_vm->id, $device); + $sth->execute($self->_data('id_vm'), $device); my ($id_domain) = $sth->fetchrow; # warn "\n".($id_domain or '')." [".$self->id."] had locked $device\n"; diff --git a/lib/Ravada/Domain/KVM.pm b/lib/Ravada/Domain/KVM.pm index 0a86f5fc3..06fab8788 100644 --- a/lib/Ravada/Domain/KVM.pm +++ b/lib/Ravada/Domain/KVM.pm @@ -3651,6 +3651,9 @@ sub _validate_xml($self, $doc) { } sub reload_config($self, $doc) { + if (!ref($doc)) { + $doc = XML::LibXML->load_xml(string => $doc); + } $self->_validate_xml($doc) if $self->_vm->vm->get_major_version >= 4; my $new_domain; @@ -3693,6 +3696,10 @@ sub get_config($self) { return XML::LibXML->load_xml( string => $self->xml_description()); } +sub get_config_txt($self) { + return $self->xml_description(); +} + sub _change_xml_address($self, $doc, $address, $bus) { my $type_def = $address->getAttribute('type'); return $self->_change_xml_address_ide($doc, $address, 1, 1) if $bus eq 'ide'; @@ -3905,6 +3912,8 @@ sub remove_config_node($self, $path, $content, $doc) { if ( _xml_equal_hostdev($content, $element_s) ) { $parent->removeChild($element); } else { +=pod + my @lines_c = split /\n/,$content; my @lines_e = split /\n/,$element_s; warn $element->getName." ".(scalar(@lines_c)." ".scalar(@lines_e)); @@ -3914,6 +3923,7 @@ sub remove_config_node($self, $path, $content, $doc) { } warn $content; die $self->name if $element->getName eq 'hostdev'; +=cut } } } @@ -3921,6 +3931,11 @@ sub remove_config_node($self, $path, $content, $doc) { sub _xml_equal_hostdev($doc1, $doc2) { return 1 if $doc1 eq $doc2; + + $doc1 =~ s/\n//g; + $doc2 =~ s/\n//g; + return 1 if $doc1 eq $doc2; + my $parser = XML::LibXML->new() or die $!; $doc1 =~ s{(parse_string($doc1); @@ -3966,12 +3981,14 @@ sub add_config_node($self, $path, $content, $doc) { die "Error: I found ".scalar(@parent)." nodes for $dir, expecting 1" unless scalar(@parent)==1; - my $element; + my @element; eval { - ($element) = $parent[0]->findnodes($entry); + (@element) = $parent[0]->findnodes($entry); }; die $@ if $@ && $@ !~ /Undefined namespace prefix/; - return if $element && $element->toString eq $content; + for my $element (@element) { + return if $element && $element->toString eq $content; + } if ($content =~ /_fix_pci_slot(\$content); $parent[0]->appendWellBalancedChunk($content); } +} + +sub set_config_node($self, $path, $content, $doc) { + + my ($dir,$entry) = $path =~ m{(.*)/(.*)}; + confess "Error: missing entry in '$path'" if !$entry; + + my @parent = $doc->findnodes($dir); + if (scalar(@parent)==0) { + @parent = $self->_xml_create_path($doc, $dir); + } + + die "Error: I found ".scalar(@parent)." nodes for $dir, expecting 1" + unless scalar(@parent)==1; + + my $parent = $parent[0]; + + for my $child ($parent->findnodes($entry)) { + $parent->removeChild($child); + } + + for my $curr_entry (@$content) { + $parent->appendWellBalancedChunk($curr_entry); + } } @@ -4096,7 +4137,6 @@ sub remove_host_devices($self) { my ($dev) = $doc->findnodes("/domain/devices"); for my $hostdev ( $dev->findnodes("hostdev") ) { $dev->removeChild($hostdev); - warn $hostdev->toString(); } $self->reload_config($doc); } diff --git a/lib/Ravada/Domain/Void.pm b/lib/Ravada/Domain/Void.pm index 2eae1a04f..6ad11390c 100644 --- a/lib/Ravada/Domain/Void.pm +++ b/lib/Ravada/Domain/Void.pm @@ -13,6 +13,7 @@ use Hash::Util qw(lock_keys lock_hash unlock_hash); use IPC::Run3 qw(run3); use Mojo::JSON qw(decode_json); use Moose; +use Storable qw(dclone); use YAML qw(Load Dump LoadFile DumpFile); use Image::Magick; use MIME::Base64; @@ -1162,12 +1163,34 @@ sub add_config_node($self, $path, $content, $data) { $found = $found->{$item}; } + my $old; if (ref($found) eq 'ARRAY') { + $old = dclone($found); push @$found, ( $content_hash ); } else { my ($item) = keys %$content_hash; + $old = dclone($found->{$item}); $found->{$item} = $content_hash->{$item}; } + return $old; +} + +sub set_config_node($self, $path, $content, $data) { + my $found = $data; + my ($pre,$last) = $path =~ m{(.+)/(.*)}; + if ($pre) { + for my $item (split m{/}, $pre) { + next if !$item; + + confess "Error, no $item in ".Dumper($found) + if !exists $found->{$item}; + + $found = $found->{$item}; + } + } else { + ($last) = $path =~ m{.*/(.*)}; + } + $found->{$last} = $content; } sub remove_config_node($self, $path, $content, $data) { @@ -1211,28 +1234,38 @@ sub _equal_hash($a,$b) { } sub add_config_unique_node($self, $path, $content, $data) { - my $content_hash; - eval { $content_hash = Load($content) }; + my $content_hash = $content; + eval { $content_hash = Load($content) if !ref($content) }; confess $@."\n$content" if $@; $data->{hardware}->{host_devices} = [] if $path eq "/hardware/host_devices" && !exists $data->{hardware}->{host_devices}; my $found = $data; + my $parent; for my $item (split m{/}, $path ) { next if !$item; - confess "Error, no $item in ".Dumper($found) - if !exists $found->{$item}; - + if ( !exists $found->{$item} ) { + $found->{$item} ={}; + } + $parent = $found; $found = $found->{$item}; } + my $old; if (ref($found) eq 'ARRAY') { push @$found, ( $content_hash ); } else { my ($item) = keys %$content_hash; - $found->{$item} = $content_hash->{$item}; + if ($item) { + $old = dclone($found); + $found->{$item} = $content_hash->{$item}; + } else { + my ($last) = $path =~ m{.*/(.*)}; + $parent->{$last} = $content_hash; + } } + return $old; } @@ -1252,7 +1285,14 @@ sub get_config($self) { return $self->_load(); } +sub get_config_txt($self) { + return $self->_vm->read_file($self->_config_file); +} + sub reload_config($self, $data) { + if (!ref($data)) { + $data = Load($data); + } eval { DumpFile($self->_config_file(), $data) }; confess $@ if $@; } diff --git a/lib/Ravada/HostDevice/Templates.pm b/lib/Ravada/HostDevice/Templates.pm index 75dc5acb3..027e7c68a 100644 --- a/lib/Ravada/HostDevice/Templates.pm +++ b/lib/Ravada/HostDevice/Templates.pm @@ -177,6 +177,26 @@ our @TEMPLATES_VOID = ( } ] } + ,{ name => "GPU Mediated Device" + ,list_command => "lsusb " + ,list_filter => '.*' + ,template_args => encode_json( + { uuid => '^(.*?) '} + ) + + ,templates => [{path => "/hardware/host_devices" + ,type => 'node' + ,template => Dump( device => { + uuid => '<%= $uuid %>' + }) + } + ,{ path => "/features" + ,type => 'unique_node' + ,template => Dump( { hidden => 'on' } ) + } + + ] + } ); my %TEMPLATES = ( diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index 2f28630ae..2bba34ba5 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -483,6 +483,7 @@ sub _around_create_domain { confess "ERROR: Unknown args ".Dumper(\%args) if keys %args; $self->_check_duplicate_name($name); + my $create_volatile; if ($id_base) { my $vm_local = $self; $vm_local = $self->new( host => 'localhost') if !$vm_local->is_local; @@ -493,6 +494,7 @@ sub _around_create_domain { unless $owner->allowed_access($base->id); $volatile = $base->volatile_clones if (! defined($volatile)); + $create_volatile=$volatile if !$base->list_host_devices(); if ($add_to_pool) { confess "Error: you can't add to pool and also pick from pool" if $from_pool; $from_pool = 0; @@ -530,7 +532,7 @@ sub _around_create_domain { $args_create{listen_ip} = $self->listen_ip($remote_ip); } - my $domain = $self->$orig(%args_create, volatile => $volatile); + my $domain = $self->$orig(%args_create, volatile => $create_volatile); $self->_add_instance_db($domain->id); $domain->add_volume_swap( size => $swap ) if $swap; $domain->_data('is_compacted' => 1); diff --git a/lib/Ravada/VM/Void.pm b/lib/Ravada/VM/Void.pm index b68973f82..0e552e768 100644 --- a/lib/Ravada/VM/Void.pm +++ b/lib/Ravada/VM/Void.pm @@ -177,6 +177,7 @@ sub create_domain { $domain->change_hardware('network', $index ,{name => $network}) } } + $active = 0 if $domain_base->list_host_devices(); } elsif (!exists $args{config}) { $storage = $self->default_storage_pool_name() if !$storage; my ($vda_name) = "$args{name}-vda-".Ravada::Utils::random_name(4).".void"; @@ -194,7 +195,7 @@ sub create_domain { } $domain->set_memory($args{memory}) if $args{memory}; - if ( $volatile || $user->is_temporary ) { + if ( $active ) { $domain->_store( is_active => 1 ); } # $domain->start(); diff --git a/t/device/00_host_device.t b/t/device/00_host_device.t index 1e720c15e..b384be9ba 100644 --- a/t/device/00_host_device.t +++ b/t/device/00_host_device.t @@ -315,6 +315,7 @@ sub test_host_device_usb($vm) { _check_hostdev($clone, 1); shutdown_domain_internal($clone); + _check_hostdev($clone, 1) or exit; eval { $clone->start(user_admin) }; is(''.$@, '') or exit; _check_hostdev($clone, 1) or exit; @@ -417,12 +418,12 @@ sub test_host_device_usb_mock($vm, $n_hd=1) { like( ''.$@,qr(Did not find USB device)) if $vm->type eq 'KVM'; is( ''.$@, '' ) if $vm->type eq 'Void'; _check_hostdev($clone, $n_hd); + is(scalar($clone->list_host_devices_attached()), $n_hd, $clone->name); } - is(scalar($clone->list_host_devices_attached()), $n_hd, $clone->name); push @clones,($clone); } $clones[0]->shutdown_now(user_admin); - _check_hostdev($clones[0], $n_hd); + _check_hostdev($clones[0], 0); my @devs_attached = $clones[0]->list_host_devices_attached(); is(scalar(@devs_attached), $n_hd); is($devs_attached[0]->{is_locked},0); diff --git a/t/device/10_templates.t b/t/device/10_templates.t index f3fd80b60..c613648e4 100644 --- a/t/device/10_templates.t +++ b/t/device/10_templates.t @@ -60,7 +60,7 @@ sub test_hostdev_not_in_domain_config($domain) { sub test_hostdev_in_domain_void($domain) { my $config = $domain->_load(); - ok(exists $config->{hardware}->{host_devices}) or return; + ok(exists $config->{hardware}->{host_devices}) or confess $domain->name; ok(scalar(@{ $config->{hardware}->{host_devices}})) or die "Expecting host_devices" ." in ".Dumper($config->{hardware}); } @@ -211,7 +211,7 @@ sub test_grab_free_device($base) { ok($down_dev->{name}); ($up_dev) = $up->list_host_devices_attached(); is($up_dev->{is_locked},0); - test_hostdev_in_domain_config($up, $expect_feat_kvm); + test_hostdev_not_in_domain_config($up); test_hostdev_in_domain_config($down, $expect_feat_kvm); eval { $up->start(user_admin) }; @@ -238,7 +238,7 @@ sub test_grab_free_device($base) { my ($third_dev_down) = $third->list_host_devices_attached(); is($third_dev_down->{is_locked},0) or die Dumper($third_dev_down); test_hostdev_in_domain_config($up, $expect_feat_kvm); - test_hostdev_in_domain_config($third, $expect_feat_kvm); + test_hostdev_not_in_domain_config($third); } @@ -356,8 +356,6 @@ sub test_templates_start_nohd($vm) { is($req->error, '') or exit; test_hostdev_not_in_domain_config($domain); $domain->shutdown_now(user_admin); - my $device_configured = $domain->_device_already_configured($hd); - is($device_configured, undef); $req = Ravada::Request->start_domain( uid => user_admin->id ,id_domain => $domain->id @@ -535,13 +533,6 @@ sub _mangle_dom_hd($domain) { sub _mangle_dom_hd_void($domain) { my $config = $domain->_load(); - die "Error: ho host devices in ".$domain->name - if !exists $config->{hardware}->{host_devices} - || !exists $config->{hardware}->{host_devices}->[0] - || !defined $config->{hardware}->{host_devices}->[0] - ; - my ($hd) = $config->{hardware}->{host_devices}->[0]; - my $vendor_id = $hd->{vendor_id}; my $sth = connector->dbh->prepare("SELECT id,name FROM host_devices_domain " ." WHERE id_domain=?"); $sth->execute($domain->id); @@ -549,9 +540,6 @@ sub _mangle_dom_hd_void($domain) { my ($id_hd, $device_name) = $sth->fetchrow; $device_name =~ s/(.* ID ).*?(:.*)/${1}$new_id$2/; - $config->{hardware}->{host_devices}->[0]->{vendor_id} = $new_id; - $domain->_store(hardware => $config->{hardware}); - $sth = connector->dbh->prepare("UPDATE host_devices_domain set name=?" ." WHERE id=?"); $sth->execute($device_name, $id_hd); @@ -566,20 +554,15 @@ sub _mangle_dom_hd_kvm($domain) { my ($current) = $line =~ /Device (\d+)/; $device = $current if $current>$device; } - my $xml = XML::LibXML->load_xml(string => $domain->domain->get_xml_description); - - my ($address) = $xml->findnodes("/domain/devices/hostdev/source/address"); - my ($old_device) = $address->getAttribute('device'); my $sth = connector->dbh->prepare("SELECT id,name FROM host_devices_domain " ." WHERE id_domain=?"); $sth->execute($domain->id); my ($id_hd, $device_name) = $sth->fetchrow; - $address->setAttribute(device => ++$device); + $device++; $device_name =~ s/(.*Device )\d+(.*)/$1$device$2/; $sth = connector->dbh->prepare("UPDATE host_devices_domain set name=?" ." WHERE id=?"); $sth->execute($device_name, $id_hd); - $domain->reload_config($xml); } sub _create_host_devices($vm, $n) { diff --git a/t/device/40_mediated_device.t b/t/device/40_mediated_device.t index ce3bda5cd..45e97b382 100644 --- a/t/device/40_mediated_device.t +++ b/t/device/40_mediated_device.t @@ -4,6 +4,8 @@ use strict; use Carp qw(confess); use Data::Dumper; use Test::More; +use Mojo::JSON qw( encode_json decode_json ); +use YAML qw(Load Dump LoadFile DumpFile); use Ravada::HostDevice::Templates; @@ -14,6 +16,8 @@ no warnings "experimental::signatures"; use feature qw(signatures); my $BASE; +my $MOCK_MDEV; +my $N_TIMERS; #################################################################### @@ -37,35 +41,405 @@ sub _prepare_dir_mdev() { return $dir; } +sub _check_mdev($vm, $hd) { + + my $n_dev = $hd->list_available_devices(); + return _check_used_mdev($vm, $hd) if $n_dev; + + my $dir = _prepare_dir_mdev(); + $hd->_data('list_command' => "ls $dir"); + + $MOCK_MDEV=1 unless $vm->type eq 'Void'; + return $hd->list_available_devices;; +} + +sub _check_used_mdev($vm, $hd) { + return $hd->list_available_devices() if $vm->type eq 'Void'; + + my @active = $vm->vm->list_domains; + for my $dom (@active) { + my $doc = XML::LibXML->load_xml(string => $dom->get_xml_description); + + my $hd_path = "/domain/devices/hostdev/source/address"; + my ($hostdev) = $doc->findnodes($hd_path); + next if !$hostdev; + + my $uuid = $hostdev->getAttribute('uuid'); + if (!$uuid) { + warn "No uuid in ".$hostdev->toString; + next; + } + my $dom_imported = rvd_front->search_domain($dom->get_name); + $dom_imported = $vm->import_domain($dom->get_name,user_admin) + unless $dom_imported; + + $dom_imported->add_host_device($hd->id); + my ($dev) = grep /^$uuid/, $hd->list_devices; + if (!$dev) { + warn "No $uuid found in mdevctl list"; + next; + } + $dom_imported->_lock_host_device($hd, $dev); + } + return $hd->list_available_devices(); +} + +sub _req_start($domain) { + if ($MOCK_MDEV) { + $domain->_attach_host_devices(); + } else { + Ravada::Request->start_domain( + uid => user_admin->id + ,id_domain => $domain->id + ); + wait_request(); + } +} + +sub _req_shutdown($domain) { + # $domain->_dettach_host_devices(); + my $req = Ravada::Request->shutdown_domain( + uid => user_admin->id + ,id_domain => $domain->id + ); + wait_request(); + + for ( 1 .. 10 ) { + last if !$domain->is_active; + diag("Waiting for ".$domain." down"); + sleep 1; + } + +} + sub test_mdev($vm) { my $templates = Ravada::HostDevice::Templates::list_templates($vm->id); my ($mdev) = grep { $_->{name} eq "GPU Mediated Device" } @$templates; ok($mdev,"Expecting PCI template in ".$vm->name) or return; - my $dir = _prepare_dir_mdev(); + my $id = $vm->add_host_device(template => $mdev->{name}); + my $hd = Ravada::HostDevice->search_by_id($id); + + my $n_devices = _check_mdev($vm, $hd); + is( $hd->list_available_devices() , $n_devices); + + my $domain = $BASE->clone( + name =>new_domain_name + ,user => user_admin + ); + test_config_no_hd($domain); + $domain->add_host_device($id); + _req_start($domain); + is($hd->list_available_devices(), $n_devices-1); + test_config($domain); + + _req_shutdown($domain); + # $domain->_dettach_host_devices(); + is($hd->list_available_devices(), $n_devices); + + test_config_no_hd($domain); + + return ($domain, $hd); +} + +sub _change_state_on($domain) { + if ($domain->type eq 'KVM') { + _change_kvm_state_on($domain); + } elsif ($domain->type eq 'Void') { + _change_void_state_on($domain); + } +} + +sub _change_void_state_on($domain) { + my $config = $domain->_load(); + my $features = ($config->{features} or {}); + $features->{hidden}='on'; + $domain->_store(features => $features); +} + +sub _change_kvm_state_on($domain) { + my $xml = $domain->xml_description(); + my $doc = XML::LibXML->load_xml(string => $xml); + + my $kvm_path = "/domain/features/kvm"; + my ($kvm) = $doc->findnodes($kvm_path); + if (!$kvm) { + my ($features) = $doc->findnodes("/domain/features"); + $kvm = $features->addNewChild(undef,'kvm'); + my $hidden = $kvm->addNewChild(undef, 'hidden'); + $hidden->setAttribute('state' => 'on'); + } + ok($kvm,"Expecting $kvm_path") or return; + my ($state) = $kvm->findnodes("hidden"); + is($state->getAttribute('state'),'on'); + + $domain->reload_config($doc); +} + +sub _change_timer($domain) { + if ($domain->type eq 'KVM') { + _change_timer_kvm($domain); + } elsif ($domain->type eq 'Void') { + _change_timer_void($domain); + } else { + die $domain->type; + } +} + +sub _base_timers_void($domain) { + my $config = $domain->_load(); + my $clock = ($config->{clock} or []); + my @timers = ( + { name => 'rtc', tickpolicy => 'catchup'} + ,{ name => 'pit', tickpolicy => 'delay'} + ,{ name => 'hpet', present => 'no'} + ); + for my $timer (@timers) { + push @$clock ,({timer => $timer }); + } + $domain->_store(clock => $clock); +} + +sub _change_timer_void($domain) { + _base_timers_void($domain); + my $config = $domain->_load(); + my $clock = $config->{clock}; + + $N_TIMERS = scalar(@$clock); + + $domain->_store(clock => $clock); +} + +sub _change_timer_kvm($domain) { + + my $doc = XML::LibXML->load_xml(string => $domain->xml_description()); + + my $timer; + for (my ($node) = $doc->findnodes("/domain/clock/timer") ) { + $timer = $node if $node->getAttribute('name' eq 'tsc'); + } + return if $timer; + my ($clock) = $doc->findnodes("/domain/clock"); + for my $timer ( $clock->findnodes("timer") ) { + $clock->removeChild($timer) if $timer->getAttribute('name') eq 'tsc'; + } + + my @timers = $doc->findnodes("/domain/clock/timer"); + $N_TIMERS = scalar(@timers); + + $domain->reload_config($doc); + +} + +sub test_timer($domain, $field, $value) { + my ($timers,$timer_tsc); + if ($domain->type eq 'KVM') { + ($timers, $timer_tsc)=_get_timer_kvm($domain); + } elsif ($domain->type eq 'Void') { + ($timers, $timer_tsc)= _get_timer_void($domain); + } else { + die $domain->type; + } + is(scalar(@$timers), $N_TIMERS+1); + is(scalar(@$timer_tsc), 1); + is($timer_tsc->[0]->{$field},$value) or confess Dumper([$domain->name,$timer_tsc]); + +} + +sub test_no_timer($domain) { + my ($timers,$list_timer_tsc); + my $expected_tsc = undef; + if ($domain->type eq 'KVM') { + ($timers, $list_timer_tsc)=_get_timer_kvm($domain); + } elsif ($domain->type eq 'Void') { + ($timers, $list_timer_tsc)= _get_timer_void($domain); + } else { + die $domain->type; + } + is(scalar(@$timers), $N_TIMERS) or confess $domain->name; + is(scalar(@$list_timer_tsc),0); + my $timer_tsc = $list_timer_tsc->[0]; + is_deeply($timer_tsc, $expected_tsc) or confess Dumper($timer_tsc); + +} +sub _get_timer_kvm($domain) { + my $doc = XML::LibXML->load_xml(string => $domain->xml_description()); + my ($clock) = $doc->findnodes("/domain/clock"); + my @timers = $doc->findnodes("/domain/clock/timer"); + my @found_tsc; + for my $node (@timers) { + if ( $node->getAttribute('name') eq'tsc') { + my $found_tsc = {}; + for my $attrib ($node->attributes) { + $found_tsc->{$attrib->name}= $attrib->value; + } + push @found_tsc,($found_tsc); + } + } + return (\@timers,\@found_tsc); +} + +sub _get_timer_void($domain) { + my $config = $domain->_load(); + my $timers = $config->{clock}; + + my @found_tsc; + for my $timer (@$timers) { + die Dumper([$domain->name,$timer]) if ref($timer) ne 'HASH'; + die Dumper([$domain->name,$timer]) if !exists $timer->{timer}->{name}; + + if ( $timer->{timer}->{name} eq 'tsc') { + push @found_tsc , ($timer->{timer}); + } + } + return ($timers,\@found_tsc); +} + +sub _add_template_timer_void($hd) { + my $sth = connector->dbh->prepare( + "INSERT INTO host_device_templates " + ."( id_host_device, path, template,type )" + ."values( ?, '/clock', ? , 'node')" + ); + $sth->execute($hd->id,Dump({ timer => { name => 'tsc', present => 'yes' }})) +} + +sub _add_template_timer_kvm($hd) { + my $sth = connector->dbh->prepare( + "INSERT INTO host_device_templates " + ."( id_host_device, path, template,type )" + ."values( ?, '/domain/clock/timer', ? , 'node')" + ); + $sth->execute($hd->id,"" ) +} + +sub _add_template_timer($hd) { + my $vm = Ravada::VM->open($hd->id_vm); + if ($vm->type eq 'Void') { + _add_template_timer_void($hd); + } else { + _add_template_timer_kvm($hd); + } +} + +sub test_mdev_kvm_state($vm) { + + my $templates = Ravada::HostDevice::Templates::list_templates($vm->id); + my ($mdev) = grep { $_->{name} eq "GPU Mediated Device" } @$templates; + ok($mdev,"Expecting PCI template in ".$vm->name) or return; my $id = $vm->add_host_device(template => $mdev->{name}); my $hd = Ravada::HostDevice->search_by_id($id); - $hd->_data('list_command' => "ls $dir"); + my $n_devices = _check_mdev($vm, $hd); + + _add_template_timer($hd); - is( $hd->list_available_devices() , 2); + is( $hd->list_available_devices() , $n_devices); my $domain = $BASE->clone( name =>new_domain_name ,user => user_admin ); + _change_state_on($domain); + _change_timer($domain); + test_hidden($domain); + test_no_timer($domain); $domain->add_host_device($id); - $domain->_add_host_devices(); - is($hd->list_available_devices(), 1); + _req_start($domain); - test_xml($domain); + test_hidden($domain); + test_old_in_locked($domain); + test_timer($domain,'present' => 'yes'); - return ($domain, $hd); + _req_shutdown($domain); + $domain->_dettach_host_devices(); + + diag("Test hidden after dettach"); + test_hidden($domain); + test_no_timer($domain); + + $domain->remove(user_admin); + $hd->remove(); + +} + +sub test_old_in_locked($domain) { + my $sth = connector->dbh->prepare("SELECT config_no_hd FROM domains d " + ." WHERE id=?" + ); + $sth->execute($domain->id); + my ($old) = $sth->fetchrow(); + like($old,qr/\w/); +} + +sub test_hidden($domain) { + + if ($domain->type eq 'KVM') { + test_hidden_kvm($domain); + } else { + test_hidden_void($domain); + } +} + +sub test_hidden_void($domain) { + my $config = $domain->_load(); + is($config->{features}->{hidden},'on') or exit; } +sub test_hidden_kvm($domain) { + my $doc = XML::LibXML->load_xml(string => $domain->xml_description()); + my $state_path = "/domain/features/kvm/hidden"; + my ($hidden) = $doc->findnodes($state_path); + ok($hidden,"Missing $state_path ".$domain->name) or confess; +} + +sub test_config($domain) { + + if ($domain->type eq'KVM') { + test_xml($domain); + } elsif ($domain->type eq 'Void') { + test_yaml($domain); + } else { + die "unknown type ".$domain->type; + } +} + +sub test_config_no_hd($domain){ + if ($domain->type eq'KVM') { + test_xml_no_hd($domain); + } elsif ($domain->type eq 'Void') { + test_yaml_no_hd($domain); + } else { + die "unknown type ".$domain->type; + } + +} + +sub test_yaml($domain) { + my $config = $domain->_load(); + + my $hd= $config->{hardware}->{host_devices}; + ok($hd) or confess; + + my $features = $config->{features}; + ok($features); + +} + +sub test_yaml_no_hd($domain) { + my $config = $domain->_load(); + + my $hd= $config->{hardware}->{host_devices}; + ok(!$hd) or confess; + + my $features = $config->{features}; + ok(!$features || !keys(%$features)) or confess(Dumper($features)); +} + + sub test_xml($domain) { my $xml = $domain->xml_description(); @@ -87,6 +461,27 @@ sub test_xml($domain) { } +sub test_xml_no_hd($domain) { + + my $xml = $domain->xml_description(); + + my $doc = XML::LibXML->load_xml(string => $xml); + + my $hd_path = "/domain/devices/hostdev"; + my ($hostdev) = $doc->findnodes($hd_path); + ok(!$hostdev,"Expecting no $hd_path") or exit; + my $model = "/domain/devices/video/model"; + my ($video) = $doc->findnodes($model); + ok($video,"Expecting a $model ".$domain->name) or exit; + my $v_type = $video ->getAttribute('type'); + isnt($v_type,'none') or exit; + + my $kvm_path = "/domain/features/kvm/hidden"; + my ($kvm) = $doc->findnodes($kvm_path); + ok(!$kvm,"Expecting no $kvm_path in ".$domain->name) or confess; +} + + sub test_base($domain) { my @args = ( uid => user_admin->id ,id_domain => $domain->id); @@ -96,7 +491,7 @@ sub test_base($domain) { wait_request(); - test_xml($domain); + test_config_no_hd($domain); Ravada::Request->clone(@args, number => 2, remote_ip => '1.2.3.4'); wait_request(); @@ -104,8 +499,8 @@ sub test_base($domain) { for my $clone_data( $domain->clones ) { my $clone = Ravada::Domain->open($clone_data->{id}); - $clone->_add_host_devices(); - test_xml($clone); + $clone->_attach_host_devices(); + test_config($clone); $clone->remove(user_admin); } } @@ -116,27 +511,37 @@ sub test_volatile_clones($vm, $domain, $host_device) { $domain->shutdown_now(user_admin) if $domain->is_active; Ravada::Request->prepare_base(@args) if !$domain->is_base(); - wait_request(); - - is($host_device->list_available_devices(), 2) or exit; + my $n_devices = $host_device->list_available_devices(); + ok($n_devices) or exit; $domain->_data('volatile_clones' => 1); - my $n_clones = $domain->clones; + my @old_clones = $domain->clones; my $n=2; + my $max_n_device = $host_device->list_available_devices(); my $exp_avail = $host_device->list_available_devices()- $n; Ravada::Request->clone(@args, number => $n, remote_ip => '1.2.3.4'); wait_request(check_error => 0); - is(scalar($domain->clones), $n_clones+$n); + is(scalar($domain->clones), scalar(@old_clones)+$n); + + my $found_active=0; + for my $clone ($domain->clones) { + $found_active ++ if $clone->{status} =~ /(active|starting)/; + next if grep { $_->{name} eq $clone->{name} } @old_clones; + ok($clone->{status} =~ /(active|starting)/, $clone->{name}) or exit; + } + is($found_active, $n); my $n_device = $host_device->list_available_devices(); is($n_device,$exp_avail); for my $clone_data( $domain->clones ) { my $clone = Ravada::Domain->open($clone_data->{id}); - test_xml($clone); + is($clone->is_active,1) unless $MOCK_MDEV; + is($clone->is_volatile,1); + test_config($clone); $clone->shutdown_now(user_admin); $n_device = $host_device->list_available_devices(); @@ -147,16 +552,20 @@ sub test_volatile_clones($vm, $domain, $host_device) { my $clone_gone2 = $vm->search_domain($clone_data->{name}); ok(!$clone_gone2,"Expecting $clone_data->{name} removed on shutdown"); + + my $clone_gone3; + eval { $clone_gone3 = $vm->import_domain($clone_data->{name},user_admin,0) }; + ok(!$clone_gone3,"Expecting $clone_data->{name} removed on shutdown") or exit; } $domain->_data('volatile_clones' => 0); - is($host_device->list_available_devices(), 2) or exit; + is($host_device->list_available_devices(), $max_n_device) or exit; } #################################################################### clean(); -for my $vm_name ( 'KVM' ) { +for my $vm_name ('KVM', 'Void' ) { my $vm; eval { $vm = rvd_back->search_vm($vm_name) @@ -177,6 +586,7 @@ for my $vm_name ( 'KVM' ) { } else { $BASE = import_domain($vm); } + test_mdev_kvm_state($vm); my ($domain, $host_device) = test_mdev($vm); test_volatile_clones($vm, $domain, $host_device); test_base($domain); diff --git a/templates/main/run_request.html.ep b/templates/main/run_request.html.ep index 3da1bd68f..3ff5f921f 100644 --- a/templates/main/run_request.html.ep +++ b/templates/main/run_request.html.ep @@ -48,7 +48,12 @@
  • Host Devices
    • + + <%=l 'disabled' %> + + {{device.name}} +
  • @@ -106,7 +111,6 @@ -
    {{request.error}}