diff --git a/lib/Ravada.pm b/lib/Ravada.pm index 9f416f54b..5078253fe 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -1641,6 +1641,12 @@ sub _add_indexes_generic($self) { ,"UNIQUE (name)" ] + ,virtual_networks => [ + "unique(id_vm,internal_id)" + ,"unique(id_vm,name)" + ,"index(date_changed)" + ,"index(id_owner)" + ] ); my $if_not_exists = ''; $if_not_exists = ' IF NOT EXISTS ' if $CONNECTOR->dbh->{Driver}{Name} =~ /sqlite|mariadb/i; @@ -1812,6 +1818,8 @@ sub _add_grants($self) { $self->_add_grant('view_all',0,"The user can start and access the screen of any virtual machine"); $self->_add_grant('create_disk',0,'can create disk volumes'); $self->_add_grant('quota_disk',0,'disk space limit',1); + $self->_add_grant('create_networks',0,'can create virtual networks.'); + $self->_add_grant('manage_all_networks',0,'can manage all the virtual networks.'); } sub _add_grant($self, $grant, $allowed, $description, $is_int = 0, $default_admin=1) { @@ -1889,6 +1897,7 @@ sub _enable_grants($self) { ,'start_limit', 'start_many' ,'view_all' ,'create_disk', 'quota_disk' + ,'create_networks','manage_all_networks' ); my $sth = $CONNECTOR->dbh->prepare("SELECT id,name FROM grant_types"); @@ -2362,6 +2371,24 @@ sub _sql_create_tables($self) { } ] + , + [virtual_networks => { + id => 'integer PRIMARY KEY AUTO_INCREMENT', + ,id_vm => 'integer NOT NULL references `vms` (`id`) ON DELETE CASCADE', + ,name => 'varchar(200)' + ,id_owner => 'integer NOT NULL references `users` (`id`) ON DELETE CASCADE', + ,internal_id => 'char(80) not null' + ,autostart => 'integer not null' + ,bridge => 'char(80)' + ,'ip_address' => 'char(20)' + ,'ip_netmask' => 'char(20)' + ,'dhcp_start' => 'char(15)' + ,'dhcp_end' => 'char(15)' + ,'is_active' => 'integer not null default 1' + ,'is_public' => 'integer not null default 0' + ,date_changed => 'timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP' + } + ] ); for my $new_table (@tables ) { @@ -5111,7 +5138,12 @@ sub _cmd_change_hardware { my $user = Ravada::Auth::SQL->search_by_id($uid); die "Error: User ".$user->name." not allowed\n" - if $hardware ne 'memory' && !$user->is_admin; + unless $user->is_admin + || $hardware eq 'memory' + || ($hardware eq 'network' + && $user->can_change_hardware_network($domain, $data) + ) + ; $domain->change_hardware( $request->args('hardware') @@ -5810,6 +5842,7 @@ sub _refresh_active_vms ($self) { next; } $active_vm{$vm->id} = 1; + $vm->list_virtual_networks(); } return \%active_vm; } @@ -6304,6 +6337,12 @@ sub _req_method { ,move_volume => \&_cmd_move_volume ,update_iso_urls => \&_cmd_update_iso_urls + ,list_networks => \&_cmd_list_virtual_networks + ,new_network => \&_cmd_new_network + ,create_network => \&_cmd_create_network + ,remove_network => \&_cmd_remove_network + ,change_network => \&_cmd_change_network + ); return $methods{$cmd}; } @@ -6723,6 +6762,106 @@ sub _cmd_remove_files($self, $request) { $vm->remove_file(@file); } +sub _cmd_list_virtual_networks($self, $request) { + my $user=Ravada::Auth::SQL->search_by_id($request->args('uid')); + die "Error: ".$user->name." not authorized\n" + unless $user->is_admin || $user->can_manage_all_networks || $user->can_create_networks; + + my $id = $request->args('id_vm') or die "Error: missing id_vm"; + my $vm = Ravada::VM->open($id); + my @list = $vm->list_virtual_networks(); + + $request->output(encode_json(\@list)); +} + + +sub _cmd_new_network($self, $request) { + my $user=Ravada::Auth::SQL->search_by_id($request->args('uid')); + die "Error: ".$user->name." not authorized\n" + unless $user->can_create_networks; + + $request->output(encode_json({})); + + my $id = $request->args('id_vm') or die "Error: missing id_vm"; + my $vm = Ravada::VM->open($id); + my $name = ($request->defined_arg('name') or 'net'); + + my $new = $vm->new_network($name); + $new = {} if !$new; + + $request->output(encode_json( $new)); +} + +sub _cmd_create_network($self, $request) { + my $user=Ravada::Auth::SQL->search_by_id($request->args('uid')); + die "Error: ".$user->name." not authorized\n" + unless $user->can_create_networks || $user->can_manage_all_networks; + + my $id = $request->args('id_vm') or die "Error: missing id_vm"; + my $vm = Ravada::VM->open($id); + $request->output(encode_json({})); + my $id_net = $vm->create_network($request->args('data'),$request->args('uid') + , $request); + $request->output(encode_json({id_network => $id_net})); +} + +sub _cmd_remove_network($self, $request) { + + my $id = $request->args('id'); + my $name = $request->defined_arg('name'); + my $sth_net = $CONNECTOR->dbh->prepare( + "SELECT * FROM virtual_networks WHERE id=?" + ); + $sth_net->execute($id); + my $network = $sth_net->fetchrow_hashref; + if ($network && $network->{id} ) { + _check_user_authorized_network($request, $id); + } + my $id_vm = ( $network->{id_vm} or $request->defined_arg('id_vm') ); + die "Error: unknown id_vm ".Dumper([$network,$request]) if !$id_vm; + + die "Error: unkonwn network , id=$id, name='".($name or '')."' " + .Dumper($network) if ! $network->{id} && !$name; + + my $user=Ravada::Auth::SQL->search_by_id($request->args('uid')); + + my $vm = Ravada::VM->open($id_vm); + $vm->remove_network($user, ($network->{id} or $name)); +} + +sub _check_user_authorized_network($request, $id_network) { + + my $user=Ravada::Auth::SQL->search_by_id($request->args('uid')); + + my $sth = $CONNECTOR->dbh->prepare( + "SELECT * FROM virtual_networks WHERE id=?" + ); + $sth->execute($id_network); + my $network = $sth->fetchrow_hashref; + + confess "Error: network $id_network not found" if !$network->{id}; + + die "Error: ".$user->name." not authorized\n" + unless $user->is_admin + || $user->can_manage_all_networks + || ( $user->can_create_networks && $network->{id_owner} == $user->id); + + return $network; +} + +sub _cmd_change_network($self, $request) { + + my $data = $request->args('data'); + die "Error: network.id required" if !exists $data->{id} || !$data->{id}; + + my $network = _check_user_authorized_network($request, $data->{id}); + + $data->{internal_id} = $network->{internal_id} if !$data->{internal_id}; + my $vm = Ravada::VM->open($network->{id_vm}); + + $vm->change_network($data, $request->args('uid')); +} + sub _cmd_active_storage_pool($self, $request) { my $user = Ravada::Auth::SQL->search_by_id($request->args('uid')); die "Error: ".$user->name." not authorized to manage storage pools" diff --git a/lib/Ravada/Auth/SQL.pm b/lib/Ravada/Auth/SQL.pm index fb39df37e..c9d80f6e2 100644 --- a/lib/Ravada/Auth/SQL.pm +++ b/lib/Ravada/Auth/SQL.pm @@ -378,6 +378,7 @@ sub is_operator { || $self->can_view_groups() || $self->can_manage_groups() || $self->can_view_all() + || $self->can_create_networks() ; return 0; } @@ -635,9 +636,25 @@ sub remove($self) { my $sth = $$CON->dbh->prepare("DELETE FROM grants_user where id_user=?"); $sth->execute($self->id); + $self->_remove_networks(); + $sth = $$CON->dbh->prepare("DELETE FROM users where id=?"); $sth->execute($self->id); $sth->finish; + +} + +sub _remove_networks($self) { + my $sth = $$CON->dbh->prepare("SELECT id,id_vm,name FROM virtual_networks WHERE id_owner=?"); + $sth->execute($self->id); + while (my ($id, $id_vm, $name) = $sth->fetchrow) { + Ravada::Request->remove_network( + uid => Ravada::Utils::user_daemon->id + ,id_vm => $id_vm + ,id => $id + ,name => $name + ); + } } =head2 can_do @@ -1235,6 +1252,46 @@ sub disk_used($self) { return $used; } +sub _load_network($network) { + confess "Error: undefined network" + if !defined $network; + + my $sth = $$CON->dbh->prepare( + "SELECT * FROM virtual_networks where name=?" + ); + $sth->execute($network); + my $row = $sth->fetchrow_hashref; + + die "Error: network '$network' not found" + if !$row->{id}; + + lock_hash(%$row); + return $row; +} + +=head2 can_change_hardware_network + +Returns true if the user can change the network in a virtual machine, +false elsewhere + +=cut + +sub can_change_hardware_network($user, $domain, $data) { + return 1 if $user->is_admin; + return 1 if $user->can_manage_all_networks() + && $domain->id_owner == $user->id; + + confess "Error: undefined network ".Dumper($data) + if !exists $data->{network} || !defined $data->{network}; + + my $net = _load_network($data->{network}); + + return 1 if $user->id == $domain->id_owner + && ( $net->{is_public} || $user->id == $net->{id_owner}); + return 0; +} + + sub AUTOLOAD($self, $domain=undef) { my $name = $AUTOLOAD; diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 03d2c252d..37e533f13 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -344,7 +344,7 @@ sub _around_start($orig, $self, @arg) { if ( Ravada::setting(undef,"/backend/display_password") ) { # We'll see if we set it from the network, defaults to 0 meanwhile my $set_password = 0; - my $network = Ravada::Network->new(address => $remote_ip); + my $network = Ravada::Route->new(address => $remote_ip); $set_password = 1 if $network->requires_password(); $arg{set_password} = $set_password; } diff --git a/lib/Ravada/Domain/Void.pm b/lib/Ravada/Domain/Void.pm index 20f0b4303..7a07f59b2 100644 --- a/lib/Ravada/Domain/Void.pm +++ b/lib/Ravada/Domain/Void.pm @@ -758,6 +758,8 @@ sub _set_default_info($self, $listen_ip=undef) { hwaddr => $info->{mac} ,address => $info->{ip} ,type => 'nat' + ,driver => 'virtio' + ,name => "net1" }; $self->_store(hardware => $hardware ); @@ -921,6 +923,18 @@ sub _internal_autostart { return $self->_value('autostart'); } +sub _new_network($self) { + my $hardware = $self->_value('hardware'); + my $list = ( $hardware->{'network'} or [] ); + my $data = { + hwaddr => _new_mac() + ,address => '' + ,type => 'nat' + ,driver => 'virtio' + ,name => "net".(scalar(@$list)+1) + }; +} + sub set_controller($self, $name, $number=undef, $data=undef) { my $hardware = $self->_value('hardware'); @@ -939,12 +953,16 @@ sub set_controller($self, $name, $number=undef, $data=undef) { my @list2; if (!defined $number) { @list2 = @$list; - push @list2,($data or " $name z 1"); + $data = $self->_new_network() if $name eq 'network' && !$data; + push @list2,($data or "$name z 1"); } else { my $count = 0; for my $item ( @$list ) { $count++; if ($number == $count) { + $data = $self->_new_network() + if $name eq 'network' && (!$data || ! keys %$data); + my $data2 = ( $data or " $name a ".($count+1)); $data2 = " $name b ".($count+1) if defined $data2 && ref($data2) && !keys %$data2; @@ -953,7 +971,7 @@ sub set_controller($self, $name, $number=undef, $data=undef) { } $item = { driver => 'spice' , port => 'auto' , listen_ip => $self->_vm->listen_ip } if $name eq 'display' && !defined $item; - push @list2,($item or " $name b ".($count+1)); + push @list2,($item or " $name c ".($count+1)); } } $hardware->{$name} = \@list2; diff --git a/lib/Ravada/Front.pm b/lib/Ravada/Front.pm index 945071d84..fd3e8639e 100644 --- a/lib/Ravada/Front.pm +++ b/lib/Ravada/Front.pm @@ -21,7 +21,7 @@ use Ravada; use Ravada::Auth::LDAP; use Ravada::Front::Domain; use Ravada::Front::Domain::KVM; -use Ravada::Network; +use Ravada::Route; use feature qw(signatures); no warnings "experimental::signatures"; @@ -1201,7 +1201,7 @@ sub list_bases_anonymous { my $self = shift; my $ip = shift or confess "Missing remote IP"; - my $net = Ravada::Network->new(address => $ip); + my $net = Ravada::Route->new(address => $ip); my $sth = $CONNECTOR->dbh->prepare( "SELECT id, alias, name, id_base, is_public, file_screenshot " @@ -1697,6 +1697,63 @@ sub list_storage_pools($self, $uid, $id_vm, $active=undef) { return _filter_active($pools, $active); } +=head2 list_networks + +List the virtual networks for a Virtual Machine Manager + +Arguments: id vm , id user + +Returns: list ref of networks + +=cut + +sub list_networks($self, $id_vm ,$id_user) { + my $query = "SELECT * FROM virtual_networks " + ." WHERE id_vm=?"; + + my $user = Ravada::Auth::SQL->search_by_id($id_user); + my $owned = 0; + unless ($user->is_admin || $user->can_manage_all_networks) { + $query .= " AND ( id_owner=? or is_public=1) "; + $owned = 1; + } + $query .= " ORDER BY name"; + my $sth = $CONNECTOR->dbh->prepare($query); + if ($owned) { + $sth->execute($id_vm, $id_user); + } else { + $sth->execute($id_vm); + } + my @networks; + my %owner; + while ( my $row = $sth->fetchrow_hashref ) { + $self->_search_user($row->{id_owner},\%owner); + $row->{_owner} = $owner{$row->{id_owner}}; + $row->{_can_change}=0; + + $row->{_can_change}=1 + if $user->is_admin || $user->can_manage_all_networks + || ($user->can_create_networks && $user->id == $row->{id_owner}); + + push @networks,($row); + } + return \@networks; +} + +sub _search_user($self,$id, $users) { + return if $users->{$id}; + + my $sth = $self->_dbh->prepare( + "SELECT * FROM users WHERE id=?" + ); + $sth->execute($id); + my $row = $sth->fetchrow_hashref(); + for my $field (keys %$row) { + delete $row->{$field} if $field =~ /passw/; + } + $users->{$id}=$row; +} + sub _filter_active($pools, $active) { return $pools if !defined $active; diff --git a/lib/Ravada/Front/Domain/Void.pm b/lib/Ravada/Front/Domain/Void.pm index beff8f98b..95440df67 100644 --- a/lib/Ravada/Front/Domain/Void.pm +++ b/lib/Ravada/Front/Domain/Void.pm @@ -13,6 +13,7 @@ our %GET_CONTROLLER_SUB = ( 'mock' => \&_get_controller_mock ,'disk' => \&_get_controller_disk ,'display' => \&_get_controller_display + ,'network' => \&_get_controller_network ); @@ -86,4 +87,14 @@ sub _get_controller_display(@args) { return Ravada::Front::Domain::_get_controller_display(@args); } +sub _get_controller_generic($self, $item) { + my $hardware = $self->_value('hardware'); + return () if !exists $hardware->{$item}; + return @{$hardware->{$item}}; +} + +sub _get_controller_network($self) { + return $self->_get_controller_generic('network'); +} + 1; diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm index c215b80f2..4040031bb 100644 --- a/lib/Ravada/Request.pm +++ b/lib/Ravada/Request.pm @@ -167,6 +167,13 @@ our %VALID_ARG = ( ,list_unused_volumes => {uid => 1, id_vm => 1, start => 2, limit => 2 } ,remove_files => { uid => 1, id_vm => 1, files => 1 } ,move_volume => { uid => 1, id_domain => 1, volume => 1, storage => 1 } + + ,list_networks => { uid => 1, id_vm => 1} + ,new_network => { uid => 1, id_vm => 1, name => 2 } + ,create_network => { uid => 1, id_vm => 1, data => 1 } + ,remove_network => { uid => 1, id => 1, id_vm => 2, name => 2 } + ,change_network => { uid => 1, data => 1 } + ); $VALID_ARG{shutdown} = $VALID_ARG{shutdown_domain}; @@ -183,6 +190,8 @@ our %CMD_SEND_MESSAGE = map { $_ => 1 } shutdown_node reboot_node start_node compact purge start_domain + + create_network change_network remove_network ); our %CMD_NO_DUPLICATE = map { $_ => 1 } diff --git a/lib/Ravada/Network.pm b/lib/Ravada/Route.pm similarity index 96% rename from lib/Ravada/Network.pm rename to lib/Ravada/Route.pm index 129eec17f..10d4e14c0 100644 --- a/lib/Ravada/Network.pm +++ b/lib/Ravada/Route.pm @@ -1,11 +1,11 @@ -package Ravada::Network; +package Ravada::Route; use strict; use warnings; =head1 NAME -Ravada::Network - Networks management library for Ravada +Ravada::Route - Routes management library for Ravada =cut @@ -31,7 +31,7 @@ our $CONNECTOR; =head1 Description - my $net = Ravada::Network->new(address => '127.0.0.1/32'); + my $net = Ravada::Route->new(address => '127.0.0.1/32'); if ( $net->allowed( $domain->id ) ) { =cut diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index eee2ebd54..7bd1d023c 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -25,6 +25,7 @@ use Net::OpenSSH; use IO::Socket; use IO::Interface; use Net::Domain qw(hostfqdn); +use Storable qw(dclone); use Ravada::HostDevice; use Ravada::Utils; @@ -129,6 +130,13 @@ has 'netssh' => ( , lazy => 1 , clearer => 'clear_netssh' ); + +has 'has_networking' => ( + isa => 'Bool' + , is => 'ro' + , default => 0 +); + ############################################################ # # Method Modifiers definition @@ -147,6 +155,12 @@ around 'ping' => \&_around_ping; around 'connect' => \&_around_connect; after 'disconnect' => \&_post_disconnect; +around 'new_network' => \&_around_new_network; +around 'create_network' => \&_around_create_network; +around 'remove_network' => \&_around_remove_network; +around 'list_virtual_networks' => \&_around_list_networks; +around 'change_network' => \&_around_change_network; + around 'copy_file_storage' => \&_around_copy_file_storage; ############################################################# @@ -191,6 +205,8 @@ sub open { } else { $args{id} = shift; } + confess "Error: undefind id in ".Dumper(\%args) if !$args{id}; + my $class=ref($proto) || $proto; my $self = {}; @@ -606,7 +622,7 @@ sub _add_instance_db($self, $id_domain) { sub _define_spice_password($self, $remote_ip) { my $spice_password = Ravada::Utils::random_name(4); if ($remote_ip) { - my $network = Ravada::Network->new(address => $remote_ip); + my $network = Ravada::Route->new(address => $remote_ip); $spice_password = undef if !$network->requires_password; } return $spice_password; @@ -1433,6 +1449,183 @@ sub _around_ping($orig, $self, $option=undef, $cache=1) { return $ping; } +sub _insert_network($self, $net) { + delete $net->{id}; + $net->{id_owner} = Ravada::Utils::user_daemon->id + if !exists $net->{id_owner}; + + $net->{id_vm} = $self->id; + $net->{is_public}=1 if !exists $net->{is_public}; + + my @fields = grep !/^_/, sort keys %$net; + + my $sql = "INSERT INTO virtual_networks (" + .join(",",@fields).")" + ." VALUES(".join(",",map { '?' } @fields).")"; + my $sth = $self->_dbh->prepare($sql); + $sth->execute(map { $net->{$_} } @fields); + + $net->{id} = Ravada::Utils::last_insert_id($$CONNECTOR->dbh); +} + +sub _update_network($self, $net) { + my $sth = $self->_dbh->prepare("SELECT * FROM virtual_networks " + ." WHERE id_vm=? AND ( internal_id=? OR name = ?)" + ); + $sth->execute($self->id,$net->{internal_id}, $net->{name}); + my $db_net = $sth->fetchrow_hashref(); + if (!$db_net || !$db_net->{id}) { + $self->_insert_network($net); + } else { + $self->_update_network_db($db_net, $net); + $net->{id} = $db_net->{id}; + } + delete $db_net->{date_changed}; + # delete $db_net->{id}; +} + +sub _update_network_db($self, $old, $new0) { + my $new = dclone($new0); + my $id = $old->{id} or confess "Missing id"; + my $sql = ""; + for my $field (sort keys %$new) { + if ( exists $old->{$field} && defined $old->{$field} + && exists $new->{$field} && defined $new->{$field} + && $new->{$field} eq $old->{$field}) { + delete $new->{$field}; + next; + } + $sql .= ", " if $sql; + $sql .=" $field=? " + } + if ($sql) { + $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); + } +} + +sub _around_new_network($orig, $self, $name) { + my $data = $self->$orig($name); + $data->{id_vm} = $self->id; + $data->{is_active}=1; + $data->{autostart}=1; + return $data; +} + +sub _around_create_network($orig, $self,$data, $id_owner, $request=undef) { + my $owner = Ravada::Auth::SQL->search_by_id($id_owner) or confess "Unknown user id: $id_owner"; + die "Error: Access denied: ".$owner->name." can not create networks" + unless $owner->can_create_networks(); + + for my $field (qw(name bridge)) { + next if !$data->{$field}; + my ($found) = grep { $_->{$field} eq $data->{$field} } + $self->list_virtual_networks(); + + die "Error: network $field=$data->{$field} already exists\n" + if $found; + } + + $data->{is_active}=0 if !exists $data->{is_active}; + $data->{autostart}=0 if !exists $data->{autostart}; + my $ip = $data->{ip_address} or die "Error: missing ip address"; + $ip =~ s/(.*)\..*/$1/; + $data->{dhcp_start}="$ip.2" if !exists $data->{dhcp_start}; + $data->{dhcp_end}="$ip.254" if !exists $data->{dhcp_end}; + delete $data->{_update}; + delete $data->{internal_id} if exists $data->{internal_id}; + eval { $self->$orig($data) }; + $request->error(''.$@) if $@ && $request; + $data->{id_owner} = $id_owner; + $data->{is_public} = 0 if !$data->{is_public}; + $self->_insert_network($data); +} + +sub _around_remove_network($orig, $self, $user, $id_net) { + die "Error: Access denied: ".$user->name." can not remove networks" + unless $user->can_create_networks(); + + confess "Error: undefined network" if !defined $id_net; + + my $name = $id_net; + if ($id_net =~ /^\d+$/) { + my ($net) = grep { $_->{id} eq $id_net } $self->list_virtual_networks(); + die "Error: network id $id_net not found" if !$net; + $name = $net->{name}; + } + + + $self->$orig($name); + + my $sth = $self->_dbh->prepare("DELETE FROM virtual_networks WHERE id=?"); + $sth->execute($id_net); +} + +sub _around_change_network($orig, $self, $data, $uid) { + delete $data->{date_changed}; + + for my $field (keys %$data) { + delete $data->{$field} if $field =~ /^_/; + } + + my $data2 = dclone($data); + + my $id_owner = delete $data->{id_owner}; + + my $id = delete $data2->{id}; + my $sth0 = $self->_dbh->prepare("SELECT * FROM virtual_networks WHERE id=?"); + $sth0->execute($id); + my $old = $sth0->fetchrow_hashref; + + # this field is only in DB + delete $data->{is_public}; + my $changed = $orig->($self, $data); + + my $user=Ravada::Auth::SQL->search_by_id($uid); + + $self->_update_network_db($old, $data2); + +} + +sub _check_networks($self) { } + +sub _around_list_networks($orig, $self) { + my $sth_previous = $self->_dbh->prepare( + "SELECT count(*) FROM virtual_networks " + ." WHERE id_vm=?" + ); + $sth_previous->execute($self->id); + my ($count) = $sth_previous->fetchrow; + my $first_time = !$count; + + my @list = $orig->($self); + for my $net ( @list ) { + $net->{id_vm}=$self->id; + $self->_update_network($net); + } + my $sth = $self->_dbh->prepare( + "SELECT id,name,id_owner,is_public FROM virtual_networks " + ." WHERE id_vm=?"); + my $sth_delete = $self->_dbh->prepare("DELETE FROM virtual_networks where id=?"); + $sth->execute($self->id); + while (my ($id, $name, $id_owner, $is_public) = $sth->fetchrow) { + my ($found) = grep {$_->{name} eq $name } @list; + if ( $found ) { + $found->{id_owner} = $id_owner; + $found->{id} = $id; + $found->{is_public} = $is_public; + next; + } + $sth_delete->execute($id); + } + + $self->_check_networks() if $first_time; + + return @list; +} + =head2 is_active Returns if the domain is active. The active state is cached for some seconds. @@ -2343,36 +2536,20 @@ sub list_network_interfaces($self, $type) { sub _list_nat_interfaces($self) { - my @cmd = ( '/usr/bin/virsh','net-list'); - my ($out,$err) = $self->run_command(@cmd); - - my @lines = split /\n/,$out; - shift @lines; - shift @lines; - + my @nets = $self->list_virtual_networks(); my @networks; - for (@lines) { - /\s*(.*?)\s+.*/; - push @networks,($1) if $1; + for my $net (@nets) { + push @networks,($net->{name}) if $net->{name}; } return @networks; } -sub _get_nat_bridge($self, $net) { - my @cmd = ( '/usr/bin/virsh','net-info', $net); - my ($out,$err) = $self->run_command(@cmd); - - for my $line (split /\n/, $out) { - my ($bridge) = $line =~ /^Bridge:\s+(.*)/; - return $bridge if $bridge; - } -} - sub _list_qemu_bridges($self) { my %bridge; - my @networks = $self->_list_nat_interfaces(); + my @networks = $self->list_virtual_networks(); for my $net (@networks) { - my $nat_bridge = $self->_get_nat_bridge($net); + next if !$net->{bridge}; + my $nat_bridge = $net->{bridge}; $bridge{$nat_bridge}++; } return keys %bridge; @@ -2680,6 +2857,39 @@ sub _around_copy_file_storage($orig, $self, $file, $storage) { return $new_file; } +sub _list_files_local($self, $dir, $pattern) { + opendir my $ls,$dir or die "$! $dir"; + my @list; + while (my $file = readdir $ls) { + next if defined $pattern && $file !~ $pattern; + push @list,($file); + } + return @list; +} + +sub _list_files_remote($self, $dir, $pattern) { + my @cmd=("ls",$dir); + my ($out, $err) = $self->run_command(@cmd); + my @list; + for my $file (split /\n/,$out) { + push @list,($file) if !defined $pattern || $file =~ $pattern; + } + return @list; +} + +=head2 list_files + +List files in a Virtual Manager + +Arguments: dir , optional regexp pattern + +=cut + +sub list_files($self, $dir, $pattern=undef) { + return $self->_list_files_local($dir, $pattern) if $self->is_local; + return $self->_list_files_remote($dir, $pattern); +} + 1; diff --git a/lib/Ravada/VM/KVM.pm b/lib/Ravada/VM/KVM.pm index c46d02ced..ff0dbe70f 100644 --- a/lib/Ravada/VM/KVM.pm +++ b/lib/Ravada/VM/KVM.pm @@ -55,6 +55,12 @@ has type => ( ,default => 'KVM' ); +has has_networking => ( + isa => 'Bool' + , is => 'ro' + , default => 1 +); + #########################################################################3 # @@ -119,7 +125,6 @@ sub _connect { }; confess $@ if $@; } - $self->_check_networks($vm); return $vm; } @@ -143,17 +148,22 @@ sub _check_default_storage($self) { } } -sub _check_networks { - my $self = shift; - my $vm = shift; +sub _check_networks($self, $vm=$self->vm) { + my @found; for my $net ($vm->list_all_networks) { - next if $net->is_active; - - warn "INFO: Activating KVM network ".$net->get_name."\n"; - $net->create; - $net->set_autostart(1); + return if $net->is_active; + push @found,($net); } + + my ($net) = grep {$_->get_name eq 'default' } @found; + + $net = $found[0] if !$net; + + return if !$net; + warn "INFO: Activating KVM network ".$net->get_name."\n"; + $net->create; + $net->set_autostart(1); } =head2 disconnect @@ -1829,6 +1839,7 @@ sub _xml_modify_options($self, $doc, $options=undef) { my $machine = delete $options->{machine}; my $arch = delete $options->{arch}; my $bios = delete $options->{'bios'}; + my $network = delete $options->{'network'}; confess "Error: bios=$bios and uefi=$uefi clash" if defined $uefi && defined $bios @@ -1863,7 +1874,12 @@ sub _xml_modify_options($self, $doc, $options=undef) { } else { $self->_xml_set_pci_noe($doc); } + $self->_xml_set_network($doc, $network) if $network; +} +sub _xml_set_network($self, $doc, $network) { + my ($net_source) = $doc->findnodes('/domain/devices/interface/source'); + $net_source->setAttribute('network' => $network); } sub _xml_set_arch($self, $doc, $arch) { @@ -2664,13 +2680,13 @@ sub _xml_add_graphics_streaming { $listen->setAttribute(mode => 'filter'); } -=head2 list_networks +=head2 list_routes Returns a list of networks known to this VM. Each element is a Ravada::NetInterface object =cut -sub list_networks { +sub list_routes { my $self = shift; $self->connect() if !$self->vm; @@ -2959,4 +2975,202 @@ sub get_library_version($self) { return $self->vm->get_library_version(); } +sub list_virtual_networks($self) { + my @networks; + 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 $data= { + is_active => $net->is_active() + ,autostart => $net->get_autostart() + ,bridge => $net->get_bridge_name() + ,id_vm => $self->id + ,name => $net->get_name + ,ip_address => $ip + ,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; + } + push @networks,($data); + } + return @networks; +} + +sub new_network($self, $name='net') { + my @networks = $self->list_virtual_networks(); + + my %base = ( + name => $name + ,ip_address => ['192.168.',122,'.1'] + ,bridge => 'virbr' + ); + my $new = {ip_netmask => '255.255.255.0'}; + for my $field ( keys %base) { + my %old = map { $_->{$field} => 1 } @networks; + my ($last) = reverse sort keys %old; + my ($z,$n) = $last =~ /.*?(0*)(\d+)/; + $z=$last if !defined $z; + $n=0 if !defined $n; + $n++; + $n = "$z$n"; + + my $template = $base{$field}; + if (ref($template)) { + $n=$template->[1]; + } + my $value; + for ( 0 .. 255 ) { + if (ref($template)) { + $value = $template->[0].$n.$template->[2] + } else { + $value = $template.$n; + } + last if !exists $old{$value}; + $n++; + } + $new->{$field} = $value; + } + return $new; +} + +sub create_network($self, $data) { + + confess if !$data->{name} || !$data->{ip_address}; + + my $xml = XML::LibXML->load_xml(string => + "<network><name>$data->{name}</name></network>" + ); + my ($xml_net) = $xml->findnodes("/network"); + + my $forward = $xml_net->addNewChild(undef,'forward'); + $forward->setAttribute('mode' => 'nat'); + + my $ip = $xml_net->addNewChild(undef,'ip'); + $ip->setAttribute('address' => $data->{ip_address}); + $ip->setAttribute('netmask' => $data->{ip_netmask}); + + if ($data->{dhcp_start} || $data->{dhcp_end}) { + my $dhcp = $ip->addNewChild(undef, 'dhcp'); + my $range = $dhcp->addNewChild(undef,'range'); + $range->setAttribute('start' => $data->{dhcp_start}); + $range->setAttribute('end' => $data->{dhcp_end}); + } + + if ($data->{bridge}) { + my ($bridge) = $xml_net->addNewChild(undef,'bridge'); + $bridge->setAttribute(name => $data->{bridge}); + } + + $self->vm->define_network($xml->toString()); + + my $new_network=$self->vm->get_network_by_name($data->{name}); + my $xml2=XML::LibXML->load_xml(string =>$new_network->get_xml_description()); + my ($uuid) = $xml2->findnodes("/network/uuid/text()"); + $data->{internal_id} = ''.$uuid; + + $new_network->create() if $data->{is_active}; + + $new_network->set_autostart($data->{autostart}); + + $data->{is_active} = $new_network->is_active; + + return { internal_uid => ''.$uuid }; +} + +sub remove_network($self, $name) { + my $net = $self->vm->get_network_by_name($name); + + $net->destroy() if $net->is_active; + $net->undefine(); +} + +sub change_network($self, $data) { + my $network = $self->vm->get_network_by_uuid($data->{internal_id}); + die "Error: Unknown network $data->{name}" if !$network; + + my $doc = XML::LibXML->load_xml(string => $network->get_xml_description); + my $changed = 0; + my $bridge = delete $data->{bridge}; + if (defined $bridge) { + my ($bridge_xml) = $doc->findnodes("/network/bridge"); + if (!defined $bridge_xml->getAttribute('name') + || $bridge_xml->getAttribute('name') ne $bridge) { + $bridge_xml->setAttribute(name => $bridge); + $changed++; + } + } + my $dhcp_start = delete $data->{dhcp_start}; + my $dhcp_end = delete $data->{dhcp_end}; + my ($dhcp_xml) = $doc->findnodes("/network/ip/dhcp/range"); + if ($dhcp_xml->getAttribute('start') ne $dhcp_start) { + $dhcp_xml->setAttribute('start' => $dhcp_start); + $changed++; + } + if ($dhcp_xml->getAttribute('end') ne $dhcp_end) { + $dhcp_xml->setAttribute('end' => $dhcp_end); + $changed++; + } + my $ip_address = delete $data->{ip_address}; + my ($ip_xml) = $doc->findnodes("/network/ip"); + if ($ip_address ne $ip_xml->getAttribute('address')) { + $ip_xml->setAttribute('address' => $ip_address); + $changed++; + } + my $ip_netmask = delete $data->{ip_netmask}; + if ($ip_netmask ne $ip_xml->getAttribute('netmask')) { + $ip_xml->setAttribute('netmask' => $ip_netmask); + $changed++; + } + my $name = delete $data->{name}; + my ($name_xml) = $doc->findnodes("/network/name/text()"); + if (''.$name_xml ne $name) { + die "Error: networks can not be renamed"; + } + + if ($changed) { + $network->destroy() if $network->is_active; + $network= $self->vm->define_network($doc->toString); + } + + my $is_active = delete $data->{is_active}; + if (defined $is_active) { + if ($is_active && ! $network->is_active) { + $network->create(); + $changed++; + } + if (!$is_active && $network->is_active) { + $network->destroy; + $changed++; + } + } + + my $autostart = delete $data->{autostart}; + if (defined $autostart) { + if ($autostart && ! $network->get_autostart) { + $network->set_autostart(1); + $changed++; + } + if (!$autostart && $network->get_autostart) { + $network->set_autostart(0); + $changed++; + } + } + + for ('id_vm','internal_id','id' ,'_old_name', 'date_changed') { + delete $data->{$_}; + } + die "Error: unexpected args ".Dumper($data) if keys %$data; + + return $changed; +} + 1; diff --git a/lib/Ravada/VM/Void.pm b/lib/Ravada/VM/Void.pm index d05269aa7..5e562c56a 100644 --- a/lib/Ravada/VM/Void.pm +++ b/lib/Ravada/VM/Void.pm @@ -11,7 +11,7 @@ use Moose; use Socket qw( inet_aton inet_ntoa ); use Sys::Hostname; use URI; -use YAML qw(Dump LoadFile); +use YAML qw(Dump Load); use Ravada::Domain::Void; use Ravada::NetInterface::Void; @@ -34,6 +34,12 @@ has 'vm' => ( ,lazy => 1 ); +has has_networking => ( + isa => 'Bool' + , is => 'ro' + , default => 1 +); + our $CONNECTOR = \$Ravada::CONNECTOR; ########################################################################## @@ -336,10 +342,129 @@ sub search_domain { return; } -sub list_networks { +sub list_routes { return Ravada::NetInterface::Void->new(); } +sub list_virtual_networks($self) { + + my $dir_net = $self->dir_img."/networks/"; + if (!$self->file_exists($dir_net)) { + my ($out, $err) = $self->run_command("mkdir", $dir_net); + die $err if $err; + } + my @files = $self->list_files($dir_net,qr/.yml$/); + my @list; + for my $file(@files) { + my $net; + eval { $net = Load($self->read_file("$dir_net/$file")) }; + confess $@ if $@; + + $net->{id_vm} = $self->id if !$net->{id_vm}; + $net->{is_active}=0 if !defined $net->{is_active}; + push @list,($net); + } + if (!@list) { + my $net = {name => 'default' + , autostart => 1 + , internal_id => 1 + , bridge => 'voidbr0' + ,ip_address => '192.51.100.1' + ,is_active => 1 + }; + + my $file_out = $self->dir_img."/networks/".$net->{name}.".yml"; + $self->write_file($file_out,Dump($net)); + push @list,($net); + } + return @list; +} + +sub _new_net_id($self) { + my %id; + for my $net ( $self->list_virtual_networks() ) { + $id{$net->{internal_id}}++; + } + my $new_id = 0; + for (;;) { + return $new_id if !exists $id{++$new_id}; + } +} + +sub _new_net_bridge ($self) { + my %bridge; + my $n = 0; + for my $net ( $self->list_virtual_networks() ) { + $bridge{$net->{bridge}}++; + } + my $new_id = 0; + for (;;) { + my $new_bridge = 'voidbr'.$new_id; + return $new_bridge if !exists $bridge{$new_bridge}; + $new_id++; + } +} + +sub new_network($self, $name='net') { + + my @networks = $self->list_virtual_networks(); + + my %base = ( + name => $name + ,ip_address => ['192.168.','.0'] + ,bridge => 'voidbr' + ); + my $new = {ip_netmask => '255.255.255.0'}; + for my $field ( keys %base) { + my %old = map { $_->{$field} => 1 } @networks; + my $n = 0; + my $base = ($base{$field} or $field); + my $value; + for ( 0 .. 255 ) { + if (ref($base)) { + $value = $base->[0].$n.$base->[1] + } else { + $value = $base.$n; + } + last if !$old{$value}; + $n++; + } + $new->{$field} = $value; + } + return $new; +} + +sub create_network($self, $data, $id_owner=undef, $request=undef) { + + $data->{internal_id} = $self->_new_net_id(); + my $file_out = $self->dir_img."/networks/".$data->{name}.".yml"; + die "Error: network $data->{name} already created" + if $self->file_exists($file_out); + + $data->{bridge} = $self->_new_net_bridge() + if !exists $data->{bridge} || ! defined $data->{bridge}; + + delete $data->{is_public}; + + for my $field ('bridge','ip_address') { + $self->_check_duplicated_network($field,$data); + } + + delete $data->{is_public}; + delete $data->{id}; + delete $data->{id_vm}; + + $self->write_file($file_out,Dump($data)); + + return $data; +} + +sub remove_network($self, $name) { + my $file_out = $self->dir_img."/networks/$name.yml"; + unlink $file_out or die "$! $file_out" if $self->file_exists($file_out); +} + + sub search_volume($self, $pattern) { return $self->_search_volume_remote($pattern) if !$self->is_local; @@ -470,7 +595,7 @@ sub _init_storage_pool_default($self) { my $config_dir = Ravada::Front::Domain::Void::_config_dir(); my $file_sp = "$config_dir/.storage_pools.yml"; - return if -e $file_sp; + return if $self->file_exists($file_sp); my @list = ({ name => 'default', path => $config_dir, is_active => 1 }); @@ -504,7 +629,7 @@ sub list_storage_pools($self, $info=0) { $self->_init_storage_pool_default(); my $file_sp = "$config_dir/.storage_pools.yml"; - my $extra= LoadFile($file_sp); + my $extra= Load($self->read_file($file_sp)); push @list,(@$extra) if $extra; my ($default) = grep { $_->{name} eq 'default'} @list; @@ -668,11 +793,65 @@ sub active_storage_pool($self, $name, $value) { $self->write_file($file_sp, Dump( \@list)); } +sub has_networking { return 1 }; + +sub _check_duplicated_network($self, $field, $data) { + my @networks = $self->list_virtual_networks(); + my ($found) = grep {$data->{name} ne $_->{name} + && $_->{$field} eq $data->{$field} } @networks; + return if !$found; + + $field = 'Network' if $field eq 'ip_address'; + die "Error: $field is already in use in $found->{name}"; +} + +sub change_network($self,$data) { + my $id = delete $data->{internal_id} or confess "Missing internal_id ".Dumper($data); + confess if exists $data->{is_public}; + + my @networks = $self->list_virtual_networks(); + my ($net0) = grep { $_->{internal_id} eq $id } @networks; + + my $file_out = $self->dir_img."/networks/".$net0->{name}.".yml"; + my $net= {}; + eval { $net = Load($self->read_file($file_out)) }; + confess $@ if $@; + + my $changed = 0; + for my $field ('name', sort keys %$data) { + next if $field =~ /^_/ || $field eq 'is_public'; + if (!exists $net->{$field}) { + $net->{$field} = $data->{$field}; + $changed++; + next; + } + next if exists $data->{$field} && exists $net->{$field} + && defined $data->{$field} && defined $net->{$field} + && $data->{$field} eq $net->{$field}; + + if ($field eq 'name') { + die "Error: network can not be renamed"; + } + + if ($field eq 'bridge' || $field eq 'ip_address') { + $self->_check_duplicated_network($field,$data); + } + $net->{$field} = $data->{$field}; + $changed++; + } + return if !$changed; + + delete $net->{is_public}; + delete $net->{id}; + delete $net->{id_vm}; + + $self->write_file($file_out,Dump($net)); +} + 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 $sp_list = Load($self->read_file($file_sp)); my @sp2; for my $sp (@$sp_list) { push @sp2,($sp) if $sp->{name} ne $name; diff --git a/lib/Ravada/WebSocket.pm b/lib/Ravada/WebSocket.pm index f9b83f239..96ae5a663 100644 --- a/lib/Ravada/WebSocket.pm +++ b/lib/Ravada/WebSocket.pm @@ -49,6 +49,7 @@ my %SUB = ( ,list_next_bookings_today => \&_list_next_bookings_today ,log_active_domains => \&_log_active_domains + ,list_networks => \&_list_networks ); our %TABLE_CHANNEL = ( @@ -60,6 +61,7 @@ our %TABLE_CHANNEL = ( ,list_requests => 'requests' ,machine_info => 'domains' ,log_active_domains => 'log_active_domains' + ,list_networks => 'virtual_networks' ); my $A_WHILE; @@ -381,6 +383,18 @@ sub _log_active_domains($rvd, $args) { return Ravada::Front::Log::list_active_recent($unit,$time); } +sub _list_networks($rvd, $args) { + my @networks; + my $sth = $rvd->_dbh->prepare( + "SELECT * FROM virtual_networks ORDER BY name " + ); + $sth->execute(); + while (my $row = $sth->fetchrow_hashref) { + push @networks,($row); + } + return \@networks; +} + sub _its_been_a_while_channel($channel) { if (!$A_WHILE{$channel} || time -$A_WHILE{$channel} > 5) { $A_WHILE{$channel} = time; diff --git a/public/js/admin.js b/public/js/admin.js index d7f84318b..72727a686 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -7,10 +7,12 @@ ravadaApp.directive("solShowMachine", swMach) .controller("usersPage", usersPageC) .controller("messagesPage", messagesPageC) .controller("manage_nodes",manage_nodes) + .controller("manage_routes",manage_routes) .controller("manage_networks",manage_networks) + .controller("settings_network",settings_network) .controller("settings_node",settings_node) .controller("settings_storage",settings_storage) - .controller("settings_network",settings_network) + .controller("settings_route",settings_route) .controller("new_node", newNodeCtrl) .controller("new_storage", new_storage) .controller("settings_global", settings_global_ctrl) @@ -868,29 +870,93 @@ ravadaApp.directive("solShowMachine", swMach) $scope.list_nodes(); $interval($scope.list_nodes,30 * 1000); }; - function manage_networks($scope, $http, $interval, $timeout) { - list_networks= function() { - $http.get('/list_networks.json').then(function(response) { + $scope.init = function(id_vm) { + $scope.list_networks(id_vm); + $scope.loaded_networks=false; + } + $scope.list_networks = function(id_vm) { + $http.get('/v2/vm/list_networks/'+id_vm).then(function(response) { + $scope.networks=response.data; + $scope.loaded_networks=true; + }); + } + } + + function manage_routes($scope, $http, $interval, $timeout) { + list_routes = function() { + $http.get('/list_routes.json').then(function(response) { for (var i=0; i<response.data.length; i++) { var item = response.data[i]; - $scope.networks[item.id] = item; + $scope.routes[item.id] = item; } }); } $scope.update_network= function(id, field) { - var value = $scope.networks[id][field]; + var value = $scope.routes[id][field]; var args = { 'id': id }; args[field] = value; - $http.post('/v1/network/set' + $http.post('/v2/route/set' , JSON.stringify( args )) .then(function(response) { }); }; - $scope.networks={}; - list_networks(); + $scope.routes={}; + list_routes(); + } + + function settings_network($scope, $http, $interval, $timeout) { + $scope.init = function(id,url, id_vm) { + if ( id ) { + $scope.load_network(id); + } else { + $scope.new_network(id_vm); + } + }; + $scope.new_network = function(id_vm) { + $scope.network = { }; + $http.get('/v2/network/new/'+id_vm) + .then(function(response) { + $scope.network=response.data; + console.log(response.data); + }); + }; + + $scope.load_network = function(id) { + $http.get('/v2/network/info/'+id) + .then(function(response) { + $scope.network = response.data; + $scope.network._old_name = $scope.network.name; + }); + + }; + + $scope.update_network = function() { + + var update = $scope.network['id']; + $http.post('/v2/network/set/' + , JSON.stringify($scope.network)) + .then(function(response) { + $scope.error=response.data.error; + if (!update) { + if (response.data['id_network']) { + window.location.assign('/network/settings/' + +response.data['id_network']+'.html'); + } + } + }); + }; + $scope.remove_network = function() { + $http.post('/request/remove_network' + ,JSON.stringify({'id': $scope.network.id })) + .then(function(response) { + $scope.network._removed = true; + }); + }; + + } @@ -1128,11 +1194,11 @@ ravadaApp.directive("solShowMachine", swMach) }; - function settings_network($scope, $http, $timeout) { + function settings_route($scope, $http, $timeout) { var url_ws; $scope.init = function(id_network) { if (typeof id_network == 'undefined') { - $scope.network = { + $scope.route= { 'name': '' ,'all_domains': 1 }; @@ -1142,30 +1208,30 @@ ravadaApp.directive("solShowMachine", swMach) } } $scope.check_no_domains = function() { - if ( $scope.network.no_domains == 1 ){ - $scope.network.all_domains = 0; + if ( $scope.route.no_domains == 1 ){ + $scope.route.all_domains = 0; } }; $scope.check_all_domains = function() { - if ( $scope.network.all_domains == 1 ){ - $scope.network.no_domains = 0; + if ( $scope.route.all_domains == 1 ){ + $scope.route.no_domains = 0; } }; $scope.update_network= function(field) { - var data = $scope.network; + var data = $scope.route; if (typeof field != 'undefined') { var data = {}; - data[field] = $scope.network[field]; + data[field] = $scope.route[field]; } $scope.saved = false; $scope.error = ''; - $http.post('/v1/network/set/' + $http.post('/v2/route/set/' , JSON.stringify(data)) // , JSON.stringify({ value: $scope.network[field]})) .then(function(response) { if (response.data.ok == 1){ $scope.saved = true; - if (!$scope.network.id) { + if (!$scope.route.id) { $scope.new_saved = true; } } @@ -1177,19 +1243,19 @@ ravadaApp.directive("solShowMachine", swMach) $scope.load_network = function(id_network) { $scope.error = ''; $scope.saved = false; - $http.get('/network/info/'+id_network+'.json').then(function(response) { - $scope.network = response.data; + $http.get('/route/info/'+id_network).then(function(response) { + $scope.route = response.data; $scope.formNetwork.$setPristine(); - $scope.network._old_name = $scope.network.name; + $scope.route._old_name = $scope.route.name; }); }; $scope.list_domains_network = function(id_network) { - $http.get('/network/list_domains/'+id_network).then(function(response) { + $http.get('/route/list_domains/'+id_network).then(function(response) { $scope.machines = response.data; }); }; $scope.set_network_domain= function(id_domain, field, allowed) { - $http.get("/network/set/"+$scope.network.id+ "/" + field+ "/" +id_domain+"/" + $http.get("/v2/route/set/"+$scope.route.id+ "/" + field+ "/" +id_domain+"/" +allowed) .then(function(response) { }); @@ -1200,26 +1266,26 @@ ravadaApp.directive("solShowMachine", swMach) }); }; - $scope.remove_network = function(id_network) { - if ($scope.network.name == 'default') { - $scope.error = $scope.network.name + " network can't be removed"; + $scope.remove_route = function(id_network) { + if ($scope.route.name == 'default') { + $scope.error = $scope.route.name + " network can't be removed"; return; } - $http.get('/v1/network/remove/'+id_network).then(function(response) { - $scope.message = "Network "+$scope.network.name+" removed"; - $scope.network ={}; + console.log(id_network); + $http.get('/v2/route/remove/'+id_network).then(function(response) { + window.location.assign('/admin/routes'); }); }; $scope.check_duplicate = function(field) { var args = {}; - if (typeof ($scope.network['id']) != 'undefined') { - args['id'] = $scope.network['id']; + if (typeof ($scope.route['id']) != 'undefined') { + args['id'] = $scope.route['id']; } - args[field] = $scope.network[field]; + args[field] = $scope.route[field]; $http.post("/v1/exists/networks",JSON.stringify(args)) .then(function(response) { - $scope.network["_duplicated_"+field]=response.data.id; + $scope.route["_duplicated_"+field]=response.data.id; }); }; $scope.new_saved = false; diff --git a/public/js/ravada.js b/public/js/ravada.js index 0660dd8fb..89120e38c 100644 --- a/public/js/ravada.js +++ b/public/js/ravada.js @@ -596,11 +596,11 @@ } }); } + list_interfaces(); if (is_admin) { $scope.init_domain_access(); $scope.init_ldap_access(); $scope.list_ldap_attributes(); - list_interfaces(); list_users(); list_host_devices(); list_access_groups(); @@ -616,9 +616,12 @@ var list_interfaces = function() { if (! $scope.network_nats) { - $http.get('/network/interfaces/'+$scope.showmachine.type+'/nat') + $http.get('/v2/vm/list_networks/'+$scope.showmachine.id_vm) .then(function(response) { - $scope.network_nats = response.data; + $scope.network_nats= []; + for (var i=0; i<response.data.length; i++ ) { + $scope.network_nats.push(response.data[i].name); + } }); } if (! $scope.network_bridges ) { diff --git a/script/rvd_front b/script/rvd_front index b017b8a2f..1e4d1793a 100644 --- a/script/rvd_front +++ b/script/rvd_front @@ -31,7 +31,7 @@ use Ravada::Booking; use Ravada::Front; use Ravada::Front::Domain; use Ravada::HostDevice::Templates; -use Ravada::Network; +use Ravada::Route; use Ravada::WebSocket; use POSIX qw(locale_h strftime); @@ -383,6 +383,24 @@ get '/admin/storage' => sub($c) { }; +get '/admin/networks/*id_vm' => { id_vm => undef} => sub($c) { + + return access_denied($c) unless $USER->is_admin + || $USER->can_create_networks || $USER->can_manage_all_networks; + + _add_admin_libs($c); + + _select_vm($c) if !defined $c->stash('id_vm'); + + Ravada::Request->list_networks( + uid => $USER->id + ,id_vm => $c->stash('id_vm') + ); + + return $c->render( template => '/main/admin_networks' ); + +}; + any '/admin/#type' => sub { my $c = shift; @@ -493,46 +511,147 @@ get '/node/host_device/remove/(:id)' => sub($c) { return $c->render(json => { ok => 1 }); }; -get '/list_networks.json' => sub { +get '/list_routes.json' => sub { my $c = shift; - $c->render(json => [ Ravada::Network->list_networks ]); + $c->render(json => [ Ravada::Route->list_networks ]); }; -get '/network/info/#id' => sub($c) { +get '/v2/vm/list_networks/(:id_vm)' => sub { + my $c = shift; + + return access_denied($c) unless $USER->is_admin + || $USER->can_create_networks || $USER->can_manage_all_networks; + + $c->render(json => Ravada::Front->list_networks($c->stash('id_vm'), $USER->id) ); +}; + +get '/route/info/#id' => sub($c) { my $sth = $RAVADA->_dbh->prepare("SELECT * from networks WHERE id=?"); $sth->execute($c->stash('id')); my $row = $sth->fetchrow_hashref; return $c->render(json => $row); }; -get '/v1/network/remove/#id' => sub($c) { +get '/v2/route/remove/#id' => sub($c) { my $sth = $RAVADA->_dbh->prepare("DELETE from networks WHERE id=?"); $sth->execute($c->stash('id')); + $USER->send_message("Network ".$c->stash('id')." removed"); return $c->render(json => { ok => 1 }); }; -get '/network/list_domains/#id' => sub($c) { +get '/route/list_domains/#id' => sub($c) { return $c->render( json => $RAVADA->list_bases_network($c->stash('id'))); }; +any '/network/new' => sub($c) { + return access_denied($c) unless $USER->is_admin + || $USER->can_create_networks || $USER->can_manage_all_networks; + + _add_admin_libs($c); + _select_vm($c); + $c->stash(item => 'network', id => undef, tab => undef); + + return $c->render( template => '/main/network_new' ); +}; + +any '/v2/network/new/#id_vm' => sub($c) { + + return access_denied($c) unless $USER->is_admin + || $USER->can_create_networks || $USER->can_manage_all_networks; + + my $name = 'net'; + if ( $c->req->method eq 'POST') { + my $arg = decode_json($c->req->body); + $name = $arg->{name} if $arg->{name}; + } + + my $req = Ravada::Request->new_network( + uid => $USER->id + ,id_vm => $c->stash('id_vm') + ,name => $name + ); + $RAVADA->wait_request($req); + my $data = {}; + if ($req->status eq 'done' && $req->output && $req->output =~ /^\{/) { + eval { + $data = decode_json($req->output); + }; + warn $@ if $@; + } + $c->render(json => $data); +}; + +get '/v2/network/info/#id' => sub($c) { + my $sth = $RAVADA->_dbh->prepare("SELECT * from virtual_networks WHERE id=?"); + $sth->execute($c->stash('id')); + my $row = $sth->fetchrow_hashref; + return $c->render(json => $row); +}; + +sub _network_id_owner($id_network) { + + confess "Error: undefined id_network "if !defined $id_network; + my $sth = $RAVADA->_dbh->prepare("SELECT * FROM virtual_networks " + ." WHERE id=?"); + $sth->execute($id_network); + my $row = $sth->fetchrow_hashref; + warn "Error: network id=$id_network not found " if !$row->{id}; + + return ($row->{id_owner} or 0 ); +} + +post '/v2/network/set' => sub($c) { + my $arg = decode_json($c->req->body); + delete $arg->{_owner}; + + return access_denied_json($c) + unless $USER->is_admin || $USER->can_manage_all_networks + || ($USER->can_create_networks + && ( !$arg->{id} || $USER->id == _network_id_owner($arg->{id})) + ); + + my $req; + my %data; + if ($arg->{id}) { + $req = Ravada::Request->change_network( + uid => $USER->id + ,data => $arg + ); + } else { + my $id_vm = delete $arg->{id_vm}; + $req = Ravada::Request->create_network( + uid => $USER->id + ,id_vm => $id_vm + ,data => $arg + ); + $RAVADA->wait_request($req, 120); + my $out = {}; + eval { $out = decode_json($req->output) if $req->output }; + warn $@ if $@; + %data = %$out if $out && keys %$out; + } + $data{error} = $req->error; + return $c->render(json => \%data, output => $req->output , error => $req->error); +}; + sub _add_admin_libs($c) { push @{$c->stash->{js}}, '/js/admin.js?v='.$RAVADA->version; push @{$c->stash->{css}}, '/css/admin.css'; } -any '/network/new' => sub($c) { +any '/route/new' => sub($c) { _add_admin_libs($c); - return $c->render(template => "/main/network_new"); + return $c->render(template => "/main/route_new"); }; -post '/v1/network/set' => sub($c) { +post '/v2/route/set' => sub($c) { return access_denied($c) if !$USER->is_admin; my $arg = decode_json($c->req->body); return _update_fields($c, "networks", $arg); }; -get '/network/set/#id/#field/#id_domain/#value' => sub ($c) { +get '/v2/route/set/#id/#field/#id_domain/#value' => sub ($c) { my $id_network = $c->stash('id'); my $field = $c->stash('field'); my $id_domain = $c->stash('id_domain'); @@ -598,6 +717,7 @@ post '/v1/exists/:item' => sub($c) { $query .= " AND id <> $id " if defined $id; + my $sth = $RAVADA->_dbh->prepare("SELECT id FROM $table WHERE $query "); $sth->execute(map { $arg->{$_} } @fields ); @@ -740,7 +860,7 @@ get '/list_machine_types.json' => sub { get '/list_cpu_models.json' => sub { my $c = shift; - return access_denied($c) unless _logged_in($c) + return $c->render(json => []) unless _logged_in($c) && $USER->can_create_machine(); my $id_domain = $c->param('id_domain'); @@ -829,6 +949,35 @@ any '/(:item)/settings/(:id).html' => sub($c) { _add_admin_libs($c); $c->stash(tab => ($c->param('tab') or '')); + my $table = $c->stash('item')."s"; + $table = 'virtual_networks' if $table eq 'networks'; + $table = 'networks' if $table eq 'routes'; + + return access_denied($c) if $table !~ /^[a-z][a-z_]+$/i; + + if ($table eq 'virtual_networks') { + my $id = $c->stash('id'); # get id network + return access_denied_json($c) + unless $USER->is_admin || $USER->can_manage_all_networks + || ($USER->can_create_networks && $USER->id == _network_id_owner($id)) + ; + } + + my $id_vm; + if ($table eq 'nodes') { + $id_vm = $c->stash('id'); + } elsif ($table ne 'networks') { + my $sth = $RAVADA->_dbh->prepare( + "SELECT id,id_vm FROM $table WHERE id=?" + ); + $sth->execute($c->stash('id')); + my $id2; + ($id2, $id_vm) = $sth->fetchrow; + return not_found($c, 404, $c->stash('item')." ".$c->stash('id') + ." not found in $table") if !$id2; + + } + $c->stash('id_vm' => $id_vm); return $c->render( template => '/main/settings_generic' ); }; @@ -1847,6 +1996,7 @@ post '/request/(:name)/' => sub { my $name = $c->stash('name'); my $args = decode_json($c->req->body); + my $wait = delete $args->{_wait}; for (qw(remote_ip uid)) { confess "Error: $_ should not be provided".Dumper($args) @@ -1889,11 +2039,19 @@ post '/request/(:name)/' => sub { }; warn $@ if $@; } + my $error = ( $@ or ''); + my $output = ''; + if ($wait) { + $RAVADA->wait_request($req, 60); + $error => $req->error if !$error; + $output = $req->output if $req->output; + } $RAVADA->_cache_clean(); return $c->render(json => { ok => 0, error => $@ }) if !$req; - return $c->render(json => { ok => 1, request => $req->id }); + return $c->render(json => { ok => 1, request => $req->id + ,error => $error, output => $output }); }; get '/request/(:id).(:type)' => sub { @@ -3031,6 +3189,32 @@ sub access_denied_json($c, $msg='Access denied') { return access_denied($c, $msg); } +sub not_found($c,$code=404, $msg="Not found") { + my $agent = $c->req->headers->user_agent; + if ($agent =~ /Mojolicious/) { + return $c->render( text => $msg , status => $code ); + } + if (defined $c->stash('type') && $c->stash('type') eq 'json') { + return $c->render(json => { error => $msg }, status => $code); + } + my @css_snippets = ["\t.intro {\n\t\tbackground:" + ." url($CONFIG_FRONT->{login_bg_file})" + ." no-repeat bottom center scroll;\n\t}"]; + + return $c->render( + template => ($CONFIG_FRONT->{not_found} or '/main/not_found') + ,css => ['/css/main.css'] + ,csssnippets => @css_snippets + ,js => ['/js/main.js?v='.$RAVADA->version] + ,navbar_custom => 1 + ,error => $msg + ,can_login => ((! $USER) || ($USER->is_temporary)) + ,guide => $CONFIG_FRONT->{guide} + ,status => $code + ); + +} + sub access_denied { my $c = shift; my $msg = shift; @@ -3043,6 +3227,11 @@ sub access_denied { if (defined $c->stash('type') && $c->stash('type') eq 'json') { return $c->render(json => { error => $msg }, status => 403); } + + my $agent = $c->req->headers->user_agent; + if ($agent =~ /Mojolicious/) { + return $c->render(text => $msg, status => 403); + } my @css_snippets = ["\t.intro {\n\t\tbackground:" ." url($CONFIG_FRONT->{login_bg_file})" ." no-repeat bottom center scroll;\n\t}"]; diff --git a/t/30_request.t b/t/30_request.t index f531ad5e3..9297b5248 100644 --- a/t/30_request.t +++ b/t/30_request.t @@ -310,6 +310,7 @@ sub test_force() { my $req = Ravada::Request->refresh_vms(uid => user_admin->id); ok($req); wait_request( debug => 0); + is($req->error, '') or exit; my $req3 = Ravada::Request->refresh_vms(uid => user_admin->id); ok($req3); diff --git a/t/70_networks.t b/t/70_networks.t index 194068b79..a32492c51 100644 --- a/t/70_networks.t +++ b/t/70_networks.t @@ -10,7 +10,7 @@ no warnings "experimental::signatures"; use feature qw(signatures); use_ok('Ravada'); -use_ok('Ravada::Network'); +use_ok('Ravada::Route'); use lib 't/lib'; use Test::Ravada; @@ -28,7 +28,7 @@ sub test_allow_all { my $domain = shift; my $ip = '192.168.1.2/32'; - my $net = Ravada::Network->new(address => $ip); + my $net = Ravada::Route->new(address => $ip); ok(!$net->allowed($domain->id),"Expecting not allowed from unknown network"); #check list bases, default allowed @@ -53,7 +53,7 @@ sub test_allow_all { ok(!$net->allowed_anonymous($domain->id),"Expecting denied anonymous from known network"); ok($net->allowed($domain->id),"Expecting allowed from known network"); - my $net2 = Ravada::Network->new(address => '192.168.1.22/32'); + my $net2 = Ravada::Route->new(address => '192.168.1.22/32'); ok($net2->allowed($domain->id),"Expecting allowed from known network"); ok(!$net2->allowed_anonymous($domain->id),"Expecting denied anonymous from known network"); { # test list bases anonymous @@ -68,7 +68,7 @@ sub test_allow_domain { my $domain = shift; my $ip = '10.1.1.1/32'; - my $net = Ravada::Network->new(address => $ip); + my $net = Ravada::Route->new(address => $ip); ok(!$net->allowed($domain->id),"Expecting not allowed from unknown network"); { # test list bases anonymous @@ -197,7 +197,7 @@ sub test_deny_all { my $ip = '10.0.0.2/32'; - my $net = Ravada::Network->new(address => $ip); + my $net = Ravada::Route->new(address => $ip); ok(!$net->allowed($domain->id),"Expecting not allowed from unknown network"); { # test list bases anonymous @@ -318,11 +318,11 @@ $domain->is_public(1); test_conflict_allowed(); -my $net = Ravada::Network->new(address => '127.0.0.1/32'); +my $net = Ravada::Route->new(address => '127.0.0.1/32'); ok($net->allowed($domain->id)); deny_everything_any(); -my $net2 = Ravada::Network->new(address => '10.0.0.0/32'); +my $net2 = Ravada::Route->new(address => '10.0.0.0/32'); ok(!$net2->allowed($domain->id), "Address unknown should not be allowed to anything"); test_allow_all($domain); diff --git a/t/kvm/p10_password.t b/t/kvm/p10_password.t index 4b397bac6..000354614 100644 --- a/t/kvm/p10_password.t +++ b/t/kvm/p10_password.t @@ -16,13 +16,15 @@ init(); my @VMS = vm_names(); my $USER = create_user("foo","bar", 1); +use_ok('Ravada::Route'); + ####################################################### sub test_domain_no_password { my $vm_name = shift; my $vm = rvd_back->search_vm($vm_name); - my $net = Ravada::Network->new(address => '127.0.0.1/32'); + my $net = Ravada::Route->new(address => '127.0.0.1/32'); ok(!$net->requires_password); my $domain_name = new_domain_name(); @@ -43,7 +45,7 @@ sub test_domain_no_password { } is($domain->is_active,0) or return; - my $net2 = Ravada::Network->new(address => '10.0.0.1/32'); + my $net2 = Ravada::Route->new(address => '10.0.0.1/32'); ok(!$net2->requires_password,"Expecting net requires password "); $domain->start(user => $USER, remote_ip => '10.0.0.1'); @@ -69,7 +71,7 @@ sub test_domain_password2 { my $vm_name = shift; my $vm = rvd_back->search_vm($vm_name); - my $net = Ravada::Network->new(address => '127.0.0.1/32'); + my $net = Ravada::Route->new(address => '127.0.0.1/32'); ok(!$net->requires_password) or return; my $domain_name = new_domain_name(); @@ -90,7 +92,7 @@ sub test_domain_password2 { } is($domain->is_active,0) or return; - my $net2 = Ravada::Network->new(address => '10.0.0.1/32'); + my $net2 = Ravada::Route->new(address => '10.0.0.1/32'); ok($net2->requires_password,"Expecting net requires password ") or return; @@ -117,7 +119,7 @@ sub test_domain_password2 { sub test_domain_password1($vm_name, $requires_password=1) { my $vm = rvd_back->search_vm($vm_name); - my $net2 = Ravada::Network->new(address => '10.0.0.1/32'); + my $net2 = Ravada::Route->new(address => '10.0.0.1/32'); ok($net2->requires_password,"Expecting net requires password ") or return; diff --git a/t/lib/Test/Ravada.pm b/t/lib/Test/Ravada.pm index 4d16f095f..7ac192c18 100644 --- a/t/lib/Test/Ravada.pm +++ b/t/lib/Test/Ravada.pm @@ -1485,6 +1485,48 @@ sub _qemu_storage_pool { return $pool_name; } +sub remove_void_networks($vm=undef) { + if (!defined $vm) { + eval { $vm = rvd_back->search_vm('Void') }; + } + my $dir_net = $vm->dir_img()."/networks"; + return if ! -e $dir_net; + my $base = base_domain_name(); + + opendir my $dir, $dir_net or die "$! $dir_net"; + while(my $filename = readdir $dir) { + my $file = "$dir_net/$filename"; + next unless $file =~ /^$base.*\.yml$/; + unlink $file or warn "$! $file"; + } + +} + +sub remove_qemu_networks($vm=undef) { + return if !$VM_VALID{'KVM'} || $>; + if (!defined $vm) { + eval { $vm = rvd_back->search_vm('KVM') }; + if ($@ && $@ !~ /Missing qemu-img/) { + warn $@; + } + if ( !$vm ) { + $VM_VALID{'KVM'} = 0; + return; + } + } + + my $base = base_domain_name(); + $vm->connect(); + for my $network ( $vm->vm->list_all_networks) { + my $name = $network->get_name; + next if $name !~ /^$base/; + diag("removing network $name"); + $network->destroy() if $network->is_active; + $network->undefine(); + } + +} + sub remove_qemu_pools($vm=undef) { return if !$vm && (!$VM_VALID{'KVM'} || $>); return if defined $vm && $vm->type eq 'Void'; @@ -1553,6 +1595,11 @@ sub remove_old_pools { remove_qemu_pools(); } +sub remove_old_networks { + remove_qemu_networks(); + remove_void_networks(); +} + sub _remove_old_entries($table) { my $sth = connector()->dbh->prepare("DELETE FROM $table" ." WHERE name like ? " @@ -1588,6 +1635,7 @@ sub clean($ldap=undef) { remove_old_domains(); remove_old_disks(); remove_old_pools(); + remove_old_networks(); _remove_old_entries('vms'); _remove_old_entries('networks'); _remove_old_groups_ldap(); diff --git a/t/mojo/30_settings.t b/t/mojo/30_settings.t index d03b2ffc3..f7983bc47 100644 --- a/t/mojo/30_settings.t +++ b/t/mojo/30_settings.t @@ -101,8 +101,10 @@ sub test_nodes($vm_name) { } sub test_settings_item($id, $item) { - $t->get_ok('/'.$item.'/settings/'.$id.'.html'); - is($t->tx->res->code(),200) or die $t->tx->res->body; + $item = 'route' if $item eq 'network'; + my $url = '/'.$item.'/settings/'.$id.'.html'; + $t->get_ok($url); + is($t->tx->res->code(),200, "Expecting $url") or die $t->tx->res->body; } sub test_exists_node($id_node, $name) { @@ -147,56 +149,245 @@ sub test_exists_network($id_network, $field, $name) { is($result_exists->{id}, $id_network); } -sub _remove_network($address) { - my @list_networks = Ravada::Network::list_networks(); +sub _remove_route($address) { + my @list_networks = Ravada::Route::list_networks(); my ($found) = grep { $_->{address} eq $address} @list_networks; return if !$found; - $t->get_ok("/v1/network/remove/".$found->{id}); + $t->get_ok("/v2/route/remove/".$found->{id}); is($t->tx->res->code(),200) or die $t->tx->res->body; } -sub test_networks($vm_name) { +sub _remove_networks($id_vm) { + my $sth = connector->dbh->prepare("SELECT vn.id FROM virtual_networks vn, vms v" + ." WHERE vn.id_vm=v.id " + ." AND v.id=? AND vn.name like ?" + ); + $sth->execute($id_vm, base_domain_name."%"); + + while ( my ($id) = $sth->fetchrow) { + my $id_req = mojo_request($t, "remove_network", { id => $id}); + if ($id_req) { + my $req = Ravada::Request->open($id_req); + die "Error in ".$req->command." id=$id" if $req->error; + } + } + +} + +sub _id_vm($vm_name) { + my $sth = connector->dbh->prepare("SELECT id,hostname FROM vms " + ." WHERE vm_type=? AND is_active=1"); + $sth->execute($vm_name); + my @vm; + while (my $row = $sth->fetchrow_hashref ) { + push @vm,($row); + } + my ($vm) = grep { $_->{hostname} eq 'localhost' } @vm; + + my $id_vm; + $id_vm = $vm->{id} if $vm; + $id_vm = $vm[0]->{id} if !$id_vm; + + return $id_vm; +} + +sub test_networks_access($vm_name) { + + my ($name, $pass) = (new_domain_name, "$$ $$"); + my $user = create_user($name, $pass,0 ); + is($user->is_admin,0 ); + mojo_login($t, $name, $pass); + + my $id_vm = _id_vm($vm_name); + + my @urls =( + "/admin/networks", "/network/new" + , "/v2/vm/list_networks/$id_vm","/v2/network/new/".$id_vm); + for my $url (@urls) { + $t->get_ok($url)->status_is(403); + } + + user_admin->grant($user,'create_networks'); + for my $url (@urls) { + $t->get_ok($url)->status_is(200); + } + + user_admin->revoke($user,'create_networks'); + user_admin->grant($user,'manage_all_networks'); + for my $url (@urls) { + $t->get_ok($url)->status_is(200); + } + + $user->remove(); + mojo_login($t, $USERNAME, $PASSWORD); + +} + +sub test_networks_access_grant($vm_name) { + + my ($name, $pass) = (new_domain_name, "$$ $$"); + my $user = create_user($name, $pass,0 ); + user_admin->grant($user,"create_networks"); + mojo_login($t, $name, $pass); + + my $id_vm = _id_vm($vm_name); + + $t->post_ok("/v2/network/new/".$id_vm => json => { name => base_domain_name() }); + my $data = decode_json($t->tx->res->body); + ok(keys %$data) or die Dumper($data); + + $t->post_ok("/v2/network/set/" => json => $data ); + my $new_ok = decode_json($t->tx->res->body); + ok($new_ok->{id_network}) or return; + + $t->get_ok("/settings/network".$new_ok->{id_network}.".html"); + + $t->get_ok("/v2/vm/list_networks/".$id_vm); + my $networks2 = decode_json($t->tx->res->body); + my ($old) = grep { $_->{name} ne $data->{name} } @$networks2; + ok($old,"Expecting more networks for VM $vm_name [ $id_vm ]") + or die Dumper([map {$_->{name} } @$networks2]); + + is($old->{_can_change},0) or exit; + + $t->get_ok("/network/settings/".$old->{id}.".html")->status_is(403); + $old->{autostart}=0; + + $t->post_ok("/v2/network/set/" => json => $old)->status_is(403); + + my ($new) = grep { $_->{name} eq $data->{name} } @$networks2; + ok($new,"Expecting new network $data->{name}") + or die Dumper([map {$_->{name} } @$networks2]); + is($new->{_owner}->{id},$user->id); + is($new->{_can_change},1) or exit; + + for ( 1 .. 2 ) { + $new->{is_active} = (!$new->{is_active} or 0); + $t->post_ok("/v2/network/set/" => json => $new)->status_is(200); + wait_request(); + + $t->get_ok("/v2/vm/list_networks/".$id_vm); + + my $networks3 = decode_json($t->tx->res->body); + my ($net3) = grep { $_->{name} eq $new->{name}} @$networks3; + is($net3->{is_active}, $new->{is_active}) or exit; + } + + for ( 1 .. 2 ) { + $new->{is_public} = (!$new->{is_public} or 0); + $t->post_ok("/v2/network/set/" => json => $new)->status_is(200); + wait_request(); + + $t->get_ok("/v2/vm/list_networks/".$id_vm); + my $networks4 = decode_json($t->tx->res->body); + my ($net4) = grep { $_->{name} eq $new->{name}} @$networks4; + is($net4->{is_public}, $new->{is_public}) or exit; + } + + mojo_login($t, $USERNAME, $PASSWORD); + +} + +sub test_networks_admin($vm_name) { + mojo_check_login($t); + + for my $url (qw( /admin/networks/ /network/new) ) { + $t->get_ok($url); + is($t->tx->res->code(),200, "Expecting access to $url"); + } + + my $id_vm = _id_vm($vm_name); + die "Error: I can't find if for vm type = $vm_name" if !$id_vm; + + _remove_networks($id_vm); + + $t->get_ok("/v2/vm/list_networks/".$id_vm); + my $networks = decode_json($t->tx->res->body); + ok(scalar(@$networks)); + + $t->post_ok("/v2/network/new/".$id_vm => json => { name => base_domain_name() }); + my $data = decode_json($t->tx->res->body); + + $t->post_ok("/v2/network/set/" => json => $data ); + + my $new_ok = decode_json($t->tx->res->body); + ok($new_ok->{id_network}) or die Dumper([$t->tx->res->body, $new_ok]); + + $t->get_ok("/v2/vm/list_networks/".$id_vm); + my $networks2 = decode_json($t->tx->res->body); + my ($new) = grep { $_->{name} eq $data->{name} } @$networks2; + + ok($new); + is($new->{_can_change},1) or exit; + is($new->{_owner}->{id},user_admin->id) or exit; + $new->{is_active} = 0; + + $t->post_ok("/v2/network/set/" => json => $new); + wait_request(debug => 1); + $t->get_ok("/v2/vm/list_networks/".$id_vm); + + my $networks3 = decode_json($t->tx->res->body); + my ($changed) = grep { $_->{name} eq $data->{name} } @$networks3; + is($changed->{is_active},0) or die $changed->{name}; + + $t->get_ok("/v2/network/info/".$changed->{id}); + + my $changed4 = decode_json($t->tx->res->body); + is($changed4->{is_active},0) or exit; + + $new->{is_public}=1; + $t->post_ok("/v2/network/set/" => json => $new); + wait_request(debug => 1); + $t->get_ok("/v2/vm/list_networks/".$id_vm); + + my $networks5 = decode_json($t->tx->res->body); + my ($changed5) = grep { $_->{name} eq $data->{name} } @$networks5; + is($changed5->{is_public},1) or warn Dumper($changed5); + +} + +sub test_routes($vm_name) { mojo_check_login($t); my $name = new_domain_name(); my $address = '1.2.3.0/24'; - _remove_network($address); + _remove_route($address); - $t->post_ok('/v1/network/set' => json => { + $t->post_ok('/v2/route/set' => json => { name => $name , address => $address }); is($t->tx->res->code(),200) or die $t->tx->res->to_string(); exit if !$t->success; - my @list_networks = Ravada::Network::list_networks(); + my @list_networks = Ravada::Route::list_networks(); my ($found) = grep { $_->{name} eq $name } @list_networks; ok($found,"Expecting $name in list vms ".Dumper(\@list_networks)) or return; my $id_network = $found->{id}; my $name2 = new_domain_name(); - $t->post_ok("/v1/network/set", json => { + $t->post_ok("/v2/route/set", json => { id => $found->{id} , name => $name2 } ); is($t->tx->res->code(),200) or die $t->tx->res->body; - @list_networks = Ravada::Network::list_networks(); + @list_networks = Ravada::Route::list_networks(); ($found) = grep { $_->{id} == $id_network } @list_networks; is($found->{name}, $name2) or die Dumper($found); my $new_name = new_domain_name(); - $t->post_ok("/v1/network/set", json => { + $t->post_ok("/v2/route/set", json => { id => $id_network , name => $new_name } ); - @list_networks = Ravada::Network::list_networks(); + @list_networks = Ravada::Route::list_networks(); ($found) = grep { $_->{id} == $id_network } @list_networks; is($found->{name}, $new_name) or die Dumper(\@list_networks); @@ -206,10 +397,10 @@ sub test_networks($vm_name) { test_settings_item( $id_network, 'network' ); - $t->get_ok("/v1/network/remove/".$found->{id}); + $t->get_ok("/v2/route/remove/".$found->{id}); is($t->tx->res->code(),200) or die $t->tx->res->body; - ok(! grep { $_->{id} == $found->{id} } Ravada::Network::list_networks()); + ok(! grep { $_->{id} == $found->{id} } Ravada::Route::list_networks()); } @@ -329,10 +520,7 @@ sub test_storage_pools($vm_name) { my $sp = decode_json($t->tx->res->body); ok(scalar(@$sp)); - my $sth = connector->dbh->prepare("SELECT id FROM vms where vm_type=?" - ." AND hostname='localhost'"); - $sth->execute($vm_name); - my ($id_vm) = $sth->fetchrow; + my $id_vm = _id_vm($vm_name); $t->get_ok("/list_storage_pools/$id_vm"); @@ -418,11 +606,15 @@ for my $vm_name (reverse @{rvd_front->list_vm_types} ) { diag("Testing settings in $vm_name"); + test_networks_access( $vm_name ); + test_networks_access_grant($vm_name); + test_networks_admin( $vm_name ); test_storage_pools($vm_name); test_nodes( $vm_name ); - test_networks( $vm_name ); + test_routes( $vm_name ); } clean_clones(); +remove_old_users(); done_testing(); diff --git a/t/mojo/40_anonymous.t b/t/mojo/40_anonymous.t index 88fa75bc9..cd5180ea5 100644 --- a/t/mojo/40_anonymous.t +++ b/t/mojo/40_anonymous.t @@ -57,7 +57,7 @@ sub _allow_anonymous_base() { my $id_domain = _id_domain('zz-test-base-alpine'); mojo_login($t, user_admin->name,"$$ $$"); - $t->get_ok("/network/set/$id_net/anonymous/$id_domain/1"); + $t->get_ok("/v2/route/set/$id_net/anonymous/$id_domain/1"); my $sth = connector->dbh->prepare("UPDATE domains set is_public=1" ." WHERE id=?"); @@ -97,6 +97,7 @@ _allow_anonymous_base(); $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'); diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t index 0ceb540f4..0e5cfdeb6 100644 --- a/t/nodes/10_basic.t +++ b/t/nodes/10_basic.t @@ -1626,6 +1626,11 @@ sub test_displays($vm, $node, $no_builtin=0) { $domain->remove(user_admin); } +sub test_network($vm, $node) { + my @vm_nets= $vm->list_virtual_networks(); + my $node_nets = $node->list_virtual_networks(); +} + ################################################################################## if ($>) { @@ -1639,7 +1644,7 @@ clean(); $Ravada::Domain::MIN_FREE_MEMORY = 256 * 1024; my $tls; -for my $vm_name (vm_names() ) { +for my $vm_name (reverse vm_names() ) { my $vm; eval { $vm = rvd_back->search_vm($vm_name) }; @@ -1666,6 +1671,7 @@ for my $vm_name (vm_names() ) { $tls = 1 if check_libvirt_tls() && $vm_name eq 'KVM'; my $node = remote_node($vm_name) or next; clean_remote_node($node); + test_network($vm,$node); ok($node->vm,"[$vm_name] expecting a VM inside the node") or do { remove_node($node); diff --git a/t/request/30_hardware.t b/t/request/30_hardware.t index 5891af05c..2547f2458 100644 --- a/t/request/30_hardware.t +++ b/t/request/30_hardware.t @@ -1104,7 +1104,17 @@ sub test_change_network_bridge($vm, $domain, $index) { skip("No bridges found in this system",6) if !scalar @bridges; my $info = $domain->info(user_admin); - is ($info->{hardware}->{network}->[$index]->{type}, 'NAT') or exit; + if ($info->{hardware}->{network}->[$index]->{type} eq 'bridge') { + my $req = Ravada::Request->change_hardware( + id_domain => $domain->id + ,hardware => 'network' + ,index => $index + ,data => { type => 'NAT', network => 'default'} + ,uid => user_admin->id + ); + wait_request(); + + } ok(scalar @bridges,"No network bridges defined in this host") or return; @@ -1509,6 +1519,8 @@ sub _remove_usbs($domain, $hardware) { sub test_change_drivers($domain, $hardware) { + return if $domain->type eq 'Void' && $hardware eq 'network'; + _remove_usbs($domain, $hardware); my $info = $domain->info(user_admin); @@ -1566,6 +1578,9 @@ sub test_change_drivers($domain, $hardware) { } sub test_all_drivers($domain, $hardware) { + + return if $domain->type eq 'Void' && $hardware eq 'network'; + my $info = $domain->info(user_admin); my $options = $info->{drivers}->{$hardware}; ok(scalar @$options,"No driver options for $hardware") or exit; @@ -1712,7 +1727,7 @@ for my $vm_name (vm_names()) { my %controllers = $domain_b0->list_controllers; lock_hash(%controllers); - for my $hardware ('display', sort keys %controllers ) { + for my $hardware ( sort keys %controllers ) { next if $hardware eq 'video'; my $name= new_domain_name(); my $domain_b = $BASE->clone( diff --git a/t/request/30_hardware_clones.t b/t/request/30_hardware_clones.t index bd869ab2d..b033fedac 100644 --- a/t/request/30_hardware_clones.t +++ b/t/request/30_hardware_clones.t @@ -142,6 +142,8 @@ sub _clean_hw($name, @hw) { $item->{file} = ''; } elsif ($name eq 'filesystem') { $item->{_id} = ''; + } elsif ($name eq 'network') { + delete $item->{hwaddr}; } lock_hash(%$item); } diff --git a/t/request/70_network.t b/t/request/70_network.t index 29b81b020..2335bdc72 100644 --- a/t/request/70_network.t +++ b/t/request/70_network.t @@ -44,6 +44,7 @@ sub test_list_nats($vm) { } sub _remove_qemu_bridges($vm, $bridges) { + return @$bridges if $vm->type eq 'Void'; my @nat = $vm->list_network_interfaces('nat'); my %bridges = map { $_ => 1 } @$bridges; diff --git a/t/user/20_grants.t b/t/user/20_grants.t index 55e119e38..9c2d7b000 100644 --- a/t/user/20_grants.t +++ b/t/user/20_grants.t @@ -961,7 +961,6 @@ sub test_view_all($vm) { } for my $req ( $req_start_admin, $req_prepare_admin, $req_start ,$req_refresh, $req_refresh_ports) { - diag($req->command); is($req->status,'done'); next if $req->command =~ /refresh_machine_ports/i; is($req->error,'', $req->command) or exit; diff --git a/t/user/networks.t b/t/user/networks.t new file mode 100644 index 000000000..9e5690532 --- /dev/null +++ b/t/user/networks.t @@ -0,0 +1,198 @@ +#!perl + +use strict; +use warnings; +use Test::More; + +use lib 't/lib'; +use Test::Ravada; + +use Ravada; + +use Data::Dumper; +use Mojo::JSON qw(decode_json); + +no warnings "experimental::signatures"; +use feature qw(signatures); + +############################################################################## + +sub test_create_network($vm) { + my $req_new= Ravada::Request->new_network( + uid => user_admin->id + ,id_vm => $vm->id + ,name => base_domain_name() + ); + wait_request(); + my $data = decode_json($req_new->output); + + my $req = Ravada::Request->create_network( + uid => user_admin->id + ,id_vm => $vm->id + ,data => $data + ); + wait_request(debug => 0); + my @networks = $vm->list_virtual_networks(); + my ($network) = grep {$_->{name} eq $data->{name} } @networks; + ok($network) or die "Error, network $data->{name} not created " + .Dumper(\@networks); + + return $network; +} + +sub test_default_owner(@networks) { + for my $net (@networks) { + is($net->{id_owner},Ravada::Utils::user_daemon->id); + } +} + +sub test_grant_access($vm) { + my $user = create_user(); + user_admin->grant($user,'create_networks'); + + my $req_new= Ravada::Request->new_network( + uid => $user->id + ,id_vm => $vm->id + ,name => base_domain_name() + ); + wait_request(); + is($req_new->error,''); + my $data = decode_json($req_new->output); + + my $req_create = Ravada::Request->create_network( + uid => $user->id + ,id_vm => $vm->id + ,data => $data + ); + + wait_request(); + + my ($network) = grep {$_->{name} eq $data->{name} } + $vm->list_virtual_networks(); + + ok($network) or die "Error: network not created ".Dumper($data); + + my $networks = rvd_front->list_networks($vm->id , $user->id); + my ($network_f) = grep {$_->{name} eq $data->{name}} @$networks; + + ok($network_f) or die "Network $data->{name} not found "; + + is($network_f->{_owner}->{id}, $user->id); + is($network_f->{_owner}->{name}, $user->name); +} + +sub test_deny_access($vm) { + my $user = create_user(); + my $networks = rvd_front->list_networks($vm->id , $user->id); + ok(scalar(@$networks)); + + $networks = rvd_front->list_networks($vm->id , user_admin->id); + ok(scalar(@$networks)); + + test_default_owner(@$networks); + + my $network = test_create_network($vm); + + my $req_new= Ravada::Request->new_network( + uid => $user->id + ,id_vm => $vm->id + ,name => base_domain_name() + ); + wait_request(check_error => 0); + like($req_new->error,qr/not authorized/); + + my $req_change = Ravada::Request->change_network( + uid => $user->id + ,data => $network + ); + wait_request(check_error => 0); + like($req_change->error,qr/not authorized/); + + my $req_delete = Ravada::Request->remove_network( + uid => $user->id + ,id => $network->{id} + ); + wait_request(check_error => 0, debug => 0); + like($req_delete->error,qr/not authorized/); + my $networks2 = rvd_front->list_networks($vm->id , user_admin->id); + my ($found2) = grep { $_->{name} eq $network->{name} } @$networks2; + ok($found2,"Expecting network $network->{name} $network->{id} not removed ".Dumper($networks2)) or return; + + my $req_create = Ravada::Request->create_network( + uid => $user->id + ,id_vm => $vm->id + ,data => $network + ); + wait_request(check_error => 0); + like($req_create->error,qr/not authorized/); + + my $req_list = Ravada::Request->list_networks( + uid => $user->id + ,id_vm => $vm->id + ); + wait_request(check_error => 0); + like($req_list->error,qr/not authorized/); + + user_admin->grant($user,'create_networks'); + $req_new->status('requested'); + $req_change->status('requested'); + $req_list->status('requested'); + + wait_request(check_error => 0); + is($req_new->error,''); + like($req_change->error,qr/not authorized/); + is($req_change->status(),'done'); + is($req_list->error,''); + + $req_delete->status('requested'); + wait_request(check_error => 0); + like($req_delete->error,qr/not authorized/); + + $req_create->status('requested'); + my $new_data = decode_json($req_new->output); + $req_create->arg('data' => $new_data); + $req_create->status('requested'); + wait_request(); + + is($req_create->error, ''); + + $req_list->status('requested'); + wait_request(); + + my $new_list = decode_json($req_list->output); + my ($found) = grep { $_->{name} eq $new_data->{name} } @$new_list; + + ok($found,"Expecting new network $new_data->{name}"); + $user->remove(); +} + +############################################################################## + +init(); + +for my $vm_name( vm_names() ) { + my $vm; + eval { $vm = rvd_back->search_vm($vm_name) }; + + SKIP: { + my $msg = "SKIPPED test: No $vm_name VM found "; + if ($vm && $vm_name =~ /kvm/i && $>) { + $msg = "SKIPPED: Test must run as root"; + $vm= undef; + } + + diag($msg) if !$vm; + skip $msg,10 if !$vm; + + diag("Testing networks access on $vm_name"); + + Ravada::Request->list_networks(id_vm => $vm->id, uid => user_admin->id); + wait_request( debug => 0); + test_deny_access($vm); + test_grant_access($vm); + } + +} + +end(); +done_testing(); diff --git a/t/vm/networking.t b/t/vm/networking.t new file mode 100644 index 000000000..a4d4f231b --- /dev/null +++ b/t/vm/networking.t @@ -0,0 +1,809 @@ +use warnings; +use strict; + +use Carp qw(confess); +use Data::Dumper; +use Mojo::JSON qw(decode_json); +use Storable qw(dclone); +use Test::More; + +use YAML qw(Dump LoadFile DumpFile); + +no warnings "experimental::signatures"; +use feature qw(signatures); + +use lib 't/lib'; +use Test::Ravada; + +my $N = 100; +######################################################################## + +sub test_list_networks($vm) { + my @list = $vm->list_virtual_networks(); + my ($default)= @list; + ok($default) or exit; + + my $public=0; + for my $net ( @list ) { + ok($net->{id_vm}) or die "Error: network missing id_vm ".Dumper($net); + my ($found) = _search_network(internal_id=>$net->{internal_id}); + is($found->{name}, $net->{name}); + is($found->{id_vm}, $net->{id_vm}); + $public++ if $found->{is_public}; + ($found) = _search_network(id_vm => $net->{id_vm}, name => $net->{name}); + die "Error: network not found id_vm= $net->{id_vm}, name=$net->{name}" + if !$found; + is($found->{internal_id}, $net->{internal_id}) or die Dumper($found); + } + ok($public,"Expecting at least one public network discovered, got $public") or exit; + +} + +sub _search_network(%args) { + my $sql = "SELECT * FROM virtual_networks " + ." WHERE ".join(" AND ",map { "$_=?" } sort keys %args); + my $sth = connector->dbh->prepare($sql); + $sth->execute(map { $args{$_} } sort keys %args); + my $found = $sth->fetchrow_hashref; + return $found; +} + +sub _get_active_network($vm) { + my($active) = grep { $_->{is_active} } $vm->list_virtual_networks(); + return $active if $active; + + my @networks = $vm->list_virtual_networks(); + for my $old (@networks) { + Ravada::Request->change_network( + uid => user_admin->id + ,data => { %$old, is_active => 1 } + ); + wait_request(); + my ($net) = grep {$_->{id} == $old->{id}} $vm->list_virtual_networks(); + return $net if $net->{is_active}; + } + die "Error: No network could be activated ".Dumper(\@networks); +} +sub test_create_fail ($vm) { + + my @networks = $vm->list_virtual_networks(); + + my $net0 = _get_active_network($vm); + my $name = new_domain_name; + my $net = { + name => $name + ,id_vm => $vm->id + ,ip_address => $net0->{ip_address} + ,ip_netmask => '255.255.255.0' + ,is_active => 1 + }; + my $req = Ravada::Request->create_network( + uid => user_admin->id + ,id_vm => $vm->id + ,data => $net + ); + wait_request(check_error => 0, debug => 0); + like($req->error,qr/Network is already in use/) or die $name; + my $out = decode_json($req->output); + like($out->{id_network},qr/^\d+$/) or exit; + + my ($old) = grep { $_->{id} eq $out->{id_network} } @networks; + ok(!$old,"Expecting new network is created"); +} + +sub test_duplicate_add($vm, $net) { + my $net2 = dclone($net); + delete $net2->{id}; + delete $net2->{internal_id}; + my $req = Ravada::Request->create_network( + data => $net2 + ,uid => user_admin->id + ,id_vm => $vm->id + ); + wait_request( check_error => 0, debug => 0); + like($req->error,qr/already exist/); +} + +sub test_duplicate_bridge_add($vm, $net) { + my ($net0) = grep { $_->{name} eq $net->{name}} $vm->list_virtual_networks(); + my $net2 = dclone($net); + delete $net2->{id}; + delete $net2->{internal_id}; + delete $net2->{dhcp_start}; + delete $net2->{dhcp_end}; + $net2->{bridge} = $net0->{bridge}; + $net2->{name} = new_domain_name(); + $net2->{ip_address} = '192.51.200.1'; + + my $req = Ravada::Request->create_network( + data => $net2 + ,uid => user_admin->id + ,id_vm => $vm->id + ); + wait_request( check_error => 0, debug => 0); + is($req->output,'{}'); + like($req->error,qr/already exists/) or exit; + + my ($net_created) = grep {$net2->{name} eq $_->{name} } + $vm->list_virtual_networks(); + + ok(!$net_created) or die "Expecting $net2->{name} not created"; + + Ravada::Request->remove_network( + uid => user_admin->id + ,id => $net_created->{id} + ) if $net_created; +} + +# only admins or users that can manage all networks can do this +sub test_change_owner($vm) { + my $user = create_user(); + my $new = $vm->new_network(new_domain_name); + my $req = Ravada::Request->create_network( + uid => user_admin->id + ,id_vm => $vm->id + ,data => $new + ); + wait_request(); + + my($new2) = grep { $_->{name} eq $new->{name} } $vm->list_virtual_networks(); + ok($new2) or return; + $new2->{id_owner} = $user->id; + my $req_change = Ravada::Request->change_network( + uid => $user->id + ,data => $new2 + ); + + wait_request(check_error => 0, debug => 0); + + my($new2b) = grep { $_->{name} eq $new->{name} } $vm->list_virtual_networks(); + is($new2b->{id_owner}, user_admin->id); + + like($req_change->error,qr/not authorized/) or exit; + + user_admin->grant($user, 'create_networks'); + $req_change->status('requested'); + + wait_request(check_error => 0); + like($req_change->error,qr/not authorized/); + my($new2c) = grep { $_->{name} eq $new->{name} } $vm->list_virtual_networks(); + is($new2c->{id_owner}, user_admin->id); + + user_admin->grant($user, 'manage_all_networks'); + $req_change->status('requested'); + wait_request(check_error => 0); + is($req_change->error, ''); + + my($new3) = grep { $_->{name} eq $new->{name} } $vm->list_virtual_networks(); + is($new3->{id_owner}, $user->id); + + $new2->{id_owner} = user_admin->id; + my $req_change2 = Ravada::Request->change_network( + uid => $user->id + ,data => $new2 + ); + + wait_request(check_error => 0); + + is($req_change2->error, ''); + + my($new4) = grep { $_->{name} eq $new->{name} } $vm->list_virtual_networks(); + is($new4->{id_owner}, user_admin->id); + +} + +sub test_add_network($vm) { + my $req_new = Ravada::Request->new_network( + uid => user_admin->id + ,id_vm => $vm->id + ,name => base_domain_name() + ); + wait_request(debug => 0); + like($req_new->output , qr/\d+/) or exit; + + my $net = decode_json($req_new->output); + my $name = $net->{name}; + + my $user = create_user(); + my $req = Ravada::Request->create_network( + uid => $user->id + ,id_vm => $vm->id + ,data => $net + ); + wait_request(check_error => 0); + like($req->error,qr/not authorized/); + my($new0) = grep { $_->{name} eq $name } $vm->list_virtual_networks(); + ok(!$new0,"Expecting no new network $name created") or return; + + $req = Ravada::Request->create_network( + uid => user_admin->id + ,id_vm => $vm->id + ,data => $net + ); + wait_request( debug => 0); + + my $out = decode_json($req->output); + my($new) = grep { $_->{name} eq $name } $vm->list_virtual_networks(); + ok($new,"Expecting new network $name created") or die Dumper([$vm->list_virtual_networks]); + isa_ok($out,'HASH') + && is($out->{id_network},$new->{id}); + + like($new->{dhcp_start},qr/.*\.2$/); + like($new->{dhcp_end},qr/.*\.254$/); + ok($new->{internal_id}); + is($new->{is_active},1); + is($new->{autostart},1); + is($new->{is_public},0); + return $new; +} + +sub test_remove_user($vm) { + my $user = create_user(); + user_admin->make_admin($user->id); + my $req = Ravada::Request->new_network( + uid => $user->id + ,id_vm => $vm->id + ,name => base_domain_name() + ); + wait_request(debug => 0); + + my $data = decode_json($req->output); + is($data->{id_vm},$vm->id); + + my $req_create = Ravada::Request->create_network( + uid => $user->id + ,id_vm => $vm->id + ,data => $data + ); + wait_request(debug => 0); + + my($new0) = grep { $_->{name} eq $data->{name} } $vm->list_virtual_networks(); + is($new0->{id_owner}, $user->id) or exit; + + $user->remove(); + wait_request(debug => 0); + + my($new) = grep { $_->{name} eq $data->{name} } $vm->list_virtual_networks(); + ok(!$new,"Expecting removed network $new0->{id} $data->{name}") or exit; +} +sub test_remove_network($vm, $net) { + my $user = create_user(); + my $req = Ravada::Request->remove_network( + uid => $user->id + ,id => $net->{id} + ); + wait_request(check_error => 0); + like($req->error,qr/not authorized/); + user_admin->make_admin($user->id); + $req->status('requested'); + + wait_request(debug => 0); + + my($new) = grep { $_->{name} eq $net->{name} } $vm->list_virtual_networks(); + ok(!$new,"Expecting removed network $net->{name}") or exit; +} + +sub _check_network_changed($net, $field) { + my $sth = connector->dbh->prepare( + "SELECT * FROM virtual_networks WHERE id=?" + ); + $sth->execute($net->{id}); + my $row = $sth->fetchrow_hashref; + is($row->{$field},$net->{$field}, $field) or exit; +} + +sub test_change_network($net) { + my %net2 = %$net; + $net2{dhcp_end} =~ s/(.*)\.\d+$/$1.100/; + my $req = Ravada::Request->change_network( + uid => user_admin->id + ,data => \%net2 + ); + wait_request(); + my $vm = Ravada::VM->open($net->{id_vm}); + + my($new) = grep { $_->{name} eq $net->{name} } $vm->list_virtual_networks(); + is($new->{dhcp_end},$net2{dhcp_end}) or die $net->{name}; + + _check_network_changed($new,'dhcp_end'); + + for my $is (0, 1) { + $net2{is_active} = $is; + $req = Ravada::Request->change_network( + uid => user_admin->id + ,data => \%net2 + ); + wait_request(debug => 0); + + ($new) = grep { $_->{name} eq $net->{name} } $vm->list_virtual_networks(); + is($new->{is_active},$is); + _check_network_changed($new,'is_active'); + } + + $net2{autostart} = 0; + $req = Ravada::Request->change_network( + uid => user_admin->id + ,data => \%net2 + ); + wait_request(); + + ($new) = grep { $_->{name} eq $net->{name} } $vm->list_virtual_networks(); + is($new->{autostart},0); + _check_network_changed($new,'autostart'); + + for ( 1 .. 2 ) { + $net2{is_active} = (!$net2{is_active} or 0); + $req = Ravada::Request->change_network( + uid => user_admin->id + ,data => \%net2 + ); + wait_request(); + my ($new2) = grep { $_->{name} eq $net2{name} } $vm->list_virtual_networks(); + is($new2->{is_active},$net2{is_active}); + } + + for ( 1 .. 2 ) { + $net2{is_public} = (!$net2{is_public} or 0); + $req = Ravada::Request->change_network( + uid => user_admin->id + ,data => \%net2 + ); + wait_request(debug => 0); + my ($new2) = grep { $_->{name} eq $net2{name} } $vm->list_virtual_networks(); + is($new2->{is_public}, $net2{is_public},"Expecting is_public=$net2{is_public}") + or die $net2{name}; + } + + my ($default) = grep { $_->{name} eq 'default' } $vm->list_virtual_networks(); + $net2{bridge} = $default->{bridge}; + $req = Ravada::Request->change_network( + uid => user_admin->id + ,data => \%net2 + ); + wait_request(check_error => 0, debug => 0); + like($req->error,qr/already in use/); + + $net2{name} = new_domain_name(); + $net2{bridge}='virbr99'; + $req = Ravada::Request->change_network( + uid => user_admin->id + ,data => \%net2 + ); + wait_request(check_error => 0 ); + + like($req->error,qr/can not be renamed/); + +} + +sub test_assign_network($vm, $net) { + my $id_iso = search_id_iso('Alpine%'); + my $name = new_domain_name(); + + my $req = Ravada::Request->create_domain( + id_iso => $id_iso + ,vm => $vm->type + ,name => $name + ,id_owner => user_admin->id + ,options => { + network => $net->{name} + } + ); + wait_request(debug => 1); + + my $domain = rvd_back->search_domain($name); + ok($domain); + + _check_domain_network($domain, $net->{name}); +} + +sub test_assign_network_clone($vm, $net, $volatile) { + my $id_iso = search_id_iso('Alpine%'); + my $name = new_domain_name(); + + my $req = Ravada::Request->create_domain( + id_iso => $id_iso + ,vm => $vm->type + ,name => $name + ,id_owner => user_admin->id + ); + wait_request(debug => 0); + + my $domain = rvd_back->search_domain($name); + $domain->prepare_base(user_admin); + $domain->volatile_clones(1); + + my $name_clone = new_domain_name(); + my $req2 = Ravada::Request->create_domain( + id_base => $domain->id + ,id_owner => user_admin->id + ,options => { network => $net->{name}} + ,name => $name_clone + ); + wait_request(debug => 1); + my $clone = rvd_back->search_domain($name_clone); + ok($clone); + + _check_domain_network($clone, $net->{name}); +} + +sub _check_domain_network_kvm($domain, $net_name) { + + my $doc = XML::LibXML->load_xml(string => $domain->xml_description()); + + my ($net_source) = $doc->findnodes("/domain/devices/interface/source"); + is($net_source->getAttribute('network'),$net_name); +} + +sub _check_domain_network_void($domain, $net_name) { + my $config = $domain->get_config(); + ok($config->{hardware}->{network}->[0]->{name}, $net_name); +} + +sub _check_domain_network($domain, $net_name) { + if ($domain->type eq 'KVM') { + _check_domain_network_kvm($domain, $net_name); + } else { + _check_domain_network_void($domain, $net_name); + } +} + +sub test_change_network_internal($vm, $net) { + if ($vm->type eq 'KVM') { + test_change_network_internal_kvm($vm, $net); + } elsif ($vm->type eq 'Void') { + test_change_network_internal_void($vm, $net); + } +} + +sub test_change_network_internal_void($vm, $net) { + my $file_out = $vm->dir_img."/networks/".$net->{name}.".yml"; + + my $start_new = $net->{dhcp_start}; + my ($n) = $start_new =~ /.*\.(\d+)/; + $n++; + $start_new =~ s/(.*)\.(\d+)/$1.$n/; + $net->{dhcp_start}=$start_new; + + DumpFile($file_out,$net); + + my ($net2) = grep { $_->{name} eq $net->{name} } $vm->list_virtual_networks(); + is($net2->{dhcp_start},$start_new) or die Dumper($net2); +} + +sub test_change_network_internal_kvm($vm, $net) { + my $network = $vm->vm->get_network_by_name($net->{name}); + die "Error: I can't find network $net->{name}" if !$network; + + my $doc = XML::LibXML->load_xml( string => $network->get_xml_description ); + my ($range) = $doc->findnodes("/network/ip/dhcp/range"); + my $start_new = $range->getAttribute('start'); + my ($n) = $start_new =~ /.*\.(\d+)/; + $n++; + $start_new =~ s/(.*)\.(\d+)/$1.$n/; + $range->setAttribute('start' , $start_new); + + $network->destroy(); + $network= $vm->vm->define_network($doc->toString); + $network->create(); + + my $network2 = $vm->vm->get_network_by_name($net->{name}); + my ($range2) = $doc->findnodes("/network/ip/dhcp/range"); + is($range2->getAttribute('start'), $start_new) or exit; + + my ($net2) = grep { $_->{name} eq $net->{name} } $vm->list_virtual_networks(); + is($net2->{dhcp_start},$start_new) or exit; + +} + +sub test_changed_uuid($vm) { + if($vm->type eq 'KVM') { + test_changed_uuid_kvm($vm); + } +} + +sub test_changed_uuid_kvm($vm) { + my $net = test_add_network($vm); + + my $network = $vm->vm->get_network_by_name($net->{name}); + my $doc = XML::LibXML->load_xml(string => $network->get_xml_description()); + + $network->destroy() if $network->is_active; + $network->undefine(); + + my ($uuid_xml) = $doc->findnodes("/network/uuid"); + $uuid_xml->removeChildNodes(); + my $new_uuid = $vm->_unique_uuid(); + $uuid_xml->appendText($new_uuid); + + $vm->vm->define_network($doc->toString()); + + my ($net2) = grep { $_->{name} eq $net->{name} } $vm->list_virtual_networks(); + ok($net2,"Expecting $net->{name} found"); + is($net2->{internal_id},$new_uuid) or die Dumper($net2); + + my $sth = connector->dbh->prepare("SELECT * FROM virtual_networks " + ." WHERE name=?" + ); + $sth->execute($net->{name}); + my $row = $sth->fetchrow_hashref; + is($row->{internal_id},$new_uuid) or exit; + +} + +sub _remove_network_internal($vm,$name) { + if ($vm->type eq 'KVM') { + _remove_network_internal_kvm($vm, $name); + } elsif ($vm->type eq 'Void') { + _remove_network_internal_void($vm, $name); + } else { + die $vm->type; + } +} + +sub _remove_network_internal_kvm($vm, $name) { + my $network = $vm->vm->get_network_by_name($name); + $network->destroy() if $network->is_active; + $network->undefine(); +} + +sub _remove_network_internal_void($vm, $name) { + my $file_out = $vm->dir_img."/networks/$name.yml"; + unlink $file_out or die "$! $file_out" if $vm->file_exists($file_out); +} + +sub test_disapeared_network($vm) { + my ($default0) = $vm->list_virtual_networks(); + + my $net = test_add_network($vm); + _remove_network_internal($vm, $net->{name}); + + my ($net2) = grep { $_->{name} eq $net->{name} } $vm->list_virtual_networks(); + ok(!$net2, "Expecting $net->{name} removed"); + + my $sth = connector->dbh->prepare("SELECT * FROM virtual_networks WHERE name=?"); + $sth->execute($net->{name}); + my $row = $sth->fetchrow_hashref; + ok(!$row,"Expected $net->{name} removed from db".Dumper($row)) or exit; + + my ($default) = grep { $_->{name} eq $default0->{name} } $vm->list_virtual_networks(); + ok($default) or exit; + + $sth->execute($default0->{name}); + $row = $sth->fetchrow_hashref; + ok($row,"Expected default not removed from db".Dumper($row)) or exit; + +} + +sub test_add_down_network($vm) { + + my $test = test_add_network($vm); + $test->{is_active} = 0; + my $req = Ravada::Request->change_network( + uid => user_admin->id + ,data => $test + ); + wait_request(); + + my $sth = connector->dbh->prepare("DELETE FROM virtual_networks " + ." WHERE name=? " + ); + $sth->execute($test->{name}); + + my ($net2) = grep { $_->{name} eq $test->{name} } $vm->list_virtual_networks(); + ok($net2,"Expecting $test->{name} network listed") or exit; +} + +sub test_public_network($vm, $net) { + + my $net2 = dclone($net); + my $user2 = create_user(); + + my $req_change = Ravada::Request->change_network( + uid => $user2->id + ,data => {%$net, id_owner => $user2->id } + ); + + wait_request(check_error => 0); + like($req_change->error,qr/not authorized/); + + my $req_change2 = Ravada::Request->change_network( + uid => user_admin->id + ,data => {%$net, id_owner => $user2->id, is_public=>1 } + ); + + wait_request(check_error => 0, debug => 0); + is($req_change2->error,''); + + my $domain = create_domain($vm); + $domain->is_public(1); + $domain->prepare_base(user_admin); + + user_admin->grant($user2,'change_settings'); + user_admin->grant($user2,'create_networks'); + + my $clone = $domain->clone(user => $user2,name => new_domain_name); + + my $hw_net = $clone->info(user_admin)->{hardware}->{network}->[0]; + ok($hw_net) or die $clone->name; + my %hw_net2 = %$hw_net; + + my $list_nets = rvd_front->list_networks($vm->id,$user2->id); + ok(scalar(@$list_nets) >= 1,"Expecting at least 1 network allowed, got " + .scalar(@$list_nets)) or exit; + + $hw_net2{network}=$net->{name}; + is($user2->can_change_hardware_network($clone, \%hw_net2),1) or exit; + + my $req = Ravada::Request->change_hardware( + uid => $user2->id + ,id_domain => $clone->id + ,hardware => 'network' + ,index => 0 + ,data => \%hw_net2 + ); + wait_request(check_error => 0, debug => 0); + is($req->error,''); + + + my $net3 = _search_network(id => $net->{id}); + is($net3->{id_owner}, $user2->id) or exit; + + is($user2->can_change_hardware_network($clone, {network => $net3->{name}}),1) or exit; + + $req->status('requested'); + wait_request(check_error => 0); + is($req->error, ''); + + my $clone4 = Ravada::Front::Domain->open($clone->id); + my $hw_net4 = $clone4->info(user_admin)->{hardware}->{network}->[0]; + + is($hw_net2{network}, $net->{name}); +} + +sub test_manage_all_networks($vm, $net) { + + my $net2 = dclone($net); + my $user2 = create_user(); + + my $req_change2 = Ravada::Request->change_network( + uid => user_admin->id + ,data => {%$net, is_public=>0 } + ); + + wait_request(check_error => 0, debug => 0); + is($req_change2->error,''); + + my $domain = create_domain($vm); + $domain->is_public(1); + $domain->prepare_base(user_admin); + + user_admin->grant($user2,'change_settings'); + user_admin->grant($user2,'manage_all_networks'); + + my $clone = $domain->clone(user => $user2,name => new_domain_name); + + my $hw_net = $clone->info(user_admin)->{hardware}->{network}->[0]; + ok($hw_net) or die $clone->name; + my %hw_net2 = %$hw_net; + + $hw_net2{network}=$net->{name}; + is($user2->can_change_hardware_network($clone, \%hw_net2),1) or exit; + + my $req = Ravada::Request->change_hardware( + uid => $user2->id + ,id_domain => $clone->id + ,hardware => 'network' + ,index => 0 + ,data => \%hw_net2 + ); + wait_request(check_error => 0, debug => 0); + is($req->error,''); + + my $clone4 = Ravada::Front::Domain->open($clone->id); + my $hw_net4 = $clone4->info(user_admin)->{hardware}->{network}->[0]; + + is($hw_net2{network}, $net->{name}); +} + +sub test_new_network($vm) { + my $req = Ravada::Request->new_network( + uid => user_admin->id + ,id_vm => $vm->id + ,name => base_domain_name()."_" + ); + wait_request(); + my $data = decode_json($req->output); + is($data->{id_vm},$vm->id); + + my $req_create = Ravada::Request->create_network( + uid => user_admin->id + ,id_vm => $vm->id + ,data => $data + ); + wait_request(debug => 0); + + my $req2 = Ravada::Request->new_network( + uid => user_admin->id + ,id_vm => $vm->id + ,name => base_domain_name()."_" + ); + wait_request(); + my $new_net = decode_json($req_create->output); + + my $data2 = decode_json($req2->output); + for my $field( keys %$data) { + next if $field =~ /^(id_vm|ip_netmask|is_active|autostart)/; + + isnt($data2->{$field}, $data->{$field},$field); + } + Ravada::Request->remove_network( + uid => user_admin->id + ,id => $new_net->{id_network} + ); + wait_request(); + +} + +######################################################################## + +init(); +clean(); + +for my $vm_name ( vm_names() ) { + diag("testing $vm_name"); + + SKIP: { + + my $msg = "SKIPPED test: No $vm_name VM found "; + my $vm; + if ($vm_name eq 'KVM' && $>) { + $msg = "SKIPPED: Test must run as root"; + } else { + $vm = Ravada::VM->open( type => $vm_name ); + } + + diag($msg) if !$vm; + skip $msg,10 if !$vm; + + is($vm->has_networking,1) if $vm_name eq 'KVM' + || $vm_name eq 'Void'; + next if !$vm->has_networking(); + + test_remove_user($vm); + + test_create_fail($vm); + + test_list_networks($vm); + + my $net = test_add_network($vm); + test_assign_network($vm, $net); + test_assign_network_clone($vm, $net, 0); + test_assign_network_clone($vm, $net, 1); # volatile clone + + test_manage_all_networks($vm,$ net); + test_public_network($vm, $net); + + test_change_owner($vm); + + test_new_network($vm); + + test_duplicate_add($vm, $net); + + test_duplicate_bridge_add($vm, $net); + + test_change_network_internal($vm, $net); + test_change_network($net); + + test_changed_uuid($vm); + + test_disapeared_network($vm); + test_add_down_network($vm); + + test_remove_network($vm,$net); + } +} + +end(); + +done_testing(); + diff --git a/t/vm/v10_volatile.t b/t/vm/v10_volatile.t index a41de96c1..a3efdc752 100644 --- a/t/vm/v10_volatile.t +++ b/t/vm/v10_volatile.t @@ -13,7 +13,7 @@ no warnings "experimental::signatures"; use feature qw(signatures); use_ok('Ravada'); -use_ok('Ravada::Network'); +use_ok('Ravada::Route'); use lib 't/lib'; use Test::Ravada; diff --git a/templates/bootstrap/navigation.html.ep b/templates/bootstrap/navigation.html.ep index 81459d664..233e7a5a5 100644 --- a/templates/bootstrap/navigation.html.ep +++ b/templates/bootstrap/navigation.html.ep @@ -45,9 +45,13 @@ navbar-dark bg-dark fixed-top navbar-expand-lg navbar-inverse"> % } % if ($_user->is_admin) { <a class="dropdown-item" href="/admin/nodes"><i class="fa fa-server" aria-hidden="true"></i> <%=l 'Nodes' %></a> - <a class="dropdown-item" href="/admin/networks"><i class="fa fa-sitemap" aria-hidden="true"></i> <%=l 'Networks' %></a> + <a class="dropdown-item" href="/admin/routes"><i class="fa fa-globe" aria-hidden="true"></i> <%=l 'Routes' %></a> <a class="dropdown-item" href="/admin/storage"><i class="fa fa-hdd" aria-hidden="true"></i> <%=l 'Storage' %></a> % } + +% if ($_user->can_create_networks) { + <a class="dropdown-item" href="/admin/networks"><i class="fa fa-sitemap" aria-hidden="true"></i> <%=l 'Networks' %></a> +% } <a class="dropdown-item" href="/admin/messages"><i class="fa fa-envelope" aria-hidden="true"></i> <%=l 'Messages' %></a> % if ($monitoring) { <a class="dropdown-item" href="/admin/monitoring"><i class="fas fa-tachometer-alt" aria-hidden="true"></i> <%=l 'Monitoring' %></a> diff --git a/templates/main/admin_networks.html.ep b/templates/main/admin_networks.html.ep index 28fcb576d..69867c8dc 100644 --- a/templates/main/admin_networks.html.ep +++ b/templates/main/admin_networks.html.ep @@ -4,7 +4,8 @@ <body id="page-top" data-spy="scroll" data-target=".navbar-fixed-top" role="document"> <div id="wrapper"> %= include 'bootstrap/navigation' - <div id="page-wrapper" ng-controller="manage_networks"> + <div id="page-wrapper" ng-controller='manage_networks' + ng-init="init('<%= $id_vm %>')"> <div id="admin-content"> <div class="row"> <div class="col-md-8"><h2><%=l 'Networks' %></h2></div> @@ -15,36 +16,66 @@ </h2> </div> </div> + <div class="row"> - <div class="col-md-4"></div> - <!-- <div class="col-md-2">enabled</div> --> - <div class="col-md-2"> - <%=l 'requires password' %> + <div class="col-md-2"><b><%=l 'Name' %></b></div> + <div class="col-md-1" + ng-show="<%= ( $_user->is_admin or $_user->can_manage_all_networks or 0 ) %>" + ><b><%=l 'Public' %></b></div> + + <div class="col-md-2" + ng-show="<%= ( $_user->is_admin or $_user->can_manage_all_networks or 0 ) %>" + ><b><%=l 'Owner' %></b></div> + <div class="col-md-2"><b><%=l 'Bridge' %></b></div> + <div class="col-md-2"><b><%=l 'Address' %></b></div> + <div class="col-md-1"><b><%=l 'Active' %></b></div> + <div class="col-md-2"><b><%=l 'Auto Start' %></b></div> + </div> + + <div class="row" ng-show="loaded_networks && networks.length==0"> + <div class="col-md-12"> + <div class="alert alert-warning"> + <%=l 'No networks found' %> + </div> </div> </div> - <div ng-repeat="network in networks | orderObjectBy:'n_order'" - class="row" + + <div ng-repeat="network in networks | orderObjectBy:'name'" + class="row" ng-cloak > <div class="col-md-2"> - <a href="/network/settings/{{network.id}}.html">{{network.name}}</a> + <a href="/network/settings/{{network.id}}.html" + ng-show="network._can_change">{{network.name}}</a> + <span ng-show="!network._can_change">{{network.name}}</span> </div> - <div class="col-md-2">{{network.address}}</div> - <!-- - <div class="col-md-2"> - <input type="checkbox" ng-model="network.no_domains" - ng-true-value="0" ng-false-value="1" - ng-change - ="update_network(network.id,'no_domains')" + <div class="col-md-1" + ng-show="<%= ( $_user->is_admin or $_user->can_manage_all_networks or 0 ) %>" > + + <span ng-show="network.is_public" + >✔</span> + <span ng-show="!network.is_public" + >✕</span> + </div> + + <div class="col-md-2" + ng-show="<%= ( $_user->is_admin or $_user->can_manage_all_networks or 0 ) %>" + >{{network._owner.name}}</div> + <div class="col-md-2">{{network.bridge}}</div> + <div class="col-md-2">{{network.ip_address}}</div> + <div class="col-md-1"> + <span ng-show="network.is_active" + >✔</span> + <span ng-show="!network.is_active" + >✕</span> </div> - --> <div class="col-md-2"> - <input type="checkbox" ng-model="network.requires_password" - ng-true-value="1" ng-false-value="0" - ng-change - ="update_network(network.id,'requires_password')" - > + <span ng-show="network.autostart" + >✔</span> + <span ng-show="!network.autostart" + >✕</span> </div> + </div> </div> </div> diff --git a/templates/main/admin_routes.html.ep b/templates/main/admin_routes.html.ep new file mode 100644 index 000000000..afed4cbe9 --- /dev/null +++ b/templates/main/admin_routes.html.ep @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<html ng-app="ravada.app"> +%= include 'bootstrap/header' +<body id="page-top" data-spy="scroll" data-target=".navbar-fixed-top" role="document"> +<div id="wrapper"> + %= include 'bootstrap/navigation' + <div id="page-wrapper" ng-controller="manage_routes"> + <div id="admin-content"> + <div class="row"> + <div class="col-md-8"><h2><%=l 'Routes' %></h2></div> + <div class="col-md-4" align="right"> + <h2><a type="button" + class="btn btn-success" href="/route/new"> + <b><%=l 'New Route' %></b></a> + </h2> + </div> + </div> + <div class="row"> + <div class="col-md-4"></div> + <!-- <div class="col-md-2">enabled</div> --> + <div class="col-md-2"> + <%=l 'requires password' %> + </div> + </div> + <div ng-repeat="network in routes | orderObjectBy:'n_order'" + class="row" + > + <div class="col-md-2"> + <a href="/route/settings/{{network.id}}.html">{{network.name}}</a> + </div> + <div class="col-md-2">{{network.address}}</div> + <!-- + <div class="col-md-2"> + <input type="checkbox" ng-model="network.no_domains" + ng-true-value="0" ng-false-value="1" + ng-change + ="update_network(network.id,'no_domains')" + > + </div> + --> + <div class="col-md-2"> + <input type="checkbox" ng-model="network.requires_password" + ng-true-value="1" ng-false-value="0" + ng-change + ="update_network(network.id,'requires_password')" + > + </div> + </div> + </div> + </div> +</div> + +%= include $footer +%= include 'bootstrap/scripts' +</body> +</html> diff --git a/templates/main/manage_machine_edit_net.html.ep b/templates/main/manage_machine_edit_net.html.ep index 9afaedc55..8606774e8 100644 --- a/templates/main/manage_machine_edit_net.html.ep +++ b/templates/main/manage_machine_edit_net.html.ep @@ -9,17 +9,23 @@ </li> <li class="list-group-item list-group-item-primary"><%=l 'type' %></li> <li class="list-group-item"> + +% if ($USER->is_admin || $USER->can_create_networks || $USER->can_manage_all_networks ) { <select ng-model="item.type" ng-change="network_edit[$index]=true" ng-options="type for type in ['bridge','NAT']" > </select> +% } else { + {{item.type}} +% } </li> <li class="list-group-item list-group-item-primary"> <span ng-show="item.type == 'NAT'"><%=l 'nat' %></span> <span ng-show="item.type == 'bridge'"><%=l 'bridge' %></span> </li> <li class="list-group-item"> +% if ($USER->is_admin || $USER->can_create_networks || $USER->can_manage_all_networks ) { <select ng-model="item.network" ng-change="network_edit[$index]=true" ng-show="item.type == 'NAT'" @@ -35,6 +41,10 @@ <span ng-hide="item.type == 'NAT' || network_bridges[0]"> <%=l 'No bridges found' %> </span> +% } else { + <span ng-show="item.type=='NAT'">{{item.network}}</span> + <span ng-show="item.type=='bridge'">{{item.bridge}}</span> +% } </li> </ul> </div> diff --git a/templates/main/network_new.html.ep b/templates/main/network_new.html.ep index b20f5efe4..79eb53170 100644 --- a/templates/main/network_new.html.ep +++ b/templates/main/network_new.html.ep @@ -9,7 +9,7 @@ <div class="page-header"> <div class="card" ng-controller="settings_network" - ng-init="init()" + ng-init="new_network(<%= $id_vm %>)" > <div class="card-header"> <h2><%=l 'New network' %></h2> @@ -17,9 +17,6 @@ <div class="card-body"> %= include '/main/network_options' </div> - <div class="col-md-8 alert alert-info" ng-show="error && formNetwork.$pristine"> - {{error}} - </div> </div><!-- card --> </div> <!-- page-header --> diff --git a/templates/main/network_options.html.ep b/templates/main/network_options.html.ep index d03ed5842..1376e0fac 100644 --- a/templates/main/network_options.html.ep +++ b/templates/main/network_options.html.ep @@ -1,92 +1,97 @@ -<div class="container-fluid" ng-hide="new_saved"> -<form name="formNetwork"> -<div class="row"> - <div class="col-md-3" align="right"><%=l 'name' %></div> +<div class="container-fluid" ng-cloak> + +<div class="col-md-8 alert alert-warning" ng-show="error"> +{{error}} +</div> + +<i class="fas fa-sync-alt fa-spin" ng-hide="network.id_vm"></i> + +<form name="form_network" ng-hide="network._removed || !network.id_vm"> + +<div class="form-group row"> + <div class="col-md-3" align="right"><%=l 'Name' %></div> <div class="col-md-8"> - <input type="text" name="name" ng-model="network.name" required - ng-change='check_duplicate("name")' - > - <span ng-show="formNetwork.name.$error.required"> - <%=l 'Network name is required' %> - </span> - <span ng-show="network._duplicated_name"> - <%=l 'This name is duplicated' %> - </span> + <input type="text" ng-model="network.name" required + name="name" + ng-disabled="network.id"/> </div> </div> -<div class="row"> - <div class="col-md-3" align="right"><%=l 'address' %></div> - <div class="col-md-8" align="left"> - <input type="text" name="address" ng-model="network.address" required ipaddress - ng-change='check_duplicate("address")' - > - <span ng-show="formNetwork.address.$error.required"><%=l 'Network address is required.' %></span> - <span ng-show="formNetwork.$error.ipformat"><%=l 'Invalid IP network address. Expecting a.b.c.d/e' %></span> - <span ng-show="network._duplicated_address"><%=l 'This address is duplicated' %></span> +<div class="form-group row"> + <div class="col-md-3" align="right"><%=l 'Bridge' %></div> + <div class="col-md-8"> + <input type="text" ng-model="network.bridge" required + name="bridge" + /> </div> </div> -<div class="row"> - <div class="col-md-3" align="right"><%=l 'password' %></div> - <div class="col-md-1" align="left"> - <input type="checkbox" name="address" ng-model="network.requires_password" - ng-true-value="1" ng-false-value="0" - > +<div class="form-group row"> + <div class="col-md-3" align="right"><%=l 'IP address' %></div> + <div class="col-md-3"> + <input type="text" required ng-model="network.ip_address"/> + </div> +</div> + +<div class="form-group row"> + <div class="col-md-3" align="right"><%=l 'Netmask' %></div> + <div class="col-md-3"> + <input type="text" required ng-model="network.ip_netmask"/> + </div> +</div> + +<div class="form-group row"> + <div class="col-md-3" align="right"><%=l 'DHCP start' %></div> + <div class="col-md-2"> + <input type="text" ng-model="network.dhcp_start"/> + </div> +</div> + +<div class="form-group row"> + <div class="col-md-3" align="right"><%=l 'DHCP end' %></div> + <div class="col-md-3"> + <input type="text" ng-model="network.dhcp_end"/> </div> - <div class="col-md-8"><%=l 'Requires viewer password when implemented' %></div> </div> -<div class="row"> - <div class="col-md-3" align="right"> - <%=l 'All machines' %> - </div> - <div class="col-md-1" align="left"> - <input type="checkbox" name="address" ng-model="network.all_domains" +<div class="form-group row"> + <div class="col-md-3" align="right"><%=l 'Auto Start' %></div> + <div class="col-md-3"> + <input type="checkbox" ng-model="network.autostart" ng-true-value="1" ng-false-value="0" - ng-change="check_all_domains()" - > + /> </div> - <div class="col-md-8"><%=l 'Users from this network can run all virtual machines' %></div> </div> -<div class="row"> - <div class="col-md-3" align="right"><%=l 'No machines' %></div> - <div class="col-md-1" align="left"> - <input type="checkbox" name="address" ng-model="network.no_domains" +<div class="form-group row"> + <div class="col-md-3" align="right"><%=l 'Active' %></div> + <div class="col-md-3"> + <input type="checkbox" ng-model="network.is_active" ng-true-value="1" ng-false-value="0" - ng-change="check_no_domains()" - > + /> </div> - <div class="col-md-8"><%=l 'Users from this network can run no virtual machines' %></div> </div> +<div class="form-group row"> + <div class="col-md-3" align="right"><%=l 'Public' %></div> + <div class="col-md-3"> + <input type="checkbox" ng-model="network.is_public" + ng-true-value="1" ng-false-value="0" + /> + </div> +</div> -<div class="row"> +<div class="form-group row"> <div class="col-md-3" align="right"></div> <div class="col-md-4" align="left"> <button ng-show="network.id" class="btn btn-outline-secondary" - ng-disabled="formNetwork.$pristine" - ng-click="load_network(network.id)"><%=l 'Cancel' %></button> - <button - class="btn btn-primary" - ng-click="update_network()" - ng-disabled="!formNetwork.$valid || formNetwork.$pristine - || (!network.all_domains && !network.no_domains) - || network._duplicated_name - || network._duplicated_address - "><%=l 'Save' %></button> - </div> - <div ng-show="!network.all_domains && !network.no_domains"> - <%=l 'Set either' %> <i><%=l 'all machines' %></i> <%=l 'or' %> <i><%=l 'no machines' %></i> <%=l 'for users in this network' %> + ng-click="load_network(network.id)"><%=l 'Cancel' %></button> + <button class="btn btn-primary" + ng-click="update_network(); form_options.$pristine=true" + ><%=l 'Save' %></button> </div> </div> </form> </div> -<div class="row"> - <div class="col-md-8 alert alert-info" ng-show="saved && formNetwork.$pristine"> - <%=l 'Network' %> {{network.name}} <%=l 'saved.' %> - </div> -</div> diff --git a/templates/main/network_remove.html.ep b/templates/main/network_remove.html.ep index 9043f8044..5d738edd3 100644 --- a/templates/main/network_remove.html.ep +++ b/templates/main/network_remove.html.ep @@ -1,8 +1,14 @@ -<div ng-show="network && network.id"> +<div ng-show="network && network.id && !network._removed"> <%=l 'Are you sure you want to remove the' %> <%= $item %> {{network.name}} + <br/> - <button ng-click="remove_network(network.id)"><%=l 'Yes' %></button> - <button onclick="location='/admin/networks'"><%=l 'Cancel' %></button> + <button class="btn btn-outline-secondary" ng-click="remove_network(network.id)"><%=l 'Yes' %></button> + <button class="btn btn-outline-secondary" onclick="location='/admin/networks'"><%=l 'Cancel' %></button> +</div> + +<div ng-show="network && network._removed"> + <%=l 'Network removed' %>. + <a href='/admin/networks'><%=l 'List Networks' %></a> </div> <div ng-show="message">{{message}}</div> diff --git a/templates/main/not_found.html.ep b/templates/main/not_found.html.ep new file mode 100644 index 000000000..d1b308f46 --- /dev/null +++ b/templates/main/not_found.html.ep @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +%= include 'bootstrap/header' +<body role="document"> + %= include 'bootstrap/navigation' + <header id="page-top" class="intro"> + <div class="intro-body"> + <div class="container" ng-controller=""> + <h2 class="form-signin-heading">ERROR</h2> + <p><%= $error %></p> + </div> + </div> + </header> + + %= include 'bootstrap/scripts' + %= include $footer +</body> +</html> diff --git a/templates/main/network_machines.html.ep b/templates/main/route_machines.html.ep similarity index 91% rename from templates/main/network_machines.html.ep rename to templates/main/route_machines.html.ep index 6c4311b11..f84beda01 100644 --- a/templates/main/network_machines.html.ep +++ b/templates/main/route_machines.html.ep @@ -5,7 +5,7 @@ <div class="row"> <div class="col-md-3" align="right"><%=l 'All machines' %></div> <div class="col-md-1" align="left"> - <input type="checkbox" name="address" ng-model="network.all_domains" + <input type="checkbox" name="address" ng-model="route.all_domains" ng-true-value="1" ng-false-value="0" ng-change="check_all_domains(); update_network()" > @@ -18,7 +18,7 @@ <div class="row"> <div class="col-md-3" align="right"><%=l 'No machines' %></div> <div class="col-md-1" align="left"> - <input type="checkbox" name="address" ng-model="network.no_domains" + <input type="checkbox" name="address" ng-model="route.no_domains" ng-true-value="1" ng-false-value="0" ng-change="check_no_domains(); update_network()" > @@ -30,7 +30,7 @@ <hr/> -<div ng-show="network.no_domains == 0"> +<div ng-show="route.no_domains == 0"> <div class="row"> <div class="col-md-2"> <%=l 'Public' %> diff --git a/templates/main/route_new.html.ep b/templates/main/route_new.html.ep new file mode 100644 index 000000000..4f00021a2 --- /dev/null +++ b/templates/main/route_new.html.ep @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html ng-app="ravada.app"> +%= include 'bootstrap/header' +<body id="page-top" data-spy="scroll" data-target="fixed-top" role="document"> +<div id="wrapper"> + %= include 'bootstrap/navigation' +<div id="page-wrapper"> + <!--BASES AND DOMAINS LIST--> + <div class="page-header"> + <div class="card" + ng-controller="settings_route" + ng-init="init()" + > + <div class="card-header"> + <h2><%=l 'New Route' %></h2> + </div> + <div class="card-body"> +%= include '/main/route_options' + </div> + <div class="col-md-8 alert alert-info" ng-show="error && formNetwork.$pristine"> + {{error}} + </div> + </div><!-- card --> + </div> <!-- page-header --> + +</div><!-- page-wrapper --> +</div><!-- wrapper --> +%= include $footer +%= include 'bootstrap/scripts' +</body> +</html> diff --git a/templates/main/route_options.html.ep b/templates/main/route_options.html.ep new file mode 100644 index 000000000..910c7daa5 --- /dev/null +++ b/templates/main/route_options.html.ep @@ -0,0 +1,92 @@ +<div class="container-fluid" ng-hide="new_saved"> +<form name="formNetwork"> +<div class="row"> + <div class="col-md-3" align="right"><%=l 'name' %></div> + <div class="col-md-8"> + <input type="text" name="name" ng-model="route.name" required + ng-change='check_duplicate("name")' + > + <span ng-show="formNetwork.name.$error.required"> + <%=l 'Network name is required' %> + </span> + <span ng-show="route._duplicated_name"> + <%=l 'This name is duplicated' %> + </span> + </div> +</div> + +<div class="row"> + <div class="col-md-3" align="right"><%=l 'address' %></div> + <div class="col-md-8" align="left"> + <input type="text" name="address" ng-model="route.address" required ipaddress + ng-change='check_duplicate("address")' + > + <span ng-show="formNetwork.address.$error.required"><%=l 'Network address is required.' %></span> + <span ng-show="formNetwork.$error.ipformat"><%=l 'Invalid IP network address. Expecting a.b.c.d/e' %></span> + <span ng-show="route._duplicated_address"><%=l 'This address is duplicated' %></span> + </div> +</div> + +<div class="row"> + <div class="col-md-3" align="right"><%=l 'password' %></div> + <div class="col-md-1" align="left"> + <input type="checkbox" name="address" ng-model="route.requires_password" + ng-true-value="1" ng-false-value="0" + > + </div> + <div class="col-md-8"><%=l 'Requires viewer password when implemented' %></div> +</div> + +<div class="row"> + <div class="col-md-3" align="right"> + <%=l 'All machines' %> + </div> + <div class="col-md-1" align="left"> + <input type="checkbox" name="address" ng-model="route.all_domains" + ng-true-value="1" ng-false-value="0" + ng-change="check_all_domains()" + > + </div> + <div class="col-md-8"><%=l 'Users from this network can run all virtual machines' %></div> +</div> + +<div class="row"> + <div class="col-md-3" align="right"><%=l 'No machines' %></div> + <div class="col-md-1" align="left"> + <input type="checkbox" name="address" ng-model="route.no_domains" + ng-true-value="1" ng-false-value="0" + ng-change="check_no_domains()" + > + </div> + <div class="col-md-8"><%=l 'Users from this network can run no virtual machines' %></div> +</div> + + +<div class="row"> + <div class="col-md-3" align="right"></div> + <div class="col-md-4" align="left"> + <button ng-show="route.id" class="btn btn-outline-secondary" + ng-disabled="formNetwork.$pristine" + ng-click="load_network(route.id)"><%=l 'Cancel' %></button> + <button + class="btn btn-primary" + ng-click="update_network()" + ng-disabled="!formNetwork.$valid || formNetwork.$pristine + || (!route.all_domains && !route.no_domains) + || route._duplicated_name + || route._duplicated_address + "><%=l 'Save' %></button> + </div> + <div ng-show="!route.all_domains && !route.no_domains"> + <%=l 'Set either' %> <i><%=l 'all machines' %></i> <%=l 'or' %> <i><%=l 'no machines' %></i> <%=l 'for users in this network' %> + </div> +</div> + +</form> +</div> + +<div class="row"> + <div class="col-md-8 alert alert-info" ng-show="saved && formNetwork.$pristine"> + <%=l 'Network' %> {{route.name}} <%=l 'saved.' %> + </div> +</div> diff --git a/templates/main/route_remove.html.ep b/templates/main/route_remove.html.ep new file mode 100644 index 000000000..8fcfd8243 --- /dev/null +++ b/templates/main/route_remove.html.ep @@ -0,0 +1,8 @@ +<div ng-show="route && route.id"> + <%=l 'Are you sure you want to remove the' %> <%= $item %> {{route.name}} + + <button ng-click="remove_route(route.id)"><%=l 'Yes' %></button> + <button onclick="location='/admin/routes'"><%=l 'Cancel' %></button> +</div> + +<div ng-show="message">{{message}}</div> diff --git a/templates/main/settings_generic.html.ep b/templates/main/settings_generic.html.ep index 0760b8ae4..0f19a787d 100644 --- a/templates/main/settings_generic.html.ep +++ b/templates/main/settings_generic.html.ep @@ -10,9 +10,22 @@ <div class="card" ng-cloak="1" ng-controller="settings_<%= $item %>" - ng-init="init('<%= $id %>', '<%= url_for('ws_subscribe')->to_abs %>')" + ng-init="init('<%= $id %>', '<%= url_for('ws_subscribe')->to_abs %>' + ,'<%= $id_vm %>')" > - <div class="card-header"><h2><%= $item %> {{<%= $item %>._old_name}}</h2></div> + <div class="card-header"> + <div class="row"> + <div class="col"> + <span class="title"> + <%= $item %> {{<%= $item %>._old_name}} + </span> + </div> + <div class="col" align="right"> + <a type="button" class="btn btn-outline-secondary" align="right" + href="/admin/<%= $item %>s">back</a> + </div> + </div> + </div> <div class="card-body"> <div class="row"> %= include "main/settings_generic_tabs" diff --git a/templates/main/settings_generic_tabs.html.ep b/templates/main/settings_generic_tabs.html.ep index 925f3246f..8ef327dee 100644 --- a/templates/main/settings_generic_tabs.html.ep +++ b/templates/main/settings_generic_tabs.html.ep @@ -1,7 +1,8 @@ % my %tabs_item = ( % node => ['options' , 'bases', 'remove' , 'hostdev'] -% ,network => ['options', 'machines', 'remove'] +% ,route => ['options', 'machines', 'remove'] % ,storage => ['list', 'options', 'purge'] +% ,network => ['options','remove'] % ); % my $tabs = $tabs_item{$item}; @@ -12,7 +13,7 @@ % my $active="active"; % my $selected="true"; <div class="col-2"> -<div class="card-body" ng-show="<%= $item %>.id"> +<div class="card-body"> <div class="nav flex-column nav-pills bg-light" id="v-pills-tab" role="tablist" aria-orientation="vertical"> % for my $current (@$tabs) { @@ -28,6 +29,7 @@ } %> <a class="nav-link <%= $active %>" id="v-pills-<%= $current %>-tab" + ng-hide="<%= $item %>._removed" href="#v-pills-<%= $current %>" data-toggle="pill" role="tab" aria-controls="v-pills-bases" aria-selected="<%= $selected %>"><%=l $current %></a> % $active = ''; @@ -52,6 +54,7 @@ } %> <div class="tab-pane fade show <%= $active %>" + ng-show="<%= $item %>.id" id="v-pills-<%= $current %>" role="tabpanel" aria-labelledby="v-pills-<%= $current %>-tab"> %= include "main/${item}_$current"