package Ravada::Domain; use warnings; use strict; =head1 NAME Ravada::Domain - Domains ( Virtual Machines ) library for Ravada =cut use Carp qw(carp confess croak cluck); use Data::Dumper; use Hash::Util qw(lock_hash); use Image::Magick; use IPC::Run3 qw(run3); use JSON::XS; use Moose::Role; use Sys::Statistics::Linux; use IPTables::ChainMgr; no warnings "experimental::signatures"; use feature qw(signatures); use Ravada::Domain::Driver; use Ravada::Utils; our $TIMEOUT_SHUTDOWN = 20; our $CONNECTOR; our $MIN_FREE_MEMORY = 1024*1024; our $IPTABLES_CHAIN = 'RAVADA'; _init_connector(); requires 'name'; requires 'remove'; requires 'display'; requires 'is_active'; requires 'is_hibernated'; requires 'is_paused'; requires 'start'; requires 'shutdown'; requires 'shutdown_now'; requires 'force_shutdown'; requires '_do_force_shutdown'; requires 'pause'; requires 'resume'; requires 'prepare_base'; requires 'rename'; #storage requires 'add_volume'; requires 'list_volumes'; requires 'disk_device'; requires 'disk_size'; requires 'spinoff_volumes'; requires 'clean_swap_volumes'; #hardware info requires 'get_info'; requires 'set_memory'; requires 'set_max_mem'; requires 'hybernate'; # neworking requires 'ip'; ########################################################## has 'domain' => ( isa => 'Any' ,is => 'rw' ); has 'timeout_shutdown' => ( isa => 'Int' ,is => 'ro' ,default => $TIMEOUT_SHUTDOWN ); has 'readonly' => ( isa => 'Int' ,is => 'ro' ,default => 0 ); has 'storage' => ( is => 'ro' ,isa => 'Object' ,required => 0 ); has '_vm' => ( is => 'ro', ,isa => 'Object' ,required => 1 ); has 'tls' => ( is => 'rw' ,isa => 'Int' ,default => 0 ); has 'description' => ( is => 'rw' ,isa => 'Str' ,required => 0 ,trigger => \&_update_description ); ##################################################################################3 # ##################################################################################3 # # Method Modifiers # before 'display' => \&_allowed; before 'remove' => \&_pre_remove_domain; #\&_allow_remove; after 'remove' => \&_after_remove_domain; before 'prepare_base' => \&_pre_prepare_base; after 'prepare_base' => \&_post_prepare_base; before 'start' => \&_pre_start; after 'start' => \&_post_start; before 'pause' => \&_allow_manage; after 'pause' => \&_post_pause; before 'hybernate' => \&_allow_manage; after 'hybernate' => \&_post_pause; before 'resume' => \&_allow_manage; after 'resume' => \&_post_resume; before 'shutdown' => \&_pre_shutdown; after 'shutdown' => \&_post_shutdown; after 'shutdown_now' => \&_post_shutdown_now; before 'force_shutdown' => \&_pre_shutdown_now; after 'force_shutdown' => \&_post_shutdown_now; before 'remove_base' => \&_pre_remove_base; after 'remove_base' => \&_post_remove_base; before 'rename' => \&_pre_rename; after 'rename' => \&_post_rename; after 'screenshot' => \&_post_screenshot; ################################################## # sub BUILD { my $self = shift; $self->is_known(); } sub _vm_connect { my $self = shift; $self->_vm->connect(); } sub _vm_disconnect { my $self = shift; $self->_vm->disconnect(); } sub _pre_start { my ($self) = @_; if (scalar @_ %2 ) { _allow_manage_args(@_); } else { _allow_manage(@_); } _clean_iptables(); _check_free_memory(); _check_used_memory(@_); } sub _update_description { my $self = shift; return if defined $self->description && defined $self->_data('description') && $self->description eq $self->_data('description'); my $sth = $$CONNECTOR->dbh->prepare( "UPDATE domains SET description=? " ." WHERE id=? "); $sth->execute($self->description,$self->id); $sth->finish; $self->{_data}->{description} = $self->{description}; } sub _allow_manage_args { my $self = shift; confess "Disabled from read only connection" if $self->readonly; my %args = @_; confess "Missing user arg ".Dumper(\%args) if !$args{user} ; $self->_allowed($args{user}); } sub _allow_manage { my $self = shift; return $self->_allow_manage_args(@_) if scalar(@_) % 2 == 0; my ($user) = @_; return $self->_allow_manage_args( user => $user); } sub _allow_remove { my $self = shift; my ($user) = @_; $self->_allowed($user); $self->_check_has_clones() if $self->is_known(); } sub _pre_prepare_base { my $self = shift; my ($user, $request) = @_; $self->_allowed($user); # TODO: if disk is not base and disks have not been modified, do not generate them # again, just re-attach them $self->_check_disk_modified() if $self->is_base(); $self->_check_has_clones(); $self->is_base(0); $self->_post_remove_base(); if ($self->is_active) { $self->shutdown(user => $user); $self->{_was_active} = 1; for ( 1 .. $TIMEOUT_SHUTDOWN ) { last if !$self->is_active; sleep 1; } if ($self->is_active ) { $request->status('working' ,"Domain ".$self->name." still active, forcing hard shutdown") if $request; $self->force_shutdown($user); sleep 1; } } if ($self->id_base ) { $self->spinoff_volumes(); } }; sub _post_prepare_base { my $self = shift; my ($user) = @_; $self->is_base(1); if ($self->{_was_active} ) { $self->start($user) if !$self->is_active; } delete $self->{_was_active}; $self->_remove_id_base(); }; sub _check_has_clones { my $self = shift; return if !$self->is_known(); my @clones = $self->clones; die "Domain ".$self->name." has ".scalar @clones." clones : ".Dumper(\@clones) if $#clones>=0; } sub _check_free_memory{ my $lxs = Sys::Statistics::Linux->new( memstats => 1 ); my $stat = $lxs->get; die "ERROR: No free memory. Only ".int($stat->memstats->{realfree}/1024) ." MB out of ".int($MIN_FREE_MEMORY/1024)." MB required." if ( $stat->memstats->{realfree} < $MIN_FREE_MEMORY ); } sub _check_used_memory { my $self = shift; my $used_memory = 0; my $lxs = Sys::Statistics::Linux->new( memstats => 1 ); my $stat = $lxs->get; # We get mem total less the used for the system my $mem_total = $stat->{memstats}->{memtotal} - 1*1024*1024; for my $domain ( $self->_vm->list_domains ) { my $alive; eval { $alive = 1 if $domain->is_active && !$domain->is_paused }; next if !$alive; my $info = $domain->get_info; $used_memory += $info->{memory}; } confess "ERROR: Out of free memory. Using $used_memory RAM of $mem_total available" if $used_memory>= $mem_total; } sub _check_disk_modified { my $self = shift; if ( !$self->is_base() ) { return; } my $last_stat_base = 0; for my $file_base ( $self->list_files_base ) { my @stat_base = stat($file_base); $last_stat_base = $stat_base[9] if$stat_base[9] > $last_stat_base; # warn $last_stat_base; } my $files_updated = 0; for my $file ( $self->disk_device ) { my @stat = stat($file) or next; $files_updated++ if $stat[9] > $last_stat_base; # warn "\ncheck\t$file ".$stat[9]."\n vs \tfile_base $last_stat_base $files_updated\n"; } die "Base already created and no disk images updated" if !$files_updated; } sub _allowed { my $self = shift; my ($user) = @_; confess "Missing user" if !defined $user; confess "ERROR: User '$user' not class user , it is ".(ref($user) or 'SCALAR') if !ref $user || ref($user) !~ /Ravada::Auth/; return if $user->is_admin; my $id_owner; eval { $id_owner = $self->id_owner }; my $err = $@; die "User ".$user->name." [".$user->id."] not allowed to access ".$self->domain ." owned by ".($id_owner or '')."\n".Dumper($self) if (defined $id_owner && $id_owner != $user->id ); confess $err if $err; } ##################################################################################3 sub _init_connector { return if $CONNECTOR && $$CONNECTOR; $CONNECTOR = \$Ravada::CONNECTOR if $Ravada::CONNECTOR; $CONNECTOR = \$Ravada::Front::CONNECTOR if !defined $$CONNECTOR && defined $Ravada::Front::CONNECTOR; } =head2 id Returns the id of the domain my $id = $domain->id(); =cut sub id { return $_[0]->_data('id'); } ################################################################################## sub _data { my $self = shift; my $field = shift or confess "Missing field name"; _init_connector(); return $self->{_data}->{$field} if exists $self->{_data}->{$field}; $self->{_data} = $self->_select_domain_db( name => $self->name); confess "No DB info for domain ".$self->name if !$self->{_data}; confess "No field $field in domains" if !exists$self->{_data}->{$field}; return $self->{_data}->{$field}; } =head2 open Open a domain Argument: id Returns: Domain object read only =cut sub open($class, $id) { my $self = {}; bless $self,$class; my $row = $self->_select_domain_db ( id => $id ); die "Domain id = $id not found" if !keys %$row; die "Domain ".$row->{name}." has no VM " .Dumper($row) if !$row->{vm}; my $vm0 = {}; my $vm_class = "Ravada::VM::".$row->{vm}; bless $vm0, $vm_class; my $vm = $vm0->new( readonly => 1); return $vm->search_domain($row->{name}); } =head2 is_known Returns if the domain is known in Ravada. =cut sub is_known { my $self = shift; return $self->_select_domain_db(name => $self->name); } sub _select_domain_db { my $self = shift; my %args = @_; _init_connector(); if (!keys %args) { my $id; eval { $id = $self->id }; if ($id) { %args =( id => $id ); } else { %args = ( name => $self->name ); } } my $sth = $$CONNECTOR->dbh->prepare( "SELECT * FROM domains WHERE ".join(",",map { "$_=?" } sort keys %args ) ); $sth->execute(map { $args{$_} } sort keys %args); my $row = $sth->fetchrow_hashref; $sth->finish; $self->{_data} = $row; $self->description($row->{description}) if defined $row->{description}; return $row if $row->{id}; } sub _prepare_base_db { my $self = shift; my @file_img = @_; if (!$self->_select_domain_db) { confess "CRITICAL: The data should be already inserted"; # $self->_insert_db( name => $self->name, id_owner => $self->id_owner ); } my $sth = $$CONNECTOR->dbh->prepare( "INSERT INTO file_base_images " ." (id_domain , file_base_img, target )" ." VALUES(?,?,?)" ); for my $file_img (@file_img) { my $target; ($file_img, $target) = @$file_img if ref $file_img; $sth->execute($self->id, $file_img, $target ); } $sth->finish; $sth = $$CONNECTOR->dbh->prepare( "UPDATE domains SET is_base=1 " ." WHERE id=?"); $sth->execute($self->id); $sth->finish; $self->_select_domain_db(); } sub _set_spice_password { my $self = shift; my $password = shift; my $sth = $$CONNECTOR->dbh->prepare( "UPDATE domains set spice_password=?" ." WHERE id=?" ); $sth->execute($password, $self->id); $sth->finish; $self->{_data}->{spice_password} = $password; } =head2 spice_password Returns the password defined for the spice viewers =cut sub spice_password { my $self = shift; return $self->_data('spice_password'); } =head2 display_file Returns a file with the display information. Defaults to spice. =cut sub display_file($self,$user) { return $self->_display_file_spice($user); } # taken from isard-vdi thanks to @tuxinthejungle Alberto Larraz sub _display_file_spice($self,$user) { my ($ip,$port) = $self->display($user) =~ m{spice://(\d+\.\d+\.\d+\.\d+):(\d+)}; die "I can't find ip port in ".$self->display if !$ip ||!$port; my $ret = "[virt-viewer]\n" ."type=spice\n" ."host=$ip\n"; if ($self->tls) { $ret .= "tls-port=%s\n"; } else { $ret .= "port=$port\n"; } # $ret .="password=%s\n" if $self->spice_password(); $ret .= "fullscreen=1\n" ."title=".$self->name." - Press SHIFT+F12 to exit\n" ."enable-smartcard=0\n" ."enable-usb-autoshare=1\n" ."delete-this-file=1\n" ."usb-filter=-1,-1,-1,-1,0\n"; $ret .=";" if !$self->tls; $ret .= "tls-ciphers=DEFAULT\n" .";host-subject=O=".$ip.",CN=?\n"; $ret .=";" if !$self->tls; $ret .="ca=CA\n" ."toggle-fullscreen=shift+f11\n" ."release-cursor=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"; return $ret; } sub _insert_db { my $self = shift; my %field = @_; _init_connector(); for (qw(name id_owner)) { confess "Field $_ is mandatory ".Dumper(\%field) if !exists $field{$_}; } my ($vm) = ref($self) =~ /.*\:\:(\w+)$/; confess "Unknown domain from ".ref($self) if !$vm; $field{vm} = $vm; my $query = "INSERT INTO domains " ."(" . join(",",sort keys %field )." )" ." VALUES (". join(",", map { '?' } keys %field )." ) " ; my $sth = $$CONNECTOR->dbh->prepare($query); eval { $sth->execute( map { $field{$_} } sort keys %field ) }; if ($@) { #warn "$query\n".Dumper(\%field); confess $@; } $sth->finish; } =head2 pre_remove Code to run before removing the domain. It can be implemented in each domain. It is not expected to run by itself, the remove function calls it before proceeding. $domain->pre_remove(); # This isn't likely to be necessary $domain->remove(); # Automatically calls the domain pre_remove method =cut sub pre_remove { } sub _pre_remove_domain { my $self = shift; eval { $self->id }; $self->_allow_remove(@_); $self->shutdown_now(@_) if $self->is_active(); $self->pre_remove(); } sub _after_remove_domain { my $self = shift; if ($self->is_base) { $self->_do_remove_base(@_); $self->_remove_files_base(); } return if !$self->{_data}; $self->_remove_ports_db(); $self->_remove_base_db(); $self->_remove_domain_db(); } sub _remove_ports_db { my $self = shift; my $sth = $$CONNECTOR->dbh->prepare("DELETE FROM domain_ports where id_domain=?"); $sth->execute($self->id); $sth->finish; } sub _remove_domain_db { my $self = shift; return if !$self->is_known(); $self->_select_domain_db or return; my $sth = $$CONNECTOR->dbh->prepare("DELETE FROM domains " ." WHERE id=?"); $sth->execute($self->id); $sth->finish; } sub _remove_files_base { my $self = shift; for my $file ( $self->list_files_base ) { unlink $file or die "$! $file" if -e $file; } } sub _remove_id_base { my $self = shift; my $sth = $$CONNECTOR->dbh->prepare( "UPDATE domains set id_base=NULL " ." WHERE id=?" ); $sth->execute($self->id); $sth->finish; } =head2 is_base Returns true or false if the domain is a prepared base =cut sub is_base { my $self = shift; my $value = shift; $self->_select_domain_db or return 0; if (defined $value ) { my $sth = $$CONNECTOR->dbh->prepare( "UPDATE domains SET is_base=? " ." WHERE id=?"); $sth->execute($value, $self->id ); $sth->finish; return $value; } my $ret = $self->_data('is_base'); $ret = 0 if $self->_data('is_base') =~ /n/i; return $ret; }; =head2 is_locked Shows if the domain has running or pending requests. It could be considered too as the domain is busy doing something like starting, shutdown or prepare base. Returns true if locked. =cut sub is_locked { my $self = shift; $self->_init_connector() if !defined $$CONNECTOR; my $sth = $$CONNECTOR->dbh->prepare("SELECT id FROM requests " ." WHERE id_domain=? AND status <> 'done'"); $sth->execute($self->id); my ($id) = $sth->fetchrow; $sth->finish; return ($id or 0); } =head2 id_owner Returns the id of the user that created this domain =cut sub id_owner { my $self = shift; return $self->_data('id_owner',@_); } =head2 id_base Returns the id from the base this domain is based on, if any. =cut sub id_base { my $self = shift; return $self->_data('id_base',@_); } =head2 vm Returns a string with the name of the VM ( Virtual Machine ) this domain was created on =cut sub vm { my $self = shift; return $self->_data('vm'); } =head2 clones Returns a list of clones from this virtual machine my @clones = $domain->clones =cut sub clones { my $self = shift; _init_connector(); my $sth = $$CONNECTOR->dbh->prepare("SELECT id, name FROM domains " ." WHERE id_base = ?"); $sth->execute($self->id); my @clones; while (my $row = $sth->fetchrow_hashref) { # TODO: open the domain, now it returns only the id push @clones , $row; } return @clones; } =head2 has_clones Returns the number of clones from this virtual machine my $has_clones = $domain->has_clones =cut sub has_clones { my $self = shift; _init_connector(); return scalar $self->clones; } =head2 list_files_base Returns a list of the filenames of this base-type domain =cut sub list_files_base { my $self = shift; my $with_target = shift; return if !$self->is_known(); my $id; eval { $id = $self->id }; return if $@ && $@ =~ /No DB info/i; die $@ if $@; my $sth = $$CONNECTOR->dbh->prepare("SELECT file_base_img, target " ." FROM file_base_images " ." WHERE id_domain=?"); $sth->execute($self->id); my @files; while ( my ($img, $target) = $sth->fetchrow) { push @files,($img) if !$with_target; push @files,[$img,$target] if $with_target; } $sth->finish; return @files; } =head2 list_files_base_target Returns a list of the filenames and targets of this base-type domain =cut sub list_files_base_target { return $_[0]->list_files_base("target"); } =head2 json Returns the domain information as json =cut sub json { my $self = shift; my $id = $self->_data('id'); my $data = $self->{_data}; $data->{is_active} = $self->is_active; return encode_json($data); } =head2 can_screenshot Returns wether this domain can take an screenshot. =cut sub can_screenshot { return 0; } sub _convert_png { my $self = shift; my ($file_in ,$file_out) = @_; my $in = Image::Magick->new(); my $err = $in->Read($file_in); confess $err if $err; $in->Write("png24:$file_out"); chmod 0755,$file_out or die "$! chmod 0755 $file_out"; } =head2 remove_base Makes the domain a regular, non-base virtual machine and removes the base files. =cut sub remove_base { my $self = shift; return $self->_do_remove_base(); } sub _do_remove_base { my $self = shift; $self->is_base(0); for my $file ($self->list_files_base) { next if ! -e $file; unlink $file or die "$! unlinking $file"; } $self->storage_refresh() if $self->storage(); } sub _pre_remove_base { _allow_manage(@_); _check_has_clones(@_); $_[0]->spinoff_volumes(); } sub _post_remove_base { my $self = shift; $self->_remove_base_db(@_); $self->_post_remove_base_domain(); } sub _pre_shutdown_domain {} sub _post_remove_base_domain {} sub _remove_base_db { my $self = shift; my $sth = $$CONNECTOR->dbh->prepare("DELETE FROM file_base_images " ." WHERE id_domain=?"); $sth->execute($self->id); $sth->finish; } =head2 clone Clones a domain =head3 arguments =over =item user => $user : The user that owns the clone =item name => $name : Name of the new clone =back =cut sub clone { my $self = shift; my %args = @_; my $name = $args{name} or confess "ERROR: Missing domain cloned name"; confess "ERROR: Missing request user" if !$args{user}; my $uid = $args{user}->id; $self->prepare_base($args{user}) if !$self->is_base(); my $id_base = $self->id; my $clone = $self->_vm->create_domain( name => $name ,id_base => $id_base ,id_owner => $uid ,vm => $self->vm ,_vm => $self->_vm ); $clone->description($self->description) if defined $self->description; return $clone; } sub _post_pause { my $self = shift; $self->_remove_iptables(); } sub _pre_shutdown { my $self = shift; $self->_allow_manage_args(@_); $self->_pre_shutdown_domain(); if ($self->is_paused) { my %args = @_; $self->resume(user => $args{user}); } } sub _post_shutdown { my $self = shift; my %arg = @_; my $timeout = delete $arg{timeout}; my $user = delete $arg{user}; $self->_remove_temporary_machine(@_); $self->_remove_iptables() if $self->is_known(); $self->clean_swap_volumes(user => $user) if $self->is_known && $self->id_base() && !$self->is_active; if (defined $timeout) { if ($timeout<2 && $self->is_active) { sleep $timeout; return $self->_do_force_shutdown() if $self->is_active; } confess "ERROR: Missing user " if !$user; my $req = Ravada::Request->force_shutdown_domain( name => $self->name , uid => $user->id , at => time+$timeout ); } } sub _pre_shutdown_now { my $self = shift; return if !$self->is_active; } sub _post_shutdown_now { my $self = shift; my $user = shift; $self->_post_shutdown(user => $user); } =head2 can_hybernate Returns wether a domain supports hybernation =cut sub can_hybernate { 0 }; =head2 add_volume_swap Adds a swap volume to the virtual machine Arguments: size => $kb name => $name (optional) =cut sub add_volume_swap { my $self = shift; my %arg = @_; $arg{name} = $self->name if !$arg{name}; $self->add_volume(%arg, swap => 1); } =head2 expose Expose a TCP port from the domain Arguments: - user - number of the port - optional name Returns: public ip and port =cut sub expose($self, $user, $internal_port, $name=undef) { die "User ".$user->name." [".$user->id."] not allowed.\n" if $self->id_owner != $user->id && !$user->is_admin; my $sth = $$CONNECTOR->dbh->prepare( "INSERT INTO domain_ports (id_domain" ." ,public_port, internal_port" ." ,public_ip, internal_ip" ." ,name" .")" ." VALUES (?,?,?,?,?,?)" ); my $internal_ip; if ($self->is_active) { $internal_ip = $self->ip or warn "No internal IP for domain ".$self->name; } my $public_ip = $self->_vm->ip; my $public_port = $self->_new_free_port(); # TODO # if ($self->is_network_nat) { # $public_ip = $self->_vm->ip; # $public_port = $self->_new_free_port(); # } else { # $public_ip = $self->ip; # $public_port = $internal_port # } $sth->execute($self->id , $public_port, $internal_port , $public_ip, $internal_ip, ($name or undef)); $sth->finish; if ($internal_ip) { my $remote_ip = $self->remote_ip(); $self->_add_iptable($user, $remote_ip, $public_ip, $public_port); $self->_add_iptable_nat($user, $public_ip, $public_port, $internal_ip, $internal_port); } return($public_ip, $public_port); } =head2 remove_expose Remove a exposed TCP port from the domain Arguments: - number of the port - user =cut sub remove_expose($self, $user, $internal_port) { $self->_allow_manage($user); my ($public_ip, $public_port) = $self->public_address($internal_port); $self->_remove_iptables(d_port => $public_port) if $public_port; my $sth = $$CONNECTOR->dbh->prepare( "DELETE FROM domain_ports WHERE id_domain=? AND internal_port=?" ); $sth->execute($self->id, $internal_port); } =head2 list_ports List of exposed TCP ports =cut sub list_ports($self) { $self->_init_connector(); my $sth = $$CONNECTOR->dbh->prepare("SELECT * FROM domain_ports " ." WHERE id_domain=?"); $sth->execute($self->id); my @ports; while (my $row = $sth->fetchrow_hashref) { push @ports,($row); } return @ports; } =head2 public_address Returns the public IP address and port for a TCP service running in the domain. Arguments: The internal port =cut sub public_address($self, $internal_port=undef) { return $self->_vm->ip if !$internal_port; my $sth = $$CONNECTOR->dbh->prepare( "SELECT public_ip,public_port " ." FROM domain_ports " ." WHERE id_domain=?" ." AND internal_port=?" ); $sth->execute($self->id, $internal_port); my ($public_ip, $public_port) = $sth->fetchrow(); return ($public_ip, $public_port); } =head2 client_ip Returns the IP of the remote client that requested the start of the domain =cut sub client_ip($self) { my @active_iptables = $self->_active_iptables(); die Dumper(\@active_iptables); } sub _remove_iptables { my $self = shift; my %args = @_; my $d_port = delete $args{d_port}; croak "Unknown params:". join(", ", keys %args) if %args; my $ipt_obj = _obj_iptables(); my $sth = $$CONNECTOR->dbh->prepare( "UPDATE iptables SET time_deleted=?" ." WHERE id=?" ); for my $row ($self->_active_iptables()) { my ($id, $iptables) = @$row; if ($d_port) { # warn "$d_port\n".Dumper($iptables)."\n"; next if !exists $iptables->[5]->{d_port} || $iptables->[5]->{d_port} ne $d_port; } my ($rv, $out, $err) = $ipt_obj->delete_ip_rule(@$iptables); # if (!$rv) { # warn Dumper($out,$err); ## exit; # } $sth->execute(Ravada::Utils::now(), $id); } } # clean iptables left from down domains sub _clean_iptables { my $sth = $$CONNECTOR->dbh->prepare( "SELECT id,id_domain,iptables FROM iptables " ." WHERE time_deleted IS NULL" ); my $sth_delete = $$CONNECTOR->dbh->prepare( "DELETE FROM iptables WHERE id=?" ); my ( $id, $id_domain, $iptables); $sth->execute; $sth->bind_columns(\( $id, $id_domain, $iptables)); while ($sth->fetch) { my $domain; eval { $domain = Ravada::Domain->open($id_domain) }; warn $@ if $@ && $@ !~ /Domain.*not found/i; if (!$domain) { $sth_delete->execute($id); next; } next if $domain->is_active; $domain->_remove_iptables(); } $sth->finish; } sub _remove_temporary_machine { my $self = shift; my %args = @_; my $user; return if !$self->is_known(); eval { $user = Ravada::Auth::SQL->search_by_id($self->id_owner) }; return if !$user; if ($user->is_temporary) { $self->remove($user); my $req= $args{request}; $req->status( "removing" ,"Removing domain ".$self->name." after shutdown" ." because user " .$user->name." is temporary") if $req; } } sub _post_resume { return _post_start(@_); } sub _post_start { my $self = shift; $self->_add_iptable_display(@_); $self->_open_iptables(@_); } sub _open_iptables { my $self = shift; # we need the remote_ip => ip , user => user return if scalar @_ % 2 ==1; my %args = @_; my $user = $args{user}; my $remote_ip = $args{remote_ip}; my @ports = $self->list_ports(); return if !@ports; # TODO check if no NAT, public ip is domain ip my $public_ip = $self->_vm->ip(); my $internal_ip = $self->ip(); for ( 0 .. 2 ) { $internal_ip = $self->ip(); last if $internal_ip; sleep 1; } return if !$internal_ip; for my $row (@ports) { $self->_add_iptable($user, $remote_ip, $public_ip , $row->{public_port}); $self->_add_iptable_nat($user, $public_ip, $row->{public_port} , $internal_ip, $row->{internal_port}); } } sub _add_iptable_display { my $self = shift; return if scalar @_ % 2; my %args = @_; my $remote_ip = $args{remote_ip} or return; my $user = $args{user}; my $display = $self->display($user); my ($local_ip, $local_port) = $display =~ m{\w+://(.*):(\d+)}; $self->_add_iptable( $user, $remote_ip, $local_ip, $local_port); } sub _add_iptable($self, $user, $remote_ip, $local_ip, $local_port) { my $ipt_obj = _obj_iptables(); # append rule at the end of the RAVADA chain in the filter table to # allow all traffic from $local_ip to $remote_ip via port $local_port # my $filter = 'filter'; my $chain = $IPTABLES_CHAIN; my @iptables_arg = ($remote_ip ,$local_ip, $filter, $chain, 'ACCEPT', ,{'protocol' => 'tcp', 's_port' => 0, 'd_port' => $local_port}); my ($rv, $out_ar, $errs_ar) = $ipt_obj->append_ip_rule(@iptables_arg); $self->_log_iptable(iptables => \@iptables_arg, remote_ip => $remote_ip, user => $user); @iptables_arg = ( '0.0.0.0' ,$local_ip, $filter, $chain, 'DROP', ,{'protocol' => 'tcp', 's_port' => 0, 'd_port' => $local_port}); ($rv, $out_ar, $errs_ar) = $ipt_obj->append_ip_rule(@iptables_arg); $self->_log_iptable(iptables => \@iptables_arg, remote_ip => $remote_ip, user => $user); } sub _add_iptable_nat($self,$user, $public_ip, $public_port, $internal_ip, $internal_port) { confess "Undefined internal_ip (arg 4)" if !defined $internal_ip; my $filter = 'nat'; my $chain = 'PREROUTING'; my $ipt_obj = _obj_iptables(); my @iptables_arg = ( '0.0.0.0/0', $public_ip, $filter, $chain, 'DNAT' ,{ protocol => 'tcp', d_port => $public_port , to_port => $internal_port , to_ip => $internal_ip } ); my ($rv, $out_ar, $errs_ar) = $ipt_obj->run_ipt_cmd( "/sbin/iptables -t nat -A PREROUTING" ." -d $public_ip" ." -m tcp -p tcp --dport $public_port" ." -j DNAT --to-destination $internal_ip:$internal_port" ); $self->_log_iptable(iptables => \@iptables_arg , remote_ip => $public_ip , user => $user); } =head2 open_iptables Open iptables for a remote client =over =item user =item remote_ip =back =cut sub open_iptables { my $self = shift; my %args = @_; my $user = Ravada::Auth::SQL->search_by_id($args{uid}); $args{user} = $user; delete $args{uid}; $self->_add_iptable_display(%args); } sub _obj_iptables { my %opts = ( 'use_ipv6' => 0, # can set to 1 to force ip6tables usage 'ipt_rules_file' => '', # optional file path from # which to read iptables rules 'iptout' => '/tmp/iptables.out', 'ipterr' => '/tmp/iptables.err', 'debug' => 0, 'verbose' => 0, ### advanced options 'ipt_alarm' => 5, ### max seconds to wait for iptables execution. 'ipt_exec_style' => 'waitpid', ### can be 'waitpid', ### 'system', or 'popen'. 'ipt_exec_sleep' => 0, ### add in time delay between execution of ### iptables commands (default is 0). ); my $ipt_obj = IPTables::ChainMgr->new(%opts) or die "[*] Could not acquire IPTables::ChainMgr object"; my $rv = 0; my $out_ar = []; my $errs_ar = []; #check_chain_exists ($rv, $out_ar, $errs_ar) = $ipt_obj->chain_exists('filter', $IPTABLES_CHAIN); if (!$rv) { $ipt_obj->create_chain('filter', $IPTABLES_CHAIN); $ipt_obj->add_jump_rule('filter','INPUT', 1, $IPTABLES_CHAIN); } # set the policy on the FORWARD table to DROP # $ipt_obj->set_chain_policy('filter', 'FORWARD', 'DROP'); return $ipt_obj; } sub _log_iptable { my $self = shift; if (scalar(@_) %2 ) { carp "Odd number ".Dumper(\@_); return; } my %args = @_; my $remote_ip = $args{remote_ip};#~ or return; my $user = $args{user}; my $uid = $args{uid}; confess "Chyoose wether uid or user " if $user && $uid; lock_hash(%args); $uid = $args{user}->id if !$uid; my $iptables = $args{iptables}; my $sth = $$CONNECTOR->dbh->prepare( "INSERT INTO iptables " ."(id_domain, id_user, remote_ip, time_req, iptables)" ."VALUES(?, ?, ?, ?, ?)" ); $sth->execute($self->id, $uid, $remote_ip, Ravada::Utils::now() ,encode_json($iptables)); $sth->finish; } sub _active_iptables($self) { my $sth = $$CONNECTOR->dbh->prepare( "SELECT id,iptables FROM iptables " ." WHERE " ." id_domain=?" ." AND time_deleted IS NULL" ." ORDER BY time_req DESC " ); $sth->execute($self->id); my @iptables; while (my ($id, $iptables) = $sth->fetchrow) { push @iptables, [ $id, decode_json($iptables)]; } return @iptables; } sub _check_duplicate_domain_name { my $self = shift; # TODO # check name not in current domain in db # check name not in other VM domain $self->id(); } sub _rename_domain_db { my $self = shift; my %args = @_; my $new_name = $args{name} or confess "Missing new name"; my $sth = $$CONNECTOR->dbh->prepare("UPDATE domains set name=?" ." WHERE id=?"); $sth->execute($new_name, $self->id); $sth->finish; } =head2 is_public Sets or get the domain public $domain->is_public(1); if ($domain->is_public()) { ... } =cut sub is_public { my $self = shift; my $value = shift; _init_connector(); if (defined $value) { my $sth = $$CONNECTOR->dbh->prepare("UPDATE domains set is_public=?" ." WHERE id=?"); $sth->execute($value, $self->id); $sth->finish; } return $self->_data('is_public'); } =head2 clean_swap_volumes Check if the domain has swap volumes defined, and clean them $domain->clean_swap_volumes(); =cut sub clean_swap_volumes { my $self = shift; for my $file ( $self->list_volumes) { $self->clean_disk($file) if $file =~ /\.SWAP\.\w+$/; } } sub _pre_rename { my $self = shift; my %args = @_; my $name = $args{name}; my $user = $args{user}; $self->_check_duplicate_domain_name(@_); $self->shutdown(user => $user) if $self->is_active; } sub _post_rename { my $self = shift; my %args = @_; $self->_rename_domain_db(@_); } sub _post_screenshot { my $self = shift; my ($filename) = @_; return if !defined $filename; my $sth = $$CONNECTOR->dbh->prepare( "UPDATE domains set file_screenshot=? " ." WHERE id=?" ); $sth->execute($filename, $self->id); $sth->finish; } =head2 drivers List the drivers available for a domain. It may filter for a given type. my @drivers = $domain->drivers(); my @video_drivers = $domain->drivers('video'); =cut sub drivers { my $self = shift; my $name = shift; my $type = (shift or $self->_vm->type); _init_connector(); $type = 'qemu' if $type =~ /^KVM$/; my $query = "SELECT id from domain_drivers_types " ." WHERE vm=?"; $query .= " AND name=?" if $name; my $sth = $$CONNECTOR->dbh->prepare($query); my @sql_args = ($type); push @sql_args,($name) if $name; $sth->execute(@sql_args); my @drivers; while ( my ($id) = $sth->fetchrow) { push @drivers,Ravada::Domain::Driver->new(id => $id, domain => $self); } return $drivers[0] if !wantarray && $name && scalar@drivers< 2; return @drivers; } =head2 set_driver_id Sets the driver of a domain given it id. The id must be one from the table domain_drivers_options $domain->set_driver_id($id_driver); =cut sub set_driver_id { my $self = shift; my $id = shift; my $sth = $$CONNECTOR->dbh->prepare( "SELECT d.name,o.value " ." FROM domain_drivers_types d, domain_drivers_options o" ." WHERE d.id=o.id_driver_type " ." AND o.id=?" ); $sth->execute($id); my ($type, $value) = $sth->fetchrow; confess "Unknown driver option $id" if !$type || !$value; $self->set_driver($type => $value); $sth->finish; } sub _remote_data { my $self = shift; my $sth = $$CONNECTOR->dbh->prepare( "SELECT * FROM iptables " ." WHERE " ." id_domain=?" ." AND time_deleted IS NULL" ." ORDER BY time_req DESC " ); $sth->execute($self->id); my @rows; while (my $row = $sth->fetchrow_hashref) { my $iptables = decode_json($row->{iptables}); next if $iptables->[3] ne 'RAVADA' || $iptables->[4] ne 'ACCEPT'; next if scalar@rows && $row->{id_user} == $rows[0]->{id_user} && $row->{remote_ip} eq $rows[0]->{remote_ip}; push @rows,($row); } die "Too many active tables for this domain" .Dumper(\@rows) if scalar @rows>1; return $rows[0]->{remote_ip} , $rows[0]->{id_user} } =head2 remote_ip Returns the IP of the remote host that launched this virtual machine =cut sub remote_ip($self) { my ( $remote_ip ) = $self->_remote_data(); return $remote_ip; } =head2 remote_user Returns the user that launched this virtual machine =cut sub remote_user($self) { my (undef, $remote_uid) = $self->_remote_data(); return if !$remote_uid; return Ravada::Auth::SQL->search_by_id($remote_uid); } sub _new_free_port { my $self = shift; my $used_port = {}; $self->_list_used_ports_sql($used_port); $self->_list_used_ports_netstat($used_port); my $free_port = 5950; for (;;) { last if !$used_port->{$free_port}; $free_port++ ; } return $free_port; } sub _dbh { my $self = shift; _init_connector() if !$CONNECTOR || !$$CONNECTOR; return $$CONNECTOR->dbh; } sub _list_used_ports_sql { my $self = shift; my $used_port = shift; 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}++ }; } sub _list_used_ports_netstat { my $self = shift; my $used_port = shift; my @cmd = ('netstat', '-tln'); my ($in, $out, $err); run3(\@cmd, \$in, \$err, \$out); for my $line (split /\n/, $out) { my ($port) = $line =~ /^tcp \s+\d+ \s+\d+ \s+\d+\.\d+\.\d+\.\d+ \:(\d+)/; $used_port->{$port}++ if $port; } } 1;