Commit f9a12366 authored by Francesc Guasch's avatar Francesc Guasch
Browse files

Merge branch 'develop'

parents a76f2b44 4525c37b
......@@ -3,7 +3,7 @@ package Ravada;
use warnings;
use strict;
our $VERSION = '0.5.0-rc1';
our $VERSION = '0.5.0-rc8';
use Carp qw(carp croak);
use Data::Dumper;
......@@ -2314,9 +2314,10 @@ sub _execute {
return;
}
$request->status('working','') unless $request->status() eq 'waiting';
$request->pid($$);
$request->start_time(time);
$request->error('');
$request->status('working','');
if ($dont_fork || !$CAN_FORK) {
$self->_do_execute_command($sub, $request);
return;
......@@ -2349,6 +2350,7 @@ sub _do_execute_command {
# local *STDERR = $f_err;
# }
$request->status('working','') unless $request->status() eq 'working';
$request->pid($$);
my $t0 = [gettimeofday];
eval {
......@@ -2358,6 +2360,19 @@ sub _do_execute_command {
my $elapsed = tv_interval($t0,[gettimeofday]);
$request->run_time($elapsed);
$request->error($err) if $err;
if ($err) {
my $user = $request->defined_arg('user');
if ($user) {
warn "sending message to ".$user->id." ".$user->name;
my $subject = $err;
my $message = '';
if (length($subject) > 40 ) {
$message = $subject;
$subject = substr($subject,0,40);
$user->send_message($subject, $message);
}
}
}
if ($err && $err =~ /retry.?$/i) {
my $retry = $request->retry;
if (defined $retry && $retry>0) {
......@@ -2550,6 +2565,7 @@ sub _can_fork {
$req->error($msg);
$req->at_time(time+10);
$req->status('waiting') if $req->status() !~ 'waiting';
$req->at_time(time+10);
return 0;
}
sub _wait_pids {
......@@ -2981,6 +2997,11 @@ sub _cmd_shutdown {
die "Unknown domain '$id_domain'\n" if !$domain
}
Ravada::Request->refresh_machine(
uid => $uid
,id_domain => $id_domain
,after_request => $request->id
);
my $user = Ravada::Auth::SQL->search_by_id( $uid);
$domain->shutdown(timeout => $timeout, user => $user
......@@ -3084,8 +3105,8 @@ sub _cmd_refresh_machine($self, $request) {
my $domain = Ravada::Domain->open($id_domain) or confess "Error: domain $id_domain not found";
$domain->check_status();
$domain->list_volumes_info();
$self->_remove_unnecessary_downs($domain) if !$domain->is_active;
$domain->info($user);
$self->_remove_unnecessary_downs($domain);
}
......
......@@ -161,6 +161,24 @@ sub unshown_messages {
}
=head2 send_message
Send a message to this user
$user->send_message($subject, $message)
=cut
sub send_message($self, $subject, $message='') {
_init_connector() if !$$CONNECTOR;
my $sth = $$CONNECTOR->dbh->prepare(
"INSERT INTO messages (id_user, subject, message) "
." VALUES(?, ? , ? )");
$sth->execute($self->id, $subject, $message);
}
=head2 show_message
......
......@@ -1079,7 +1079,7 @@ sub open($class, @args) {
sub check_status($self) {
$self->_search_already_started() if !$self->is_base;
$self->_check_clean_shutdown() if $self->domain;
$self->_check_clean_shutdown() if $self->domain && !$self->is_active;
}
=head2 is_known
......@@ -1300,6 +1300,7 @@ sub info($self, $user) {
,is_base => $self->is_base
,id_base => $self->id_base
,is_active => $is_active
,is_hibernated => $self->is_hibernated
,spice_password => $self->spice_password
,description => $self->description
,msg_timeout => ( $self->_msg_timeout or undef)
......@@ -1351,6 +1352,7 @@ sub info($self, $user) {
push @cdrom,($disk->{file}) if $disk->{file} && $disk->{file} =~ /\.iso$/;
}
$info->{cdrom} = \@cdrom;
$info->{requests} = $self->list_requests();
return $info;
}
......@@ -2041,6 +2043,7 @@ sub _post_shutdown {
}
my $is_active = $self->is_active;
$self->_data(status => 'shutdown')
if $self->is_known && !$self->is_volatile && !$is_active;
......@@ -2064,6 +2067,11 @@ sub _post_shutdown {
return $self->_do_force_shutdown() if !$self->is_removed && $is_active;
}
Ravada::Request->refresh_machine(
at => time+int($timeout/2)
, uid => Ravada::Utils::user_daemon->id
, id_domain => $self->id
);
my $req = Ravada::Request->force_shutdown_domain(
id_domain => $self->id
,id_vm => $self->_vm->id
......@@ -2208,6 +2216,10 @@ sub expose($self, @args) {
$internal_port = delete $args{port};
$internal_port = delete $args{internal_port} if exists $args{internal_port};
delete $args{internal_ip};
# remove old fields
for (qw(public_ip active description)) {
delete $args{$_};
}
confess "Error: Missing port" if !defined $internal_port && !$id_port;
confess "Error: internal port not a number '".($internal_port or '<UNDEF>')."'"
......@@ -2262,10 +2274,10 @@ sub _update_expose($self, %args) {
if ($self->is_active) {
my $sth=$$CONNECTOR->dbh->prepare(
"SELECT internal_port,restricted FROM domain_ports where id=?");
"SELECT internal_port,name,restricted FROM domain_ports where id=?");
$sth->execute($id);
my ($internal_port, $restricted) = $sth->fetchrow;
$self->_open_exposed_port($internal_port, $restricted);
my ($internal_port, $name, $restricted) = $sth->fetchrow;
$self->_open_exposed_port($internal_port, $name, $restricted);
}
}
......@@ -2287,22 +2299,35 @@ sub _add_expose($self, $internal_port, $name, $restricted) {
);
$sth->finish;
$self->_open_exposed_port($internal_port, $restricted) if $self->is_active && $self->ip;
$self->_open_exposed_port($internal_port, $name, $restricted)
if $self->is_active && $self->ip;
return $public_port;
}
sub _open_exposed_port($self, $internal_port, $restricted) {
my $sth = $$CONNECTOR->dbh->prepare("SELECT public_port FROM domain_ports"
sub _open_exposed_port($self, $internal_port, $name, $restricted) {
my $sth = $$CONNECTOR->dbh->prepare("SELECT id,public_port FROM domain_ports"
." WHERE id_domain=? AND internal_port=?"
);
$sth->execute($self->id, $internal_port);
my ($public_port) = $sth->fetchrow();
my ($id_port, $public_port) = $sth->fetchrow();
if (!$public_port) {
$public_port = $self->_vm->_new_free_port();
my $sth = $$CONNECTOR->dbh->prepare("UPDATE domain_ports set public_port=?"
." WHERE id_domain=? AND internal_port=?"
);
$sth->execute($public_port, $self->id, $internal_port);
if ($id_port) {
my $sth = $$CONNECTOR->dbh->prepare("UPDATE domain_ports set public_port=?"
." WHERE id_domain=? AND internal_port=?"
);
$sth->execute($public_port, $self->id, $internal_port);
} else {
my $sth = $$CONNECTOR->dbh->prepare("INSERT INTO domain_ports "
."(id_domain, public_port, internal_port, name, restricted)"
." VALUES(?,?,?,?,?) "
);
$sth->execute( $self->id
,$public_port, $internal_port
,( $name or undef )
,$restricted
);
}
}
my $local_ip = $self->_vm->ip;
......@@ -2383,7 +2408,8 @@ sub open_exposed_ports($self) {
}
for my $expose ( @ports ) {
$self->_open_exposed_port($expose->{internal_port}, $expose->{restricted});
$self->_open_exposed_port($expose->{internal_port}, $expose->{name}
,$expose->{restricted});
}
}
......@@ -2413,9 +2439,6 @@ sub _close_exposed_port($self,$internal_port_req=undef) {
$self->_close_exposed_port_nat($iptables, %port);
$self->_close_exposed_port_client($iptables, %port);
$sth = $$CONNECTOR->dbh->prepare("DELETE FROM requests WHERE id_domain=? "
." AND command='open_exposed_ports'");
$sth->execute($self->id);
$sth->finish;
}
......@@ -2505,10 +2528,25 @@ sub list_ports($self) {
." FROM domain_ports WHERE id_domain=?");
$sth->execute($self->id);
my @list;
my %clone_port;
while (my $data = $sth->fetchrow_hashref) {
lock_hash(%$data);
push @list,($data);
$clone_port{$data->{internal_port}}++;
}
if ($self->id_base) {
my $base = Ravada::Front::Domain->open($self->id_base);
my @ports_base = $base->list_ports();
for my $data (@ports_base) {
next if exists $clone_port{$data->{internal_port}};
unlock_hash(%$data);
$data->{public_port} = $self->_vm->_new_free_port();
lock_hash(%$data);
push @list,($data);
}
}
return @list;
}
......@@ -2629,7 +2667,7 @@ sub _post_start {
Ravada::Request->open_exposed_ports(
uid => Ravada::Utils::user_daemon->id
,id_domain => $self->id
,retry => 5
,retry => 20
) if $remote_ip && $self->list_ports();
if ($self->run_timeout) {
......@@ -2637,7 +2675,7 @@ sub _post_start {
id_domain => $self->id
, uid => $arg{user}->id
, at => time+$self->run_timeout
, timeout => 59
, timeout => $TIMEOUT_SHUTDOWN
);
}
......
......@@ -872,7 +872,7 @@ sub list_requests($self, $id_domain_req=undef, $seconds=60) {
, $error, $id_domain, $domain, $date));
while ( $sth->fetch) {
my $epoch_date_changed;
my $epoch_date_changed = time;
if ($date_changed) {
my ($y,$m,$d,$hh,$mm,$ss) = $date_changed =~ /(\d{4})-(\d\d)-(\d\d) (\d+):(\d+):(\d+)/;
if ($y) {
......
......@@ -122,11 +122,13 @@ our %VALID_ARG = (
our %CMD_SEND_MESSAGE = map { $_ => 1 }
qw( create start shutdown prepare_base remove remove_base rename_domain screenshot download
clone
set_base_vm remove_base_vm
domain_autostart hibernate hybernate
change_owner
change_max_memory change_curr_memory
add_hardware remove_hardware set_driver change_hardware
expose remove_expose
set_base_vm
shutdown_node start_node
);
......@@ -523,15 +525,18 @@ sub _new_request {
}
my %args = @_;
$args{status} = 'requested';
$args{status} = 'initializing';
if ($args{name}) {
$args{domain_name} = $args{name};
delete $args{name};
}
my $uid;
if ( ref $args{args} ) {
$args{args}->{uid} = $args{args}->{id_owner}
if !exists $args{args}->{uid};
$uid = $args{args}->{uid} if exists $args{args}->{uid};
$args{at_time} = $args{args}->{at} if exists $args{args}->{at};
my $id_domain_args = $args{args}->{id_domain};
......@@ -572,7 +577,11 @@ sub _new_request {
." WHERE id=?");
$sth->execute($self->{id});
return $self->open($self->{id});
my $request = $self->open($self->{id});
$request->status('requested');
return $request;
}
sub _last_insert_id {
......@@ -668,6 +677,7 @@ sub _send_message {
$uid = $self->args('id_owner') if $self->defined_arg('id_owner');
$uid = $self->args('uid') if !$uid && $self->defined_arg('uid');
return if !$uid;
my $domain_name = $self->defined_arg('name');
......
package Ravada::WebSocket;
use warnings;
use strict;
use Data::Dumper;
use Hash::Util qw( lock_hash unlock_hash);
use Moose;
no warnings "experimental::signatures";
use feature qw(signatures);
has clients => (
is => 'ro'
,isa => 'HashRef'
,default => sub { return {}}
);
has ravada => (
is => 'ro'
,isa => 'Ravada::Front'
,required => 1
);
my %SUB = (
list_alerts => \&_list_alerts
,list_machines => \&_list_machines
,list_requests => \&_list_requests
,machine_info => \&_get_machine_info
);
######################################################################
sub _list_alerts($rvd, $args) {
my $login = $args->{login} or die "Error: no login arg ".Dumper($args);
my $user = Ravada::Auth::SQL->new(name => $login) or die "Error: uknown user $login";
my $ret_old = $args->{ret};
my @ret = map { $_->{time} = time; $_ } $user->unshown_messages();
my @ret2=();
my %new;
for my $alert (@ret) {
my $cmd_machine = $alert->{subject};
$cmd_machine =~ s{(.*?\s.*?)\s+.*}{$1};
$new{$cmd_machine}++;
}
for my $alert (@$ret_old) {
my $cmd_machine = $alert->{subject};
$cmd_machine =~ s{(.*?\s.*?)\s+.*}{$1};
push @ret2,($alert) if time - $alert->{time} < 10
&& $cmd_machine && !$new{$cmd_machine};
}
return [@ret2,@ret];
}
sub _list_machines($rvd, $args) {
my $login = $args->{login} or die "Error: no login arg ".Dumper($args);
my $user = Ravada::Auth::SQL->new(name => $login)
or die "Error: uknown user $login";
return []
unless (
$user->can_list_machines
|| $user->can_list_own_machines()
|| $user->can_list_clones()
|| $user->can_list_clones_from_own_base()
|| $user->is_admin()
);
return $rvd->list_machines($user);
}
sub _list_requests($rvd, $args) {
my $login = $args->{login} or die "Error: no login arg ".Dumper($args);
my $user = Ravada::Auth::SQL->new(name => $login) or die "Error: uknown user $login";
return [] unless $user->is_operator || $user->is_admin;
return $rvd->list_requests;
}
sub _get_machine_info($rvd, $args) {
my ($id_domain) = $args->{channel} =~ m{/(\d+)};
my $domain = $rvd->search_domain_by_id($id_domain) or do {
warn "Error: domain $id_domain not found.";
return {};
};
my $login = $args->{login} or die "Error: no login arg ".Dumper($args);
my $user = Ravada::Auth::SQL->new(name => $login) or die "Error: uknown user $login";
my $info = $domain->info($user);
if ($info->{is_active} && !$info->{ip}) {
Ravada::Request->refresh_machine(id_domain => $info->{id}, uid => $user->id);
}
return $info;
}
sub _different_list($list1, $list2) {
return 1 if scalar(@$list1) != scalar (@$list2);
for my $i (0 .. scalar(@$list1)-1) {
my $h1 = $list1->[$i];
my $h2 = $list2->[$i];
return 1 if _different($h1, $h2);
}
return 0;
}
sub _different_hash($h1,$h2) {
return 1 if keys %$h1 != keys %$h2;
for my $key (keys %$h1) {
next if !defined $h1->{$key} && !defined $h2->{$key};
if (!exists $h2->{$key}
|| !defined $h1->{$key} && defined $h2->{$key}
|| defined $h1->{$key} && !defined $h2->{$key}
|| _different($h1->{$key}, $h2->{$key})) {
return 1;
}
}
return 0;
}
sub _different($var1, $var2) {
return 1 if ref($var1) ne ref($var2);
return _different_list($var1, $var2) if ref($var1) eq 'ARRAY';
return _different_hash($var1, $var2) if ref($var1) eq 'HASH';
return $var1 ne $var2;
}
sub BUILD {
my $self = shift;
Mojo::IOLoop->recurring(1 => sub {
for my $key ( keys %{$self->clients} ) {
my $ws_client = $self->clients->{$key}->{ws};
my $channel = $self->clients->{$key}->{channel};
$channel =~ s{/.*}{};
my $exec = $SUB{$channel} or die "Error: unknown channel $channel";
my $ret = $exec->($self->ravada, $self->clients->{$key});
my $old_ret = $self->clients->{$key}->{ret};
if ( _different($ret, $old_ret )) {
$ws_client->send( { json => $ret } );
$self->clients->{$key}->{ret} = $ret;
}
}
});
}
sub subscribe($self, %args) {
my $ws = $args{ws};
$self->clients->{$ws} = {
ws => $ws
, %args
, ret => []
};
}
sub unsubscribe($self, $ws) {
delete $self->clients->{$ws};
}
1;
......@@ -28,6 +28,7 @@ use lib 'lib';
use Ravada::Front;
use Ravada::Front::Domain;
use Ravada::Auth;
use Ravada::WebSocket;
use POSIX qw(locale_h);
my $help;
......@@ -114,6 +115,7 @@ our $SESSION_TIMEOUT = ($CONFIG_FRONT->{session_timeout} or 5 * 60);
# session times out in 15 minutes for admin users
our $SESSION_TIMEOUT_ADMIN = ($CONFIG_FRONT->{session_timeout_admin} or 15 * 60);
my $WS = Ravada::WebSocket->new(ravada => $RAVADA);
init();
############################################################################3
......@@ -672,6 +674,8 @@ get '/machine/set/#id/#field/#value' => sub {
return access_denied($c) if !$USER->can_manage_machine($c->stash('id'));
my $domain = Ravada::Front::Domain->open($id) or die "Unkown domain $id";
$USER->send_message("Setting $field to $value in ".$domain->name)
if $domain->_data($field) ne $value;
return $c->render(json => { $field => $domain->_data($field, $value)});
};
......@@ -1206,6 +1210,23 @@ get '/iso/download/(#id).json' => sub {
return $c->render(json => {request => $req->id});
};
websocket '/ws/subscribe' => sub {
my $c = shift;
my $expiration = $SESSION_TIMEOUT;
$expiration = $SESSION_TIMEOUT_ADMIN if $USER->is_admin;
$c->inactivity_timeout( $expiration );
$c->on(message => sub {
my ($ws, $channel ) = @_;
$WS->subscribe( ws => $ws
, channel => $channel
, login => $USER->name
, remote_ip => _remote_ip($c)
);
});
$c->on(finish => sub { my $ws = shift; $WS->unsubscribe($ws) });
} => 'ws_subscribe';
###################################################
#
# session settings
......@@ -2129,7 +2150,7 @@ sub copy_machine {
} else {
push @reqs,(copy_machine_many($base, $number, \@create_args));
}
return $c->render(json => { request => [map { $_->id } @ reqs ] } );
return $c->render(json => { request => [map { $_->id } @reqs ] } );
}
sub new_machine_copy($c) {
......
......@@ -233,7 +233,14 @@ sub test_shutdown {
ok(!$domain->is_active);
rvd_back->_remove_unnecessary_downs($domain);
@reqs = $domain->list_requests(1);
ok(!scalar @reqs,$domain->name) or exit;
# 1 request for refresh_machine
my @req_refresh = grep { $_->command eq 'refresh_machine' } @reqs;
is(scalar(@req_refresh),1);
# no other requests
my @req_other = grep { $_->command ne 'refresh_machine' } @reqs;
is(scalar(@req_other),0);
is(scalar @reqs,1,$domain->name) or exit;
$domain->remove(user_admin);
}
......
......@@ -47,14 +47,12 @@ sub test_run_timeout {
is($clone->is_active,1);
rvd_back->_process_all_requests_dont_fork();
is($clone->is_active,1);
sleep($timeout);
rvd_back->_process_all_requests_dont_fork();
for ( 1 .. 60 ) {
last if !$clone->is_active;
for ( 1 .. $timeout + 60 ) {
last if !$clone->is_active || ! scalar($clone->list_requests(1));
sleep 1;
rvd_back->_process_all_requests_dont_fork();
}
is($clone->is_active,0);
is($clone->is_active,0, "Expecting ".$clone->name." timed out shutdown") or exit;
$clone->remove(user_admin);
$domain->remove(user_admin);
......
......@@ -86,7 +86,7 @@
<script type="text/ng-template" id="alert.html">
<div ng-transclude></div>
</script>
<div uib-alert ng-repeat="alert in alerts"
<div uib-alert ng-repeat="alert in alerts_ws"
ng-class="'alert-' + (alert.type || 'warning')"
ng-click="closeAlert($index)"
ng-mouseover="alert.showMessage = false"
......