From 5fe0f053d21a4c824cb160f542ad82dfcb837a53 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 20 Sep 2023 10:12:59 +0200 Subject: [PATCH 01/17] fix: properly check free storage size (#1980) fix: properly check free storage size --- lib/Ravada/Domain.pm | 13 ++++++------ t/lib/Test/Ravada.pm | 9 ++++++--- t/vm/20_base.t | 24 +++++++++++----------- t/vm/s30_storage.t | 48 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 21 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index f35d21220..75fd05df2 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -704,7 +704,9 @@ sub _around_add_volume { ($name) = $file =~ m{.*/(.*)} if !$name && $file; $name = $self->name if !$name; - $name .= "-".$args{target}."-".Ravada::Utils::random_name(4); + $name .= "-".$args{target}."-".Ravada::Utils::random_name(4) + if $name !~ /\.iso$/; + $args{name} = $name; } @@ -717,10 +719,12 @@ sub _around_add_volume { $args{allocation} = Ravada::Utils::size_to_number($args{allocation}) if exists $args{allocation} && defined $args{allocation}; - my $free = $self->_vm->free_disk(); + my $storage = $args{storage}; + + my $free = $self->_vm->free_disk($storage); my $free_out = int($free / 1024 / 1024 / 1024 ) * 1024 *1024 *1024; - confess "Error creating volume, out of space $size . Disk free: " + die "Error creating volume, out of space $size . Disk free: " .Ravada::Utils::number_to_size($free_out) ."\n" if exists $args{size} && $args{size} && $args{size} >= $free; @@ -1945,9 +1949,6 @@ sub display($self, $user) { my ($display_info) = grep { $_->{driver} !~ /-tls$/ } @display_info; - confess "Error: I can't find builtin display info for ".$self->name." ".ref($self)."\n".Dumper($display_info) - if !exists $display_info->{port}; - return '' if !$display_info->{driver} || !$display_info->{ip} || !$display_info->{port}; diff --git a/t/lib/Test/Ravada.pm b/t/lib/Test/Ravada.pm index b0be1e64d..d693d7232 100644 --- a/t/lib/Test/Ravada.pm +++ b/t/lib/Test/Ravada.pm @@ -86,6 +86,8 @@ create_domain remove_old_users remove_old_users_ldap + remove_qemu_pools + mangle_volume test_volume_contents test_volume_format @@ -273,6 +275,7 @@ sub create_domain_v2(%args) { my $user = (delete $args{user} or $USER_ADMIN); my $iso_name = delete $args{iso_name}; my $id_iso = delete $args{id_iso}; + my $disk = delete $args{disk}; croak "Error: supply either iso_name or id_iso ".Dumper(\%args) if $iso_name && $id_iso; @@ -298,13 +301,12 @@ sub create_domain_v2(%args) { if keys %args; my $iso; - my $disk; if ($vm->type eq 'KVM' && (!$iso_name || $iso_name !~ /Alpine/i)) { $iso = $vm->_search_iso($id_iso); - $disk = ($iso->{min_disk_size} or 2 ); + $disk = ($iso->{min_disk_size} or 2 ) if !$disk; diag("Creating [$id_iso] $iso->{name}"); } else { - $disk = 2; + $disk = 2 if !$disk; } if ($vm->type eq 'KVM' && !$options ) { @@ -1510,6 +1512,7 @@ sub remove_old_storage_pools_void() { } sub remove_old_storage_pools_kvm() { + remove_qemu_pools(); } sub remove_old_storage_pools() { diff --git a/t/vm/20_base.t b/t/vm/20_base.t index df26050d6..fe9d12073 100644 --- a/t/vm/20_base.t +++ b/t/vm/20_base.t @@ -300,17 +300,15 @@ sub test_displays_added_on_refresh($domain, $n_expected, $delete=1) { _wait_ip($domain); + my $domain_f0 = Ravada::Front::Domain->open($domain->id); + my $display0_raw = $domain_f0->info(user_admin)->{hardware}->{display}; + + my @display0 = grep { $_->{is_secondary} == 0 } @$display0_raw; + my $n_expected2 = scalar(@display0); + if ($delete) { my $sth = connector->dbh->prepare("DELETE FROM domain_displays WHERE id_domain=?"); $sth->execute($domain->id); - }else { - my $sth = connector->dbh->prepare("SELECT count(*) FROM domain_displays WHERE id_domain=?"); - $sth->execute($domain->id); - my ($n_exp2) = $sth->fetchrow; - if ($n_exp2 && $n_expected != $n_exp2) { - cluck "n_expected was $n_expected, but it should be $n_exp2"; - $n_expected = $n_exp2; - } } my $req; for ( 1 .. 3 ) { @@ -333,7 +331,7 @@ sub test_displays_added_on_refresh($domain, $n_expected, $delete=1) { my $count; for ( 1 .. 10 ) { my $sth_count = connector->dbh->prepare( - "SELECT count(*) FROM domain_displays WHERE id_domain=?"); + "SELECT count(*) FROM domain_displays WHERE id_domain=? AND is_secondary=0"); $sth_count->execute($domain->id); ($count) = $sth_count->fetchrow; last if $count; @@ -344,11 +342,13 @@ sub test_displays_added_on_refresh($domain, $n_expected, $delete=1) { ); wait_request(); } - ok($count>=$n_expected,"Got $count, expecting >$n_expected displays on table domain_displays for ".$domain->name) or confess; + ok($count>=$n_expected || $count >=$n_expected2,"Got $count, expecting >$n_expected or $n_expected2 displays on table domain_displays for ".$domain->name) or confess; my $domain_f = Ravada::Front::Domain->open($domain->id); - my $display = $domain_f->info(user_admin)->{hardware}->{display}; - is(scalar(@$display), $n_expected,"Expecting $n_expected displays on info->{hardware}->{display} in ".$domain->name) or confess Dumper($display); + my $display_raw = $domain_f->info(user_admin)->{hardware}->{display}; + my @display = grep { $_->{is_secondary} == 0 } @$display_raw; + ok(scalar(@display) == $n_expected || scalar(@display) == $n_expected2 + ,"Expecting $n_expected or $n_expected2 displays on info->{hardware}->{display} in ".$domain->name) or confess Dumper(@display); } diff --git a/t/vm/s30_storage.t b/t/vm/s30_storage.t index 2ea8e4b46..a13f2de23 100644 --- a/t/vm/s30_storage.t +++ b/t/vm/s30_storage.t @@ -85,6 +85,48 @@ sub _clean_local { my $file = "$dir/check_storage";# or die "$!"; unlink $file or die "$! $file" if -e $file; } + +sub test_storage_full($vm) { + + my $dir = "/run"; + $dir.="/user/".$< if $<; + my $storage_name = new_domain_name(); + + $dir.= "/".$storage_name; + + mkdir $dir or die "$! $dir" if ! -e $dir; + + if (! grep { $_ eq $storage_name} $vm->list_storage_pools) { + $vm->create_storage_pool($storage_name,$dir); + } + + my($out,$err) = $vm->run_command("df"); + + my ($available) = $out =~ m{(\d+)\s+\d+\% /run}ms; + + $available = $available*10; + + $vm->default_storage_pool_name($storage_name); + + my $name = new_domain_name; + my $req = Ravada::Request->create_domain( + vm => $vm->type + ,id_owner => user_admin->id + ,name => $name + ,disk => $available + ,storage => 'default' + ,id_iso => search_id_iso('Alpine%64') + ); + wait_request(debug => 0); + + my $domain = $vm->search_domain($name); + for my $vol ($domain->list_volumes) { + next if $vol =~ /iso$/; + unlike($vol,qr{^/run}); + } + +} + ########################################################### _clean_local(); @@ -101,6 +143,12 @@ for my $vm_name (vm_names() ) { } skip($msg,10) if !$vm; + + if ($vm->type eq 'KVM') { + remove_qemu_pools($vm); + } + test_storage_full($vm); + test_storage_pools($vm); test_storage_pools_fail($vm); } From 125b2dc4b94f194ecfef9fdc0bc417a4e0fa2b57 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 12 Apr 2023 13:28:46 +0200 Subject: [PATCH 02/17] fix: SSO user management --- lib/Ravada/Auth.pm | 1 + lib/Ravada/Auth/SSO.pm | 6 ++++++ templates/main/manage_user.html.ep | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/Ravada/Auth.pm b/lib/Ravada/Auth.pm index 10109c4f0..6fcb8a071 100644 --- a/lib/Ravada/Auth.pm +++ b/lib/Ravada/Auth.pm @@ -6,6 +6,7 @@ use strict; our $LDAP_OK; our $SSO_OK; +use Data::Dumper; use Ravada::Auth::SQL; =head1 NAME diff --git a/lib/Ravada/Auth/SSO.pm b/lib/Ravada/Auth/SSO.pm index 240cd1aa4..c01043ed6 100644 --- a/lib/Ravada/Auth/SSO.pm +++ b/lib/Ravada/Auth/SSO.pm @@ -72,6 +72,7 @@ sub _get_session_userid_by_ticket my ($cookie) = @_; my $result; die 'Can\'t read pubkey file (sso->cookie->pub_key value at ravada.conf file)' if (! -r $$CONFIG->{sso}->{cookie}->{pub_key}); + eval { $result = Authen::ModAuthPubTkt::pubtkt_verify(publickey => $$CONFIG->{sso}->{cookie}->{pub_key}, keytype => $$CONFIG->{sso}->{cookie}->{type}, ticket => $cookie); }; die $@ ? $@ : 'Cannot validate ticket' if ((! $result) || ($@)); my %data = Authen::ModAuthPubTkt::pubtkt_parse($cookie); @@ -123,6 +124,11 @@ sub init { return 0; } } + if (!$$CONFIG->{sso}->{cookie}->{type}) { + $ERR = "Error: missing sso / cookie / type in config file\n"; + warn $ERR unless $warn++; + return 0; + } for my $field (qw(priv_key pub_key)) { if ( !exists $$CONFIG->{sso}->{cookie}->{$field} || ! $$CONFIG->{sso}->{cookie}->{$field}) { diff --git a/templates/main/manage_user.html.ep b/templates/main/manage_user.html.ep index 8d784a992..18e49a446 100644 --- a/templates/main/manage_user.html.ep +++ b/templates/main/manage_user.html.ep @@ -13,7 +13,7 @@ (<%= ($user->external_auth or $origin) %>) % } -% if ($user->external_auth && $user->ldap_entry ) { +% if ($user->external_auth && $user->external_auth eq 'ldap' && $user->ldap_entry ) { <%= $user->ldap_entry->dn %> % } From 410a724466325449eadc8d018c4ee05ed46c6f27 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 2 Oct 2023 10:07:37 +0200 Subject: [PATCH 03/17] refactor: faster clone screenshot --- public/js/ravada.js | 4 ---- templates/main/list_bases_ng.html.ep | 7 +++++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/public/js/ravada.js b/public/js/ravada.js index b37a25c85..ced2a5f14 100644 --- a/public/js/ravada.js +++ b/public/js/ravada.js @@ -139,7 +139,6 @@ if (typeof(machine.clone) != 'undefined' && machine.clone) { machine.is_active = machine.clone.is_active; - machine.screenshot= machine.clone.screenshot; if (machine.clone.description && machine.clone.description.length) { machine.description2 = machine.clone.description; @@ -254,9 +253,6 @@ } else { machine.list_clones[i].is_active = data.list_clones[i].is_active; machine.list_clones[i].screenshot = data.list_clones[i].screenshot; - if (machine.clone.id == machine.list_clones[i].id) { - machine.screenshot = machine.list_clones[i].screenshot; - } } } diff --git a/templates/main/list_bases_ng.html.ep b/templates/main/list_bases_ng.html.ep index 18bc764bc..baf0adc5f 100644 --- a/templates/main/list_bases_ng.html.ep +++ b/templates/main/list_bases_ng.html.ep @@ -34,10 +34,13 @@
- {{machine.alias}} - {{machine.alias}} + {{machine.alias}} From 8b654ffa386c1fcd53da5d3d4ebf545f31a99a83 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 2 Oct 2023 12:40:53 +0200 Subject: [PATCH 04/17] refactor: fix wrong patch --- templates/main/list_bases_ng.html.ep | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/main/list_bases_ng.html.ep b/templates/main/list_bases_ng.html.ep index baf0adc5f..d40c6a49b 100644 --- a/templates/main/list_bases_ng.html.ep +++ b/templates/main/list_bases_ng.html.ep @@ -38,8 +38,8 @@ ng-src="data:image/png;base64,{{machine.screenshot}}" alt="{{machine.alias}}" class="img-thumbnail" width="260" > {{machine.alias}} + ng-src="data:image/png;base64,{{machine.clone.screenshot}}" alt="{{machine.alias}}" class="img-thumbnail" width="260" + > {{machine.alias}} Date: Mon, 2 Oct 2023 17:12:32 +0200 Subject: [PATCH 05/17] fix: Kali linux per year (#1985) * fix: Kali linux per year --- lib/Ravada.pm | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index af23b6d79..6c7305d6e 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -344,6 +344,8 @@ sub _update_isos { my $self = shift; my $table = 'iso_images'; my $field = 'name'; + my @now = localtime(time); + my $year = $now[5]+1900; my %data = ( androidx86 => { name => 'Android 8.1 x86' @@ -788,24 +790,24 @@ sub _update_isos { ,min_disk_size => '10' } ,kali_64 => { - name => 'Kali Linux 2022' - ,description => 'Kali Linux 2022 64 Bits' + name => "Kali Linux $year" + ,description => "Kali Linux $year 64 Bits" ,arch => 'x86_64' ,xml => 'jessie-amd64.xml' ,xml_volume => 'jessie-volume.xml' - ,url => 'https://cdimage.kali.org/kali-2022.\d+/' - ,file_re => 'kali-linux-202\d.\d+-installer-amd64.iso' + ,url => "https://cdimage.kali.org/kali-$year".'.\d+/' + ,file_re => "kali-linux-$year.".'\d+-installer-amd64.iso' ,sha256_url => '$url/SHA256SUMS' ,min_disk_size => '10' } ,kali_64_netinst => { - name => 'Kali Linux 2022 (NetInstaller)' - ,description => 'Kali Linux 2022 64 Bits (light NetInstall)' + name => "Kali Linux $year (NetInstaller)" + ,description => "Kali Linux $year 64 Bits (light NetInstall)" ,arch => 'x86_64' ,xml => 'jessie-amd64.xml' ,xml_volume => 'jessie-volume.xml' - ,url => 'https://cdimage.kali.org/kali-2022.\d+/' - ,file_re => 'kali-linux-202\d.\d+-installer-netinst-amd64.iso' + ,url => "https://cdimage.kali.org/kali-$year".'.\d+/' + ,file_re => "kali-linux-$year.".'\d+-installer-netinst-amd64.iso' ,sha256_url => '$url/SHA256SUMS' ,min_disk_size => '10' } From 39fbdad4dab132a0e73295a11f9ead2ae10f7321 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 4 Oct 2023 13:34:16 +0200 Subject: [PATCH 06/17] refactor: empty DE strings (#1990) --- lib/Ravada/I18N/de.po | 1057 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1057 insertions(+) diff --git a/lib/Ravada/I18N/de.po b/lib/Ravada/I18N/de.po index ab7efaae2..fcc7319c2 100644 --- a/lib/Ravada/I18N/de.po +++ b/lib/Ravada/I18N/de.po @@ -912,3 +912,1060 @@ msgstr "Denken Sie dran, dass Nutzer den Klone verwenden könnten" msgid "Yes, shutwdown all the clones" msgstr "Ja, alle Klone herunterfahren" + +msgid "-- choose hardware --" +msgstr "" + +msgid "A node with that address already exists." +msgstr "" + +msgid "Accept" +msgstr "" + +msgid "Access" +msgstr "" + +msgid "Action" +msgstr "" + +msgid "Active" +msgstr "" + +msgid "Add another user" +msgstr "" + +msgid "Add groups" +msgstr "" + +msgid "Add new Display" +msgstr "" + +msgid "Add new disk" +msgstr "" + +msgid "Add to group" +msgstr "" + +msgid "Admin users can still log in from" +msgstr "" + +msgid "Advanced options" +msgstr "" + +msgid "All events" +msgstr "" + +msgid "All machines" +msgstr "" + +msgid "Anonymous" +msgstr "" + +msgid "Any remote client can access this port" +msgstr "" + +msgid "Apply" +msgstr "" + +msgid "Are you sure you want to migrate" +msgstr "" + +msgid "Are you sure you want to prepare the base of" +msgstr "" + +msgid "Are you sure you want to remove the" +msgstr "" + +msgid "Are you sure you want to remove the base of" +msgstr "" + +msgid "Are you sure you want to remove the node" +msgstr "" + +msgid "Are you sure you want to remove this group ?" +msgstr "" + +msgid "Are you sure you want to spinoff from the base?" +msgstr "" + +msgid "Assign this virtual machine to the pre-started pool" +msgstr "" + +msgid "Attachment" +msgstr "" + +msgid "Autostart" +msgstr "" + +msgid "BIOS" +msgstr "" + +msgid "Balance" +msgstr "" + +msgid "Base" +msgstr "" + +msgid "Bases" +msgstr "" + +msgid "Bookings" +msgstr "" + +msgid "Bookings require LDAP authentication." +msgstr "" + +msgid "Bridge" +msgstr "" + +msgid "CPU Features" +msgstr "" + +msgid "CPUs" +msgstr "" + +msgid "Can expose virtual machine ports." +msgstr "" + +msgid "Can get a screenshot of own virtual machines." +msgstr "" + +msgid "Can have an unlimited amount of machines started." +msgstr "" + +msgid "Can manage groups." +msgstr "" + +msgid "Can reboot all virtual machines." +msgstr "" + +msgid "Can reboot clones own virtual machines." +msgstr "" + +msgid "Can reboot own virtual machines." +msgstr "" + +msgid "Can rename any virtual machine owned by the user." +msgstr "" + +msgid "Can rename any virtual machine." +msgstr "" + +msgid "Can rename clones from virtual machines owned by the user." +msgstr "" + +msgid "Can shutdown own virtual machines." +msgstr "" + +msgid "Can view groups." +msgstr "" + +msgid "Change" +msgstr "" + +msgid "Changing the base of a virtual machine is potentially dangerous and can't be undone." +msgstr "" + +msgid "Check" +msgstr "" + +msgid "Check connection to" +msgstr "" + +msgid "Choose the virtualization type of the Node." +msgstr "" + +msgid "Choose the virtualization type of the Virtual Machine." +msgstr "" + +msgid "Client" +msgstr "" + +msgid "Client headers" +msgstr "" + +msgid "Clones are volatile, so all the pool will be started." +msgstr "" + +msgid "Clones created from this machine will be removed on shutdown." +msgstr "" + +msgid "Close" +msgstr "" + +msgid "Color" +msgstr "" + +msgid "Compact" +msgstr "" + +msgid "Compact disk volumes" +msgstr "" + +msgid "Configure LDAP Authentication" +msgstr "" + +msgid "Confirm" +msgstr "" + +msgid "Confirm disable node" +msgstr "" + +msgid "Confirm password can not exceed 20 characters" +msgstr "" + +msgid "Confirm password can only contain words and numbers" +msgstr "" + +msgid "Confirm password is required" +msgstr "" + +msgid "Confirm password must be at least 5 characters" +msgstr "" + +msgid "Congrats" +msgstr "" + +msgid "Contact Method" +msgstr "" + +msgid "Content will be cleaned on restore" +msgstr "" + +msgid "Content will be cleaned on restore and shutdown" +msgstr "" + +msgid "Content will be kept on restore" +msgstr "" + +msgid "Create a new group" +msgstr "" + +msgid "Current memory (MB)" +msgstr "" + +msgid "Danger: This will destroy all the disk data permantently." +msgstr "" + +msgid "Data" +msgstr "" + +msgid "Debug" +msgstr "" + +msgid "Debug Ports" +msgstr "" + +msgid "Debug Ports Exposed" +msgstr "" + +msgid "Delete" +msgstr "" + +msgid "Delete the event" +msgstr "" + +msgid "Disable" +msgstr "" + +msgid "Disabled" +msgstr "" + +msgid "Disabling this node will shut all the" +msgstr "" + +msgid "Display" +msgstr "" + +msgid "Display IP :" +msgstr "" + +msgid "Display Password" +msgstr "" + +msgid "Display Port :" +msgstr "" + +msgid "Display Port secure :" +msgstr "" + +msgid "Display URL :" +msgstr "" + +msgid "Edit the event" +msgstr "" + +msgid "Email" +msgstr "" + +msgid "Enable" +msgstr "" + +msgid "Enables display password when available" +msgstr "" + +msgid "End" +msgstr "" + +msgid "Enter New Password" +msgstr "" + +msgid "Enter Old Password" +msgstr "" + +msgid "Enter group name" +msgstr "" + +msgid "Error:" +msgstr "" + +msgid "Error: the LDAP entry for this user has been removed." +msgstr "" + +msgid "Error: you want to have" +msgstr "" + +msgid "Event properties" +msgstr "" + +msgid "Event saved successfully" +msgstr "" + +msgid "Exec clones sequentially" +msgstr "" + +msgid "Exec. time" +msgstr "" + +msgid "Fail" +msgstr "" + +msgid "Fallback" +msgstr "" + +msgid "Fill required camps with valid data" +msgstr "" + +msgid "For Spice client setup follow this " +msgstr "" + +msgid "Force Reboot" +msgstr "" + +msgid "Force ShutDown" +msgstr "" + +msgid "Force change password on first access" +msgstr "" + +msgid "From" +msgstr "" + +msgid "Frontend" +msgstr "" + +msgid "Global Settings" +msgstr "" + +msgid "Group" +msgstr "" + +msgid "Group name" +msgstr "" + +msgid "Group name can not exceed 80 characters" +msgstr "" + +msgid "Group name is required" +msgstr "" + +msgid "Groups" +msgstr "" + +msgid "Groups require a LDAP server configured." +msgstr "" + +msgid "Groups with permissions" +msgstr "" + +msgid "Hardware" +msgstr "" + +msgid "Hardware address" +msgstr "" + +msgid "Hibernated" +msgstr "" + +msgid "Hide active" +msgstr "" + +msgid "Hide clones" +msgstr "" + +msgid "I can't find" +msgstr "" + +msgid "I don't know settings tabs for:" +msgstr "" + +msgid "ISO file" +msgstr "" + +msgid "If you have any issue, you can contact the Suport crew and we will try to help you. Please, only contact Support Center if it is strictly necessary." +msgstr "" + +msgid "Import" +msgstr "" + +msgid "Internal IP" +msgstr "" + +msgid "Invalid IP network address. Expecting a.b.c.d/e" +msgstr "" + +msgid "Invalid Template" +msgstr "" + +msgid "Keep the CD for the clones" +msgstr "" + +msgid "LDAP" +msgstr "" + +msgid "LDAP groups are required to set up bookings. Some groups where found but no members belong to them. Add new entries here." +msgstr "" + +msgid "Legacy" +msgstr "" + +msgid "Loading ..." +msgstr "" + +msgid "Loading machine" +msgstr "" + +msgid "Loading nodes" +msgstr "" + +msgid "Machine" +msgstr "" + +msgid "Machine Information" +msgstr "" + +msgid "Machines Notifications" +msgstr "" + +msgid "Maintenance" +msgstr "" + +msgid "Maintenance End" +msgstr "" + +msgid "Maintenance Start" +msgstr "" + +msgid "Manage machine" +msgstr "" + +msgid "Max Memory" +msgstr "" + +msgid "Max memory (MB)" +msgstr "" + +msgid "Minimum port number to expose virtual machine services." +msgstr "" + +msgid "Monitoring" +msgstr "" + +msgid "NAT" +msgstr "" + +msgid "Network" +msgstr "" + +msgid "Network address is required." +msgstr "" + +msgid "Network name is required" +msgstr "" + +msgid "Networks" +msgstr "" + +msgid "New Booking" +msgstr "" + +msgid "New Group" +msgstr "" + +msgid "New Network" +msgstr "" + +msgid "New Password can only contain words and numbers" +msgstr "" + +msgid "New Password is required" +msgstr "" + +msgid "New Password must be at least 6 characters" +msgstr "" + +msgid "New feature" +msgstr "" + +msgid "New group member" +msgstr "" + +msgid "New name" +msgstr "" + +msgid "New network" +msgstr "" + +msgid "New password" +msgstr "" + +msgid "New user" +msgstr "" + +msgid "No LDAP groups created." +msgstr "" + +msgid "No Results Found" +msgstr "" + +msgid "No bases found" +msgstr "" + +msgid "No bridges found" +msgstr "" + +msgid "No groups found" +msgstr "" + +msgid "No machines" +msgstr "" + +msgid "No machines found" +msgstr "" + +msgid "No members found" +msgstr "" + +msgid "No message to show!" +msgstr "" + +msgid "No, cancel" +msgstr "" + +msgid "Node" +msgstr "" + +msgid "Node disabled" +msgstr "" + +msgid "Node down" +msgstr "" + +msgid "Node with machines from the same user" +msgstr "" + +msgid "Node with more free memory" +msgstr "" + +msgid "Nodes" +msgstr "" + +msgid "Nothing to monitoring" +msgstr "" + +msgid "Number of virtual machines that normal users can have running at the same time" +msgstr "" + +msgid "Old Password can only contain words and numbers" +msgstr "" + +msgid "Old Password is required" +msgstr "" + +msgid "Old and New Passwords match!" +msgstr "" + +msgid "Only remote client can access this port" +msgstr "" + +msgid "Only remote client can access this port if restricted" +msgstr "" + +msgid "Only users from these groups will be allowed to execute this machine" +msgstr "" + +msgid "Oops!" +msgstr "" + +msgid "Open ports" +msgstr "" + +msgid "Password and their confirmation do not match!" +msgstr "" + +msgid "Passwords do not match!" +msgstr "" + +msgid "Permission granted to user" +msgstr "" + +msgid "Permission revoked from user" +msgstr "" + +msgid "Phone" +msgstr "" + +msgid "Phone Number" +msgstr "" + +msgid "Please insert" +msgstr "" + +msgid "Please select an ISO file" +msgstr "" + +msgid "Please, make sure you have the right path and release, according to your PC configuration." +msgstr "" + +msgid "Pool" +msgstr "" + +msgid "Port" +msgstr "" + +msgid "Port expose" +msgstr "" + +msgid "Ports" +msgstr "" + +msgid "Poweroff" +msgstr "" + +msgid "Prepare Base" +msgstr "" + +msgid "Prepare this machine as a base to create clones from it." +msgstr "" + +msgid "PrepareBase" +msgstr "" + +msgid "Press SHIFT + F12 to exit the virtual machine" +msgstr "" + +msgid "Primary can not be unset, enable it in another video device" +msgstr "" + +msgid "Primary video devices will move to first" +msgstr "" + +msgid "Public" +msgstr "" + +msgid "Public Port" +msgstr "" + +msgid "Purge" +msgstr "" + +msgid "Purge disk volumes" +msgstr "" + +msgid "RAM (Gb)" +msgstr "" + +msgid "Ravada Apache documentation" +msgstr "" + +msgid "Re-Enter New Password" +msgstr "" + +msgid "Rebase" +msgstr "" + +msgid "Rebase all the clones to a new base" +msgstr "" + +msgid "Rebase this machine to another virtual machine" +msgstr "" + +msgid "Reboot" +msgstr "" + +msgid "Recent requests" +msgstr "" + +msgid "Refresh" +msgstr "" + +msgid "Refresh ports" +msgstr "" + +msgid "Reload" +msgstr "" + +msgid "Remove Base" +msgstr "" + +msgid "Remove Clones" +msgstr "" + +msgid "Remove bases in node" +msgstr "" + +msgid "Remove group" +msgstr "" + +msgid "Remove the base prepared from this machine" +msgstr "" + +msgid "Removing Virtual Machine" +msgstr "" + +msgid "Repeat" +msgstr "" + +msgid "Repeated New Password is required" +msgstr "" + +msgid "Request" +msgstr "" + +msgid "Requests" +msgstr "" + +msgid "Requires viewer password when implemented" +msgstr "" + +msgid "Restricted" +msgstr "" + +msgid "Run Timeout" +msgstr "" + +msgid "SSO/CAS Login" +msgstr "" + +msgid "Save" +msgstr "" + +msgid "Save changes" +msgstr "" + +msgid "Schedule" +msgstr "" + +msgid "Search group" +msgstr "" + +msgid "See documentation about:" +msgstr "" + +msgid "Select the .iso file the machine will utilize when installing the OS." +msgstr "" + +msgid "Server in maintenance until" +msgstr "" + +msgid "Set New Password" +msgstr "" + +msgid "Set defaults" +msgstr "" + +msgid "Set either" +msgstr "" + +msgid "Setup a LDAP Server" +msgstr "" + +msgid "Show active" +msgstr "" + +msgid "Show clones" +msgstr "" + +msgid "Show/Hide clones" +msgstr "" + +msgid "ShutDown" +msgstr "" + +msgid "Shutdown Timeout" +msgstr "" + +msgid "Shutdown disconnected" +msgstr "" + +msgid "Sorry" +msgstr "" + +msgid "Source Machine" +msgstr "" + +msgid "Spinoff clone" +msgstr "" + +msgid "Spinoff this clone from its base." +msgstr "" + +msgid "Start Limit" +msgstr "" + +msgid "Start after create the virtual machine" +msgstr "" + +msgid "Start after migration" +msgstr "" + +msgid "Support Contact" +msgstr "" + +msgid "Swap" +msgstr "" + +msgid "System Disk: (GB)" +msgstr "" + +msgid "Template selection is required" +msgstr "" + +msgid "Testing connection to" +msgstr "" + +msgid "The Minimum Disk Size needed for this ISO is" +msgstr "" + +msgid "The Minimum Swap Disk Size needed for this ISO is" +msgstr "" + +msgid "The VM is" +msgstr "" + +msgid "The machine will power off after this minutes after shutdown." +msgstr "" + +msgid "The machine will turn off after this time" +msgstr "" + +msgid "The new user" +msgstr "" + +msgid "The source machine is not a base. It must be prepared before it can be copied. This process may take some minutes." +msgstr "" + +msgid "There are no LDAP groups defined." +msgstr "" + +msgid "There are no active virtual machines" +msgstr "" + +msgid "This ISO is being downloaded. The virtual machine will be created after." +msgstr "" + +msgid "This Virtual Machine has no display hardware attached" +msgstr "" + +msgid "This address is duplicated" +msgstr "" + +msgid "This base can't be removed because the domain has clones." +msgstr "" + +msgid "This base has" +msgstr "" + +msgid "This base has clones" +msgstr "" + +msgid "This booking overlaps already scheduled reservations" +msgstr "" + +msgid "This can't be undone" +msgstr "" + +msgid "This event" +msgstr "" + +msgid "This event and the following" +msgstr "" + +msgid "This event and the following with same day of week" +msgstr "" + +msgid "This feature requires a LDAP server configured." +msgstr "" + +msgid "This field is required" +msgstr "" + +msgid "This group has been removed." +msgstr "" + +msgid "This is a main node and can't be removed." +msgstr "" + +msgid "This machine has a CD-ROM" +msgstr "" + +msgid "This machine is base and can't be modified." +msgstr "" + +msgid "This machine is base and it can't be modified." +msgstr "" + +msgid "This machine is hibernated and can't be renamed." +msgstr "" + +msgid "This machine is locked by process" +msgstr "" + +msgid "This machine is running and can't be modified." +msgstr "" + +msgid "This machine is running and can't be renamed." +msgstr "" + +msgid "This machine will shut down" +msgstr "" + +msgid "This name is duplicated" +msgstr "" + +msgid "This name is invalid. It can only contain alphabetic, numbers, undercores and dashes and must start by a letter." +msgstr "" + +msgid "This node has" +msgstr "" + +msgid "This node is local" +msgstr "" + +msgid "This server has reservations for today. Machines from users out of\n the booking list will be shutdown." +msgstr "" + +msgid "This user has never logged in the Ravada server." +msgstr "" + +msgid "This virtual machine has already been compacted" +msgstr "" + +msgid "This virtual machine has clones that are bases and won't be removed" +msgstr "" + +msgid "This virtual machine has no backups to purge" +msgstr "" + +msgid "This virtual machine has no group restrictions." +msgstr "" + +msgid "This virtual machine is running. It must be shut down before migrate." +msgstr "" + +msgid "Time" +msgstr "" + +msgid "Title" +msgstr "" + +msgid "To" +msgstr "" + +msgid "To make this possible, copy the content of" +msgstr "" + +msgid "Today Schedule" +msgstr "" + +msgid "Type" +msgstr "" + +msgid "Type the ISO pathname" +msgstr "" + +msgid "Type the name of the volume disk to confirm:" +msgstr "" + +msgid "Type the template name" +msgstr "" + +msgid "UEFI" +msgstr "" + +msgid "Until" +msgstr "" + +msgid "User is not member of any group." +msgstr "" + +msgid "User not found" +msgstr "" + +msgid "Users from this network can run all virtual machines" +msgstr "" + +msgid "Users from this network can run no virtual machines" +msgstr "" + +msgid "Virtual Machine will be shutdown when user disconnects." +msgstr "" + +msgid "Virtual Machine will start on host start." +msgstr "" + +msgid "Virtual machines in the pool." +msgstr "" + +msgid "Virtual machines pre-started" +msgstr "" + +msgid "Volatile" +msgstr "" + +msgid "Waiting for machine to start" +msgstr "" + +msgid "Waiting for network to come up" +msgstr "" + +msgid "Waiting for requests to complete" +msgstr "" + +msgid "Warning" +msgstr "" + +msgid "Warning: anonymous machines won't show up unless you enable allowed." +msgstr "" + +msgid "Warning: anonymous machines won't show up unless you set them public." +msgstr "" + +msgid "Warning: this virtual machine will be prepared as a base. This may take long." +msgstr "" + +msgid "Web Service connection failed." +msgstr "" + +msgid "Weekly" +msgstr "" + +msgid "What do you want to do now?" +msgstr "" + +msgid "Will be destroyed on shutdown" +msgstr "" + +msgid "Yes, rebase" +msgstr "" + From 8a753016b650dab6d858635f4c884955121e69bc Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 4 Oct 2023 17:34:56 +0200 Subject: [PATCH 07/17] test: checks for missing translated strings --- etc/missing_strings.pl | 76 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100755 etc/missing_strings.pl diff --git a/etc/missing_strings.pl b/etc/missing_strings.pl new file mode 100755 index 000000000..aef0b5556 --- /dev/null +++ b/etc/missing_strings.pl @@ -0,0 +1,76 @@ +#!/usr/bin/perl + +use warnings; +use strict; +use Data::Dumper; + +my $DIR = "lib/Ravada/I18N"; + +my %LIST = map { $_ => 1 } @ARGV; + +sub selected { + my $file = shift; + my ($name) = $file =~ m{(.*)\.\w+}; + + return 0 if !exists $LIST{$name}; + return $LIST{$name}; +} + +sub load_strings { + my $file = shift; + + if ($file !~ m{/}) { + $file = "$DIR/$file"; + } + open my $in,"<",$file or die "$! $file"; + + my $msgid; + my %found; + my $string; + while (my $line = <$in>) { + my ($msgstr) = $line =~ /^msgstr/; + if ($msgstr && $string) { + $found{$string}++; + $string = undef; + next; + } + my ($string1) = $line =~ /^msgid "(.*)"/; + if (defined $string1) { + $string = $string1; + next; + } + if (!defined $string1 && defined $string) { + my ($string2) = $line =~ /^"(.*)"/; + if (defined $string2) { + $msgid=0; + $string = "$string$string2"; + } + } + next if !$string; + } + close $in; + return \%found; +} + +my $english = load_strings('en.po'); +my $found=0; + + +opendir my $in,$DIR or die "$! $DIR"; +while (my $file = readdir $in) { + next if $file !~ /\.po$/; + next if keys %LIST && !selected($file); + my $path = "$DIR/$file"; + next if !-f $path; + print "$path\n"; + + my $string = load_strings($file); + for my $key (sort keys %$english) { + next if $string->{$key}; + print "msgid \"$key\"\n" + ."msgstr \"\"\n\n"; + $found++; + } + last if $found; +} +closedir $in; From 427eb22d12e70361830d45db866a77a99b08b6e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20K=C3=B6hler?= Date: Fri, 6 Oct 2023 14:59:14 +0200 Subject: [PATCH 08/17] Update de.po (#1991) Added more german localizations --- lib/Ravada/I18N/de.po | 176 +++++++++++++++++++++--------------------- 1 file changed, 88 insertions(+), 88 deletions(-) diff --git a/lib/Ravada/I18N/de.po b/lib/Ravada/I18N/de.po index fcc7319c2..02ba13f6e 100644 --- a/lib/Ravada/I18N/de.po +++ b/lib/Ravada/I18N/de.po @@ -914,61 +914,61 @@ msgid "Yes, shutwdown all the clones" msgstr "Ja, alle Klone herunterfahren" msgid "-- choose hardware --" -msgstr "" +msgstr "-- Hardware auswählen --" msgid "A node with that address already exists." -msgstr "" +msgstr "Es existiert bereits ein Knoten mit dieser Adresse." msgid "Accept" -msgstr "" +msgstr "Akzeptieren" msgid "Access" -msgstr "" +msgstr "Zugreifen" msgid "Action" -msgstr "" +msgstr "Aktion" msgid "Active" -msgstr "" +msgstr "Aktiv" msgid "Add another user" -msgstr "" +msgstr "Weiteren Nutzer anlegen" msgid "Add groups" -msgstr "" +msgstr "Gruppe hinzufügen" msgid "Add new Display" -msgstr "" +msgstr "Neuen Bildschirm hinzufügen" msgid "Add new disk" -msgstr "" +msgstr "Neue Festplatte hinzufügen" msgid "Add to group" -msgstr "" +msgstr "Zur Gruppe hinzufügen" msgid "Admin users can still log in from" -msgstr "" +msgstr "Administratoren können sich weiterhin einloggen" msgid "Advanced options" -msgstr "" +msgstr "Erweiterte Optionen" msgid "All events" -msgstr "" +msgstr "Alle Ereignisse" msgid "All machines" -msgstr "" +msgstr "Alle Maschinen" msgid "Anonymous" -msgstr "" +msgstr "Anonym" msgid "Any remote client can access this port" -msgstr "" +msgstr "Jeder verbundene Client kann auf diesen Port zugreifen" msgid "Apply" -msgstr "" +msgstr "Anwenden" msgid "Are you sure you want to migrate" -msgstr "" +msgstr "Möchten Sie wirklich migrieren" msgid "Are you sure you want to prepare the base of" msgstr "" @@ -980,103 +980,103 @@ msgid "Are you sure you want to remove the base of" msgstr "" msgid "Are you sure you want to remove the node" -msgstr "" +msgstr "Möchten Sie diesen Knoten wirlich entfernen" msgid "Are you sure you want to remove this group ?" -msgstr "" +msgstr "Möchten Sie diese Gruppe wirklich entfernen?" msgid "Are you sure you want to spinoff from the base?" msgstr "" msgid "Assign this virtual machine to the pre-started pool" -msgstr "" +msgstr "Diese virtuelle Maschine einem bereits gestartetem pool zuweisen" msgid "Attachment" -msgstr "" +msgstr "Anhang" msgid "Autostart" -msgstr "" +msgstr "Autostart" msgid "BIOS" -msgstr "" +msgstr "BIOS" msgid "Balance" -msgstr "" +msgstr "Balance" msgid "Base" -msgstr "" +msgstr "Basis" msgid "Bases" msgstr "" msgid "Bookings" -msgstr "" +msgstr "Buchungen" msgid "Bookings require LDAP authentication." -msgstr "" +msgstr "Buchungen benötigen LDAP authentifizierung" msgid "Bridge" -msgstr "" +msgstr "Brücke" msgid "CPU Features" -msgstr "" +msgstr "CPU Features" msgid "CPUs" -msgstr "" +msgstr "CPUs" msgid "Can expose virtual machine ports." -msgstr "" +msgstr "Kann Ports der virtuellen Maschine freigeben" msgid "Can get a screenshot of own virtual machines." -msgstr "" +msgstr "Kann Bildschirmaufnahmen von eigenen virtuellen Maschinen erstellen" msgid "Can have an unlimited amount of machines started." -msgstr "" +msgstr "Kann eine unbegrenzte Anzahl von Maschinen starten" msgid "Can manage groups." -msgstr "" +msgstr "Kann Gruppen verwalten" msgid "Can reboot all virtual machines." -msgstr "" +msgstr "Kann alle virtuellen Maschinen neu starten" msgid "Can reboot clones own virtual machines." msgstr "" msgid "Can reboot own virtual machines." -msgstr "" +msgstr "Kann eigene virtuelle Maschinen neu starten" msgid "Can rename any virtual machine owned by the user." -msgstr "" +msgstr "Kann jede viruelle Maschine dieses Nutzers umbenennen" msgid "Can rename any virtual machine." -msgstr "" +msgstr "Kann jede virtuelle Maschine umbenennen" msgid "Can rename clones from virtual machines owned by the user." -msgstr "" +msgstr "Kann Klone von virtuellen Maschinen dieses Nutzers umbenennen" msgid "Can shutdown own virtual machines." -msgstr "" +msgstr "Kann eigene virtuelle Maschinen umbenennen" msgid "Can view groups." -msgstr "" +msgstr "Kann Gruppen anzeigen" msgid "Change" -msgstr "" +msgstr "Ändern" msgid "Changing the base of a virtual machine is potentially dangerous and can't be undone." -msgstr "" +msgstr "Das ändern der Grundlage einer virtuellen maschine ist potentiell gefährlich und kann nicht rückgängig gemacht werden." msgid "Check" -msgstr "" +msgstr "Kontrollieren" msgid "Check connection to" -msgstr "" +msgstr "Kontrolliere die Verbindung zu" msgid "Choose the virtualization type of the Node." -msgstr "" +msgstr "Virtualisierungstyp des Knoten wählen." msgid "Choose the virtualization type of the Virtual Machine." -msgstr "" +msgstr "Virtualisierungstyp der virtuellen Maschine wählen." msgid "Client" msgstr "" @@ -1088,94 +1088,94 @@ msgid "Clones are volatile, so all the pool will be started." msgstr "" msgid "Clones created from this machine will be removed on shutdown." -msgstr "" +msgstr "Klone die auf dieser Maschine erstellt wurden, werden beim herunterfahren gelöscht." msgid "Close" -msgstr "" +msgstr "Schließen" msgid "Color" -msgstr "" +msgstr "Farbe" msgid "Compact" -msgstr "" +msgstr "Kompakt" msgid "Compact disk volumes" msgstr "" msgid "Configure LDAP Authentication" -msgstr "" +msgstr "LDAP Authentifizierung einrichten" msgid "Confirm" -msgstr "" +msgstr "Bestätigen" msgid "Confirm disable node" -msgstr "" +msgstr "Deaktivieren des Knoten bestätigen" msgid "Confirm password can not exceed 20 characters" -msgstr "" +msgstr "Passwort bestätigen darf nicht länger sein als 20 Zeichen" msgid "Confirm password can only contain words and numbers" -msgstr "" +msgstr "Passwort bestätigen darf nur aus zahlen und wörtern bestehen" msgid "Confirm password is required" -msgstr "" +msgstr "Passwort bestätigen wird benötigt" msgid "Confirm password must be at least 5 characters" -msgstr "" +msgstr "Passwort bestätigen muss mindestend 5 Zeichen lang sein" msgid "Congrats" -msgstr "" +msgstr "Glückwunsch" msgid "Contact Method" -msgstr "" +msgstr "Kontaktmethode" msgid "Content will be cleaned on restore" -msgstr "" +msgstr "Inhalt wird beim wiederherstellen gelöscht" msgid "Content will be cleaned on restore and shutdown" -msgstr "" +msgstr "Inhalt wird beim wiederherstellen und herunterfahren gelöscht" msgid "Content will be kept on restore" -msgstr "" +msgstr "Inhalt wird beim wiederherstellen behalten" msgid "Create a new group" -msgstr "" +msgstr "Neue Gruppe erstellen" msgid "Current memory (MB)" -msgstr "" +msgstr "Aktueller Speicher (MB)" msgid "Danger: This will destroy all the disk data permantently." -msgstr "" +msgstr "Achtung: Alle daten werden dieser Festplatte werden permanent gelöscht." msgid "Data" -msgstr "" +msgstr "Daten" msgid "Debug" msgstr "" msgid "Debug Ports" -msgstr "" +msgstr "Debug Ports" msgid "Debug Ports Exposed" -msgstr "" +msgstr "Debug Ports öffentlich" msgid "Delete" -msgstr "" +msgstr "Löschen" msgid "Delete the event" -msgstr "" +msgstr "Das Ereignis löschen" msgid "Disable" -msgstr "" +msgstr "Deaktivieren" msgid "Disabled" -msgstr "" +msgstr "Deaktiviert" msgid "Disabling this node will shut all the" msgstr "" msgid "Display" -msgstr "" +msgstr "Anzeige" msgid "Display IP :" msgstr "" @@ -1196,40 +1196,40 @@ msgid "Edit the event" msgstr "" msgid "Email" -msgstr "" +msgstr "E-Mail" msgid "Enable" -msgstr "" +msgstr "Aktivieren" msgid "Enables display password when available" msgstr "" msgid "End" -msgstr "" +msgstr "Ende" msgid "Enter New Password" -msgstr "" +msgstr "Neues Passwort eigeben" msgid "Enter Old Password" -msgstr "" +msgstr "Alten Passwort eingeben" msgid "Enter group name" -msgstr "" +msgstr "Gruppenname eingeben" msgid "Error:" -msgstr "" +msgstr "Fehler:" msgid "Error: the LDAP entry for this user has been removed." -msgstr "" +msgstr "Fehler: Der LDAP Eintrag für diesen Benutzer wurde gelöscht." msgid "Error: you want to have" msgstr "" msgid "Event properties" -msgstr "" +msgstr "Ereigniseigenschaften" msgid "Event saved successfully" -msgstr "" +msgstr "Ereignis erfolgreich gespeichert" msgid "Exec clones sequentially" msgstr "" @@ -1238,10 +1238,10 @@ msgid "Exec. time" msgstr "" msgid "Fail" -msgstr "" +msgstr "Fehlgeschlagen" msgid "Fallback" -msgstr "" +msgstr "Fallback" msgid "Fill required camps with valid data" msgstr "" From efb00bfac23276bfed6957f4f6a55e89c39a1f81 Mon Sep 17 00:00:00 2001 From: Claire-me Date: Tue, 10 Oct 2023 07:57:59 +0100 Subject: [PATCH 09/17] Update it.po (#1997) --- lib/Ravada/I18N/it.po | 518 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 518 insertions(+) diff --git a/lib/Ravada/I18N/it.po b/lib/Ravada/I18N/it.po index 4d81b94a2..42785e52f 100644 --- a/lib/Ravada/I18N/it.po +++ b/lib/Ravada/I18N/it.po @@ -1076,3 +1076,521 @@ msgstr "Rimuovi gruppo" msgid "Are you sure you want to remove this group ?" msgstr "Sei sicuro di voler rimuovere questo gruppo ?" + +msgid "Schedule" +msgstr "Programma" + +msgid "Confirm" +msgstr "Confermare" + +msgid "Data" +msgstr "Dati" + +msgid "Node with more free memory" +msgstr "Nodo con più memoria libera" + +msgid "locked" +msgstr "bloccato" + +msgid "Can rename any virtual machine." +msgstr "Può rinominare qualsiasi macchina virtuale." + +msgid "Error: you want to have" +msgstr "Errore: si vuole avere" + +msgid "Remove Clones" +msgstr "Rimuovere i cloni" + +msgid "The new user" +msgstr "Il nuovo utente" + +msgid "has been added successfully!" +msgstr "è stato aggiunto con successo!" + +msgid "Set defaults" +msgstr "Imposta valori predefiniti" + +msgid "What do you want to do now?" +msgstr "Cosa vuoi fare ora?" + +msgid "Shutdown Timeout" +msgstr "Timeout spegnimento" + +msgid "" +"This server has reservations for today. Machines from users out of\n" +" the booking list will be shutdown." +msgstr "" +"Questo server ha prenotazioni per oggi. Macchine da utenti fuori\n" +" la lista delle prenotazioni sarà chiusa." + +msgid "filter" +msgstr "filtro" + +msgid "Invalid Template" +msgstr "Modello non valido" + +msgid "Start after create the virtual machine" +msgstr "Avvio dopo la creazione della macchina virtuale" + +msgid "Advanced options" +msgstr "Opzioni avanzate" + +msgid "The Minimum Disk Size needed for this ISO is" +msgstr "La dimensione minima del disco necessaria per questa ISO è" + +msgid "Requests" +msgstr "Richieste" + +msgid "There are no LDAP groups defined." +msgstr "Non sono definiti gruppi LDAP." + +msgid "This virtual machine has no group restrictions." +msgstr "Questa macchina virtuale non ha restrizioni di gruppo." + +msgid "Can reboot all virtual machines." +msgstr "Può riavviare tutte le macchine virtuali." + +msgid "The source machine is not a base. It must be prepared before it can be copied. This process may take some minutes." +msgstr "" +"La macchina sorgente non è una base. Deve essere preparata prima di poter " +"essere copiata. Questo processo può richiedere alcuni minuti." + +msgid "Can shutdown own virtual machines." +msgstr "Può spegnere le proprie macchine virtuali." + +msgid "Restricted" +msgstr "Limitato" + +msgid "remove" +msgstr "rimuovere" + +msgid "Machines Notifications" +msgstr "Notifiche delle macchine" + +msgid "Change" +msgstr "Cambiamento" + +msgid "Warning" +msgstr "Avvertenze" + +msgid "Old and New Passwords match!" +msgstr "Le vecchie e le nuove password coincidono!" + +msgid "bases" +msgstr "basi" + +msgid "Clones created from this machine will be removed on shutdown." +msgstr "I cloni creati da questa macchina verranno rimossi allo spegnimento." + +msgid "Event properties" +msgstr "Proprietà dell'evento" + +msgid "New name" +msgstr "Nuovo nome" + +msgid "Add another user" +msgstr "Aggiungere un altro utente" + +msgid "The machine will power off after this minutes after shutdown." +msgstr "La macchina si spegnerà dopo questi minuti dallo spegnimento." + +msgid "Re-Enter New Password" +msgstr "Reinserire la nuova password" + +msgid "allowed" +msgstr "consentito" + +msgid "Can have an unlimited amount of machines started." +msgstr "È possibile avviare un numero illimitato di macchine." + +msgid "Monitoring" +msgstr "Monitoraggio" + +msgid "Any remote client can access this port" +msgstr "Qualsiasi client remoto può accedere a questa porta" + +msgid "New Password can only contain words and numbers" +msgstr "La nuova password può contenere solo parole e numeri" + +msgid "To" +msgstr "A" + +msgid "Can view groups." +msgstr "Può visualizzare gruppi." + +msgid "LDAP groups are required to set up bookings. Some groups where found but no members belong to them. Add new entries here." +msgstr "" +"I gruppi LDAP sono necessari per impostare le prenotazioni. Alcuni gruppi " +"sono stati trovati ma nessun membro vi appartiene. Aggiungi nuove voci qui." + +msgid "Swap" +msgstr "Scambio" + +msgid "Nothing to monitoring" +msgstr "Niente da monitorare" + +msgid "Can reboot own virtual machines." +msgstr "Può riavviare le macchine virtuali." + +msgid "Phone" +msgstr "Telefono" + +msgid "Can expose virtual machine ports." +msgstr "Può esporre le porte delle macchine virtuali." + +msgid "Enter Old Password" +msgstr "Inserire la vecchia password" + +msgid "All events" +msgstr "Tutti gli eventi" + +msgid "added" +msgstr "aggiunto" + +msgid "This field is required" +msgstr "Questo campo è obbligatorio" + +msgid "Type the ISO pathname" +msgstr "Digitare il percorso ISO" + +msgid "can change the settings of any virtual machine cloned from one base owned by the user." +msgstr "" +"può modificare le impostazioni di qualsiasi macchina virtuale clonata da una " +"base di proprietà dell'utente." + +msgid "Public Port" +msgstr "Porto pubblico" + +msgid "Support Contact" +msgstr "Contatto di supporto" + +msgid "This machine is hibernated and can't be renamed." +msgstr "Questa macchina è ibernata e non può essere rinominata." + +msgid "Rebase" +msgstr "rebase" + +msgid "Port" +msgstr "Porto" + +msgid "Can rename any virtual machine owned by the user." +msgstr "Può rinominare qualsiasi macchina virtuale di proprietà dell'utente." + +msgid "Spinoff this clone from its base." +msgstr "Spinoff di questo clone dalla sua base." + +msgid "stopped" +msgstr "fermato" + +msgid "but there are only" +msgstr "ma ci sono solo" + +msgid "Can rename clones from virtual machines owned by the user." +msgstr "" +"Può rinominare i cloni delle macchine virtuali di proprietà dell'utente." + +msgid "Create a new group" +msgstr "Creare un nuovo gruppo" + +msgid "System Disk: (GB)" +msgstr "Disco di sistema: (GB)" + +msgid "This feature requires a LDAP server configured." +msgstr "Questa funzione richiede la configurazione di un server LDAP." + +msgid "User not found" +msgstr "Utente non trovato" + +msgid "Save changes" +msgstr "Salva le modifiche" + +msgid "Only remote client can access this port" +msgstr "Solo il client remoto può accedere a questa porta" + +msgid "Type the template name" +msgstr "Digitare il nome del modello" + +msgid "Password and their confirmation do not match!" +msgstr "La password e la sua conferma non corrispondono!" + +msgid "Reload" +msgstr "Ricaricare" + +msgid "Source Machine" +msgstr "Fonte Macchina" + +msgid "UEFI" +msgstr "UEFI" + +msgid "Select the .iso file the machine will utilize when installing the OS." +msgstr "" +"Selezionare il file .iso che la macchina utilizzerà per l'installazione del " +"sistema operativo." + +msgid "Poweroff" +msgstr "Spegnimento" + +msgid "Hide active" +msgstr "Nascondi attivo" + +msgid "Group name is required" +msgstr "Il nome del gruppo è richiesto" + +msgid "BIOS" +msgstr "BIOS" + +msgid "No, cancel" +msgstr "No, annullare" + +msgid "Group name" +msgstr "Nome del gruppo" + +msgid "Permission granted to user" +msgstr "Autorizzazione concessa all'utente" + +msgid "Remove the base prepared from this machine" +msgstr "Rimuovere la base preparata dalla macchina" + +msgid "The Minimum Swap Disk Size needed for this ISO is" +msgstr "La dimensione minima del disco di swap necessaria per questa ISO è" + +msgid "Max memory (MB)" +msgstr "Memoria massima (MB)" + +msgid "Title" +msgstr "Titolo" + +msgid "Only users from these groups will be allowed to execute this machine" +msgstr "Solo gli utenti di questi gruppi potranno eseguire questa macchina" + +msgid "Spinoff clone" +msgstr "Clone spinoff" + +msgid "Fill required camps with valid data" +msgstr "Riempire i campi richiesti con dati validi" + +msgid "This virtual machine has clones that are bases and won't be removed" +msgstr "" +"Questa macchina virtuale ha dei cloni che sono basi e non verranno rimossi" + +msgid "Until" +msgstr "Fino a" + +msgid "options" +msgstr "opzioni" + +msgid "Repeated New Password is required" +msgstr "È necessario ripetere la nuova password" + +msgid "Congrats" +msgstr "Congratulazioni" + +msgid "Only remote client can access this port if restricted" +msgstr "Solo il client remoto può accedere a questa porta, se limitata" + +msgid "Changing the base of a virtual machine is potentially dangerous and can't be undone." +msgstr "" +"La modifica della base di una macchina virtuale è potenzialmente pericolosa " +"e non può essere annullata." + +msgid "This event and the following" +msgstr "Questo evento e i seguenti" + +msgid "From" +msgstr "Da" + +msgid "For Spice client setup follow this " +msgstr "Per la configurazione del client Spice segui questo ' " + +msgid "Refresh ports" +msgstr "Aggiorna le porte" + +msgid "Edit the event" +msgstr "Modificare l'evento" + +msgid "Prepare Base" +msgstr "Preparare la base" + +msgid "Groups with permissions" +msgstr "Gruppi con autorizzazioni" + +msgid "Enter group name" +msgstr "Inserire il nome del gruppo" + +msgid "Current memory (MB)" +msgstr "Memoria attuale (MB)" + +msgid "Node with machines from the same user" +msgstr "Nodo con macchine dello stesso utente" + +msgid "Phone Number" +msgstr "Numero di telefono" + +msgid "hide" +msgstr "nascondersi" + +msgid "Can reboot clones own virtual machines." +msgstr "Può riavviare i cloni delle proprie macchine virtuali." + +msgid "Machine" +msgstr "Macchina" + +msgid "This booking overlaps already scheduled reservations" +msgstr "Questa prenotazione si sovrappone a prenotazioni già programmate" + +msgid "Are you sure you want to spinoff from the base?" +msgstr "Sei sicura di voler fare uno spin-off dalla base?" + +msgid "not allowed" +msgstr "non consentito" + +msgid "This ISO is being downloaded. The virtual machine will be created after." +msgstr "Questa ISO viene scaricata. La macchina virtuale verrà creata dopo." + +msgid "This event" +msgstr "Questo evento" + +msgid "Balance" +msgstr "Equilibrio" + +msgid "Run Timeout" +msgstr "Timeout esecuzione" + +msgid "Old Password can only contain words and numbers" +msgstr "La vecchia password può contenere solo parole e numeri" + +msgid "New Password is required" +msgstr "È richiesta una nuova password" + +msgid "Old Password is required" +msgstr "È richiesta la vecchia password" + +msgid "Contact Method" +msgstr "Metodo di contatto" + +msgid "Yes, rebase" +msgstr "Sì, ribasare" + +msgid "disabled" +msgstr "disabilitato" + +msgid "Email" +msgstr "Email" + +msgid "groups" +msgstr "gruppi" + +msgid "Virtual Machine will be shutdown when user disconnects." +msgstr "La macchina virtuale verrà spenta quando l'utente si disconnette." + +msgid "Please select an ISO file" +msgstr "Selezionare un file ISO" + +msgid "Delete the event" +msgstr "Cancellare l'evento" + +msgid "Passwords do not match!" +msgstr "Le password non corrispondono!" + +msgid "Can manage groups." +msgstr "Può gestire gruppi." + +msgid "volatile" +msgstr "volatili" + +msgid "Rebase this machine to another virtual machine" +msgstr "Reimpostare questa macchina su un'altra macchina virtuale" + +msgid "Clones are volatile, so all the pool will be started." +msgstr "I cloni sono volatili, quindi tutto il pool verrà avviato." + +msgid "Delete" +msgstr "Cancellare" + +msgid "settings" +msgstr "impostazioni" + +msgid "Color" +msgstr "Colore" + +msgid "Permission revoked from user" +msgstr "Autorizzazione revocata all'utente" + +msgid "If you have any issue, you can contact the Suport crew and we will try to help you. Please, only contact Support Center if it is strictly necessary." +msgstr "" +"In caso di problemi, è possibile contattare il gruppo di supporto e " +"cercheremo di aiutarvi. Contattare il Centro di assistenza solo se è " +"strettamente necessario." + +msgid "can manage users." +msgstr "può gestire gli utenti." + +msgid "Will be destroyed on shutdown" +msgstr "Verrà distrutto al momento dello spegnimento" + +msgid "Can get a screenshot of own virtual machines." +msgstr "È possibile ottenere uno screenshot delle proprie macchine virtuali." + +msgid "No Results Found" +msgstr "Nessun risultato trovato" + +msgid "Search group" +msgstr "Gruppo di ricerca" + +msgid "Group name can not exceed 80 characters" +msgstr "Il nome del gruppo non può superare gli 80 caratteri" + +msgid "Event saved successfully" +msgstr "Evento salvato con successo" + +msgid "clones defined." +msgstr "cloni definiti." + +msgid "Repeat" +msgstr "Ripetere" + +msgid "Virtual Machine will start on host start." +msgstr "La macchina virtuale si avvia all'avvio dell'host." + +msgid "Legacy" +msgstr "Eredità" + +msgid "can have their own limit on started machines." +msgstr "possono avere un proprio limite di macchine avviate." + +msgid "networks" +msgstr "reti" + +msgid "Rebase all the clones to a new base" +msgstr "Reimpostare tutti i cloni su una nuova base" + +msgid "Client headers" +msgstr "Intestazioni del client" + +msgid "nodes" +msgstr "nodi" + +msgid "Enter New Password" +msgstr "Inserire la nuova password" + +msgid "Attachment" +msgstr "Allegato" + +msgid "The VM is" +msgstr "La macchina virtuale è" + +msgid "Choose the virtualization type of the Virtual Machine." +msgstr "Scegliere il tipo di virtualizzazione della macchina virtuale." + +msgid "This event and the following with same day of week" +msgstr "Questo evento e il successivo con lo stesso giorno della settimana" + +msgid "Weekly" +msgstr "Settimanale" + +msgid "Today Schedule" +msgstr "Programma di oggi" + +msgid "Shutdown disconnected" +msgstr "Spegnimento scollegato" From d000683bbc80847c217d16948a51c97a1489dde7 Mon Sep 17 00:00:00 2001 From: Ritish Srivastava <121374890+Ritish134@users.noreply.github.com> Date: Tue, 17 Oct 2023 14:16:18 +0530 Subject: [PATCH 10/17] Updated the changes of pr#1993 (#1994) Co-authored-by: Ritish134 --- lib/Ravada/I18N/en.po | 7 ++----- templates/main/requirements.html.ep | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/Ravada/I18N/en.po b/lib/Ravada/I18N/en.po index 8526e3eb8..36c0f7c5c 100644 --- a/lib/Ravada/I18N/en.po +++ b/lib/Ravada/I18N/en.po @@ -303,9 +303,6 @@ msgstr "Welcome" msgid "A viewer is required to run the virtual machines." msgstr "A viewer is required to run the virtual machines." -msgid "It is required a viewer to run the virtual machines." -msgstr "It is required a viewer to run the virtual machines." - msgid "Virtual Machines" msgstr "Virtual Machines" @@ -2170,5 +2167,5 @@ msgstr "Source Machine" msgid "Monitoring" msgstr "Monitoring" -msgid "For Spice client setup follow this " -msgstr "For Spice client setup follow this " +msgid "Follow these steps for Spice client setup" +msgstr "Follow these steps for Spice client setup" diff --git a/templates/main/requirements.html.ep b/templates/main/requirements.html.ep index 54ad4f434..4e7daa59b 100644 --- a/templates/main/requirements.html.ep +++ b/templates/main/requirements.html.ep @@ -24,7 +24,7 @@ <%=l 'To make this possible, copy the content of' %> spice.reg <%=l 'to an ASCII file and save it with a .reg extension, then execute the file.' %>
<%=l 'Please, make sure you have the right path and release, according to your PC configuration.' %>

<%=l 'For more information, check' %> <%=l 'the Windows Clients documentation' %>.


- ©

<%=l 'For Spice client setup follow this ' %> link .

+ ©

<%=l 'Follow these steps for Spice client setup' %> link .

From 40fe94ce400562457451a334f154342e89ce5560 Mon Sep 17 00:00:00 2001 From: Claire-me Date: Thu, 19 Oct 2023 08:35:25 +0100 Subject: [PATCH 11/17] Update Korean Translation (#2000) Update ko.po --- lib/Ravada/I18N/ko.po | 579 +++++++++++++++++++++--------------------- 1 file changed, 293 insertions(+), 286 deletions(-) diff --git a/lib/Ravada/I18N/ko.po b/lib/Ravada/I18N/ko.po index 616d53bed..85ac2b94d 100644 --- a/lib/Ravada/I18N/ko.po +++ b/lib/Ravada/I18N/ko.po @@ -2,871 +2,878 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) msgid "" msgstr "" +"PO-Revision-Date: 2023-10-13 20:02+0000\n" +"Last-Translator: Claire Charles \n" +"Language-Team: Korean \n" "Language: ko\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 5.1-dev\n" msgid "Ravada broker" -msgstr "" +msgstr "라바다 브로커" msgid "Machine Name" -msgstr "" +msgstr "시스템 이름" msgid "Tools" -msgstr "" +msgstr "도구" msgid "Choose a Machine to Start" -msgstr "" +msgstr "시동할 기계 선택" msgid "Start" -msgstr "" +msgstr "시작" msgid "Stop" -msgstr "" +msgstr "중지" msgid "View" -msgstr "" +msgstr "보기" msgid "Prepare base" -msgstr "" +msgstr "베이스 준비" msgid "Clone" -msgstr "" +msgstr "클론" msgid "Clone/s" -msgstr "" +msgstr "클론" msgid "Users List" -msgstr "" +msgstr "사용자 목록" msgid "Name" -msgstr "" +msgstr "이름" msgid "Log Out" -msgstr "" +msgstr "로그아웃" msgid "Available Machines" -msgstr "" +msgstr "사용 가능한 기계" msgid "Help" -msgstr "" +msgstr "도움말" msgid "Requirements" -msgstr "" +msgstr "요구 사항" msgid "About" -msgstr "" +msgstr "대해" msgid "Mark all as read" -msgstr "" +msgstr "모두 읽음으로 표시" msgid "Settings" -msgstr "" +msgstr "설정" msgid "New Machine" -msgstr "" +msgstr "새로운 기계" msgid "I want to change my password" -msgstr "" +msgstr "비밀번호를 변경하고 싶어요" msgid "I want to change my language" -msgstr "" +msgstr "언어를 변경하고 싶어요" msgid "Language:" -msgstr "" +msgstr "언어:" msgid "English" -msgstr "" +msgstr "영어" msgid "Spanish" -msgstr "" +msgstr "스페인어" msgid "Catalan" -msgstr "" +msgstr "카탈로니아어" msgid "Galician" -msgstr "" +msgstr "갈리시아어" msgid "French" -msgstr "" +msgstr "프랑스어" msgid "Hindi" -msgstr "" +msgstr "힌디어" msgid "Indonesian" -msgstr "" +msgstr "인도네시아어" msgid "Persian" -msgstr "" +msgstr "페르시아어" msgid "Japanese" -msgstr "" +msgstr "일본어" msgid "German" -msgstr "" +msgstr "독일어" msgid "New Password:" -msgstr "" +msgstr "새 비밀번호:" msgid "Confirm Password:" -msgstr "" +msgstr "비밀번호 확인:" msgid "Submit" -msgstr "" +msgstr "제출" msgid "Your language has been changed successfully" -msgstr "" +msgstr "언어가 성공적으로 변경되었습니다" msgid "Your password has been changed successfully" -msgstr "" +msgstr "비밀번호가 성공적으로 변경되었습니다" msgid "Password is too short" -msgstr "" +msgstr "암호가 너무 짧습니다" msgid "Password fields do not match" -msgstr "" +msgstr "암호 필드가 일치하지 않습니다" msgid "Some of the password fields are empty" -msgstr "" +msgstr "일부 암호 필드가 비어 있습니다" msgid "Status" -msgstr "" +msgstr "상태" msgid "Actions" -msgstr "" +msgstr "작업" msgid "Down" -msgstr "" +msgstr "아래로" msgid "Running" -msgstr "" +msgstr "실행 중" msgid "Shutdown" -msgstr "" +msgstr "종료" msgid "Screenshot" -msgstr "" +msgstr "스크린샷" msgid "Paused" -msgstr "" +msgstr "일시 중지" msgid "Machine locked by request" -msgstr "" +msgstr "요청에 의해 기계가 잠김" msgid "Process" -msgstr "" +msgstr "프로세스" msgid "Copy" -msgstr "" +msgstr "복사" msgid "Error!" -msgstr "" +msgstr "오류!" msgid "Error" -msgstr "" +msgstr "오류" msgid "Backend not available!" -msgstr "" +msgstr "백엔드를 사용할 수 없습니다!" msgid "Subject" -msgstr "" +msgstr "제목" msgid "Date" -msgstr "" +msgstr "날짜" msgid "Mark as read" -msgstr "" +msgstr "읽은 상태로 표시" msgid "Mark as unread" -msgstr "" +msgstr "읽지 않은 상태로 표시" msgid "No messages to show" -msgstr "" +msgstr "표시할 메시지가 없습니다" msgid "Admin" -msgstr "" +msgstr "관리자" msgid "Machines" -msgstr "" +msgstr "기계" msgid "machines" -msgstr "" +msgstr "기계" msgid "Users" -msgstr "" +msgstr "사용자" msgid "users" -msgstr "" +msgstr "사용자" msgid "Messages" -msgstr "" +msgstr "메시지" msgid "messages" -msgstr "" +msgstr "메시지" msgid "Admin tools" -msgstr "" +msgstr "관리 도구" msgid "Remove base" -msgstr "" +msgstr "베이스 제거" msgid "Restore" -msgstr "" +msgstr "복원" msgid "Hibernate" -msgstr "" +msgstr "최대 절전 모드" msgid "Are you sure?" -msgstr "" +msgstr "확실한가요?" msgid "Yes" -msgstr "" +msgstr "네" msgid "No" -msgstr "" +msgstr "아니요" msgid "Make public" -msgstr "" +msgstr "공개하기" msgid "Machine settings" -msgstr "" +msgstr "기계 설정" msgid "Cannot remove base, machine has clones" -msgstr "" +msgstr "베이스를 제거할 수 없습니다. 시스템에 클론이 있습니다" msgid "show clones" -msgstr "" +msgstr "클론 표시" msgid "hide clones" -msgstr "" +msgstr "클론 숨기기" msgid "For Spice redirection you will need to install" -msgstr "" +msgstr "Spice 리디렉션을 위해서는 설치가 필요합니다" msgid "in your computer." -msgstr "" +msgstr "컴퓨터에서." msgid "Search in your distro, e.g. in Debian/Ubuntu with" -msgstr "" +msgstr "배포판에서 검색하십시오 (예 : Debian / Ubuntu에서" msgid "You will need to install" -msgstr "" +msgstr "다음을 설치해야 합니다" msgid "and USB drivers (" -msgstr "" +msgstr "및 USB 드라이버(" msgid "" "Be aware that in Windows, Spice redirection is not automatic. It is " "necessary to associate protocol with the app." -msgstr "" +msgstr "Windows에서 Spice 리디렉션은 자동이 아닙니다. 프로토콜을 연결해야합니다" msgid "To make this possible, download" -msgstr "" +msgstr "이를 가능하게 하려면 다운로드하십시오." msgid "" "or copy the following lines in an ASCII file and save with extension .reg, " "then execute the file." -msgstr "" +msgstr "또는 ASCII 파일에서 다음 줄을 복사하고 확장명 .reg로 저장합니다." +"그런 다음 파일을 실행하십시오." msgid "Login" -msgstr "" +msgstr "로그인" msgid "Welcome" -msgstr "" +msgstr "환영합니다" msgid "A viewer is required to run the virtual machines." -msgstr "" +msgstr "뷰어는 가상 머신을 실행하는 데 필요합니다." msgid "It is required a viewer to run the virtual machines." -msgstr "" +msgstr "가상 머신을 실행하려면 뷰어가 필요합니다." msgid "Virtual Machines" -msgstr "" +msgstr "가상 머신" msgid "User" -msgstr "" +msgstr "사용자" msgid "Password" -msgstr "" +msgstr "암호" msgid "Start session" -msgstr "" +msgstr "세션 시작" msgid "bits) in your computer." -msgstr "" +msgstr "bits)를 컴퓨터에 저장합니다." msgid "Permissions" -msgstr "" +msgstr "권한을" msgid "can change the settings of owned virtual machines." -msgstr "" +msgstr "소유한 가상 컴퓨터의 설정을 변경할 수 있습니다." msgid "can change the settings of any virtual machines." -msgstr "" +msgstr "모든 가상 컴퓨터의 설정을 변경할 수 있습니다." msgid "can change the settings of any virtual machines " -msgstr "" +msgstr "모든 가상 컴퓨터의 설정을 변경할 수 있음" msgid "cloned from one base owned by the user." -msgstr "" +msgstr "사용자가 소유한 하나의 베이스에서 복제됩니다." msgid "can clone public virtual machines." -msgstr "" +msgstr "공용 가상 머신을 복제할 수 있습니다." msgid "can clone any virtual machine." -msgstr "" +msgstr "모든 가상 머신을 복제할 수 있습니다." msgid "can create bases." -msgstr "" +msgstr "기초를 만들 수 있습니다." msgid "can create virtual machines." -msgstr "" +msgstr "가상 시스템을 생성할 수 있습니다." msgid "can grant permissions to other users" -msgstr "" +msgstr "다른 사용자에게 권한을 부여할 수 있습니다." msgid "can hibernate any virtual machine." -msgstr "" +msgstr "모든 가상 컴퓨터를 최대 절전 모드로 전환할 수 있습니다." msgid "can hibernate clones from virtual machines owned by the user." -msgstr "" +msgstr "사용자가 소유한 가상 시스템의 클론을 최대 절전 모드로 생성할 수 있습니다." msgid "can hibernate any clone." -msgstr "" +msgstr "모든 클론을 최대 절전 모드로 전환할 수 있습니다." msgid "can remove any virtual machines owned by the user." -msgstr "" +msgstr "사용자가 소유한 모든 가상 머신을 제거할 수 있습니다." msgid "can remove any virtual machine." -msgstr "" +msgstr "모든 가상 머신을 제거할 수 있습니다." msgid "can remove clones from virtual machines owned by the user." -msgstr "" +msgstr "사용자가 소유한 가상 시스템에서 클론을 제거할 수 있습니다." msgid "can remove any clone." -msgstr "" +msgstr "모든 클론을 제거할 수 있습니다." msgid "can take a screenshot of any virtual machine owned by the user." -msgstr "" +msgstr "사용자가 소유한 가상 머신의 스크린샷을 찍을 수 있습니다." msgid "can take a screenshot of any virtual machine." -msgstr "" +msgstr "모든 가상 머신의 스크린샷을 찍을 수 있습니다." msgid "can shutdown any virtual machine." -msgstr "" +msgstr "모든 가상 머신을 종료할 수 있습니다." msgid "can shutdown clones from virtual machines owned by the user." -msgstr "" +msgstr "사용자가 소유한 가상 시스템에서 클론을 종료할 수 있습니다." msgid "Submit Query" -msgstr "" +msgstr "쿼리 제출" msgid "From Template" -msgstr "" +msgstr "템플릿에서" msgid "From Machine" -msgstr "" +msgstr "기계에서" msgid "Backend" -msgstr "" +msgstr "백엔드" msgid "Select Template" -msgstr "" +msgstr "템플릿 선택" msgid "This ISO image has not been downloaded yet. It may take some minutes, even hours until the file is fetched from the Internet." -msgstr "" +msgstr "이 ISO 이미지는 아직 다운로드되지 않았습니다. 인터넷에서 파일을 가져올 때까지 몇 분, 심지어 몇 시간이 걸릴 수 있습니다." msgid "Download now" -msgstr "" +msgstr "지금 다운로드" msgid "Select ISO" -msgstr "" +msgstr "ISO 선택" msgid "Template" -msgstr "" +msgstr "템플릿" msgid "Disk Size: (GB)" -msgstr "" +msgstr "디스크 크기: (GB)" msgid "Swap Size: (GB)" -msgstr "" +msgstr "스왑 크기: (GB)" msgid "Ram: (GB)" -msgstr "" +msgstr "RAM(GB):" msgid "Machine name is required." -msgstr "" +msgstr "시스템 이름이 필요합니다." msgid "Machine name can't exceed 20 characters." -msgstr "" +msgstr "컴퓨터 이름은 20자를 초과할 수 없습니다." msgid "Backend selection is required." -msgstr "" +msgstr "백엔드를 선택해야 합니다." msgid "ISO image selection is required." -msgstr "" +msgstr "ISO 이미지를 선택해야 합니다." msgid "Template selection is required." -msgstr "" +msgstr "템플릿을 선택해야 합니다." msgid "A machine with that name already exists." -msgstr "" +msgstr "해당 이름의 컴퓨터가 이미 있습니다." msgid "The machine name is only allowed to consist of alphabetic characters, numbers, dashes and points." -msgstr "" +msgstr "컴퓨터 이름은 알파벳 문자, 숫자, 대시 및 점으로만 구성될 수 있습니다." msgid "Create" -msgstr "" +msgstr "생성" msgid "" "can change the settings of all virtual machines " "cloned from one base owned by the user." -msgstr "" +msgstr "모든 가상 컴퓨터의 설정을 변경할 수 있음 사용자가 소유한 하나의 베이스에서 복제됩니다." msgid "monitoring" -msgstr "" +msgstr "모니터링" msgid "Are you sure you want to remove the Base of" -msgstr "" +msgstr "베이스를 제거하시겠습니까?" msgid "Are you sure you want to prepare the Base of" -msgstr "" +msgstr "베이스를 준비하시겠습니까?" msgid "Are you sure you want to change the Public state of" -msgstr "" +msgstr "의 공개 상태를 변경하시겠습니까?" msgid "Search" -msgstr "" +msgstr "검색" msgid "There are no public bases available in this system." -msgstr "" +msgstr "이 시스템에 사용할 수 있는 공공 기지가 없습니다." msgid "There are no machines available in this system." -msgstr "" +msgstr "이 시스템에 사용할 수 있는 장비가 없습니다." msgid "Create one." -msgstr "" +msgstr "하나를 만듭니다." msgid "not public" -msgstr "" +msgstr "비공개" msgid "User Settings" -msgstr "" +msgstr "사용자 설정" msgid "Create a new account" -msgstr "" +msgstr "새 계정 만들기" msgid "Username" -msgstr "" +msgstr "사용자 이름" msgid "Hide Private" -msgstr "" +msgstr "비공개 숨기기" msgid "Show All" -msgstr "" +msgstr "모두 표시" msgid "Shutting down ..." -msgstr "" +msgstr "종료 중..." msgid "It may take a couple of minutes." -msgstr "" +msgstr "몇 분 정도 걸릴 수 있습니다." msgid "Hibernating ..." -msgstr "" +msgstr "최대 절전 모드..." msgid "Description" -msgstr "" +msgstr "설명" msgid "Rename" -msgstr "" +msgstr "이름 바꾸기" msgid "Options" -msgstr "" +msgstr "옵션" msgid "Drivers" -msgstr "" +msgstr "드라이버" msgid "Graphics" -msgstr "" +msgstr "그래픽" msgid "Remove Machine" -msgstr "" +msgstr "기계 제거" msgid "System overview" -msgstr "" +msgstr "시스템 개요" msgid "Danger Zone" -msgstr "" +msgstr "위험 구역" msgid "Once you delete the machine, there is no going back. Please be certain." -msgstr "" +msgstr "컴퓨터를 삭제하면 다시 돌아갈 수 없습니다. 확실하게 해 주십시오." msgid "Danger" -msgstr "" +msgstr "위험" msgid "This will remove all the contents of the machine" -msgstr "" +msgstr "이렇게 하면 기계의 모든 내용물이 제거됩니다" msgid "This action can't be undone" -msgstr "" +msgstr "이 작업은 실행 취소할 수 없습니다" msgid "Are you sure ?" -msgstr "" +msgstr "정말이야?" msgid "Yes, remove" -msgstr "" +msgstr "예, 삭제합니다." msgid "Virtual Machine" -msgstr "" +msgstr "가상 머신" msgid "removed" -msgstr "" +msgstr "제거됨" msgid "This will remove all the" -msgstr "" +msgstr "이렇게 하면 모든" msgid "clones of the machine" -msgstr "" +msgstr "시스템의 클론" msgid "Yes, remove all the clones" -msgstr "" +msgstr "예, 모든 클론을 제거합니다." msgid "This virtual machine can't be removed because it has" -msgstr "" +msgstr "이 가상 머신은 다음과 같은 이유로 제거할 수 없습니다." msgid "clones" -msgstr "" +msgstr "클론" msgid "Clones" -msgstr "" +msgstr "클론" msgid "This machine has not Graphics parameters!" -msgstr "" +msgstr "이 컴퓨터에 그래픽 매개변수가 없습니다!" msgid "recommended" -msgstr "" +msgstr "권장" msgid "to" -msgstr "" +msgstr "에" msgid "Changing Public State" -msgstr "" +msgstr "공용 상태 변경" msgid "Changing Base State" -msgstr "" +msgstr "기본 상태 변경" msgid "action" -msgstr "" +msgstr "조치" msgid "Grants" -msgstr "" +msgstr "보조금" msgid "public" -msgstr "" +msgstr "공공" msgid "New Base" -msgstr "" +msgstr "새로운 기지" msgid "Version" -msgstr "" +msgstr "버전" msgid "Authors" -msgstr "" +msgstr "저자" msgid "Development" -msgstr "" +msgstr "개발" msgid "Below are listed the technologies used in this project:" -msgstr "" +msgstr "이 프로젝트에 사용된 기술은 다음과 같습니다." msgid "The code is available on " -msgstr "" +msgstr "코드는 에서 사용할 수 있습니다." msgid "The code is " -msgstr "" +msgstr "코드는 다음과 같습니다 " msgid "License" -msgstr "" +msgstr "사용권" msgid "Machine locked by" -msgstr "" +msgstr "잠긴 기계" msgid "This Machine is a base" -msgstr "" +msgstr "이 기계는 기본입니다" msgid "Cloned" -msgstr "" +msgstr "복제됨" msgid "is admin" -msgstr "" +msgstr "관리자임" msgid "Add new user" -msgstr "" +msgstr "새 사용자 추가" msgid "Register" -msgstr "" +msgstr "등록" msgid "Current Password:" -msgstr "" +msgstr "현재 비밀번호:" msgid "For more information, check" -msgstr "" +msgstr "자세한 내용은 다음을 확인하십시오" msgid "the Windows Clients documentation" -msgstr "" +msgstr "Windows 클라이언트 설명서" msgid "It is programmed in" -msgstr "" +msgstr "에 프로그래밍되어 있습니다" msgid "with perl framework" -msgstr "" +msgstr "Perl 프레임워크 사용" msgid "and" -msgstr "" +msgstr "그리고" msgid "Memory" -msgstr "" +msgstr "메모리" msgid "MB available" -msgstr "" +msgstr "MB 사용 가능" msgid "The machine will shutdown after these minutes" -msgstr "" +msgstr "이 몇 분 후에 기계가 종료됩니다" msgid "Cancel" -msgstr "" +msgstr "취소" msgid "Please enter the following information to create the account" -msgstr "" +msgstr "계정을 만들려면 다음 정보를 입력하십시오." msgid "Enter Username" -msgstr "" +msgstr "사용자 이름 입력" msgid "Enter Password" -msgstr "" +msgstr "비밀번호 입력" msgid "Confirm Password" -msgstr "" +msgstr "비밀번호 확인" msgid "Username is required" -msgstr "" +msgstr "사용자 이름은 필수입니다." msgid "Username can not exceed 20 characters" -msgstr "" +msgstr "사용자 이름은 20자를 초과할 수 없습니다." msgid "Username must be at least 5 characters" -msgstr "" +msgstr "사용자 이름은 5자 이상이어야 합니다." msgid "Username can only contain words, numbers, dashes, dots and underscores" -msgstr "" +msgstr "사용자 이름에는 단어, 숫자, 대시, 점 및 밑줄만 포함될 수 있습니다." msgid "Password is required" -msgstr "" +msgstr "비밀번호가 필요합니다" msgid "Password can not exceed 20 characters" -msgstr "" +msgstr "비밀번호는 20자를 초과할 수 없습니다." msgid "Password must be at least 5 characters" -msgstr "" +msgstr "비밀번호는 5자 이상이어야 합니다." msgid "Password can only contain words and numbers" -msgstr "" +msgstr "비밀번호는 단어와 숫자만 포함할 수 있습니다." msgid "Confirm Password is required" -msgstr "" +msgstr "비밀번호 확인이 필요합니다" msgid "Confirm Password can not exceed 20 characters" -msgstr "" +msgstr "비밀번호 확인은 20자를 초과할 수 없습니다." msgid "Confirm Password must be at least 5 characters" -msgstr "" +msgstr "비밀번호 확인은 5자 이상이어야 합니다." msgid "Confirm Password can only contain words and numbers" -msgstr "" +msgstr "비밀번호 확인은 단어와 숫자만 포함할 수 있습니다." msgid "This information will be available to the users" -msgstr "" +msgstr "이 정보는 사용자가 사용할 수 있습니다" msgid "Volatile Clones" -msgstr "" +msgstr "휘발성 클론" msgid "Owner" -msgstr "" +msgstr "소유자" msgid "Change the owner of the machine" -msgstr "" +msgstr "컴퓨터 소유자 변경" msgid "Press SHIFT + F12 to exit" -msgstr "" +msgstr "종료하려면 SHIFT + F12를 누릅니다." msgid "If you can not see the machine screen in a few seconds check for a file called" -msgstr "" +msgstr "몇 초 안에 기계 화면을 볼 수 없다면 다음 파일을 확인하십시오" msgid "in your downloads folder." -msgstr "" +msgstr "다운로드 폴더에서." msgid "copied to clipboard" -msgstr "" +msgstr "클립보아에 복사됨" msgid "Portuguese" -msgstr "" +msgstr "포르투갈어" msgid "Read more." -msgstr "" +msgstr "자세히 보기." msgid "The password for this virtual machine connection is :" -msgstr "" +msgstr "이 가상 컴퓨터 연결의 암호는 다음과 같습니다." msgid "view" -msgstr "" +msgstr "보기" msgid "Start again" -msgstr "" +msgstr "다시 시작" msgid "The machine is down." -msgstr "" +msgstr "기계가 다운되었습니다." msgid "Remove" -msgstr "" +msgstr "Remove" msgid "The machine must be started to take the screenshot." -msgstr "" +msgstr "스크린샷을 찍으려면 컴퓨터를 시동해야 합니다." msgid "Take screenshot" -msgstr "" +msgstr "스크린샷 찍기" msgid "Set base picture" -msgstr "" +msgstr "기본 그림 설정" msgid "Saving the screenshot, please wait a moment..." -msgstr "" +msgstr "스크린샷을 저장하는 중입니다. 잠시 기다려 주십시오..." msgid "You need to run the machine in order to take a screenshot." -msgstr "" +msgstr "스크린샷을 찍으려면 기계를 실행해야합니다." msgid "Copying the screenshot, please wait a moment..." -msgstr "" +msgstr "스크린샷을 복사하는 중입니다. 잠시 기다려 주십시오..." msgid "You need a screenshot in order to copy it to the base machine." -msgstr "" +msgstr "기본 컴퓨터에 복사하려면 스크린샷이 필요합니다." msgid "Add" -msgstr "" +msgstr "추가" msgid "The changes will apply on next restart" -msgstr "" +msgstr "변경 사항은 다음에 다시 시작할 때 적용됩니다." msgid "This VM is running and can't be renamed." -msgstr "" +msgstr "이 VM이 실행 중이므로 이름을 바꿀 수 없습니다." msgid "Thanks to:" -msgstr "" +msgstr "감사:" msgid "licensed." -msgstr "" +msgstr "라이센스가 부여되었습니다." msgid "New Node" -msgstr "" +msgstr "새 노드" msgid "Address" -msgstr "" +msgstr "주소" msgid "A node with that name already exists." -msgstr "" +msgstr "해당 이름의 노드가 이미 있습니다." msgid "Setting your default language" -msgstr "" +msgstr "기본 언어 설정" msgid "Change password" -msgstr "" +msgstr "암호 변경" msgid "Update password" -msgstr "" +msgstr "비밀번호 업데이트" msgid "Start all clones" -msgstr "" +msgstr "모든 클론 시작" msgid "Access denied" -msgstr "" +msgstr "액세스 거부됨" msgid "Empty login name" -msgstr "" +msgstr "빈 로그인 이름" msgid "Empty password" -msgstr "" +msgstr "빈 암호" msgid "Session timeout" -msgstr "" +msgstr "세션 시간 초과" msgid "Vietnamese" -msgstr "" +msgstr "베트남어" msgid "Italian" -msgstr "" +msgstr "이탈리아어" msgid "Type a typical LDAP user name to fetch the attribute list" -msgstr "" +msgstr "일반적인 LDAP 사용자 이름을 입력하여 속성 목록을 가져옵니다" msgid "User name" -msgstr "" +msgstr "사용자 이름" msgid "not found in LDAP server" -msgstr "" +msgstr "LDAP 서버에서 찾을 수 없습니다." msgid "Attribute" -msgstr "" +msgstr "속성" msgid "Value" -msgstr "" +msgstr "값" msgid "Allowed" -msgstr "" +msgstr "허용" msgid "Last" -msgstr "" +msgstr "최근" msgid "Default" -msgstr "" +msgstr "Default" msgid "If none of the previous match, access is allowed." -msgstr "" +msgstr "이전에 일치하는 항목이 없으면 액세스가 허용됩니다." msgid "If none of the previous match, access is denied." -msgstr "" +msgstr "이전 일치 항목이 없으면 액세스가 거부됩니다." msgid "verify" -msgstr "" +msgstr "확인" msgid "save" -msgstr "" +msgstr "저장" msgid "Verifying" -msgstr "" +msgstr "검증" msgid "No entries found" -msgstr "" +msgstr "항목을 찾을 수 없습니다." msgid "has at least" -msgstr "" +msgstr "적어도" msgid "entries." -msgstr "" +msgstr "항목." msgid "Enable last for not allowed restrictions." -msgstr "" +msgstr "허용되지 않는 제한에 대해 last를 사용하도록 설정합니다." msgid "Add description" -msgstr "" +msgstr "설명 추가" msgid "These actions affect all the clones on the machine" -msgstr "" +msgstr "이러한 작업은 시스템의 모든 클론에 영향을 미칩니다." msgid "Shutdown all clones" -msgstr "" +msgstr "모든 클론을 종료합니다" msgid "Keep in mind that there may be users using a clone" -msgstr "" +msgstr "클론을 사용하는 사용자가 있을 수 있습니다." msgid "Yes, shutwdown all the clones" -msgstr "" +msgstr "예, 모든 클론을 종료합니다." From f92b195e3a2d7accc93e568e3f55ced4e7d4735f Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Tue, 24 Oct 2023 09:19:16 +0200 Subject: [PATCH 12/17] Fix pool duplicated (#1996) fix: ignore linked duplicated pools --- lib/Ravada/VM.pm | 61 +++++++- lib/Ravada/VM/KVM.pm | 53 +++---- lib/Ravada/VM/Void.pm | 44 ++++-- t/lib/Test/Ravada.pm | 27 ++-- t/storage_list_unused.t | 325 ++++++++++++++++++++++++++++++++++------ t/vm/s30_storage.t | 4 +- 6 files changed, 406 insertions(+), 108 deletions(-) diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index 8e44ec4a0..be6d03fd9 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -146,8 +146,6 @@ around 'ping' => \&_around_ping; around 'connect' => \&_around_connect; after 'disconnect' => \&_post_disconnect; -around '_list_used_volumes' => \&_around_list_used_volumes; - ############################################################# # # method modifiers @@ -2515,6 +2513,32 @@ sub dir_backup($self) { return $dir_backup; } + +sub _follow_link($self, $file) { + my ($dir, $name) = $file =~ m{(.*)/(.*)}; + my $file2 = $file; + if ($dir) { + my $dir2 = $self->_follow_link($dir); + $file2 = "$dir2/$name"; + } + + if (!defined $self->{_is_link}->{$file2} ) { + my ($out,$err) = $self->run_command("stat","-c",'"%N"', $file2); + chomp $out; + my ($link) = $out =~ m{ -> '(.+)'}; + if ($link) { + if ($link !~ m{^/}) { + my ($path) = $file2 =~ m{(.*/)}; + $path = "/" if !$path; + $link = "$path$link"; + } + $self->{_is_link}->{$file2} = $link; + } + } + $self->{_is_link}->{$file2} = $file2 if !exists $self->{_is_link}->{$file2}; + return $self->{_is_link}->{$file2}; +} + sub _is_link_remote($self, $vol) { my ($out,$err) = $self->run_command("stat",$vol); @@ -2557,12 +2581,33 @@ sub _is_link($self,$vol) { return $path_link if $path_link; } -sub _around_list_used_volumes($orig, $self) { - my @vols = $self->$orig(); - my @links; - for my $vol ( @vols ) { - my $link = $self->_is_link($vol); - push @links,($link) if $link; +=head2 list_unused_volumes + +Returns a list of unused volume files + +=cut + +sub list_unused_volumes($self) { + my @all_vols = $self->list_volumes(); + + my @used = $self->list_used_volumes(); + my %used; + for my $vol (@used) { + $used{$vol}++; + my $link = $self->_follow_link($vol); + $used{$link}++ if $link; + } + + my @vols; + my %duplicated; + for my $vol ( @all_vols ) { + next if $used{$vol}; + + my $link = $self->_follow_link($vol); + next if $used{$link}; + next if $duplicated{$link}++; + + push @vols,($link); } return @vols; } diff --git a/lib/Ravada/VM/KVM.pm b/lib/Ravada/VM/KVM.pm index b98bd34e3..49b22e8df 100644 --- a/lib/Ravada/VM/KVM.pm +++ b/lib/Ravada/VM/KVM.pm @@ -336,6 +336,8 @@ sub search_volume_re($self,$pattern,$refresh=0) { my @vols; for ( 1 .. 10) { eval { @vols = $pool->list_all_volumes() }; + last if $@ && ref($@) && $@->code == 55; + last if $@ && $@ =~ /code: (55|38),/; last if !$@ || $@ =~ / no storage pool with matching uuid/; warn "WARNING: on search volume_re: $@"; sleep 1; @@ -383,15 +385,16 @@ sub remove_file($self,@files) { sub _list_volumes($self) { my @volumes; for my $pool (_list_storage_pools($self->vm)) { - next if !$pool->is_active; - my @vols; - for ( 1 .. 10) { + my @vols; + for ( 1 .. 10) { + next if !$pool->is_active; eval { @vols = $pool->list_all_volumes() }; + last if $@ && ref($@) && $@->code == 55; last if !$@ || $@ =~ / no storage pool with matching uuid/; warn "WARNING: on search volume_re: $@"; sleep 1; - } - push @volumes,@vols; + } + push @volumes,@vols; } return @volumes; } @@ -449,7 +452,7 @@ sub _find_all_volumes($self, $xml) { return @used; } -sub _list_used_volumes($self) { +sub list_used_volumes($self) { my @used =$self->_list_used_volumes_known(); for my $name ( $self->discover ) { my $dom = $self->vm->get_domain_by_name($name); @@ -458,20 +461,14 @@ sub _list_used_volumes($self) { return @used; } -sub list_unused_volumes($self) { - my %used = map { $_ => 1 } $self->_list_used_volumes(); - my @unused; +sub list_volumes($self) { my $file; + my @volumes; - my $n_found=0; for my $vol ( $self->_list_volumes ) { eval { ($file) = $vol->get_path }; confess $@ if $@ && $@ !~ /libvirt error code: 50,/; - next if $used{$file}; - - my $link = $self->_is_link($file); - next if $link && $used{$link}; my $info; eval { $info = $vol->get_info() }; @@ -480,11 +477,10 @@ sub list_unused_volumes($self) { next if !$info || $info->{type} eq 2; - # cluck Dumper([ $file, [sort grep /2023/,keys %used]]) if $file =~/2023/; - push @unused,($file); + push @volumes,($file); } - return @unused; + return @volumes; } sub refresh_storage_pools($self) { @@ -595,6 +591,7 @@ sub file_exists($self, $file) { sub _file_exists_remote($self, $file) { $file = $self->_follow_link($file) unless $file =~ /which$/; for my $pool ($self->vm->list_all_storage_pools ) { + next if !$pool->is_active; $self->_wait_storage( sub { $pool->refresh() } ); my @volumes = $self->_wait_storage( sub { $pool->list_all_volumes }); for my $vol ( @volumes ) { @@ -617,20 +614,6 @@ sub _file_exists_remote($self, $file) { return scalar(@ls); } -sub _follow_link($self, $file) { - my ($dir, $name) = $file =~ m{(.*)/(.*)}; - if (!defined $self->{_is_link}->{$dir} ) { - my ($out,$err) = $self->run_command("stat", $dir ); - chomp $out; - $out =~ m{ -> (/.*)}; - $self->{_is_link}->{$dir} = $1; - } - my $path = $self->{_is_link}->{$dir}; - return $file if !$path; - return "$path/$name"; - -} - sub _wait_storage($self, $sub) { my @ret; for ( 1 .. 10 ) { @@ -720,6 +703,14 @@ 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); + return if !$sp; + + $sp->destroy if $sp->is_active; + $sp->undefine(); +} + sub _create_default_pool($self, $vm=$self->vm) { my $dir = "/var/lib/libvirt/images"; mkdir $dir if ! -e $dir; diff --git a/lib/Ravada/VM/Void.pm b/lib/Ravada/VM/Void.pm index 08f6b6c01..1cee3f0c7 100644 --- a/lib/Ravada/VM/Void.pm +++ b/lib/Ravada/VM/Void.pm @@ -352,7 +352,7 @@ sub search_volume($self, $pattern) { return; } -sub _list_used_volumes($self) { +sub list_used_volumes($self) { my @disk; for my $domain ($self->list_domains) { push @disk,($domain->list_disks()); @@ -363,32 +363,33 @@ sub _list_used_volumes($self) { return @disk } -sub _list_volumes($self) { +sub _list_volumes_sp($self, $sp) { die "Error: TODO remote!" if !$self->is_local; + confess if !defined $sp; + my $dir = $sp->{path} or die "Error: unknown path ".Dumper($sp); + return if ! -e $dir; + my @vol; - opendir my $ls,$self->dir_img or die $!; - my $dir = $self->dir_img; + opendir my $ls,$dir or die "$! $dir"; while (my $file = readdir $ls) { - push @vol,("$dir/$file"); + push @vol,("$dir/$file") if -f "$dir/$file"; } closedir $ls; + return @vol; } -sub list_unused_volumes($self) { - die "Error: TODO remote!" if !$self->is_local; - my %used = map { $_ => 1 } $self->_list_used_volumes(); - my @unused; - for my $vol ( sort $self->_list_volumes ) { - next if ! -f $vol; - next if $vol =~ m{/\..*yml$}; - - push @unused,($vol) unless $used{$vol}; +sub list_volumes($self) { + my @volumes; + for my $sp ($self->list_storage_pools(1)) { + for my $vol ( $self->_list_volumes_sp($sp) ) { + push @volumes,($vol); + } } - return @unused; + return @volumes; } sub _search_volume_remote($self, $pattern) { @@ -648,6 +649,19 @@ sub active_storage_pool($self, $name, $value) { $self->write_file($file_sp, Dump( \@list)); } +sub remove_storage_pool($self, $name) { + die "TODO remote VM" unless $self->is_local; + + my $file_sp = $self->dir_img."/.storage_pools.yml"; + my $sp_list = LoadFile($file_sp); + my @sp2; + for my $sp (@$sp_list) { + push @sp2,($sp) if $sp->{name} ne $name; + } + + $self->write_file($file_sp, Dump( \@sp2)); +} + #########################################################################3 1; diff --git a/t/lib/Test/Ravada.pm b/t/lib/Test/Ravada.pm index d693d7232..0f62448f6 100644 --- a/t/lib/Test/Ravada.pm +++ b/t/lib/Test/Ravada.pm @@ -1023,7 +1023,12 @@ sub _activate_storage_pools($vm) { next if $sp->is_active; next unless $sp->get_name =~ /^tst_/; diag("Activating sp ".$sp->get_name." on ".$vm->name); - $sp->build() unless $sp->is_active; + my $xml = XML::LibXML->load_xml(string => $sp->get_xml_description()); + my ($path) = $xml->findnodes('/pool/target/path'); + my $dir = $path->textContent(); + mkdir $dir or die "$! $dir" if ! -e $dir; + + $sp->build(); $sp->create() unless $sp->is_active; } } @@ -1426,7 +1431,7 @@ sub _qemu_storage_pool { } sub remove_qemu_pools($vm=undef) { - return if !$VM_VALID{'KVM'} || $>; + return if !$vm && (!$VM_VALID{'KVM'} || $>); return if defined $vm && $vm->type eq 'Void'; if (!defined $vm) { eval { $vm = rvd_back->search_vm('KVM') }; @@ -1441,18 +1446,22 @@ sub remove_qemu_pools($vm=undef) { my $base = base_pool_name(); $vm->connect(); - for my $pool ( Ravada::VM::KVM::_list_storage_pools($vm->vm)) { + for my $pool ( $vm->vm->list_all_storage_pools) { my $name = $pool->get_name; + next if $name !~ qr/^$base/; + diag($name); + eval {$pool->build(Sys::Virt::StoragePool::BUILD_NEW); $pool->create() }; warn $@ if $@ && $@ !~ /already active/; - next if $name !~ qr/^$base/; - diag("Removing ".$vm->name." storage_pool ".$pool->get_name); - for my $vol ( $pool->list_volumes ) { - diag("Removing ".$pool->get_name." vol ".$vol->get_name); - $vol->delete(); + if ($pool->is_active) { + diag("Removing ".$vm->name." storage_pool ".$pool->get_name); + for my $vol ( $pool->list_volumes ) { + diag("Removing ".$pool->get_name." vol ".$vol->get_name); + $vol->delete(); + } } _delete_qemu_pool($pool); - $pool->destroy(); + $pool->destroy() if $pool->is_active; eval { $pool->undefine() }; warn $@ if $@; warn $@ if$@ && $@ !~ /libvirt error code: 49,/; diff --git a/t/storage_list_unused.t b/t/storage_list_unused.t index a6ae69460..ead72aba0 100644 --- a/t/storage_list_unused.t +++ b/t/storage_list_unused.t @@ -14,7 +14,6 @@ use Test::Ravada; no warnings "experimental::signatures"; use feature qw(signatures); -my @FILES; ######################################################################## sub _new_file($vm) { @@ -27,14 +26,14 @@ sub _new_file($vm) { return $file; } -sub test_links($vm, $machine) { +sub test_links($vm) { + my $machine = _create_clone($vm); my $dir = $vm->dir_img(); my ($vol) = $machine->list_volumes(); my ($file) = $vol =~ m{.*/(.*)}; my $dst = "/var/tmp/$file"; unlink $dst or die "$! $dst" if -e $dst; - push @FILES,($dst); copy($vol,$dst) or die "$! $vol -> $dst"; unlink $vol or die "$! $vol"; @@ -59,6 +58,7 @@ sub test_links($vm, $machine) { ok(!$found,"Expecting $vol not found") or die Dumper([$machine->list_volumes]); + remove_domain($machine); } sub test_links_dir($vm, $machine) { @@ -67,19 +67,16 @@ sub test_links_dir($vm, $machine) { my ($vol) = $machine->list_volumes(); my ($file) = $vol =~ m{.*/(.*)}; my $dir_dst = "/var/tmp/".new_domain_name(); + remove_dir($dir_dst); mkdir $dir_dst if ! -e $dir_dst; - push @FILES,($dir_dst); - my $dir_link = "/var/tmp/".new_domain_name(); my $file_link = "$dir_link/$file"; - push @FILES,($file_link); unlink($dir_link) or die "$! $dir_link" if -e $dir_link; symlink($dir_dst, $dir_link) or die "$! $dir_dst -> $dir_link"; - push @FILES,($dir_link); my $dst = "$dir_dst/$file"; copy($vol,$dst) or die "$vol -> $dst"; @@ -118,6 +115,8 @@ sub test_links_dir($vm, $machine) { ok(!$found,"Expecting $exp not found") or die Dumper([$machine->list_volumes]); } + remove_dir($dir_dst); + remove_dir($dir_link); } @@ -131,11 +130,16 @@ sub test_list_unused_discover($vm, $machine) { ); $sth->execute($machine->id); + my %used = map { $_ => 1 } $vm->list_used_volumes(); + for my $vol (@volumes) { + ok($used{$vol}) or die Dumper([sort keys %used]); + } + my $req = Ravada::Request->list_unused_volumes( uid => user_admin->id ,id_vm => $vm->id ,start => 0 - ,limit => 1000 + ,limit => 0 ); wait_request(); my $out_json = $req->output; @@ -197,18 +201,12 @@ sub test_list_unused($vm, $machine, $hidden_vols) { my $dir = $vm->dir_img(); my $file = _new_file($vm); - push @FILES,($file); - my $new_dir = $dir."/".new_domain_name(); - push @FILES,($new_dir); if (! -e $new_dir) { mkdir $new_dir or die "$! $new_dir"; } - open my $out,">",$file or die "$! $file"; - print $out "hi\n"; - close $out; $vm->refresh_storage(); my $req = Ravada::Request->list_unused_volumes( @@ -235,6 +233,8 @@ sub test_list_unused($vm, $machine, $hidden_vols) { ok(!$found_dir,"Expecting not found $new_dir"); _test_vm($vm, $machine, $output); + unlink $file; + remove_dir($new_dir); } sub test_page($vm) { @@ -300,30 +300,6 @@ sub _used_volumes($machine) { return @used; } -sub _clean_files($files=\@FILES) { - my @dirs; - for my $file (@$files) { - if (-f $file || -l $file) { - unlink $file or warn "$! $file"; - } elsif (-d $file) { - push @dirs,($file); - } - - } - - for my $file (@dirs) { - my @files; - my $pattern = base_domain_name(); - opendir my $ls,$file or die "$! $file"; - while (my $in = readdir $ls) { - push @files,("$file/$in") if $in =~ /^$pattern/; - } - _clean_files(\@files); - rmdir($file) or warn "$! $file"; - } - -} - sub _create_clone($vm) { my $base0 = create_domain($vm); $base0->prepare_base(user_admin); @@ -361,7 +337,9 @@ sub _create_clone_hide_bs($clone) { return $clone2; } -sub test_remove($vm, $clone) { +sub test_remove($vm) { + my $clone = _create_clone($vm); + my $file = _new_file($vm); ok(-e $file) or exit; my $user = create_user(new_domain_name(),"bar"); @@ -435,6 +413,247 @@ sub test_more($vm) { ok(!$more); } +sub test_linked_sp_here($vm) { + diag("test_linked_sp_heare"); + my $new_name1=new_domain_name(); + + my $new_dir = "/var/tmp/".$new_name1; + remove_dir($new_dir); + mkdir $new_dir or die "$! $new_dir"; + $vm->create_storage_pool($new_name1, $new_dir) + if !grep { $_->{name} eq $new_name1} $vm->list_storage_pools(1); + + my $new_name2 = new_domain_name(); + my $new_link2 = "/var/tmp/".$new_name2; + remove_dir($new_link2); + symlink($new_dir,$new_link2) or die "$new_dir -> $new_link2"; + + $vm->create_storage_pool($new_name2, $new_link2) + if !grep { $_ eq $new_name2} $vm->list_storage_pools; + + my $file = _touch_file($vm, $new_dir); + + $vm->refresh_storage_pools(); + + my $req = Ravada::Request->list_unused_volumes( + uid => user_admin->id + ,id_vm => $vm->id + ,limit => 0 + ); + wait_request(); + my $out_json = $req->output; + $out_json = '{}' if !defined $out_json; + my $output = decode_json($out_json); + + my $list = $output->{list}; + my @found = grep ($_->{file} =~ /^$new_dir/, @$list); + is( scalar(@found),1, "Found something ".Dumper(\@found)); + + remove_dir($new_dir); + remove_dir($new_link2); + $vm->remove_storage_pool($new_name1); + $vm->remove_storage_pool($new_name2); +} + +sub test_linked_sp($vm) { + diag("test linked sp"); + + my $dir = $vm->dir_img(); + my $new_name=new_domain_name(); + + my $new_dir = "/var/tmp/".$new_name; + remove_dir($new_dir); + + symlink($dir,$new_dir) or die "$dir -> $new_dir"; + + $vm->create_storage_pool($new_name, $new_dir) + if !grep { $_ eq $new_name} $vm->list_storage_pools; + + my $new_filename = _touch_file($vm, $dir); + + if ($vm->type eq 'KVM') { + my $pool = $vm->vm->get_storage_pool_by_name($new_name); + $pool->create() if !$pool->is_active; + $pool->refresh(); + } + + my $req = Ravada::Request->list_unused_volumes( + uid => user_admin->id + ,id_vm => $vm->id + ,limit => 0 + ); + wait_request(); + my $out_json = $req->output; + $out_json = '{}' if !defined $out_json; + my $output = decode_json($out_json); + + my $list = $output->{list}; + my @found = grep ($_->{file} =~ /$new_filename/, @$list); + is( scalar(@found),1) or die Dumper(\@found); + + $vm->remove_storage_pool($new_name); + unlink $new_dir; + unlink "$dir/$new_filename" or die $!; +} + +sub remove_dir($dir) { + if ( -l $dir || -f $dir ) { + unlink $dir or die "$! $dir"; + return; + } + my $base = base_domain_name; + if (-d $dir) { + opendir my $in,$dir or die "$! $dir"; + while (my $file = readdir $in) { + next if $file =~ /^\.+$/; + die "I will not delete $dir/$file" if $file !~ /^$base/; + my $path = "$dir/$file"; + remove_dir($path); + } + closedir $in; + rmdir $dir or die "$! $dir"; + } else { + die "I don't know what is $dir" if -e $dir; + } +} +sub _touch_file($vm, $dir) { + my $new_filename = new_domain_name(); + my $new_file ="$dir/$new_filename"; + open my $out,">",$new_file or die "$! $new_file"; + close $out; + $vm->refresh_storage_pools(); + + return $new_filename; +} + +sub test_linked_sp_level2($vm) { + my $dir = $vm->dir_img(); + my $new_name=new_domain_name(); + + my $new_dir = "/var/tmp/".$new_name; + + remove_dir($new_dir); + mkdir $new_dir or die "$! $new_dir"; + + my $new_link = "/run/user"; + $new_link .= "/$<" if $<; + $new_link .= "/".new_domain_name(); + + remove_dir($new_link); + symlink($new_dir,$new_link) or die "$new_dir -> $new_link"; + my $new_link0 = $new_link; + + $new_link .="/".new_domain_name(); + symlink($dir, $new_link) or die "$! $dir -> $new_link"; + + my $followed = $vm->_follow_link($new_link); + is($followed,$dir) or die "Expecting followed link matches"; + + $vm->create_storage_pool($new_name, $new_link) + if !grep { $_ eq $new_name} $vm->list_storage_pools; + + my $new_filename = _touch_file($vm, $new_link); + + my $req = Ravada::Request->list_unused_volumes( + uid => user_admin->id + ,id_vm => $vm->id + ,limit => 0 + ); + wait_request(); + my $out_json = $req->output; + $out_json = '{}' if !defined $out_json; + my $output = decode_json($out_json); + + my $list = $output->{list}; + my @found = grep ($_->{file} =~ /$new_filename$/, @$list); + is( scalar(@found),1); + + $vm->remove_storage_pool($new_name); + unlink "$dir/$new_filename" or die $!; + unlink "$new_link/$new_filename" or warn $!if -e "$new_link/$new_filename"; + remove_dir($new_dir); + remove_dir($new_link0); + remove_dir($new_link); +} + +sub test_linked_sp_level0($vm) { + return if $<; + + my $dir = $vm->dir_img(); + my $new_name = new_domain_name(); + my $new_link = "/".$new_name; + unlink $new_link if -e $new_link; + + symlink($dir,$new_link) or die "$dir -> $new_link"; + + my $followed = $vm->_follow_link($new_link); + is($followed,$dir) or die "Expecting followed link matches"; + + $vm->create_storage_pool($new_name, $new_link) + if !grep { $_ eq $new_name} $vm->list_storage_pools; + + my $new_filename = _touch_file($vm, $dir); + + if ($vm->type eq 'KVM') { + my $pool = $vm->vm->get_storage_pool_by_name($new_name); + $pool->create() if !$pool->is_active; + $pool->refresh(); + } + + my $req = Ravada::Request->list_unused_volumes( + uid => user_admin->id + ,id_vm => $vm->id + ,limit => 0 + ); + wait_request(); + my $out_json = $req->output; + $out_json = '{}' if !defined $out_json; + my $output = decode_json($out_json); + + my $list = $output->{list}; + my @found = grep ($_->{file} =~ /$new_filename$/, @$list); + is( scalar(@found),1) or die "Expecting 1 $new_filename on $dir or $new_link"; + + $vm->remove_storage_pool($new_name); + unlink($new_link) or die $!; + unlink("$dir/$new_filename") or die $!; +} + +sub _check_leftovers($vm, $delete=0) { + my $dir_run = "/run/user"; + $dir_run .= "/$<" if $<; + + my $base = base_domain_name(); + for my $dir ($vm->dir_img, "/var/tmp",$dir_run) { + opendir my $ls,$dir or die "$! $dir"; + while (my $file=readdir $ls) { + next if $file =~ /.(yml|void|lock|pid|qcow2)$/; + if ( $file =~ /$base/ ) { + if ($delete) { + remove_dir("$dir/$file"); + } else { + confess "$dir/$file"; + } + } + } + } + for my $sp ( $vm->list_storage_pools(1)) { + confess "SP found ".$sp->{name} if $sp->{name} =~ /$base/; + } +} + +sub _clean_old_sps($vm) { + remove_qemu_pools($vm) if $vm->type eq 'KVM'; + 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; + } + } +} + ######################################################################## init(); @@ -454,28 +673,48 @@ for my $vm_name ( vm_names() ) { diag($msg) if !$vm; skip $msg,10 if !$vm; + _clean_old_sps($vm); + + _check_leftovers($vm,1); + my $clone = _create_clone($vm); my @hidden_bs = _create_clone_hide_bs($clone); + _check_leftovers($vm); + + test_linked_sp($vm); + _check_leftovers($vm); + test_linked_sp_here($vm); + _check_leftovers($vm); + test_linked_sp_level0($vm); + _check_leftovers($vm); + + test_linked_sp_level2($vm); + _check_leftovers($vm); test_list_unused_discover($vm, $clone); test_list_unused_discover2($vm); + _check_leftovers($vm); test_list_unused($vm, $clone, \@hidden_bs); - + _check_leftovers($vm); test_links_dir($vm, $clone); - test_links($vm, $clone); + _check_leftovers($vm); + test_links($vm); + _check_leftovers($vm); test_page($vm); - test_remove($vm, $clone); + test_remove($vm); test_remove_many($vm); test_more($vm); + remove_domain($clone); + + _check_leftovers($vm); } } -_clean_files(); end(); done_testing(); diff --git a/t/vm/s30_storage.t b/t/vm/s30_storage.t index a13f2de23..27409f1ba 100644 --- a/t/vm/s30_storage.t +++ b/t/vm/s30_storage.t @@ -49,7 +49,7 @@ sub _add_fstab($vm) { } sub remove_fstab($vm, $dir) { - my $file = "$dir/check_storage";# or die "$!"; + my $file = "$dir/".base_domain_name()."check_storage";# or die "$!"; unlink $file or die "$! $file" if -e $file; copy("/etc/fstab.tst_rvd_backup","/etc/fstab") } @@ -82,7 +82,7 @@ sub test_storage_pools_fail($vm) { sub _clean_local { my $dir = "$DIR/".base_domain_name(); - my $file = "$dir/check_storage";# or die "$!"; + my $file = "$dir/".base_domain_name()."_check_storage";# or die "$!"; unlink $file or die "$! $file" if -e $file; } From 59176eb09929d3561c4f301b06a5fe3b68abb592 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 28 Sep 2023 09:32:30 +0200 Subject: [PATCH 13/17] wip: fixed anonymous login --- lib/Ravada/Front.pm | 10 ++++++---- public/js/ravada.js | 6 +++++- t/mojo/40_anonymous.t | 6 ++++++ templates/bootstrap/navigation.html.ep | 6 +++++- templates/main/list_bases_ng.html.ep | 5 +---- 5 files changed, 23 insertions(+), 10 deletions(-) diff --git a/lib/Ravada/Front.pm b/lib/Ravada/Front.pm index 470a2f60c..82a393048 100644 --- a/lib/Ravada/Front.pm +++ b/lib/Ravada/Front.pm @@ -1203,17 +1203,18 @@ sub list_bases_anonymous { my $net = Ravada::Network->new(address => $ip); my $sth = $CONNECTOR->dbh->prepare( - "SELECT id, name, id_base, is_public, file_screenshot " + "SELECT id, alias, name, id_base, is_public, file_screenshot " ."FROM domains where is_base=1 " ."AND is_public=1"); $sth->execute(); - my ($id, $name, $id_base, $is_public, $screenshot); - $sth->bind_columns(\($id, $name, $id_base, $is_public, $screenshot)); + my ($id, $alias, $name, $id_base, $is_public, $screenshot); + $sth->bind_columns(\($id, $alias, $name, $id_base, $is_public, $screenshot)); my @bases; while ( $sth->fetch) { next if !$net->allowed_anonymous($id); my %base = ( id => $id, name => Encode::decode_utf8($name) + , alias => Encode::decode_utf8($alias or $name) , is_public => ($is_public or 0) , screenshot => ($screenshot or '') , is_active => 0 @@ -1221,8 +1222,9 @@ sub list_bases_anonymous { , name_clone => undef , is_locked => undef , can_hibernate => 0 + ); - $base{screenshot} =~ s{^/var/www}{}; + $base{list_clones} = []; lock_hash(%base); push @bases, (\%base); } diff --git a/public/js/ravada.js b/public/js/ravada.js index ced2a5f14..1aee9c160 100644 --- a/public/js/ravada.js +++ b/public/js/ravada.js @@ -159,7 +159,11 @@ if (machine.clone) { id=machine.clone.id; } - window.location.assign('/machine/view/' + id + '.html'); + var url_view = '/machine/view/' + id + '.html'; + if ($scope.anonymous) { + url_view = "/anonymous/"+id+".html"; + } + window.location.assign(url_view); } else { window.location.assign('/machine/clone/' + machine.id + '.html'); } diff --git a/t/mojo/40_anonymous.t b/t/mojo/40_anonymous.t index 6af9f8c8c..88fa75bc9 100644 --- a/t/mojo/40_anonymous.t +++ b/t/mojo/40_anonymous.t @@ -99,6 +99,12 @@ $t->get_ok("/anonymous"); is($t->tx->res->code(), 200 ) or exit; is(list_anonymous_users(), $n_anonymous + 1); +my $bases = rvd_front->list_bases_anonymous('127.0.0.1'); +ok($bases->[0]->{alias}); +ok($bases->[0]->{list_clones}); +my $url_view = "/anonymous/".$bases->[0]->{id}.".html"; +$t->get_ok($url_view) or exit; + _deny_anonymous_base(); $t->get_ok("/logout"); diff --git a/templates/bootstrap/navigation.html.ep b/templates/bootstrap/navigation.html.ep index c43e2f38e..81459d664 100644 --- a/templates/bootstrap/navigation.html.ep +++ b/templates/bootstrap/navigation.html.ep @@ -5,7 +5,11 @@ navbar-custom \ % } navbar-dark bg-dark fixed-top navbar-expand-lg navbar-inverse"> - RAVADA VDI +% if ($_user && $_anonymous ) { + Ravada VDI +% } else { + Ravada VDI +% } diff --git a/templates/main/list_bases_ng.html.ep b/templates/main/list_bases_ng.html.ep index d40c6a49b..2571c49e2 100644 --- a/templates/main/list_bases_ng.html.ep +++ b/templates/main/list_bases_ng.html.ep @@ -4,9 +4,6 @@
%= include 'bootstrap/navigation' -
%= include 'main/list_bases_ng_head' @@ -119,7 +116,7 @@ ng-click="restore(machine.clone.id);host_restore=0;machine.action=false" ><%=l 'Yes' %>
-% if ($user +% if ($user && !$_anonymous % && ( $user->can_change_settings || $user->can_change_settings_all)){ From 16b48f5ef082e29793fa5f94a19e54f93ba25935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20K=C3=B6hler?= Date: Wed, 25 Oct 2023 07:51:04 +0200 Subject: [PATCH 14/17] Some more german translations (#2002) --- lib/Ravada/I18N/de.po | 318 +++++++++++++++++++++--------------------- 1 file changed, 159 insertions(+), 159 deletions(-) diff --git a/lib/Ravada/I18N/de.po b/lib/Ravada/I18N/de.po index 02ba13f6e..7dbe61095 100644 --- a/lib/Ravada/I18N/de.po +++ b/lib/Ravada/I18N/de.po @@ -1235,7 +1235,7 @@ msgid "Exec clones sequentially" msgstr "" msgid "Exec. time" -msgstr "" +msgstr "Ausführzeit" msgid "Fail" msgstr "Fehlgeschlagen" @@ -1250,58 +1250,58 @@ msgid "For Spice client setup follow this " msgstr "" msgid "Force Reboot" -msgstr "" +msgstr "Neustart erzwingen" msgid "Force ShutDown" -msgstr "" +msgstr "Herunterfahren erzwingen" msgid "Force change password on first access" -msgstr "" +msgstr "Erzwinge Passwortänderung beim ersten Zugriff" msgid "From" -msgstr "" +msgstr "Von" msgid "Frontend" -msgstr "" +msgstr "Frontend" msgid "Global Settings" -msgstr "" +msgstr "Globale Einstellungen" msgid "Group" -msgstr "" +msgstr "Gruppe" msgid "Group name" -msgstr "" +msgstr "Gruppenname" msgid "Group name can not exceed 80 characters" -msgstr "" +msgstr "Gruppenname darf nicht länger als 80 Zeichen sein" msgid "Group name is required" -msgstr "" +msgstr "Gruppenname erforderlich" msgid "Groups" -msgstr "" +msgstr "Gruppen" msgid "Groups require a LDAP server configured." -msgstr "" +msgstr "Gruppen erfordern einen konfigurierten LDAP Server" msgid "Groups with permissions" -msgstr "" +msgstr "Gruppen mit Rechten" msgid "Hardware" -msgstr "" +msgstr "Hardware" msgid "Hardware address" -msgstr "" +msgstr "Hardwareadresse" msgid "Hibernated" -msgstr "" +msgstr "Im Ruhezustand" msgid "Hide active" -msgstr "" +msgstr "Aktive ausblenden" msgid "Hide clones" -msgstr "" +msgstr "Klone ausblenden" msgid "I can't find" msgstr "" @@ -1310,286 +1310,286 @@ msgid "I don't know settings tabs for:" msgstr "" msgid "ISO file" -msgstr "" +msgstr "Datenträgeabbild" msgid "If you have any issue, you can contact the Suport crew and we will try to help you. Please, only contact Support Center if it is strictly necessary." -msgstr "" +msgstr "Bei Problemen, kontaktieren Sie bitte unseren Support. Wir bitten Sie von unnötigen Anfragen ab." msgid "Import" -msgstr "" +msgstr "Importieren" msgid "Internal IP" -msgstr "" +msgstr "Interne IP" msgid "Invalid IP network address. Expecting a.b.c.d/e" -msgstr "" +msgstr "Ungültige Netzwerkadresse. Erwartet: a.b.c.d/e" msgid "Invalid Template" -msgstr "" +msgstr "Ungültige Vorlage" msgid "Keep the CD for the clones" -msgstr "" +msgstr "CD für Klone behalten" msgid "LDAP" -msgstr "" +msgstr "LDAP" msgid "LDAP groups are required to set up bookings. Some groups where found but no members belong to them. Add new entries here." -msgstr "" +msgstr "LDAP Gruppen werden benötigt, um Reservierungen zu aktivieren. Es wurden Gruppen ohne Mitglieder gefunden. Neuer Eintrag." msgid "Legacy" -msgstr "" +msgstr "Legacy" msgid "Loading ..." -msgstr "" +msgstr "Lade ..." msgid "Loading machine" -msgstr "" +msgstr "Lade Maschine" msgid "Loading nodes" -msgstr "" +msgstr "Lade Knoten" msgid "Machine" -msgstr "" +msgstr "Maschine" msgid "Machine Information" -msgstr "" +msgstr "Maschineninformationen" msgid "Machines Notifications" -msgstr "" +msgstr "Maschinenbenachrichtigungen" msgid "Maintenance" -msgstr "" +msgstr "Wartung" msgid "Maintenance End" -msgstr "" +msgstr "Ende der Wartung" msgid "Maintenance Start" -msgstr "" +msgstr "Beginn der Wartung" msgid "Manage machine" -msgstr "" +msgstr "Maschine verwalten" msgid "Max Memory" -msgstr "" +msgstr "Max. Speicher" msgid "Max memory (MB)" -msgstr "" +msgstr "Max. Speicher (MB)" msgid "Minimum port number to expose virtual machine services." -msgstr "" +msgstr "Kleinste Port-Nummer zur Veröffentlichung von VM Diensten" msgid "Monitoring" -msgstr "" +msgstr "Überwachung" msgid "NAT" -msgstr "" +msgstr "NAT" msgid "Network" -msgstr "" +msgstr "Netzwerk" msgid "Network address is required." -msgstr "" +msgstr "Netzwerkadresse ist erforderlich" msgid "Network name is required" -msgstr "" +msgstr "Netzwerkname ist erforderlich" msgid "Networks" -msgstr "" +msgstr "Netzwerke" msgid "New Booking" -msgstr "" +msgstr "Neue Reservierung" msgid "New Group" -msgstr "" +msgstr "Neue Gruppe" msgid "New Network" -msgstr "" +msgstr "Neues Netzwerk" msgid "New Password can only contain words and numbers" -msgstr "" +msgstr "Neues Passwort darf nur aus Buchstaben und Zahlen bestehen" msgid "New Password is required" -msgstr "" +msgstr "Neues Passwort ist erforderlich" msgid "New Password must be at least 6 characters" -msgstr "" +msgstr "Neues Passwort muss mindestens 6 Zeichen lang sein" msgid "New feature" -msgstr "" +msgstr "Neues Feature" msgid "New group member" -msgstr "" +msgstr "Neues Gruppenmitglied" msgid "New name" -msgstr "" +msgstr "Neuer Name" msgid "New network" -msgstr "" +msgstr "Neues Netzwerk" msgid "New password" -msgstr "" +msgstr "Neues Passwort" msgid "New user" -msgstr "" +msgstr "Neuer Benutzer" msgid "No LDAP groups created." -msgstr "" +msgstr "Keine LDAP Gruppen erstellt." msgid "No Results Found" -msgstr "" +msgstr "Keine Ergebnisse gefunden" msgid "No bases found" msgstr "" msgid "No bridges found" -msgstr "" +msgstr "Keine Brücken gefunden" msgid "No groups found" -msgstr "" +msgstr "Keine Gruppen gefunden" msgid "No machines" -msgstr "" +msgstr "Keine Maschinen" msgid "No machines found" -msgstr "" +msgstr "Keine Maschinen gefunden" msgid "No members found" -msgstr "" +msgstr "Keine Mitglieder gefunden" msgid "No message to show!" -msgstr "" +msgstr "Keine Nachricht zum Anzeigen!" msgid "No, cancel" -msgstr "" +msgstr "Nein, Abbrechen" msgid "Node" -msgstr "" +msgstr "Knoten" msgid "Node disabled" -msgstr "" +msgstr "Knoten deaktiviert" msgid "Node down" -msgstr "" +msgstr "Knote offline" msgid "Node with machines from the same user" -msgstr "" +msgstr "Knoten mit Maschinen des selben Nutzers" msgid "Node with more free memory" -msgstr "" +msgstr "Knoten mit freiem Speicher" msgid "Nodes" -msgstr "" +msgstr "Knoten" msgid "Nothing to monitoring" -msgstr "" +msgstr "Nichts zu überwachen" msgid "Number of virtual machines that normal users can have running at the same time" -msgstr "" +msgstr "Max. Anzahl virtueller Maschinen, die ein Nutzer gleichzeitig starten kann" msgid "Old Password can only contain words and numbers" -msgstr "" +msgstr "Altes Passwort darf nur aus Buchstaben und Zahlen bestehen" msgid "Old Password is required" -msgstr "" +msgstr "Altes Passwort ist erforderlich" msgid "Old and New Passwords match!" -msgstr "" +msgstr "Altes und neues Passwort stimmen überein!" msgid "Only remote client can access this port" -msgstr "" +msgstr "Nur Remote Clients können diesen Port verwenden" msgid "Only remote client can access this port if restricted" -msgstr "" +msgstr "Nur Remote Clients können diesen Port verwenden, wenn eingeschränkt" msgid "Only users from these groups will be allowed to execute this machine" -msgstr "" +msgstr "Nur Mitglieder dieser Gruppe dürfen diese Maschine ausführen" msgid "Oops!" -msgstr "" +msgstr "Ups!" msgid "Open ports" -msgstr "" +msgstr "Offene Ports" msgid "Password and their confirmation do not match!" -msgstr "" +msgstr "Passwort und Passwortwiederholung stimmen nicht überein!" msgid "Passwords do not match!" -msgstr "" +msgstr "Passwörter stimmen nicht überein" msgid "Permission granted to user" -msgstr "" +msgstr "Rechte wurden Benutzer hinzugefügt" msgid "Permission revoked from user" -msgstr "" +msgstr "Rechte wurden Benutzer entzogen" msgid "Phone" -msgstr "" +msgstr "Telefon" msgid "Phone Number" -msgstr "" +msgstr "Telefonnummer" msgid "Please insert" -msgstr "" +msgstr "Bitte eingeben" msgid "Please select an ISO file" -msgstr "" +msgstr "Bitte ISO-Datei auswählen" msgid "Please, make sure you have the right path and release, according to your PC configuration." msgstr "" msgid "Pool" -msgstr "" +msgstr "Pool" msgid "Port" -msgstr "" +msgstr "Port" msgid "Port expose" -msgstr "" +msgstr "Öffentlicher Port" msgid "Ports" -msgstr "" +msgstr "Ports" msgid "Poweroff" -msgstr "" +msgstr "Ausschalten" msgid "Prepare Base" msgstr "" msgid "Prepare this machine as a base to create clones from it." -msgstr "" +msgstr "Diese Maschine als Vorlage vorbereiten, um Klone damit zu erstellen." msgid "PrepareBase" msgstr "" msgid "Press SHIFT + F12 to exit the virtual machine" -msgstr "" +msgstr "Umschalt + F12 drücken, um Maus und Tastatur freizugeben" msgid "Primary can not be unset, enable it in another video device" msgstr "" msgid "Primary video devices will move to first" -msgstr "" +msgstr "Primäres Videogerät wird an die erste Stelle verschoben" msgid "Public" -msgstr "" +msgstr "Öffentlich" msgid "Public Port" -msgstr "" +msgstr "Öffentlicher Port" msgid "Purge" -msgstr "" +msgstr "Löschen" msgid "Purge disk volumes" -msgstr "" +msgstr "Festplatte löschen" msgid "RAM (Gb)" -msgstr "" +msgstr "RAM (Gb)" msgid "Ravada Apache documentation" -msgstr "" +msgstr "Ravada Apache Dokumentation" msgid "Re-Enter New Password" -msgstr "" +msgstr "Neues Passwort erneut eigeben" msgid "Rebase" msgstr "" @@ -1601,118 +1601,118 @@ msgid "Rebase this machine to another virtual machine" msgstr "" msgid "Reboot" -msgstr "" +msgstr "Neustart" msgid "Recent requests" -msgstr "" +msgstr "Letzte Anfragen" msgid "Refresh" -msgstr "" +msgstr "Aktualisieren" msgid "Refresh ports" -msgstr "" +msgstr "Ports aktualisieren" msgid "Reload" -msgstr "" +msgstr "Neu laden" msgid "Remove Base" msgstr "" msgid "Remove Clones" -msgstr "" +msgstr "Klone löschen" msgid "Remove bases in node" msgstr "" msgid "Remove group" -msgstr "" +msgstr "Gruppe entfernen" msgid "Remove the base prepared from this machine" msgstr "" msgid "Removing Virtual Machine" -msgstr "" +msgstr "Entferne virtuelle Maschine" msgid "Repeat" -msgstr "" +msgstr "Wiederholen" msgid "Repeated New Password is required" -msgstr "" +msgstr "Wiederholtes, neues Passwort ist erforderlich" msgid "Request" -msgstr "" +msgstr "Anfrage" msgid "Requests" -msgstr "" +msgstr "Anfragen" msgid "Requires viewer password when implemented" -msgstr "" +msgstr "Benötigt Anzeigepasswort, wenn implementiert" msgid "Restricted" -msgstr "" +msgstr "Eingeschränkt" msgid "Run Timeout" msgstr "" msgid "SSO/CAS Login" -msgstr "" +msgstr "SSO/CAS Login" msgid "Save" -msgstr "" +msgstr "Speichern" msgid "Save changes" -msgstr "" +msgstr "Änderungen speichern" msgid "Schedule" -msgstr "" +msgstr "Planen" msgid "Search group" -msgstr "" +msgstr "Gruppe suchen" msgid "See documentation about:" -msgstr "" +msgstr "Dokumentation prüfen für:" msgid "Select the .iso file the machine will utilize when installing the OS." -msgstr "" +msgstr "Wählen Sie eine ISO-Datei aus, welches das Betriebssystem enthält." msgid "Server in maintenance until" -msgstr "" +msgstr "Serverwartung bis" msgid "Set New Password" -msgstr "" +msgstr "Neues Passwort setzen" msgid "Set defaults" -msgstr "" +msgstr "Setze Standardwerte" msgid "Set either" msgstr "" msgid "Setup a LDAP Server" -msgstr "" +msgstr "LDAP Server einrichten" msgid "Show active" -msgstr "" +msgstr "Aktive anzeigen" msgid "Show clones" -msgstr "" +msgstr "Klone anzeigen" msgid "Show/Hide clones" -msgstr "" +msgstr "Klone anzeigen/verstecken" msgid "ShutDown" -msgstr "" +msgstr "Herunterfahren" msgid "Shutdown Timeout" -msgstr "" +msgstr "Herunterfahren Zeitüberschreitung" msgid "Shutdown disconnected" -msgstr "" +msgstr "Herunterfahren getrennt" msgid "Sorry" -msgstr "" +msgstr "Entschuldige" msgid "Source Machine" -msgstr "" +msgstr "Quellmaschine" msgid "Spinoff clone" msgstr "" @@ -1721,61 +1721,61 @@ msgid "Spinoff this clone from its base." msgstr "" msgid "Start Limit" -msgstr "" +msgstr "Startlimit" msgid "Start after create the virtual machine" -msgstr "" +msgstr "Nach erstellen starten" msgid "Start after migration" -msgstr "" +msgstr "Nach migration starten" msgid "Support Contact" -msgstr "" +msgstr "Supportkontakt" msgid "Swap" -msgstr "" +msgstr "Swap" msgid "System Disk: (GB)" -msgstr "" +msgstr "System Disk: (GB)" msgid "Template selection is required" -msgstr "" +msgstr "Auswahl einer Vorlage ist erforderlich" msgid "Testing connection to" -msgstr "" +msgstr "Teste verbindung zu" msgid "The Minimum Disk Size needed for this ISO is" -msgstr "" +msgstr "Minimale Festplattengröße für diese ISO beträgt" msgid "The Minimum Swap Disk Size needed for this ISO is" -msgstr "" +msgstr "Minimale Swap Größe für diese ISO beträgt" msgid "The VM is" -msgstr "" +msgstr "Die VM ist" msgid "The machine will power off after this minutes after shutdown." -msgstr "" +msgstr "Nach dem Herunterfahren wird die Maschine nach dieser Zeit ausgeschaltet" msgid "The machine will turn off after this time" -msgstr "" +msgstr "Die Maschine wird nach dieser Zeit ausgeschaltet" msgid "The new user" -msgstr "" +msgstr "Der neue Benutzer" msgid "The source machine is not a base. It must be prepared before it can be copied. This process may take some minutes." msgstr "" msgid "There are no LDAP groups defined." -msgstr "" +msgstr "Es wurden keine LDAP Gruppen definiert." msgid "There are no active virtual machines" -msgstr "" +msgstr "Es gibt keine aktiven virtuellen Maschinen" msgid "This ISO is being downloaded. The virtual machine will be created after." -msgstr "" +msgstr "Diese ISO wird gerade heruntergeladen. Die virtuelle Maschine wird danach gestartet." msgid "This Virtual Machine has no display hardware attached" -msgstr "" +msgstr "Diese virtuelle Maschine verfügt über kein angeschlossenes Display" msgid "This address is duplicated" msgstr "" From fb104687b0ad0982b9ad6131456d6e672ba9ecf5 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 26 Oct 2023 09:13:00 +0200 Subject: [PATCH 15/17] Feature upload users (#1970) feat: upload users information --- lib/Ravada/Auth/SQL.pm | 2 +- lib/Ravada/Front.pm | 58 ++++++ public/js/admin.js | 5 + script/rvd_back | 34 ++++ script/rvd_front | 58 +++++- t/mojo/60_upload.t | 230 ++++++++++++++++++++++ templates/bootstrap/new_user.html.ep | 4 + templates/main/manage_user.html.ep | 14 +- templates/main/manage_user_remove.html.ep | 20 ++ templates/main/standard_message.html.ep | 24 +++ templates/main/upload_users.html.ep | 87 ++++++++ 11 files changed, 533 insertions(+), 3 deletions(-) create mode 100644 t/mojo/60_upload.t create mode 100644 templates/main/manage_user_remove.html.ep create mode 100644 templates/main/standard_message.html.ep create mode 100644 templates/main/upload_users.html.ep diff --git a/lib/Ravada/Auth/SQL.pm b/lib/Ravada/Auth/SQL.pm index 82625e978..fb39df37e 100644 --- a/lib/Ravada/Auth/SQL.pm +++ b/lib/Ravada/Auth/SQL.pm @@ -141,7 +141,7 @@ sub add_user { ." VALUES(?,?,?,?,?,?)"); }; confess $@ if $@; - if ($password) { + if ($password && !$external_auth) { $password = sha1_hex($password); } else { $password = '*LK* no pss'; diff --git a/lib/Ravada/Front.pm b/lib/Ravada/Front.pm index 82a393048..6ea3f8ffe 100644 --- a/lib/Ravada/Front.pm +++ b/lib/Ravada/Front.pm @@ -1694,6 +1694,64 @@ sub _filter_active($pools, $active) { } +=head2 upload_users + +Upload a list of users to the database + +=head3 Arguments + +=over + +=item * string with users and passwords in each line + +=item * type: it can be SQL, LDAP or SSO + +=item * create: optionally create the entries in LDAP + +=back + +=cut + +sub upload_users($self, $users, $type, $create=0) { + + my @external; + if ($type ne 'sql') { + @external = ( is_external => 1, external_auth => $type ); + } + + my ($found,$count) = (0,0); + my @error; + for my $line (split /\n/,$users) { + my ($name, $password) = split(/:/,$line); + $found++; + my $user = Ravada::Auth::SQL->new(name => $name); + if ($user && $user->id) { + push @error,("User $name already added"); + next; + } + if ($type ne 'sql' && $create) { + if ($type eq 'ldap') { + if (!$password) { + push @error,("Error: user $name , password empty"); + next; + } + eval { $user = Ravada::Auth::LDAP::add_user($name,$password) }; + push @error, ($@) if $@; + } else { + push @error,("$type users can't be created from Ravada"); + } + } + if ($type eq 'sql' && !$password) { + push @error,("Error: user $name requires password"); + next; + } + Ravada::Auth::SQL::add_user(name => $name, password => $password + ,@external); + $count++; + } + return ($found, $count, \@error); +} + =head2 version Returns the version of the main module diff --git a/public/js/admin.js b/public/js/admin.js index 13181bdf7..aa43e9136 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -16,6 +16,7 @@ ravadaApp.directive("solShowMachine", swMach) .controller("settings_global", settings_global_ctrl) .controller("admin_groups", admin_groups_ctrl) .controller('admin_charts', admin_charts_ctrl) + .controller('upload_users', upload_users) ; ravadaApp.directive('ipaddress', function() { @@ -1624,4 +1625,8 @@ ravadaApp.directive("solShowMachine", swMach) }; + + function upload_users($scope, $http) { + $scope.type = 'sql'; + }; }()); diff --git a/script/rvd_back b/script/rvd_back index 13c5f390d..829db9bf3 100755 --- a/script/rvd_back +++ b/script/rvd_back @@ -16,6 +16,7 @@ use File::Path; use File::Basename; use Ravada; +use Ravada::Front; use Ravada::Auth::SQL; use Ravada::Auth::LDAP; use Ravada::Utils; @@ -35,6 +36,7 @@ my $FILE_CONFIG_DEFAULT = "/etc/ravada.conf"; my $FILE_CONFIG; my $ADD_USER_LDAP; +my $ADD_USER_FILE; my $ADD_GROUP_LDAP; my $RM_GROUP_LDAP; my $ADD_USER_GROUP; @@ -77,8 +79,12 @@ my $BACKUP; my $RESTORE; my $TIME_CONNECTION = 10; +my $TYPE; +my $CREATE; + my $USAGE = "$0 " ." [--debug] [--config=$FILE_CONFIG_DEFAULT] [--add-user=name] [--add-user-ldap=name]" + ." [--add-user-file=filename]" ." [--change-password] [--make-admin=username] [--import-vbox=image_file.vdi]" ." [--test-ldap] " ." [-X] [start|stop|status]" @@ -89,6 +95,7 @@ my $USAGE = "$0 " ."\n" ." --add-user : adds a new db user\n" ." --add-user-ldap : adds a new LDAP user\n" + ." --add-user-file: adds new users from a file\n" ." --remove-user : removes a db user\n" ." --add-group-ldap : creates a new LDAP group\n" ." --remove-group-ldap : removes a LDAP group\n" @@ -125,6 +132,9 @@ my $USAGE = "$0 " ." --log: saves STDOUT and STDERR app traces to the specified filename\n" ." --list-unused-volumes\n" ." --show-volume\n" + ."Create users modifiers\n" + ." --type : plain, ldap or sso. Defaults to plain\n" + ." --create : only valid for LDAP users, creates the entries.\n" ."\n" ; @@ -153,6 +163,7 @@ GetOptions ( help => \$help ,'remove-admin=s' => \$REMOVE_ADMIN_USER ,'change-password'=> \$CHANGE_PASSWORD ,'add-user-ldap=s'=> \$ADD_USER_LDAP + ,'add-user-file=s'=> \$ADD_USER_FILE ,'add-group-ldap=s'=> \$ADD_GROUP_LDAP ,'remove-group-ldap=s'=> \$RM_GROUP_LDAP ,'add-user-group=s'=> \$ADD_USER_GROUP @@ -177,6 +188,8 @@ GetOptions ( help => \$help ,"purge-nodes" => \$PURGE_NODES ,"time-connection=s" => \$TIME_CONNECTION + ,"type=s" => \$TYPE + ,"create" => \$CREATE ) or exit; $START = 1 if $DEBUG || $FILE_CONFIG || $NOFORK; @@ -344,6 +357,26 @@ sub add_user { , is_admin => $is_admin); } +sub add_user_file($ADD_USER_FILE) { + my $ravada = Ravada::Front->new(); + die "Error: File $ADD_USER_FILE does not exist\n" + if !-e $ADD_USER_FILE; + + open my $in,"<",$ADD_USER_FILE or die "$! $ADD_USER_FILE\n"; + my $users = join("",<$in>); + close $in; + + my ($found, $count, $error)=$ravada->upload_users($users, ($TYPE or 'sql'), $CREATE); + + print "$found found, $count added.\n"; + if (scalar (@$error)) { + print "Errors:\n"; + for (sort @$error) { + print " - $_\n"; + } + } +} + sub add_user_ldap { my $login = shift; @@ -1144,6 +1177,7 @@ if $HIBERNATE_DOMAIN || $SHUTDOWN_DOMAIN; add_user($ADD_USER) if $ADD_USER; add_user_ldap($ADD_USER_LDAP) if $ADD_USER_LDAP; +add_user_file($ADD_USER_FILE) if $ADD_USER_FILE; add_group_ldap($ADD_GROUP_LDAP) if $ADD_GROUP_LDAP; remove_group_ldap($RM_GROUP_LDAP) if $RM_GROUP_LDAP; add_user_group($rvd_back, $ADD_USER_GROUP) if $ADD_USER_GROUP; diff --git a/script/rvd_front b/script/rvd_front index 1c63b4303..0c7242576 100644 --- a/script/rvd_front +++ b/script/rvd_front @@ -1307,6 +1307,62 @@ any '/group/new' => sub { return new_group($c); }; +any '/admin/users/upload.#req' => sub($c) { + return access_denied_json($c) unless $USER->is_admin; + + _add_admin_libs($c); + + my $type = $c->req->param('type'); + + return $c->render(template => "/main/upload_users", done => 0, count => 0, found => 0, type => 'sql') if !$type; + + my $create = ( $c->req->param('create') or 0); + + return $c->render(json => { error => "Unknown type $type" }) + if $type !~ /^(sql|ldap|sso)/; + + my $csv = $c->req->upload('users'); + if($csv->headers->content_type !~ m{text/(csv|plain)}) { + return $c->render(status => 400 + ,text => "Wrong content type ".$csv->headers->content_type + ." , it should be text/csv or plain" + ); + } + + my ($found, $count, $error) = $RAVADA->upload_users( + $csv->slurp, $type, $create + ); + + return $c->render(json => + { output => "$count users added" + ,error => $error + }) if $c->stash('req') eq 'json'; + + return $c->render(template => "/main/upload_users" + ,count => $count + ,found => $found + ,error => $error + ,done => 1 + ); +}; + +get '/admin/user/remove/#id' => sub($c) { + return access_denied($c) unless $USER->is_admin; + my $id = $c->stash('id'); + my $user = Ravada::Auth::SQL->search_by_id($id); + return $c->render(message => "User not found. id=$id." + , link => ['/admin/users','Users'] + ,template => '/main/standard_message' + ,status => 404 + ) if !$user; + $user->remove(); + + return $c->render(message => "User removed : ".$user->name + , link => ['/admin/users','Users'] + ,template => '/main/standard_message' + ); +}; + any '/admin/user/(#id).(:type)' => sub { my $c = shift; @@ -1351,6 +1407,7 @@ any '/admin/user/(#id).(:type)' => sub { } $c->stash(user => $user); + return $c->render(json => {name => $user->name}) if $c->stash('type') eq 'json'; return $c->render(template => 'main/manage_user'); }; @@ -2515,7 +2572,6 @@ sub login($c, $status=200) { my $url = ($c->param('url') or $c->req->url->to_abs->path); $url = '/' if $url =~ m{^/login}; - my @error =(); if (((defined $login) || (defined $password) || ((defined $c->param('submit')) && ($c->param('submit') ne 'sso'))) && (! $ticket)) { diff --git a/t/mojo/60_upload.t b/t/mojo/60_upload.t new file mode 100644 index 000000000..b91b1d4c8 --- /dev/null +++ b/t/mojo/60_upload.t @@ -0,0 +1,230 @@ +use warnings; +use strict; + +use Carp qw(confess); +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; + +no warnings "experimental::signatures"; +use feature qw(signatures); + +my $SECONDS_TIMEOUT = 15; + +my $t; + +my $URL_LOGOUT = '/logout'; +my $SCRIPT = path(__FILE__)->dirname->sibling('../script/rvd_front'); + +################################################################################ + +sub _clean(@name) { + for my $name (@name) { + my $user = Ravada::Auth::SQL->new(name => $name); + $user->remove() if $user; + } +} + +sub _clean_ldap(@name) { + for my $name (@name) { + if ( Ravada::Auth::LDAP::search_user($name) ) { + Ravada::Auth::LDAP::remove_user($name) + } + } +} + +sub _create($type, %users) { + return if $type eq 'sql'; + while (my ($name, $pass) = each %users) { + if ( $type eq 'ldap') { + create_ldap_user($name, $pass); + } + } +} + +sub test_upload_users_nopassword( $type, $mojo=0 ) { + + my $user1 = new_domain_name(); + my $user2 = new_domain_name(); + + _clean_ldap($user1, $user2); + _clean($user1, $user2); + + my $users = $user1."\n" + .$user2."\n" + ; + + if ($mojo) { + $t->post_ok('/admin/users/upload.json' => form => { + type => $type + ,create => 0 + ,users => { content => $users, filename => 'users.txt', 'Content-Type' => 'text/csv' }, + })->status_is(200); + die $t->tx->res->body if $t->tx->res->code != 200; + + my $response = $t->tx->res->json(); + like($response->{output}, qr/2 users added/); + is_deeply($response->{error},[]); + } else { + rvd_front->upload_users($users, $type); + } + + test_users_added($type, $user1,$user2); +} + +sub test_upload_users( $type, $create=0, $mojo=0 ) { + + my ($user1, $pass1) = ( new_domain_name(), $$.1); + my ($user2, $pass2) = ( new_domain_name(), $$.2); + _clean_ldap($user1, $user2); + + _create($type, $user1, $pass1, $user2, $pass2) if !$create; + _clean($user1, $user2); + + my $users = join(":",($user1, $pass1)) ."\n" + .join(":",($user2, $pass2)) ."\n" + ; + + if ($mojo) { + $t->post_ok('/admin/users/upload.json' => form => { + type => $type + ,create => $create + ,users => { content => $users, filename => 'users.txt', 'Content-Type' => 'text/csv' }, + })->status_is(200); + die $t->tx->res->body if $t->tx->res->code != 200; + + my $response = $t->tx->res->json(); + like($response->{output}, qr/2 users added/); + is_deeply($response->{error},[]); + } else { + rvd_front->upload_users($users, $type, $create); + } + if ($type ne 'sso') { + $t->post_ok('/login' => form => {login => $user1, password => $pass1}) + ->status_is(302); + $t->get_ok('/logout'); + $t->post_ok('/login' => form => {login => $user2, password => $pass2}) + ->status_is(302); + $t->get_ok('/logout'); + + _login($t); + } + $t->post_ok('/admin/users/upload.json' => form => { + type => 'sql' + ,users => { content => $users, filename => 'users.txt', 'Content-Type' => 'text/csv' }, +})->status_is(200); + + exit if $t->tx->res->code == 401; + die $t->tx->res->body if $t->tx->res->code != 200; + + my $response = $t->tx->res->json(); + like($response->{output}, qr/0 users added/); + is(scalar(@{$response->{error}}),2); + + test_users_added($type, $user1, $user2); + + for my $name ($user1, $user2) { + my $user = Ravada::Auth::SQL->new(name => $name); + + $t->get_ok('/admin/user/'.$user->id.".html")->status_is(200); + die $t->tx->res->body if $t->tx->res->code != 200; + + $t->get_ok('/admin/user/'.$user->id.".json")->status_is(200); + + my $body = $t->tx->res->body; + my $json; + eval { $json = decode_json($body) }; + is($@, '') or die $body; + + is($json->{name}, $user->name); + } + +} + +sub test_users_added($type, @name) { + my $sth = connector->dbh->prepare( + "SELECT * FROM users WHERE name=?" + ); + for my $name (@name) { + $sth->execute($name); + my $row = $sth->fetchrow_hashref; + is($row->{name},$name); + if ($type eq 'sql') { + is($row->{external_auth}, undef); + } else { + is($row->{external_auth},$type,"Expecting $name in $type"); + } + } +} + +sub _login($t) { + my $user_name = new_domain_name(); + + my $user_db = Ravada::Auth::SQL->new( name => $user_name); + $user_db->remove(); + + my $user = create_user($user_name, $$); + user_admin->make_admin($user->id); + + mojo_login($t, $user_name, $$); +} + +sub test_upload_no_admin($t) { + my $user_name = new_domain_name(); + + my $user_db = Ravada::Auth::SQL->new( name => $user_name); + $user_db->remove(); + + my $user = create_user($user_name, $$); + die "Error, it shouldn't be admin" if $user->is_admin; + + mojo_login($t,$user_name, $$); + my $users = join(":",('a','b' )); + + for my $type ( ('json','html', 'foo')) { + $t->post_ok("/admin/users/upload.$type" => form => { + type => 'sql' + ,users => { content => $users, filename => 'users.txt', 'Content-Type' => 'text/csv' }, + })->status_is(403); + + die $t->tx->res->body if $t->tx->res->code != 403; + } + +} + +################################################################################ + +$ENV{MOJO_MODE} = 'development'; +init('/etc/ravada.conf',0); +my $connector = rvd_back->connector; +like($connector->{driver} , qr/mysql/i) or BAIL_OUT; + +$t = Test::Mojo->new($SCRIPT); +$t->ua->inactivity_timeout(900); +$t->ua->connect_timeout(60); + +test_upload_no_admin($t); + +_login($t); + +for my $type ('ldap','sso') { + test_upload_users_nopassword( $type ); + test_upload_users_nopassword( $type, 1 ); +} + +test_upload_users( 'sql',0,1 ); #test with mojo +test_upload_users( 'sql' ); # test without mojo + +test_upload_users( 'ldap', 1 ); # create users in Ravada +test_upload_users( 'ldap', 1, 1 ); # create users in Ravada +for my $type ( 'ldap', 'sso' ) { + test_upload_users( $type, 0 ,1 ); # do not create users in Ravada +} + +end(); +done_testing(); diff --git a/templates/bootstrap/new_user.html.ep b/templates/bootstrap/new_user.html.ep index f030724a0..dd70970d6 100644 --- a/templates/bootstrap/new_user.html.ep +++ b/templates/bootstrap/new_user.html.ep @@ -10,8 +10,12 @@
+

<%=l 'Create a new account' %>

+ <%=l 'Batch upload' %> +
%= include '/ng-templates/new_user'
diff --git a/templates/main/manage_user.html.ep b/templates/main/manage_user.html.ep index 18e49a446..32f167b8a 100644 --- a/templates/main/manage_user.html.ep +++ b/templates/main/manage_user.html.ep @@ -53,6 +53,12 @@ +% } +% if ($_user->is_admin) { + + % } @@ -79,9 +85,15 @@
% } % if ( $_user->is_admin && $user->id && $user->is_external && $user->external_auth eq 'ldap' && $user->ldap_entry ) { -
+
%= include '/main/manage_user_groups'
+% } +% if ( $_user->is_admin) { +
+ %= include '/main/manage_user_remove' +
+ % }
diff --git a/templates/main/manage_user_remove.html.ep b/templates/main/manage_user_remove.html.ep new file mode 100644 index 000000000..3274a0eff --- /dev/null +++ b/templates/main/manage_user_remove.html.ep @@ -0,0 +1,20 @@ +
+
+ <%=l 'Are you sure you want to remove this user ?' %> + <%= $user->name %> +
+ +
+ +
+ + <%=l 'Cancel' %> + + <%=l 'Remove' %> + +
diff --git a/templates/main/standard_message.html.ep b/templates/main/standard_message.html.ep new file mode 100644 index 000000000..23b5fdbdf --- /dev/null +++ b/templates/main/standard_message.html.ep @@ -0,0 +1,24 @@ + + +%= include 'bootstrap/header' + +
+%= include 'bootstrap/navigation' +
+ +
+
+

+ <%= $message %> +

+% if ($link) { + <%= $link->[1] %> +% } +
+
+
+
+%= include $footer +%= include 'bootstrap/scripts' + + diff --git a/templates/main/upload_users.html.ep b/templates/main/upload_users.html.ep new file mode 100644 index 000000000..22cf2d77c --- /dev/null +++ b/templates/main/upload_users.html.ep @@ -0,0 +1,87 @@ + + +%= include '/bootstrap/header' + +
+ %= include '/bootstrap/navigation' +
+ + +
+
+%= include $footer +%= include '/bootstrap/scripts' + + + From 46525a4dd66a4023e7020f5f3f34a7fc4c96a1bd Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 26 Oct 2023 14:52:01 +0200 Subject: [PATCH 16/17] Fix/1920 csp (#2003) fix(frontend): CSP more accurate Define the directives for our CDNs. More info about it: https://content-security-policy.com/ closes #1920 Co-authored-by: Fernando Verdugo --- .gitignore | 1 + etc/rvd_front.conf.example | 3 + lib/Ravada.pm | 53 ++++++++++++ lib/Ravada/Front.pm | 19 ++++- public/js/admin.js | 25 +++++- script/rvd_front | 48 +++++++++-- t/mojo/40_security_policy.t | 85 ++++++++++++++++++++ templates/bootstrap/header.html.ep | 2 +- templates/main/admin_settings.html.ep | 65 ++++++++++----- templates/main/admin_settings_submit.html.ep | 13 +++ 10 files changed, 284 insertions(+), 30 deletions(-) create mode 100644 t/mojo/40_security_policy.t create mode 100644 templates/main/admin_settings_submit.html.ep diff --git a/.gitignore b/.gitignore index f52e0a830..2d8e3ba74 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ log rvd_front.conf pkg-debian-out public/img/screenshots +public/js/custom yarn.lock node_modules/ t/vm/b10* diff --git a/etc/rvd_front.conf.example b/etc/rvd_front.conf.example index 7f0c2679f..369aa24dc 100644 --- a/etc/rvd_front.conf.example +++ b/etc/rvd_front.conf.example @@ -40,4 +40,7 @@ # Insert widget in /js/custom/insert_here_widget.js # this widget embed js in templates/bootstrap/scripts.html.ep ,widget => '' +# Content-Security-Policy HTTP response header helps you reduce XSS risks +# define custom directives. More info https://content-security-policy.com/ + ,security_policy => 'foo.bar.com' }; diff --git a/lib/Ravada.pm b/lib/Ravada.pm index 6c7305d6e..3b1287872 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -2543,6 +2543,59 @@ sub _sql_insert_defaults($self){ ,name => 'auto_view' ,value => $conf->{auto_view} } + ,{ id_parent => $id_frontend + ,name => "widget" + } + ,{ + id_parent => $id_frontend + ,name => 'content_security_policy' + } + ,{ + id_parent => "/frontend/content_security_policy" + ,name => "all" + ,value => '' + } + ,{ + id_parent => "/frontend/content_security_policy" + ,name => "default-src" + ,value => '' + } + ,{ + id_parent => "/frontend/content_security_policy" + ,name => "style-src" + ,value => '' + } + ,{ + id_parent => "/frontend/content_security_policy" + ,name => "script-src" + ,value => '' + } + ,{ + id_parent => "/frontend/content_security_policy" + ,name => "object-src" + ,value => '' + } + ,{ + id_parent => "/frontend/content_security_policy" + ,name => "frame-src" + ,value => '' + } + ,{ + id_parent => "/frontend/content_security_policy" + ,name => "font-src" + ,value => '' + } + + ,{ + id_parent => "/frontend/content_security_policy" + ,name => "connect-src" + ,value => '' + } + ,{ + id_parent => "/frontend/content_security_policy" + ,name => "media-src" + ,value => '' + } ,{ id_parent => $id_backend ,name => 'start_limit' diff --git a/lib/Ravada/Front.pm b/lib/Ravada/Front.pm index 6ea3f8ffe..945071d84 100644 --- a/lib/Ravada/Front.pm +++ b/lib/Ravada/Front.pm @@ -16,6 +16,7 @@ use Hash::Util qw(lock_hash); use IPC::Run3 qw(run3); use JSON::XS; use Moose; +use Storable qw(dclone); use Ravada; use Ravada::Auth::LDAP; use Ravada::Front::Domain; @@ -1481,6 +1482,19 @@ sub _settings_by_id($self) { return $orig_settings; } +sub _settings_by_parent($self,$parent) { + my $data = $self->_setting_data($parent); + my $sth = $self->_dbh->prepare("SELECT name,value FROM settings " + ." WHERE id_parent = ? "); + $sth->execute($data->{id}); + my $ret; + while (my ($name, $value) = $sth->fetchrow) { + $value = '' if !defined $value; + $ret->{$name} = $value; + } + return $ret; +} + =head2 feature Returns if a feature is available @@ -1521,9 +1535,10 @@ sub update_settings_global($self, $arg, $user, $reload, $orig_settings = $self-> confess Dumper([$field,$arg->{$field}]) if !ref($arg->{$field}); if ( scalar(keys %{$arg->{$field}})>2 ) { confess if !keys %{$arg->{$field}}; - $self->update_settings_global($arg->{$field}, $user, $reload, $orig_settings); + my $field2 = dclone($arg->{$field}); + $self->update_settings_global($field2, $user, $reload, $orig_settings); } - confess "Error: invalid field $field" if $field !~ /^\w+$/; + confess "Error: invalid field $field" if $field !~ /^\w[\w\-]+$/; my ( $value, $id ) = ($arg->{$field}->{value} , $arg->{$field}->{id} diff --git a/public/js/admin.js b/public/js/admin.js index aa43e9136..d7f84318b 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -1453,7 +1453,27 @@ ravadaApp.directive("solShowMachine", swMach) function settings_global_ctrl($scope, $http) { $scope.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - $scope.init = function() { + $scope.csp_locked = false; + $scope.set_csp_locked=function() { + var keys = Object.keys($scope.settings.frontend.content_security_policy); + var found = 0; + for ( var n_key=0 ; n_key0; + if ($scope.csp_locked && !$scope.csp_advanced) { + $scope.csp_advanced = true; + } + }; + $scope.init = function(url, csp_advanced) { + $scope.csp_advanced=false; + if (csp_advanced) { + $scope.csp_advanced=true; + } $http.get('/settings_global.json').then(function(response) { $scope.settings = response.data; var now = new Date(); @@ -1472,10 +1492,12 @@ ravadaApp.directive("solShowMachine", swMach) $scope.settings.frontend.maintenance_end.value =new Date($scope.settings.frontend.maintenance_end.value); } + $scope.set_csp_locked(); }); }; $scope.load_settings = function() { $scope.init(); + $scope.set_csp_locked(); $scope.formSettings.$setPristine(); }; $scope.update_settings = function() { @@ -1483,6 +1505,7 @@ ravadaApp.directive("solShowMachine", swMach) $http.post('/settings_global' ,JSON.stringify($scope.settings) ).then(function(response) { + $scope.set_csp_locked(); if (response.data.reload) { window.location.reload(); } diff --git a/script/rvd_front b/script/rvd_front index 0c7242576..44f60cce3 100644 --- a/script/rvd_front +++ b/script/rvd_front @@ -147,21 +147,50 @@ sub _time() { return strftime('%Y/%m/%d:%H:%M:%S %z',localtime); } +sub _security_policy() { + + my $config=$RAVADA->_settings_by_parent("/frontend/content_security_policy"); + my $all = ($config->{all} or ''); + my %src = ( + "default-src" => "'self' sha256- sha384 http: https: data:" + ,"style-src" => "'self' cdnjs.cloudflare.com stackpath.bootstrapcdn.com cdn.ckeditor.com cdn.jsdelivr.net use.fontawesome.com 'unsafe-inline'" + ,"script-src" => "'self' code.jquery.com cdn.ckeditor.com cdnjs.cloudflare.com stackpath.bootstrapcdn.com ajax.googleapis.com cdn.jsdelivr.net 'unsafe-inline' 'unsafe-eval'" + ,"object-src" => " 'self'" + ,"media-src" => "'self'" + ,"frame-src" => "'self'" + ,"font-src" => "'self' data: use.fontawesome.com" + ,"connect-src" => "'self'" + ); + my $sec = ''; + for my $field (sort keys %src) { + $sec .= " " if $sec; + $sec .= $field." ".$src{$field}; + $sec .= " $all" if $all; + if ( exists $config->{$field} ) { + $sec .= " ".${config}->{$field}; + } + $field =~ s/-/_/g; + if ( exists $config->{$field} ) { + $sec .= " ".$config->{$field}; + } + + $sec .= ";"; + } + return $sec; +} + hook before_routes => sub { my $c = shift; - $c ->res->headers->content_security_policy ( - "object-src 'auto';" - ."media-src 'self';" - ." frame-src 'self';" - ." connect-src 'self'; "); - + my $sec = _security_policy(); + $c ->res->headers->content_security_policy($sec); $USER = undef; $c->stash(version => $RAVADA->version); my $url = $c->req->url->to_abs->path; my $host = $c->req->url->to_abs->host; + my $widget=( $CONFIG_FRONT->{widget} or $RAVADA->setting('/frontend/widget')); $c->stash(css=>['/css/sb-admin.css'] ,js_mod=>[ ## angular modules '/js/booking/booking.module.js?v='.$RAVADA->version @@ -184,7 +213,7 @@ hook before_routes => sub { ,host => $host ,bookings => $RAVADA->setting('/backend/bookings') ,FEATURE => {} - ,widget => $CONFIG_FRONT->{widget} + ,widget => $widget ); $USER = _logged_in($c); @@ -2810,6 +2839,11 @@ sub admin { return access_denied($c) unless $USER->is_admin; my $url = $c->req->url->to_abs->path; my $host = $c->req->url->to_abs->host; + my $csp = $RAVADA->_settings_by_parent("/frontend/content_security_policy"); + my $csp_advanced = 0; + $csp_advanced = grep $csp->{$_}, grep /-/,keys %$csp; + + $c->stash( csp => $csp , csp_advanced => $csp_advanced); $c->stash(url_login => "/login"); } if ($page eq 'storage') { diff --git a/t/mojo/40_security_policy.t b/t/mojo/40_security_policy.t new file mode 100644 index 000000000..4e4af063d --- /dev/null +++ b/t/mojo/40_security_policy.t @@ -0,0 +1,85 @@ +use warnings; +use strict; + +use Carp qw(confess); +use Data::Dumper; +use HTML::Lint; +use Test::More; +use Test::Mojo; +use Mojo::File 'path'; +use Mojo::JSON qw(decode_json); +use Storable qw(dclone); + +use lib 't/lib'; +use Test::Ravada; + +no warnings "experimental::signatures"; +use feature qw(signatures); + +my $SECONDS_TIMEOUT = 15; + +my $t; + +my $URL_LOGOUT = '/logout'; +my ($USERNAME, $PASSWORD) = (user_admin->name, "$$ $$"); +my $SCRIPT = path(__FILE__)->dirname->sibling('../script/rvd_front'); + +$ENV{MOJO_MODE} = 'devel'; +init('/etc/ravada.conf',0); +my $connector = rvd_back->connector; +like($connector->{driver} , qr/mysql/i) or BAIL_OUT; + +$Test::Ravada::BACKGROUND=1; + +$t = Test::Mojo->new($SCRIPT); +$t->ua->inactivity_timeout(900); +$t->ua->connect_timeout(60); + +mojo_login($t, $USERNAME, $PASSWORD); + +my $sth = rvd_front->_dbh->prepare("UPDATE settings set value='' WHERE id_parent=?"); + +$t->get_ok("/settings_global.json")->status_is(200); +my $body = $t->tx->res->body(); +my $settings = decode_json($body); + +$sth->execute($settings->{frontend}->{content_security_policy}->{id}); + +my $new = dclone($settings); +my $exp_default = "foodefault.example.com"; +my $exp_all = "fooall.example.com"; +$new->{frontend}->{content_security_policy}->{'default-src'}->{value} = $exp_default; +$new->{frontend}->{content_security_policy}->{'all'}->{value} = $exp_all; +delete $new->{backend}; + +my $reload=0; +rvd_front->update_settings_global($new,user_admin,$reload); + +$t->post_ok("/settings_global", json => $new ); + +$t->get_ok("/settings_global.json")->status_is(200); +$body = $t->tx->res->body(); +my $settings2 = decode_json($body); +is($settings2->{frontend}->{content_security_policy}->{'all'}->{value} , $exp_all) or exit; +is($settings2->{frontend}->{content_security_policy}->{'default-src'}->{value} , $exp_default) or exit; + +my $config_csp = rvd_front->_settings_by_parent("/frontend/content_security_policy"); +is($config_csp->{all}, $exp_all); +is($config_csp->{'default-src'}, $exp_default); + +my $header = $t->tx->res->headers->content_security_policy(); +my %csp; +for my $entry (split /;/,$header) { + my ($key,$value) = $entry =~ /\s*(.*?)\s+(.*)/; + $csp{$key}=$value; +} + +like($csp{'default-src'},qr/$exp_all/); +like($csp{'default-src'},qr/$exp_default/); + +$sth->execute($settings->{frontend}->{content_security_policy}->{id}); + +$new->{frontend}->{content_security_policy}->{'default-src'}->{value} = ''; +$new->{frontend}->{content_security_policy}->{'all'}->{value} = ''; +$t->post_ok("/settings_global", json => $new ); +done_testing(); diff --git a/templates/bootstrap/header.html.ep b/templates/bootstrap/header.html.ep index fc50da478..afcd3049f 100644 --- a/templates/bootstrap/header.html.ep +++ b/templates/bootstrap/header.html.ep @@ -12,7 +12,7 @@ Ravada VDI % if ( !$fallback ) { - + diff --git a/templates/main/admin_settings.html.ep b/templates/main/admin_settings.html.ep index c737fa445..b44a125f5 100644 --- a/templates/main/admin_settings.html.ep +++ b/templates/main/admin_settings.html.ep @@ -6,7 +6,8 @@ %= include 'bootstrap/navigation'
-
-
+
@@ -71,7 +71,7 @@
-
+
@@ -79,6 +79,47 @@ type="datetime-local">
+ +
+
+
<%=l 'Widget' %> + +
+
+ +
+
+ +
+
Content Security Policy
+
+
+
+
+
+
+
+ + +
+
+
+ % for my $item (sort keys %$csp) { +
+
+
<%= $item %>
+
+ +
+
+ % } +
+ +%= include "/main/admin_settings_submit" +
@@ -211,21 +252,7 @@
-
- -
-
- - -
-
- +%= include "/main/admin_settings_submit"
diff --git a/templates/main/admin_settings_submit.html.ep b/templates/main/admin_settings_submit.html.ep new file mode 100644 index 000000000..faa325bc7 --- /dev/null +++ b/templates/main/admin_settings_submit.html.ep @@ -0,0 +1,13 @@ +
+
+
+ + +
+
From 715113a62ecc59fb41fbf0daf56d2f23986f6356 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Fri, 27 Oct 2023 09:24:39 +0200 Subject: [PATCH 17/17] fix: lock and shutdown machines before prepare (#1986) fix: lock and shutdown machines before prepare --- lib/Ravada.pm | 44 ++++++++++++++++++++++++++------ lib/Ravada/Request.pm | 1 + t/mojo/10_login.t | 59 +++++++++++++++++++++++++------------------ t/mojo/20_ws.t | 7 +++++ t/request/40_base.t | 24 ++++++++++++++++++ t/vm/70_clone.t | 1 + 6 files changed, 104 insertions(+), 32 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index 3b1287872..1a211f90a 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -3308,6 +3308,7 @@ sub _req_add_disk($uid, $id_domain, $type, $size, $request, $storage=undef) { ,name => 'disk' ,data => $data ,@after_req + ,at => time + 1 ); } sub _start_domain_after_create($domain, $request, $uid,$previous_request) { @@ -3862,7 +3863,7 @@ sub process_requests { "pid=".($req->pid or '')." ".$req->status() ."$txt_retry " .$req->command - ." ".Dumper($req->args) if $DEBUG || $debug; + ." ".Dumper($req->args) if ( $DEBUG || $debug ) && $req->command ne 'set_time'; my ($n_retry) = $req->status() =~ /retry (\d+)/; $n_retry = 0 if !$n_retry; @@ -3872,14 +3873,16 @@ sub process_requests { next if !$DEBUG && !$debug; warn ''.localtime." req ".$req->id." , cmd: ".$req->command." ".$req->status() - ." , err: '".($req->error or '')."'\n" if $DEBUG || $VERBOSE; + ." , err: '".($req->error or '')."'\n" if ($DEBUG || $VERBOSE ) + && $req->command ne 'set_time'; # sleep 1 if $DEBUG; } + my @reqs2 = grep { $_->command ne 'set_time' } @reqs; warn Dumper([map { $_->id." ".($_->pid or '')." ".$_->command." ".$_->status } - grep { $_->id } @reqs ]) - if ($DEBUG || $debug ) && @reqs; + grep { $_->id } @reqs2 ]) + if ($DEBUG || $debug ) && @reqs2; return scalar(@reqs); } @@ -4520,7 +4523,7 @@ sub _wait_pids($self) { $request->status('done') if $request->status =~ /working/i; }; warn("$$ request id=$id_req ".$request->command." ".$request->status() - .", error='".($request->error or '')."'\n") if $DEBUG && $request; + .", error='".($request->error or '')."'\n") if $DEBUG && $request && $request->command ne 'set_time'; } } @@ -4894,14 +4897,38 @@ sub _cmd_prepare_base { my $id_domain = $request->id_domain or confess "Missing request id_domain"; my $uid = $request->args('uid') or confess "Missing argument uid"; + my $domain = $self->search_domain_by_id($id_domain); + die "Unknown domain id '$id_domain'\n" if !$domain; + my $user = Ravada::Auth::SQL->search_by_id( $uid) or confess "Error: Unknown user id $uid in request ".Dumper($request); - my $with_cd = $request->defined_arg('with_cd'); + die "User ".$user->name." [".$user->id."] not allowed to prepare base " + .$domain->name."\n" + unless $user->is_admin || ( + $domain->id_owner == $user->id && $user->can_create_base()); - my $domain = $self->search_domain_by_id($id_domain); - die "Unknown domain id '$id_domain'\n" if !$domain; + my $with_cd = $request->defined_arg('with_cd'); + + if ($domain->is_active) { + my $req_shutdown = Ravada::Request->shutdown_domain( + uid => $user->id + ,id_domain => $domain->id + ,timeout => 0 + ); + $request->after_request($req_shutdown->id); + $request->at(time + 10); + if ( !defined $request->retry() ) { + $request->retry(5); + $request->status("retry"); + } elsif($request->retry>0) { + $request->retry($request->retry-1); + $request->status("retry"); + } + $request->error("Machine must be shut down ".$domain->name." [".$domain->id."]"); + return; + } $self->_remove_unnecessary_request($domain); $self->_remove_unnecessary_downs($domain); @@ -6200,6 +6227,7 @@ sub _req_method { ,list_cpu_models => \&_cmd_list_cpu_models ,enforce_limits => \&_cmd_enforce_limits ,force_shutdown => \&_cmd_force_shutdown +,force_shutdown_domain => \&_cmd_force_shutdown ,force_reboot => \&_cmd_force_reboot ,shutdown_start => \&_cmd_shutdown_start ,rebase => \&_cmd_rebase diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index 4b4ecef6b..3bc83e6c0 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -71,6 +71,7 @@ our %VALID_ARG = ( , check => 2 , id_vm => 2 } ,force_shutdown_domain => { id_domain => 1, uid => 1, at => 2, id_vm => 2 } + ,force_shutdown => { id_domain => 1, uid => 1, at => 2, id_vm => 2 } ,reboot_domain => { name => 2, id_domain => 2, uid => 1, timeout => 2, at => 2 , id_vm => 2 } ,force_reboot_domain => { id_domain => 1, uid => 1, at => 2, id_vm => 2 } diff --git a/t/mojo/10_login.t b/t/mojo/10_login.t index a13ccf7eb..fcb4713b6 100644 --- a/t/mojo/10_login.t +++ b/t/mojo/10_login.t @@ -228,7 +228,7 @@ sub test_login_non_admin($t, $base, $clone){ for ( 1 .. 10 ) { my $clone2 = rvd_front->search_domain($clone->name); last if $clone2->is_base || !$clone2->list_requests; - _wait_request(debug => 1, background => 1, check_error => 1); + _wait_request(debug => 0, background => 1, check_error => 1); mojo_check_login($t, $USERNAME, $PASSWORD); } is($clone->is_base,1) or next; @@ -246,10 +246,9 @@ sub test_login_non_admin($t, $base, $clone){ login($name, $pass); $t->get_ok('/')->status_is(200)->content_like(qr/choose a machine/i); - $t->get_ok("/machine/clone/".$clone->id.".html") ->status_is(200); - wait_request(debug => 1, check_error => 1, background => 1, timeout => 120); + wait_request(debug => 0, check_error => 1, background => 1, timeout => 120); mojo_check_login($t, $name, $pass); my $clone_new_name = $base->name."-".$name; @@ -269,7 +268,7 @@ sub test_login_non_admin($t, $base, $clone){ for ( 1 .. 60 ) { my ($req) = grep { $_->status ne 'done' } $user->list_requests(); last if !$req; - wait_request(debug => 1, check_error => 1, background => 1, timeout => 120); + wait_request(debug => 0, check_error => 1, background => 1, timeout => 120); delete_request('open_exposed_ports'); } my ($req) = reverse $user->list_requests(); @@ -300,7 +299,7 @@ sub test_list_ldap_attributes($t, $expected_code=200) { sub test_login_non_admin_req($t, $base, $clone){ mojo_check_login($t, $USERNAME, $PASSWORD); - my $clone2; + my $clone2 = $clone; for ( 1 .. 3 ) { if (!$clone->is_base) { @@ -313,7 +312,7 @@ sub test_login_non_admin_req($t, $base, $clone){ for ( 1 .. 60 ) { $clone2 = rvd_front->search_domain($clone->name); last if $clone2->is_base || !$clone2->list_requests; - _wait_request(debug => 1, background => 1, check_error => 1); + _wait_request(debug => 0, background => 1, check_error => 1); mojo_check_login($t, $USERNAME, $PASSWORD); } last if $clone2->is_base; @@ -340,7 +339,7 @@ sub test_login_non_admin_req($t, $base, $clone){ } ); - wait_request(debug => 1, check_error => 1, background => 1, timeout => 120); + wait_request(debug => 0, check_error => 1, background => 1, timeout => 120); mojo_check_login($t, $name, $pass); my $clone_new = rvd_front->search_domain($clone_new_name); @@ -358,7 +357,7 @@ sub test_login_non_admin_req($t, $base, $clone){ for ( 1 .. 10 ) { - wait_request(debug => 1, check_error => 1, background => 1, timeout => 120); + wait_request(debug => 0, check_error => 1, background => 1, timeout => 120); $clone_new = rvd_front->search_domain($clone_new_name); last if $clone_new; } @@ -426,11 +425,11 @@ sub test_copy_without_prepare($clone) { is(scalar @clones, $n_clones_clone+$n_clones,"Expecting clones from ".$clone->name) or exit; mojo_request($t, "spinoff", { id_domain => $clone->id }); - wait_request(debug => 1, check_error => 1, background => 1, timeout => 120); + wait_request(debug => 0, check_error => 1, background => 1, timeout => 120); # is($clone->id_base,0 ); mojo_check_login($t); mojo_request($t, "clone", { id_domain => $clone->id, number => $n_clones }); - wait_request(debug => 1, check_error => 1, background => 1, timeout => 120); + wait_request(debug => 0, check_error => 1, background => 1, timeout => 120); is($clone->is_base, 1 ); my @n_clones_clone_2= $clone->clones(); is(scalar @n_clones_clone_2, $n_clones_clone+$n_clones*2) or exit; @@ -569,21 +568,28 @@ sub _add_displays($t, $domain) { } -sub _clone_and_base($vm_name, $t, $base0) { +sub _clone_and_base($vm_name, $t) { mojo_check_login($t); - my $base1 = $base0; + my ($base,$base1); if ($vm_name eq 'KVM') { - my $base = rvd_front->search_domain($BASE_NAME); + $base = rvd_front->search_domain($BASE_NAME); die "Error: test base $BASE_NAME not found" if !$base; + } else { + $base = test_create_base($t, $vm_name, new_domain_name()); + } + confess if !defined $base; + + { my $name = new_domain_name()."-".$vm_name."-$$"; mojo_request_url_post($t,"/machine/copy",{id_base => $base->id, new_name => $name, copy_ram => 0.128, copy_number => 1}); - for ( 1 .. 60 ) { + for ( 1 .. 90 ) { $base1 = rvd_front->search_domain($name); last if $base1; wait_request(); } ok($base1, "Expecting domain $name created") or exit; + mojo_request($t,"spinoff",{id_domain => $base1->id}); } mojo_check_login($t); @@ -667,7 +673,7 @@ sub _download_iso($iso_name) { my $req = Ravada::Request->download(id_iso => $id_iso); for ( 1 .. 300 ) { last if $req->status eq 'done'; - _wait_request(debug => 1, background => 1, check_error => 1); + _wait_request(debug => 0, background => 1, check_error => 1); } is($req->status,'done'); is($req->error, '') or exit; @@ -746,9 +752,9 @@ sub test_new_machine_default($t, $vm_name, $empty_iso_file=undef) { for ( 1 .. 10 ) { ($data) = grep { $_->{file} =~ /DATA/ } @$disks; last if $data; - sleep 1; + wait_request(); } - ok($data,"Expecting a data disk volume") or exit; + ok($data,"Expecting a data disk volume in ".$domain->name) or exit; my ($iso) = grep { $_->{file} =~ /iso$/ } @$disks; ok($iso,"Expecting an ISO cdrom disk volume"); @@ -860,6 +866,7 @@ sub test_create_base($t, $vm_name, $name) { ,disk => 1 ,ram => 1 ,swap => 1 + ,start => 0 ,submit => 1 } )->status_is(302); @@ -869,7 +876,7 @@ sub test_create_base($t, $vm_name, $name) { my @requests = $user->list_requests(); my ($req_create) = grep { $_->command eq 'create' } @requests; - _wait_request(debug => 1, background => 1, check_error => 1); + _wait_request(debug => 0, background => 1, check_error => 1); my $base; for ( 1 .. 120 ) { $base = rvd_front->search_domain($name); @@ -952,7 +959,7 @@ sub test_clone_same_name($t, $base) { wait_request(); for ( 1 .. 10 ) { - wait_request(debug => 1); + wait_request(debug => 0); @clones2 = $base->clones(); last if scalar(@clones2)>scalar(@clones); sleep 1; @@ -966,6 +973,7 @@ sub test_clone_same_name($t, $base) { die Dumper(\@clones2) if !$clone; + mojo_request($t,"force_shutdown", {id_domain => $clone->{id} }); mojo_request($t,"prepare_base", {id_domain => $clone->{id} }); sleep 1; wait_request(); @@ -979,7 +987,7 @@ sub test_clone_same_name($t, $base) { $t->get_ok("/machine/clone/".$base->id.".html") ->status_is(200); - wait_request(); + wait_request( debug => 0); my @clones3; for ( 1 .. 20 ) { @@ -989,7 +997,8 @@ sub test_clone_same_name($t, $base) { wait_request( background=>1); } @clones3 = $base->clones(); - is(scalar(@clones3) , scalar(@clones2)+1) or exit; + is(scalar(@clones3) , scalar(@clones2)+1) or die "Expecting ".(scalar(@clones2)+1) + ." clones of ".$base->name; for my $c ($base->clones) { mojo_request($t,"remove_domain", {name => $c->{name} }); @@ -1064,8 +1073,10 @@ for my $vm_name (reverse @{rvd_front->list_vm_types} ) { } test_admin_can_do_anything($t, $base0); - my $base2 =test_create_base($t, $vm_name, new_domain_name()."-$vm_name-$$"); - push @bases,($base2->name); + my $base2a =test_create_base($t, $vm_name, new_domain_name()."-$vm_name-$$"); + push @bases,($base2a->name); + + my $base2 = _clone_and_base($vm_name, $t); mojo_request($t, "add_hardware", { id_domain => $base0->id, name => 'network' }); wait_request(debug => 0, check_error => 1, background => 1, timeout => 120); @@ -1073,7 +1084,7 @@ for my $vm_name (reverse @{rvd_front->list_vm_types} ) { test_validate_html("/machine/manage/".$base0->id.".html"); - my $base1 = _clone_and_base($vm_name, $t, $base0); + my $base1 = _clone_and_base($vm_name, $t); push @bases,($base1->name); is($base1->is_base,1) or next; diff --git a/t/mojo/20_ws.t b/t/mojo/20_ws.t index 51210cb56..3332b0def 100644 --- a/t/mojo/20_ws.t +++ b/t/mojo/20_ws.t @@ -89,6 +89,9 @@ sub test_bases($t, $bases) { my $n_bases = 0; my $n_machines = scalar(@$bases); for my $base ( @$bases ) { + + mojo_request($t, "force_shutdown", { id_domain => $base->id }); + my $url = "/machine/prepare/".$base->id.".json"; $t->get_ok($url)->status_is(200); wait_mojo_request($t, $url); @@ -159,6 +162,10 @@ sub test_list_machines_non_admin($t, $bases) { my @list_machines = list_machines($t); is(scalar(@list_machines),0) or die Dumper([map {[$_->{id_base},$_->{name}]} @list_machines]); + Ravada::Request->force_shutdown( + uid => user_admin->id + ,id_domain => $clone->{id} + ); my $req = Ravada::Request->prepare_base( uid => user_admin->id ,id_domain => $clone->{id} diff --git a/t/request/40_base.t b/t/request/40_base.t index 9c050c4b0..1b783b46f 100644 --- a/t/request/40_base.t +++ b/t/request/40_base.t @@ -172,6 +172,24 @@ sub test_req_create_domain { return $name; } +sub test_req_prepare_base_active($vm) { + my $domain; + if ($vm->type ne 'Void') { + my $base = import_domain($vm); + $domain = $base->clone(user => user_admin, name => new_domain_name); + } else { + $domain = create_domain($vm); + } + $domain->start(user_admin); + my $req = Ravada::Request->prepare_base(uid => user_admin->id + ,id_domain => $domain->id + ); + wait_request(debug => 0); + is($domain->is_active,0); + is($domain->is_base,1); +} + + sub test_req_prepare_base { my $vm_name = shift; my $name = shift; @@ -196,6 +214,10 @@ sub test_req_prepare_base { ok($req->status); ok($domain->is_locked,"Domain $name should be locked when preparing base"); + $req->status('working'); + ok($domain->is_locked,"Domain $name should be locked when preparing base"); + $req->status('requested'); + } wait_request(background => 0); @@ -665,6 +687,8 @@ for my $vm_name ( vm_names ) { my $iso = $vm_connected->_search_iso($ID_ISO); $vm_connected->_iso_name($iso, undef); } + test_req_prepare_base_active($vm_connected); + test_domain_name_iso($vm_connected); test_swap($vm_name); diff --git a/t/vm/70_clone.t b/t/vm/70_clone.t index d4c58f611..ea4ff0902 100644 --- a/t/vm/70_clone.t +++ b/t/vm/70_clone.t @@ -159,6 +159,7 @@ sub test_many_clones($vm) { ,id_domain => $base->id ); $base->is_public(1); + wait_request(); my $req = Ravada::Request->clone( uid => $user->id ,id_domain => $base->id