Commit 4b0a6826 authored by Francesc Guasch's avatar Francesc Guasch Committed by Fernando Verdugo
Browse files

feat(frontend): start virtual machine (#762)

* wip(clone): run a request and let user know progress

issue #751

* feat(frontend): improved start machine

Start machine from clone waits with Javascript pulls

issue #751

* wip(frontend): moved viewer hint to bottom

issue #751

* wip(frontend): added timeout and description messages

issue #751

* wip(frontend): remove old run machine code

issue #751

* wip(frontend): show fresh machine information

issue #751

* wip(clone): run a request and let user know progress

issue #751

* feat(frontend): improved start machine

Start machine from clone waits with Javascript pulls

issue #751

* wip(frontend): moved viewer hint to bottom

issue #751

* wip(frontend): added timeout and description messages

issue #751

* wip(frontend): remove old run machine code

issue #751

* wip(frontend): show fresh machine information

issue #751

* fix(frontend): don't show display info if down

issue #751

* wip(grants): force shutdown timeout only if still up

issue #751

* wip(frontend): post start volatile domains

issue #751

* wip(frontend): post start volatile domains typo

issue #751
parent 7ed999df
......@@ -1191,6 +1191,7 @@ sub create_domain {
confess "I can't find any vm ".Dumper($self->vm) if !$vm;
my $domain;
$request->status("creating") if $request;
eval { $domain = $vm->create_domain(@_) };
my $error = $@;
if ( $request ) {
......@@ -1198,6 +1199,17 @@ sub create_domain {
if ($error =~ /has \d+ requests/) {
$request->status('retry');
}
if (!$error && $request->defined_arg('start')) {
$request->status("starting");
eval {
my $user = Ravada::Auth::SQL->search_by_id($request->args('id_owner'));
$domain->start(
user => $user
,remote_ip => $request->defined_arg('remote_ip')
)
};
$request->error($error) if $error;
}
}
return $domain;
}
......@@ -1822,6 +1834,7 @@ sub _cmd_create{
.$request->args('name')."</a>"
." created."
;
$request->id_domain($domain->id);# if !$request->args('id_domain');
$request->status('done',$msg);
}
......@@ -2008,9 +2021,13 @@ sub _cmd_start {
my $self = shift;
my $request = shift;
my $name = $request->args('name');
my ($name, $id_domain);
$name = $request->defined_arg('name');
$id_domain = $request->defined_arg('id_domain');
my $domain = $self->search_domain($name);
my $domain;
$domain = $self->search_domain($name) if $name;
$domain = $self->search_domain_by_id($id_domain) if $id_domain;
die "Unknown domain '$name'" if !$domain;
my $uid = $request->args('uid');
......@@ -2019,7 +2036,7 @@ sub _cmd_start {
$domain->start(user => $user, remote_ip => $request->args('remote_ip'));
my $msg = 'Domain '
."<a href=\"/machine/view/".$domain->id.".html\">"
.$request->args('name')."</a>"
.$domain->name."</a>"
." started"
;
$request->status('done', $msg);
......
......@@ -796,6 +796,8 @@ sub can_change_settings($self, $id_domain=undef) {
=cut
sub can_manage_machine($self, $domain) {
return 1 if $self->is_admin;
$domain = Ravada::Front::Domain->open($domain) if !ref $domain;
return 1 if $self->can_change_settings($domain);
......
......@@ -18,6 +18,7 @@ use JSON::XS;
use Moose::Role;
use IPC::Run3 qw(run3);
use Sys::Statistics::Linux;
use Time::Piece;
use IPTables::ChainMgr;
no warnings "experimental::signatures";
......@@ -782,8 +783,8 @@ sub _display_file_spice($self,$user) {
$ret .=";" if !$self->tls;
$ret .="ca=CA\n"
."toggle-fullscreen=shift+f11\n"
."release-cursor=shift+f12\n"
."release-cursor=shift+f11\n"
."toggle-fullscreen=shift+f12\n"
."secure-attention=ctrl+alt+end\n";
$ret .=";" if !$self->tls;
$ret .="secure-channels=main;inputs;cursor;playback;record;display;usbredir;smartcard\n";
......@@ -791,6 +792,51 @@ sub _display_file_spice($self,$user) {
return $ret;
}
=head2 info
Return information about the domain.
=cut
sub info($self, $user) {
my $info = {
id => $self->id
,name => $self->name
,is_active => $self->is_active
,spice_password => $self->spice_password
,display_url => $self->display($user)
,description => $self->description
,msg_timeout => $self->_msg_timeout
};
if (!$info->{description} && $self->id_base) {
my $base = Ravada::Front::Domain->open($self->id_base);
$info->{description} = $base->description;
}
if ($self->is_active) {
my $display = $self->display($user);
my ($local_ip, $local_port) = $display =~ m{\w+://(.*):(\d+)};
$info->{display_ip} = $local_ip;
$info->{display_port} = $local_port;
}
return $info;
}
sub _msg_timeout($self) {
return undef if !$self->run_timeout;
my $msg_timeout = '';
for my $request ( $self->list_all_requests ) {
if ( $request->command =~ 'shutdown' ) {
my $t1 = Time::Piece->localtime($request->at_time);
my $t2 = localtime();
$msg_timeout = " in ".($t1 - $t2)->pretty;
}
}
return $msg_timeout;
}
sub _insert_db {
my $self = shift;
my %field = @_;
......@@ -1306,8 +1352,8 @@ sub _post_shutdown {
$self->clean_swap_volumes(@_) if !$self->is_active;
}
if (defined $timeout && !$self->is_removed) {
if ($timeout<2 && !$self->is_removed && $self->is_active) {
if (defined $timeout && !$self->is_removed && $self->is_active) {
if ($timeout<2) {
sleep $timeout;
$self->_data(status => 'shutdown') if !$self->is_active;
return $self->_do_force_shutdown() if !$self->is_removed && $self->is_active;
......
......@@ -45,6 +45,8 @@ our %VALID_ARG = (
,memory => 2
,disk => 2
,network => 2
,remote_ip => 2
,start => 2
}
,open_iptables => $args_manage_iptables
,remove_base => $args_remove_base
......@@ -57,7 +59,7 @@ our %VALID_ARG = (
,screenshot_domain => { id_domain => 1, filename => 2 }
,autostart_domain => { id_domain => 1 , uid => 1, value => 2 }
,copy_screenshot => { id_domain => 1, filename => 2 }
,start_domain => {%$args_manage, remote_ip => 1 }
,start_domain => {%$args_manage, remote_ip => 1, name => 2, id_domain => 2 }
,rename_domain => { uid => 1, name => 1, id_domain => 1}
,set_driver => {uid => 1, id_domain => 1, id_option => 1}
,hybernate=> {uid => 1, id_domain => 1}
......@@ -132,6 +134,27 @@ sub open {
return $row;
}
=head2 info
Returns information of the request
=cut
sub info {
my $self = shift;
my $user = shift;
confess "USER ".$user->name." not authorized"
unless $user->is_admin
|| ($self->defined_arg('uid') && $user->id == $self->args('uid'))
|| ($self->defined_arg('id_owner') && $user->id == $self->args('id_owner'));
return {
status => $self->status
,error => $self->error
,id_domain => $self->id_domain
}
}
=head2 create_domain
my $req = Ravada::Request->create_domain( name => 'bla'
......@@ -202,10 +225,13 @@ sub start_domain {
my $args = _check_args('start_domain', @_);
confess "ERROR: choose either id_domain or name "
if $args->{id_domain} && $args->{name};
my $self = {};
bless($self,$class);
return $self->_new_request(command => 'start' , args => encode_json($args));
return $self->_new_request(command => 'start' , args => $args);
}
=head2 pause_domain
......
......@@ -162,6 +162,7 @@ sub _around_create_domain {
}
my $user = Ravada::Auth::SQL->search_by_id($id_owner);
$domain->is_volatile(1) if $user->is_temporary() ||($base && $base->volatile_clones());
$domain->_post_start($owner) if $domain->is_active;
$domain->start($owner) if $domain->is_volatile && ! $domain->is_active;
$domain->get_info();
......@@ -350,6 +351,9 @@ sub _check_require_base {
my $id_owner = delete $args{id_owner}
or confess "ERROR: id_owner required ";
delete $args{start};
delete $args{remote_ip};
delete @args{'_vm','name','vm', 'memory','description'};
confess "ERROR: Unknown arguments ".join(",",keys %args)
......
......@@ -151,7 +151,6 @@ sub _load_storage_pool {
}
for my $pool ($self->vm->list_storage_pools) {
warn $pool->get_name;
my $info = $pool->get_info();
next if defined $available
&& $info->{available} <= $available;
......
var ravadaApp = angular.module("ravada.app",['ngResource','ngSanitize'])
.config( [
'$compileProvider',
function( $compileProvider )
{
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|spice|mailto|chrome-extension):/);
// Angular before v1.2 uses $compileProvider.urlSanitizationWhitelist(...)
}
])
.directive("solShowSupportform", swSupForm)
//TODO check if the next directive may be removed
.directive("solShowNewmachine", swNewMach)
......@@ -21,6 +29,7 @@
.controller("singleMachinePage", singleMachinePageC)
.controller("notifCrtl", notifCrtl)
.controller("run_domain",run_domain_ctrl)
.controller("run_domain_req",run_domain_req_ctrl)
......@@ -283,6 +292,68 @@
};
function run_domain_req_ctrl($scope, $http, $timeout, request ) {
$scope.get_domain_info = function() {
if ($scope.id_domain) {
var seconds = 1000;
$http.get('/machine/info/'+$scope.id_domain+'.json').then(function(response) {
$scope.domain = response.data;
if ($scope.domain.spice_password) {
var copyTextarea = document.querySelector('.js-copytextarea');
copyTextarea.value = $scope.domain.spice_password;
copyTextarea.length = 5;
}
if ($scope.domain.is_active) {
seconds = 5000;
}
$timeout(function() {
$scope.get_domain_info();
},seconds);
});
}
};
$scope.wait_request = function() {
console.log("id_request: "+$scope.id_request);
$scope.dots += '.';
if ($scope.id_request) {
$http.get('/request/'+$scope.id_request+'.json').then(function(response) {
if (response.data.status == 'done' ) {
$scope.id_domain=response.data.id_domain;
$scope.request=response.data;
$scope.get_domain_info();
}
});
}
if ( !$scope.id_domain ) {
$timeout(function() {
$scope.wait_request();
},1000);
}
}
$scope.copy_password= function() {
$scope.view_password=1;
var copyTextarea = document.querySelector('.js-copytextarea');
if (copyTextarea) {
copyTextarea.select();
try {
var successful = document.execCommand('copy');
var msg = successful ? 'successful' : 'unsuccessful';
console.log('Copying text command was ' + msg);
$scope.password_clipboard=successful;
} catch (err) {
console.log('Oops, unable to copy');
}
}
};
$scope.dots = '...';
$scope.wait_request();
$scope.view_clicked=false;
};
function run_domain_ctrl($scope, $http, request ) {
$http.get('/auto_view').then(function(response) {
$scope.auto_view = response.auto_view;
......@@ -302,6 +373,7 @@
var successful = document.execCommand('copy');
var msg = successful ? 'successful' : 'unsuccessful';
console.log('Copying text command was ' + msg);
$scope.password_clipboard=successful;
} catch (err) {
console.log('Oops, unable to copy');
}
......
......@@ -16,6 +16,9 @@ use Mojo::Home;
#my $self->plugin('I18N');
#package Ravada::I18N:en;
#####
#
no warnings "experimental::signatures";
use feature qw(signatures);
use lib 'lib';
......@@ -336,7 +339,7 @@ get '/machine/info/(:id).(:type)' => sub {
return access_denied($c) unless $USER->is_admin
|| $domain->id_owner == $USER->id;
$c->render(json => $RAVADA->domain_info(id => $id));
$c->render(json => $domain->info($USER) );
};
any '/machine/settings/(:id).(:type)' => sub {
......@@ -570,6 +573,10 @@ get '/request/(:id).(:type)' => sub {
my $c = shift;
my $id = $c->stash('id');
if ($c->stash('type') eq 'json') {
my $request = Ravada::Request->open($id);
return $c->render(json => $request->info($USER));
}
return _show_request($c,$id);
};
......@@ -942,16 +949,6 @@ sub render_machines_user {
);
}
sub create_domain {
my ($c, $id_base, $domain_name, $ram, $disk) = @_;
return $c->redirect_to('/login') if !$USER;
my $base = $RAVADA->search_domain_by_id($id_base) or die "I can't find base $id_base";
my $domain = provision($c, $id_base, $domain_name, $ram, $disk);
return show_failure($c, $domain_name) if !$domain;
return show_link($c,$domain);
}
sub quick_start_domain {
my ($c, $id_base, $name) = @_;
......@@ -963,16 +960,12 @@ sub quick_start_domain {
my $base = $RAVADA->search_domain_by_id($id_base) or die "I can't find base $id_base";
my $domain_name = $base->name."-".$name;
$domain_name =~ tr/[\.]/[\-]/;
my $domain = $RAVADA->search_clone(id_base => $base->id, id_owner => $USER->id);
$domain = provision($c, $id_base, $domain_name)
if !$domain || $domain->is_base;
return show_failure($c, $domain_name) if !$domain;
my $domain = $RAVADA->search_clone(id_base => $base->id, id_owner => $USER->id);
$domain_name = $domain->name if $domain;
return show_link($c,$domain);
return run_request($c,provision_req($c, $id_base, $domain_name));
}
......@@ -1129,156 +1122,50 @@ sub base_id {
return $base->id;
}
sub provision {
my $c = shift;
my $id_base = shift;
my $name = shift or confess "Missing name";
my $ram = shift;
my $disk = shift;
die "Missing id_base " if !defined $id_base;
die "Missing name " if !defined $name;
sub provision_req($c, $id_base, $name, $ram=0, $disk=0) {
my $domain;
$domain = $RAVADA->search_domain($name) if $RAVADA->domain_exists($name);
return $domain if $domain && !$domain->is_base;
if ($domain) {
my $count = 2;
my $name2;
while ($domain && $domain->is_base) {
$name2 = "$name-$count";
$domain = $RAVADA->search_domain($name2);
$count++;
if ( $RAVADA->domain_exists($name) ) {
my $domain = $RAVADA->search_domain($name);
if ( !$domain->is_base ) {
if ($domain->is_active) {
return Ravada::Request->open_iptables(
uid => $USER->id
, id_domain => $domain->id
, remote_ip => _remote_ip($c)
);
}
return Ravada::Request->start_domain(
uid => $USER->id
, id_domain => $domain->id
, remote_ip => _remote_ip($c)
)
}
return $domain if $domain;
$name = $name2;
$name = _new_domain_name($name);
}
my @create_args = ( memory => $ram ) if $ram;
push @create_args , ( disk => $disk) if $disk;
my @create_args = ( start => 1, remote_ip => _remote_ip($c));
push @create_args, ( memory => $ram ) if $ram;
push @create_args, ( disk => $disk) if $disk;
my $req = Ravada::Request->create_domain(
name => $name
, id_base => $id_base
, id_owner => $USER->id
,@create_args
);
$RAVADA->wait_request($req, 60);
if ( $req->status ne 'done' ) {
$c->stash(error_title => "Request ".$req->command." ".$req->status());
$c->stash(error =>
"Domain provisioning request not finished, status='".$req->status."'.");
my $req_link = "/request/".$req->id.".html";
$req_link = "/anonymous$req_link" if $USER->is_temporary;
$c->stash(link => $req_link);
$c->stash(link_msg => '');
return;
}
$domain = $RAVADA->search_domain($name);
if ( $req->error ) {
$c->stash(error => $req->error)
} elsif (!$domain) {
$c->stash(error => "I dunno why but no domain $name");
}
return $domain;
}
sub show_link {
my $c = shift;
my $domain = shift or confess "Missing domain";
confess "Domain is not a ref $domain " if !ref $domain;
return access_denied($c) if $USER->id != $domain->id_owner && !$USER->is_admin;
my $req;
if ( !$domain->is_active ) {
$req = Ravada::Request->start_domain(
uid => $USER->id
,name => $domain->name
,remote_ip => _remote_ip($c)
);
$RAVADA->wait_request($req);
warn "ERROR: req id: ".$req->id." error:".$req->error if $req->error();
return $c->render(data => 'ERROR starting domain '
."status:'".$req->status."' ( ".$req->error.")")
if $req->error
&& $req->error !~ /already running/i
&& $req->status ne 'waiting';
my $req_link = "/request/".$req->id.".html";
$req_link = "/anonymous$req_link" if $USER->is_temporary;
return $c->redirect_to($req_link);
# if !$req->status eq 'done';
}
if ( $domain->is_paused) {
$req = Ravada::Request->resume_domain(name => $domain->name, uid => $USER->id
, remote_ip => _remote_ip($c)
);
$RAVADA->wait_request($req);
warn "ERROR: ".$req->error if $req->error();
return $c->render(data => 'ERROR resuming domain '.$req->error)
if $req->error && $req->error !~ /already running/i;
return $c->redirect_to("/request/".$req->id.".html")
if !$req->status eq 'done';
}
my $uri = $domain->display($USER) if $domain->is_active;
if (!$uri) {
my $name = '';
$name = $domain->name if $domain;
$c->render(template => 'fail', name => $domain->name);
return;
}
_open_iptables($c,$domain)
if !$req;
my $uri_file = "/machine/display/".$domain->id;
$c->stash(url => $uri_file) if $c->session('auto_view') && ! $domain->spice_password;
my ($display_ip, $display_port) = $uri =~ m{\w+://(\d+\.\d+\.\d+\.\d+):(\d+)};
my $description = $domain->description;
if (!$description && $domain->id_base) {
my $base = Ravada::Front::Domain->open($domain->id_base);
$description = $base->description();
sub _new_domain_name {
my $name = shift;
my $count = 1;
my $name2;
for ( ;; ) {
$name2 = "$name-".++$count;
return $name2 if !$RAVADA->domain_exists($name2);
}
$c->stash(description => $description);
$c->stash(domain => $domain );
$c->stash(msg_timeout => _message_timeout($domain));
$c->render(template => 'main/run'
,name => $domain->name
,password => $domain->spice_password
,url_display => $uri
,url_display_file => $uri_file
,display_ip => $display_ip
,display_port => $display_port
,description => $description
,login => $c->session('login'));
}
sub _message_timeout {
my $domain = shift;
my $msg_timeout = '';
if (int ($domain->run_timeout / 60 )) {
$msg_timeout = "in ".int($domain->run_timeout / 60 )." minutes.";
}
for my $request ( $domain->list_all_requests ) {
if ( $request->command =~ 'shutdown' ) {
my $t1 = Time::Piece->localtime($request->at_time);
my $t2 = localtime();
$msg_timeout = " in ".($t1 - $t2)->pretty;
}
}
return $msg_timeout;
sub run_request($c, $request) {
return $c->render(template => 'main/run_request', request => $request );
}
sub _open_iptables {
......@@ -1522,7 +1409,12 @@ sub view_machine {
$domain = _search_requested_machine($c) if !$domain;
return $c->render(template => 'main/fail') if !$domain;
return show_link($c, $domain);
return run_request($c, Ravada::Request->start_domain(
uid => $USER->id
,id_domain => $domain->id
, remote_ip => _remote_ip($c)
)
);
}
sub clone_machine {
......
<!DOCTYPE html>
<html ng-app="ravada.app">
%= include 'bootstrap/header'