Unverified Commit 1e248344 authored by Francesc Guasch's avatar Francesc Guasch Committed by GitHub
Browse files

Feature #265 ports (#1089)

feature(ports): expose_ports grant admin default

Issue [#265]
parent 160824bb
......@@ -924,7 +924,8 @@ sub _add_grants($self) {
$self->_add_grant('rename_clones', 0,"Can rename clones from virtual machines owned by the user.");
$self->_add_grant('shutdown', 1,"Can shutdown own virtual machines.");
$self->_add_grant('screenshot', 1,"Can get a screenshot of own virtual machines.");
$self->_add_grant('start_many',0,"Can have more than one machine started.")
$self->_add_grant('start_many',0,"Can have more than one machine started.");
$self->_add_grant('expose_ports',0,"Can expose virtual machine ports.");
}
sub _add_grant($self, $grant, $allowed, $description) {
......@@ -949,8 +950,6 @@ sub _add_grant($self, $grant, $allowed, $description) {
$sth->execute($grant, $description);
$sth->finish;
return if !$allowed;
$sth = $CONNECTOR->dbh->prepare("SELECT id FROM grant_types WHERE name=?");
$sth->execute($grant);
my ($id_grant) = $sth->fetchrow;
......@@ -959,10 +958,10 @@ sub _add_grant($self, $grant, $allowed, $description) {
my $sth_insert = $CONNECTOR->dbh->prepare(
"INSERT INTO grants_user (id_user, id_grant, allowed) VALUES(?,?,?) ");
$sth = $CONNECTOR->dbh->prepare("SELECT id FROM users ");
$sth = $CONNECTOR->dbh->prepare("SELECT id,name FROM users WHERE is_temporary = 0");
$sth->execute;
while (my ($id_user) = $sth->fetchrow ) {
while (my ($id_user, $name) = $sth->fetchrow ) {
eval { $sth_insert->execute($id_user, $id_grant, $allowed) };
die $@ if $@ && $@ !~/Duplicate entry /;
}
......@@ -990,6 +989,7 @@ sub _enable_grants($self) {
my @grants = (
'change_settings', 'change_settings_all', 'change_settings_clones'
,'clone', 'clone_all', 'create_base', 'create_machine'
,'expose_ports'
,'grant'
,'manage_users'
,'rename', 'rename_all', 'rename_clones'
......@@ -3272,6 +3272,11 @@ sub _req_method {
,change_hardware => \&_cmd_change_hardware
,change_max_memory => \&_cmd_change_max_memory
,change_curr_memory => \&_cmd_change_curr_memory
# Domain ports
,expose => \&_cmd_expose
,remove_expose => \&_cmd_remove_expose
,open_exposed_ports => \&_cmd_open_exposed_ports
# Virtual Managers or Nodes
,shutdown_node => \&_cmd_shutdown_node
,start_node => \&_cmd_start_node
......@@ -3479,6 +3484,26 @@ sub _post_login_locale($self, $request) {
}
}
sub _cmd_expose($self, $request) {
my $domain = Ravada::Domain->open($request->id_domain);
$domain->expose(
port => $request->args('port')
,name => $request->defined_arg('name')
,id_port => $request->defined_arg('id_port')
,restricted => $request->defined_arg('restricted')
);
}
sub _cmd_remove_expose($self, $request) {
my $domain = Ravada::Domain->open($request->id_domain);
$domain->remove_expose($request->args('port'));
}
sub _cmd_open_exposed_ports($self, $request) {
my $domain = Ravada::Domain->open($request->id_domain);
$domain->open_exposed_ports();
}
sub DESTROY($self) {
$self->_wait_pids();
}
......
......@@ -446,6 +446,7 @@ sub can_list_machines {
|| $self->can_clone_all()
|| $self->can_remove_all()
|| $self->can_rename_all()
|| $self->expose_ports()
|| $self->can_shutdown_all();
return 0;
}
......@@ -667,7 +668,6 @@ sub _load_grants($self) {
$self->{_grant_disabled}->{$grant_alias} = !$enabled;
}
$sth->finish;
}
sub _reload_grants($self) {
......
......@@ -1274,6 +1274,7 @@ sub info($self, $user) {
}
$info->{bases} = $self->_bases_vm();
$info->{clones} = $self->_clones_vm();
$info->{ports} = [$self->list_ports()];
return $info;
}
......@@ -1390,6 +1391,7 @@ sub _after_remove_domain {
my ($user, $cascade) = @_;
$self->_remove_iptables( );
$self->remove_expose();
$self->_remove_domain_cascade($user) if !$cascade;
if ($self->is_known && $self->is_base) {
......@@ -1899,7 +1901,10 @@ sub _post_shutdown {
my %arg = @_;
my $timeout = delete $arg{timeout};
$self->_remove_iptables() if $self->_vm->is_active;
if ( $self->_vm->is_active ) {
$self->_remove_iptables();
$self->_close_exposed_port();
}
my $is_active = $self->is_active;
$self->_data(status => 'shutdown')
......@@ -2048,6 +2053,283 @@ sub add_volume_swap {
$self->add_volume(%arg, swap => 1);
}
=head2 expose
Expose a TCP port from the domain
Arguments:
- number of the port
- optional name
Returns: public ip and port
=cut
sub expose($self, @args) {
my ($id_port, $internal_port, $name, $restricted);
if (scalar @args == 1 ) {
$internal_port=shift @args;
} else {
my %args = @args;
$id_port = delete $args{id_port};
$internal_port = delete $args{port};
$internal_port = delete $args{internal_port} if exists $args{internal_port};
confess "Error: Missing port" if !defined $internal_port && !$id_port;
confess "Error: internal port not a number '".($internal_port or '<UNDEF>')."'"
if defined $internal_port && $internal_port !~ /^\d+$/;
$name = delete $args{name};
$restricted = ( delete $args{restricted} or 0);
confess "Error: Unknown args ".Dumper(\%args) if keys %args;
}
if ($id_port) {
$self->_update_expose(@args);
} else {
$self->_add_expose($internal_port, $name, $restricted);
}
}
sub _update_expose($self, %args) {
my $id = delete $args{id_port};
$args{internal_port} = delete $args{port}
if exists $args{port} && !exists $args{internal_port};
if ($self->is_active) {
my $sth=$$CONNECTOR->dbh->prepare("SELECT internal_port FROM domain_ports where id=?");
$sth->execute($id);
my ($internal_port) = $sth->fetchrow;
$self->_close_exposed_port($internal_port) if $self->is_active;
}
my $sql = "UPDATE domain_ports SET ".join(",", map { "$_=?" } sort keys %args)
." WHERE id=?"
;
my @values = map { $args{$_} } sort keys %args;
my $sth = $$CONNECTOR->dbh->prepare($sql);
$sth->execute(@values, $id);
if ($self->is_active) {
my $sth=$$CONNECTOR->dbh->prepare(
"SELECT internal_port,restricted FROM domain_ports where id=?");
$sth->execute($id);
my ($internal_port, $restricted) = $sth->fetchrow;
$self->_open_exposed_port($internal_port, $restricted);
}
}
sub _add_expose($self, $internal_port, $name, $restricted) {
my $sth = $$CONNECTOR->dbh->prepare(
"INSERT INTO domain_ports (id_domain"
." ,public_port, internal_port"
." ,name, restricted"
.")"
." VALUES (?,?,?,?,?)"
);
my $public_port = $self->_vm->_new_free_port();
$sth->execute($self->id
, $public_port, $internal_port
, ($name or undef)
, $restricted
);
$sth->finish;
$self->_open_exposed_port($internal_port, $restricted) if $self->is_active;
return $public_port;
}
sub _open_exposed_port($self, $internal_port, $restricted) {
my $sth = $$CONNECTOR->dbh->prepare("SELECT public_port FROM domain_ports"
." WHERE id_domain=? AND internal_port=?"
);
$sth->execute($self->id, $internal_port);
my ($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);
}
my $local_ip = $self->_vm->ip;
my $internal_ip = $self->ip;
confess "Error: I can't get the internal IP of ".$self->name
if !$internal_ip || $internal_ip !~ /^(\d+\.\d+)/;
$self->_vm->iptables(
t => 'nat'
,A => 'PREROUTING'
,p => 'tcp'
,d => $local_ip
,dport => $public_port
,j => 'DNAT'
,'to-destination' => "$internal_ip:$internal_port"
) if !$>;
if ($restricted) {
$self->_open_exposed_port_client($public_port);
}
}
sub _open_exposed_port_client($self, $public_port) {
my $remote_ip = $self->remote_ip;
return if !$remote_ip;
my $local_ip = $self->_vm->ip;
$self->_vm->iptables(
A => $IPTABLES_CHAIN
,s => $remote_ip
,d => $local_ip
,m => 'tcp'
,p => 'tcp'
,dport => $public_port
,j => 'ACCEPT'
);
$self->_vm->iptables(
A => $IPTABLES_CHAIN
,d => $local_ip
,m => 'tcp'
,p => 'tcp'
,dport => $public_port
,j => 'DROP'
);
}
sub open_exposed_ports($self) {
my @ports = $self->list_ports();
return if !@ports;
if ( ! $self->ip ) {
Ravada::Request->open_exposed_ports(
uid => Ravada::Utils::user_daemon->id
,id_domain => $self->id
,at => time + 10
);
return;
}
for my $expose ( @ports ) {
$self->_open_exposed_port($expose->{internal_port}, $expose->{restricted});
}
}
sub _close_exposed_port($self,$internal_port_req=undef) {
my $query = "SELECT public_port,internal_port "
." FROM domain_ports"
." WHERE id_domain=? ";
$query .= " AND internal_port=?" if $internal_port_req;
my $sth = $$CONNECTOR->dbh->prepare($query);
if ($internal_port_req) {
$sth->execute($self->id, $internal_port_req);
} else {
$sth->execute($self->id);
}
my %port;
while ( my ($public_port, $internal_port) = $sth->fetchrow() ) {
$port{$public_port} = $internal_port;
}
my $iptables = $self->_vm->iptables_list();
$self->_close_exposed_port_nat($iptables, %port);
$self->_close_exposed_port_client($iptables, %port);
}
sub _close_exposed_port_client($self, $iptables, %port) {
my $ip = $self->_vm->ip."/32";
for my $line (@{$iptables->{'filter'}}) {
my %args = @$line;
next if $args{A} ne 'RAVADA';
if (exists $args{j}
&& exists $args{d} && $args{d} eq $ip
&& exists $args{dport} && $port{$args{dport}}) {
my @delete = (
D => 'RAVADA'
, p => 'tcp', m => 'tcp'
, d => $ip
, dport => $args{dport}
, j => $args{j}
);
push @delete , (s => $args{s}) if exists $args{s};
$self->_vm->iptables(@delete);
}
}
}
sub _close_exposed_port_nat($self, $iptables, %port) {
my $ip = $self->_vm->ip."/32";
for my $line (@{$iptables->{'nat'}}) {
my %args = @$line;
next if $args{A} ne 'PREROUTING';
if (exists $args{j} && $args{j} eq 'DNAT'
&& exists $args{d} && $args{d} eq $ip
&& exists $args{dport}
&& exists $args{'to-destination'}
) {
my $internal_port = $port{$args{dport}} or next;
if ( $args{'to-destination'}=~/\:$internal_port$/ ) {
my %delete = %args;
delete $delete{A};
delete $delete{dport};
delete $delete{m};
delete $delete{p};
my $to_destination = delete $delete{'to-destination'};
my @delete = (
t => 'nat'
,D => 'PREROUTING'
,m => 'tcp', p => 'tcp'
,dport => $args{dport}
);
push @delete, %delete;
push @delete,(
'to-destination',$to_destination
);
$self->_vm->iptables(@delete);
}
}
}
}
sub remove_expose($self, $internal_port=undef) {
$self->_close_exposed_port($internal_port);
my $query = "DELETE FROM domain_ports WHERE id_domain=?";
$query .= " AND internal_port=?" if defined $internal_port;
my $sth = $$CONNECTOR->dbh->prepare($query);
my @args = $self->id;
push @args,($internal_port) if defined $internal_port;
$sth->execute(@args);
}
=head2 list_ports
List of exposed TCP ports
=cut
sub list_ports($self) {
my $sth = $$CONNECTOR->dbh->prepare("SELECT *"
." FROM domain_ports WHERE id_domain=?");
$sth->execute($self->id);
my @list;
while (my $data = $sth->fetchrow_hashref) {
push @list,($data);
}
return @list;
}
sub _remove_iptables {
my $self = shift;
......@@ -2178,6 +2460,7 @@ sub _post_start {
$self->display($arg{user});
$self->display_file($arg{user});
$self->info($arg{user});
$self->open_exposed_ports();
}
Ravada::Request->enforce_limits(at => time + 60);
$self->post_resume_aux;
......
......@@ -84,6 +84,10 @@ our %VALID_ARG = (
,enforce_limits => { timeout => 2, _force => 2 }
,refresh_machine => { id_domain => 1, uid => 1 }
,rebase_volumes => { uid => 1, id_base => 1, id_domain => 1 }
# ports
,expose => { uid => 1, id_domain => 1, port => 1, name => 2, restricted => 2, id_port => 2}
,remove_expose => { uid => 1, id_domain => 1, port => 1}
,open_exposed_ports => {uid => 1, id_domain => 1 }
# Virtual Managers or Nodes
,refresh_vms => { _force => 2, timeout_shutdown => 2 }
......@@ -457,6 +461,15 @@ sub shutdown_domain {
return $self->_new_request(command => 'shutdown' , args => $args);
}
sub new_request($self, $command, @args) {
die "Error: Unknown request '$command'" if !$VALID_ARG{$command};
return _new_request(
$self
,command => $command
,args => _check_args($command, @args)
);
}
sub _new_request {
my $self = shift;
if ( !ref($self) ) {
......
......@@ -410,6 +410,12 @@ sub _around_create_domain {
if ($id_base) {
$domain->run_timeout($base->run_timeout)
if defined $base->run_timeout();
for my $port ( $base->list_ports ) {
my %port = %$port;
delete @port{'id','id_domain','public_port'};
$domain->expose(%port);
}
}
my $user = Ravada::Auth::SQL->search_by_id($id_owner);
$domain->is_volatile(1) if $user->is_temporary() ||($base && $base->volatile_clones());
......@@ -1238,7 +1244,6 @@ sub iptables_list($self) {
push( @{ $ret->{$current_table} }, \@option );
}
return $ret;
}
......@@ -1452,6 +1457,48 @@ sub _check_free_disk($self, $size, $storage_pool=undef) {
if $size > $free;
}
sub _list_used_ports_sql($self, $used_port) {
my $sth = $$CONNECTOR->dbh->prepare("SELECT public_port FROM domain_ports ");
$sth->execute();
my $port;
$sth->bind_columns(\$port);
while ($sth->fetch ) { $used_port->{$port}++ if defined $port };
}
sub _list_used_ports_ss($self, $used_port) {
my @cmd = ("/bin/ss","-tln");
my ($out, $err) = $self->run_command(@cmd);
for my $line ( split /\n/,$out ) {
my ($port) = $line=~ m{^LISTEN.*?\d.\d\:(\d+)};
$used_port->{$port}++ if $port;
}
}
sub _list_used_ports_iptables($self, $used_port) {
my $iptables = $self->iptables_list();
for my $rule ( @{$iptables->{nat}} ) {
my %rule = @{$rule};
next if !exists $rule{A} || $rule{A} ne 'PREROUTING';
$used_port->{dport}++;
}
}
sub _new_free_port($self) {
my $used_port = {};
$self->_list_used_ports_sql($used_port);
$self->_list_used_ports_ss($used_port);
$self->_list_used_ports_iptables($used_port);
my $free_port = 5950;
for (;;) {
last if !$used_port->{$free_port};
$free_port++ ;
}
return $free_port;
}
1;
......
......@@ -399,6 +399,39 @@
$scope.ldap_verifying = false;
});
};
$scope.expose = function(port, name, restricted, id_port) {
console.log(restricted);
if (restricted == "1" || restricted == true) {
restricted = 1;
} else {
restricted = 0;
}
console.log(restricted);
$http.post('/request/expose/'
,JSON.stringify({
'id_domain': $scope.showmachine.id
,'port': port
,'name': name
,'restricted': restricted
,'id_port': id_port
})
).then(function(response) {
$scope.refresh_machine();
});
$scope.init_new_port();
};
$scope.remove_expose = function(port) {
$http.post('/request/remove_expose/'
,JSON.stringify({
'id_domain': $scope.showmachine.id
,'port': port
})
).then(function(response) {
$scope.refresh_machine();
});
};
$scope.add_ldap_access = function() {
$http.get('/add_ldap_access/'+$scope.showmachine.id+'/'+$scope.ldap_attribute+'/'
+$scope.ldap_attribute_value+"/"+$scope.ldap_attribute_allowed
......@@ -438,6 +471,11 @@
$scope.ldap_attributes_default = response.data.default;
});
};
$scope.init_new_port = function() {
$scope.new_port = null;
$scope.new_port_name = null;
$scope.new_port_restricted = false;
};
$scope.list_nodes = function() {
$http.get('/list_nodes.json').then(function(response) {
$scope.nodes = response.data;
......
......@@ -922,6 +922,19 @@ get '/set_ldap_access/(#id_domain)/(#id_access)/(#allowed)/(#last)' => sub {
};
##############################################
post '/request/(:name)/' => sub {
my $c = shift;
my $args = decode_json($c->req->body);
warn Dumper($args);
my $req = Ravada::Request->new_request(
$c->stash('name')
,uid => $USER->id
,%$args
);
return $c->render(json => { ok => 1 });
};
get '/request/(:id).(:type)' => sub {
my $c = shift;
......
......@@ -3,16 +3,11 @@ CREATE TABLE `domain_ports` (
`id_domain` int(11) DEFAULT NULL,
`public_port` int(11) DEFAULT NULL,
`internal_port` int(11) DEFAULT NULL,
`public_ip` varchar(255) DEFAULT NULL,
`internal_ip` varchar(255) DEFAULT NULL,
`name` varchar(32) DEFAULT NULL,
`description` varchar(255) DEFAULT NULL,
`active` int(11) DEFAULT 0,
`restricted` int(1) DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `domain_port` (`id_domain`,`internal_port`),
UNIQUE KEY `description` (`id_domain`,`description`),
UNIQUE KEY `name` (`id_domain`,`name`),
UNIQUE KEY `internal` (`internal_port`,`internal_ip`),
UNIQUE KEY `public` (`public_port`,`public_ip`)
UNIQUE KEY `public` (`public_port`)
);
......@@ -3,14 +3,9 @@ CREATE TABLE `domain_ports` (
, `id_domain` integer DEFAULT NULL
, `public_port` integer DEFAULT NULL
, `internal_port` integer DEFAULT NULL
, `public_ip` varchar(255) DEFAULT NULL