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

Feature booking (#1512)

feat: bookings

* feat(cli): manage LDAP groups and users
* feat(frontend): manage LDAP groups and users

issue #1337
parent 1f45a31e
......@@ -4,7 +4,7 @@ TZ=Europe/Madrid
# If you change password remember update dockerfy/dockers/back/ravada.conf
MYSQL_DATABASE=ravada
MYSQL_ROOT_PASSWORD=Pword12345*
MYSQL_HOST=127.0.0.1
MYSQL_HOST=0.0.0.0
MYSQL_PORT=33306
MYSQL_USER=rvd_user
MYSQL_PASSWORD=Pword12345*
......
......@@ -11,6 +11,8 @@ services:
env_file: .env
command: --default-authentication-plugin=mysql_native_password
restart: unless-stopped
ports:
- "33306:33306"
ravada-front:
container_name: ravada-front
......@@ -24,10 +26,9 @@ services:
networks:
- ravada_network
#By default download from dockerhub
image: ravada/front
env_file: .env
#If you want to local build
#build: dockers/front/.
build: dockers/front/.
restart: unless-stopped
depends_on:
......@@ -50,10 +51,9 @@ services:
networks:
- ravada_network
#By default download from dockerhub
image: ravada/back
env_file: .env
#If you want to local build
#build: dockers/back/.
build: dockers/back/.
privileged: true
restart: unless-stopped
......
......@@ -14,12 +14,13 @@ RUN apt-get update \
liblwp-useragent-determined-perl libvirt-clients supervisor net-tools openssh-client apt-utils curl libpbkdf2-tiny-perl \
libio-stringy-perl libvirt-daemon-system libvirt-clients netcat-openbsd qemu-kvm qemu-utils iproute2 wget bridge-utils firewalld dnsmasq iptables ebtables \
libnet-openssh-perl libdatetime-format-dateparse-perl file\
&& apt-get clean
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y tzdata \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
ENV TZ=Europe/Madrid
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y tzdata \
RUN echo "listen_tls = 0" >> /etc/libvirt/libvirtd.conf \
&& echo 'listen_tcp = 1' >> /etc/libvirt/libvirtd.conf \
# && mkdir -p /root/.ssh \
......@@ -40,5 +41,5 @@ COPY supervisord.conf /etc/supervisord.conf
#ADD src/ravada /ravada
COPY ravada.conf /etc/ravada.conf
WORKDIR /ravada
ENV PERL5LIB /ravada/lib
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
db:
user: rvd_user
password: Pword12345*
host: ravada-mysql
\ No newline at end of file
host: ravada-mysql
ldap:
server: 10.1.36.224
admin_user:
dn: cn=Directory Manager
password: 12345678
base: 'dc=example,dc=com'
......@@ -13,12 +13,16 @@ RUN apt-get update \
libfile-rsync-perl libdate-calc-perl libparallel-forkmanager-perl libdatetime-perl libencode-locale-perl netcat-openbsd \
libio-stringy-perl libvirt-clients liblwp-useragent-determined-perl supervisor net-tools apt-utils lsof mysql-client \
curl bash vim wget libnet-openssh-perl libdatetime-format-dateparse-perl \
&& apt-get clean
&& apt-get clean \
ENV TZ=Europe/Madrid
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y tzdata \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN mkdir -p /var/log/supervisor \
&& mkdir -p /run/sshd
......
db:
user: rvd_user
password: Pword12345*
host: ravada-mysql
\ No newline at end of file
host: ravada-mysql
ldap:
server: 10.1.36.224
admin_user:
dn: cn=Directory Manager
password: 12345678
base: 'dc=example,dc=com'
......@@ -7,7 +7,7 @@ logfile_maxbytes=0
[program:rvd_front]
environment=PERL5LIB="./lib"
command=morbo ./script/rvd_front
command=morbo -v ./script/rvd_front
autostart=true
autorestart=true
startsecs=5
......
https://cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.css morris.js/
https://use.fontawesome.com/releases/v5.10.1/fontawesome-free-5.10.1-web.zip
https://cdnjs.cloudflare.com/ajax/libs/intro.js/2.7.0/introjs.css intro.js/bin/
https://code.jquery.com/jquery-3.5.0.min.js jquery/
https://code.jquery.com/jquery-3.5.1.slim.min.js jquery/
https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js
https://jqueryui.com/resources/download/jquery-ui-1.11.4.zip
https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js
https://code.angularjs.org/1.7.8/angular-1.7.8.zip
https://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/2.5.0/ui-bootstrap.min.js
https://cdn.jsdelivr.net/npm/ui-bootstrap4@3.0.6/dist/ui-bootstrap-tpls.js
https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js raphael.js/
https://github.com/snapappointments/bootstrap-select/archive/v1.13.15.zip
https://cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.min.js morris.js/
......@@ -17,3 +18,13 @@ https://readthedocs.org/projects/ravada/badge/?version=latest ../img/latest.svg
https://img.shields.io/badge/License-AGPL%20v3-blue.svg ../img/License-AGPL%20v3-blue.svg
https://download.cksource.com/CKEditor/CKEditor/CKEditor%204.12.1/ckeditor_4.12.1_standard_easyimage.zip
https://ajax.googleapis.com/ajax/libs/angular_material/1.1.0/angular-material.min.js angular-material/
# bookings
https://cdn.jsdelivr.net/npm/fullcalendar@5.1.0/main.css bookings/
https://cdn.jsdelivr.net/npm/clockpicker@0.0.7/dist/bootstrap-clockpicker.min.css bookings/
https://cdn.jsdelivr.net/npm/angularjs-toast@latest/angularjs-toast.css bookings/
https://cdn.jsdelivr.net/npm/angularjs-toast@latest/angularjs-toast.js bookings/
https://cdn.jsdelivr.net/npm/fullcalendar@5.1.0/main.min.js bookings/
https://cdn.jsdelivr.net/npm/fullcalendar-scheduler@5.1.0/locales-all.min.js bookings/
https://cdn.jsdelivr.net/npm/moment@2.27.0/min/moment-with-locales.min.js bookings/
https://cdn.jsdelivr.net/npm/angular-moment@1.3.0/angular-moment.min.js bookings/
https://cdn.jsdelivr.net/npm/clockpicker@0.0.7/dist/bootstrap-clockpicker.min.js bookings/
......@@ -44,10 +44,12 @@ sub download($url, $dst = $DIR_FALLBACK) {
print "$url downloaded to $dst\n";
$res->content->asset->move_to($dst);
}
elsif ($res->is_error) { print $res->message."\n" }
elsif ($res->is_error) { print $res->message."\n"; exit }
elsif ($res->code == 301) { print $res->headers->location."\n" }
else { print "Error ".$res->code." ".$res->message
." downloading $url\n"}
." downloading $url\n";
exit;
}
return $dst;
}
......
......@@ -9,7 +9,8 @@ use Carp qw(carp croak cluck);
use Data::Dumper;
use DBIx::Connector;
use File::Copy;
use Hash::Util qw(unlock_hash lock_hash);
use Hash::Util qw(lock_hash unlock_hash);
use IPC::Run3 qw(run3);
use JSON::XS;
use Moose;
use POSIX qw(WNOHANG);
......@@ -23,6 +24,7 @@ no warnings "experimental::signatures";
use feature qw(signatures);
use Ravada::Auth;
use Ravada::Booking;
use Ravada::Request;
use Ravada::Repository::ISO;
use Ravada::VM::Void;
......@@ -1085,21 +1087,34 @@ sub _add_indexes_generic($self) {
,settings => [
"index(id_parent,name)"
]
,booking_entries => [
"index(id_booking)"
]
,booking_entry_ldap_groups => [
"index(id_booking_entry,ldap_group)"
]
,booking_entry_users => [
"index(id_booking_entry,id_user)"
]
,booking_entry_bases => [
"index(id_booking_entry,id_base)"
]
,vms=> [
"unique(hostname, vm_type)"
]
);
my $if_not_exists = '';
$if_not_exists = ' IF NOT EXISTS ' if $CONNECTOR->dbh->{Driver}{Name} =~ /sqlite|mariadb/i;
for my $table ( keys %index ) {
my $known = $self->_get_indexes($table);
my $known;
for my $change (@{$index{$table}} ) {
my ($type,$fields ) =$change =~ /(\w+)\((.*)\)/;
my ($name) = $change =~ /:(.*)/;
$name = $fields if !$name;
$name =~ s/,/_/g;
$name =~ s/ //g;
$known = $self->_get_indexes($table) if !defined $known;
next if $known->{$name};
$type .=" INDEX " if $type=~ /^unique/i;
......@@ -1183,6 +1198,8 @@ sub _add_grants($self) {
$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('expose_ports',0,"Can expose virtual machine ports.");
$self->_add_grant('view_groups',0,'Can view groups.');
$self->_add_grant('manage_groups',0,'Can manage groups.');
$self->_add_grant('start_limit',0,"can have their own limit on started machines.", 1);
}
......@@ -1254,6 +1271,8 @@ sub _enable_grants($self) {
,'shutdown', 'shutdown_all', 'shutdown_clone'
,'reboot', 'reboot_all', 'reboot_clones'
,'screenshot'
,'start_many'
,'view_groups', 'manage_groups'
,'start_limit', 'start_many'
);
......@@ -1312,6 +1331,7 @@ sub _set_url_isos($self, $new_url='http://localhost/iso/') {
$sth->finish;
}
sub _upgrade_table {
my $self = shift;
my ($table, $field, $definition) = @_;
......@@ -1455,6 +1475,8 @@ sub _create_tables {
# return if $CONNECTOR->dbh->{Driver}{Name} !~ /mysql/i;
my $driver = lc($CONNECTOR->dbh->{Driver}{Name});
$driver = 'mysql' if $driver =~ /mariadb/i;
$DIR_SQL =~ s{(.*)/.*}{$1/$driver};
opendir my $ls,$DIR_SQL or die "$! $DIR_SQL";
......@@ -1473,7 +1495,7 @@ sub _sql_create_tables($self) {
my %tables = (
domain_displays => {
id => 'integer NOT NULL PRIMARY KEY AUTO_INCREMENT'
,id_domain => 'integer NOT NULL references domains(id)'
,id_domain => 'integer NOT NULL references domains(id) on delete cascade'
,port => 'char(5) DEFAULT NULL'
,ip => 'varchar(254)'
,listen_ip => 'varchar(254)'
......@@ -1492,6 +1514,46 @@ sub _sql_create_tables($self) {
, name => 'varchar(64) NOT NULL'
, value => 'varchar(128) DEFAULT NULL'
}
,bookings => {
id => 'integer NOT NULL PRIMARY KEY AUTO_INCREMENT'
,title => 'varchar(80)'
,description => 'varchar(255)'
,date_start => 'date not null'
,date_end => 'date not null'
,id_owner => 'int not null'
,background_color => 'varchar(20)'
,date_created => 'datetime DEFAULT CURRENT_TIMESTAMP'
}
,booking_entries => {
id => 'integer NOT NULL PRIMARY KEY AUTO_INCREMENT'
,title => 'varchar(80)'
,description => 'varchar(255)'
,id_booking => 'int not null references bookings(id) ON DELETE CASCADE'
,time_start => 'time not null'
,time_end => 'time not null'
,date_booking => 'date'
,visibility => "enum ('private','public') default 'public'"
}
,booking_entry_ldap_groups => {
id => 'integer NOT NULL PRIMARY KEY AUTO_INCREMENT'
,id_booking_entry
=> 'int not null references booking_entries(id) ON DELETE CASCADE'
,ldap_group => 'varchar(255) not null'
}
,booking_entry_users => {
id => 'integer NOT NULL PRIMARY KEY AUTO_INCREMENT'
,id_booking_entry
=> 'int not null references booking_entries(id) ON DELETE CASCADE'
,id_user => 'int not null references users(id) ON DELETE CASCADE'
}
,booking_entry_bases=> {
id => 'integer NOT NULL PRIMARY KEY AUTO_INCREMENT'
,id_booking_entry
=> 'int not null references booking_entries(id) ON DELETE CASCADE'
,id_base => 'int not null references domains(id) ON DELETE CASCADE'
}
);
for my $table ( keys %tables ) {
my $sth = $CONNECTOR->dbh->table_info('%',undef,$table,'TABLE');
......@@ -1588,6 +1650,16 @@ sub _sql_insert_defaults($self){
,name => 'start_limit'
,value => 1
}
,{
id_parent => $id_backend
,name => 'time_zone'
,value => _default_time_zone()
}
,{
id_parent => $id_backend
,name => 'bookings'
,value => 0
}
,{
id_parent => $id_backend
,name => 'debug'
......@@ -1623,6 +1695,26 @@ sub _sql_insert_defaults($self){
}
}
sub _default_time_zone() {
return $ENV{TZ} if exists $ENV{TZ};
my $timedatectl = `which timedatectl`;
chomp $timedatectl;
if (!$timedatectl) {
warn "Warning: No time zone found, checked TZ, missing timedatectl";
return 'UTC';
}
my @cmd = ( $timedatectl, '-p', 'Timezone','show');
my ($in, $out, $err);
run3(\@cmd,\$in,\$out,\$err);
my ($tz) = $out =~ /=(.*)/;
chomp $out;
if (!$tz) {
warn "Warning: No timezone found in @cmd\n$out";
return 'UTC'
}
return $tz;
}
sub _sql_insert_values($self, $table, $entry) {
my $sql = "INSERT INTO $table "
."( "
......@@ -1638,11 +1730,21 @@ sub _sql_insert_values($self, $table, $entry) {
}
sub _port_definition($driver, $definition0){
return $definition0 if $driver eq 'mysql';
return $definition0 if $driver =~ /mysql|mariadb/i;
if ($driver eq 'sqlite') {
$definition0 =~ s/(.*) AUTO_INCREMENT$/$1 AUTOINCREMENT/i;
return $definition0 if $definition0 =~ /^(int|integer|char|varchar) /i;
if ($definition0 =~ /^enum /) {
my ($default) = $definition0 =~ / (default.*)/i;
$default = '' if !defined $default;
my @found = $definition0 =~ /'(.*?)'/g;
my ($size) = sort map { length($_) } @found;
return " varchar($size) $default";
}
}
return $definition0;
}
sub _clean_iso_mini {
......@@ -1723,7 +1825,6 @@ sub _upgrade_tables {
#$self->_upgrade_table('domains','display_file','text DEFAULT NULL');
$self->_upgrade_table('domains','info','varchar(255) DEFAULT NULL');
$self->_upgrade_table('domains','internal_id','varchar(64) DEFAULT NULL');
$self->_upgrade_table('domains','id_vm','int default null');
$self->_upgrade_table('domains','volatile_clones','int NOT NULL default 0');
$self->_upgrade_table('domains','comment',"varchar(80) DEFAULT ''");
......@@ -1814,6 +1915,8 @@ sub _connect_dbh {
, PrintError=> 0 });
$con->dbh();
};
$con->dbh->do("PRAGMA foreign_keys = ON") if $driver =~ /sqlite/i;
return $con if $con && !$@;
sleep 1;
warn "Try $try $@\n";
......@@ -1993,7 +2096,7 @@ sub _connect_vm {
sub _create_vm_lxc {
my $self = shift;
return;
return ;
}
sub _create_vm_void {
......@@ -2631,6 +2734,7 @@ sub process_requests {
next if $duplicated{$req->command.":$domain"}++;
push @reqs,($req);
}
$sth->finish;
for my $req (sort { $a->priority <=> $b->priority } @reqs) {
next if $req eq 'refresh_vms' && scalar@reqs > 2;
......@@ -2652,7 +2756,6 @@ sub process_requests {
# sleep 1 if $DEBUG;
}
$sth->finish;
$self->_timeout_requests();
}
......@@ -4702,6 +4805,38 @@ sub import_domain {
sub _cmd_enforce_limits($self, $request=undef) {
_enforce_limits_active($self, $request);
$self->_shutdown_disconnected();
$self->_shutdown_bookings() if $self->setting('/backend/bookings');
}
sub _shutdown_bookings($self) {
my @bookings = Ravada::Booking::bookings();
return if !scalar(@bookings);
my @domains = $self->list_domains_data(status => 'active');
for my $dom ( @domains ) {
next if $dom->{autostart};
next if $self->_user_is_admin($dom->{id_owner});
if ( Ravada::Booking::user_allowed($dom->{id_owner}, $dom->{id_base}) ) {
# warn "\tuser $dom->{id_owner} allowed to start clones from $dom->{id_base}";
next;
}
my $user = Ravada::Auth::SQL->search_by_id($dom->{id_owner});
$user->send_message("The server is booked. Shutting down ".$dom->{name});
Ravada::Request->shutdown_domain(
uid => Ravada::Utils::user_daemon->id
,id_domain => $dom->{id}
);
}
}
sub _user_is_admin($self, $id_user) {
my $sth = $CONNECTOR->dbh->prepare("SELECT is_admin FROM users where id=? ");
$sth->execute($id_user);
my ($is_admin) = $sth->fetchrow;
return $is_admin;
}
sub _enforce_limits_active($self, $request) {
......@@ -4894,31 +5029,8 @@ Returns the value of a configuration setting
=cut
sub setting($self, $name, $new_value=undef) {
my $sth = $CONNECTOR->dbh->prepare(
"SELECT id,value "
." FROM settings "
." WHERE id_parent=? AND name=?"
);
my ($id, $value);
my $id_parent = 0;
for my $item (split m{/},$name) {
next if !$item;
$sth->execute($id_parent, $item);
($id, $value) = $sth->fetchrow;
die "Error: I can-t find setting $item inside id_parent: $id_parent"
if !$id;
$id_parent = $id;
}
if (defined $new_value && $new_value ne $value ) {
$sth = $CONNECTOR->dbh->prepare(
"UPDATE settings set value=? WHERE id=?"
);
$sth->execute($new_value , $id);
return $new_value;
}
return $value;
sub setting {
return Ravada::Front::setting(@_);
}
sub DESTROY($self) {
......
......@@ -193,11 +193,13 @@ sub search_user {
my $ldap = (delete $args{ldap} or _init_ldap_admin());
my $base = (delete $args{base} or _dc_base());
my $typesonly= (delete $args{typesonly} or 0);
my $escape_username = 1;
$escape_username = delete $args{escape_username} if exists $args{escape_username};
confess "ERROR: Unknown fields ".Dumper(\%args) if keys %args;
confess "ERROR: I can't connect to LDAP " if!$ldap;
$username = escape_filter_value($username);
$username = escape_filter_value($username) if $escape_username;
$username =~ s/ /\\ /g;
my $filter = "($field=$username)";
......@@ -250,6 +252,9 @@ Add a group to the LDAP
sub add_group {
my $name = shift;
my $base = (shift or _dc_base());
my $class = ( shift or [
'groupOfUniqueNames','nsMemberOf','posixGroup','top'
]);
$name = escape_filter_value($name);
......@@ -257,17 +262,32 @@ sub add_group {
cn => $name
,dn => "cn=$name,ou=groups,$base"
, attrs => [ cn=>$name
,objectClass => ['groupOfUniqueNames','top']
,objectClass => $class
,ou => 'Groups'
,description => "Group for $name"
,gidNumber => _search_new_gid()
]
);
if ($mesg->code) {
die "Error afegint $name ".$mesg->error;
die "Error creating group $name : ".$mesg->error."\n";
}
}
sub _search_new_gid() {
my %gid;
for my $group ( search_group( name => '*' ) ) {
my $gid_number = $group->get_value('gidNumber');
next if !$gid_number;
$gid{$gid_number}++;
}
my $new_gid = 100;
for (;;) {
return $new_gid if !$gid{$new_gid};
$new_gid++;
}
}
=head2 remove_group
Removes the group from the LDAP directory. Use with caution
......@@ -301,7 +321,7 @@ sub remove_group {
sub search_group {
my %args = @_;
my $name = delete $args{name} or confess "Missing group name";
my $name = delete $args{name};
my $base = ( delete $args{base} or "ou=groups,"._dc_base() );
my $ldap = ( delete $args{ldap} or _init_ldap_admin());
my $retry =( delete $args{retry} or 0);
......@@ -309,9 +329,6 @@ sub search_group {
confess "ERROR: Unknown fields ".Dumper(\%args) if keys %args;
confess "ERROR: I can't connect to LDAP " if!$ldap;
$name = escape_filter_value($name);
my $mesg = $ldap ->search (
filter => "cn=$name"
,base => $base
......@@ -332,7 +349,8 @@ sub search_group {
}
my @entries = $mesg->entries;
return $entries[0]
return @entries if wantarray;
return $entries[0];
}
=head2 add_to_group
......
......@@ -379,7 +379,10 @@ sub is_operator {
|| $self->can_list_clones()
|| $self->can_list_clones_from_own_base()
|| $self->can_list_machines()
|| $self->is_user_manager();
|| $self->is_user_manager()
|| $self->can_view_groups()
|| $self->can_manage_groups()
;
return 0;
}
......
package Ravada::Booking;
use Carp qw(carp croak);
use Data::Dumper;
use DateTime::Format::DateParse;
use Moose;
use Ravada::Booking::Entry;
use Ravada::Front;
no warnings "experimental::signatures";
use feature qw(signatures);
our $CONNECTOR = \$Ravada::CONNECTOR;
our $TZ;
sub BUILD($self, $args) {
return $self->_open($args->{id}) if $args->{id};
my $date = delete $args->{date_booking};
my $date_start = delete $args->{date_start};
my $date_end = delete $args->{date_end};
confess "Error: supply either date or date_start"
if (!defined $date && !defined $date_start);
$date = $date_start if !defined $date;