From 2e095663b7d2c207a3d4af306fa6fce75f29b2fb Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 27 May 2024 18:08:38 +0200 Subject: [PATCH] Feat: hostdev nodes (#2058) issue #2035 --- lib/Ravada.pm | 9 +- lib/Ravada/Domain.pm | 124 +++-- lib/Ravada/Domain/KVM.pm | 11 + lib/Ravada/Domain/Void.pm | 3 +- lib/Ravada/Front.pm | 30 +- lib/Ravada/HostDevice.pm | 82 +++- lib/Ravada/Request.pm | 53 ++ lib/Ravada/VM.pm | 42 +- lib/Ravada/VM/KVM.pm | 16 +- lib/Ravada/VM/Void.pm | 3 + lib/Ravada/WebSocket.pm | 63 ++- public/js/admin.js | 119 ++++- script/rvd_front | 52 +- sql/mysql/vms.sql | 4 +- sql/sqlite/vms.sql | 2 - t/device/00_host_device.t | 12 +- t/device/10_templates.t | 228 +++++++-- t/device/20_misc.t | 9 +- t/device/30_pci.t | 3 +- t/device/40_mediated_device.t | 6 + t/device/50_nodes.t | 645 +++++++++++++++++++++++++ t/kvm/n10_nodes.t | 2 - t/lib/Test/Ravada.pm | 19 +- templates/bootstrap/navigation.html.ep | 1 + templates/main/admin_hostdev.html.ep | 174 +++++++ templates/main/node_hostdev.html.ep | 113 +---- templates/main/vm_hostdev.html.ep | 34 +- 27 files changed, 1580 insertions(+), 279 deletions(-) create mode 100644 t/device/50_nodes.t create mode 100644 templates/main/admin_hostdev.html.ep diff --git a/lib/Ravada.pm b/lib/Ravada.pm index d19316a71..51e30e510 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -1640,7 +1640,7 @@ sub _add_indexes_generic($self) { ,vms=> [ "unique(hostname, vm_type): hostname_type" - ,"UNIQUE (name)" + ,"UNIQUE (name,vm_type)" ] ,domain_share => [ @@ -2240,7 +2240,7 @@ sub _sql_create_tables($self) { ,list_command => 'varchar(128) not null' ,list_filter => 'varchar(128) not null' ,template_args => 'varchar(255) not null' - ,devices => 'TEXT' + ,devices_node => 'TEXT' ,enabled => "integer NOT NULL default 1" ,'date_changed' => 'timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP' @@ -2271,6 +2271,7 @@ sub _sql_create_tables($self) { ,id_vm => 'integer NOT NULL references `vms`(`id`) ON DELETE CASCADE' ,id_domain => 'integer NOT NULL references `domains`(`id`) ON DELETE CASCADE' ,name => 'varchar(255)' + ,'time_changed' => 'integer' } ] ,[ @@ -4570,7 +4571,7 @@ sub _cmd_list_host_devices($self, $request) { $id_host_device ); - $hd->list_devices; + my %list= $hd->list_devices_nodes; } @@ -5840,7 +5841,7 @@ sub _cmd_list_cpu_models($self, $request) { my $info = $domain->get_info(); my $vm = $domain->_vm->vm; - my @out = $vm->get_cpu_model_names('x86_64'); + my @out = $domain->_vm->get_cpu_model_names('x86_64'); $request->output(encode_json(\@out)); } diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 5e85598e0..47a5fd9ce 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -324,7 +324,7 @@ sub _around_start($orig, $self, @arg) { $enable_host_devices = 1 if !defined $enable_host_devices; for (1 .. 5) { - eval { $self->_start_checks(@arg) }; + eval { $self->_start_checks(@arg, enable_host_devices => $enable_host_devices) }; my $error = $@; if ($error) { if ( $error =~/base file not found/ && !$self->_vm->is_local) { @@ -332,12 +332,13 @@ sub _around_start($orig, $self, @arg) { next; } elsif ($error =~ /No free memory/) { warn $error; - die $error if $self->is_local; + die $error if $self->is_local || $self->is_volatile; my $vm_local = $self->_vm->new( host => 'localhost' ); $self->migrate($vm_local, $request); next; } } + warn $error if $error; die $error if $error; if (!defined $listen_ip) { my $display_ip; @@ -469,13 +470,14 @@ sub _start_checks($self, @args) { my $vm_local = $self->_vm->new( host => 'localhost' ); my $vm = $vm_local; - my ($id_vm, $request); + my ($id_vm, $request, $enable_host_devices); if (!(scalar(@args) % 2)) { my %args = @args; # We may be asked to start the machine in a specific id_vmanager $id_vm = delete $args{id_vm}; $request = delete $args{request} if exists $args{request}; + $enable_host_devices = delete $args{enable_host_devices}; } # If not specific id_manager we go to the last id_vmanager unless it was localhost # If the last VManager was localhost it will try to balance here. @@ -483,11 +485,15 @@ sub _start_checks($self, @args) { if !$id_vm && defined $self->_data('id_vm') && $self->_data('id_vm') != $vm_local->id; + # check the requested id_vm is suitable if ($id_vm) { $vm = Ravada::VM->open($id_vm); if ( !$vm->enabled || !$vm->ping ) { $vm = $vm_local; $id_vm = undef; + } elsif ($enable_host_devices && !$self->_available_hds($vm)) { + $vm = $vm_local; + $id_vm = undef; } } @@ -508,7 +514,8 @@ sub _start_checks($self, @args) { if ($id_vm) { $self->_set_vm($vm); } else { - $self->_balance_vm($request); + $self->_balance_vm($request, $enable_host_devices) + if !$self->is_volatile; } if ( !$self->is_volatile && !$self->_vm->is_local() ) { if (!base_in_vm($self->id_base, $self->_vm->id)) { @@ -586,16 +593,57 @@ sub _search_already_started($self, $fast = 0) { return keys %started; } -sub _balance_vm($self, $request=undef) { +sub _available_hds($self, $vm) { + + my @host_devices = $self->list_host_devices(); + return 1 if !@host_devices; + + my $available=1; + for my $hd (@host_devices) { + if (! $hd->list_available_devices($vm->id) ) { + $available=0; + last; + } + } + return $available; +} + +sub _filter_vm_available_hd($self, @vms) { + + my @host_devices = $self->list_host_devices(); + + return @vms if !@host_devices; + + my @vms_ret; + + for my $vm ( @vms ) { + my $available = 1; + for my $hd (@host_devices) { + if (! $hd->list_available_devices($vm->id) ) { + $available=0; + last; + } + } + push @vms_ret,($vm) if $available; + } + + die "No host devices available in any node.\n" if !@vms_ret; + + return @vms_ret; +} + +sub _balance_vm($self, $request=undef, $host_devices=undef) { return if $self->{_migrated}; my $base; $base = Ravada::Domain->open($self->id_base) if $self->id_base; my $vm_free; - for (;;) { - $vm_free = $self->_vm->balance_vm($self->_data('id_owner'),$base, $self->id); + for (my $count=0;$count<10;$count++) { + $vm_free = $self->_vm->balance_vm($self->_data('id_owner'),$base + , $self->id, $host_devices); return if !$vm_free; + next if !$vm_free->vm || !$vm_free->is_active; last if $vm_free->id == $self->_vm->id; eval { $self->migrate($vm_free, $request) }; @@ -611,7 +659,7 @@ sub _balance_vm($self, $request=undef) { } die $@; } - return if !$vm_free; + return if !$vm_free || !$vm_free->vm || !$vm_free->is_active; return $vm_free->id; } @@ -1034,6 +1082,7 @@ sub _check_free_vm_memory { my $self = shift; return if !Ravada::Front::setting(undef,"/backend/limits/startup_ram"); + return if !$self->is_known(); my $vm_free_mem = $self->_vm->free_memory; @@ -2943,6 +2992,7 @@ sub clone { $vm = $node if $node->is_local; } } + my $clone = $vm->create_domain( name => $name ,id_base => $self->id @@ -5356,7 +5406,7 @@ Returns a list for virtual machine managers where this domain is base =cut -sub list_vms($self) { +sub list_vms($self, $check_host_devices=0) { confess "Domain is not base" if !$self->is_base; my $sth = $$CONNECTOR->dbh->prepare("SELECT id_vm FROM bases_vm WHERE id_domain=? AND enabled = 1"); @@ -5373,7 +5423,9 @@ sub list_vms($self) { push @vms,($vm_local); $self->set_base_vm(vm => $vm_local, user => Ravada::Utils::user_daemon); } - return @vms; + return @vms if !$check_host_devices; + + return $self->_filter_vm_available_hd(@vms); } =head2 base_in_vm @@ -7126,6 +7178,8 @@ sub add_host_device($self, $host_device) { my $id_hd = $host_device; $id_hd = $host_device->id if ref($host_device); + confess if !$id_hd; + my $sth = $$CONNECTOR->dbh->prepare("INSERT INTO host_devices_domain " ."(id_host_device, id_domain) " ." VALUES ( ?, ? ) " @@ -7165,6 +7219,7 @@ sub list_host_devices($self) { my @found; while (my $row = $sth->fetchrow_hashref) { $row->{devices} = '' if !defined $row->{devices}; + $row->{devices_node} = '{}' if !defined $row->{devices_node}; push @found,(Ravada::HostDevice->new(%$row)); } @@ -7222,15 +7277,13 @@ sub _attach_host_devices($self, @args) { return if !@host_devices; return if $self->is_active(); - my $vm_local = $self->_vm->new( host => 'localhost' ); - my $vm = $vm_local; - - my ($id_vm, $request); + my ($request); if (!(scalar(@args) % 2)) { my %args = @args; $request = delete $args{request} if exists $args{request}; } + $self->_clean_old_hd_locks(); $self->_backup_config_no_hd(); my $doc = $self->get_config(); for my $host_device ( @host_devices ) { @@ -7239,7 +7292,9 @@ sub _attach_host_devices($self, @args) { my $device; if ( $device_configured ) { - if ( $host_device->enabled() && $host_device->is_device($device_configured) && $self->_lock_host_device($host_device) ) { + if ( $host_device->enabled() + && $host_device->is_device($device_configured, $self->_vm->id) + && $self->_lock_host_device($host_device) ) { $device = $device_configured; } else { $self->_dettach_host_device($host_device, $doc, $device_configured); @@ -7270,13 +7325,13 @@ sub _attach_host_devices($self, @args) { } sub _search_free_device($self, $host_device) { - my ($device) = $host_device->list_available_devices(); + my ($device) = $host_device->list_available_devices($self->_data('id_vm')); 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"; + die "Error: No available devices in ".$self->_vm->name." for ".$host_device->name."\n"; } } return $device; @@ -7287,6 +7342,7 @@ sub _dettach_host_devices($self) { for my $host_device ( @host_devices ) { $self->_dettach_host_device($host_device); } + $self->_unlock_host_devices(); $self->_restore_config_no_hd(); } @@ -7331,17 +7387,19 @@ sub _lock_host_device($self, $host_device, $device=undef) { } my $id_domain_locked = $self->_check_host_device_already_used($device); + + my $id_vm = $self->_data('id_vm'); + $id_vm = $self->_vm->id if !$id_vm; + return 1 if defined $id_domain_locked && $self->id == $id_domain_locked; return 0 if defined $id_domain_locked; - my $query = "INSERT INTO host_devices_domain_locked (id_domain,id_vm,name) VALUES(?,?,?)"; + my $query = "INSERT INTO host_devices_domain_locked (id_domain,id_vm,name,time_changed) VALUES(?,?,?,?)"; my $sth = $$CONNECTOR->dbh->prepare($query); - 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) }; + eval { $sth->execute($self->id,$id_vm, $device,time) }; if ($@) { warn $@; $self->_data(status => 'shutdown'); @@ -7356,29 +7414,37 @@ sub _lock_host_device($self, $host_device, $device=undef) { return 1; } -sub _unlock_host_devices($self) { +sub _clean_old_hd_locks($self) { my $sth = $$CONNECTOR->dbh->prepare("DELETE FROM host_devices_domain_locked " - ." WHERE id_domain=?" + ." WHERE id_domain=? AND id_vm <> ?" ); - $sth->execute($self->id); + $sth->execute($self->id, $self->_vm->id); + +} + +sub _unlock_host_devices($self, $time_changed=3) { + my $sth = $$CONNECTOR->dbh->prepare("DELETE FROM host_devices_domain_locked " + ." WHERE id_domain=? AND time_changed<=?" + ); + $sth->execute($self->id, time-$time_changed); } sub _unlock_host_device($self, $name) { my $sth = $$CONNECTOR->dbh->prepare("DELETE FROM host_devices_domain_locked " - ." WHERE id_domain=? AND name=?" + ." WHERE id_domain=? AND name=? AND time_changedexecute($self->id, $name); + $sth->execute($self->id, $name,time-60); } sub _check_host_device_already_used($self, $device) { - my $query = "SELECT id_domain FROM host_devices_domain_locked " + my $query = "SELECT id_domain,time_changed FROM host_devices_domain_locked " ." WHERE id_vm=? AND name=?" ; my $sth = $$CONNECTOR->dbh->prepare($query); $sth->execute($self->_data('id_vm'), $device); - my ($id_domain) = $sth->fetchrow; + my ($id_domain,$time_changed) = $sth->fetchrow; # warn "\n".($id_domain or '')." [".$self->id."] had locked $device\n"; return if !defined $id_domain; @@ -7386,7 +7452,7 @@ sub _check_host_device_already_used($self, $device) { my $domain = Ravada::Domain->open($id_domain); - return $id_domain if $domain->is_active; + return $id_domain if time-$time_changed < 10 || $domain->is_active; $sth = $$CONNECTOR->dbh->prepare("DELETE FROM host_devices_domain_locked " ." WHERE id_domain=?"); diff --git a/lib/Ravada/Domain/KVM.pm b/lib/Ravada/Domain/KVM.pm index 7542aeff4..36cc1d8e5 100644 --- a/lib/Ravada/Domain/KVM.pm +++ b/lib/Ravada/Domain/KVM.pm @@ -3671,12 +3671,22 @@ sub _validate_xml($self, $doc) { } } +sub _fix_uuid($self, $doc) { + my ($uuid) = $doc->findnodes("/domain/uuid/text()"); + confess "I cant'find /domain/uuid in ".$self->name if !$uuid; + + $uuid->setData($self->domain->get_uuid_string); + +} + 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; + $self->_fix_uuid($doc); + my $new_domain; eval { @@ -3955,6 +3965,7 @@ sub _xml_equal_hostdev($doc1, $doc2) { $doc1 =~ s/\n//g; $doc2 =~ s/\n//g; + return 1 if $doc1 eq $doc2; my $parser = XML::LibXML->new() or die $!; diff --git a/lib/Ravada/Domain/Void.pm b/lib/Ravada/Domain/Void.pm index 6ad11390c..50a964f67 100644 --- a/lib/Ravada/Domain/Void.pm +++ b/lib/Ravada/Domain/Void.pm @@ -1293,8 +1293,7 @@ sub reload_config($self, $data) { if (!ref($data)) { $data = Load($data); } - eval { DumpFile($self->_config_file(), $data) }; - confess $@ if $@; + $self->_vm->write_file($self->_config_file(), Dump($data)); } sub has_nat_interfaces($self) { diff --git a/lib/Ravada/Front.pm b/lib/Ravada/Front.pm index 4386afa56..30002b6ef 100644 --- a/lib/Ravada/Front.pm +++ b/lib/Ravada/Front.pm @@ -618,6 +618,35 @@ sub list_vms($self, $type=undef) { return @list; } +=head2 list_nodes_by_id + +Returns a list of Nodes by id + +=cut + +sub list_nodes_by_id($self, $type=undef) { + + my $sql = "SELECT id,name,hostname,is_active, vm_type, enabled FROM vms "; + + my @args = (); + if ($type) { + $sql .= "WHERE (vm_type=? or vm_type=?)"; + my $type2 = $type; + $type2 = 'qemu' if $type eq 'KVM'; + @args = ( $type, $type2); + } + my $sth = $CONNECTOR->dbh->prepare($sql." ORDER BY vm_type,name"); + $sth->execute(@args); + + my %list; + while (my $row = $sth->fetchrow_hashref) { + $list{$row->{id}}= $row->{name}; + } + $sth->finish; + return \%list; +} + + sub _list_bases_vm($self, $id_node) { my $sth = $CONNECTOR->dbh->prepare( "SELECT d.id FROM domains d,bases_vm bv" @@ -1625,7 +1654,6 @@ Update the host device information, then it requests a list of the current avail sub update_host_device($self, $args) { my $id = delete $args->{id} or die "Error: missing id ".Dumper($args); Ravada::Utils::check_sql_valid_params(keys %$args); - $args->{devices} = undef; my $query = "UPDATE host_devices SET ".join(" , ", map { "$_=?" } sort keys %$args); $query .= " WHERE id=?"; my $sth = $self->_dbh->prepare($query); diff --git a/lib/Ravada/HostDevice.pm b/lib/Ravada/HostDevice.pm index 6b4c87bab..45803ef2d 100644 --- a/lib/Ravada/HostDevice.pm +++ b/lib/Ravada/HostDevice.pm @@ -9,6 +9,7 @@ Ravada::HostDevice - Host Device basic library for Ravada =cut +use Carp qw(croak cluck); use Data::Dumper; use Hash::Util qw(lock_hash); use IPC::Run3 qw(run3); @@ -60,7 +61,7 @@ has 'enabled' => ( ,is => 'rw' ); -has 'devices' => ( +has 'devices_node' => ( isa => 'Str' ,is => 'rw' ,default => '' @@ -78,15 +79,49 @@ sub search_by_id($self, $id) { $sth->execute($id); my $row = $sth->fetchrow_hashref; die "Error: device id='$id' not found" if !exists $row->{id}; - $row->{devices} = '' if !defined $row->{devices}; + $row->{devices_node} = encode_json({}) if !defined $row->{devices_node}; return Ravada::HostDevice->new(%$row); } -sub list_devices($self) { +sub list_devices_nodes($self) { my $vm = Ravada::VM->open($self->id_vm); + my $sth = $$CONNECTOR->dbh->prepare( + "SELECT id,name,is_active,enabled FROM vms WHERE id <> ? AND vm_type=?"); + $sth->execute($vm->id, $vm->type); - die "Error: No list_command in host_device ".$self->id_vm + my @nodes = ([$vm->id,$vm->name,1,1]); + + while ( my ($id,$name, $is_active,$enabled) = $sth->fetchrow) { + push @nodes,([$id, $name, $is_active, $enabled]); + } + + my %devices; + for my $ndata (@nodes) { + if (!$ndata->[2] || !$ndata->[3]) { + $devices{$ndata->[1]}=[]; + next; + } + my $node = Ravada::VM->open($ndata->[0]); + next if !$node || !$node->vm; + my @current_devs; + eval { + @current_devs = $self->list_devices($node->id) + if $node->is_active; + }; + warn $@ if $@; + # push @devices, @current_devs; + $devices{$node->id}=\@current_devs; + } + + $self->_data( devices_node => \%devices ); + return %devices; +} + +sub list_devices($self, $id_vm=$self->id_vm) { + my $vm = Ravada::VM->open($id_vm); + return [] unless $vm->is_active; + die "Error: No list_command in host_device ".$self->id if !$self->list_command; my @command = split /\s+/, $self->list_command; @@ -99,34 +134,32 @@ sub list_devices($self) { for my $line (split /\n/, $out ) { push @device,($line) if !defined $filter || $line =~ qr($filter)i; } - my $encoded = encode_json(\@device); - $self->_data( devices => $encoded ); return @device; } -sub is_device($self, $device) { +sub is_device($self, $device, $id_vm) { return if !defined $device; - for my $dev ( $self->list_devices ) { + for my $dev ( $self->list_devices($id_vm) ) { return 1 if $dev eq $device; } return 0; } -sub _device_locked($self, $name) { +sub _device_locked($self, $name, $id_vm=$self->id_vm) { my $sth = $$CONNECTOR->dbh->prepare("SELECT id FROM host_devices_domain_locked " ." WHERE id_vm=? AND name=? " ); - $sth->execute($self->id_vm, $name); + $sth->execute($id_vm, $name); my ($is_locked) = $sth->fetchrow; $is_locked = 0 if !defined $is_locked; - return 1 if $is_locked; + return $is_locked; } -sub list_available_devices($self) { +sub list_available_devices($self, $id_vm=$self->id_vm) { my @device; - for my $dev_entry ( $self->list_devices ) { - next if $self->_device_locked($dev_entry); + for my $dev_entry ( $self->list_devices($id_vm) ) { + next if $self->_device_locked($dev_entry, $id_vm); push @device, ($dev_entry); } return @device; @@ -134,17 +167,21 @@ sub list_available_devices($self) { sub remove($self) { _init_connector(); + my $id = $self->id; my $sth = $$CONNECTOR->dbh->prepare("SELECT id_domain FROM host_devices_domain " ." WHERE id_host_device=?" ); - $sth->execute($self->id); + $sth->execute($id); while ( my ( $id_domain ) = $sth->fetchrow) { my $domain = Ravada::Domain->open($id_domain); $domain->remove_host_device($self); } + $sth = $$CONNECTOR->dbh->prepare("DELETE FROM host_devices WHERE id=?"); - $sth->execute($self->id); + $sth->execute($id); + + Ravada::Request::remove('requested', id_host_device => $id ); } sub _fetch_template_args($self, $device) { @@ -216,7 +253,16 @@ sub _data($self, $field, $value=undef) { ); $sth->execute($value, $self->id); $self->meta->get_attribute($field)->set_value($self, $value); - $self->_dettach_in_domains() if $field =~ /^(devices|list_)/; + if ( $field =~ /^(devices|list_)/ ) { + $self->_dettach_in_domains(); + if ($field =~ /^list_/) { + Ravada::Request->list_host_devices( + uid => Ravada::Utils::user_daemon->id + ,id_host_device => $self->id + ); + $self->_data('devices_node' => ''); + } + } return $value; } else { my $sth = $$CONNECTOR->dbh->prepare("SELECT * FROM host_devices" @@ -224,7 +270,7 @@ sub _data($self, $field, $value=undef) { ); $sth->execute($self->id); my $row = $sth->fetchrow_hashref(); - die "Error: No field '$field' in host_devices" if !exists $row->{$field}; + croak "Error: No field '$field' in host_devices" if !exists $row->{$field}; return $row->{$field}; } } diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index 03bea87d3..8da82430f 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -1824,6 +1824,59 @@ sub redo($self) { $sth->execute($self->id); } +=head2 remove + +Remove all requests that comply with the conditions + +=cut + +sub remove($status, %args) { + my $sth = _dbh->prepare( + "SELECT * FROM requests where status = ? " + ); + $sth->execute($status); + + my $sth_del = _dbh->prepare("DELETE FROM requests where id=?"); + + while ( my $row = $sth->fetchrow_hashref ) { + my $req_args = {}; + + eval { + $req_args = decode_json($row->{args}) if $row->{args}; + }; + warn "Warning: $@ ".$row->{args} + ."\n".Dumper($row) if $@; + + next if $row->{status} ne $status; + delete $req_args->{uid}; + next if scalar(keys%args) != scalar(keys(%$req_args)); + + my $found = 1; + for my $key (keys %$req_args) { + + next if exists $args{$key} + && !defined $args{$key} && !defined $req_args->{$key}; + + $found=0 if + !exists $args{$key} || + $args{$key} ne $req_args->{$key}; + } + next if !$found; + + for my $key (keys %args) { + next if exists $req_args->{$key} + && !defined $args{$key} && !defined $req_args->{$key}; + + $found=0 if + !exists $req_args->{$key} || + $args{$key} ne $req_args->{$key}; + } + next if !$found; + + $sth_del->execute($row->{id}); + } +} + sub AUTOLOAD { my $self = shift; diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index 2bba34ba5..c057ede04 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -401,8 +401,8 @@ sub _connect_ssh($self) { confess "Don't connect to local ssh" if $self->is_local; - if ( $self->readonly || $> ) { - confess $self->name." readonly or not root, don't do ssh"; + if ( $self->readonly ) { + confess $self->name." readonly, don't do ssh"; return; } @@ -483,7 +483,6 @@ 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,8 +492,11 @@ sub _around_create_domain { die "Error: user ".$owner->name." can not clone from ".$base->name unless $owner->allowed_access($base->id); - $volatile = $base->volatile_clones if (! defined($volatile)); - $create_volatile=$volatile if !$base->list_host_devices(); + if ( $base->list_host_devices() ) { + $args_create{volatile}=0; + } elsif (!defined $args_create{volatile}) { + $args_create{volatile} = $base->volatile_clones; + } if ($add_to_pool) { confess "Error: you can't add to pool and also pick from pool" if $from_pool; $from_pool = 0; @@ -523,8 +525,7 @@ sub _around_create_domain { return $base->_search_pool_clone($owner) if $from_pool; - if ($self->is_local && $base && $base->is_base - && ( $volatile || $owner->is_temporary )) { + if ($self->is_local && $base && $base->is_base && $args_create{volatile}) { $request->status("balancing") if $request; my $vm = $self->balance_vm($owner->id, $base) or die "Error: No free nodes available."; $request->status("creating machine on ".$vm->name) if $request; @@ -532,7 +533,7 @@ sub _around_create_domain { $args_create{listen_ip} = $self->listen_ip($remote_ip); } - my $domain = $self->$orig(%args_create, volatile => $create_volatile); + my $domain = $self->$orig(%args_create); $self->_add_instance_db($domain->id); $domain->add_volume_swap( size => $swap ) if $swap; $domain->_data('is_compacted' => 1); @@ -1851,7 +1852,12 @@ sub write_file( $self, $file, $contents ) { return $self->_write_file_local($file, $contents ) if $self->is_local; my $ssh = $self->_ssh or confess "Error: no ssh connection"; - my ($rin, $pid) = $self->_ssh->pipe_in("cat > $file") + unless ( $file =~ /^[a-z0-9 \/_:\-\.]+$/i ) { + my $file2=$file; + $file2 =~ tr/[a-z0-9 \/_:\-\.]/*/c; + die "Error: insecure character in '$file': '$file2'"; + } + my ($rin, $pid) = $self->_ssh->pipe_in("cat > '$file'") or die "pipe_in method failed ".$self->_ssh->error; print $rin $contents; @@ -2050,24 +2056,24 @@ Arguments =cut -sub balance_vm($self, $uid, $base=undef, $id_domain=undef) { +sub balance_vm($self, $uid, $base=undef, $id_domain=undef, $host_devices=undef) { my @vms; if ($base) { confess "Error: base is not an object ".Dumper($base) if !ref($base); - @vms = $base->list_vms(); + @vms = $base->list_vms($host_devices); } else { @vms = $self->list_nodes(); } - return $vms[0] if scalar(@vms)<=1; - my @vms_active; for my $vm (@vms) { - push @vms_active,($vm) if $vm->is_active && $vm->enabled; + push @vms_active,($vm) if $vm && $vm->vm && $vm->is_active && $vm->enabled; } + return $vms_active[0] if scalar(@vms_active)==1; + if ($base && $base->_data('balance_policy') == 1 ) { my $vm = $self->_balance_already_started($uid, $id_domain, \@vms_active); return $vm if $vm; @@ -2113,7 +2119,7 @@ sub _balance_free_memory($self , $base, $vms) { my @status; for my $vm (_random_list( @$vms )) { - next if !$vm->enabled(); + next if !$vm || !$vm->vm || !$vm->enabled(); my $active = 0; eval { $active = $vm->is_active() }; my $error = $@; @@ -2656,7 +2662,7 @@ sub _check_equal_storage_pools($vm1, $vm2) { my ($path1, $path2) = ($vm1->_storage_path($pool), $vm2->_storage_path($pool)); - die "Error: Storage pool '$pool' different. In ".$vm1->name." $path1 , " + confess "Error: Storage pool '$pool' different. In ".$vm1->name." $path1 , " ." in ".$vm2->name." $path2" if $path1 ne $path2; } return 1; @@ -2701,7 +2707,10 @@ sub add_host_device($self, %args) { ; my $sth = $$CONNECTOR->dbh->prepare($query); + eval { $sth->execute(map { $info->{$_} } sort keys %$info ); + }; + confess Dumper([$info,$@]) if $@; my $id = Ravada::Request->_last_insert_id( $$CONNECTOR ); @@ -2734,6 +2743,7 @@ sub list_host_devices($self) { my @found; while (my $row = $sth->fetchrow_hashref) { $row->{devices} = '' if !defined $row->{devices}; + $row->{devices_node} = '' if !defined $row->{devices_node}; push @found,(Ravada::HostDevice->new(%$row)); } diff --git a/lib/Ravada/VM/KVM.pm b/lib/Ravada/VM/KVM.pm index 97dd1efec..2c773cc75 100644 --- a/lib/Ravada/VM/KVM.pm +++ b/lib/Ravada/VM/KVM.pm @@ -1105,7 +1105,7 @@ sub _domain_create_common { my %args = @_; my $id_owner = delete $args{id_owner} or confess "ERROR: The id_owner is mandatory"; - my $is_volatile = delete $args{is_volatile}; + my $volatile = delete $args{volatile}; my $listen_ip = delete $args{listen_ip}; my $spice_password = delete $args{spice_password}; my $user = Ravada::Auth::SQL->search_by_id($id_owner) @@ -1133,7 +1133,7 @@ sub _domain_create_common { for ( 1 .. 10 ) { eval { - if ($user->is_temporary || $is_volatile && !$host_devices ) { + if ( $volatile) { $dom = $self->vm->create_domain($xml->toString()); } else { $dom = $self->vm->define_domain($xml->toString()); @@ -1168,7 +1168,7 @@ sub _domain_create_common { , domain => $dom , storage => $self->storage_pool , id_owner => $id_owner - , active => ($user->is_temporary || $is_volatile || $host_devices) + , active => $volatile ); return ($domain, $spice_password); } @@ -1254,9 +1254,11 @@ sub _domain_create_from_base { $base = $vm_local->_search_domain_by_id($args{id_base}) if $args{id_base}; confess "Unknown base id: $args{id_base}" if !$base; + confess Dumper(\%args) if $args{name} eq 'tst_device_50_nodes_01' + && ( !exists $args{volatile} || !defined $args{volatile}); + my $volatile = $base->volatile_clones; $volatile = delete $args{volatile} if exists $args{volatile} && defined $args{volatile}; - my $options = delete $args{options}; my $network = delete $options->{network}; @@ -1282,7 +1284,7 @@ sub _domain_create_from_base { $self->_xml_set_network($xml, $network) if $network; my ($domain, $spice_password) - = $self->_domain_create_common($xml,%args, is_volatile=>$volatile, base => $base); + = $self->_domain_create_common($xml,%args, volatile=>$volatile, base => $base); $domain->_insert_db(name=> $args{name}, id_base => $base->id, id_owner => $args{id_owner} , id_vm => $self->id ); @@ -2995,6 +2997,10 @@ sub get_library_version($self) { return $self->vm->get_library_version(); } +sub get_cpu_model_names($self,$arch='x86_64') { + return $self->vm->get_cpu_model_names($arch); +} + sub can_list_cpu_models($self) { return 1; } diff --git a/lib/Ravada/VM/Void.pm b/lib/Ravada/VM/Void.pm index 0e552e768..83f4d8d84 100644 --- a/lib/Ravada/VM/Void.pm +++ b/lib/Ravada/VM/Void.pm @@ -804,6 +804,9 @@ sub active_storage_pool($self, $name, $value) { $self->write_file($file_sp, Dump( \@list)); } +sub get_cpu_model_names($self,$arch='x86_64') { + return qw(486 qemu32 qemu64); +} sub has_networking { return 1 }; diff --git a/lib/Ravada/WebSocket.pm b/lib/Ravada/WebSocket.pm index 7c578f068..45a2a90de 100644 --- a/lib/Ravada/WebSocket.pm +++ b/lib/Ravada/WebSocket.pm @@ -231,7 +231,7 @@ sub _list_host_devices($rvd, $args) { my $user = Ravada::Auth::SQL->new(name => $login) or die "Error: uknown user $login"; - my $sth = $rvd->_dbh->prepare( "SELECT id,name,list_command,list_filter,devices,date_changed " + my $sth = $rvd->_dbh->prepare( "SELECT id,name,list_command,list_filter,devices_node,date_changed " ." FROM host_devices WHERE id_vm=?"); $sth->execute($id_vm); @@ -239,6 +239,7 @@ sub _list_host_devices($rvd, $args) { my @found; while (my $row = $sth->fetchrow_hashref) { _list_domains_with_device($rvd, $row); + _list_devices_node($rvd, $row); push @found, $row; next unless _its_been_a_while_channel($args->{channel}); my $req = Ravada::Request->list_host_devices( @@ -249,6 +250,65 @@ sub _list_host_devices($rvd, $args) { return \@found; } +sub _list_devices_node($rvd, $row) { + my $devices = {}; + eval { + $devices = decode_json($row->{devices_node}) if $row->{devices_node}; + }; + warn "Warning: $@ $row->{devices_node}" if $@; + $row->{_n_devices}=0; + + my %ret; + my %attached = _list_devices_attached($rvd); + if (%$devices) { + $row->{_nodes} = [sort keys %{$devices}]; + for (@{$row->{_nodes}}) { + $row->{_n_devices} += scalar(@{$devices->{$_}}); + } + $row->{_loading} = 0; + for my $id_node ( keys %$devices ) { + my @devs; + for my $name ( @{$devices->{$id_node}} ) { + my $dev = { name => $name }; + + $dev->{domain} = $attached{"$id_node.$name"} + if exists $attached{"$id_node.$name"}; + + push @devs,($dev); + } + $ret{$id_node} = \@devs; + } + } else { + $row->{_nodes} = []; + } + + $row->{devices_node} = \%ret; +} + +sub _list_devices_attached($rvd) { + my $sth=$rvd->_dbh->prepare( + "SELECT d.id,d.name,d.is_base, d.status, l.id, l.name " + ." ,l.id_vm " + ." FROM host_devices_domain hdd, domains d" + ." LEFT JOIN host_devices_domain_locked l" + ." ON d.id=l.id_domain " + ." WHERE d.id= hdd.id_domain " + ." ORDER BY d.name" + ); + $sth->execute(); + my %devices; + while ( my ($id,$name,$is_base, $status, $is_locked, $device, $id_vm) = $sth->fetchrow ) { + next if !$device; + $is_locked = 0 if !$is_locked || $status ne 'active'; + my $domain = { id => $id ,name => $name, is_locked => $is_locked + ,is_base => $is_base ,device => $device + }; + $devices{"$id_vm.$device"} = $domain; + } + return %devices; + +} + sub _list_domains_with_device($rvd,$row) { my $id_hd = $row->{id}; @@ -286,7 +346,6 @@ sub _list_domains_with_device($rvd,$row) { $row->{_domains} = \@domains; $row->{_bases} = \@bases; - $row->{devices} = [values %devices]; } sub _get_domain_with_device($rvd, $dev) { diff --git a/public/js/admin.js b/public/js/admin.js index f17605397..cbeb60d2e 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -9,6 +9,7 @@ ravadaApp.directive("solShowMachine", swMach) .controller("manage_nodes",manage_nodes) .controller("manage_routes",manage_routes) .controller("manage_networks",manage_networks) + .controller("manage_host_devices",manage_host_devices) .controller("settings_network",settings_network) .controller("settings_node",settings_node) .controller("settings_storage",settings_storage) @@ -210,11 +211,13 @@ ravadaApp.directive("solShowMachine", swMach) && $scope.id_iso.options['machine']) { var types = $scope.machine_types[$scope.backend][$scope.id_iso.arch]; var option = $scope.id_iso.options['machine']; - for (var i=0; i sub($c) { }; +get '/admin/hostdev/(#vm_type)' => sub($c) { + _admin_host_devices($c); +}; + +sub _select_vm_by_type($c) { + + my $type = $c->stash('vm_type'); + + my $sql = "SELECT id,name,vm_type FROM vms " + ." WHERE hostname='localhost'"; + $sql .= " AND vm_type=?" if defined $type; + + my $sth = $RAVADA->_dbh->prepare($sql); + + if (defined ($type)) { + $sth->execute($type); + } else { + $sth->execute(); + } + my ($id,$name,$vm_type) = $sth->fetchrow; + + return not_found($c, 404, "vm type ".($type or 'UNDEF')." not found") + if !$id; + + $c->stash('vm_type' => $vm_type); + $c->stash('id_vm' => $id); + +} + +sub _admin_host_devices($c) { + _add_admin_libs($c); + + _select_vm_by_type($c); + + $c->stash(tab => ($c->param('tab') or '')); + $c->stash('id' => $c->stash('id_vm')); + + return $c->render( template => '/main/admin_hostdev' ); + +}; + + get '/admin/networks/*id_vm' => { id_vm => undef} => sub($c) { return access_denied($c) unless $USER->is_admin @@ -503,6 +545,13 @@ get '/list_nodes.json' => sub { $c->render(json => [$RAVADA->list_vms]); }; +get '/list_nodes_by_id.json' => sub { + my $c = shift; + return _access_denied($c) if !$USER || !$USER->is_admin; + + $c->render(json => $RAVADA->list_nodes_by_id); +}; + get '/list_host_devices/(:id_vm)' => sub($c) { $c->render(json => Ravada::WebSocket::_list_host_devices($RAVADA , { @@ -3463,10 +3512,11 @@ sub admin { if ($page eq 'storage') { _select_vm($c); } + return _admin_host_devices($c) if ($page eq 'hostdev'); $c->render( template => 'main/admin_'.$page); }; -sub _select_vm($c) { +sub _select_vm($c , $type =undef) { my $sth = $RAVADA->_dbh->prepare("SELECT id,name FROM vms WHERE hostname='localhost' AND vm_type=?"); my $types = Ravada::Front::list_vm_types(); for my $type ('KVM',@$types) { diff --git a/sql/mysql/vms.sql b/sql/mysql/vms.sql index 4522bd332..7e8385508 100644 --- a/sql/mysql/vms.sql +++ b/sql/mysql/vms.sql @@ -6,7 +6,5 @@ create table vms ( `default_storage` varchar(64) DEFAULT 'default', `security` varchar(20) default null, `is_active` int default 0, - PRIMARY KEY (`id`), - UNIQUE KEY `name` (`name`), - UNIQUE KEY `hostname_type` (`hostname`,`vm_type`) + PRIMARY KEY (`id`) ); diff --git a/sql/sqlite/vms.sql b/sql/sqlite/vms.sql index a363f0477..2c6e01619 100644 --- a/sql/sqlite/vms.sql +++ b/sql/sqlite/vms.sql @@ -5,6 +5,4 @@ create table vms ( , `hostname` varchar(128) NOT NULL , `default_storage` varchar(64) DEFAULT 'default' , `security` varchar(20) default null -, UNIQUE (`name`) -, UNIQUE (`hostname`,`vm_type`) ); diff --git a/t/device/00_host_device.t b/t/device/00_host_device.t index b384be9ba..8366d80b0 100644 --- a/t/device/00_host_device.t +++ b/t/device/00_host_device.t @@ -306,7 +306,7 @@ sub test_host_device_usb($vm) { ); my @list_hostdev_c = $clone->list_host_devices(); is(scalar @list_hostdev_c, 1) or exit; - my $device = $list_hostdev_c[0]->{devices}; + my ($device) = $list_hostdev_c[0]->list_devices; test_kvm_usb_template_args($device, $list_hostdev_c[0]); @@ -342,6 +342,8 @@ sub test_host_device_usb($vm) { } sub test_kvm_usb_template_args($device_usb, $hostdev) { + + confess "Error: undefined device_usb " if !defined $device_usb; my ($bus, $device, $vendor_id, $product_id) = $device_usb =~ /Bus 0*([0-9a-f]+) Device 0*([0-9a-f]+).*ID 0*([0-9a-f]+):0*([0-9a-f]+) /; my $args = $hostdev->_fetch_template_args($device_usb); @@ -422,11 +424,14 @@ sub test_host_device_usb_mock($vm, $n_hd=1) { } push @clones,($clone); } + sleep 2; + $clones[0]->shutdown_now(user_admin); + sleep 1; $clones[0]->shutdown_now(user_admin); _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); + is($devs_attached[0]->{is_locked},0) or die Dumper(\@devs_attached); for (@list_hostdev) { $_->_data('enabled' => 0 ); @@ -601,11 +606,12 @@ sub test_check_list_command($vm) { } } - for my $something ('lssomething' , 'findsomething') { + for my $something ('ls' , 'find') { $hdev->_data('list_command' => $something); is($hdev->list_command, $something); is($hdev->_data('list_command'), $something); } + wait_request(); $hdev->remove(); } diff --git a/t/device/10_templates.t b/t/device/10_templates.t index c613648e4..b0d8be1f8 100644 --- a/t/device/10_templates.t +++ b/t/device/10_templates.t @@ -95,6 +95,25 @@ sub _fix_host_device($hd) { } elsif ($hd->{name} =~ /USB/ ) { _set_hd_usb($hd); } + _purge_hd($hd); +} + +sub _purge_hd($hd) { + my $sth = connector->dbh->prepare( + "DELETE FROM host_devices_domain WHERE id_host_device=? AND id_domain NOT IN (select id FROM domains)" + ); + $sth->execute($hd->{id}); + + $sth = connector->dbh->prepare( + "DELETE FROM host_devices_domain WHERE id_host_device=? AND id_domain NOT IN (select id FROM domains WHERE status='active')" + ); + $sth->execute($hd->{id}); + + $sth = connector->dbh->prepare( + "DELETE FROM host_devices_domain_locked WHERE id_domain NOT IN (select id FROM domains WHERE status='active')" + ); + $sth->execute(); + } sub _create_domain_hd($vm, $hd) { @@ -103,11 +122,33 @@ sub _create_domain_hd($vm, $hd) { return $domain if $vm->type ne 'KVM'; + if ($domain->type eq 'KVM' && $hd->{name} =~ /USB/) { + _req_add_usb($domain); + } return $domain; } +sub _req_add_usb($domain) { + Ravada::Request->add_hardware( + uid => user_admin->id + ,id_domain => $domain->id + ,name => 'usb controller' + ); + wait_request(debug => 0); +} + +sub _shutdown_all($vm) { + for my $dom ($vm->list_domains) { + $dom->shutdown_now(user_admin); + $dom->_unlock_host_devices(0); + } + my $sth = connector->dbh->prepare("DELETE FROM host_devices_domain_locked"); + $sth->execute(); +} + sub test_hd_in_domain($vm , $hd) { + _shutdown_all($vm); my $domain = create_domain($vm); if ($vm->type eq 'KVM') { if ($hd->{name} =~ /PCI/) { @@ -122,7 +163,6 @@ sub test_hd_in_domain($vm , $hd) { _set_hd_usb($hd); } } - diag("Testing HD ".$hd->{name}." ".$hd->list_filter." in ".$vm->type); $domain->add_host_device($hd); if ($hd->list_devices) { @@ -131,6 +171,7 @@ sub test_hd_in_domain($vm , $hd) { } $domain->prepare_base(user_admin); + _shutdown_all($vm); my $n_locked = _count_locked(); for my $count (reverse 0 .. $hd->list_devices ) { my $clone = $domain->clone(name => new_domain_name() ,user => user_admin); @@ -138,6 +179,7 @@ sub test_hd_in_domain($vm , $hd) { _compare_hds($domain, $clone); test_device_unlocked($clone); + my $t0 = time; if ($hd->list_devices) { eval { $clone->start(user_admin) }; if (!$count) { @@ -146,9 +188,11 @@ sub test_hd_in_domain($vm , $hd) { last; } is(_count_locked(),++$n_locked) or exit; + next; test_device_locked($clone); test_hostdev_in_domain_config($clone, ($hd->name =~ /PCI/ && $vm->type eq 'KVM')); } + sleep(3) if time-$t0<3; $clone->shutdown_now(user_admin); test_device_unlocked($clone); @@ -175,6 +219,20 @@ sub test_hd_in_domain($vm , $hd) { } +sub _select_clone_up($base) { + + my $sth = connector->dbh->prepare("SELECT id FROM host_devices_domain_locked "); + $sth->execute(); + while ( my ($id) = $sth->fetchrow) { + my $clone = Ravada::Domain->open($id); + next if !$clone->is_active; + + return $clone; + } + + die "Error: no clone active with host devices locked"; +} + sub test_grab_free_device($base) { wait_request(); rvd_back->_cmd_refresh_vms(); @@ -183,10 +241,9 @@ sub test_grab_free_device($base) { die "Error: I need 3 clones . I can't try to grab only ".scalar(@clones) if scalar(@clones)<3; - my ($up) = grep { $_->{status} eq 'active' } @clones; + my $up = _select_clone_up($base); my ($down) = grep { $_->{status} ne 'active' } @clones; ok($down && exists $down->{id}) or die Dumper(\@clones); - $up = Ravada::Domain->open($up->{id}); $down = Ravada::Domain->open($down->{id}); my ($up_dev) = $up->list_host_devices_attached(); @@ -201,6 +258,10 @@ sub test_grab_free_device($base) { test_hostdev_in_domain_config($up, $expect_feat_kvm); test_hostdev_not_in_domain_config($down); + $up->shutdown_now(user_admin); + wait_request(); + sleep 3; + is($up->is_active,0); $up->shutdown_now(user_admin); ($up_dev) = $up->list_host_devices_attached(); is($up_dev->{is_locked},0); @@ -303,9 +364,12 @@ sub _compare_hds($base, $clone) { } sub _count_locked() { - my $sth = connector->dbh->prepare("SELECT count(*) FROM host_devices_domain_locked "); + my $n=0; + my $sth = connector->dbh->prepare("SELECT * FROM host_devices_domain_locked ORDER BY id_domain, name"); $sth->execute(); - my ($n) = $sth->fetchrow; + while (my $row = $sth->fetchrow_hashref) { + $n++; + } return $n; } @@ -336,6 +400,8 @@ sub test_templates_start_nohd($vm) { my @list_hostdev = $vm->list_host_devices(); my ($hd) = $list_hostdev[-1]; + next if !config_host_devices($hd->name,0); + _fix_host_device($hd) if $vm->type eq 'KVM'; next if !$hd->list_devices; @@ -429,21 +495,28 @@ sub test_templates_gone_usb_2($vm) { my $domain = _create_domain_hd($vm, $hd); _fix_usb_ports($domain); + my $t0=time; $domain->start(user_admin); my $dev_config = $domain->_device_already_configured($hd); ok($dev_config) or exit; is(scalar($hd->list_domains_with_device()),1); + sleep(3) if time-$t0<3; + $domain->shutdown_now(user_admin); $hd->_data('list_filter',"no match"); - diag("try to start again, it should fail"); + Ravada::Request->list_host_devices( + uid => user_admin->id + ,id_host_device => $hd->id + ); + wait_request(); my $req = Ravada::Request->start_domain(uid => user_admin->id ,id_domain => $domain->id ); wait_request(check_error => 0, debug => 0); my $req2 = Ravada::Request->open($req->id); - like($req2->error,qr/No available devices/); + like($req2->error,qr/No available devices/) or exit; my $req_no_hd = Ravada::Request->start_domain(uid => user_admin->id ,id_domain => $domain->id @@ -487,12 +560,12 @@ sub test_templates_gone_usb($vm) { ok($dev_config) or exit; is(scalar($hd->list_domains_with_device()),1); + sleep 3; $domain->shutdown_now(user_admin); is($domain->_device_already_configured($hd), $dev_config) or exit; _mangle_dom_hd($domain); $hd->_data('list_filter',"no match"); - diag("try to start again, it should fail"); my $req = Ravada::Request->start_domain(uid => user_admin->id ,id_domain => $domain->id ); @@ -566,13 +639,22 @@ sub _mangle_dom_hd_kvm($domain) { } sub _create_host_devices($vm, $n) { + my @hd; if ($vm->type eq 'Void') { - return _create_host_devices_void($vm,$n); + @hd = _create_host_devices_void($vm,$n); } elsif ($vm->type eq 'KVM') { - return _create_host_devices_kvm($vm,$n); + @hd = _create_host_devices_kvm($vm,$n); } else { die "Error: I don't know how to create host devices for ".$vm->type; } + wait_request(debug => 0); + for my $hd (@hd) { + my $devices_node = $hd->_data('devices_node'); + die unless $devices_node; + my $data = decode_json($devices_node); + die unless keys %$data; + } + return @hd; } sub _create_host_devices_void($vm, $n) { @@ -591,12 +673,16 @@ sub _create_host_devices_kvm($vm,$n) { my ($template) = grep { $_->{list_command} =~ /lspci/ } @$templates; + if (!config_host_devices($template->{name},0)) { + ($template) = grep { $_->{list_command} =~ /lsusb/ } @$templates; + } + my @hds; for ( 1 .. $n ) { my $id_hd = $vm->add_host_device(template => $template->{name}); my $hd = Ravada::HostDevice->search_by_id($id_hd); - my $config = config_host_devices('pci'); + my $config = config_host_devices($template->{name}); $hd->_data('list_filter' => $config); push @hds,($hd); } @@ -626,16 +712,22 @@ sub test_frontend_list($vm) { my ($dev_attached) = ($domain->list_host_devices_attached); my $found=0; + my $fd_found; for my $fd ( @$front_devices ) { - for my $dev ( @{$fd->{devices}} ) { - if ($dev->{name} eq $dev_attached->{name}) { - ok($dev->{domain} , "Expecting domains listed in ".$dev->{name}) or next; + next unless $fd->{name} eq $hd1->name; + $fd_found = $fd; + my $dn = $fd->{devices_node}; + for my $node (keys %$dn) { + for my $dev ( @{$dn->{$node}} ) { + next if !$dev->{domain}; is($dev->{domain}->{id}, $domain->id,"Expecting ".$domain->name." attached in ".$dev->{name}); + is($dev->{domain}->{device},$dev_attached->{name}); $found++ if $dev->{domain}->{id} == $domain->id; } } } - is($found,2) or die Dumper($front_devices); + is($found,1,"Expected device '".$hd1->name."' in " + .$domain->id) or die Dumper($fd_found); remove_domain($domain); @@ -701,12 +793,12 @@ sub test_templates_change_devices($vm) { my ($dev_attached) = ($domain->list_host_devices_attached); $domain->shutdown_now(user_admin); - is($hostdev->is_device($dev_attached->{name}),1) or exit; + is($hostdev->is_device($dev_attached->{name},$vm->id),1) or exit; my $file = "$path/".$dev_attached->{name}; unlink $file or die "$! $file"; - is($hostdev->is_device($dev_attached->{name}),0) or exit; + is($hostdev->is_device($dev_attached->{name}, $vm->id),0) or exit; $domain->start(user_admin); my ($dev_attached2) = ($domain->list_host_devices_attached); @@ -720,10 +812,10 @@ sub test_templates_change_filter($vm) { ok(@$templates); for my $first (@$templates) { - diag("Testing $first->{name} Hostdev on ".$vm->type); $vm->add_host_device(template => $first->{name}); my @list_hostdev = $vm->list_host_devices(); my ($hd) = $list_hostdev[-1]; + next if $vm->type eq 'KVM' && !config_host_devices($first->{name},0); _fix_host_device($hd) if $vm->type eq 'KVM'; @@ -761,6 +853,27 @@ sub _remove_host_devices($vm) { } } +sub _get_frontend_devices($vm, $id_hd) { + + my $ws_args = { + channel => '/'.$vm->id + ,login => user_admin->name + }; + my $front_devs = Ravada::WebSocket::_list_host_devices(rvd_front(), $ws_args); + my @devices; + my $n_hds = scalar(@$front_devs); + for my $curr_hd ( @$front_devs ) { + next unless $curr_hd->{id} == $id_hd; + for my $node ( keys %{$curr_hd->{devices_node}} ) { + my $dn = $curr_hd->{devices_node}->{$node}; + for my $dev (@$dn) { + push @devices, ($dev->{name}) + } + } + } + return ($n_hds, \@devices); +} + sub test_templates($vm) { my $templates = Ravada::HostDevice::Templates::list_templates($vm->type); ok(@$templates); @@ -785,6 +898,7 @@ sub test_templates($vm) { my $host_device = $list_hostdev[-1]; + next if $vm->type eq 'KVM' && !config_host_devices($host_device->{name},0); _fix_host_device($host_device) if $vm->type eq 'KVM'; test_hd_in_domain($vm, $host_device); @@ -797,45 +911,31 @@ sub test_templates($vm) { ); wait_request( debug => 0); is($req->status, 'done'); - my $ws_args = { - channel => '/'.$vm->id - ,login => user_admin->name - }; - my $devices = Ravada::WebSocket::_list_host_devices(rvd_front(), $ws_args); - is(scalar(@$devices), 2+$n) or die Dumper($devices, $host_device); + my ($n_hd,$devices) = _get_frontend_devices($vm, $host_device->id); + is($n_hd, 2+$n) or die Dumper($devices, $host_device); $n+=2; - next if !(scalar(@{$devices->[-1]->{devices}})>1); my $list_filter = $host_device->_data('list_filter'); $host_device->_data('list_filter' => 'fail match'); - my $req2 = Ravada::Request->list_host_devices( - uid => user_admin->id - ,id_host_device => $host_device->id - ,_force => 1 - ); - wait_request(); - is($req2->status, 'done'); - is($req2->error, ''); - my $devices2 = Ravada::WebSocket::_list_host_devices(rvd_front(), $ws_args); + wait_request(debug => 0); + my ($n_hd2, $devices2) = _get_frontend_devices($vm, $host_device->id); my $equal; - my $dev0 = $devices->[-1]->{devices}; - my $dev2 = $devices2->[-1]->{devices}; - $equal = scalar(@$dev0) == scalar (@$dev2); + $equal = scalar(@$devices) == scalar (@$devices2); if ($equal ) { - for ( 0 .. scalar(@$dev0)-1) { - if ($dev0->[$_] ne $dev2->[$_]) { + for ( 0 .. scalar(@$devices)-1) { + if ($devices->[$_] ne $devices2->[$_]) { $equal = 0; last; } } } - ok(!$equal) or die Dumper($dev0, $dev2); + ok(!$equal) or die Dumper($devices, $devices2); $host_device->_data('list_filter' => $list_filter); } my $n = $vm->list_host_devices; for my $hd ( $vm->list_host_devices ) { - _fix_host_device($hd); + _fix_host_device($hd) unless $vm->type eq 'KVM' && !config_host_devices($hd->{name},0); test_hd_remove($vm, $hd); is($vm->list_host_devices,--$n, $hd->name) or die Dumper([$vm->list_host_devices]); } @@ -862,8 +962,26 @@ sub test_hd_dettach($vm, $host_device) { $domain->remove(user_admin); } +sub _req_remove($host_device) { + + my $req = Ravada::Request->remove_host_device( + uid => user_admin->id + ,id_host_device => $host_device->id + ); + wait_request(debug => 0); + is($req->status,'done'); + is($req->error, '') or exit; + +} + sub test_hd_remove($vm, $host_device) { my $start_fails; + my $type = $host_device->{name}; + $type = 'usb' if $type =~ /usb/i; + if ( $vm->type eq 'KVM' && !config_host_devices($type,0)) { + _req_remove($host_device); + return; + } if ($host_device->name =~ /^PCI/ && $vm->type eq 'KVM' ) { _set_hd_nvidia($host_device); if (!$host_device->list_devices) { @@ -877,18 +995,15 @@ sub test_hd_remove($vm, $host_device) { _fix_usb_ports($domain); _fix_host_device($host_device) if $vm->type eq 'KVM'; $domain->add_host_device($host_device); - + _count_locked(); eval { $domain->start(user_admin) }; - is(''.$@, '') unless $start_fails; + if (!$start_fails) { + is(''.$@, '') or confess "Error starting ".$domain->name." " + ."[ ".$domain->id."]"; + } $domain->shutdown_now(user_admin); - my $req = Ravada::Request->remove_host_device( - uid => user_admin->id - ,id_host_device => $host_device->id - ); - wait_request(); - is($req->status,'done'); - is($req->error, '') or exit; + my $req = _req_remove($host_device); my $sth = connector->dbh->prepare( "SELECT * FROM host_devices WHERE id=?" @@ -896,13 +1011,21 @@ sub test_hd_remove($vm, $host_device) { $sth->execute($host_device->id); my ($found) = $sth->fetchrow; ok(!$found); + + $sth = connector->dbh->prepare( + "SELECT * FROM host_devices_domain WHERE id_host_device=?" + ); + $sth->execute($host_device->id); + ($found) = $sth->fetchrow; + ok(!$found); + } #################################################################### clean(); -for my $vm_name ( vm_names()) { +for my $vm_name (vm_names()) { my $vm; eval { $vm = rvd_back->search_vm($vm_name) }; @@ -915,6 +1038,8 @@ for my $vm_name ( vm_names()) { diag("Testing host devices in $vm_name"); + test_templates($vm); + test_frontend_list($vm); test_templates_gone_usb_2($vm); @@ -925,7 +1050,6 @@ for my $vm_name ( vm_names()) { test_templates_start_nohd($vm); test_templates_change_filter($vm); - test_templates($vm); test_templates_change_devices($vm); } diff --git a/t/device/20_misc.t b/t/device/20_misc.t index dc79144f9..979df3c78 100644 --- a/t/device/20_misc.t +++ b/t/device/20_misc.t @@ -216,6 +216,10 @@ EOT my $domain = create_domain($vm); my $xml_config = XML::LibXML->load_xml(string => $config); + + my ($name) = $xml_config->findnodes("/domain/name/text()"); + + $name->setData($domain->name); $domain->reload_config($xml_config); $domain->remove_config_node("/domain/devices/hostdev", $content, $xml_config); @@ -230,10 +234,11 @@ clean(); for my $vm_name ( 'KVM' ) { SKIP: { - my $vm = rvd_back->search_vm($vm_name); + my $vm; + $vm = rvd_back->search_vm($vm_name) if !$>; my $msg = "SKIPPED test: No $vm_name VM found "; - if ($vm && $>) { + if ($vm_name eq 'KVM' && $>) { $msg = "SKIPPED: Test must run as root"; $vm = undef; } diff --git a/t/device/30_pci.t b/t/device/30_pci.t index e0e798bfd..746ebabad 100644 --- a/t/device/30_pci.t +++ b/t/device/30_pci.t @@ -62,7 +62,8 @@ clean(); for my $vm_name ( 'KVM' ) { SKIP: { - my $vm = rvd_back->search_vm($vm_name); + my $vm; + $vm = rvd_back->search_vm($vm_name) if !$<; my $msg = "SKIPPED test: No $vm_name VM found "; if ($vm && $>) { diff --git a/t/device/40_mediated_device.t b/t/device/40_mediated_device.t index 45e97b382..54cf46274 100644 --- a/t/device/40_mediated_device.t +++ b/t/device/40_mediated_device.t @@ -134,7 +134,13 @@ sub test_mdev($vm) { is($hd->list_available_devices(), $n_devices-1); test_config($domain); + sleep 1; _req_shutdown($domain); + for ( 1 .. 3 ) { + last if $hd->list_available_devices() <= $n_devices; + _req_shutdown($domain); + sleep 1; + } # $domain->_dettach_host_devices(); is($hd->list_available_devices(), $n_devices); diff --git a/t/device/50_nodes.t b/t/device/50_nodes.t new file mode 100644 index 000000000..22253fd25 --- /dev/null +++ b/t/device/50_nodes.t @@ -0,0 +1,645 @@ +use warnings; +use strict; + +use Carp qw(confess); +use Data::Dumper; +use File::Path qw(make_path); +use IPC::Run3 qw(run3); +use Mojo::JSON qw(decode_json encode_json); +use Ravada::HostDevice::Templates; +use Ravada::WebSocket; +use Test::More; +use YAML qw( Dump ); + +use lib 't/lib'; +use Test::Ravada; + +no warnings "experimental::signatures"; +use feature qw(signatures); + +my $N_DEVICE = 0; +my $MOCK_DEVICES = 1; + +my $PATH = "/var/tmp/$run_command("mkdir","-p",$PATH) if !$vm->file_exists($PATH); + + my $name = base_domain_name()."_${type} ID"; + + for my $n ( 1 .. $n_devices ) { + my $file= "$PATH/${name} $N_DEVICE$value${n} Foobar " + .$vm->name; + $vm->write_file($file,"fff6f017-3417-4ad3-b05e-17ae3e1a461".int(rand(10))); + } + $N_DEVICE ++; + + return ("find $PATH/",$name); +} + +sub _number($value, $length=3) { + my $dev = $value; + for ( length($dev) .. $length-1) { + $dev .= int(rand(10)); + } + return $dev; +} + +sub _hex($value, $length=4) { + my $hex=$value; + for ( length($hex) .. $length-1) { + $hex .= chr(ord('a')+int(rand(7))); + } + return $hex; +} +sub _create_mock_devices_kvm($vm, $n_devices, $type, $value="fff:fff") { + $vm->run_command("mkdir","-p",$PATH) if !$vm->file_exists($PATH); + + my $name = base_domain_name()."_${type}_KVM "; + for my $n ( 1 .. $n_devices ) { + my $dev = _number($N_DEVICE.$n); + my $bus = _number($N_DEVICE.$n); + my $vendor = _hex($N_DEVICE.$n); + my $id = _hex($N_DEVICE.$n); + + my $file= "$PATH/${name} ".$vm->name + ." Bus $bus Device $dev: ID $vendor:$id"; + + $vm->write_file($file,"fff6f017-3417-4ad3-b05e-17ae3e1a461".int(rand(10))); + } + $N_DEVICE ++; + + return ("find $PATH/",$name); + +} + +sub _create_mock_devices($vm, $n_devices, $type, $value="fff:fff") { + + $MOCK_DEVICES=1; + + if ($vm->type eq 'KVM') { + return _create_mock_devices_kvm($vm, $n_devices, $type, $value ); + } elsif ($vm->type eq 'Void') { + return _create_mock_devices_void($vm, $n_devices, $type, $value ); + } +} + +sub _create_host_devices($node,$number, $type=undef) { + + my $vm = $node->[0]; + + my $templates = Ravada::HostDevice::Templates::list_templates($vm->type); + my ($first) = $templates->[0]; + if ($type) { + ($first) = grep { $_->{name} =~ /$type/i } @$templates; + return if !$first && $type && $vm->type eq 'Void'; + die "Error no template $type found in ".Dumper($templates) if !$first; + } + + $vm->add_host_device(template => $first->{name}); + + my $config = config_host_devices($first->{name},0); + + my ($hd, $found); + my @list_hostdev = $vm->list_host_devices(); + ($hd) = $list_hostdev[-1]; + if ($config) { + $hd->_data('list_filter' => $config); + + my %devices_nodes = $hd->list_devices_nodes(); + + $found=1; + for my $n (0 .. scalar(@$node)-1) { + my $curr_node = $node->[$n]; + my $devices = $devices_nodes{$curr_node->id}; + $found=0 unless scalar(@$devices) >= $number->[0]; + } + $MOCK_DEVICES=0; + return $hd if $found; + + } + if ( $type && !$found ) { + $hd->remove; + return; + } + diag("creating mock devices because not enough found"); + my ($list_command,$list_filter) = _create_mock_devices($node->[0], $number->[0], "USB" ); + for my $i (1..scalar(@$node)-1) { + die "Error, missing number[$i] ".Dumper($number) unless defined $number->[$i]; + _create_mock_devices($node->[$i], $number->[$i], "USB" ); + } + + $hd->_data('list_command',$list_command); + $hd->_data('list_filter',$list_filter); + + return $hd; +} + +sub test_devices_v2($node, $number, $volatile=0, $type=undef) { + _clean_devices(@$node); + my $vm = $node->[0]; + + my $hd = _create_host_devices($node, $number, $type); + return if $type && !$hd; + + die "Error: no hd found" if !$hd; + + test_assign_v2($hd,$node,$number, $volatile); + + _clean_devices(@$node); + $hd->remove(); + +} + +sub test_devices($vm, $node, $n_local=3, $n_node=3) { + + _clean_devices($vm, $node); + my ($list_command,$list_filter) = _create_mock_devices($vm, $n_local , "USB" ); + my ($list_command2,$list_filter2) = _create_mock_devices($node, $n_node , "USB" ); + + my $templates = Ravada::HostDevice::Templates::list_templates($vm->type); + my ($first) = $templates->[0]; + + $vm->add_host_device(template => $first->{name}); + my @list_hostdev = $vm->list_host_devices(); + my ($hd) = $list_hostdev[-1]; + $hd->_data('list_command',$list_command); + $hd->_data('list_filter',$list_filter); + + my $vm_name = $vm->name; + my $node_name = $node->name; + + my %devices_nodes = $hd->list_devices_nodes(); + warn Dumper(\%devices_nodes); + my %dupe; + for my $node (keys %devices_nodes) { + for my $dev (@{$devices_nodes{$node}}) { + $dupe{$dev}++; + } + } + is(scalar(keys %dupe), $n_local+ $n_node); + for my $dev (keys %dupe) { + is($dupe{$dev},1); + } + + test_assign($vm, $node, $hd, $n_local, $n_node); + + _clean_devices($vm, $node); + + $hd->remove(); + +} + +sub test_assign_v2($hd, $node, $number, $volatile=0) { + my $vm = $node->[0]; + my $base = create_domain($vm); + $base->add_host_device($hd); + $base->volatile_clones($volatile); + Ravada::Request->add_hardware( + uid => user_admin->id + ,id_domain => $base->id + ,name => 'usb controller' + ); + $base->prepare_base(user_admin); + for my $curr_node (@$node) { + $base->set_base_vm(id_vm => $curr_node->id, user => user_admin); + } + + wait_request(debug=>0); + my %found; + my %dupe; + my $n_expected = 0; + map { $n_expected+= $_ } @$number; + die Dumper($number) if !$n_expected; + + my $list_nodes = [ map { {$_->id, $_->name} } @$node]; + + my %devices_nodes = $hd->list_devices_nodes(); + my $ws = { channel => "/".$vm->id + ,login => user_admin->name + }; + my $fd; + warn $n_expected; + for my $n (1 .. $n_expected) { + + $fd = Ravada::WebSocket::_list_host_devices(rvd_front(),$ws); + + my $name = new_domain_name; + my $domain = _req_clone($base, $name); + is($domain->is_active,1) if $vm->type eq 'Void'; + check_hd_from_node($domain,\%devices_nodes); + my $hd_checked = check_host_device($domain); + next if $MOCK_DEVICES; + push(@{$dupe{$hd_checked}},($domain->name." ".$base->id)); + my $id_vm = $domain->_data('id_vm'); + $found{$id_vm}++; + + warn Dumper(\%found); + + } + warn Dumper($fd); + ok(scalar(keys %found) > 1); + test_clone_nohd($hd, $base); + test_start_in_another_node($hd, $base); + + remove_domain($base); +} + +sub test_start_in_another_node($hd, $base) { + return if $base->volatile_clones; + + my ($clone1, $clone2); + for my $clone ($base->clones) { + next if $clone->{status} ne 'active' + && !( $base->type eq 'KVM' && $MOCK_DEVICES ); + + if (!defined $clone1) { + $clone1 = $clone; + next; + } + if ($clone->{id_vm} != $clone1->{id_vm}) { + $clone2 = $clone; + last; + } + } + die "Error. I couldn't find a clone in each node" unless $clone1 && $clone2; + + _req_shutdown($clone1->{id}); + _req_clone($base); + _req_shutdown($clone2->{id}); + + #_list_locked($clone1->{id}); + _force_wrong_lock($clone1->{id_vm},$clone1->{id}); + _req_start($clone1->{id}); + + my $clone1b = Ravada::Domain->open($clone1->{id}); + is($clone1b->_data('id_vm'), $clone2->{id_vm}); + + my %devices_nodes = $hd->list_devices_nodes(); + check_hd_from_node($clone1b,\%devices_nodes); + + check_host_device($clone1b); +} + +sub _force_wrong_lock($id_vm, $id_domain) { + my $sth = connector->dbh->prepare( + "INSERT INTO host_devices_domain_locked " + ." ( id_vm, id_domain, name, time_changed )" + ." values (?, ?, ?, 0) " + ); + $sth->execute($id_vm, $id_domain, 'fake'); +} + +sub _list_locked($id_domain) { + my $sth = connector->dbh->prepare("SELECT * FROM host_devices_domain_locked WHERE id_domain=?"); + $sth->execute($id_domain); + while ( my $row = $sth->fetchrow_hashref ) { + warn Dumper($row); + } +} + +sub _req_shutdown($id) { + my $sth_locked = connector->dbh->prepare( + "SELECT * FROM host_devices_domain_locked " + ." WHERE id_domain=?" + ); + $sth_locked->execute($id); + my ($locked) = $sth_locked->fetchrow; + my $req = Ravada::Request->force_shutdown_domain( + uid => user_admin->id + ,id_domain => $id + ); + wait_request(); + return if !$locked; + sleep 4; + $req->status('requested'); + wait_request(debug => 0); +} + +sub _req_start($id) { + + my $domain = Ravada::Domain->open($id); + + _mock_start($domain); + + return if $domain->type eq 'KVM' && $MOCK_DEVICES; + + Ravada::Request->start_domain( + uid => user_admin->id + ,id_domain => $id + ); + wait_request(); +} + + +sub _req_clone($base, $name=undef) { + $name = new_domain_name() if !defined $name; + my $start=1; + $start=0 if $base->type eq 'KVM' && $MOCK_DEVICES; + my $req = Ravada::Request->clone( + uid => user_admin->id + ,id_domain => $base->id + ,name => $name + ,start => $start + ); + wait_request(debug => 0, check_error => 0); + if ($base->type eq 'KVM' && $MOCK_DEVICES) { + diag($req->error); + } else { + is($req->error, '') or confess; + } + + my $domain = rvd_back->search_domain($name); + die "Error: $name not created" if !$domain; + + _mock_start($domain); + + return $domain; +} + +sub _mock_start($domain) { + return unless $domain->type eq 'KVM' && $MOCK_DEVICES; + + eval { + $domain->_start_checks( enable_host_devices => 1 ); + }; + my $error = $@; + return if ($error && $error =~ /No host devices available/); + + warn $error if $error; + $domain->_add_host_devices(); + $domain->_data('status' => 'active'); +} + +sub test_assign($vm, $node, $hd, $n_expected_in_vm, $n_expected_in_node) { + my $base = create_domain($vm); + $base->add_host_device($hd); + Ravada::Request->add_hardware( + uid => user_admin->id + ,id_domain => $base->id + ,name => 'usb controller' + ); + $base->prepare_base(user_admin); + $base->set_base_vm(id_vm => $node->id, user => user_admin); + + my $base2 = create_domain($vm); + $base2->add_host_device($hd); + $base2->prepare_base(user_admin); + $base2->set_base_vm(id_vm => $node->id, user => user_admin); + + wait_request(); + my $found_in_node=0; + my $found_in_vm=0; + my %dupe; + + my %devices_nodes = $hd->list_devices_nodes(); + for my $n (1 .. $n_expected_in_vm+$n_expected_in_node) { + my $name = new_domain_name; + my $domain = _req_clone($base, $name); + $domain->_data('status','active'); + is($domain->is_active,1) if $vm->type eq 'Void'; + check_hd_from_node($domain,\%devices_nodes); + my $hd = check_host_device($domain); + push(@{$dupe{$hd}},($base->name." ".$base->id)); + is(scalar(@{$dupe{$hd}}),1) or die Dumper(\%dupe); + $found_in_node++ if $domain->_data('id_vm')==$node->id; + $found_in_vm++ if $domain->_data('id_vm')==$vm->id; + } + ok($found_in_node,"Expecting in node, found $found_in_node"); + ok($found_in_vm,"Expecting in node, found $found_in_vm"); + is($found_in_node, $n_expected_in_node); + is($found_in_vm, $n_expected_in_vm); + + test_clone_nohd($hd, $base); + + remove_domain($base2, $base); +} + +sub check_hd_from_node($domain, $devices_node) { + my $id_vm = $domain->_data('id_vm'); + is($domain->_vm->id,$id_vm); + + my @devices = $domain->list_host_devices_attached(); + my @locked = grep { $_->{is_locked} } @devices; + + ok(@locked) or return; + is(scalar(@locked),1) or die Dumper(\@locked); + my ($locked) = @locked; + + my $vm = Ravada::VM->open($id_vm); + my $devices = $devices_node->{$vm->id}; + + my ($match) = grep { $_ eq $locked->{name} } @$devices; + ok($match,"Expecting $locked->{name} in ".Dumper($devices)) or confess; +} + +sub _count_devices($hd) { + my %devices_nodes = $hd->list_devices_nodes(); + my $n = 0; + for my $id ( keys %devices_nodes) { + $n+= scalar( @{$devices_nodes{$id}}); + } + return $n; +} + +sub test_clone_nohd($hd, $base) { + return if $MOCK_DEVICES; + + my ($clone_hd) = $base->clones; + + my ($name, $req, $domain0); + for ( 1 .. _count_devices($hd) ) { + diag("trying to overflow"); + $name = new_domain_name(); + my $req0 = Ravada::Request->clone( + uid => user_admin->id + ,id_domain => $base->id + ,name => $name + ,start => 0 + ); + wait_request(); + + $domain0 = rvd_back->search_domain($name); + $req = Ravada::Request->start_domain( + uid => user_admin->id + ,id_domain => $domain0->id + ); + + wait_request( check_error => 0); + + last if $req->error; + } + like($req->error,qr/host devices/i) or exit; + Ravada::Request->refresh_machine(uid => user_admin->id, id_domain => $domain0->id); + + my $domain = rvd_back->search_domain($name); + is($domain->is_active,0); + + my $req2 = Ravada::Request->start_domain( + uid => user_admin->id + ,id_domain => $domain0->id + ,enable_host_devices => 0 + ); + + wait_request( check_error => 0); + + my $domain2 = rvd_back->search_domain($name); + is($domain2->is_active,1); + + _req_shutdown($domain2->id); + _req_shutdown($clone_hd->{id}); + + _check_no_hd_locked($domain2->id); + _check_no_hd_locked($clone_hd->{id}); + + wait_request(); + + _req_start($domain0->id); + + my $domain2b = rvd_back->search_domain($name); + is($domain2b->is_active,1) unless $domain2b->type eq 'KVM' && $MOCK_DEVICES; + check_host_device($domain2b); + + my %devices_nodes = $hd->list_devices_nodes(); + check_hd_from_node($domain2b,\%devices_nodes); +} + +sub _check_no_hd_locked($id_domain) { + my $sth = connector->dbh->prepare( + "SELECT * FROM host_devices_domain_locked " + ." WHERE id_domain=?" + ); + $sth->execute($id_domain); + my $row = $sth->fetchrow_hashref; + ok(!$row) or die Dumper($row); +} + +sub check_host_device($domain) { + my $sth = connector->dbh->prepare("SELECT * FROM host_devices_domain_locked " + ." WHERE id_domain=?"); + $sth->execute($domain->id); + my @found; + while ( my $row = $sth->fetchrow_hashref) { + push @found,($row); + } + is(scalar(@found),1) or confess "Domain ".$domain->name." should have 1 HD locked\n".Dumper(\@found); + if ($domain->type eq 'Void') { + return check_host_device_void($domain); + } else { + return check_host_device_kvm($domain); + } +} + +sub check_host_device_void($domain) { + my $doc = $domain->_load(); + my @hostdev; + for my $dev ( @{ $doc->{hardware}->{host_devices} } ) { + push @hostdev,($dev); + for my $item ( keys %$dev ) { + like($item,qr/^\w+$/); + like($dev->{$item}, qr(^[0-9a-z]+$)) or die Dumper($dev); + } + } + + is(scalar(@hostdev),1) or do { + my $vm = Ravada::VM->open($domain->_data('id_vm')); + die $domain->name." ".$vm->name; + }; + my $ret=''; + for my $key (sort keys %{$hostdev[0]}) { + $ret .= "$key: ".$hostdev[0]->{$key}; + } + return $ret; +} + +sub check_host_device_kvm($domain) { + my $doc = $domain->xml_description(); + my $xml = XML::LibXML->load_xml(string => $doc); + my ($hd_source) = $xml->findnodes("/domain/devices/hostdev/source"); + ok($hd_source) or return; + my ($vendor) = $hd_source->findnodes("vendor"); + my $vendor_id=$vendor->getAttribute('id'); + my ($product) = $hd_source->findnodes("product"); + my $product_id=$product->getAttribute('id'); + my ($address) = $hd_source->findnodes("address"); + + return "$vendor_id-$product_id-".$address->getAttribute('bus')."-" + .$address->getAttribute('device'); + +} + +sub _clean_devices(@nodes) { + my $base = base_domain_name(); + for my $vm (@nodes) { + next if !defined $vm; + $vm->run_command("mkdir","-p",$PATH) if !$vm->file_exists($PATH); + my ($out, $err) = $vm->run_command("ls",$PATH); + for my $line ( split /\n/,$out ) { + next if $line !~ /$base/; + if ($vm->is_local) { + unlink "$PATH/$line" or die "$! $PATH/$line"; + next; + } + my ($out, $err) = $vm->run_command("rm","'$PATH/$line'"); + die $err if $err; + } + } +} +######################################################### + +init(); +clean(); + +for my $vm_name (vm_names() ) { + my $vm; + eval { $vm = rvd_back->search_vm($vm_name) }; + + SKIP: { + + my $msg = "SKIPPED: $vm_name virtual manager not found ".($@ or ''); + if ($vm && $>) { + $msg = "SKIPPED: Test must run as root"; + $vm = undef; + } + + diag($msg) if !$vm; + skip($msg,10) if !$vm; + + diag("Testing host devices in $vm_name"); + + my ($node1, $node2) = remote_node_2($vm_name); + clean_remote_node($node1, $node2); + + # TODO: volatile clones + # test_devices_v2([$vm,$node1,$node2],[1,1,1],1); + + if ($vm->type eq 'KVM') { + test_devices_v2([$vm,$node1,$node2],[1,1,1], undef, 'pci'); + } + + test_devices_v2([$vm,$node1,$node2],[1,1,1]); + test_devices_v2([$vm,$node1,$node2],[1,3,1]); + test_devices_v2([$vm,$node1,$node2],[1,1,3]); + + clean_remote_node($node1, $node2); + $node1->remove(); + $node2->remove(); + + if ($ENV{TEST_LONG}) { + my $node = remote_node($vm_name) or next; + clean_remote_node($node); + test_devices($vm, $node,1,3); + test_devices($vm, $node,3,1); + + clean_remote_node($node); + $node->remove(); + } + + $MOCK_DEVICES=0; + } +} + +end(); +done_testing(); diff --git a/t/kvm/n10_nodes.t b/t/kvm/n10_nodes.t index 7b1a88ab5..ec5dcae7c 100644 --- a/t/kvm/n10_nodes.t +++ b/t/kvm/n10_nodes.t @@ -1183,8 +1183,6 @@ SKIP: { test_bases_node($vm_name, $node); test_clone_make_base($vm_name, $node); - test_migrate_back($node); - test_sync_base($vm_name, $node); test_sync_back($node); diff --git a/t/lib/Test/Ravada.pm b/t/lib/Test/Ravada.pm index 651dd9676..8c94b27d1 100644 --- a/t/lib/Test/Ravada.pm +++ b/t/lib/Test/Ravada.pm @@ -193,7 +193,10 @@ sub config_host_devices($type, $die=1) { die "Error loading $FILE_CONFIG_HOST_DEVICES $@" if $@; - die "Error: no host devices config in $FILE_CONFIG_HOST_DEVICES for $type" + $type = lc($type); + $type = 'usb' if $type =~ /usb/i; + $type = 'pci' if $type =~ /pci/i; + confess "Error: no host devices config in $FILE_CONFIG_HOST_DEVICES for $type" if ( !exists $config->{$type} || !$config->{$type} ) && $die; return $config->{$type}; } @@ -2223,7 +2226,7 @@ sub start_node($node) { for my $try ( 1 .. 3) { my $is_active; - for ( 1 .. 60 ) { + for ( 1 .. 90 ) { eval { $node->disconnect; $node->clear_netssh(); @@ -2273,11 +2276,13 @@ sub start_node($node) { $node->is_active(1); $node->enabled(1); $node2 = Ravada::VM->open(id => $node->id); - last if $node2->is_active(1) && $node2->ip && $node2->_ssh; - diag("Waiting for node ".$node2->name." active ... $_") if !($_ % 10); - $node2->disconnect(); - $node2->connect(); - $node2->clear_netssh(); + if ($node2) { + last if $node2->is_active(1) && $node2->ip && $node2->_ssh; + diag("Waiting for node ".$node2->name." active ... $_") if !($_ % 10); + $node2->disconnect(); + $node2->connect(); + $node2->clear_netssh(); + } sleep 1; } eval { $node2->run_command("hwclock","--hctosys") }; diff --git a/templates/bootstrap/navigation.html.ep b/templates/bootstrap/navigation.html.ep index 5c8fab373..e59a1cd54 100644 --- a/templates/bootstrap/navigation.html.ep +++ b/templates/bootstrap/navigation.html.ep @@ -47,6 +47,7 @@ navbar-dark bg-dark fixed-top navbar-expand-lg navbar-inverse">  <%=l 'Nodes' %>  <%=l 'Routes' %>  <%=l 'Storage' %> +  <%=l 'Host Devices' %> % } % if ($_user->can_create_networks) { diff --git a/templates/main/admin_hostdev.html.ep b/templates/main/admin_hostdev.html.ep new file mode 100644 index 000000000..d7584bfae --- /dev/null +++ b/templates/main/admin_hostdev.html.ep @@ -0,0 +1,174 @@ + + +%= include 'bootstrap/header' + +
+ %= include 'bootstrap/navigation' +
to_abs %>')" + > + + + +
+ + + + + +
+ +
+ +
+ + + + + + + + + {{hdev.name}} + + + +
+
+ This host device is configured in these virtual machines +
    +
  • {{name}}
  • +
+
+ <%=l ' Are you sure you want to remove this host device ?' %> + + +
+ + + + + +
+ list command: + filter: + +
+
    +
  • + {{nodes[node]}} + +
  • +
+ +
+ Bases + + +
+ +
+ + +
+ +
+ Machines + + +
+
+
+ + +
+
+ {{domain.name}} + {{domain.name}} + + {{domain.device}} +
+
+
+ +
+ + +
+
+
+
+ +%= include $footer +%= include 'bootstrap/scripts' + + diff --git a/templates/main/node_hostdev.html.ep b/templates/main/node_hostdev.html.ep index 675bc906c..6ecf5ddb8 100644 --- a/templates/main/node_hostdev.html.ep +++ b/templates/main/node_hostdev.html.ep @@ -1,112 +1 @@ - -
- - - - - -
- -
- -
- - - - - - {{hdev.name}} - - - -
-
- This host device is configured in these virtual machines -
    -
  • {{name}}
  • -
-
- <%=l ' Are you sure you want to remove this host device ?' %> - - -
- - - - - -
- list command: - filter: -
No devices found
-
- -
- Bases - - -
- -
- - -
- -
- Machines - - -
-
-
- - -
-
- {{domain.name}} - {{domain.name}} - - {{domain.device}} -
-
-
- -
- - -
-
+<%=l 'Host Devices' %> diff --git a/templates/main/vm_hostdev.html.ep b/templates/main/vm_hostdev.html.ep index de44793bd..4759d6189 100644 --- a/templates/main/vm_hostdev.html.ep +++ b/templates/main/vm_hostdev.html.ep @@ -11,21 +11,33 @@ ng-disabled="showmachine.is_active" ng-model="hdev.is_attached" ng-click="toggle_host_device(hdev.id)"/> {{hdev.name}} -