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

Refactor spinoff (#1237)

refactor(backend): spinoff volumes

Base in frontend actions slightly improved. Need some work though

issue #852
parent 964d6204
......@@ -2967,12 +2967,26 @@ sub _cmd_remove_base {
die "Unknown domain id '$id_domain'\n" if !$domain;
$domain->_vm->disconnect();
$self->_disconnect_vm();
$domain->remove_base($user);
}
sub _cmd_spinoff($self, $request) {
my $id_domain = $request->id_domain or confess "Missing request id_domain";
my $uid = $request->args('uid') or confess "Missing argument uid";
my $user = Ravada::Auth::SQL->search_by_id( $uid);
my $domain = $self->search_domain_by_id($id_domain);
die "Unknown domain id '$id_domain'\n" if !$domain;
$domain->spinoff();
}
sub _cmd_hybernate {
my $self = shift;
......@@ -3574,9 +3588,12 @@ sub _req_method {
,add_disk => \&_cmd_add_disk
,copy_screenshot => \&_cmd_copy_screenshot
,cmd_cleanup => \&_cmd_cleanup
,remove_base => \&_cmd_remove_base
,spinoff => \&_cmd_spinoff
,set_base_vm => \&_cmd_set_base_vm
,remove_base_vm => \&_cmd_set_base_vm
,refresh_vms => \&_cmd_refresh_vms
,ping_backend => \&_cmd_ping_backend
,prepare_base => \&_cmd_prepare_base
......
......@@ -11,7 +11,7 @@ Ravada::Domain - Domains ( Virtual Machines ) library for Ravada
use Carp qw(carp confess croak cluck);
use Data::Dumper;
use File::Copy;
use File::Copy qw(copy move);
use File::Rsync;
use Hash::Util qw(lock_hash unlock_hash);
use Image::Magick;
......@@ -70,8 +70,6 @@ requires 'disk_device';
requires 'disk_size';
requires 'spinoff_volumes';
#hardware info
requires 'get_info';
......@@ -177,6 +175,7 @@ around 'force_shutdown' => \&_around_shutdown_now;
before 'remove_base' => \&_pre_remove_base;
after 'remove_base' => \&_post_remove_base;
after 'spinoff' => \&_post_spinoff;
before 'rename' => \&_pre_rename;
after 'rename' => \&_post_rename;
......@@ -530,7 +529,7 @@ sub _around_add_volume {
my $free = $self->_vm->free_disk();
my $free_out = int($free / 1024 / 1024 / 1024 ) * 1024 *1024 *1024;
die "Error creating volume, out of space $size . Disk free: "
confess "Error creating volume, out of space $size . Disk free: "
.Ravada::Utils::number_to_size($free_out)
."\n"
if exists $args{size} && $args{size} >= $free;
......@@ -605,6 +604,10 @@ sub _around_prepare_base($orig, $self, @args) {
}
$self->_pre_prepare_base($user, $request);
if (!$self->is_local) {
my $vm_local = $self->_vm->new( host => 'localhost' );
$self->_vm($vm_local);
}
my @base_img = $self->$orig($with_cd);
die "Error: No information files returned from prepare_base"
......@@ -678,13 +681,13 @@ sub _pre_prepare_base($self, $user, $request = undef ) {
sleep 1;
}
}
if ($self->id_base ) {
$self->spinoff_volumes();
}
if (!$self->is_local) {
my $vm_local = Ravada::VM->open( type => $self->vm );
$self->migrate($vm_local);
}
if ($self->id_base ) {
$self->spinoff();
}
$self->_check_free_space_prepare_base();
}
......@@ -714,6 +717,32 @@ sub _post_prepare_base {
$self->autostart(0,$user);
};
=pod
=head2 spinoff
Makes volumes indpendent from base
=cut
sub spinoff {
my $self = shift;
$self->_do_force_shutdown() if $self->is_active;
confess "Error: spinoff from remote nodes not available. Node: ".$self->_vm->name
if !$self->is_local;
for my $volume ($self->list_volumes_info ) {
next if !$volume->file || $volume->file =~ /\.iso$/i;
my $bf;
eval { $bf = $volume->backing_file };
die $@ if $@ && $@ !~ /No backing file/;
next if !$bf;
$volume->spinoff;
}
}
sub _around_autostart($orig, $self, @arg) {
my ($value, $user) = @arg;
$self->_allowed($user) if defined $value;
......@@ -1503,7 +1532,7 @@ sub _after_remove_domain {
$self->_remove_domain_cascade($user) if !$cascade;
if ($self->is_known && $self->is_base) {
$self->_do_remove_base($user);
# $self->_do_remove_base($user);
$self->_remove_files_base();
}
$self->_remove_all_volumes();
......@@ -1849,11 +1878,29 @@ sub _do_remove_base($self, $user) {
}
}
$self->is_base(0);
for my $vol ($self->list_volumes_info) {
next if !$vol->file || $vol->file =~ /\.iso$/;
my $backing_file = $vol->backing_file;
next if !$backing_file;
# confess "Error: no backing file for ".$vol->file if !$backing_file;
$vol->block_commit();
unlink $vol->file or die "$! ".$vol->file;
my @stat = stat($backing_file);
move($backing_file, $vol->file) or die "$! $backing_file -> ".$vol->file;
my $mask = oct(7777);
my $mode = $stat[2] & $mask;
my $w = oct(200);
$mode = $mode ^ $w;
chmod($mode,$vol->file);
chown($stat[4],$stat[5], $vol->file);
}
for my $file ($self->list_files_base) {
next if $file =~ /\.iso$/i;
next if ! -e $file;
unlink $file or die "$! unlinking $file";
}
$self->storage_refresh() if $self->storage();
}
......@@ -1864,10 +1911,11 @@ sub _pre_remove_base {
if (!$domain->is_local) {
my $vm_local = $domain->_vm->new( host => 'localhost' );
my $domain_local = $vm_local->search_domain($domain->name);
$domain = $domain_local if $domain_local;
confess "Error: I can't find local virtual manager ".$domain->type
if !$vm_local;
$domain->_vm($vm_local);
}
$domain->spinoff_volumes();
}
sub _post_remove_base {
......@@ -1888,6 +1936,11 @@ sub _remove_all_bases($self) {
}
}
sub _post_spinoff($self) {
my $sth = $$CONNECTOR->dbh->prepare("UPDATE domains set id_base=NULL WHERE id=?");
$sth->execute($self->id);
}
sub _pre_shutdown_domain {}
sub _post_remove_base_domain {}
......@@ -3131,8 +3184,10 @@ sub clean_swap_volumes {
my $self = shift;
for my $vol ( $self->list_volumes_info) {
if ($vol->file && $vol->file =~ /\.SWAP\.\w+$/) {
eval { $vol->backing_file };
my $backing_file;
eval { $backing_file = $vol->backing_file };
confess $@ if $@ && $@ !~ /No backing file/i;
next if !$backing_file;
$vol->restore() if !$@;
}
}
......
......@@ -367,6 +367,8 @@ sub _disk_device($self, $with_info=undef, $attribute=undef, $value=undef) {
}
sub _volume_info($self, $file, $refresh=0) {
confess "Error: No vm connected" if !$self->_vm->vm;
my ($name) = $file =~ m{.*/(.*)};
my $vol;
......@@ -381,7 +383,7 @@ sub _volume_info($self, $file, $refresh=0) {
}
if (!$vol) {
warn "Error: Volume $file not found";
confess "Error: Volume $file not found ".$self->name;
return;
}
......@@ -1491,6 +1493,8 @@ sub disk_size {
=pod
=cut
sub rename_volumes {
my $self = shift;
my $new_dom_name = shift;
......@@ -1530,54 +1534,6 @@ sub rename_volumes {
}
}
=cut
=head2 spinoff_volumes
Makes volumes indpendent from base
=cut
sub spinoff_volumes {
my $self = shift;
$self->_do_force_shutdown() if $self->is_active;
for my $volume ($self->list_disks) {
confess "ERROR: Domain ".$self->name
." volume '$volume' does not exists"
if ! -e $volume;
#TODO check mktemp or something
my $volume_tmp = "$volume.$$.tmp";
unlink($volume_tmp) or die "ERROR $! removing $volume.tmp"
if -e $volume_tmp;
my @cmd = ('qemu-img'
,'convert'
,'-O','qcow2'
,$volume
,$volume_tmp
);
my ($in, $out, $err);
run3(\@cmd,\$in,\$out,\$err);
warn $out if $out;
warn $err if $err;
die "ERROR: Temporary output file $volume_tmp not created at "
.join(" ",@cmd)
.($out or '')
.($err or '')
."\n"
if (! -e $volume_tmp );
copy($volume_tmp,$volume) or die "$! $volume_tmp -> $volume";
unlink($volume_tmp) or die "ERROR $! removing $volume_tmp";
}
}
sub _set_spice_ip($self, $set_password, $ip=undef) {
my $doc = XML::LibXML->load_xml(string
......
......@@ -575,10 +575,6 @@ sub disk_size {
return -s $disk;
}
sub spinoff_volumes {
return;
}
sub ip {
my $self = shift;
my $info = $self->_value('info');
......
......@@ -58,6 +58,7 @@ our %VALID_ARG = (
,open_iptables => $args_manage_iptables
,remove_base => $args_remove_base
,prepare_base => $args_prepare
,spinoff => { id_domain => 1, uid => 1 }
,pause_domain => $args_manage
,resume_domain => {%$args_manage, remote_ip => 1 }
,remove_domain => $args_manage
......
......@@ -1266,6 +1266,7 @@ sub read_file( $self, $file ) {
}
sub _read_file_local( $self, $file ) {
confess "Error: file undefined" if !defined $file;
CORE::open my $in,'<',$file or die "$! $file";
return join('',<$in>);
}
......@@ -1605,7 +1606,7 @@ sub _check_free_disk($self, $size, $storage_pool=undef) {
my $free = $self->free_disk($storage_pool);
my $free_out = int($free / 1024 / 1024 / 1024 ) * 1024 *1024 *1024;
die "Error creating volume, out of space."
confess "Error creating volume, out of space."
." Requested: ".Ravada::Utils::number_to_size($size_out)
." , Disk free: ".Ravada::Utils::number_to_size($free_out)
."\n"
......
......@@ -83,6 +83,8 @@ our $CACHE_DOWNLOAD = 1;
our $VERIFY_ISO = 1;
our %_CREATED_DEFAULT_STORAGE = ();
our $MIN_CAPACITY = 1024 * 10;
##########################################################################
......@@ -721,7 +723,8 @@ sub create_volume {
$img_file);
if ($capacity) {
confess "Size '$capacity' too small" if $capacity< 1024*10;
confess "Size '$capacity' too small, min : $MIN_CAPACITY"
if $capacity< $MIN_CAPACITY;
$doc->findnodes('/volume/allocation/text()')->[0]->setData(int($allocation));
$doc->findnodes('/volume/capacity/text()')->[0]->setData($capacity);
}
......
......@@ -10,6 +10,7 @@ use feature qw(signatures);
requires 'clone';
requires 'backing_file';
requires 'prepare_base';
requires 'spinoff';
around 'prepare_base' => \&_around_prepare_base;
around 'clone' => \&_around_clone;
......
......@@ -24,7 +24,7 @@ sub capacity($self) {
}
sub backing_file($self) {
return $self->file;
return;
}
sub clone($self, $filename) {
......@@ -41,4 +41,8 @@ sub base_filename($self) {
return $self->file;
}
sub spinoff($self) {
confess "Error: ISO files can't be spinned off";
}
1;
package Ravada::Volume::QCOW2;
use Data::Dumper;
use Moose;
extends 'Ravada::Volume';
......@@ -96,7 +97,6 @@ sub backing_file($self) {
die $err if $err;
my ($base) = $out =~ m{^backing file: (.*)}mi;
confess "No backing file for ".$self->file." in $out" if !$base;
return $base;
}
......@@ -108,4 +108,44 @@ sub rebase($self, $new_base) {
}
sub spinoff($self) {
my $file = $self->file;
my $volume_tmp = $self->file.".$$.tmp";
$self->vm->remove_file($volume_tmp);
my @cmd = ($QEMU_IMG
,'convert'
,'-O','qcow2'
,$file
,$volume_tmp
);
my ($out, $err) = $self->vm->run_command(@cmd);
warn $out if $out;
warn $err if $err;
confess "ERROR: Temporary output file $volume_tmp not created at "
.join(" ",@cmd)
.($out or '')
.($err or '')
."\n"
if (! $self->vm->file_exists($volume_tmp) );
$self->copy_file($volume_tmp,$file) or die "$! $volume_tmp -> $file";
$self->vm->remove_file($volume_tmp) or die "ERROR $! removing $volume_tmp";
}
sub block_commit($self) {
my @cmd = ($QEMU_IMG,'commit','-q','-d');
my ($out, $err) = $self->vm->run_command(@cmd, $self->file);
warn $err if $err;
for (;;) {
return if !-e $self->file;
my $t0 = time;
my @stat = stat($self->file);
my $mtime = $stat[9];
return if $t0 - $mtime > 0;
sleep 1;
}
}
1;
......@@ -44,8 +44,8 @@ sub _load($self) {
return Load($self->vm->read_file($self->file));
}
sub _save($self, $data) {
$self->vm->write_file($self->file, Dump($data));
sub _save($self, $data, $file = $self->file) {
$self->vm->write_file($file, Dump($data));
}
sub clone($self, $clone_file) {
......@@ -67,8 +67,7 @@ sub clone($self, $clone_file) {
sub backing_file($self) {
my $data = $self->_load();
my $backing_file = $data->{backing_file}
or confess "Error: No backing file from ".Dumper($data);
return ( $data->{backing_file} or undef);
}
sub rebase($self, $file) {
......@@ -77,4 +76,30 @@ sub rebase($self, $file) {
$self->_save($data);
}
sub spinoff($self) {
my $data = $self->_load();
confess "Error: no backing file ".Dumper($self->file,$data)
if !$self->backing_file;
my $data_bf = Load($self->vm->read_file($self->backing_file));
for my $key (keys %$data_bf) {
next if $key =~ /^(origin|capacity|is_base)$/;
$data->{$key} = $data_bf->{$key} unless exists $data->{$key};
}
delete $data->{backing_file};
$self->_save($data);
}
sub block_commit($self) {
my $data = $self->_load();
confess "Error: no backing file ".Dumper($self->file,$data)
if !$self->backing_file;
my $data_bf = Load($self->vm->read_file($self->backing_file));
for my $key (keys %$data) {
next if $key =~ /^(origin|capacity|is_base|backing_file)$/;
$data_bf->{$key} = $data->{$key};
}
$self->_save($data_bf, $self->backing_file);
}
1;
......@@ -10,7 +10,7 @@ use Hash::Util qw(lock_hash unlock_hash);
use IPC::Run3 qw(run3);
use Mojo::File 'path';
use Test::More;
use YAML qw(LoadFile DumpFile);
use YAML qw(Load LoadFile Dump DumpFile);
use feature qw(signatures);
no warnings "experimental::signatures";
......@@ -67,6 +67,9 @@ create_domain
mojo_request
remove_old_user
mangle_volume
test_volume_contents
);
our $DEFAULT_CONFIG = "t/etc/ravada.conf";
......@@ -104,6 +107,12 @@ our %VM_VALID = ( KVM => 1
our @NODES;
my $URL_LOGOUT = '/logout';
my $MOD_NBD= 0;
my $DEV_NBD = "/dev/nbd10";
my $MNT_RVD= "/mnt/test_rvd";
my $QEMU_NBD = `which qemu-nbd`;
chomp $QEMU_NBD;
sub user_admin {
return $USER_ADMIN if $USER_ADMIN;
......@@ -1631,4 +1640,140 @@ sub create_storage_pool($vm) {
}
sub mangle_volume($vm,$name,@vol) {
for my $file (@vol) {
if ($file =~ /\.void$/) {
my $data = Load($vm->read_file($file));
$data->{$name} = "c" x 20;
$vm->write_file($file, Dump($data));
} elsif ($file =~ /\.qcow2$/) {
_mount_qcow($vm, $file);
open my $out,">","/mnt/test_rvd/$name";
print $out ("c" x 20)."\n";
close $out;
_umount_qcow();
} else {
confess "Error: I don't know how to mangle volume $file";
}
}
}
sub _mount_qcow($vm, $vol) {
my ($in,$out, $err);
if (!$MOD_NBD++) {
my @cmd =("/sbin/modprobe","nbd", "max_part=63");
run3(\@cmd, \$in, \$out, \$err);
die join(" ",@cmd)." : $? $err" if $?;
}
$vm->run_command($QEMU_NBD,"-d", $DEV_NBD);
for ( 1 .. 10 ) {
($out, $err) = $vm->run_command($QEMU_NBD,"-c",$DEV_NBD, $vol);
last if !$err || $err !~ /NBD socket/;
sleep 1;
}
confess "qemu-nbd -c $DEV_NBD $vol\n?:$?\n$out\n$err" if $? || $err;
_create_part($DEV_NBD);
($out, $err) = $vm->run_command("/sbin/mkfs.ext4","${DEV_NBD}p1");
die "Error on mkfs $err" if $?;
mkdir "$MNT_RVD" if ! -e $MNT_RVD;
$vm->run_command("/bin/mount","${DEV_NBD}p1",$MNT_RVD);
exit if $?;
}
sub _create_part($dev) {
my @cmd = ("/sbin/fdisk","-l",$dev);
my ($in,$out, $err);
for my $retry ( 1 .. 10 ) {
run3(\@cmd, \$in, \$out, \$err);
last if !$err && $err =~ /(Input\/output error|Unexpected end-of-file)/i;
warn $err if $err && $retry>2;
sleep 1;
}
confess join(" ",@cmd)."\n$?\n$out\n$err\n" if $err || $?;
return if $out =~ m{/dev/\w+\d+p\d+}mi;
for (1 .. 10) {
@cmd = ("/sbin/fdisk",$dev);
$in = "n\np\n1\n\n\n\nw\np\n";
run3(\@cmd, \$in, \$out, \$err);
chomp $err;
last if !$err || $err !~ /evice.*busy/;
diag($err." retrying");
sleep 1;
}
ok(!$err) or die join(" ",@cmd)."\n$?\nIN: $in\nOUT:\n$out\nERR:\n$err";
}
sub _umount_qcow() {
mkdir $MNT_RVD if ! -e $MNT_RVD;
my @cmd = ("umount",$MNT_RVD);
my ($in, $out, $err);
for ( ;; ) {
run3(\@cmd, \$in, \$out, \$err);
last if $err !~ /busy/i || $err =~ /not mounted/;
sleep 1;
}
die $err if $err && $err !~ /busy/ && $err !~ /not mounted/;
`qemu-nbd -d $DEV_NBD`;
}
sub _mangle_vol2($vm,$name,@vol) {
for my $file (@vol) {
if ($file =~ /\.void$/) {
my $data = Load($vm->read_file($file));
$data->{$name} = "c" x 20;
$vm->write_file($file, Dump($data));
} elsif ($file =~ /\.qcow2$/) {
_mount_qcow($vm, $file);
open my $out,">","/mnt/test_rvd/$name";
print $out ("c" x 20)."\n";