From 25c69638056785ed6f82731142786a3eae50ef42 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Fri, 9 May 2025 12:45:50 +0200 Subject: [PATCH 001/141] test: check base only in node --- t/nodes/10_basic.t | 416 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 398 insertions(+), 18 deletions(-) diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index dd5007d70..0e986dcfa 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -89,7 +89,7 @@ sub test_remove($clone, $node) { for my $file ( @volumes ) { ok( -e $file, "Expecting file '$file' in localhost"); - my ($out, $err) = $node->run_command("ls $file"); + my ($out, $err) = $node->run_command("ls", $file); ok($out, "Expecting file '$file' in ".$node->name) or exit; } @@ -97,7 +97,7 @@ sub test_remove($clone, $node) { $clone->remove(user_admin); for my $file ( @volumes ) { ok(! -e $file, "Expecting no file '$file' in localhost") or exit; - my ($out, $err) = $node->run_command("ls $file"); + my ($out, $err) = $node->run_command("ls", $file); ok(!$out, "Expecting no file '$file' in ".$node->name) or exit; } } @@ -349,11 +349,29 @@ sub test_removed_remote_swap($vm, $node) { $base->remove(user_admin); } +sub _req_clone($base) { + my $name = new_domain_name(); + my $req = Ravada::Request->clone( + uid => user_admin->id + ,name => $name + ,id_domain => $base->id + ); + wait_request(); + is($req->error,'') or exit; + my ($clone0) = grep { $_->{name} eq $name } $base->clones; + return Ravada::Domain->open($clone0->{id}); +} + sub test_removed_base_file($vm, $node) { diag("Testing removed base in ".$vm->type); my $base = create_domain($vm); $base->prepare_base(user_admin); - $base->set_base_vm(node => $node, user => user_admin); + Ravada::Request->set_base_vm( + uid => user_admin->id + ,id_domain => $base->id + ,id_vm => $node->id + ); + wait_request(); is($base->base_in_vm($node->id),1); for my $file ( $base->list_files_base ) { @@ -363,8 +381,9 @@ sub test_removed_base_file($vm, $node) { my $found_req; my $found_clone; for my $try ( 1 .. 20 ) { - my $clone1 = $base->clone(name => new_domain_name, user => user_admin); - $clone1->start(user_admin); + my $clone1 = _req_clone($base); + Ravada::Request->start_domain(uid => user_admin->id, id_domain => $clone1->id); + wait_request(); $found_clone = $clone1; my @req = $base->list_requests(); next if !scalar @req; @@ -881,7 +900,7 @@ sub test_remove_base($vm, $node, $volatile) { my @volumes = $base->list_files_base(); $base->set_base_vm(node => $node, user => user_admin); for my $file ( @volumes ) { - my ($out, $err) = $node->run_command("ls $file"); + my ($out, $err) = $node->run_command("ls", $file); ok($out, "Expecting file '$file' in ".$node->name) or exit; } @@ -903,7 +922,7 @@ sub test_remove_base($vm, $node, $volatile) { for my $file ( @volumes ) { ok(!-e $file, "Expecting no file '$file' in local") or exit; - my ($out, $err) = $node->run_command("ls $file"); + my ($out, $err) = $node->run_command("ls", $file); ok(!$out, "Expecting no file '$file' in ".$node->name) or exit; } @@ -1174,16 +1193,26 @@ sub test_pc_other($vm, $node) { } -sub test_fill_memory($vm, $node, $migrate) { - #TODO: Void VMs - return if $vm->type eq 'Void'; +sub _check_files_exist($domain) { + for my $file ($domain->list_volumes()) { + ok($domain->_vm->file_exists($file),"Expecting in ".$domain->_vm->name + ." file exists $file") or exit; + } +} + +sub test_fill_memory($vm, $node, $migrate, $start=0) { diag("Testing fill memory ".$vm->type.", migrate=$migrate"); - my $base = rvd_back->search_domain($BASE_NAME); - $base = import_domain('KVM', $BASE_NAME, 1) if !$base; - if (!$base) { - diag("SKIPPING: base $BASE_NAME must be installed to test"); - return; + my $base; + if ($vm->type eq 'Void') { + $base = create_domain_v2(vm => $vm, swap => 1 , data => 1); + } else { + $base = rvd_back->search_domain($BASE_NAME); + $base = import_domain($vm->type, $BASE_NAME, 1) if !$base; + if (!$base) { + diag("SKIPPING: base $BASE_NAME must be installed to test"); + return; + } } $base->prepare_base(user_admin) if !$base->is_base; Ravada::Request->set_base_vm(id_vm => $node->id @@ -1203,6 +1232,7 @@ sub test_fill_memory($vm, $node, $migrate) { my $error; my %nodes; my @clones; + my $created_in_node=0; for ( 1 .. 100 ) { my $clone_name = new_domain_name(); diag("Try $_ , $clone_name may go to ".$node->name); @@ -1211,14 +1241,24 @@ sub test_fill_memory($vm, $node, $migrate) { ,id_owner => user_admin->id ,id_base => $base->id ,memory => int($memory) + ,start => $start ); - wait_request(debug => 0); - is($req->error, ''); + wait_request(debug => 0, check_error => 0); + like($req->error, qr/^(No free memory|$)/); is($req->status,'done'); push @clones,($clone_name); my $clone = rvd_back->search_domain($clone_name) or last; ok($clone,"Expecting clone $clone_name") or exit; - $clone->migrate($node) if $migrate; + $created_in_node++ if $clone->_data('id_vm') == $node->id; + diag($clone->name." ".$clone->_data('id_vm')." [node=".$node->id."]"); + _check_files_exist($clone); + + Ravada::Request->migrate( uid => user_admin->id + ,id_domain => $clone->id + ,id_node => $node->id + ,shutdown => 1 + ,shutdown_timeout => 1 + ) if $migrate; wait_request(debug => 0); eval { $clone->start(user_admin) }; $error = $@; @@ -1235,11 +1275,58 @@ sub test_fill_memory($vm, $node, $migrate) { $memory = int($memory*1.5); } } + ok($created_in_node,"Expecting some clones created in node ".$node->name) or exit; ok(exists $nodes{$vm->name},"Expecting some clones to node ".$vm->name." ".$vm->id); ok(exists $nodes{$node->name},"Expecting some clones to node ".$node->name." ".$node->id) or exit; + + my ($clone) = grep { $_->{id_vm} == $node->id } $base->clones; + test_rsync_back($vm, $clone); + _remove_clones($base); } +sub test_rsync_back($vm, $clone) { + if ($clone->{id_vm} == $vm->id) { + diag("Warning: ".$clone->{name}." already in node ".$vm->name); + return; + } + Ravada::Request->force_shutdown(uid => user_admin->id + ,id_domain => $clone->{id} + ); + wait_request(); + my $req_back; + my $clone2 = Ravada::Domain->open($clone->{id}); + my $node = $clone2->_vm; + for my $req ( $clone2->list_requests) { + if ($req->command eq 'rsync_back') { + $req_back = $req; + $req->run_at(0); + } + } + $req_back = Ravada::Request->rsync_back( + uid =>user_admin->id + ,id_domain => $clone->{id} + ,id_node => $vm->id + ) if !$req_back; + wait_request( debug => 0); + is($req_back->error,''); + + $clone2 = Ravada::Domain->open($clone->{id}); + is($clone2->_data('id_vm'), $node->id); + my @instances = $clone2->list_instances(); + is(scalar(@instances),2,"Expecting 2 instances"); + + $req_back->status('requested'); + wait_request( debug => 0); + is($req_back->error,''); + + $clone2 = Ravada::Domain->open($clone->{id}); + is($clone2->_data('id_vm'), $node->id); + + @instances = $clone2->list_instances(); + is(scalar(@instances),2,"Expecting 2 instances"); +} + sub test_migrate($vm, $node) { diag("Test migrate"); @@ -1685,6 +1772,285 @@ sub test_network($vm, $node) { my $node_nets = $node->list_virtual_networks(); } +sub test_volumes_exist($domain, $node, $expected=1) { + + my @volumes = $domain->list_volumes(); + my @files_base = $domain->list_files_base(); + for my $file ( @volumes, @files_base) { + my $curr_expected = $expected; + $curr_expected = 1 if $file =~ /\.iso$/; + is(( $node->file_exists($file) or 0), $curr_expected,"Expected in ".$node->name." =$curr_expected " + .$file) or confess; + } +} + +sub _req_create($vm, $start=0) { + my $name = new_domain_name(); + my $req = Ravada::Request->create_domain( + name => $name + ,id_vm => $vm->id + ,id_owner => user_admin->id + ,id_iso => search_id_iso('Alpine%64') + ,disk => 10240 + ,swap => 10240 + ,data => 10240 + ,start => $start + ); + wait_request(); + is($req->error,''); + my $base = rvd_back->search_domain($name); + ok($base) or return; + is($base->_vm->id, $vm->id); + + return $base; +} + +sub test_start_remote($domain) { + my $node = $domain->_vm; + die "Not remote ".$node->name if $node->is_local(); + + my $req = Ravada::Request->start_domain( + uid => user_admin->id + ,id_domain => $domain->id + ); + wait_request(); + is($req->error,''); + + my $domain2 = Ravada::Domain->open($domain->id); + is($domain2->_vm->id, $node->id); +} + +sub test_shutdown_remote($domain) { + my $node = $domain->_vm; + die "Not remote ".$node->name if $node->is_local(); + + my $req = Ravada::Request->force_shutdown( + uid => user_admin->id + ,id_domain => $domain->id + ); + wait_request(); + is($req->error,''); + test_no_rsync_back($domain->id); + + my $domain2 = Ravada::Domain->open($domain->id); + is($domain2->_vm->id, $node->id); + + test_no_rsync_back($domain->id); + + for ( 1 .. 10 ) { + is($domain2->is_active,0 ); + last if !$domain2->is_active; + diag("Waiting for ".$domain2->name." is down"); + sleep 1; + } + Ravada::Request->refresh_machine( + uid => user_admin->id + ,id_domain => $domain->id + ); + wait_request(); + + test_no_rsync_back($domain->id); +} + +sub test_no_rsync_back($id_domain) { + my $sth = connector->dbh->prepare( + "SELECT * FROM requests WHERE id_domain=?" + ." AND command='rsync_back'" + ); + $sth->execute($id_domain); + + my $row = $sth->fetchrow_hashref; + ok(!$row) or exit; +} +sub test_prepare_base_remote($base) { + my $node = $base->_vm; + die "Not remote ".$node->name if $node->is_local(); + + Ravada::Request->force_shutdown( + uid => user_admin->id + ,id_domain => $base->id + ); + my $req = Ravada::Request->prepare_base( + uid => user_admin->id + ,id_domain => $base->id + ); + wait_request($req); + is($req->status,'done'); + is($req->error,''); + + my $vm_local = $base->_vm->new(host => 'localhost'); + my $not_found = $vm_local->search_domain($base->name); + ok(!$not_found,"Expecting ".$base->name." not found in ".$vm_local->name); + + test_volumes_exist($base,$node,1); + test_volumes_exist($base,$vm_local,0); +} + +sub test_clone_only_remote($base,$start, $volatile=0) { + my $node = $base->_vm; + die "Not remote ".$node->name if $node->is_local(); + + my $name = new_domain_name(); + + my $req = Ravada::Request->clone( + uid => user_admin->id + ,id_domain => $base->id + ,name => $name + ,start => $start + ,volatile => $volatile + ); + wait_request($req); + is($req->status,'done'); + is($req->error,'') or confess; + + my $vm_local = rvd_back->search_vm($node->type,'localhost'); + + my $clone_local = $vm_local->search_domain($name); + ok(!$clone_local); + + my $clone = rvd_back->search_domain($name); + ok($clone) or return; + is($clone->_vm->id , $node->id); + + return $clone; +} +sub test_migrate_clone($node1, $node2) { + my $base = _req_create($node1); + Ravada::Request->prepare_base(uid => user_admin->id + ,id_domain => $base->id + ); + wait_request(); + + is($base->base_in_vm($node1->id),1); + is($base->base_in_vm($node2->id),0); + + my $clone = _req_clone($base); + + my $req=Ravada::Request->migrate( + uid => user_admin->id + ,id_domain => $clone->id + ,id_node => $node2->id + ); + wait_request(debug => 0); + is($req->error,''); + + is($base->base_in_vm($node1->id),1); + is($base->base_in_vm($node2->id),1); + + my $clone2 = Ravada::Domain->open(id => $clone->id); + is($clone2->_data('id_vm'),$node2->id, "Expecting ".$clone2->name." in ".$node2->name) or die; + my @instances = $clone2->list_instances(); + my ($i_n1) = grep { $_->{id_vm} == $node1->id } @instances; + ok($i_n1,"Expecting instance in ".$node1->name); + my ($i_n2) = grep { $_->{id_vm} == $node2->id } @instances; + ok($i_n2,"Expecting instance in ".$node2->name); + is(@instances,2) or exit; + + test_remove_instances($clone, $node1, $node2); + test_remove_instances($base, $node1, $node2); +} + +sub test_migrate_standalone($node1, $node2) { + + my $domain = _req_create($node1, 0); # create and not start + my $req = Ravada::Request->migrate( + uid => user_admin->id + ,id_domain => $domain->id + ,id_node => $node2->id + ); + wait_request(); + + test_volumes_exist($domain, $node1,1); + test_volumes_exist($domain, $node2,1); + + test_remove_instances($domain, $node1, $node2); +} +sub test_spinoff_remote($vm, $node) { + + my $base = _req_create($node); + Ravada::Request->prepare_base(uid => user_admin->id + ,id_domain => $base->id + ); + wait_request(); + my $clone = test_clone_only_remote($base, 1); + + my $req_spinoff = Ravada::Request->spinoff( + uid => user_admin->id + ,id_domain => $clone->id + ); + wait_request(); + is($req_spinoff->error,''); + + my $clone2 = Ravada::Domain->open($clone->id); + is($clone2->id_base, undef); + + test_remove_instances($clone, $vm, $node); + test_remove_instances($base, $vm, $node); +} +sub test_base_only_in_node($vm, $node, $start=0) { + + diag("Test base only in node , start=$start"); + + my $base = _req_create($node,$start); + + my $not_found = $vm->search_domain($base->name); + ok(!$not_found); + + test_volumes_exist($base,$node,1); + test_volumes_exist($base,$vm,0); + + test_start_remote($base); + test_shutdown_remote($base); + + test_remove_instances($base, $vm, $node); + + $base = _req_create($node, $start); + test_prepare_base_remote($base); + + my $clone = test_clone_only_remote($base, $start); + + if ( $clone ) { + test_start_remote($clone); + + my $clone2 = test_clone_only_remote($clone, $start); + test_remove_instances($clone2) if $clone2; + + test_remove_instances($clone); + } + + my $clone_volatile = test_clone_only_remote($base, $start, 1); + test_remove_instances($clone_volatile) if $clone_volatile; + + test_remove_instances($base, $vm, $node); + +} + +sub test_remove_instances($base, @nodes) { + my @volumes = $base->list_volumes(); + my @files_base = $base->list_files_base(); + $base->remove(user_admin); + for my $file ( @volumes, @files_base ) { + if ($file =~ /\.iso$/) { + ok(-e $file); + } else { + ok(! -e $file, "Expecting no file '$file' in localhost") or exit; + } + for my $node (@nodes) { + + if ($file =~ /\.iso$/) { + ok($node->file_exists($file)); + next; + } + ok(!$node->file_exists($file)); + my ($out, $err) = $node->run_command("ls", $file); + ok(!$out, "Expecting no file '$file' in ".$node->name) or exit; + + } + } + +} + + ################################################################################## if ($>) { @@ -1735,6 +2101,20 @@ for my $vm_name (reverse vm_names() ) { start_node($node); + test_migrate_clone($node, $vm); + test_migrate_clone($vm, $node); + + test_spinoff_remote($vm, $node); + + test_migrate_standalone($vm, $node); + test_migrate_standalone($node, $vm); + + test_base_only_in_node($vm, $node, 1); #start after create = 1 + test_base_only_in_node($vm, $node); + goto NEXT; + + test_removed_base_file($vm, $node); + test_volatile_req($vm, $node); test_domain_gone($vm, $node); From 801b997238fd6bfcbd171afd068c0bf0cf7e9121 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Fri, 9 May 2025 16:16:45 +0200 Subject: [PATCH 002/141] wip: Remove base in nodes --- lib/Ravada/Domain.pm | 52 ++++++++++++++++-------------------------- lib/Ravada/VM.pm | 54 ++++---------------------------------------- t/nodes/10_basic.t | 26 ++++++--------------- 3 files changed, 31 insertions(+), 101 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 7a81c5ea3..767e87c5d 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -419,10 +419,12 @@ sub _around_start($orig, $self, @arg) { && $error !~ /Could not run .*swtpm/i && $error !~ /virtiofs/ && $error !~ /child process/i - && $error !~ /host doesn.t support/ - && $error !~ /device not found/ ; + if ($error && $self->is_known && $self->id_base && !$self->is_local && $self->_vm->enabled) { + $self->_request_set_base(); + next; + } die $error; } $self->_post_start(%arg); @@ -594,13 +596,8 @@ sub _start_checks($self, @args) { # 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($id_vm)) { - $vm = $vm_local; - $id_vm = undef; - } + die "Error: node ".$vm->name." not available.\n" + if !$vm->enabled || !$vm->ping; } # if it is a clone ( it is not a base ) @@ -610,7 +607,6 @@ sub _start_checks($self, @args) { if ( !$vm->is_alive ) { $vm->disconnect(); $vm->connect; - $vm = $vm_local if !$vm->is_local && !$vm->is_alive; die "Error: node ".$vm->name." is not alive" if !$vm->is_alive; }; if ($id_vm) { @@ -967,10 +963,6 @@ sub _around_prepare_base($orig, $self, @args) { } $self->_pre_prepare_base($user, $request); - if (!$self->is_local) { - my $vm_local = $self->_vm->new( host => 'localhost' ); - $self->_vm($vm_local); - } $self->pre_prepare_base(); my @base_img = $self->$orig($with_cd); @@ -1079,11 +1071,6 @@ sub _pre_prepare_base($self, $user, $request = undef ) { sleep 1; } } - # $self->_post_remove_base(); - if (!$self->is_local) { - my $vm_local = Ravada::VM->open( type => $self->vm ); - $self->migrate($vm_local, $request); - } $self->_check_free_space_prepare_base(); } @@ -1210,14 +1197,17 @@ sub _check_tmp_volumes($self) { if !$self->id_base; my $vm = $self->_vm; - my $base = Ravada::Domain->open($self->id_base); - my @volumes = $base->list_files_base_target; for my $vol ( $self->list_volumes_info) { - next unless $vol->file && $vol->file =~ /\.(TMP|SWAP)\.\w+$/; - my ($file_base) = grep { $_->[1] eq $vol->info->{target} } @volumes; - next if !$file_base; + next unless $vol->file && $vol->file =~ /\.(TMP|SWAP)\./; $vol->delete(); + my $base = Ravada::Domain->open($self->id_base); + my @volumes = $base->list_files_base_target; + my ($file_base) = grep { $_->[1] eq $vol->info->{target} } @volumes; + if (!$file_base) { + warn "Error: I can't find base volume for target ".$vol->info->{target} + .Dumper(\@volumes); + } my $vol_base = Ravada::Volume->new( file => $file_base->[0] , is_base => 1 , vm => $vm @@ -1497,9 +1487,6 @@ sub _insert_display( $self, $display ) { confess Dumper($display) if $display->{driver} =~ /-tls/ && !$display->{is_secondary}; - $display->{listen_ip} = $self->_vm->ip - if !exists $display->{listen_ip} || !$display->{listen_ip}; - lock_hash(%$display); $self->_clean_display_order($display->{n_order}) if $display->{n_order}; @@ -2661,7 +2648,10 @@ sub _remove_domain_cascade($self,$user, $cascade = 1) { next if $instance->{id_vm} == $self->_vm->id; my $vm; eval { $vm = Ravada::VM->open($instance->{id_vm}) }; - die $@ if $@ && $@ !~ /I can't find VM ||libvirt error code: 38,/i; + if ( $@ && $@ !~ /I can't find VM ||libvirt error code: 38,/i ) { + warn ''.$@ if $@; + die $@; + } my $domain; $@ = ''; eval { $domain = $vm->search_domain($domain_name) } if $vm; @@ -2751,7 +2741,7 @@ sub _remove_files_base { for my $file ( $self->list_files_base ) { next if $file =~ /\.iso$/; - unlink $file or die "$! $file" if -e $file; + $self->_vm->remove_file($file) if $self->_vm->file_exists($file); } } @@ -3197,10 +3187,6 @@ sub clone { my $vm = $self->_vm; if ($volatile) { $vm = $vm->balance_vm($uid, $self); - } elsif( !$vm->is_local ) { - for my $node ($self->_vm->list_nodes) { - $vm = $node if $node->is_local; - } } my $clone; diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index 2c61671a1..1f1b3b4ed 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -318,7 +318,6 @@ sub BUILD { } $self->id; - $self->_which_cache_fetch(); } sub _open_type { @@ -480,7 +479,8 @@ sub _around_create_domain { my %args_create = %args; my $id_owner = delete $args{id_owner} or confess "ERROR: Missing id_owner"; - my $owner = Ravada::Auth::SQL->search_by_id($id_owner) or confess "Unknown user id: $id_owner"; + my $owner = Ravada::Auth::SQL->search_by_id($id_owner); + die "Unknown user id: $id_owner" if !$owner; my $base; my $volatile = delete $args{volatile}; my $id_base = delete $args{id_base}; @@ -512,9 +512,7 @@ sub _around_create_domain { $self->_check_duplicate_name($name, $volatile); if ($id_base) { - my $vm_local = $self; - $vm_local = $self->new( host => 'localhost') if !$vm_local->is_local; - $base = $vm_local->search_domain_by_id($id_base) + $base = $self->search_domain_by_id($id_base) or confess "Error: I can't find domain $id_base on ".$self->name; die "Error: user ".$owner->name." can not clone from ".$base->name @@ -1854,7 +1852,6 @@ sub _do_is_active($self, $force=undef) { } sub _cached_active($self, $value=undef) { - $self->_which_cache_flush() if defined $value && $value && !$self->_data('is_active'); return $self->_data('is_active', $value); } @@ -2819,50 +2816,9 @@ sub _list_qemu_bridges($self) { return keys %bridge; } -sub _which_cache_fetch($self) { - my $sth = $self->_dbh->prepare( - "SELECT command,path FROM vm_which " - ." WHERE id_vm=?" - ); - $sth->execute($self->id); - while (my ($command, $path)) { - $self->{_which}->{$command} = $path; - } - $sth->finish; -} - - -sub _which_cache_get($self, $command) { - return $self->{_which}->{$command} if exists $self->{_which} && exists $self->{_which}->{$command}; -} - -sub _which_cache_set($self, $command, $path) { - $self->{_which}->{$command} = $path; - - eval { - my $sth = $self->_dbh->prepare( - "INSERT INTO vm_which (id_vm, command, path)" - ." VALUES (?,?,?) " - ); - $sth->execute($self->id, $command, $path); - }; - warn("Warning: $@ vm_which = ( ".$self->id.", $command, $path )") - if $@ && $@ !~ /Duplicate entry/i - && $@ !~ /UNIQUE constraint failed/i - ; -} - -sub _which_cache_flush($self) { - my $sth = $self->_dbh->prepare( - "DELETE FROM vm_which where id_vm=?" - ); - $sth->execute($self->id); -} - sub _which($self, $command) { - my $cached = $self->_which_cache_get($command); - return $cached if $cached; + return $self->{_which}->{$command} if exists $self->{_which} && exists $self->{_which}->{$command}; my $bin_which = $self->{_which}->{which}; if (!$bin_which) { @@ -2879,8 +2835,8 @@ sub _which($self, $command) { my ($out,$err) = $self->run_command(@cmd); chomp $out; - $self->_which_cache_set($command,$out); + $self->{_which}->{$command} = $out; return $out; } diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index 0e986dcfa..ab92c9a7d 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -1940,11 +1940,7 @@ sub test_migrate_clone($node1, $node2) { my $clone2 = Ravada::Domain->open(id => $clone->id); is($clone2->_data('id_vm'),$node2->id, "Expecting ".$clone2->name." in ".$node2->name) or die; my @instances = $clone2->list_instances(); - my ($i_n1) = grep { $_->{id_vm} == $node1->id } @instances; - ok($i_n1,"Expecting instance in ".$node1->name); - my ($i_n2) = grep { $_->{id_vm} == $node2->id } @instances; - ok($i_n2,"Expecting instance in ".$node2->name); - is(@instances,2) or exit; + is(@instances,1) or exit; test_remove_instances($clone, $node1, $node2); test_remove_instances($base, $node1, $node2); @@ -2029,22 +2025,14 @@ sub test_remove_instances($base, @nodes) { my @volumes = $base->list_volumes(); my @files_base = $base->list_files_base(); $base->remove(user_admin); - for my $file ( @volumes, @files_base ) { - if ($file =~ /\.iso$/) { - ok(-e $file); - } else { - ok(! -e $file, "Expecting no file '$file' in localhost") or exit; - } - for my $node (@nodes) { - + my $vm = $base->_vm; + for my $vm ( $base->_vm, @nodes ) { + for my $file ( @volumes, @files_base ) { if ($file =~ /\.iso$/) { - ok($node->file_exists($file)); - next; + ok($vm->file_exists($file)); + } else { + ok(!$vm->file_exists($file), "Expecting no file '$file' in ".$vm->name) or exit; } - ok(!$node->file_exists($file)); - my ($out, $err) = $node->run_command("ls", $file); - ok(!$out, "Expecting no file '$file' in ".$node->name) or exit; - } } From d0a7b9cfe5e53057c1b2249fe2b385241f2a8460 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 12 May 2025 11:40:58 +0200 Subject: [PATCH 003/141] wip: create remote base, clone and manage instances --- lib/Ravada/Domain.pm | 50 +++++++++++++++++++++++++++++++------- lib/Ravada/VM.pm | 8 ++++-- lib/Ravada/Volume/Class.pm | 1 + lib/Ravada/Volume/QCOW2.pm | 2 +- t/nodes/10_basic.t | 5 ++-- 5 files changed, 52 insertions(+), 14 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 767e87c5d..8e31ab43c 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -1119,8 +1119,6 @@ sub spinoff { my $self = shift; $self->_do_force_shutdown() if $self->is_active; - confess "Error: spinoff from remote nodes not available. Node: ".$self->_vm->name - if !$self->is_local; for my $volume ($self->list_volumes_info ) { next if !$volume->file || $volume->file =~ /\.iso$/i; @@ -2640,6 +2638,10 @@ sub _remove_domain_cascade($self,$user, $cascade = 1) { my $domain_name = $self->name or confess "Unknown my self name $self ".Dumper($self->{_data}); my @instances = $self->list_instances(); + my $bases_vm = $self->_bases_vm(); + for my $id_vm ( keys %$bases_vm) { + push @instances,( { id_vm => $id_vm }); + } return if !scalar(@instances); my $sth_delete = $$CONNECTOR->dbh->prepare("DELETE FROM domain_instances " @@ -3154,6 +3156,18 @@ sub clone { return $self->_clone_from_pool(@_) if $from_pool; + if ($self->is_active) { + warn $self->name." is active, req shutdown"; + my $req = Ravada::Request->shutdown(uid => $user->id + ,id_domain => $self->id + ); + if ($request && $req) { + $request->after_request($req->id); + $request->retry(2); + } + die "Warning: virtual machine ".$self->name." is active when cloning. Retry.\n"; + } + my %args2 = @_; delete $args2{from_pool}; return $self->_copy_clone(%args2) if !$self->is_base && $self->id_base(); @@ -3280,8 +3294,9 @@ sub _copy_volumes($self, $copy) { my %volumes = map { $_->info->{target} => $_->file } @volumes; my %copy_volumes = map { $_->info->{target} => $_->file } @copy_volumes; for my $target (keys %volumes) { - copy($volumes{$target}, $copy_volumes{$target}) - or die "$! $volumes{$target}, $copy_volumes{$target}" + my $dst = $copy_volumes{$target}; + $self->_vm->remove_file($dst) if $self->_vm->file_exists($dst); + $self->_vm->copy_file($volumes{$target}, $copy_volumes{$target}); } } @@ -4938,10 +4953,11 @@ sub clean_swap_volumes { next if !$self->_vm->file_exists($vol->file); my $backing_file; eval { $backing_file = $vol->backing_file }; - confess $@ if $@ && $@ !~ /No backing file/i; + my $error = $@; + confess $error if $error && $error !~ /No backing file/i; next if !$backing_file; next if !$self->_vm->file_exists($backing_file); - $vol->restore() if !$@; + $vol->restore() if !$error; } } } @@ -5439,7 +5455,6 @@ sub _rsync_volumes_back($self, $node, $request=undef) { $request->status("syncing") if $request; $request->error($msg) if $request; warn "$msg\n" if $DEBUG_RSYNC; - my $t0 = time; $rsync->exec(src => 'root@'.$node->host.":".$file ,dest => $file ); if ( $rsync->err ) { $request->status("done",join(" ",@{$rsync->err})) if $request; @@ -5466,7 +5481,9 @@ sub _pre_migrate($self, $node, $request = undef) { if !$base->base_in_vm($node->id); confess "ERROR: base id ".$self->id_base." not found." if !$base; - return unless $self->_check_all_parents_in_node($node); + if ( ! $self->_check_all_parents_in_node($node, $request) ) { + die "Warning: base not in node. Retry.\n"; + } } $node->_add_instance_db($self->id); @@ -5477,6 +5494,7 @@ sub _post_migrate($self, $node, $request = undef) { $self->_vm($node); $self->_update_id_vm(); + $node->_add_instance_db($node->id); # TODO: update db instead set this value $self->{_migrated} = 1; @@ -5607,7 +5625,7 @@ sub set_base_vm($self, %args) { return $self->_set_base_vm_db($vm->id, $value); } -sub _check_all_parents_in_node($self, $vm) { +sub _check_all_parents_in_node($self, $vm, $request=undef) { my @bases; my $base = $self; for ( ;; ) { @@ -5618,10 +5636,20 @@ sub _check_all_parents_in_node($self, $vm) { } return 1 if !@bases; my $req; + if ($request && $request->after_request) { + $req = Ravada::Request->open($request->after_request); + } for my $base ( reverse @bases) { $base->_set_base_vm_db($vm->id,0); my @after_req; @after_req = ( after_request_ok => $req->id) if $req; + $req = Ravada::Request->migrate( + uid => Ravada::Utils::user_daemon->id + ,id_domain => $base->id + ,id_node => $vm->id + ,@after_req + ); + @after_req = ( after_request_ok => $req->id) if $req; $req = Ravada::Request->set_base_vm( uid => Ravada::Utils::user_daemon->id ,id_domain => $base->id @@ -5629,6 +5657,9 @@ sub _check_all_parents_in_node($self, $vm) { ,@after_req ); } + if ($request) { + $request->after_request($req->id); + } return 0; } @@ -7364,6 +7395,7 @@ sub list_instances($self, $id=undef) { lock_hash(%$row); push @instances, ( $row ); } + return @instances; } diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index 1f1b3b4ed..d1ce74ee2 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -683,10 +683,14 @@ sub _set_alias_unique($self, $alias) { } sub _add_instance_db($self, $id_domain) { + _add_instance_db_data($self->id, $id_domain); +} + +sub _add_instance_db_data($id_vm, $id_domain) { my $sth = $$CONNECTOR->dbh->prepare("SELECT * FROM domain_instances " ." WHERE id_domain=? AND id_vm=?" ); - $sth->execute($id_domain, $self->id); + $sth->execute($id_domain, $id_vm); my ($row) = $sth->fetchrow; return if $row; @@ -694,7 +698,7 @@ sub _add_instance_db($self, $id_domain) { ." VALUES (?, ?)" ); eval { - $sth->execute($id_domain, $self->id); + $sth->execute($id_domain, $id_vm); }; confess $@ if $@; } diff --git a/lib/Ravada/Volume/Class.pm b/lib/Ravada/Volume/Class.pm index 4355d5a3d..4d98415aa 100644 --- a/lib/Ravada/Volume/Class.pm +++ b/lib/Ravada/Volume/Class.pm @@ -96,6 +96,7 @@ sub copy_file($self, $src, $dst) { my @cmd = ('/bin/cp' ,$src, $dst ); my ($out, $err) = $self->vm->run_command(@cmd); die $err if $err; + return $dst; } sub backup($self) { diff --git a/lib/Ravada/Volume/QCOW2.pm b/lib/Ravada/Volume/QCOW2.pm index 9d718039c..d00c05bb3 100644 --- a/lib/Ravada/Volume/QCOW2.pm +++ b/lib/Ravada/Volume/QCOW2.pm @@ -63,7 +63,7 @@ sub clone($self, $file_clone) { my $n = 10; for (;;) { my @stat = stat($self->file); - last if time-$stat[9] || $n--<0; + last if @stat || !defined $stat[9] || time-$stat[9] || $n--<0; sleep 1; die "Error: ".$self->file." looks active" if $n-- <0; } diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index ab92c9a7d..48c3b4171 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -1914,6 +1914,7 @@ sub test_clone_only_remote($base,$start, $volatile=0) { return $clone; } + sub test_migrate_clone($node1, $node2) { my $base = _req_create($node1); Ravada::Request->prepare_base(uid => user_admin->id @@ -1940,7 +1941,7 @@ sub test_migrate_clone($node1, $node2) { my $clone2 = Ravada::Domain->open(id => $clone->id); is($clone2->_data('id_vm'),$node2->id, "Expecting ".$clone2->name." in ".$node2->name) or die; my @instances = $clone2->list_instances(); - is(@instances,1) or exit; + is(@instances,2); test_remove_instances($clone, $node1, $node2); test_remove_instances($base, $node1, $node2); @@ -1983,6 +1984,7 @@ sub test_spinoff_remote($vm, $node) { test_remove_instances($clone, $vm, $node); test_remove_instances($base, $vm, $node); } + sub test_base_only_in_node($vm, $node, $start=0) { diag("Test base only in node , start=$start"); @@ -2099,7 +2101,6 @@ for my $vm_name (reverse vm_names() ) { test_base_only_in_node($vm, $node, 1); #start after create = 1 test_base_only_in_node($vm, $node); - goto NEXT; test_removed_base_file($vm, $node); From 35d3fcf2d96f0d01029ca867b6ba3e88dcd6ef3f Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Tue, 13 May 2025 11:42:08 +0200 Subject: [PATCH 004/141] wip: balance clone on create --- lib/Ravada/VM.pm | 8 +++----- t/nodes/10_basic.t | 23 ++++++++++++----------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index d1ce74ee2..6d2bff7bc 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -551,20 +551,18 @@ sub _around_create_domain { return $base->_search_pool_clone($owner) if $from_pool; - if ($self->is_local && $base && $base->is_base && $args_create{volatile} && !$base->list_host_devices ) { + if ($base && $base->is_base) { $request->status("balancing") if $request; my $vm = $self->balance_vm($owner->id, $base); if (!$vm) { die "Error: No free nodes available.\n"; } - if (!$vm->is_local) { - if ( $base->_base_files_in_vm($vm) + if ( $base->_base_files_in_vm($vm) && $base->_check_all_parents_in_node($vm)) { $self = $vm; - } } - $request->status("creating machine on ".$vm->name) if $request; + $request->status("creating machine on ".$self->name) if $request; $args_create{listen_ip} = $self->listen_ip($remote_ip); } diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index 48c3b4171..9e60acf39 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -378,7 +378,6 @@ sub test_removed_base_file($vm, $node) { $node->remove_file($file); } - my $found_req; my $found_clone; for my $try ( 1 .. 20 ) { my $clone1 = _req_clone($base); @@ -386,16 +385,15 @@ sub test_removed_base_file($vm, $node) { wait_request(); $found_clone = $clone1; my @req = $base->list_requests(); - next if !scalar @req; + my $found_req; for my $req (@req) { if($req->command eq 'set_base_vm') { $found_req = $req; last; } } - last if $found_req; + last if $found_req || $clone1->is_active; } - ok($found_req,"Expecting request to set base vm"); wait_request(); is($base->base_in_vm($node->id),1); is(scalar($base->list_vms),2) or exit; @@ -1201,7 +1199,7 @@ sub _check_files_exist($domain) { } sub test_fill_memory($vm, $node, $migrate, $start=0) { - diag("Testing fill memory ".$vm->type.", migrate=$migrate"); + diag("Testing fill memory ".$vm->type.", migrate=$migrate, start=$start"); my $base; if ($vm->type eq 'Void') { @@ -1271,16 +1269,18 @@ sub test_fill_memory($vm, $node, $migrate, $start=0) { $nodes{$clone->_vm->name}++; last if $migrate && exists $nodes{$vm->name} && $nodes{$vm->name} > 2; - if ($migrate || keys(%nodes) > 0) { - $memory = int($memory*1.5); - } } ok($created_in_node,"Expecting some clones created in node ".$node->name) or exit; ok(exists $nodes{$vm->name},"Expecting some clones to node ".$vm->name." ".$vm->id); ok(exists $nodes{$node->name},"Expecting some clones to node ".$node->name." ".$node->id) or exit; - my ($clone) = grep { $_->{id_vm} == $node->id } $base->clones; - test_rsync_back($vm, $clone); + my ($clone) = grep { $_->{id_vm} == $vm->id } $base->clones; + for my $clone0 ( $base-> clones ) { + next if $clone0->{id_vm} eq $vm->id; + my $clone0b = Ravada::Front::Domain->open($clone0->{id}); + next if $clone0b->list_instances<2; + test_rsync_back($vm, $clone); + } _remove_clones($base); } @@ -1314,7 +1314,8 @@ sub test_rsync_back($vm, $clone) { $clone2 = Ravada::Domain->open($clone->{id}); is($clone2->_data('id_vm'), $node->id); my @instances = $clone2->list_instances(); - is(scalar(@instances),2,"Expecting 2 instances"); + is(scalar(@instances),2,"Expecting 2 instances of ".$clone->{name}. "[ ".$clone->{id}." ]") + or exit; $req_back->status('requested'); wait_request( debug => 0); From 2cbbf5ac80608d95ae992f4ae3d5623809c7b943 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Tue, 13 May 2025 12:52:07 +0200 Subject: [PATCH 005/141] wip: copy file in KVM --- lib/Ravada.pm | 2 ++ lib/Ravada/Request.pm | 7 +++++-- lib/Ravada/VM/KVM.pm | 38 ++++++++++++++++++++++++++++++++++---- t/nodes/10_basic.t | 2 +- 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index 26d8fd8db..0d633389c 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -5114,6 +5114,8 @@ sub _cmd_start { $domain = $self->search_domain($name) if $name && !$id_domain; $domain = $self->search_domain_by_id($id_domain) if $id_domain; + die "Error: machine not found ".($name or $id_domain) if !$domain; + $domain->status('starting'); my $uid = $request->args('uid'); diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index 2d4b6d727..613fedd2c 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -89,7 +89,7 @@ our %VALID_ARG = ( ,set_driver => {uid => 1, id_domain => 1, id_option => 1} ,hybernate=> {uid => 1, id_domain => 1} ,download => {uid => 2, id_iso => 1, id_vm => 2, vm => 2, verbose => 2, delay => 2, test => 2} - ,refresh_storage => { id_vm => 2, uid => 2 } + ,refresh_storage => { id_vm => 2, uid => 1 } ,list_storage_pools => { id_vm => 1 , uid => 1, data => 2 } ,active_storage_pool => { uid => 1, id_vm => 1, name => 1, value => 1} ,check_storage => { uid => 1 } @@ -212,6 +212,7 @@ qw( refresh_machine refresh_machine_ports refresh_vms + refresh_storage set_time open_exposed_ports manage_pools @@ -265,7 +266,9 @@ our %COMMAND = ( ,important=> { limit => 20 ,priority => 1 - ,commands => ['clone','start','start_clones','shutdown_clones','create','open_iptables','list_network_interfaces','list_isos','ping_backend','refresh_machine'] + ,commands => ['clone','start','start_clones','shutdown_clones','create','open_iptables','list_network_interfaces','list_isos','ping_backend','refresh_machine' + ,'refresh_storage' + ] } ,iptables => { diff --git a/lib/Ravada/VM/KVM.pm b/lib/Ravada/VM/KVM.pm index 243c1d6c0..1a642f726 100644 --- a/lib/Ravada/VM/KVM.pm +++ b/lib/Ravada/VM/KVM.pm @@ -1249,11 +1249,8 @@ sub _domain_create_from_base { confess "argument id_base or base required ".Dumper(\%args) if !$args{id_base} && !$args{base}; - my $vm_local = $self; - $vm_local = $self->new( host => 'localhost') if !$vm_local->is_local; - my $base = $args{base}; - $base = $vm_local->_search_domain_by_id($args{id_base}) if $args{id_base}; + $base = $self->_search_domain_by_id($args{id_base}) if $args{id_base}; confess "Unknown base id: $args{id_base}" if !$base; my $volatile; @@ -3316,4 +3313,37 @@ sub change_network($self, $data) { return $changed; } +sub _search_pool_volume($self, $file) { + confess "ERROR: undefined file" if !defined $file; + + my ($name) = $file =~ m{.*/(.*)}; + $name = $file if !defined $name; + + my $vol; + for my $pool (_list_storage_pools($self->vm)) { + next if !$pool->is_active; + eval { $vol = $pool->get_volume_by_name($name) }; + die $@ if $@ && $@ !~ /^libvirt error code: 50,/; + return ($pool, $vol) if $vol; + } + +} + +sub copy_file($self, $orig, $dst) { + my ($sp,$vol) = $self->_search_pool_volume($orig); + die "Error: volume $orig not found" if !$vol; + + my $xml = XML::LibXML->load_xml(string => $vol->get_xml_description()); + + my ($name) = $dst =~ m{.*/(.*)}; + $xml->findnodes("/volume/name/text()")->[0]->setData($name); + $xml->findnodes("/volume/key/text()")->[0]->setData($dst); + $xml->findnodes("/volume/target/path/text()")->[0]->setData($dst); + + my $vol_dst; + eval { $vol_dst = $sp->clone_volume($xml, $vol) }; + confess $@ if $@; + +} + 1; diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index 9e60acf39..69d106ecd 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -2055,7 +2055,7 @@ clean(); $Ravada::Domain::MIN_FREE_MEMORY = 256 * 1024; my $tls; -for my $vm_name (reverse vm_names() ) { +for my $vm_name (vm_names() ) { my $vm; eval { $vm = rvd_back->search_vm($vm_name) }; From d1aaba91eed043e398dd4751c2705216eb4d0947 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 14 May 2025 15:43:26 +0200 Subject: [PATCH 006/141] wip: remove volatiles after shutdown just in case --- lib/Ravada.pm | 62 ++++++++++++++++++++++++++++++++++--------- lib/Ravada/Request.pm | 8 +++--- lib/Ravada/VM/Void.pm | 1 + script/rvd_back | 2 +- t/nodes/10_basic.t | 9 ++++--- t/vm/h10_hybernate.t | 3 ++- 6 files changed, 65 insertions(+), 20 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index 0d633389c..8bbd62c7e 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -3559,21 +3559,28 @@ sub remove_domain { my $self = shift; my %arg = @_; - my $name = delete $arg{name} or confess "Argument name required "; + my $name = delete $arg{name}; + my $id_domain = delete $arg{id_domain}; + + die "Error: Argument name or id required" if !$name && !$id_domain; confess "Argument uid required " if !$arg{uid}; lock_hash(%arg); - my $sth = $CONNECTOR->dbh->prepare("SELECT id,vm FROM domains WHERE name = ?"); - $sth->execute($name); + if (!$id_domain ) { + my $sth = $CONNECTOR->dbh->prepare("SELECT id,vm FROM domains WHERE name = ?"); + $sth->execute($name); - my ($id,$vm_type)= $sth->fetchrow; - if (!$id) { - warn "Error: Unknown domain $name, maybe already removed.\n"; - return; + ($id_domain)= $sth->fetchrow; + + if (!$id_domain) { + warn "Error: Unknown domain $name, maybe already removed.\n"; + return; + } } + my $id = $id_domain; my $user = Ravada::Auth::SQL->search_by_id( $arg{uid}); die "Error: user id:$arg{uid} removed\n" if !$user; @@ -3589,7 +3596,17 @@ sub remove_domain { warn "Warning: $@" if $@; if (!$domain0) { - warn "Warning: I can't find domain [$id ] '$name' , maybe already removed.\n"; + warn "Warning: I can't find domain [$id] '" + .($name or '') + ."' , maybe already removed.\n"; + + my $domain = Ravada::Front::Domain->open($id); + my @volumes = $domain->list_volumes(); + my $vm = Ravada::VM->open($domain->_data('id_vm')); + for my $vol (@volumes) { + next if $vol->file =~ /\.iso$/; + $vm->remove_file($vol->file) + } Ravada::Domain::_remove_domain_data_db($id); return; }; @@ -4803,7 +4820,15 @@ sub _cmd_remove { confess "Unknown user id ".$request->args->{uid} if !defined $request->args->{uid}; - $self->remove_domain(name => $request->args('name'), uid => $request->args('uid')); + my $name = $request->defined_arg('name'); + my $id_domain = $request->defined_arg('id_domain'); + + die "Error: Missing domain name or id" if !$name && !$id_domain; + my @args; + push @args,(name => $name) if $name && !$id_domain; + push @args,(id_domain => $id_domain ) if $id_domain; + + $self->remove_domain(@args, uid => $request->args('uid')); } sub _cmd_remove_clones($self, $request) { @@ -5114,7 +5139,7 @@ sub _cmd_start { $domain = $self->search_domain($name) if $name && !$id_domain; $domain = $self->search_domain_by_id($id_domain) if $id_domain; - die "Error: machine not found ".($name or $id_domain) if !$domain; + die "Error: machine unknown ".($name or $id_domain) if !$domain; $domain->status('starting'); @@ -5559,7 +5584,9 @@ sub _cmd_shutdown { } else { $domain = $self->search_domain($name); } - die "Unknown domain '$name'\n" if !$domain; + if (!$domain) { + die "Unknown domain '$name'\n"; + } } if ($id_domain) { my $domain2 = Ravada::Domain->open(id => $id_domain, id_vm => $id_vm); @@ -6535,12 +6562,23 @@ sub _refresh_volatile_domains($self) { eval { $domain = Ravada::Domain->open(id => $id_domain, _force => 1) } ; next if $domain && $domain->is_locked; if ( !$domain || $domain->status eq 'down' || !$domain->is_active) { + warn "Not domain" if !$domain; + warn $domain->status." is_active=".$domain->is_active if $domain; if ($domain && !$domain->is_locked ) { if ($domain->_vm && $domain->_vm->is_active(1)) { - Ravada::Request->shutdown_domain( + my $req_shutdown = Ravada::Request->shutdown_domain( uid => $USER_DAEMON->id ,id_domain => $id_domain ); + my @after; + @after = ( after_request => $req_shutdown->id) + if $req_shutdown; + Ravada::Request->remove_domain( + uid => $USER_DAEMON->id + ,id_domain => $id_domain + ,name => $name + ,@after + ) } } else { my $user; diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index 613fedd2c..be795cdb8 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -66,7 +66,7 @@ our %VALID_ARG = ( ,spinoff => { id_domain => 1, uid => 1 } ,pause_domain => $args_manage ,resume_domain => {%$args_manage, remote_ip => 1 } - ,remove_domain => $args_manage + ,remove_domain => {%$args_manage, id_domain => 2 } ,restore_domain => { id_domain => 1, uid => 1 } ,shutdown_domain => { name => 2, id_domain => 2, uid => 1, timeout => 2, at => 2 , check => 2 @@ -426,8 +426,10 @@ sub remove_domain { my $class=ref($proto) || $proto; my %args = @_; - confess "Missing domain name" if !$args{name}; - confess "Name is not scalar" if ref($args{name}); + confess "Missing domain name or id" + if !$args{name} && !$args{id_domain}; + + confess "Name is not scalar" if $args{name} && ref($args{name}); confess "Missing uid" if !$args{uid}; for (keys %args) { diff --git a/lib/Ravada/VM/Void.pm b/lib/Ravada/VM/Void.pm index 6a243b511..d5a7c3067 100644 --- a/lib/Ravada/VM/Void.pm +++ b/lib/Ravada/VM/Void.pm @@ -5,6 +5,7 @@ use Data::Dumper; use Encode; use Encode::Locale; use Fcntl qw(:flock O_WRONLY O_EXCL O_CREAT); +use File::Copy qw(copy); use Hash::Util qw(lock_hash); use IPC::Run3 qw(run3); use Moose; diff --git a/script/rvd_back b/script/rvd_back index 863518de4..508b2336b 100755 --- a/script/rvd_back +++ b/script/rvd_back @@ -347,7 +347,7 @@ sub start { $ravada->_clean_tls(); autostart_machines($ravada); Ravada::Request->update_iso_urls(uid =>Ravada::Utils::user_daemon->id); - Ravada::Request->refresh_storage(); + Ravada::Request->refresh_storage(uid =>Ravada::Utils::user_daemon->id); Ravada::Request->list_host_devices(uid =>Ravada::Utils::user_daemon->id); } diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index 69d106ecd..fd57508be 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -382,7 +382,7 @@ sub test_removed_base_file($vm, $node) { for my $try ( 1 .. 20 ) { my $clone1 = _req_clone($base); Ravada::Request->start_domain(uid => user_admin->id, id_domain => $clone1->id); - wait_request(); + wait_request(check_error => 0); $found_clone = $clone1; my @req = $base->list_requests(); my $found_req; @@ -687,6 +687,7 @@ sub test_volatile_req($vm, $node) { shutdown_domain_internal($clone); _wait_machine_removed($clone); + diag("Checking ". $clone->name." removed"); for my $vol ( $clone->list_volumes ) { ok(!$vm->file_exists($vol),$vol) or exit; ok(!$node->file_exists($vol),$vol." in ".$node->name) or exit; @@ -702,10 +703,10 @@ sub _wait_machine_removed($clone) { last if !$clone2; rvd_back->_cmd_refresh_vms(); - wait_request(); + wait_request(debug => 1); } - wait_request(); + wait_request(debug => 1); } sub test_domain_gone($vm, $node) { @@ -2092,6 +2093,8 @@ for my $vm_name (vm_names() ) { start_node($node); + test_volatile_req($vm, $node); + test_migrate_clone($node, $vm); test_migrate_clone($vm, $node); diff --git a/t/vm/h10_hybernate.t b/t/vm/h10_hybernate.t index e58f6a160..a933964ca 100644 --- a/t/vm/h10_hybernate.t +++ b/t/vm/h10_hybernate.t @@ -40,6 +40,8 @@ sub test_hybernate_clone { my ($vm_name, $domain) = @_; $domain->is_public(1); + $domain->shutdown_now(user_admin) if $domain->is_active; + my $clone = $domain->clone(name => new_domain_name(), user => $USER); eval {$clone->start($USER) if !$clone->is_active }; @@ -79,7 +81,6 @@ sub test_remove_hybernated { eval{ $clone->remove($USER) }; ok(!$@,"Expecting no error removing , got : ".($@ or '')); - } ################################################################ From 33b44c43d2f75c16677afc44dbaf5be5053a1770 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 15 May 2025 12:01:49 +0200 Subject: [PATCH 007/141] wip: nodes remove base --- lib/Ravada/Domain.pm | 23 +++++++---------------- script/rvd_front | 2 +- t/nodes/10_basic.t | 4 +++- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 8e31ab43c..f1d0c60a0 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -5589,17 +5589,11 @@ sub set_base_vm($self, %args) { $id_request = $request->id; } $self->_set_base_vm_db($vm->id, $value, $id_request); - - if ($vm->is_local) { - $self->_set_vm($vm,1); # force set vm on domain - if (!$value) { - $request->status("working","Removing base") if $request; - $self->remove_base($user); - } else { - $self->prepare_base($user) if !$self->is_base(); + if ($value) { + if ( !$self->is_base() ) { $request->status("working","Preparing base") if $request; + $self->prepare_base($user) } - } elsif ($value) { $self->_check_all_parents_in_node($vm); $request->status("working", "Syncing base volumes to ".$vm->host) if $request; @@ -5611,17 +5605,14 @@ sub set_base_vm($self, %args) { $self->_set_base_vm_db($vm->id, 0); die $err; } + $vm->_add_instance_db($self->id); $self->_set_clones_autostart(0); + } else { - $self->_set_vm($vm,1); # force set vm on domain - $self->_do_remove_base($user); + $request->status("working","Removing base") if $request; + $vm->remove_base(); } - if (!$vm->is_local) { - my $vm_local = $self->_vm->new( host => 'localhost' ); - $self->_set_vm($vm_local, 1); - } - $vm->_add_instance_db($self->id); return $self->_set_base_vm_db($vm->id, $value); } diff --git a/script/rvd_front b/script/rvd_front index 854617628..74721b5da 100644 --- a/script/rvd_front +++ b/script/rvd_front @@ -3786,7 +3786,7 @@ sub new_machine { $c->redirect_to("/admin/machines"); } } else { - my $req = Ravada::Request->refresh_storage(); + my $req = Ravada::Request->refresh_storage(uid => Ravada::Utils::user_daemon->id); $RAVADA->_cache_delete('list_isos'); # TODO handle possible errors } diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index fd57508be..6ff819af8 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -382,7 +382,7 @@ sub test_removed_base_file($vm, $node) { for my $try ( 1 .. 20 ) { my $clone1 = _req_clone($base); Ravada::Request->start_domain(uid => user_admin->id, id_domain => $clone1->id); - wait_request(check_error => 0); + wait_request(check_error => 0, debug => 1); $found_clone = $clone1; my @req = $base->list_requests(); my $found_req; @@ -2093,6 +2093,8 @@ for my $vm_name (vm_names() ) { start_node($node); + test_base_unset($vm,$node); + test_removed_base_file($vm, $node); test_volatile_req($vm, $node); test_migrate_clone($node, $vm); From 8273d131a7e33615cdc19ca10d1558bc960adba1 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 15 May 2025 13:45:14 +0200 Subject: [PATCH 008/141] wip: migrate domains when removing base in node --- lib/Ravada/Domain.pm | 16 +++++++++++++++- lib/Ravada/VM.pm | 13 +++++++++++++ t/nodes/10_basic.t | 1 + 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index f1d0c60a0..d002716ed 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -5610,7 +5610,21 @@ sub set_base_vm($self, %args) { } else { $request->status("working","Removing base") if $request; - $vm->remove_base(); + $self->_set_base_vm_db($vm->id, $value); + my $bases_vm = $self->_bases_vm(); + my $count=0; + my $node2; + for my $id_vm ( keys %$bases_vm) { + if ($bases_vm->{$id_vm}) { + $node2 = $id_vm; + $count++; + } + } + if (!$count) { + $self->remove_base(); + } else { + $vm->_migrate_domains($node2); + } } return $self->_set_base_vm_db($vm->id, $value); diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index 6d2bff7bc..df17c2e77 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -3193,6 +3193,19 @@ sub _set_active_machines_isolated($self, $network) { } } +sub _migrate_domains($self, $id_node) { + confess "Error: node undefined" if !defined $id_node; + for my $domain ( $self->list_domains) { + warn ref($domain); + Ravada::Request->migrate( + uid => Ravada::Utils::user_daemon->id + ,id_domain => $domain->id + ,id_node => $id_node + ,shutdown => 1 + ); + } +} + 1; diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index 6ff819af8..0f0a1bf8c 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -1070,6 +1070,7 @@ sub test_base_unset($vm, $node) { $base->set_base_vm(id_vm => $node->id,value => 0, user => user_admin); is($base->base_in_vm($node->id),0) or exit; is(Ravada::Domain::base_in_vm($base->id,$node->id),0) or exit; + wait_request(debug => 1); my $clone2 = Ravada::Domain->open($clone->id); $clone2->start(user_admin); From 8761485af73f4623219136edba8a7a933bb47af6 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 22 May 2025 09:31:28 +0200 Subject: [PATCH 009/141] wip: move all base instances to another node when removing base --- lib/Ravada/Domain.pm | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index d002716ed..f2bf59568 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -5618,6 +5618,7 @@ sub set_base_vm($self, %args) { if ($bases_vm->{$id_vm}) { $node2 = $id_vm; $count++; + last if Ravada::VM::is_local($id_vm); } } if (!$count) { @@ -5646,6 +5647,12 @@ sub _check_all_parents_in_node($self, $vm, $request=undef) { } for my $base ( reverse @bases) { $base->_set_base_vm_db($vm->id,0); + my @instances = $base->list_instances(); + for my $i (@instances) { + next if $i->{id_vm} == $vm->id; + $base->_data(id_vm => $i->{id_vm}); + last; + } my @after_req; @after_req = ( after_request_ok => $req->id) if $req; $req = Ravada::Request->migrate( @@ -7397,6 +7404,7 @@ sub list_instances($self, $id=undef) { my @instances; while (my $row = $sth->fetchrow_hashref) { + $row->{is_local} = Ravada::VM::is_local($row->{id_vm}); lock_hash(%$row); push @instances, ( $row ); } From 75787427ae71cd49dfbf14339faafc29a89551bf Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 22 May 2025 09:32:22 +0200 Subject: [PATCH 010/141] wip: is local by id node --- lib/Ravada/VM.pm | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index df17c2e77..fd003c336 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -177,7 +177,8 @@ sub _init_connector { && defined $Ravada::Front::CONNECTOR; } -sub _dbh($self) { +sub _dbh($self=undef) { + _init_connector(); return $$CONNECTOR->dbh(); } @@ -1391,10 +1392,25 @@ Returns wether this virtual manager is in the local host =cut sub is_local($self) { + if (ref($self)) { return 1 if !$self->host || $self->host eq 'localhost' || $self->host eq '127.0.0,1' ; + } elsif ($self =~ /^\d+$/) { + my $id=$self; + my $sth = _dbh->prepare("SELECT hostname " + ." FROM vms " + ." WHERE id=?" + ); + $sth->execute($id); + my ($host) = $sth->fetchrow; + return 1 if !$host + || $host eq 'localhost' + || $host eq '127.0.0,1' + ; + + } return 0; } @@ -3196,7 +3212,6 @@ sub _set_active_machines_isolated($self, $network) { sub _migrate_domains($self, $id_node) { confess "Error: node undefined" if !defined $id_node; for my $domain ( $self->list_domains) { - warn ref($domain); Ravada::Request->migrate( uid => Ravada::Utils::user_daemon->id ,id_domain => $domain->id From 1652b3ee9b28115fcdc34cc5f35a4a1bdc207e1b Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 22 May 2025 09:33:12 +0200 Subject: [PATCH 011/141] wip: test clones remote --- t/nodes/10_basic.t | 59 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index 0f0a1bf8c..c68afbb57 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -363,6 +363,10 @@ sub _req_clone($base) { } sub test_removed_base_file($vm, $node) { + # TODO + return; + # TODO. When files are manually removed, clone again from main node + # diag("Testing removed base in ".$vm->type); my $base = create_domain($vm); $base->prepare_base(user_admin); @@ -377,6 +381,11 @@ sub test_removed_base_file($vm, $node) { for my $file ( $base->list_files_base ) { $node->remove_file($file); } + Ravada::Request->refresh_storage( + uid => user_admin->id + ,id_vm => $node->id + ); + wait_request(); my $found_clone; for my $try ( 1 .. 20 ) { @@ -1200,19 +1209,41 @@ sub _check_files_exist($domain) { } } +sub _import_clone($vm) { + if ($vm->type eq 'Void') { + return create_domain_v2(vm => $vm, swap => 1 , data => 1); + } + my $base0 = rvd_front->search_domain($BASE_NAME); + $base0 = import_domain($vm->type, $BASE_NAME, 1) if !$base0; + return if !$base0; + my $name = new_domain_name(); + Ravada::Request->clone( + name => $name + ,uid => user_admin->id + ,id_domain => $base0->id + ); + wait_request(); + my $clone = rvd_back->search_domain($name); + my $req = Ravada::Request->spinoff( + uid => user_admin->id + ,id_domain => $clone->id + ); + Ravada::Request->prepare_base( + uid => user_admin->id + ,id_domain => $clone->id + ,after_request => $req->id + ); + wait_request(); + return $clone; +} + sub test_fill_memory($vm, $node, $migrate, $start=0) { diag("Testing fill memory ".$vm->type.", migrate=$migrate, start=$start"); - my $base; - if ($vm->type eq 'Void') { - $base = create_domain_v2(vm => $vm, swap => 1 , data => 1); - } else { - $base = rvd_back->search_domain($BASE_NAME); - $base = import_domain($vm->type, $BASE_NAME, 1) if !$base; - if (!$base) { + my $base = _import_clone($vm); + if (!$base) { diag("SKIPPING: base $BASE_NAME must be installed to test"); return; - } } $base->prepare_base(user_admin) if !$base->is_base; Ravada::Request->set_base_vm(id_vm => $node->id @@ -1243,10 +1274,11 @@ sub test_fill_memory($vm, $node, $migrate, $start=0) { ,memory => int($memory) ,start => $start ); - wait_request(debug => 0, check_error => 0); + wait_request(debug => 1, check_error => 0); like($req->error, qr/^(No free memory|$)/); is($req->status,'done'); push @clones,($clone_name); + diag($req->command." ".$req->status); my $clone = rvd_back->search_domain($clone_name) or last; ok($clone,"Expecting clone $clone_name") or exit; $created_in_node++ if $clone->_data('id_vm') == $node->id; @@ -1259,7 +1291,7 @@ sub test_fill_memory($vm, $node, $migrate, $start=0) { ,shutdown => 1 ,shutdown_timeout => 1 ) if $migrate; - wait_request(debug => 0); + wait_request(debug => 1); eval { $clone->start(user_admin) }; $error = $@; diag($error) if $error; @@ -1705,8 +1737,7 @@ sub _download_alpine64 { sub test_displays($vm, $node, $no_builtin=0) { my $base; if ( $vm->type eq 'KVM') { - $base = rvd_back->search_domain($BASE_NAME); - $base = import_domain('KVM', $BASE_NAME, 1) if !$base; + $base = _import_clone($vm); } else { return; # $base = create_domain($vm); @@ -1816,7 +1847,7 @@ sub test_start_remote($domain) { uid => user_admin->id ,id_domain => $domain->id ); - wait_request(); + wait_request(debug => 1); is($req->error,''); my $domain2 = Ravada::Domain->open($domain->id); @@ -2094,9 +2125,9 @@ for my $vm_name (vm_names() ) { start_node($node); + test_fill_memory($vm, $node, 1); # migrate test_base_unset($vm,$node); test_removed_base_file($vm, $node); - test_volatile_req($vm, $node); test_migrate_clone($node, $vm); test_migrate_clone($vm, $node); From 6f6f51fb698c781c479748495b6fc4d649f99624 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 22 May 2025 15:15:55 +0200 Subject: [PATCH 012/141] wip: overwrite base file --- lib/Ravada/Domain.pm | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index f2bf59568..090b323e0 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -1002,15 +1002,22 @@ Prepares the virtual machine as a base: =cut -sub prepare_base($self, $with_cd) { +sub prepare_base($self, $with_cd, $overwrite=undef) { my @base_img; for my $volume ($self->list_volumes_info()) { next if !$volume->file; my $base_file = $volume->base_filename; next if !$base_file || $base_file =~ /\.iso$/; - confess "Error: file '$base_file' already exists in ".$self->_vm->name - if $self->_vm->file_exists($base_file); + + if ($self->_vm->file_exists($base_file)) { + if ($overwrite) { + $self->_vm->remove_file($base_file); + next; + } + confess "Error: file '$base_file' already exists in " + .$self->_vm->name; + } } for my $volume ($self->list_volumes_info()) { From 1a73fbcecf57d74c38914d110c3c632f7e411483 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 22 May 2025 15:16:59 +0200 Subject: [PATCH 013/141] wip: overwrite base file --- lib/Ravada/Volume/QCOW2.pm | 3 ++- t/nodes/10_basic.t | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/Ravada/Volume/QCOW2.pm b/lib/Ravada/Volume/QCOW2.pm index d00c05bb3..c7ede6ae8 100644 --- a/lib/Ravada/Volume/QCOW2.pm +++ b/lib/Ravada/Volume/QCOW2.pm @@ -25,7 +25,8 @@ sub prepare_base($self) { my $base_img = $self->base_filename(); confess $base_img if $base_img !~ /\.ro/; - confess "Error: '$base_img' already exists" if -e $base_img; + confess "Error: '$base_img' already exists" + if $self->vm->file_exists($base_img); confess if $file_img =~ /\.iso$/i; my @cmd = _cmd_convert($file_img,$base_img); diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index c68afbb57..3b4952ef3 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -515,7 +515,9 @@ sub test_set_vm_fail($vm, $node) { my $pool2 = create_storage_pool($vm); $vm->default_storage_pool_name($pool2); $base->add_volume( size => 11000 ); - $base->prepare_base(user_admin); + + # ,with_cd ,overwrite + $base->prepare_base(user_admin,0 ,1); $base->_set_base_vm_db($node->id, 1); @@ -1025,7 +1027,6 @@ sub test_create_active($vm, $node) { ); wait_request(debug => 0); $clone = rvd_front->search_domain($name); - ok($vm->search_domain($name),"Expecting clone $name in master node") or exit; last if $clone->display(user_admin) =~ /$remote_ip/; } like($clone->display(user_admin), qr($remote_ip)); From 33a412d94f57893d1731a80eec08364d8dd9b555 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Fri, 23 May 2025 09:20:23 +0200 Subject: [PATCH 014/141] wip: setting to enable status cpu and ram --- lib/Ravada.pm | 16 ++++++++++++++++ lib/Ravada/Domain.pm | 10 +++++++--- lib/Ravada/Domain/KVM.pm | 14 +++++++++++--- t/lib/Test/Ravada.pm | 3 +++ t/nodes/10_basic.t | 3 +-- 5 files changed, 38 insertions(+), 8 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index 8bbd62c7e..3282d012d 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -2794,6 +2794,22 @@ sub _sql_insert_defaults($self){ ,name => 'startup_ram' ,value => 1 } + ,{ + id_parent => '/backend' + ,name => 'stats' + ,value => undef + } + + ,{ + id_parent => '/backend/stats' + ,name => 'cpu' + ,value => 0 + } + ,{ + id_parent => '/backend/stats' + ,name => 'memory' + ,value => 0 + } ] diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 090b323e0..addfb44ec 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -951,7 +951,7 @@ sub _around_list_volumes_info($orig, $self, $attribute=undef, $value=undef) { sub _around_prepare_base($orig, $self, @args) { #sub _around_prepare_base($orig, $self, $user, $request = undef) { - my ($user, $request, $with_cd); + my ($user, $request, $with_cd, $overwrite); if(ref($args[0]) =~/^Ravada::/) { ($user, $request) = @args; } else { @@ -959,12 +959,13 @@ sub _around_prepare_base($orig, $self, @args) { $user = delete $args{user}; $request = delete $args{request}; $with_cd = delete $args{with_cd}; + $overwrite = delete $args{overwrite}; confess "Error: uknown args". Dumper(\%args) if keys %args; } $self->_pre_prepare_base($user, $request); $self->pre_prepare_base(); - my @base_img = $self->$orig($with_cd); + my @base_img = $self->$orig($with_cd, $overwrite); die "Error: No information files returned from prepare_base" if !scalar (\@base_img); @@ -6121,7 +6122,10 @@ sub log_status($self, $name, $value, $time='N') { return if exists $self->{_log_status_time} && $self->{_log_status_time} == $time0; $self->{_log_status_time} = $time0; - my ($cpu_time, $mem) = $self->get_stats(); + my ($cpu_time, $mem); + ($cpu_time, $mem) = $self->get_stats() + if Ravada::Front->setting('/backend/stats/cpu') + || Ravada::Front->setting('/backend/stats/memory'); if ($cpu_time || $mem) { RRDs::update ($file , "--template", "$name:cpu:memory", "$time:$value:$cpu_time:$mem"); } else { diff --git a/lib/Ravada/Domain/KVM.pm b/lib/Ravada/Domain/KVM.pm index e3140c486..d73dec29e 100644 --- a/lib/Ravada/Domain/KVM.pm +++ b/lib/Ravada/Domain/KVM.pm @@ -4288,12 +4288,20 @@ sub get_stats($self) { my $mem; my $cpu_time; - my $mem_stats = $self->domain->memory_stats(); + my @cpu_stats; + @cpu_stats = $self->domain->get_cpu_stats(-1,1) + if Ravada::Front->setting(undef,'/backend/stats/cpu'); + + $cpu_time = int($cpu_stats[0]->{cpu_time}/1024/1024); + + my $mem_stats; + $mem_stats = $self->domain->memory_stats() + if Ravada::Front->setting('/backend/stats/memory'); + if (exists $mem_stats->{rss} && exists $mem_stats->{actual_balloon}) { $mem = int(($mem_stats->{rss}/$mem_stats->{actual_balloon})*100); } - my @cpu_stats = $self->domain->get_cpu_stats(-1,1); - $cpu_time = int($cpu_stats[0]->{cpu_time}/1024/1024); + return ($cpu_time, $mem); } diff --git a/t/lib/Test/Ravada.pm b/t/lib/Test/Ravada.pm index 217d4804b..f3d5078ff 100644 --- a/t/lib/Test/Ravada.pm +++ b/t/lib/Test/Ravada.pm @@ -2316,6 +2316,9 @@ sub start_node($node) { } eval { $node2->run_command("hwclock","--hctosys") }; is($@,'',"Expecting no error setting clock on ".$node->name." ".($@ or '')); + + my $sth = $CONNECTOR->dbh->prepare("DELETE FROM domains WHERE name=?"); + $sth->execute($domain->name); } sub remove_node($node) { diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index 3b4952ef3..2eab731dd 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -516,8 +516,7 @@ sub test_set_vm_fail($vm, $node) { $vm->default_storage_pool_name($pool2); $base->add_volume( size => 11000 ); - # ,with_cd ,overwrite - $base->prepare_base(user_admin,0 ,1); + $base->prepare_base(user => user_admin, overwrite => 1); $base->_set_base_vm_db($node->id, 1); From 8dc2cf2d3372dd74b1e8894f2d952fd3f4950c98 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Fri, 23 May 2025 10:19:24 +0200 Subject: [PATCH 015/141] wip: removed debug --- t/nodes/10_basic.t | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index 2eab731dd..d28bcc089 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -391,7 +391,7 @@ sub test_removed_base_file($vm, $node) { for my $try ( 1 .. 20 ) { my $clone1 = _req_clone($base); Ravada::Request->start_domain(uid => user_admin->id, id_domain => $clone1->id); - wait_request(check_error => 0, debug => 1); + wait_request(check_error => 0, debug => 0); $found_clone = $clone1; my @req = $base->list_requests(); my $found_req; @@ -713,10 +713,10 @@ sub _wait_machine_removed($clone) { last if !$clone2; rvd_back->_cmd_refresh_vms(); - wait_request(debug => 1); + wait_request(debug => 0); } - wait_request(debug => 1); + wait_request(debug => 0); } sub test_domain_gone($vm, $node) { @@ -757,7 +757,7 @@ sub test_volatile_req_clone($vm, $node, $machine='pc-i440fx') { $base->volatile_clones(1); ok($base->base_in_vm($node->id)); _check_base_in_vm_db($base, $node->id,$req->id, 1); - wait_request(debug => 1); + wait_request(debug => 0); _check_base_in_vm_db($base, $node->id,undef, 1); my $clone; @@ -1079,7 +1079,7 @@ sub test_base_unset($vm, $node) { $base->set_base_vm(id_vm => $node->id,value => 0, user => user_admin); is($base->base_in_vm($node->id),0) or exit; is(Ravada::Domain::base_in_vm($base->id,$node->id),0) or exit; - wait_request(debug => 1); + wait_request(debug => 0); my $clone2 = Ravada::Domain->open($clone->id); $clone2->start(user_admin); @@ -1274,7 +1274,7 @@ sub test_fill_memory($vm, $node, $migrate, $start=0) { ,memory => int($memory) ,start => $start ); - wait_request(debug => 1, check_error => 0); + wait_request(debug => 0, check_error => 0); like($req->error, qr/^(No free memory|$)/); is($req->status,'done'); push @clones,($clone_name); @@ -1291,7 +1291,7 @@ sub test_fill_memory($vm, $node, $migrate, $start=0) { ,shutdown => 1 ,shutdown_timeout => 1 ) if $migrate; - wait_request(debug => 1); + wait_request(debug => 0); eval { $clone->start(user_admin) }; $error = $@; diag($error) if $error; @@ -1847,7 +1847,7 @@ sub test_start_remote($domain) { uid => user_admin->id ,id_domain => $domain->id ); - wait_request(debug => 1); + wait_request(debug => 0); is($req->error,''); my $domain2 = Ravada::Domain->open($domain->id); From 190aed0529c4b76af6e29907606d892ee095976e Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Fri, 23 May 2025 12:52:07 +0200 Subject: [PATCH 016/141] wip: list isos from node --- lib/Ravada/Front.pm | 4 ++++ lib/Ravada/WebSocket.pm | 3 +++ public/js/ravada.js | 4 ++-- t/nodes/10_basic.t | 6 ++++-- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/Ravada/Front.pm b/lib/Ravada/Front.pm index c12a1e8ad..3926f8ec3 100644 --- a/lib/Ravada/Front.pm +++ b/lib/Ravada/Front.pm @@ -812,12 +812,16 @@ Returns a reference to a list of the ISOs known by the system sub iso_file ($self, $id_vm, $uid) { + confess "Error: undefined id node" + if !defined $id_vm; + my $key = "list_isos_$id_vm"; my $cache = $self->_cache_get($key); return $cache if $cache; Ravada::Request->refresh_storage( id_vm=> $id_vm + ,uid => Ravada::Utils->user_daemon->id ); my $req = Ravada::Request->list_isos( diff --git a/lib/Ravada/WebSocket.pm b/lib/Ravada/WebSocket.pm index f95834deb..356724aa5 100644 --- a/lib/Ravada/WebSocket.pm +++ b/lib/Ravada/WebSocket.pm @@ -791,6 +791,9 @@ sub subscribe($self, %args) { delete $args2{ws}; warn "Subscribe ".Dumper(\%args2) if $DEBUG; if (!exists $self->clients->{$ws}) { + die "Error: missig id_node in ".$args{channel} + if $args{channel} eq 'list_isos'; + $self->clients->{$ws} = { ws => $ws , %args diff --git a/public/js/ravada.js b/public/js/ravada.js index 716b8a322..6ddfef47f 100644 --- a/public/js/ravada.js +++ b/public/js/ravada.js @@ -528,7 +528,7 @@ var subscribe_isos = function(url) { var ws = new WebSocket(url); - ws.onopen = function(event) { ws.send('list_isos') }; + ws.onopen = function(event) { ws.send('list_isos/'+$scope.showmachine.id_vm) }; ws.onmessage = function(event) { var data = JSON.parse(event.data); $scope.$apply(function () { @@ -598,7 +598,6 @@ subscribe_machine_info(url); subscribe_bases(url); subscribe_requests(url); - subscribe_isos(url); // other data will be subscribed on loading machine info }; @@ -687,6 +686,7 @@ }); $scope.list_shares(); + subscribe_isos(url_ws); } list_interfaces(); if (is_admin) { diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index d28bcc089..589b1f379 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -1136,9 +1136,10 @@ sub test_change_clone($vm, $node) { ok(-e $vol); ok($node->file_exists($vol), $vol) if $vol !~ /iso$/; } + my $node_clone = $clone->_vm; for my $vol (@volumes_clone) { - ok(-e $vol, $vol); - ok(!$node->file_exists($vol), $vol) if $vol !~ /iso$/; + ok($node_clone->file_exists($vol), $vol) or exit + if $vol !~ /iso$/; } $clone->remove(user_admin); $base->remove(user_admin); @@ -2124,6 +2125,7 @@ for my $vm_name (vm_names() ) { is($node->is_local,0,"Expecting ".$node->name." ".$node->ip." is remote" ) or BAIL_OUT(); start_node($node); + test_change_clone($vm, $node); test_fill_memory($vm, $node, 1); # migrate test_base_unset($vm,$node); From 5fe16f65fda70f11b0bd1daa6fb42c4da2f01982 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Fri, 23 May 2025 15:30:36 +0200 Subject: [PATCH 017/141] wip: todo some tests, better file check --- lib/Ravada/WebSocket.pm | 1 - t/nodes/10_basic.t | 19 ++++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/Ravada/WebSocket.pm b/lib/Ravada/WebSocket.pm index 356724aa5..acb42b6cb 100644 --- a/lib/Ravada/WebSocket.pm +++ b/lib/Ravada/WebSocket.pm @@ -827,7 +827,6 @@ sub unsubscribe($self, $ws) { sub unsubscribe_all($self) { for my $ws ( keys %{$self->clients()} ) { - warn $ws; delete $self->clients->{$ws}; } } diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index 589b1f379..dd7e0cce5 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -444,6 +444,8 @@ sub _remove_tmp($domain, $vm = $domain->_vm) { } sub test_removed_base_file_and_swap_remote($vm, $node) { + # TODO + return; diag("Testing removed remote base and swap in ".$vm->type); my $base = create_domain($vm); $base->add_volume(size => 128*1024 , type => 'tmp'); @@ -569,10 +571,7 @@ sub test_set_vm($vm, $node) { is($req->status, 'done'); like($req->error, qr{^($|rsync done)}); - is($base->_vm->id, $vm->id); - my $base2 = Ravada::Domain->open($base->id); - is($base2->_vm->id, $vm->id); $info = $base2->info(user_admin); is($info->{bases}->{$vm->id},1,Dumper($info->{bases})) or exit; @@ -909,15 +908,16 @@ sub test_remove_base($vm, $node, $volatile) { my @volumes = $base->list_files_base(); $base->set_base_vm(node => $node, user => user_admin); for my $file ( @volumes ) { - my ($out, $err) = $node->run_command("ls", $file); - ok($out, "Expecting file '$file' in ".$node->name) or exit; + ok($node->file_exists($file),1) + or die "Expecting file '$file' in ".$node->name; } $base->remove_base_vm(node => $node, user => user_admin); for my $file ( @volumes , @volumes0 ) { ok(!$node->file_exists($file)); - ok(-e $file, "Expecting file '$file' in local") or exit; + ok($vm->file_exists($file), "Expecting file '$file' in local") or exit; } + delete $base->{_data}->{id_vm}; isnt($base->_data('id_vm'), $node->id); $base->set_base_vm(node => $node, user => user_admin); @@ -930,9 +930,10 @@ sub test_remove_base($vm, $node, $volatile) { wait_request( debug => 0 ); for my $file ( @volumes ) { - ok(!-e $file, "Expecting no file '$file' in local") or exit; - my ($out, $err) = $node->run_command("ls", $file); - ok(!$out, "Expecting no file '$file' in ".$node->name) or exit; + is($vm->file_exists($file),0 + , "Expecting no file '$file' in local") or exit; + is($node->file_exists($file),0 + , "Expecting no file '$file' in local") or exit; } $base->remove(user_admin); From 93fc5277ab904cc65a29bad2b4030a1c49cbcbf2 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 26 May 2025 10:24:05 +0200 Subject: [PATCH 018/141] wip: remove instance in node --- lib/Ravada.pm | 2 -- lib/Ravada/Domain.pm | 32 ++++++++++++++++++++++++-------- lib/Ravada/VM.pm | 1 + lib/Ravada/VM/KVM.pm | 1 + t/nodes/10_basic.t | 44 +++++++++++++++++++++++++++++++++++--------- 5 files changed, 61 insertions(+), 19 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index 3282d012d..e557fc676 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -6578,8 +6578,6 @@ sub _refresh_volatile_domains($self) { eval { $domain = Ravada::Domain->open(id => $id_domain, _force => 1) } ; next if $domain && $domain->is_locked; if ( !$domain || $domain->status eq 'down' || !$domain->is_active) { - warn "Not domain" if !$domain; - warn $domain->status." is_active=".$domain->is_active if $domain; if ($domain && !$domain->is_locked ) { if ($domain->_vm && $domain->_vm->is_active(1)) { my $req_shutdown = Ravada::Request->shutdown_domain( diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index addfb44ec..e9c7f73f6 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -2652,8 +2652,6 @@ sub _remove_domain_cascade($self,$user, $cascade = 1) { } return if !scalar(@instances); - my $sth_delete = $$CONNECTOR->dbh->prepare("DELETE FROM domain_instances " - ." WHERE id=? "); for my $instance ( @instances ) { next if $instance->{id_vm} == $self->_vm->id; my $vm; @@ -2666,14 +2664,29 @@ sub _remove_domain_cascade($self,$user, $cascade = 1) { $@ = ''; eval { $domain = $vm->search_domain($domain_name) } if $vm; warn $@ if $@; - eval { - $domain->remove($user, $cascade) if $domain; - }; - warn $@ if $@; - $sth_delete->execute($instance->{id}); + $domain->_remove_instance($user, $cascade, $instance->{id}) if $domain; } } +sub _remove_instance($self, $user, $cascade, $id_instance=undef) { + + my $sth = $self->_dbh->prepare( + "SELECT id FROM domain_instances WHERE id_domain=? AND id_vm=?" + ); + $sth->execute($self->id, $self->_data('id_vm')); + my ($id_instance2) = $sth->fetchrow; + warn Dumper([$id_instance, $id_instance2]); + $id_instance = $id_instance2 if !defined $id_instance; + my $sth_delete = $$CONNECTOR->dbh->prepare("DELETE FROM domain_instances " + ." WHERE id=? "); + eval { + $self->remove($user, $cascade); + }; + warn $@ if $@; + $sth_delete->execute($id_instance) if defined $id_instance; + +} + sub _remove_domain_data_db($id, $type=undef) { _finish_requests_db($id); for my $table ( @@ -2749,8 +2762,10 @@ sub _finish_requests_db($id) { sub _remove_files_base { my $self = shift; + warn $self->_vm->name; for my $file ( $self->list_files_base ) { next if $file =~ /\.iso$/; + warn $file; $self->_vm->remove_file($file) if $self->_vm->file_exists($file); } } @@ -5624,7 +5639,7 @@ sub set_base_vm($self, %args) { my $node2; for my $id_vm ( keys %$bases_vm) { if ($bases_vm->{$id_vm}) { - $node2 = $id_vm; + $node2 = Ravada::VM->open($id_vm); $count++; last if Ravada::VM::is_local($id_vm); } @@ -5633,6 +5648,7 @@ sub set_base_vm($self, %args) { $self->remove_base(); } else { $vm->_migrate_domains($node2); + $self->_remove_instance($user,1); } } diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index fd003c336..70a437427 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -3211,6 +3211,7 @@ sub _set_active_machines_isolated($self, $network) { sub _migrate_domains($self, $id_node) { confess "Error: node undefined" if !defined $id_node; + $id_node = $id_node->id if ref($id_node); for my $domain ( $self->list_domains) { Ravada::Request->migrate( uid => Ravada::Utils::user_daemon->id diff --git a/lib/Ravada/VM/KVM.pm b/lib/Ravada/VM/KVM.pm index 1a642f726..b649b5641 100644 --- a/lib/Ravada/VM/KVM.pm +++ b/lib/Ravada/VM/KVM.pm @@ -387,6 +387,7 @@ sub search_volume_re($self,$pattern,$refresh=0) { sub remove_file($self,@files) { for my $file (@files) { + warn $self->name." ".$file if $file =~ /\.ro\./; my $vol = $self->search_volume($file); if (!$vol) { $self->_refresh_storage_pools(); diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index dd7e0cce5..9eb787b15 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -905,20 +905,28 @@ sub test_remove_base($vm, $node, $volatile) { or exit; $base->prepare_base(user_admin); - my @volumes = $base->list_files_base(); + my @volumes_base = $base->list_files_base(); $base->set_base_vm(node => $node, user => user_admin); - for my $file ( @volumes ) { - ok($node->file_exists($file),1) + for my $file ( @volumes_base ) { + ok($node->file_exists($file)) or die "Expecting file '$file' in ".$node->name; + ok($vm->file_exists($file)) + or die "Expecting file '$file' in ".$vm->name; } $base->remove_base_vm(node => $node, user => user_admin); - for my $file ( @volumes , @volumes0 ) { - ok(!$node->file_exists($file)); + for my $file ( @volumes_base ) { + # too complicated to know if can be removed + # ok(!$node->file_exists($file)); + ok($vm->file_exists($file), "Expecting file '$file' in local") or exit; + } + for my $file ( @volumes0 ) { + # too complicated to know if can be removed + # ok(!$node->file_exists($file),$file); ok($vm->file_exists($file), "Expecting file '$file' in local") or exit; } delete $base->{_data}->{id_vm}; - isnt($base->_data('id_vm'), $node->id); + isnt($base->_data('id_vm'), $vm->id); $base->set_base_vm(node => $node, user => user_admin); is(scalar($base->list_vms), 2) or exit; @@ -929,7 +937,7 @@ sub test_remove_base($vm, $node, $volatile) { ok(grep {$_->command eq 'remove_base_vm' } @req) or die Dumper(\@req); wait_request( debug => 0 ); - for my $file ( @volumes ) { + for my $file ( @volumes_base ) { is($vm->file_exists($file),0 , "Expecting no file '$file' in local") or exit; is($node->file_exists($file),0 @@ -1239,9 +1247,19 @@ sub _import_clone($vm) { return $clone; } +sub _shutdown_domains(@nodes) { + for my $node (@nodes) { + for my $domain ($node->list_domains(active => 1)) { + $domain->shutdown_now(user_admin); + } + } +} + sub test_fill_memory($vm, $node, $migrate, $start=0) { diag("Testing fill memory ".$vm->type.", migrate=$migrate, start=$start"); + _shutdown_domains($vm,$node); + my $base = _import_clone($vm); if (!$base) { diag("SKIPPING: base $BASE_NAME must be installed to test"); @@ -1296,9 +1314,10 @@ sub test_fill_memory($vm, $node, $migrate, $start=0) { wait_request(debug => 0); eval { $clone->start(user_admin) }; $error = $@; - diag($error) if $error; + diag("error=$error");# if $error; like($error, qr/(^$|No free memory)/); exit if $error && $error !~ /No free memory/; + $created_in_node++ if $clone->_data('id_vm') == $node->id; last if $error; $clone = Ravada::Domain->open($clone->id); @@ -1306,8 +1325,10 @@ sub test_fill_memory($vm, $node, $migrate, $start=0) { last if $migrate && exists $nodes{$vm->name} && $nodes{$vm->name} > 2; } + warn Dumper(\%nodes); ok($created_in_node,"Expecting some clones created in node ".$node->name) or exit; - ok(exists $nodes{$vm->name},"Expecting some clones to node ".$vm->name." ".$vm->id); + ok(exists $nodes{$vm->name},"Expecting some clones to node ".$vm->name." ".$vm->id) + or die Dumper(\%nodes); ok(exists $nodes{$node->name},"Expecting some clones to node ".$node->name." ".$node->id) or exit; my ($clone) = grep { $_->{id_vm} == $vm->id } $base->clones; @@ -1322,6 +1343,7 @@ sub test_fill_memory($vm, $node, $migrate, $start=0) { } sub test_rsync_back($vm, $clone) { + confess if !$clone || !exists $clone->{id_vm} || !defined $clone->{id_vm}; if ($clone->{id_vm} == $vm->id) { diag("Warning: ".$clone->{name}." already in node ".$vm->name); return; @@ -2126,6 +2148,10 @@ for my $vm_name (vm_names() ) { is($node->is_local,0,"Expecting ".$node->name." ".$node->ip." is remote" ) or BAIL_OUT(); start_node($node); + + for my $volatile (1,0) { + test_remove_base($vm, $node, $volatile); + } test_change_clone($vm, $node); test_fill_memory($vm, $node, 1); # migrate From 9f8d8634d58747fe372537e6f63069cfc4e30f29 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 26 May 2025 16:27:57 +0200 Subject: [PATCH 019/141] wip: use libvirt to remove base --- lib/Ravada/Domain.pm | 47 ++++++++++++++-------------------------- lib/Ravada/Domain/KVM.pm | 7 ++++++ lib/Ravada/VM/KVM.pm | 9 ++++++-- lib/Ravada/VM/Void.pm | 3 ++- t/lib/Test/Ravada.pm | 32 +++++++++++++++++++++++++++ 5 files changed, 64 insertions(+), 34 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index e9c7f73f6..f32d01f95 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -2595,10 +2595,15 @@ sub _check_active_node($self) { sub _after_remove_domain($self, $user, $cascade=undef) { + my $vm_local = $self->_vm->new( host => 'localhost' ); + $self->_rrd_remove(); $self->_remove_iptables( ); $self->remove_expose(); - $self->_remove_domain_cascade($user) if !$cascade; + + return if $cascade; + + $self->_remove_domain_cascade($user); my $id_base; $id_base = $self->_data('id_base') if $self->is_known(); @@ -2675,12 +2680,13 @@ sub _remove_instance($self, $user, $cascade, $id_instance=undef) { ); $sth->execute($self->id, $self->_data('id_vm')); my ($id_instance2) = $sth->fetchrow; - warn Dumper([$id_instance, $id_instance2]); $id_instance = $id_instance2 if !defined $id_instance; my $sth_delete = $$CONNECTOR->dbh->prepare("DELETE FROM domain_instances " ." WHERE id=? "); + + my $vm_local = $self->_vm->new( host => 'localhost' ); eval { - $self->remove($user, $cascade); + $self->remove_instance($user); }; warn $@ if $@; $sth_delete->execute($id_instance) if defined $id_instance; @@ -2762,10 +2768,8 @@ sub _finish_requests_db($id) { sub _remove_files_base { my $self = shift; - warn $self->_vm->name; for my $file ( $self->list_files_base ) { next if $file =~ /\.iso$/; - warn $file; $self->_vm->remove_file($file) if $self->_vm->file_exists($file); } } @@ -3030,51 +3034,31 @@ sub _cascade_remove_base_in_nodes($self) { } sub _do_remove_base($self, $user) { - return - if $self->is_base && $self->is_local - && $self->_cascade_remove_base_in_nodes(); - $self->is_base(0) if $self->is_local; - my $vm_local = $self->_vm->new( host => 'localhost' ); + $self->is_base(0); for my $vol ($self->list_volumes_info) { next if !$vol->file || $vol->file =~ /\.iso$/; next if !$self->_vm->file_exists($vol->file); my ($dir) = $vol->file =~ m{(.*)/}; - next if !$self->is_local && $self->_vm->shared_storage($vm_local, $dir); my $backing_file = $vol->backing_file; next if !$backing_file; # confess "Error: no backing file for ".$vol->file if !$backing_file; - if (!$self->is_local) { - my ($dir) = $backing_file =~ m{(.*/)}; - next if $self->_vm->shared_storage($vm_local, $dir); - $self->_vm->remove_file($vol->file); - $self->_vm->remove_file($backing_file); - $self->_vm->refresh_storage_pools(); - next; - } $vol->block_commit(); - unlink $vol->file or die "$! ".$vol->file; - my @stat = stat($backing_file) or confess "Error: missing $backing_file"; - move($backing_file, $vol->file) or die "$! $backing_file -> ".$vol->file; - my $mask = oct(7777); - my $mode = $stat[2] & $mask; - my $w = oct(200); - $mode = $mode ^ $w; - chmod($mode,$vol->file); - chown($stat[4],$stat[5], $vol->file); + $self->_vm->remove_file($vol->file); + $self->_vm->copy_file($backing_file, $vol->file,mode => '0700'); + # move($backing_file, $vol->file) or die "$! $backing_file -> ".$vol->file; } for my $file ($self->list_files_base) { next if $file =~ /\.iso$/i; next if ! $self->_vm->file_exists($file); - my ($dir) = $file =~ m{(.*/)}; - next if !$self->_vm->is_local && $self->_vm->shared_storage($vm_local, $dir); $self->_vm->remove_file($file); } + $self->_cascade_remove_base_in_nodes(); } sub _pre_remove_base { @@ -5648,7 +5632,8 @@ sub set_base_vm($self, %args) { $self->remove_base(); } else { $vm->_migrate_domains($node2); - $self->_remove_instance($user,1); + my $instance = $vm->search_domain($self->name); + $instance->_remove_instance($user,1) if $instance; } } diff --git a/lib/Ravada/Domain/KVM.pm b/lib/Ravada/Domain/KVM.pm index d73dec29e..4f3705190 100644 --- a/lib/Ravada/Domain/KVM.pm +++ b/lib/Ravada/Domain/KVM.pm @@ -341,6 +341,13 @@ sub remove { } +sub remove_instance($self, $user) { + + eval { $self->domain->undefine(Sys::Virt::Domain::UNDEFINE_NVRAM) if $self->domain && !$self->is_removed }; + confess $@ if $@ && $@ !~ /libvirt error code: 42/; + eval { $self->remove_disks() if $self->is_known }; + confess $@ if $@ && $@ !~ /libvirt error code: 42/; +} sub _remove_file_image { my $self = shift; diff --git a/lib/Ravada/VM/KVM.pm b/lib/Ravada/VM/KVM.pm index b649b5641..a503498ba 100644 --- a/lib/Ravada/VM/KVM.pm +++ b/lib/Ravada/VM/KVM.pm @@ -387,7 +387,6 @@ sub search_volume_re($self,$pattern,$refresh=0) { sub remove_file($self,@files) { for my $file (@files) { - warn $self->name." ".$file if $file =~ /\.ro\./; my $vol = $self->search_volume($file); if (!$vol) { $self->_refresh_storage_pools(); @@ -3330,7 +3329,10 @@ sub _search_pool_volume($self, $file) { } -sub copy_file($self, $orig, $dst) { +sub copy_file($self, $orig, $dst, %args) { + + my $mode = delete $args{mode}; + my ($sp,$vol) = $self->_search_pool_volume($orig); die "Error: volume $orig not found" if !$vol; @@ -3341,6 +3343,9 @@ sub copy_file($self, $orig, $dst) { $xml->findnodes("/volume/key/text()")->[0]->setData($dst); $xml->findnodes("/volume/target/path/text()")->[0]->setData($dst); + $xml->findnodes("/volume/target/permissions/mode/text()")->[0] + ->setData($mode) if $mode; + my $vol_dst; eval { $vol_dst = $sp->clone_volume($xml, $vol) }; confess $@ if $@; diff --git a/lib/Ravada/VM/Void.pm b/lib/Ravada/VM/Void.pm index d5a7c3067..0d87e54a4 100644 --- a/lib/Ravada/VM/Void.pm +++ b/lib/Ravada/VM/Void.pm @@ -900,7 +900,8 @@ sub remove_storage_pool($self, $name) { $self->write_file($file_sp, Dump( \@sp2)); } -sub copy_file($self, $orig, $dst) { +sub copy_file($self, $orig, $dst, %args) { + my $mode = delete $args{mode}; if ($self->is_local) { copy($orig, $dst) or die "$! $orig $dst"; } else { diff --git a/t/lib/Test/Ravada.pm b/t/lib/Test/Ravada.pm index f3d5078ff..bc63829cb 100644 --- a/t/lib/Test/Ravada.pm +++ b/t/lib/Test/Ravada.pm @@ -35,6 +35,7 @@ require Exporter; create_domain create_domain_v2 create_base import_domain + import_clone test_chain_prerouting find_ip_rule search_id_iso @@ -302,6 +303,37 @@ sub import_domain($vm, $name=$BASE_NAME, $import_base=1) { return $domain; } +sub import_clone($vm, %options) { + + my $base0; + if ($vm->type eq 'Void') { + $base0 = create_domain_v2(vm => $vm, swap => 1 , data => 1); + } else { + $base0 = rvd_front()->search_domain($BASE_NAME); + $base0 = import_domain($vm->type, $BASE_NAME, 1) if !$base0; + } + return if !$base0; + my $name = new_domain_name(); + Ravada::Request->clone( + name => $name + ,uid => user_admin->id + ,id_domain => $base0->id + ); + wait_request(); + my $clone = rvd_back()->search_domain($name); + my $req = Ravada::Request->spinoff( + uid => user_admin->id + ,id_domain => $clone->id + ); + Ravada::Request->prepare_base( + uid => user_admin->id + ,id_domain => $clone->id + ,after_request => $req->id + ); + wait_request(); + return $clone; +} + sub create_base($vm) { my $domain = create_domain_v2(vm => $vm); $domain->prepare_base(user_admin); From 744a6bee2b42ca1fc7a6c60a6d93605e22018646 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 26 May 2025 16:30:02 +0200 Subject: [PATCH 020/141] test: remove base --- t/vm/11_base.t | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 t/vm/11_base.t diff --git a/t/vm/11_base.t b/t/vm/11_base.t new file mode 100644 index 000000000..2c9ba1f8c --- /dev/null +++ b/t/vm/11_base.t @@ -0,0 +1,75 @@ +use warnings; +use strict; + +use Carp qw(confess); +use Data::Dumper; +use Hash::Util qw(lock_hash); +use IPC::Run3 qw(run3); +use JSON::XS; +use Test::More; + +no warnings "experimental::signatures"; +use feature qw(signatures); + +use lib 't/lib'; +use Test::Ravada; + +sub test_remove_base($vm) { + + my $base = import_clone($vm); + + my @volumes_base = $base->list_files_base(); + + my @volumes= $base->list_volumes(); + for my $vol ($base->list_volumes_info()) { + my $backing = $vol->backing_file; + ok($vm->file_exists($backing),$backing); + } + + $base->remove_base(user_admin); + + for my $vol ($base->list_volumes_info()) { + my $backing = $vol->backing_file; + ok(!$backing,"Expecting no backing from file ".$vol->file); + } + + for my $vol (@volumes_base) { + ok(!$vm->file_exists($vol),"Expecting base file '$vol' removed"); + } + + $base->start(user_admin); + my $ip = wait_ip($base); + + ok($ip); + + remove_domain($base); + +} + +######################################################################## + +init(); +clean(); + +for my $vm_name ( vm_names() ) { + my $vm; + + eval { $vm = rvd_back->search_vm($vm_name) }; + + SKIP: { + my $msg = "SKIPPED test: No $vm_name VM found "; + if ($vm && $vm_name =~ /kvm/i && $>) { + $msg = "SKIPPED: Test must run as root"; + $vm = undef; + } + + diag($msg) if !$vm; + skip $msg,10 if !$vm; + + test_remove_base($vm); + } +} + +######################################################################## +end(); +done_testing(); From 361934201ed43e431e6284a6a705119e23fb71e5 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 28 May 2025 13:47:48 +0200 Subject: [PATCH 021/141] wip: remove base --- lib/Ravada/Domain.pm | 81 ++++++++++++++++++++++++++++---------- lib/Ravada/Domain/KVM.pm | 18 +++++++-- lib/Ravada/Request.pm | 2 +- lib/Ravada/VM/KVM.pm | 12 ++++-- lib/Ravada/Volume/QCOW2.pm | 3 +- t/nodes/10_basic.t | 69 ++++++++++++++++++++++++++------ 6 files changed, 146 insertions(+), 39 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index f32d01f95..ededfa8f3 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -2684,7 +2684,6 @@ sub _remove_instance($self, $user, $cascade, $id_instance=undef) { my $sth_delete = $$CONNECTOR->dbh->prepare("DELETE FROM domain_instances " ." WHERE id=? "); - my $vm_local = $self->_vm->new( host => 'localhost' ); eval { $self->remove_instance($user); }; @@ -3002,13 +3001,20 @@ Makes the domain a regular, non-base virtual machine and removes the base files. =cut sub remove_base($self, $user) { - return $self->_do_remove_base($user); + $self->_cascade_remove_base_in_nodes(); + $self->_do_remove_base($user); } sub _cascade_remove_base_in_nodes($self) { my $req_nodes; + my $vm_local; + warn "cascade"; for my $vm ( $self->list_vms ) { - next if $vm->is_local; + if ( $vm->is_local ) { + $vm_local= $vm; + next; + } + warn $vm->name; my @after; push @after,(after_request => $req_nodes->id) if $req_nodes; $req_nodes = Ravada::Request->remove_base_vm( @@ -3016,20 +3022,22 @@ sub _cascade_remove_base_in_nodes($self) { ,id_domain => $self->id ,uid => Ravada::Utils::user_daemon->id ,_force => 1 + ,migrate => 0 ,@after ); } - if ( $req_nodes ) { - my $vm_local = $self->_vm->new( host => 'localhost' ); - Ravada::Request->remove_base_vm( + return if !$vm_local; + my @after; + push @after,(after_request => $req_nodes->id) if $req_nodes; + Ravada::Request->remove_base_vm( id_vm => $vm_local->id ,id_domain => $self->id ,uid => Ravada::Utils::user_daemon->id - ,after_request => $req_nodes->id + ,migrate => 0 ,_force => 1 - ); - $self->is_base(0); - } + ,@after + ); + $self->is_base(0); return $req_nodes; } @@ -3058,7 +3066,6 @@ sub _do_remove_base($self, $user) { $self->_vm->remove_file($file); } - $self->_cascade_remove_base_in_nodes(); } sub _pre_remove_base { @@ -3077,7 +3084,6 @@ sub _pre_remove_base { sub _post_remove_base { my $self = shift; - return if !$self->_vm->is_local; $self->_remove_base_db(@_); $self->_post_remove_base_domain(); @@ -5570,6 +5576,8 @@ sub set_base_vm($self, %args) { my $vm = delete $args{vm}; my $node = delete $args{node}; my $request = delete $args{request}; + my $migrate = delete $args{migrate}; + $migrate = 1 if !defined $migrate; confess "ERROR: Unknown arguments, valid are id_vm, value, user, node and vm " .Dumper(\%args) if keys %args; @@ -5580,6 +5588,7 @@ sub set_base_vm($self, %args) { confess "ERROR: user required" if !$user; + $request->status("working") if $request; $vm = $node if $node; $vm = Ravada::VM->open($id_vm) if !$vm; @@ -5590,6 +5599,11 @@ sub set_base_vm($self, %args) { $value = 1 if !defined $value; + warn "preparing base=$value from [".$self->_vm->id."] " + .$self->_vm->name." to " + ."[".$vm->id."]".$vm->name; + + my $id_request; if ($request) { $request->status("working"); @@ -5604,10 +5618,16 @@ sub set_base_vm($self, %args) { $self->_check_all_parents_in_node($vm); $request->status("working", "Syncing base volumes to ".$vm->host) if $request; + + warn "migrating ".$self->name." from ".$self->_vm->name + ." to ".$vm->name; + eval { $self->migrate($vm, $request); }; my $err = $@; + warn "migrated err='".($err or '')."'"; + warn $err if $err; if ( $err ) { $self->_set_base_vm_db($vm->id, 0); die $err; @@ -5619,27 +5639,49 @@ sub set_base_vm($self, %args) { $request->status("working","Removing base") if $request; $self->_set_base_vm_db($vm->id, $value); my $bases_vm = $self->_bases_vm(); - my $count=0; + my @nodes; my $node2; + warn Dumper($bases_vm); for my $id_vm ( keys %$bases_vm) { + next if $id_vm == $vm->id; if ($bases_vm->{$id_vm}) { - $node2 = Ravada::VM->open($id_vm); - $count++; - last if Ravada::VM::is_local($id_vm); + my $node = Ravada::VM->open($id_vm); + push @nodes,($node); + $node2 = $node if $node->is_local(); } } - if (!$count) { - $self->remove_base(); + ($node2) = @nodes if !$node2; + $vm->_migrate_domains($node2) if $node2 && $migrate; + if (!@nodes) { + $self->_do_remove_base($user); } else { - $vm->_migrate_domains($node2); my $instance = $vm->search_domain($self->name); $instance->_remove_instance($user,1) if $instance; + $self->_remove_files_not_shared(@nodes); + $self->_vm($node2); + $self->_data('id_vm' => $node2->id); } } return $self->_set_base_vm_db($vm->id, $value); } +sub _remove_files_not_shared($self, @nodes){ + + for my $file ($self->list_volumes,$self->list_files_base) { + next if $file =~ /\.iso$/; + next if !$self->_vm->file_exists($file); + + my ($dir) = $file =~ m{(.*)/}; + my $shared=0; + for my $node(@nodes) { + $shared++ if $self->_vm->shared_storage($node, $dir); + last if $shared; + } + $self->_vm->remove_file($file) if !$shared; + } +} + sub _check_all_parents_in_node($self, $vm, $request=undef) { my @bases; my $base = $self; @@ -5757,7 +5799,6 @@ Returns a list for virtual machine managers where this domain is base =cut sub list_vms($self, $check_host_devices=0, $only_available=0) { - confess "Domain is not base" if !$self->is_base; $check_host_devices = 1 if !defined $check_host_devices; diff --git a/lib/Ravada/Domain/KVM.pm b/lib/Ravada/Domain/KVM.pm index 4f3705190..3c7dc859a 100644 --- a/lib/Ravada/Domain/KVM.pm +++ b/lib/Ravada/Domain/KVM.pm @@ -343,9 +343,14 @@ sub remove { sub remove_instance($self, $user) { - eval { $self->domain->undefine(Sys::Virt::Domain::UNDEFINE_NVRAM) if $self->domain && !$self->is_removed }; + warn $self->_vm->name." ".$self->domain; + eval { + $self->domain->undefine(Sys::Virt::Domain::UNDEFINE_NVRAM) if $self->domain;# && !$self->is_removed + }; + warn $@ if $@; confess $@ if $@ && $@ !~ /libvirt error code: 42/; eval { $self->remove_disks() if $self->is_known }; + warn $@ if $@; confess $@ if $@ && $@ !~ /libvirt error code: 42/; } @@ -582,9 +587,10 @@ sub _set_backing_store($self, $disk, $backing_file) { sub _set_volumes_backing_store($self) { my $doc = XML::LibXML->load_xml(string - => $self->xml_description(Sys::Virt::Domain::XML_INACTIVE)) + => $self->domain->get_xml_description(Sys::Virt::Domain::XML_INACTIVE)) or die "ERROR: $!\n"; + my ($uuid) = $doc->findnodes("/domain/uuid/text()"); my @volumes_info = grep { defined($_) && $_->file } $self->list_volumes_info; my %vol = map { $_->file => $_ } @volumes_info; for my $disk ($doc->findnodes('/domain/devices/disk')) { @@ -2877,7 +2883,8 @@ sub _check_uuid($self, $doc, $node) { my @other_uuids; for my $domain ($node->vm->list_all_domains, $self->_vm->vm->list_all_domains) { - push @other_uuids,($domain->get_uuid_string); + push @other_uuids,($domain->get_uuid_string) + unless $domain->get_name eq $self->name; } return if !(grep /^$uuid$/,@other_uuids); @@ -2967,6 +2974,11 @@ sub internal_id($self) { return $self->domain->get_id(); } +sub _internal_uuid($self) { + confess "ERROR: Missing internal domain" if !$self->domain; + return $self->domain->get_id(); +} + sub autostart { return _internal_autostart(@_) } sub _internal_autostart($self, $value=undef, $user=undef) { diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index be795cdb8..0627864ef 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -94,7 +94,7 @@ our %VALID_ARG = ( ,active_storage_pool => { uid => 1, id_vm => 1, name => 1, value => 1} ,check_storage => { uid => 1 } ,create_storage_pool => { uid => 1, id_vm => 1, name => 1, directory => 1 } - ,set_base_vm=> {uid => 1, id_vm=> 1, id_domain => 1, value => 2 } + ,set_base_vm=> {uid => 1, id_vm=> 1, id_domain => 1, value => 2, migrate => 2 } ,cleanup => { timeout => 2 } ,clone => { uid => 1, id_domain => 1, name => 2, memory => 2, number => 2, volatile => 2, id_owner => 2 # If base has pools, from_pool = 1 if undefined diff --git a/lib/Ravada/VM/KVM.pm b/lib/Ravada/VM/KVM.pm index a503498ba..f23acb4ac 100644 --- a/lib/Ravada/VM/KVM.pm +++ b/lib/Ravada/VM/KVM.pm @@ -393,7 +393,7 @@ sub remove_file($self,@files) { $vol = $self->search_volume($file); } if (!$vol) { - warn "Warning: '$file' not found\n"; + confess "Warning: '$file' not found\n"; } $vol->delete if $vol; } @@ -604,7 +604,13 @@ Returns true if the file exists in this virtual manager storage =cut sub file_exists($self, $file) { - return -e $file if $self->is_local; + if ($self->is_local) { + if (-e $file) { + return 1; + } else { + return 0; + } + } return $self->_file_exists_remote($file); } @@ -613,7 +619,7 @@ sub _file_exists_remote($self, $file) { return 1 if $found; $file = $self->_follow_link($file) unless $file =~ /which$/; - return if !$self->vm; + return 0 if !$self->vm; for my $pool ($self->vm->list_all_storage_pools ) { next if !$pool->is_active; $self->_wait_storage( sub { $pool->refresh() } ); diff --git a/lib/Ravada/Volume/QCOW2.pm b/lib/Ravada/Volume/QCOW2.pm index c7ede6ae8..bf36678ff 100644 --- a/lib/Ravada/Volume/QCOW2.pm +++ b/lib/Ravada/Volume/QCOW2.pm @@ -133,7 +133,8 @@ sub spinoff($self) { my $file = $self->file; my $volume_tmp = $self->file.".$$.tmp"; - $self->vm->remove_file($volume_tmp); + $self->vm->remove_file($volume_tmp) + if $self->vm->file_exists($volume_tmp); my @cmd = ($QEMU_IMG ,'convert' diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index 9eb787b15..c2510b1e7 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -898,55 +898,102 @@ sub _test_clones($base, $vm) { } sub test_remove_base($vm, $node, $volatile) { - my $base = create_domain($vm); + my $base = create_domain_v2(vm => $vm, disk => 0.5, data => 0, swap => 0); + my $uuid = $base->domain->get_uuid_string(); $base->volatile_clones($volatile); my @volumes0 = $base->list_volumes( device => 'disk'); ok(!grep(/iso$/,@volumes0),"Expecting no iso files on device list ".Dumper(\@volumes0)) or exit; $base->prepare_base(user_admin); + is($base->domain->get_uuid_string(), $uuid); my @volumes_base = $base->list_files_base(); $base->set_base_vm(node => $node, user => user_admin); + is($base->domain->get_uuid_string(), $uuid) or exit; for my $file ( @volumes_base ) { ok($node->file_exists($file)) or die "Expecting file '$file' in ".$node->name; ok($vm->file_exists($file)) or die "Expecting file '$file' in ".$vm->name; } + test_domain_internal($base->name,$vm, $node); $base->remove_base_vm(node => $node, user => user_admin); + test_domain_internal($base->name,$vm); + test_domain_internal_not($base->name, $node); + is($base->domain->get_uuid_string(), $uuid); + + my $base_gone = $node->search_domain($base->name); + ok(!$base_gone,"Expecting ".$base->name." removed in ".$node->name) or exit; for my $file ( @volumes_base ) { - # too complicated to know if can be removed - # ok(!$node->file_exists($file)); + ok(!$node->file_exists($file)); ok($vm->file_exists($file), "Expecting file '$file' in local") or exit; } for my $file ( @volumes0 ) { - # too complicated to know if can be removed - # ok(!$node->file_exists($file),$file); + ok(!$node->file_exists($file),$file); ok($vm->file_exists($file), "Expecting file '$file' in local") or exit; } + + $base = Ravada::Domain->open($base->id); + is($base->domain->get_uuid_string(), $uuid); delete $base->{_data}->{id_vm}; - isnt($base->_data('id_vm'), $vm->id); + is($base->_data('id_vm'), $vm->id); $base->set_base_vm(node => $node, user => user_admin); is(scalar($base->list_vms), 2) or exit; - $base->remove_base(user_admin); - my @req = $base->list_requests(); - is(scalar @req,2); - ok(grep {$_->command eq 'remove_base_vm' } @req) or die Dumper(\@req); + test_domain_internal($base->name,$vm, $node); wait_request( debug => 0 ); + is($base->domain->get_uuid_string(), $uuid) or exit; + $base = Ravada::Domain->open($base->id); + diag("*******************remove base"); + diag("*******************remove base"); + $base->remove_base(user_admin); + is($base->domain->get_uuid_string(), $uuid); + + # my @req = $base->list_requests(); + # is(scalar @req,2); + # ok(grep {$_->command eq 'remove_base_vm' } @req) or die Dumper(\@req); + wait_request( debug => 1 ); + for my $file ( @volumes_base ) { is($vm->file_exists($file),0 , "Expecting no file '$file' in local") or exit; is($node->file_exists($file),0 - , "Expecting no file '$file' in local") or exit; + , "Expecting no file '$file' in node") or exit; } $base->remove(user_admin); } +sub test_domain_internal($name, @vms) { + + for my $vm (@vms) { + if (ref($vm) =~ /KVM$/) { + my $domain; + eval { $domain = $vm->vm->get_domain_by_name($name) }; + my $error = ($@ or ''); + ok($domain,"Expecting $name in ".$vm->name." $error") or confess; + } else { + die "TODO" . ref($vm); + } + } +} + +sub test_domain_internal_not($name, @vms) { + + for my $vm (@vms) { + if (ref($vm) =~ /KVM$/) { + my $domain; + eval { $domain = $vm->vm->get_domain_by_name($name) }; + my $error = ($@ or ''); + ok(!$domain,"Expecting not $name in ".$vm->name) or confess; + } else { + die "TODO" . ref($vm); + } + } +} sub _check_internal_autostart($domain, $expected) { if ($domain->type eq 'KVM') { ok($domain->domain->get_autostart) if $expected; From b56fba06e2ed9710f97daf84075a1c579a015ff0 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 28 May 2025 17:06:34 +0200 Subject: [PATCH 022/141] wip: remove base --- lib/Ravada.pm | 1 + lib/Ravada/Domain.pm | 21 +++++---------------- lib/Ravada/Domain/KVM.pm | 2 -- t/nodes/10_basic.t | 12 ++++++++++++ 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index e557fc676..ce7cd3a4a 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -6652,6 +6652,7 @@ sub _do_cmd_set_base_vm($self, $uid, $id_vm, $id_domain, $value, $request) { ,user => $user ,value => $value ,request => $request + ,migrate => $request->defined_arg('migrate') ); } diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index ededfa8f3..bc3cbc822 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -3001,8 +3001,8 @@ Makes the domain a regular, non-base virtual machine and removes the base files. =cut sub remove_base($self, $user) { - $self->_cascade_remove_base_in_nodes(); - $self->_do_remove_base($user); + $self->_cascade_remove_base_in_nodes() + or $self->_do_remove_base($user); } sub _cascade_remove_base_in_nodes($self) { @@ -3052,11 +3052,10 @@ sub _do_remove_base($self, $user) { my $backing_file = $vol->backing_file; next if !$backing_file; - # confess "Error: no backing file for ".$vol->file if !$backing_file; $vol->block_commit(); $self->_vm->remove_file($vol->file); $self->_vm->copy_file($backing_file, $vol->file,mode => '0700'); - # move($backing_file, $vol->file) or die "$! $backing_file -> ".$vol->file; + $self->_vm->remove_file($backing_file); } for my $file ($self->list_files_base) { @@ -5599,9 +5598,6 @@ sub set_base_vm($self, %args) { $value = 1 if !defined $value; - warn "preparing base=$value from [".$self->_vm->id."] " - .$self->_vm->name." to " - ."[".$vm->id."]".$vm->name; my $id_request; @@ -5637,11 +5633,9 @@ sub set_base_vm($self, %args) { } else { $request->status("working","Removing base") if $request; - $self->_set_base_vm_db($vm->id, $value); my $bases_vm = $self->_bases_vm(); my @nodes; my $node2; - warn Dumper($bases_vm); for my $id_vm ( keys %$bases_vm) { next if $id_vm == $vm->id; if ($bases_vm->{$id_vm}) { @@ -5653,6 +5647,7 @@ sub set_base_vm($self, %args) { ($node2) = @nodes if !$node2; $vm->_migrate_domains($node2) if $node2 && $migrate; if (!@nodes) { + $self->_vm($vm); $self->_do_remove_base($user); } else { my $instance = $vm->search_domain($self->name); @@ -5755,13 +5750,7 @@ Removes a base in a Virtual Machine Manager node. =cut sub remove_base_vm($self, %args) { - my $user = delete $args{user}; - my $vm = delete $args{vm}; - $vm = delete $args{node} if !$vm; - confess "ERROR: Unknown arguments ".join(',',sort keys %args).", valid are user and vm." - if keys %args; - - return $self->set_base_vm(vm => $vm, user => $user, value => 0); + return $self->set_base_vm(%args, value => 0); } =head2 file_screenshot diff --git a/lib/Ravada/Domain/KVM.pm b/lib/Ravada/Domain/KVM.pm index 3c7dc859a..37e1489b8 100644 --- a/lib/Ravada/Domain/KVM.pm +++ b/lib/Ravada/Domain/KVM.pm @@ -342,8 +342,6 @@ sub remove { } sub remove_instance($self, $user) { - - warn $self->_vm->name." ".$self->domain; eval { $self->domain->undefine(Sys::Virt::Domain::UNDEFINE_NVRAM) if $self->domain;# && !$self->is_removed }; diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index c2510b1e7..55eabcd49 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -934,6 +934,12 @@ sub test_remove_base($vm, $node, $volatile) { ok($vm->file_exists($file), "Expecting file '$file' in local") or exit; } + my $bases_vm = $base->_bases_vm(); + for my $id_vm ( keys %$bases_vm) { + delete $bases_vm->{$id_vm} if !$bases_vm->{$id_vm}; + } + is(scalar(keys %$bases_vm),1); + $base = Ravada::Domain->open($base->id); is($base->domain->get_uuid_string(), $uuid); delete $base->{_data}->{id_vm}; @@ -941,6 +947,12 @@ sub test_remove_base($vm, $node, $volatile) { $base->set_base_vm(node => $node, user => user_admin); is(scalar($base->list_vms), 2) or exit; + $bases_vm = $base->_bases_vm(); + for my $id_vm ( keys %$bases_vm) { + delete $bases_vm->{$id_vm} if !$bases_vm->{$id_vm}; + } + is(scalar(keys %$bases_vm),2); + test_domain_internal($base->name,$vm, $node); wait_request( debug => 0 ); From b616acc1bff7b6cd939f7770ffd3fc8d21cc66a2 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 29 May 2025 12:55:57 +0200 Subject: [PATCH 023/141] wip: remove all instances --- lib/Ravada/Domain.pm | 11 +++-------- lib/Ravada/Volume.pm | 2 +- t/nodes/10_basic.t | 16 +++++++--------- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index bc3cbc822..825521575 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -1203,10 +1203,11 @@ sub _check_tmp_volumes($self) { if !$self->id_base; my $vm = $self->_vm; + $self->_vm->refresh_storage_pools(); for my $vol ( $self->list_volumes_info) { next unless $vol->file && $vol->file =~ /\.(TMP|SWAP)\./; - $vol->delete(); + $vol->delete() ; my $base = Ravada::Domain->open($self->id_base); my @volumes = $base->list_files_base_target; my ($file_base) = grep { $_->[1] eq $vol->info->{target} } @volumes; @@ -2608,7 +2609,6 @@ sub _after_remove_domain($self, $user, $cascade=undef) { $id_base = $self->_data('id_base') if $self->is_known(); if ($self->is_known && $self->is_base) { - # $self->_do_remove_base($user); $self->_remove_files_base(); } for my $backup ( $self->list_backups ) { @@ -2688,6 +2688,7 @@ sub _remove_instance($self, $user, $cascade, $id_instance=undef) { $self->remove_instance($user); }; warn $@ if $@; + $self->_remove_files_base(); $sth_delete->execute($id_instance) if defined $id_instance; } @@ -3008,13 +3009,11 @@ sub remove_base($self, $user) { sub _cascade_remove_base_in_nodes($self) { my $req_nodes; my $vm_local; - warn "cascade"; for my $vm ( $self->list_vms ) { if ( $vm->is_local ) { $vm_local= $vm; next; } - warn $vm->name; my @after; push @after,(after_request => $req_nodes->id) if $req_nodes; $req_nodes = Ravada::Request->remove_base_vm( @@ -5615,14 +5614,10 @@ sub set_base_vm($self, %args) { $request->status("working", "Syncing base volumes to ".$vm->host) if $request; - warn "migrating ".$self->name." from ".$self->_vm->name - ." to ".$vm->name; - eval { $self->migrate($vm, $request); }; my $err = $@; - warn "migrated err='".($err or '')."'"; warn $err if $err; if ( $err ) { $self->_set_base_vm_db($vm->id, 0); diff --git a/lib/Ravada/Volume.pm b/lib/Ravada/Volume.pm index bc9fc44ad..94d858e04 100644 --- a/lib/Ravada/Volume.pm +++ b/lib/Ravada/Volume.pm @@ -221,7 +221,7 @@ sub set_info($self, $name, $value) { sub delete($self) { my $file = $self->file; - $self->vm->remove_file($file) if $file; + $self->vm->remove_file($file) if $file && $self->vm->file_exists($file); my $sth = $self->_dbh->prepare("DELETE FROM volumes WHERE file=? AND id_domain=?"); $sth->execute($file, $self->domain->id); } diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index 55eabcd49..902399522 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -281,13 +281,12 @@ sub _create_2_clones_same_port($vm, $node, $base, $ip_local, $ip_remote) { } sub _start_clone_in_node($vm, $node, $base) { + ok(scalar($base->list_vms) >1) or confess Dumper([map { $_->name } $base->list_vms]); my $found_clone; for my $try ( 1 .. 20 ) { my $clone1 = $base->clone(name => new_domain_name, user => user_admin); - _remove_tmp($clone1,$vm); - ok(scalar($base->list_vms) >1) or confess Dumper([map { $_->name } $base->list_vms]); eval { $clone1->start(user_admin) }; - is($@,'') or die "Error $@ starting ".$clone1->name; + is($@,'') or confess "Error $@ starting ".$clone1->name; $found_clone = $clone1; last if $clone1->_vm->id == $node->id; ok(scalar($base->list_vms) >1) or confess Dumper([map { $_->name } $base->list_vms]); @@ -426,16 +425,16 @@ sub _remove_base_files($base, $node) { } sub _remove_tmp($domain, $vm = $domain->_vm) { + $vm->refresh_storage_pools(); my ($found_swap, $found_tmp); for my $vol ( $domain->list_volumes ) { if ( $vol =~ /TMP/ ) { - $vm->remove_file($vol); $found_tmp= 1; } if ( $vol =~ /SWAP/ ) { - $vm->remove_file($vol); $found_swap = 1; } + $vm->remove_file($vol) if $vm->file_exists($vol); } die "Error: no swap found in ".$domain->name if !$found_swap; die "Error: no tmp found in ".$domain->name if !$found_tmp; @@ -959,15 +958,13 @@ sub test_remove_base($vm, $node, $volatile) { is($base->domain->get_uuid_string(), $uuid) or exit; $base = Ravada::Domain->open($base->id); - diag("*******************remove base"); - diag("*******************remove base"); $base->remove_base(user_admin); is($base->domain->get_uuid_string(), $uuid); # my @req = $base->list_requests(); # is(scalar @req,2); # ok(grep {$_->command eq 'remove_base_vm' } @req) or die Dumper(\@req); - wait_request( debug => 1 ); + wait_request( debug => 0 ); for my $file ( @volumes_base ) { is($vm->file_exists($file),0 @@ -2144,13 +2141,14 @@ sub test_remove_instances($base, @nodes) { my @volumes = $base->list_volumes(); my @files_base = $base->list_files_base(); $base->remove(user_admin); + wait_request(debug => 0); my $vm = $base->_vm; for my $vm ( $base->_vm, @nodes ) { for my $file ( @volumes, @files_base ) { if ($file =~ /\.iso$/) { ok($vm->file_exists($file)); } else { - ok(!$vm->file_exists($file), "Expecting no file '$file' in ".$vm->name) or exit; + ok(!$vm->file_exists($file), "Expecting no file '$file' in ".$vm->name) or confess; } } } From dd14c8e61c02fb259fe30ecb03e21af73ededb32 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 29 May 2025 15:52:28 +0200 Subject: [PATCH 024/141] wip: remove tmp vols and create on node with HD --- lib/Ravada/Domain.pm | 2 -- lib/Ravada/VM.pm | 5 ++++- t/nodes/10_basic.t | 15 ++++++++------- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 825521575..ce8528a05 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -5802,7 +5802,6 @@ sub list_vms($self, $check_host_devices=0, $only_available=0) { my @host_devices = $self->list_host_devices(); while (my ($id_vm, $name_vm, $id_request, $is_active, $enabled, $cached_down) = $sth->fetchrow) { next if $only_available && ( !$is_active || !$enabled); - my $t1 = time; if ($only_available && $cached_down) { next if time-$cached_down < $self->_vm->timeout_down_cache(); } @@ -5814,7 +5813,6 @@ sub list_vms($self, $check_host_devices=0, $only_available=0) { next if $check_host_devices && !$self->_available_hds($id_vm, \@host_devices); my $vm; eval { $vm = Ravada::VM->open($id_vm) }; - warn "id_domain: ".$self->id."\n".$@ if $@; push @vms,($vm) if $vm; } return @vms; diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index 70a437427..26921a5d2 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -554,7 +554,8 @@ sub _around_create_domain { if ($base && $base->is_base) { $request->status("balancing") if $request; - my $vm = $self->balance_vm($owner->id, $base); + my $check_hd= ($active or $args_create{volatile} or 0); + my $vm = $self->balance_vm($owner->id, $base, undef, $check_hd); if (!$vm) { die "Error: No free nodes available.\n"; @@ -2253,6 +2254,8 @@ sub balance_vm($self, $uid, $base=undef, $id_domain=undef, $host_devices=1) { } return $vms_active[0] if scalar(@vms_active)==1; + die "Error: No free nodes available.\n" if !scalar(@vms_active); + if ($base && $base->_data('balance_policy') == 1 ) { my $vm = $self->_balance_already_started($uid, $id_domain, \@vms_active); return $vm if $vm; diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index 902399522..2a415bd97 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -434,7 +434,8 @@ sub _remove_tmp($domain, $vm = $domain->_vm) { if ( $vol =~ /SWAP/ ) { $found_swap = 1; } - $vm->remove_file($vol) if $vm->file_exists($vol); + $vm->remove_file($vol) + if $vol =~ /TMP|SWAP/ && $vm->file_exists($vol); } die "Error: no swap found in ".$domain->name if !$found_swap; die "Error: no tmp found in ".$domain->name if !$found_tmp; @@ -898,17 +899,19 @@ sub _test_clones($base, $vm) { sub test_remove_base($vm, $node, $volatile) { my $base = create_domain_v2(vm => $vm, disk => 0.5, data => 0, swap => 0); - my $uuid = $base->domain->get_uuid_string(); + my $uuid; + $uuid = $base->domain->get_uuid_string() if $vm->type eq 'KVM'; $base->volatile_clones($volatile); my @volumes0 = $base->list_volumes( device => 'disk'); ok(!grep(/iso$/,@volumes0),"Expecting no iso files on device list ".Dumper(\@volumes0)) or exit; $base->prepare_base(user_admin); - is($base->domain->get_uuid_string(), $uuid); + is($base->domain->get_uuid_string(), $uuid) if $vm->type eq 'KVM'; + my @volumes_base = $base->list_files_base(); $base->set_base_vm(node => $node, user => user_admin); - is($base->domain->get_uuid_string(), $uuid) or exit; + is($base->domain->get_uuid_string(), $uuid) if $vm->type eq 'KVM'; for my $file ( @volumes_base ) { ok($node->file_exists($file)) or die "Expecting file '$file' in ".$node->name; @@ -920,7 +923,7 @@ sub test_remove_base($vm, $node, $volatile) { $base->remove_base_vm(node => $node, user => user_admin); test_domain_internal($base->name,$vm); test_domain_internal_not($base->name, $node); - is($base->domain->get_uuid_string(), $uuid); + is($base->domain->get_uuid_string(), $uuid) if $vm->type eq 'KVM'; my $base_gone = $node->search_domain($base->name); ok(!$base_gone,"Expecting ".$base->name." removed in ".$node->name) or exit; @@ -1227,7 +1230,6 @@ sub _machine_types($vm) { $types{$1} = [ $version,$machine ] if !exists $types{$1} || $version > $types{$1}->[0]; } - warn Dumper(\%types); for (keys %types) { push @types,($types{$_}->[1]); } @@ -1381,7 +1383,6 @@ sub test_fill_memory($vm, $node, $migrate, $start=0) { last if $migrate && exists $nodes{$vm->name} && $nodes{$vm->name} > 2; } - warn Dumper(\%nodes); ok($created_in_node,"Expecting some clones created in node ".$node->name) or exit; ok(exists $nodes{$vm->name},"Expecting some clones to node ".$vm->name." ".$vm->id) or die Dumper(\%nodes); From 1a9bdefa0670bc0a0e7b5f4e02f06d8e12b7da48 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 29 May 2025 16:05:07 +0200 Subject: [PATCH 025/141] wip: req vm local when in nodes too --- lib/Ravada/Domain.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index ce8528a05..bb20ad773 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -3025,7 +3025,7 @@ sub _cascade_remove_base_in_nodes($self) { ,@after ); } - return if !$vm_local; + return if !$vm_local || !$req_nodes; my @after; push @after,(after_request => $req_nodes->id) if $req_nodes; Ravada::Request->remove_base_vm( From ae19c3493cf0f4fe0fe9726cee3fc26f831e566a Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Fri, 30 May 2025 15:17:10 +0200 Subject: [PATCH 026/141] wip: set device after downloading --- lib/Ravada.pm | 9 ++++++++- lib/Ravada/Domain.pm | 3 ++- t/request/15_download.t | 1 + t/vm/60_new_args.t | 29 +++++++++++++++++++++++++++++ 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index ce7cd3a4a..a513bd56c 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -5411,6 +5411,11 @@ sub _cmd_download { my $device_cdrom = $vm->search_volume_path_re(qr($iso->{file_re})); if ($device_cdrom) { + my $sth = $CONNECTOR->dbh->prepare( + "UPDATE iso_images SET device = ? " + ." WHERE id=?" + ); + $sth->execute($device_cdrom,$id_iso); $request->status('done',"$iso->{name} already downloaded"); return; } @@ -5421,7 +5426,9 @@ sub _cmd_download { $self->_download_local_and_rsync($request, $vm, $iso); } - Ravada::Request->refresh_storage(id_vm => $vm->id); + Ravada::Request->refresh_storage(id_vm => $vm->id + ,uid => Ravada::Utils->user_daemon->id + ); } sub _download_local_and_rsync($self, $request, $vm, $iso) { diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index bb20ad773..c7fe264d8 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -1213,7 +1213,8 @@ sub _check_tmp_volumes($self) { my ($file_base) = grep { $_->[1] eq $vol->info->{target} } @volumes; if (!$file_base) { warn "Error: I can't find base volume for target ".$vol->info->{target} - .Dumper(\@volumes); + ."\n"; + next; } my $vol_base = Ravada::Volume->new( file => $file_base->[0] , is_base => 1 diff --git a/t/request/15_download.t b/t/request/15_download.t index d523da5c3..6e269185a 100644 --- a/t/request/15_download.t +++ b/t/request/15_download.t @@ -27,6 +27,7 @@ sub test_download($vm, $iso0, $test=0) { , id_vm => $vm->id # , delay => 4 , test => $test + , uid => user_admin->id ); is($req1->status, 'requested'); diff --git a/t/vm/60_new_args.t b/t/vm/60_new_args.t index 788e43e2b..8691ccc04 100644 --- a/t/vm/60_new_args.t +++ b/t/vm/60_new_args.t @@ -252,6 +252,33 @@ sub test_small { } +sub test_download { + + my $vm_name = shift; + + my $id_iso = search_id_iso('Alpine'); + my $req = Ravada::Request->download( + vm => $vm_name + ,id_iso => $id_iso + ,uid => user_admin->id + ,test => 1 + ); + + wait_request(debug => 0); + + + my $sth = connector->dbh->prepare( + "SELECT device FROM iso_images " + ." WHERE id=?" + ); + $sth->execute($id_iso); + + my ($device) = $sth->fetchrow; + + ok($device) or exit; + +} + ####################################################################### clean(); @@ -281,6 +308,8 @@ for my $vm_name ( vm_names() ) { skip $msg,10 if !$vm_ok; use_ok("Ravada::VM::$vm_name"); + + test_download($vm_name); test_args($vm_name); test_small($vm_name); }; From eefbb6ff4292d8a6b1a74aeb6ffadf05fc723634 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Fri, 30 May 2025 15:20:02 +0200 Subject: [PATCH 027/141] wip: set id for refresh storage --- lib/Ravada.pm | 1 + lib/Ravada/VM/KVM.pm | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index a513bd56c..cec0a3db1 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -3532,6 +3532,7 @@ sub _add_extra_iso($domain, $request, $previous_request) { if $previous_request; my $req = Ravada::Request->refresh_storage(id_vm => $domain->_vm->id + ,uid => Ravada::Utils->user_daemon->id ,@after_request); @after_request = ( after_request => $req->id ) if $req; diff --git a/lib/Ravada/VM/KVM.pm b/lib/Ravada/VM/KVM.pm index f23acb4ac..4f4655544 100644 --- a/lib/Ravada/VM/KVM.pm +++ b/lib/Ravada/VM/KVM.pm @@ -1548,7 +1548,6 @@ sub _download_file_external($self, $url, $device, $verbose=1, $test=0) { # return $url; - warn join(" ",@cmd)."\n"; warn join(" ",@cmd)."\n" if $verbose; my ($out, $err) = $self->run_command(@cmd); warn "out=$out" if $out && $verbose; From 3e25239241172bb75c7d80889b685eb6cc278391 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Fri, 30 May 2025 15:55:11 +0200 Subject: [PATCH 028/141] wip: write only if allowed --- lib/Ravada/VM/KVM.pm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/Ravada/VM/KVM.pm b/lib/Ravada/VM/KVM.pm index 4f4655544..b118b0e66 100644 --- a/lib/Ravada/VM/KVM.pm +++ b/lib/Ravada/VM/KVM.pm @@ -1538,7 +1538,8 @@ sub _download_file_external($self, $url, $device, $verbose=1, $test=0) { $device = $self->dir_img()."/".$device; } confess "I can't find filename in '$url'" unless $device; - $self->write_file($device,"mock") unless $self->file_exists($device); + $self->write_file($device,"mock") + if !$self->file_exists($device) && !$<; return $self->_download_file_external_headers($url); } return $url if $self->file_exists($device); From e1d7d42094fecf55bab6e43d80b947e88d8e10d0 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 2 Jun 2025 10:29:55 +0200 Subject: [PATCH 029/141] wip: create the storage pool for backups --- lib/Ravada/VM.pm | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index 26921a5d2..f9837e5a3 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -3027,9 +3027,29 @@ sub dir_backup($self) { die "Error on mkdir -p $dir_backup $error" if $error; } } + $self->_create_pool_backup($dir_backup); return $dir_backup; } +sub _create_pool_backup($self, $dir) { + my %pool_name; + for my $pool ($self->list_storage_pools(1)) { + my $path = $pool->{path}; + if ($path && $path eq $dir) { + return; + } + $pool_name{$pool->{name}}++; + } + my ($name0) = $dir =~ m{.*/(.*)}; + my $name = $name0; + my $cont=2; + for (;;) { + last if !$pool_name{$name}; + $name = $name0."_".$cont++; + } + $self->create_storage_pool($name, $dir); +} + sub _follow_link($self, $file) { my ($dir, $name) = $file =~ m{(.*)/(.*)}; my $file2 = $file; From d927ae9f13594f230f90f3754e881bf32ad1c3a5 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 2 Jun 2025 10:30:21 +0200 Subject: [PATCH 030/141] wip: test different same VM and clone instances --- t/nodes/10_basic.t | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index 2a415bd97..6a395fe8e 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -30,33 +30,33 @@ sub test_reuse_vm($node) { $domain->set_base_vm(vm => $node, user => user_admin); my $clone1 = $domain->clone(name => new_domain_name, user => user_admin); - my $clone2 = $domain->clone(name => new_domain_name, user => user_admin); - is($clone1->_vm, $clone2->_vm, $clone1->_vm->name); - is($clone1->_vm->id, $clone2->_vm->id); + + $domain->set_base_vm(vm => $node, user => user_admin); is($clone1->list_instances,1); + my $vm_local = rvd_back->search_vm($node->type,'localhost'); + + $clone1->migrate($vm_local); $clone1->migrate($node); is($clone1->_data('id_vm'), $node->id); $clone2->migrate($node); is($clone2->_data('id_vm'), $node->id); - is($clone1->list_instances,2); + is($clone1->list_instances,2) or die $node->type." ".$clone1->name; - is($clone1->_vm, $clone2->_vm); - is($clone1->_vm, $clone2->_vm); is($clone1->_vm->{_ssh}, $clone2->_vm->{_ssh}); is($clone1->is_local, 0 ); test_remove($clone1, $node); - my $vm_local = rvd_back->search_vm($node->type,'localhost'); is($vm_local->is_local, 1); $clone2->migrate($vm_local); is($clone2->is_local, 1 ); test_remove($clone2, $node); + remove_domain($domain); } sub test_remove_req($vm, $node) { @@ -868,7 +868,7 @@ sub test_clone_remote($vm, $node) { is($clone->list_instances,1); _test_old_base($base, $vm); - _test_clones($base, $vm); + _test_clones($base, $clone->_vm); $clone->remove(user_admin); is($clone->list_instances,undef); $base->remove(user_admin); From b45c15558f5f81b5eb4803327def1c19411cd34b Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 5 Jun 2025 12:39:47 +0200 Subject: [PATCH 031/141] wip: properly remove instance and check for leftovers --- lib/Ravada/Domain.pm | 3 ++- lib/Ravada/Domain/Void.pm | 10 +++++++--- lib/Ravada/Front/Domain.pm | 2 ++ lib/Ravada/VM/Void.pm | 5 ++++- t/lib/Test/Ravada.pm | 8 ++++++-- t/nodes/10_basic.t | 38 ++++++++++++++++++++++++++++++-------- 6 files changed, 51 insertions(+), 15 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index c7fe264d8..c04337ac6 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -56,6 +56,7 @@ _init_connector(); requires 'name'; requires 'remove'; +requires 'remove_instance'; requires 'display_info'; requires 'is_active'; @@ -5506,7 +5507,7 @@ sub _post_migrate($self, $node, $request = undef) { $self->_vm($node); $self->_update_id_vm(); - $node->_add_instance_db($node->id); + $node->_add_instance_db($self->id); # TODO: update db instead set this value $self->{_migrated} = 1; diff --git a/lib/Ravada/Domain/Void.pm b/lib/Ravada/Domain/Void.pm index 60a1127ba..cf6886d4f 100644 --- a/lib/Ravada/Domain/Void.pm +++ b/lib/Ravada/Domain/Void.pm @@ -210,9 +210,12 @@ sub resume { return $self->_store(is_paused => 0 ); } -sub remove { - my $self = shift; +sub remove($self, $user) { + + $self->remove_instance($user); +} +sub remove_instance($self, $user) { $self->remove_disks(); my $config_file = $self->_config_file; @@ -221,8 +224,9 @@ sub remove { warn $err if $err; } if ($self->_vm->file_exists($config_file.".lock")) { - $self->_vm->run_command("/bin/rm",$config_file.".lock"); + $self->_vm->remove_file($config_file.".lock"); } + } sub can_hibernate { return 1; } diff --git a/lib/Ravada/Front/Domain.pm b/lib/Ravada/Front/Domain.pm index c017d2410..5e4c5f5d0 100644 --- a/lib/Ravada/Front/Domain.pm +++ b/lib/Ravada/Front/Domain.pm @@ -173,9 +173,11 @@ sub name($self) { return $self->_data('name') } +# those should run a request sub pause { confess "TODO" } sub prepare_base { confess "TODO" } sub remove { confess "TODO" } +sub remove_instance { confess "TODO" } sub rename { confess "TODO" } sub resume { confess "TODO" } sub screenshot { confess "TODO" } diff --git a/lib/Ravada/VM/Void.pm b/lib/Ravada/VM/Void.pm index 0d87e54a4..ed4ac2184 100644 --- a/lib/Ravada/VM/Void.pm +++ b/lib/Ravada/VM/Void.pm @@ -720,7 +720,10 @@ Returns true if the file exists in this virtual manager storage =cut sub file_exists( $self, $file ) { - return -e $file if $self->is_local; + if ( $self->is_local) { + return 1 if -e $file; + return 0; + } my $ssh = $self->_ssh; confess "Error: no ssh connection to ".$self->name if ! $ssh; diff --git a/t/lib/Test/Ravada.pm b/t/lib/Test/Ravada.pm index bc63829cb..b46e13eb1 100644 --- a/t/lib/Test/Ravada.pm +++ b/t/lib/Test/Ravada.pm @@ -110,6 +110,8 @@ create_domain config_host_devices + check_leftovers + end ); @@ -2349,8 +2351,6 @@ sub start_node($node) { eval { $node2->run_command("hwclock","--hctosys") }; is($@,'',"Expecting no error setting clock on ".$node->name." ".($@ or '')); - my $sth = $CONNECTOR->dbh->prepare("DELETE FROM domains WHERE name=?"); - $sth->execute($domain->name); } sub remove_node($node) { @@ -2697,6 +2697,10 @@ sub _check_leftovers { } +sub check_leftovers { + _check_leftovers(); +} + sub _check_removed_nbd { return if $<; my ($in, $out, $err); diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index 6a395fe8e..7914764c3 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -78,6 +78,8 @@ sub test_remove_req($vm, $node) { is($req->error, ''); $domain->remove(user_admin); + + check_leftovers(); } sub test_remove($clone, $node) { @@ -100,6 +102,8 @@ sub test_remove($clone, $node) { my ($out, $err) = $node->run_command("ls", $file); ok(!$out, "Expecting no file '$file' in ".$node->name) or exit; } + + check_leftovers(); } sub test_iptables($node, $node2) { @@ -226,6 +230,7 @@ sub test_iptables_close($vm, $node) { sub _remove_clones($domain) { _remove_domain($domain,0); + check_leftovers(); } sub _remove_domain($domain, $remove_base=0) { @@ -242,6 +247,7 @@ sub _remove_domain($domain, $remove_base=0) { if $remove_base; wait_request(); + check_leftovers(); } sub _create_2_clones_same_port($vm, $node, $base, $ip_local, $ip_remote) { @@ -415,6 +421,7 @@ sub test_removed_base_file($vm, $node) { $req->stop; } $base->remove(user_admin); + check_leftovers(); } sub _remove_base_files($base, $node) { @@ -716,6 +723,7 @@ sub _wait_machine_removed($clone) { } wait_request(debug => 0); + check_leftovers(); } sub test_domain_gone($vm, $node) { @@ -908,9 +916,9 @@ sub test_remove_base($vm, $node, $volatile) { $base->prepare_base(user_admin); is($base->domain->get_uuid_string(), $uuid) if $vm->type eq 'KVM'; - my @volumes_base = $base->list_files_base(); $base->set_base_vm(node => $node, user => user_admin); + check_leftovers(); is($base->domain->get_uuid_string(), $uuid) if $vm->type eq 'KVM'; for my $file ( @volumes_base ) { ok($node->file_exists($file)) @@ -943,7 +951,7 @@ sub test_remove_base($vm, $node, $volatile) { is(scalar(keys %$bases_vm),1); $base = Ravada::Domain->open($base->id); - is($base->domain->get_uuid_string(), $uuid); + is($base->domain->get_uuid_string(), $uuid) if $vm->type eq 'KVM'; delete $base->{_data}->{id_vm}; is($base->_data('id_vm'), $vm->id); @@ -959,10 +967,10 @@ sub test_remove_base($vm, $node, $volatile) { test_domain_internal($base->name,$vm, $node); wait_request( debug => 0 ); - is($base->domain->get_uuid_string(), $uuid) or exit; + is($base->domain->get_uuid_string(), $uuid) if $vm->type eq 'KVM'; $base = Ravada::Domain->open($base->id); $base->remove_base(user_admin); - is($base->domain->get_uuid_string(), $uuid); + is($base->domain->get_uuid_string(), $uuid) if $vm->type eq 'KVM'; # my @req = $base->list_requests(); # is(scalar @req,2); @@ -977,6 +985,7 @@ sub test_remove_base($vm, $node, $volatile) { } $base->remove(user_admin); + check_leftovers(); } sub test_domain_internal($name, @vms) { @@ -988,7 +997,9 @@ sub test_domain_internal($name, @vms) { my $error = ($@ or ''); ok($domain,"Expecting $name in ".$vm->name." $error") or confess; } else { - die "TODO" . ref($vm); + my $domain = $vm->search_domain($name); + my $config_file = $domain->_config_file; + ok($vm->file_exists($config_file)); } } } @@ -1002,7 +1013,12 @@ sub test_domain_internal_not($name, @vms) { my $error = ($@ or ''); ok(!$domain,"Expecting not $name in ".$vm->name) or confess; } else { - die "TODO" . ref($vm); + my $domain = $vm->search_domain($name); + ok(!$domain); + if ($domain) { + my $config_file = $domain->_config_file; + ok(!$vm->file_exists($config_file)); + } } } } @@ -1308,7 +1324,11 @@ sub _import_clone($vm) { sub _shutdown_domains(@nodes) { for my $node (@nodes) { for my $domain ($node->list_domains(active => 1)) { - $domain->shutdown_now(user_admin); + my $base_name=base_domain_name(); + if ( $domain->name =~ /$base_name/ ) { + warn $domain->name; + $domain->shutdown_now(user_admin); + } } } } @@ -1317,6 +1337,7 @@ sub test_fill_memory($vm, $node, $migrate, $start=0) { diag("Testing fill memory ".$vm->type.", migrate=$migrate, start=$start"); _shutdown_domains($vm,$node); + start_node($node); my $base = _import_clone($vm); if (!$base) { @@ -2153,6 +2174,7 @@ sub test_remove_instances($base, @nodes) { } } } + check_leftovers(); } @@ -2208,7 +2230,7 @@ for my $vm_name (vm_names() ) { start_node($node); for my $volatile (1,0) { - test_remove_base($vm, $node, $volatile); + test_remove_base($vm, $node, $volatile); } test_change_clone($vm, $node); From 4fbbb4f483e2806882c07d5499d157d2648a8520 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 5 Jun 2025 16:07:32 +0200 Subject: [PATCH 032/141] wip: request with uid --- script/rvd_back | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/rvd_back b/script/rvd_back index e7b2c5ebf..44778a0ff 100755 --- a/script/rvd_back +++ b/script/rvd_back @@ -345,8 +345,8 @@ sub start { $ravada->set_debug_value(); $ravada->_wait_pids(); $ravada->_clean_tls(); - Ravada::Request->refresh_vms(); - Ravada::Request->refresh_storage(); + Ravada::Request->refresh_vms(uid =>Ravada::Utils::user_daemon->id); + Ravada::Request->refresh_storage(uid =>Ravada::Utils::user_daemon->id); Ravada::Request->update_iso_urls(uid =>Ravada::Utils::user_daemon->id); Ravada::Request->list_host_devices(uid =>Ravada::Utils::user_daemon->id); autostart_machines($ravada); From 6ddf925da4abcddbf2ed30046c199316820194db Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Fri, 6 Jun 2025 10:05:15 +0200 Subject: [PATCH 033/141] wip: file missing returns 0 --- t/vm/20_base.t | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/vm/20_base.t b/t/vm/20_base.t index 496066ac7..220993a97 100644 --- a/t/vm/20_base.t +++ b/t/vm/20_base.t @@ -1710,7 +1710,7 @@ sub test_prepare_base_disk_missing($vm) { my ($two) = grep(!/iso$/i, reverse @volumes); unlink $two or die "$! $two"; - is($vm->file_exists($two),undef); + is($vm->file_exists($two),0); eval { $domain->prepare_base(user_admin); From baf0823ad88e940ab191972d01c3d9c42ec6ff57 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 12 Jun 2025 13:07:05 +0200 Subject: [PATCH 034/141] wip: improved download ISO --- lib/Ravada.pm | 5 +++-- lib/Ravada/Front.pm | 7 +++++-- lib/Ravada/VM/KVM.pm | 33 +++++++++++++++++++++++---------- t/front/20_create_domain.t | 4 +--- t/kvm/a10_pools.t | 3 ++- t/lib/Test/Ravada.pm | 1 + t/repository/10_iso.t | 2 +- t/request/15_download.t | 5 ++++- 8 files changed, 40 insertions(+), 20 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index e8975b3eb..bbe2de869 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -588,7 +588,7 @@ sub _update_isos { ,xml => 'bionic-amd64.xml' ,xml_volume => 'bionic64-volume.xml' ,url => 'https://download.opensuse.org/distribution/leap/15.4/iso/' - ,file_re => 'openSUSE-Leap-15.\d-NET-x86_64-Current.iso' + ,file_re => 'openSUSE-Leap-15.\d-\w+-x86_64-Current.iso' } ,xubuntu_noble => { @@ -5423,6 +5423,7 @@ sub _cmd_download { my $test = $request->defined_arg('test'); my $iso = $vm->_search_iso($id_iso); + confess Dumper($iso) if !$iso->{file_re}; my $device_cdrom = $vm->search_volume_path_re(qr($iso->{file_re})); if ($device_cdrom) { @@ -5432,7 +5433,7 @@ sub _cmd_download { ); $sth->execute($device_cdrom,$id_iso); $request->status('done',"$iso->{name} already downloaded"); - return; + return if !$test; } if ($vm->is_local) { diff --git a/lib/Ravada/Front.pm b/lib/Ravada/Front.pm index 3926f8ec3..fcfe01c69 100644 --- a/lib/Ravada/Front.pm +++ b/lib/Ravada/Front.pm @@ -792,8 +792,11 @@ sub _fix_iso_file_re($row) { if ($row->{rename_file}) { $row->{file_re} = $row->{rename_file}; } elsif ($row->{url} ) { - my ($file_re) = $row->{url} =~ m{.*/([^/]+)$}; - $row->{file_re}= $file_re if $file_re; + my ($url,$file_re) = $row->{url} =~ m{(.*)/([^/]+)$}; + if ($file_re && $url) { + $row->{file_re}= $file_re; + $row->{url}= $url; + } } if ($row->{file_re}) { diff --git a/lib/Ravada/VM/KVM.pm b/lib/Ravada/VM/KVM.pm index b118b0e66..878f34750 100644 --- a/lib/Ravada/VM/KVM.pm +++ b/lib/Ravada/VM/KVM.pm @@ -1059,8 +1059,8 @@ sub _domain_create_from_iso { if ( $iso_file ne "" || $iso_file ) { $device_cdrom = $iso_file; } - } elsif ($iso->{has_cd}) { - $device_cdrom = $self->search_volume_path_re(qr($iso->{file_re})); + } elsif ($iso->{has_cd} && $iso->{url}) { + $device_cdrom = $self->search_volume_path_re(qr($iso->{file_re})) if $iso->{file_re}; if (!$device_cdrom) { my $req_download = Ravada::Request->download( uid => Ravada::Utils::user_daemon->id @@ -1518,11 +1518,14 @@ sub _download_file_external_headers($self,$url) { } sub _download_file_external($self, $url, $device, $verbose=1, $test=0) { - $url .= "/" if $url !~ m{/$} && $url !~ m{.*/([^/]+\.[^/]+)$}; - my ($filename) = $device =~ m{.*/(.*)}; + if ( $url !~ m{/$} && $url !~ m{.*/([^/]+\.[^/]+)$}) { + $url .= "/"; + } else { + $filename = undef; + } - if ($url =~ m{[^*]}) { + if ($filename && $url =~ m{[^*]}) { my @found = $self->_search_url_file($url, $filename); die "Error: URL not found '$url'" if !scalar @found; $url = $found[-1]; @@ -1582,9 +1585,11 @@ sub _search_iso($self, $id_iso, $file_iso=undef) { return $row if $file_iso && $self->file_exists($file_iso); + my $file_re = $row->{file_re}; + my $url = $row->{url}; Ravada::Front::_fix_iso_file_re($row); - if ( !$row->{device} || ! $self->file_exists($row->{device}) ) { + if (( !$row->{device} || ! $self->file_exists($row->{device})) && $row->{file_re} ) { my $device_cdrom = $self->search_volume_path_re(qr($row->{file_re})); $row->{device} = $device_cdrom if ($device_cdrom); @@ -1593,6 +1598,10 @@ sub _search_iso($self, $id_iso, $file_iso=undef) { ($row->{filename}) = $row->{device} =~ m{.*/(.*)} if $row->{device} && $self->file_exists($row->{device}); + if (!$row->{filename}) { + $row->{file_re} = $file_re; + $row->{url} = $url; + } $self->_fetch_filename($row);# if $row->{file_re}; if ($VERIFY_ISO) { $self->_fetch_md5($row) if !$row->{md5} && $row->{md5_url}; @@ -1761,8 +1770,7 @@ sub _fetch_filename { $sth->execute($row->{device}, $row->{id}); return; } else { - warn Dumper([$row->{url}, $row->{file_re}]); - @found = $self->_search_url_file($row->{url}, $row->{file_re}) if !@found; + @found = $self->_search_url_file($row->{url}, $row->{file_re}); die "No ".qr($row->{file_re})." found on $row->{url}" if !@found; } @@ -1770,6 +1778,7 @@ sub _fetch_filename { my ($file) = $url =~ m{.*/(.*)}; $row->{url} = $url; + $row->{file_re} = undef; $row->{filename} = ($row->{rename_file} or $file); # $row->{url} .= "/" if $row->{url} !~ m{/$}; @@ -1787,7 +1796,8 @@ sub _search_url_file($self, $url_re, $file_re=undef) { $url_re =~ s{(.*)/.*/\.\.$}{$1}; } } else { - $url_re =~ s{(.*)/.*$}{$1}; + # this failed on http://cdimage.ubuntu.com/ubuntu-mate/releases/24.04.*/release + # $url_re =~ s{(.*)/.*$}{$1}; } $file_re .= '$' if $file_re !~ m{\$$}; @@ -3352,8 +3362,11 @@ sub copy_file($self, $orig, $dst, %args) { $xml->findnodes("/volume/target/permissions/mode/text()")->[0] ->setData($mode) if $mode; + my ($dst_path) = $dst =~ m{(.*)/.*?}; + die "Error: I can't find path in 'dst'" if !$dst_path; + my $sp_dst = $self->vm->get_storage_pool_by_target_path($dst_path); my $vol_dst; - eval { $vol_dst = $sp->clone_volume($xml, $vol) }; + eval { $vol_dst = $sp_dst->clone_volume($xml, $vol) }; confess $@ if $@; } diff --git a/t/front/20_create_domain.t b/t/front/20_create_domain.t index 196ede73d..1d49b2d70 100644 --- a/t/front/20_create_domain.t +++ b/t/front/20_create_domain.t @@ -23,11 +23,9 @@ my $RVD_FRONT = Ravada::Front->new( @rvd_args my $USER = create_user('foo','bar', 1); -add_ubuntu_minimal_iso(); - my %CREATE_ARGS = ( Void => { id_iso => search_id_iso('Alpine'), id_owner => $USER->id } - ,KVM => { id_iso => search_id_iso('Ubuntu % Minimal'), id_owner => $USER->id } + ,KVM => { id_iso => search_id_iso('Alpine'), id_owner => $USER->id } ,LXC => { id_template => 1, id_owner => $USER->id } ); diff --git a/t/kvm/a10_pools.t b/t/kvm/a10_pools.t index f1c58adaf..35593b195 100644 --- a/t/kvm/a10_pools.t +++ b/t/kvm/a10_pools.t @@ -234,7 +234,7 @@ sub test_volumes_in_two_pools { test_base($domain); for my $file (@volumes) { - ok(-e $file,"Expecting volume $file exists, got : ".(-e $file or 0)); + ok(-e $file,"Expecting volume $file exists, got : ".(-e $file or 0)) or die $domain->name; } $domain->remove($USER); for my $file (@volumes) { @@ -871,6 +871,7 @@ SKIP: { skip($msg,10) if !$vm; + test_volumes_in_two_pools($vm_name); test_create_pool_fail($vm); diff --git a/t/lib/Test/Ravada.pm b/t/lib/Test/Ravada.pm index b46e13eb1..a868aed90 100644 --- a/t/lib/Test/Ravada.pm +++ b/t/lib/Test/Ravada.pm @@ -2623,6 +2623,7 @@ sub _file_db { unlink $file_db or die("$! $file_db"); } $FILE_DB = $file_db; + warn $file_db; return $file_db; } diff --git a/t/repository/10_iso.t b/t/repository/10_iso.t index e9b63e075..780759382 100644 --- a/t/repository/10_iso.t +++ b/t/repository/10_iso.t @@ -117,7 +117,7 @@ sub test_download_iso($vm, $id_iso, $name) { rvd_back->_process_all_requests_dont_fork(); is($req1->status, 'done'); is($req1->error,'',$iso->{name}); - like($req1->output,qr/^http.*/); + like($req1->output,qr/^http.*/) or exit; } sub test_post_login() { diff --git a/t/request/15_download.t b/t/request/15_download.t index 6e269185a..7acba7cee 100644 --- a/t/request/15_download.t +++ b/t/request/15_download.t @@ -33,7 +33,7 @@ sub test_download($vm, $iso0, $test=0) { rvd_back->_process_all_requests_dont_fork(); is($req1->status, 'done'); - is($req1->error,'',$iso->{name}); + is($req1->error,'',$iso->{name}) or exit; like($req1->output,qr/^http.*/); } @@ -95,6 +95,9 @@ for my $vm_name ('KVM') { # || $iso->{name} =~ /Mate.* 2/i # || $iso->{name} =~ /De.*an.*12/i; #next unless $iso->{name} =~ /Dev.*an.*12/i; + #next unless $iso->{name} =~ /Mate 24.04/; + next unless $iso->{name} =~ /suse/i; + diag($iso->{name}); test_download($vm, $iso,1); } } From 3c7072a2812958d4cdda4596bba02eadefa7ef32 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 16 Jun 2025 11:56:25 +0200 Subject: [PATCH 035/141] wip: download ISO --- lib/Ravada.pm | 12 +++----- lib/Ravada/Front.pm | 28 +++++++++++------ lib/Ravada/VM/KVM.pm | 68 ++++++++++++++++------------------------- lib/Ravada/VM/Void.pm | 1 + public/js/admin.js | 4 +-- t/lib/Test/Ravada.pm | 1 - t/request/15_download.t | 4 +-- 7 files changed, 54 insertions(+), 64 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index bbe2de869..c0f26ee82 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -518,8 +518,7 @@ sub _update_isos { ,arch => 'x86_64' ,xml => 'alpine-amd64.xml' ,xml_volume => 'alpine381_64-volume.xml' - #,url => 'http://dl-cdn.alpinelinux.org/alpine/v3.16/releases/x86_64/' - ,url => 'http://localhost/isos/' + ,url => 'http://dl-cdn.alpinelinux.org/alpine/v3.16/releases/x86_64/' ,file_re => 'alpine-standard-3.16.*-x86_64.iso' ,sha256_url => '$url/alpine-standard-3.16.*.iso.sha256' ,min_disk_size => '2' @@ -532,8 +531,7 @@ sub _update_isos { ,arch => 'i686' ,xml => 'alpine-i386.xml' ,xml_volume => 'alpine381_32-volume.xml' - #,url => 'http://dl-cdn.alpinelinux.org/alpine/v3.16/releases/x86/' - ,url => 'http://localhost/isos/' + ,url => 'http://dl-cdn.alpinelinux.org/alpine/v3.16/releases/x86/' ,options => { machine => 'pc-i440fx' } ,file_re => 'alpine-standard-3.16.*-x86.iso' ,sha256_url => '$url/alpine-standard-3.16.*.iso.sha256' @@ -5423,8 +5421,8 @@ sub _cmd_download { my $test = $request->defined_arg('test'); my $iso = $vm->_search_iso($id_iso); - confess Dumper($iso) if !$iso->{file_re}; - my $device_cdrom = $vm->search_volume_path_re(qr($iso->{file_re})); + my $device_cdrom; + $device_cdrom = $vm->search_volume_path_re(qr($iso->{device_re})) if $iso->{device_re}; if ($device_cdrom) { my $sth = $CONNECTOR->dbh->prepare( @@ -5449,7 +5447,7 @@ sub _cmd_download { sub _download_local_and_rsync($self, $request, $vm, $iso) { my $vm_local = $vm->new(host => 'localhost'); - my $found = $vm_local->search_volume_path_re(qr($iso->{file_re})); + my $found = $vm_local->search_volume_path_re(qr($iso->{device_re})); if (!$found) { my $req_local = Ravada::Request->download( diff --git a/lib/Ravada/Front.pm b/lib/Ravada/Front.pm index fcfe01c69..cfc535113 100644 --- a/lib/Ravada/Front.pm +++ b/lib/Ravada/Front.pm @@ -12,7 +12,7 @@ Ravada::Front - Web Frontend library for Ravada use Carp qw(carp); use DateTime; use DateTime::Format::DateParse; -use Hash::Util qw(lock_hash lock_keys); +use Hash::Util qw(lock_hash lock_keys unlock_hash); use IPC::Run3 qw(run3); use JSON::XS; use Moose; @@ -779,8 +779,8 @@ sub list_iso_images { if $row->{options}; $row->{min_ram} = 0.2 if !$row->{min_ram}; + _get_device_re($row); lock_keys(%$row); - _fix_iso_file_re($row); push @iso,($row); } @@ -788,21 +788,29 @@ sub list_iso_images { return \@iso; } -sub _fix_iso_file_re($row) { +sub _get_device_re($row) { + + return if !$row->{url}; + if ($row->{rename_file}) { - $row->{file_re} = $row->{rename_file}; + $row->{device_re} = $row->{rename_file}; + } elsif ($row->{file_re}) { + $row->{device_re} = $row->{file_re}; } elsif ($row->{url} ) { my ($url,$file_re) = $row->{url} =~ m{(.*)/([^/]+)$}; if ($file_re && $url) { - $row->{file_re}= $file_re; - $row->{url}= $url; + $row->{device_re}= $file_re; } } - if ($row->{file_re}) { - $row->{file_re} = '^'.$row->{file_re} unless $row->{file_re} =~ /\^/; - $row->{file_re} .= '$' unless $row->{file_re} =~ /\$/; - } + confess "Error: no device found in $row->{name} from " + .($row->{file_re} or '') + ." or ".($row->{rename_file} or '') + ." or ".$row->{url} + if !$row->{device_re}; + + $row->{device_re} = '^'.$row->{device_re} unless $row->{device_re} =~ /\^/; + $row->{device_re} .= '$' unless $row->{device_re} =~ /\$/; } diff --git a/lib/Ravada/VM/KVM.pm b/lib/Ravada/VM/KVM.pm index 878f34750..960429f10 100644 --- a/lib/Ravada/VM/KVM.pm +++ b/lib/Ravada/VM/KVM.pm @@ -667,6 +667,7 @@ Returns the directory where disk images are stored in this Virtual Manager sub dir_img { my $self = shift; + return '/var/tmp' if $<; my $pool = $self->_load_storage_pool(); $pool = $self->_create_default_pool() if !$pool; my $xml = XML::LibXML->load_xml(string => $pool->get_xml_description()); @@ -1384,20 +1385,21 @@ sub _iso_name($self, $iso, $req=undef, $verbose=1) { $test = 1 if $req && $req->defined_arg('test'); if ($test || !$device || ! $self->file_exists($device) ) { - $req->status("downloading ".$iso->{file_re}." file" - ,"Downloading ISO file for ".$iso->{file_re} + $req->status("downloading ".($iso->{file_re} or '')." file" + ,"Downloading ISO file for ".($iso->{file_re} or '') ." from $iso->{url}. It may take several minutes" ) if $req; _fill_url($iso); $self->_set_iso_downloading($iso,1); if ( !$device ) { - $self->_fetch_filename($iso); + $self->_fetch_filename($iso, $test); $device = $self->dir_img()."/".$iso->{filename}; my $sth = $self->_dbh->prepare("UPDATE iso_images set device=?" ." WHERE id=?" ); $sth->execute($device, $iso->{id}); + $iso->{url} .= "/".$iso->{filename}; } my $url = $self->_download_file_external($iso->{url}, $device, $verbose, $test); $self->_set_iso_downloading($iso,0); @@ -1452,7 +1454,9 @@ sub _fill_url($iso) { $iso->{filename} = ''; return; } - confess "Error: Missing field file_re for ".$iso->{name}; + confess "Error: Missing field file_re for ".$iso->{name} if !$iso->{filename}; + $iso->{url} .= "/" if $iso->{url} !~ m{/$}; + $iso->{url} .= $iso->{filename}; } sub _check_md5 { @@ -1518,21 +1522,13 @@ sub _download_file_external_headers($self,$url) { } sub _download_file_external($self, $url, $device, $verbose=1, $test=0) { - my ($filename) = $device =~ m{.*/(.*)}; - if ( $url !~ m{/$} && $url !~ m{.*/([^/]+\.[^/]+)$}) { - $url .= "/"; - } else { - $filename = undef; - } - if ($filename && $url =~ m{[^*]}) { - my @found = $self->_search_url_file($url, $filename); - die "Error: URL not found '$url'" if !scalar @found; + if ($url =~ m{[^*]}) { + my ($url2, $filename) = $url =~ m{(.*)/(.*)}; + my @found = $self->_search_url_file($url2, $filename); + confess "Error: URL not found '$url2' / $filename" if !scalar @found; $url = $found[-1]; } - if ( $url =~ m{/$} ) { - $url = "$url$filename"; - } $url =~ s{/./}{/}g; if ( $test ) { @@ -1585,12 +1581,10 @@ sub _search_iso($self, $id_iso, $file_iso=undef) { return $row if $file_iso && $self->file_exists($file_iso); - my $file_re = $row->{file_re}; - my $url = $row->{url}; - Ravada::Front::_fix_iso_file_re($row); + Ravada::Front::_get_device_re($row); - if (( !$row->{device} || ! $self->file_exists($row->{device})) && $row->{file_re} ) { - my $device_cdrom = $self->search_volume_path_re(qr($row->{file_re})); + if ( !$row->{device} || ! $self->file_exists($row->{device}) ) { + my $device_cdrom = $self->search_volume_path_re(qr($row->{device_re})); $row->{device} = $device_cdrom if ($device_cdrom); } @@ -1598,11 +1592,7 @@ sub _search_iso($self, $id_iso, $file_iso=undef) { ($row->{filename}) = $row->{device} =~ m{.*/(.*)} if $row->{device} && $self->file_exists($row->{device}); - if (!$row->{filename}) { - $row->{file_re} = $file_re; - $row->{url} = $url; - } - $self->_fetch_filename($row);# if $row->{file_re}; + $self->_fetch_filename($row); if ($VERIFY_ISO) { $self->_fetch_md5($row) if !$row->{md5} && $row->{md5_url}; $self->_fetch_sha256($row) if !$row->{sha256} && $row->{sha256_url}; @@ -1611,10 +1601,6 @@ sub _search_iso($self, $id_iso, $file_iso=undef) { if ( !$row->{device} && $row->{filename}) { if (my $volume = $self->search_volume_re(qr("^".$row->{filename}))) { $row->{device} = $volume->get_path; - my $sth = $$CONNECTOR->dbh->prepare( - "UPDATE iso_images SET device=? WHERE id=?" - ); - $sth->execute($volume->get_path, $row->{id}); } } @@ -1731,9 +1717,7 @@ sub _cache_filename($url) { return "$dir/$file"; } -sub _fetch_filename { - my $self = shift; - my $row = shift; +sub _fetch_filename($self, $row, $test=undef) { if (!$row->{file_re} && !$row->{url} && $row->{device}) { my ($file) = $row->{device} =~ m{.*/(.*)}; @@ -1741,16 +1725,18 @@ sub _fetch_filename { return; } return if !$row->{file_re} && !$row->{url} && !$row->{device}; - if (!$row->{file_re}) { + if (!$row->{file_re} && !$row->{filename}) { my ($new_url, $file); ($new_url, $file) = $row->{url} =~ m{(.*)/(.*)} if $row->{url}; ($file) = $row->{device} =~ m{.*/(.*)} if !$file && $row->{device}; + confess "No filename in $row->{name} $row->{url}" if !$file; $row->{url} = $new_url; $row->{file_re} = "^$file"; } + $row->{file_re} = $row->{filename} if !$row->{file_re} && $row->{filename}; confess "No file_re" if !$row->{file_re}; $row->{file_re} .= '$' if $row->{file_re} !~ m{\$$}; @@ -1768,18 +1754,18 @@ sub _fetch_filename { ." WHERE id=?" ); $sth->execute($row->{device}, $row->{id}); - return; - } else { + return if !$test; + } + @found = $self->_search_url_file($row->{url}, $row->{file_re}); die "No ".qr($row->{file_re})." found on $row->{url}" if !@found; - } my $url = $found[-1]; - my ($file) = $url =~ m{.*/(.*)}; + my ($url_path,$file) = $url =~ m{(.*)/(.*)}; - $row->{url} = $url; $row->{file_re} = undef; - $row->{filename} = ($row->{rename_file} or $file); + $row->{url} = $url_path; + $row->{filename} = $file; # $row->{url} .= "/" if $row->{url} !~ m{/$}; # $row->{url} .= $file; @@ -1797,7 +1783,7 @@ sub _search_url_file($self, $url_re, $file_re=undef) { } } else { # this failed on http://cdimage.ubuntu.com/ubuntu-mate/releases/24.04.*/release - # $url_re =~ s{(.*)/.*$}{$1}; + $url_re =~ s{(.*)/.*\..*$}{$1} if $url_re =~ /\.iso/; } $file_re .= '$' if $file_re !~ m{\$$}; diff --git a/lib/Ravada/VM/Void.pm b/lib/Ravada/VM/Void.pm index ed4ac2184..bef6a6988 100644 --- a/lib/Ravada/VM/Void.pm +++ b/lib/Ravada/VM/Void.pm @@ -747,6 +747,7 @@ sub _search_iso($self, $id, $device = undef) { $sth->execute($id); my $row = $sth->fetchrow_hashref; $row->{device} = $device if defined $device; + Ravada::Front::_get_device_re($row); return $row; } diff --git a/public/js/admin.js b/public/js/admin.js index eb9fdfbdd..41214ef6a 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -260,8 +260,8 @@ ravadaApp.directive("solShowMachine", swMach) if ( $scope.swap.value < iso.min_swap_size ) { $scope.swap.value = iso.min_swap_size + 0.1; } - if (iso.file_re ) { - file_re = new RegExp(iso.file_re); + if (iso.device_re ) { + file_re = new RegExp(iso.device_re); } else { return ''; } diff --git a/t/lib/Test/Ravada.pm b/t/lib/Test/Ravada.pm index a868aed90..b46e13eb1 100644 --- a/t/lib/Test/Ravada.pm +++ b/t/lib/Test/Ravada.pm @@ -2623,7 +2623,6 @@ sub _file_db { unlink $file_db or die("$! $file_db"); } $FILE_DB = $file_db; - warn $file_db; return $file_db; } diff --git a/t/request/15_download.t b/t/request/15_download.t index 7acba7cee..29ab8777d 100644 --- a/t/request/15_download.t +++ b/t/request/15_download.t @@ -94,9 +94,7 @@ for my $vm_name ('KVM') { # || $iso->{name} =~ /Mint.*22/i # || $iso->{name} =~ /Mate.* 2/i # || $iso->{name} =~ /De.*an.*12/i; - #next unless $iso->{name} =~ /Dev.*an.*12/i; - #next unless $iso->{name} =~ /Mate 24.04/; - next unless $iso->{name} =~ /suse/i; + # next unless $iso->{name} =~ /Kubuntu.*20/i; diag($iso->{name}); test_download($vm, $iso,1); } From 9b2b8f811b5ddb1ab3a50b9315af3ba62a30cd76 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Tue, 17 Jun 2025 08:54:19 +0200 Subject: [PATCH 036/141] wip: reload md5sum --- lib/Ravada/Domain.pm | 1 + lib/Ravada/VM/KVM.pm | 9 +++++---- t/request/15_download.t | 1 - 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 2563cd516..f88b4228f 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -1208,6 +1208,7 @@ sub _check_tmp_volumes($self) { for my $vol ( $self->list_volumes_info) { next unless $vol->file && $vol->file =~ /\.(TMP|SWAP)\./; + next unless $vol->backing_file; $vol->delete() ; my $base = Ravada::Domain->open($self->id_base); my @volumes = $base->list_files_base_target; diff --git a/lib/Ravada/VM/KVM.pm b/lib/Ravada/VM/KVM.pm index 960429f10..843a16c77 100644 --- a/lib/Ravada/VM/KVM.pm +++ b/lib/Ravada/VM/KVM.pm @@ -1583,6 +1583,7 @@ sub _search_iso($self, $id_iso, $file_iso=undef) { Ravada::Front::_get_device_re($row); + return $row if !$row->{device_re}; if ( !$row->{device} || ! $self->file_exists($row->{device}) ) { my $device_cdrom = $self->search_volume_path_re(qr($row->{device_re})); $row->{device} = $device_cdrom @@ -1611,7 +1612,7 @@ sub _download($self, $url) { $url =~ s{(http://.*)//(.*)}{$1/$2}; if ($url =~ m{[^*]}) { my @found = $self->_search_url_file($url); - die "Error: URL not found '$url'" if !scalar @found; + confess "Error: URL not found '$url'" if !scalar @found; $url = $found[-1]; } @@ -1827,15 +1828,15 @@ sub _fetch_this($self, $row, $type, $file = $row->{filename}){ $file=~ s{.*/(.+)}{$1} if $file =~ m{/} && $file !~ m{/$}; - my ($url, $file2) = $row->{url} =~ m{(.*)/(.+)}; - $url = $row->{url} if $row->{url} =~ m{/$}; + my ($url, $file2) = $row->{url} =~ m{(.*)/(.+)} if $row->{url} =~ m{\.iso.?$}; + $url = $row->{url} if !$url; my $url_orig = $row->{"${type}_url"}; $file = $file2 if $file2 && $file2 !~ /\*|\^/ && $file2 !~ m{/$}; $url_orig =~ s{(.*)\$url(.*)}{$1$url$2} if $url_orig =~ /\$url/; confess "error: file missing '$file' ".Dumper($row) if $file =~ m{/$}; - confess "error " if $url_orig =~ /\$/; + confess "error $url_orig" if $url_orig =~ /\$/; my $content = $self->_download($url_orig); diff --git a/t/request/15_download.t b/t/request/15_download.t index 29ab8777d..9c5bf0f78 100644 --- a/t/request/15_download.t +++ b/t/request/15_download.t @@ -94,7 +94,6 @@ for my $vm_name ('KVM') { # || $iso->{name} =~ /Mint.*22/i # || $iso->{name} =~ /Mate.* 2/i # || $iso->{name} =~ /De.*an.*12/i; - # next unless $iso->{name} =~ /Kubuntu.*20/i; diag($iso->{name}); test_download($vm, $iso,1); } From 946da27fc22ed14f29d4e7b3bb8ffba3789cee3d Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Tue, 17 Jun 2025 11:47:28 +0200 Subject: [PATCH 037/141] wip: sync ISO to default image path --- lib/Ravada.pm | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index c0f26ee82..b35f420f6 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -5462,21 +5462,19 @@ sub _download_local_and_rsync($self, $request, $vm, $iso) { die $msg; } - my ($path) = $found =~ m{(.*/)}; + my ($path, $file) = $found =~ m{(.*/)(.*)}; if ( $vm_local->shared_storage($vm, $path) ) { die "Warning: shared storage, $iso should be there"; } my $rsync = File::Rsync->new(update => 1, sparse => 1, archive => 1); - my $dst = 'root@'.$vm->host.":".$found; - warn "$found -> $dst"; + my $dst = 'root@'.$vm->host.":".$vm->dir_img()."/".$file; $rsync->exec(src => $found, dest => $dst); confess "error syncing from $found to $dst \n" .join(' ',@{$rsync->err}) if $rsync->err; - warn "rsync done"; } sub _cmd_add_hardware { From 62dbd33bcb5fbb8cde59e407d81f8400812ad602 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Tue, 17 Jun 2025 11:53:50 +0200 Subject: [PATCH 038/141] refactor: Variable declared in conditional statement --- lib/Ravada/VM/KVM.pm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/Ravada/VM/KVM.pm b/lib/Ravada/VM/KVM.pm index 843a16c77..ce0f4b71d 100644 --- a/lib/Ravada/VM/KVM.pm +++ b/lib/Ravada/VM/KVM.pm @@ -1828,7 +1828,8 @@ sub _fetch_this($self, $row, $type, $file = $row->{filename}){ $file=~ s{.*/(.+)}{$1} if $file =~ m{/} && $file !~ m{/$}; - my ($url, $file2) = $row->{url} =~ m{(.*)/(.+)} if $row->{url} =~ m{\.iso.?$}; + my ($url, $file2); + ($url, $file2) = $row->{url} =~ m{(.*)/(.+)} if $row->{url} =~ m{\.iso.?$}; $url = $row->{url} if !$url; my $url_orig = $row->{"${type}_url"}; $file = $file2 if $file2 && $file2 !~ /\*|\^/ && $file2 !~ m{/$}; From ed0b5e16353e24ac0dfb37dac00b3c532ae460fd Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 18 Jun 2025 11:36:48 +0200 Subject: [PATCH 039/141] wip(test): properly remove test storage pools --- t/lib/Test/Ravada.pm | 17 +++++++++++++++++ t/storage_move.t | 6 +++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/t/lib/Test/Ravada.pm b/t/lib/Test/Ravada.pm index b46e13eb1..f3b30e1dc 100644 --- a/t/lib/Test/Ravada.pm +++ b/t/lib/Test/Ravada.pm @@ -1717,6 +1717,16 @@ sub remove_qemu_networks($vm=undef) { } +sub _remove_dir($dir) { + die $dir if $dir !~ m{/tst_}; + opendir (my $ls,$dir) or die "$! $dir"; + while (my $file = readdir $ls) { + next if $file =~ /^\./; + unlink "$dir/$file" or die "$dir/$file"; + } + closedir $ls; +} + sub remove_qemu_pools($vm=undef) { return if !$vm && (!$VM_VALID{'KVM'} || $>); return if defined $vm && $vm->type eq 'Void'; @@ -1743,6 +1753,13 @@ sub remove_qemu_pools($vm=undef) { diag("Removing ".$vm->name." storage_pool ".$pool->get_name); for my $vol ( $pool->list_volumes ) { diag("Removing ".$pool->get_name." vol ".$vol->get_name); + my $xml = XML::LibXML->load_xml(string => $vol->get_xml_description()); + my ($format_h) = $xml->findnodes("/volume/target/format"); + my $format = $format_h->getAttribute('type'); + if ( $format eq 'dir') { + my $dir = $xml->findnodes('/volume/target/path/text()'); + _remove_dir($dir); + } $vol->delete(); } } diff --git a/t/storage_move.t b/t/storage_move.t index 42bb3609c..907f464b5 100644 --- a/t/storage_move.t +++ b/t/storage_move.t @@ -81,7 +81,6 @@ sub test_do_not_overwrite($vm) { } sub _search_free_space($dir) { - diag($dir); open my $mounts,"<","/proc/mounts" or die $!; my $found; while (my $line = <$mounts>) { @@ -181,7 +180,7 @@ sub test_queue_move($vm) { is($req2->after_request, $req1->id); remove_domain($domain); - + $vm->remove_storage_pool($sp); } sub test_queue_change_hw($vm) { @@ -219,6 +218,7 @@ sub test_queue_change_hw($vm) { $domain->remove(user_admin); + $vm->remove_storage_pool($sp); rmdir($dir) or die "$! $dir"; } @@ -240,7 +240,6 @@ sub test_move_volume($vm) { my ($filename)= $vol =~ m{.*/(.*)}; if ( -e "$dir/$filename" ) { - diag("removing previously copied $dir/$filename"); unlink("$dir/$filename") or die "$! $dir/$filename"; $vm->refresh_storage(); } @@ -280,6 +279,7 @@ sub test_move_volume($vm) { for my $vol (@volumes) { ok(!-e $vol); } + $vm->remove_storage_pool($sp); rmdir($dir) or die "$! $dir"; } From b292b2fc22acc8a3fcf1bc94590207437da1bf9a Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 18 Jun 2025 12:07:09 +0200 Subject: [PATCH 040/141] wip: refresh pools twice just in case --- lib/Ravada/VM/KVM.pm | 1 - t/request/50_refresh_storage.t | 17 ++++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/lib/Ravada/VM/KVM.pm b/lib/Ravada/VM/KVM.pm index ce0f4b71d..c7ba99a48 100644 --- a/lib/Ravada/VM/KVM.pm +++ b/lib/Ravada/VM/KVM.pm @@ -370,7 +370,6 @@ sub search_volume_re($self,$pattern,$refresh=0) { confess $@ if $@ && $@ !~ /libvirt error code: 50,/; next if !$file || $file !~ $pattern; - return $vol if !wantarray; push @volume,($vol); } diff --git a/t/request/50_refresh_storage.t b/t/request/50_refresh_storage.t index fb03a8442..4f597d9a8 100644 --- a/t/request/50_refresh_storage.t +++ b/t/request/50_refresh_storage.t @@ -32,7 +32,7 @@ sub test_request { unlink $old_file or die "$! $old_file" if -e $old_file; } - $vm->_refresh_storage_pools(); + $vm->refresh_storage_pools(); # check there are no files @files = $vm->search_volume_path($file); @@ -50,19 +50,22 @@ sub test_request { my $request; eval { - my @args; - @args = (id_vm => $id_vm ) if $id_vm; + my @args = ( uid => user_admin->id ); + push @args, (id_vm => $id_vm ) if $id_vm; $request = Ravada::Request->refresh_storage(@args); }; is($@,''); ok($request,"Expecting a request") or next; - rvd_back->_process_all_requests_dont_fork(); + wait_request(debug => 0); is($request->status,'done'); is($request->error,''); + $request->status('requested'); + wait_request(debug => 0); + @files = $vm->search_volume_path($file); - ok(scalar @files,"Expecting $file exists on storage pool"); + ok(scalar @files,"Expecting $file exists on storage pool ".$vm->dir_img); } ######################################################### @@ -79,10 +82,6 @@ for my $vm_name ( vm_names() ) { $msg = "SKIPPED: Test must run as root"; $vm= undef; } - if ($vm_name eq 'Void') { - $msg = "SKIPPED: Refresh storage missing in Void"; - $vm= undef; - } }; From 251f41113d030a41798224d6581622ff7d719307 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 19 Jun 2025 11:44:25 +0200 Subject: [PATCH 041/141] wip: test fail download on create --- lib/Ravada.pm | 2 +- lib/Ravada/Request.pm | 2 +- lib/Ravada/VM.pm | 8 +++++++- lib/Ravada/VM/KVM.pm | 2 +- t/request/15_download.t | 32 ++++++++++++++++++++++++++++++++ 5 files changed, 42 insertions(+), 4 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index b35f420f6..96637a7d3 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -5455,7 +5455,7 @@ sub _download_local_and_rsync($self, $request, $vm, $iso) { ,id_iso => $iso->{id} ,test => $request->defined_arg('test') ); - $request->after_request($req_local->id); + $request->after_request_ok($req_local->id); $request->retry(2); my $msg = "ISO pending to rsync. retry.\n"; warn $msg; diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index 0b512456a..f713878e1 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -1077,7 +1077,7 @@ sub _check_downloading($self) { if (! $req_download) { _mark_iso_downloaded($id_iso2); } else { - $self->after_request($req_download->id); + $self->after_request_ok($req_download->id); } $sth = $$CONNECTOR->dbh->prepare("SELECT args FROM requests" ." WHERE id=?" diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index 3f89d9dda..aae1b4d77 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -1943,7 +1943,13 @@ sub run_command($self, @command) { my $ssh = $self->_ssh or confess "Error: Error connecting to ".$self->host; - my ($out, $err) = $ssh->capture2({timeout => 10},join " ",@command); + my $timeout = 10; + + $timeout = 60*60 if $command[0] =~ /cp|qemu/; + my $t0 = time; + my ($out, $err) = $ssh->capture2({timeout => $timeout},join " ",@command); + warn ''.(time -$t0)." @command" if $command[0] =~ /cp|qemu/i && $command[1] ne 'info'; + chomp $err if $err; $err = '' if !defined $err; diff --git a/lib/Ravada/VM/KVM.pm b/lib/Ravada/VM/KVM.pm index c7ba99a48..62718250b 100644 --- a/lib/Ravada/VM/KVM.pm +++ b/lib/Ravada/VM/KVM.pm @@ -1758,7 +1758,7 @@ sub _fetch_filename($self, $row, $test=undef) { } @found = $self->_search_url_file($row->{url}, $row->{file_re}); - die "No ".qr($row->{file_re})." found on $row->{url}" if !@found; + die "No ".qr($row->{file_re})." found on $row->{url}\n" if !@found; my $url = $found[-1]; my ($url_path,$file) = $url =~ m{(.*)/(.*)}; diff --git a/t/request/15_download.t b/t/request/15_download.t index 9c5bf0f78..2f546c9ad 100644 --- a/t/request/15_download.t +++ b/t/request/15_download.t @@ -68,6 +68,34 @@ sub test_debians() { ok($found,"Expecting some debian entries found"); } +sub test_fail_download($vm) { + my $id_iso = search_id_iso('Alpine'); + my $sth = connector->dbh->prepare( + "SELECT url,file_re FROM iso_images WHERE id=?" + ); + $sth->execute($id_iso); + my ($url, $file_re) = $sth->fetchrow; + + $sth = connector->dbh->prepare( + "UPDATE iso_images SET url=?,file_re=? WHERE id=?" + ); + $sth->execute('http://localhost/fail/','alpine.iso', $id_iso); + + my $req = Ravada::Request->create_domain( + id_owner => user_admin->id() + ,id_vm => $vm->id + ,id_iso => $id_iso + ,name => new_domain_name() + ,disk => 1024 * 1024 + ); + wait_request( debug => 1, check_error => 0); + diag($req->error); + + $sth->execute($url,$file_re, $id_iso); + + like($req->error,qr/No.* found on http.*/); +} + ################################################################## SKIP: { @@ -84,6 +112,9 @@ for my $vm_name ('KVM') { } skip($msg,10) if !$vm; + diag($vm_name); + + test_fail_download($vm); test_debians(); ################################################ @@ -94,6 +125,7 @@ for my $vm_name ('KVM') { # || $iso->{name} =~ /Mint.*22/i # || $iso->{name} =~ /Mate.* 2/i # || $iso->{name} =~ /De.*an.*12/i; + next unless $iso->{name} =~ /^Ubuntu 20.04/; diag($iso->{name}); test_download($vm, $iso,1); } From 87ae181de20cda7c6cd219c42dcf1b716b1de5f4 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Fri, 20 Jun 2025 08:48:27 +0200 Subject: [PATCH 042/141] wip: warn when removing missing file --- lib/Ravada/VM/KVM.pm | 2 +- t/mojo/80_check_resources.t | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Ravada/VM/KVM.pm b/lib/Ravada/VM/KVM.pm index 62718250b..f97ea2385 100644 --- a/lib/Ravada/VM/KVM.pm +++ b/lib/Ravada/VM/KVM.pm @@ -392,7 +392,7 @@ sub remove_file($self,@files) { $vol = $self->search_volume($file); } if (!$vol) { - confess "Warning: '$file' not found\n"; + warn "Warning: '$file' not found\n"; } $vol->delete if $vol; } diff --git a/t/mojo/80_check_resources.t b/t/mojo/80_check_resources.t index f924e17a7..5b9ccf282 100644 --- a/t/mojo/80_check_resources.t +++ b/t/mojo/80_check_resources.t @@ -24,7 +24,7 @@ my $URL_LOGOUT = '/logout'; $Test::Ravada::BACKGROUND=1; my $t; -my $BASE_NAME="zz-test-base-ubuntu"; +my $BASE_NAME="zz-test-base-alpine"; my $BASE; ######################################################### From ae8e7d97cdeca86e6d74a2fc8aa834471977242c Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 9 Mar 2026 17:51:39 +0100 Subject: [PATCH 043/141] wip: fixes downloads when urls have dots --- lib/Ravada/VM/KVM.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Ravada/VM/KVM.pm b/lib/Ravada/VM/KVM.pm index efafa01c0..bc2b9221c 100644 --- a/lib/Ravada/VM/KVM.pm +++ b/lib/Ravada/VM/KVM.pm @@ -1394,7 +1394,7 @@ sub _iso_name($self, $iso, $req=undef, $verbose=1) { } sub _fill_url($iso) { - return if $iso->{url} =~ m{.*/[^/]+\.[^/]+$}; + return if $iso->{url} =~ m{.*/[^/]+\.iso$}; if ($iso->{file_re}) { $iso->{url} .= "/" if $iso->{url} !~ m{/$}; $iso->{url} .= $iso->{file_re}; @@ -1720,7 +1720,7 @@ sub _search_url_file($self, $url_re, $file_re=undef) { } } else { # this failed on http://cdimage.ubuntu.com/ubuntu-mate/releases/24.04.*/release - $url_re =~ s{(.*)/.*\..*$}{$1} if $url_re =~ /\.iso/; + $url_re =~ s{(.*)/.*\..*$}{$1} if $url_re =~ /\.iso$/; } $file_re .= '$' if $file_re !~ m{\$$}; From f685c59c83c6415b9e37c2d1f5720ebafc7de4bb Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Tue, 10 Mar 2026 13:37:30 +0100 Subject: [PATCH 044/141] wip: fixed file octal permissions --- lib/Ravada/Domain.pm | 10 ++++++---- lib/Ravada/Volume/Class.pm | 14 +++++++++++++- lib/Ravada/Volume/QCOW2.pm | 22 ++++++++++------------ lib/Ravada/Volume/Void.pm | 11 +++++++++++ t/kvm/71_description_clones.t | 7 ++++++- 5 files changed, 46 insertions(+), 18 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index a87ed1e25..a9702cc88 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -3165,15 +3165,18 @@ sub _do_remove_base($self, $user) { my $backing_file = $vol->backing_file; next if !$backing_file; # confess "Error: no backing file for ".$vol->file if !$backing_file; + my $file = $vol->file; + $vol->block_commit(); + $vol->delete(); + my $vol_backing = Ravada::Volume->new( file => $backing_file ,domain => $self ); - $vol_backing->_copy($vol->file, 0600); - $vol_backing->remove(); + $vol_backing->copy($file, 0o600); + $vol_backing->delete(); } - for my $file ($self->list_files_base) { next if $file =~ /\.iso$/i; next if ! $self->_vm->file_exists($file); @@ -3202,7 +3205,6 @@ sub _post_remove_base { $self->_remove_base_db(@_); $self->_post_remove_base_domain(); $self->_vm->refresh_storage(); - } sub _post_spinoff($self) { diff --git a/lib/Ravada/Volume/Class.pm b/lib/Ravada/Volume/Class.pm index 3fd77f6ab..b5ee68c30 100644 --- a/lib/Ravada/Volume/Class.pm +++ b/lib/Ravada/Volume/Class.pm @@ -35,7 +35,7 @@ sub _post_prepare_base($self, $base_file) { return $base_file if ! $self->clone_base_after_prepare; return $base_file if !$self->vm->file_exists($base_file); - $self->_chmod(oct(400),$base_file); + $self->_chmod(0o400,$base_file); $self->vm->refresh_storage_pools(); $self->vm->remove_file($self->file); @@ -135,4 +135,16 @@ sub backup($self) { return $vol_backup; } +sub _copy_sys($self, $dst, $mode=undef) { + my $file = $self->file; + if ($self->vm) { + my ($out, $err) = $self->vm->run_command("cp",$file,$dst); + die $err if $err; + } else { + copy($file,$dst); + } + $self->_chmod($mode, $dst) if $mode; +} + + 1; diff --git a/lib/Ravada/Volume/QCOW2.pm b/lib/Ravada/Volume/QCOW2.pm index f814d63cc..21714811f 100644 --- a/lib/Ravada/Volume/QCOW2.pm +++ b/lib/Ravada/Volume/QCOW2.pm @@ -71,6 +71,10 @@ sub _convert($self, $dst, $req=undef) { } +sub copy($self, @args) { + return $self->_copy(@args); +} + sub _copy($self, $dst, $mode=undef) { my $src = $self->file; @@ -104,7 +108,7 @@ sub _copy($self, $dst, $mode=undef) { $doc->findnodes('/volume/key/text()')->[0]->setData($dst); $doc->findnodes('/volume/target/path/text()')->[0]->setData( $dst); $doc->findnodes('/volume/target/permissions/mode/text()')->[0] - ->setData( $mode ) if $mode; + ->setData( sprintf("%o",$mode) ) if $mode; my $vol_dst; my $err; @@ -125,17 +129,6 @@ sub _copy($self, $dst, $mode=undef) { return $vol_dst; } -sub _copy_sys($self, $dst, $mode=undef) { - my $file = $self->file; - if ($self->vm) { - my ($out, $err) = $self->vm->run_command("cp",$file,$dst); - die $err if $err; - } else { - copy($file,$dst); - } - $self->_chmod($mode, $dst) if $mode; -} - sub clone($self, $file_clone) { confess if $self->file =~ /ISO$/i; confess if $file_clone =~ /ISO$/i; @@ -321,4 +314,9 @@ sub compact($self, $keep_backup=1) { return int(100*($du_backup-$du)/$du_backup)." % compacted. "; } +sub delete($self) { + my $vol = $self->vm->search_volume($self->file); + $vol->delete(); +} + 1; diff --git a/lib/Ravada/Volume/Void.pm b/lib/Ravada/Volume/Void.pm index 6081d05df..10f24c33b 100644 --- a/lib/Ravada/Volume/Void.pm +++ b/lib/Ravada/Volume/Void.pm @@ -100,6 +100,12 @@ sub block_commit($self) { next if $key =~ /^(origin|capacity|is_base|backing_file)$/; $data_bf->{$key} = $data->{$key}; } + my $vol_backing = Ravada::Volume->new( + file => $self->backing_file + ,domain => $self->domain + ); + $vol_backing->_chmod(0o700); + $self->_save($data_bf, $self->backing_file); } @@ -109,4 +115,9 @@ sub compact($self, $keep_backup) { return $self->info->{target}." 100% compacted. "; } + +sub copy($self,@args) { + return $self->_copy_sys(@args); +} + 1; diff --git a/t/kvm/71_description_clones.t b/t/kvm/71_description_clones.t index ed25051fa..d25cc2597 100644 --- a/t/kvm/71_description_clones.t +++ b/t/kvm/71_description_clones.t @@ -112,10 +112,14 @@ sub test_prepare_base { eval { $domain->remove_base( user_admin); + }; + is(''.$@,'') or exit; + + eval { $domain->prepare_base( user_admin ); $@ = ''; }; - is(''.$@,''); + is(''.$@,'') or exit; ok($domain->is_base); my $name_clone = new_domain_name(); @@ -200,6 +204,7 @@ sub test_description { my $description = "This is a description test"; my $domain2 = rvd_back->search_domain($domain->name); ok ($domain2->description eq $description, "I can't find description"); + remove_domain($domain); } sub test_remove_base { From 7d4be57956d14871f3de8dd135db63cd728f6855 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Tue, 10 Mar 2026 13:49:07 +0100 Subject: [PATCH 045/141] wip: we test downloads elsewhere --- t/vm/60_new_args.t | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/t/vm/60_new_args.t b/t/vm/60_new_args.t index ff969189f..a6ff00749 100644 --- a/t/vm/60_new_args.t +++ b/t/vm/60_new_args.t @@ -263,33 +263,6 @@ sub test_small($vm_name, $vm) { } -sub test_download { - - my $vm_name = shift; - - my $id_iso = search_id_iso('Alpine'); - my $req = Ravada::Request->download( - vm => $vm_name - ,id_iso => $id_iso - ,uid => user_admin->id - ,test => 1 - ); - - wait_request(debug => 0); - - - my $sth = connector->dbh->prepare( - "SELECT device FROM iso_images " - ." WHERE id=?" - ); - $sth->execute($id_iso); - - my ($device) = $sth->fetchrow; - - ok($device) or exit; - -} - ####################################################################### clean(); @@ -321,7 +294,6 @@ for my $vm_name ( vm_names() ) { use_ok("Ravada::VM::$vm_name"); - test_download($vm_name); test_args($vm_name); test_small($vm_name, $vm); }; From 9732b529cc40af94a33012020a30689ce4dfd653 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 11 Mar 2026 12:18:17 +0100 Subject: [PATCH 046/141] wip: fixed volumes modes --- lib/Ravada/Domain.pm | 16 ++++++++++++---- lib/Ravada/Volume/Class.pm | 11 +++++++++-- lib/Ravada/Volume/QCOW2.pm | 8 ++++++-- t/20_volumes.t | 11 +++++------ t/mojo/15_list_bases.t | 1 + t/request/30_hardware_clones.t | 3 ++- t/vm/20_base.t | 22 +++++++++++++++++----- t/vm/r20_rebase.t | 2 +- 8 files changed, 53 insertions(+), 21 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index a9702cc88..ab0b56240 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -1314,7 +1314,7 @@ sub _check_tmp_volumes($self) { , is_base => 1 , vm => $vm ); - $vol_base->clone(file => $vol->file); + $vol_base->clone(file => $vol->file, domain => $self); } } @@ -3166,13 +3166,16 @@ sub _do_remove_base($self, $user) { next if !$backing_file; # confess "Error: no backing file for ".$vol->file if !$backing_file; my $file = $vol->file; - $vol->block_commit(); - $vol->delete(); - my $vol_backing = Ravada::Volume->new( file => $backing_file ,domain => $self ); + + $vol_backing->_chmod(0o700); + $vol->block_commit(); + $vol->delete(); + + $vol_backing->copy($file, 0o600); $vol_backing->delete(); @@ -3428,6 +3431,11 @@ sub _copy_volumes($self, $copy) { my $dst = $copy_volumes{$target}; $self->_vm->remove_file($dst) if $self->_vm->file_exists($dst); $self->_vm->copy_file($volumes{$target}, $copy_volumes{$target}); + my $vol = Ravada::Volume->new( + vm => $self->_vm + ,file => $dst + ); + $vol->_chmod(0o600) } } diff --git a/lib/Ravada/Volume/Class.pm b/lib/Ravada/Volume/Class.pm index b5ee68c30..80d08b03e 100644 --- a/lib/Ravada/Volume/Class.pm +++ b/lib/Ravada/Volume/Class.pm @@ -91,6 +91,7 @@ sub _new_clone_filename($self,$name0) { sub _around_clone($orig, $self, %args) { my $name = delete $args{name}; my $file_clone = ( delete $args{file} or $self->_new_clone_filename($name)); + my $domain = (delete $args{domain} or $self->domain); confess "Error: unkonwn args ".Dumper(\%args) if keys %args; confess "Error: empty clone filename" if !defined $file_clone || !length($file_clone); @@ -102,12 +103,18 @@ sub _around_clone($orig, $self, %args) { if $self->domain; confess "Error: file $file_clone already exists in domain $id_domain_file.$we" - if !$self->domain || $self->domain->id != $id_domain_file; + if !$domain || $domain->id != $id_domain_file; } + my @domain =(); + if ($domain) { + push @domain , ( domain => $domain ); + } else { + push @domain ,( vm => $self->vm ) + } my $ret = $self->new( file => $orig->($self, $file_clone) - ,vm => $self->vm + ,@domain ); $self->_chmod(oct(600), $file_clone); diff --git a/lib/Ravada/Volume/QCOW2.pm b/lib/Ravada/Volume/QCOW2.pm index 21714811f..f9532fb59 100644 --- a/lib/Ravada/Volume/QCOW2.pm +++ b/lib/Ravada/Volume/QCOW2.pm @@ -250,7 +250,7 @@ sub spinoff($self) { sub block_commit($self) { my @cmd = ($QEMU_IMG,'commit','-q','-d'); my ($out, $err) = $self->vm->run_command(@cmd, $self->file); - warn $err if $err; + die $err if $err; } sub _qemu_info($self, $field=undef) { @@ -316,7 +316,11 @@ sub compact($self, $keep_backup=1) { sub delete($self) { my $vol = $self->vm->search_volume($self->file); - $vol->delete(); + if ($vol) { + $vol->delete(); + } else { + $self->vm->remove_file($self->file); + } } 1; diff --git a/t/20_volumes.t b/t/20_volumes.t index c0c817672..720823635 100644 --- a/t/20_volumes.t +++ b/t/20_volumes.t @@ -484,10 +484,9 @@ sub test_no_extension($vm) { } sub test_qcow_format($vm) { - return if $vm->type ne 'KVM'; my $base = create_domain($vm); - $base->add_volume(type => 'swap', size => 1024*1024); - $base->add_volume(type => 'data', size => 1024*1024); + $base->add_volume(type => 'swap', size => 1024*1024, format => 'qcow2'); + $base->add_volume(type => 'data', size => 1024*1024, format => 'qcow2'); wait_request(); my $clone = $base->clone( @@ -497,7 +496,7 @@ sub test_qcow_format($vm) { my $QEMU_IMG = `which qemu-img`; chomp $QEMU_IMG; for my $vol ( $clone->list_volumes_info ) { - next if $vol->file && $vol->file =~ /iso$/; + next if $vol->file && $vol->file =~ /(void|iso)$/; my @cmd = ($QEMU_IMG,'create' ,'-f','qcow2' ,'-F','qcow2' @@ -511,11 +510,11 @@ sub test_qcow_format($vm) { is($bff, 'qcow2'); } eval { $clone->start(user_admin) }; - is(''.$@,''); + is(''.$@,'') or exit; $clone->shutdown_now(user_admin); for my $vol ( $clone->list_volumes_info ) { - next if !$vol->file || $vol->file =~ /iso$/; + next if !$vol->file || $vol->file =~ /(void|iso)$/; my @cmd_info = ($QEMU_IMG , 'info', $vol->file); my ($out, $err) = $clone->_vm->run_command(@cmd_info); diff --git a/t/mojo/15_list_bases.t b/t/mojo/15_list_bases.t index 2bc64e469..f76b5dad0 100644 --- a/t/mojo/15_list_bases.t +++ b/t/mojo/15_list_bases.t @@ -237,6 +237,7 @@ if (!ping_backend()) { $Test::Ravada::BACKGROUND=1; remove_old_domains_req(1); # 0=do not wait for them +clean(); $t = Test::Mojo->new($SCRIPT); $t->ua->inactivity_timeout(900); diff --git a/t/request/30_hardware_clones.t b/t/request/30_hardware_clones.t index f6ee00e93..5a270e809 100644 --- a/t/request/30_hardware_clones.t +++ b/t/request/30_hardware_clones.t @@ -171,9 +171,10 @@ sub _test_change_disk($base, $clone) { my $disks_clone2 = $clone->info(user_admin)->{hardware}->{disk}; my $data_base2 = dclone($disks_base2->[0]); + delete $data_base2->{backing}; my $data_clone2 = dclone($disks_clone2->[0]); - is_deeply($data_base2, $data_base); + is_deeply($data_base2, $data_base) or exit; is($data_clone2->{file}, $data_clone->{file}) or exit; } diff --git a/t/vm/20_base.t b/t/vm/20_base.t index e8badcb15..7d6dda66a 100644 --- a/t/vm/20_base.t +++ b/t/vm/20_base.t @@ -49,6 +49,8 @@ sub test_create_domain { .($domain->name or '') ." for VM $vm_name" ); + $domain->add_volume(type => 'swap', size => 1024*1024, format => 'qcow2'); + $domain->add_volume(type => 'data', size => 1024*1024, format => 'qcow2'); return $domain; } @@ -1108,6 +1110,7 @@ sub test_remove_base_level($vm_name) { my @clones; for ( 1 .. 3 ) { my $clone = $base->clone(name => new_domain_name, user => user_admin); + _check_volume_mode_domain($clone); push @clones,($clone); } @@ -1121,6 +1124,7 @@ sub test_remove_base_level($vm_name) { for ( 1 .. 3 ) { for my $base2 ($base, @clones ) { my $clone = $base2->clone(name => new_domain_name, user => user_admin); + _check_volume_mode_domain($clone); push @clones2,($clone); } } @@ -1181,7 +1185,7 @@ sub _do_test_remove_base($domain, $base=undef) { ok(!$domain->is_base,"Domain ".$domain->name." should be base") or return; for my $file (@files) { - die $file if $file !~ m{^[0-9a-z_/\-\.]+$}; + die $file if $file !~ m{^[0-9a-zA-Z_/\-\.]+$}; if ($file =~ /\.iso$/) { ok(-e $file,"Expecting file base '$file' removed" ); } else { @@ -1199,8 +1203,15 @@ sub _do_test_remove_base($domain, $base=undef) { } } +sub _check_volume_mode_domain($domain) { + for my $file ( $domain->list_volumes) { + _check_volume_mode($file); + } +} + sub _check_volume_mode($file) { my $mode = stat($file)->mode; + my $fail = 0; if ($file =~ /\.iso$/) { ok($mode & S_IRUSR); # user can read @@ -1210,11 +1221,12 @@ sub _check_volume_mode($file) { ok($mode & S_IRUSR); # User can read ok($mode & S_IWUSR); # User can write - ok(!($mode & S_IRGRP)); # Group can not read - ok(!($mode & S_IROTH)); # Others can not read - ok(!($mode & S_IWGRP)); # Group can not write - ok(!($mode & S_IWOTH)); # Others can not write + ok(!($mode & S_IRGRP)) || $fail++; # Group can not read + ok(!($mode & S_IROTH)) || $fail++; # Others can not read + ok(!($mode & S_IWGRP)) || $fail++; # Group can not write + ok(!($mode & S_IWOTH)) || $fail++; # Others can not write } + confess $file if $fail; } diff --git a/t/vm/r20_rebase.t b/t/vm/r20_rebase.t index 58e361da6..126fd1552 100644 --- a/t/vm/r20_rebase.t +++ b/t/vm/r20_rebase.t @@ -250,7 +250,7 @@ sub test_prepare_remove($vm) { sub test_rebase_clone($vm) { my $base0 = create_domain($vm); - $base0->add_volume( format => 'qcow2' ); + $base0->add_volume( format => 'qcow2', size => $VOL_SIZE ); Ravada::Request->prepare_base( id_domain => $base0->id From 2387fdbd772cde8339a8cc00e69d072b01cdcbc9 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 11 Mar 2026 13:46:59 +0100 Subject: [PATCH 047/141] wip: test many volumes in base --- t/vm/20_base.t | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/t/vm/20_base.t b/t/vm/20_base.t index 7d6dda66a..04a82a7e1 100644 --- a/t/vm/20_base.t +++ b/t/vm/20_base.t @@ -70,7 +70,7 @@ sub test_files_base { my @files = $domain->list_files_base(); ok(scalar @files == $n_expected,"Expecting $n_expected files base , got " - .scalar @files); + .scalar @files) or confess; return; } @@ -778,7 +778,7 @@ sub _test_compare_list($display1, $display2, $domain=undef) { sub test_prepare_base { my $vm_name = shift; my $domain = shift; - my $n_volumes = (shift or 1); + my $n_volumes = (shift or 3); test_files_base($domain,0); $domain->shutdown_now($USER) if $domain->is_active(); @@ -1959,6 +1959,8 @@ for my $vm_name ( vm_names() ) { $BASE = create_domain($vm); } flush_rules() if !$<; + my $domaina = test_create_domain($vm_name); + test_prepare_base($vm_name, $domaina); test_remove_base($vm_name); test_remove_base_level($vm_name);# if $ENV{TEST_LONG}; @@ -2019,13 +2021,13 @@ for my $vm_name ( vm_names() ) { $domain = undef; my $domain2 = test_create_domain_swap($vm_name); - test_prepare_base($vm_name, $domain2 , 2); + test_prepare_base($vm_name, $domain2 , 4); $domain2->remove( user_admin ); $domain2 = test_create_domain_swap($vm_name); $domain2->start( user_admin ); $domain2->shutdown_now( user_admin ); - test_prepare_base($vm_name, $domain2 , 2); + test_prepare_base($vm_name, $domain2 , 4); $domain2->remove( user_admin ); NEXT: From e618e5f2f1a1cba4afd254395737fa34b0a04ee5 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Fri, 13 Mar 2026 11:24:33 +0100 Subject: [PATCH 048/141] Fix expose (#2300) * fix: force ports expose when ip is given closes #2299 --- lib/Ravada.pm | 4 +++ lib/Ravada/Domain.pm | 38 ++++++++++++++++++--- lib/Ravada/Domain/Void.pm | 8 +++++ lib/Ravada/Front/Domain.pm | 14 ++++++++ lib/Ravada/I18N/en.po | 3 ++ lib/Ravada/Request.pm | 44 +++++++++++++++++++++++- t/vm/93_ports_bridge.t | 2 ++ t/vm/94_ports_exposed.t | 54 +++++++++++++++++++++++++++--- templates/main/run_request.html.ep | 6 ++-- 9 files changed, 160 insertions(+), 13 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index f6f930a67..04f55a437 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -3006,6 +3006,7 @@ sub _upgrade_tables { $self->_upgrade_table('domains','show_clones' , 'int not null default 1'); $self->_upgrade_table('domains','config_no_hd' , 'text'); $self->_upgrade_table('domains','networking' , 'varchar(32)'); + $self->_upgrade_table('domains','ports_exposed','int not null default 0'); $self->_upgrade_table('domains_network','allowed','int not null default 1'); @@ -5853,6 +5854,7 @@ sub _cmd_refresh_machine($self, $request) { return; } $domain->_fetch_networking_mode() if $domain->is_known(); + $domain->_data('ports_exposed' => 0) if $domain->_data('ports_exposed'); } $domain->info($user); $domain->client_status(1) if $is_active; @@ -7126,6 +7128,7 @@ sub _cmd_remove_expose($self, $request) { sub _cmd_open_exposed_ports($self, $request) { my $domain = Ravada::Domain->open($request->id_domain) or return; return if !$domain->list_ports(); + $domain->_data('ports_exposed' => 1); my $uid = $request->args('uid'); my $user = Ravada::Auth::SQL->search_by_id( $uid ) @@ -7144,6 +7147,7 @@ sub _cmd_open_exposed_ports($self, $request) { ,id_domain => $domain->id ,retry => 20 ,timeout => 180 + ,_force => 1 ); } diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 67f822b06..e59a541e1 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -229,6 +229,7 @@ around 'remove_controller' => \&_around_remove_hardware; around 'change_hardware' => \&_around_change_hardware; around 'name' => \&_around_name; +around 'ip' => \&_around_ip; ################################################## # @@ -332,6 +333,7 @@ sub _around_start($orig, $self, @arg) { if ( !$self->is_active ) { $self->_unlock_host_devices(0); $self->_fetch_networking_mode(); + $self->_data('ports_exposed',0); } $self->_start_preconditions(@arg); @@ -2379,6 +2381,7 @@ sub info($self, $user) { ,bundle => ($self->bundle() or undef) ,is_volatile => $self->_data('is_volatile') ,networking => $self->_data('networking') + ,ports_exposed => $self->_data('ports_exposed') }; $info->{alias} = ( $self->_data('alias') or $info->{name} ); @@ -3474,6 +3477,7 @@ sub _post_shutdown { } } + $self->_data('ports_exposed',0); if (defined $timeout && $timeout && !$self->is_removed && $is_active) { if ($timeout<2) { sleep $timeout; @@ -3521,6 +3525,8 @@ sub _post_shutdown { $self->needs_restart(0) if $self->is_known() && $self->needs_restart() && !$is_active; + + $self->_data('ports_exposed',0); } sub _schedule_compact($self) { @@ -4162,20 +4168,23 @@ Performs an iptables open of all the exposed ports of the domain sub open_exposed_ports($self, $remote_ip=undef) { my @ports = $self->list_ports(); - return if !@ports; - return if !$self->is_active; - return if $self->_data('networking') eq 'isolated'; + if ( !@ports || !$self->is_active || $self->_data('networking') eq 'isolated' ) { + $self->_data('ports_exposed', 0); + return; + } my $ip = $self->ip; if ( ! $ip ) { die "Error: No ip in domain ".$self->name.". Retry.\n"; } + $self->_data('ports_exposed', 1); $self->display_info(Ravada::Utils::user_daemon); for my $expose ( @ports ) { $self->_open_exposed_port($expose->{internal_port}, $expose->{name} ,$expose->{restricted}, $remote_ip); } + $self->_data('ports_exposed', 2); } sub _close_exposed_port($self,$internal_port_req=undef) { @@ -4203,6 +4212,7 @@ sub _close_exposed_port($self,$internal_port_req=undef) { $self->_close_exposed_port_nat($iptables, %port); $self->_close_exposed_port_client($iptables, %port); + $self->_data('ports_exposed',0) if $self->is_known(); $sth->finish; } @@ -4514,7 +4524,7 @@ sub _post_start { ,id_domain => $self->id ,retry => 20 ,remote_ip => $remote_ip - ) if $is_active && $remote_ip && $self->list_ports(); + ) if $remote_ip && $self->list_ports(); if ($self->run_timeout) { my $req = Ravada::Request->shutdown_domain( @@ -6777,6 +6787,26 @@ sub _around_remove_hardware($orig, $self, $hardware, $index=undef, $options=unde } +sub _around_ip($orig, $self, @args) { + my $ip = $self->$orig(@args); + + if (!$self->readonly() && $self->list_ports()) { + if ($ip && !$self->_data('ports_exposed')) { + $self->_data('ports_exposed' => 1); + my $req = Ravada::Request->open_exposed_ports( + uid => Ravada::Utils::user_daemon->id + ,id_domain => $self->id + ,retry => 20 + ,_force => 1 + ); + } + if (!$ip && $self->_data('ports_exposed')) { + $self->_data('ports_exposed' => 0); + } + } + return $ip; +} + sub _hardware_enabled($self, $name, $index, $options ) { if ( $name eq 'filesystem') { my $sth = $self->_dbh->prepare("SELECT id,enabled " diff --git a/lib/Ravada/Domain/Void.pm b/lib/Ravada/Domain/Void.pm index 60a1127ba..0a2108932 100644 --- a/lib/Ravada/Domain/Void.pm +++ b/lib/Ravada/Domain/Void.pm @@ -348,7 +348,15 @@ sub shutdown { for my $display (@{$hardware->{'display'}}) { $display->{port} = 'auto'; } + for my $if (@{$hardware->{'network'}}) { + $if->{address} = ''; + } + $self->_store(hardware => $hardware); + + my $info = $self->_value('info'); + $info->{ip} = ''; + $self->_store(info => $info); } sub force_shutdown { diff --git a/lib/Ravada/Front/Domain.pm b/lib/Ravada/Front/Domain.pm index fdc223615..4073bb592 100644 --- a/lib/Ravada/Front/Domain.pm +++ b/lib/Ravada/Front/Domain.pm @@ -269,4 +269,18 @@ sub set_time($self) { , retry => 10 ); } + +sub ip($self) { + my $info = $self->info(Ravada::Utils->user_daemon); + return $info->{ip} if exists $info->{ip}; + my @network = $self->_get_controller_network(); + return '' if !@network; + + for my $net (@network) { + return $net->{address} if exists $net->{address} && $net->{address}; + } + + return ''; +} + 1; diff --git a/lib/Ravada/I18N/en.po b/lib/Ravada/I18N/en.po index 7edbb271e..11b2b3525 100644 --- a/lib/Ravada/I18N/en.po +++ b/lib/Ravada/I18N/en.po @@ -2203,3 +2203,6 @@ msgstr "Show the base when the user already created a clone" msgid "Show Clones" msgstr "Show Clones" + +msgid "starting" +msgstr "starting" diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index 6b4156d64..352ccb637 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -203,6 +203,9 @@ our %CMD_SEND_MESSAGE = map { $_ => 1 } create_network change_network remove_network ); +our %CMD_DO_NOT_SEND_ERROR = map { $_ => 1 } + qw(refresh_machine_ports); + our %CMD_NO_DUPLICATE = map { $_ => 1 } qw( clone @@ -293,6 +296,8 @@ our %CMD_VALIDATE = ( ,compact => \&_validate_compact ,spinoff => \&_validate_compact ,prepare_base => \&_validate_compact + ,open_exposed_ports => \&_validate_open_exposed_ports + ,close_exposed_ports => \&_validate_close_exposed_ports ); sub _init_connector { @@ -1128,6 +1133,42 @@ sub _validate_clone($self if !$base->is_public; } +sub _validate_open_exposed_ports($self) { + + my $id_domain = $self->defined_arg('id_domain'); + return if !$id_domain; + + my $domain_f; + eval { $domain_f = Ravada::Front::Domain->open($id_domain) }; + if ($@) { + my ($line) = $@ =~ m{(.*)}m; + chomp $line; + $self->error($line); + $self->status('done'); + return; + } + $domain_f->_data('ports_exposed' => 1); +} + +sub _validate_close_exposed_ports($self) { + + my $id_domain = $self->defined_arg('id_domain'); + return if !$id_domain; + + my $domain_f; + eval { $domain_f = Ravada::Front::Domain->open($id_domain) }; + if ($@) { + my ($line) = $@ =~ m{(.*)}m; + chomp $line; + $self->error($line); + $self->status('done'); + return; + } + + $domain_f->_data('ports_exposed' => 0); +} + + sub _last_insert_id { _init_connector(); return Ravada::Utils::last_insert_id($$CONNECTOR->dbh); @@ -1175,7 +1216,8 @@ sub status { } $self->_send_message($status, $message) - if $CMD_SEND_MESSAGE{$self->command} || $self->error ; + if $CMD_SEND_MESSAGE{$self->command} + || ( $self->error && !$CMD_DO_NOT_SEND_ERROR{$self->command}); if ($status eq 'done' && $date_changed && $date_changed eq $self->date_changed) { sleep 1; diff --git a/t/vm/93_ports_bridge.t b/t/vm/93_ports_bridge.t index 85f83ebbc..f23730fae 100644 --- a/t/vm/93_ports_bridge.t +++ b/t/vm/93_ports_bridge.t @@ -84,6 +84,8 @@ sub test_bridge($vm) { wait_request(debug => 0); my $internal_ip = _wait_ip($domain); + $domain->ip; + wait_request(debug => 0); my $ip_info = $domain->ip_info(); ok($ip_info->{type} eq 'bridge'); diff --git a/t/vm/94_ports_exposed.t b/t/vm/94_ports_exposed.t index d10434c02..a32275e88 100644 --- a/t/vm/94_ports_exposed.t +++ b/t/vm/94_ports_exposed.t @@ -58,30 +58,73 @@ sub test_expose_port($vm) { my $remote_ip2 = '10.0.0.2'; my $local_ip = $vm->ip; + is($domain->_data('ports_exposed'),undef); my $req = Ravada::Request->start_domain( uid => user_admin->id ,id_domain => $domain->id ,remote_ip => $remote_ip1 ); + wait_request( request => $req); + delete $domain->{_data}; + my $internal_ip = _wait_ip2($vm->type, $domain) or die "Error: no ip for ".$domain->name; + Ravada::Request->refresh_machine( + uid => user_admin->id + ,id_domain => $domain->id + ,_force => 1 + ); wait_request( request => $req); + my $domain_f = Ravada::Front::Domain->open($domain->id); + is($domain_f->ip(), $internal_ip); + + delete $domain->{_data}; + is($domain->_data('ports_exposed'),2); + my ($port) = $domain->list_ports(); + is($req->error,''); + is($domain->_data('ports_exposed'),2); + + my @out_nat = split /\n/, `iptables-save -t nat`; + my @prerouting= (grep /--to-destination $internal_ip:22/, @out_nat); + is(scalar(@prerouting),1); + my @out= split /\n/, `iptables-save`; + my @forward = (grep /-s $remote_ip1\/32 -d $internal_ip.* --dport 22.*-j ACCEPT/, @out); + is(scalar(@forward),1,"-s $remote_ip1\/32 -d $internal_ip.* --dport 22.*-j ACCEPT") or die $domain->name." ". Dumper([grep /FORWARD/,@out]); + + # test open from another ip Ravada::Request->open_exposed_ports( uid => user_admin->id ,id_domain => $domain->id ,remote_ip => $remote_ip2 ); wait_request(debug=>0); - is($req->error,''); - my @out_nat = split /\n/, `iptables-save -t nat`; - my @prerouting= (grep /--to-destination $internal_ip:22/, @out_nat); + @out_nat = split /\n/, `iptables-save -t nat`; + @prerouting= (grep /--to-destination $internal_ip:22/, @out_nat); is(scalar(@prerouting),1); - my @out= split /\n/, `iptables-save`; - my @forward = (grep /-s $remote_ip2\/32 -d $internal_ip.* --dport 22.*-j ACCEPT/, @out); + @out= split /\n/, `iptables-save`; + @forward = (grep /-s $remote_ip2\/32 -d $internal_ip.* --dport 22.*-j ACCEPT/, @out); is(scalar(@forward),1,"-s $remote_ip2\/32 -d $internal_ip.* --dport 22.*-j ACCEPT") or die $domain->name." ". Dumper([grep /FORWARD/,@out]); + + # test shutdown ports closed + Ravada::Request->shutdown_domain( + uid => user_admin->id + ,id_domain => $domain->id + ); + wait_request(); + delete $domain->{_data}; + is($domain->_data('ports_exposed'),0) or die $domain->name; + + @out_nat = split /\n/, `iptables-save -t nat`; + @prerouting= (grep /--to-destination $internal_ip:22/, @out_nat); + is(scalar(@prerouting),0); + @out= split /\n/, `iptables-save`; + @forward = (grep /-s $remote_ip2\/32 -d $internal_ip.* --dport 22.*-j ACCEPT/, @out); + is(scalar(@forward),0,"-s $remote_ip2\/32 -d $internal_ip.* --dport 22.*-j ACCEPT") or die $domain->name." ". Dumper([grep /FORWARD/,@out]); + is($domain->_data('ports_exposed'),0); + remove_domain($domain0); } @@ -115,6 +158,7 @@ SKIP: { diag($msg) if !$vm; skip $msg,10 if !$vm; + diag("testing $vm_name"); _import_base($vm); test_expose_port($vm); } diff --git a/templates/main/run_request.html.ep b/templates/main/run_request.html.ep index 1e49ee016..030b654b5 100644 --- a/templates/main/run_request.html.ep +++ b/templates/main/run_request.html.ep @@ -110,9 +110,9 @@ diff --git a/templates/main/admin_settings.html.ep b/templates/main/admin_settings.html.ep index 046e6442f..49eb82976 100644 --- a/templates/main/admin_settings.html.ep +++ b/templates/main/admin_settings.html.ep @@ -134,7 +134,7 @@ > <%=l 'Only previously authorized users can log in.' %> <%=l 'You can grant access to users from the users administration form:'%> - upload + <%=l 'upload' %> diff --git a/templates/main/list_bases_ng.html.ep b/templates/main/list_bases_ng.html.ep index 2571c49e2..93fd92382 100644 --- a/templates/main/list_bases_ng.html.ep +++ b/templates/main/list_bases_ng.html.ep @@ -93,7 +93,7 @@ Prepare base + ><%=l 'Prepare base' %>
<%=l 'Preparing a base will create a template for all the users to clone.' %> diff --git a/templates/main/settings_generic.html.ep b/templates/main/settings_generic.html.ep index 182757bc5..f790c1ae6 100644 --- a/templates/main/settings_generic.html.ep +++ b/templates/main/settings_generic.html.ep @@ -17,13 +17,13 @@
- <%= $item %> + <%=l "\u$item" %> {{<%= $item %>._old_name}}
-
+
back + href="/admin/<%= $item %>s"><%=l 'back' %>
diff --git a/templates/main/storage_list.html.ep b/templates/main/storage_list.html.ep index a26afd7d3..b2fe5c719 100644 --- a/templates/main/storage_list.html.ep +++ b/templates/main/storage_list.html.ep @@ -9,12 +9,12 @@
- - - - - - + + + + + + diff --git a/templates/main/upload_users.html.ep b/templates/main/upload_users.html.ep index fbb81a09f..9a5ac4196 100644 --- a/templates/main/upload_users.html.ep +++ b/templates/main/upload_users.html.ep @@ -28,7 +28,7 @@ novalidate >
- +
- <%=l 'down' %> - <%=l 'up' %> - + <%=l 'down' %> + <%=l 'starting' %> + <%=l 'up' %> {{port.name}} From e85121a98001d8045c965a55552943c3cbf5f608 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Fri, 13 Mar 2026 11:47:28 +0100 Subject: [PATCH 049/141] fix: check connection status in request (#2303) * fix: check connection status in request issue #2293 --- lib/Ravada/Front/Domain/KVM.pm | 2 ++ script/rvd_back | 63 ++++++++++++++++------------------ 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/lib/Ravada/Front/Domain/KVM.pm b/lib/Ravada/Front/Domain/KVM.pm index e24bbd5b4..2eca26754 100644 --- a/lib/Ravada/Front/Domain/KVM.pm +++ b/lib/Ravada/Front/Domain/KVM.pm @@ -513,4 +513,6 @@ sub xml_description($self) { return $self->_data_extra('xml'); } +sub can_hybernate { 1 }; + 1; diff --git a/script/rvd_back b/script/rvd_back index ac745eebb..1bb4937e0 100755 --- a/script/rvd_back +++ b/script/rvd_back @@ -767,13 +767,6 @@ sub start_domain { if !$found; } -sub _client_status($domain_f) { - my $domain = Ravada::Domain->open($domain_f->id) or return ''; - my $status = $domain->client_status(1); - return 'disconnected' if!defined $status || $status eq ''; - return $status; -} - sub shutdown_domain(@domains) { my $down = 0; @@ -815,27 +808,29 @@ sub shutdown_domain(@domains) { } sub _verify_connection { - my $domain_f = shift; - my $domain = Ravada::Domain->open($domain_f->id); + + my $domain = shift; + print "Verifying connection for ".$domain->name - ." ".($domain->remote_ip or '')." " - if $VERBOSE; - for ( 1 .. $TIME_CONNECTION ) { - my $status = 'disconnected'; - eval { - $status = $domain->client_status(1); - }; - warn "Error checking ".$domain_f->name." $@" if $@; - if ( $status && $status ne 'disconnected' ) { - print "\n\t".$status." ".$domain->remote_ip - ." Shutdown dismissed.\n"; - return 1; - } - print "." if $VERBOSE && !($_ % 5); - sleep 1; - } - print "\n" if $VERBOSE; + ." ".($domain->remote_ip or '')." " + if $VERBOSE; + + my $req = Ravada::Request->refresh_machine( + uid => Ravada::Utils::user_daemon->id + ,id_domain => $domain->id + ); + _wait_request($req); + + $domain = Ravada::Front::Domain->open($domain->id); + my $status = $domain->client_status(); + if ( $status && $status ne 'disconnected' ) { + print "\n\t".$status." ".$domain->remote_ip + ." Shutdown dismissed.\n" if $VERBOSE; + return 1; + } + print "\n" if $VERBOSE; return 0; + } sub _dump_ldap_entry($ldap_entry) { @@ -1047,18 +1042,18 @@ sub backup($rvd_back) { sub _wait_request(@reqs) { my $t0 = time; - for (;;) { + for ( 0 .. $TIME_CONNECTION ) { last if !@reqs; my $pending = 0; my @reqs2; for my $req (@reqs) { if ($req->status eq 'done') { - print "Finished ".$req->command; + print "Finished ".$req->command if ($req->error || $req->output); + if ($req->error) { warn $req->error."\n"; } print "\n\t".$req->output if $req->output; - print "\n"; } else { push @reqs2,($req); } @@ -1120,7 +1115,8 @@ sub _list_domains($rvd_back my %name = map { $_ => 1 } @ARGV ; my @domains; - for my $domain ($rvd_back->list_domains) { + for my $domain0 ($rvd_back->list_domains_data) { + my $domain = Ravada::Front::Domain->open($domain0->{id}); next if $domain->is_base; @@ -1130,11 +1126,12 @@ sub _list_domains($rvd_back || ( $hibernated && $domain->is_hibernated ) || ( $disconnected && $domain->is_active && !$domain->autostart - && _client_status($domain) eq 'disconnected') + && $domain->client_status() eq 'disconnected') ) { delete $name{$domain->name}; - if ( $disconnected && $domain->client_status() eq 'disconnected') { + if ( $disconnected ) { + next if $domain->client_status() ne 'disconnected'; next if _verify_connection($domain); } next if $all && $HIBERNATE_DOMAIN && !$domain->is_active; @@ -1146,7 +1143,7 @@ sub _list_domains($rvd_back next; } - if ($SHUTDOWN_DOMAIN && $domain->status eq 'down' && $VERBOSE) { + if ($SHUTDOWN_DOMAIN && !$domain->is_active && $VERBOSE) { print $domain->name." already down.\n"; next; } From f27da67a0ca91c320bc86c853c72b8764b6019d8 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Fri, 13 Mar 2026 15:22:50 +0100 Subject: [PATCH 050/141] wip: fixed typo and removed obsolete --- t/storage_move.t | 4 ---- t/vm/10_domain.t | 6 ------ 2 files changed, 10 deletions(-) diff --git a/t/storage_move.t b/t/storage_move.t index 583db1a11..eed344e4b 100644 --- a/t/storage_move.t +++ b/t/storage_move.t @@ -192,10 +192,6 @@ sub test_queue_move($vm) { remove_domain($domain); $vm->remove_storage_pool($sp); -<<<<<<< HEAD -======= - ->>>>>>> develop } sub test_queue_change_hw($vm) { diff --git a/t/vm/10_domain.t b/t/vm/10_domain.t index 5e2bd88f0..8a23a9f0b 100644 --- a/t/vm/10_domain.t +++ b/t/vm/10_domain.t @@ -648,12 +648,6 @@ sub test_create_domain_nocd { my $id_iso = search_id_iso('Alpine'); - my $sth = connector->dbh->prepare( - "UPDATE iso_images set device=NULL WHERE id=?" - ); - $sth->execute($id_iso); - $sth->finish; - my $iso; eval { $iso = $vm->_search_iso($id_iso,'')}; return if $@ && $@ =~ /Can't locate object method/; From 8aa66ab9d3d9de1759e4a217c2de077b1dd142ee Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 16 Mar 2026 13:27:45 +0100 Subject: [PATCH 051/141] wip: testing volumes in clones --- lib/Ravada.pm | 4 ++-- lib/Ravada/Domain.pm | 3 +++ lib/Ravada/VM/Void.pm | 4 +++- t/kvm/a10_pools.t | 2 +- t/vm/35_remove2.t | 32 ++++++++++++++++++++++++++++++-- t/vm/h10_hybernate.t | 3 +++ 6 files changed, 42 insertions(+), 6 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index bc6dd1ca3..45a4cc678 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -251,11 +251,11 @@ sub _add_internal_network($self) { for my $net (split /\n/,$out) { next if $net =~ /dev virbr/; my ($address) = $net =~ m{(^[\d\.]+/\d+)}; - next if !$address || $done{address}++; + next if !$address || $done{$address}++; eval { $sth->execute("internal$n",$address, ++$n+1); }; - warn $@ if $@; + warn $@ if $@ && $@ !~ /UNIQUE constraint/; } } diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index ab0b56240..7200d1285 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -883,6 +883,9 @@ sub _around_add_volume { if scalar @_ % 2; my %args = @_; + die "Error: volumes can not be added to bases.\n" + if $self->is_base(); + my $file = ($args{file} or $args{path}); confess if $args{id_iso} && !$file; my $name = $args{name}; diff --git a/lib/Ravada/VM/Void.pm b/lib/Ravada/VM/Void.pm index 391e15966..0a33727a5 100644 --- a/lib/Ravada/VM/Void.pm +++ b/lib/Ravada/VM/Void.pm @@ -142,7 +142,9 @@ sub create_domain { ,is_base => 1 ,vm => $domain_base->_vm ); - my $vol_clone = $vol_base->clone(name => "$args{name}-$target"); + my $vol_clone = $vol_base->clone(name => "$args{name}-$target" + ."-".Ravada::Utils::random_name() + ); $domain->add_volume(name => $vol_clone->name , target => $target , file => $vol_clone->file diff --git a/t/kvm/a10_pools.t b/t/kvm/a10_pools.t index 35593b195..131a6eb39 100644 --- a/t/kvm/a10_pools.t +++ b/t/kvm/a10_pools.t @@ -23,7 +23,7 @@ my $USER = create_user("foo","bar", 1); ######################################################################### -sub create_pool($vm_name, $dir="/var/tmp/".new_pool_name()) { +sub create_pool($vm_name, $dir="/var/tmp/$search_vm($vm_name) or return; diff --git a/t/vm/35_remove2.t b/t/vm/35_remove2.t index 3eae36aa1..10e3ccab9 100644 --- a/t/vm/35_remove2.t +++ b/t/vm/35_remove2.t @@ -70,9 +70,31 @@ sub _clone($base, $name=new_domain_name) { ); } +sub _check_volumes_different($d1, $d2) { + + my %vols_d1 = sort $d1->list_volumes; + my %vols_d2 = sort $d2->list_volumes; + for my $vol (keys %vols_d1) { + ok(!exists $vols_d2{$vol}) or die $vol; + } + for my $vol (keys %vols_d2) { + ok(!exists $vols_d1{$vol}) or die $vol; + } + +} + +sub _check_backing_file($d) { + for my $vol0 ($d->list_volumes_info) { + ok($vol0->backing_file) or die $vol0->{file}; + like($vol0->backing_file(),qr/\.ro\./); + } + +} + sub test_remove_rename($vm) { Test::Ravada::_check_leftovers_domains(); my $base= create_domain($vm->type); + $base->add_volume(format => 'qcow2', size => 1*1024*1024); my $name = new_domain_name(); my $base2 = _clone($base, $name); $base2->prepare_base(user_admin); @@ -87,9 +109,15 @@ sub test_remove_rename($vm) { is($@,'') or exit; $clone2 = rvd_back->search_domain($name); ok($clone2); - $clone2->remove(user_admin); - for my $vol (@volumes_base, $base2->list_volumes) { + _check_volumes_different($clone2, $base2); + _check_backing_file($clone2); + + $clone2->remove(user_admin); + for my $vol (reverse $base2->list_volumes) { + ok( -e $vol,$vol) or die $base2->name; + } + for my $vol (@volumes_base) { ok( -e $vol,$vol); } diff --git a/t/vm/h10_hybernate.t b/t/vm/h10_hybernate.t index a933964ca..73e0c387c 100644 --- a/t/vm/h10_hybernate.t +++ b/t/vm/h10_hybernate.t @@ -63,7 +63,10 @@ sub test_hybernate_clone { sub test_hybernate_clone_swap { my ($vm_name, $domain) = @_; + $domain->remove_base(user_admin); + $domain->add_volume_swap( size => 1024*512); + $domain->add_volume_swap( size => 1024*512, format => 'qcow2'); test_hybernate_clone($vm_name,$domain); } From 56e28c980df7322900b8d62d9db0dd4ea4dbba80 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 16 Mar 2026 15:42:43 +0100 Subject: [PATCH 052/141] wip: remove base before add vols --- t/vm/40_volumes.t | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/t/vm/40_volumes.t b/t/vm/40_volumes.t index e10390b0f..5e0b15e22 100644 --- a/t/vm/40_volumes.t +++ b/t/vm/40_volumes.t @@ -55,6 +55,15 @@ sub test_create_domain { return $domain; } +sub _remove_base($domain) { + return if !$domain->is_base; + for my $clone0 ( $domain->clones ) { + my $clone = Ravada::Domain->open($clone0->{id}); + $clone->remove(user_admin); + } + $domain->remove_base($USER); +} + sub test_add_volume { my $vm = shift; my $domain = shift; @@ -62,6 +71,7 @@ sub test_add_volume { my $swap = shift; $domain->shutdown_now($USER) if $domain->is_active; + _remove_base($domain) if $domain->is_base; my @volumes = $domain->list_volumes(); From 1b3504ea9c32c66a0ce3a057d67c8bb06e8ce628 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 19 Mar 2026 12:37:56 +0100 Subject: [PATCH 053/141] wip: fixed backing store refresh on clone and pool test --- lib/Ravada/VM/KVM.pm | 1 + t/vm/45_vol_swap.t | 2 +- t/vm/p10_pools.t | 33 +++++++++++++++++++++++++-------- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/lib/Ravada/VM/KVM.pm b/lib/Ravada/VM/KVM.pm index bc2b9221c..1ec810c5c 100644 --- a/lib/Ravada/VM/KVM.pm +++ b/lib/Ravada/VM/KVM.pm @@ -1280,6 +1280,7 @@ sub _domain_create_from_base { , id_vm => $self->id ); $domain->_set_spice_password($spice_password); + $domain->_set_volumes_backing_store(); $domain->xml_description(); return $domain; } diff --git a/t/vm/45_vol_swap.t b/t/vm/45_vol_swap.t index 43e752c21..247f3f97f 100644 --- a/t/vm/45_vol_swap.t +++ b/t/vm/45_vol_swap.t @@ -88,7 +88,7 @@ sub test_clone_raw($domain ) { my $doc = XML::LibXML->load_xml( string => $backing ); my ($format) = $doc->findnodes('/backingStore/format'); ok($format,"Expecing in backing: ".$doc->toString) or next; - is($format->getAttribute('type'),'qcow2',"Expecting format ".$format->toString) + like($format->getAttribute('type'),qr/qcow2|raw/,"Expecting format ".$format->toString) or exit; } is($found,2); diff --git a/t/vm/p10_pools.t b/t/vm/p10_pools.t index 7d3d0448a..3410627e9 100644 --- a/t/vm/p10_pools.t +++ b/t/vm/p10_pools.t @@ -342,15 +342,32 @@ sub test_exposed_port($vm) { is($req->error,''); is($base->is_base,1) or exit; - my $req_refresh = Ravada::Request->refresh_vms( _no_duplicate => 1); - wait_request( debug => 0 ,skip => 'set_time' ); - is($req_refresh->status, 'done'); - is(scalar($base->clones), $n); - - my $clone = $base->clone(name => new_domain_name(), user => user_admin); + my $clone; + for ( 1 .. 10 ) { + my $req_refresh = Ravada::Request->refresh_vms( _force => 1); + wait_request( debug => 0 ,skip => 'set_time' ); + is($req_refresh->status, 'done'); + is(scalar($base->clones), $n); - for my $clone ( $base->clones ) { - Ravada::Domain->open($clone->{id})->remove(user_admin); + my $name = new_domain_name(); + my $req_clone = Ravada::Request->clone( + uid => user_admin->id + ,id_domain => $base->id + ,name => $name + ); + wait_request(debug => 0, check_error => 0); + ($clone) = grep { $_->{id_owner} == user_admin->id } $base->clones(); + last if $clone; + } + ok($clone) or exit; + is($clone->{id_owner}, user_admin->id); + + for my $clone0 ( $base->clones ) { + my $clone2 = Ravada::Domain->open($clone0->{id}); + ok($clone2->list_ports); + my ($port22) = grep { $_->{internal_port} == 22 } $clone2->list_ports(); + ok($port22); + $clone2->remove(user_admin); } $base->remove(user_admin); From d9c6e8a08153d2c1f9219847c8db859f1cce63b8 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 19 Mar 2026 15:13:57 +0100 Subject: [PATCH 054/141] wip: set only backing if not volatile --- lib/Ravada/VM/KVM.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Ravada/VM/KVM.pm b/lib/Ravada/VM/KVM.pm index 1ec810c5c..ab4ef54d6 100644 --- a/lib/Ravada/VM/KVM.pm +++ b/lib/Ravada/VM/KVM.pm @@ -1280,7 +1280,7 @@ sub _domain_create_from_base { , id_vm => $self->id ); $domain->_set_spice_password($spice_password); - $domain->_set_volumes_backing_store(); + $domain->_set_volumes_backing_store() if !$volatile; $domain->xml_description(); return $domain; } From 0174ae9bc1be92e47255b86bc9fe1c2121572bbe Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Fri, 20 Mar 2026 09:00:08 +0100 Subject: [PATCH 055/141] wip: volumes can not be added to bases --- t/kvm/a20_xml.t | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/t/kvm/a20_xml.t b/t/kvm/a20_xml.t index ed14fddc2..994a3f307 100644 --- a/t/kvm/a20_xml.t +++ b/t/kvm/a20_xml.t @@ -75,6 +75,9 @@ SKIP: { my @volumes = $clone->list_volumes(); is(scalar @volumes,1); + $clone->remove(user_admin); + $clone=undef; + $domain->remove_base(user_admin); $domain->add_volume( name => $domain->name.'.vdb' , size => 1000 *1024); my @volumes_domain = $domain->list_volumes(); @@ -87,10 +90,9 @@ SKIP: { if ($clone2) { my @volumes_clone2= $clone2->list_volumes(); - is(scalar @volumes_clone2, 1); + is(scalar @volumes_clone2, 2); } - $clone->remove( user_admin ); $clone2->remove( user_admin ); $domain->remove_base( user_admin ); From 322f2355e039a549b06f92ca1a3686d7f7ba5e04 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 23 Mar 2026 11:38:35 +0100 Subject: [PATCH 056/141] wip: fixed tests for backing stores and raw --- lib/Ravada.pm | 4 ++++ lib/Ravada/Domain.pm | 1 + lib/Ravada/VM.pm | 2 ++ lib/Ravada/VM/KVM.pm | 2 ++ t/lib/Test/Ravada.pm | 2 +- t/storage_move.t | 6 ++++++ t/user/upgrade_grants.t | 2 +- t/vm/23_display.t | 6 ++++-- t/vm/40_volumes.t | 16 +--------------- t/vm/55_rename.t | 5 +++-- t/vm/d10_not_download.t | 1 + 11 files changed, 26 insertions(+), 21 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index 45a4cc678..5e4b8e7e5 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -907,6 +907,7 @@ sub _update_isos { ,xml_volume => 'jessie-volume.xml' ,min_disk_size => '0' ,has_cd => 0 + ,arch => 'i686' } ,empty_64bits => { name => 'Empty Machine 64 bits' @@ -1727,6 +1728,9 @@ sub _add_indexes_generic($self) { "index(id_vm)" ,"unique(id_vm,command,path)" ] + ,file_base_images => [ + "unique(id_domain,target)" + ] ); my $if_not_exists = ''; $if_not_exists = ' IF NOT EXISTS ' if $CONNECTOR->dbh->{Driver}{Name} =~ /sqlite|mariadb/i; diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 7200d1285..654af3d53 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -1207,6 +1207,7 @@ sub _after_prepare_base { $self->autostart(0,$user); $self->_vm->refresh_storage_pools(); + $self->after_prepare_base(); }; =pod diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index e15dfe43d..ae5ac5e32 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -1161,10 +1161,12 @@ sub _get_name_by_id($id) { sub _clean($self) { my $name = $self->{_data}->{name}; my $id = $self->{_data}->{id}; + my $is_active = $self->{_data}->{is_active}; delete $self->{_data}; delete $self->{$FIELD_TIMEOUT}; $self->{_data}->{name} = $name if $name; $self->{_data}->{id} = $id if $id; + $self->{_data}->{is_active} = $is_active; return $self; } diff --git a/lib/Ravada/VM/KVM.pm b/lib/Ravada/VM/KVM.pm index ab4ef54d6..240f58d59 100644 --- a/lib/Ravada/VM/KVM.pm +++ b/lib/Ravada/VM/KVM.pm @@ -300,6 +300,7 @@ sub search_volume($self,$file,$refresh=0) { } eval { $vol = $pool->get_volume_by_name($name) }; die $@ if $@ && $@ !~ /^libvirt error code: 50,/; + $vol = undef if $vol && $vol->get_path ne $file; } return $vol if $vol; @@ -1231,6 +1232,7 @@ sub _domain_create_from_base { my $base = $args{base}; $base = $self->_search_domain_by_id($args{id_base}) if $args{id_base}; + $base->_set_volumes_backing_store(); confess "Unknown base id: $args{id_base}" if !$base; my $volatile; diff --git a/t/lib/Test/Ravada.pm b/t/lib/Test/Ravada.pm index 3e69a0eb6..fd77f8982 100644 --- a/t/lib/Test/Ravada.pm +++ b/t/lib/Test/Ravada.pm @@ -942,7 +942,7 @@ sub remove_domain_and_clones_req($domain_data, $wait=1, $run_request=0) { ,uid => user_admin->id ,@after_req ); - wait_request(debug => 0) if $wait; + wait_request(debug => 0,check_error => 0) if $wait; return $req; } diff --git a/t/storage_move.t b/t/storage_move.t index eed344e4b..2a0f5cb3e 100644 --- a/t/storage_move.t +++ b/t/storage_move.t @@ -243,6 +243,11 @@ sub test_move_volume($vm, $domain=undef) { ,format => "raw" ,size => 1024*10 ); + $domain->add_volume( name => new_domain_name().".qcow2" + ,format => "qcow2" + ,size => 1024*10 + ); + my ($sp, $dir) = _create_storage_pool($vm); test_fail_nonvol($domain, $sp); @@ -270,6 +275,7 @@ sub test_move_volume($vm, $domain=undef) { wait_request( debug => 0); is($req->status,'done'); is($req->error, ''); + ok(-e "$dir/$filename", "Expecting $dir/$filename") or exit; if ($vol =~ /iso$/) { ok( -e $vol) or die "Expecting $vol not removed"; } else { diff --git a/t/user/upgrade_grants.t b/t/user/upgrade_grants.t index 72ae92513..76b84dab5 100644 --- a/t/user/upgrade_grants.t +++ b/t/user/upgrade_grants.t @@ -124,7 +124,7 @@ sub test_upgrade_default_user() { } ###################################################################### -init(); +clean(); my $vm_name = 'Void'; my $vm; diff --git a/t/vm/23_display.t b/t/vm/23_display.t index bd9406745..b2038afe6 100644 --- a/t/vm/23_display.t +++ b/t/vm/23_display.t @@ -67,7 +67,9 @@ sub test_display_conflict($vm) { my $display = $domain->info(user_admin)->{hardware}->{display}; last if defined $display->[0]->{port} && defined $display->[1]->{port} - && $display->[0]->{port} ne $display->[1]->{port}; + && $display->[0]->{port} ne $display->[1]->{port} + && $display->[0]->{is_active} + && $display->[1]->{is_active}; Ravada::Request->refresh_machine(uid => user_admin->id ,id_domain=> $domain->id ,_force => 1 @@ -78,7 +80,7 @@ sub test_display_conflict($vm) { my $display = $domain->info(user_admin)->{hardware}->{display}; isnt($display->[0]->{port}, $display->[1]->{port}) or die Dumper($display); is($display->[0]->{is_active},1); - is($display->[1]->{is_active},1); + is($display->[1]->{is_active},1) or die $domain->name; my $port3; for ( 1 .. 10 ) { diff --git a/t/vm/40_volumes.t b/t/vm/40_volumes.t index 5e0b15e22..da31f30b3 100644 --- a/t/vm/40_volumes.t +++ b/t/vm/40_volumes.t @@ -124,7 +124,7 @@ sub test_backing_store($domain) { for my $backing_store ($disk->findnodes('backingStore')) { $found_bs++; my ($format) = $backing_store->findnodes('format'); - ok($format) or die "Expecting format in backing store ".$backing_store->toString(); + ok($format) or die "Expecting format in backing store ".$disk->toString(); my ($source) = $backing_store->findnodes('source'); ok($source) or die "Expecting source in backing store ".$backing_store->toString(); @@ -645,17 +645,6 @@ sub _check_backing_store($xml, $name=undef) { return 1; } -sub _convert_file_to_raw($vm, @files) { - for my $file ( @files ) { - my $file_dst = "$file.raw"; - my @cmd = ('qemu-img','convert',"-O","raw",$file,$file_dst); - my ($out, $err) = $vm->run_command(@cmd); - die $err if $err; - copy($file_dst,$file) or die "$! $file_dst -> $file"; - unlink $file_dst or die "$! $file_dst"; - } -} - sub _create_domain_no_backing_store($vm) { #standalone has no backingStore entries my $standalone = create_domain($vm); @@ -669,9 +658,6 @@ sub _create_domain_no_backing_store($vm) { $base->add_volume(type => 'TMP' , format => 'raw', size => 1024 * 10); $base->prepare_base(user_admin); - my ($file) = grep { /TMP/ } $base->list_files_base; - _convert_file_to_raw($vm, $file); - my $base_doc = _remove_backing_store($base->get_xml_base); my $sth = connector->_dbh->prepare( "UPDATE base_xml set xml=? WHERE id_domain = ? " diff --git a/t/vm/55_rename.t b/t/vm/55_rename.t index 0993e8248..c87198bba 100644 --- a/t/vm/55_rename.t +++ b/t/vm/55_rename.t @@ -101,7 +101,6 @@ sub _change_hardware_ram($domain) { } sub _add_hardware_disk($domain) { - diag("add disk"); Ravada::Request->shutdown_domain( uid => user_admin->id ,id_domain => $domain->id @@ -117,7 +116,9 @@ sub _add_hardware_disk($domain) { ); wait_request(); my $domain2 = Ravada::Front::Domain->open($domain->id); - diag($domain2->_data('config_no_hd')); + like($domain2->_data('config_no_hd'),qr/./) or die $domain->name; + my $name = $domain2->name; + ok(grep /$name/, $domain->list_volumes); } sub test_req_rename_domain { diff --git a/t/vm/d10_not_download.t b/t/vm/d10_not_download.t index 9b011b501..7d5dbe8e3 100644 --- a/t/vm/d10_not_download.t +++ b/t/vm/d10_not_download.t @@ -29,6 +29,7 @@ sub test_windows($vm) { next unless $iso->{name} =~ /windows/i || !$iso->{url}; is($iso->{has_cd},1) unless $iso->{name} =~ /^Empty/; is($iso->{url}, undef); + next if $iso->{arch} eq 'i686'; my $name = new_domain_name(); my @args =( id_owner => user_admin->id From e63d0e16ba3324cf75a159b6c5b5348ea58e6a5f Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 23 Mar 2026 15:21:17 +0100 Subject: [PATCH 057/141] wip: fixed more uefi tests --- lib/Ravada.pm | 22 ---------------------- lib/Ravada/Domain/KVM.pm | 1 + t/kvm/uefi.t | 34 +++++++++++++++------------------- t/mojo/50_utf8.t | 2 +- 4 files changed, 17 insertions(+), 42 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index 5e4b8e7e5..a447191f4 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -761,28 +761,6 @@ sub _update_isos { ,min_ram => 3 ,options => { machine => 'pc-q35', bios => 'UEFI' } } - ,devuan_beowulf_amd64=> { - name =>'Devuan 10 Beowulf 64 bits' - ,description => 'Devuan Beowulf Desktop Live (amd64)' - ,arch => 'x86_64' - ,url => 'http://tw1.mirror.blendbyte.net/devuan-cd/devuan_beowulf/desktop-live/' - ,file_re => 'devuan_beowulf_.*_amd64_desktop-live.iso' - ,sha256_url => '$url/SHASUMS.txt' - ,xml => 'jessie-amd64.xml' - ,xml_volume => 'jessie-volume.xml' - ,min_disk_size => '10' - } - ,devuan_beowulf_i386=> { - name =>'Devuan 10 Beowulf 32 bits' - ,description => 'Devuan Beowulf Desktop Live (i386)' - ,arch => 'i686' - ,url => 'http://tw1.mirror.blendbyte.net/devuan-cd/devuan_beowulf/desktop-live/' - ,file_re => 'devuan_beowulf_.*_i386_desktop-live.iso' - ,sha256_url => '$url/SHASUMS.txt' - ,xml => 'jessie-i386.xml' - ,xml_volume => 'jessie-volume.xml' - ,min_disk_size => '10' - } ,devuan_daedalus_amd64=> { name =>'Devuan 12 Daedalus 64 bits' ,description => 'Devuan Daedalus Desktop Live (amd64)' diff --git a/lib/Ravada/Domain/KVM.pm b/lib/Ravada/Domain/KVM.pm index 345272b8a..d35594c24 100644 --- a/lib/Ravada/Domain/KVM.pm +++ b/lib/Ravada/Domain/KVM.pm @@ -2914,6 +2914,7 @@ sub _check_machine($self,$doc, $node) { my ($machine_bare) = $machine =~ /(.*)-\d+\.\d+$/; my %machine_types = $node->list_machine_types; my $new_machine = $machine; + return $new_machine if !defined $machine_bare; my $arch = $os_type->getAttribute('arch'); for my $try ( @{$machine_types{$arch}} ) { diff --git a/t/kvm/uefi.t b/t/kvm/uefi.t index 9a0787412..6241483b6 100644 --- a/t/kvm/uefi.t +++ b/t/kvm/uefi.t @@ -190,21 +190,12 @@ sub test_req_machine_types2($vm) { .Dumper($types)); } -sub _mock_device($vm,$iso, $mock_device) { - my $device = $iso->{device}; - return if $device && -e $device; - - my $sth = connector->dbh->prepare( - "UPDATE iso_images SET device=? WHERE id=?" - ); - $sth->execute($mock_device,$iso->{id}); - $iso->{device} = $mock_device; -} - sub _search_iso_alpine($vm) { my $id_alpine = search_id_iso('Alpine%32'); my $iso = $vm->_search_iso($id_alpine); - return $iso->{device}; + + my $file = $vm->search_volume_path($iso->{file_re}); + return $file; } sub test_isos($vm) { @@ -233,8 +224,6 @@ sub test_isos($vm) { next if !$iso->{arch} || $iso->{arch} !~ /^(i686|x86_64)$/; next if grep {$iso->{name} =~ /$_/} @skip; - _mock_device($vm,$iso, $device_iso); - die Dumper($iso) if !$iso->{device} || !-e $iso->{device}; for my $machine (@{$machine_types->{$iso->{arch}}}) { next if $machine eq 'ubuntu'; for my $uefi ( 0,1 ) { @@ -242,6 +231,10 @@ sub test_isos($vm) { next if $iso->{name} =~ /Windows 11/ && (!$uefi || $machine !~ /q35/); + + next if $uefi && $iso->{arch} ne 'x86_64'; + next if $uefi && $machine =~ /i440/; + next if !$ENV{TEST_LONG} && ( $iso->{description} =~ /Debian \d /i || $iso->{description} =~ /Mint (18|20)/i @@ -263,9 +256,10 @@ sub test_isos($vm) { , arch => $iso->{arch} , uefi => $uefi } - ,iso_file => $iso->{device} + ,iso_file => $device_iso ); - wait_request(debug => 0); + wait_request(debug => 1); + is($req->error,''); my $domain = $vm->search_domain($name); ok($domain); wait_request(); @@ -288,7 +282,9 @@ sub test_isos($vm) { } $domain->start(user_admin); - test_drives($doc); + my $n_expected = 4; + $n_expected = 3 if $iso->{name} =~ /Empty Machine/; + test_drives($doc, $n_expected); $domain->shutdown_now(user_admin); $domain->remove(user_admin) if $domain; } @@ -298,9 +294,9 @@ sub test_isos($vm) { ok($found_4m,"Expected some 4M ovmf found"); } -sub test_drives($doc) { +sub test_drives($doc, $n_expected=4) { my @drives = $doc->findnodes("/domain/devices/disk"); - die "Error: only ".scalar(@drives) if scalar(@drives)<4; + die "Error: only ".scalar(@drives) if scalar(@drives)<$n_expected; my $previous = ''; my $prev_target = ''; my $prev_file = ''; diff --git a/t/mojo/50_utf8.t b/t/mojo/50_utf8.t index ba943abd0..bac38714f 100644 --- a/t/mojo/50_utf8.t +++ b/t/mojo/50_utf8.t @@ -266,7 +266,7 @@ $t->ua->inactivity_timeout(900); $t->ua->connect_timeout(60); remove_old_domains_req(0); # 0=do not wait for them -for my $vm_name (sort @{rvd_front->list_vm_types} ) { +for my $vm_name (reverse sort @{rvd_front->list_vm_types} ) { test_utf8($t, $vm_name); } From d9584bb8106cfc93870f7980bb1a92248992c0cf Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Tue, 24 Mar 2026 09:04:21 +0100 Subject: [PATCH 058/141] wip: storage pool may be already removed --- lib/Ravada/VM/KVM.pm | 7 ++++++- t/request/30_hardware_clones.t | 1 - 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/Ravada/VM/KVM.pm b/lib/Ravada/VM/KVM.pm index 240f58d59..48e9ad0c8 100644 --- a/lib/Ravada/VM/KVM.pm +++ b/lib/Ravada/VM/KVM.pm @@ -706,7 +706,12 @@ sub create_storage_pool($self, $name, $dir, $vm=$self->vm) { } sub remove_storage_pool($self, $name) { - my $sp = $self->vm->get_storage_pool_by_name($name); + my $sp; + eval { $sp = $self->vm->get_storage_pool_by_name($name) }; + + return if $@ && ref($@) eq 'Sys::Virt::Error' + && $@->code == 49; # Missing storage pool + die $@ if $@; return if !$sp; $sp->destroy if $sp->is_active; diff --git a/t/request/30_hardware_clones.t b/t/request/30_hardware_clones.t index 5a270e809..acb393a40 100644 --- a/t/request/30_hardware_clones.t +++ b/t/request/30_hardware_clones.t @@ -171,7 +171,6 @@ sub _test_change_disk($base, $clone) { my $disks_clone2 = $clone->info(user_admin)->{hardware}->{disk}; my $data_base2 = dclone($disks_base2->[0]); - delete $data_base2->{backing}; my $data_clone2 = dclone($disks_clone2->[0]); is_deeply($data_base2, $data_base) or exit; From ecfccca893525dbf3c139158f688d498e30ae6bf Mon Sep 17 00:00:00 2001 From: Yago Diaz Date: Wed, 25 Mar 2026 09:11:13 +0100 Subject: [PATCH 059/141] Amagar contrasenya per terminal i afegir package corresponent a les dependencies --- debian/control-debian-10 | 2 +- debian/control-ubuntu-18.04 | 2 +- debian/control-ubuntu-19.04 | 2 +- debian/control-ubuntu-20.04 | 2 +- script/rvd_back | 13 +++++++++++++ 5 files changed, 17 insertions(+), 4 deletions(-) diff --git a/debian/control-debian-10 b/debian/control-debian-10 index 3cef433f4..e62476acf 100644 --- a/debian/control-debian-10 +++ b/debian/control-debian-10 @@ -4,7 +4,7 @@ Architecture: all Section: utils Priority: optional Maintainer: Francesc Guasch -Depends: perl (>=5.18),libmojolicious-perl,mysql-common,libauthen-passphrase-perl, libdatetime-perl, libdbd-mysql-perl,libdbi-perl,libdbix-connector-perl,libipc-run3-perl,libio-stringy-perl,libnet-ldap-perl,libproc-pid-file-perl,libvirt-daemon-system,libsys-virt-perl,libxml-libxml-perl,libconfig-yaml-perl,libmoose-perl,libjson-xs-perl,qemu-utils,perlmagick,libmoosex-types-netaddr-ip-perl,libio-interface-perl,libnet-dns-perl,wget,liblocale-maketext-lexicon-perl,libmojolicious-plugin-i18n-perl,libdbd-sqlite3-perl, debconf (>= 0.2.26), adduser, libdigest-sha-perl, qemu-kvm, net-tools, libfile-rsync-perl, libnet-openssh-perl, bridge-utils, libencode-locale-perl, libpbkdf2-tiny-perl, libdatetime-format-dateparse-perl, libguestfs-tools, iptstate, libfile-tee-perl, librrds-perl, rrdtool +Depends: perl (>=5.18),libmojolicious-perl,mysql-common,libauthen-passphrase-perl, libdatetime-perl, libdbd-mysql-perl,libdbi-perl,libdbix-connector-perl,libipc-run3-perl,libio-stringy-perl,libnet-ldap-perl,libproc-pid-file-perl,libvirt-daemon-system,libsys-virt-perl,libxml-libxml-perl,libconfig-yaml-perl,libmoose-perl,libjson-xs-perl,qemu-utils,perlmagick,libmoosex-types-netaddr-ip-perl,libio-interface-perl,libnet-dns-perl,wget,liblocale-maketext-lexicon-perl,libmojolicious-plugin-i18n-perl,libdbd-sqlite3-perl, debconf (>= 0.2.26), adduser, libdigest-sha-perl, qemu-kvm, net-tools, libfile-rsync-perl, libnet-openssh-perl, bridge-utils, libencode-locale-perl, libpbkdf2-tiny-perl, libdatetime-format-dateparse-perl, libguestfs-tools, iptstate, libfile-tee-perl, librrds-perl, rrdtool, libterm-readkey-perl Description: Remote Virtual Desktops Manager Ravada is a software that allows the user to connect to a remote virtual desktop. diff --git a/debian/control-ubuntu-18.04 b/debian/control-ubuntu-18.04 index 320c3dbbf..151a88704 100644 --- a/debian/control-ubuntu-18.04 +++ b/debian/control-ubuntu-18.04 @@ -4,7 +4,7 @@ Architecture: all Section: utils Priority: optional Maintainer: Francesc Guasch -Depends: perl (>=5.18),libmojolicious-perl,mysql-common,libauthen-passphrase-perl, libdatetime-perl, libdbd-mysql-perl,libdbi-perl,libdbix-connector-perl,libipc-run3-perl,libio-stringy-perl,libnet-ldap-perl,libproc-pid-file-perl,libvirt-bin,libvirt-daemon-system,libsys-virt-perl,libxml-libxml-perl,libconfig-yaml-perl,libmoose-perl,libjson-xs-perl,qemu-utils,perlmagick,libmoosex-types-netaddr-ip-perl,libio-interface-perl,libnet-dns-perl,wget,liblocale-maketext-lexicon-perl,libmojolicious-plugin-i18n-perl,libdbd-sqlite3-perl, debconf (>= 0.2.26), adduser, libdigest-sha-perl, qemu-kvm, net-tools, libfile-rsync-perl, libnet-openssh-perl, bridge-utils, libencode-locale-perl, libpbkdf2-tiny-perl, libdatetime-format-dateparse-perl, libguestfs-tools, iptstate, libfile-tee-perl, ovmf, librrds-perl, rrdtool +Depends: perl (>=5.18),libmojolicious-perl,mysql-common,libauthen-passphrase-perl, libdatetime-perl, libdbd-mysql-perl,libdbi-perl,libdbix-connector-perl,libipc-run3-perl,libio-stringy-perl,libnet-ldap-perl,libproc-pid-file-perl,libvirt-bin,libvirt-daemon-system,libsys-virt-perl,libxml-libxml-perl,libconfig-yaml-perl,libmoose-perl,libjson-xs-perl,qemu-utils,perlmagick,libmoosex-types-netaddr-ip-perl,libio-interface-perl,libnet-dns-perl,wget,liblocale-maketext-lexicon-perl,libmojolicious-plugin-i18n-perl,libdbd-sqlite3-perl, debconf (>= 0.2.26), adduser, libdigest-sha-perl, qemu-kvm, net-tools, libfile-rsync-perl, libnet-openssh-perl, bridge-utils, libencode-locale-perl, libpbkdf2-tiny-perl, libdatetime-format-dateparse-perl, libguestfs-tools, iptstate, libfile-tee-perl, ovmf, librrds-perl, rrdtool, libterm-readkey-perl Description: Remote Virtual Desktops Manager Ravada is a software that allows the user to connect to a remote virtual desktop. diff --git a/debian/control-ubuntu-19.04 b/debian/control-ubuntu-19.04 index 88c5938dc..73999fb22 100644 --- a/debian/control-ubuntu-19.04 +++ b/debian/control-ubuntu-19.04 @@ -4,7 +4,7 @@ Architecture: all Section: utils Priority: optional Maintainer: Francesc Guasch -Depends: perl (>=5.18),libmojolicious-perl,mysql-common,libauthen-passphrase-perl, libdatetime-perl, libdbd-mysql-perl,libdbi-perl,libdbix-connector-perl,libipc-run3-perl,libio-stringy-perl,libnet-ldap-perl,libproc-pid-file-perl,libvirt-daemon-system,libsys-virt-perl,libxml-libxml-perl,libconfig-yaml-perl,libmoose-perl,libjson-xs-perl,qemu-utils,perlmagick,libmoosex-types-netaddr-ip-perl,libio-interface-perl,libnet-dns-perl,wget,liblocale-maketext-lexicon-perl,libmojolicious-plugin-i18n-perl,libdbd-sqlite3-perl, debconf (>= 0.2.26), adduser, libdigest-sha-perl, qemu-kvm, net-tools, libfile-rsync-perl, libnet-openssh-perl, bridge-utils, libpbkdf2-tiny-perl, libdatetime-format-dateparse-perl, libguestfs-tools, iptstate, libfile-tee-perl, ovmf, librrds-perl, rrdtool +Depends: perl (>=5.18),libmojolicious-perl,mysql-common,libauthen-passphrase-perl, libdatetime-perl, libdbd-mysql-perl,libdbi-perl,libdbix-connector-perl,libipc-run3-perl,libio-stringy-perl,libnet-ldap-perl,libproc-pid-file-perl,libvirt-daemon-system,libsys-virt-perl,libxml-libxml-perl,libconfig-yaml-perl,libmoose-perl,libjson-xs-perl,qemu-utils,perlmagick,libmoosex-types-netaddr-ip-perl,libio-interface-perl,libnet-dns-perl,wget,liblocale-maketext-lexicon-perl,libmojolicious-plugin-i18n-perl,libdbd-sqlite3-perl, debconf (>= 0.2.26), adduser, libdigest-sha-perl, qemu-kvm, net-tools, libfile-rsync-perl, libnet-openssh-perl, bridge-utils, libpbkdf2-tiny-perl, libdatetime-format-dateparse-perl, libguestfs-tools, iptstate, libfile-tee-perl, ovmf, librrds-perl, rrdtool, libterm-readkey-perl Description: Remote Virtual Desktops Manager Ravada is a software that allows the user to connect to a remote virtual desktop. diff --git a/debian/control-ubuntu-20.04 b/debian/control-ubuntu-20.04 index 88c5938dc..73999fb22 100644 --- a/debian/control-ubuntu-20.04 +++ b/debian/control-ubuntu-20.04 @@ -4,7 +4,7 @@ Architecture: all Section: utils Priority: optional Maintainer: Francesc Guasch -Depends: perl (>=5.18),libmojolicious-perl,mysql-common,libauthen-passphrase-perl, libdatetime-perl, libdbd-mysql-perl,libdbi-perl,libdbix-connector-perl,libipc-run3-perl,libio-stringy-perl,libnet-ldap-perl,libproc-pid-file-perl,libvirt-daemon-system,libsys-virt-perl,libxml-libxml-perl,libconfig-yaml-perl,libmoose-perl,libjson-xs-perl,qemu-utils,perlmagick,libmoosex-types-netaddr-ip-perl,libio-interface-perl,libnet-dns-perl,wget,liblocale-maketext-lexicon-perl,libmojolicious-plugin-i18n-perl,libdbd-sqlite3-perl, debconf (>= 0.2.26), adduser, libdigest-sha-perl, qemu-kvm, net-tools, libfile-rsync-perl, libnet-openssh-perl, bridge-utils, libpbkdf2-tiny-perl, libdatetime-format-dateparse-perl, libguestfs-tools, iptstate, libfile-tee-perl, ovmf, librrds-perl, rrdtool +Depends: perl (>=5.18),libmojolicious-perl,mysql-common,libauthen-passphrase-perl, libdatetime-perl, libdbd-mysql-perl,libdbi-perl,libdbix-connector-perl,libipc-run3-perl,libio-stringy-perl,libnet-ldap-perl,libproc-pid-file-perl,libvirt-daemon-system,libsys-virt-perl,libxml-libxml-perl,libconfig-yaml-perl,libmoose-perl,libjson-xs-perl,qemu-utils,perlmagick,libmoosex-types-netaddr-ip-perl,libio-interface-perl,libnet-dns-perl,wget,liblocale-maketext-lexicon-perl,libmojolicious-plugin-i18n-perl,libdbd-sqlite3-perl, debconf (>= 0.2.26), adduser, libdigest-sha-perl, qemu-kvm, net-tools, libfile-rsync-perl, libnet-openssh-perl, bridge-utils, libpbkdf2-tiny-perl, libdatetime-format-dateparse-perl, libguestfs-tools, iptstate, libfile-tee-perl, ovmf, librrds-perl, rrdtool, libterm-readkey-perl Description: Remote Virtual Desktops Manager Ravada is a software that allows the user to connect to a remote virtual desktop. diff --git a/script/rvd_back b/script/rvd_back index ac745eebb..b3af09ddb 100755 --- a/script/rvd_back +++ b/script/rvd_back @@ -2,6 +2,7 @@ use warnings; use strict; +use Term::ReadKey; no warnings "experimental::signatures"; use feature qw(signatures); @@ -365,9 +366,12 @@ sub add_user { my $ravada = Ravada->new( %CONFIG); print "$login password: "; + ReadMode('noecho'); my $password = ; chomp $password; + ReadMode('restore'); + print "\n"; print "is admin ? : [y/n] "; my $is_admin_q = ; my $is_admin = 0; @@ -412,8 +416,11 @@ sub add_user_ldap { my $ravada = Ravada->new( %CONFIG); print "password : "; + ReadMode('noecho'); my $password = ; chomp $password; + ReadMode('restore'); + print "\n"; Ravada::Auth::LDAP::add_user_posix(name => $login, password => $password); } @@ -530,8 +537,11 @@ sub change_password { die "ERROR: Unknown user '$login'\n" if !$user->id; print "password : "; + ReadMode('noecho'); my $password = ; chomp $password; + ReadMode('restore'); + print "\n"; $user->change_password($password); } @@ -893,8 +903,11 @@ sub test_ldap { } print "\nType the password if you want to check the user connection or CTRL-C to stop\npassword: "; + ReadMode('noecho'); my $password = ; chomp $password; + ReadMode('restore'); + print "\n"; my $ok= Ravada::Auth::LDAP->new(name => $name, password => $password); if ($ok) { if (!$ok->{_ldap_entry}) { From 842b10d8f98aa33d350ae85dcd859d34de8dc95e Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 25 Mar 2026 10:30:02 +0100 Subject: [PATCH 060/141] wip: set base again, test mdevs, hide rsync_back in listing --- lib/Ravada/Domain.pm | 13 ++++++++----- lib/Ravada/Front.pm | 1 + lib/Ravada/Request.pm | 6 ++++++ t/device/40_mediated_device.t | 17 ++++++++++++----- t/kvm/uefi.t | 2 +- 5 files changed, 28 insertions(+), 11 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 654af3d53..b3b06909e 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -1026,7 +1026,11 @@ sub prepare_base($self, @args) { $self->_set_base_vm_db($self->_vm->id, 1); $self->_after_prepare_base($user, $request); - $self->is_base(0) if $pending_post; + if ( $pending_post ) { + $self->is_base(0) + } else { + $self->is_base(1) + } } =head2 pre_prepare_base @@ -1106,6 +1110,7 @@ sub post_prepare_base($self) { } sub _after_post_prepare_base($self) { $self->after_prepare_base(); + $self->is_base(1) } sub _clone_volumes_base($self) { @@ -2921,10 +2926,8 @@ sub is_base { $sth->execute($value, $self->id ); $sth->finish; - if (!$value) { - $sth =$$CONNECTOR->dbh->prepare("UPDATE bases_vm SET enabled=? WHERE id_domain=?"); - $sth->execute(0, $self->id); - } + $sth =$$CONNECTOR->dbh->prepare("UPDATE bases_vm SET enabled=? WHERE id_domain=?"); + $sth->execute($value, $self->id); return $value; } my $ret = $self->_data('is_base'); diff --git a/lib/Ravada/Front.pm b/lib/Ravada/Front.pm index 6b8b3b3d0..62ca49231 100644 --- a/lib/Ravada/Front.pm +++ b/lib/Ravada/Front.pm @@ -1230,6 +1230,7 @@ sub list_requests($self, $id_domain_req=undef, $seconds=60) { || $command eq 'list_storage_pools' || $command eq 'list_cpu_models' || $command eq 'list_networks' + || $command eq 'rsync_back' ; next if ( $command eq 'force_shutdown' || $command eq 'force_reboot' diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index a59a143b9..4a79eb1fa 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -1144,6 +1144,12 @@ sub _validate_clone($self , $id_base= $self->args('id_domain') , $uid=$self->args('uid')) { + my $number = $self->defined_arg('number'); + if (defined $number && ($number !~ /^\d+$/ || $number<1) ) { + $self->error("Error: $number clones requested"); + $self->status('done'); + return; + } my $base = Ravada::Front::Domain->open($id_base); if ( !$uid ) { diff --git a/t/device/40_mediated_device.t b/t/device/40_mediated_device.t index 887d83f7a..3bbc926de 100644 --- a/t/device/40_mediated_device.t +++ b/t/device/40_mediated_device.t @@ -25,7 +25,7 @@ $Ravada::Domain::TTL_REMOVE_VOLATILE=3; #################################################################### -sub _prepare_dir_mdev() { +sub _prepare_dir_mdev($n) { my $dir = "/run/user/"; @@ -37,7 +37,7 @@ sub _prepare_dir_mdev() { my $uuid="3913694f-ca45-a946-efbf-94124e5c09"; - for (1 .. 2 ) { + for (1 .. $n ) { open my $out, ">","$dir/$uuid$_$_ " or die $!; print $out "\n"; close $out; @@ -50,10 +50,12 @@ 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(); + my $n_devs = 2; + my $dir = _prepare_dir_mdev($n_devs); $hd->_data('list_command' => "ls $dir"); $MOCK_MDEV=1 unless $vm->type eq 'Void'; + is($hd->list_available_devices,$n_devs) or die Dumper($hd); return $hd->list_available_devices;; } @@ -721,6 +723,9 @@ sub test_change_hd_in_clone($domain) { $hd1 = _create_mdev($domain->_vm) if !$hd1; isnt($hd1->id, $hd0->id); + $hd0->_data('list_filter'=>''); + confess Dumper($hd0) if $hd0->list_devices <2; + _filter_hds($hd0, $hd1); die "Error: no available devices in ".$hd0->name @@ -730,8 +735,10 @@ sub test_change_hd_in_clone($domain) { if scalar($hd1->list_available_devices) < 1; my @args = ( uid => user_admin->id ,id_domain => $domain->id); - Ravada::Request->clone(@args, number => scalar($hd0->list_available_devices)-1); + my $req = Ravada::Request->clone(@args + , number => scalar($hd0->list_available_devices)-1 ); wait_request(debug => 0); + is($req->error,'') or exit; _req_start($domain->clones); @@ -748,7 +755,7 @@ sub test_change_hd_in_clone($domain) { my @clone_hd = $clone->list_host_devices; is($clone_hd[0]->id,$hd0->id); - my $req = Ravada::Request->remove_host_device( + $req = Ravada::Request->remove_host_device( uid => user_admin->id ,id_domain => $clone->id ,id_host_device => $hd0->id diff --git a/t/kvm/uefi.t b/t/kvm/uefi.t index 6241483b6..908ee7e78 100644 --- a/t/kvm/uefi.t +++ b/t/kvm/uefi.t @@ -258,7 +258,7 @@ sub test_isos($vm) { } ,iso_file => $device_iso ); - wait_request(debug => 1); + wait_request(debug => 0); is($req->error,''); my $domain = $vm->search_domain($name); ok($domain); From 0a2b717686122c8db1fe9cd6b39465e0efb4d591 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 25 Mar 2026 13:33:33 +0100 Subject: [PATCH 061/141] wip: fixed tests for ubuntu 26 --- t/request/30_hardware.t | 6 +----- t/request/31_hw_boot_order.t | 8 +++++++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/t/request/30_hardware.t b/t/request/30_hardware.t index 41c46e16c..6dd7409f1 100644 --- a/t/request/30_hardware.t +++ b/t/request/30_hardware.t @@ -545,19 +545,16 @@ sub test_add_cdrom($domain) { $n++; } - my $data = { device => 'cdrom' , boot => 2 }; my $file_iso = "/var/tmp/test_30_hardware.iso"; + my $data = { device => 'cdrom' , boot => 2, file => $file_iso }; if ($domain->type eq 'KVM') { eval { $domain->_set_boot_hd(1) }; is(''.$@,'') or exit; eval { $domain->_set_boot_hd(0) }; is(''.$@,'') or exit; - my $iso = $domain->_vm->_search_iso(search_id_iso('Alpine')); - $data->{file} = $iso->{device}; } else { $data->{boot} = 2; } - $data->{file} = $file_iso if !$data->{file}; my $found = 0; test_add_hardware_request($domain->_vm, $domain,'disk', $data); @@ -571,7 +568,6 @@ sub test_add_cdrom($domain) { is($device->info->{boot}, 2, $domain->name) or die Dumper($device->info); } } - unlink $file_iso; } diff --git a/t/request/31_hw_boot_order.t b/t/request/31_hw_boot_order.t index a7ed5067b..189ba944a 100644 --- a/t/request/31_hw_boot_order.t +++ b/t/request/31_hw_boot_order.t @@ -31,7 +31,7 @@ my $xml =<2 - hvm + hvm @@ -194,6 +194,12 @@ sub _fix_domain_config($domain) { my ($uuid) = $doc->findnodes('/domain/uuid/text()'); $uuid->setData($old_uuid); + my ($node_type) = $doc->findnodes('/domain/os/type'); + my $type_re = $node_type->getAttribute('machine'); + $type_re =~ s/-[\d\.]$//; + my $machine = $domain->_vm->_find_machine_type($node_type->getAttribute('arch'), $type_re); + $node_type->setAttribute('machine' => $machine); + for my $volume ( $doc->findnodes("/domain/devices/disk/source") ) { my $old_file = $volume->getAttribute('file'); my ($path,$ext) = $old_file =~ m{(.*)/.*(-sd.*)}; From a93d8c6f8328aa62374d9a836b8932ec71c9801e Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 26 Mar 2026 15:48:42 +0100 Subject: [PATCH 062/141] wip: sort volumes and fix test for U26 --- lib/Ravada.pm | 2 ++ lib/Ravada/Domain.pm | 13 ++++++++----- t/kvm/uefi.t | 4 ++++ t/repository/10_iso.t | 1 + t/vm/20_base.t | 4 +++- t/vm/82_route.t | 2 +- 6 files changed, 19 insertions(+), 7 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index a447191f4..33dcf91a5 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -1708,6 +1708,7 @@ sub _add_indexes_generic($self) { ] ,file_base_images => [ "unique(id_domain,target)" + ,"unique(id_domain,file_base_img)" ] ); my $if_not_exists = ''; @@ -2423,6 +2424,7 @@ sub _sql_create_tables($self) { ,id_domain => 'integer NOT NULL references `domains` (`id`) ON DELETE CASCADE' ,file_base_img => ' varchar(255) DEFAULT NULL' ,target => 'varchar(64) DEFAULT NULL' + ,n_order => 'int not null' } ] , diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index b3b06909e..59b4ebece 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -415,11 +415,11 @@ sub _around_start($orig, $self, @arg) { next if $error && ref($error) && $error->code == 1 && $error !~ /internal error.*unexpected address/ && $error !~ /process exited while connecting to monitor/ + && $error !~ /QEMU unexpectedly closed the monitor/ && $error !~ /Could not run .*swtpm/i && $error !~ /virtiofs/ && $error !~ /child process/i ; - if ($error && $self->is_known && $self->id_base && !$self->is_local && $self->_vm->enabled) { $self->_request_set_base(); next; @@ -2206,14 +2206,15 @@ sub _prepare_base_db { } my $sth = $$CONNECTOR->dbh->prepare( "INSERT INTO file_base_images " - ." (id_domain , file_base_img, target )" - ." VALUES(?,?,?)" + ." (id_domain , file_base_img, target, n_order )" + ." VALUES(?,?,?,?)" ); + my $n_order=0; for my $file_img (@file_img) { my $target; ($file_img, $target) = @$file_img if ref $file_img; next if !$file_img; - $sth->execute($self->id, $file_img, $target ); + $sth->execute($self->id, $file_img, $target, $n_order++ ); } $sth->finish; @@ -3071,7 +3072,9 @@ sub list_files_base { my $sth = $$CONNECTOR->dbh->prepare("SELECT file_base_img, target " ." FROM file_base_images " - ." WHERE id_domain=?"); + ." WHERE id_domain=?" + ." ORDER BY n_order" + ); $sth->execute($self->id); my @files; diff --git a/t/kvm/uefi.t b/t/kvm/uefi.t index 908ee7e78..42d6942a3 100644 --- a/t/kvm/uefi.t +++ b/t/kvm/uefi.t @@ -107,6 +107,10 @@ sub test_machine_types($vm) { my $iso = $vm->_search_iso($id_iso); diag("$arch $machine $iso->{name}"); + # it can not be selected by the user, so we don't test it + # pc-i440fx and UEFI will not work + next if $arch eq 'x86_64' && $machine =~ /pc-i440/ && $iso->{options}->{bios} =~ /UEFI/; + my $name = new_domain_name(); my $options = dclone($iso->{options}); diff --git a/t/repository/10_iso.t b/t/repository/10_iso.t index 780759382..90813aa6f 100644 --- a/t/repository/10_iso.t +++ b/t/repository/10_iso.t @@ -67,6 +67,7 @@ sub test_download($iso_name) { , id_vm => $vm->id # , delay => 4 , test => 1 + , _force => 1 ); is($req1->status, 'requested'); diff --git a/t/vm/20_base.t b/t/vm/20_base.t index 04a82a7e1..0e7993662 100644 --- a/t/vm/20_base.t +++ b/t/vm/20_base.t @@ -912,14 +912,16 @@ sub test_prepare_base_with_cd { ,id_owner => user_admin->id ); my @volumes_clone = $clone->list_volumes_info; + my %dupe; for my $vol (@volumes_clone) { like(ref $vol->domain, qr/^Ravada::Domain/); like(ref $vol->vm, qr/^Ravada::VM/); + die "Duplicated vol ".$vol->file if $dupe{$vol->file}++; } my ($cd_clone ) = grep { defined $_->file && $_->file =~ /\.iso$/ } @volumes_clone; ok($cd_clone,"Expecting a CD in clone ".Dumper([ map { delete $_->{domain}; delete $_->{vm}; $_ } @volumes_clone])) or exit; - is($cd_clone->info->{target}, $cd_base->[1]) or exit; + is($cd_clone->info->{target}, $cd_base->[1]) or die Dumper($clone->name, $cd_clone->info()); $clone->remove(user_admin); $domain->remove(user_admin); diff --git a/t/vm/82_route.t b/t/vm/82_route.t index 52b47e000..203737de9 100644 --- a/t/vm/82_route.t +++ b/t/vm/82_route.t @@ -62,7 +62,7 @@ sub test_slim_route($vm) { eval { $domain->start(user => user_admin, remote_ip => $remote_ip); }; - like($@, qr/binding socket to/) if$vm->type ne 'Void'; + like($@, qr/binding socket to/) or die $domain->name if $vm->type ne 'Void'; my $display = $domain->info(user_admin)->{hardware}->{display}; for my $dp (@$display) { unlike($dp->{listen_ip}, qr/^192.168.12/); From ecaa42cc34345cfaff5f485f13ba0ac3d2e2a791 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 30 Mar 2026 17:08:53 +0200 Subject: [PATCH 063/141] wip: migrate from node to node via local --- lib/Ravada.pm | 1 + lib/Ravada/Domain.pm | 20 +++++++++++++++---- lib/Ravada/Request.pm | 23 ++++++++++++++++++++++ lib/Ravada/VM.pm | 1 + t/nodes/20_dead_node.t | 10 ++++------ t/nodes/80_policy.t | 44 +++++++++++++++++++++++++++++++----------- t/vm/d20_disks.t | 8 ++++++-- 7 files changed, 84 insertions(+), 23 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index 33dcf91a5..f21767ee3 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -3633,6 +3633,7 @@ sub remove_domain { my $domain = Ravada::Front::Domain->open($id); my @volumes = $domain->list_volumes(); my $vm = Ravada::VM->open($domain->_data('id_vm')); + die "No vm ".$domain->_data('id_vm') if !$vm || !$vm->vm; for my $vol (@volumes) { next if $vol->file =~ /\.iso$/; $vm->remove_file($vol->file) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 59b4ebece..3f5591123 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -1077,8 +1077,14 @@ sub _do_prepare_base($self, $with_cd, $overwrite, $req=undef) { .$self->_vm->name; } } + my @vols_info = $self->list_volumes_info(); + my %dupe; + for my $volume (@vols_info) { + my $target = $volume->info->{target}; + + die "Error: target duplicated ".Dumper([$self->name, \@vols_info]) + if $dupe{$target}++; - for my $volume ($self->list_volumes_info()) { next if !$volume->info->{target} && $volume->info->{device} eq 'cdrom'; next if $volume->info->{device} eq 'cdrom' && (!$with_cd || !$volume->file); confess "Undefined info->target ".Dumper($volume) @@ -2214,7 +2220,10 @@ sub _prepare_base_db { my $target; ($file_img, $target) = @$file_img if ref $file_img; next if !$file_img; - $sth->execute($self->id, $file_img, $target, $n_order++ ); + eval { + $sth->execute($self->id, $file_img, $target, $n_order++ ); + }; + confess Dumper([$self->name,\@file_img,$@]) if $@; } $sth->finish; @@ -3640,7 +3649,11 @@ sub _id_vm_local($self) { ." AND ( hostname='127.0.0.1'" ." OR hostname='localhost') " ); - $sth->execute($self->_vm->type); + my $type = $self->_data('vm'); + $type = $self->_vm->type if defined $self->_vm; + $sth->execute($type); + my ($id_vm_local) = $sth->fetchrow; + return $id_vm_local; } sub _schedule_compact($self) { @@ -6008,7 +6021,6 @@ sub _bases_vm($self) { my %base; while (my ($id_vm, $hostname) = $sth->fetchrow) { $base{$id_vm} = 0; - $base{$id_vm} = 1 if $self->is_base && $hostname =~ /localhost|127/; } $sth->finish; diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index 4a79eb1fa..9b1c59ccc 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -1554,11 +1554,34 @@ sub set_base_vm { my $id_vm = $args->{id_vm}; $id_vm = $args->{id_node} if exists $args->{id_node} && $args->{id_node}; + _set_base_vm_requirement($req, $id_vm, $domain); + $domain->_set_base_vm_db($id_vm, $args->{value}, $req->id); return $req; } +sub _set_base_vm_requirement($req, $id_vm, $domain) { + return if $req->defined_arg('value') && $req->defined_arg('value')==0; + + my $id_vm_local = $domain->_id_vm_local(); + die "Error: node local not found for ".$domain->type + if !defined $id_vm_local; + + return if $id_vm == $id_vm_local; + + my $bases_vm = $domain->_bases_vm(); + + return if $bases_vm->{$id_vm_local}; + + my $req_local = Ravada::Request->set_base_vm( + uid => Ravada::Utils::user_daemon->id + ,id_domain => $domain->id + ,id_vm => $id_vm_local + ); + $req->after_request($req_local->id); +} + =head2 remove_base_vm Disables a base in a Virtual Manager diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index ae5ac5e32..72630a34f 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -1944,6 +1944,7 @@ sub run_command($self, @command) { $command[0] .= " $args" if $args; } return $self->_run_command_local(@command) if $self->is_local(); + confess "@command" if !$exec; my $ssh = $self->_ssh or confess "Error: Error connecting to ".$self->host; diff --git a/t/nodes/20_dead_node.t b/t/nodes/20_dead_node.t index aa2b07dd1..7a9851acd 100644 --- a/t/nodes/20_dead_node.t +++ b/t/nodes/20_dead_node.t @@ -40,23 +40,21 @@ sub test_down_node($vm, $node) { is($req->status, 'done'); like($req->error, qr{^($|checked)},"Expecting no error after refresh vms"); - warn 1; - is($clone[0]->is_active, 0, "Expecting clone not active after node shutdown"); - warn 2; + my $clone_f = Ravada::Front::Domain->open($clone[0]->id); + is($clone_f->is_active, 0, "Expecting clone not active after node shutdown"); + start_node($node); my@req; - for (@clone) { + for (@clone, $domain) { push @req,(Ravada::Request->remove_domain(uid => user_admin->id , name=> $_->name )); } for my $req (@req) { - warn $req->command; rvd_back->_process_requests_dont_fork(); next if $req->status ne 'done'; is($req->error ,''); } - $domain->remove(user_admin); } sub test_disabled_node($vm, $node) { diff --git a/t/nodes/80_policy.t b/t/nodes/80_policy.t index f0be35f95..0b836ad79 100644 --- a/t/nodes/80_policy.t +++ b/t/nodes/80_policy.t @@ -35,19 +35,27 @@ sub test_same_node_hibernate($vm, $node1, $node2) { } sub _set_base($node1, $node2) { - for my $node ( $node1, $node2 ) { - $BASE->set_base_vm( + + for my $node ($BASE->_vm, $node1, $node2 ) { + Ravada::Request->set_base_vm( id_vm => $node->id - ,user => user_admin + ,uid => user_admin->id + ,id_domain => $BASE->id ); } + wait_request(); my $domain = $BASE->clone( name => new_domain_name() ,user => user_admin ); - $domain->prepare_base(user_admin); + Ravada::Request->prepare_base( + uid => user_admin->id + ,id_domain => $domain->id + ); + wait_request(); $domain->_data('balance_policy'=>1); + $domain->is_public(1); return $domain; } @@ -58,10 +66,12 @@ sub test_same_node($vm, $node1, $node2, $hibernate=undef) { my $domain = _set_base($node1, $node2); for my $node ( $node1, $node2 ) { - $domain->set_base_vm( + Ravada::Request->set_base_vm( id_vm => $node->id - ,user => user_admin + ,uid => user_admin->id + ,id_domain => $domain->id ); + wait_request(debug => 1); } my ($clone1,@clone) = _create_clones($domain, $user, 4); @@ -126,11 +136,23 @@ sub _shutdown(@clones) { sub _create_clones($base, $user, $n) { my @clone; for (1 .. $n ) { - my $clone = $base->clone( - name => new_domain_name() - ,user => $user - ,memory => 128*1024 + my $name = new_domain_name(); + my $req = Ravada::Request->clone( + uid => $user->id + ,id_domain => $base->id + ,name => $name ); + ok($req->id); + wait_request(debug => 0); + is($req->status(),'done'); + is($req->error,''); + my $clone; + for ( 1 .. 10 ) { + $clone = rvd_back->search_domain($name); + last if $clone; + wait_request( debug => 1); + } + die "$name not found " if !$clone; push @clone,($clone); } return @clone; @@ -147,6 +169,7 @@ sub _migrate($node, $clone) { return if $clone_f->_data('id_vm') == $node->id; + delete_request('migrate'); my $req = Ravada::Request->migrate( id_node => $node->id ,id_domain => $clone->id @@ -155,7 +178,6 @@ sub _migrate($node, $clone) { ,start => 1 ); wait_request(debug => 0); - sleep 1; } ########################################################################## diff --git a/t/vm/d20_disks.t b/t/vm/d20_disks.t index 60ac649b3..f738cf19b 100644 --- a/t/vm/d20_disks.t +++ b/t/vm/d20_disks.t @@ -4,6 +4,7 @@ use warnings; use strict; +use Carp qw(confess); use Data::Dumper; use Mojo::JSON qw(decode_json); use Storable qw(dclone); @@ -472,7 +473,7 @@ sub _req_create($vm, $iso, $options) { my $req = Ravada::Request->create_domain(@args); wait_request( debug => 0); my $domain = $vm->search_domain($name); - ok($domain) or die "No machine $name ".Dumper($iso); + ok($domain) or confess "No machine $name ".Dumper($iso); $domain->shutdown_now(user_admin); return $domain; } @@ -533,6 +534,9 @@ sub _req_add_cd($domain) { sub _search_iso_alpine($vm) { my $id_alpine = search_id_iso('Alpine%32'); my $iso = $vm->_search_iso($id_alpine); + my $file_re = $iso->{file_re}; + $iso->{device} = $vm->search_volume_path_re(qr($file_re)); + die "Missing iso file from ".Dumper($iso) if !$iso->{device}; return $iso->{device}; } sub _machine_types($vm) { @@ -574,7 +578,6 @@ sub test_cdrom($vm) { eval { $iso = $vm->_search_iso($iso_frontend->{id}, $device_iso) }; next if $@ && $@ =~ /No.*iso.*found/; die $@ if $@; - $iso->{device} = $device_iso; my %done; for my $bios (undef, 'legacy','uefi') { @@ -588,6 +591,7 @@ sub test_cdrom($vm) { $options{bios}=$bios if defined $bios; $options{machine}=$machine if defined $machine; + $iso->{device} = $device_iso; my $domain = _req_create($vm, $iso, \%options); _req_add_cd($domain); From dd81240344c66c434ee8b2989c3f510227a2ff28 Mon Sep 17 00:00:00 2001 From: Yago Diaz Date: Tue, 31 Mar 2026 10:22:25 +0200 Subject: [PATCH 064/141] Add ReadKey prerequisite to MakeFile.PL --- Makefile.PL | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile.PL b/Makefile.PL index 979f82a33..91b2cdd5c 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -31,6 +31,7 @@ WriteMakefile( ,'DateTime::Format::DateParse'=> 0 ,'PBKDF2::Tiny' => 0 ,'HTML::Lint' => 0 + ,'Term::ReadKey' => 0 }, BUILD_REQUIRES => { 'Test::Perl::Critic' => 0 From 580530940e03d6f139d470ae47c7b0f53f717295 Mon Sep 17 00:00:00 2001 From: Yago004 <147386653+Yago004@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:43:46 +0200 Subject: [PATCH 065/141] Amagar contrasenya per terminal i afegir package corresponent a les dependencies (#2306) Disable echo when typing passwords --- Makefile.PL | 1 + debian/control-debian-10 | 2 +- debian/control-ubuntu-18.04 | 2 +- debian/control-ubuntu-19.04 | 2 +- debian/control-ubuntu-20.04 | 2 +- script/rvd_back | 13 +++++++++++++ 6 files changed, 18 insertions(+), 4 deletions(-) diff --git a/Makefile.PL b/Makefile.PL index 979f82a33..91b2cdd5c 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -31,6 +31,7 @@ WriteMakefile( ,'DateTime::Format::DateParse'=> 0 ,'PBKDF2::Tiny' => 0 ,'HTML::Lint' => 0 + ,'Term::ReadKey' => 0 }, BUILD_REQUIRES => { 'Test::Perl::Critic' => 0 diff --git a/debian/control-debian-10 b/debian/control-debian-10 index 3cef433f4..e62476acf 100644 --- a/debian/control-debian-10 +++ b/debian/control-debian-10 @@ -4,7 +4,7 @@ Architecture: all Section: utils Priority: optional Maintainer: Francesc Guasch -Depends: perl (>=5.18),libmojolicious-perl,mysql-common,libauthen-passphrase-perl, libdatetime-perl, libdbd-mysql-perl,libdbi-perl,libdbix-connector-perl,libipc-run3-perl,libio-stringy-perl,libnet-ldap-perl,libproc-pid-file-perl,libvirt-daemon-system,libsys-virt-perl,libxml-libxml-perl,libconfig-yaml-perl,libmoose-perl,libjson-xs-perl,qemu-utils,perlmagick,libmoosex-types-netaddr-ip-perl,libio-interface-perl,libnet-dns-perl,wget,liblocale-maketext-lexicon-perl,libmojolicious-plugin-i18n-perl,libdbd-sqlite3-perl, debconf (>= 0.2.26), adduser, libdigest-sha-perl, qemu-kvm, net-tools, libfile-rsync-perl, libnet-openssh-perl, bridge-utils, libencode-locale-perl, libpbkdf2-tiny-perl, libdatetime-format-dateparse-perl, libguestfs-tools, iptstate, libfile-tee-perl, librrds-perl, rrdtool +Depends: perl (>=5.18),libmojolicious-perl,mysql-common,libauthen-passphrase-perl, libdatetime-perl, libdbd-mysql-perl,libdbi-perl,libdbix-connector-perl,libipc-run3-perl,libio-stringy-perl,libnet-ldap-perl,libproc-pid-file-perl,libvirt-daemon-system,libsys-virt-perl,libxml-libxml-perl,libconfig-yaml-perl,libmoose-perl,libjson-xs-perl,qemu-utils,perlmagick,libmoosex-types-netaddr-ip-perl,libio-interface-perl,libnet-dns-perl,wget,liblocale-maketext-lexicon-perl,libmojolicious-plugin-i18n-perl,libdbd-sqlite3-perl, debconf (>= 0.2.26), adduser, libdigest-sha-perl, qemu-kvm, net-tools, libfile-rsync-perl, libnet-openssh-perl, bridge-utils, libencode-locale-perl, libpbkdf2-tiny-perl, libdatetime-format-dateparse-perl, libguestfs-tools, iptstate, libfile-tee-perl, librrds-perl, rrdtool, libterm-readkey-perl Description: Remote Virtual Desktops Manager Ravada is a software that allows the user to connect to a remote virtual desktop. diff --git a/debian/control-ubuntu-18.04 b/debian/control-ubuntu-18.04 index 320c3dbbf..151a88704 100644 --- a/debian/control-ubuntu-18.04 +++ b/debian/control-ubuntu-18.04 @@ -4,7 +4,7 @@ Architecture: all Section: utils Priority: optional Maintainer: Francesc Guasch -Depends: perl (>=5.18),libmojolicious-perl,mysql-common,libauthen-passphrase-perl, libdatetime-perl, libdbd-mysql-perl,libdbi-perl,libdbix-connector-perl,libipc-run3-perl,libio-stringy-perl,libnet-ldap-perl,libproc-pid-file-perl,libvirt-bin,libvirt-daemon-system,libsys-virt-perl,libxml-libxml-perl,libconfig-yaml-perl,libmoose-perl,libjson-xs-perl,qemu-utils,perlmagick,libmoosex-types-netaddr-ip-perl,libio-interface-perl,libnet-dns-perl,wget,liblocale-maketext-lexicon-perl,libmojolicious-plugin-i18n-perl,libdbd-sqlite3-perl, debconf (>= 0.2.26), adduser, libdigest-sha-perl, qemu-kvm, net-tools, libfile-rsync-perl, libnet-openssh-perl, bridge-utils, libencode-locale-perl, libpbkdf2-tiny-perl, libdatetime-format-dateparse-perl, libguestfs-tools, iptstate, libfile-tee-perl, ovmf, librrds-perl, rrdtool +Depends: perl (>=5.18),libmojolicious-perl,mysql-common,libauthen-passphrase-perl, libdatetime-perl, libdbd-mysql-perl,libdbi-perl,libdbix-connector-perl,libipc-run3-perl,libio-stringy-perl,libnet-ldap-perl,libproc-pid-file-perl,libvirt-bin,libvirt-daemon-system,libsys-virt-perl,libxml-libxml-perl,libconfig-yaml-perl,libmoose-perl,libjson-xs-perl,qemu-utils,perlmagick,libmoosex-types-netaddr-ip-perl,libio-interface-perl,libnet-dns-perl,wget,liblocale-maketext-lexicon-perl,libmojolicious-plugin-i18n-perl,libdbd-sqlite3-perl, debconf (>= 0.2.26), adduser, libdigest-sha-perl, qemu-kvm, net-tools, libfile-rsync-perl, libnet-openssh-perl, bridge-utils, libencode-locale-perl, libpbkdf2-tiny-perl, libdatetime-format-dateparse-perl, libguestfs-tools, iptstate, libfile-tee-perl, ovmf, librrds-perl, rrdtool, libterm-readkey-perl Description: Remote Virtual Desktops Manager Ravada is a software that allows the user to connect to a remote virtual desktop. diff --git a/debian/control-ubuntu-19.04 b/debian/control-ubuntu-19.04 index 88c5938dc..73999fb22 100644 --- a/debian/control-ubuntu-19.04 +++ b/debian/control-ubuntu-19.04 @@ -4,7 +4,7 @@ Architecture: all Section: utils Priority: optional Maintainer: Francesc Guasch -Depends: perl (>=5.18),libmojolicious-perl,mysql-common,libauthen-passphrase-perl, libdatetime-perl, libdbd-mysql-perl,libdbi-perl,libdbix-connector-perl,libipc-run3-perl,libio-stringy-perl,libnet-ldap-perl,libproc-pid-file-perl,libvirt-daemon-system,libsys-virt-perl,libxml-libxml-perl,libconfig-yaml-perl,libmoose-perl,libjson-xs-perl,qemu-utils,perlmagick,libmoosex-types-netaddr-ip-perl,libio-interface-perl,libnet-dns-perl,wget,liblocale-maketext-lexicon-perl,libmojolicious-plugin-i18n-perl,libdbd-sqlite3-perl, debconf (>= 0.2.26), adduser, libdigest-sha-perl, qemu-kvm, net-tools, libfile-rsync-perl, libnet-openssh-perl, bridge-utils, libpbkdf2-tiny-perl, libdatetime-format-dateparse-perl, libguestfs-tools, iptstate, libfile-tee-perl, ovmf, librrds-perl, rrdtool +Depends: perl (>=5.18),libmojolicious-perl,mysql-common,libauthen-passphrase-perl, libdatetime-perl, libdbd-mysql-perl,libdbi-perl,libdbix-connector-perl,libipc-run3-perl,libio-stringy-perl,libnet-ldap-perl,libproc-pid-file-perl,libvirt-daemon-system,libsys-virt-perl,libxml-libxml-perl,libconfig-yaml-perl,libmoose-perl,libjson-xs-perl,qemu-utils,perlmagick,libmoosex-types-netaddr-ip-perl,libio-interface-perl,libnet-dns-perl,wget,liblocale-maketext-lexicon-perl,libmojolicious-plugin-i18n-perl,libdbd-sqlite3-perl, debconf (>= 0.2.26), adduser, libdigest-sha-perl, qemu-kvm, net-tools, libfile-rsync-perl, libnet-openssh-perl, bridge-utils, libpbkdf2-tiny-perl, libdatetime-format-dateparse-perl, libguestfs-tools, iptstate, libfile-tee-perl, ovmf, librrds-perl, rrdtool, libterm-readkey-perl Description: Remote Virtual Desktops Manager Ravada is a software that allows the user to connect to a remote virtual desktop. diff --git a/debian/control-ubuntu-20.04 b/debian/control-ubuntu-20.04 index 88c5938dc..73999fb22 100644 --- a/debian/control-ubuntu-20.04 +++ b/debian/control-ubuntu-20.04 @@ -4,7 +4,7 @@ Architecture: all Section: utils Priority: optional Maintainer: Francesc Guasch -Depends: perl (>=5.18),libmojolicious-perl,mysql-common,libauthen-passphrase-perl, libdatetime-perl, libdbd-mysql-perl,libdbi-perl,libdbix-connector-perl,libipc-run3-perl,libio-stringy-perl,libnet-ldap-perl,libproc-pid-file-perl,libvirt-daemon-system,libsys-virt-perl,libxml-libxml-perl,libconfig-yaml-perl,libmoose-perl,libjson-xs-perl,qemu-utils,perlmagick,libmoosex-types-netaddr-ip-perl,libio-interface-perl,libnet-dns-perl,wget,liblocale-maketext-lexicon-perl,libmojolicious-plugin-i18n-perl,libdbd-sqlite3-perl, debconf (>= 0.2.26), adduser, libdigest-sha-perl, qemu-kvm, net-tools, libfile-rsync-perl, libnet-openssh-perl, bridge-utils, libpbkdf2-tiny-perl, libdatetime-format-dateparse-perl, libguestfs-tools, iptstate, libfile-tee-perl, ovmf, librrds-perl, rrdtool +Depends: perl (>=5.18),libmojolicious-perl,mysql-common,libauthen-passphrase-perl, libdatetime-perl, libdbd-mysql-perl,libdbi-perl,libdbix-connector-perl,libipc-run3-perl,libio-stringy-perl,libnet-ldap-perl,libproc-pid-file-perl,libvirt-daemon-system,libsys-virt-perl,libxml-libxml-perl,libconfig-yaml-perl,libmoose-perl,libjson-xs-perl,qemu-utils,perlmagick,libmoosex-types-netaddr-ip-perl,libio-interface-perl,libnet-dns-perl,wget,liblocale-maketext-lexicon-perl,libmojolicious-plugin-i18n-perl,libdbd-sqlite3-perl, debconf (>= 0.2.26), adduser, libdigest-sha-perl, qemu-kvm, net-tools, libfile-rsync-perl, libnet-openssh-perl, bridge-utils, libpbkdf2-tiny-perl, libdatetime-format-dateparse-perl, libguestfs-tools, iptstate, libfile-tee-perl, ovmf, librrds-perl, rrdtool, libterm-readkey-perl Description: Remote Virtual Desktops Manager Ravada is a software that allows the user to connect to a remote virtual desktop. diff --git a/script/rvd_back b/script/rvd_back index 1bb4937e0..53b922f80 100755 --- a/script/rvd_back +++ b/script/rvd_back @@ -2,6 +2,7 @@ use warnings; use strict; +use Term::ReadKey; no warnings "experimental::signatures"; use feature qw(signatures); @@ -365,9 +366,12 @@ sub add_user { my $ravada = Ravada->new( %CONFIG); print "$login password: "; + ReadMode('noecho'); my $password = ; chomp $password; + ReadMode('restore'); + print "\n"; print "is admin ? : [y/n] "; my $is_admin_q = ; my $is_admin = 0; @@ -412,8 +416,11 @@ sub add_user_ldap { my $ravada = Ravada->new( %CONFIG); print "password : "; + ReadMode('noecho'); my $password = ; chomp $password; + ReadMode('restore'); + print "\n"; Ravada::Auth::LDAP::add_user_posix(name => $login, password => $password); } @@ -530,8 +537,11 @@ sub change_password { die "ERROR: Unknown user '$login'\n" if !$user->id; print "password : "; + ReadMode('noecho'); my $password = ; chomp $password; + ReadMode('restore'); + print "\n"; $user->change_password($password); } @@ -888,8 +898,11 @@ sub test_ldap { } print "\nType the password if you want to check the user connection or CTRL-C to stop\npassword: "; + ReadMode('noecho'); my $password = ; chomp $password; + ReadMode('restore'); + print "\n"; my $ok= Ravada::Auth::LDAP->new(name => $name, password => $password); if ($ok) { if (!$ok->{_ldap_entry}) { From c97fee7fa2d816f0003b88813ca42007972cf9c4 Mon Sep 17 00:00:00 2001 From: Yago Diaz Date: Tue, 31 Mar 2026 11:58:11 +0200 Subject: [PATCH 066/141] Fix isos Ravada.pm (add newer versions, delete discontinued) --- lib/Ravada.pm | 114 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 83 insertions(+), 31 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index 04f55a437..cbc062a18 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -404,6 +404,15 @@ sub _update_isos { ,md5_url => '' ,md5 => '1d6bdf5cbc6ca98c31f02d23e418dd96' }, + arch_2603 => { + name => 'Arch Linux 26.03' + ,description => 'Arch Linux 2026.03.01 64 bits' + ,arch => 'x86_64' + ,xml => 'bionic-amd64.xml' + ,xml_volume => 'bionic64-volume.xml' + ,url => 'https://archive.archlinux.org/iso/2026.03.01/' + ,file_re => 'archlinux-2026.03.01-x86_64.iso' + }, mate_noble => { name => 'Ubuntu Mate 24.04 Noble Numbat 64 bits' ,description => 'Ubuntu Mate 24.04 Noble Nubat m64 bits' @@ -554,10 +563,34 @@ sub _update_isos { ,file_re => 'alpine-standard-3.16.*-x86.iso' ,sha256_url => '$url/alpine-standard-3.16.*.iso.sha256' ,min_disk_size => '1' + } + ,alpine323_64 => { + name => 'Alpine 3.23 64 bits' + ,description => 'Alpine Linux 3.23 64 bits ( Minimal Linux Distribution )' + ,arch => 'x86_64' + ,xml => 'alpine-amd64.xml' + ,xml_volume => 'alpine323_64-volume.xml' + ,url => 'http://dl-cdn.alpinelinux.org/alpine/v3.23/releases/x86_64/' + ,file_re => 'alpine-standard-3.23.*-x86_64.iso' + ,sha256_url => '$url/alpine-standard-3.23.*.iso.sha256' + ,min_disk_size => '2' + ,options => { machine => 'pc-q35', bios => 'UEFI' } + } + ,alpine323_32 => { + name => 'Alpine 3.23 32 bits' + ,description => 'Alpine Linux 3.23 32 bits ( Minimal Linux Distribution )' + ,arch => 'i686' + ,xml => 'alpine-i386.xml' + ,xml_volume => 'alpine323_32-volume.xml' + ,url => 'http://dl-cdn.alpinelinux.org/alpine/v3.23/releases/x86/' + ,options => { machine => 'pc-i440fx' } + ,file_re => 'alpine-standard-3.23.*-x86.iso' + ,sha256_url => '$url/alpine-standard-3.23.*.iso.sha256' + ,min_disk_size => '1' } ,kubuntu_64_noble => { name => 'Kubuntu 24.04 Noble Nombat' - ,description => 'Kubuntu 22.04 Noble Nombat 64 bits' + ,description => 'Kubuntu 24.04 Noble Nombat 64 bits' ,arch => 'x86_64' ,xml => 'noble-amd64.xml' ,xml_volume => 'focal_fossa64-volume.xml' @@ -606,6 +639,16 @@ sub _update_isos { ,url => 'https://download.opensuse.org/distribution/leap/15.4/iso/' ,file_re => 'openSUSE-Leap-15.\d-NET-x86_64-Current.iso' + } + ,suse_16 => { + name => "openSUSE Leap 16" + ,description => "openSUSE Leap 16 64 bits" + ,arch => 'x86_64' + ,xml => 'bionic-amd64.xml' + ,xml_volume => 'bionic64-volume.xml' + ,url => 'https://download.opensuse.org/distribution/leap/16.0/iso/' + ,file_re => 'openSUSE-Leap-16.\d-NET-x86_64-Current.iso' + } ,xubuntu_noble => { name => 'Xubuntu 24.04 Noble Nombat 64 bits' @@ -673,6 +716,17 @@ sub _update_isos { ,min_disk_size => '10' ,arch => 'i686' } + ,debian_stretch_64 => { + name =>'Debian 9 Stretch 64 bits' + ,description => 'Debian 9 Stretch 64 bits (XFCE desktop)' + ,url => 'https://cdimage.debian.org/cdimage/archive/^9\.1\d+.*\d$/amd64/iso-cd/' + ,file_re => 'debian-9.[\d\.]+-amd64-xfce-CD-1.iso' + ,md5_url => '$url/MD5SUMS' + ,xml => 'jessie-amd64.xml' + ,xml_volume => 'jessie-volume.xml' + ,min_disk_size => '10' + ,arch => 'x86_64' + } ,debian_buster_64=> { name =>'Debian 10 Buster 64 bits' ,description => 'Debian 10 Buster 64 bits (XFCE desktop)' @@ -758,28 +812,6 @@ sub _update_isos { ,min_ram => 3 ,options => { machine => 'pc-q35', bios => 'UEFI' } } - ,devuan_beowulf_amd64=> { - name =>'Devuan 10 Beowulf 64 bits' - ,description => 'Devuan Beowulf Desktop Live (amd64)' - ,arch => 'x86_64' - ,url => 'http://tw1.mirror.blendbyte.net/devuan-cd/devuan_beowulf/desktop-live/' - ,file_re => 'devuan_beowulf_.*_amd64_desktop-live.iso' - ,sha256_url => '$url/SHASUMS.txt' - ,xml => 'jessie-amd64.xml' - ,xml_volume => 'jessie-volume.xml' - ,min_disk_size => '10' - } - ,devuan_beowulf_i386=> { - name =>'Devuan 10 Beowulf 32 bits' - ,description => 'Devuan Beowulf Desktop Live (i386)' - ,arch => 'i686' - ,url => 'http://tw1.mirror.blendbyte.net/devuan-cd/devuan_beowulf/desktop-live/' - ,file_re => 'devuan_beowulf_.*_i386_desktop-live.iso' - ,sha256_url => '$url/SHASUMS.txt' - ,xml => 'jessie-i386.xml' - ,xml_volume => 'jessie-volume.xml' - ,min_disk_size => '10' - } ,devuan_daedalus_amd64=> { name =>'Devuan 12 Daedalus 64 bits' ,description => 'Devuan Daedalus Desktop Live (amd64)' @@ -801,17 +833,37 @@ sub _update_isos { ,xml_volume => 'jessie-volume.xml' ,min_disk_size => '10' } - - ,parrot_xfce_amd64 => { - name => 'Parrot Home Edition XFCE' - ,description => 'Parrot Home Edition XFCE 64 Bits' + ,devuan_excalibur_amd64=> { + name =>'Devuan 13 Excalibur 64 bits' + ,description => 'Devuan Excalibur Desktop Live (amd64)' ,arch => 'x86_64' + ,url => 'http://tw1.mirror.blendbyte.net/devuan-cd/devuan_excalibur/desktop-live/' + ,file_re => 'devuan_excalibur_.*_amd64_desktop-live.iso' ,xml => 'jessie-amd64.xml' ,xml_volume => 'jessie-volume.xml' - ,url => 'https://download.parrot.sh/parrot/iso/6.*/' - ,file_re => 'Parrot-home-6.*_amd64.iso' - ,sha256_url => '' - ,min_disk_size => '11' + ,min_disk_size => '10' + ,options => { machine => 'pc-q35'} + } + ,devuan_excalibur_i386=> { + name =>'Devuan 13 Excalibur 32 bits' + ,description => 'Devuan Excalibur Desktop Live (i386)' + ,arch => 'i686' + ,url => 'http://tw1.mirror.blendbyte.net/devuan-cd/devuan_excalibur/desktop-live/' + ,file_re => 'devuan_excalibur_.*_i386_desktop-live.iso' + ,xml => 'jessie-i386.xml' + ,xml_volume => 'jessie-volume.xml' + ,min_disk_size => '10' + } + ,parrot_xfce_amd64 => { + name => 'Parrot Home Edition XFCE 7.1' + ,description => 'Parrot Home Edition XFCE 7.1 64 Bits' + ,arch => 'x86_64' + ,xml => 'jessie-amd64.xml' + ,xml_volume => 'jessie-volume.xml' + ,url => 'https://download.parrot.sh/parrot/iso/7.1/' + ,file_re => 'Parrot-home-7\.1.*_amd64\.iso' + ,sha256_url => '$url/signed-hashes.txt' + ,min_disk_size => '15' } ,kali_64 => { name => "Kali Linux $year" From a803e140db560a88169896ecafaba8bbd9e4b0fd Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Tue, 31 Mar 2026 16:22:37 +0200 Subject: [PATCH 067/141] wip: backup volume order --- lib/Ravada/Domain.pm | 18 +++++++++++------- t/vm/89_ports_unique.t | 3 ++- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 3f5591123..5823b6da9 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -3079,7 +3079,7 @@ sub list_files_base { return if $@ && $@ =~ /No DB info/i; die $@ if $@; - my $sth = $$CONNECTOR->dbh->prepare("SELECT file_base_img, target " + my $sth = $$CONNECTOR->dbh->prepare("SELECT file_base_img, target, n_order " ." FROM file_base_images " ." WHERE id_domain=?" ." ORDER BY n_order" @@ -3087,9 +3087,12 @@ sub list_files_base { $sth->execute($self->id); my @files; - while ( my ($img, $target) = $sth->fetchrow) { - push @files,($img) if !$with_target; - push @files,[$img,$target] if $with_target; + while ( my ($img, $target, $n_order) = $sth->fetchrow) { + if ($with_target) { + push @files,[$img,$target, $n_order]; + } else { + push @files,($img); + } } $sth->finish; return @files; @@ -8510,12 +8513,13 @@ sub _restore_base_volumes_metadata($self, $data) { my $sth = $$CONNECTOR->dbh->prepare( "INSERT INTO file_base_images " - ." (id_domain , file_base_img, target )" - ." VALUES(?,?,?)" + ." (id_domain , file_base_img, target,n_order )" + ." VALUES(?,?,?,?)" ); + my $count=0; for my $vol ( @{$data->{base_volumes}}) { - $sth->execute($self->id, $vol->[0], $vol->[1]); + $sth->execute($self->id, $vol->[0], $vol->[1],( $vol->[2] or $count++) ); } unlock_hash(%$data); delete $data->{base_volumes}; diff --git a/t/vm/89_ports_unique.t b/t/vm/89_ports_unique.t index a1dfc02c3..03a964fb3 100644 --- a/t/vm/89_ports_unique.t +++ b/t/vm/89_ports_unique.t @@ -69,7 +69,8 @@ for my $vm_name ( reverse vm_names() ) { my $string = join(" ", map { $_ or '' } @$rule); next if $string eq 'A POSTROUTING j LIBVIRT_PRT'; - die Dumper($rule) if $dupe{$string}++; + next if $string =~ /A LIBVIRT_/; + die Dumper([$string,$rule]) if $dupe{$string}++; } } From 1729bf20166b6bad64ee5475e5c5a1a14b794345 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Tue, 31 Mar 2026 16:23:12 +0200 Subject: [PATCH 068/141] wip: checking balance policy=1 --- lib/Ravada.pm | 10 ++++++++++ lib/Ravada/Domain.pm | 3 +-- t/nodes/80_policy.t | 41 +++++++++++++++++++++++++---------------- 3 files changed, 36 insertions(+), 18 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index f21767ee3..5fccae1ce 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -6320,6 +6320,16 @@ sub _cmd_migrate($self, $request) { } $self->_remove_unnecessary_downs($domain); + if (!$domain->_vm->is_local && !$node->is_local) { + my $req_pre = Ravada::Request->migrate( + uid => Ravada::Utils->user_daemon->id + ,id_domain => $domain->id + ,id_node => $domain->_id_vm_local() + ); + $request->after_request_ok($req_pre->id); + $request->retry(10) if !defined $request->retry(); + die "Can not migrate from remote to remote. Retry.\n"; + } $domain->migrate($node, $request); my @remote_ip; diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 3f5591123..70640769c 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -589,8 +589,7 @@ sub _start_checks($self, @args) { # 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. $id_vm = $self->_data('id_vm') - if !$id_vm && defined $self->_data('id_vm') - && $self->_data('id_vm') != $vm_local->id; + if !$id_vm && defined $self->_data('id_vm'); # check the requested id_vm is suitable if ($id_vm) { diff --git a/t/nodes/80_policy.t b/t/nodes/80_policy.t index 0b836ad79..a280ed1bb 100644 --- a/t/nodes/80_policy.t +++ b/t/nodes/80_policy.t @@ -54,12 +54,14 @@ sub _set_base($node1, $node2) { ,id_domain => $domain->id ); wait_request(); + # Node with more machins from same user $domain->_data('balance_policy'=>1); + $domain->is_public(1); return $domain; } -sub test_same_node($vm, $node1, $node2, $hibernate=undef) { +sub test_same_node($vm, $node1, $node2, $hibernate=0) { my $user = create_user(); user_admin->grant($user, 'start_limit', 10); @@ -71,19 +73,24 @@ sub test_same_node($vm, $node1, $node2, $hibernate=undef) { ,uid => user_admin->id ,id_domain => $domain->id ); - wait_request(debug => 1); + wait_request(debug => 0); } - my ($clone1,@clone) = _create_clones($domain, $user, 4); for my $node0 ( $node1, $node2, $vm ) { - + my ($clone1,@clone) = _create_clones($domain, $user, 4); + # We migrate the first clone to the node _migrate($node0, $clone1); - Ravada::Request->hybernate( - uid => $user->id - ,id_domain => $clone1->id - ) if $hibernate; + # then all the other clones should go there too because + # of the balance policy = 1 for my $clone ( @clone ) { + diag("Testing ".$clone->name." same node ".$node0->id." ".$node0->name); + + Ravada::Request->hybernate( + uid => $user->id + ,id_domain => $clone->id + ) if $hibernate; + my $req_s = Ravada::Request->start_domain( id_domain => $clone->id ,uid => $user->id @@ -92,14 +99,13 @@ sub test_same_node($vm, $node1, $node2, $hibernate=undef) { my $clone_f = Ravada::Front::Domain->open($clone->id); is($clone_f->_data('id_vm'), $node0->id - ,"Expecting ".$clone->name." same node in ".$vm->type) - or exit; + ,"Expecting ".$clone->name." same node in ".$vm->type + ." hibernate=$hibernate" + ) + or exit; + _shutdown($clone); } - if ( $hibernate ) { - $clone1->remove(user_admin); - ($clone1) = _create_clones($domain, $user,1); - } - _shutdown($clone1, @clone); + remove_domain($clone1, @clone); } remove_domain($domain); @@ -177,7 +183,10 @@ sub _migrate($node, $clone) { ,shutdown => 1 ,start => 1 ); - wait_request(debug => 0); + wait_request(debug => 1); + + my $clone2 = Ravada::Front::Domain->open($clone->id); + is($clone2->_data('id_vm'), $node->id) or confess $clone->name; } ########################################################################## From 29d5da810610ead6228b9a2d29861f10b9f5926d Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Tue, 31 Mar 2026 16:24:55 +0200 Subject: [PATCH 069/141] wip: migrate anyway --- t/nodes/80_policy.t | 2 -- 1 file changed, 2 deletions(-) diff --git a/t/nodes/80_policy.t b/t/nodes/80_policy.t index 0b836ad79..09e7aa65a 100644 --- a/t/nodes/80_policy.t +++ b/t/nodes/80_policy.t @@ -167,8 +167,6 @@ sub _migrate($node, $clone) { my $clone_f = Ravada::Front::Domain->open($clone->id); - return if $clone_f->_data('id_vm') == $node->id; - delete_request('migrate'); my $req = Ravada::Request->migrate( id_node => $node->id From b11782dbc4f9668a785522c6154dea628e59f02c Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 1 Apr 2026 15:24:10 +0200 Subject: [PATCH 070/141] wip: migrate from remote to remote via local --- deb/debianize.pl | 8 +++-- debian/control-ubuntu-19.04 | 10 ------ lib/Ravada.pm | 16 +++++++++ t/lib/Test/Ravada.pm | 2 -- t/nodes/50_three_nodes.t | 67 +++++++++++++++++++++++++++++++++---- t/nodes/80_policy.t | 12 ++++--- 6 files changed, 91 insertions(+), 24 deletions(-) delete mode 100644 debian/control-ubuntu-19.04 diff --git a/deb/debianize.pl b/deb/debianize.pl index 2b6fba005..8e7fbec21 100755 --- a/deb/debianize.pl +++ b/deb/debianize.pl @@ -17,12 +17,14 @@ my $DIR_DST; my $DEBIAN = "DEBIAN"; my $FORCE; +my $OS; my $help; my $usage = "$0 [--help] [--force]\n"; GetOptions( force => \$FORCE ,help => \$help + ,'os=s' => \$OS ) or die $usage; if ($help) { @@ -365,11 +367,13 @@ sub list_dists { while ( my $file = readdir $dir ) { my ($dist) = $file =~ /control-(.*)/; + next if !$dist; + next if $OS && $dist !~ /$OS/; push @dists,($dist) if $dist; } closedir $dir; - die "Error: no dists control files found in 'debian' dir" + die "Error: no dists control files found in 'debian' dir $OS" if !@dists; return reverse @dists; @@ -418,7 +422,7 @@ sub copy_identical_releases { get_fallback(); -for my $dist (list_dists) { +for my $dist (list_dists()) { $DIR_DST = "$DIR_SRC/../ravada-$VERSION-$dist"; clean(); diff --git a/debian/control-ubuntu-19.04 b/debian/control-ubuntu-19.04 deleted file mode 100644 index 88c5938dc..000000000 --- a/debian/control-ubuntu-19.04 +++ /dev/null @@ -1,10 +0,0 @@ -Package: ravada -Version: 0.2.5 -Architecture: all -Section: utils -Priority: optional -Maintainer: Francesc Guasch -Depends: perl (>=5.18),libmojolicious-perl,mysql-common,libauthen-passphrase-perl, libdatetime-perl, libdbd-mysql-perl,libdbi-perl,libdbix-connector-perl,libipc-run3-perl,libio-stringy-perl,libnet-ldap-perl,libproc-pid-file-perl,libvirt-daemon-system,libsys-virt-perl,libxml-libxml-perl,libconfig-yaml-perl,libmoose-perl,libjson-xs-perl,qemu-utils,perlmagick,libmoosex-types-netaddr-ip-perl,libio-interface-perl,libnet-dns-perl,wget,liblocale-maketext-lexicon-perl,libmojolicious-plugin-i18n-perl,libdbd-sqlite3-perl, debconf (>= 0.2.26), adduser, libdigest-sha-perl, qemu-kvm, net-tools, libfile-rsync-perl, libnet-openssh-perl, bridge-utils, libpbkdf2-tiny-perl, libdatetime-format-dateparse-perl, libguestfs-tools, iptstate, libfile-tee-perl, ovmf, librrds-perl, rrdtool -Description: Remote Virtual Desktops Manager - Ravada is a software that allows the user to connect to a - remote virtual desktop. diff --git a/lib/Ravada.pm b/lib/Ravada.pm index f21767ee3..5403503f4 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -6299,7 +6299,23 @@ sub _cmd_migrate($self, $request) { die "Error: user ".$user->name." not allowed to migrate domain ".$domain->name unless $user->is_operator; + return if $domain->_vm->id == $request->args('id_node'); + my $node = Ravada::VM->open($request->args('id_node')); + + if (!$node->is_local && !$domain->is_local ) { + my $req_migrate = Ravada::Request->migrate( + uid => $uid + ,id_domain => $id_domain + ,id_node => $domain->_id_vm_local() + ,shutdown => $request->defined_arg('shutdown') + ); + die "Error: could not migrate ".$domain->name." to local " if !$req_migrate; + + $request->after_request_ok($req_migrate->id); + $request->retry(10) if !defined $req_migrate->retry(); + die "Virtual Machine ".$domain->name." migrating to local. Retry.\n"; + } $self->_migrate_base($domain, $node, $uid, $request) if $domain->id_base; if ($domain->is_active) { diff --git a/t/lib/Test/Ravada.pm b/t/lib/Test/Ravada.pm index fd77f8982..340bee744 100644 --- a/t/lib/Test/Ravada.pm +++ b/t/lib/Test/Ravada.pm @@ -448,8 +448,6 @@ sub create_domain($vm_name, $user=$USER_ADMIN, $id_iso='Alpine%64', $swap=undef) $id_iso = search_id_iso($iso_name, $vm); warn "I can't find iso $iso_name" if !defined $id_iso; } - confess "ERROR: Domains can only be created at localhost" - if $vm->host ne 'localhost'; =pod // TODO: use create v2 from now on diff --git a/t/nodes/50_three_nodes.t b/t/nodes/50_three_nodes.t index 86159dfcb..a84ab3ef6 100644 --- a/t/nodes/50_three_nodes.t +++ b/t/nodes/50_three_nodes.t @@ -13,6 +13,26 @@ no warnings "experimental::signatures"; use feature qw(signatures); ################################################################################## +sub test_remote_standalone( @nodes ) { + for my $node0 (@nodes) { + my $domain = create_domain($node0); + for my $node1 (@nodes) { + diag("Migrate from ".$domain->_vm->name." to ".$node1->name); + my $req = Ravada::Request->migrate( + uid => user_admin->id + ,id_domain => $domain->id + ,id_node => $node1->id + ,shutdown => 1 + ,start => 1 + ); + wait_request( debug => 1); + my $domain_f = Ravada::Front::Domain->open($domain->id); + is($domain_f->_data('id_vm'),$node1->id); + } + remove_domain($domain); + } +} + sub test_remove_n($vm, @nodes ) { my $domain = create_domain($vm); @@ -21,14 +41,27 @@ sub test_remove_n($vm, @nodes ) { my $n=1; for my $node ( @nodes ) { - $domain->set_base_vm(vm => $node, user => user_admin); + Ravada::Request->set_base_vm( + id_vm=> $node->id + ,uid => user_admin->id + ,id_domain => $domain->id + ); + wait_request(); is($domain->list_instances, ++$n); } for my $node1 ( @nodes ) { my $clone = $domain->clone(user => user_admin, name => new_domain_name); - $clone->migrate($node1); - $clone->start(user_admin); + diag("Migrate clone ".$clone->name." from ".$clone->_vm->name + ." to ".$node1->name); + my $req = Ravada::Request->migrate( + uid => user_admin->id + ,id_domain => $clone->id + ,id_node => $node1->id + ,shutdown => 1 + ,start => 1 + ); + wait_request(); for my $node2 ( @nodes ) { next if $node2->id == $node1->id; diag("Migrating ".$clone->name." from ".$node1->name." to ".$node2->name); @@ -69,14 +102,32 @@ sub test_remove($vm, $node1, $node2) { , name => new_domain_name ); is($clone1->list_instances,1); - $clone1->migrate($node1); + + Ravada::Request->migrate( + uid => user_admin->id + ,id_domain => $clone1->id + ,id_node => $node1->id + ); + wait_request(); + is($clone1->list_instances,2); my $clone2 = $domain->clone( user => user_admin , name => new_domain_name ); - $clone2->migrate($node1); - $clone2->migrate($node2); + Ravada::Request->migrate( + uid => user_admin->id + ,id_domain => $clone2->id + ,id_node => $node1->id + ); + wait_request(); + Ravada::Request->migrate( + uid => user_admin->id + ,id_domain => $clone2->id + ,id_node => $node2->id + ); + wait_request(); + is($clone2->list_instances,3); my @name = ( $clone1->name, $clone2->name, $domain->name); @@ -156,6 +207,10 @@ for my $vm_name ( 'Void', 'KVM') { my $node_shared = remote_node_shared($vm_name) or next; + test_remote_standalone($node1, $node2, $node_shared); + + test_remove_n($node1, $node2, $node_shared); + # create on local first test_remove_n($vm, $node1, $node2, $node_shared); test_remove($vm, $node1, $node2); diff --git a/t/nodes/80_policy.t b/t/nodes/80_policy.t index 09e7aa65a..16ea32579 100644 --- a/t/nodes/80_policy.t +++ b/t/nodes/80_policy.t @@ -77,13 +77,21 @@ sub test_same_node($vm, $node1, $node2, $hibernate=undef) { my ($clone1,@clone) = _create_clones($domain, $user, 4); for my $node0 ( $node1, $node2, $vm ) { + diag("First migrate ".$clone1->name." from ".$domain->_vm->name." to node ".$node0->name); _migrate($node0, $clone1); Ravada::Request->hybernate( uid => $user->id ,id_domain => $clone1->id ) if $hibernate; + $clone1 = Ravada::Front::Domain->open($clone1->id); + if ($clone1->_data('id_vm') != $node0->id) { + diag("Error: could not start clone ".$clone1->name." in node ".$node0->name." ".$node0->id); + next; + } + diag("Expecting other clones go to ".$node0->name); for my $clone ( @clone ) { + diag("Expecting clone ".$clone->name." go to ".$node0->name); my $req_s = Ravada::Request->start_domain( id_domain => $clone->id ,uid => $user->id @@ -95,10 +103,6 @@ sub test_same_node($vm, $node1, $node2, $hibernate=undef) { ,"Expecting ".$clone->name." same node in ".$vm->type) or exit; } - if ( $hibernate ) { - $clone1->remove(user_admin); - ($clone1) = _create_clones($domain, $user,1); - } _shutdown($clone1, @clone); } From 15cdf37b2d1d2e59a5beb5d578fe50f6405058bf Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 2 Apr 2026 10:16:38 +0200 Subject: [PATCH 071/141] wip: fixed instances count and rrd in tests --- lib/Ravada/VM.pm | 3 ++- t/nodes/50_three_nodes.t | 14 ++++++++++++-- t/vm/n20_name.t | 8 +++++--- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index 72630a34f..9be1eff47 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -1953,7 +1953,8 @@ sub run_command($self, @command) { $timeout = 60*60 if $command[0] =~ /cp|qemu/; my $t0 = time; my ($out, $err) = $ssh->capture2({timeout => $timeout},join " ",@command); - warn ''.(time -$t0)." @command" if $command[0] =~ /cp|qemu/i && $command[1] ne 'info'; + warn ''.(time -$t0)." @command" + if $command[0] =~ /cp|qemu/i && !defined $command[1] && $command[1] ne 'info'; chomp $err if $err; $err = '' if !defined $err; diff --git a/t/nodes/50_three_nodes.t b/t/nodes/50_three_nodes.t index a84ab3ef6..e5f432d05 100644 --- a/t/nodes/50_three_nodes.t +++ b/t/nodes/50_three_nodes.t @@ -40,6 +40,7 @@ sub test_remove_n($vm, @nodes ) { $domain->prepare_base(user_admin); my $n=1; + $n++ if !$domain->_vm->is_local; for my $node ( @nodes ) { Ravada::Request->set_base_vm( id_vm=> $node->id @@ -47,7 +48,8 @@ sub test_remove_n($vm, @nodes ) { ,id_domain => $domain->id ); wait_request(); - is($domain->list_instances, ++$n); + is($domain->list_instances, ++$n) + or die Dumper([$domain->list_instances]); } for my $node1 ( @nodes ) { @@ -103,6 +105,14 @@ sub test_remove($vm, $node1, $node2) { ); is($clone1->list_instances,1); + my $n_exp_instances=2; + if ($clone1->_vm->id == $node1->id ) { + # does nothing + $n_exp_instances=1; + } elsif ( !$clone1->_vm->is_local && !$node1->is_local ) { + # does a pass via local + $n_exp_instances=3; + } Ravada::Request->migrate( uid => user_admin->id ,id_domain => $clone1->id @@ -110,7 +120,7 @@ sub test_remove($vm, $node1, $node2) { ); wait_request(); - is($clone1->list_instances,2); + is($clone1->list_instances,$n_exp_instances); my $clone2 = $domain->clone( user => user_admin , name => new_domain_name diff --git a/t/vm/n20_name.t b/t/vm/n20_name.t index d588f8028..c33f1f4c1 100644 --- a/t/vm/n20_name.t +++ b/t/vm/n20_name.t @@ -87,7 +87,8 @@ sub test_nat($vm_name) { ok($domain,"[$vm_name] Expecting the domain $domain_name") or exit; my $file_config = "/tmp/config_display.yml"; - DumpFile($file_config,{ display_ip => $display_ip, vm => \@VMS }); + DumpFile($file_config,{ display_ip => $display_ip, vm => \@VMS + ,dir_rrd => '/var/tmp/ravada/rrd/$$'}); my $rvd_back = Ravada->new( connector => connector() , config => $file_config @@ -123,7 +124,8 @@ sub test_nat($vm_name) { #-------------------------------------------------------------------------------- # Now with Nat # - DumpFile($file_config,{ display_ip => $display_ip, nat_ip => $NAT_IP, vm => \@VMS }); + DumpFile($file_config,{ display_ip => $display_ip, nat_ip => $NAT_IP, vm => \@VMS + , dir_rrd => '/var/tmp/ravada/rrd/$$'}); $rvd_back = Ravada->new( connector => connector() , config => $file_config @@ -154,7 +156,7 @@ sub test_nat($vm_name) { $domain->remove(user_admin); - DumpFile($file_config,{ vm => \@VMS }); + DumpFile($file_config,{ vm => \@VMS, dir_rrd => '/var/tmp/ravada/rrd/$$'}); $rvd_back = Ravada->new( connector => connector() , config => $file_config From c04af93f9a5068d8c41079431e36200eddc7f5bc Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 2 Apr 2026 12:02:00 +0200 Subject: [PATCH 072/141] wip: do not test nbd if not loaded --- t/lib/Test/Ravada.pm | 1 + t/vm/compact.t | 1 + 2 files changed, 2 insertions(+) diff --git a/t/lib/Test/Ravada.pm b/t/lib/Test/Ravada.pm index 340bee744..3d48a88e4 100644 --- a/t/lib/Test/Ravada.pm +++ b/t/lib/Test/Ravada.pm @@ -2782,6 +2782,7 @@ sub check_leftovers { sub _check_removed_nbd { return if $<; + return if !$NBD_LOADED; my ($in, $out, $err); my @cmd = ('rmmod',"nbd"); run3(\@cmd,\$in,\$out,\$err); diff --git a/t/vm/compact.t b/t/vm/compact.t index 47c088f97..cd651a0d3 100644 --- a/t/vm/compact.t +++ b/t/vm/compact.t @@ -148,6 +148,7 @@ sub test_compact_clone($vm) { ####################################################### init(); +clean(); test_settings(); if ($>) { From 28093cda701339c42c5ad77ae31814be219cc805 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Tue, 7 Apr 2026 13:00:56 +0200 Subject: [PATCH 073/141] wip: requirements for migrate and prepare --- lib/Ravada.pm | 15 +-------------- lib/Ravada/Domain.pm | 14 ++++++++------ lib/Ravada/Request.pm | 32 +++++++++++++++++++++++++++++--- lib/Ravada/VM.pm | 2 ++ t/kvm/n30_nodes_front.t | 2 +- 5 files changed, 41 insertions(+), 24 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index 8afef8af3..aab530d12 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -3,7 +3,7 @@ package Ravada; use warnings; use strict; -our $VERSION = '2.4.2'; +our $VERSION = '2.5.0-alpha4'; use utf8; @@ -6303,19 +6303,6 @@ sub _cmd_migrate($self, $request) { my $node = Ravada::VM->open($request->args('id_node')); - if (!$node->is_local && !$domain->is_local ) { - my $req_migrate = Ravada::Request->migrate( - uid => $uid - ,id_domain => $id_domain - ,id_node => $domain->_id_vm_local() - ,shutdown => $request->defined_arg('shutdown') - ); - die "Error: could not migrate ".$domain->name." to local " if !$req_migrate; - - $request->after_request_ok($req_migrate->id); - $request->retry(10) if !defined $req_migrate->retry(); - die "Virtual Machine ".$domain->name." migrating to local. Retry.\n"; - } $self->_migrate_base($domain, $node, $uid, $request) if $domain->id_base; if ($domain->is_active) { diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 7bd601c1f..48ff9cf03 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -993,7 +993,7 @@ sub prepare_base($self, @args) { my @base_img = $self->_do_prepare_base($with_cd, $overwrite, $request); die "Error: No information files returned from prepare_base" - if !scalar(@base_img); + if !scalar(@base_img) && $self->list_volumes() ; my $pending_post = 0; if ($request) { @@ -1062,7 +1062,8 @@ Prepares the virtual machine as a base: sub _do_prepare_base($self, $with_cd, $overwrite, $req=undef) { my @base_img; - for my $volume ($self->list_volumes_info()) { + my @vols_info = $self->list_volumes_info(); + for my $volume (@vols_info) { next if !$volume->file; my $base_file = $volume->base_filename; next if !$base_file || $base_file =~ /\.iso$/; @@ -1076,7 +1077,6 @@ sub _do_prepare_base($self, $with_cd, $overwrite, $req=undef) { .$self->_vm->name; } } - my @vols_info = $self->list_volumes_info(); my %dupe; for my $volume (@vols_info) { my $target = $volume->info->{target}; @@ -2080,7 +2080,9 @@ sub open($class, @args) { } sub _check_proper_id_vm($self, $id, $id_vm) { - my @instances = ({ id_vm => $$id_vm } , $self->list_instances($id) ); + # my @instances = ({ id_vm => $$id_vm } , $self->list_instances($id) ); + my @instances = $self->list_instances($id); + warn Dumper(\@instances); for my $instance ( @instances ) { my $vm; eval { @@ -5565,7 +5567,7 @@ sub rsync($self, @args) { next if _check_stat($file, $vm_local, $node); my $msg = $self->_msg_log_rsync($file, $node, "rsync", $request); - $request->status("syncing") if $request; + $request->status("working") if $request; $request->error("Syncing $file") if $request; $request->error($msg) if $request && $DEBUG_RSYNC; warn "$msg\n" if $DEBUG_RSYNC; @@ -5616,7 +5618,7 @@ sub _rsync_volumes_back($self, $node, $request=undef) { my $msg = $self->_msg_log_rsync($file, $node, "rsync_back", $request); - $request->status("syncing") if $request; + $request->status("working") if $request; $request->error($msg) if $request; warn "$msg\n" if $DEBUG_RSYNC; $rsync->exec(src => 'root@'.$node->host.":".$file ,dest => $file ); diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index 9b1c59ccc..6e2727045 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -303,6 +303,8 @@ our %CMD_VALIDATE = ( ,spinoff => \&_validate_compact ,prepare_base => \&_validate_prepare_base ,remove_base => \&_validate_remove_base + ,migrate => \&_validate_migrate + ,set_base_vm=> \&_validate_set_base_vm ); sub _init_connector { @@ -1554,23 +1556,47 @@ sub set_base_vm { my $id_vm = $args->{id_vm}; $id_vm = $args->{id_node} if exists $args->{id_node} && $args->{id_node}; - _set_base_vm_requirement($req, $id_vm, $domain); - $domain->_set_base_vm_db($id_vm, $args->{value}, $req->id); return $req; } -sub _set_base_vm_requirement($req, $id_vm, $domain) { +sub _validate_migrate($req) { + warn Dumper($req->args); + return if $req->defined_arg('value') && $req->defined_arg('value')==0; + + my $domain = Ravada::Front::Domain->open($req->args('id_domain')); + my $id_vm_local = $domain->_id_vm_local(); + die "Error: node local not found for ".$domain->type + if !defined $id_vm_local; + + my $id_node = $req->args('id_node'); + + return if $id_node==$id_vm_local || $domain->_data('id_vm')==$id_vm_local; + + my $req_local = Ravada::Request->migrate( + uid => Ravada::Utils::user_daemon->id + ,id_domain => $domain->id + ,id_node => $id_vm_local + ); + $req->after_request_ok($req_local->id); +} + + +sub _validate_set_base_vm($req) { return if $req->defined_arg('value') && $req->defined_arg('value')==0; + my $domain = Ravada::Front::Domain->open($req->args('id_domain')); my $id_vm_local = $domain->_id_vm_local(); die "Error: node local not found for ".$domain->type if !defined $id_vm_local; + my $id_vm = $req->defined_arg('id_vm'); + $id_vm = $req->defined_arg('id_node') if !defined $id_vm; return if $id_vm == $id_vm_local; my $bases_vm = $domain->_bases_vm(); + warn Dumper([$id_vm, $bases_vm]); return if $bases_vm->{$id_vm_local}; diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index 9be1eff47..b32d3566b 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -212,6 +212,8 @@ sub open { } my $force = delete $args{force}; + confess "Error: wrong id '$args{id}' " if $args{id} && $args{id} !~ /^\d+$/; + confess "Error: undefind id in ".Dumper(\%args) if !$args{id}; my $class=ref($proto) || $proto; diff --git a/t/kvm/n30_nodes_front.t b/t/kvm/n30_nodes_front.t index b863b36f4..224f6ef7d 100644 --- a/t/kvm/n30_nodes_front.t +++ b/t/kvm/n30_nodes_front.t @@ -138,9 +138,9 @@ sub test_list_remote($node, $migrate=0) { my $base = create_domain($vm); $base->prepare_base(user_admin); - $base->set_base_vm(vm => $node, user => user_admin); my $domain = $base->clone(name => new_domain_name, user => user_admin); + $base->set_base_vm(vm => $node, user => user_admin); $domain->migrate($node); is($domain->_vm->host, $node->host); From 06a1c1a45f1cd30251d77cbd997572b0669689c8 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Tue, 7 Apr 2026 15:05:06 +0200 Subject: [PATCH 074/141] wip: test nodes reqs dependencies --- t/nodes/00_req.t | 115 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 t/nodes/00_req.t diff --git a/t/nodes/00_req.t b/t/nodes/00_req.t new file mode 100644 index 000000000..c4beef3d3 --- /dev/null +++ b/t/nodes/00_req.t @@ -0,0 +1,115 @@ +use warnings; +use strict; + +use utf8; +use Carp qw(confess); +use Data::Dumper; +use Digest::MD5; +use Test::More; + +use lib 't/lib'; +use Test::Ravada; + +no warnings "experimental::signatures"; +use feature qw(signatures); + +my $N_IP = 2; +############################################################################### + +sub _create_remote_node($vm_name) { + my %config = ( + name => new_domain_name() + ,host => '192.168.18.'.$N_IP++ + ); + + my $vm = rvd_back->search_vm($vm_name); + + my $node = $vm->new(%config); + return $node; +} + +sub test_req_migrate($vm, $node1, $node2) { + my $domain = create_domain($vm); + $domain->_data('id_vm' => $node1->id); + my $req = Ravada::Request->migrate( + uid => user_admin->id + ,id_domain => $domain->id + ,id_node => $node2->id + ); + ok($req->after_request_ok()); + my $id_req_prev = $req->after_request_ok(); + ok($id_req_prev); + _mock_fail($id_req_prev); + + wait_request(debug => 1, check_error => 0); + is($req->status(),'done'); + is($req->error,'failure'); + + $domain->_remove_domain_data_db(); +} + +sub test_req_prepare_base($vm, $node1, $node2) { + my $domain = create_domain($vm); + $domain->_data('id_vm' => $node1->id); + my $req = Ravada::Request->set_base_vm( + uid => user_admin->id + ,id_domain => $domain->id + ,id_vm => $node2->id + ); + my $id_req_prev = $req->after_request_ok(); + ok($id_req_prev) or exit; + + _mock_fail($id_req_prev); + + wait_request(debug => 1, check_error => 0); + is($req->status(),'done'); + is($req->error,'failure'); + $domain->_remove_domain_data_db(); +} + +sub _mock_fail($id_req) { + my $sth = connector->dbh->prepare( + "UPDATE requests set status='done',error='failure' " + ." WHERE id = ?" + ); + $sth->execute($id_req); +} + + +############################################################################### + +init(); +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 ''); + my $REMOTE_CONFIG = remote_config($vm_name); + if (!keys %$REMOTE_CONFIG) { + my $msg = "skipped, missing the remote configuration for $vm_name in the file " + .$Test::Ravada::FILE_CONFIG_REMOTE; + diag($msg); + skip($msg,10); + } + + diag($msg) if !$vm; + skip($msg,10) if !$vm; + + diag("Testing remote node in $vm_name"); + + my $node1 = _create_remote_node($vm_name); + my $node2 = _create_remote_node($vm_name); + + test_req_migrate($vm, $node1, $node2); + test_req_prepare_base($vm, $node1, $node2); + } +} + +END: { + end(); + done_testing(); +} + + From 69d335ab999f7a4d9e9b8433d53d844bafdc0757 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 8 Apr 2026 15:18:32 +0200 Subject: [PATCH 075/141] wip: stack request requirements --- lib/Ravada.pm | 4 ++-- lib/Ravada/Request.pm | 54 +++++++++++++++++++++++++++++++++++-------- t/nodes/00_req.t | 31 +++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 12 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index aab530d12..09ef2a8c7 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -2939,8 +2939,8 @@ sub _upgrade_tables { $self->_upgrade_table('requests','pid','int(11) DEFAULT NULL'); $self->_upgrade_table('requests','start_time','int(11) DEFAULT NULL'); $self->_upgrade_table('requests','output','text DEFAULT NULL'); - $self->_upgrade_table('requests','after_request','int(11) DEFAULT NULL'); - $self->_upgrade_table('requests','after_request_ok','int(11) DEFAULT NULL'); + $self->_upgrade_table('requests','after_request','varchar(80) DEFAULT NULL'); + $self->_upgrade_table('requests','after_request_ok','varchar(80) DEFAULT NULL'); $self->_upgrade_table('requests','at_time','int(11) DEFAULT NULL'); $self->_upgrade_table('requests','run_time','float DEFAULT NULL'); diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index 6e2727045..a07dcec6d 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -9,7 +9,7 @@ Ravada::Request - Requests library for Ravada =cut -use Carp qw(confess cluck); +use Carp qw(confess cluck carp); use Data::Dumper; use Hash::Util qw(lock_hash); use JSON::XS; @@ -137,6 +137,7 @@ our %VALID_ARG = ( ,migrate => { uid => 1, id_node => 1, id_domain => 1, start => 2, remote_ip => 2 ,shutdown => 2, shutdown_timeout => 2 } + ,post_migrate => { uid => 1, id_node => 1, id_domain => 1 } ,compact => { uid => 1, id_domain => 1 , keep_backup => 2 } ,purge => { uid => 1, id_domain => 1 } ,backup => { uid => 1, id_domain => 1, compress => 2} @@ -1562,10 +1563,24 @@ sub set_base_vm { } sub _validate_migrate($req) { - warn Dumper($req->args); return if $req->defined_arg('value') && $req->defined_arg('value')==0; my $domain = Ravada::Front::Domain->open($req->args('id_domain')); + my $req_post = Ravada::Request->post_migrate( + uid => $req->args('uid') + ,id_domain => $domain->id + ,id_node => $req->args('id_node') + ,after_request => $req->id + ); + + if ($domain->_data('status') ne 'shutdown') { + my $req_shutdown = Ravada::Request->shutdown_domain( + uid => $req->args('uid') + ,id_domain => $domain->id + ,_force => 1 + ); + $req->after_request_ok($req_shutdown->id); + } my $id_vm_local = $domain->_id_vm_local(); die "Error: node local not found for ".$domain->type if !defined $id_vm_local; @@ -1596,7 +1611,6 @@ sub _validate_set_base_vm($req) { return if $id_vm == $id_vm_local; my $bases_vm = $domain->_bases_vm(); - warn Dumper([$id_vm, $bases_vm]); return if $bases_vm->{$id_vm_local}; @@ -1605,7 +1619,7 @@ sub _validate_set_base_vm($req) { ,id_domain => $domain->id ,id_vm => $id_vm_local ); - $req->after_request($req_local->id); + $req->after_request_ok($req_local->id); } =head2 remove_base_vm @@ -1972,13 +1986,27 @@ sub requirements_done($self) { $ok = 1 if !$req || $req->status eq 'done'; } if ($after_request_ok) { - $ok = 0; - my $req = Ravada::Request->open($self->after_request_ok); - if ($req->status eq 'done' && $req->error ) { - $self->status('done'); - $self->error($req->error); + my $ids = $after_request_ok; + if ( $ids =~ /^\[/ ) { + $ids = decode_json($after_request_ok) + } else { + $ids = [ $ids ]; + } + + my $fail = 0; + for my $id (@$ids) { + my $req = Ravada::Request->open($id); + if ($req->status eq 'done' && $req->error ) { + $self->status('done'); + $self->error($req->error); + } + $fail++ if $req->status eq 'done' && ( defined $req->error && $req->error ); + } + if ($fail) { + $ok = 0; + } else { + $ok = 1; } - $ok = 1 if $req->status eq 'done' && ( !defined $req->error || $req->error eq '' ); } return $ok; } @@ -2064,6 +2092,12 @@ sub _data($self, $field, $value=undef) { confess "ERROR: field $field is read only" if $FIELD_RO{$field}; + if ($field =~ /^after_request/) { + my $prev_req_id = $self->{_data}->{$field}; + $value = "[$prev_req_id,$value]" if $prev_req_id; + + } + $self->{_data}->{$field} = $value; my $sth = $$CONNECTOR->dbh->prepare( "UPDATE requests set $field=?" diff --git a/t/nodes/00_req.t b/t/nodes/00_req.t index c4beef3d3..bf72888ee 100644 --- a/t/nodes/00_req.t +++ b/t/nodes/00_req.t @@ -5,6 +5,7 @@ use utf8; use Carp qw(confess); use Data::Dumper; use Digest::MD5; +use Mojo::JSON qw(decode_json); use Test::More; use lib 't/lib'; @@ -48,6 +49,35 @@ sub test_req_migrate($vm, $node1, $node2) { $domain->_remove_domain_data_db(); } +sub test_req_migrate_active($vm, $node1, $node2) { + my $domain = create_domain($vm); + $domain->_data('id_vm' => $node1->id); + $domain->_data('status' => 'active'); + my $req = Ravada::Request->migrate( + uid => user_admin->id + ,id_domain => $domain->id + ,id_node => $node2->id + ,shutdown => 1 + ); + ok($req->after_request_ok()); + my $id_req_prev = $req->after_request_ok(); + ok($id_req_prev) or return; + + my $ids = decode_json($id_req_prev); + + my ($req_prev_migrate,$req_prev_shutdown); + for my $id ( @$ids ) { + my $req_prev = Ravada::Request->open($id); + $req_prev_migrate = $req_prev if $req_prev->command eq 'migrate'; + $req_prev_shutdown = $req_prev if $req_prev->command eq 'shutdown'; + } + ok($req_prev_migrate) or exit; + ok($req_prev_shutdown) or exit; + + $domain->_remove_domain_data_db(); +} + + sub test_req_prepare_base($vm, $node1, $node2) { my $domain = create_domain($vm); $domain->_data('id_vm' => $node1->id); @@ -102,6 +132,7 @@ for my $vm_name (vm_names() ) { my $node1 = _create_remote_node($vm_name); my $node2 = _create_remote_node($vm_name); + test_req_migrate_active($vm, $node1, $node2); test_req_migrate($vm, $node1, $node2); test_req_prepare_base($vm, $node1, $node2); } From 609c5139c7729db7d435a3ddf4d3e2efd636a4ba Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 9 Apr 2026 11:50:29 +0200 Subject: [PATCH 076/141] wip: chain requirements, main node proper name --- lib/Ravada.pm | 19 +++++++++++ lib/Ravada/Domain.pm | 1 - lib/Ravada/Request.pm | 76 ++++++++++++++++++++++++++++--------------- lib/Ravada/VM.pm | 6 ++-- t/nodes/00_req.t | 36 +++++++++++++++----- 5 files changed, 99 insertions(+), 39 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index 09ef2a8c7..8c3dc94e1 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -6288,6 +6288,24 @@ sub _migrate_base($self, $domain, $id_node, $uid, $request) { die "Base ".$base->name." still not prepared in node $id_node. Retry\n"; } +sub _cmd_post_migrate($self, $request) { + + my $req_migrate; + my $ids = $request->after_request(); + + for my $id ( $ids ) { + $req_migrate = Ravada::Request->open($id); + last if $req_migrate->command eq 'migrate'; + $req_migrate = undef; + } + if ( $req_migrate->error ) { + my $domain_f = Ravada::Front::Domain->open($request->args('id_domain')); + my $node = Ravada::VM->open($request->args('id_node')); + my $domain = $node->search_domain($domain_f->name,1); + $domain->remove_instance(); + } +} + sub _cmd_migrate($self, $request) { my $uid = $request->args('uid'); my $id_domain = $request->args('id_domain') or die "ERROR: Missing id_domain"; @@ -6953,6 +6971,7 @@ sub _req_method { ,start_node => \&_cmd_start_node ,connect_node => \&_cmd_connect_node ,migrate => \&_cmd_migrate + ,post_migrate => \&_cmd_post_migrate ,rsync_back => \&_cmd_rsync_back #users diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 48ff9cf03..dc8416fe5 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -2082,7 +2082,6 @@ sub open($class, @args) { sub _check_proper_id_vm($self, $id, $id_vm) { # my @instances = ({ id_vm => $$id_vm } , $self->list_instances($id) ); my @instances = $self->list_instances($id); - warn Dumper(\@instances); for my $instance ( @instances ) { my $vm; eval { diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index a07dcec6d..521254db9 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -1979,36 +1979,40 @@ sub requirements_done($self) { my $ok = 0; if ($after_request) { - $ok = 0; - my $req; - eval { $req = Ravada::Request->open($self->after_request) }; - die $@ if $@ && $@!~ /I can't find|not found/i; - $ok = 1 if !$req || $req->status eq 'done'; + $ok = $self->_requirements_done_ids($after_request); + return 0 if !$ok; } if ($after_request_ok) { - my $ids = $after_request_ok; - if ( $ids =~ /^\[/ ) { - $ids = decode_json($after_request_ok) - } else { - $ids = [ $ids ]; - } + $ok = $self->_requirements_done_ids($after_request_ok, 1); + } + return $ok; +} + +sub _requirements_done_ids($self, $ids, $propagate=undef) { + + $ids = [ $ids ] unless ref($ids) eq 'ARRAY'; - my $fail = 0; - for my $id (@$ids) { - my $req = Ravada::Request->open($id); - if ($req->status eq 'done' && $req->error ) { + my $fail = 0; + for my $id (@$ids) { + next if !_req_exists($id); + my $req = Ravada::Request->open($id); + if ($req->status eq 'done' && $req->error ) { + if ($propagate) { $self->status('done'); $self->error($req->error); } - $fail++ if $req->status eq 'done' && ( defined $req->error && $req->error ); - } - if ($fail) { - $ok = 0; - } else { - $ok = 1; } + return 0 if $req->status() ne 'done'; } - return $ok; + return 1; +} + +sub _req_exists($id) { + my $sth = $$CONNECTOR->dbh->prepare("SELECT id FROM requests WHERE id=?"); + $sth->execute($id); + my ($ok) = $sth->fetchrow(); + return 1 if $ok; + return 0 if !$ok; } =head2 redo @@ -2078,9 +2082,16 @@ sub remove($status, %args) { } } +sub _push($value, $id) { + my $list = $value; + $list = [$list] if !ref($list); + push @$list,($id); + return $list; +} + sub _data($self, $field, $value=undef) { confess if $field eq 'after_request' && defined $value - && $value == $self->id; + && length($value) && $value == $self->id; if (defined $value && ( @@ -2094,11 +2105,11 @@ sub _data($self, $field, $value=undef) { if ($field =~ /^after_request/) { my $prev_req_id = $self->{_data}->{$field}; - $value = "[$prev_req_id,$value]" if $prev_req_id; - + $value = _push($prev_req_id, $value) if $prev_req_id; } - $self->{_data}->{$field} = $value; + $value = encode_json($value) if ref($value) eq 'ARRAY'; + my $sth = $$CONNECTOR->dbh->prepare( "UPDATE requests set $field=?" ." WHERE id=?" @@ -2106,7 +2117,7 @@ sub _data($self, $field, $value=undef) { $sth->execute($value, $self->id); $sth->finish; - return $value; + return $self->{_data}->{$field}; } return $self->{_data}->{$field} if exists $self->{_data}->{$field} && defined $self->{_data}->{$field}; @@ -2136,6 +2147,17 @@ sub _select_db($self) { return if !$row; + for my $key (keys %$row) { + my $value = $row->{$key}; + next if !defined $value || $value !~ /^[\[}]/; + my $value_decoded; + eval { + $value_decoded = decode_json($value); + }; + warn "Error decoding '$value' for request id=$row->{id} $@" if $@; + $row->{$key} = $value_decoded if defined $value_decoded; + } + return $row; } diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index b32d3566b..f98300ce4 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -802,8 +802,9 @@ sub name { return $self->_data('name') if defined $self->{_data}->{name}; - my ($ref) = ref($self) =~ /.*::(.*)/; - return ($ref or ref($self))."_".$self->host; + my ($out,$err) = $self->run_command('hostname'); + chomp $out; + return $out; } =head2 search_domain_by_id @@ -1224,6 +1225,7 @@ sub _insert_vm_db { ); my %args = @_; my $name = ( delete $args{name} or $self->name); + cluck $name if $name =~ /localhost/; my $host = ( delete $args{hostname} or $self->host ); my $public_ip = ( delete $args{public_ip} or '' ); delete $args{vm_type}; diff --git a/t/nodes/00_req.t b/t/nodes/00_req.t index bf72888ee..efc8e65f1 100644 --- a/t/nodes/00_req.t +++ b/t/nodes/00_req.t @@ -6,6 +6,7 @@ use Carp qw(confess); use Data::Dumper; use Digest::MD5; use Mojo::JSON qw(decode_json); +use Storable qw(dclone); use Test::More; use lib 't/lib'; @@ -42,11 +43,11 @@ sub test_req_migrate($vm, $node1, $node2) { ok($id_req_prev); _mock_fail($id_req_prev); - wait_request(debug => 1, check_error => 0); + wait_request(debug => 0, check_error => 0); is($req->status(),'done'); is($req->error,'failure'); - $domain->_remove_domain_data_db(); + Ravada::Domain::_remove_domain_data_db($domain->id); } sub test_req_migrate_active($vm, $node1, $node2) { @@ -60,13 +61,11 @@ sub test_req_migrate_active($vm, $node1, $node2) { ,shutdown => 1 ); ok($req->after_request_ok()); - my $id_req_prev = $req->after_request_ok(); + my $id_req_prev = dclone($req->after_request_ok()); ok($id_req_prev) or return; - my $ids = decode_json($id_req_prev); - my ($req_prev_migrate,$req_prev_shutdown); - for my $id ( @$ids ) { + for my $id ( @$id_req_prev ) { my $req_prev = Ravada::Request->open($id); $req_prev_migrate = $req_prev if $req_prev->command eq 'migrate'; $req_prev_shutdown = $req_prev if $req_prev->command eq 'shutdown'; @@ -74,7 +73,23 @@ sub test_req_migrate_active($vm, $node1, $node2) { ok($req_prev_migrate) or exit; ok($req_prev_shutdown) or exit; - $domain->_remove_domain_data_db(); + $req->_data('after_request_ok' => 99); + delete $req->{_data}; + + my $new_ids = $id_req_prev; + push @$new_ids,(99); + is_deeply($req->after_request_ok(), $new_ids) + or die Dumper([$req->after_request_ok(), $new_ids]); + + $req->_data('after_request' => ''); + for ( 100 .. 103 ) { + $req->_data('after_request' => $_ ); + } + is_deeply($req->after_request(), ["100","101","102","103"]); + $req->_data('after_request' => ''); + $req->_data('after_request_ok' => ''); + + Ravada::Domain::_remove_domain_data_db($domain->id); } @@ -91,10 +106,10 @@ sub test_req_prepare_base($vm, $node1, $node2) { _mock_fail($id_req_prev); - wait_request(debug => 1, check_error => 0); + wait_request(debug => 0, check_error => 0); is($req->status(),'done'); is($req->error,'failure'); - $domain->_remove_domain_data_db(); + Ravada::Domain::_remove_domain_data_db($domain->id); } sub _mock_fail($id_req) { @@ -129,12 +144,15 @@ for my $vm_name (vm_names() ) { diag("Testing remote node in $vm_name"); + isnt($vm->name,'Void_localhost'); my $node1 = _create_remote_node($vm_name); my $node2 = _create_remote_node($vm_name); test_req_migrate_active($vm, $node1, $node2); test_req_migrate($vm, $node1, $node2); test_req_prepare_base($vm, $node1, $node2); + $node1->remove(); + $node2->remove(); } } From a28b8fb95c956d57fb623eb2f0046c972c0c4c63 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Fri, 10 Apr 2026 16:25:05 +0200 Subject: [PATCH 077/141] wip: chain set bases in nodes --- lib/Ravada/Domain.pm | 10 +++++--- lib/Ravada/Request.pm | 41 ++++++++++++++---------------- lib/Ravada/Volume/Class.pm | 11 ++++++++ lib/Ravada/Volume/QCOW2.pm | 2 +- t/nodes/00_req.t | 52 +++++++++++++++++++++++++++++++++++++- t/nodes/10_basic.t | 45 ++++++++++++++++++++++++--------- 6 files changed, 121 insertions(+), 40 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index dc8416fe5..028e319d0 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -359,6 +359,7 @@ sub _around_start($orig, $self, @arg) { for (1 .. 2) { eval { $self->_start_checks(%arg, enable_host_devices => $enable_host_devices) }; my $error = $@; + warn $error if $error; if ($error) { if ( $error =~/base file not found/ && !$self->_vm->is_local) { $self->_request_set_base(); @@ -589,7 +590,8 @@ sub _start_checks($self, @args) { # 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. $id_vm = $self->_data('id_vm') - if !$id_vm && defined $self->_data('id_vm'); + 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) { @@ -2080,8 +2082,7 @@ sub open($class, @args) { } sub _check_proper_id_vm($self, $id, $id_vm) { - # my @instances = ({ id_vm => $$id_vm } , $self->list_instances($id) ); - my @instances = $self->list_instances($id); + my @instances = ({ id_vm => $$id_vm } , $self->list_instances($id) ); for my $instance ( @instances ) { my $vm; eval { @@ -5647,7 +5648,8 @@ sub _pre_migrate($self, $node, $request = undef) { confess "ERROR: base id ".$self->id_base." not found." if !$base; if ( ! $self->_check_all_parents_in_node($node, $request) ) { - die "Warning: base not in node. Retry.\n"; + die "Warning: base of ".$self->name." , ".$base->name." not in node ".$node->id + .". Retry.\n"; } } diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index 521254db9..901ad4087 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -1581,20 +1581,20 @@ sub _validate_migrate($req) { ); $req->after_request_ok($req_shutdown->id); } - my $id_vm_local = $domain->_id_vm_local(); - die "Error: node local not found for ".$domain->type - if !defined $id_vm_local; - my $id_node = $req->args('id_node'); - return if $id_node==$id_vm_local || $domain->_data('id_vm')==$id_vm_local; + if ($domain->_data('id_base')) { + my $base = Ravada::Front::Domain->open($domain->_data('id_base')); + if (!$base->base_in_vm($id_node)) { + my $req_prev = Ravada::Request->set_base_vm( + uid => $req->args('uid') + ,id_domain => $base->id + ,id_vm => $id_node + ); + $req->_data('after_request_ok' => $req_prev->id); + } + } - my $req_local = Ravada::Request->migrate( - uid => Ravada::Utils::user_daemon->id - ,id_domain => $domain->id - ,id_node => $id_vm_local - ); - $req->after_request_ok($req_local->id); } @@ -1602,24 +1602,21 @@ sub _validate_set_base_vm($req) { return if $req->defined_arg('value') && $req->defined_arg('value')==0; my $domain = Ravada::Front::Domain->open($req->args('id_domain')); - my $id_vm_local = $domain->_id_vm_local(); - die "Error: node local not found for ".$domain->type - if !defined $id_vm_local; my $id_vm = $req->defined_arg('id_vm'); $id_vm = $req->defined_arg('id_node') if !defined $id_vm; - return if $id_vm == $id_vm_local; - my $bases_vm = $domain->_bases_vm(); + return if !$domain->id_base; - return if $bases_vm->{$id_vm_local}; + my $base = Ravada::Front::Domain->open($domain->id_base); + return if $base->base_in_vm($id_vm); - my $req_local = Ravada::Request->set_base_vm( + my $req_prev = Ravada::Request->set_base_vm( uid => Ravada::Utils::user_daemon->id - ,id_domain => $domain->id - ,id_vm => $id_vm_local + ,id_domain => $base->id + ,id_vm => $id_vm ); - $req->after_request_ok($req_local->id); + $req->after_request_ok($req_prev->id); } =head2 remove_base_vm @@ -2090,7 +2087,7 @@ sub _push($value, $id) { } sub _data($self, $field, $value=undef) { - confess if $field eq 'after_request' && defined $value + confess "Error: recursive requirement" if $field =~ /after_request/ && defined $value && length($value) && $value == $self->id; if (defined $value diff --git a/lib/Ravada/Volume/Class.pm b/lib/Ravada/Volume/Class.pm index 80d08b03e..ed496e453 100644 --- a/lib/Ravada/Volume/Class.pm +++ b/lib/Ravada/Volume/Class.pm @@ -153,5 +153,16 @@ sub _copy_sys($self, $dst, $mode=undef) { $self->_chmod($mode, $dst) if $mode; } +sub _mv_sys($self, $dst, $mode=undef) { + my $file = $self->file; + if ($self->vm) { + my ($out, $err) = $self->vm->run_command("mv",$file,$dst); + die $err if $err; + } else { + copy($file,$dst) or die "$! $file -> $dst"; + unlink $file or die "$! $file"; + } + $self->_chmod($mode, $dst) if $mode; +} 1; diff --git a/lib/Ravada/Volume/QCOW2.pm b/lib/Ravada/Volume/QCOW2.pm index f9532fb59..1d7e78b0a 100644 --- a/lib/Ravada/Volume/QCOW2.pm +++ b/lib/Ravada/Volume/QCOW2.pm @@ -36,7 +36,7 @@ sub prepare_base($self, $req=undef) { }; if ($format && $format eq 'qcow2') { - $self->_copy($base_img, '0400'); + $self->_copy_sys($base_img, '0400'); } else { $self->_convert($base_img, $req); } diff --git a/t/nodes/00_req.t b/t/nodes/00_req.t index efc8e65f1..44220d15b 100644 --- a/t/nodes/00_req.t +++ b/t/nodes/00_req.t @@ -61,7 +61,8 @@ sub test_req_migrate_active($vm, $node1, $node2) { ,shutdown => 1 ); ok($req->after_request_ok()); - my $id_req_prev = dclone($req->after_request_ok()); + my $id_req_prev = $req->after_request_ok(); + $id_req_prev = [$id_req_prev] if !ref($id_req_prev); ok($id_req_prev) or return; my ($req_prev_migrate,$req_prev_shutdown); @@ -120,10 +121,58 @@ sub _mock_fail($id_req) { $sth->execute($id_req); } +sub test_req_migrate_nested($vm, $node1) { + my $base1 = create_base($vm); + $base1->_data('is_base' => 1); + + my $base2 = create_base($vm); + $base2->_data('is_base' => 1); + $base2->_data('id_base' => $base1->id); + + my $base3 = create_base($vm); + $base2->_data('is_base' => 1); + $base3->_data('id_base' => $base2->id); + + my $clone = create_domain($vm); + $clone->_data('id_base' => $base3->id); + + my $req = Ravada::Request->migrate( + uid => user_admin->id + ,id_domain => $clone->id + ,id_node => $node1->id + ); + my $id_req_prev = $req->after_request_ok(); + ok($id_req_prev) or return; + my $req_prev = Ravada::Request->open($id_req_prev); + is($req_prev->id_domain, $clone->id_base); + is($req_prev->id_domain, $base3->id); + is($req_prev->command(), 'set_base_vm'); + + $id_req_prev = $req_prev->after_request_ok(); + ok($id_req_prev) or return; + isnt($id_req_prev, $req_prev->id) or exit; + $req_prev = Ravada::Request->open($id_req_prev); + is($req_prev->id_domain, $base2->id); + is($req_prev->id_domain, $base3->id_base); + is($req_prev->command(), 'set_base_vm'); + + $id_req_prev = $req_prev->after_request_ok(); + ok($id_req_prev) or return; + isnt($id_req_prev, $req_prev->id) or exit; + $req_prev = Ravada::Request->open($id_req_prev); + is($req_prev->id_domain, $base1->id); + is($req_prev->id_domain, $base2->id_base); + is($req_prev->command(), 'set_base_vm'); + + $id_req_prev = $req_prev->after_request_ok(); + ok(!$id_req_prev); + remove_domain($clone); +} ############################################################################### init(); +clean(); for my $vm_name (vm_names() ) { my $vm; eval { $vm = rvd_back->search_vm($vm_name) }; @@ -148,6 +197,7 @@ for my $vm_name (vm_names() ) { my $node1 = _create_remote_node($vm_name); my $node2 = _create_remote_node($vm_name); + test_req_migrate_nested($vm, $node1); test_req_migrate_active($vm, $node1, $node2); test_req_migrate($vm, $node1, $node2); test_req_prepare_base($vm, $node1, $node2); diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index 7914764c3..f034098ba 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -1350,6 +1350,7 @@ sub test_fill_memory($vm, $node, $migrate, $start=0) { ,id_domain => $base->id ); wait_request(); + ok($base->_id_base_in_vm($node->id)); my $master_free_memory = $vm->free_memory; my $node_free_memory = $node->free_memory; @@ -1377,7 +1378,6 @@ sub test_fill_memory($vm, $node, $migrate, $start=0) { like($req->error, qr/^(No free memory|$)/); is($req->status,'done'); push @clones,($clone_name); - diag($req->command." ".$req->status); my $clone = rvd_back->search_domain($clone_name) or last; ok($clone,"Expecting clone $clone_name") or exit; $created_in_node++ if $clone->_data('id_vm') == $node->id; @@ -1391,8 +1391,13 @@ sub test_fill_memory($vm, $node, $migrate, $start=0) { ,shutdown_timeout => 1 ) if $migrate; wait_request(debug => 0); - eval { $clone->start(user_admin) }; - $error = $@; + $clone = Ravada::Domain->open($clone->id); + my $req_start = Ravada::Request->start_domain( + uid => user_admin->id + ,id_domain => $clone->id + ); + wait_request(debug => 1, check_error => 0); + $error = $req_start->error; diag("error=$error");# if $error; like($error, qr/(^$|No free memory)/); exit if $error && $error !~ /No free memory/; @@ -1400,9 +1405,14 @@ sub test_fill_memory($vm, $node, $migrate, $start=0) { last if $error; $clone = Ravada::Domain->open($clone->id); - $nodes{$clone->_vm->name}++; + my $node_name = $clone->_vm->name; + $nodes{$node_name}++; last if $migrate && exists $nodes{$vm->name} && $nodes{$vm->name} > 2; + if (!$error && !$clone->is_active) { + die $clone->_vm->name." ".$clone->name if !$error && !$clone->is_active; + } + $memory *= 1.5; } ok($created_in_node,"Expecting some clones created in node ".$node->name) or exit; ok(exists $nodes{$vm->name},"Expecting some clones to node ".$vm->name." ".$vm->id) @@ -1637,7 +1647,7 @@ sub test_volumes_levels($domain, $level) { unlike($file,qr{--+},$domain->name) or exit; } is(scalar(@backings), $level, "Expecting ".$domain->name - ." : ".$vol->file." level $level\n".Dumper(\@backings)) or exit; + ." : ".$vol->file." level $level\n".Dumper(\@backings)) or confess; } } @@ -1674,6 +1684,7 @@ sub test_domain_volumes_levels($domain, $level) { sub test_nested_base($vm, $node, $levels=1) { my $base0 = create_domain($vm); + $base0->add_volume(swap => 1, size => 10 * 1024); $base0->add_volume(type => 'tmp', size => 10 * 1024); $base0->add_volume(type => 'data', size => 10 * 1024); @@ -1683,11 +1694,22 @@ sub test_nested_base($vm, $node, $levels=1) { my @bases = ( ); for my $n ( 1 .. $levels ) { diag("Cloning from ".$base1->name." level $n / $levels"); - $base1->prepare_base(user_admin) if !$base1->is_base; - $clone = $base1->clone( - name => new_domain_name - ,user => user_admin + if (!$base1->is_base) { + Ravada::Request->prepare_base( + uid => user_admin->id + ,id_domain => $base1->id + ); + wait_request(); + } + diag("Cloning from ".$base1->name." ".($base1->id_base or ' ')." level $n / $levels"); + my $name = new_domain_name(); + Ravada::Request->clone( + name => $name + ,uid => user_admin->id + ,id_domain => $base1->id ); + wait_request(); + $clone = $vm->search_domain($name); is($clone->id_base,$base1->id); push @bases,($base1); wait_request(); @@ -1713,6 +1735,7 @@ sub _test_migrate_nested($vm, $node, $bases, $clone, $levels) { ,id_domain => $clone->id ,uid => user_admin->id ,shutdown => 1 + ,_force => 1 ); for ( 1 .. $levels+3 ) { last if $req->status eq 'done'; @@ -2229,12 +2252,12 @@ for my $vm_name (vm_names() ) { start_node($node); + test_nested_base($vm, $node, 3); for my $volatile (1,0) { test_remove_base($vm, $node, $volatile); } test_change_clone($vm, $node); - test_fill_memory($vm, $node, 1); # migrate test_base_unset($vm,$node); test_removed_base_file($vm, $node); @@ -2263,8 +2286,6 @@ for my $vm_name (vm_names() ) { test_pc_other($vm,$node); - test_fill_memory($vm, $node, 1); # migrate - # test displays with no builtin added test_displays($vm, $node,1) if $tls; # test displays with only builtin From a3da33c378a5267c0d322a889f111e57634f56ba Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Fri, 10 Apr 2026 16:29:00 +0200 Subject: [PATCH 078/141] wip: move to create read only file --- lib/Ravada/Volume/Class.pm | 2 +- lib/Ravada/Volume/QCOW2.pm | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/Ravada/Volume/Class.pm b/lib/Ravada/Volume/Class.pm index ed496e453..32e40f8f2 100644 --- a/lib/Ravada/Volume/Class.pm +++ b/lib/Ravada/Volume/Class.pm @@ -153,7 +153,7 @@ sub _copy_sys($self, $dst, $mode=undef) { $self->_chmod($mode, $dst) if $mode; } -sub _mv_sys($self, $dst, $mode=undef) { +sub _move_sys($self, $dst, $mode=undef) { my $file = $self->file; if ($self->vm) { my ($out, $err) = $self->vm->run_command("mv",$file,$dst); diff --git a/lib/Ravada/Volume/QCOW2.pm b/lib/Ravada/Volume/QCOW2.pm index 1d7e78b0a..c60d146d7 100644 --- a/lib/Ravada/Volume/QCOW2.pm +++ b/lib/Ravada/Volume/QCOW2.pm @@ -37,6 +37,7 @@ sub prepare_base($self, $req=undef) { if ($format && $format eq 'qcow2') { $self->_copy_sys($base_img, '0400'); + $self->_move_sys($base_img, '0400'); } else { $self->_convert($base_img, $req); } From 4331dbf6b367f79aaccc505a1c553dcf1197a99e Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 13 Apr 2026 13:05:17 +0200 Subject: [PATCH 079/141] wip: cascade migrate parents and children --- lib/Ravada.pm | 2 +- lib/Ravada/Domain.pm | 31 +++++++-- lib/Ravada/Front.pm | 13 ++-- lib/Ravada/Request.pm | 95 ++++++++++++++++++++++++++- lib/Ravada/Volume/Class.pm | 2 +- public/js/ravada.js | 2 +- t/lib/Test/Ravada.pm | 37 +++++++++++ t/nodes/00_req.t | 115 +++++++++++++++++++++++++++++++-- t/nodes/10_basic.t | 29 ++++++++- templates/main/vm_base.html.ep | 10 ++- 10 files changed, 306 insertions(+), 30 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index 8c3dc94e1..5e5497fee 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -6302,7 +6302,7 @@ sub _cmd_post_migrate($self, $request) { my $domain_f = Ravada::Front::Domain->open($request->args('id_domain')); my $node = Ravada::VM->open($request->args('id_node')); my $domain = $node->search_domain($domain_f->name,1); - $domain->remove_instance(); + $domain->remove_instance($node->id); } } diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 028e319d0..75b0e4663 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -2498,7 +2498,7 @@ sub info($self, $user) { #} $info->{drivers} = $self->_load_drivers(); - $info->{bases} = $self->_bases_vm(); + $info->{bases} = $self->_bases_vm_info(); $info->{clones} = $self->_clones_vm(); $info->{ports} = [$self->list_ports()]; my @cdrom = (); @@ -5796,7 +5796,6 @@ sub set_base_vm($self, %args) { } } ($node2) = @nodes if !$node2; - $vm->_migrate_domains($node2) if $node2 && $migrate; if (!@nodes) { $self->_vm($vm); $self->_do_remove_base($user); @@ -6018,24 +6017,42 @@ sub _base_files_in_vm($self,$vm) { return 1; } -sub _bases_vm($self) { +sub _bases_vm_info($self, $only_enabled=0) { + return $self->_bases_vm($only_enabled,1); +} + +sub _bases_vm($self, $only_enabled=0, $info=0) { my $sth = $$CONNECTOR->dbh->prepare( "SELECT id, hostname FROM vms WHERE vm_type=?" ); $sth->execute($self->type); my %base; while (my ($id_vm, $hostname) = $sth->fetchrow) { - $base{$id_vm} = 0; + if ($info ) { + $base{$id_vm} = {enabled => 0}; + } else { + $base{$id_vm} = 0; + } } $sth->finish; for my $id_vm ( sort keys %base ) { $sth = $$CONNECTOR->dbh->prepare( - "SELECT enabled FROM bases_vm WHERE id_domain=? AND id_vm=?" + "SELECT enabled,id_request FROM bases_vm WHERE id_domain=? AND id_vm=?" ); $sth->execute($self->id, $id_vm); - while (my ($enabled) = $sth->fetchrow) { - $base{$id_vm} = $enabled; + while (my $row = $sth->fetchrow_hashref) { + if ($info) { + $base{$id_vm}=$row; + } else { + $base{$id_vm} = $row->{enabled}; + } + } + } + # delete disabled entries when checking for only enabled + if ($only_enabled) { + for my $id_vm (keys %base) { + delete $base{$id_vm} if !$base{$id_vm}; } } return \%base; diff --git a/lib/Ravada/Front.pm b/lib/Ravada/Front.pm index 62ca49231..728ffccfa 100644 --- a/lib/Ravada/Front.pm +++ b/lib/Ravada/Front.pm @@ -854,7 +854,11 @@ sub iso_file ($self, $id_vm, $uid) { return [] if $req->status ne 'done'; my $isos = []; - $isos = decode_json($req->output()) if $req->output; + eval { + $isos = decode_json($req->output()) + if $req->output && !ref($req->output()) + }; + warn $@." for request=".$req->id." ".Dumper($req->output) if $@; $self->_cache_store($key, $isos); @@ -1512,7 +1516,7 @@ sub list_network_interfaces($self, %args) { } return [] if $req->status ne 'done' || !length($req->output); - my $interfaces = decode_json($req->output()); + my $interfaces = $req->output; $self->{$cache_key} = $interfaces; return $interfaces; @@ -1814,7 +1818,7 @@ sub list_cpu_models($self, $uid, $id_domain) { return {} if $req->status ne 'done'; my $models= {}; - $models = decode_json($req->output()) if $req->output; + $models = $req->output() if $req->output; $self->_cache_store($key,$models); @@ -1858,7 +1862,8 @@ sub list_storage_pools($self, $uid, $id_vm, $active=undef) { return _filter_active($cache, $active) if $req->status ne 'done'; my $pools = []; - $pools = decode_json($req->output()) if $req->output; + + $pools = $req->output() if $req->output; $self->_cache_store($key,$pools) if scalar(@$pools); diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index 901ad4087..18fd1ce4b 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -306,6 +306,7 @@ our %CMD_VALIDATE = ( ,remove_base => \&_validate_remove_base ,migrate => \&_validate_migrate ,set_base_vm=> \&_validate_set_base_vm + ,remove_base_vm=> \&_validate_remove_base_vm ); sub _init_connector { @@ -853,7 +854,7 @@ sub _new_request { my $request; eval { $request = $self->open($self->{id}) }; - warn $@ if $@ && $@ !~ /I can't find id=/; + warn "Error in request=$self->{id} $@" if $@ && $@ !~ /I can't find id=/; return if !$request; $request->_validate(); $request->status('requested') if $request->status ne'done'; @@ -1581,8 +1582,22 @@ sub _validate_migrate($req) { ); $req->after_request_ok($req_shutdown->id); } + my $id_vm_local = $domain->_id_vm_local(); + die "Error: node local not found for ".$domain->type + if !defined $id_vm_local; + my $id_node = $req->args('id_node'); + unless ($id_node==$id_vm_local || $domain->_data('id_vm')==$id_vm_local) { + my $req_local = Ravada::Request->migrate( + uid => Ravada::Utils::user_daemon->id + ,id_domain => $domain->id + ,id_node => $id_vm_local + ,shutdown => 1 + ); + $req->after_request_ok($req_local->id); + } + if ($domain->_data('id_base')) { my $base = Ravada::Front::Domain->open($domain->_data('id_base')); if (!$base->base_in_vm($id_node)) { @@ -1599,15 +1614,29 @@ sub _validate_migrate($req) { sub _validate_set_base_vm($req) { - return if $req->defined_arg('value') && $req->defined_arg('value')==0; + + return _validate_remove_base_vm($req) + if $req->defined_arg('value') && $req->defined_arg('value')==0; my $domain = Ravada::Front::Domain->open($req->args('id_domain')); + my $id_vm_local = $domain->_id_vm_local(); + die "Error: node local not found for ".$domain->type + if !defined $id_vm_local; my $id_vm = $req->defined_arg('id_vm'); $id_vm = $req->defined_arg('id_node') if !defined $id_vm; - return if !$domain->id_base; + my $bases_vm = $domain->_bases_vm(); + if ( $id_vm != $id_vm_local && !$bases_vm->{$id_vm_local}) { + my $req_local = Ravada::Request->set_base_vm( + uid => Ravada::Utils::user_daemon->id + ,id_domain => $domain->id + ,id_vm => $id_vm_local + ); + $req->after_request_ok($req_local->id); + } + return if !$domain->id_base; my $base = Ravada::Front::Domain->open($domain->id_base); return if $base->base_in_vm($id_vm); @@ -1642,6 +1671,66 @@ sub remove_base_vm { } +sub _node_is_active($id) { + my $sth = $$CONNECTOR->dbh->prepare( + "SELECT id FROM vms WHERE enabled=1 AND is_active=1 " + ." AND id=?" + ); + $sth->execute($id); + my ($found) = $sth->fetchrow; + return $found; +} + +sub _validate_remove_base_vm($req) { + + my $domain = Ravada::Front::Domain->open($req->args('id_domain')); + + my $id_vm = $req->defined_arg('id_vm'); + $id_vm = $req->defined_arg('id_node') if !defined $id_vm; + + my $bases_vm = $domain->_bases_vm(1); + my @other_vms; + for my $id_vm (keys %$bases_vm) { + push @other_vms,($id_vm) if _node_is_active($id_vm); + } + + if ( !@other_vms ) { + $req->error("Error: there are no other VMs to migrate clones when removing base " + .$domain->id." ".$domain->name); + $req->status('done'); + return; + } + + for my $clone ($domain->clones) { + + # migrate clones to other vms + if ( $clone->{id_vm} == $id_vm ) { + my $start = 0; + $start = 1 if $clone->{status} eq 'active'; + my $req_migrate = Ravada::Request->migrate( + uid => Ravada::Utils::user_daemon->id + ,id_domain => $clone->{id} + ,id_node => $other_vms[0] + ,shutdown => 1 + ,start => $start + ); + $req->after_request_ok($req_migrate->id); + } + + # remove child bases + if ( $clone->{is_base} ) { + + my $req_prev = Ravada::Request->remove_base_vm( + uid => Ravada::Utils::user_daemon->id + ,id_domain => $clone->{id} + ,id_vm => $id_vm + ); + $req->after_request_ok($req_prev->id); + } + } +} + + =head2 type diff --git a/lib/Ravada/Volume/Class.pm b/lib/Ravada/Volume/Class.pm index 32e40f8f2..fd7efc50c 100644 --- a/lib/Ravada/Volume/Class.pm +++ b/lib/Ravada/Volume/Class.pm @@ -38,7 +38,7 @@ sub _post_prepare_base($self, $base_file) { $self->_chmod(0o400,$base_file); $self->vm->refresh_storage_pools(); - $self->vm->remove_file($self->file); + $self->vm->remove_file($self->file) if $self->vm->file_exists($self->file); my @domain = (); @domain = ( domain => $self->domain) if $self->domain; diff --git a/public/js/ravada.js b/public/js/ravada.js index 9f1506e97..ceb043f5c 100644 --- a/public/js/ravada.js +++ b/public/js/ravada.js @@ -856,11 +856,11 @@ $http.get("/machine/public/"+machineId+"/"+value); }; $scope.set_base= function(vmId,machineId, value) { - $scope.showmachine.bases[vmId]=value; var url = 'set_base_vm'; if (value == 0 || !value) { url = 'remove_base_vm'; } + $scope.showmachine.bases[vmId]={'enabled': value, 'id_request': -1 }; $http.get("/machine/"+url+"/" +vmId+ "/" +machineId+".json") .then(function(response) { }); diff --git a/t/lib/Test/Ravada.pm b/t/lib/Test/Ravada.pm index 3d48a88e4..7a0ee9a09 100644 --- a/t/lib/Test/Ravada.pm +++ b/t/lib/Test/Ravada.pm @@ -77,6 +77,7 @@ create_domain remove_old_domains_req remove_domain_and_clones_req remove_domain + remove_domain_db remove_volatile_clones mojo_init mojo_clean @@ -902,6 +903,42 @@ sub remove_domain(@bases) { } +sub remove_domain_db(@bases) { + + for my $base0 (@bases) { + confess if !defined $base0; + my $base = $base0; + + my $id = $base0->{id}; + $id = $base0->id if !defined $id; + + my $sth = $CONNECTOR->dbh->prepare( + "DELETE FROM requests " + ." WHERE id_domain=?"); + $sth->execute($id); + + $base = Ravada::Front::Domain->open($id) + unless ref($base) =~ /^Ravada::/; + + if (!defined $base) { + warn "I can't find base '$id'"; + next; + } + + for my $clone ($base->clones) { + my $d_clone = Ravada::Front::Domain->open($clone->{id}); + if ( $d_clone ) { + remove_domain_db($d_clone); + } else { + Ravada::Domain::_remove_domain_data_db($clone->{id}) + } + } + Ravada::Domain::_remove_domain_data_db($id) + } + +} + + sub remove_domain_and_clones_req($domain_data, $wait=1, $run_request=0) { my $domain; if (ref($domain_data) =~ /Ravada.*Domain/) { diff --git a/t/nodes/00_req.t b/t/nodes/00_req.t index 44220d15b..28d0cfc38 100644 --- a/t/nodes/00_req.t +++ b/t/nodes/00_req.t @@ -47,7 +47,7 @@ sub test_req_migrate($vm, $node1, $node2) { is($req->status(),'done'); is($req->error,'failure'); - Ravada::Domain::_remove_domain_data_db($domain->id); + remove_domain_db($domain); } sub test_req_migrate_active($vm, $node1, $node2) { @@ -74,10 +74,10 @@ sub test_req_migrate_active($vm, $node1, $node2) { ok($req_prev_migrate) or exit; ok($req_prev_shutdown) or exit; + my $new_ids = dclone($id_req_prev); $req->_data('after_request_ok' => 99); delete $req->{_data}; - my $new_ids = $id_req_prev; push @$new_ids,(99); is_deeply($req->after_request_ok(), $new_ids) or die Dumper([$req->after_request_ok(), $new_ids]); @@ -90,7 +90,7 @@ sub test_req_migrate_active($vm, $node1, $node2) { $req->_data('after_request' => ''); $req->_data('after_request_ok' => ''); - Ravada::Domain::_remove_domain_data_db($domain->id); + remove_domain_db($domain); } @@ -103,14 +103,14 @@ sub test_req_prepare_base($vm, $node1, $node2) { ,id_vm => $node2->id ); my $id_req_prev = $req->after_request_ok(); - ok($id_req_prev) or exit; + ok($id_req_prev) or die Dumper([$domain->id, $req->id]); _mock_fail($id_req_prev); wait_request(debug => 0, check_error => 0); is($req->status(),'done'); is($req->error,'failure'); - Ravada::Domain::_remove_domain_data_db($domain->id); + remove_domain_db($domain); } sub _mock_fail($id_req) { @@ -166,9 +166,109 @@ sub test_req_migrate_nested($vm, $node1) { $id_req_prev = $req_prev->after_request_ok(); ok(!$id_req_prev); - remove_domain($clone); + remove_domain_db($clone); } +sub test_req_prepare_nested($vm, $node1) { + my $base1 = create_base($vm); + $base1->_data('is_base' => 1); + + my $base2 = create_base($vm); + $base2->_data('is_base' => 1); + $base2->_data('id_base' => $base1->id); + + my $base3 = create_base($vm); + $base2->_data('is_base' => 1); + $base3->_data('id_base' => $base2->id); + + my $clone = create_domain($vm); + $clone->_data('id_base' => $base3->id); + + my $req = Ravada::Request->set_base_vm( + uid => user_admin->id + ,id_domain => $clone->id + ,id_vm => $node1->id + ); + my $id_req_prev = $req->after_request_ok(); + ok($id_req_prev) or return; + + #First prepare clone on node vm local + my $req_prev = Ravada::Request->open($id_req_prev->[0]); + is($req_prev->id_domain, $clone->id) or exit; + is($req_prev->args('id_vm'), $vm->id); + is($req_prev->command(), 'set_base_vm'); + + #Then prepare base on node + $req_prev = Ravada::Request->open($id_req_prev->[1]); + is($req_prev->id_domain, $base3->id) or exit; + is($req_prev->args('id_vm'), $node1->id); + is($req_prev->command(), 'set_base_vm'); + + # Chain prepare base requirements + $id_req_prev = $req_prev->after_request_ok(); + ok($id_req_prev) or die; + isnt($id_req_prev, $req_prev->id) or exit; + $req_prev = Ravada::Request->open($id_req_prev); + is($req_prev->id_domain, $base2->id); + is($req_prev->id_domain, $base3->id_base); + is($req_prev->command(), 'set_base_vm'); + + $id_req_prev = $req_prev->after_request_ok(); + ok($id_req_prev) or return; + isnt($id_req_prev, $req_prev->id) or exit; + $req_prev = Ravada::Request->open($id_req_prev); + is($req_prev->id_domain, $base1->id); + is($req_prev->id_domain, $base2->id_base); + is($req_prev->command(), 'set_base_vm'); + + $id_req_prev = $req_prev->after_request_ok(); + ok(!$id_req_prev); + + for my $base ( $base1, $base2, $base3, $clone) { + $base->_set_base_vm_db( $node1->id,1); + for ( 1 .. 2 ) { + my $clone2 = create_domain($vm); + $clone2->_data('id_base' => $base->id); + $clone2->_data('id_vm' => $node1->id); + } + } + test_req_remove_base_nested($vm, $node1, $clone, $base3, $base2, $base1); + remove_domain_db($base1); +} + +sub test_req_remove_base_nested($vm, $node1, $clone, $base3, $base2, $base1) { + my $req = Ravada::Request->remove_base_vm( + uid => user_admin->id + ,id_domain => $base1->id + ,id_vm => $node1->id + ); + test_remove_requirements($base1, $req); +} + +sub test_remove_requirements($base, $req) { + diag("testing remove requirements for ".$base->id." ".$base->name); + my @clones = $base->clones; + return if !@clones; + + my $id_req_prev = $req->after_request_ok(); + ok($id_req_prev) or die "Expecting requirements for " + .$req->id." ".$req->command." ".Dumper($req->args); + $id_req_prev=[$id_req_prev] if !ref($id_req_prev); + for my $clone ( @clones ) { + my $req_prev; + for my $id (@$id_req_prev) { + $req_prev = Ravada::Request->open($id); + last if $req_prev->id_domain == $clone->{id}; + } + ok($req_prev, "Expecting req for clone $clone->{id}") or die ; + if ($clone->{is_base}) { + my $base2 = Ravada::Front::Domain->open($clone->{id}); + test_remove_requirements($base2, $req_prev); + } + } +} + + ############################################################################### init(); @@ -197,10 +297,11 @@ for my $vm_name (vm_names() ) { my $node1 = _create_remote_node($vm_name); my $node2 = _create_remote_node($vm_name); + test_req_prepare_nested($vm, $node1); test_req_migrate_nested($vm, $node1); + test_req_prepare_base($vm, $node1, $node2); test_req_migrate_active($vm, $node1, $node2); test_req_migrate($vm, $node1, $node2); - test_req_prepare_base($vm, $node1, $node2); $node1->remove(); $node2->remove(); } diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index f034098ba..fae0aac14 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -1719,7 +1719,8 @@ sub test_nested_base($vm, $node, $levels=1) { } _test_migrate_nested($vm, $node, \@bases, $clone, $levels); - $base0->remove_base_vm(vm => $node, user => user_admin); + _migrate_fast($clone, $vm); + _remove_base_vm($base0, $node); _test_migrate_nested($vm, $node, \@bases, $clone, $levels); my ($file) = $base0->list_files_base; $node->remove_file($file); @@ -1729,14 +1730,36 @@ sub test_nested_base($vm, $node, $levels=1) { } } +sub _migrate_fast($domain, $node) { + Ravada::Request->migrate( + uid => user_admin->id + ,id_domain => $domain->id + ,id_node => $node->id + ,shutdown => 1 + ); + wait_request(debug => 1); +} + +sub _remove_base_vm($base,$node) { + diag("Remove base from ".$node->name." for ".$base->name); + Ravada::Request->remove_base_vm( + uid => user_admin->id + ,id_domain => $base->id + ,id_vm => $node->id + ); + wait_request(debug => 1); +} + sub _test_migrate_nested($vm, $node, $bases, $clone, $levels) { + delete_request('migrate','set_base_vm'); + diag("migrating clone ".$clone->name." to ".$node->name." ".$node->id); my $req = Ravada::Request->migrate( id_node => $node->id ,id_domain => $clone->id ,uid => user_admin->id ,shutdown => 1 - ,_force => 1 ); + warn Dumper([$req->id,$req->after_request_ok()]); for ( 1 .. $levels+3 ) { last if $req->status eq 'done'; wait_request(debug => 0, check_error => 0); @@ -1748,7 +1771,7 @@ sub _test_migrate_nested($vm, $node, $bases, $clone, $levels) { is($base->base_in_vm($node->id),1); } my $clone2 = Ravada::Domain->open($clone->id); - is($clone2->_vm->id,$node->id) or exit; + is($clone2->_vm->id,$node->id) or confess; eval { $clone2->start(user_admin) }; is(''.$@, '', $clone2->name) or exit; } diff --git a/templates/main/vm_base.html.ep b/templates/main/vm_base.html.ep index 2d905c6d8..919f1dd55 100644 --- a/templates/main/vm_base.html.ep +++ b/templates/main/vm_base.html.ep @@ -40,13 +40,17 @@

<%=l 'Nodes' %>

+{{showmachine.bases}}
- + "> + +
{{node.name}} From 816dd55879f578a2a5a2a60661d97b42b2e6fe8e Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 13 Apr 2026 17:00:46 +0200 Subject: [PATCH 080/141] wip: do not dupe remove base vm --- lib/Ravada/Domain.pm | 3 +-- lib/Ravada/Request.pm | 4 ++-- t/nodes/10_basic.t | 12 ++++++------ 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 75b0e4663..4d6153e0f 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -5648,8 +5648,7 @@ sub _pre_migrate($self, $node, $request = undef) { confess "ERROR: base id ".$self->id_base." not found." if !$base; if ( ! $self->_check_all_parents_in_node($node, $request) ) { - die "Warning: base of ".$self->name." , ".$base->name." not in node ".$node->id - .". Retry.\n"; + die "Error: base of ".$self->name." , ".$base->name." not in node ".$node->id." ".$node->name; } } diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index 18fd1ce4b..3f988d671 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -735,7 +735,7 @@ sub _duplicated_request($self=undef, $command=undef, $args=undef) { } confess "Error: missing command " if !$command; # delete $args_d->{uid} unless $command eq 'clone'; - delete $args_d->{uid} if $command =~ /(cleanup|refresh_vms|set_base_vm)/; + delete $args_d->{uid} if $command =~ /(cleanup|refresh_vms|set_base_vm|remove_base_vm)/; delete $args_d->{uid} if exists $args_d->{uid} && !defined $args_d->{uid}; delete $args_d->{at}; delete $args_d->{status}; @@ -1984,7 +1984,7 @@ sub done_recently($self, $seconds=60,$command=undef, $args=undef) { next if join(".",sort keys %$args_d) ne join(".",sort keys %$args_found_d); my $args_d_s = join(".",map { $args_d->{$_} } sort keys %$args_d); - my $args_found_s = join(".",map {$args_found_d->{$_} } sort keys %$args_found_d); + my $args_found_s = join(".",map {$args_found_d->{$_} or '' } sort keys %$args_found_d); next if $args_d_s ne $args_found_s; return Ravada::Request->open($id); diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index fae0aac14..00284fd77 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -1072,7 +1072,7 @@ sub test_duplicated_set_base_vm($vm, $node) { , at => time + 3 ); my $req4 = Ravada::Request->remove_base_vm(id_vm => $node->id - , uid => 2 + , uid => 1 , id_domain => $domain->id , at => time + 4 ); @@ -1721,6 +1721,8 @@ sub test_nested_base($vm, $node, $levels=1) { _test_migrate_nested($vm, $node, \@bases, $clone, $levels); _migrate_fast($clone, $vm); _remove_base_vm($base0, $node); + diag("###########################################"); + diag("test migrate nested again"); _test_migrate_nested($vm, $node, \@bases, $clone, $levels); my ($file) = $base0->list_files_base; $node->remove_file($file); @@ -2275,7 +2277,7 @@ for my $vm_name (vm_names() ) { start_node($node); - test_nested_base($vm, $node, 3); + test_duplicated_set_base_vm($vm, $node); for my $volatile (1,0) { test_remove_base($vm, $node, $volatile); } @@ -2319,10 +2321,8 @@ for my $vm_name (vm_names() ) { test_nat($vm, $node, 1); # also set deprecated localhost ip test_duplicated_set_base_vm($vm, $node); - if ($vm_name eq 'KVM') { - test_nested_base($vm, $node, 3); - test_nested_base($vm, $node); - } + test_nested_base($vm, $node, 3); + test_nested_base($vm, $node); test_removed_base_file($vm, $node); From 61ade05b306e4f7f29bd7bfbcd86c264fb125c33 Mon Sep 17 00:00:00 2001 From: Yago Diaz Date: Tue, 14 Apr 2026 09:57:26 +0200 Subject: [PATCH 081/141] ISO - Add min ram/disk requirements --- lib/Ravada.pm | 58 ++++++++++++++++++++++++++------------------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index cbc062a18..a2c9b737b 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -419,10 +419,12 @@ sub _update_isos { ,arch => 'x86_64' ,xml => 'noble-amd64.xml' ,xml_volume => 'focal_fossa64-volume.xml' - ,url => 'http://cdimage.ubuntu.com/ubuntu-mate/releases/24.04.*/release/ubuntu-mate-24.04.*-desktop-amd64.iso' + ,url => 'http://cdimage. + ubuntu.com/ubuntu-mate/releases/24.04.*/release/ubuntu-mate-24.04.*-desktop-amd64.iso' ,sha256_url => '$url/SHA256SUMS' ,options => { machine => 'pc-q35', bios => 'UEFI' } - ,min_ram => 3 + ,min_ram => 4 + ,min_disk_size => '20' }, mate_jammy=> { name => 'Ubuntu Mate 22.04 Jammy Jellyfish 64 bits' @@ -433,7 +435,8 @@ sub _update_isos { ,url => 'http://cdimage.ubuntu.com/ubuntu-mate/releases/22.04.*/release/ubuntu-mate-22.04.*-desktop-amd64.iso' ,sha256_url => '$url/SHA256SUMS' ,options => { machine => 'pc-q35', bios => 'UEFI' } - ,min_ram => 1 + ,min_ram => 4 + ,min_disk_size => '20' }, mate_focal_fossa => { @@ -445,7 +448,8 @@ sub _update_isos { ,url => 'http://cdimage.ubuntu.com/ubuntu-mate/releases/20.04.*/release/ubuntu-mate-20.04.*-desktop-amd64.iso' ,sha256_url => '$url/SHA256SUMS' ,options => { machine => 'pc-q35', bios => 'UEFI' } - ,min_ram => 1 + ,min_ram => 4 + ,min_disk_size => '20' }, mate_bionic => { name => 'Ubuntu Mate 18.04 Bionic 64 bits' @@ -455,7 +459,8 @@ sub _update_isos { ,xml_volume => 'bionic64-volume.xml' ,url => 'http://cdimage.ubuntu.com/ubuntu-mate/releases/18.04.*/release/ubuntu-mate-18.04.*-desktop-amd64.iso' ,sha256_url => '$url/SHA256SUMS' - ,min_ram => 1 + ,min_ram => 4 + ,min_disk_size => '20' }, mate_bionic_i386 => { name => 'Ubuntu Mate 18.04 Bionic 32 bits' @@ -465,7 +470,8 @@ sub _update_isos { ,xml_volume => 'bionic32-volume.xml' ,url => 'http://cdimage.ubuntu.com/ubuntu-mate/releases/18.04.*/release/ubuntu-mate-18.04.*-desktop-i386.iso' ,sha256_url => '$url/SHA256SUMS' - ,min_ram => 1 + ,min_ram => 4 + ,min_disk_size => '20' }, ,focal_fossa=> { name => 'Ubuntu 20.04 Focal Fossa' @@ -476,8 +482,8 @@ sub _update_isos { ,url => 'http://releases.ubuntu.com/20.04/' ,file_re => '^ubuntu-20.04.*-desktop-amd64.iso' ,sha256_url => '$url/SHA256SUMS' - ,min_disk_size => '9' - ,min_ram => 1 + ,min_disk_size => '25' + ,min_ram => 4 ,options => { machine => 'pc-q35', bios => 'UEFI' } ,arch => 'x86_64' } @@ -490,7 +496,7 @@ sub _update_isos { ,url => 'http://releases.ubuntu.com/22.04/' ,file_re => '^ubuntu-22.04.*-desktop-amd64.iso' ,sha256_url => '$url/SHA256SUMS' - ,min_disk_size => '14' + ,min_disk_size => '25' ,min_ram => 4 ,options => { machine => 'pc-q35', bios => 'UEFI' } ,arch => 'x86_64' @@ -505,7 +511,7 @@ sub _update_isos { ,url => 'http://releases.ubuntu.com/24.04/' ,file_re => '^ubuntu-24.04.*-desktop-amd64.iso' ,sha256_url => '$url/SHA256SUMS' - ,min_disk_size => '14' + ,min_disk_size => '25' ,min_ram => 4 ,options => { machine => 'pc-q35', bios => 'UEFI' } ,arch => 'x86_64' @@ -599,8 +605,8 @@ sub _update_isos { ,file_re => 'kubuntu-24.04.*-desktop-amd64.iso' ,rename_file => 'kubuntu_noble.iso' ,options => { machine => 'pc-q35', bios => 'UEFI' } - ,min_ram => 3 - ,min_disk_size => 11 + ,min_ram => 4 + ,min_disk_size => '25' } ,kubuntu_64_jammy => { @@ -614,8 +620,8 @@ sub _update_isos { ,file_re => 'kubuntu-22.04.*-desktop-amd64.iso' ,rename_file => 'kubuntu_jammy.iso' ,options => { machine => 'pc-q35', bios => 'UEFI' } - ,min_ram => 3 - ,min_disk_size => 11 + ,min_ram => 4 + ,min_disk_size => '25' } ,kubuntu_64_focal_fossa => { name => 'Kubuntu 20.04 Focal Fossa 64 bits' @@ -628,7 +634,8 @@ sub _update_isos { ,file_re => 'kubuntu-20.04.*-desktop-amd64.iso' ,rename_file => 'kubuntu_focal_fossa_64.iso' ,options => { machine => 'pc-q35', bios => 'UEFI' } - ,min_ram => 1 + ,min_ram => 4 + ,min_disk_size => '25' } ,suse_15 => { name => "openSUSE Leap 15" @@ -636,7 +643,7 @@ sub _update_isos { ,arch => 'x86_64' ,xml => 'bionic-amd64.xml' ,xml_volume => 'bionic64-volume.xml' - ,url => 'https://download.opensuse.org/distribution/leap/15.4/iso/' + ,url => 'https://download.opensuse.org/distribution/leap/15.6/iso/' ,file_re => 'openSUSE-Leap-15.\d-NET-x86_64-Current.iso' } @@ -646,8 +653,8 @@ sub _update_isos { ,arch => 'x86_64' ,xml => 'bionic-amd64.xml' ,xml_volume => 'bionic64-volume.xml' - ,url => 'https://download.opensuse.org/distribution/leap/16.0/iso/' - ,file_re => 'openSUSE-Leap-16.\d-NET-x86_64-Current.iso' + ,url => 'https://download.opensuse.org/distribution/leap/16.0/installer/iso/' + ,file_re => 'agama-installer\.x86_64-.*-Leap_16\.0-Build.*\.iso' } ,xubuntu_noble => { @@ -659,6 +666,8 @@ sub _update_isos { ,url => 'https://ftp.lysator.liu.se/ubuntu-dvd/xubuntu/releases/24.04.*/release/' ,file_re => 'xubuntu.*desktop.*.iso' ,options => { machine => 'pc-q35', bios => 'UEFI' } + ,min_ram => 2 + ,min_disk_size => '20' } ,xubuntu_bionic => { name => 'Xubuntu 18.04 Bionic Beaver 32 bits' @@ -670,6 +679,8 @@ sub _update_isos { ,url => 'http://archive.ubuntu.com/ubuntu/dists/bionic/main/installer-i386/current/images/netboot/' ,file_re => 'mini.iso' ,rename_file => 'xubuntu_bionic_32.iso' + ,min_ram => 2 + ,min_disk_size => '20' } ,lubuntu_noble => { name => 'Lubuntu 24.04 Noble Nombat' @@ -680,6 +691,7 @@ sub _update_isos { ,xml => 'yakkety64-amd64.xml' ,xml_volume => 'yakkety64-volume.xml' ,min_disk_size => '10' + ,min_ram => 1 ,arch => 'x86_64' ,options => { machine => 'pc-q35', bios => 'UEFI' } } @@ -844,16 +856,6 @@ sub _update_isos { ,min_disk_size => '10' ,options => { machine => 'pc-q35'} } - ,devuan_excalibur_i386=> { - name =>'Devuan 13 Excalibur 32 bits' - ,description => 'Devuan Excalibur Desktop Live (i386)' - ,arch => 'i686' - ,url => 'http://tw1.mirror.blendbyte.net/devuan-cd/devuan_excalibur/desktop-live/' - ,file_re => 'devuan_excalibur_.*_i386_desktop-live.iso' - ,xml => 'jessie-i386.xml' - ,xml_volume => 'jessie-volume.xml' - ,min_disk_size => '10' - } ,parrot_xfce_amd64 => { name => 'Parrot Home Edition XFCE 7.1' ,description => 'Parrot Home Edition XFCE 7.1 64 Bits' From f0c2d6c649497c409043aedf56b9c8f14eb25c3f Mon Sep 17 00:00:00 2001 From: Yago Diaz Date: Tue, 14 Apr 2026 10:14:39 +0200 Subject: [PATCH 082/141] Fix error link --- lib/Ravada.pm | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index a2c9b737b..8b4a41e61 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -419,8 +419,7 @@ sub _update_isos { ,arch => 'x86_64' ,xml => 'noble-amd64.xml' ,xml_volume => 'focal_fossa64-volume.xml' - ,url => 'http://cdimage. - ubuntu.com/ubuntu-mate/releases/24.04.*/release/ubuntu-mate-24.04.*-desktop-amd64.iso' + ,url => 'http://cdimage.ubuntu.com/ubuntu-mate/releases/24.04.*/release/ubuntu-mate-24.04.*-desktop-amd64.iso' ,sha256_url => '$url/SHA256SUMS' ,options => { machine => 'pc-q35', bios => 'UEFI' } ,min_ram => 4 From 29e77ef208df49f96d985c15776e6c9459c20677 Mon Sep 17 00:00:00 2001 From: Yago004 <147386653+Yago004@users.noreply.github.com> Date: Tue, 14 Apr 2026 11:20:51 +0200 Subject: [PATCH 083/141] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/Ravada.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index 8b4a41e61..c0a1bfe93 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -574,7 +574,7 @@ sub _update_isos { ,description => 'Alpine Linux 3.23 64 bits ( Minimal Linux Distribution )' ,arch => 'x86_64' ,xml => 'alpine-amd64.xml' - ,xml_volume => 'alpine323_64-volume.xml' + ,xml_volume => 'alpine381_64-volume.xml' ,url => 'http://dl-cdn.alpinelinux.org/alpine/v3.23/releases/x86_64/' ,file_re => 'alpine-standard-3.23.*-x86_64.iso' ,sha256_url => '$url/alpine-standard-3.23.*.iso.sha256' From b4c97e70e93bcbe9786d5596b12b63e692feefaf Mon Sep 17 00:00:00 2001 From: Yago Diaz Date: Tue, 14 Apr 2026 12:02:14 +0200 Subject: [PATCH 084/141] Fix ISO errors --- lib/Ravada.pm | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index c0a1bfe93..dff8dc175 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -412,10 +412,11 @@ sub _update_isos { ,xml_volume => 'bionic64-volume.xml' ,url => 'https://archive.archlinux.org/iso/2026.03.01/' ,file_re => 'archlinux-2026.03.01-x86_64.iso' + ,sha256_url => '$url/sha256sums.txt' }, mate_noble => { name => 'Ubuntu Mate 24.04 Noble Numbat 64 bits' - ,description => 'Ubuntu Mate 24.04 Noble Nubat m64 bits' + ,description => 'Ubuntu Mate 24.04 Noble Numbat 64 bits' ,arch => 'x86_64' ,xml => 'noble-amd64.xml' ,xml_volume => 'focal_fossa64-volume.xml' @@ -586,7 +587,7 @@ sub _update_isos { ,description => 'Alpine Linux 3.23 32 bits ( Minimal Linux Distribution )' ,arch => 'i686' ,xml => 'alpine-i386.xml' - ,xml_volume => 'alpine323_32-volume.xml' + ,xml_volume => 'alpine316_32-volume.xml' ,url => 'http://dl-cdn.alpinelinux.org/alpine/v3.23/releases/x86/' ,options => { machine => 'pc-i440fx' } ,file_re => 'alpine-standard-3.23.*-x86.iso' From eed1a164cb7f4bad13cc379015e94ef1875ca894 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 15 Apr 2026 17:02:40 +0200 Subject: [PATCH 085/141] wip: manage base dependencies --- lib/Ravada/Domain.pm | 17 ++--- lib/Ravada/Domain/KVM.pm | 4 ++ lib/Ravada/Request.pm | 25 +++++--- lib/Ravada/VM.pm | 2 +- t/nodes/10_basic.t | 130 ++++++++++++++++++++++++++++++--------- 5 files changed, 132 insertions(+), 46 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 4d6153e0f..f89c872d1 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -1282,7 +1282,7 @@ sub _check_has_clones { return if !$self->is_known(); my @clones = $self->clones; - die "Domain ".$self->name." has ".scalar @clones." clones.\n" + confess "Domain ".$self->name." has ".scalar @clones." clones.\n" if $#clones>=0; } @@ -3314,7 +3314,6 @@ sub clone { return $self->_clone_from_pool(@_) if $from_pool; if ($self->is_active) { - warn $self->name." is active, req shutdown"; my $req = Ravada::Request->shutdown(uid => $user->id ,id_domain => $self->id ); @@ -5646,11 +5645,6 @@ sub _pre_migrate($self, $node, $request = undef) { confess "ERROR: base ".$base->name." not prepared in node ".$node->name if !$base->base_in_vm($node->id); confess "ERROR: base id ".$self->id_base." not found." if !$base; - - if ( ! $self->_check_all_parents_in_node($node, $request) ) { - die "Error: base of ".$self->name." , ".$base->name." not in node ".$node->id." ".$node->name; - } - } $node->_add_instance_db($self->id); } @@ -5765,7 +5759,6 @@ sub set_base_vm($self, %args) { $request->status("working","Preparing base") if $request; $self->prepare_base($user) } - $self->_check_all_parents_in_node($vm); $request->status("working", "Syncing base volumes to ".$vm->host) if $request; @@ -5826,6 +5819,14 @@ sub _remove_files_not_shared($self, @nodes){ } } +sub _check_all_base_parents_in_node($self, $vm) { + return 1 if !$self->id_base; + + my $base = Ravada::Front::Domain->open($self->id_base); + return 0 if !$base->base_in_vm($vm->id); + return $base->_check_all_base_parents_in_node($vm); +} + sub _check_all_parents_in_node($self, $vm, $request=undef) { my @bases; my $base = $self; diff --git a/lib/Ravada/Domain/KVM.pm b/lib/Ravada/Domain/KVM.pm index d35594c24..d9dfd2ea8 100644 --- a/lib/Ravada/Domain/KVM.pm +++ b/lib/Ravada/Domain/KVM.pm @@ -343,6 +343,10 @@ sub remove { } sub remove_instance($self, $user) { + eval { + $self->domain->destroy() if $self->domain->is_active(); + }; + warn $@ if $@; eval { $self->domain->undefine(Sys::Virt::Domain::UNDEFINE_NVRAM) if $self->domain;# && !$self->is_removed }; diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index 3f988d671..10588173e 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -1167,6 +1167,13 @@ sub _validate_clone($self $self->error("Error: user id='$uid' does not exist"); return; } + if ($base->is_active) { + my $req_shutdown = Ravada::Request->shutdown_domain( + uid => $uid + ,id_domain => $base->id + ); + $self->after_request($req_shutdown->id); + } my ($req_base) = grep { $_->command eq 'prepare_base' } $base->list_requests; @@ -1564,8 +1571,6 @@ sub set_base_vm { } sub _validate_migrate($req) { - return if $req->defined_arg('value') && $req->defined_arg('value')==0; - my $domain = Ravada::Front::Domain->open($req->args('id_domain')); my $req_post = Ravada::Request->post_migrate( uid => $req->args('uid') @@ -1690,8 +1695,8 @@ sub _validate_remove_base_vm($req) { my $bases_vm = $domain->_bases_vm(1); my @other_vms; - for my $id_vm (keys %$bases_vm) { - push @other_vms,($id_vm) if _node_is_active($id_vm); + for my $id_vm_other (keys %$bases_vm) { + push @other_vms,($id_vm_other) if $id_vm_other != $id_vm && _node_is_active($id_vm_other); } if ( !@other_vms ) { @@ -1704,10 +1709,11 @@ sub _validate_remove_base_vm($req) { for my $clone ($domain->clones) { # migrate clones to other vms + my $req_migrate; if ( $clone->{id_vm} == $id_vm ) { my $start = 0; $start = 1 if $clone->{status} eq 'active'; - my $req_migrate = Ravada::Request->migrate( + $req_migrate = Ravada::Request->migrate( uid => Ravada::Utils::user_daemon->id ,id_domain => $clone->{id} ,id_node => $other_vms[0] @@ -1725,6 +1731,7 @@ sub _validate_remove_base_vm($req) { ,id_domain => $clone->{id} ,id_vm => $id_vm ); + $req_prev->after_request_ok($req_migrate->id) if $req_migrate; $req->after_request_ok($req_prev->id); } } @@ -1983,7 +1990,7 @@ sub done_recently($self, $seconds=60,$command=undef, $args=undef) { delete $args_found_d->{at}; next if join(".",sort keys %$args_d) ne join(".",sort keys %$args_found_d); - my $args_d_s = join(".",map { $args_d->{$_} } sort keys %$args_d); + my $args_d_s = join(".",map { $args_d->{$_} or '' } sort keys %$args_d); my $args_found_s = join(".",map {$args_found_d->{$_} or '' } sort keys %$args_found_d); next if $args_d_s ne $args_found_s; @@ -2194,7 +2201,11 @@ sub _data($self, $field, $value=undef) { $value = _push($prev_req_id, $value) if $prev_req_id; } $self->{_data}->{$field} = $value; - $value = encode_json($value) if ref($value) eq 'ARRAY'; + my $value0 = $value; + eval { + $value = encode_json($value) if ref($value) eq 'ARRAY'; + }; + confess Dumper([$@,$value0]) if $@; my $sth = $$CONNECTOR->dbh->prepare( "UPDATE requests set $field=?" diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index f98300ce4..b6b176588 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -565,7 +565,7 @@ sub _around_create_domain { die "Error: No free nodes available.\n"; } if ( $base->_base_files_in_vm($vm) - && $base->_check_all_parents_in_node($vm)) { + && $base->_check_all_base_parents_in_node($vm)) { $self = $vm; } $request->status("creating machine on ".$self->name) if $request; diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index 00284fd77..a90fa5218 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -152,7 +152,6 @@ sub test_iptables($node, $node2) { ,jump => 'DROP' ); is(scalar @found,0,Dumper(\@found)); - # warn Dumper($list->{filter}); $clone1->remove(user_admin); @@ -1156,11 +1155,29 @@ sub test_keep_node($node, $clone) { sub test_base_unset($vm, $node) { my $base = create_domain($vm); $base->prepare_base(user_admin); - $base->set_base_vm(vm => $node, user => user_admin); + Ravada::Request->set_base_vm( + uid => user_admin->id + ,id_domain => $base->id + ,id_vm => $node->id + ); + wait_request(); my $clone = $base->clone(name => new_domain_name, user => user_admin); - $clone->migrate($node); - $base->set_base_vm(id_vm => $node->id,value => 0, user => user_admin); + Ravada::Request->migrate( + uid => user_admin->id + ,id_domain => $clone->id + ,id_node => $node->id + ); + wait_request(); + + Ravada::Request->set_base_vm( + uid => user_admin->id + ,id_domain => $base->id + ,id_vm => $node->id + ,value => 0 + ); + wait_request(); + is($base->base_in_vm($node->id),0) or exit; is(Ravada::Domain::base_in_vm($base->id,$node->id),0) or exit; wait_request(debug => 0); @@ -1327,10 +1344,14 @@ sub _shutdown_domains(@nodes) { my $base_name=base_domain_name(); if ( $domain->name =~ /$base_name/ ) { warn $domain->name; - $domain->shutdown_now(user_admin); + Ravada::Request->force_shutdown( + uid => user_admin->id + ,id_domain => $domain->id + ); } } } + wait_request(); } sub test_fill_memory($vm, $node, $migrate, $start=0) { @@ -1366,7 +1387,6 @@ sub test_fill_memory($vm, $node, $migrate, $start=0) { my $created_in_node=0; for ( 1 .. 100 ) { my $clone_name = new_domain_name(); - diag("Try $_ , $clone_name may go to ".$node->name); my $req = Ravada::Request->create_domain( name => $clone_name ,id_owner => user_admin->id @@ -1381,7 +1401,6 @@ sub test_fill_memory($vm, $node, $migrate, $start=0) { my $clone = rvd_back->search_domain($clone_name) or last; ok($clone,"Expecting clone $clone_name") or exit; $created_in_node++ if $clone->_data('id_vm') == $node->id; - diag($clone->name." ".$clone->_data('id_vm')." [node=".$node->id."]"); _check_files_exist($clone); Ravada::Request->migrate( uid => user_admin->id @@ -1396,9 +1415,8 @@ sub test_fill_memory($vm, $node, $migrate, $start=0) { uid => user_admin->id ,id_domain => $clone->id ); - wait_request(debug => 1, check_error => 0); + wait_request(debug => 0, check_error => 0); $error = $req_start->error; - diag("error=$error");# if $error; like($error, qr/(^$|No free memory)/); exit if $error && $error !~ /No free memory/; $created_in_node++ if $clone->_data('id_vm') == $node->id; @@ -1685,9 +1703,9 @@ sub test_nested_base($vm, $node, $levels=1) { my $base0 = create_domain($vm); - $base0->add_volume(swap => 1, size => 10 * 1024); - $base0->add_volume(type => 'tmp', size => 10 * 1024); - $base0->add_volume(type => 'data', size => 10 * 1024); + $base0->add_volume(swap => 1, size => 10 * 1024, format => 'qcow2'); + $base0->add_volume(type => 'tmp', size => 10 * 1024, format => 'qcow2'); + $base0->add_volume(type => 'data', size => 10 * 1024, format => 'qcow2'); my $base1 = $base0; my $clone; @@ -1701,15 +1719,14 @@ sub test_nested_base($vm, $node, $levels=1) { ); wait_request(); } - diag("Cloning from ".$base1->name." ".($base1->id_base or ' ')." level $n / $levels"); - my $name = new_domain_name(); Ravada::Request->clone( - name => $name - ,uid => user_admin->id + uid => user_admin->id ,id_domain => $base1->id + ,number => 3 ); wait_request(); - $clone = $vm->search_domain($name); + my @clones = $base1->clones(); + $clone = $vm->search_domain($clones[0]->{name}); is($clone->id_base,$base1->id); push @bases,($base1); wait_request(); @@ -1721,14 +1738,28 @@ sub test_nested_base($vm, $node, $levels=1) { _test_migrate_nested($vm, $node, \@bases, $clone, $levels); _migrate_fast($clone, $vm); _remove_base_vm($base0, $node); - diag("###########################################"); - diag("test migrate nested again"); _test_migrate_nested($vm, $node, \@bases, $clone, $levels); - my ($file) = $base0->list_files_base; - $node->remove_file($file); - for my $domain ($clone, reverse @bases ) { - $domain->remove(user_admin); + _test_all_children_ok($base0); + + remove_domain($base0); +} + +sub _test_all_children_ok($base) { + for my $clone( $base->clones ) { + Ravada::Request->refresh_machine( + uid => user_admin->id + ,id_domain => $clone->{id} + ); + if ($clone->{is_base}) { + _test_all_children_ok(Ravada::Front::Domain->open($clone->{id})); + } else { + Ravada::Request->start_domain( + uid => user_admin->id + ,id_domain => $clone->{id} + ); + } + wait_request(); } } @@ -1740,28 +1771,35 @@ sub _migrate_fast($domain, $node) { ,shutdown => 1 ); wait_request(debug => 1); + my $domain2 = Ravada::Front::Domain->open($domain->id); + is($domain2->_data('id_vm'),$node->id) or confess; } sub _remove_base_vm($base,$node) { - diag("Remove base from ".$node->name." for ".$base->name); - Ravada::Request->remove_base_vm( + diag("Remove base from ".$node->id." ".$node->name." for ".$base->id." " + .$base->name); + delete_request('migrate','set_base_vm','remove_base_vm'); + my $req = Ravada::Request->remove_base_vm( uid => user_admin->id ,id_domain => $base->id ,id_vm => $node->id ); + wait_request(debug => 1); + my $base2=Ravada::Domain->open($base->id); + isnt($base2->_data('id_vm'), $node->id) or confess $base->id." ".$base->name; + + _check_clones_in_node($base2, $base2->_data('id_vm')); } sub _test_migrate_nested($vm, $node, $bases, $clone, $levels) { - delete_request('migrate','set_base_vm'); - diag("migrating clone ".$clone->name." to ".$node->name." ".$node->id); + delete_request('migrate','set_base_vm','remove_base_vm'); my $req = Ravada::Request->migrate( id_node => $node->id ,id_domain => $clone->id ,uid => user_admin->id ,shutdown => 1 ); - warn Dumper([$req->id,$req->after_request_ok()]); for ( 1 .. $levels+3 ) { last if $req->status eq 'done'; wait_request(debug => 0, check_error => 0); @@ -1770,7 +1808,7 @@ sub _test_migrate_nested($vm, $node, $bases, $clone, $levels) { is($req->status,'done'); for my $base ( @$bases ) { is($base->is_base,1,"Expecting ".$base->name." is base") or exit; - is($base->base_in_vm($node->id),1); + is($base->base_in_vm($node->id),1) or confess Dumper([$clone->id,$base->id]); } my $clone2 = Ravada::Domain->open($clone->id); is($clone2->_vm->id,$node->id) or confess; @@ -1778,6 +1816,17 @@ sub _test_migrate_nested($vm, $node, $bases, $clone, $levels) { is(''.$@, '', $clone2->name) or exit; } +sub _check_clones_in_node($base0, $id_node) { + my $base = $base0; + $base = Ravada::Front::Domain->open($base0->{id}) if ref($base) !~/Ravada/; + for my $clone ( $base->clones ) { + is($clone->{id_vm}, $id_node) or confess $clone->{name}; + if ( $clone->{is_base}) { + _check_clones_in_node($clone, $id_node); + } + } +} + sub test_display_ip($vm, $node, $set_localhost_dp=0) { my $vm_ip = $vm->ip; if ($set_localhost_dp == 1) { @@ -2169,6 +2218,21 @@ sub test_spinoff_remote($vm, $node) { test_remove_instances($base, $vm, $node); } +sub test_base_only_in_node_add_hw($vm, $node) { + + my $base = _req_create($node); + Ravada::Request->add_hardware( + name => 'disk' + ,uid => user_admin->id + ,id_domain => $base->id + ,data => { size => '0.5G' } + ); + wait_request(debug => 0); + + test_volumes_exist($base,$node,1); + test_volumes_exist($base,$vm,0); +} + sub test_base_only_in_node($vm, $node, $start=0) { diag("Test base only in node , start=$start"); @@ -2210,7 +2274,10 @@ sub test_base_only_in_node($vm, $node, $start=0) { sub test_remove_instances($base, @nodes) { my @volumes = $base->list_volumes(); my @files_base = $base->list_files_base(); - $base->remove(user_admin); + Ravada::Request->remove_domain( + uid => user_admin->id + ,name => $base->name + ); wait_request(debug => 0); my $vm = $base->_vm; for my $vm ( $base->_vm, @nodes ) { @@ -2277,6 +2344,9 @@ for my $vm_name (vm_names() ) { start_node($node); + test_base_only_in_node_add_hw($vm, $node); #start after create = 1 + + test_base_unset($vm,$node); test_duplicated_set_base_vm($vm, $node); for my $volatile (1,0) { test_remove_base($vm, $node, $volatile); From 9376407ba1a682321b586e400240a1c51221d17f Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 16 Apr 2026 13:57:41 +0200 Subject: [PATCH 086/141] wip: keep track of set base request --- lib/Ravada/Domain.pm | 22 ++++++++++++++------- lib/Ravada/Request.pm | 2 +- script/rvd_front | 1 - t/nodes/00_req.t | 30 ++++++++++++++++++++++++++++ t/nodes/10_basic.t | 36 ++++++++++++++++++++++------------ templates/main/vm_base.html.ep | 3 +-- 6 files changed, 70 insertions(+), 24 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index f89c872d1..d9010543a 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -5685,20 +5685,29 @@ sub _set_base_vm_db($self, $id_vm, $value, $id_request=undef) { if (!defined $id_is_base) { return if !$value && !$self->is_known; my $sth = $$CONNECTOR->dbh->prepare( - "INSERT INTO bases_vm (id_domain, id_vm, enabled, id_request) " - ." VALUES(?, ?, ?, ?)" + "INSERT INTO bases_vm (id_domain, id_vm, id_request) " + ." VALUES(?, ?, ?)" ); - $sth->execute($self->id, $id_vm, $value, $id_request); + $sth->execute($self->id, $id_vm, $id_request); $sth->finish; } else { my $sth = $$CONNECTOR->dbh->prepare( - "UPDATE bases_vm SET enabled=?, id_request=?" + "UPDATE bases_vm SET id_request=?" ." WHERE id_domain=? AND id_vm=?" ); - $value = 0 if !defined $value; - $sth->execute($value, $id_request, $self->id, $id_vm); + $sth->execute($id_request, $self->id, $id_vm); $sth->finish; } + if (defined $value) { + $is_base = $self->base_in_vm($id_vm) if !$is_base; + my $sth = $$CONNECTOR->dbh->prepare( + "UPDATE bases_vm SET enabled=?" + ." WHERE id_domain=? AND id_vm=?" + ); + $sth->execute($value, $self->id, $id_vm); + $sth->finish; + + } } =head2 set_base_vm @@ -5753,7 +5762,6 @@ sub set_base_vm($self, %args) { $request->status("working"); $id_request = $request->id; } - $self->_set_base_vm_db($vm->id, $value, $id_request); if ($value) { if ( !$self->is_base() ) { $request->status("working","Preparing base") if $request; diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index 10588173e..f43c15e2f 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -1565,7 +1565,7 @@ sub set_base_vm { my $id_vm = $args->{id_vm}; $id_vm = $args->{id_node} if exists $args->{id_node} && $args->{id_node}; - $domain->_set_base_vm_db($id_vm, $args->{value}, $req->id); + $domain->_set_base_vm_db($id_vm, undef, $req->id); return $req; } diff --git a/script/rvd_front b/script/rvd_front index 3b2b1cd2e..bc4fbc0bf 100644 --- a/script/rvd_front +++ b/script/rvd_front @@ -4673,7 +4673,6 @@ sub set_base_vm( $c, $new_value) { return $c->render(json => {message => 'access denied'}); } - $domain->_set_base_vm_db($id_vm, $new_value); if ($new_value) { my $req = Ravada::Request->set_base_vm( id_vm => $id_vm diff --git a/t/nodes/00_req.t b/t/nodes/00_req.t index 28d0cfc38..784b115a0 100644 --- a/t/nodes/00_req.t +++ b/t/nodes/00_req.t @@ -169,6 +169,35 @@ sub test_req_migrate_nested($vm, $node1) { remove_domain_db($clone); } +sub test_req_failed($vm) { + + my $base = create_base($vm); + my $req = Ravada::Request->set_base_vm( + uid => user_admin->id + ,id_domain => $base->id + ,id_vm => $vm->id + ); + my $bases_vm = $base->_bases_vm_info(); + warn Dumper($bases_vm); + is($bases_vm->{$vm->id}->{enabled},undef) or exit; + is($bases_vm->{$vm->id}->{id_request},$req->id); + + my $req_rm = Ravada::Request->remove_base_vm( + uid => user_admin->id + ,id_domain => $base->id + ,id_vm => $vm->id + ); + warn Dumper([$req_rm->id,$req_rm->after_request_ok]); + warn Dumper($base->_bases_vm_info()); + + wait_request(debug => 1); + + $bases_vm = $base->_bases_vm_info(); + warn Dumper($bases_vm); + die $base->id." ".$base->name; + +} + sub test_req_prepare_nested($vm, $node1) { my $base1 = create_base($vm); $base1->_data('is_base' => 1); @@ -297,6 +326,7 @@ for my $vm_name (vm_names() ) { my $node1 = _create_remote_node($vm_name); my $node2 = _create_remote_node($vm_name); + test_req_failed($vm); test_req_prepare_nested($vm, $node1); test_req_migrate_nested($vm, $node1); test_req_prepare_base($vm, $node1, $node2); diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index a90fa5218..ac5c40368 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -321,6 +321,8 @@ sub test_removed_local_swap($vm, $node) { } sub test_removed_remote_swap($vm, $node) { + return; + # TODO diag("Testing removed remote swap in ".$vm->type); my $base = create_domain($vm); $base->add_volume(size => 128*1024 , type => 'tmp'); @@ -565,7 +567,7 @@ sub test_set_vm_fail($vm, $node) { sub test_set_vm($vm, $node) { my $base = create_domain($vm); my $info = $base->info(user_admin); - is($info->{bases}->{$vm->id},0); + is($info->{bases}->{$vm->id}->{enabled},0); my $req = Ravada::Request->set_base_vm( id_domain => $base->id @@ -574,22 +576,23 @@ sub test_set_vm($vm, $node) { , uid => user_admin->id ); rvd_back->_process_requests_dont_fork(); + wait_request(); is($req->status, 'done'); like($req->error, qr{^($|rsync done)}); my $base2 = Ravada::Domain->open($base->id); $info = $base2->info(user_admin); - is($info->{bases}->{$vm->id},1,Dumper($info->{bases})) or exit; - is($info->{bases}->{$node->id},1,$node->id." " + is($info->{bases}->{$vm->id}->{enabled},1,Dumper($info->{bases})) or exit; + is($info->{bases}->{$node->id}->{enabled},1,$node->id." " .Dumper($info->{bases})) or exit; is($base->list_instances,2) or exit; my $base_f = Ravada::Front::Domain->open($base->id); $info = $base_f->info(user_admin); - is($info->{bases}->{$vm->id},1) or exit; - is($info->{bases}->{$node->id},1) or exit; + is($info->{bases}->{$vm->id}->{enabled},1) or exit; + is($info->{bases}->{$node->id}->{enabled},1) or exit; is($base_f->list_instances,2) or exit; @@ -676,7 +679,12 @@ sub test_volatile_req($vm, $node) { my $base = create_domain($vm); $base->volatile_clones(1); $base->prepare_base(user_admin); - $base->set_base_vm(user => user_admin, node => $node); + Ravada::Request->set_base_vm( + uid => user_admin->id + ,id_domain => $base->id + ,id_vm => $node->id + ); + wait_request(debug => 0); ok($base->base_in_vm($node->id)); my @clones; my $clone; @@ -892,7 +900,7 @@ sub _test_old_base($base, $vm) { my $base_f = Ravada::Front::Domain->open($base->id); my $info = $base_f->info(user_admin); - is($info->{bases}->{$vm->id},1) ; + is($info->{bases}->{$vm->id}->{enabled},1) ; is(scalar keys %{$info->{bases}}, 2); } @@ -1184,7 +1192,7 @@ sub test_base_unset($vm, $node) { my $clone2 = Ravada::Domain->open($clone->id); $clone2->start(user_admin); - is($clone2->_vm->name, $vm->name) or exit; + is($clone2->_vm->name, $vm->name) or confess; _remove_domain($base); } @@ -1759,7 +1767,7 @@ sub _test_all_children_ok($base) { ,id_domain => $clone->{id} ); } - wait_request(); + wait_request(debug => 0); } } @@ -1770,7 +1778,7 @@ sub _migrate_fast($domain, $node) { ,id_node => $node->id ,shutdown => 1 ); - wait_request(debug => 1); + wait_request(debug => 0); my $domain2 = Ravada::Front::Domain->open($domain->id); is($domain2->_data('id_vm'),$node->id) or confess; } @@ -1785,7 +1793,7 @@ sub _remove_base_vm($base,$node) { ,id_vm => $node->id ); - wait_request(debug => 1); + wait_request(debug => 0); my $base2=Ravada::Domain->open($base->id); isnt($base2->_data('id_vm'), $node->id) or confess $base->id." ".$base->name; @@ -2307,7 +2315,7 @@ clean(); $Ravada::Domain::MIN_FREE_MEMORY = 256 * 1024; my $tls; -for my $vm_name (vm_names() ) { +for my $vm_name (reverse vm_names() ) { my $vm; eval { $vm = rvd_back->search_vm($vm_name) }; @@ -2344,9 +2352,11 @@ for my $vm_name (vm_names() ) { start_node($node); + test_base_unset($vm,$node); + test_set_vm($vm, $node); + test_base_only_in_node_add_hw($vm, $node); #start after create = 1 - test_base_unset($vm,$node); test_duplicated_set_base_vm($vm, $node); for my $volatile (1,0) { test_remove_base($vm, $node, $volatile); diff --git a/templates/main/vm_base.html.ep b/templates/main/vm_base.html.ep index 919f1dd55..f83c1d3c1 100644 --- a/templates/main/vm_base.html.ep +++ b/templates/main/vm_base.html.ep @@ -58,8 +58,7 @@
<%=l 'Node down' %>
<%=l 'Node disabled' %>
-
<%=l 'This base has clones' %>
-
<%=l 'This node has' %> {{ showmachine.clones[node.id].length }} <%=l 'clones' %>
+
<%=l 'This node has' %> {{ showmachine.clones[node.id].length }} <%=l 'clones' %>
From 16d431ea7cc2473ba20178980b977f78f0994216 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 16 Apr 2026 16:15:48 +0200 Subject: [PATCH 087/141] set or remove base after work done --- lib/Ravada/Domain.pm | 4 ++-- lib/Ravada/Request.pm | 9 ++++++++- t/nodes/00_req.t | 27 +++++++++++++++++++-------- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index d9010543a..a4f709898 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -5685,8 +5685,8 @@ sub _set_base_vm_db($self, $id_vm, $value, $id_request=undef) { if (!defined $id_is_base) { return if !$value && !$self->is_known; my $sth = $$CONNECTOR->dbh->prepare( - "INSERT INTO bases_vm (id_domain, id_vm, id_request) " - ." VALUES(?, ?, ?)" + "INSERT INTO bases_vm (id_domain, id_vm, enabled, id_request) " + ." VALUES(?, ?, 0, ?)" ); $sth->execute($self->id, $id_vm, $id_request); $sth->finish; diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index f43c15e2f..c97d1ad30 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -1669,11 +1669,18 @@ sub remove_base_vm { my $self = {}; bless($self,$class); - return $self->_new_request( + my $req = $self->_new_request( command => 'remove_base_vm' , args => $args ); + my $id_vm = $req->defined_arg('id_vm'); + $id_vm = $req->defined_arg('id_node') if !defined $id_vm; + + my $domain = Ravada::Front::Domain->open($req->args('id_domain')); + $domain->_set_base_vm_db($id_vm, undef, $req->id); + + return $req; } sub _node_is_active($id) { diff --git a/t/nodes/00_req.t b/t/nodes/00_req.t index 784b115a0..58f5d1576 100644 --- a/t/nodes/00_req.t +++ b/t/nodes/00_req.t @@ -171,30 +171,41 @@ sub test_req_migrate_nested($vm, $node1) { sub test_req_failed($vm) { - my $base = create_base($vm); + my $base = create_domain($vm); + diag(" test req failed ".$base->id." ".$base->name." ".$vm->name." [".$vm->id."]"); + + my $bases_vm = $base->_bases_vm_info(); + is($bases_vm->{$vm->id}->{enabled},0) or exit; + my $req = Ravada::Request->set_base_vm( uid => user_admin->id ,id_domain => $base->id ,id_vm => $vm->id ); - my $bases_vm = $base->_bases_vm_info(); - warn Dumper($bases_vm); - is($bases_vm->{$vm->id}->{enabled},undef) or exit; + $bases_vm = $base->_bases_vm_info(); + is($bases_vm->{$vm->id}->{enabled},0) or exit; is($bases_vm->{$vm->id}->{id_request},$req->id); + wait_request(); + + $bases_vm = $base->_bases_vm_info(); + is($bases_vm->{$vm->id}->{enabled},1) or exit; + is($bases_vm->{$vm->id}->{id_request},undef) or exit; my $req_rm = Ravada::Request->remove_base_vm( uid => user_admin->id ,id_domain => $base->id ,id_vm => $vm->id ); - warn Dumper([$req_rm->id,$req_rm->after_request_ok]); - warn Dumper($base->_bases_vm_info()); + $bases_vm = $base->_bases_vm_info(); + is($bases_vm->{$vm->id}->{enabled},1) or exit; + is($bases_vm->{$vm->id}->{id_request},$req_rm->id) or exit; wait_request(debug => 1); $bases_vm = $base->_bases_vm_info(); - warn Dumper($bases_vm); - die $base->id." ".$base->name; + is($bases_vm->{$vm->id}->{enabled},0) or exit; + + remove_domain($base); } From 8f556650e8059a6a806f5bf0d3857cb41911b3da Mon Sep 17 00:00:00 2001 From: Yago Diaz Date: Fri, 17 Apr 2026 12:00:32 +0200 Subject: [PATCH 088/141] Fix Alpine 3.32 for 32 --- lib/Ravada.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index dff8dc175..401e53d1e 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -587,7 +587,7 @@ sub _update_isos { ,description => 'Alpine Linux 3.23 32 bits ( Minimal Linux Distribution )' ,arch => 'i686' ,xml => 'alpine-i386.xml' - ,xml_volume => 'alpine316_32-volume.xml' + ,xml_volume => 'alpine381_32-volume.xml' ,url => 'http://dl-cdn.alpinelinux.org/alpine/v3.23/releases/x86/' ,options => { machine => 'pc-i440fx' } ,file_re => 'alpine-standard-3.23.*-x86.iso' From 6bf2df61ee8232f88a5526062604c1e0d0a359c9 Mon Sep 17 00:00:00 2001 From: Yago004 <147386653+Yago004@users.noreply.github.com> Date: Fri, 17 Apr 2026 16:28:04 +0200 Subject: [PATCH 089/141] Update ISO Images (#2310) --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/Ravada.pm | 148 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 101 insertions(+), 47 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index 04f55a437..401e53d1e 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -404,16 +404,27 @@ sub _update_isos { ,md5_url => '' ,md5 => '1d6bdf5cbc6ca98c31f02d23e418dd96' }, + arch_2603 => { + name => 'Arch Linux 26.03' + ,description => 'Arch Linux 2026.03.01 64 bits' + ,arch => 'x86_64' + ,xml => 'bionic-amd64.xml' + ,xml_volume => 'bionic64-volume.xml' + ,url => 'https://archive.archlinux.org/iso/2026.03.01/' + ,file_re => 'archlinux-2026.03.01-x86_64.iso' + ,sha256_url => '$url/sha256sums.txt' + }, mate_noble => { name => 'Ubuntu Mate 24.04 Noble Numbat 64 bits' - ,description => 'Ubuntu Mate 24.04 Noble Nubat m64 bits' + ,description => 'Ubuntu Mate 24.04 Noble Numbat 64 bits' ,arch => 'x86_64' ,xml => 'noble-amd64.xml' ,xml_volume => 'focal_fossa64-volume.xml' ,url => 'http://cdimage.ubuntu.com/ubuntu-mate/releases/24.04.*/release/ubuntu-mate-24.04.*-desktop-amd64.iso' ,sha256_url => '$url/SHA256SUMS' ,options => { machine => 'pc-q35', bios => 'UEFI' } - ,min_ram => 3 + ,min_ram => 4 + ,min_disk_size => '20' }, mate_jammy=> { name => 'Ubuntu Mate 22.04 Jammy Jellyfish 64 bits' @@ -424,7 +435,8 @@ sub _update_isos { ,url => 'http://cdimage.ubuntu.com/ubuntu-mate/releases/22.04.*/release/ubuntu-mate-22.04.*-desktop-amd64.iso' ,sha256_url => '$url/SHA256SUMS' ,options => { machine => 'pc-q35', bios => 'UEFI' } - ,min_ram => 1 + ,min_ram => 4 + ,min_disk_size => '20' }, mate_focal_fossa => { @@ -436,7 +448,8 @@ sub _update_isos { ,url => 'http://cdimage.ubuntu.com/ubuntu-mate/releases/20.04.*/release/ubuntu-mate-20.04.*-desktop-amd64.iso' ,sha256_url => '$url/SHA256SUMS' ,options => { machine => 'pc-q35', bios => 'UEFI' } - ,min_ram => 1 + ,min_ram => 4 + ,min_disk_size => '20' }, mate_bionic => { name => 'Ubuntu Mate 18.04 Bionic 64 bits' @@ -446,7 +459,8 @@ sub _update_isos { ,xml_volume => 'bionic64-volume.xml' ,url => 'http://cdimage.ubuntu.com/ubuntu-mate/releases/18.04.*/release/ubuntu-mate-18.04.*-desktop-amd64.iso' ,sha256_url => '$url/SHA256SUMS' - ,min_ram => 1 + ,min_ram => 4 + ,min_disk_size => '20' }, mate_bionic_i386 => { name => 'Ubuntu Mate 18.04 Bionic 32 bits' @@ -456,7 +470,8 @@ sub _update_isos { ,xml_volume => 'bionic32-volume.xml' ,url => 'http://cdimage.ubuntu.com/ubuntu-mate/releases/18.04.*/release/ubuntu-mate-18.04.*-desktop-i386.iso' ,sha256_url => '$url/SHA256SUMS' - ,min_ram => 1 + ,min_ram => 4 + ,min_disk_size => '20' }, ,focal_fossa=> { name => 'Ubuntu 20.04 Focal Fossa' @@ -467,8 +482,8 @@ sub _update_isos { ,url => 'http://releases.ubuntu.com/20.04/' ,file_re => '^ubuntu-20.04.*-desktop-amd64.iso' ,sha256_url => '$url/SHA256SUMS' - ,min_disk_size => '9' - ,min_ram => 1 + ,min_disk_size => '25' + ,min_ram => 4 ,options => { machine => 'pc-q35', bios => 'UEFI' } ,arch => 'x86_64' } @@ -481,7 +496,7 @@ sub _update_isos { ,url => 'http://releases.ubuntu.com/22.04/' ,file_re => '^ubuntu-22.04.*-desktop-amd64.iso' ,sha256_url => '$url/SHA256SUMS' - ,min_disk_size => '14' + ,min_disk_size => '25' ,min_ram => 4 ,options => { machine => 'pc-q35', bios => 'UEFI' } ,arch => 'x86_64' @@ -496,7 +511,7 @@ sub _update_isos { ,url => 'http://releases.ubuntu.com/24.04/' ,file_re => '^ubuntu-24.04.*-desktop-amd64.iso' ,sha256_url => '$url/SHA256SUMS' - ,min_disk_size => '14' + ,min_disk_size => '25' ,min_ram => 4 ,options => { machine => 'pc-q35', bios => 'UEFI' } ,arch => 'x86_64' @@ -554,10 +569,34 @@ sub _update_isos { ,file_re => 'alpine-standard-3.16.*-x86.iso' ,sha256_url => '$url/alpine-standard-3.16.*.iso.sha256' ,min_disk_size => '1' + } + ,alpine323_64 => { + name => 'Alpine 3.23 64 bits' + ,description => 'Alpine Linux 3.23 64 bits ( Minimal Linux Distribution )' + ,arch => 'x86_64' + ,xml => 'alpine-amd64.xml' + ,xml_volume => 'alpine381_64-volume.xml' + ,url => 'http://dl-cdn.alpinelinux.org/alpine/v3.23/releases/x86_64/' + ,file_re => 'alpine-standard-3.23.*-x86_64.iso' + ,sha256_url => '$url/alpine-standard-3.23.*.iso.sha256' + ,min_disk_size => '2' + ,options => { machine => 'pc-q35', bios => 'UEFI' } + } + ,alpine323_32 => { + name => 'Alpine 3.23 32 bits' + ,description => 'Alpine Linux 3.23 32 bits ( Minimal Linux Distribution )' + ,arch => 'i686' + ,xml => 'alpine-i386.xml' + ,xml_volume => 'alpine381_32-volume.xml' + ,url => 'http://dl-cdn.alpinelinux.org/alpine/v3.23/releases/x86/' + ,options => { machine => 'pc-i440fx' } + ,file_re => 'alpine-standard-3.23.*-x86.iso' + ,sha256_url => '$url/alpine-standard-3.23.*.iso.sha256' + ,min_disk_size => '1' } ,kubuntu_64_noble => { name => 'Kubuntu 24.04 Noble Nombat' - ,description => 'Kubuntu 22.04 Noble Nombat 64 bits' + ,description => 'Kubuntu 24.04 Noble Nombat 64 bits' ,arch => 'x86_64' ,xml => 'noble-amd64.xml' ,xml_volume => 'focal_fossa64-volume.xml' @@ -566,8 +605,8 @@ sub _update_isos { ,file_re => 'kubuntu-24.04.*-desktop-amd64.iso' ,rename_file => 'kubuntu_noble.iso' ,options => { machine => 'pc-q35', bios => 'UEFI' } - ,min_ram => 3 - ,min_disk_size => 11 + ,min_ram => 4 + ,min_disk_size => '25' } ,kubuntu_64_jammy => { @@ -581,8 +620,8 @@ sub _update_isos { ,file_re => 'kubuntu-22.04.*-desktop-amd64.iso' ,rename_file => 'kubuntu_jammy.iso' ,options => { machine => 'pc-q35', bios => 'UEFI' } - ,min_ram => 3 - ,min_disk_size => 11 + ,min_ram => 4 + ,min_disk_size => '25' } ,kubuntu_64_focal_fossa => { name => 'Kubuntu 20.04 Focal Fossa 64 bits' @@ -595,7 +634,8 @@ sub _update_isos { ,file_re => 'kubuntu-20.04.*-desktop-amd64.iso' ,rename_file => 'kubuntu_focal_fossa_64.iso' ,options => { machine => 'pc-q35', bios => 'UEFI' } - ,min_ram => 1 + ,min_ram => 4 + ,min_disk_size => '25' } ,suse_15 => { name => "openSUSE Leap 15" @@ -603,9 +643,19 @@ sub _update_isos { ,arch => 'x86_64' ,xml => 'bionic-amd64.xml' ,xml_volume => 'bionic64-volume.xml' - ,url => 'https://download.opensuse.org/distribution/leap/15.4/iso/' + ,url => 'https://download.opensuse.org/distribution/leap/15.6/iso/' ,file_re => 'openSUSE-Leap-15.\d-NET-x86_64-Current.iso' + } + ,suse_16 => { + name => "openSUSE Leap 16" + ,description => "openSUSE Leap 16 64 bits" + ,arch => 'x86_64' + ,xml => 'bionic-amd64.xml' + ,xml_volume => 'bionic64-volume.xml' + ,url => 'https://download.opensuse.org/distribution/leap/16.0/installer/iso/' + ,file_re => 'agama-installer\.x86_64-.*-Leap_16\.0-Build.*\.iso' + } ,xubuntu_noble => { name => 'Xubuntu 24.04 Noble Nombat 64 bits' @@ -616,6 +666,8 @@ sub _update_isos { ,url => 'https://ftp.lysator.liu.se/ubuntu-dvd/xubuntu/releases/24.04.*/release/' ,file_re => 'xubuntu.*desktop.*.iso' ,options => { machine => 'pc-q35', bios => 'UEFI' } + ,min_ram => 2 + ,min_disk_size => '20' } ,xubuntu_bionic => { name => 'Xubuntu 18.04 Bionic Beaver 32 bits' @@ -627,6 +679,8 @@ sub _update_isos { ,url => 'http://archive.ubuntu.com/ubuntu/dists/bionic/main/installer-i386/current/images/netboot/' ,file_re => 'mini.iso' ,rename_file => 'xubuntu_bionic_32.iso' + ,min_ram => 2 + ,min_disk_size => '20' } ,lubuntu_noble => { name => 'Lubuntu 24.04 Noble Nombat' @@ -637,6 +691,7 @@ sub _update_isos { ,xml => 'yakkety64-amd64.xml' ,xml_volume => 'yakkety64-volume.xml' ,min_disk_size => '10' + ,min_ram => 1 ,arch => 'x86_64' ,options => { machine => 'pc-q35', bios => 'UEFI' } } @@ -673,6 +728,17 @@ sub _update_isos { ,min_disk_size => '10' ,arch => 'i686' } + ,debian_stretch_64 => { + name =>'Debian 9 Stretch 64 bits' + ,description => 'Debian 9 Stretch 64 bits (XFCE desktop)' + ,url => 'https://cdimage.debian.org/cdimage/archive/^9\.1\d+.*\d$/amd64/iso-cd/' + ,file_re => 'debian-9.[\d\.]+-amd64-xfce-CD-1.iso' + ,md5_url => '$url/MD5SUMS' + ,xml => 'jessie-amd64.xml' + ,xml_volume => 'jessie-volume.xml' + ,min_disk_size => '10' + ,arch => 'x86_64' + } ,debian_buster_64=> { name =>'Debian 10 Buster 64 bits' ,description => 'Debian 10 Buster 64 bits (XFCE desktop)' @@ -758,28 +824,6 @@ sub _update_isos { ,min_ram => 3 ,options => { machine => 'pc-q35', bios => 'UEFI' } } - ,devuan_beowulf_amd64=> { - name =>'Devuan 10 Beowulf 64 bits' - ,description => 'Devuan Beowulf Desktop Live (amd64)' - ,arch => 'x86_64' - ,url => 'http://tw1.mirror.blendbyte.net/devuan-cd/devuan_beowulf/desktop-live/' - ,file_re => 'devuan_beowulf_.*_amd64_desktop-live.iso' - ,sha256_url => '$url/SHASUMS.txt' - ,xml => 'jessie-amd64.xml' - ,xml_volume => 'jessie-volume.xml' - ,min_disk_size => '10' - } - ,devuan_beowulf_i386=> { - name =>'Devuan 10 Beowulf 32 bits' - ,description => 'Devuan Beowulf Desktop Live (i386)' - ,arch => 'i686' - ,url => 'http://tw1.mirror.blendbyte.net/devuan-cd/devuan_beowulf/desktop-live/' - ,file_re => 'devuan_beowulf_.*_i386_desktop-live.iso' - ,sha256_url => '$url/SHASUMS.txt' - ,xml => 'jessie-i386.xml' - ,xml_volume => 'jessie-volume.xml' - ,min_disk_size => '10' - } ,devuan_daedalus_amd64=> { name =>'Devuan 12 Daedalus 64 bits' ,description => 'Devuan Daedalus Desktop Live (amd64)' @@ -801,17 +845,27 @@ sub _update_isos { ,xml_volume => 'jessie-volume.xml' ,min_disk_size => '10' } - - ,parrot_xfce_amd64 => { - name => 'Parrot Home Edition XFCE' - ,description => 'Parrot Home Edition XFCE 64 Bits' + ,devuan_excalibur_amd64=> { + name =>'Devuan 13 Excalibur 64 bits' + ,description => 'Devuan Excalibur Desktop Live (amd64)' ,arch => 'x86_64' + ,url => 'http://tw1.mirror.blendbyte.net/devuan-cd/devuan_excalibur/desktop-live/' + ,file_re => 'devuan_excalibur_.*_amd64_desktop-live.iso' ,xml => 'jessie-amd64.xml' ,xml_volume => 'jessie-volume.xml' - ,url => 'https://download.parrot.sh/parrot/iso/6.*/' - ,file_re => 'Parrot-home-6.*_amd64.iso' - ,sha256_url => '' - ,min_disk_size => '11' + ,min_disk_size => '10' + ,options => { machine => 'pc-q35'} + } + ,parrot_xfce_amd64 => { + name => 'Parrot Home Edition XFCE 7.1' + ,description => 'Parrot Home Edition XFCE 7.1 64 Bits' + ,arch => 'x86_64' + ,xml => 'jessie-amd64.xml' + ,xml_volume => 'jessie-volume.xml' + ,url => 'https://download.parrot.sh/parrot/iso/7.1/' + ,file_re => 'Parrot-home-7\.1.*_amd64\.iso' + ,sha256_url => '$url/signed-hashes.txt' + ,min_disk_size => '15' } ,kali_64 => { name => "Kali Linux $year" From 78ebfa2a81d4e23aade5d6e74f6a8e9ab86ae041 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Fri, 17 Apr 2026 16:28:35 +0200 Subject: [PATCH 090/141] wip: chain base requirements --- lib/Ravada/Domain.pm | 3 --- lib/Ravada/Request.pm | 15 +++++++++++---- t/nodes/00_req.t | 4 +--- t/nodes/10_basic.t | 8 ++++++-- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index a4f709898..54255eaa1 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -5999,9 +5999,6 @@ sub base_in_vm($self,$id_vm) { confess "ERROR: id_vm must be a number, it is '$id_vm'" if $id_vm !~ /^\d+$/; - confess "ERROR: Domain ".$self->name." is not a base" - if ref($self) && !$self->is_base; - confess "Undefined id_vm " if !defined $id_vm; my $sth = $$CONNECTOR->dbh->prepare( diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index c97d1ad30..3c26c278a 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -1620,8 +1620,16 @@ sub _validate_migrate($req) { sub _validate_set_base_vm($req) { - return _validate_remove_base_vm($req) - if $req->defined_arg('value') && $req->defined_arg('value')==0; + my $value; + if ( $req->command eq 'set_base_vm') { + $value = 1; + my $args = $req->args(); + $value = $args->{value} if exists $args->{value}; + } elsif ($req->command eq 'remove_base_vm') { + $value = 0; + } + + return _validate_remove_base_vm($req) if !$value; my $domain = Ravada::Front::Domain->open($req->args('id_domain')); my $id_vm_local = $domain->_id_vm_local(); @@ -1706,7 +1714,7 @@ sub _validate_remove_base_vm($req) { push @other_vms,($id_vm_other) if $id_vm_other != $id_vm && _node_is_active($id_vm_other); } - if ( !@other_vms ) { + if ( !@other_vms && $domain->clones ) { $req->error("Error: there are no other VMs to migrate clones when removing base " .$domain->id." ".$domain->name); $req->status('done'); @@ -1732,7 +1740,6 @@ sub _validate_remove_base_vm($req) { # remove child bases if ( $clone->{is_base} ) { - my $req_prev = Ravada::Request->remove_base_vm( uid => Ravada::Utils::user_daemon->id ,id_domain => $clone->{id} diff --git a/t/nodes/00_req.t b/t/nodes/00_req.t index 58f5d1576..88fb6240a 100644 --- a/t/nodes/00_req.t +++ b/t/nodes/00_req.t @@ -172,7 +172,6 @@ sub test_req_migrate_nested($vm, $node1) { sub test_req_failed($vm) { my $base = create_domain($vm); - diag(" test req failed ".$base->id." ".$base->name." ".$vm->name." [".$vm->id."]"); my $bases_vm = $base->_bases_vm_info(); is($bases_vm->{$vm->id}->{enabled},0) or exit; @@ -200,7 +199,7 @@ sub test_req_failed($vm) { $bases_vm = $base->_bases_vm_info(); is($bases_vm->{$vm->id}->{enabled},1) or exit; is($bases_vm->{$vm->id}->{id_request},$req_rm->id) or exit; - wait_request(debug => 1); + wait_request(debug => 0); $bases_vm = $base->_bases_vm_info(); is($bases_vm->{$vm->id}->{enabled},0) or exit; @@ -286,7 +285,6 @@ sub test_req_remove_base_nested($vm, $node1, $clone, $base3, $base2, $base1) { } sub test_remove_requirements($base, $req) { - diag("testing remove requirements for ".$base->id." ".$base->name); my @clones = $base->clones; return if !@clones; diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index ac5c40368..21f47eff2 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -501,7 +501,7 @@ sub _check_base_in_vm_db($base, $id_node, $id_req, $value) { $sth->execute($base->id, $id_node); my $found = $sth->fetchrow_hashref; ok($found) or exit; - is($found->{enabled}, $value); + is($found->{enabled}, $value) or confess; is($found->{id_request}, $id_req) or confess; my @vms = $base->list_vms(); @@ -1163,6 +1163,8 @@ sub test_keep_node($node, $clone) { sub test_base_unset($vm, $node) { my $base = create_domain($vm); $base->prepare_base(user_admin); + is(Ravada::Domain::base_in_vm($base->id,$vm->id),1) or exit; + Ravada::Request->set_base_vm( uid => user_admin->id ,id_domain => $base->id @@ -1178,18 +1180,20 @@ sub test_base_unset($vm, $node) { ); wait_request(); + delete_request('migrate','set_base_vm','remove_base_vm'); Ravada::Request->set_base_vm( uid => user_admin->id ,id_domain => $base->id ,id_vm => $node->id ,value => 0 ); - wait_request(); + wait_request(debug => 0); is($base->base_in_vm($node->id),0) or exit; is(Ravada::Domain::base_in_vm($base->id,$node->id),0) or exit; wait_request(debug => 0); my $clone2 = Ravada::Domain->open($clone->id); + is($clone2->_data('id_vm'), $vm->id) or exit; $clone2->start(user_admin); is($clone2->_vm->name, $vm->name) or confess; From 39443b9a1cc8e740d76365294a99df87c12afb0f Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 20 Apr 2026 13:44:03 +0200 Subject: [PATCH 091/141] wip: deprecate rsync back, check for failed requests on setbase --- lib/Ravada.pm | 2 + lib/Ravada/Domain.pm | 53 +++++++--- lib/Ravada/VM.pm | 2 - t/kvm/n10_nodes.t | 239 ++++++++++++++++++++++++++++++++----------- t/nodes/00_req.t | 32 ++++++ t/nodes/10_basic.t | 14 ++- 6 files changed, 258 insertions(+), 84 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index 5e5497fee..689566937 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -5996,6 +5996,7 @@ sub _cmd_refresh_machine($self, $request) { $domain->info($user); $domain->client_status(1) if $is_active; $domain->_check_port_conflicts(); + $domain->_check_set_base_reqs(); Ravada::Request->refresh_machine_ports(id_domain => $domain->id, uid => $user->id ,timeout => 60, retry => 10) @@ -6363,6 +6364,7 @@ sub _cmd_migrate($self, $request) { } sub _cmd_rsync_back($self, $request) { + return; my $uid = $request->args('uid'); my $id_domain = $request->args('id_domain') or die "ERROR: Missing id_domain"; diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 54255eaa1..2f2a16a07 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -2937,8 +2937,11 @@ sub is_base { $sth->execute($value, $self->id ); $sth->finish; - $sth =$$CONNECTOR->dbh->prepare("UPDATE bases_vm SET enabled=? WHERE id_domain=?"); - $sth->execute($value, $self->id); + $sth =$$CONNECTOR->dbh->prepare( + "UPDATE bases_vm SET enabled=? WHERE id_domain=?" + ." AND id_vm=?" + ); + $sth->execute($value, $self->id, $self->_data('id_vm')); return $value; } my $ret = $self->_data('is_base'); @@ -3624,20 +3627,6 @@ sub _post_shutdown { $self->_set_displays_active(0, $info); delete $info->{ip}; $self->_data(info => encode_json($info)); - # only if not volatile - my $request; - $request = $arg{request} if exists $arg{request}; - if ( !$self->is_local && !$self->is_volatile && $self->has_non_shared_storage()) { - my @instances = $self->list_instances(); - my ($instance_local) = grep { $_->{id_vm} == $self->_id_vm_local() } @instances; - my $req; - $req = Ravada::Request->rsync_back( - uid => Ravada::Utils::user_daemon->id - ,id_domain => $self->id - ,id_node => $self->_vm->id - ,at => time + Ravada::setting(undef,"/backend/delay_migrate_back") - ) if $instance_local; - } $self->_schedule_compact(); $self->needs_restart(0) if $self->is_known() @@ -5808,7 +5797,37 @@ sub set_base_vm($self, %args) { } } - return $self->_set_base_vm_db($vm->id, $value); + $self->_set_base_vm_db($vm->id, $value); + + if (!$value) { + + my $bases_vm = $self->_bases_vm(1); #enabled bases vm + if (!keys %$bases_vm) { + $self->_post_remove_base(); + } + } +} + +sub _check_set_base_reqs($self) { + my $sth = $$CONNECTOR->dbh->prepare( + "SELECT id,id_request FROM bases_vm " + ." WHERE id_domain=? " + ." AND id_request is NOT NULL" + ); + $sth->execute($self->id); + + my $sth_update = $$CONNECTOR->dbh->prepare( + "UPDATE bases_vm SET id_request=NULL WHERE id=?" + ); + while ( my ($id, $id_request) = $sth->fetchrow ) { + my $req; + eval { $req = Ravada::Request->open($id_request)}; + warn $@ if $@; + + if (!$req || $req->status() eq 'done') { + $sth_update->execute($id); + } + } } sub _remove_files_not_shared($self, @nodes){ diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index b6b176588..19b386f6d 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -1957,8 +1957,6 @@ sub run_command($self, @command) { $timeout = 60*60 if $command[0] =~ /cp|qemu/; my $t0 = time; my ($out, $err) = $ssh->capture2({timeout => $timeout},join " ",@command); - warn ''.(time -$t0)." @command" - if $command[0] =~ /cp|qemu/i && !defined $command[1] && $command[1] ne 'info'; chomp $err if $err; $err = '' if !defined $err; diff --git a/t/kvm/n10_nodes.t b/t/kvm/n10_nodes.t index e1e24ffed..303d3ac54 100644 --- a/t/kvm/n10_nodes.t +++ b/t/kvm/n10_nodes.t @@ -117,15 +117,17 @@ sub test_node($vm_name,$node) { sub test_sync { my ($vm_name, $node, $base, $clone) = @_; - eval { $clone->rsync($node) }; - is(''.$@,'') or return; + Ravada::Request->migrate( + uid => user_admin->id + ,id_domain => $clone->id + ,id_node => $node->id + ); + wait_request(); # TODO test synced files + is($base->base_in_vm($node->id),1); + my $clone2=Ravada::Front::Domain->open($clone->id); + is($clone2->_data('id_vm'),$node->id); - eval { $base->rsync($node) }; - is($@,'') or return; - - eval { $clone->rsync($node) }; - is($@,'') or return; } sub test_domain_ip($vm_name, $node) { @@ -158,22 +160,29 @@ sub test_domain { is($base->_vm->host, 'localhost'); $base->prepare_base(user_admin); - $base->migrate_base(node => $node, user => user_admin); + Ravada::Request->set_base_vm( + uid => user_admin->id + ,id_domain => $base->id + ,id_vm => $node->id + ); + wait_request(); my $clone = $base->clone(name => new_domain_name ,user => user_admin ); test_sync($vm_name, $node, $base, $clone); - eval { $clone->migrate($node) }; - is(''.$@ , '') or return; - - my @start_arg = ( user => user_admin ); + my @start_arg; push @start_arg , ( remote_ip => $remote_ip ) if $remote_ip; eval { $clone->start(@start_arg) }; + Ravada::Request->start_domain( + uid => user_admin->id + ,id_domain => $clone->id + ,@start_arg + ); + wait_request(); - ok(!$@,$node->name." Expecting no error, got ".($@ or '')); is($clone->is_active,1) or return; my $local_ip = $node->ip; @@ -208,7 +217,12 @@ sub test_domain_on_remote { }; is($@,'',"Expecting no domain in remote node by now"); - $domain->remove(user_admin) if $domain; + my $req = Ravada::Request->remove_domain( + uid => user_admin->id + ,name => $domain->name + ); + wait_request(); + is($req->error,''); } sub test_remove_domain_from_local { @@ -531,6 +545,7 @@ sub test_rsync_newer { diag("Skipping: Volumes not implemented for $vm_name"); return; } + diag("test rsync newer"); my $domain = test_domain($vm_name, $node) or return; $domain->shutdown_now(user_admin) if $domain->is_active; @@ -578,28 +593,44 @@ sub test_rsync_newer { is($vol3_remote->get_info->{capacity}, $vol3->get_info->{capacity}); } - $domain->remove(user_admin); + remove_domain($domain); } sub test_bases_node { my ($vm_name, $node) = @_; + diag("Test bases node $vm_name ".$node->name); + my $vm = rvd_back->search_vm($vm_name); my $domain = create_domain($vm_name); - eval { $domain->base_in_vm($domain->_vm->id)}; - like($@,qr'is not a base'); + my $is; + eval { $is=$domain->base_in_vm($domain->_vm->id)}; + is($@,''); + is($is,0); # is($domain->base_in_vm($domain->_vm->id),1); eval { $domain->base_in_vm($node->id) }; - like($@,qr'is not a base'); - $domain->prepare_base(user_admin); + is($@,''); + is($is,0); + + Ravada::Request->prepare_base( + uid => user_admin->id + ,id_domain => $domain->id + ); + wait_request(); is($domain->base_in_vm($domain->_vm->id), 1); is($domain->base_in_vm($node->id), 0); - $domain->migrate($node); + Ravada::Request->migrate( + uid => user_admin->id + ,id_domain => $domain->id + ,id_node => $node->id + ); + wait_request(); + $domain = Ravada::Domain->open($domain->id); is($domain->_vm->id, $node->id) or exit; is($domain->base_in_vm($node->id), 1); @@ -609,18 +640,34 @@ sub test_bases_node { ok($vol_path,"[$vm_name] Expecting file '$name' in node ".$node->name) or exit; } - $domain->set_base_vm(vm => $node, value => 0, user => user_admin); + Ravada::Request->set_base_vm( + uid => user_admin->id + ,id_domain => $domain->id + ,id_vm => $node->id + ,value => 0 + ); + wait_request(); + is($domain->base_in_vm($node->id), 0); for my $file ( $domain->list_files_base ) { ok($vm->file_exists($file)) or die "File $file doesn't exist in ".$vm->name; } + Ravada::Request->set_base_vm( + uid => user_admin->id + ,id_domain => $domain->id + ,id_vm => $vm->id + ,value => 0 + ); + wait_request(); + + $domain = Ravada::Domain->open($domain->id); - $domain->set_base_vm(vm => $vm, value => 0, user => user_admin); is($domain->is_base(),0); - eval { is($domain->base_in_vm($vm->id), 0) }; - like($@,qr'is not a base'); - eval { is($domain->base_in_vm($node->id), 0) }; - like($@,qr'is not a base'); + is($domain->base_in_vm($vm->id), 0); + is($domain->base_in_vm($node->id), 0); + my $bases_vm = $domain->_bases_vm(1); + ok(!keys %$bases_vm); + is(scalar($domain->list_files_base()),0) or exit; user_admin->mark_all_messages_read(); my $req = Ravada::Request->set_base_vm( @@ -630,7 +677,7 @@ sub test_bases_node { ); rvd_back->_process_all_requests_dont_fork(); is($req->status,'done') or die Dumper($req); - is($req->error,''); + is($req->error,'') or confess Dumper($domain->id." ".$domain->name); is($domain->base_in_vm($vm->id), 1); is(scalar user_admin->unread_messages , 2, Dumper(user_admin->unread_messages)); @@ -641,8 +688,7 @@ sub test_bases_node { ,id_domain => $domain->id ); rvd_back->_process_all_requests_dont_fork(); - eval { $domain->base_in_vm($vm->id)}; - like($@,qr'is not a base'); + is($domain->base_in_vm($vm->id),0); $domain->remove(user_admin); } @@ -657,7 +703,7 @@ sub test_clone_make_base { $domain->prepare_base(user_admin); is($domain->base_in_vm($domain->_vm->id), 1); - is($domain->base_in_vm($node->id), 0) or exit; + is($domain->base_in_vm($node->id), 0); $domain->set_base_vm(vm => $node, user => user_admin); is($domain->base_in_vm($node->id), 1); @@ -667,14 +713,28 @@ sub test_clone_make_base { ,user => user_admin ); - $clone->migrate($node); - eval { $clone->base_in_vm($node->id) }; - like($@,qr(is not a base)); + Ravada::Request->migrate( + uid => user_admin->id + ,id_domain => $clone->id + ,id_node => $node->id + ); + wait_request(); - eval { $clone->base_in_vm($vm->id) }; - like($@,qr(is not a base)); + is($clone->base_in_vm($node->id),0); + is($clone->base_in_vm($vm->id),0); + + Ravada::Request->migrate( + uid => user_admin->id + ,id_domain => $clone->id + ,id_node => $vm->id + ); + wait_request(); - $clone->prepare_base(user_admin); + Ravada::Request->prepare_base( + uid => user_admin->id + ,id_domain=> $clone->id + ); + wait_request(); is($clone->base_in_vm($vm->id),1); is($clone->base_in_vm($node->id),0); @@ -732,20 +792,36 @@ sub test_clone_not_in_node { $domain->prepare_base(user_admin); is($domain->base_in_vm($vm->id), 1); - $domain->set_base_vm(vm => $node, user => user_admin); + Ravada::Request->set_base_vm( + uid => user_admin->id + ,id_domain => $domain->id + ,id_vm => $node->id + ); wait_request(debug => 0); - is($domain->base_in_vm($node->id), 1); + $domain->_data('id_vm' => $vm->id); my @clones; for ( 1 .. 10 ) { my $clone1 = $domain->clone(name => new_domain_name, user => user_admin); - diag($clone1->name); push @clones,($clone1); - is($clone1->_vm->host, 'localhost'); - eval { $clone1->start(user_admin) }; - is(''.$@,'',"[$vm_name] Clone of ".$domain->name." failed ".$clone1->name) or return; + Ravada::Request->start_domain( + uid => user_admin->id + ,id_domain =>$clone1->id + ); + wait_request(); + for ( 1 .. 10 ) { + last if $clone1->is_active; + $clone1 = Ravada::Domain->open($clone1->id); + Ravada::Request->refresh_machine( + uid => user_admin->id + ,id_domain => $clone1->id + ); + wait_request(); + $clone1 = Ravada::Domain->open($clone1->id); + } is($clone1->is_active,1); + $clone1 = Ravada::Domain->open($clone1->id); # search the domain in the underlying VM if ($vm_name eq 'KVM') { @@ -763,10 +839,7 @@ sub test_clone_not_in_node { isnt($clones[-1]->_vm->host, $clones[0]->_vm->host,"[$vm_name] " .$clones[-1]->name ." - ".$clones[0]->name.Dumper({map {$_->name => $_->_vm->host} @clones})) or exit; - for (@clones) { - $_->remove(user_admin); - } - $domain->remove(user_admin); + remove_domain_and_clones_req($domain); } sub test_domain_already_started { @@ -777,15 +850,24 @@ sub test_domain_already_started { my $domain = create_domain($vm_name); $domain->prepare_base(user_admin); - $domain->set_base_vm(vm => $node, user => user_admin); + Ravada::Request->set_base_vm( + uid => user_admin->id + ,id_domain => $domain->id + ,id_vm => $node->id + ); + wait_request(); is($domain->base_in_vm($node->id), 1); my $clone = $domain->clone(name => new_domain_name, user => user_admin); - is($clone->_vm->host, 'localhost'); - eval { $clone->migrate($node) }; - is(''.$@,'') or return; + Ravada::Request->migrate( + uid => user_admin->id + ,id_domain => $clone->id + ,id_node => $node->id + ); + wait_request(); + $clone = Ravada::Domain->open($clone->id); is($clone->_vm->host, $node->host); is($clone->_vm->id, $node->id) or return; @@ -831,15 +913,13 @@ sub test_prepare_sets_vm { my $vm = rvd_back->search_vm($vm_name); my $domain = create_domain($vm_name); - eval { $domain->base_in_vm($vm->id) }; - like($@,qr'is not a base'); + is($domain->base_in_vm($vm->id),0); $domain->prepare_base(user_admin); is($domain->base_in_vm($vm->id),1); $domain->remove_base(user_admin); - eval { $domain->base_in_vm($vm->id) }; - like($@,qr'is not a base'); + is($domain->base_in_vm($vm->id),0); $domain->remove(user_admin); } @@ -881,18 +961,45 @@ sub test_remove_base_main($node) { my $domain = create_domain($vm); $domain->prepare_base(user_admin); - $domain->set_base_vm(vm => $node, user => user_admin); + Ravada::Request->set_base_vm( + uid => user_admin->id + ,id_domain => $domain->id + ,id_vm => $node->id + ); + wait_request(); is($domain->base_in_vm($vm->id),1); is($domain->base_in_vm($node->id),1); - $domain->remove_base(user_admin); + Ravada::Request->remove_base( + uid => user_admin->id + ,id_domain => $domain->id + ); wait_request(debug => 0); is($domain->is_base,0); - $domain->prepare_base(user_admin); + + is($domain->base_in_vm($vm->id),0); + is($domain->base_in_vm($node->id),0); + + Ravada::Request->migrate( + uid => user_admin->id + ,id_domain => $domain->id + ,id_node => $vm->id + ); + wait_request(debug => 0); + + is($domain->base_in_vm($vm->id),0); + is($domain->base_in_vm($node->id),0); + + Ravada::Request->prepare_base( + uid => user_admin->id + ,id_domain => $domain->id + ); + wait_request(debug => 0); is($domain->base_in_vm($vm->id),1); - is($domain->base_in_vm($node->id),0) or exit; + is($domain->base_in_vm($node->id),0 + ,"Expecting no base in node ".$node->name." ".$node->id) or exit; $domain->remove(user_admin); } @@ -927,6 +1034,7 @@ sub test_node_inactive($vm_name, $node) { } sub test_sync_back($node) { + #TODO diag("Testing sync back on remote non shared storage node"); my $vm = rvd_back->search_vm($node->type, 'localhost'); my $domain = create_domain($vm); @@ -952,6 +1060,11 @@ sub test_sync_back($node) { _shutdown_nicely($clone); fast_forward_requests(); + Ravada::Request->migrate( + uid => user_admin->id + ,id_domain => $clone->id + ,id_node => $vm->id + ); wait_request(); is ( $clone->is_active, 0 ); for my $file ($clone->list_volumes) { @@ -1038,6 +1151,12 @@ sub test_shutdown($node) { my $clone2 = Ravada::Domain->open($clone->id); #open will clean internal shutdown is($clone2->is_active,0) or exit; + Ravada::Request->migrate( + uid => user_admin->id + ,id_domain => $clone->id + ,id_node => $vm->id + ); + wait_request(); for my $file ($clone->list_volumes) { my $md5 = _md5($file, $vm); @@ -1163,12 +1282,14 @@ SKIP: { diag("Testing remote node in $vm_name"); + test_rsync_newer($vm_name, $node); ok($node->vm,"[$vm_name] expecting a VM inside the node") or do { remove_node($node); next; }; is($node->is_local,0,"Expecting ".$node->name." ".$node->ip." is remote" ) or BAIL_OUT(); + test_remove_base_main($node); test_already_started_hibernated($vm_name, $node); is($vm->is_active,1); @@ -1180,12 +1301,10 @@ SKIP: { test_migrate_back($node); test_remove_base_main($node); - test_status($node); test_bases_node($vm_name, $node); test_clone_make_base($vm_name, $node); test_sync_base($vm_name, $node); - test_sync_back($node); test_status($node); diff --git a/t/nodes/00_req.t b/t/nodes/00_req.t index 88fb6240a..781d936fd 100644 --- a/t/nodes/00_req.t +++ b/t/nodes/00_req.t @@ -169,6 +169,36 @@ sub test_req_migrate_nested($vm, $node1) { remove_domain_db($clone); } +sub test_req_gone($vm) { + + my $base = create_domain($vm); + + my $bases_vm = $base->_bases_vm_info(); + is($bases_vm->{$vm->id}->{enabled},0) or exit; + + my $req = Ravada::Request->set_base_vm( + uid => user_admin->id + ,id_domain => $base->id + ,id_vm => $vm->id + ); + $bases_vm = $base->_bases_vm_info(); + is($bases_vm->{$vm->id}->{enabled},0) or exit; + is($bases_vm->{$vm->id}->{id_request},$req->id); + $req->status('done'); + + Ravada::Request->refresh_machine( + uid => user_admin->id + ,id_domain => $base->id + ); + wait_request(); + + $bases_vm = $base->_bases_vm_info(); + is($bases_vm->{$vm->id}->{enabled},0) or exit; + is($bases_vm->{$vm->id}->{id_request},undef); + + remove_domain($base); +} + sub test_req_failed($vm) { my $base = create_domain($vm); @@ -335,6 +365,8 @@ for my $vm_name (vm_names() ) { my $node1 = _create_remote_node($vm_name); my $node2 = _create_remote_node($vm_name); + test_req_gone($vm); + test_req_failed($vm); test_req_prepare_nested($vm, $node1); test_req_migrate_nested($vm, $node1); diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index 21f47eff2..08aedee50 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -761,16 +761,15 @@ sub test_volatile_req_clone($vm, $node, $machine='pc-i440fx') { my $base = create_domain_v2(vm => $vm, options => { machine => $machine }); $base->prepare_base(user_admin); + $base->volatile_clones(1); my $req = Ravada::Request->set_base_vm( uid => user_admin->id ,id_domain => $base->id ,id_vm => $node->id ,value => 1 ); - _check_base_in_vm_db($base, $node->id,$req->id, 1); - $base->volatile_clones(1); - ok($base->base_in_vm($node->id)); - _check_base_in_vm_db($base, $node->id,$req->id, 1); + _check_base_in_vm_db($base, $node->id,$req->id, 0); + ok(!$base->base_in_vm($node->id)); wait_request(debug => 0); _check_base_in_vm_db($base, $node->id,undef, 1); @@ -1444,7 +1443,8 @@ sub test_fill_memory($vm, $node, $migrate, $start=0) { } $memory *= 1.5; } - ok($created_in_node,"Expecting some clones created in node ".$node->name) or exit; + ok($created_in_node,"Expecting some clones created in node ".$node->name) + or confess; ok(exists $nodes{$vm->name},"Expecting some clones to node ".$vm->name." ".$vm->id) or die Dumper(\%nodes); ok(exists $nodes{$node->name},"Expecting some clones to node ".$node->name." ".$node->id) or exit; @@ -1671,6 +1671,7 @@ sub test_volumes_levels($domain, $level) { for my $vol ($domain->list_volumes_info) { next if !$vol->file; + next if $vol->file !~ /.qcow/; my @backings = _get_backing_files($vol->file); for my $file (@backings) { like($file ,qr{-vd[a-z][\.-]},$domain->name) or exit; @@ -1694,6 +1695,7 @@ sub _get_backing_xml($disk) { } sub test_domain_volumes_levels($domain, $level) { + return if $domain->type eq 'Void'; my $doc =XML::LibXML->load_xml(string => $domain->xml_description()); my $found = 0; @@ -2356,6 +2358,8 @@ for my $vm_name (reverse vm_names() ) { start_node($node); + test_nested_base($vm, $node, 3); + test_base_unset($vm,$node); test_set_vm($vm, $node); From afcff3b27192cec518ca5c624bde9f1bcb048142 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 20 Apr 2026 16:58:51 +0200 Subject: [PATCH 092/141] wip: use requests in tests --- t/kvm/n10_nodes.t | 43 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/t/kvm/n10_nodes.t b/t/kvm/n10_nodes.t index 303d3ac54..b2d4c0a14 100644 --- a/t/kvm/n10_nodes.t +++ b/t/kvm/n10_nodes.t @@ -940,19 +940,41 @@ sub test_remove_base($node) { is($domain->base_in_vm($vm->id),1); is($domain->base_in_vm($node->id),0); - $domain->set_base_vm(vm => $node, user => user_admin); + Ravada::Request->set_base_vm( + uid => user_admin->id + ,id_domain => $domain->id + , id_vm => $node->id + ); + wait_request(); is($domain->base_in_vm($node->id),1); + Ravada::Request->remove_base_vm( + uid => user_admin->id + ,id_domain => $domain->id + , id_vm => $vm->id + ); + Ravada::Request->remove_base_vm( + uid => user_admin->id + ,id_domain => $domain->id + , id_vm => $node->id + ); - $domain->remove_base_vm(user => user_admin, vm => $vm); + wait_request(); is($domain->is_base,0); - wait_request(debug => 0); + $domain = Ravada::Domain->open($domain->id); - $domain->prepare_base(user_admin); - is($domain->base_in_vm($vm->id),1); - is($domain->base_in_vm($node->id),0); + Ravada::Request->prepare_base( + uid => user_admin->id + ,id_domain => $domain->id + ); + wait_request(); + is($domain->base_in_vm($domain->_data('id_vm')),1); - $domain->remove(user_admin); + my $id_other_vm = $node->id; + $id_other_vm = $vm->id if $id_other_vm == $domain->_data('id_vm'); + is($domain->base_in_vm($id_other_vm),0); + + remove_domain($domain); } sub test_remove_base_main($node) { @@ -1001,7 +1023,7 @@ sub test_remove_base_main($node) { is($domain->base_in_vm($node->id),0 ,"Expecting no base in node ".$node->name." ".$node->id) or exit; - $domain->remove(user_admin); + remove_domain($domain); } @@ -1282,11 +1304,14 @@ SKIP: { diag("Testing remote node in $vm_name"); - test_rsync_newer($vm_name, $node); ok($node->vm,"[$vm_name] expecting a VM inside the node") or do { remove_node($node); next; }; + test_remove_base($node); + test_domain_on_remote($vm_name, $node); + my $domain22 = test_domain($vm_name, $node); + test_remove_domain_from_local($vm_name, $node, $domain22) if $domain22; is($node->is_local,0,"Expecting ".$node->name." ".$node->ip." is remote" ) or BAIL_OUT(); test_remove_base_main($node); From e27d6f803e6c3aa996eebb05830799cb1010754f Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Tue, 21 Apr 2026 15:48:30 +0200 Subject: [PATCH 093/141] wip: chain migration when remove base --- lib/Ravada/Domain.pm | 3 +++ lib/Ravada/Request.pm | 25 ++++++++++++++++++++----- t/kvm/n10_nodes.t | 9 +++++++++ t/nodes/00_req.t | 38 ++++++++++++++++++++++++++++++-------- 4 files changed, 62 insertions(+), 13 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 2f2a16a07..652d08543 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -1992,6 +1992,9 @@ sub _assert_update($self, $table, $field, $value) { if ($field eq 'is_base' && !$value && $self->clones ) { confess "Error: You can not set $field=$value if there are clones"; } + if ($field eq 'id_base' && defined $value && $value == $self->id) { + confess "Error: A domain can not be base of itself"; + } } =head2 open diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index 3c26c278a..5621372e9 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -978,6 +978,15 @@ sub _validate_start_domain($self) { sub _validate_prepare_base($self) { $self->_validate_compact(); + for my $command (qw (prepare_base post_prepare_base + set_base_vm remove_base_vm )) { + + my $req= $self->_search_request($command + , id_domain => $self->args('id_domain')); + + $self->after_request($req->id) if $req; + } + my $req_create = $self->_search_request('create' , id_base=> $self->args('id_domain')); @@ -1721,10 +1730,12 @@ sub _validate_remove_base_vm($req) { return; } + my ($req_migrate, $req_rm); + my ($req_migrate_prev, $req_rm_prev); + for my $clone ($domain->clones) { # migrate clones to other vms - my $req_migrate; if ( $clone->{id_vm} == $id_vm ) { my $start = 0; $start = 1 if $clone->{status} eq 'active'; @@ -1735,20 +1746,24 @@ sub _validate_remove_base_vm($req) { ,shutdown => 1 ,start => $start ); - $req->after_request_ok($req_migrate->id); + $req_migrate->after_request_ok($req_migrate_prev->id) if $req_migrate_prev; + $req_migrate_prev = $req_migrate; } # remove child bases if ( $clone->{is_base} ) { - my $req_prev = Ravada::Request->remove_base_vm( + $req_rm = Ravada::Request->remove_base_vm( uid => Ravada::Utils::user_daemon->id ,id_domain => $clone->{id} ,id_vm => $id_vm ); - $req_prev->after_request_ok($req_migrate->id) if $req_migrate; - $req->after_request_ok($req_prev->id); + $req_rm->after_request_ok($req_migrate->id) if $req_migrate; + $req_rm->after_request_ok($req_rm_prev->id) if $req_rm_prev; + $req_rm_prev = $req_rm; } } + $req->after_request_ok($req_rm->id) if $req_rm; + $req->after_request_ok($req_migrate->id) if $req_migrate; } diff --git a/t/kvm/n10_nodes.t b/t/kvm/n10_nodes.t index b2d4c0a14..c71860d79 100644 --- a/t/kvm/n10_nodes.t +++ b/t/kvm/n10_nodes.t @@ -183,6 +183,15 @@ sub test_domain { ); wait_request(); + for ( 1 .. 3 ) { + last if $clone->is_active; + Ravada::Request->refresh_machine( + uid => user_admin->id + ,id_domain => $clone->id + ); + wait_request(); + $clone = Ravada::Domain->open($clone->id); + } is($clone->is_active,1) or return; my $local_ip = $node->ip; diff --git a/t/nodes/00_req.t b/t/nodes/00_req.t index 781d936fd..a3c3bf7e7 100644 --- a/t/nodes/00_req.t +++ b/t/nodes/00_req.t @@ -246,10 +246,16 @@ sub test_req_prepare_nested($vm, $node1) { $base2->_data('is_base' => 1); $base2->_data('id_base' => $base1->id); + my $clone2 = create_base($vm); + $clone2->_data('id_base' => $base1->id); + my $base3 = create_base($vm); - $base2->_data('is_base' => 1); + $base3->_data('is_base' => 1); $base3->_data('id_base' => $base2->id); + my $clone3 = create_base($vm); + $clone3->_data('is_base' => 1); + my $clone = create_domain($vm); $clone->_data('id_base' => $base3->id); @@ -306,15 +312,21 @@ sub test_req_prepare_nested($vm, $node1) { } sub test_req_remove_base_nested($vm, $node1, $clone, $base3, $base2, $base1) { + for ( 1 .. 3 ) { + my $clone = create_domain($vm); + $clone->_data('id_base' => $base1->id); + $clone->_data('id_vm' => $node1->id); + } + my $req = Ravada::Request->remove_base_vm( uid => user_admin->id ,id_domain => $base1->id ,id_vm => $node1->id ); - test_remove_requirements($base1, $req); + test_remove_requirements($base1, $node1->id, $req); } -sub test_remove_requirements($base, $req) { +sub test_remove_requirements($base, $id_node, $req) { my @clones = $base->clones; return if !@clones; @@ -322,12 +334,11 @@ sub test_remove_requirements($base, $req) { ok($id_req_prev) or die "Expecting requirements for " .$req->id." ".$req->command." ".Dumper($req->args); $id_req_prev=[$id_req_prev] if !ref($id_req_prev); + for my $clone ( @clones ) { - my $req_prev; - for my $id (@$id_req_prev) { - $req_prev = Ravada::Request->open($id); - last if $req_prev->id_domain == $clone->{id}; - } + next if $clone->{id_vm} != $id_node; + my $req_prev = _search_request_previous($clone->{id}, $id_req_prev); + ok($req_prev, "Expecting a request to migrate clone ".$clone->{id}." ".$clone->{name}) or exit; ok($req_prev, "Expecting req for clone $clone->{id}") or die ; if ($clone->{is_base}) { my $base2 = Ravada::Front::Domain->open($clone->{id}); @@ -336,6 +347,17 @@ sub test_remove_requirements($base, $req) { } } +sub _search_request_previous($id_domain, $id_req_prev) { + return if !$id_req_prev; + $id_req_prev = [$id_req_prev] if !ref($id_req_prev); + for my $id (@$id_req_prev) { + my $req_prev = Ravada::Request->open($id); + return $req_prev if $req_prev->id_domain == $id_domain; + + $req_prev = _search_request_previous($id_domain, $req_prev->after_request_ok()); + return $req_prev if $req_prev; + } +} ############################################################################### From 6e43ddbe7d88520815ea116d82ae024ee577ebd9 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 22 Apr 2026 16:46:09 +0200 Subject: [PATCH 094/141] wip: chain already requested clones --- lib/Ravada.pm | 2 +- lib/Ravada/Request.pm | 20 ++++++++++++++++++++ lib/Ravada/VM.pm | 12 ------------ 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index 689566937..1833223de 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -3,7 +3,7 @@ package Ravada; use warnings; use strict; -our $VERSION = '2.5.0-alpha4'; +our $VERSION = '2.5.0-alpha5'; use utf8; diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index 5621372e9..dc2ce02cd 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -270,6 +270,7 @@ our %COMMAND = ( ,'remove_domain', 'remove', 'refresh_machine_ports' ,'connect_node','start_node','shutdown_node' ,'post_login' + ,'migrate','post_migrate' ] } @@ -1764,8 +1765,27 @@ sub _validate_remove_base_vm($req) { } $req->after_request_ok($req_rm->id) if $req_rm; $req->after_request_ok($req_migrate->id) if $req_migrate; + + _chain_requested_clone($req, $domain->id); } +sub _chain_requested_clone($req, $id_domain) { + + for my $req_clone ( + $req->_search_request('clone', id_domain => $id_domain) + , $req->_search_request('create_domain', id_base => $id_domain) + ) { + next if $req_clone->status eq 'done'; + + # if it is requested chain after this one + if ($req_clone->status eq 'requested') { + $req_clone->after_request($req->id); + # if running, chain this request after + } elsif ($req_clone->status() eq 'running') { + $req->after_request($req_clone->id); + } + } +} =head2 type diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index 19b386f6d..e2392b6c4 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -1064,18 +1064,6 @@ sub _check_require_base { my %ignore_requests = map { $_ => 1 } qw(clone refresh_machine set_base_vm start_clones shutdown_clones shutdown force_shutdown refresh_machine_ports set_time open_exposed_ports manage_pools screenshot remove_clones list_cpu_models ); - my @requests; - for my $req ( $base->list_requests ) { - push @requests,($req) if !$ignore_requests{$req->command}; - } - if (@requests) { - confess "ERROR: Domain ".$base->name." has ".scalar(@requests) - ." requests.\n" - .Dumper(\@requests) - unless scalar @requests == 1 && $request - && $requests[0]->id eq $request->id; - } - die "ERROR: Domain ".$self->name." is not base" if !$base->is_base(); From 768c2f0c493e997dccfaf9c3a4d39b852c566e67 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 22 Apr 2026 17:02:54 +0200 Subject: [PATCH 095/141] wip: error just dies --- lib/Ravada/Domain.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 652d08543..12f29d192 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -5630,7 +5630,7 @@ sub _pre_migrate($self, $node, $request = undef) { $self->_internal_autostart(0); $self->check_status(); - confess "ERROR: Active domains can't be migrated" if $self->is_active; + die "ERROR: Active domains can't be migrated.\n" if $self->is_active; if ( $self->id_base ) { my $base = Ravada::Domain->open($self->id_base); From bc0ac8c5a700bc8e1758972e20c7498f630e8f39 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Fri, 24 Apr 2026 15:10:37 +0200 Subject: [PATCH 096/141] wip: remote storage pools --- lib/Ravada/Domain.pm | 29 ++++++++------------- t/lib/Test/Ravada.pm | 10 ++++++++ t/nodes/70_shared.t | 61 +++++++++++++++++++++++++++++--------------- 3 files changed, 61 insertions(+), 39 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 12f29d192..e07c6f034 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -2824,7 +2824,6 @@ sub _remove_instance($self, $user, $cascade, $id_instance=undef) { $self->remove_instance($user); }; warn $@ if $@; - $self->_remove_files_base(); $sth_delete->execute($id_instance) if defined $id_instance; } @@ -5639,6 +5638,7 @@ sub _pre_migrate($self, $node, $request = undef) { confess "ERROR: base id ".$self->id_base." not found." if !$base; } $node->_add_instance_db($self->id); + $node->refresh_storage_pools(); } sub _post_migrate($self, $node, $request = undef) { @@ -5650,6 +5650,7 @@ sub _post_migrate($self, $node, $request = undef) { # TODO: update db instead set this value $self->{_migrated} = 1; + $node->refresh_storage_pools(); } sub _around_migrate($orig, $self, $node, $request=undef) { @@ -5762,6 +5763,7 @@ sub set_base_vm($self, %args) { $request->status("working", "Syncing base volumes to ".$vm->host) if $request; + $vm->refresh_storage_pools(); eval { $self->migrate($vm, $request); }; @@ -5776,34 +5778,35 @@ sub set_base_vm($self, %args) { } else { $request->status("working","Removing base") if $request; - my $bases_vm = $self->_bases_vm(); + + my $bases_vm = $self->_bases_vm(1); my @nodes; my $node2; for my $id_vm ( keys %$bases_vm) { - next if $id_vm == $vm->id; - if ($bases_vm->{$id_vm}) { + if ( $id_vm != $vm->id ) { my $node = Ravada::VM->open($id_vm); - push @nodes,($node); $node2 = $node if $node->is_local(); + push @nodes,($node); } } ($node2) = @nodes if !$node2; if (!@nodes) { $self->_vm($vm); $self->_do_remove_base($user); + $vm->refresh_storage_pools(); } else { my $instance = $vm->search_domain($self->name); $instance->_remove_instance($user,1) if $instance; - $self->_remove_files_not_shared(@nodes); + $self->_remove_files_not_shared(@nodes, $self->_vm); $self->_vm($node2); $self->_data('id_vm' => $node2->id); + $node2->refresh_storage_pools(); } } $self->_set_base_vm_db($vm->id, $value); if (!$value) { - my $bases_vm = $self->_bases_vm(1); #enabled bases vm if (!keys %$bases_vm) { $self->_post_remove_base(); @@ -6868,13 +6871,6 @@ sub _add_hardware_disk($orig, $self, $index, $data) { die "Error: new disk volumes can not be added to bases\n" if $self->is_base; - my $real_id_vm; - if (!$self->_vm->is_local) { - $real_id_vm = $self->_vm->id; - my $vm_local = $self->_vm->new( host => 'localhost' ); - $self->_set_vm($vm_local, 1); - } - $self->_check_duplicated_volume_name($data->{file}); $orig->($self, 'disk', $index, $data); @@ -6885,11 +6881,6 @@ sub _add_hardware_disk($orig, $self, $index, $data) { $self->list_volumes_info(); $self->_redefine_instances(); - if ( $real_id_vm ) { - my $id_vm = $real_id_vm; - my $vm = Ravada::VM->open($id_vm); - $self->_set_vm($vm, 1); - } } sub _fix_filesystem_data($self,$data) { diff --git a/t/lib/Test/Ravada.pm b/t/lib/Test/Ravada.pm index 7a0ee9a09..30be73696 100644 --- a/t/lib/Test/Ravada.pm +++ b/t/lib/Test/Ravada.pm @@ -64,6 +64,7 @@ create_domain create_ram_fs create_storage_pool + start_storage_pool local_ips wait_request @@ -1243,6 +1244,15 @@ sub wait_mojo_request($t, $url) { _wait_mojo_request($t, $url); } +sub start_storage_pool($vm, $sp_name) { + my $sp = $vm->vm->get_storage_pool_by_name($sp_name); + return if !$sp; + return $sp if $sp->is_active(); + + $sp->create(); + return $sp; +} + sub _activate_storage_pools($vm) { my @sp = $vm->vm->list_all_storage_pools(); for my $sp (@sp) { diff --git a/t/nodes/70_shared.t b/t/nodes/70_shared.t index 1e0ee0174..9e51fdcec 100644 --- a/t/nodes/70_shared.t +++ b/t/nodes/70_shared.t @@ -35,7 +35,7 @@ sub test_shared($vm, $node) { ,id_domain => $domain->id ,id_vm => $node->id ); - rvd_back->_process_requests_dont_fork(1); + wait_request(); ok($req->status, 'done'); is($req->error, '') or exit; @@ -61,7 +61,7 @@ sub test_shared($vm, $node) { ,id_vm => $node->id ,value => 0 ); - rvd_back->_process_requests_dont_fork(1); + wait_request(); ok($req->status, 'done'); is($req->error, '') or exit; @@ -70,12 +70,7 @@ sub test_shared($vm, $node) { is($domain->base_in_vm($vm->id),1); for my $vol (@files_base) { - my $ok; - for ( 1 .. 5 ) { - $ok = -e $vol; - last if $ok; - sleep 1; - } + my $ok = $vm->file_exists($vol); ok($ok,"Volume $vol should exist") or exit; ok($node->file_exists($vol), "Volume $vol should exist in ".$node->name); } @@ -162,14 +157,16 @@ sub test_change_ram($vm, $node, $start=0, $prepare_base=0, $migrate=0) { sub _test_volumes_exist($domain) { my $domain_local = Ravada::Domain->open($domain->id); + my $vm = $domain_local->_vm; + $vm->refresh_storage(); for my $disk ( $domain_local->list_volumes ) { - ok(-e $disk,"Expecting ".$domain_local->name." $disk exist in " + ok($vm->file_exists($disk),"Expecting ".$domain_local->name." $disk exist in " .$domain_local->_vm->name) or confess; } if ($domain->id_base) { my $base = Ravada::Domain->open($domain->id_base); for my $disk ($base->list_files_base) { - ok(-e $disk,"Expecting $disk exist"); + ok($vm->file_exists($disk),"Expecting $disk exist"); } } } @@ -180,9 +177,21 @@ sub test_add_disk($vm, $node, $start=0, $prepare_base=0, $migrate=0) { my $domain = $base; if ($prepare_base) { - $base->prepare_base(user_admin); - $base->set_base_vm(vm => $node, user => user_admin); + Ravada::Request->prepare_base( + uid => user_admin->id + ,id_domain => $domain->id + ); + wait_request(); $domain = $base->clone(name => new_domain_name, user => user_admin); + for my $node0 ( $vm, $node ) { + Ravada::Request->set_base_vm( + uid => user_admin->id + ,id_domain => $base->id + ,id_vm => $node0->id + ); + } + wait_request(); + } my $n = scalar($domain->list_volumes); my @volumes0 = map { $_->{file} } $domain->list_volumes_info; @@ -206,8 +215,7 @@ sub test_add_disk($vm, $node, $start=0, $prepare_base=0, $migrate=0) { is(scalar($domain->list_volumes),$n+1); _test_volumes_exist($domain); - $domain->remove(user_admin); - # $base->remove(user_admin) if $base->id != $domain->id; + remove_domain($base); } sub req_start($domain) { @@ -233,8 +241,8 @@ sub req_migrate($node, $domain, $start=0) { is($req->status,'done'); is($req->error,''); - my $domain2 = Ravada::Domain->open($domain->id); - is($domain2->_vm->id,$node->id); + $domain = Ravada::Domain->open($domain->id); + is($domain->_vm->id,$node->id); } sub import_base($vm) { @@ -246,6 +254,14 @@ sub import_base($vm) { } } +sub _check_sp($sp, @nodes) { + for my $node (@nodes) { + my $sp = $node->vm->get_storage_pool_by_name($sp); + confess "SP $sp not active in ".$node->name if !$sp->is_active; + } + +} + ################################################################################# if ($>) { my $msg = "SKIPPED: Test must run as root"; @@ -283,8 +299,11 @@ the file " } if ($vm && !grep /^$SHARED_SP$/,$vm->list_storage_pools) { - $msg = "SKIPPED: Missing storage pool '$SHARED_SP' in node ".$vm->name; - $vm = undef; + my $sp = start_storage_pool($vm,$SHARED_SP); + if (!$sp) { + $msg = "SKIPPED: Missing storage pool '$SHARED_SP' in node ".$vm->name; + $vm = undef; + } } diag($msg) if !$vm; @@ -313,8 +332,6 @@ the file " import_base($vm); - test_is_shared($vm, $node); - test_shared($vm, $node); for my $default_sp ( 0,1 ) { $node->default_storage_pool_name('default') if !$default_sp; @@ -322,6 +339,7 @@ the file " for my $start ( 0,1 ) { for my $prepare_base ( 0,1 ) { for my $migrate( 1, 0 ) { + _check_sp($SHARED_SP, $vm, $node); test_change_ram($vm,$node, $start, $prepare_base, $migrate); test_add_disk($vm,$node, $start, $prepare_base, $migrate); } @@ -329,6 +347,9 @@ the file " } } + test_is_shared($vm, $node); + test_shared($vm, $node); + NEXT: clean_remote_node($node); remove_node($node); From 6261810259b2b9053091be936c2e64b2b45a041b Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 27 Apr 2026 11:00:33 +0200 Subject: [PATCH 097/141] doc: we usually work with main branch --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index adff66f7b..9836e519b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -241,14 +241,14 @@ git fetch upstream Now we create a new branch: ```sh -git checkout develop -git checkout -b feature/cool_thing upstream/develop +git checkout main +git checkout -b feature/cool_thing upstream/main ``` Reset this branch, now it will be an exact replica of upstream UPC/develop: ```sh -git reset --hard upstream/develop +git reset --hard upstream/main git push --set-upstream origin feature/cool_thing ``` From f81b66a9674a5037e503b752daf617670fc38e30 Mon Sep 17 00:00:00 2001 From: Yago004 <147386653+Yago004@users.noreply.github.com> Date: Tue, 28 Apr 2026 12:58:45 +0200 Subject: [PATCH 098/141] Ubuntu 26.04 iso changes (#2312) * Add 26.04 to Ubuntu based ISOs * Update RAM requirements (26.04) * Fix ubuntu 26.04 name * Update lib/Ravada.pm Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix rename_file kubuntu --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/Ravada.pm | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index 401e53d1e..d53a91eaf 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -517,6 +517,21 @@ sub _update_isos { ,arch => 'x86_64' } + ,ubuntu_resolute => { + name => 'Ubuntu 26.04 Resolute Raccoon' + ,description => 'Ubuntu 26.04 Resolute Raccoon 64 bits' + ,xml => 'noble-amd64.xml' + ,xml_volume => 'focal_fossa64-volume.xml' + ,url => 'http://releases.ubuntu.com/26.04/' + ,file_re => '^ubuntu-26.04.*-desktop-amd64.iso' + ,sha256_url => '$url/SHA256SUMS' + ,min_disk_size => '25' + ,min_ram => 6 + ,options => { machine => 'pc-q35', bios => 'UEFI' } + ,arch => 'x86_64' + + } + @@ -594,6 +609,21 @@ sub _update_isos { ,sha256_url => '$url/alpine-standard-3.23.*.iso.sha256' ,min_disk_size => '1' } + ,kubuntu_64_resolute => { + name => 'Kubuntu 26.04 Resolute Raccoon' + ,description => 'Kubuntu 26.04 Resolute Raccoon 64 bits' + ,arch => 'x86_64' + ,xml => 'noble-amd64.xml' + ,xml_volume => 'focal_fossa64-volume.xml' + ,sha256_url => '$url/SHA256SUMS' + ,url => 'http://cdimage.ubuntu.com/kubuntu/releases/26.04.*/release/' + ,file_re => 'kubuntu-26.04.*-desktop-amd64.iso' + ,rename_file => 'kubuntu_resolute.iso' + ,options => { machine => 'pc-q35', bios => 'UEFI' } + ,min_ram => 6 + ,min_disk_size => '25' + } + ,kubuntu_64_noble => { name => 'Kubuntu 24.04 Noble Nombat' ,description => 'Kubuntu 24.04 Noble Nombat 64 bits' @@ -657,6 +687,18 @@ sub _update_isos { ,file_re => 'agama-installer\.x86_64-.*-Leap_16\.0-Build.*\.iso' } + ,xubuntu_resolute => { + name => 'Xubuntu 26.04 Resolute Raccoon 64 bits' + ,description => 'Xubuntu 26.04 Resolute Raccoon 64 bits' + ,arch => 'x86_64' + ,xml => 'bionic-amd64.xml' + ,xml_volume => 'bionic64-volume.xml' + ,url => 'https://ftp.lysator.liu.se/ubuntu-dvd/xubuntu/releases/26.04.*/release/' + ,file_re => 'xubuntu.*desktop.*.iso' + ,options => { machine => 'pc-q35', bios => 'UEFI' } + ,min_ram => 2 + ,min_disk_size => '20' + } ,xubuntu_noble => { name => 'Xubuntu 24.04 Noble Nombat 64 bits' ,description => 'Xubuntu 24.04 Noble Nombat 64 bits' @@ -682,9 +724,22 @@ sub _update_isos { ,min_ram => 2 ,min_disk_size => '20' } + ,lubuntu_resolute => { + name => 'Lubuntu 26.04 Resolute Raccoon' + ,description => 'Lubuntu 26.04 Resolute Raccoon 64 bits (LTS)' + ,url => 'http://cdimage.ubuntu.com/lubuntu/releases/26.04.*/release/' + ,file_re => 'lubuntu-26.04.*-desktop-amd64.iso' + ,sha256_url => '$url/SHA256SUMS' + ,xml => 'yakkety64-amd64.xml' + ,xml_volume => 'yakkety64-volume.xml' + ,min_disk_size => '10' + ,min_ram => 2 + ,arch => 'x86_64' + ,options => { machine => 'pc-q35', bios => 'UEFI' } + } ,lubuntu_noble => { name => 'Lubuntu 24.04 Noble Nombat' - ,description => 'Xubuntu 24.04 Noble Nombat 64 bits (LTS)' + ,description => 'Lubuntu 24.04 Noble Nombat 64 bits (LTS)' ,url => 'http://cdimage.ubuntu.com/lubuntu/releases/24.04.*/release/' ,file_re => 'lubuntu-24.04.*-desktop-amd64.iso' ,sha256_url => '$url/SHA256SUMS' From 583e5caf950952d9e1ac937d9175f144637a4371 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:36:50 +0000 Subject: [PATCH 099/141] Initial plan From 64dfc806159105055fe8f2c55f9e939a83d8af5d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:42:07 +0000 Subject: [PATCH 100/141] Add translatable text routes for chart time options and update admin.js to use them Agent-Logs-Url: https://github.com/UPC/ravada/sessions/6b81e06e-9f7d-4e83-948b-18c3d7c73941 Co-authored-by: frankiejol <136835+frankiejol@users.noreply.github.com> --- lib/Ravada/I18N/en.po | 84 +++++++++++++++++++++++++++++++++++++++++++ lib/Ravada/I18N/es.po | 84 +++++++++++++++++++++++++++++++++++++++++++ public/js/admin.js | 53 ++++++++------------------- script/rvd_front | 63 ++++++++++++++++++++++++++++++++ 4 files changed, 246 insertions(+), 38 deletions(-) diff --git a/lib/Ravada/I18N/en.po b/lib/Ravada/I18N/en.po index 11b2b3525..4ac874bfc 100644 --- a/lib/Ravada/I18N/en.po +++ b/lib/Ravada/I18N/en.po @@ -2206,3 +2206,87 @@ msgstr "Show Clones" msgid "starting" msgstr "starting" + +msgid "hours" +msgstr "hours" + +msgid "1 hour" +msgstr "1 hour" + +msgid "2 hours" +msgstr "2 hours" + +msgid "3 hours" +msgstr "3 hours" + +msgid "6 hours" +msgstr "6 hours" + +msgid "8 hours" +msgstr "8 hours" + +msgid "days" +msgstr "days" + +msgid "1 day" +msgstr "1 day" + +msgid "2 days" +msgstr "2 days" + +msgid "3 days" +msgstr "3 days" + +msgid "6 days" +msgstr "6 days" + +msgid "weeks" +msgstr "weeks" + +msgid "1 week" +msgstr "1 week" + +msgid "2 weeks" +msgstr "2 weeks" + +msgid "3 weeks" +msgstr "3 weeks" + +msgid "4 weeks" +msgstr "4 weeks" + +msgid "months" +msgstr "months" + +msgid "1 month" +msgstr "1 month" + +msgid "2 months" +msgstr "2 months" + +msgid "3 months" +msgstr "3 months" + +msgid "6 months" +msgstr "6 months" + +msgid "9 months" +msgstr "9 months" + +msgid "years" +msgstr "years" + +msgid "1 year" +msgstr "1 year" + +msgid "2 years" +msgstr "2 years" + +msgid "3 years" +msgstr "3 years" + +msgid "6 years" +msgstr "6 years" + +msgid "9 years" +msgstr "9 years" diff --git a/lib/Ravada/I18N/es.po b/lib/Ravada/I18N/es.po index 6c271ba85..7ee77ea40 100644 --- a/lib/Ravada/I18N/es.po +++ b/lib/Ravada/I18N/es.po @@ -2319,3 +2319,87 @@ msgstr "Compresión automática" msgid "Show Clones" msgstr "Mostrar clones" + +msgid "hours" +msgstr "horas" + +msgid "1 hour" +msgstr "1 hora" + +msgid "2 hours" +msgstr "2 horas" + +msgid "3 hours" +msgstr "3 horas" + +msgid "6 hours" +msgstr "6 horas" + +msgid "8 hours" +msgstr "8 horas" + +msgid "days" +msgstr "días" + +msgid "1 day" +msgstr "1 día" + +msgid "2 days" +msgstr "2 días" + +msgid "3 days" +msgstr "3 días" + +msgid "6 days" +msgstr "6 días" + +msgid "weeks" +msgstr "semanas" + +msgid "1 week" +msgstr "1 semana" + +msgid "2 weeks" +msgstr "2 semanas" + +msgid "3 weeks" +msgstr "3 semanas" + +msgid "4 weeks" +msgstr "4 semanas" + +msgid "months" +msgstr "meses" + +msgid "1 month" +msgstr "1 mes" + +msgid "2 months" +msgstr "2 meses" + +msgid "3 months" +msgstr "3 meses" + +msgid "6 months" +msgstr "6 meses" + +msgid "9 months" +msgstr "9 meses" + +msgid "years" +msgstr "años" + +msgid "1 year" +msgstr "1 año" + +msgid "2 years" +msgstr "2 años" + +msgid "3 years" +msgstr "3 años" + +msgid "6 years" +msgstr "6 años" + +msgid "9 years" +msgstr "9 años" diff --git a/public/js/admin.js b/public/js/admin.js index 1469eb827..7931c144d 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -1849,44 +1849,21 @@ ravadaApp.directive("solShowMachine", swMach) $scope.year = 0; var max_y = 10; - $scope.options_h = [ - {id:0, title: 'hours'} - ,{id:1 , title: '1 hour'} - ,{id:2 , title: '2 hours'} - ,{id:3 , title: '3 hours'} - ,{id:6 , title: '6 hours'} - ,{id:8 , title: '8 hours'} - ]; - $scope.options_d = [ - {id:0 , title: 'days'} - ,{id:1 , title: '1 day'} - ,{id:2 , title: '2 days'} - ,{id:3 , title: '3 days'} - ,{id:6 , title: '6 days'} - ]; - $scope.options_w = [ - {id:0 , title: 'weeks'} - ,{id:1 , title: '1 week'} - ,{id:2 , title: '2 weeks'} - ,{id:3 , title: '3 weeks'} - ,{id:4 , title: '4 weeks'} - ]; - $scope.options_m = [ - {id:0 , title: 'months'} - ,{id:1 , title: '1 month'} - ,{id:2 , title: '2 months'} - ,{id:3 , title: '3 months'} - ,{id:6 , title: '6 months'} - ,{id:9 , title: '9 months'} - ]; - $scope.options_y = [ - {id:0 , title: 'years'} - ,{id:1 , title: '1 year'} - ,{id:2 , title: '2 years'} - ,{id:3 , title: '3 years'} - ,{id:6 , title: '6 years'} - ,{id:9 , title: '9 years'} - ]; + $http.get('/text/hours').then(function(response) { + $scope.options_h = response.data; + }); + $http.get('/text/days').then(function(response) { + $scope.options_d = response.data; + }); + $http.get('/text/weeks').then(function(response) { + $scope.options_w = response.data; + }); + $http.get('/text/months').then(function(response) { + $scope.options_m = response.data; + }); + $http.get('/text/years').then(function(response) { + $scope.options_y = response.data; + }); var url; diff --git a/script/rvd_front b/script/rvd_front index eff6622f2..70dd48df3 100644 --- a/script/rvd_front +++ b/script/rvd_front @@ -3203,6 +3203,69 @@ get '/translations' => sub($c) { return $c->render(json => $lang_name); }; +get '/text/hours' => sub($c) { + my $lh = Ravada::I18N->get_handle($USER->language()); + my @options = ( + {id => 0, title => $lh->maketext('hours')} + ,{id => 1, title => $lh->maketext('1 hour')} + ,{id => 2, title => $lh->maketext('2 hours')} + ,{id => 3, title => $lh->maketext('3 hours')} + ,{id => 6, title => $lh->maketext('6 hours')} + ,{id => 8, title => $lh->maketext('8 hours')} + ); + return $c->render(json => \@options); +}; + +get '/text/days' => sub($c) { + my $lh = Ravada::I18N->get_handle($USER->language()); + my @options = ( + {id => 0, title => $lh->maketext('days')} + ,{id => 1, title => $lh->maketext('1 day')} + ,{id => 2, title => $lh->maketext('2 days')} + ,{id => 3, title => $lh->maketext('3 days')} + ,{id => 6, title => $lh->maketext('6 days')} + ); + return $c->render(json => \@options); +}; + +get '/text/weeks' => sub($c) { + my $lh = Ravada::I18N->get_handle($USER->language()); + my @options = ( + {id => 0, title => $lh->maketext('weeks')} + ,{id => 1, title => $lh->maketext('1 week')} + ,{id => 2, title => $lh->maketext('2 weeks')} + ,{id => 3, title => $lh->maketext('3 weeks')} + ,{id => 4, title => $lh->maketext('4 weeks')} + ); + return $c->render(json => \@options); +}; + +get '/text/months' => sub($c) { + my $lh = Ravada::I18N->get_handle($USER->language()); + my @options = ( + {id => 0, title => $lh->maketext('months')} + ,{id => 1, title => $lh->maketext('1 month')} + ,{id => 2, title => $lh->maketext('2 months')} + ,{id => 3, title => $lh->maketext('3 months')} + ,{id => 6, title => $lh->maketext('6 months')} + ,{id => 9, title => $lh->maketext('9 months')} + ); + return $c->render(json => \@options); +}; + +get '/text/years' => sub($c) { + my $lh = Ravada::I18N->get_handle($USER->language()); + my @options = ( + {id => 0, title => $lh->maketext('years')} + ,{id => 1, title => $lh->maketext('1 year')} + ,{id => 2, title => $lh->maketext('2 years')} + ,{id => 3, title => $lh->maketext('3 years')} + ,{id => 6, title => $lh->maketext('6 years')} + ,{id => 9, title => $lh->maketext('9 years')} + ); + return $c->render(json => \@options); +}; + sub _translations($c) { my $lang_name = _languages(); $c->stash(lang_name => $lang_name); From 2f8efad1c3c717d590fb5d7bb28c7105506b1798 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:43:54 +0000 Subject: [PATCH 101/141] Address code review: add language fallback and JS error handling Agent-Logs-Url: https://github.com/UPC/ravada/sessions/6b81e06e-9f7d-4e83-948b-18c3d7c73941 Co-authored-by: frankiejol <136835+frankiejol@users.noreply.github.com> --- public/js/admin.js | 10 ++++++++++ script/rvd_front | 10 +++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/public/js/admin.js b/public/js/admin.js index 7931c144d..97a6911cc 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -1851,18 +1851,28 @@ ravadaApp.directive("solShowMachine", swMach) var max_y = 10; $http.get('/text/hours').then(function(response) { $scope.options_h = response.data; + }).catch(function() { + $scope.options_h = [{id:0,title:'hours'},{id:1,title:'1 hour'},{id:2,title:'2 hours'},{id:3,title:'3 hours'},{id:6,title:'6 hours'},{id:8,title:'8 hours'}]; }); $http.get('/text/days').then(function(response) { $scope.options_d = response.data; + }).catch(function() { + $scope.options_d = [{id:0,title:'days'},{id:1,title:'1 day'},{id:2,title:'2 days'},{id:3,title:'3 days'},{id:6,title:'6 days'}]; }); $http.get('/text/weeks').then(function(response) { $scope.options_w = response.data; + }).catch(function() { + $scope.options_w = [{id:0,title:'weeks'},{id:1,title:'1 week'},{id:2,title:'2 weeks'},{id:3,title:'3 weeks'},{id:4,title:'4 weeks'}]; }); $http.get('/text/months').then(function(response) { $scope.options_m = response.data; + }).catch(function() { + $scope.options_m = [{id:0,title:'months'},{id:1,title:'1 month'},{id:2,title:'2 months'},{id:3,title:'3 months'},{id:6,title:'6 months'},{id:9,title:'9 months'}]; }); $http.get('/text/years').then(function(response) { $scope.options_y = response.data; + }).catch(function() { + $scope.options_y = [{id:0,title:'years'},{id:1,title:'1 year'},{id:2,title:'2 years'},{id:3,title:'3 years'},{id:6,title:'6 years'},{id:9,title:'9 years'}]; }); var url; diff --git a/script/rvd_front b/script/rvd_front index 70dd48df3..8bf1320f4 100644 --- a/script/rvd_front +++ b/script/rvd_front @@ -3204,7 +3204,7 @@ get '/translations' => sub($c) { }; get '/text/hours' => sub($c) { - my $lh = Ravada::I18N->get_handle($USER->language()); + my $lh = Ravada::I18N->get_handle($USER->language() || 'en'); my @options = ( {id => 0, title => $lh->maketext('hours')} ,{id => 1, title => $lh->maketext('1 hour')} @@ -3217,7 +3217,7 @@ get '/text/hours' => sub($c) { }; get '/text/days' => sub($c) { - my $lh = Ravada::I18N->get_handle($USER->language()); + my $lh = Ravada::I18N->get_handle($USER->language() || 'en'); my @options = ( {id => 0, title => $lh->maketext('days')} ,{id => 1, title => $lh->maketext('1 day')} @@ -3229,7 +3229,7 @@ get '/text/days' => sub($c) { }; get '/text/weeks' => sub($c) { - my $lh = Ravada::I18N->get_handle($USER->language()); + my $lh = Ravada::I18N->get_handle($USER->language() || 'en'); my @options = ( {id => 0, title => $lh->maketext('weeks')} ,{id => 1, title => $lh->maketext('1 week')} @@ -3241,7 +3241,7 @@ get '/text/weeks' => sub($c) { }; get '/text/months' => sub($c) { - my $lh = Ravada::I18N->get_handle($USER->language()); + my $lh = Ravada::I18N->get_handle($USER->language() || 'en'); my @options = ( {id => 0, title => $lh->maketext('months')} ,{id => 1, title => $lh->maketext('1 month')} @@ -3254,7 +3254,7 @@ get '/text/months' => sub($c) { }; get '/text/years' => sub($c) { - my $lh = Ravada::I18N->get_handle($USER->language()); + my $lh = Ravada::I18N->get_handle($USER->language() || 'en'); my @options = ( {id => 0, title => $lh->maketext('years')} ,{id => 1, title => $lh->maketext('1 year')} From e10c61d52a3e380352d08c198fcbd12061658d37 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 30 Apr 2026 16:15:35 +0200 Subject: [PATCH 102/141] wip: remove domains in shared storage --- lib/Ravada.pm | 16 ++------ lib/Ravada/Domain.pm | 30 ++------------ lib/Ravada/Domain/KVM.pm | 24 +++++------- lib/Ravada/Request.pm | 20 ++++++++++ lib/Ravada/VM/KVM.pm | 2 +- t/lib/Test/Ravada.pm | 47 ++++++++++++++++++---- t/nodes/70_shared.t | 84 +++++++++++++++++++++++++++++++++------- 7 files changed, 146 insertions(+), 77 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index 1833223de..c97dd4e4d 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -6312,6 +6312,10 @@ sub _cmd_migrate($self, $request) { my $id_domain = $request->args('id_domain') or die "ERROR: Missing id_domain"; my $user = Ravada::Auth::SQL->search_by_id($uid); + + my $domain_f = Ravada::Front::Domain->open($id_domain); + return if $domain_f->_data('id_vm') == $request->args('id_node'); + my $domain = Ravada::Domain->open($id_domain) or confess "Error: domain $id_domain not found"; @@ -6322,8 +6326,6 @@ sub _cmd_migrate($self, $request) { my $node = Ravada::VM->open($request->args('id_node')); - $self->_migrate_base($domain, $node, $uid, $request) if $domain->id_base; - if ($domain->is_active) { if ($request->defined_arg('shutdown')) { my @timeout; @@ -6342,16 +6344,6 @@ sub _cmd_migrate($self, $request) { } $self->_remove_unnecessary_downs($domain); - if (!$domain->_vm->is_local && !$node->is_local) { - my $req_pre = Ravada::Request->migrate( - uid => Ravada::Utils->user_daemon->id - ,id_domain => $domain->id - ,id_node => $domain->_id_vm_local() - ); - $request->after_request_ok($req_pre->id); - $request->retry(10) if !defined $request->retry(); - die "Can not migrate from remote to remote. Retry.\n"; - } $domain->migrate($node, $request); my @remote_ip; diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index e07c6f034..17e1c4a40 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -2732,8 +2732,6 @@ sub _check_active_node($self) { sub _after_remove_domain($self, $user, $cascade=undef) { - my $vm_local = $self->_vm->new( host => 'localhost' ); - $self->_rrd_remove(); $self->_remove_iptables( ); $self->remove_expose(); @@ -2769,14 +2767,8 @@ sub _after_remove_domain($self, $user, $cascade=undef) { } sub _remove_all_volumes($self) { - my $vm_local = $self->_vm; - $vm_local = $self->_vm->new( host => 'localhost' ) if !$self->is_local; for my $vol (@{$self->{_volumes}}) { next if $vol =~ /iso$/; - if (!$self->is_local) { - my ($dir) = $vol =~ m{(.*)/}; - next if $vm_local->shared_storage($self->_vm, $dir); - } $self->remove_volume($vol); } } @@ -3151,10 +3143,6 @@ sub _cascade_remove_base_in_nodes($self) { my $req_nodes; my $vm_local; for my $vm ( $self->list_vms ) { - if ( $vm->is_local ) { - $vm_local= $vm; - next; - } my @after; push @after,(after_request => $req_nodes->id) if $req_nodes; $req_nodes = Ravada::Request->remove_base_vm( @@ -3166,18 +3154,6 @@ sub _cascade_remove_base_in_nodes($self) { ,@after ); } - return if !$vm_local || !$req_nodes; - my @after; - push @after,(after_request => $req_nodes->id) if $req_nodes; - Ravada::Request->remove_base_vm( - id_vm => $vm_local->id - ,id_domain => $self->id - ,uid => Ravada::Utils::user_daemon->id - ,migrate => 0 - ,_force => 1 - ,@after - ); - $self->is_base(0); return $req_nodes; } @@ -5215,6 +5191,7 @@ sub get_controllers($self) { my $info; my %controllers = $self->list_controllers(); for my $name ( sort keys %controllers ) { + next if $name eq 'disk' && $self->_vm && $self->_vm->id != $self->_data('id_vm'); $info->{$name} = [$self->get_controller($name)]; } @@ -5800,7 +5777,7 @@ sub set_base_vm($self, %args) { $self->_remove_files_not_shared(@nodes, $self->_vm); $self->_vm($node2); $self->_data('id_vm' => $node2->id); - $node2->refresh_storage_pools(); + $vm->refresh_storage_pools(); } } @@ -5848,7 +5825,8 @@ sub _remove_files_not_shared($self, @nodes){ $shared++ if $self->_vm->shared_storage($node, $dir); last if $shared; } - $self->_vm->remove_file($file) if !$shared; + next if $shared; + $self->_vm->remove_file($file); } } diff --git a/lib/Ravada/Domain/KVM.pm b/lib/Ravada/Domain/KVM.pm index d9dfd2ea8..d62680216 100644 --- a/lib/Ravada/Domain/KVM.pm +++ b/lib/Ravada/Domain/KVM.pm @@ -298,19 +298,7 @@ sub remove { my @volumes; if (!$self->is_removed ) { - my @vols_info; - for ( 1 .. 10 ) { - eval { @vols_info = $self->list_volumes_info }; - last if !$@; - warn "WARNING: remove, volumes info: $@"; - sleep 1; - } - for my $vol ( @vols_info ) { - push @volumes,($vol->{file}) - if exists $vol->{file} - && exists $vol->{device} - && $vol->{device} eq 'file'; - } + @volumes = grep (/!\.iso$/,$self->list_volumes()); } if (!$self->is_removed && $self->domain && $self->domain->is_active) { @@ -407,8 +395,14 @@ sub _disk_device($self, $with_info=undef, $attribute=undef, $value=undef) { my ($boot_node) = $disk->findnodes('boot'); my $info = {}; - eval { $info = $self->_volume_info($file) - if $file && ( $device eq 'disk' or $device eq 'cdrom') }; + eval { + $info = $self->_volume_info($file) + if $with_info + && $file && ( $device eq 'disk' or $device eq 'cdrom') + && ( !defined $self->_data('id_vm') + || $self->_vm->id == $self->_data('id_vm')) + } + ; die $@ if $@ && $@ !~ /not found/i; $info->{device} = $device; if (!$info->{name} ) { diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index dc2ce02cd..69f8ed118 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -941,6 +941,21 @@ sub _validate_remove_base($self) { ) { $reqs_base[-1]->status('done'); $reqs_base[-2]->status('done'); + } else { + my $bases_vm = $domain->_bases_vm(1); + my %done; + my $req_prev; + for my $id_vm ($domain->_data('id_vm'),keys %$bases_vm ) { + next if $done{$id_vm}++; + my $req = Ravada::Request->remove_base_vm( + uid => $self->args('uid') + ,id_domain => $id_domain + ,id_vm => $id_vm + ); + $req->after_request_ok($req_prev->id) if $req_prev; + $self->after_request_ok($req->id); + $req_prev = $req; + } } } @@ -1582,6 +1597,11 @@ sub set_base_vm { sub _validate_migrate($req) { my $domain = Ravada::Front::Domain->open($req->args('id_domain')); + if ($domain->_data('id_vm') == $req->args('id_node')) { + $req->status('done'); + $req->error("Already migrated"); + return; + } my $req_post = Ravada::Request->post_migrate( uid => $req->args('uid') ,id_domain => $domain->id diff --git a/lib/Ravada/VM/KVM.pm b/lib/Ravada/VM/KVM.pm index 48e9ad0c8..ba7930123 100644 --- a/lib/Ravada/VM/KVM.pm +++ b/lib/Ravada/VM/KVM.pm @@ -601,7 +601,7 @@ sub _file_exists_remote($self, $file) { } } - die "Error: invalid file '$file'" if $file =~ /[`;(\[" ]/; + confess "Error: invalid file '$file'" if $file =~ /[`;(\[" ]/; my $ssh = $self->_ssh; confess "Error: no _ssh ".$self->name if !$ssh; my ($out,$err) = $ssh->capture2("ls $file"); diff --git a/t/lib/Test/Ravada.pm b/t/lib/Test/Ravada.pm index 30be73696..a7ba0c522 100644 --- a/t/lib/Test/Ravada.pm +++ b/t/lib/Test/Ravada.pm @@ -791,7 +791,7 @@ sub _discover() { my $out = $req->output; warn $req->error if $req->error; next if !$out; - my $discover = decode_json($out); + my $discover = $out; my @list = grep { $_ =~ /^$name/ } @$discover; for my $name (@list) { diag("Importing $name in ".$vm_name); @@ -812,7 +812,7 @@ sub _discover() { $domain = rvd_front->search_domain($name); last if $domain; sleep 1; - wait_request(debug => 1); + wait_request(debug => 0); } eval { $sth_instances->execute($domain->id, $id_vm); @@ -960,19 +960,15 @@ sub remove_domain_and_clones_req($domain_data, $wait=1, $run_request=0) { diag("Waiting for clones of domain ".$domain->name." removed " .scalar($domain->clones)) if !(time % 10); if ($run_request) { - wait_request(); + wait_request(debug => 0); } else { sleep 1; } } } - my $req_rm; - $req_rm = Ravada::Request->remove_base(uid => user_admin->id, id_domain => $domain->id) - if $domain->is_base; my @after_req; @after_req = ( after_request => $req_clone->id ) if $req_clone; - @after_req = ( after_request => $req_rm->id ) if $req_rm; my $req= Ravada::Request->remove_domain( name => $domain->name ,uid => user_admin->id @@ -1020,6 +1016,11 @@ sub _remove_old_domains_vm($vm_name) { && $@ !~ /libvirt error code: 55,/ ; + my $domain0 = Ravada::Domain->open($domain->id); + if ($domain0 && $domain0->is_active) { + $domain0->shutdown_now(); + sleep 1; + } $domain = $vm->search_domain($domain->name); eval {$domain->remove( $USER_ADMIN ) } if $domain; warn "Error shutdown ".$domain->name." $@" if $@ && $@ !~ /No DB info/i @@ -1104,6 +1105,9 @@ sub _remove_old_domains_kvm { warn "WARNING: error $@ trying to shutdown ".$domain_name." on ".$vm->name if $@ && $@ !~ /error code: (42|55),/; + my $domain0 = rvd_back->search_domain($domain_name); + $domain0->shutdown_now() if $domain0 && $domain0->is_active; + eval { $domain->managed_save_remove() if $domain->has_managed_save_image(); @@ -1245,6 +1249,7 @@ sub wait_mojo_request($t, $url) { } sub start_storage_pool($vm, $sp_name) { + return if $vm->type eq 'Void'; my $sp = $vm->vm->get_storage_pool_by_name($sp_name); return if !$sp; return $sp if $sp->is_active(); @@ -1303,10 +1308,18 @@ sub _remove_old_disks_kvm { for my $volume ( @volumes ) { next if $volume->get_name !~ /^${name}_\d+.*\.(img|raw|ro\.qcow2|qcow2|void|backup)$/; + _chmod_w($vm, $volume->get_path, $pool); eval { $volume->delete() }; if ($@) { if ($@->code == 38 ) { - $vm->remove_file($volume->get_path); + for ( 1 .. 2 ) { + eval { + $vm->remove_file($volume->get_path); + }; + warn "warning: $@" if $@; + last if !$@; + _chmod_w($vm, $volume->get_path, $pool); + } } else { warn "Error $@ removing ".$volume->get_name." in ".$vm->name if $@; } @@ -1327,6 +1340,24 @@ sub _remove_old_disks_void($node=undef){ } } +sub _chmod_w($vm, $file, $pool) { + my @stat = stat($file); + return if !$stat[2]; + my $txt_mode = sprintf("%04o",$stat[2]); + + my $ret_mode = $stat[2]; + $ret_mode &= 070; + + my $user_w = $ret_mode & 020; + + return if $user_w; + + my ($out, $err) = $vm->run_command("chmod","+w",$file) or warn "$! $file"; + warn $err if $err; + chmod(0700,$file) if -e $file; + $pool->refresh(); +} + sub _remove_old_disks_void_remote($node) { confess "Remote node must be defined" if !defined $node; return if !$node->ping(undef,0); diff --git a/t/nodes/70_shared.t b/t/nodes/70_shared.t index 9e51fdcec..80210b698 100644 --- a/t/nodes/70_shared.t +++ b/t/nodes/70_shared.t @@ -120,7 +120,7 @@ sub _change_ram($domain) { } sub test_change_ram($vm, $node, $start=0, $prepare_base=0, $migrate=0) { - diag("start=$start , prepare_base=$prepare_base , migrate=$migrate"); + diag("test change ram start=$start , prepare_base=$prepare_base , migrate=$migrate"); my $base = $BASE->clone(name => new_domain_name, user => user_admin); $base->spinoff(); @@ -132,27 +132,56 @@ sub test_change_ram($vm, $node, $start=0, $prepare_base=0, $migrate=0) { } req_migrate($node, $domain, $start) if $migrate; - my $new_mem = _change_ram($domain); if ( $migrate ) { my $domain2 = Ravada::Domain->open($domain->id); is($domain2->_vm->id, $node->id); + $domain=$domain2; } + my $new_mem = _change_ram($domain); _test_volumes_exist($domain); my $domain_local = $vm->search_domain($domain->name); - is($domain_local->_vm->id,$vm->id); - my $mem2 = $domain_local->info(user_admin)->{memory}; - is($mem2, $new_mem); + if ( $domain_local ) { + is($domain_local->_vm->id,$vm->id); + my $mem2 = $domain_local->info(user_admin)->{memory}; + is($mem2, $new_mem); + } req_start($domain); req_migrate($node, $domain, 1); $domain = Ravada::Domain->open($domain->id); - $mem2 = $domain->info(user_admin)->{memory}; + my $mem2 = $domain->info(user_admin)->{memory}; is($mem2, $new_mem); _test_volumes_exist($domain); - $domain->remove(user_admin); - $base->remove(user_admin) if $base->id != $domain->id; + my @volumes = ($base->list_volumes, $domain->list_volumes, $base->list_files_base); + remove_domain_and_clones_req($base,1,1); + test_files_removed([$vm, $node], \@volumes); +} + +sub test_files_removed($nodes, $volumes) { + my %done; + for my $vm ( @$nodes ) { + for my $file (@$volumes) { + + my $key = $vm->name.".".$file; + return if $done{$key}++; + + ok(!$vm->file_exists($file)) or die $file; + ok(!-e $file) or die $file; + } + } + +} + +sub test_domains_removed(@domain) { + my %done; + for my $domain (@domain) { + next if $done{$domain->name}++; + for my $vm (rvd_back->list_vms) { + + } + } } sub _test_volumes_exist($domain) { @@ -204,9 +233,9 @@ sub test_add_disk($vm, $node, $start=0, $prepare_base=0, $migrate=0) { } _test_volumes_exist($domain); + my @volumes1 = $domain->list_volumes(); my $domain_local = $vm->search_domain($domain->name); is($domain_local->_vm->id,$vm->id); - my @volumes1 = map { $_->{file} } $domain->list_volumes_info; is(scalar($domain_local->list_volumes),$n+1,Dumper(\@volumes0,\@volumes1)) or exit; req_start($domain); @@ -215,7 +244,9 @@ sub test_add_disk($vm, $node, $start=0, $prepare_base=0, $migrate=0) { is(scalar($domain->list_volumes),$n+1); _test_volumes_exist($domain); - remove_domain($base); + my @volumes = ($base->list_volumes, $domain->list_volumes); + remove_domain_and_clones_req($base); + test_files_removed([$vm, $node], \@volumes); } sub req_start($domain) { @@ -229,6 +260,8 @@ sub req_start($domain) { } sub req_migrate($node, $domain, $start=0) { + my $domain2 = Ravada::Front::Domain->open($domain->id); + return if $domain2->_data('id_vm') == $node->id; my $req = Ravada::Request->migrate( id_domain => $domain->id ,uid => user_admin->id @@ -241,8 +274,8 @@ sub req_migrate($node, $domain, $start=0) { is($req->status,'done'); is($req->error,''); - $domain = Ravada::Domain->open($domain->id); - is($domain->_vm->id,$node->id); + my $domain_f = Ravada::Front::Domain->open($domain->id); + is($domain_f->_data('id_vm'),$node->id); } sub import_base($vm) { @@ -257,11 +290,29 @@ sub import_base($vm) { sub _check_sp($sp, @nodes) { for my $node (@nodes) { my $sp = $node->vm->get_storage_pool_by_name($sp); - confess "SP $sp not active in ".$node->name if !$sp->is_active; + confess "SP ".$sp->get_name." not active in ".$node->name if !$sp->is_active; } } +sub test_remove_base($vm, $node) { + my $base = $BASE->clone(name => new_domain_name, user => user_admin); + $base->spinoff(); + + $base->prepare_base(user_admin); + $base->set_base_vm(vm => $node, user => user_admin); + my @vols_base = $base->list_files_base; + Ravada::Request->remove_base( + uid => user_admin->id + ,id_domain => $base->id + ); + wait_request(debug => 0); + for my $vol ( @vols_base ) { + ok(!-e $vol) or die $vol; + } + remove_domain_and_clones_req($base); +} + ################################################################################# if ($>) { my $msg = "SKIPPED: Test must run as root"; @@ -332,16 +383,18 @@ the file " import_base($vm); + test_remove_base($vm, $node); + _check_sp($SHARED_SP, $vm, $node); for my $default_sp ( 0,1 ) { $node->default_storage_pool_name('default') if !$default_sp; $node->default_storage_pool_name($SHARED_SP) if $default_sp; for my $start ( 0,1 ) { - for my $prepare_base ( 0,1 ) { + for my $prepare_base ( 1,0 ) { for my $migrate( 1, 0 ) { - _check_sp($SHARED_SP, $vm, $node); test_change_ram($vm,$node, $start, $prepare_base, $migrate); test_add_disk($vm,$node, $start, $prepare_base, $migrate); + _check_sp($SHARED_SP, $vm, $node); } } } @@ -351,6 +404,7 @@ the file " test_shared($vm, $node); NEXT: + remove_old_domains_req(); clean_remote_node($node); remove_node($node); } From 70760576e3662a8538de12495a425ffa4966e04b Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 4 May 2026 14:04:34 +0200 Subject: [PATCH 103/141] wip: chain remove instances * deal with domains active in other nodes --- lib/Ravada.pm | 5 ++- lib/Ravada/Domain.pm | 46 +++++++++++++++-------- lib/Ravada/Request.pm | 71 +++++++++++++++++++++++------------ lib/Ravada/VM.pm | 1 + lib/Ravada/VM/Void.pm | 3 +- t/device/40_mediated_device.t | 6 ++- t/kvm/71_description_clones.t | 3 ++ t/kvm/n10_nodes.t | 47 +++++++++++++++++++---- t/lib/Test/Ravada.pm | 4 +- t/nodes/10_basic.t | 64 +++++++++++++++++++++---------- t/nodes/70_shared.t | 43 +++++++++++++++------ t/request/70_network.t | 11 ++++-- t/vm/40_volumes.t | 2 +- 13 files changed, 220 insertions(+), 86 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index c97dd4e4d..f80611373 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -4955,8 +4955,9 @@ sub _cmd_clone($self, $request) { if ( $request->defined_arg('number') && $request->defined_arg('number') > 1) || (! $request->defined_arg('name') && $request->defined_arg('add_to_pool')); - my $domain = $self->search_domain_by_id($request->args('id_domain')) - or die "Error: Domain ".$request->args('id_domain')." not found"; + my $domain = Ravada::Domain->open($request->args('id_domain')); + die "Error: Domain ".$request->args('id_domain')." not found" + if !$domain; my $args = $request->args(); $args->{request} = $request; diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 17e1c4a40..ec80c8b46 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -2069,21 +2069,35 @@ sub open($class, @args) { my $domain; eval { $domain = $vm->search_domain($row->{name}, $force) }; if ( !$domain ) { - return if $vm->is_local; - - $vm_local = {}; - my $vm_class = "Ravada::VM::".$row->{vm}; - bless $vm_local, $vm_class; - - $vm = $vm_local->new(); - $domain = $vm->search_domain($row->{name}, $force) or return; - $vm_changed = $vm; + $domain = _search_domain_in_instances($id, $row->{name}, $force); } $domain->_insert_db_extra() if $domain && !$domain->is_known_extra(); $domain->_data('id_vm' => $vm_changed->id) if $vm_changed; return $domain; } +sub _search_domain_in_instances($id_domain, $name, $force) { + + my ($found, $found_active); + for my $instance ( list_instances(undef, $id_domain)) { + my $vm; + eval { $vm = Ravada::VM->open($instance->{id_vm}) }; + warn $@ if $@; + next if !$vm; + my $domain = $vm->search_domain($name, $force); + next if !$domain; + if ($domain->is_active) { + $found_active = $domain; + last; + } + $found = $domain; + } + $found = $found_active if $found_active; + return if !$found; + $found->_data('id_vm' => $found->_vm->id); + return $found; +} + sub _check_proper_id_vm($self, $id, $id_vm) { my @instances = ({ id_vm => $$id_vm } , $self->list_instances($id) ); for my $instance ( @instances ) { @@ -3143,6 +3157,7 @@ sub _cascade_remove_base_in_nodes($self) { my $req_nodes; my $vm_local; for my $vm ( $self->list_vms ) { + next if $vm->id == $self->_vm->id; my @after; push @after,(after_request => $req_nodes->id) if $req_nodes; $req_nodes = Ravada::Request->remove_base_vm( @@ -5191,7 +5206,8 @@ sub get_controllers($self) { my $info; my %controllers = $self->list_controllers(); for my $name ( sort keys %controllers ) { - next if $name eq 'disk' && $self->_vm && $self->_vm->id != $self->_data('id_vm'); + next if $name eq 'disk' && defined $self->_data('id_vm') + && $self->_vm && $self->_vm->id != $self->_data('id_vm'); $info->{$name} = [$self->get_controller($name)]; } @@ -5774,7 +5790,7 @@ sub set_base_vm($self, %args) { } else { my $instance = $vm->search_domain($self->name); $instance->_remove_instance($user,1) if $instance; - $self->_remove_files_not_shared(@nodes, $self->_vm); + $self->_remove_files_not_shared($vm,@nodes, $self->_vm); $self->_vm($node2); $self->_data('id_vm' => $node2->id); $vm->refresh_storage_pools(); @@ -5805,7 +5821,6 @@ sub _check_set_base_reqs($self) { while ( my ($id, $id_request) = $sth->fetchrow ) { my $req; eval { $req = Ravada::Request->open($id_request)}; - warn $@ if $@; if (!$req || $req->status() eq 'done') { $sth_update->execute($id); @@ -5813,7 +5828,7 @@ sub _check_set_base_reqs($self) { } } -sub _remove_files_not_shared($self, @nodes){ +sub _remove_files_not_shared($self, $vm, @nodes){ for my $file ($self->list_volumes,$self->list_files_base) { next if $file =~ /\.iso$/; @@ -5822,11 +5837,12 @@ sub _remove_files_not_shared($self, @nodes){ my ($dir) = $file =~ m{(.*)/}; my $shared=0; for my $node(@nodes) { - $shared++ if $self->_vm->shared_storage($node, $dir); + next if $vm->id == $node->id; + $shared++ if $vm->shared_storage($node, $dir); last if $shared; } next if $shared; - $self->_vm->remove_file($file); + $vm->remove_file($file); } } diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index 69f8ed118..cbb08449f 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -294,6 +294,8 @@ our %CMD_VALIDATE = ( clone => \&_validate_clone ,create => \&_validate_create_domain ,create_domain => \&_validate_create_domain + ,remove_domain => \&_validate_remove_domain + ,remove => \&_validate_remove_domain ,remove_hardware => \&_validate_remove_hardware ,start_domain => \&_validate_start_domain ,start => \&_validate_start_domain @@ -925,6 +927,22 @@ sub _validate($self) { $method->($self); } +sub _validate_remove_domain($self) { + my $id_domain = $self->defined_arg('id_domain'); + if (!$id_domain) { + my $name = $self->defined_arg('name'); + $id_domain = $self->_search_domain_id($name); + if (!defined $id_domain) { + $self->output("Already removed"); + $self->status('done'); + return; + } + } + my $domain = Ravada::Front::Domain->open($id_domain); + $self->_chain_remove_bases_nodes($domain) + if $domain->is_base(); +} + sub _validate_remove_base($self) { my $id_domain = $self->args('id_domain'); my $domain = Ravada::Front::Domain->open($id_domain); @@ -941,21 +959,27 @@ sub _validate_remove_base($self) { ) { $reqs_base[-1]->status('done'); $reqs_base[-2]->status('done'); - } else { - my $bases_vm = $domain->_bases_vm(1); - my %done; - my $req_prev; - for my $id_vm ($domain->_data('id_vm'),keys %$bases_vm ) { - next if $done{$id_vm}++; - my $req = Ravada::Request->remove_base_vm( - uid => $self->args('uid') - ,id_domain => $id_domain - ,id_vm => $id_vm - ); - $req->after_request_ok($req_prev->id) if $req_prev; - $self->after_request_ok($req->id); - $req_prev = $req; - } + } + $self->_chain_remove_bases_nodes($domain); +} + +sub _chain_remove_bases_nodes($self, $domain) { + my $bases_vm = $domain->_bases_vm(1); + return if keys %$bases_vm < 2; + my %done; + my $req_prev; + my $req = Ravada::Request->open($self->id); + for my $id_vm ($domain->_data('id_vm'),keys %$bases_vm ) { + next if $done{$id_vm}++; + my $req = Ravada::Request->remove_base_vm( + uid => $self->args('uid') + ,id_domain => $domain->id + ,id_vm => $id_vm + ,_force => 1 + ); + $req->after_request_ok($req_prev->id) if $req_prev; + $self->after_request_ok($req->id); + $req_prev = $req; } } @@ -1597,6 +1621,15 @@ sub set_base_vm { sub _validate_migrate($req) { my $domain = Ravada::Front::Domain->open($req->args('id_domain')); + if ($domain->_data('status') ne 'shutdown') { + my $req_shutdown = Ravada::Request->shutdown_domain( + uid => $req->args('uid') + ,id_domain => $domain->id + ,_force => 1 + ); + $req->after_request_ok($req_shutdown->id); + } + if ($domain->_data('id_vm') == $req->args('id_node')) { $req->status('done'); $req->error("Already migrated"); @@ -1609,14 +1642,6 @@ sub _validate_migrate($req) { ,after_request => $req->id ); - if ($domain->_data('status') ne 'shutdown') { - my $req_shutdown = Ravada::Request->shutdown_domain( - uid => $req->args('uid') - ,id_domain => $domain->id - ,_force => 1 - ); - $req->after_request_ok($req_shutdown->id); - } my $id_vm_local = $domain->_id_vm_local(); die "Error: node local not found for ".$domain->type if !defined $id_vm_local; diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index e2392b6c4..7f2c7c0e4 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -2609,6 +2609,7 @@ Arguments: =cut sub shared_storage($self, $node, $dir) { + return 0 if $self->id == $node->id; $dir .= '/' if $dir !~ m{/$}; my $shared_cache = $self->_shared_storage_cache($node, $dir); return $shared_cache if defined $shared_cache; diff --git a/lib/Ravada/VM/Void.pm b/lib/Ravada/VM/Void.pm index 0a33727a5..ca3c1d816 100644 --- a/lib/Ravada/VM/Void.pm +++ b/lib/Ravada/VM/Void.pm @@ -297,8 +297,9 @@ sub _list_domains_remote($self, %args) { my $active = delete $args{active}; confess "Wrong arguments ".Dumper(\%args) if keys %args; + my $dir = Ravada::Front::Domain::Void::_config_dir(); - my ($out, $err) = $self->run_command("ls -1 ".$self->dir_img); + my ($out, $err) = $self->run_command("ls -1 ".$dir); my @domain; for my $file (split /\n/,$out) { diff --git a/t/device/40_mediated_device.t b/t/device/40_mediated_device.t index 3bbc926de..09f5033a7 100644 --- a/t/device/40_mediated_device.t +++ b/t/device/40_mediated_device.t @@ -673,7 +673,7 @@ sub _filter_hds($hd0, $hd1) { FOUND1: for my $n ( 1 .. scalar(@devices1)-1 ) { my @words = split(/\s+/,$devices1[$n]); for my $word0 ( reverse @words ) { - next if $word0 =~ /nvidia/; + next if $word0 =~ /nvidia/ || $word0 =~ /[\(\)\{\})]/; $word0 =~ s/(\d+\:\d+\:\d+)\.\d+/$1/; next if $word0 eq $hd1->_data('list_filter'); $hd1->_data('list_filter' => $word0); @@ -726,6 +726,8 @@ sub test_change_hd_in_clone($domain) { $hd0->_data('list_filter'=>''); confess Dumper($hd0) if $hd0->list_devices <2; + # set both filters empty and make sure mock devices are different + $hd1->_data('list_filter'=>''); _filter_hds($hd0, $hd1); die "Error: no available devices in ".$hd0->name @@ -736,7 +738,7 @@ sub test_change_hd_in_clone($domain) { my @args = ( uid => user_admin->id ,id_domain => $domain->id); my $req = Ravada::Request->clone(@args - , number => scalar($hd0->list_available_devices)-1 ); + , number => ( scalar($hd0->list_available_devices)-1 or 1) ); wait_request(debug => 0); is($req->error,'') or exit; diff --git a/t/kvm/71_description_clones.t b/t/kvm/71_description_clones.t index d25cc2597..8dda3fdbb 100644 --- a/t/kvm/71_description_clones.t +++ b/t/kvm/71_description_clones.t @@ -115,6 +115,9 @@ sub test_prepare_base { }; is(''.$@,'') or exit; + $domain->remove_base( user_admin); + is($domain->is_base,0); + eval { $domain->prepare_base( user_admin ); $@ = ''; diff --git a/t/kvm/n10_nodes.t b/t/kvm/n10_nodes.t index c71860d79..dd0936b83 100644 --- a/t/kvm/n10_nodes.t +++ b/t/kvm/n10_nodes.t @@ -240,6 +240,24 @@ sub test_remove_domain_from_local { my $vm = rvd_back->search_vm($vm_name); my $domain = $vm->search_domain($domain_orig->name); + if (!$domain) { + my $req = Ravada::Request->migrate( + uid => user_admin->id + ,id_domain => $domain_orig->id + ,id_node => $vm->id + ); + Ravada::Request->migrate( + uid => user_admin->id + ,id_domain => $domain_orig->id + ,id_node => $node->id + ,after_request => $req->id + ); + wait_request(); + $domain = $vm->search_domain($domain_orig->name); + } + + confess "Domain ".$domain_orig->name." not found in ".$vm->name + if !$domain; my @volumes = $domain->list_volumes(); @@ -533,14 +551,26 @@ sub _create_clone($node) { $clone->shutdown_now(user_admin) if $clone->is_active; is($clone->is_active,0); - eval { $base->set_base_vm(vm => $node, user => user_admin); }; - is(''.$@,'') or return; + Ravada::Request->set_base_vm( + uid => user_admin->id + ,id_domain => $base->id + ,id_vm => $node->id + ); + wait_request(); + for my $volume ( $clone->list_volumes ) { ok(-e $volume,"Expecting volume $volume of machine ".$clone->name); } - eval { $clone->migrate($node); }; - is(''.$@,'') or BAIL_OUT(); + is(''.$@,'') or return; + + Ravada::Request->migrate( + uid => user_admin->id + ,id_domain => $clone->id + ,id_node => $node->id + ); + wait_request(); + $clone = Ravada::Domain->open($clone->id); is($clone->_vm->host, $node->host) or exit; @@ -852,6 +882,7 @@ sub test_clone_not_in_node { } sub test_domain_already_started { + diag("test domain already started"); my ($vm_name, $node) = @_; my $vm = rvd_back->search_vm($vm_name); @@ -907,6 +938,7 @@ sub test_domain_already_started { { # clone is active, it should be found in node my $clone3 = rvd_back->search_domain($clone->name); + die "Error: clone ".$clone->name." not found " if !$clone3; $clone3->check_status(); is($clone3->id, $clone->id); is($clone3->_vm->host , $node->host,"Expecting ".$clone3->name @@ -1263,9 +1295,9 @@ sub test_status($node) { is(''.$@,'',"[".$node->type."] expecting no error on clone shutdown"); diag("test status of local domain"); - $clone->_set_vm($vm->id, 1); - $clone->start(user_admin); - is($clone->_vm->name, $vm->name); + my $clone2 = $vm->search_domain($clone->name); + $clone2->start(user_admin); + is($clone2->_vm->name, $vm->name); my $domain_remote = $node->search_domain($clone->name); is($domain_remote->is_active, 0 ); @@ -1317,6 +1349,7 @@ SKIP: { remove_node($node); next; }; + test_domain_already_started($vm_name, $node); test_remove_base($node); test_domain_on_remote($vm_name, $node); my $domain22 = test_domain($vm_name, $node); diff --git a/t/lib/Test/Ravada.pm b/t/lib/Test/Ravada.pm index a7ba0c522..f70a16df0 100644 --- a/t/lib/Test/Ravada.pm +++ b/t/lib/Test/Ravada.pm @@ -1922,7 +1922,7 @@ sub remove_old_storage_pools_req() { wait_request(); my $out = $req->output; next if !$out; - my $sp_list = decode_json($out); + my $sp_list = $out; my $name = base_pool_name(); for my $sp (@$sp_list) { next if $sp->{name} !~ /^$name/; @@ -1944,7 +1944,7 @@ sub remove_old_storage_pools_void() { my $list = LoadFile($file_sp); my $name = base_domain_name(); - my @list2 = grep /^$name/, @$list; + my @list2 = grep !/^$name/, @$list; DumpFile($file_sp,\@list2); } diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index 08aedee50..9ef1aebd3 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -912,6 +912,7 @@ sub _test_clones($base, $vm) { } sub test_remove_base($vm, $node, $volatile) { + diag("tst remove base, volatile_clones=$volatile ".$vm->name." ".$node->name); my $base = create_domain_v2(vm => $vm, disk => 0.5, data => 0, swap => 0); my $uuid; $uuid = $base->domain->get_uuid_string() if $vm->type eq 'KVM'; @@ -923,7 +924,12 @@ sub test_remove_base($vm, $node, $volatile) { is($base->domain->get_uuid_string(), $uuid) if $vm->type eq 'KVM'; my @volumes_base = $base->list_files_base(); - $base->set_base_vm(node => $node, user => user_admin); + Ravada::Request->set_base_vm( + uid => user_admin->id + ,id_domain => $base->id + ,id_vm => $node->id + ); + wait_request(); check_leftovers(); is($base->domain->get_uuid_string(), $uuid) if $vm->type eq 'KVM'; for my $file ( @volumes_base ) { @@ -934,7 +940,12 @@ sub test_remove_base($vm, $node, $volatile) { } test_domain_internal($base->name,$vm, $node); - $base->remove_base_vm(node => $node, user => user_admin); + Ravada::Request->remove_base_vm( + uid => user_admin->id + ,id_domain => $base->id + ,id_vm => $node->id + ); + wait_request(); test_domain_internal($base->name,$vm); test_domain_internal_not($base->name, $node); is($base->domain->get_uuid_string(), $uuid) if $vm->type eq 'KVM'; @@ -942,7 +953,7 @@ sub test_remove_base($vm, $node, $volatile) { my $base_gone = $node->search_domain($base->name); ok(!$base_gone,"Expecting ".$base->name." removed in ".$node->name) or exit; for my $file ( @volumes_base ) { - ok(!$node->file_exists($file)); + ok(!$node->file_exists($file)) or exit; ok($vm->file_exists($file), "Expecting file '$file' in local") or exit; } for my $file ( @volumes0 ) { @@ -961,7 +972,12 @@ sub test_remove_base($vm, $node, $volatile) { delete $base->{_data}->{id_vm}; is($base->_data('id_vm'), $vm->id); - $base->set_base_vm(node => $node, user => user_admin); + Ravada::Request->set_base_vm( + uid => user_admin->id + ,id_domain => $base->id + ,id_vm => $node->id + ); + wait_request(); is(scalar($base->list_vms), 2) or exit; $bases_vm = $base->_bases_vm(); for my $id_vm ( keys %$bases_vm) { @@ -974,8 +990,12 @@ sub test_remove_base($vm, $node, $volatile) { wait_request( debug => 0 ); is($base->domain->get_uuid_string(), $uuid) if $vm->type eq 'KVM'; + Ravada::Request->remove_base( + uid => user_admin->id + ,id_domain => $base->id + ); + wait_request(debug => 0); $base = Ravada::Domain->open($base->id); - $base->remove_base(user_admin); is($base->domain->get_uuid_string(), $uuid) if $vm->type eq 'KVM'; # my @req = $base->list_requests(); @@ -1014,13 +1034,15 @@ sub test_domain_internal_not($name, @vms) { for my $vm (@vms) { if (ref($vm) =~ /KVM$/) { + $vm->_reconnect(); my $domain; eval { $domain = $vm->vm->get_domain_by_name($name) }; my $error = ($@ or ''); + warn $error if $error; ok(!$domain,"Expecting not $name in ".$vm->name) or confess; } else { my $domain = $vm->search_domain($name); - ok(!$domain); + ok(!$domain,"Expecting $name not in node ".$vm->name) or confess; if ($domain) { my $config_file = $domain->_config_file; ok(!$vm->file_exists($config_file)); @@ -1626,7 +1648,7 @@ sub _migrate($domain, $node,$active) { .". It is in ".$domain2->_vm->name if $node->id != $domain2->_vm->id; - die "Error: domain ".$domain2->name." should be active=$active, got ".$domain2->is_active + confess "Error: domain ".$domain2->name." should be active=$active, got ".$domain2->is_active if $domain2->is_active != $active; } @@ -1751,7 +1773,11 @@ sub test_nested_base($vm, $node, $levels=1) { _test_migrate_nested($vm, $node, \@bases, $clone, $levels); _migrate_fast($clone, $vm); + my @volumes_base = $base0->list_files_base; _remove_base_vm($base0, $node); + for my $file ( $clone->list_volumes() ) { + ok($vm->file_exists($file)) or die $file; + } _test_migrate_nested($vm, $node, \@bases, $clone, $levels); _test_all_children_ok($base0); @@ -1833,8 +1859,14 @@ sub _test_migrate_nested($vm, $node, $bases, $clone, $levels) { sub _check_clones_in_node($base0, $id_node) { my $base = $base0; $base = Ravada::Front::Domain->open($base0->{id}) if ref($base) !~/Ravada/; + my $vm = Ravada::VM->open($id_node); for my $clone ( $base->clones ) { is($clone->{id_vm}, $id_node) or confess $clone->{name}; + my $clone2 = Ravada::Front::Domain->open($clone->{id}); + for my $volume ( $clone2->list_volumes) { + ok($vm->file_exists($volume->{file})) + or confess "File from ".$clone->{name}." not in node ".$vm->name." ".$volume->{file}; + } if ( $clone->{is_base}) { _check_clones_in_node($clone, $id_node); } @@ -2294,7 +2326,11 @@ sub test_remove_instances($base, @nodes) { ); wait_request(debug => 0); my $vm = $base->_vm; + my %done; for my $vm ( $base->_vm, @nodes ) { + next if $done{$vm->id()}; + my $domain = $vm->search_domain($base->name); + ok(!$domain); for my $file ( @volumes, @files_base ) { if ($file =~ /\.iso$/) { ok($vm->file_exists($file)); @@ -2358,22 +2394,12 @@ for my $vm_name (reverse vm_names() ) { start_node($node); - test_nested_base($vm, $node, 3); + test_migrate_req($vm, $node); - test_base_unset($vm,$node); - test_set_vm($vm, $node); + test_migrate_clone($node, $vm); test_base_only_in_node_add_hw($vm, $node); #start after create = 1 - test_duplicated_set_base_vm($vm, $node); - for my $volatile (1,0) { - test_remove_base($vm, $node, $volatile); - } - test_change_clone($vm, $node); - - test_base_unset($vm,$node); - test_removed_base_file($vm, $node); - test_migrate_clone($node, $vm); test_migrate_clone($vm, $node); diff --git a/t/nodes/70_shared.t b/t/nodes/70_shared.t index 80210b698..c5b44519c 100644 --- a/t/nodes/70_shared.t +++ b/t/nodes/70_shared.t @@ -127,8 +127,26 @@ sub test_change_ram($vm, $node, $start=0, $prepare_base=0, $migrate=0) { my $domain = $base; if ($prepare_base) { $base->prepare_base(user_admin); - $base->set_base_vm(vm => $node, user => user_admin); - $domain = $base->clone(name => new_domain_name, user => user_admin); + Ravada::Request->set_base_vm( + uid => user_admin->id + ,id_domain => $base->id + ,id_vm => $node->id + ); + wait_request(); + + ok($vm->search_domain($base->name),"Expecting base in ".$vm->name) + or exit; + ok($node->search_domain($base->name),"Expecting base ".$base->name + ." in ".$node->name." ".$node->type) + or exit; + my $name = new_domain_name(); + Ravada::Request->clone( + uid => user_admin->id + ,id_domain => $base->id + ,name => $name + ); + wait_request(); + $domain = rvd_back->search_domain($name); } req_migrate($node, $domain, $start) if $migrate; @@ -260,6 +278,7 @@ sub req_start($domain) { } sub req_migrate($node, $domain, $start=0) { + confess if !defined $domain; my $domain2 = Ravada::Front::Domain->open($domain->id); return if $domain2->_data('id_vm') == $node->id; my $req = Ravada::Request->migrate( @@ -270,12 +289,12 @@ sub req_migrate($node, $domain, $start=0) { ,start => 1 ,id_node => $node->id ); - wait_request(); + wait_request(debug => 0); is($req->status,'done'); is($req->error,''); my $domain_f = Ravada::Front::Domain->open($domain->id); - is($domain_f->_data('id_vm'),$node->id); + is($domain_f->_data('id_vm'),$node->id) or confess; } sub import_base($vm) { @@ -289,8 +308,8 @@ sub import_base($vm) { sub _check_sp($sp, @nodes) { for my $node (@nodes) { - my $sp = $node->vm->get_storage_pool_by_name($sp); - confess "SP ".$sp->get_name." not active in ".$node->name if !$sp->is_active; + my ($sp) = grep {$_->{name} eq $sp} $node->list_storage_pools(1); + confess "SP ".$sp." not active in ".$node->name if !$sp->{is_active}; } } @@ -328,7 +347,7 @@ clean(); $Ravada::Domain::MIN_FREE_MEMORY = 256 * 1024; -for my $vm_name ( vm_names() ) { +for my $vm_name (reverse vm_names() ) { my $vm; eval { $vm = rvd_back->search_vm($vm_name) }; @@ -352,7 +371,7 @@ the file " if ($vm && !grep /^$SHARED_SP$/,$vm->list_storage_pools) { my $sp = start_storage_pool($vm,$SHARED_SP); if (!$sp) { - $msg = "SKIPPED: Missing storage pool '$SHARED_SP' in node ".$vm->name; + $msg = "SKIPPED: Missing storage pool '$SHARED_SP' in node ".$vm->name." ".$vm->type; $vm = undef; } } @@ -382,9 +401,8 @@ the file " is($vm->shared_storage($node, $storage_path),1,"Expecting $SHARED_SP shared") or exit; import_base($vm); - - test_remove_base($vm, $node); - _check_sp($SHARED_SP, $vm, $node); + $node->default_storage_pool_name($SHARED_SP); + test_change_ram($vm,$node, 0, 1, 1); for my $default_sp ( 0,1 ) { $node->default_storage_pool_name('default') if !$default_sp; @@ -400,6 +418,9 @@ the file " } } + test_remove_base($vm, $node); + test_remove_base($node, $vm); + test_is_shared($vm, $node); test_shared($vm, $node); diff --git a/t/request/70_network.t b/t/request/70_network.t index 2335bdc72..cbe10bc39 100644 --- a/t/request/70_network.t +++ b/t/request/70_network.t @@ -31,7 +31,12 @@ sub test_list_nats($vm) { wait_request(); is($req->status,'done'); is($req->error,''); - like($req->output,qr{\"$exp_nat[0]\"}); + my $found; + for ( @{ $req->output }) { + $found = $_ if $_ eq $exp_nat[0]; + last if $found; + } + ok($found, "Expecting $exp_nat[0] in ".Dumper($req->output)); my $nats = rvd_front->list_network_interfaces( user => user_admin @@ -72,7 +77,7 @@ sub test_list_bridges($vm) { is($req->error,''); my @exp_bridges = sort(_expected_bridges($vm)); - is($req->output,encode_json(\@exp_bridges)); + is_deeply($req->output,\@exp_bridges); my $bridges = rvd_front->list_network_interfaces( user => user_admin @@ -84,7 +89,7 @@ sub test_list_bridges($vm) { SKIP: { skip("No system bridges found",1) if !scalar @exp_bridges; - like($req->output, qr/\["[\w\d]+".*\]/); + isa_ok($req->output,'ARRAY'); } } sub _expected_bridges($vm) { diff --git a/t/vm/40_volumes.t b/t/vm/40_volumes.t index da31f30b3..cfc61819c 100644 --- a/t/vm/40_volumes.t +++ b/t/vm/40_volumes.t @@ -89,7 +89,7 @@ sub test_add_volume { ,size => 512*1024 ,swap => $swap); - my ($vm_name) = $vm->name =~ /^(.*)_/; + my ($vm_name) = $vm->type; my $vmb = rvd_back->search_vm($vm_name); ok($vmb,"I can't find a VM ".$vm_name) or return; my $domainb = $vmb->search_domain($domain->name); From 403765a42a8eff29a0c0e10984c99a867030ab52 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 4 May 2026 17:02:20 +0200 Subject: [PATCH 104/141] wip: requests output automatically encoded --- lib/Ravada.pm | 4 ++-- lib/Ravada/Request.pm | 4 ++-- script/rvd_front | 8 +++---- t/mojo/70_volatile.t | 5 ++--- t/nodes/10_basic.t | 8 ------- t/request/70_network.t | 2 +- t/storage_list_unused.t | 46 ++++++++++++++++++++--------------------- t/user/networks.t | 9 ++++---- t/vm/networking.t | 17 +++++++-------- 9 files changed, 44 insertions(+), 59 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index f80611373..22d539b34 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -7452,7 +7452,7 @@ sub _cmd_list_unused_volumes($self, $request) { } my @list = map { {file => $_} } sort @files; - $request->output(encode_json({list => \@list, more => $more})) + $request->output({list => \@list, more => $more}) if $request; return @list; } @@ -7529,7 +7529,7 @@ sub _cmd_create_network($self, $request) { die "Error: node $id not avaiable.\n" if !$vm || !$vm->vm; - $request->output(encode_json({})); + $request->output({}); my $id_net = $vm->create_network($request->args('data'),$request->args('uid') , $request); $request->output(encode_json({id_network => $id_net})); diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index cbb08449f..8583077bc 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -2297,7 +2297,7 @@ sub _data($self, $field, $value=undef) { $self->{_data}->{$field} = $value; my $value0 = $value; eval { - $value = encode_json($value) if ref($value) eq 'ARRAY'; + $value = encode_json($value) if ref($value); }; confess Dumper([$@,$value0]) if $@; @@ -2340,7 +2340,7 @@ sub _select_db($self) { for my $key (keys %$row) { my $value = $row->{$key}; - next if !defined $value || $value !~ /^[\[}]/; + next if !defined $value || $value !~ /^[\[\{}]/; my $value_decoded; eval { $value_decoded = decode_json($value); diff --git a/script/rvd_front b/script/rvd_front index bc4fbc0bf..80fa6425e 100644 --- a/script/rvd_front +++ b/script/rvd_front @@ -752,10 +752,8 @@ any '/v2/network/new/#id_vm' => sub($c) { ); $RAVADA->wait_request($req); my $data = {}; - if ($req->status eq 'done' && $req->output && $req->output =~ /^\{/) { - eval { - $data = decode_json($req->output); - }; + if ($req->status eq 'done' && $req->output) { + $data = $req->output; warn $@ if $@; $data->{_owner} = { id => $USER->id @@ -814,7 +812,7 @@ post '/v2/network/set' => sub($c) { ); $RAVADA->wait_request($req, 120); my $out = {}; - eval { $out = decode_json($req->output) if $req->output }; + $out = $req->output if $req->output; warn $@ if $@; %data = %$out if $out && keys %$out; } diff --git a/t/mojo/70_volatile.t b/t/mojo/70_volatile.t index fe66c46c2..97e75b733 100644 --- a/t/mojo/70_volatile.t +++ b/t/mojo/70_volatile.t @@ -6,7 +6,6 @@ use Data::Dumper; use Test::More; use Test::Mojo; use Mojo::File 'path'; -use Mojo::JSON qw(decode_json); use lib 't/lib'; use Test::Ravada; @@ -212,7 +211,7 @@ sub _new_network($vm_name,$id_vm) { wait_request(debug => 0); like($req_new->output , qr/\d+/) or exit; - $net = decode_json($req_new->output); + $net = $req_new->output; $net->{ip_address} =~ s/(\d+\.\d+\.)\d+(.*)/$1$cont$2/; my $name = $net->{name}; @@ -411,7 +410,7 @@ sub _remove_unused_volumes() { ); wait_request(); next if !$req->output; - my $list = decode_json($req->output); + my $list = $req->output; my @remove; for my $entry ( @{$list->{list}} ) { my $file = $entry->{file}; diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index 9ef1aebd3..708194ac4 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -2394,10 +2394,6 @@ for my $vm_name (reverse vm_names() ) { start_node($node); - test_migrate_req($vm, $node); - - test_migrate_clone($node, $vm); - test_base_only_in_node_add_hw($vm, $node); #start after create = 1 test_migrate_clone($node, $vm); @@ -2411,8 +2407,6 @@ for my $vm_name (reverse vm_names() ) { test_base_only_in_node($vm, $node, 1); #start after create = 1 test_base_only_in_node($vm, $node); - test_removed_base_file($vm, $node); - test_volatile_req($vm, $node); test_domain_gone($vm, $node); @@ -2432,8 +2426,6 @@ for my $vm_name (reverse vm_names() ) { test_iptables_close($vm, $node); - test_nat($vm, $node, 1); # also set deprecated localhost ip - test_duplicated_set_base_vm($vm, $node); test_nested_base($vm, $node, 3); test_nested_base($vm, $node); diff --git a/t/request/70_network.t b/t/request/70_network.t index cbe10bc39..818f1ea9a 100644 --- a/t/request/70_network.t +++ b/t/request/70_network.t @@ -77,7 +77,7 @@ sub test_list_bridges($vm) { is($req->error,''); my @exp_bridges = sort(_expected_bridges($vm)); - is_deeply($req->output,\@exp_bridges); + is_deeply($req->output,\@exp_bridges) or confess; my $bridges = rvd_front->list_network_interfaces( user => user_admin diff --git a/t/storage_list_unused.t b/t/storage_list_unused.t index a50b6a344..0a0c71037 100644 --- a/t/storage_list_unused.t +++ b/t/storage_list_unused.t @@ -52,8 +52,7 @@ sub test_links($vm) { ); wait_request(); my $out_json = $req2->output; - $out_json = '[]' if !defined $out_json; - my $output = decode_json($out_json); + my $output = ($out_json or []); my $found = _search_file($output, $vol); ok(!$found,"Expecting $vol not found") or die Dumper([$machine->list_volumes]); @@ -107,8 +106,7 @@ sub test_links_dir($vm, $machine) { ); wait_request(); my $out_json = $req2->output; - $out_json = '[]' if !defined $out_json; - my $output = decode_json($out_json); + my $output = ($out_json or []); for my $exp ($file_link, "$dir_dst/$file" ) { my $found = _search_file($output, $exp); @@ -143,8 +141,7 @@ sub test_list_unused_discover($vm, $machine) { ); wait_request(); my $out_json = $req->output; - $out_json = '[]' if !defined $out_json; - my $output = decode_json($out_json); + my $output = ($out_json or []); for my $vol (@volumes) { my $found = _search_file($output, $vol); @@ -186,8 +183,7 @@ sub test_list_unused_discover2($vm) { ); wait_request(); my $out_json = $req->output; - $out_json = '[]' if !defined $out_json; - my $output = decode_json($out_json); + my $output = ($out_json or []); for my $vol (@volumes) { my $found = _search_file($output, $vol); @@ -217,8 +213,7 @@ sub test_list_unused($vm, $machine, $hidden_vols) { ); wait_request(); my $out_json = $req->output; - $out_json = '[]' if !defined $out_json; - my $output = decode_json($out_json); + my $output = ($out_json or []); my $found = _search_file($output, $file); ok($found,"Expecting $file found ") or die Dumper($output); @@ -246,8 +241,7 @@ sub test_page($vm) { ); wait_request(); my $out_json = $req->output; - $out_json = '[]' if !defined $out_json; - my $output = decode_json($out_json); + my $output = ($out_json or []); my $req2 = Ravada::Request->list_unused_volumes( uid => user_admin->id @@ -257,8 +251,7 @@ sub test_page($vm) { ); wait_request(); my $out_json2 = $req2->output; - $out_json2 = '[]' if !defined $out_json2; - my $output2 = decode_json($out_json2); + my $output2 = ($out_json2 or []); isnt($output2, $output); @@ -402,8 +395,7 @@ sub test_more($vm) { ); wait_request(); my $out_json = $req->output; - $out_json = '{}' if !defined $out_json; - my $output = decode_json($out_json); + my $output = ($out_json or {}); my $list = $output->{list}; $more = $output->{more}; @@ -443,7 +435,7 @@ sub test_linked_sp_here($vm) { wait_request(); my $out_json = $req->output; $out_json = '{}' if !defined $out_json; - my $output = decode_json($out_json); + my $output = $out_json; my $list = $output->{list}; my @found = grep ($_->{file} =~ /^$new_dir/, @$list); @@ -488,8 +480,7 @@ sub test_linked_sp($vm) { ); wait_request(); my $out_json = $req->output; - $out_json = '{}' if !defined $out_json; - my $output = decode_json($out_json); + my $output = ($out_json or {}); my $list = $output->{list}; my @found = grep ($_->{file} =~ /$new_filename/, @$list); @@ -565,8 +556,7 @@ sub test_linked_sp_level2($vm) { ); wait_request(); my $out_json = $req->output; - $out_json = '{}' if !defined $out_json; - my $output = decode_json($out_json); + my $output = ($out_json or {}); my $list = $output->{list}; my @found = grep ($_->{file} =~ /$new_filename$/, @$list); @@ -611,8 +601,7 @@ sub test_linked_sp_level0($vm) { ); wait_request(); my $out_json = $req->output; - $out_json = '{}' if !defined $out_json; - my $output = decode_json($out_json); + my $output = ($out_json or {}); my $list = $output->{list}; my @found = grep ($_->{file} =~ /$new_filename$/, @$list); @@ -648,14 +637,23 @@ sub _check_leftovers($vm, $delete=0) { sub _clean_old_sps($vm) { remove_qemu_pools($vm) if $vm->type eq 'KVM'; + my $base = base_domain_name(); if ($vm->type eq 'KVM') { - my $base = base_domain_name(); for my $pool ( $vm->vm->list_all_storage_pools()) { next if $pool->get_name !~ /^$base/; $pool->destroy if $pool->is_active; $pool->undefine; } } + for my $sp ( $vm->list_storage_pools ) { + next if $sp !~/^$base/; + Ravada::Request->remove_storage_pool( + uid => user_admin->id + ,id_vm => $vm->id + ,name => $sp + ); + wait_request(); + } } ######################################################################## diff --git a/t/user/networks.t b/t/user/networks.t index df610c8a8..f4b6923ac 100644 --- a/t/user/networks.t +++ b/t/user/networks.t @@ -10,7 +10,6 @@ use Test::Ravada; use Ravada; use Data::Dumper; -use Mojo::JSON qw(decode_json); no warnings "experimental::signatures"; use feature qw(signatures); @@ -27,7 +26,7 @@ sub test_create_network($vm) { is($req_new->status, 'done'); is($req_new->error, ''); ok($req_new->output,"Expecting output") or die; - my $data = decode_json($req_new->output); + my $data = $req_new->output; my $req = Ravada::Request->create_network( uid => user_admin->id @@ -60,7 +59,7 @@ sub test_grant_access($vm) { ); wait_request(); is($req_new->error,''); - my $data = decode_json($req_new->output); + my $data = $req_new->output; my $req_create = Ravada::Request->create_network( uid => $user->id @@ -152,7 +151,7 @@ sub test_deny_access($vm) { like($req_delete->error,qr/not authorized/); $req_create->status('requested'); - my $new_data = decode_json($req_new->output); + my $new_data = $req_new->output; $req_create->arg('data' => $new_data); $req_create->status('requested'); wait_request(); @@ -164,7 +163,7 @@ sub test_deny_access($vm) { is($req_list->status,'done'); is($req_list->error,''); - my $new_list = decode_json($req_list->output); + my $new_list = $req_list->output; my ($found) = grep { $_->{name} eq $new_data->{name} } @$new_list; ok($found,"Expecting new network $new_data->{name}"); diff --git a/t/vm/networking.t b/t/vm/networking.t index d604ee0c6..4efbd8135 100644 --- a/t/vm/networking.t +++ b/t/vm/networking.t @@ -3,7 +3,6 @@ use strict; use Carp qw(confess); use Data::Dumper; -use Mojo::JSON qw(decode_json); use Storable qw(dclone); use Test::More; @@ -84,7 +83,7 @@ sub test_create_fail ($vm) { ); wait_request(check_error => 0, debug => 0); like($req->error,qr/Network is already in use/) or die $name; - my $out = decode_json($req->output); + my $out = $req->output; like($out->{id_network},qr/^\d+$/) or exit; my ($old) = grep { $_->{id} eq $out->{id_network} } @networks; @@ -121,7 +120,7 @@ sub test_duplicate_bridge_add($vm, $net) { ,id_vm => $vm->id ); wait_request( check_error => 0, debug => 0); - is($req->output,'{}'); + is_deeply($req->output,{}); like($req->error,qr/already exists/) or exit; my ($net_created) = grep {$net2->{name} eq $_->{name} } @@ -204,7 +203,7 @@ sub test_add_network($vm) { wait_request(debug => 0); like($req_new->output , qr/\d+/) or exit; - my $net = decode_json($req_new->output); + my $net = $req_new->output; my $name = $net->{name}; my $user = create_user(); @@ -225,7 +224,7 @@ sub test_add_network($vm) { ); wait_request( debug => 0); - my $out = decode_json($req->output); + my $out = $req->output; my($new) = grep { $_->{name} eq $name } $vm->list_virtual_networks(); ok($new,"Expecting new network $name created") or die Dumper([$vm->list_virtual_networks]); isa_ok($out,'HASH') @@ -262,7 +261,7 @@ sub test_remove_user($vm) { ); wait_request(debug => 0); - my $data = decode_json($req->output); + my $data = $req->output; is($data->{id_vm},$vm->id); my $req_create = Ravada::Request->create_network( @@ -833,7 +832,7 @@ sub test_new_network($vm) { ,name => base_domain_name()."_" ); wait_request(); - my $data = decode_json($req->output); + my $data = $req->output; is($data->{id_vm},$vm->id); my $req_create = Ravada::Request->create_network( @@ -849,9 +848,9 @@ sub test_new_network($vm) { ,name => base_domain_name()."_" ); wait_request(); - my $new_net = decode_json($req_create->output); + my $new_net = $req_create->output; - my $data2 = decode_json($req2->output); + my $data2 = $req2->output; for my $field( keys %$data) { next if $field =~ /^(id_vm|ip_netmask|is_active|autostart|forward_mode)/; From a38a226c350677021a6af2ddf08b83bf644cff30 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Tue, 5 May 2026 12:07:57 +0200 Subject: [PATCH 105/141] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- public/js/admin.js | 43 ++++++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/public/js/admin.js b/public/js/admin.js index 97a6911cc..1e17286aa 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -1849,30 +1849,27 @@ ravadaApp.directive("solShowMachine", swMach) $scope.year = 0; var max_y = 10; - $http.get('/text/hours').then(function(response) { - $scope.options_h = response.data; - }).catch(function() { - $scope.options_h = [{id:0,title:'hours'},{id:1,title:'1 hour'},{id:2,title:'2 hours'},{id:3,title:'3 hours'},{id:6,title:'6 hours'},{id:8,title:'8 hours'}]; - }); - $http.get('/text/days').then(function(response) { - $scope.options_d = response.data; - }).catch(function() { - $scope.options_d = [{id:0,title:'days'},{id:1,title:'1 day'},{id:2,title:'2 days'},{id:3,title:'3 days'},{id:6,title:'6 days'}]; - }); - $http.get('/text/weeks').then(function(response) { - $scope.options_w = response.data; - }).catch(function() { - $scope.options_w = [{id:0,title:'weeks'},{id:1,title:'1 week'},{id:2,title:'2 weeks'},{id:3,title:'3 weeks'},{id:4,title:'4 weeks'}]; - }); - $http.get('/text/months').then(function(response) { - $scope.options_m = response.data; - }).catch(function() { - $scope.options_m = [{id:0,title:'months'},{id:1,title:'1 month'},{id:2,title:'2 months'},{id:3,title:'3 months'},{id:6,title:'6 months'},{id:9,title:'9 months'}]; - }); - $http.get('/text/years').then(function(response) { - $scope.options_y = response.data; + var defaultTimeOptions = { + hours: [{id:0,title:'hours'},{id:1,title:'1 hour'},{id:2,title:'2 hours'},{id:3,title:'3 hours'},{id:6,title:'6 hours'},{id:8,title:'8 hours'}], + days: [{id:0,title:'days'},{id:1,title:'1 day'},{id:2,title:'2 days'},{id:3,title:'3 days'},{id:6,title:'6 days'}], + weeks: [{id:0,title:'weeks'},{id:1,title:'1 week'},{id:2,title:'2 weeks'},{id:3,title:'3 weeks'},{id:4,title:'4 weeks'}], + months: [{id:0,title:'months'},{id:1,title:'1 month'},{id:2,title:'2 months'},{id:3,title:'3 months'},{id:6,title:'6 months'},{id:9,title:'9 months'}], + years: [{id:0,title:'years'},{id:1,title:'1 year'},{id:2,title:'2 years'},{id:3,title:'3 years'},{id:6,title:'6 years'},{id:9,title:'9 years'}] + }; + + function applyTimeOptions(options) { + options = options || {}; + $scope.options_h = options.hours || defaultTimeOptions.hours; + $scope.options_d = options.days || defaultTimeOptions.days; + $scope.options_w = options.weeks || defaultTimeOptions.weeks; + $scope.options_m = options.months || defaultTimeOptions.months; + $scope.options_y = options.years || defaultTimeOptions.years; + } + + $http.get('/text/time_options').then(function(response) { + applyTimeOptions(response.data); }).catch(function() { - $scope.options_y = [{id:0,title:'years'},{id:1,title:'1 year'},{id:2,title:'2 years'},{id:3,title:'3 years'},{id:6,title:'6 years'},{id:9,title:'9 years'}]; + applyTimeOptions(); }); var url; From d79e08c544ff2bc30eacec96cec9c101458bf944 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Tue, 5 May 2026 12:10:28 +0200 Subject: [PATCH 106/141] wip: do not decode output from requests now --- t/kvm/a10_pools.t | 14 +++++--------- t/lib/Test/Ravada.pm | 5 ++--- t/mojo/50_utf8.t | 3 +-- t/vm/d30_discover.t | 2 +- 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/t/kvm/a10_pools.t b/t/kvm/a10_pools.t index 131a6eb39..23b006e7c 100644 --- a/t/kvm/a10_pools.t +++ b/t/kvm/a10_pools.t @@ -4,7 +4,6 @@ use strict; use Carp qw(confess); use Data::Dumper; use IPC::Run3; -use Mojo::JSON qw(decode_json); use Test::More; use lib 't/lib'; @@ -107,7 +106,7 @@ sub test_req_list_sp($vm) { is($req->status,'done'); is($req->error,''); my $json_out = $req->output; - my $pools = decode_json($json_out); + my $pools = $json_out; for my $pool ( @$pools ) { like($pool,qr{^[a-z][a-z0-9]*}) or die Dumper($pools); } @@ -666,7 +665,7 @@ sub test_pool_info($vm) { ); wait_request(); my $out = $req->output; - my $pools = decode_json($out); + my $pools = $out; my $pool = $pools->[0]; isa_ok($pool,'HASH'); @@ -730,8 +729,7 @@ sub test_pool_dupe($vm) { ); wait_request(); my $out_json = $req2->output; - $out_json = '[]' if !defined $out_json; - my $output = decode_json($out_json); + my $output = ($out_json or []); for my $dir ($dir, $dir_link) { my @found = grep( {$_->{file} =~ m{^$dir/} } @{$output->{list}}); @@ -786,8 +784,7 @@ sub test_pool_dupe_linked_1($vm) { ); wait_request(); my $out_json = $req2->output; - $out_json = '[]' if !defined $out_json; - my $output = decode_json($out_json); + my $output = ($out_json or []); for my $dir ($dir, $dir_link) { my @found = grep( {$_->{file} =~ m{^$dir/} } @{$output->{list}}); @@ -822,8 +819,7 @@ sub test_pool_dupe_linked($vm) { ); wait_request(); my $out_json = $req2->output; - $out_json = '[]' if !defined $out_json; - my $output = decode_json($out_json); + my $output = ($out_json or []); for my $dir ($dir, $dir_link) { my @found = grep( {$_->{file} =~ m{^$dir/} } @{$output->{list}}); diff --git a/t/lib/Test/Ravada.pm b/t/lib/Test/Ravada.pm index f70a16df0..da1111545 100644 --- a/t/lib/Test/Ravada.pm +++ b/t/lib/Test/Ravada.pm @@ -1942,10 +1942,9 @@ sub remove_old_storage_pools_void() { return if !-e $file_sp; my $list = LoadFile($file_sp); - my $name = base_domain_name(); - - my @list2 = grep !/^$name/, @$list; + my $name = base_pool_name(); + my @list2 = grep {$_->{name} !~ /^$name/ }v@$list; DumpFile($file_sp,\@list2); } diff --git a/t/mojo/50_utf8.t b/t/mojo/50_utf8.t index bac38714f..96016e201 100644 --- a/t/mojo/50_utf8.t +++ b/t/mojo/50_utf8.t @@ -9,7 +9,6 @@ use HTML::Lint; use Test::More; use Test::Mojo; use Mojo::File 'path'; -use Mojo::JSON qw(decode_json); use lib 't/lib'; use Test::Ravada; @@ -54,7 +53,7 @@ sub _test_discover($vm_name) { wait_request(); return if !$req->output; - my $json = decode_json($req->output); + my $json = $req->output; my $base_name = base_domain_name(); my @domains = grep { /^$base_name/ } @$json; ok(!scalar(@domains)) or confess Dumper(\@domains); diff --git a/t/vm/d30_discover.t b/t/vm/d30_discover.t index 964c622a6..fec864f3b 100644 --- a/t/vm/d30_discover.t +++ b/t/vm/d30_discover.t @@ -42,7 +42,7 @@ sub test_discover($vm) { my $out = $req->output; like($out,qr/./); - my $decoded = decode_json($out); + my $decoded = $out; my ($found3) = grep { $_ eq $name } @$decoded; ok($found3, "Expecting $name in requested discover for ".$vm->type) From e8caca38292c5bf3c44cfb716c3111cbd070805f Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Tue, 5 May 2026 12:57:39 +0200 Subject: [PATCH 107/141] wip: render time options at once --- script/rvd_front | 120 +++++++++++++++++++++++------------------------ 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/script/rvd_front b/script/rvd_front index 8bf1320f4..f0fc3d000 100644 --- a/script/rvd_front +++ b/script/rvd_front @@ -3203,68 +3203,68 @@ get '/translations' => sub($c) { return $c->render(json => $lang_name); }; -get '/text/hours' => sub($c) { - my $lh = Ravada::I18N->get_handle($USER->language() || 'en'); - my @options = ( - {id => 0, title => $lh->maketext('hours')} - ,{id => 1, title => $lh->maketext('1 hour')} - ,{id => 2, title => $lh->maketext('2 hours')} - ,{id => 3, title => $lh->maketext('3 hours')} - ,{id => 6, title => $lh->maketext('6 hours')} - ,{id => 8, title => $lh->maketext('8 hours')} - ); - return $c->render(json => \@options); -}; +get '/text/time_options' => sub($c) { + + my $options = _text_options($c); + + my $i18n = $c->stash->{i18n}; + for my $entry ( keys %$options ) { + my @current; + for my $item (@{$options->{$entry}}) { + $item->[1]= $i18n->localize($item->[1]); + push @current, { + id => $item->[0] + ,title => $item->[1] + }; + } + $options->{$entry} = \@current; + } + + return $c->render(json => $options); +}; + +sub _text_options($c) { + return { + hours => [ + [0, 'hours'] + ,[1, '1 hour'] + ,[2, '2 hours'] + ,[3, '3 hours'] + ,[6, '6 hours'] + ,[8, '8 hours'] + ] + ,days => [ + [0, 'days'] + ,[1, '1 day'] + ,[2, '2 days'] + ,[3, '3 days'] + ,[6, '6 days'] + ] + ,weeks => [ + [0, 'weeks'] + ,[1, '1 week'] + ,[2, '2 weeks'] + ,[3, '3 weeks'] + ,[4, '4 weeks'] + ] + ,months => [ + [0, 'months'] + ,[1, '1 month'] + ,[2, '2 months'] + ,[3, '3 months'] + ,[4, '4 months'] + ] + ,years => [ + [0, 'years'] + ,[1, '1 year'] + ,[2, '2 years'] + ,[3, '3 years'] + ,[4, '4 years'] + ] -get '/text/days' => sub($c) { - my $lh = Ravada::I18N->get_handle($USER->language() || 'en'); - my @options = ( - {id => 0, title => $lh->maketext('days')} - ,{id => 1, title => $lh->maketext('1 day')} - ,{id => 2, title => $lh->maketext('2 days')} - ,{id => 3, title => $lh->maketext('3 days')} - ,{id => 6, title => $lh->maketext('6 days')} - ); - return $c->render(json => \@options); -}; + }; +} -get '/text/weeks' => sub($c) { - my $lh = Ravada::I18N->get_handle($USER->language() || 'en'); - my @options = ( - {id => 0, title => $lh->maketext('weeks')} - ,{id => 1, title => $lh->maketext('1 week')} - ,{id => 2, title => $lh->maketext('2 weeks')} - ,{id => 3, title => $lh->maketext('3 weeks')} - ,{id => 4, title => $lh->maketext('4 weeks')} - ); - return $c->render(json => \@options); -}; - -get '/text/months' => sub($c) { - my $lh = Ravada::I18N->get_handle($USER->language() || 'en'); - my @options = ( - {id => 0, title => $lh->maketext('months')} - ,{id => 1, title => $lh->maketext('1 month')} - ,{id => 2, title => $lh->maketext('2 months')} - ,{id => 3, title => $lh->maketext('3 months')} - ,{id => 6, title => $lh->maketext('6 months')} - ,{id => 9, title => $lh->maketext('9 months')} - ); - return $c->render(json => \@options); -}; - -get '/text/years' => sub($c) { - my $lh = Ravada::I18N->get_handle($USER->language() || 'en'); - my @options = ( - {id => 0, title => $lh->maketext('years')} - ,{id => 1, title => $lh->maketext('1 year')} - ,{id => 2, title => $lh->maketext('2 years')} - ,{id => 3, title => $lh->maketext('3 years')} - ,{id => 6, title => $lh->maketext('6 years')} - ,{id => 9, title => $lh->maketext('9 years')} - ); - return $c->render(json => \@options); -}; sub _translations($c) { my $lang_name = _languages(); From 9e89761512d1a5e645a5cebc6b25536d61e0a23d Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 6 May 2026 11:12:11 +0200 Subject: [PATCH 108/141] wip: fixed months and years --- lib/Ravada/I18N/en.po | 8 ++++---- lib/Ravada/I18N/es.po | 8 ++++---- script/rvd_front | 4 +++- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/Ravada/I18N/en.po b/lib/Ravada/I18N/en.po index 4ac874bfc..10a7b4f15 100644 --- a/lib/Ravada/I18N/en.po +++ b/lib/Ravada/I18N/en.po @@ -2285,8 +2285,8 @@ msgstr "2 years" msgid "3 years" msgstr "3 years" -msgid "6 years" -msgstr "6 years" +msgid "4 years" +msgstr "4 years" -msgid "9 years" -msgstr "9 years" +msgid "5 years" +msgstr "5 years" diff --git a/lib/Ravada/I18N/es.po b/lib/Ravada/I18N/es.po index 7ee77ea40..944982914 100644 --- a/lib/Ravada/I18N/es.po +++ b/lib/Ravada/I18N/es.po @@ -2398,8 +2398,8 @@ msgstr "2 años" msgid "3 years" msgstr "3 años" -msgid "6 years" -msgstr "6 años" +msgid "4 years" +msgstr "4 años" -msgid "9 years" -msgstr "9 años" +msgid "5 years" +msgstr "5 años" diff --git a/script/rvd_front b/script/rvd_front index f0fc3d000..8ca62718a 100644 --- a/script/rvd_front +++ b/script/rvd_front @@ -3252,7 +3252,8 @@ sub _text_options($c) { ,[1, '1 month'] ,[2, '2 months'] ,[3, '3 months'] - ,[4, '4 months'] + ,[4, '6 months'] + ,[5, '9 months'] ] ,years => [ [0, 'years'] @@ -3260,6 +3261,7 @@ sub _text_options($c) { ,[2, '2 years'] ,[3, '3 years'] ,[4, '4 years'] + ,[5, '5 years'] ] }; From ddf80494e71c6c852a863ddffb5cf016c59f0292 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 6 May 2026 11:17:43 +0200 Subject: [PATCH 109/141] wip: fixed typo --- t/lib/Test/Ravada.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/lib/Test/Ravada.pm b/t/lib/Test/Ravada.pm index da1111545..657a09e7a 100644 --- a/t/lib/Test/Ravada.pm +++ b/t/lib/Test/Ravada.pm @@ -1944,7 +1944,7 @@ sub remove_old_storage_pools_void() { my $list = LoadFile($file_sp); my $name = base_pool_name(); - my @list2 = grep {$_->{name} !~ /^$name/ }v@$list; + my @list2 = grep {$_->{name} !~ /^$name/ } @$list; DumpFile($file_sp,\@list2); } From 23d7d2a7ad65f0d84486e5e6c00cd3f1e594c0cb Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 6 May 2026 13:05:15 +0200 Subject: [PATCH 110/141] wip: set default time options --- public/js/admin.js | 1 + 1 file changed, 1 insertion(+) diff --git a/public/js/admin.js b/public/js/admin.js index 1e17286aa..4225d2a32 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -1866,6 +1866,7 @@ ravadaApp.directive("solShowMachine", swMach) $scope.options_y = options.years || defaultTimeOptions.years; } + applyTimeOptions(); $http.get('/text/time_options').then(function(response) { applyTimeOptions(response.data); }).catch(function() { From 97b3b077d8e1c73cdb1598e8dd9c0837afe57db5 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 7 May 2026 12:07:49 +0200 Subject: [PATCH 111/141] wip: output from requests already comes decoded --- t/kvm/uefi.t | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/t/kvm/uefi.t b/t/kvm/uefi.t index 42d6942a3..40c39d112 100644 --- a/t/kvm/uefi.t +++ b/t/kvm/uefi.t @@ -166,8 +166,7 @@ sub test_req_machine_types($vm) { ,uid => user_admin->id ); wait_request(); - my $out_json = $req->output; - my $out = decode_json($out_json); + my $out = $req->output; my $n = 2; ok(scalar(keys %$out) >= $n,"Expecting at least $n machine architectures" . Dumper($out)); @@ -183,8 +182,7 @@ sub test_req_machine_types2($vm) { ,uid => user_admin->id ); wait_request(); - my $out_json = $req->output; - my $out = decode_json($out_json); + my $out = $req->output; my $n = 2; ok(scalar(keys %$out) >= $n,"Expecting at least $n machine architectures" . Dumper($out)); @@ -213,7 +211,7 @@ sub test_isos($vm) { like($req->output,qr/./); my $machine_types = {}; - $machine_types = decode_json($req->output()); + $machine_types = $req->output() if $req->output; my $isos = rvd_front->list_iso_images(); From 378efda57cd530097f89e95f3002d1d12ff0a269 Mon Sep 17 00:00:00 2001 From: Yago004 <147386653+Yago004@users.noreply.github.com> Date: Thu, 7 May 2026 12:16:57 +0200 Subject: [PATCH 112/141] fix: ubuntu 26/24 ISO file_re (#2317) --- lib/Ravada.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index d53a91eaf..110e68c6f 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -694,7 +694,7 @@ sub _update_isos { ,xml => 'bionic-amd64.xml' ,xml_volume => 'bionic64-volume.xml' ,url => 'https://ftp.lysator.liu.se/ubuntu-dvd/xubuntu/releases/26.04.*/release/' - ,file_re => 'xubuntu.*desktop.*.iso' + ,file_re => 'xubuntu.26.04.*desktop.*.iso' ,options => { machine => 'pc-q35', bios => 'UEFI' } ,min_ram => 2 ,min_disk_size => '20' @@ -706,7 +706,7 @@ sub _update_isos { ,xml => 'bionic-amd64.xml' ,xml_volume => 'bionic64-volume.xml' ,url => 'https://ftp.lysator.liu.se/ubuntu-dvd/xubuntu/releases/24.04.*/release/' - ,file_re => 'xubuntu.*desktop.*.iso' + ,file_re => 'xubuntu.24.04.*desktop.*.iso' ,options => { machine => 'pc-q35', bios => 'UEFI' } ,min_ram => 2 ,min_disk_size => '20' From 412ca6f2c46e0fbaacaae5932fdfed88fe400bc2 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 12:26:46 +0200 Subject: [PATCH 113/141] Translate Charts page time-period strings via Mojo routes (#2314) Add translatable text routes for chart time options and update admin.js to use them --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: frankiejol <136835+frankiejol@users.noreply.github.com> Co-authored-by: Francesc Guasch Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/Ravada/I18N/en.po | 84 +++++++++++++++++++++++++++++++++++++++++++ lib/Ravada/I18N/es.po | 84 +++++++++++++++++++++++++++++++++++++++++++ public/js/admin.js | 61 ++++++++++++------------------- script/rvd_front | 65 +++++++++++++++++++++++++++++++++ 4 files changed, 256 insertions(+), 38 deletions(-) diff --git a/lib/Ravada/I18N/en.po b/lib/Ravada/I18N/en.po index 11b2b3525..10a7b4f15 100644 --- a/lib/Ravada/I18N/en.po +++ b/lib/Ravada/I18N/en.po @@ -2206,3 +2206,87 @@ msgstr "Show Clones" msgid "starting" msgstr "starting" + +msgid "hours" +msgstr "hours" + +msgid "1 hour" +msgstr "1 hour" + +msgid "2 hours" +msgstr "2 hours" + +msgid "3 hours" +msgstr "3 hours" + +msgid "6 hours" +msgstr "6 hours" + +msgid "8 hours" +msgstr "8 hours" + +msgid "days" +msgstr "days" + +msgid "1 day" +msgstr "1 day" + +msgid "2 days" +msgstr "2 days" + +msgid "3 days" +msgstr "3 days" + +msgid "6 days" +msgstr "6 days" + +msgid "weeks" +msgstr "weeks" + +msgid "1 week" +msgstr "1 week" + +msgid "2 weeks" +msgstr "2 weeks" + +msgid "3 weeks" +msgstr "3 weeks" + +msgid "4 weeks" +msgstr "4 weeks" + +msgid "months" +msgstr "months" + +msgid "1 month" +msgstr "1 month" + +msgid "2 months" +msgstr "2 months" + +msgid "3 months" +msgstr "3 months" + +msgid "6 months" +msgstr "6 months" + +msgid "9 months" +msgstr "9 months" + +msgid "years" +msgstr "years" + +msgid "1 year" +msgstr "1 year" + +msgid "2 years" +msgstr "2 years" + +msgid "3 years" +msgstr "3 years" + +msgid "4 years" +msgstr "4 years" + +msgid "5 years" +msgstr "5 years" diff --git a/lib/Ravada/I18N/es.po b/lib/Ravada/I18N/es.po index 6c271ba85..944982914 100644 --- a/lib/Ravada/I18N/es.po +++ b/lib/Ravada/I18N/es.po @@ -2319,3 +2319,87 @@ msgstr "Compresión automática" msgid "Show Clones" msgstr "Mostrar clones" + +msgid "hours" +msgstr "horas" + +msgid "1 hour" +msgstr "1 hora" + +msgid "2 hours" +msgstr "2 horas" + +msgid "3 hours" +msgstr "3 horas" + +msgid "6 hours" +msgstr "6 horas" + +msgid "8 hours" +msgstr "8 horas" + +msgid "days" +msgstr "días" + +msgid "1 day" +msgstr "1 día" + +msgid "2 days" +msgstr "2 días" + +msgid "3 days" +msgstr "3 días" + +msgid "6 days" +msgstr "6 días" + +msgid "weeks" +msgstr "semanas" + +msgid "1 week" +msgstr "1 semana" + +msgid "2 weeks" +msgstr "2 semanas" + +msgid "3 weeks" +msgstr "3 semanas" + +msgid "4 weeks" +msgstr "4 semanas" + +msgid "months" +msgstr "meses" + +msgid "1 month" +msgstr "1 mes" + +msgid "2 months" +msgstr "2 meses" + +msgid "3 months" +msgstr "3 meses" + +msgid "6 months" +msgstr "6 meses" + +msgid "9 months" +msgstr "9 meses" + +msgid "years" +msgstr "años" + +msgid "1 year" +msgstr "1 año" + +msgid "2 years" +msgstr "2 años" + +msgid "3 years" +msgstr "3 años" + +msgid "4 years" +msgstr "4 años" + +msgid "5 years" +msgstr "5 años" diff --git a/public/js/admin.js b/public/js/admin.js index 1469eb827..4225d2a32 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -1849,44 +1849,29 @@ ravadaApp.directive("solShowMachine", swMach) $scope.year = 0; var max_y = 10; - $scope.options_h = [ - {id:0, title: 'hours'} - ,{id:1 , title: '1 hour'} - ,{id:2 , title: '2 hours'} - ,{id:3 , title: '3 hours'} - ,{id:6 , title: '6 hours'} - ,{id:8 , title: '8 hours'} - ]; - $scope.options_d = [ - {id:0 , title: 'days'} - ,{id:1 , title: '1 day'} - ,{id:2 , title: '2 days'} - ,{id:3 , title: '3 days'} - ,{id:6 , title: '6 days'} - ]; - $scope.options_w = [ - {id:0 , title: 'weeks'} - ,{id:1 , title: '1 week'} - ,{id:2 , title: '2 weeks'} - ,{id:3 , title: '3 weeks'} - ,{id:4 , title: '4 weeks'} - ]; - $scope.options_m = [ - {id:0 , title: 'months'} - ,{id:1 , title: '1 month'} - ,{id:2 , title: '2 months'} - ,{id:3 , title: '3 months'} - ,{id:6 , title: '6 months'} - ,{id:9 , title: '9 months'} - ]; - $scope.options_y = [ - {id:0 , title: 'years'} - ,{id:1 , title: '1 year'} - ,{id:2 , title: '2 years'} - ,{id:3 , title: '3 years'} - ,{id:6 , title: '6 years'} - ,{id:9 , title: '9 years'} - ]; + var defaultTimeOptions = { + hours: [{id:0,title:'hours'},{id:1,title:'1 hour'},{id:2,title:'2 hours'},{id:3,title:'3 hours'},{id:6,title:'6 hours'},{id:8,title:'8 hours'}], + days: [{id:0,title:'days'},{id:1,title:'1 day'},{id:2,title:'2 days'},{id:3,title:'3 days'},{id:6,title:'6 days'}], + weeks: [{id:0,title:'weeks'},{id:1,title:'1 week'},{id:2,title:'2 weeks'},{id:3,title:'3 weeks'},{id:4,title:'4 weeks'}], + months: [{id:0,title:'months'},{id:1,title:'1 month'},{id:2,title:'2 months'},{id:3,title:'3 months'},{id:6,title:'6 months'},{id:9,title:'9 months'}], + years: [{id:0,title:'years'},{id:1,title:'1 year'},{id:2,title:'2 years'},{id:3,title:'3 years'},{id:6,title:'6 years'},{id:9,title:'9 years'}] + }; + + function applyTimeOptions(options) { + options = options || {}; + $scope.options_h = options.hours || defaultTimeOptions.hours; + $scope.options_d = options.days || defaultTimeOptions.days; + $scope.options_w = options.weeks || defaultTimeOptions.weeks; + $scope.options_m = options.months || defaultTimeOptions.months; + $scope.options_y = options.years || defaultTimeOptions.years; + } + + applyTimeOptions(); + $http.get('/text/time_options').then(function(response) { + applyTimeOptions(response.data); + }).catch(function() { + applyTimeOptions(); + }); var url; diff --git a/script/rvd_front b/script/rvd_front index eff6622f2..8ca62718a 100644 --- a/script/rvd_front +++ b/script/rvd_front @@ -3203,6 +3203,71 @@ get '/translations' => sub($c) { return $c->render(json => $lang_name); }; +get '/text/time_options' => sub($c) { + + my $options = _text_options($c); + + my $i18n = $c->stash->{i18n}; + for my $entry ( keys %$options ) { + my @current; + for my $item (@{$options->{$entry}}) { + $item->[1]= $i18n->localize($item->[1]); + push @current, { + id => $item->[0] + ,title => $item->[1] + }; + } + $options->{$entry} = \@current; + } + + return $c->render(json => $options); +}; + +sub _text_options($c) { + return { + hours => [ + [0, 'hours'] + ,[1, '1 hour'] + ,[2, '2 hours'] + ,[3, '3 hours'] + ,[6, '6 hours'] + ,[8, '8 hours'] + ] + ,days => [ + [0, 'days'] + ,[1, '1 day'] + ,[2, '2 days'] + ,[3, '3 days'] + ,[6, '6 days'] + ] + ,weeks => [ + [0, 'weeks'] + ,[1, '1 week'] + ,[2, '2 weeks'] + ,[3, '3 weeks'] + ,[4, '4 weeks'] + ] + ,months => [ + [0, 'months'] + ,[1, '1 month'] + ,[2, '2 months'] + ,[3, '3 months'] + ,[4, '6 months'] + ,[5, '9 months'] + ] + ,years => [ + [0, 'years'] + ,[1, '1 year'] + ,[2, '2 years'] + ,[3, '3 years'] + ,[4, '4 years'] + ,[5, '5 years'] + ] + + }; +} + + sub _translations($c) { my $lang_name = _languages(); $c->stash(lang_name => $lang_name); From 6878854a348ace22565218694a5978c90e5a4390 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 7 May 2026 12:52:21 +0200 Subject: [PATCH 114/141] feat: Ubuntu 26 package --- deb/debianize.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deb/debianize.pl b/deb/debianize.pl index 2b6fba005..272d65680 100755 --- a/deb/debianize.pl +++ b/deb/debianize.pl @@ -31,7 +31,7 @@ } my %COPY_RELEASES = ( - 'ubuntu-20.04'=> ['ubuntu-22.04','ubuntu-24.04'] + 'ubuntu-20.04'=> ['ubuntu-22.04','ubuntu-24.04','ubuntu-26.04'] ,'debian-10' => ['debian-11','debian-12','debian-13'] ); my %DIR = ( From d09413b1703519a24678c76d63bce608302f44f8 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Fri, 8 May 2026 08:54:42 +0200 Subject: [PATCH 115/141] fix: enable setting isolated port on adding network (#2318) * fix: enable setting isolated port on adding network closes #2309 --- lib/Ravada/Domain/KVM.pm | 27 +++++++++++++++++++-------- t/request/30_hardware.t | 20 ++++++++++++++++++++ 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/lib/Ravada/Domain/KVM.pm b/lib/Ravada/Domain/KVM.pm index 208f1bd1e..8ba9cd0f3 100644 --- a/lib/Ravada/Domain/KVM.pm +++ b/lib/Ravada/Domain/KVM.pm @@ -2528,6 +2528,7 @@ sub _set_controller_network($self, $number, $data) { my $type = ( delete $data->{type} or 'NAT' ); my $network =(delete $data->{network} or 'default'); my $bridge = (delete $data->{bridge} or ''); + my $isolated = _network_port_arguments($data); confess "Error: unkonwn fields in data ".Dumper($data) if keys %$data; @@ -2546,6 +2547,8 @@ sub _set_controller_network($self, $number, $data) { die "Error adding network, unknown type '$type'"; } + $device .= "" if $isolated; + $device .= "
@@ -3717,6 +3720,21 @@ sub _change_xml($xml, $name, $data) { return $changed; } +sub _network_port_arguments($data) { + return if !exists $data->{port}; + + my $isolated = delete $data->{port}->{isolated}; + + die "Error: wrong isolated '$isolated'. It must be 'yes' or 'no'" + if defined $isolated && !( $isolated eq 'yes' || $isolated eq 'no'); + + die "Error: Unknown arguments in port ".Dumper($data->{port}) if keys %{$data->{port}}; + + delete $data->{port}; + + return $isolated; +} + sub _change_hardware_network($self, $index, $data) { die "Error: index number si required.\n" if !defined $index; @@ -3734,14 +3752,7 @@ sub _change_hardware_network($self, $index, $data) { my $driver = lc(delete $data->{driver} or ''); my $bridge = delete $data->{bridge}; my $network = delete $data->{network}; - my $isolated = delete $data->{port}->{isolated}; - - die "Error: wrong isolated '$isolated'. It must be 'yes' or 'no'" - if defined $isolated && !( $isolated eq 'yes' || $isolated eq 'no'); - - die "Error: Unknown arguments in port ".Dumper($data->{port}) if keys %{$data->{port}}; - - delete $data->{port}; + my $isolated = _network_port_arguments($data); die "Error: Unknown arguments ".Dumper($data) if keys %$data; diff --git a/t/request/30_hardware.t b/t/request/30_hardware.t index 41c46e16c..32f9c57f8 100644 --- a/t/request/30_hardware.t +++ b/t/request/30_hardware.t @@ -696,7 +696,27 @@ sub test_add_network_nat($domain) { is($req->error,''); } +sub test_add_network_isolated($domain, $isolated) { + my $req = Ravada::Request->add_hardware( + uid => user_admin->id + ,name => 'network' + ,id_domain => $domain->id + ,data => { + driver => 'virtio' + ,type => 'NAT' + ,network => 'default' + ,port => { isolated => $isolated } + } + ); + wait_request(); + is($req->error,''); +} + + sub test_add_network($domain) { + + test_add_network_isolated($domain,'yes'); + test_add_network_isolated($domain,'no'); test_add_network_bridge($domain); test_add_network_nat($domain); } From be542a2f05b56dfe0398282e93d647c59b4ee8b6 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Fri, 8 May 2026 11:47:20 +0200 Subject: [PATCH 116/141] fix: clean requests when downgrading (#2322) --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/workflows/github-action-test.yml | 2 ++ lib/Ravada.pm | 8 +++++ t/downgrade_reqs.t | 44 ++++++++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 t/downgrade_reqs.t diff --git a/.github/workflows/github-action-test.yml b/.github/workflows/github-action-test.yml index 8041897ce..327dc76be 100644 --- a/.github/workflows/github-action-test.yml +++ b/.github/workflows/github-action-test.yml @@ -62,3 +62,5 @@ jobs: run: prove -lr t/request/25_create_from_iso.t t/vm/d10_not_download.t - name: Test hardware run: prove -lr t/request/30_hardware.t + - name: Test downgrade + run: prove -lr t/downgrade_reqs.t diff --git a/lib/Ravada.pm b/lib/Ravada.pm index 110e68c6f..5c340747e 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -2164,6 +2164,14 @@ sub _upgrade_table($self, $table, $field, $definition) { ." $row->{TYPE_NAME} -> $new_type \n" ." in $table\n$definition\n" if !$FIRST_TIME_RUN && $0 !~ /\.t$/; print "-" if $FIRST_TIME_RUN && $ENV{TERM}; + + if ($table eq 'requests' && $field =~ /^after_request/ + && $row->{TYPE_NAME} =~ /TEXT|CHAR/i && $new_type =~ /int|INTEGER/i) { + my $sth_clean = $CONNECTOR->dbh->prepare( + "UPDATE requests set $field=NULL " + ); + $sth_clean->execute; + } $dbh->do("alter table $table change $field $field $definition"); $self->_create_constraints($table, [$field, $constraint]) if $constraint; diff --git a/t/downgrade_reqs.t b/t/downgrade_reqs.t new file mode 100644 index 000000000..fc9a708c7 --- /dev/null +++ b/t/downgrade_reqs.t @@ -0,0 +1,44 @@ +use warnings; +use strict; + +use Data::Dumper; +use IPC::Run3 qw(run3); +use YAML qw(DumpFile LoadFile); +use Test::More; + +use lib 't/lib'; +use Test::Ravada; + +use feature qw(signatures); +no warnings "experimental::signatures"; + +####################################################################################### + +sub test_downgrade($field) { + + my $sth = connector->dbh->prepare( + "ALTER table requests change column $field $field text(80)" + ); + $sth->execute(); + my $req = Ravada::Request->refresh_vms( + uid => user_admin->id + ,$field => '[3,4]' + ); + eval { + rvd_back->_sql_create_tables(); + rvd_back->_upgrade_tables(); + }; + is($@,''); + +} + +####################################################################################### + +init('/etc/ravada.conf',0); + +test_downgrade('after_request'); +test_downgrade('after_request_ok'); + +end(); + +done_testing(); From 85bd44da67c63a2fbdbba47307448e71efb039fd Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Fri, 8 May 2026 16:14:19 +0200 Subject: [PATCH 117/141] Refactor lock (#2308) * feat: show domain requests coses #2076 --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: frankiejol <136835+frankiejol@users.noreply.github.com> --- lib/Ravada.pm | 2 + lib/Ravada/Request.pm | 10 ++- lib/Ravada/WebSocket.pm | 41 +++++++++- public/css/sb-admin.css | 9 +++ public/js/ravada.js | 35 +++++++++ t/mojo/20_ws.t | 74 ++++++++++++++++++- templates/main/needs_restart.html.ep | 2 +- templates/main/settings_machine.html.ep | 1 + .../main/settings_machine_locked.html.ep | 23 ++++++ 9 files changed, 188 insertions(+), 9 deletions(-) create mode 100644 templates/main/settings_machine_locked.html.ep diff --git a/lib/Ravada.pm b/lib/Ravada.pm index 5c340747e..2e455c630 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -4394,6 +4394,8 @@ sub _domain_working { confess "Missing request" if !defined $req; + return if $req->command =~ /list_cpu_models/; + if (!$id_domain) { $id_domain = $req->defined_arg('id_base'); if (!$id_domain) { diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index 352ccb637..19b970afc 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -223,6 +223,7 @@ qw( manage_pools screenshot prepare_base + list_cpu_models ); our $TIMEOUT_SHUTDOWN = 120; @@ -271,7 +272,8 @@ our %COMMAND = ( ,important=> { limit => 20 ,priority => 1 - ,commands => ['clone','start','start_clones','shutdown_clones','create','open_iptables','list_network_interfaces','list_isos','ping_backend','refresh_machine'] + ,commands => ['clone','start','start_clones','shutdown_clones','create','open_iptables','list_network_interfaces','list_isos','ping_backend','refresh_machine' + ,'list_cpu_models' ] } ,iptables => { @@ -2066,12 +2068,12 @@ sub AUTOLOAD { ); } - confess "ERROR: Unknown field $name " - if !exists $self->{$name} && !exists $FIELD{$name} && !exists $FIELD_RO{$name}; - confess "Can't locate object method $name via package $self" if !ref($self); + confess "ERROR: Unknown field $name " + if !exists $self->{$name} && !exists $FIELD{$name} && !exists $FIELD_RO{$name}; + my $value = shift; $name =~ tr/[a-z][A-Z]_/_/c; diff --git a/lib/Ravada/WebSocket.pm b/lib/Ravada/WebSocket.pm index 7d0a18f1b..351307306 100644 --- a/lib/Ravada/WebSocket.pm +++ b/lib/Ravada/WebSocket.pm @@ -40,6 +40,7 @@ my %SUB = ( ,list_machines_user_including_privates => \&_list_machines_user_including_privates ,list_bases_anonymous => \&_list_bases_anonymous ,list_requests => \&_list_requests + ,list_domain_requests => \&_list_domain_requests ,machine_info => \&_get_machine_info ,node_info => \&_get_node_info ,ping_backend => \&_ping_backend @@ -58,6 +59,7 @@ our %TABLE_CHANNEL = ( ,list_machines_user_including_privates => ['domains','bookings','booking_entries' ,'booking_entry_ldap_groups', 'booking_entry_users','booking_entry_bases'] ,list_requests => 'requests' + ,list_domain_requests => 'requests.id_domain' ,machine_info => 'domains' ,log_active_domains => 'log_active_domains' ,list_networks => 'virtual_networks' @@ -456,6 +458,31 @@ sub _list_requests($rvd, $args) { return $rvd->list_requests; } +sub _list_domain_requests($rvd, $args) { + my ($id_domain) = $args->{channel} =~ m{/(\d+)}; + my $domain = $rvd->search_domain_by_id($id_domain) or do { + warn "Error: domain $id_domain not found."; + return []; + }; + + my $login = $args->{login} or die "Error: no login arg ".Dumper($args); + my $user = Ravada::Auth::SQL->new(name => $login) or die "Error: uknown user $login"; + return [] unless $user->is_operator || $user->is_admin || $domain->id_owner == $user->id; + + my $sth = $rvd->_dbh->prepare( + "SELECT id, id_domain, command, status, date_req FROM requests WHERE id_domain=? " + ." AND status <> 'done'" + ." ORDER BY date_req " + ); + $sth->execute($id_domain); + my @ret; + + while ( my $row = $sth->fetchrow_hashref ) { + push @ret,($row); + } + return \@ret; + +} sub _get_machine_info($rvd, $args) { my ($id_domain) = $args->{channel} =~ m{/(\d+)}; my $domain = $rvd->search_domain_by_id($id_domain) or do { @@ -672,12 +699,18 @@ sub _clean_info($self, $key) { $self->_old_info($key,0,0); } -sub _date_changed_table($self, $table, $id) { +sub _date_changed_table($self, $table0, $id) { + my ($table, $field_id) = $table0 =~ /(.*)\.(.*)/; + if (!defined $table ) { + $table = $table0; + $field_id = 'id'; + } + die "Error: wrong id field '$field_id' " if $field_id !~ /^[a-zA-Z_]+$/; my $rvd = $self->ravada; my $sth; if (defined $id) { $sth = $rvd->_dbh->prepare("SELECT MAX(date_changed) FROM $table " - ." WHERE id=?"); + ." WHERE $field_id=?"); $sth->execute($id); } else { $sth = $rvd->_dbh->prepare("SELECT MAX(date_changed) FROM $table"); @@ -687,7 +720,9 @@ sub _date_changed_table($self, $table, $id) { return ($date or ''); } -sub _count_table($rvd, $table) { +sub _count_table($rvd, $table0) { + my $table=$table0; + $table =~ s/\..*//; my $sth = $rvd->_dbh->prepare("SELECT count(*) FROM $table"); $sth->execute; my ($count) = $sth->fetchrow; diff --git a/public/css/sb-admin.css b/public/css/sb-admin.css index 141f92f52..51cb40bef 100644 --- a/public/css/sb-admin.css +++ b/public/css/sb-admin.css @@ -508,6 +508,15 @@ td.box{ color: orange } +ul.compact { + margin: 0; +} + +li.compact { + padding: 0; + margin: 0; +} + label.small_row { font-size: 80%; font-weight: 600; diff --git a/public/js/ravada.js b/public/js/ravada.js index 243214620..4efc57fde 100644 --- a/public/js/ravada.js +++ b/public/js/ravada.js @@ -351,6 +351,7 @@ $scope.storage_pools=['default']; $scope.shared_user_count = -1 $scope.access_groups=[]; + $scope.show_lock_details=false; var fields_option=[ 'volatile_clones','autostart' ,'shutdown_disconnected','balance_policy' @@ -480,6 +481,7 @@ if (!subscribed_extra) { subscribed_extra = true; subscribe_nodes(url,data.type); + subscribe_domain_requests(url, data.id); //subscribe_bases(url); } if ($scope.edit) { $scope.lock_info = true } @@ -521,6 +523,39 @@ } }; + var domainRequestsSocket = null; + + var subscribe_domain_requests=function(url, id) { + // Close any previous socket before opening a new one + if (domainRequestsSocket && domainRequestsSocket.readyState === WebSocket.OPEN) { + try { + domainRequestsSocket.close(); + } catch (e) { + // ignore errors on close + } + } + + domainRequestsSocket = new WebSocket(url); + var ws = domainRequestsSocket; + ws.onopen = function(event) { ws.send('list_domain_requests/'+id) }; + ws.onmessage = function(event) { + var data = JSON.parse(event.data); + $scope.$apply(function () { + $scope.domain_requests = data; + }); + }; + }; + + $scope.$on('$destroy', function () { + if (domainRequestsSocket) { + try { + domainRequestsSocket.close(); + } catch (e) { + // ignore errors on close + } + domainRequestsSocket = null; + } + }); var subscribe_requests = function(url) { var ws = new WebSocket(url); ws.onopen = function(event) { ws.send('list_requests') }; diff --git a/t/mojo/20_ws.t b/t/mojo/20_ws.t index 54970a56d..95ce64182 100644 --- a/t/mojo/20_ws.t +++ b/t/mojo/20_ws.t @@ -214,7 +214,7 @@ sub test_list_machines_non_admin($t, $bases) { } sub test_shutdown($user, $clone) { - if ($clone->{status} eq 'active') { + if ($clone->{status} && $clone->{status} eq 'active') { my $req = Ravada::Request->shutdown( uid => $user->id ,id_domain => $clone->{id} @@ -444,6 +444,76 @@ sub test_node_info($vm_name) { } +sub test_domain_requests($t, $base) { + my $user = create_user(); + my $other_user = create_user(); + my $is_base = $base->is_base(); + if (!$is_base) { + Ravada::Request->shutdown_domain(uid => user_admin->id + ,id_domain => $base->id); + Ravada::Request->prepare_base(uid => user_admin->id + ,id_domain => $base->id); + } + + my $is_public = $base->is_public(1); + mojo_login($t, $user->name,"$$"); + + $t->websocket_ok("/ws/subscribe")->send_ok("list_domain_requests/".$base->id)->message_ok->finish_ok; + is($t->message->[1],'[]'); + + mojo_request($t, "clone", { id_domain => $base->id }); + my ($clone) = grep { $_->{id_owner} == $user->id } $base->clones; + Ravada::Request->start_domain(uid => $user->id + ,id_domain => $clone->{id}); + Ravada::Request->shutdown_domain(uid => $user->id + ,name => $clone->{name}); + Ravada::Request->prepare_base(uid => user_admin->id + ,id_domain => $clone->{id}); + + $t->websocket_ok("/ws/subscribe")->send_ok("list_domain_requests/".$clone->{id})->message_ok->finish_ok; + my $list0 = $t->message->[1]; + my $list= decode_json($list0); + isa_ok($list,'ARRAY'); + ok(@$list,"Expecting pending requests for owner"); + for my $request (@$list) { + is_deeply([sort keys %$request],[qw(command date_req id id_domain status)]); + ok($request->{id} =~ /^\d+$/); + ok($request->{id} > 0); + is($request->{id_domain},$clone->{id}); + ok($request->{command}); + like($request->{date_req}, qr/^\d{4}-\d{2}-\d{2}/); + isnt($request->{status},'done'); + } + + mojo_login($t, $other_user->name,"$$"); + $t->websocket_ok("/ws/subscribe")->send_ok("list_domain_requests/".$clone->{id})->message_ok->finish_ok; + is($t->message->[1],'[]'); + + mojo_login($t, $user->name,"$$"); + + my $message = $t->websocket_ok("/ws/subscribe")->send_ok("list_domain_requests/".$clone->{id}) + ->message_ok; + + for ( 1 .. 5 ) { + sleep 1; + $message = $message->message_ok; + last if $t->message->[1] eq '[]'; + } + + is($t->message->[1],'[]'); + $message->finish_ok(); + + $base->is_public($is_public); + user_admin->make_admin($user->id); + + Ravada::Request->remove(uid => user_admin->id + ,name => $clone->{name}); + wait_request(); + + mojo_request($t, "remove_base", { id_domain => $base->id }) + if !$is_base; + +} ######################################################################################## init('/etc/ravada.conf',0); @@ -500,6 +570,8 @@ for my $vm_name ( @{rvd_front->list_vm_types} ) { test_list_machines_non_admin($t,\@bases); test_bases_access($t,\@bases); + test_domain_requests($t, $bases[0]); + remove_old_domains_req(); while( list_machines_user($t) ) { remove_old_domains_req(); diff --git a/templates/main/needs_restart.html.ep b/templates/main/needs_restart.html.ep index a21714e39..f4fa45c51 100644 --- a/templates/main/needs_restart.html.ep +++ b/templates/main/needs_restart.html.ep @@ -1,4 +1,4 @@ -
+
<%=l 'The changes will apply on next restart' %> + +
    +
  • [{{request.id}}] {{request.command}} {{request.status}}
  • +
+
+ +
+ +
From 6c886ce7145ed7b08a1a1b1c9ac01b22e3398622 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 11 May 2026 16:59:08 +0200 Subject: [PATCH 118/141] wip: this test no longer applies --- t/nodes/10_basic.t | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index 708194ac4..081a32c46 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -881,7 +881,6 @@ sub test_clone_remote($vm, $node) { is($clone->list_instances,1); - _test_old_base($base, $vm); _test_clones($base, $clone->_vm); $clone->remove(user_admin); is($clone->list_instances,undef); @@ -889,21 +888,6 @@ sub test_clone_remote($vm, $node) { is($base->list_instances,undef); } -sub _test_old_base($base, $vm) { - my $sth = connector->dbh->prepare( - "DELETE FROM bases_vm " - ." WHERE id_domain=? AND id_vm=?" - ); - $sth->execute($base->id, $vm->id); - - my $base_f = Ravada::Front::Domain->open($base->id); - - my $info = $base_f->info(user_admin); - is($info->{bases}->{$vm->id}->{enabled},1) ; - - is(scalar keys %{$info->{bases}}, 2); -} - sub _test_clones($base, $vm) { my $info = $base->info(user_admin); ok($info->{clones}) or return; From f57941684916da97656e0176e46969e02249bc64 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Tue, 12 May 2026 11:47:55 +0200 Subject: [PATCH 119/141] test: check ISO images entries are unique (#2326) * test: check ISO images entries are unique * test: run the iso test in gh actions --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/workflows/github-action-test.yml | 2 +- t/repository/10_iso.t | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/.github/workflows/github-action-test.yml b/.github/workflows/github-action-test.yml index 327dc76be..fefd4dd3d 100644 --- a/.github/workflows/github-action-test.yml +++ b/.github/workflows/github-action-test.yml @@ -59,7 +59,7 @@ jobs: - name: Test mock VM run: prove -lr t/vm/60_new_args.t t/30_request.t - name: Test create from ISO - run: prove -lr t/request/25_create_from_iso.t t/vm/d10_not_download.t + run: prove -lr t/request/25_create_from_iso.t t/vm/d10_not_download.t t/repository/10_iso.t - name: Test hardware run: prove -lr t/request/30_hardware.t - name: Test downgrade diff --git a/t/repository/10_iso.t b/t/repository/10_iso.t index e9b63e075..6f8b9571e 100644 --- a/t/repository/10_iso.t +++ b/t/repository/10_iso.t @@ -139,10 +139,33 @@ sub test_post_login() { } } +sub test_unique_entries() { + my $sth = connector->dbh->prepare("SELECT * FROM iso_images"); + $sth->execute(); + my %uniq; + while ( my $row = $sth->fetchrow_hashref) { + if (defined $row->{url}) { + if ( defined $row->{file_re} ) { + $row->{url_file_re}=$row->{url}." : ".$row->{file_re}; + } else { + $row->{url_file_re}=$row->{url}; + } + } + for my $field ( qw(name description url_file_re file_re)) { + my $value = $row->{$field}; + next if !defined $value; + $uniq{$field}->{$value} //= 0; + ok(!$uniq{$field}->{$value}++,"Expecting unique $field '$value'"); + } + } +} + #################################################################### +test_unique_entries(); test_insert_locale(); test_insert_request(); +test_unique_entries(); SKIP: { skip("SKIPPED: Test must run as root",8) if $<; From 0563c28c4e33eec1adf92814824e929e09ccee18 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Tue, 12 May 2026 12:45:33 +0200 Subject: [PATCH 120/141] wip: tests for downloading ISO in node --- lib/Ravada/Request.pm | 13 ++++++++++++- lib/Ravada/VM/Void.pm | 11 ++--------- t/nodes/10_basic.t | 3 +++ 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index 9de9a065d..d4dad0051 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -315,6 +315,7 @@ our %CMD_VALIDATE = ( ,remove_base_vm=> \&_validate_remove_base_vm ,open_exposed_ports => \&_validate_open_exposed_ports ,close_exposed_ports => \&_validate_close_exposed_ports + ,download => \&_validate_download ); sub _init_connector { @@ -1132,10 +1133,14 @@ sub _check_downloading($self) { my $req_download = $self->_search_request('download', id_iso => $id_iso2); if ($has_cd && !$req_download) { + my @args_vm; + push @args_vm,( vm => $self->defined_arg('vm') ) if $self->defined_arg('vm'); + push @args_vm,( id_vm => $self->defined_arg('id_vm') ) if $self->defined_arg('id_vm'); + $req_download = Ravada::Request->download( id_iso => $id_iso2 ,uid => Ravada::Utils::user_daemon->id - ,vm => $self->defined_arg('vm') + ,@args_vm ); } if (! $req_download) { @@ -1280,6 +1285,12 @@ sub _validate_close_exposed_ports($self) { $domain_f->_data('ports_exposed' => 0); } +sub _validate_download($self) { + if (!$self->defined_arg('id_vm') && !$self->defined_arg('vm')) { + $self->error("Error: provide either id_vm or vm"); + $self->status('done'); + } +} sub _last_insert_id { _init_connector(); diff --git a/lib/Ravada/VM/Void.pm b/lib/Ravada/VM/Void.pm index ca3c1d816..94ab63d7a 100644 --- a/lib/Ravada/VM/Void.pm +++ b/lib/Ravada/VM/Void.pm @@ -216,15 +216,8 @@ sub _add_cdrom($self, $domain, %args) { $sth->execute($id_iso); my $row = $sth->fetchrow_hashref(); return if !$row->{has_cd}; - $iso_file = $row->{device}; - if (!$iso_file) { - $iso_file = $row->{name}; - $iso_file =~ s/\s/_/g; - $iso_file=$self->dir_img."/".lc($iso_file).".iso"; - if (! -e $iso_file ) { - $self->write_file($iso_file,Dump({iso => "ISO mock $row->{name}"})); - } - } + $iso_file = $self->search_volume_path_re(qr($row->{file_re})); + confess "$row->{file_re} not found in ".$self->name if !$iso_file; } $iso_file = '' if $iso_file eq ''; $domain->add_volume( diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index 081a32c46..7be9f4927 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -2249,6 +2249,9 @@ sub test_spinoff_remote($vm, $node) { } sub test_base_only_in_node_add_hw($vm, $node) { + my $iso = $node->_search_iso( search_id_iso('Alpine%64') ); + my $device_cdrom = $node->search_volume_path_re(qr($iso->{file_re})); + $node->remove_file($device_cdrom); my $base = _req_create($node); Ravada::Request->add_hardware( From f27439be11f2974908759f48e2ca0d0197888509 Mon Sep 17 00:00:00 2001 From: Yago004 <147386653+Yago004@users.noreply.github.com> Date: Wed, 13 May 2026 11:25:40 +0200 Subject: [PATCH 121/141] Add Translations (#2325) * Add translations * Fix spelling mistakes * Fix generic file --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/Ravada/I18N/ca.po | 348 +++++++++++++++++- lib/Ravada/I18N/en.po | 240 +++++++++++- lib/Ravada/I18N/es.po | 219 ++++++++++- lib/Ravada/I18N/id.po | 2 +- lib/Ravada/I18N/tr.po | 2 +- script/rvd_front | 8 +- templates/main/admin_hostdev.html.ep | 6 +- templates/main/admin_machines.html.ep | 2 +- templates/main/admin_settings.html.ep | 2 +- templates/main/list_bases_ng.html.ep | 2 +- templates/main/settings_generic.html.ep | 6 +- templates/main/storage_list.html.ep | 12 +- templates/main/upload_users.html.ep | 4 +- templates/main/vm_hostdev.html.ep | 2 +- .../list_next_bookings_today.html.ep | 4 +- 15 files changed, 801 insertions(+), 58 deletions(-) diff --git a/lib/Ravada/I18N/ca.po b/lib/Ravada/I18N/ca.po index 7dfb7451f..e340bfb22 100644 --- a/lib/Ravada/I18N/ca.po +++ b/lib/Ravada/I18N/ca.po @@ -1962,10 +1962,11 @@ msgstr "La nova contrasenya només pot contenir paraules i nombres" msgid "Password and their confirmation do not match!" msgstr "La confirmació de la contrasenya no coincideix!" -msgid "This server has reservations for today. Machines from users out of the booking list will be shutdown." -msgstr "" -"Aquest servidor té reserves per avui. Les màquines d'usuaris fora de la " -"reserva seran aturades." +msgid "This server has reservations for today." +msgstr "Aquest servidor té reserves per avui." + +msgid "Machines from users out of the booking list will be shutdown." +msgstr "Les màquines d'usuaris fora de la reserva seran aturades." msgid "disabled" msgstr "desactivat" @@ -2249,14 +2250,6 @@ msgstr "Guardar canvis" msgid "This booking overlaps already scheduled reservations" msgstr "Aquesta reserva es superposa a les reserves ja programades" -#, fuzzy -msgid "" -"This server has reservations for today. Machines from users out of\n" -" the booking list will be shutdown." -msgstr "" -"Aquest servidor té reserves per avui. Màquines d'usuaris fora de\n" -" la llista de reserves es tancarà." - msgid "This field is required" msgstr "Aquest camp és obligatori" @@ -2352,7 +2345,7 @@ msgstr "Seguiu aquests passos per a la configuració del client Spice" msgid "Share" msgstr "Comparteix" -msgid "Mange Host Devices" +msgid "Manage Host Devices" msgstr "Gestionar dispositius amfitrions" msgid "Compact data volumes daily." @@ -2362,7 +2355,334 @@ msgid "Auto Compact" msgstr "Auto compressió" msgid "Host Devices" -msgstr "Dispositiu amfitrió" +msgstr "Dispositius amfitrions" msgid "Access denied: user not found or insufficient permissions." msgstr "Accés denegat: usuari no trobat o permissos insuficients." + +msgid "Add host device" +msgstr "Afegir dispositiu amfitrió" + +msgid "LDAP groups" +msgstr "Grups LDAP" + +msgid "Local groups" +msgstr "Grups locals" + +msgid "Create a group." +msgstr "Crea un grup." + +msgid "This virtual machine has no local group restrictions." +msgstr "Aquesta màquina virtual no té restriccions locals" + +msgid "Routes" +msgstr "Rutes" + +msgid "Storage" +msgstr "Emmagatzematge" + +msgid "list" +msgstr "llista" + +msgid "purge" +msgstr "purgar" + +msgid "default" +msgstr "per defecte" + +msgid "Enabled" +msgstr "Habilitat" + +msgid "Size" +msgstr "Espai" + +msgid "Used" +msgstr "Utilitzat" + +msgid "Available" +msgstr "Disponible" + +msgid "Path" +msgstr "Ruta" + +msgid "New Storage" +msgstr "Nou emmagatzematge" + +msgid "Unused volumes" +msgstr "Volums no utilitzats" + +msgid "Remove Selected" +msgstr "Esborrar seleccionat" + +msgid "back" +msgstr "enrere" + +msgid "IP address" +msgstr "Adreça IP" + +msgid "Netmask" +msgstr "Màscara de red" + +msgid "DHCP start" +msgstr "Inici DHCP" + +msgid "DHCP end" +msgstr "Final DHCP" + +msgid "Auto Start" +msgstr "Inici automàtic" + +msgid "Auto create users" +msgstr "Crear usuaris automàticament" + +msgid "Authenticated users are allowed to log in" +msgstr "Usuaris autenticats poden inciar sessió" + +msgid "Auth OpenID" +msgstr "Autenticació OpenID" + +msgid "Logout URL" +msgstr "URL de tancament de sessió" + +msgid "Charts" +msgstr "Gràfics" + +msgid "share" +msgstr "comparteix" + +msgid "Allow users to clone this base" +msgstr "Permet als usuaris clonar aquesta base" + +msgid "Show the base when the user already created a clone" +msgstr "Mostra la base quan l'usuari ja ha creat un clon" + +msgid "There are no groups defined." +msgstr "No hi han grups definits." + +msgid "This must be enabled in global settings too." +msgstr "També s'ha d'activar en la configuració global." + +msgid "This base is available for all users" +msgstr "Aquesta base està disponible per tots els usuaris" + +msgid "A new clone of this base will be created." +msgstr "Un nou clon d'aquesta base serà creat." + +msgid "new bundle" +msgstr "nou ramat" + +msgid "All the virtual machines in the bundle will share the same private virtual network" +msgstr "Totes les màquines virtuals en el ramat compartiran la mateixa xarxa privada" + +msgid "create" +msgstr "crear" + +msgid "Groups are required to set up bookings. No groups found." +msgstr "Els grups son necessaris per configurar reserves. No s'han trobat grups." + +msgid "Groups are required to set up bookings. Some groups were found but no members belong to them." +msgstr "Els grups son necessaris per configurar reserves. S'han trobat grups però no tenen membres." + +msgid "Add new entries here." +msgstr "Afegeix aquí noves entrades." + +msgid "Clones will inherit this value unless defined in their options." +msgstr "Els clons heretaran aquest valor tret que estigui definit a les seves opcions." + +msgid "Batch upload" +msgstr "Pujada per lots" + +msgid "upload members" +msgstr "pujar membres" + +msgid "New Route" +msgstr "Nova Ruta" + +msgid "storage" +msgstr "emmagatzematge" + +msgid "filter" +msgstr "filtre" + +msgid "apply" +msgstr "aplicar" + +msgid "You must restart already connected virtual machines to apply network changes." +msgstr "Cal reiniciar les màquines virtuals ja connectades per aplicar els canvis de xarxa." + +msgid "available" +msgstr "disponible" + +msgid "Install remmina with RDP plugin to view Windows and Linux virtual machines with RDP." +msgstr "Instal·lar remmina amb el plugin RDP per veure les màquines virtuals de Windows i Linux amb RDP." + +msgid "Another tool that works well with SPICE is remmina." +msgstr "Una altre eina que funciona bé amb SPICE és remmina." + +msgid "If you installed it for RDP support it will work for SPICE too." +msgstr "Si l'has instal·lat per suport RPD funcionarà també per SPICE." + +msgid "RDP works out of the box because the client is installed by default." +msgstr "RDP funciona des del primer moment perquè el client està instal·lat per defecte." + +msgid "Install Microsoft Remote Desktop from the Mac App Store." +msgstr "Instal·lar Microsoft Remote Desktop des de la Mac App Store." + +msgid "list command" +msgstr "comanda per llistar" + +msgid "Auto compact virtual machines." +msgstr "Auto comprimir màquines virtuals." + +msgid "Time when compact will be scheduled." +msgstr "Hora en la que es programarà la compressió." + +msgid "Directory" +msgstr "Directori" + +msgid "Only previously authorized users can log in." +msgstr "Només usuaris prèviament autoritzats poden iniciar sessió." + +msgid "You can grant access to users from the users administration form:" +msgstr "Podeu concedir accés a usuaris des del formulari d'administració d'usuaris:" + +msgid "upload" +msgstr "pujar" + +msgid "Time period" +msgstr "Període de temps" + +msgid "Groups allowed" +msgstr "Grups permesos" + +msgid "Only Host Devices" +msgstr "Només Dispositius Amfitrions" + +msgid "Upload Users" +msgstr "Puja Usuaris" + +msgid "Upload a text file with the user names and passwords separated by a colon :" +msgstr "Puja un fitxer de text amb els noms i les contrasenyes dels usuaris separats per dos punts :" + +msgid "Upload" +msgstr "Pujar" + +msgid "This virtual machine will be prepared as a base, then cloned." +msgstr "Aquesta màquina virtual es prepararà com a base i després es clonarà." + +msgid "Enables schedule server bookings for exclusive use" +msgstr "Habilita programar reserves de servidor per ús exclusiu" + +msgid "connected" +msgstr "conectat" + +msgid "Session disconnected" +msgstr "Sessió desconectada" + +msgid "connect again" +msgstr "torna a conectar" + +msgid "disconnected" +msgstr "desconnectat" + +msgid "This machine is base and all changes will be applied to its clones." +msgstr "Aquesta màquina és base i tots els canvis s'aplicaràn als seus clons." + +msgid "Private" +msgstr "Privat" + +msgid "Show all" +msgstr "Mostra tot" + +msgid "This machine is running and can't be changed" +msgstr "Aquesta màquina està en marxa i no pot ser modificada" + +msgid "Current CPUs" +msgstr "CPUs actuals" + +msgid "Used %" +msgstr "Utilitzat %" + +msgid "hours" +msgstr "hores" + +msgid "1 hour" +msgstr "1 hora" + +msgid "2 hours" +msgstr "2 hores" + +msgid "3 hours" +msgstr "3 hores" + +msgid "6 hours" +msgstr "6 hores" + +msgid "8 hours" +msgstr "8 hores" + +msgid "days" +msgstr "dies" + +msgid "1 day" +msgstr "1 dia" + +msgid "2 days" +msgstr "2 dies" + +msgid "3 days" +msgstr "3 dies" + +msgid "6 days" +msgstr "6 dies" + +msgid "weeks" +msgstr "setmanes" + +msgid "1 week" +msgstr "1 setmana" + +msgid "2 weeks" +msgstr "2 setmanes" + +msgid "3 weeks" +msgstr "3 setmanes" + +msgid "4 weeks" +msgstr "4 setmanes" + +msgid "months" +msgstr "mesos" + +msgid "1 month" +msgstr "1 mes" + +msgid "2 months" +msgstr "2 mesos" + +msgid "3 months" +msgstr "3 mesos" + +msgid "6 months" +msgstr "6 mesos" + +msgid "9 months" +msgstr "9 mesos" + +msgid "years" +msgstr "anys" + +msgid "1 year" +msgstr "1 any" + +msgid "2 years" +msgstr "2 anys" + +msgid "3 years" +msgstr "3 anys" + +msgid "4 years" +msgstr "4 anys" + +msgid "5 years" +msgstr "5 anys" diff --git a/lib/Ravada/I18N/en.po b/lib/Ravada/I18N/en.po index 10a7b4f15..a48c922d1 100644 --- a/lib/Ravada/I18N/en.po +++ b/lib/Ravada/I18N/en.po @@ -1825,12 +1825,11 @@ msgstr "Password and their confirmation do not match!" msgid "Today Schedule" msgstr "Today Schedule" -msgid "" -"This server has reservations for today. Machines from users out of\n" -" the booking list will be shutdown." -msgstr "" -"This server has reservations for today. Machines from users out of\n" -" the booking list will be shutdown." +msgid "This server has reservations for today." +msgstr "This server has reservations for today." + +msgid "Machines from users out of the booking list will be shutdown." +msgstr "Machines from users out of the booking list will be shutdown." msgid "allowed" msgstr "allowed" @@ -2177,8 +2176,8 @@ msgstr "Share" msgid "Host Devices" msgstr "Host Devices" -msgid "Mange Host Devices" -msgstr "Mange Host Devices" +msgid "Manage Host Devices" +msgstr "Manage Host Devices" msgid "Auto Compact" msgstr "Auto Compact" @@ -2207,6 +2206,228 @@ msgstr "Show Clones" msgid "starting" msgstr "starting" +msgid "Add host device" +msgstr "Add host device" + +msgid "LDAP groups" +msgstr "LDAP groups" + +msgid "Local groups" +msgstr "Local groups" + +msgid "Create a group." +msgstr "Create a group." + +msgid "This virtual machine has no local group restrictions." +msgstr "This virtual machine has no local group restrictions." + +msgid "Routes" +msgstr "Routes" + +msgid "Storage" +msgstr "Storage" + +msgid "list" +msgstr "list" + +msgid "purge" +msgstr "purge" + +msgid "default" +msgstr "default" + +msgid "Enabled" +msgstr "Enabled" + +msgid "Size" +msgstr "Size" + +msgid "Used" +msgstr "Used" + +msgid "Available" +msgstr "Available" + +msgid "Path" +msgstr "Path" + +msgid "New Storage" +msgstr "New Storage" + +msgid "Unused volumes" +msgstr "Unused volumes" + +msgid "Remove Selected" +msgstr "Remove Selected" + +msgid "back" +msgstr "back" + +msgid "IP address" +msgstr "IP address" + +msgid "Netmask" +msgstr "Netmask" + +msgid "DHCP start" +msgstr "DHCP start" + +msgid "DHCP end" +msgstr "DHCP end" + +msgid "Auto Start" +msgstr "Auto Start" + +msgid "Auto create users" +msgstr "Auto create users" + +msgid "Authenticated users are allowed to log in" +msgstr "Authenticated users are allowed to log in" + +msgid "Auth OpenID" +msgstr "Auth OpenID" + +msgid "Logout URL" +msgstr "Logout URL" + +msgid "Charts" +msgstr "Charts" + +msgid "share" +msgstr "share" + +msgid "There are no groups defined." +msgstr "There are no groups defined." + +msgid "A new clone of this base will be created." +msgstr "A new clone of this base will be created." + +msgid "new bundle" +msgstr "new bundle" + +msgid "All the virtual machines in the bundle will share the same private virtual network" +msgstr "All the virtual machines in the bundle will share the same private virtual network" + +msgid "create" +msgstr "create" + +msgid "Groups are required to set up bookings. No groups found." +msgstr "Groups are required to set up bookings. No groups found." + +msgid "Groups are required to set up bookings. Some groups were found but no members belong to them." +msgstr "Groups are required to set up bookings. Some groups were found but no members belong to them." + +msgid "Add new entries here." +msgstr "Add new entries here." + +msgid "Batch upload" +msgstr "Batch upload" + +msgid "upload members" +msgstr "upload members" + +msgid "New Route" +msgstr "New Route" + +msgid "storage" +msgstr "storage" + +msgid "list command" +msgstr "list command" + +msgid "apply" +msgstr "apply" + +msgid "You must restart already connected virtual machines to apply network changes." +msgstr "You must restart already connected virtual machines to apply network changes." + +msgid "available" +msgstr "available" + +msgid "Install remmina with RDP plugin to view Windows and Linux virtual machines with RDP." +msgstr "Install remmina with RDP plugin to view Windows and Linux virtual machines with RDP." + +msgid "Another tool that works well with SPICE is remmina." +msgstr "Another tool that works well with SPICE is remmina." + +msgid "If you installed it for RDP support it will work for SPICE too." +msgstr "If you installed it for RDP support it will work for SPICE too." + +msgid "RDP works out of the box because the client is installed by default." +msgstr "RDP works out of the box because the client is installed by default." + +msgid "Install Microsoft Remote Desktop from the Mac App Store." +msgstr "Install Microsoft Remote Desktop from the Mac App Store." + +msgid "Auto compact virtual machines." +msgstr "Auto compact virtual machines." + +msgid "Time when compact will be scheduled." +msgstr "Time when compact will be scheduled." + +msgid "Directory" +msgstr "Directory" + +msgid "Only previously authorized users can log in." +msgstr "Only previously authorized users can log in." + +msgid "You can grant access to users from the users administration form:" +msgstr "You can grant access to users from the users administration form:" + +msgid "upload" +msgstr "upload" + +msgid "Time period" +msgstr "Time period" + +msgid "Groups allowed" +msgstr "Groups allowed" + +msgid "Only Host Devices" +msgstr "Only Host Devices" + +msgid "Upload Users" +msgstr "Upload Users" + +msgid "Upload a text file with the user names and passwords separated by a colon :" +msgstr "Upload a text file with the user names and passwords separated by a colon :" + +msgid "Upload" +msgstr "Upload" + +msgid "This virtual machine will be prepared as a base, then cloned." +msgstr "This virtual machine will be prepared as a base, then cloned." + +msgid "Enables schedule server bookings for exclusive use" +msgstr "Enables schedule server bookings for exclusive use" + +msgid "connected" +msgstr "connected" + +msgid "Session disconnected" +msgstr "Session disconnected" + +msgid "connect again" +msgstr "connect again" + +msgid "disconnected" +msgstr "disconnected" + +msgid "This machine is base and all changes will be applied to its clones." +msgstr "This machine is base and all changes will be applied to its clones." + +msgid "Private" +msgstr "Private" + +msgid "Show all" +msgstr "Show all" + +msgid "This machine is running and can't be changed" +msgstr "This machine is running and can't be changed" + +msgid "Current CPUs" +msgstr "Current CPUs" + msgid "hours" msgstr "hours" @@ -2290,3 +2511,6 @@ msgstr "4 years" msgid "5 years" msgstr "5 years" + +msgid "Used %" +msgstr "Used %" diff --git a/lib/Ravada/I18N/es.po b/lib/Ravada/I18N/es.po index 944982914..b9c61fe50 100644 --- a/lib/Ravada/I18N/es.po +++ b/lib/Ravada/I18N/es.po @@ -2045,10 +2045,11 @@ msgstr "" msgid "Choose the virtualization type of the Virtual Machine." msgstr "Elige el tipo de virtualización para este tipo de máquinas virtuales." -msgid "This server has reservations for today. Machines from users out of the booking list will be shutdown." -msgstr "" -"Este servidor tiene reservas hoy. Las máquinas de usuarios sin reserva serán " -"apagadas." +msgid "This server has reservations for today." +msgstr "Este servidor tiene reservas hoy." + +msgid "Machines from users out of the booking list will be shutdown." +msgstr "Las máquinas de usuarios fuera de la lista de reservas se apagarán." msgid "Data" msgstr "Data" @@ -2205,13 +2206,6 @@ msgstr "" "puede cambiar los parámetros de cualquier máquina virtual clonada de una " "base en posesión." -msgid "" -"This server has reservations for today. Machines from users out of\n" -" the booking list will be shutdown." -msgstr "" -"Este servidor tiene reservas para hoy. Las máquinas de usuarios fuera de\n" -" la lista de reservas se apagarán." - msgid "options" msgstr "opciones" @@ -2296,7 +2290,7 @@ msgstr "Compartir" msgid "Host Devices" msgstr "Dispositivos anfitriones" -msgid "Mange Host Devices" +msgid "Manage Host Devices" msgstr "Administrar anfitriones" msgid "Clones will inherit this value unless defined in their options." @@ -2320,6 +2314,201 @@ msgstr "Compresión automática" msgid "Show Clones" msgstr "Mostrar clones" +msgid "This base is available for all users" +msgstr "Esta base está disponible para todos los usuarios" + +msgid "A new clone of this base will be created." +msgstr "Un nuevo clon de esta base será creado." + +msgid "This virtual machine has no local group restrictions." +msgstr "Esta máquina virtual no tiene restricciones locales." + +msgid "share" +msgstr "compartir" + +msgid "new bundle" +msgstr "nuevo racimo" + +msgid "All the virtual machines in the bundle will share the same private virtual network" +msgstr "Todas las máquinas virtuales en el racimo compartirán la misma red virtual privada" + +msgid "create" +msgstr "crear" + +msgid "Batch upload" +msgstr "Subida por lotes" + +msgid "upload members" +msgstr "subir miembros" + +msgid "Routes" +msgstr "Rutas" + +msgid "New Route" +msgstr "Nueva Ruta" + +msgid "Storage" +msgstr "Almacenamiento" + +msgid "New Storage" +msgstr "Nuevo Almacenamiento" + +msgid "Enabled" +msgstr "Habilitado" + +msgid "Size" +msgstr "Tamaño" + +msgid "Used" +msgstr "Usado" + +msgid "Available" +msgstr "Disponible" + +msgid "Path" +msgstr "Ruta" + +msgid "Directory" +msgstr "Directorio" + +msgid "back" +msgstr "atrás" + +msgid "Remove Selected" +msgstr "Eliminar selección" + +msgid "Add host device" +msgstr "Añadir dispositivo anfitrión" + +msgid "list command" +msgstr "comando para listar" + +msgid "apply" +msgstr "aplicar" + +msgid "IP address" +msgstr "Dirección IP" + +msgid "Netmask" +msgstr "Máscara de red" + +msgid "DHCP start" +msgstr "Inicio DHCP" + +msgid "DHCP end" +msgstr "Fin DHCP" + +msgid "Auto Start" +msgstr "Inicio automático" + +msgid "You must restart already connected virtual machines to apply network changes." +msgstr "Debes reiniciar las máquinas virtuales que ya están conectadas para aplicar los cambios de red." + +msgid "Auto create users" +msgstr "Crear usuarios automáticamente" + +msgid "Authenticated users are allowed to log in" +msgstr "Usuarios autenticados pueden iniciar sesión" + +msgid "Auth OpenID" +msgstr "Autenticación OpenID" + +msgid "Logout URL" +msgstr "URL de cierre de sesión" + +msgid "Auto compact virtual machines." +msgstr "Auto comprimir máquinas virtuales." + +msgid "Time when compact will be scheduled." +msgstr "Hora en la que se progamará la compresión." + +msgid "Only previously authorized users can log in." +msgstr "Solo usuarios previamente autorizados pueden iniciar sesión." + +msgid "You can grant access to users from the users administration form:" +msgstr "Puedes conceder acceso a usuarios desde el formulario de administración de usuarios:" + +msgid "upload" +msgstr "subir" + +msgid "Charts" +msgstr "Gráficos" + +msgid "Time period" +msgstr "Periodo de tiempo" + +msgid "Install remmina with RDP plugin to view Windows and Linux virtual machines with RDP." +msgstr "Instalar remmina con el plugin RDP para visualizar las máquinas virtuales de Windows y Linux con RDP." + +msgid "Another tool that works well with SPICE is remmina." +msgstr "Otra herramienta que funciona bien con SPICE es remmina." + +msgid "If you installed it for RDP support it will work for SPICE too." +msgstr "Si lo instalaste para soporte RDP funcionará también para SPICE." + +msgid "RDP works out of the box because the client is installed by default." +msgstr "RDP funciona desde el primer momento porque el cliente está instalado por defecto." + +msgid "Install Microsoft Remote Desktop from the Mac App Store." +msgstr "Instala Microsoft Remote Desktop desde la Mac App Store." + +msgid "Groups allowed" +msgstr "Grupos permitidos" + +msgid "Only Host Devices" +msgstr "Solo Dispositivos Anfitriones" + +msgid "Groups are required to set up bookings. No groups found." +msgstr "Los grupos son necesarios para configurar reservas. No se han encontrado grupos." + +msgid "Groups are required to set up bookings. Some groups were found but no members belong to them." +msgstr "Los grupos son necesarios para configurar reservas. Se encontraron algunos grupos pero no tienen miembros." + +msgid "Add new entries here." +msgstr "Añade aquí nuevas entradas." + +msgid "Upload Users" +msgstr "Sube Usuarios" + +msgid "Upload a text file with the user names and passwords separated by a colon :" +msgstr "Sube un fichero de texto con los nombres y las contraseñas de los usuarios separadas por dos puntos :" + +msgid "Upload" +msgstr "Subir" + +msgid "This virtual machine will be prepared as a base, then cloned." +msgstr "Esta máquina virtual se preparará como base y después se clonará." + +msgid "Enables schedule server bookings for exclusive use" +msgstr "Habilita programar reservas de servidor para uso exclusivo" + +msgid "connected" +msgstr "conectado" + +msgid "Session disconnected" +msgstr "Sesión desconectada" + +msgid "connect again" +msgstr "vuelve a conectar" + +msgid "disconnected" +msgstr "desconectado" + +msgid "This machine is base and all changes will be applied to its clones." +msgstr "Esta máquina es base y todos los cambios se aplicarán a sus clones." + +msgid "Private" +msgstr "Privado" + +msgid "Show all" +msgstr "Mostrar todo" + +msgid "This machine is running and can't be changed" +msgstr "Esta máquina está en marcha y no puede ser modificada" + +msgid "Current CPUs" +msgstr "CPUs actuales" + msgid "hours" msgstr "horas" @@ -2403,3 +2592,9 @@ msgstr "4 años" msgid "5 years" msgstr "5 años" + +msgid "Used %" +msgstr "Usado %" + +msgid "Unused volumes" +msgstr "Volúmenes sin utilizar" diff --git a/lib/Ravada/I18N/id.po b/lib/Ravada/I18N/id.po index 030b56a3b..f90a68cce 100644 --- a/lib/Ravada/I18N/id.po +++ b/lib/Ravada/I18N/id.po @@ -2343,7 +2343,7 @@ msgstr "Bagikan" msgid "Follow these steps for Spice client setup" msgstr "Ikuti langkah-langkah berikut untuk pengaturan klien Spice" -msgid "Mange Host Devices" +msgid "Manage Host Devices" msgstr "Kelola Perangkat Host" msgid "Auto Compact" diff --git a/lib/Ravada/I18N/tr.po b/lib/Ravada/I18N/tr.po index dd656556c..cc2b30c7c 100644 --- a/lib/Ravada/I18N/tr.po +++ b/lib/Ravada/I18N/tr.po @@ -2197,7 +2197,7 @@ msgstr "Paylaş" msgid "Host Devices" msgstr "Ana Makineler" -msgid "Mange Host Devices" +msgid "Manage Host Devices" msgstr "Ana Makineleri Yönet" msgid "Auto Compact" diff --git a/script/rvd_front b/script/rvd_front index 8ca62718a..a3a6c2bb2 100644 --- a/script/rvd_front +++ b/script/rvd_front @@ -2914,14 +2914,18 @@ get '/booking' => sub($c) { } if (!$groups_local && !@groups_ldap) { + my $i18n = $c->stash->{i18n}; return $c->render(template => '/ng-templates/error' - ,message => "Groups are required to set up bookings. No groups found. Add new entries here." + ,message => $i18n->localize("Groups are required to set up bookings. No groups found.") + ."".' '.$i18n->localize("Add new entries here.")."" ); } if (!$members_found) { + my $i18n = $c->stash->{i18n}; return $c->render(template => '/ng-templates/error' - ,message => "Groups are required to set up bookings. Some groups where found but no members belong to them. Add new entries here." + ,message => $i18n->localize("Groups are required to set up bookings. Some groups were found but no members belong to them.") + ."".' '.$i18n->localize("Add new entries here.")."" ) } diff --git a/templates/main/admin_hostdev.html.ep b/templates/main/admin_hostdev.html.ep index e93771e8a..4b03768c6 100644 --- a/templates/main/admin_hostdev.html.ep +++ b/templates/main/admin_hostdev.html.ep @@ -79,8 +79,8 @@
- list command: - filter: + <%=l 'list command' %>: + <%=l 'filter' %>:
- Machines + <%=l 'Machines' %>
- +
<%=l 'Machine' %> <%=l 'locked' %>
<%=l 'This Machine is a base' %>
EnabledSizeUsedAvailableUsed %Path<%=l 'Enabled' %><%=l 'Size' %><%=l 'Used' %><%=l 'Available' %><%=l 'Used %' %><%=l 'Path' %>
From dd159758325690981a76287d4be4297f10920e5e Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 13 May 2026 11:59:31 +0200 Subject: [PATCH 122/141] wip: output from requests already decoded --- lib/Ravada/Front.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Ravada/Front.pm b/lib/Ravada/Front.pm index 728ffccfa..1fd484376 100644 --- a/lib/Ravada/Front.pm +++ b/lib/Ravada/Front.pm @@ -1787,7 +1787,7 @@ sub list_machine_types($self, $uid, $vm_type) { return {} if $req->status ne 'done'; my $types = {}; - $types = decode_json($req->output()) if $req->output; + $types = $req->output() if $req->output; $self->_cache_store($key,$types); From c9744aa6efbd04be67bc34efa85d052ac1cce724 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 13 May 2026 12:47:27 +0200 Subject: [PATCH 123/141] wip: reqs for machine types and isos come decoded now --- lib/Ravada.pm | 2 +- lib/Ravada/Front.pm | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index c9801d2ae..d995e547d 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -6322,7 +6322,7 @@ sub _cmd_list_isos($self, $request){ @isos = sort { "\L$a" cmp "\L$b" } $vm->search_volume_path_re(qr(.*\.iso$)) if $vm && $vm->vm; - $request->output(encode_json(\@isos)); + $request->output(\@isos); } sub _cmd_list_machine_types($self, $request) { diff --git a/lib/Ravada/Front.pm b/lib/Ravada/Front.pm index 1fd484376..b5b7e81cc 100644 --- a/lib/Ravada/Front.pm +++ b/lib/Ravada/Front.pm @@ -855,8 +855,7 @@ sub iso_file ($self, $id_vm, $uid) { my $isos = []; eval { - $isos = decode_json($req->output()) - if $req->output && !ref($req->output()) + $isos = $req->output() if $req->output; }; warn $@." for request=".$req->id." ".Dumper($req->output) if $@; From c03ce9e6b84e8a8aace33fa2d2d5c6c97f10de26 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 13 May 2026 13:47:36 +0200 Subject: [PATCH 124/141] wip: test machine types and isos --- lib/Ravada/Request.pm | 1 + t/front/15_misc.t | 70 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 t/front/15_misc.t diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index d4dad0051..d971640f6 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -219,6 +219,7 @@ qw( rsync_back cleanup list_host_devices + list_machine_types list_storage_pools refresh_machine refresh_machine_ports diff --git a/t/front/15_misc.t b/t/front/15_misc.t new file mode 100644 index 000000000..b71f6e8a4 --- /dev/null +++ b/t/front/15_misc.t @@ -0,0 +1,70 @@ +use warnings; +use strict; + +use Data::Dumper; +use Test::More; + +use lib 't/lib'; +use Test::Ravada; + +use feature qw(signatures); +no warnings "experimental::signatures"; + +my $RVD_BACK; +my $RVD_FRONT; + +sub _init() { + $RVD_BACK = rvd_back( ); + $RVD_FRONT = rvd_front(); + + ok($Ravada::CONNECTOR,"\$Ravada::Connector wasn't set"); + ok($Ravada::CONNECTOR,"\$Ravada::Connector wasn't set"); + + ok($RVD_BACK->connector()); +} + +#################################################################################### + +sub test_machine_types($vm_name) { + my $req = Ravada::Request->list_machine_types( + vm_type => $vm_name + ,uid => user_admin->id + ); + wait_request(debug => 0); + + isa_ok($req->output,'HASH'); + + my $types = $RVD_FRONT->list_machine_types(user_admin->id, $vm_name); + + ok(ref($types)); + isa_ok($types,'HASH') +} + +sub test_list_isos($vm_name) { + my $vm = Ravada::VM->_open_type(type => $vm_name); + + my $req = Ravada::Request->list_isos( + uid => user_admin->id + ,id_vm => $vm->id + ); + wait_request(); + isa_ok($req->output,'ARRAY'); + + my $iso = $RVD_FRONT->iso_file(user_admin->id, $vm_name); + isa_ok($iso,'ARRAY'); +} +#################################################################################### + +init(); +clean(); + +SKIP: { + _init(); + for my $vm_name ( vm_names() ) { + test_machine_types($vm_name); + test_list_isos($vm_name); + } +} + +end(); +done_testing(); From d02575a7eb7e1a29eaf6290e923dcd7d79dd9134 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 13 May 2026 14:19:11 +0200 Subject: [PATCH 125/141] wip: mock cdrom for test --- .github/workflows/github-action-test.yml | 2 ++ lib/Ravada/VM/Void.pm | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/.github/workflows/github-action-test.yml b/.github/workflows/github-action-test.yml index 4da11e961..8acea7eb5 100644 --- a/.github/workflows/github-action-test.yml +++ b/.github/workflows/github-action-test.yml @@ -66,3 +66,5 @@ jobs: run: prove -lr t/20_volumes.t - name: Test downgrade run: prove -lr t/downgrade_reqs.t + - name: Test Front + run: prove -lr t/front/15_misc.t diff --git a/lib/Ravada/VM/Void.pm b/lib/Ravada/VM/Void.pm index 94ab63d7a..f75dad238 100644 --- a/lib/Ravada/VM/Void.pm +++ b/lib/Ravada/VM/Void.pm @@ -217,6 +217,13 @@ sub _add_cdrom($self, $domain, %args) { my $row = $sth->fetchrow_hashref(); return if !$row->{has_cd}; $iso_file = $self->search_volume_path_re(qr($row->{file_re})); + if (!$iso_file) { + $iso_file = $row->{file_re}; + $iso_file =~ s/\*/_/g; + $iso_file = $self->dir_img()."/".$iso_file; + open my $out,">",$iso_file or die "$! $iso_file"; + close $out; + } confess "$row->{file_re} not found in ".$self->name if !$iso_file; } $iso_file = '' if $iso_file eq ''; From fd98164c542f40baec98ce0c3bf83742272e140c Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 14 May 2026 15:58:13 +0200 Subject: [PATCH 126/141] wip: test rebase void --- lib/Ravada/VM/Void.pm | 5 +++-- t/vm/r20_rebase.t | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/Ravada/VM/Void.pm b/lib/Ravada/VM/Void.pm index f75dad238..cd28d0ef9 100644 --- a/lib/Ravada/VM/Void.pm +++ b/lib/Ravada/VM/Void.pm @@ -1,6 +1,6 @@ package Ravada::VM::Void; -use Carp qw(carp croak); +use Carp qw(carp croak cluck); use Data::Dumper; use Encode; use Encode::Locale; @@ -222,6 +222,7 @@ sub _add_cdrom($self, $domain, %args) { $iso_file =~ s/\*/_/g; $iso_file = $self->dir_img()."/".$iso_file; open my $out,">",$iso_file or die "$! $iso_file"; + print $out Dump({ iso => $iso_file }); close $out; } confess "$row->{file_re} not found in ".$self->name if !$iso_file; @@ -755,7 +756,7 @@ sub _iso_name($self, $iso, $request=undef, $verbose=0) { $name = $self->_storage_path($self->default_storage_pool_name)."/".$name unless $name =~ m{^/}; open my $out,">",$name or die "$! $name"; - print $out "...\n"; + print $out Dump({ iso => $name }); close $out; return $name; diff --git a/t/vm/r20_rebase.t b/t/vm/r20_rebase.t index 126fd1552..94bd01efb 100644 --- a/t/vm/r20_rebase.t +++ b/t/vm/r20_rebase.t @@ -307,6 +307,8 @@ sub test_rebase_clone($vm) { clean(); $ENV{LANG}='C'; +unlink '/var/tmp/rvd_void/frankie/alpine-standard-3.23-x86_64.iso'; + for my $vm_name (vm_names() ) { ok($vm_name); SKIP: { From 8750aaf1272f0ec42259f2841bd954412486c54d Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Tue, 19 May 2026 11:24:07 +0200 Subject: [PATCH 127/141] wip: work around clone too fast and tests improved * fixed tests for new req output format --- lib/Ravada/Volume/Class.pm | 6 +++++- t/vm/20_base.t | 3 +-- t/vm/94_ports_exposed.t | 2 +- t/vm/d20_disks.t | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/Ravada/Volume/Class.pm b/lib/Ravada/Volume/Class.pm index fd7efc50c..eff0767a1 100644 --- a/lib/Ravada/Volume/Class.pm +++ b/lib/Ravada/Volume/Class.pm @@ -62,7 +62,7 @@ sub _chmod($self, $mode, $file=$self->file) { my ($out,$err) = $vm->run_command("chmod",$mode_o,$file); die $err if $err; } else { - confess if !-e $file; + confess $file if !-e $file; chmod $mode,$file or die "$! chmod $mode $file"; } } @@ -116,6 +116,10 @@ sub _around_clone($orig, $self, %args) { file => $orig->($self, $file_clone) ,@domain ); + for ( 1 .. 2 ) { + last if $self->vm->file_exists($file_clone); + sleep 1; + } $self->_chmod(oct(600), $file_clone); return $ret; diff --git a/t/vm/20_base.t b/t/vm/20_base.t index 0e7993662..224360ffe 100644 --- a/t/vm/20_base.t +++ b/t/vm/20_base.t @@ -1961,6 +1961,7 @@ for my $vm_name ( vm_names() ) { $BASE = create_domain($vm); } flush_rules() if !$<; + my $domaina = test_create_domain($vm_name); test_prepare_base($vm_name, $domaina); @@ -1977,8 +1978,6 @@ for my $vm_name ( vm_names() ) { test_prepare_base_volatile($vm); test_change_display_settings($vm); - test_display_drivers($vm,0); - test_display_drivers($vm,1); #remove after testing display type test_display_iptables($vm); diff --git a/t/vm/94_ports_exposed.t b/t/vm/94_ports_exposed.t index a32275e88..65547e3ee 100644 --- a/t/vm/94_ports_exposed.t +++ b/t/vm/94_ports_exposed.t @@ -58,7 +58,7 @@ sub test_expose_port($vm) { my $remote_ip2 = '10.0.0.2'; my $local_ip = $vm->ip; - is($domain->_data('ports_exposed'),undef); + is($domain->_data('ports_exposed'),0); my $req = Ravada::Request->start_domain( uid => user_admin->id ,id_domain => $domain->id diff --git a/t/vm/d20_disks.t b/t/vm/d20_disks.t index f738cf19b..8d00d184c 100644 --- a/t/vm/d20_disks.t +++ b/t/vm/d20_disks.t @@ -549,7 +549,7 @@ sub _machine_types($vm) { like($req->output,qr/./); my $machine_types = {}; - $machine_types = decode_json($req->output()); + $machine_types = $req->output(); return $machine_types; } From bbaf297adb03ab3c6bc5887e75ef5e6743e35f1a Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Fri, 29 May 2026 12:58:52 +0200 Subject: [PATCH 128/141] wip: removed debug --- templates/main/vm_base.html.ep | 1 - 1 file changed, 1 deletion(-) diff --git a/templates/main/vm_base.html.ep b/templates/main/vm_base.html.ep index f83c1d3c1..09f03fbfe 100644 --- a/templates/main/vm_base.html.ep +++ b/templates/main/vm_base.html.ep @@ -40,7 +40,6 @@

<%=l 'Nodes' %>

-{{showmachine.bases}}
Date: Mon, 1 Jun 2026 10:22:16 +0200 Subject: [PATCH 129/141] wip: improved chain requests --- lib/Ravada/Request.pm | 57 +++++++++++++++++++++++-- lib/Ravada/VM.pm | 1 - t/kvm/n10_nodes.t | 19 +++++---- t/nodes/00_req.t | 96 +++++++++++++++++++++++++++++++++++++++---- 4 files changed, 152 insertions(+), 21 deletions(-) diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index cd1f720bf..2bd3e6e73 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -1815,6 +1815,9 @@ sub _validate_remove_base_vm($req) { my $domain = Ravada::Front::Domain->open($req->args('id_domain')); + $req->_chain_previous_set_base($domain); + $req->_chain_previous_migrate_children($domain); + my $id_vm = $req->defined_arg('id_vm'); $id_vm = $req->defined_arg('id_node') if !defined $id_vm; @@ -1831,6 +1834,52 @@ sub _validate_remove_base_vm($req) { return; } + $req->_chain_migrate_clones($domain, $id_vm, \@other_vms); + $req->_chain_requested_clone( $domain->id); +} + +sub _chain_previous_migrate_children($self, $domain) { + my $sth = $$CONNECTOR->dbh->prepare( + "SELECT id FROM requests " + ." WHERE id_domain=?" + ." AND id <> ? " + ." AND ( status='requested' OR status='working' ) " + ." AND ( command = 'migrate' )" + ); + $sth->execute($domain->id, $self->id); + while ( my ($id_prev) = $sth->fetchrow ) { + $self->_data('after_request_ok' => $id_prev) + } + + for my $clone ($domain->clones) { + $sth->execute($clone->{id}, $self->id); + while ( my ($id_prev) = $sth->fetchrow ) { + $self->_data('after_request_ok' => $id_prev) + } + if ( $clone->{is_base} ) { + my $domain2 = Ravada::Front::Domain->open($clone->{id}); + $self->_chain_previous_migrate_children($domain2) + } + } +} + +sub _chain_previous_set_base($self, $domain) { + my $sth = $$CONNECTOR->dbh->prepare( + "SELECT id FROM requests " + ." WHERE id_domain=?" + ." AND id <> ? " + ." AND ( status='requested' OR status='working' ) " + ." AND ( command = 'prepare_base' OR command='set_base_vm' " + ." OR command='remove_base_vm' )" + ); + $sth->execute($domain->id, $self->id); + while ( my ($id_prev) = $sth->fetchrow ) { + $self->_data('after_request_ok' => $id_prev) + } +} + +sub _chain_migrate_clones($self, $domain, $id_vm, $other_vms) { + my ($req_migrate, $req_rm); my ($req_migrate_prev, $req_rm_prev); @@ -1843,7 +1892,7 @@ sub _validate_remove_base_vm($req) { $req_migrate = Ravada::Request->migrate( uid => Ravada::Utils::user_daemon->id ,id_domain => $clone->{id} - ,id_node => $other_vms[0] + ,id_node => $other_vms->[0] ,shutdown => 1 ,start => $start ); @@ -1863,10 +1912,10 @@ sub _validate_remove_base_vm($req) { $req_rm_prev = $req_rm; } } - $req->after_request_ok($req_rm->id) if $req_rm; - $req->after_request_ok($req_migrate->id) if $req_migrate; + $self->after_request_ok($req_rm->id) if $req_rm; + $self->after_request_ok($req_migrate->id) if $req_migrate; + - _chain_requested_clone($req, $domain->id); } sub _chain_requested_clone($req, $id_domain) { diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index 7f2c7c0e4..87c438d17 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -466,7 +466,6 @@ sub _connect_ssh($self) { sub _ssh($self) { my $ssh = $self->netssh; - return if !$ssh; return $ssh if $ssh->check_master; warn "WARNING: ssh error '".$ssh->error."'" if $ssh->error; $self->netssh->disconnect; diff --git a/t/kvm/n10_nodes.t b/t/kvm/n10_nodes.t index dd0936b83..1baf8627b 100644 --- a/t/kvm/n10_nodes.t +++ b/t/kvm/n10_nodes.t @@ -216,15 +216,16 @@ sub test_iptables($node, $remote_ip, $local_ip, $local_port) { sub test_domain_on_remote { my ($vm_name, $node) = @_; - my $domain; - eval { - $domain = $node->create_domain( - name => new_domain_name - ,id_owner => user_admin->id - ,id_iso => search_id_iso('Alpine') - ); - }; - is($@,'',"Expecting no domain in remote node by now"); + my $name = new_domain_name(); + my $req_create = Ravada::Request->create_domain( + name => $name + ,id_owner => user_admin->id + ,id_iso => search_id_iso('Alpine') + ,id_vm => $node->id + ); + wait_request(); + is($req_create->error,''); + my $domain = rvd_back->search_domain($name);; my $req = Ravada::Request->remove_domain( uid => user_admin->id diff --git a/t/nodes/00_req.t b/t/nodes/00_req.t index a3c3bf7e7..21ea4d505 100644 --- a/t/nodes/00_req.t +++ b/t/nodes/00_req.t @@ -359,6 +359,92 @@ sub _search_request_previous($id_domain, $id_req_prev) { } } +sub test_chain_children($vm, $node1, $node2) { + + my $base1 = create_domain($vm); + $base1->_data('id_vm' => $node1->id); + + my $base2 = create_base($vm); + $base2->_data('is_base' => 1); + $base2->_data('id_base' => $base1->id); + $base2->_data('id_vm' => $node1->id); + + my $clone = create_domain_v2(vm => $vm); + $clone->_data('id_base' => $base2->id); + $clone->_data('id_vm' => $node1->id); + + my $req_migrate = Ravada::Request->migrate( + uid => user_admin->id + ,id_domain => $clone->id + ,id_node => $node2->id + ); + + my $req_rm = Ravada::Request->remove_base_vm( + uid => user_admin->id + ,id_domain => $base1->id + ,id_vm => $node1->id + ); + my $requirements = $req_rm->_data('after_request_ok'); + $requirements = [$requirements] unless ref($requirements); + + my ($found) = grep { $_ == $req_migrate->id } @$requirements; + ok($found,"Expecting ".$req_migrate->id." in @$requirements") + or die Dumper($requirements); + remove_domain_db($base1); +} + +sub test_chain_remove_vm($vm, $node1, $node2) { + + my $base = create_domain($vm); + + $base->_data('id_vm' => $node1->id); + + my $req = Ravada::Request->set_base_vm( + uid => user_admin->id + ,id_domain => $base->id + ,id_vm => $node2->id + ); + + my $expected = [ $req->id, $req->after_request_ok]; + my $req_rm = Ravada::Request->remove_base_vm( + uid => user_admin->id + ,id_domain => $base->id + ,id_vm => $node2->id + ); + is_deeply($req_rm->_data('after_request_ok'), $expected); + $req_rm->_delete(); + + $req_rm = Ravada::Request->remove_base_vm( + uid => user_admin->id + ,id_domain => $base->id + ,id_vm => $node2->id + ); + is_deeply($req_rm->_data('after_request_ok'), $expected); + + $req_rm->_delete(); + + $req_rm = Ravada::Request->set_base_vm( + uid => user_admin->id + ,id_domain => $base->id + ,id_vm => $node2->id + ,value => 0 + ); + is_deeply($req_rm->_data('after_request_ok'), $expected) + or die Dumper($req_rm->_data('after_request_ok')); + + $req_rm->_delete(); + + my $req_set = Ravada::Request->set_base_vm( + uid => user_admin->id + ,id_domain => $base->id + ,id_vm => $node2->id + ,value => 1 + ); + is($req_set->id(),$req->id); + + remove_domain_db($base); +} + ############################################################################### init(); @@ -370,13 +456,6 @@ for my $vm_name (vm_names() ) { SKIP: { my $msg = "SKIPPED: $vm_name virtual manager not found ".($@ or ''); - my $REMOTE_CONFIG = remote_config($vm_name); - if (!keys %$REMOTE_CONFIG) { - my $msg = "skipped, missing the remote configuration for $vm_name in the file " - .$Test::Ravada::FILE_CONFIG_REMOTE; - diag($msg); - skip($msg,10); - } diag($msg) if !$vm; skip($msg,10) if !$vm; @@ -387,6 +466,9 @@ for my $vm_name (vm_names() ) { my $node1 = _create_remote_node($vm_name); my $node2 = _create_remote_node($vm_name); + test_chain_remove_vm($vm, $node1, $node2); + test_chain_children($vm, $node1, $node2); + test_req_gone($vm); test_req_failed($vm); From 8e432dc7e5650f2295c529b0cd2ffdcbf94c4a07 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 3 Jun 2026 13:46:44 +0200 Subject: [PATCH 130/141] wip: improved ssh when remote busy --- lib/Ravada/Domain.pm | 4 ++++ lib/Ravada/VM.pm | 21 ++++++++++++++++----- lib/Ravada/VM/KVM.pm | 22 +++++++++++++++++----- 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 924b1561f..d92a0464d 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -1320,7 +1320,11 @@ sub _check_tmp_volumes($self) { next unless $vol->file && $vol->file =~ /\.(TMP|SWAP)\./; next unless $vol->backing_file; $vol->delete() ; + my $base = Ravada::Domain->open($self->id_base); + die "Error: Base not found for ".$self->name." [id=".$self->id_base."]" + if !$base; + my @volumes = $base->list_files_base_target; my ($file_base) = grep { $_->[1] eq $vol->info->{target} } @volumes; if (!$file_base) { diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index 87c438d17..d58a66bd1 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -456,7 +456,7 @@ sub _connect_ssh($self) { if ( $ssh->error ) { $self->_cached_active(0); $self->_data('cached_down' => time); - # warn "Error connecting to ".$self->host." : ".$ssh->error(); + warn "ERROR: connecting to ".$self->host." : ".$ssh->error(); return; } } @@ -466,11 +466,22 @@ sub _connect_ssh($self) { sub _ssh($self) { my $ssh = $self->netssh; - return $ssh if $ssh->check_master; - warn "WARNING: ssh error '".$ssh->error."'" if $ssh->error; - $self->netssh->disconnect; + if ($ssh) { + return $ssh if $ssh->check_master; + warn "WARNING: ssh error '".$ssh->error."'" if $ssh && $ssh->error; + $self->netssh->disconnect; + } + $self->clear_netssh(); + + $ssh = $self->netssh(); + return $ssh if $ssh && $ssh->check_master(); + + $self->disconnect(); + $self->clear_netssh(); - return $self->netssh; + $ssh = $self->netssh(); + return $ssh if $ssh; + return $self->_connect_ssh(); } sub _around_create_domain { diff --git a/lib/Ravada/VM/KVM.pm b/lib/Ravada/VM/KVM.pm index ba7930123..cfb78bcf4 100644 --- a/lib/Ravada/VM/KVM.pm +++ b/lib/Ravada/VM/KVM.pm @@ -113,8 +113,9 @@ sub _connect { my $address = $con_type."+".$transport ."://".'root@'.$self->host ."/system"; - eval { - $vm = Sys::Virt->new( + for ( 1 .. 3 ) { + eval { + $vm = Sys::Virt->new( address => $address ,auth => 1 ,credlist => [ @@ -122,7 +123,11 @@ sub _connect { Sys::Virt::CRED_PASSPHRASE, ] ); - }; + }; + warn $@ if $@; + last if $vm && $vm->is_alive; + sleep 1; + } my $error = $@; my $is_alive; eval { $is_alive = $vm->is_alive if $vm }; @@ -398,7 +403,7 @@ sub remove_file($self,@files) { if ($self->file_exists($file)) { $self->_remove_file_os($file); } else { - warn "Warning: '$file' not found\n"; + warn "Warning: ".$self->name." '$file' not found\n"; } } $vol->delete if $vol; @@ -602,7 +607,14 @@ sub _file_exists_remote($self, $file) { } confess "Error: invalid file '$file'" if $file =~ /[`;(\[" ]/; - my $ssh = $self->_ssh; + my $ssh; + for ( 1 .. 3 ) { + $ssh = $self->_ssh; + last if $ssh; + warn "retry ssh"; + $self->disconnect(); + sleep 1; + } confess "Error: no _ssh ".$self->name if !$ssh; my ($out,$err) = $ssh->capture2("ls $file"); my @ls = split /\n/,$out; From 1797ed3303f3dc390ce03c3616749633534cba4e Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 4 Jun 2026 11:50:11 +0200 Subject: [PATCH 131/141] wip: do not remove base with nodes --- lib/Ravada/Domain.pm | 6 +++++- lib/Ravada/Request.pm | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index d92a0464d..75b549cea 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -5744,9 +5744,13 @@ sub set_base_vm($self, %args) { confess "ERROR: user required" if !$user; + $vm = $node if $node; + $id_vm = $vm->id if !defined $id_vm; + + die "Error: there are already nodes in this node.\n" + if $self->clones(id_vm => $id_vm); $request->status("working") if $request; - $vm = $node if $node; $vm = Ravada::VM->open($id_vm) if !$vm; if ( !$vm || !$vm->is_active || !$vm->vm) { diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index 2bd3e6e73..b0103487d 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -1817,6 +1817,7 @@ sub _validate_remove_base_vm($req) { $req->_chain_previous_set_base($domain); $req->_chain_previous_migrate_children($domain); + $req->_chain_requested_clone( $domain->id); my $id_vm = $req->defined_arg('id_vm'); $id_vm = $req->defined_arg('id_node') if !defined $id_vm; @@ -1835,7 +1836,6 @@ sub _validate_remove_base_vm($req) { } $req->_chain_migrate_clones($domain, $id_vm, \@other_vms); - $req->_chain_requested_clone( $domain->id); } sub _chain_previous_migrate_children($self, $domain) { From d59e946e00a45364014faedaecf9129b1cbf65e3 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 4 Jun 2026 11:55:57 +0200 Subject: [PATCH 132/141] wip: do not remove base when there are clones in node --- lib/Ravada/Domain.pm | 2 +- lib/Ravada/Request.pm | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 75b549cea..a266b486c 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -5748,7 +5748,7 @@ sub set_base_vm($self, %args) { $id_vm = $vm->id if !defined $id_vm; die "Error: there are already nodes in this node.\n" - if $self->clones(id_vm => $id_vm); + if $self->_data('id_vm') == $id_vm || $self->clones(id_vm => $id_vm); $request->status("working") if $request; $vm = Ravada::VM->open($id_vm) if !$vm; diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index b0103487d..b348791a9 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -1828,7 +1828,7 @@ sub _validate_remove_base_vm($req) { push @other_vms,($id_vm_other) if $id_vm_other != $id_vm && _node_is_active($id_vm_other); } - if ( !@other_vms && $domain->clones ) { + if ( !@other_vms && ( $domain->_data('id_vm')==$id_vm || $domain->clones ) ) { $req->error("Error: there are no other VMs to migrate clones when removing base " .$domain->id." ".$domain->name); $req->status('done'); @@ -1883,6 +1883,14 @@ sub _chain_migrate_clones($self, $domain, $id_vm, $other_vms) { my ($req_migrate, $req_rm); my ($req_migrate_prev, $req_rm_prev); + if ( $domain->_data('id_vm') == $id_vm ) { + $req_migrate_prev = Ravada::Request->migrate( + uid => user_daemon->id + ,id_domain => $domain->id + ,id_node => $other_vms->[0] + ); + } + for my $clone ($domain->clones) { # migrate clones to other vms From e03bb4dab27b59a9962ad14310aa286a6e601de6 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 4 Jun 2026 12:01:19 +0200 Subject: [PATCH 133/141] wip: request by user daemon --- lib/Ravada/Request.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index b348791a9..17ab75d75 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -1885,7 +1885,7 @@ sub _chain_migrate_clones($self, $domain, $id_vm, $other_vms) { if ( $domain->_data('id_vm') == $id_vm ) { $req_migrate_prev = Ravada::Request->migrate( - uid => user_daemon->id + uid => Ravada::Utils::user_daemon->id ,id_domain => $domain->id ,id_node => $other_vms->[0] ); From 61c7576cce968d731c0424c4717447a5d54a6625 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 8 Jun 2026 13:23:35 +0200 Subject: [PATCH 134/141] wip: chain prepare and remove base --- lib/Ravada.pm | 42 +++++++++++- lib/Ravada/Domain.pm | 18 +++--- lib/Ravada/Domain/KVM.pm | 1 + lib/Ravada/Request.pm | 86 ++++++++++++++++++------- lib/Ravada/VM/KVM.pm | 2 +- t/30_request.t | 4 +- t/nodes/10_basic.t | 5 +- t/nodes/60_hardware.t | 134 ++++++++++++++++++++++++++++++++++----- 8 files changed, 235 insertions(+), 57 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index e589f3d01..2c4e33f55 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -3769,7 +3769,14 @@ sub remove_domain { .($name or '') ."' , maybe already removed.\n"; - my $domain = Ravada::Front::Domain->open($id); + my $domain; + eval { + $domain = Ravada::Front::Domain->open($id); + }; + warn $@ if $@; + if ( !$domain ) { + die "Machine $id not found. Maybe already removed.\n"; + } my @volumes = $domain->list_volumes(); my $vm = Ravada::VM->open($domain->_data('id_vm')); die "No vm ".$domain->_data('id_vm') if !$vm || !$vm->vm; @@ -4171,6 +4178,7 @@ sub process_requests { $self->_kill_stale_process(); $self->_kill_dead_process(); $self->_timeout_requests(); + $self->_finish_failed_initialized(); } my $sth = $CONNECTOR->dbh->prepare("SELECT id,id_domain,command FROM requests " @@ -4284,7 +4292,7 @@ sub _timeout_requests($self) { my $sth = $CONNECTOR->dbh->prepare( "SELECT id,pid, start_time, date_changed " ." FROM requests " - ." WHERE ( status = 'working' or status = 'stopping' )" + ." WHERE ( status = 'working' or status = 'stopping' or status = 'initializing' )" ." AND date_changed >= ? " ." AND command <> 'move_volume'" ." ORDER BY date_req " @@ -4413,6 +4421,31 @@ sub _kill_stale_process($self) { $sth->finish; } +sub _finish_failed_initialized($self) { + + my $sth = $CONNECTOR->dbh->prepare( + "SELECT id,pid,command,start_time " + ." FROM requests " + ." WHERE start_time '')" + ); + $sth->execute(time - 2); + while (my ($id, $pid, $command, $start_time) = $sth->fetchrow) { + next if -e "/proc/$pid"; + if ($pid == $$ ) { + warn "HOLY COW! I should kill pid $pid stale for ".(time - $start_time) + ." seconds, but I won't because it is myself"; + next; + } + my $request = Ravada::Request->open($id); + $request->stop(0); # do not show warning + warn "stopping ".$request->id." ".$request->command; + } + $sth->finish; +} + + sub _kill_dead_process($self) { my $sth = $CONNECTOR->dbh->prepare( @@ -5710,6 +5743,9 @@ sub _cmd_change_hardware { && $data->{n_virt_cpu} <= $info->{max_virt_cpu}) ; + die "Error: $hardware can only be changed while machine down.\n" + if $hardware eq 'disk' && $domain->is_active; + $domain->change_hardware( $request->args('hardware') ,$request->defined_arg('index') @@ -6352,7 +6388,7 @@ sub _cmd_list_cpu_models($self, $request) { my $id_domain = $request->args('id_domain'); my $domain = Ravada::Domain->open($id_domain); - return [] if !$domain->_vm || !$domain->_vm->can_list_cpu_models(); + return [] if !$domain || !$domain->_vm || !$domain->_vm->can_list_cpu_models(); my $info = $domain->get_info(); my $vm = $domain->_vm->vm; diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index a266b486c..cbcda4371 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -867,12 +867,12 @@ sub _allow_shutdown { } my $user = $args{user} || confess "ERROR: Missing user arg"; + return if $user->can_shutdown_all; + if ( $self->id_base() && $user->can_shutdown_clone()) { - my $base = Ravada::Domain->open($self->id_base) + my $base = Ravada::Front::Domain->open($self->id_base) or confess "ERROR: Base domain id: ".$self->id_base." not found"; return if $base->id_owner == $user->id; - } elsif($user->can_shutdown_all) { - return; } confess "User ".$user->name." [".$user->id."] not allowed to shutdown ".$self->name ." owned by ".($self->id_owner or '') @@ -3057,7 +3057,7 @@ sub clones($self, %filter) { lock_hash(%$row); push @clones , $row; } - $self->_data('has_clones' => scalar(@clones)); + $self->_data('has_clones' => scalar(@clones)) unless %filter; return @clones; } @@ -5747,8 +5747,10 @@ sub set_base_vm($self, %args) { $vm = $node if $node; $id_vm = $vm->id if !defined $id_vm; - die "Error: there are already nodes in this node.\n" - if $self->_data('id_vm') == $id_vm || $self->clones(id_vm => $id_vm); + $value = 1 if !defined $value; + + die "Error: there are already clones in this node.\n" + if !$value && $self->clones(id_vm => $id_vm); $request->status("working") if $request; $vm = Ravada::VM->open($id_vm) if !$vm; @@ -5757,10 +5759,6 @@ sub set_base_vm($self, %args) { die "Error: VM ".Ravada::VM::_search_name($id_vm)." not available\n" } - $value = 1 if !defined $value; - - - my $id_request; if ($request) { $request->status("working"); diff --git a/lib/Ravada/Domain/KVM.pm b/lib/Ravada/Domain/KVM.pm index 59503b7f8..b1744b627 100644 --- a/lib/Ravada/Domain/KVM.pm +++ b/lib/Ravada/Domain/KVM.pm @@ -331,6 +331,7 @@ sub remove { } sub remove_instance($self, $user) { + return if !$self->domain; eval { $self->domain->destroy() if $self->domain->is_active(); }; diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index 17ab75d75..1c5229b92 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -788,7 +788,7 @@ sub _new_request { } my %args = @_; - $args{status} = 'requested'; + $args{status} = 'initializing'; $self->{command} = $args{command}; if ($args{name}) { @@ -996,13 +996,21 @@ sub _validate_remove_hardware($self) { my $args = $self->args(); - die "Error: you must pass option or index" - if !exists $args->{option} && !exists $args->{index} - && !defined $args->{option} && !defined $args->{index}; + if ( !exists $args->{option} && !exists $args->{index} + && !defined $args->{option} && !defined $args->{index}) { - die "Error: attribute value must be defined ". - join(" ", map { $_ or '' } %{$args->{option}}) - if $args->{option} && grep { !defined } values %{$args->{option}}; + $self->error("Error: you must pass option or index"); + $self->status('done'); + return; + } + + if ( $args->{option} && grep { !defined } values %{$args->{option}}) { + + $self->error("Error: attribute value must be defined ". + join(" ", map { $_ or '' } %{$args->{option}})); + $self->status('done'); + return; + } } @@ -1676,6 +1684,15 @@ sub set_base_vm { sub _validate_migrate($req) { my $domain = Ravada::Front::Domain->open($req->args('id_domain')); + + if ( $domain->is_volatile ) { + + $req->_status_error('done' + ,"Error: unsupported volatile domains migration." + ); + return; + } + if ($domain->_data('status') ne 'shutdown') { my $req_shutdown = Ravada::Request->shutdown_domain( uid => $req->args('uid') @@ -1698,8 +1715,10 @@ sub _validate_migrate($req) { ); my $id_vm_local = $domain->_id_vm_local(); - die "Error: node local not found for ".$domain->type - if !defined $id_vm_local; + if ( !defined $id_vm_local ) { + return $req->_status_error('done' + ,"Error: node local not found for ".$domain->type); + } my $id_node = $req->args('id_node'); @@ -1742,9 +1761,14 @@ sub _validate_set_base_vm($req) { return _validate_remove_base_vm($req) if !$value; my $domain = Ravada::Front::Domain->open($req->args('id_domain')); + + $req->_chain_prepare_base($domain); + my $id_vm_local = $domain->_id_vm_local(); - die "Error: node local not found for ".$domain->type - if !defined $id_vm_local; + if ( !defined $id_vm_local ) { + return $req->_status_error('done' + ,"Error: node local not found for ".$domain->type); + } my $id_vm = $req->defined_arg('id_vm'); $id_vm = $req->defined_arg('id_node') if !defined $id_vm; @@ -1771,6 +1795,21 @@ sub _validate_set_base_vm($req) { $req->after_request_ok($req_prev->id); } +sub _chain_prepare_base($self, $domain) { + my $sth = $$CONNECTOR->dbh->prepare( + "SELECT id FROM requests " + ." WHERE id_domain=? " + ." AND id <> ? " + ." AND ( command='prepare_base' OR command='post_prepare_base' " + ." OR command='set_base_vm' OR command='remove_base_vm' )" + ." AND ( status='requested' OR status='working' )" + ); + $sth->execute($domain->id, $self->id); + while ( my ($id) = $sth->fetchrow ) { + $self->after_request($id); + } +} + =head2 remove_base_vm Disables a base in a Virtual Manager @@ -1828,14 +1867,15 @@ sub _validate_remove_base_vm($req) { push @other_vms,($id_vm_other) if $id_vm_other != $id_vm && _node_is_active($id_vm_other); } - if ( !@other_vms && ( $domain->_data('id_vm')==$id_vm || $domain->clones ) ) { - $req->error("Error: there are no other VMs to migrate clones when removing base " - .$domain->id." ".$domain->name); - $req->status('done'); - return; + if ( $domain->clones ) { + if ( !@other_vms ) { + $req->error("Error: there are no other VMs to migrate clones when removing base " + .$domain->id." ".$domain->name); + $req->status('done'); + return; + } + $req->_chain_migrate_clones($domain, $id_vm, \@other_vms) } - - $req->_chain_migrate_clones($domain, $id_vm, \@other_vms); } sub _chain_previous_migrate_children($self, $domain) { @@ -2233,19 +2273,21 @@ Stops a request killing the process. =cut -sub stop($self) { +sub stop($self, $show_warn=1) { my $stale = ''; my $run_time = ''; if ($self->start_time) { $run_time = time - $self->start_time; - $stale = ", stale for $run_time seconds."; + $stale = ", stale for $run_time seconds." if $run_time; } warn "Killing ".$self->command ." , pid: ".( $self->pid or '') .$stale - ."\n"; + ."\n" if $show_warn; kill (15,$self->pid) if $self->pid; - $self->status('done',"Killed start process after $run_time seconds."); + $self->status('done'); + $self->error("Killed start process after $run_time seconds.") + if $self->pid; } sub _delete($self) { diff --git a/lib/Ravada/VM/KVM.pm b/lib/Ravada/VM/KVM.pm index cfb78bcf4..d22a2500e 100644 --- a/lib/Ravada/VM/KVM.pm +++ b/lib/Ravada/VM/KVM.pm @@ -403,7 +403,7 @@ sub remove_file($self,@files) { if ($self->file_exists($file)) { $self->_remove_file_os($file); } else { - warn "Warning: ".$self->name." '$file' not found\n"; + # warn "Warning: ".$self->name." '$file' not found\n"; } } $vol->delete if $vol; diff --git a/t/30_request.t b/t/30_request.t index f9d379035..96dbf79fe 100644 --- a/t/30_request.t +++ b/t/30_request.t @@ -243,7 +243,7 @@ sub test_requests_by_domain { is($domain->list_requests,1) or die Dumper([$domain->list_requests]); my $req2 = Ravada::Request->remove_base(uid => user_admin->id, id_domain => $domain->id); - is($domain->list_requests, 0); + is($domain->list_requests, 2); my $clone_name = new_domain_name(); my $req_clone = Ravada::Request->create_domain ( @@ -254,7 +254,7 @@ sub test_requests_by_domain { ); my $req4 = Ravada::Request->prepare_base(uid => user_admin->id, id_domain => $domain->id); - is($domain->list_requests,1,Dumper([map { $_->{command} } $domain->list_requests])); + is($domain->list_requests,3,Dumper([map { $_->{command} } $domain->list_requests])); rvd_back->_process_all_requests_dont_fork(); wait_request(); diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index 7be9f4927..50bb08997 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -1952,11 +1952,12 @@ sub test_nat($vm, $node, $set_localhost_natip=0) { $node->_data(nat_ip => ''); } -sub _download_alpine64 { +sub _download_alpine64($vm) { my $id_iso = search_id_iso('Alpine%64'); my $req = Ravada::Request->download( id_iso => $id_iso + ,id_vm => $vm->id ); wait_request(); is($req->error, ''); @@ -1971,7 +1972,7 @@ sub test_displays($vm, $node, $no_builtin=0) { return; # $base = create_domain($vm); } - _download_alpine64(); + _download_alpine64($vm); my $domain = $base->clone(name => new_domain_name, user => user_admin); my $n_displays = 1; diff --git a/t/nodes/60_hardware.t b/t/nodes/60_hardware.t index f5cf246ad..4fa78102a 100644 --- a/t/nodes/60_hardware.t +++ b/t/nodes/60_hardware.t @@ -4,6 +4,7 @@ use strict; use Carp qw(confess); use Data::Dumper; use Digest::MD5; +use Storable qw(dclone); use Test::More; use lib 't/lib'; @@ -40,8 +41,17 @@ sub test_graphics($vm, $node) { } sub test_driver_clone($vm, $node, $domain, $driver_name, $option) { - $domain->remove_base(user_admin) if $domain->is_base; - wait_request(); + if ( $domain->is_base ) { + Ravada::Request->remove_clones( + uid => user_admin->id + ,id_domain => $domain->id + ); + Ravada::Request->remove_base( + uid => user_admin->id + ,id_domain => $domain->id + ); + wait_request(); + } my $req = Ravada::Request->set_driver(uid => user_admin->id , id_domain => $domain->id , id_option => $option->{id} @@ -51,19 +61,40 @@ sub test_driver_clone($vm, $node, $domain, $driver_name, $option) { is($req->error,''); is($domain->get_driver($driver_name), $option->{value} , $driver_name); - $domain->prepare_base(user_admin); - $domain->set_base_vm(node => $node, user => user_admin); + Ravada::Request->set_base_vm( + uid => user_admin->id + ,id_domain => $domain->id + ,id_vm => $node->id + ); - my $clone = $domain->clone(name => new_domain_name, user => user_admin); - $clone->migrate($node); + my $name = new_domain_name(); + Ravada::Request->clone( + uid => user_admin->id + ,id_domain => $domain->id + ,name => $name + ); + wait_request(); + my $clone = rvd_back->search_domain($name); + Ravada::Request->migrate( + uid => user_admin->id + ,id_domain => $clone->id + ,id_node => $node->id + ); + wait_request(); my $clone2 = Ravada::Domain->open($clone->id); is($clone2->_vm->id,$node->id); is($clone2->get_driver($driver_name), $option->{value} , $driver_name); - $clone->remove(user_admin); - - $domain->remove_base(user_admin); + Ravada::Request->remove_clones( + uid => user_admin->id + ,id_domain => $domain->id + ); + wait_request(); + Ravada::Request->remove_base( + uid => user_admin->id + ,id_domain => $domain->id + ); wait_request(); } @@ -73,14 +104,37 @@ sub test_driver_migrate($vm, $node, $domain, $driver_name) { diag("No driver for $driver_name in ".$domain->type); next; }; - $domain->prepare_base(user_admin); - $domain->set_base_vm(node => $node, user => user_admin); + + # make sure prepare_base and the set_base_vm are chained + Ravada::Request->prepare_base( + uid => user_admin->id + ,id_domain => $domain->id + ) if !$domain->is_base(); + Ravada::Request->set_base_vm( + uid => user_admin->id + ,id_domain => $domain->id + ,id_vm => $node->id + ); + + wait_request(debug => 0); for my $option ($driver->get_options) { next if defined $domain->get_driver($driver_name) && $domain->get_driver($driver_name) eq $option->{value}; # diag("Testing $driver_name $option->{value} then migrate"); - my $clone = $domain->clone(name => new_domain_name, user => user_admin); + my $name = new_domain_name(); + my $req_clone = Ravada::Request->clone( + uid => user_admin->id + ,id_domain => $domain->id + ,name => $name + ); + wait_request(); + my $clone; + for ( 1 .. 3 ) { + $clone = rvd_back->search_domain($name); + last if $clone; + sleep 1; + } my $req = Ravada::Request->set_driver(uid => user_admin->id , id_domain => $clone->id , id_option => $option->{id} @@ -89,22 +143,28 @@ sub test_driver_migrate($vm, $node, $domain, $driver_name) { is($req->status,'done'); is($req->error,''); - $clone->migrate($node); + Ravada::Request->migrate( + uid => user_admin->id + ,id_domain => $clone->id + ,id_node => $node->id + ); + wait_request(); my $clone2 = Ravada::Domain->open($clone->id); is($clone2->_vm->id,$node->id); is($clone2->get_driver($driver_name), $option->{value} , $driver_name) or exit; - $clone->remove(user_admin); + Ravada::Request->remove( + uid => user_admin->id + ,name => $clone->name + ); last unless $ENV{TEST_LONG}; } - $domain->remove_base(user_admin); - wait_request(); } sub test_drivers_type($type, $vm, $node) { - my $domain = create_domain($vm->type); + my $domain = create_domain($vm); my $req = Ravada::Request->add_hardware(uid => user_admin->id , id_domain => $domain->id @@ -156,6 +216,7 @@ sub test_drivers_type($type, $vm, $node) { for my $vol (@vols) { ok (! -e $vol ) or die "$vol"; } + $domain = Ravada::Domain->open($domain->id); } $domain->remove(user_admin); @@ -192,6 +253,43 @@ sub _add_hardware($domain) { wait_request(debug => 0); } +sub _change_disk_size($domain) { + $domain->shutdown_now(user_admin) if $domain->is_active; + + my $disk = $domain->info(user_admin)->{hardware}->{disk}; + my $data = dclone($disk->[0]); + my $new_capacity = int($data->{capacity}*3.5); + $data->{capacity}=$new_capacity; + + my $req = Ravada::Request->change_hardware( + uid => user_admin->id + ,id_domain => $domain->id + ,index => 0 + ,data => $data + ,hardware => 'disk' + ); + wait_request(); + + my $domain2 = Ravada::Domain->open($domain->id); + my $disk2 = $domain2->info(user_admin)->{hardware}->{disk}; + my $data2 = dclone($disk2->[0]); + + is($data2->{capacity}, $new_capacity) or die $domain->name; + +} + +sub _do_test_change_hardware($domain, $hardware) { + + my %sub = ( + 'disk' => \&_change_disk_size + ); + my $sub = $sub{$hardware}; + if (!$sub) { + diag("No method to test change $hardware"); + return; + } + $sub->($domain); +} sub test_change_hardware($vm, @nodes) { diag("[".$vm->type."] testing remove with ".scalar(@nodes)." node ".join(",",map { $_->name } @nodes)); my $domain = create_domain($vm); @@ -231,6 +329,8 @@ sub test_change_hardware($vm, @nodes) { $tls = grep {$_->{driver} =~ /-tls/} @{$info->{hardware}->{$hardware}} if $hardware eq 'display'; + _do_test_change_hardware($domain, $hardware); + #TODO disk volumes in Void #next if $vm->type eq 'Void' && $hardware =~ /disk|volume/; From 8dbcc42a1f10d02a86210d3627b88fa7f5b9eff0 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 11 Jun 2026 12:22:00 +0200 Subject: [PATCH 135/141] wip: properly change disk on remote --- lib/Ravada/Domain.pm | 12 --- lib/Ravada/Domain/KVM.pm | 8 +- t/mojo/30_settings.t | 2 +- t/nodes/60_hardware.t | 155 ++++++++++++++++++++++++++++++++------- 4 files changed, 136 insertions(+), 41 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index cbcda4371..3d179de34 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -6547,13 +6547,6 @@ sub _around_change_hardware($orig, $self, $hardware, $index=undef, $data=undef) _fix_hw_ignore_fields($data); - my $real_id_vm; - if ($hardware eq 'disk' && !$self->_vm->is_local) { - $real_id_vm = $self->_vm->id; - my $vm_local = $self->_vm->new( host => 'localhost' ); - $self->_set_vm($vm_local, 1); - } - my $is_display_builtin; if ($hardware eq 'display') { @@ -6587,11 +6580,6 @@ sub _around_change_hardware($orig, $self, $hardware, $index=undef, $data=undef) $self->_redefine_instances() if $self->is_known(); } - if ( $real_id_vm ) { - my $id_vm = $real_id_vm; - my $vm = Ravada::VM->open($id_vm); - $self->_set_vm($vm, 1); - } $self->_post_change_hardware($hardware, $index, $data); } diff --git a/lib/Ravada/Domain/KVM.pm b/lib/Ravada/Domain/KVM.pm index b1744b627..3cbdd9017 100644 --- a/lib/Ravada/Domain/KVM.pm +++ b/lib/Ravada/Domain/KVM.pm @@ -2757,7 +2757,13 @@ sub _remove_device($self, $index, $device, $attribute_name0=undef, $attribute_va $msg = " $attribute_name0=$attribute_value ".join(",",@found) if defined $attribute_name0; - confess "ERROR: $device $msg ".($index or '') + my $index_text = ''; + $index_text = $index if defined $index; + + warn "ERROR: $device $msg $index_text" + ." not removed, only ".($ind)." found in ".$self->name; + + confess "ERROR: $device $msg $index_text" ." not removed, only ".($ind)." found in ".$self->name."\n"; } diff --git a/t/mojo/30_settings.t b/t/mojo/30_settings.t index 1801bde6f..8e391397e 100644 --- a/t/mojo/30_settings.t +++ b/t/mojo/30_settings.t @@ -463,7 +463,7 @@ sub _create_storage_pool($id_vm , $vm_name) { ,name => $name ,directory => $dir ); - wait_request( ); + wait_request( $req ); is($req->error,''); return $name; diff --git a/t/nodes/60_hardware.t b/t/nodes/60_hardware.t index 4fa78102a..134e1b66b 100644 --- a/t/nodes/60_hardware.t +++ b/t/nodes/60_hardware.t @@ -193,8 +193,22 @@ sub test_drivers_type($type, $vm, $node) { ok(!$@,"Expecting no error, got : ".($@ or '')); is($domain->get_driver($type), $option->{value}, $type); - $domain->prepare_base(user_admin); - $domain->set_base_vm(node => $node, user => user_admin); + my $req_prepare = Ravada::Request->prepare_base( + uid => user_admin->id + ,id_domain => $domain->id + ); + wait_request(); + is($req_prepare->status(),'done'); + is($req_prepare->error(),''); + + my $req = Ravada::Request->set_base_vm( + uid => user_admin->id + ,id_domain => $domain->id + ,id_vm => $node->id + ); + wait_request(); + is($req->status(),'done'); + is($req->error(),''); my $clone = $domain->clone(name => new_domain_name, user => user_admin); is($clone->get_driver($type), $option->{value}, $type); @@ -256,7 +270,10 @@ sub _add_hardware($domain) { sub _change_disk_size($domain) { $domain->shutdown_now(user_admin) if $domain->is_active; - my $disk = $domain->info(user_admin)->{hardware}->{disk}; + my $info = $domain->info(user_admin); + my $disk = $info->{hardware}->{disk}; + confess Dumper([$domain->_vm->name." ".$domain->name, $disk]) if !defined $disk->[0]; + my $data = dclone($disk->[0]); my $new_capacity = int($data->{capacity}*3.5); $data->{capacity}=$new_capacity; @@ -270,35 +287,75 @@ sub _change_disk_size($domain) { ); wait_request(); - my $domain2 = Ravada::Domain->open($domain->id); - my $disk2 = $domain2->info(user_admin)->{hardware}->{disk}; - my $data2 = dclone($disk2->[0]); + my $data2; + for ( 1 .. 3 ) { + my $domain2 = Ravada::Domain->open($domain->id); + my $disk2 = $domain2->info(user_admin)->{hardware}->{disk}; + $data2 = dclone($disk2->[0]); + last if int($data2->{capacity}*0.9) == int($new_capacity*0.9); + + Ravada::Request->refresh_machine( + uid => user_admin->id + ,id_domain => $domain->id + ,_force => 1 + ); + wait_request(); + } + + is(int($data2->{capacity}*0.9), int($new_capacity*0.9)) or die $domain->name; - is($data2->{capacity}, $new_capacity) or die $domain->name; } sub _do_test_change_hardware($domain, $hardware) { + $domain = Ravada::Domain->open($domain->id); my %sub = ( 'disk' => \&_change_disk_size ); my $sub = $sub{$hardware}; if (!$sub) { - diag("No method to test change $hardware"); return; } $sub->($domain); } -sub test_change_hardware($vm, @nodes) { - diag("[".$vm->type."] testing remove with ".scalar(@nodes)." node ".join(",",map { $_->name } @nodes)); - my $domain = create_domain($vm); - _add_hardware($domain); +sub _clone_and_add_volume($domain) { + my $name = new_domain_name(); + Ravada::Request->clone( + uid => user_admin->id + ,id_domain => $domain->id + ,name => $name + ); + my $clone; + for ( 1 .. 3 ) { + $clone = rvd_back->search_domain($name); + wait_request(); + } + my $data = { + 'file' => '', + 'driver' => { + 'cache' => 'writeback', + }, + 'device' => 'disk', + 'allocation' => '200M', + 'bus' => 'virtio', + 'capacity' => '1G', + 'type' => 'data' + }; + delete $data->{driver} if $domain->type eq 'Void'; - my $clone = $domain->clone(name => new_domain_name, user => user_admin); - $clone->add_volume(size => 128*1024 , type => 'data'); - my @volumes = $clone->list_volumes(); + my $req = Ravada::Request->add_hardware( + uid => user_admin->id + ,id_domain => $clone->id + ,name => 'disk' + ,'data' => $data + ); + wait_request(); + return $clone; +} + +sub _migrate_clone($clone, @nodes) { for my $node (@nodes) { for ( 1 .. 10 ) { @@ -307,29 +364,56 @@ sub test_change_hardware($vm, @nodes) { sleep 1; } is($node->ping(),1) or die "Error: I can't ping ".$node->ip; - $domain->set_base_vm( vm => $node, user => user_admin); + + my $req_set_base = Ravada::Request->set_base_vm( + uid => user_admin->id + ,id_domain => $clone->id_base() + ,id_vm => $node->id + ); + wait_request($req_set_base); + my $clone2 = $node->search_domain($clone->name); ok(!$clone2); - $clone->migrate($node); + + my $req_migrate = Ravada::Request->migrate( + uid => user_admin->id + ,id_domain => $clone->id + ,id_node => $node->id + ); + wait_request($req_migrate); $clone2 = $node->search_domain($clone->name); ok($clone2); } +} + +sub test_change_hardware($vm, @nodes) { + diag("Change hardware nodes=".scalar(@nodes)); + my $domain = create_domain($vm); + + _add_hardware($domain); + + my $clone = _clone_and_add_volume($domain); + my @volumes = $clone->list_volumes(); + + _migrate_clone($clone,@nodes); + $clone = Ravada::Domain->open($clone->id); + my $n_instances = $domain->list_instances(); my $info = $clone->info(user_admin); my %devices; for my $hardware ( sort keys %{$info->{hardware}} ) { $devices{$hardware} = scalar(@{$info->{hardware}->{$hardware}}); } - my @hardware = grep (!/^disk$/, sort keys %{$info->{hardware}}); - push @hardware,("disk"); + my @hardware = grep (!/^(disk|display|usb)$/, sort keys %{$info->{hardware}}); + push @hardware,("display","usb","disk"); for my $hardware (reverse @hardware) { next if $hardware =~ /cpu|features|memory/; my $tls = 0; $tls = grep {$_->{driver} =~ /-tls/} @{$info->{hardware}->{$hardware}} if $hardware eq 'display'; - _do_test_change_hardware($domain, $hardware); + _do_test_change_hardware($clone, $hardware); #TODO disk volumes in Void #next if $vm->type eq 'Void' && $hardware =~ /disk|volume/; @@ -341,7 +425,13 @@ sub test_change_hardware($vm, @nodes) { $n = scalar(@{$info->{hardware}->{$hardware}})-1 if $hardware eq 'usb controller'; - $clone->remove_controller($hardware,$n); + Ravada::Request->remove_hardware( + uid => user_admin->id + ,id_domain => $clone->id + ,name => $hardware + ,index => $n + ); + wait_request(debug => 0); is (scalar($clone->list_instances()), $n_instances); my $n_expected = scalar(@{$info->{hardware}->{$hardware}})-1; @@ -356,10 +446,16 @@ sub test_change_hardware($vm, @nodes) { for my $node ($vm, @nodes) { my $clone2 = $node->search_domain($clone->name); ok($clone2,"Expecting clone ".$clone->name." in remote node ".$node->name - ." when removing $hardware") or next; + ." when removing $hardware") or next; + + my $devices2; + if ($hardware eq 'disk') { + $devices2 = [ $clone2->list_volumes ]; + } else { + my $info2 = $clone2->info(user_admin); + $devices2 = $info2->{hardware}->{$hardware}; + } - my $info2 = $clone2->info(user_admin); - my $devices2 = $info2->{hardware}->{$hardware}; if ($hardware eq 'video' && $vm->type eq 'KVM') { is( scalar(@$devices2),1); is($devices2->[0]->{type},'none'); @@ -377,8 +473,13 @@ sub test_change_hardware($vm, @nodes) { } } - $clone->remove(user_admin); - $domain->remove(user_admin); + for my $name ( $clone->name, $domain->name ) { + Ravada::Request->remove_domain( + uid => user_admin->id + ,name => $name + ); + wait_request(); + } } ################################################################################## @@ -398,7 +499,7 @@ $Ravada::Domain::MIN_FREE_MEMORY = 256 * 1024; my @nodes; -for my $vm_name ( vm_names() ) { +for my $vm_name (reverse vm_names() ) { my $vm; eval { $vm = rvd_back->search_vm($vm_name) }; From c6c5052ed6a02ab20d183f785887c7193305073c Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 11 Jun 2026 13:53:37 +0200 Subject: [PATCH 136/141] wip: improved tests with nodes --- lib/Ravada/Domain/Void.pm | 4 ++-- lib/Ravada/Request.pm | 1 + t/30_request.t | 8 ++++---- t/nodes/60_hardware.t | 4 +++- t/request/30_hardware.t | 5 +++-- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/Ravada/Domain/Void.pm b/lib/Ravada/Domain/Void.pm index f7d6e8c77..e1ed46747 100644 --- a/lib/Ravada/Domain/Void.pm +++ b/lib/Ravada/Domain/Void.pm @@ -1082,7 +1082,7 @@ sub _change_hardware_disk($self, $index, $data_new) { my $driver; $driver = delete $data_new->{bus} if exists $data_new->{bus}; lock_hash(%$data_new); - return $self->_change_driver_disk($index, $driver) if $driver; + $self->_change_driver_disk($index, $driver) if $driver; die "Error: volume $index not found, only ".scalar(@volumes)." found." if $index >= scalar(@volumes); @@ -1090,7 +1090,7 @@ sub _change_hardware_disk($self, $index, $data_new) { my $file = $volumes[$index]->{file}; my $new_file; $new_file = $data_new->{file} if exists $data_new->{file}; - return $self->_change_disk_data($index, file => $new_file) if defined $new_file; + $self->_change_disk_data($index, file => $new_file) if defined $new_file; return if !$file; my $data; diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index 1c5229b92..6954875e9 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -968,6 +968,7 @@ sub _validate_remove_base($self) { $reqs_base[-1]->status('done'); $reqs_base[-2]->status('done'); } + $self->_chain_prepare_base($domain); $self->_chain_remove_bases_nodes($domain); } diff --git a/t/30_request.t b/t/30_request.t index 96dbf79fe..7b345b4ed 100644 --- a/t/30_request.t +++ b/t/30_request.t @@ -244,6 +244,9 @@ sub test_requests_by_domain { my $req2 = Ravada::Request->remove_base(uid => user_admin->id, id_domain => $domain->id); is($domain->list_requests, 2); + wait_request(debug => 0); + Ravada::Request->prepare_base(uid => user_admin->id, id_domain => $domain->id); + wait_request(debug => 0); my $clone_name = new_domain_name(); my $req_clone = Ravada::Request->create_domain ( @@ -252,11 +255,8 @@ sub test_requests_by_domain { ,id_base => $domain->id ,vm => $vm_name ); - + wait_request(); my $req4 = Ravada::Request->prepare_base(uid => user_admin->id, id_domain => $domain->id); - is($domain->list_requests,3,Dumper([map { $_->{command} } $domain->list_requests])); - - rvd_back->_process_all_requests_dont_fork(); wait_request(); is($req1->status , 'done'); diff --git a/t/nodes/60_hardware.t b/t/nodes/60_hardware.t index 134e1b66b..d705b66f7 100644 --- a/t/nodes/60_hardware.t +++ b/t/nodes/60_hardware.t @@ -406,7 +406,9 @@ sub test_change_hardware($vm, @nodes) { $devices{$hardware} = scalar(@{$info->{hardware}->{$hardware}}); } my @hardware = grep (!/^(disk|display|usb)$/, sort keys %{$info->{hardware}}); - push @hardware,("display","usb","disk"); + push @hardware,("display"); + push @hardware,("usb") if $vm->type ne 'Void'; + push @hardware,("disk"); for my $hardware (reverse @hardware) { next if $hardware =~ /cpu|features|memory/; my $tls = 0; diff --git a/t/request/30_hardware.t b/t/request/30_hardware.t index e768116d7..d3e7cfce7 100644 --- a/t/request/30_hardware.t +++ b/t/request/30_hardware.t @@ -32,11 +32,12 @@ my $TLS; ######################################################################## # -sub _download_alpine64 { +sub _download_alpine64($id_vm) { my $id_iso = search_id_iso('Alpine%64'); my $req = Ravada::Request->download( id_iso => $id_iso + ,id_vm => $id_vm ); wait_request(); is($req->error, ''); @@ -1838,7 +1839,7 @@ for my $vm_name (vm_names()) { diag("Skipping VM $vm_name in this system"); next; } - _download_alpine64() if !$<; + _download_alpine64($vm->id) if !$<; $TLS = 0; $TLS = 1 if check_libvirt_tls() && $vm_name eq 'KVM'; for my $base ( _create_base($vm) ) { From 0da57d5cdc85d5eafdfdbe46f68dc2999739f717 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 11 Jun 2026 14:45:49 +0200 Subject: [PATCH 137/141] wip(test): remove old instance on migrate --- t/nodes/10_basic.t | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index 50bb08997..1cc6f791c 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -2222,7 +2222,7 @@ sub test_migrate_standalone($node1, $node2) { wait_request(); test_volumes_exist($domain, $node1,1); - test_volumes_exist($domain, $node2,1); + test_volumes_exist($domain, $node2,0); test_remove_instances($domain, $node1, $node2); } From 91a7126bf79908eedcdbf493013634452301bfe0 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 15 Jun 2026 13:16:35 +0200 Subject: [PATCH 138/141] wip: remove old instance after migrate --- lib/Ravada/Domain.pm | 69 +++++++++++++++++++++++++++++++-------- lib/Ravada/Domain/KVM.pm | 3 -- lib/Ravada/Domain/Void.pm | 5 +-- lib/Ravada/Request.pm | 18 +++++++--- t/nodes/10_basic.t | 14 ++++++-- 5 files changed, 83 insertions(+), 26 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 3d179de34..643ee222b 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -210,6 +210,7 @@ after 'screenshot' => \&_post_screenshot; after '_select_domain_db' => \&_post_select_domain_db; around 'migrate' => \&_around_migrate; +around 'remove_instance' => \&_around_remove_instance; around 'get_info' => \&_around_get_info; around 'set_max_mem' => \&_around_set_max_mem; @@ -2818,7 +2819,17 @@ sub _remove_domain_cascade($self,$user, $cascade = 1) { $@ = ''; eval { $domain = $vm->search_domain($domain_name) } if $vm; warn $@ if $@; - $domain->_remove_instance($user, $cascade, $instance->{id}) if $domain; + if ($domain) { + $domain->_remove_instance($user, $cascade, $instance->{id}); + $domain->_remove_all_volumes(); + } else { + for my $vol ( @{$self->{_volumes}} ) { + next if $vol =~ /\.iso$/; + $vm->remove_file($vol) if $vm && $vm->vm + && $vm->is_alive && $vm->is_active + && $vm->file_exists($vol); + } + } } } @@ -5663,9 +5674,27 @@ sub _post_migrate($self, $node, $request = undef) { sub _around_migrate($orig, $self, $node, $request=undef) { return if $self->_vm->id == $node->id; + my $instance = $self->open($self->id); $self->_pre_migrate($node, $request); $self->$orig($node, $request); $self->_post_migrate($node, $request); + + if (!$self->is_base) { + my $user = Ravada::Utils::user_daemon(); + if (defined $request) { + $user = Ravada::Auth::SQL->search_by_id($request->args('uid')); + } + $instance->remove_instance($user, $node); + } +} + +sub _around_remove_instance($orig, $self, $user, $node=undef) { + die "Error: instance in same node.\n" + if defined $node && $self->_vm->id == $node->id; + + $self->$orig($user); + + $self->_remove_files_not_shared(); } sub _id_base_in_vm($self, $id_vm) { @@ -5807,6 +5836,7 @@ sub set_base_vm($self, %args) { my $instance = $vm->search_domain($self->name); $instance->_remove_instance($user,1) if $instance; $self->_remove_files_not_shared($vm,@nodes, $self->_vm); + $self->_remove_base_files_not_shared($vm,@nodes, $self->_vm); $self->_vm($node2); $self->_data('id_vm' => $node2->id); $vm->refresh_storage_pools(); @@ -5844,22 +5874,35 @@ sub _check_set_base_reqs($self) { } } -sub _remove_files_not_shared($self, $vm, @nodes){ +sub _remove_files_not_shared($self, $vm=$self->_vm, @nodes){ for my $file ($self->list_volumes,$self->list_files_base) { - next if $file =~ /\.iso$/; - next if !$self->_vm->file_exists($file); + $self->_do_remove_file_not_shared($file, $vm, @nodes); + } +} - my ($dir) = $file =~ m{(.*)/}; - my $shared=0; - for my $node(@nodes) { - next if $vm->id == $node->id; - $shared++ if $vm->shared_storage($node, $dir); - last if $shared; - } - next if $shared; - $vm->remove_file($file); +sub _remove_base_files_not_shared($self, $vm, @nodes){ + + for my $file ($self->list_files_base) { + $self->_do_remove_file_not_shared($file, $vm, @nodes); + } +} + + +sub _do_remove_file_not_shared($self, $file, $vm, @nodes) { + + return if $file =~ /\.iso$/; + return if !$self->_vm->file_exists($file); + + my ($dir) = $file =~ m{(.*)/}; + my $shared=0; + for my $node(@nodes) { + next if $vm->id == $node->id; + $shared++ if $vm->shared_storage($node, $dir); + last if $shared; } + next if $shared; + $vm->remove_file($file); } sub _check_all_base_parents_in_node($self, $vm) { diff --git a/lib/Ravada/Domain/KVM.pm b/lib/Ravada/Domain/KVM.pm index 3cbdd9017..78ee3681e 100644 --- a/lib/Ravada/Domain/KVM.pm +++ b/lib/Ravada/Domain/KVM.pm @@ -341,9 +341,6 @@ sub remove_instance($self, $user) { }; warn $@ if $@; confess $@ if $@ && $@ !~ /libvirt error code: 42/; - eval { $self->remove_disks() if $self->is_known }; - warn $@ if $@; - confess $@ if $@ && $@ !~ /libvirt error code: 42/; } sub _remove_file_image { diff --git a/lib/Ravada/Domain/Void.pm b/lib/Ravada/Domain/Void.pm index e1ed46747..53742eaf1 100644 --- a/lib/Ravada/Domain/Void.pm +++ b/lib/Ravada/Domain/Void.pm @@ -211,13 +211,11 @@ sub resume { } sub remove($self, $user) { - $self->remove_instance($user); + $self->_remove_all_volumes(); } sub remove_instance($self, $user) { - $self->remove_disks(); - my $config_file = $self->_config_file; if ($self->_vm->file_exists($config_file)) { my ($out, $err) = $self->_vm->run_command("/bin/rm",$config_file); @@ -226,7 +224,6 @@ sub remove_instance($self, $user) { if ($self->_vm->file_exists($config_file.".lock")) { $self->_vm->remove_file($config_file.".lock"); } - } sub can_hibernate { return 1; } diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index 6954875e9..2b08e23c4 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -947,8 +947,9 @@ sub _validate_remove_domain($self) { } } my $domain = Ravada::Front::Domain->open($id_domain); - $self->_chain_remove_bases_nodes($domain) - if $domain->is_base(); + if ( $domain->is_base() ) { + my $req_rm = $self->_chain_remove_bases_nodes($domain) + } } sub _validate_remove_base($self) { @@ -977,10 +978,10 @@ sub _chain_remove_bases_nodes($self, $domain) { return if keys %$bases_vm < 2; my %done; my $req_prev; - my $req = Ravada::Request->open($self->id); + my $req; for my $id_vm ($domain->_data('id_vm'),keys %$bases_vm ) { next if $done{$id_vm}++; - my $req = Ravada::Request->remove_base_vm( + $req = Ravada::Request->remove_base_vm( uid => $self->args('uid') ,id_domain => $domain->id ,id_vm => $id_vm @@ -990,6 +991,7 @@ sub _chain_remove_bases_nodes($self, $domain) { $self->after_request_ok($req->id); $req_prev = $req; } + return $req; } sub _validate_remove_hardware($self) { @@ -1050,6 +1052,14 @@ sub _validate_prepare_base($self) { if ($req_create) { $req_create->after_request($self->id); } + my $domain = Ravada::Front::Domain->open($self->args('id_domain')); + if ($domain->is_active) { + my $req_shutdown = Ravada::Request->shutdown_domain( + uid => $self->args('uid') + ,id_domain => $domain->id + ); + $self->after_request_ok($req_shutdown); + } } sub _validate_compact($self) { diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index 1cc6f791c..8758b5cf7 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -715,7 +715,9 @@ sub test_volatile_req($vm, $node) { ok(!$vm->file_exists($vol),$vol) or exit; ok(!$node->file_exists($vol),$vol." in ".$node->name) or exit; } + warn 1; _remove_domain($base); + warn 2; } sub _wait_machine_removed($clone) { @@ -1389,6 +1391,7 @@ sub test_fill_memory($vm, $node, $migrate, $start=0) { ); wait_request(); ok($base->_id_base_in_vm($node->id)); + is($base->list_instances(),2) or die; my $master_free_memory = $vm->free_memory; my $node_free_memory = $node->free_memory; @@ -1411,7 +1414,7 @@ sub test_fill_memory($vm, $node, $migrate, $start=0) { ,memory => int($memory) ,start => $start ); - wait_request(debug => 0, check_error => 0); + wait_request(debug => 1, check_error => 0); like($req->error, qr/^(No free memory|$)/); is($req->status,'done'); push @clones,($clone_name); @@ -1420,6 +1423,7 @@ sub test_fill_memory($vm, $node, $migrate, $start=0) { $created_in_node++ if $clone->_data('id_vm') == $node->id; _check_files_exist($clone); + Ravada::Request->migrate( uid => user_admin->id ,id_domain => $clone->id ,id_node => $node->id @@ -1443,6 +1447,8 @@ sub test_fill_memory($vm, $node, $migrate, $start=0) { my $node_name = $clone->_vm->name; $nodes{$node_name}++; + diag(Dumper[$clone_name, $clone->_vm->name,\%nodes]); + last if $migrate && exists $nodes{$vm->name} && $nodes{$vm->name} > 2; if (!$error && !$clone->is_active) { die $clone->_vm->name." ".$clone->name if !$error && !$clone->is_active; @@ -1571,7 +1577,7 @@ sub test_check_instances($vm, $node) { is($domain->_vm->id, $node->id); my @instances3 = $domain->list_instances(); - is(scalar(@instances3),2, "Expecting 2 instances of ".$domain->name); + is(scalar(@instances3),1, "Expecting 1 instances of ".$domain->name); $domain->remove(user_admin); } @@ -2214,6 +2220,8 @@ sub test_migrate_clone($node1, $node2) { sub test_migrate_standalone($node1, $node2) { my $domain = _req_create($node1, 0); # create and not start + + diag("migrate ".$domain->name." to ".$node2->name); my $req = Ravada::Request->migrate( uid => user_admin->id ,id_domain => $domain->id @@ -2382,6 +2390,8 @@ for my $vm_name (reverse vm_names() ) { start_node($node); + test_fill_memory($vm, $node, 0); # balance + test_base_only_in_node_add_hw($vm, $node); #start after create = 1 test_migrate_clone($node, $vm); From 7802bd00b8f07da62fc94b13f48e8f2109168cac Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 15 Jun 2026 16:51:58 +0200 Subject: [PATCH 139/141] wip(test): fixed storage list and retry if too big SQL --- lib/Ravada/Domain/Void.pm | 3 +++ lib/Ravada/Request.pm | 29 +++++++++++++++++++++++------ script/rvd_front | 4 +--- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/lib/Ravada/Domain/Void.pm b/lib/Ravada/Domain/Void.pm index 53742eaf1..04e4e72ed 100644 --- a/lib/Ravada/Domain/Void.pm +++ b/lib/Ravada/Domain/Void.pm @@ -1089,6 +1089,9 @@ sub _change_hardware_disk($self, $index, $data_new) { $new_file = $data_new->{file} if exists $data_new->{file}; $self->_change_disk_data($index, file => $new_file) if defined $new_file; + return if !keys %$data_new + || ( keys (%$data_new)==1 && exists $data_new->{file}); + return if !$file; my $data; if ($self->is_local) { diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index 2b08e23c4..31d1d8e0b 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -2459,19 +2459,36 @@ sub _data($self, $field, $value=undef) { my $prev_req_id = $self->{_data}->{$field}; $value = _push($prev_req_id, $value) if $prev_req_id; } - $self->{_data}->{$field} = $value; my $value0 = $value; - eval { - $value = encode_json($value) if ref($value); - }; - confess Dumper([$@,$value0]) if $@; my $sth = $$CONNECTOR->dbh->prepare( "UPDATE requests set $field=?" ." WHERE id=?" ); - $sth->execute($value, $self->id); + for ( 1 .. 2 ) { + + eval { + $value = encode_json($value) if ref($value); + }; + confess Dumper([$@,$field,$value0]) if $@; + + eval { + $sth->execute($value, $self->id); + }; + last if !$@; + if ($@ =~ /Data too long/) { + if (ref($value0)) { + if (ref($value0) eq 'ARRAY') { + $#$value0 = $#$value0 / 2; + $value = $value0; + } else { + die $@." ".Dumper([$field,$value0]); + } + } + } + } $sth->finish; + $self->{_data}->{$field} = $value0; return $self->{_data}->{$field}; } diff --git a/script/rvd_front b/script/rvd_front index 2b0a6d79a..8ec70ebef 100644 --- a/script/rvd_front +++ b/script/rvd_front @@ -3456,9 +3456,7 @@ get '/storage/list_unused_volumes' => sub($c) { ); $RAVADA->wait_request($req,180); my $output = []; - if ($req->output) { - $output = decode_json($req->output); - } + $output = $req->output if $req->output; return $c->render( json => $output ); }; From 0ee28cf7e68ca09e21b04274d52b3c82d614dabb Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Tue, 23 Jun 2026 10:58:09 +0200 Subject: [PATCH 140/141] wip: considering not to auto migrate on start --- lib/Ravada/Domain.pm | 10 +-- t/kvm/n10_nodes.t | 166 ++++++++++++++++++++++++------------------- t/lib/Test/Ravada.pm | 13 +++- 3 files changed, 103 insertions(+), 86 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 643ee222b..236cc2d07 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -612,13 +612,7 @@ sub _start_checks($self, @args) { $vm->connect; die "Error: node ".$vm->name." is not alive" if !$vm->is_alive; }; - if ($id_vm) { - $self->_set_vm($vm); - } else { - $self->_balance_vm($request, $enable_host_devices) - if !$is_volatile; - } - if ( !$is_volatile && !$self->_vm->is_local() ) { + if ( !$is_volatile ) { if (!base_in_vm($id_base, $self->_vm->id)) { my $args = { uid => Ravada::Utils::user_daemon->id @@ -632,8 +626,6 @@ sub _start_checks($self, @args) { ,'set_base_vm', encode_json($args)); } - $self->rsync(request => $request) - unless defined $id_prev && $self->_vm->id == $id_prev; } } $self->_check_free_vm_memory(); diff --git a/t/kvm/n10_nodes.t b/t/kvm/n10_nodes.t index 1baf8627b..ffc5e3788 100644 --- a/t/kvm/n10_nodes.t +++ b/t/kvm/n10_nodes.t @@ -226,6 +226,7 @@ sub test_domain_on_remote { wait_request(); is($req_create->error,''); my $domain = rvd_back->search_domain($name);; + is($domain->_vm->name, $node->name); my $req = Ravada::Request->remove_domain( uid => user_admin->id @@ -327,48 +328,43 @@ sub test_sync_base { ,user => user_admin ); - eval { $clone->migrate($node); }; - like($@, qr'.'); # base not in node - - eval { $base->migrate_base(user => user_admin, vm => $node); }; - is(''.$@,''); + Ravada::Request->migrate( + uid => user_admin->id + ,id_domain => $clone->id + ,id_node => $node->id + ); + wait_request(); is($base->base_in_vm($node->id),1,"Expecting domain ".$base->id ." base in node ".$node->id ) or return; + $clone = Ravada::Domain->open($clone->id); $clone->start(user_admin); $clone->shutdown_now(user_admin); - eval { $clone->migrate($node); }; - is(''.$@,''); is($clone->_vm->host, $node->host); $clone->shutdown_now(user_admin); - my $clone2 = $vm->search_domain($clone->name); - is($clone2->_vm->host, $vm->host); - - eval { $clone2->migrate($node); }; - is(''.$@,''); - - is($clone2->_data('id_vm'),$node->id); + is($clone->_data('id_vm'),$node->id); - my $clone3 = $node->search_domain($clone2->name); - ok($clone3,"[$vm_name] expecting ".$clone2->name." found in " + my $clone3 = $node->search_domain($clone->name); + ok($clone3,"[$vm_name] expecting ".$clone->name." found in " .$node->host) or exit; my $domains = rvd_front->list_domains(); - my ($clone_f) = grep { $_->{name} eq $clone2->name } @$domains; + my ($clone_f) = grep { $_->{name} eq $clone->name } @$domains; ok($clone_f); - is($clone_f->{id}, $clone2->id); - is($clone_f->{node}, $clone2->_vm->name); + is($clone_f->{id}, $clone->id); + is($clone_f->{node}, $clone->_vm->name); is($clone_f->{id_vm}, $node->id); - $clone->remove(user_admin); - $base->remove(user_admin); - + remove_domain_and_clones_req($base); } sub test_start_twice { + return; + + # TODO: to try this we need to manually create the clone in the other node my ($vm_name, $node) = @_; if ($vm_name ne 'KVM' && $vm_name ne 'Void') { @@ -425,6 +421,9 @@ sub test_start_twice { } sub test_already_started_twice($vm_name, $node) { + # this test makes no sense since we have only one instance for clones + return; + my ($base, $clone) = _create_clone($node); my $vm = rvd_back->search_vm($vm_name); @@ -468,6 +467,8 @@ sub test_already_started_twice($vm_name, $node) { } sub test_already_started_hibernated($vm_name, $node) { + # this test makes no sense since we have only one instance for clones + return; my ($base, $clone) = _create_clone($node); my $vm = rvd_back->search_vm($vm_name); @@ -579,6 +580,8 @@ sub _create_clone($node) { } sub test_rsync_newer { + #TODO + return; my ($vm_name, $node) = @_; if ($vm_name ne 'KVM') { @@ -778,8 +781,7 @@ sub test_clone_make_base { is($clone->base_in_vm($vm->id),1); is($clone->base_in_vm($node->id),0); - $clone->remove(user_admin); - $domain->remove(user_admin); + remove_domain_and_clones_req($domain); } sub _disable_storage_pools { @@ -821,6 +823,17 @@ sub test_bases_different_storage_pools { $domain->remove(user_admin); } +sub _clone($base) { + my $new_name = new_domain_name(); + my $req = Ravada::Request->clone( + uid => user_admin->id + ,id_domain => $base->id + ,name => $new_name + ); + wait_request(); + return rvd_back->search_domain($new_name); +} + sub test_clone_not_in_node { my ($vm_name, $node) = @_; @@ -828,9 +841,13 @@ sub test_clone_not_in_node { my $vm = rvd_back->search_vm($vm_name); - my $domain = create_domain($vm_name); + my $domain = create_domain($vm); + Ravada::Request->prepare_base( + uid => user_admin->id + ,id_domain => $domain->id + ); + wait_request(debug => 0); - $domain->prepare_base(user_admin); is($domain->base_in_vm($vm->id), 1); Ravada::Request->set_base_vm( uid => user_admin->id @@ -843,7 +860,7 @@ sub test_clone_not_in_node { my @clones; for ( 1 .. 10 ) { - my $clone1 = $domain->clone(name => new_domain_name, user => user_admin); + my $clone1 = _clone($domain); push @clones,($clone1); Ravada::Request->start_domain( uid => user_admin->id @@ -1086,6 +1103,7 @@ sub test_node_inactive($vm_name, $node) { or return; is($node2->{is_active},0,Dumper($node2)) or return; + sleep 1; start_node($node); for ( 1 .. 10 ) { @@ -1141,41 +1159,59 @@ sub test_sync_back($node) { $domain->remove(user_admin); } +sub _load_md5($clone, $node=$clone->_vm) { + my %md5; + for my $file ($clone->list_volumes) { + $md5{$file} = _md5($file, $node); + } + return \%md5; + +} sub test_migrate_back($node) { diag("Testing migrate back from remote non shared storage node"); my $vm = rvd_back->search_vm($node->type, 'localhost'); my $domain = create_domain($vm); - $domain->prepare_base(user_admin); - $domain->set_base_vm(vm => $node, user => user_admin); + Ravada::Request->set_base_vm( + uid => user_admin->id + ,id_domain => $domain->id + ,id_vm => $node->id + ); + wait_request(); is($domain->base_in_vm($vm->id),1); is($domain->base_in_vm($node->id),1); - my $clone = $domain->clone( name => new_domain_name(), user => user_admin ); - $clone->migrate($node); - eval { $clone->start(user_admin) }; - is(''.$@,'',"[".$node->type."] expecting no error starting ".$clone->name) or exit; + my $clone = _clone($domain); + Ravada::Request->migrate( + uid => user_admin->id + ,id_domain => $clone->id + ,id_node => $node->id + ,start => 1 + ); + $clone = Ravada::Domain->open($clone->id); is($clone->_vm->host, $node->host); _write_in_volumes($clone); shutdown_domain_internal($clone); + my $md5_remote = _load_md5($clone); - eval { $clone->migrate($vm) }; - is(''.$@, ''); + Ravada::Request->migrate( + uid => user_admin->id + ,id_domain => $clone->id + ,id_node => $vm->id + ,start => 0 + ); wait_request(debug => 0); + $clone = Ravada::Domain->open($clone->id); - wait_request(debug => 0); for my $file ($clone->list_volumes) { my $md5 = _md5($file, $vm); - my $md5_remote = _md5($file, $node); - is( $md5_remote, $md5, "[".$node->type."] ".$clone->name." $file" ) or exit; + is( $md5_remote->{$file}, $md5, "[".$node->type."] ".$clone->name." $file" ) or exit; } - - $clone->remove(user_admin); - $domain->remove(user_admin); + remove_domain_and_clones_req($domain); } sub test_shutdown($node) { @@ -1213,6 +1249,8 @@ sub test_shutdown($node) { is($req->status,'done'); like($req->error,qr{^($|checked \d+)}); + my $md5_remote = _load_md5($clone); + my $clone2 = Ravada::Domain->open($clone->id); #open will clean internal shutdown is($clone2->is_active,0) or exit; Ravada::Request->migrate( @@ -1224,8 +1262,7 @@ sub test_shutdown($node) { for my $file ($clone->list_volumes) { my $md5 = _md5($file, $vm); - my $md5_remote = _md5($file, $node); - is($md5_remote, $md5); + is($md5_remote->{$file}, $md5); } # this would eventualy be called on check_vms request my @line = search_iptable_remote( @@ -1271,49 +1308,35 @@ sub test_status($node) { $clone->migrate($node) if $node->id ne $clone->_vm->id; is($clone->_vm->host, $node->host); - - eval { $clone->start(user_admin) }; - is(''.$@,'',"[".$node->type."] expecting no error starting clone ".$clone->name) or exit; + my @req_args = (uid => user_admin->id, id_domain => $clone->id); + my $req = Ravada::Request->start_domain(@req_args); + wait_request(); + is($req->error,''); my $vm = rvd_back->search_vm($node->type); is($vm->is_local, 1); my $domain_local = $vm->search_domain($clone->name); - is($domain_local->is_active, 0 ); - is($domain_local->_data('id_vm'), $node->id) or exit; + ok(!$domain_local); my $domain_front = Ravada::Front::Domain->open($clone->id); is($domain_front->is_active, 1); - my $domain_local2 = Ravada::Domain->open( id => $clone->id, id_vm => $vm->id); - is($domain_local2->_vm->id, $vm->id) or exit; - is($domain_local2->is_local, 1 ); - is($domain_local2->is_active, 0 ); - $domain_front = Ravada::Front::Domain->open($clone->id); is($domain_front->is_active, 1,"[".$node->type."] expecing active in front"); - eval {$clone->shutdown_now(user_admin) }; - is(''.$@,'',"[".$node->type."] expecting no error on clone shutdown"); + Ravada::Request->shutdown_domain(@req_args); + wait_request(); - diag("test status of local domain"); my $clone2 = $vm->search_domain($clone->name); - $clone2->start(user_admin); - is($clone2->_vm->name, $vm->name); + ok(!$clone2); my $domain_remote = $node->search_domain($clone->name); is($domain_remote->is_active, 0 ); $domain_front = Ravada::Front::Domain->open($clone->id); - is($domain_front->is_active, 1); - - $domain_local2 = Ravada::Domain->open( id => $clone->id, id_vm => $node->id); - is($domain_local2->is_active, 0 ); + is($domain_front->is_active, 0); - $domain_front = Ravada::Front::Domain->open($clone->id); - is($domain_front->is_active, 1); - - $clone->remove(user_admin); - $base->remove(user_admin); + remove_domain_and_clones_req($base); } @@ -1350,15 +1373,10 @@ SKIP: { remove_node($node); next; }; - test_domain_already_started($vm_name, $node); - test_remove_base($node); - test_domain_on_remote($vm_name, $node); - my $domain22 = test_domain($vm_name, $node); - test_remove_domain_from_local($vm_name, $node, $domain22) if $domain22; is($node->is_local,0,"Expecting ".$node->name." ".$node->ip." is remote" ) or BAIL_OUT(); - test_remove_base_main($node); - test_already_started_hibernated($vm_name, $node); + + test_node_inactive($vm_name, $node); is($vm->is_active,1); is($vm->shared_storage($node,'/var/tmp/'),0) or exit; diff --git a/t/lib/Test/Ravada.pm b/t/lib/Test/Ravada.pm index 657a09e7a..6131409b2 100644 --- a/t/lib/Test/Ravada.pm +++ b/t/lib/Test/Ravada.pm @@ -2429,7 +2429,11 @@ sub start_node($node) { ok($domain->_vm->host eq 'localhost'); - $domain->start(user => user_admin, remote_ip => '127.0.0.1') if !$domain->is_active; + my $req_start = Ravada::Request->start_domain( + uid => user_admin->id + ,id_domain => $domain->id + ); + wait_request(); for ( 1 .. 60 ) { last if $node->ping(undef,0); # no cache @@ -2465,7 +2469,8 @@ sub start_node($node) { $domain->shutdown_now(user_admin); sleep 2; } - $domain->start(user => user_admin, remote_ip => '127.0.0.1'); + $req_start->status('requested'); + wait_request(); } is($node->_do_is_active,1,"Expecting active node ".$node->name) or exit; @@ -2609,8 +2614,10 @@ sub local_ips($vm) { return @ips; } -sub shutdown_domain_internal($domain, $nice=0) { +sub shutdown_domain_internal($domain0, $nice=0) { + my $domain = Ravada::Domain->open($domain0->id); if ($domain->type eq 'KVM') { + return if !$domain->domain->is_active; if ($nice) { $domain->domain->shutdown(); for ( 1 .. 60 ) { From d0561fab441e4f80e0875f993b2226f75a77e71c Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Fri, 26 Jun 2026 15:22:34 +0200 Subject: [PATCH 141/141] wip: improved queue commands and after request checks --- lib/Ravada.pm | 5 ---- lib/Ravada/Domain.pm | 6 ++-- lib/Ravada/Request.pm | 59 +++++++++----------------------------- lib/Ravada/VM.pm | 39 +++++++++++++++++++------ lib/Ravada/Volume/Class.pm | 17 +++++++++++ lib/Ravada/Volume/QCOW2.pm | 5 ++-- t/20_volumes.t | 36 ++++++++++++++++------- t/nodes/00_req.t | 46 +++++++++++++++++++++++++++-- 8 files changed, 136 insertions(+), 77 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index 2c4e33f55..77655920e 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -4574,11 +4574,6 @@ sub _execute { $request->start_time(time); $request->error(''); - if ( $request->error_check_request() ) { - $request->status('done'); - return; - } - if ($dont_fork || !$CAN_FORK) { $self->_do_execute_command($sub, $request); return; diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 236cc2d07..6cfcc91ed 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -998,7 +998,6 @@ sub prepare_base($self, @args) { my @jobs; for my $req ($self->list_requests) { if ($req->command eq 'wait_job') { - $req->after_request($id_req); $id_req = $req->id; push @jobs,($req->id); } @@ -1006,8 +1005,7 @@ sub prepare_base($self, @args) { my $req_post = Ravada::Request->post_prepare_base( uid => $user->id ,id_domain => $self->id - ,after_request => $id_req - ,check_requests => \@jobs + ,after_request_ok => \@jobs ); my $sth = $self->_dbh->prepare( "UPDATE requests set after_request=? " @@ -2071,7 +2069,7 @@ sub open($class, @args) { $domain = _search_domain_in_instances($id, $row->{name}, $force); } $domain->_insert_db_extra() if $domain && !$domain->is_known_extra(); - $domain->_data('id_vm' => $vm_changed->id) if $vm_changed; + $domain->_data('id_vm' => $vm_changed->id) if $domain && $vm_changed; return $domain; } diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index 31d1d8e0b..8b2817cad 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -601,7 +601,7 @@ sub _check_args { my $args = { @_ }; my $valid_args = $VALID_ARG{$sub}; - for (qw(at after_request after_request_ok retry _no_duplicate _force uid check_requests)) { + for (qw(at after_request after_request_ok retry _no_duplicate _force uid )) { $valid_args->{$_}=2 if !exists $valid_args->{$_}; } @@ -845,6 +845,9 @@ sub _new_request { } + for my $field (keys %args) { + $args{$field} = encode_json($args{$field}) if ref($args{$field}); + } $args{date_changed} = Ravada::Utils::date_now(); $args{error} = ''; my $sth = $$CONNECTOR->dbh->prepare( @@ -1058,7 +1061,7 @@ sub _validate_prepare_base($self) { uid => $self->args('uid') ,id_domain => $domain->id ); - $self->after_request_ok($req_shutdown); + $self->after_request_ok($req_shutdown->id); } } @@ -2437,7 +2440,12 @@ sub remove($status, %args) { sub _push($value, $id) { my $list = $value; $list = [$list] if !ref($list); - push @$list,($id); + + my @id = ($id); + if (ref($id)) { + @id = @$id; + } + push @$list,@id; return $list; } @@ -2460,32 +2468,20 @@ sub _data($self, $field, $value=undef) { $value = _push($prev_req_id, $value) if $prev_req_id; } my $value0 = $value; + $value0 = dclone($value) if ref($value); my $sth = $$CONNECTOR->dbh->prepare( "UPDATE requests set $field=?" ." WHERE id=?" ); - for ( 1 .. 2 ) { + { eval { $value = encode_json($value) if ref($value); }; confess Dumper([$@,$field,$value0]) if $@; - eval { - $sth->execute($value, $self->id); - }; - last if !$@; - if ($@ =~ /Data too long/) { - if (ref($value0)) { - if (ref($value0) eq 'ARRAY') { - $#$value0 = $#$value0 / 2; - $value = $value0; - } else { - die $@." ".Dumper([$field,$value0]); - } - } - } + $sth->execute($value, $self->id); } $sth->finish; $self->{_data}->{$field} = $value0; @@ -2544,33 +2540,6 @@ sub refresh($self) { delete $self->{_data}; } -=head2 error_check_request - -Returns errors from requested jobs created from this request - -=cut - -sub error_check_request($self) { - my $check_request = $self->defined_arg('check_requests'); - return if !$check_request; - - my $error; - for my $id_req ( @$check_request ) { - my $req = Ravada::Request->open($id_req); - next if !$req->error; - if ($error) { - chomp $error; - $error .= "\n"; - } - $error .= $req->error; - } - return 0 if !$error; - - $self->error($error); - return 1; - -} - sub AUTOLOAD { my $self = shift; diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index d58a66bd1..1382a7958 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -1968,14 +1968,24 @@ sub run_command($self, @command) { return ($out, $err); } -sub _dir_queue { - my $dir = "/run/user"; +sub mkdir($self, $dir) { + if ($self->is_local) { + make_path($dir) or die "$! on mkdir $dir" + if ! -e $dir; + } elsif (!$self->file_exists($dir)) { + my ($output, $error) = $self->run_command("mkdir","-p",$dir); + die $error if $error; + } +} + +sub _dir_queue($self) { + my $dir = "/var/lib/ravada/at"; if ($<) { - $dir .= "/".$<; + $dir = "/run/user/$<"; } $dir .="/rvd_queue"; - mkdir $dir or die "$! on mkdir $dir" - if ! -e $dir; + + $self->mkdir($dir); return $dir; } @@ -1988,9 +1998,9 @@ sub _shell_quote { } @_); } -sub _new_file_queue { +sub _new_file_queue($self) { - return _dir_queue."/".$$.".".time().".".int(rand(100)); + return $self->_dir_queue()."/".time().".$$.".int(rand(100)); } =head2 queue_command @@ -2014,10 +2024,21 @@ sub _new_file_queue { =cut sub queue_command($self, $command , $id_domain=undef, $id_req=undef ) { - my $file_queue = _new_file_queue(); + my $file_queue = $self->_new_file_queue(); + + my $cmd = ''; + if (ref($command->[0])) { + for my $line (@$command) { + $cmd .= "\n" if $cmd; + $cmd .= _shell_quote(@$line); + } + }else { + $cmd .= _shell_quote(@$command); + } $self->write_file("$file_queue.sh", "#!/bin/sh\n" - ._shell_quote(@$command)." > $file_queue.out 2> $file_queue.err" + .$cmd + ." > $file_queue.out 2> $file_queue.err" ); if ($self->is_local()) { chmod(oct(700),"$file_queue.sh") or die "$! chmod $file_queue"; diff --git a/lib/Ravada/Volume/Class.pm b/lib/Ravada/Volume/Class.pm index eff0767a1..1ed77e172 100644 --- a/lib/Ravada/Volume/Class.pm +++ b/lib/Ravada/Volume/Class.pm @@ -146,6 +146,23 @@ sub backup($self) { return $vol_backup; } +sub _move_batch($self, $dst, $req, $mode=undef) { + my $file = $self->file; + confess "Error: no Virtual Manager" if !$self->vm; + my @cmd = ( + ["mv",$file,$dst] + ); + push @cmd,["chmod",$mode,$dst] if $mode; + + my $id_domain; + $id_domain = $self->domain->id if $self->domain; + + my $id_req; + $id_req = $req->id if $req; + + $self->vm->queue_command(\@cmd, $id_domain, $id_req); +} + sub _copy_sys($self, $dst, $mode=undef) { my $file = $self->file; if ($self->vm) { diff --git a/lib/Ravada/Volume/QCOW2.pm b/lib/Ravada/Volume/QCOW2.pm index c60d146d7..37bef2b9a 100644 --- a/lib/Ravada/Volume/QCOW2.pm +++ b/lib/Ravada/Volume/QCOW2.pm @@ -36,8 +36,9 @@ sub prepare_base($self, $req=undef) { }; if ($format && $format eq 'qcow2') { - $self->_copy_sys($base_img, '0400'); - $self->_move_sys($base_img, '0400'); + #$self->_copy_sys($base_img, '0400'); + #$self->_move_sys($base_img, '0400'); + $self->_move_batch($base_img,$req, '0400'); } else { $self->_convert($base_img, $req); } diff --git a/t/20_volumes.t b/t/20_volumes.t index 720823635..2d9fb6708 100644 --- a/t/20_volumes.t +++ b/t/20_volumes.t @@ -78,7 +78,7 @@ sub _test_identical($vm, $file, $base) { } -sub _test_base_qcow2($volume, $base) { +sub _test_base_qcow2($volume, $base, $domain=undef) { my @cmd = ("/usr/bin/qemu-img","info",$base); my ($out, $err) = $volume->vm->run_command(@cmd); @@ -93,10 +93,12 @@ sub _test_base_qcow2($volume, $base) { like($out,qr/^image:.*\.ro$ext$/m); - @cmd = ("/usr/bin/qemu-img","info",$volume->file); - ($out, $err) = $volume->vm->run_command(@cmd); - is($err,''); - like($out,qr/backing file:.*\.ro$ext$/m); + if ($domain) { + @cmd = ("/usr/bin/qemu-img","info",$volume->file); + ($out, $err) = $volume->vm->run_command(@cmd); + is($err,'') or confess; + like($out,qr/backing file:.*\.ro$ext$/m); + } } sub _test_base_raw($volume, $base) { @@ -130,14 +132,18 @@ sub _test_clone_qcow2($vol_base, $clone) { } -sub test_base($volume) { +sub test_base($volume, $domain=undef) { my ($ext) = $volume->file =~ m{.*\.(.*)}; $ext = 'qcow2' if $ext =~ m{^(img|raw)}; my $test = $TEST_BASE{$ext} or confess "Error: no test for $ext"; $volume->vm->refresh_storage(); - my $base = $volume->prepare_base(); + + my $base = $volume->base_filename(); + if (!defined $domain) { + $base=$volume->prepare_base(); + } if ($ext ne 'iso') { if ( $volume->file =~ /\.SWAP\./) { @@ -148,9 +154,10 @@ sub test_base($volume) { like($base,qr{(vd.|\d+)\.ro\.$ext$}, $volume->file) or exit; } } + wait_request(debug => 0); $test->($volume, $base); - _check_volume_mode($volume->file); + _check_volume_mode($volume->file) if $domain; _check_base_volume_mode($base); return $base; @@ -162,7 +169,7 @@ sub _check_base_volume_mode($file) { ok($mode & S_IRUSR); # User can read - ok(!($mode & S_IWUSR)); # User can not write + ok(!($mode & S_IWUSR)) or die $file; # User can not write ok(!($mode & S_IRGRP)); # Group can not read ok(!($mode & S_IROTH)); # Others can not read ok(!($mode & S_IWGRP)); # Group can not write @@ -345,6 +352,7 @@ sub test_rebase($volume) { } sub _check_volume_mode($file) { + confess "Missing file $file" if !-e $file; my $mode = stat($file)->mode; if ($file =~ /\.iso$/) { ok($mode & S_IRUSR); # user can read @@ -374,8 +382,16 @@ sub test_defaults($vm, $volume_type=undef) { ok(-e $volume->file,$volume->file) or exit; _check_volume_mode($volume->file); + } + + Ravada::Request->prepare_base( + uid => user_admin->id + ,id_domain => $domain->id + ); + wait_request(); + for my $volume ( $domain->list_volumes_info ) { - my $file_base = test_base($volume); + my $file_base = test_base($volume, $domain); _check_base_volume_mode($file_base); _check_volume_mode($volume->file); diff --git a/t/nodes/00_req.t b/t/nodes/00_req.t index 21ea4d505..890b2bac0 100644 --- a/t/nodes/00_req.t +++ b/t/nodes/00_req.t @@ -94,7 +94,7 @@ sub test_req_migrate_active($vm, $node1, $node2) { } -sub test_req_prepare_base($vm, $node1, $node2) { +sub test_req_set_base_vm($vm, $node1, $node2) { my $domain = create_domain($vm); $domain->_data('id_vm' => $node1->id); my $req = Ravada::Request->set_base_vm( @@ -445,6 +445,46 @@ sub test_chain_remove_vm($vm, $node1, $node2) { remove_domain_db($base); } +sub _check_req_recurse($id_req) { + + like($id_req,qr/^[0-9]+$/); + my $req = Ravada::Request->open($id_req); + ok($req) or return; + for my $after ($req->after_request() , $req->after_request_ok ) { + next if !$after; + + my @id; + if (!ref($after)) { + unlike($after,qr/ARRAY/); + @id = ( $after ); + } else { + isa_ok($after,'ARRAY'); + @id = @$after; + } + for my $id (@id) { + _check_req_recurse($id); + } + } +} + +sub test_req_prepare_base($vm) { + + my $domain = create_domain($vm); + $domain->add_volume(swap => 1, size => 10 * 1024, format => 'qcow2'); + $domain->add_volume(size => 10 * 1024, format => 'qcow2'); + + my $req = Ravada::Request->prepare_base( + uid => user_admin->id + ,id_domain => $domain->id + ); + _check_req_recurse($req->id); + rvd_back->_cmd_prepare_base($req); + for my $req2 ($domain->list_requests) { + _check_req_recurse($req2->id); + } + remove_domain($domain); +} + ############################################################################### init(); @@ -466,6 +506,8 @@ for my $vm_name (vm_names() ) { my $node1 = _create_remote_node($vm_name); my $node2 = _create_remote_node($vm_name); + test_req_prepare_base($vm); + test_chain_remove_vm($vm, $node1, $node2); test_chain_children($vm, $node1, $node2); @@ -474,7 +516,7 @@ for my $vm_name (vm_names() ) { test_req_failed($vm); test_req_prepare_nested($vm, $node1); test_req_migrate_nested($vm, $node1); - test_req_prepare_base($vm, $node1, $node2); + test_req_set_base_vm($vm, $node1, $node2); test_req_migrate_active($vm, $node1, $node2); test_req_migrate($vm, $node1, $node2); $node1->remove();