diff --git a/MANIFEST b/MANIFEST index 2905cd925..ea4e194d6 100644 --- a/MANIFEST +++ b/MANIFEST @@ -55,6 +55,6 @@ lib/Ravada/I18N/ca.po lib/Ravada/NetInterface.pm lib/Ravada/Auth.pm lib/Ravada/Domain.pm -lib/Ravada/Network.pm +lib/Ravada/Routes.pm script/rvd_front script/rvd_back diff --git a/README.md b/README.md index 37dd586cc..cb2aaf52e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ravada +# ravada [![GitHub version](https://img.shields.io/badge/version-1.8.0-brightgreen.svg)](https://github.com/UPC/ravada/releases) [![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://github.com/UPC/ravada/blob/master/LICENSE) [![Documentation Status](https://readthedocs.org/projects/ravada/badge/?version=latest)](http://ravada.readthedocs.io/en/latest/?badge=latest) @@ -10,34 +10,38 @@ ## Remote Virtual Desktops Manager -Ravada is a software that allows the user to connect to a -remote virtual desktop. -Ravada is meant for sysadmins who have some background in GNU/Linux, and want to deploy a VDI project. +Ravada is an open-source project that provides a web-based user interface for managing and accessing virtual machines (VMs) based on the QEMU/KVM virtualization technology. Ravada aims to simplify the management of virtual machines by offering a user-friendly interface accessible through a web browser.Ravada is meant for sysadmins who have some background in GNU/Linux, and want to deploy a VDI project. -Its back-end has been designed and implemented in order to allow future hypervisors to be added to the framework. Currently, it supports KVM and LXC is in the works. +Users can use Ravada to create, configure, and manage virtual machines without the need for a dedicated desktop client. It provides features such as remote console access, snapshot management, and the ability to manage multiple VMs from a central interface. Ravada's back-end has been designed and implemented in order to allow future hypervisors to be added to the framework. Currently, it supports KVM and LXC is in the works. The client only requirements are: a web-browser and a remote viewer supporting the spice protocol. In the current release we use the KVM Hypervisors: [KVM](http://www.linux-kvm.org/) as the backend for the Virtual Machines. - [LXC](https://linuxcontainers.org/) support is currently in development. +[LXC](https://linuxcontainers.org/) support is currently in development. ### Features - * KVM backend for Windows and Linux Virtual machines - * LDAP and SQL authentication - * Kiosk mode - * Remote Access with [Spice](http://www.spice-space.org/) for Windows and Linux - * Light and fast virtual machine clones for each user - * Instant clone creation - * USB redirection - * Easy and customizable end users interface - * Administration from a web browser +- KVM backend for Windows and Linux Virtual machines +- LDAP and SQL authentication +- Kiosk mode +- Remote Access with [Spice](http://www.spice-space.org/) for Windows and Linux +- Light and fast virtual machine clones for each user +- Instant clone creation +- USB redirection +- Easy and customizable end users interface +- Administration from a web browser ## Install Read [INSTALL](http://ravada.readthedocs.io/en/latest/docs/INSTALL.html). +Install Ravada in [Ubuntu](https://ravada.readthedocs.io/en/latest/docs/INSTALL_Ubuntu.html) +Install Ravada in [Debian](https://ravada.readthedocs.io/en/latest/docs/INSTALL_Debian.html) +Install Ravada on [Fedora](https://ravada.readthedocs.io/en/latest/docs/INSTALL_Fedora.html) +Install Ravada on [Rocky Linux 9 or RHEL9](https://ravada.readthedocs.io/en/latest/docs/INSTALL_Rocky9.html#install-ravada-on-rocky-linux-9-or-rhel9) +Install Ravada - [Ubuntu Xenial](https://ravada.readthedocs.io/en/latest/docs/INSTALL_ubuntu_xenial.html) +Install Ravada from [dockers](https://ravada.readthedocs.io/en/latest/docs/INSTALLfromDockers.html). ### Production @@ -48,6 +52,11 @@ for production fine-tuning guidelines. See [operation](http://ravada.readthedocs.io/en/latest/docs/operation.html). +- [Create users](https://ravada.readthedocs.io/en/latest/docs/INSTALL_Ubuntu.html) +- [Import KVM virtual machines](https://ravada.readthedocs.io/en/latest/docs/INSTALL_Ubuntu.html) +- [View all rvd_back options](https://ravada.readthedocs.io/en/latest/docs/operation.html#view-all-rvd-back-options) +- [Admin Operations](https://ravada.readthedocs.io/en/latest/docs/operation.html#admin) + ### Update See [update](http://ravada.readthedocs.io/en/latest/docs/update.html). diff --git a/lib/Ravada.pm b/lib/Ravada.pm index 04ca89227..55677e24e 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -1651,6 +1651,13 @@ sub _add_indexes_generic($self) { ,"index(date_changed)" ,"index(id_owner)" ] + ,bundles => [ + "unique (name)" + ] + ,domains_bundle => [ + "index(id_domain)" + ,"unique (id_bundle, id_domain)" + ] ); my $if_not_exists = ''; $if_not_exists = ' IF NOT EXISTS ' if $CONNECTOR->dbh->{Driver}{Name} =~ /sqlite|mariadb/i; @@ -2382,6 +2389,21 @@ sub _sql_create_tables($self) { } ] , + [ + bundles => { + id => 'integer PRIMARY KEY AUTO_INCREMENT', + name => 'char(255) NOT NULL', + private_network => 'integer NOT NULL default 0' + } + ], + [ + domains_bundle => { + id => 'integer PRIMARY KEY AUTO_INCREMENT', + id_bundle => 'integer NOT NULL references `bundles` (`id`) ON DELETE CASCADE', + id_domain => 'integer NOT NULL references `domains` (`id`) ON DELETE CASCADE', + } + ] + , [virtual_networks => { id => 'integer PRIMARY KEY AUTO_INCREMENT', ,id_vm => 'integer NOT NULL references `vms` (`id`) ON DELETE CASCADE', @@ -2888,6 +2910,7 @@ sub _upgrade_tables { $self->_upgrade_table('domains','has_backups','int not null default 0'); $self->_upgrade_table('domains','auto_compact','int default NULL'); $self->_upgrade_table('domains','date_status_change' , 'datetime'); + $self->_upgrade_table('domains','show_clones' , 'int not null default 1'); $self->_upgrade_table('domains_network','allowed','int not null default 1'); @@ -3290,6 +3313,13 @@ sub create_domain { $base = Ravada::Domain->open($id_base) or confess "Unknown base id: $id_base"; $vm = $base->_vm; + + my $net_bundle = $self->_net_bundle($base, $id_owner); + if ($net_bundle) { + unlock_hash(%args); + $args{options}->{network} = $net_bundle->{name}; + lock_hash(%args); + } } my $user = Ravada::Auth::SQL->search_by_id($id_owner) or confess "Error: Unkown user '$id_owner'"; @@ -4623,6 +4653,25 @@ sub _cmd_remove { $self->remove_domain(name => $request->args('name'), uid => $request->args('uid')); } +sub _cmd_remove_clones($self, $request) { + + my $uid = $request->args('uid'); + my $user = Ravada::Auth::SQL->search_by_id($uid); + + my $id_domain = $request->args('id_domain'); + + die "Error: user ".$user->name." not authorized to remove clones" + unless $user->is_admin(); + + my $domain = Ravada::Front::Domain->open($id_domain); + for my $clone ( $domain->clones ) { + Ravada::Request->remove_domain( + uid => $uid + ,name => $clone->{name} + ); + } +} + sub _cmd_restore_domain($self,$request) { my $domain = Ravada::Domain->open($request->args('id_domain')); return $domain->restore(Ravada::Auth::SQL->search_by_id($request->args('uid'))); @@ -4713,22 +4762,66 @@ sub _cmd_clone($self, $request) { $args->{alias} = $alias if $alias; + my $net_bundle = $self->_net_bundle($domain, $user); + + $args->{options}->{network} = $net_bundle->{name} if $net_bundle; + my $clone = $domain->clone( name => $name ,%$args ); $request->id_domain($clone->id) if $clone; + my $req_next = $request; Ravada::Request->start_domain( uid => $user->id ,id_domain => $clone->id ,remote_ip => $request->defined_arg('remote_ip') - ,after_request => $request->id + ,after_request => $req_next->id ) if $request->defined_arg('start'); } +sub _net_bundle($self, $domain, $user0) { + my $bundle = $domain->bundle(); + + return unless $bundle && exists $bundle->{private_network} + && $bundle->{private_network}; + + my $user = $user0; + $user = Ravada::Auth::SQL->search_by_id($user0) if !ref($user); + + my ($net) = grep { $_->{id_owner} == $user->id } + $domain->_vm->list_virtual_networks(); + + return $net if $net; + + my $req_new_net = Ravada::Request->new_network( + uid => Ravada::Utils::user_daemon->id + ,id_vm => $domain->_vm->id + ,name => $bundle->{name}."-".$user->name + ); + $self->_cmd_new_network($req_new_net); + my $data = decode_json($req_new_net->output); + $req_new_net->status('done'); + + my $req_network = Ravada::Request->create_network( + uid => Ravada::Utils::user_daemon->id + ,id_vm => $domain->_vm->id + ,data => $data + ); + $self->_cmd_create_network($req_network); + $req_network->status('done'); + + ($net) = grep { $_->{name} eq $data->{name} } + $domain->_vm->list_virtual_networks(); + + $domain->_vm->_update_network_db($net, {id_owner => $user->id }); + + return $net; +} + sub _new_clone_name($self, $base,$user) { my $name; my $alias = $base->name; @@ -5486,7 +5579,7 @@ sub _cmd_check_storage($self, $request) { my $path = ''.$vm->_storage_path($storage); _check_mounted($path,\%fstab,\%mtab); my ($ok,$err) = $vm->write_file("$path/check_storage",$contents); - die "Error on starage pool $storage : $err. Retry.\n" if $err; + die "Error on storage pool $storage : $err. Retry.\n" if $err; } } } @@ -5859,7 +5952,10 @@ sub _refresh_active_vms ($self) { next; } $active_vm{$vm->id} = 1; - $vm->list_virtual_networks(); + eval { + $vm->list_virtual_networks(); + }; + warn $@ if $@; } return \%active_vm; } @@ -5911,6 +6007,7 @@ sub _refresh_down_nodes($self, $request = undef ) { my $vm; eval { $vm = Ravada::VM->open($id) }; warn $@ if $@; + $vm->is_active() if $vm; } } @@ -6271,6 +6368,8 @@ sub _req_method { ,pause => \&_cmd_pause ,create => \&_cmd_create ,remove => \&_cmd_remove + ,remove_domain => \&_cmd_remove + ,remove_clones => \&_cmd_remove_clones ,restore_domain => \&_cmd_restore_domain ,resume => \&_cmd_resume ,dettach => \&_cmd_dettach @@ -6525,6 +6624,7 @@ sub _enforce_limits_active($self, $request) { my %domains; for my $domain ($self->list_domains( active => 1 )) { + next if $domain->is_in_bundle(); push @{$domains{$domain->id_owner}},$domain; $domain->client_status(); } diff --git a/lib/Ravada/Auth/SQL.pm b/lib/Ravada/Auth/SQL.pm index e737dd168..50f0ea64f 100644 --- a/lib/Ravada/Auth/SQL.pm +++ b/lib/Ravada/Auth/SQL.pm @@ -1106,7 +1106,7 @@ sub can_remove_clones($self, $id_domain=undef) { return $self->can_do('remove_clones') if !$id_domain; my $domain = Ravada::Front::Domain->open($id_domain); - confess "ERROR: domain is not a base " if !$domain->id_base; + confess "ERROR: domain ".$domain->name." is not a base " if !$domain->id_base; return 1 if $self->can_remove_clone_all(); diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 342549496..43fdeb229 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -529,8 +529,9 @@ sub _search_already_started($self, $fast = 0) { $sth->execute($self->_vm->type); my %started; while (my ($id) = $sth->fetchrow) { - my $vm = Ravada::VM->open($id); - next if !$vm->enabled; + my $vm; + eval { $vm = Ravada::VM->open($id) }; + next if !$vm || !$vm->enabled; my $vm_active; eval { @@ -539,7 +540,7 @@ sub _search_already_started($self, $fast = 0) { my $error = $@; if ($error) { warn $error; - $vm->enabled(0) if !$vm->is_local; + $vm->enabled(0) if !$vm->is_local && !$vm->ping; next; } next if !$vm_active; @@ -2069,6 +2070,8 @@ sub info($self, $user) { id => $self->id ,name => $self->name ,is_base => $self->is_base + ,is_public => $self->is_public + ,show_clones => $self->show_clones ,id_base => $self->id_base ,is_active => $is_active ,is_hibernated => $self->is_hibernated @@ -2404,13 +2407,15 @@ sub _remove_domain_cascade($self,$user, $cascade = 1) { next if $instance->{id_vm} == $self->_vm->id; my $vm; eval { $vm = Ravada::VM->open($instance->{id_vm}) }; - die $@ if $@ && $@ !~ /I can't find VM/i; - next if !$vm || !$vm->is_active; + die $@ if $@ && $@ !~ /I can't find VM ||libvirt error code: 38,/i; my $domain; $@ = ''; eval { $domain = $vm->search_domain($domain_name) } if $vm; warn $@ if $@; - $domain->remove($user, $cascade) if $domain; + eval { + $domain->remove($user, $cascade) if $domain; + }; + warn $@ if $@; $sth_delete->execute($instance->{id}); } } @@ -2873,6 +2878,8 @@ sub clone { my $volatile = delete $args{volatile}; my $id_owner = delete $args{id_owner}; my $alias = delete $args{alias}; + my $options = delete $args{options}; + my $storage = delete $args{storage}; confess "ERROR: Unknown args ".join(",",sort keys %args) if keys %args; @@ -2906,6 +2913,9 @@ sub clone { push @args_copy, ( remote_ip => $remote_ip) if $remote_ip; push @args_copy, ( from_pool => $from_pool) if defined $from_pool; push @args_copy, ( add_to_pool => $add_to_pool) if defined $add_to_pool; + push @args_copy, ( storage => $storage) if $storage; + push @args_copy, ( options => $options) if $options; + if ( $self->volatile_clones && !defined $volatile ) { $volatile = 1; } @@ -2956,6 +2966,7 @@ sub _copy_clone($self, %args) { my $id_owner = delete $args{id_owner}; $id_owner = $user->id if (! $id_owner); my $alias = delete $args{alias}; + my $options = delete $args{options}; confess "ERROR: Unknown arguments ".join(",",sort keys %args) if keys %args; @@ -2966,6 +2977,7 @@ sub _copy_clone($self, %args) { push @copy_arg, ( alias => $alias ) if $alias; push @copy_arg, ( memory => $memory ) if $memory; push @copy_arg, ( volatile => $volatile ) if $volatile; + push @copy_arg, ( options => $options ) if $options; $request->status("working","Copying domain ".$self->name ." to $name") if $request; @@ -3249,7 +3261,10 @@ sub _around_is_active($orig, $self) { } } my $is_active = 0; + eval { $is_active = $self->$orig(); + }; + warn $@ if $@; return $is_active if $self->readonly || !$self->is_known @@ -4490,6 +4505,10 @@ sub is_public { return $self->_data('is_public'); } +sub show_clones($self,$value=undef) { + return $self->_data('show_clones',$value); +} + =head2 is_volatile Returns if the domain is volatile, so it will be removed on shutdown @@ -5310,7 +5329,8 @@ sub _pre_clone($self,%args) { confess "ERROR: Missing user owner of new domain" if !$user; - for (qw(is_pool start add_to_pool from_pool with_cd volatile id_owner alias)) { + for (qw(is_pool start add_to_pool from_pool with_cd volatile id_owner + alias storage options)) { delete $args{$_}; } confess "ERROR: Unknown arguments ".join(",",sort keys %args) if keys %args; @@ -7712,4 +7732,29 @@ sub list_shares($self) { return @shares; } +sub bundle($self) { + my $sth = $self->_dbh->prepare("SELECT * FROM bundles " + ." WHERE id IN (SELECT id_bundle FROM domains_bundle " + ." WHERE id_domain=?)" + ); + $sth->execute($self->id); + my $bundle = $sth->fetchrow_hashref; + return if !keys %$bundle; + lock_hash(%$bundle); + return $bundle; + +} + +sub is_in_bundle($self) { + my $id=( $self->id_base or $self->id); + my $sth = $self->_dbh->prepare("SELECT id FROM bundles " + ." WHERE id IN (SELECT id_bundle FROM domains_bundle " + ." WHERE id_domain=?)" + ); + $sth->execute($id); + my ($id_bundle) = $sth->fetchrow; + return $id_bundle; + +} + 1; diff --git a/lib/Ravada/Domain/Void.pm b/lib/Ravada/Domain/Void.pm index 7a07f59b2..2eae1a04f 100644 --- a/lib/Ravada/Domain/Void.pm +++ b/lib/Ravada/Domain/Void.pm @@ -692,7 +692,9 @@ sub list_volumes_info($self, $attribute=undef, $value=undef) { } else { $dev->{driver}->{type} = 'void'; } - $dev->{storage_pool} = $self->_vm->_find_storage_pool($dev->{file}); + $dev->{storage_pool} = $self->_vm->_find_storage_pool($dev->{file}) + if $dev->{file}; + my $vol = Ravada::Volume->new( file => $dev->{file} ,info => $dev @@ -739,7 +741,7 @@ sub _new_mac($mac='ff:54:00:a7:49:71') { return join(":",@macparts); } -sub _set_default_info($self, $listen_ip=undef) { +sub _set_default_info($self, $listen_ip=undef, $network=undef) { my $info = { max_mem => 512*1024 ,memory => 512*1024, @@ -754,12 +756,22 @@ sub _set_default_info($self, $listen_ip=undef) { $self->_set_display($listen_ip); my $hardware = $self->_value('hardware'); + my @nets = $self->_vm->list_virtual_networks(); + my ($net) = grep { $_->{name} eq 'default'} @nets; + $net = $nets[0] if !$net; + if ($network) { + ($net) = grep { $_->{name} eq $network } @nets; + + die "Error: network $network not found ".join(" , ",@nets) + if !$net; + } + $hardware->{network}->[0] = { hwaddr => $info->{mac} ,address => $info->{ip} ,type => 'nat' ,driver => 'virtio' - ,name => "net1" + ,name => $net->{name} }; $self->_store(hardware => $hardware ); diff --git a/lib/Ravada/Front.pm b/lib/Ravada/Front.pm index d949c05ca..1aacabcde 100644 --- a/lib/Ravada/Front.pm +++ b/lib/Ravada/Front.pm @@ -130,14 +130,14 @@ Returns: listref of machines sub list_machines_user($self, $user, $access_data={}) { my $sth = $CONNECTOR->dbh->prepare( - "SELECT id,name,alias,is_public, description, screenshot, id_owner, is_base, date_changed" + "SELECT id,name,alias,is_public, description, screenshot, id_owner, is_base, date_changed, show_clones" ." FROM domains " ." WHERE ( is_base=1 OR ( id_base IS NULL AND id_owner=?))" ." ORDER BY alias" ); - my ($id, $name, $alias, $is_public, $description, $screenshot, $id_owner, $is_base, $date_changed); + my ($id, $name, $alias, $is_public, $description, $screenshot, $id_owner, $is_base, $date_changed, $show_clones); $sth->execute($user->id); - $sth->bind_columns(\($id, $name, $alias, $is_public, $description, $screenshot, $id_owner, $is_base, $date_changed)); + $sth->bind_columns(\($id, $name, $alias, $is_public, $description, $screenshot, $id_owner, $is_base, $date_changed,$show_clones)); my $bookings_enabled = $self->setting('/backend/bookings'); my @list; @@ -154,7 +154,13 @@ sub list_machines_user($self, $user, $access_data={}) { push @clones,$self->_search_shared($id, $user->id); my ($clone) = ($clones[0] or undef); - next unless $clone || $user->is_admin || ($is_public && $user->allowed_access($id)) || ($id_owner == $user->id); + + next unless + $clone && $show_clones + || $user->is_admin + || ($is_public && $user->allowed_access($id)) + || ($id_owner == $user->id); + $name = $alias if defined $alias; my $base = { id => $id, name => Encode::decode_utf8($name) , alias => Encode::decode_utf8($alias or $name) @@ -322,7 +328,7 @@ sub list_domains($self, %args) { my $query = "SELECT d.name,d.alias, d.id, id_base, is_base, id_vm, status, is_public " ." ,vms.name as node , is_volatile, client_status, id_owner " - ." ,comment, is_pool" + ." ,comment, is_pool, show_clones" ." ,d.date_changed" ." FROM domains d LEFT JOIN vms " ." ON d.id_vm = vms.id "; @@ -1873,6 +1879,58 @@ sub upload_users($self, $users, $type, $create=0) { return ($found, $count, \@error); } +=head2 create_bundle + +Creates a new bundle + +Arguments: name + +=cut + +sub create_bundle($self,$name) { + my $sth = $self->_dbh->prepare( + "INSERT INTO bundles (name) values (?)" + ); + $sth->execute($name); + + $sth = $self->_dbh->prepare( + "SELECT id FROM bundles WHERE name=?" + ); + $sth->execute($name); + my ($id)= $sth->fetchrow; + return $id; +} + +=head2 bundle_private_network + +Sets the bundle network to private + +Arguments : id_bundle, value ( defaults 1 ) + +=cut + +sub bundle_private_network($self, $id_bundle, $value=1){ + my $sth = $self->_dbh->prepare( + "UPDATE bundles set private_network=? WHERE id=?"); + $sth->execute($value, $id_bundle); +} + +=head2 add_to_bundle + +Adds a domain to a bundle + +Arguments : id_bundle, id_domain + +=cut + +sub add_to_bundle ($self, $id_bundle, $id_domain){ + my $sth = $self->_dbh->prepare( + "INSERT INTO domains_bundle (id_bundle, id_domain ) VALUES(?,?)" + ); + $sth->execute($id_bundle, $id_domain); + +} + =head2 version Returns the version of the main module diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index 4040031bb..74f0e3c18 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -105,6 +105,8 @@ our %VALID_ARG = ( ,start => 2, ,remote_ip => 2 ,with_cd => 2 + ,storage => 2 + ,options => 2 } ,change_owner => {uid => 1, id_domain => 1} ,add_hardware => {uid => 1, id_domain => 1, name => 1, number => 2, data => 2 } @@ -174,6 +176,7 @@ our %VALID_ARG = ( ,remove_network => { uid => 1, id => 1, id_vm => 2, name => 2 } ,change_network => { uid => 1, data => 1 } + ,remove_clones => { uid => 1, id_domain => 1 } ); $VALID_ARG{shutdown} = $VALID_ARG{shutdown_domain}; @@ -707,7 +710,7 @@ sub _duplicated_request($self=undef, $command=undef, $args=undef) { } confess "Error: missing command " if !$command; # delete $args_d->{uid} unless $command eq 'clone'; - delete $args_d->{uid} if $command eq 'set_base_vm'; + delete $args_d->{uid} if $command =~ /(cleanup|refresh_vms|set_base_vm)/; delete $args_d->{at}; delete $args_d->{status}; delete $args_d->{timeout}; @@ -823,8 +826,10 @@ sub _new_request { ." WHERE id=?"); $sth->execute($self->{id}); - - my $request = $self->open($self->{id}); + my $request; + eval { $request = $self->open($self->{id}) }; + warn $@ if $@ && $@ !~ /I can't find id=/; + return if !$request; $request->_validate(); $request->status('requested') if $request->status ne'done'; diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index d958a6355..3ba8bb4ed 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -234,6 +234,7 @@ sub open { $args{security} = decode_json($row->{security}) if $row->{security}; my $vm = $self->new(%args); + return if !$vm || !$vm->vm; $VM{$args{id}} = $vm unless $args{readonly}; return $vm; @@ -555,6 +556,7 @@ sub _around_create_domain { $domain->display($owner) if $domain->is_active; $domain->is_pool(1) if $add_to_pool; + return $domain; } @@ -846,7 +848,7 @@ sub _ip_a($self, $dev) { my ($out, $err) = $self->run_command_cache("/sbin/ip","-o","a"); die $err if $err; for my $line ( split /\n/,$out) { - my ($ip) = $line =~ m{^\d+:\s+$dev.*inet (.*?)/}; + my ($ip) = $line =~ m{^\d+:\s+$dev.*inet\d* (.*?)/}; return $ip if $ip; } warn "Warning $dev not found in active interfaces"; @@ -963,13 +965,13 @@ sub _check_require_base { delete $args{start}; delete $args{remote_ip}; - delete @args{'_vm','name','vm', 'memory','description','id_iso','listen_ip','spice_password','from_pool', 'volatile', 'alias','storage', 'options'}; + delete @args{'_vm','name','vm', 'memory','description','id_iso','listen_ip','spice_password','from_pool', 'volatile', 'alias','storage', 'options', 'network'}; confess "ERROR: Unknown arguments ".join(",",keys %args) if keys %args; my $base = Ravada::Domain->open($id_base); - my %ignore_requests = map { $_ => 1 } qw(clone refresh_machine set_base_vm start_clones shutdown_clones shutdown force_shutdown refresh_machine_ports set_time open_exposed_ports manage_pools screenshot); + my %ignore_requests = map { $_ => 1 } qw(clone refresh_machine set_base_vm start_clones shutdown_clones shutdown force_shutdown refresh_machine_ports set_time open_exposed_ports manage_pools screenshot remove_clones); my @requests; for my $req ( $base->list_requests ) { push @requests,($req) if !$ignore_requests{$req->command}; @@ -1502,6 +1504,7 @@ sub _update_network_db($self, $old, $new0) { $sql = "UPDATE virtual_networks set $sql WHERE id=?"; my $sth = $self->_dbh->prepare($sql); my @values = map { $new->{$_} } sort keys %$new; + $sth->execute(@values, $id); } } diff --git a/lib/Ravada/VM/KVM.pm b/lib/Ravada/VM/KVM.pm index c6a25320f..f08557677 100644 --- a/lib/Ravada/VM/KVM.pm +++ b/lib/Ravada/VM/KVM.pm @@ -1245,6 +1245,11 @@ sub _domain_create_from_base { my $volatile = $base->volatile_clones; $volatile = delete $args{volatile} if exists $args{volatile} && defined $args{volatile}; + my $options = delete $args{options}; + my $network = delete $options->{network}; + + die "Error: I can't set more options ".Dumper($options) if keys %$options; + my $vm = $self->vm; my $storage = $self->storage_pool; @@ -1262,6 +1267,8 @@ sub _domain_create_from_base { _xml_modify_disk($xml, \@device_disk);#, \@swap_disk); + $self->_xml_set_network($xml, $network) if $network; + my ($domain, $spice_password) = $self->_domain_create_common($xml,%args, is_volatile=>$volatile, base => $base); $domain->_insert_db(name=> $args{name}, id_base => $base->id, id_owner => $args{id_owner} @@ -1878,8 +1885,9 @@ sub _xml_modify_options($self, $doc, $options=undef) { } sub _xml_set_network($self, $doc, $network) { - my ($net_source) = $doc->findnodes('/domain/devices/interface/source'); - $net_source->setAttribute('network' => $network); + for my $net_source ( $doc->findnodes('/domain/devices/interface/source')) { + $net_source->setAttribute('network' => $network); + } } sub _xml_set_arch($self, $doc, $arch) { @@ -2984,8 +2992,11 @@ sub list_virtual_networks($self) { for my $net ($self->vm->list_all_networks()) { my $doc = XML::LibXML->load_xml(string => $net->get_xml_description); my ($ip_doc) = $doc->findnodes("/network/ip"); - my $ip = $ip_doc->getAttribute('address'); - my $netmask = $ip_doc->getAttribute('netmask'); + my ($ip, $netmask) = ('',''); + if ($ip_doc) { + $ip = $ip_doc->getAttribute('address'); + $netmask = $ip_doc->getAttribute('netmask'); + } my $data= { is_active => $net->is_active() ,autostart => $net->get_autostart() @@ -2996,13 +3007,15 @@ sub list_virtual_networks($self) { ,ip_netmask => $netmask ,internal_id => ''.$net->get_uuid_string }; - my ($dhcp_range) = $ip_doc->findnodes("dhcp/range"); - my ($start,$end); - if ($dhcp_range) { - $start = $dhcp_range->getAttribute('start'); - $end = $dhcp_range->getAttribute('end'); - $data->{dhcp_start} = $start if defined $start; - $data->{dhcp_end} = $end if defined $end; + if ($ip_doc) { + my ($dhcp_range) = $ip_doc->findnodes("dhcp/range"); + my ($start,$end); + if ($dhcp_range) { + $start = $dhcp_range->getAttribute('start'); + $end = $dhcp_range->getAttribute('end'); + $data->{dhcp_start} = $start if defined $start; + $data->{dhcp_end} = $end if defined $end; + } } push @networks,($data); } @@ -3019,7 +3032,16 @@ sub new_network($self, $name='net') { ); my $new = {ip_netmask => '255.255.255.0'}; for my $field ( keys %base) { - my %old = map { $_->{$field} => 1 } @networks; + my %old; + for my $current (@networks ) { + my $value = $current->{$field}; + $old{$value}=1; + if ($field eq 'ip_address') { + $value =~ s/(.*)\.\d+$/$1/; + $old{$value}=1; + } + + } my ($last) = reverse sort keys %old; my ($z,$n) = $last =~ /.*?(0*)(\d+)/; $z=$last if !defined $z; @@ -3033,12 +3055,17 @@ sub new_network($self, $name='net') { } my $value; for ( 0 .. 255 ) { + my $value_ip; if (ref($template)) { - $value = $template->[0].$n.$template->[2] + $value = $template->[0].$n.$template->[2]; + if ($field eq 'ip_address') { + $value_ip = $template->[0].$n; + } } else { $value = $template.$n; } - last if !exists $old{$value}; + last if !exists $old{$value} + && (!defined $value_ip || !exists $old{$value_ip}); $n++; } $new->{$field} = $value; diff --git a/lib/Ravada/VM/Void.pm b/lib/Ravada/VM/Void.pm index 5e562c56a..243675334 100644 --- a/lib/Ravada/VM/Void.pm +++ b/lib/Ravada/VM/Void.pm @@ -97,6 +97,10 @@ sub create_domain { my $id = delete $args{id}; my $storage = delete $args{storage}; + my $options = delete $args{options}; + my $network; + $network = $options->{network} if $options && exists $options->{network}; + my $domain = Ravada::Domain::Void->new( %args , domain => $args{name} @@ -110,7 +114,8 @@ sub create_domain { return $domain if $out && exists $args{config}; die "Error: Domain $args{name} already exists " if $out; - $domain->_set_default_info($listen_ip); + + $domain->_set_default_info($listen_ip, $network); $domain->_store( autostart => 0 ); $domain->_store( is_active => $active ); $domain->set_memory($args{memory}) if $args{memory}; @@ -167,6 +172,11 @@ sub create_domain { my $drivers = {}; $drivers = $domain_base->_value('drivers'); $domain->_store( drivers => $drivers ); + if ( $network ) { + for my $index ( 0 .. scalar(@{$clone_hw->{network}})-1) { + $domain->change_hardware('network', $index ,{name => $network}) + } + } } elsif (!exists $args{config}) { $storage = $self->default_storage_pool_name() if !$storage; my ($vda_name) = "$args{name}-vda-".Ravada::Utils::random_name(4).".void"; diff --git a/lib/Ravada/WebSocket.pm b/lib/Ravada/WebSocket.pm index 96ae5a663..b1398df17 100644 --- a/lib/Ravada/WebSocket.pm +++ b/lib/Ravada/WebSocket.pm @@ -238,8 +238,7 @@ sub _list_host_devices($rvd, $args) { my @found; while (my $row = $sth->fetchrow_hashref) { - $row->{devices} = decode_json($row->{devices}) if $row->{devices}; - $row->{_domains} = _list_domains_with_device($rvd, $row->{id}); + _list_domains_with_device($rvd, $row); push @found, $row; next unless _its_been_a_while_channel($args->{channel}); my $req = Ravada::Request->list_host_devices( @@ -250,17 +249,65 @@ sub _list_host_devices($rvd, $args) { return \@found; } -sub _list_domains_with_device($rvd,$id_hd) { - my $sth=$rvd->_dbh->prepare("SELECT d.name FROM domains d,host_devices_domain hdd" +sub _list_domains_with_device($rvd,$row) { + my $id_hd = $row->{id}; + + my %devices; + eval { + my $devices = decode_json($row->{devices}); + %devices = map { $_ => { name => $_ } } @$devices; + } if $row->{devices}; + my $sth=$rvd->_dbh->prepare("SELECT d.id,d.name,d.is_base, d.status, l.id, l.name " + ." FROM host_devices_domain hdd, domains d" + ." LEFT JOIN host_devices_domain_locked l" + ." ON d.id=l.id_domain " ." WHERE d.id= hdd.id_domain " ." AND hdd.id_host_device=?" + ." ORDER BY d.name" ); $sth->execute($id_hd); + my ( @domains, @bases); + while ( my ($id,$name,$is_base, $status, $is_locked, $device) = $sth->fetchrow ) { + $is_locked = 0 if !$is_locked || $status ne 'active'; + $device = '' if !$device; + my $domain = { id => $id ,name => $name, is_locked => $is_locked + ,is_base => $is_base ,device => $device + }; + $devices{$device}->{domain} = $domain if exists $devices{$device} && $is_locked; + if ($is_base) { + push @bases, ($domain); + } else { + push @domains, ($domain); + } + } + for my $dev ( values %devices ) { + _get_domain_with_device($rvd, $dev); + } + + $row->{_domains} = \@domains; + $row->{_bases} = \@bases; + $row->{devices} = [values %devices]; +} + +sub _get_domain_with_device($rvd, $dev) { + my $sql = + "SELECT d.id, d.name, d.is_base, d.status " + ." FROM host_devices_domain_locked l, domains d " + ." WHERE l.id_domain = d.id " + ." AND l.name=?" + ; + + my $sth = $rvd->_dbh->prepare($sql); + $sth->execute($dev->{name}); my @domains; - while ( my ($name) = $sth->fetchrow ) { - push @domains,($name); + while ( my ($id,$name,$is_base, $status, $is_locked, $device) = $sth->fetchrow ) { + $is_locked = 0 if !$is_locked || $status ne 'active'; + $device = '' if !$device; + my $domain = { id => $id ,name => $name, is_locked => $is_locked + ,is_base => $is_base ,device => $device + }; + $dev->{domain} = $domain;# if $is_locked; } - return \@domains; } sub _list_requests($rvd, $args) { diff --git a/public/js/ravada.js b/public/js/ravada.js index 258e43de5..34f2b1404 100644 --- a/public/js/ravada.js +++ b/public/js/ravada.js @@ -322,6 +322,7 @@ $scope.searching_ldap_attributes = true; $scope.shared_user_found=false; $scope.storage_pools=['default']; + $scope.shared_user_count = -1 $scope.getUnixTimeFromDate = function(date) { date = (date instanceof Date) ? date : date ? new Date(date) : new Date(); diff --git a/script/rvd_front b/script/rvd_front index fadf3e42c..23369e6ec 100644 --- a/script/rvd_front +++ b/script/rvd_front @@ -1074,13 +1074,20 @@ get '/machine/view/(:id).(:type)' => sub { my ($domain) = _search_requested_machine($c); return access_denied($c) if !$domain; - return access_denied($c) unless $USER->is_admin - || $domain->id_owner == $USER->id - || $USER->can_view_all - || $USER->can_start_machine($domain) - ; - - return view_machine($c); + return view_machine($c) if $USER->is_admin + || $USER->can_view_all; + + if ( $domain->id_owner == $USER->id || $USER->can_start_machine($domain) ) { + if ( $domain->id_base) { + my $base = Ravada::Front::Domain->open($domain->id_base); + if ($base->is_public || $base->show_clones()) { + return view_machine($c); + } + } else { + return view_machine($c); + } + } + return access_denied($c); }; any '/machine/clone/(:id).(:type)' => sub { @@ -1091,6 +1098,8 @@ any '/machine/clone/(:id).(:type)' => sub { if ( $USER && $USER->can_clone() && !$USER->is_temporary() ) { my $base = Ravada::Front::Domain->open($c->stash('id')); if (!$base->is_public) { + return access_denied($c) if !$base->show_clones; + my @clones = $base->clones(); my ($clone) = grep { $_->{id_owner} == $USER->id } @clones; return access_denied($c) if !$clone; diff --git a/t/30_request.t b/t/30_request.t index 9297b5248..ee9a51739 100644 --- a/t/30_request.t +++ b/t/30_request.t @@ -324,6 +324,22 @@ sub test_force() { } +sub test_refresh_vms() { + my $req = Ravada::Request->refresh_vms(); + ok($req); + wait_request( debug => 0); + is($req->error, '') or exit; + + $req->status('waiting'); + + my $req1 = Ravada::Request->refresh_vms(); + ok($req1); + is($req1->id, $req->id) or exit; + wait_request( debug => 0); + is($req1->error, '') or exit; + +} + sub test_dupe_open_exposed($vm) { my $req1 = Ravada::Request->open_exposed_ports( id_domain => 1 @@ -351,6 +367,8 @@ remove_old_disks(); test_force(); +test_refresh_vms(); + for my $vm_name ( vm_names() ) { my $vm; eval { @@ -379,6 +397,11 @@ for my $vm_name ( vm_names() ) { my $domain_base = test_req_create_base($vm); if ($domain_base) { + my $req_rm = Ravada::Request->remove_clones( + uid => user_admin->id + ,id_domain => $domain_base->id + ,at => time + 300 + ); $domain_base->is_public(1); is ($domain_base->_vm->readonly, 0) or next; @@ -392,6 +415,9 @@ for my $vm_name ( vm_names() ) { is(scalar @{rvd_front->list_domains( id => $domain_clone->id)}, 0) or exit; test_req_many_clones($vm, $domain_base); + is($req_rm->status,'requested'); + $req_rm->at(time + 1); + wait_request(debug => 1); test_req_remove_domain_name($vm, $domain_base->name); } diff --git a/t/device/10_templates.t b/t/device/10_templates.t index 0776258f5..b74df5022 100644 --- a/t/device/10_templates.t +++ b/t/device/10_templates.t @@ -583,14 +583,102 @@ sub _mangle_dom_hd_kvm($domain) { $domain->reload_config($xml); } -sub test_templates_change_devices($vm) { - return if $vm->type ne 'Void'; +sub _create_host_devices($vm, $n) { + if ($vm->type eq 'Void') { + return _create_host_devices_void($vm,$n); + } elsif ($vm->type eq 'KVM') { + return _create_host_devices_kvm($vm,$n); + } else { + die "Error: I don't know how to create host devices for ".$vm->type; + } +} + +sub _create_host_devices_void($vm, $n) { + my $path = "/var/tmp/$type); + ok(@$templates); + + my ($template) = grep { $_->{list_command} =~ /lspci/ } @$templates; + + my @hds; + for ( 1 .. $n ) { + my $id_hd = $vm->add_host_device(template => $template->{name}); + my $hd = Ravada::HostDevice->search_by_id($id_hd); + + my $config = config_host_devices('pci'); + $hd->_data('list_filter' => $config); + push @hds,($hd); + } + + return @hds; + +} + +sub test_frontend_list($vm) { + + my ($hd1, $hd2) = _create_host_devices($vm, 2); + + if (scalar($hd1->list_devices) != scalar($hd2->list_devices)) { + die "Error: expecting the same count of devices in both mock hds"; + } + + my $domain = _create_domain_hd($vm, $hd1); + $domain->start(user_admin); + + my $ws_args = { + channel => '/'.$vm->id + ,login => user_admin->name + }; + my $front_devices = Ravada::WebSocket::_list_host_devices(rvd_front(), $ws_args); + is(scalar(@$front_devices),2) or exit; + + my ($dev_attached) = ($domain->list_host_devices_attached); + + my $found=0; + for my $fd ( @$front_devices ) { + for my $dev ( @{$fd->{devices}} ) { + if ($dev->{name} eq $dev_attached->{name}) { + ok($dev->{domain} , "Expecting domains listed in ".$dev->{name}) or next; + is($dev->{domain}->{id}, $domain->id,"Expecting ".$domain->name." attached in ".$dev->{name}); + $found++ if $dev->{domain}->{id} == $domain->id; + } + } + } + is($found,2) or die Dumper($front_devices); + + remove_domain($domain); + + _remove_host_devices($vm); +} + +sub _mock_hd($vm, $path) { + + my ($template, $name) = _mock_devices($vm , $path); + + my $id_hd = $vm->add_host_device(template => $template->{name}); + my $hd = Ravada::HostDevice->search_by_id($id_hd); + + $hd->_data(list_command => "ls $path"); + $hd->_data(list_filter => $name); + + return $hd; +} + +sub _mock_devices($vm, $path) { my $templates = Ravada::HostDevice::Templates::list_templates($vm->type); ok(@$templates); my ($template) = grep { $_->{list_command} eq 'lsusb' } @$templates; - my $path = "/var/tmp/$type ne 'Void'; + + my $path = "/var/tmp/$add_host_device(template => $template->{name}); my ($hostdev) = $vm->list_host_devices(); $hostdev->_data(list_command => "ls $path"); @@ -666,7 +763,11 @@ sub test_templates_change_filter($vm) { $domain->remove(user_admin); } + _remove_host_devices($vm); +} + +sub _remove_host_devices($vm) { for my $hd ( $vm->list_host_devices ) { my $req = Ravada::Request->remove_host_device( uid => user_admin->id @@ -676,7 +777,6 @@ sub test_templates_change_filter($vm) { is($req->status,'done'); is($req->error, '') or exit; } - } sub test_templates($vm) { @@ -705,8 +805,11 @@ sub test_templates($vm) { _fix_host_device($host_device) if $vm->type eq 'KVM'; + warn 11; test_hd_in_domain($vm, $host_device); + warn 12; test_hd_dettach($vm, $host_device); + warn 13; my $req = Ravada::Request->list_host_devices( uid => user_admin->id @@ -833,13 +936,26 @@ for my $vm_name ( vm_names()) { diag("Testing host devices in $vm_name"); + test_frontend_list($vm); + + warn 1; + test_templates_gone_usb_2($vm); + + warn 2; + test_templates_gone_usb($vm); + warn 3; test_templates_changed_usb($vm); + warn 4; test_templates_start_nohd($vm); + warn 5; test_templates_change_filter($vm); + + warn 6; test_templates($vm); + warn 7; test_templates_change_devices($vm); } diff --git a/t/front/40_list_domains.t b/t/front/40_list_domains.t index 9068fa289..248a1eb1a 100644 --- a/t/front/40_list_domains.t +++ b/t/front/40_list_domains.t @@ -187,6 +187,75 @@ sub test_list_bases_many_clones($vm) { } +sub test_list_bases_show_clones($vm) { + my $base = create_domain($vm); + + my $list = rvd_front->list_machines_user(user_admin); + + $base->prepare_base(user_admin); + + my $clone1 = $base->clone(user => user_admin + , name => new_domain_name); + + $list = rvd_front->list_machines_user(user_admin); + my ($entry) = grep { $_->{id} == $base->id} @$list; + + ok($entry); + + $base->is_public(0); + $base->show_clones(1); + + $list = rvd_front->list_machines_user(user_admin); + ($entry) = grep { $_->{id} == $base->id} @$list; + ok($entry); + + $base->show_clones(0); + + $list = rvd_front->list_machines_user(user_admin); + ($entry) = grep { $_->{id} == $base->id} @$list; + ok($entry); + + my $user = create_user(); + $list = rvd_front->list_machines_user($user); + ($entry) = grep { $_->{id} == $base->id} @$list; + ok(!$entry); + + $base->is_public(1); + $list = rvd_front->list_machines_user($user); + ($entry) = grep { $_->{id} == $base->id} @$list; + ok($entry); + + is(scalar(@{$entry->{list_clones}}),0); + + my $clone2 = $base->clone(user => $user + , name => new_domain_name); + + $list = rvd_front->list_machines_user($user); + ($entry) = grep { $_->{id} == $base->id} @$list; + ok($entry); + ok($entry->{list_clones}->[0]); + is($entry->{list_clones}->[0]->{name},$clone2->name) or die Dumper($entry); + is($entry->{list_clones}->[0]->{id},$clone2->id) or die Dumper($entry); + + $base->is_public(0); + $base->show_clones(1); + + $list = rvd_front->list_machines_user($user); + ($entry) = grep { $_->{id} == $base->id} @$list; + ok($entry) or die Dumper($list); + + ok($entry->{list_clones}->[0]);+ is($entry->{list_clones}->[0]->{name},$clone2->name) or die Dumper($entry); + is($entry->{list_clones}->[0]->{id},$clone2->id) or die Dumper($entry); + + $base->show_clones(0); + + $list = rvd_front->list_machines_user($user); + ($entry) = grep { $_->{id} == $base->id} @$list; + ok(!$entry); + + remove_domain($base); +} + ######################################################### remove_old_domains(); @@ -219,6 +288,8 @@ for my $vm_name (reverse sort @VMS) { use_ok($CLASS); + test_list_bases_show_clones($vm); + test_list_bases_many_clones($vm); my $domain = test_create_domain($vm_name); diff --git a/t/kvm/pcie.t b/t/kvm/pcie.t new file mode 100644 index 000000000..6bb7fa5e7 --- /dev/null +++ b/t/kvm/pcie.t @@ -0,0 +1,261 @@ +use warnings; +use strict; + +use Carp qw(confess); +use Data::Dumper; +use Test::More; + +use lib 't/lib'; +use Test::Ravada; + +no warnings "experimental::signatures"; +use feature qw(signatures); + +################################################################# + +sub test_pcie($vm) { + my $base = create_domain_v2(vm => $vm, id_iso => search_id_iso('Alpine%64')); + $base->prepare_base(user_admin); + + my $clone = Ravada::Request->clone( + id_domain => $base->id + ,uid => user_admin->id + ); + wait_request(); +} + +sub test_pcie_2($vm) { + + my $name = new_domain_name(); + my $device_disk = $vm->create_volume( + name => $name + ,size => 1024 * 1024 + ,xml => "etc/xml/dsl-volume.xml"); + + my $string=< + $name + 55b671a7-77c8-4447-b21e-1771055f8bff + 135168 + 135168 + 1 + + /machine + + + + hostname: $name + + + + hvm + /usr/share/OVMF/OVMF_CODE_4M.fd + /var/lib/libvirt/qemu/nvram/$name.fd + + + + + + + + qemu64 + + + + + + + + + + + destroy + restart + destroy + + + + + + /usr/bin/kvm-spice + + + + + + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + + +
+ + + +
+ + + +
+ + + + + + + +
+ + + + + + + + + + + + + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + +
+ +