Project

General

Profile

Download (260 KB) Statistics
| Branch: | Revision:
1
#!/usr/bin/perl
2

    
3
# All rights reserved and Copyright (c) 2020 Origo Systems ApS.
4
# This file is provided with no warranty, and is subject to the terms and conditions defined in the license file LICENSE.md.
5
# The license file is part of this source code package and its content is also available at:
6
# https://www.origo.io/info/stabiledocs/licensing/stabile-open-source-license
7

    
8
package Stabile::Images;
9

    
10
use Error qw(:try);
11
use File::Basename;
12
use Data::UUID;
13
use Proc::Daemon;
14
use Time::Local;
15
#use Time::HiRes qw( time );
16
use Date::Format;
17
use Date::Parse;
18
use Getopt::Std;
19
#use Encode::Escape;
20
use String::Escape;
21
use File::Glob qw(bsd_glob);
22
use Sys::Guestfs;
23
use Data::Dumper;
24
use XML::Simple;
25
#use POSIX qw(strftime);
26
use Time::Piece;
27
use Config::Simple;
28
use lib dirname (__FILE__); # Allows us to source libraries from current directory no matter where we are called from
29
use Stabile;
30

    
31
$\ = ''; # Some of the above seems to set this to \n, resulting in every print appending a line feed
32

    
33
# Read in some settings from config
34
$backupdir = $Stabile::config->get('STORAGE_BACKUPDIR') || "/mnt/stabile/backups";
35
$backupdir = $1 if ($backupdir =~ /(.+)/); #untaint
36
my $tenders = $Stabile::config->get('STORAGE_POOLS_ADDRESS_PATHS');
37
my @tenderlist = split(/,\s*/, $tenders);
38
my $tenderpaths = $Stabile::config->get('STORAGE_POOLS_LOCAL_PATHS') || "/mnt/stabile/images";
39
my @tenderpathslist = split(/,\s*/, $tenderpaths);
40
my $tendernames = $Stabile::config->get('STORAGE_POOLS_NAMES') || "Standard storage";
41
my @tendernameslist = split(/,\s*/, $tendernames);
42
my $mountabletenders = $Stabile::config->get('STORAGE_POOLS_MOUNTABLE');
43
my @mountabletenderslist = split(/,\s*/, $mountabletenders);
44
my $storagepools = $Stabile::config->get('STORAGE_POOLS_DEFAULTS') || "0";
45
my $spoolsrdiffenabled = $Stabile::config->get('STORAGE_POOLS_RDIFF-BACKUP_ENABLED') || "0";
46
my @rdiffenabledlist = split(/,\s*/, $spoolsrdiffenabled);
47
my $rdiffenabled = $Stabile::config->get('RDIFF-BACKUP_ENABLED') || "0";
48
my $userrdiffenabled = $Stabile::config->get('RDIFF-BACKUP_USERS') || "0";
49
my $nodestorageovercommission = $Stabile::config->get('NODE_STORAGE_OVERCOMMISSION') || "1";
50
my $engineid = $Stabile::config->get('ENGINEID') || "";
51

    
52
my $valve_readlimit = $Stabile::config->get('VALVE_READ_LIMIT'); # e.g. 125829120 = 120 * 1024 * 1024 = 120 MB / s
53
my $valve_writelimit = $Stabile::config->get('VALVE_WRITE_LIMIT');
54
my $valve_iopsreadlimit = $Stabile::config->get('VALVE_IOPS_READ_LIMIT'); # e.g. 1000 IOPS
55
my $valve_iopswritelimit = $Stabile::config->get('VALVE_IOPS_WRITE_LIMIT');
56

    
57
my $valve001id = '995e86b7-ae85-4ae0-9800-320c1f59ae33';
58
my $stackspool = '/mnt/stabile/images001';
59

    
60
our %ahash; # A hash of accounts and associated privileges current user has access to
61
#our %options=();
62
# -a action -h help -f full list -p full update -u uuid -i image -m match pattern -k keywords -g args to gearman task
63
# -v verbose, include HTTP headers -s impersonate subaccount -t target [uuid or image]
64
#Getopt::Std::getopts("a:hfpu:i:g:m:k:vs:t:", \%options);
65

    
66
try {
67
    Init(); # Perform various initalization tasks
68
    process() if ($package); # Parse and process request. $package is not set if called as a library
69

    
70
} catch Error with {
71
    my $ex = shift;
72
    print header('text/html', '500 Internal Server Error') unless ($console);
73
    if ($ex->{-text}) {
74
        print "Got error: ", $ex->{-text}, " on line ", $ex->{-line}, "\n";
75
    } else {
76
        print "Status=ERROR\n";
77
    }
78
} finally {
79
};
80

    
81
1;
82

    
83
sub Init {
84

    
85
    # Tie database tables to hashes
86
    unless ( tie(%userreg,'Tie::DBI', Hash::Merge::merge({table=>'users', key=>'username', CLOBBER=>1}, $Stabile::dbopts)) ) {return "Unable to access user register"};
87
    unless ( tie(%register,'Tie::DBI', Hash::Merge::merge({table=>'images', key=>'path'}, $Stabile::dbopts)) ) {return "Unable to access image register"};
88
    unless ( tie(%networkreg,'Tie::DBI', Hash::Merge::merge({table=>'networks'}, $Stabile::dbopts)) ) {return "Unable to access network register"};
89
    unless ( tie(%imagereg,'Tie::DBI', Hash::Merge::merge({table=>'images', CLOBBER=>1}, $Stabile::dbopts)) ) {return "Unable to access image uuid register"};
90
    unless ( tie(%domreg,'Tie::DBI', Hash::Merge::merge({table=>'domains'}, $Stabile::dbopts)) ) {return "Unable to access domain register"};
91

    
92
    # simplify globals initialized in Stabile.pm
93
    $tktuser = $tktuser || $Stabile::tktuser;
94
    $user = $user || $Stabile::user;
95
    $isadmin = $isadmin || $Stabile::isadmin;
96
    $sshcmd = $sshcmd || $Stabile::sshcmd;
97
    $disablesnat = $disablesnat || $Stabile::disablesnat;
98

    
99
    # Create aliases of functions
100
    *header = \&CGI::header;
101

    
102
    *Getimagesdevice = \&Liststoragedevices;
103
    *Getbackupdevice = \&Liststoragedevices;
104
    *Listimagesdevices = \&Liststoragedevices;
105
    *Listbackupdevices = \&Liststoragedevices;
106
    *Rebase = \&Unmaster;
107

    
108
    *do_save = \&privileged_action_async;
109
    *do_sync_save = \&privileged_action;
110
    *do_sync_backup = \&privileged_action;
111
    *do_sync_clone = \&privileged_action;
112
    *do_updateregister = \&action;
113
    *do_fullupdateregister = \&action;
114
    *do_tablelistall = \&do_list;
115
    *do_tablelist = \&do_list;
116
    *Sync_save = \&Save;
117
    *Sync_backup = \&Backup;
118
    *Sync_clone = \&Clone;
119
    *do_help = \&action;
120

    
121
    *do_mount = \&privileged_action;
122
    *do_unmount = \&privileged_action;
123
    *do_convert = \&privileged_action;
124
    *do_activate = \&privileged_action;
125
    *do_publish = \&privileged_action;
126
    *do_uploadtoregistry = \&privileged_action;
127
    *do_release = \&privileged_action;
128
    *do_download = \&privileged_action;
129
    *do_linkmaster = \&privileged_action;
130
    *do_listbackups = \&privileged_action;
131
    *do_listcdroms = \&action;
132
    *do_listfiles = \&privileged_action;
133
    *do_getserverbackups = \&privileged_action;
134
    *do_listserverbackups = \&privileged_action;
135
    *Listserverbackups = \&Getserverbackups;
136
    *do_restorefiles = \&privileged_action;
137
    *do_remove = \&privileged_action;
138
    *do_removeuserimages = \&privileged_action;
139
    *do_updatedownloads = \&privileged_action;
140
    *do_master = \&privileged_action_async;
141
    *do_unmaster = \&privileged_action_async;
142
    *do_rebase = \&privileged_action_async;
143
    *do_clone = \&privileged_action_async;
144
    *do_snapshot = \&privileged_action_async;
145
    *do_unsnap = \&privileged_action_async;
146
    *do_revert = \&privileged_action_async;
147
    *do_inject = \&privileged_action_async;
148
    *do_backup = \&privileged_action_async;
149
    *do_zbackup = \&privileged_action;
150
    *do_restore = \&privileged_action_async;
151
    *do_updatebackingfile = \&privileged_action;
152
    *do_updatebtime = \&privileged_action;
153
    *do_updateallbtimes = \&privileged_action;
154
    *do_initializestorage = \&privileged_action;
155
    *do_liststoragedevices = \&privileged_action;
156
    *do_listimagesdevices = \&privileged_action;
157
    *do_listbackupdevices = \&privileged_action;
158
    *do_getimagesdevice = \&privileged_action;
159
    *do_getbackupdevice = \&privileged_action;
160
    *do_setstoragedevice = \&privileged_action;
161
    *do_backupfuel = \&privileged_action;
162

    
163
    *do_gear_save = \&do_gear_action;
164
    *do_gear_sync_save = \&do_gear_action;
165
    *do_gear_sync_backup = \&do_gear_action;
166
    *do_gear_sync_clone = \&do_gear_action;
167
    *do_gear_mount = \&do_gear_action;
168
    *do_gear_unmount = \&do_gear_action;
169
    *do_gear_convert = \&do_gear_action;
170
    *do_gear_activate = \&do_gear_action;
171
    *do_gear_publish = \&do_gear_action;
172
    *do_gear_uploadtoregistry = \&do_gear_action;
173
    *do_gear_release = \&do_gear_action;
174
    *do_gear_download = \&do_gear_action;
175
    *do_gear_linkmaster = \&do_gear_action;
176
    *do_gear_listbackups = \&do_gear_action;
177
    *do_gear_listserverbackups = \&do_gear_action;
178
    *do_gear_getserverbackups = \&do_gear_action;
179
    *do_gear_listfiles = \&do_gear_action;
180
    *do_gear_restorefiles = \&do_gear_action;
181
    *do_gear_remove = \&do_gear_action;
182
    *do_gear_removeuserimages = \&do_gear_action;
183
    *do_gear_updatedownloads = \&do_gear_action;
184
    *do_gear_master = \&do_gear_action;
185
    *do_gear_unmaster = \&do_gear_action;
186
    *do_gear_rebase = \&do_gear_action;
187
    *do_gear_clone = \&do_gear_action;
188
    *do_gear_snapshot = \&do_gear_action;
189
    *do_gear_unsnap = \&do_gear_action;
190
    *do_gear_revert = \&do_gear_action;
191
    *do_gear_inject = \&do_gear_action;
192
    *do_gear_backup = \&do_gear_action;
193
    *do_gear_zbackup = \&do_gear_action;
194
    *do_gear_restore = \&do_gear_action;
195
    *do_gear_updatebackingfile = \&do_gear_action;
196
    *do_gear_updatebtime = \&do_gear_action;
197
    *do_gear_updateallbtimes = \&do_gear_action;
198
    *do_gear_initializestorage = \&do_gear_action;
199
    *do_gear_liststoragedevices = \&do_gear_action;
200
    *do_gear_listimagesdevices = \&do_gear_action;
201
    *do_gear_listbackupdevices = \&do_gear_action;
202
    *do_gear_getimagesdevice = \&do_gear_action;
203
    *do_gear_getbackupdevice = \&do_gear_action;
204
    *do_gear_setstoragedevice = \&do_gear_action;
205
    *do_gear_backupfuel = \&do_gear_action;
206

    
207
    *Fullupdateregister = \&Updateregister;
208

    
209
    @users; # global
210
    if ($fulllist) {
211
        @users = keys %userreg;
212
        push @users, "common";
213
    } else {
214
        @users = ($user, "common");
215
    }
216

    
217
    untie %userreg;
218

    
219
#    my $mounts = decode('ascii-escape', `/bin/cat /proc/mounts`);
220
    my $mounts = `/bin/cat /proc/mounts`;
221
    @spools;
222

    
223
    # Enumerate and define the storage pools a user has access to
224
    my @spl = split(/,\s*/, $storagepools);
225
    my $reloadnfs;
226
    foreach my $p (@spl) {
227
        if ($tenderlist[$p] && $tenderpathslist[$p] && $tendernameslist[$p]) {
228
            my $rd = (defined $rdiffenabledlist[$p])?$rdiffenabledlist[$p]:"$rdiffenabledlist[0]";
229
            my %pool = ("hostpath", $tenderlist[$p],
230
                "path", $tenderpathslist[$p],
231
                "name", $tendernameslist[$p],
232
                "rdiffenabled", $rd,
233
                "mountable", ($tenderlist[$p] eq 'local') || $mountabletenderslist[$p] || '0', # local pools always mountable
234
                "lvm", 0+($tenderlist[$p] eq 'local' && ($mounts =~ m/\/dev\/mapper\/(\S+)-(\S+) $tenderpathslist[$p].+/g) ),
235
                "zfs", (($mounts =~ /(\S+) $tenderpathslist[$p] zfs/)?$1:''),
236
                "id", $p);
237
            $spools[$p] = \%pool;
238

    
239
            # Directory / mount point must exist
240
            unless (-d $tenderpathslist[$p]) {return "Status=Error $tenderpathslist[$p] could not be accessed"};
241

    
242
            # TODO: This section should be moved to pressurecontrol
243
            if ($tenderlist[$p] eq "local") {
244
                my $lpath = $tenderpathslist[$p];
245
                `mkdir "$lpath"` unless (-e $lpath);
246
                unless (`grep "$lpath 10" /etc/exports`) {
247
                    `echo "$lpath 10.0.0.0/255.255.255.0(sync,no_subtree_check,no_root_squash,rw)" >> /etc/exports`;
248
                    $reloadnfs = 1;
249
                }
250
            } elsif ($mounts =~ m/$tenderpathslist[$p]/i) {
251
                ; # do nothing
252
            } else {
253
                $main::syslogit->($user, 'info', "Mounting $tenderpathslist[$p] from $tenderlist[$p]");
254
                eval {
255
                    system("/bin/mount -o intr,noatime,nfsvers=3 $tenderlist[$p] $tenderpathslist[$p]");
256
                    1;
257
                } or {return "Status=Error $tenderpathslist[$p] could not be mounted"};
258
            }
259

    
260
            # Create user dir if it does not exist
261
            unless(-d "$tenderpathslist[$p]/$user"){
262
                umask "0000";
263
                mkdir "$tenderpathslist[$p]/$user" or {return "Status=Cannot create user dir for $user in  $tenderpathslist[$p]"};
264
            }
265
            unless(-d "$tenderpathslist[$p]/common"){
266
                umask "0000";
267
                mkdir "$tenderpathslist[$p]/common" or {return "Status=Cannot create common dir for $user in $tenderpathslist[$p]"};
268
            }
269
        }
270
    }
271
    `/usr/sbin/exportfs -r` if ($reloadnfs); #Reexport nfs shares
272

    
273
    # Create user's backupdir if it does not exist
274
    unless(-d "$backupdir/$user"){
275
        umask "0000";
276
        mkdir "$backupdir/$user" or {$postreply .= "Status=ERROR No backup dir $backupdir/$user\n"};
277
    }
278

    
279
}
280

    
281
sub getObj {
282
    my %h = %{@_[0]};
283
    my $status = $h{"status"};
284
    $console = 1 if $h{"console"};
285
    $api = 1 if $h{"api"};
286
    my $obj;
287
    $action = $action || $h{'action'};
288
    if (
289
        $action =~ /^clone|^sync_clone|^removeuserimages|^gear_removeuserimages|^activate|^gear_activate|^publish|uploadtoregistry|^release|^download|^gear_publish/
290
        || $action =~ /^gear_release|zbackup|setimagesdevice|setbackupdevice|initializestorage|setstoragedevice|backupfuel|sync_backup|overquota|^move/
291

    
292
    ) {
293
        $obj = \%h;
294
        return $obj;
295
    }
296
    my $uuid = $h{"uuid"};
297
    if ($uuid && $uuid =~ /^\// ) { # Ugly clutch
298
        $uuid = $register{$uuid}->{'uuid'};
299
    }
300
    if ($uuid eq 'this' && $curimg
301
        && ($register{$curimg}->{'user'} eq $user || $isadmin )) { # make an ugly exception
302
        $uuid = $register{$curimg}->{'uuid'};
303
    }
304
    my $objaction = lc $h{"action"};
305
    $status = "new" unless ($status || $h{'path'} || $uuid || $action eq 'inject');
306
    if ($status eq "new") {
307
        $objaction = "";
308
    }
309
    if (!$uuid && $register{$h{'path'}} && ( $register{$h{'path'}}->{'user'} eq $user || $isadmin )) {
310
        $uuid = $register{$h{'path'}}->{'uuid'};
311
    }
312
    my $img = $imagereg{$uuid};
313
    $status = $img->{'status'} if ($imagereg{$uuid});
314
    if ($objaction eq 'buildsystem' && !$uuid && $h{'master'}) { # make another exception
315
        my $master = $h{'master'};
316
        foreach my $p (@spools) {
317
            my $dir = $p->{'path'};
318
            if ($master =~ /^$dir\/(common|$user)\/.+/ && $register{$master}) { # valid master image
319
                $uuid = $register{$master}->{'uuid'};
320
                last;
321
            }
322
            elsif ($register{"$dir/common/$master"}) { # valid master image
323
                $uuid = $register{"$dir/$user/$master"}->{'uuid'};
324
                last;
325
            }
326
            elsif ($register{"$dir/$user/$master"}) { # valid master image
327
                $uuid = $register{"$dir/$user/$master"}->{'uuid'};
328
                last;
329
            }
330
        }
331
    }
332
    my $path = '';
333
    $path = $img->{'path'} unless ($status eq "new"); # Only trust path from db /co
334
    my $dbobj = $register{$path} || {};
335
    return 0 unless (($path && $dbobj->{'user'} eq $user) || $isadmin || $status eq "new"); # Security check
336

    
337
    unless (($uuid && $imagereg{$uuid} && $status ne 'new') || ($status eq 'new' && !$imagereg{$uuid} && (!$uuid || length($uuid) == 36))) {
338
        $postreply .= "Status=ERROR Invalid image " . (($uuid)?" uuid: $uuid":"") . (($path)?" path: $path":"") . "\n";
339
        return 0;
340
    }
341
    if ($isadmin && $h{"status"}) {
342
        $status = $h{"status"} unless ($status eq "new");
343
    } else {
344
        $status = $dbobj->{'status'} unless ($status eq "new"); # Read status from db for existing images
345
    }
346
    my $virtualsize = $h{"virtualsize"} || $dbobj->{'virtualsize'};
347
    # allow shorthand size specifications
348
    $virtualsize = 1024 * $virtualsize if ($virtualsize =~ /k$/i);
349
    $virtualsize = 1024*1024* $virtualsize if ($virtualsize =~ /m$/i);
350
    $virtualsize = 1024*1024*1024* $virtualsize if ($virtualsize =~ /g$/i);
351
    $virtualsize = 10737418240 if ($status eq 'new' && !$virtualsize); # 10 GB
352

    
353
    $obj = {
354
        path           => $path,
355
        uuid           => $uuid,
356
        status         => $status,
357
        name           => $h{"name"} || $dbobj->{'name'}, # || 'New Image',
358
        size           => $h{"size"} || $dbobj->{'size'},
359
        realsize       => $dbobj->{'realsize'} || 0,
360
        virtualsize    => $virtualsize,
361
        ksize          => int($virtualsize / 1024),
362
        msize          => int($virtualsize / (1024 * 1024)),
363
        type           => $h{"type"} || $dbobj->{'type'} || 'qcow2',
364
        user           => $h{"user"} || $dbobj->{'user'},
365
        reguser        => $dbobj->{'user'},
366
        master         => $dbobj->{'master'},
367
        regstoragepool => $dbobj->{'storagepool'},
368
        storagepool   => (!$h{"storagepool"} && $h{"storagepool"} ne "0") ? $dbobj->{'storagepool'} : $h{"storagepool"},
369
        bschedule      => $h{"bschedule"} || $dbobj->{'bschedule'},
370
        notes          => $h{"notes"},
371
        installable    => ($installable && $installable ne "false") ? "true" : $h{"installable"},
372
        snap1          => $dbobj->{'snap1'},
373
        managementlink => $h{"managementlink"} || $dbobj->{'managementlink'},
374
        upgradelink    => $h{"upgradelink"} || $dbobj->{'upgradelink'},
375
        terminallink   => $h{"terminallink"} || $dbobj->{'terminallink'},
376
        image2         => $h{"image2"} || $dbobj->{'image2'},
377
        mac            => $h{"mac"} || $dbobj->{'mac'},
378
        backup         => $h{"backup"} || '',
379
        domains        => $dbobj->{'domains'} || '--',
380
        domainnames    => $dbobj->{'domainnames'} || '--'
381
    };
382
    # Handle restore of files
383
    $obj->{'restorepath'} = $h{'restorepath'} if ($h{'restorepath'});
384
    $obj->{'files'} = $h{'files'} if ($h{'files'});
385
    $obj->{'sync'} = 1 if ($h{'sync'});
386
    # For backup
387
    $obj->{'skipzfs'} = 1 if ($h{'skipzfs'});
388

    
389
    # Sanity checks
390
    if (
391
        ($obj->{name} && length $obj->{name} > 255)
392
            || ($obj->{virtualsize} && ($obj->{virtualsize}<1024 || $obj->{virtualsize} >1024**5))
393
            || ($obj->{master} && length $obj->{master} > 255)
394
            || ($obj->{bschedule} && length $obj->{bschedule} > 255)
395
            || ($path && length $path > 255)
396
            || ($obj->{image2} && length $obj->{image2} > 255)
397
    ) {
398
        $postreply .= "Status=ERROR Bad image data for: $obj->{name}\n";
399
        return 0;
400
    }
401
    # Security check
402
    if (($user ne $obj->{reguser} && $objaction ne 'clone' && $objaction ne 'buildsystem' && !$isadmin && $objaction))
403
    {
404
        $postreply .= "Status=ERROR No privs\n";
405
        return 0;
406
    }
407
    if ($status eq "new" && ($obj->{reguser} || -e $path)) {
408
        $postreply .= "Status=ERROR Image \"$obj->{name}\" does already exist in $path\n";
409
        return 0;
410
    }
411
    if (!$path && $status ne "new") {
412
        $postreply .= "Status=ERROR Image $obj->{name} not found\n";
413
        return 0;
414
    }
415
    return $obj;
416
}
417

    
418
sub createNodeTask {
419
    my ($mac, $newtask, $status, $wake, $path) = @_;
420
    unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac'}, $Stabile::dbopts)) )
421
        {$postreply .= "Status=Error Node register could not be accessed"};
422

    
423
    if ($status eq "active" && $nodereg{$mac}->{'stor'} ne 'lvm') {
424
     #   $postreply .= "Status=Error Node $mac is not using LVM, unable to backup active image\n";
425
     #   $main::updateUI->({tab=>"images", user=>$user, type=>"update", path=>$path, status=>$status, message=>"Image (on node) is not on an LVM partition - suspend before backing up"});
426
        return "node is is not using LVM, unable to backup active image.";
427
    } elsif ($nodereg{$mac}->{'status'} =~ /asleep|inactive/  && !$wake) {
428
    #    $postreply .= "Status=Error Node $mac is asleep, not waking\n";
429
        return "node is asleep, please wake first!";
430
    } else {
431
        my $tasks = $nodereg{$mac}->{'tasks'};
432
        $nodereg{$mac}->{'tasks'} = $tasks . "$newtask\n";
433
        tied(%nodereg)->commit;
434
    }
435
    untie %nodereg;
436
    return 0;
437
}
438

    
439
sub Recurse {
440
	my($path) = shift; # @_;
441
	my @files;
442
	## append a trailing / if it's not there
443
	$path .= '/' if($path !~ /\/$/);
444
	## loop through the files contained in the directory
445
	for my $eachFile (bsd_glob($path.'*')) {
446
	    next if ($eachFile =~ /\/fuel$/);
447
		## if the file is a directory
448
		if( -d $eachFile) {
449
			## pass the directory to the routine ( recursion )
450
			push(@files,Recurse($eachFile));
451
		} else {
452
			push(@files,$eachFile);
453
		}
454
	}
455
	return @files;
456
}
457

    
458
# If used with the -f switch ($fulllist) from console, all users images are updated in the db
459
# If used with the -p switch ($fullupdate), also updates status information (ressource intensive - runs through all domains)
460
sub Updateregister {
461
    my ($spath, $action) = @_;
462
    if ($help) {
463
        return <<END
464
GET:image,uuid:
465
If used with the -f switch ($fulllist) from console, all users images are updated in the db.
466
If used with the -p switch ($fullupdate), also updates status information (ressource intensive - runs through all domains)
467
Only images on shared storage are updated, images on node storage are handled on the node.
468
END
469
    }
470
    return "Status=ERROR You must be an admin to do this!\n" unless ($isadmin);
471
    $fullupdate = 1 if ((!$fullupdate && $params{'fullupdate'}) || $action eq 'fullupdateregister');
472
    my $force = $params{'force'};
473
    my %userregister;
474
    my $res;
475
    # Update size information in db
476
    foreach my $u (@users) {
477
        foreach my $spool (@spools) {
478
            my $pooldir = $spool->{"path"};
479
            my $dir = "$pooldir/$u";
480
            my @thefiles = Recurse($dir);
481
            foreach my $f (@thefiles) {
482
                next if ($spath && $spath ne $f); # Only specific image being updated
483
                if ($f =~ /(.+)(-s\d\d\d\.vmdk$)/) {
484
                #   `touch "$1.vmdk" 2>/dev/null` unless -e "$1.vmdk";
485
                } elsif ($f =~ /(.+)(-flat\.vmdk$)/) {
486
                #    `touch "$1.vmdk" 2>/dev/null` unless -e "$1.vmdk";
487
                } elsif(-s $f && $f =~ /(\.vmdk$)|(\.img$)|(\.vhd$)|(\.vhdx$)|(\.qcow$)|(\.qcow2$)|(\.vdi$)|(\.iso$)/i) {
488
                    my($fname, $dirpath, $suffix) = fileparse($f, ("vmdk", "img", "vhd", "vhdx", "qcow", "qcow2", "vdi", "iso"));
489
                    my $uuid;
490
                    my $img = $register{$f};
491
                    $uuid = $img->{'uuid'};
492
            # Create a new uuid if we are dealing with a new file in the file-system
493
                    if (!$uuid) {
494
                        my $ug = new Data::UUID;
495
                        $uuid = $ug->create_str();
496
                    }
497
                    my $storagepool = $spool->{"id"};
498
            # Deal with sizes
499
                    my ($newmtime, $newbackupsize, $newsize, $newrealsize, $newvirtualsize) =
500
                        getSizes($f, $img->{'mtime'}, $img->{'status'}, $u, $force);
501
                    my $size = $newsize || $img->{'size'};
502
                    my $realsize = $newrealsize || $img->{'realsize'};
503
                    my $virtualsize = $newvirtualsize || $img->{'virtualsize'};
504
                    my $mtime = $newmtime || $img->{'mtime'};
505
                    my $created = $img->{'created'} || $mtime;
506
                    my $name = $img->{'name'} || substr($fname,0,-1);
507
                    $register{$f} = {
508
                        path=>$f,
509
                        user=>$u,
510
                        type=>$suffix,
511
                        size=>$size,
512
                        realsize=>$realsize,
513
                        virtualsize=>$virtualsize,
514
                        backupsize=>$newbackupsize,
515
                        name=>$name,
516
                        uuid=>$uuid,
517
                    #    domains=>$domains,
518
                    #    domainnames=>$domainnames,
519
                        storagepool=>$storagepool,
520
                        backup=>"", # Only set in uservalues at runtime
521
                        created=>$created,
522
                        mtime=>$mtime
523
                    };
524
                #    $postreply .= "Status=OK $f, $size, $newbackupsize\n" if ($console);
525
                }
526
            }
527
        }
528
    }
529
    # Update status information in db
530
#    my $mounts = decode('ascii-escape', `/bin/cat /proc/mounts`);
531
    my $mounts = `/bin/cat /proc/mounts`;
532
    unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
533
    foreach my $u (@users) {
534
        my @regkeys = (tied %register)->select_where("user = '$u'");
535
        foreach my $k (@regkeys) {
536
            my $valref = $register{$k};
537
            my $path = $valref->{'path'};
538
# Only update info for images the user has access to.
539
# Remove DB entries for images on removed nodes
540
            if ($valref->{'storagepool'}==-1 && $valref->{'mac'} && $valref->{'mac'} ne '--' && !$nodereg{$valref->{'mac'}}) {
541
                delete $register{$path}; # Clean up database, remove rows which don't have corresponding file
542
                $main::updateUI->({tab=>'images', user=>$u}) unless ($u eq 'common');
543
            } elsif ($valref->{'user'} eq $u && (defined $spools[$valref->{'storagepool'}]->{'id'} || $valref->{'storagepool'}==-1)) {
544
                my $path = $valref->{'path'};
545
                next if ($spath && $spath ne $path); # Only specific image being updated
546
                my $mounted = ($mounts =~ /$path/);
547
                my $domains;
548
                my $domainnames;
549
                my $regstatus = $valref->{'status'};
550
                my $status = $regstatus;
551
                if (!$status || $status eq '--') {
552
                    $status = 'unused';
553
                }
554
                if (-e $path || $valref->{'storagepool'}==-1 || -s "$path.meta") {
555
                # Deal with status
556
                    if ($valref->{'storagepool'}!=-1 && -s "$path.meta") {
557
                        if ($regstatus =~ /(downloading|uploading)/ && (-e "$path.meta")) {
558
                            my $adjective = $1;
559
                            my $percentage = `grep -Po '\\d+%' "$path.meta" | tail -n1`;
560
                            chomp $percentage;
561
                            $status = "$adjective $percentage" if ($percentage);
562
                        } else {
563
                            my $metastatus;
564
                            $metastatus = `/bin/cat "$path.meta" 2>/dev/null`;
565
                            chomp $metastatus;
566
                            if ($metastatus =~ /status=(.+)&chunk=/) {
567
                                $status = $1;
568
                            } elsif ($metastatus =~ /status=(.+)&path2:(.+)=(.+)/) {
569
                                # A move operation has been completed - update status of both involved
570
                                $status = $1;
571
                                $register{$2}->{'status'} = $3;
572
                                unless ($userregister{$2}) { # If we have not yet parsed image, it is not yet in userregister, so put it there
573
                                    my %mval = %{$register{$2}};
574
                                    $userregister{$2} = \%mval;
575
                                }
576
                                $userregister{$2}->{'status'} = $3;
577
                            } elsif ($metastatus =~ /status=(\w+)/) {
578
                                $status = $1;
579
                            } else {
580
                                #    $status = $metastatus; # Do nothing - this meta file contains no status info
581
                            }
582
                        }
583
                    } elsif (
584
                            $status eq "restoring"
585
                            || $status eq "frestoring"
586
                            || ($status eq "mounted" && $mounted)
587
                            || $status eq "snapshotting"
588
                            || $status eq "unsnapping"
589
                            || $status eq "reverting"
590
                            || $status eq "moving"
591
                            || $status eq "stormoving"
592
                            || $status eq "converting"
593
                            || $status eq "cloning"
594
                            || $status eq "copying"
595
                            || $status eq "rebasing"
596
                            || $status eq "creating"
597
                            || $status eq "resizing"
598
                        ) { # When operation is done, status is updated by piston.cgi
599
                        ; # Do nothing
600
                    } elsif ($status =~ /.(backingup)/) { # When backup is done, status is updated by steamExec
601
                        if ($valref->{'storagepool'}==-1) {
602
                        #    unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
603
                            if ($nodereg{$valref->{'mac'}}) {
604
                                my $nodestatus = $nodereg{$valref->{'mac'}}->{status};
605
                                # If node is not available, it cannot be backing up...
606
                                if ($nodestatus eq 'inactive'
607
                                    || $nodestatus eq 'asleep'
608
                                    || $nodestatus eq 'shutoff'
609
                                ) {
610
                                    $valref->{'status'} = 'unused'; # Make sure we don't end here again in endless loop
611
                                    $rstatus = Updateregister(0, $path);
612
                                    $status = $rstatus if ($rstatus);
613
                                    $main::syslogit->($user, 'info', "Updated image status for aborted backup - $user, $path, $rstatus");
614
                                }
615
                            }
616
                            #untie %nodereg;
617
                        }
618

    
619
                    } elsif ($status eq 'uploading') {
620
                        $status = 'unused' unless (-s "$path.meta");
621

    
622
                    } elsif (!$status || $status eq 'unused' || $status eq 'active') {
623
                        if ($fullupdate) {
624
                            $status = "unused";
625
                            my @domregkeys;
626
                            if ($fulllist) {@domregkeys = keys %domreg;}
627
                            else {@domregkeys = (tied %domreg)->select_where("user = '$u'");}
628
                            foreach my $domkey (@domregkeys) {
629
                                my $dom = $domreg{$domkey};
630
                                my $img = $dom->{'image'};
631
                                my $img2 = $dom->{'image2'};
632
                                my $img3 = $dom->{'image3'};
633
                                my $img4 = $dom->{'image4'};
634
                                if ($path eq $img || $path eq $img2 || $path eq $img3 || $path eq $img4) {
635
                                    my $domstatus = $dom->{'status'};
636
                                    if ($domstatus =~ /moving/) {;} # do nothing - updated by piston
637
                                    elsif ($domstatus eq "shutoff" || $domstatus eq "inactive") {$status = "used";}
638
                                    elsif ($domstatus eq "paused") {$status = "paused";}
639
                                    else {$status = "active";}
640
                                    $domains = $dom->{'uuid'};
641
                                    $domainnames = $dom->{'name'};
642
                                };
643
                            }
644
                            $valref->{'domains'} = $domains ;
645
                            $valref->{'domainnames'} = $domainnames ;
646
                        } elsif ($valref->{'domains'} && $valref->{'domains'} ne '--'){
647
                            my $dom = $domreg{$valref->{'domains'}};
648
                            if ($dom) {
649
                                my $img = $dom->{'image'};
650
                                my $img2 = $dom->{'image2'};
651
                                my $img3 = $dom->{'image3'};
652
                                my $img4 = $dom->{'image4'};
653
                                if ($path eq $img || $path eq $img2 || $path eq $img3 || $path eq $img4) {
654
                                    my $domstatus = $dom->{'status'};
655
                                    if ($domstatus =~ /moving/) {;} # do nothing - updated by piston
656
                                    elsif ($domstatus eq "shutoff" || $domstatus eq "inactive") {$status = "used";}
657
                                    elsif ($domstatus eq "paused") {$status = "paused";}
658
                                    else {$status = "active";}
659
                                    $valref->{'domainnames'} = $dom->{'name'};
660
                                };
661
                            };
662
                        }
663
                    }
664
                    # Update info in db
665
                    $valref->{'status'} = $status ;
666
                    $res .= $status if ($spath);
667
                } else {
668
                    delete $register{$path}; # Clean up database, remove rows which don't have corresponding file
669
                    $main::updateUI->({tab=>'images', user=>$u}) unless ($u eq 'common');
670
                }
671
            }
672
        }
673
    }
674
    untie %nodereg;
675
    tied(%register)->commit;
676
    $res .= "Status=OK Updated image register for " . join(', ', @users) . "\n";
677
    $res .= $postreply;
678
    return $res if ($res);
679
}
680

    
681
sub getVirtualSize {
682
    my $vpath = shift;
683
    my $macip = shift;
684
    my $qinfo;
685
    my($bname, $dirpath, $suffix) = fileparse($vpath, (".vmdk", ".img", ".vhd", ".vhdx", ".qcow", ".qcow2", ".vdi", ".iso"));
686
    if ($suffix eq ".qcow2") {
687
        if ($macip) {
688
            $qinfo = `$sshcmd $macip /usr/bin/qemu-img info --force-share "$vpath"`;
689
        } else {
690
            $qinfo = `/usr/bin/qemu-img info --force-share "$vpath"`;
691
        }
692
        $qinfo =~ /virtual size:.*\((.+) bytes\)/g;
693
        return(int($1)); # report size of new image for billing purposes
694
    } elsif ($status eq ".vdi") {
695
        if ($macip) {
696
            $qinfo = `$sshcmd $macip /usr/bin/VBoxManage showhdinfo "$vpath"`;
697
        } else {
698
            $qinfo = `/usr/bin/VBoxManage showhdinfo "$vpath"`;
699
        }
700
        $qinfo =~ /Logical size:\s*(\d+) MBytes/g;
701
        return(int($1) * 1024 * 1024); # report size of new image for billing purposes
702
    } else {
703
        if ($macip) {
704
            return `$sshcmd $macip perl -e 'my @stat=stat("$vpath"); print $stat[7];'`;
705
        } else {
706
            my @stat = stat($vpath);
707
            return($stat[7]); # report size of new image for billing purposes
708
        }
709
    }
710
}
711

    
712
sub getSizes {
713
    my ($f, $lmtime, $status, $buser, $force) = @_;
714

    
715
    my @stat = stat($f);
716
    my $size = $stat[7];
717
    my $realsize = $stat[12] * 512;
718
    my $virtualsize = $size;
719
    my $backupsize = 0;
720
    my $mtime = $stat[9];
721
    my($fname, $dirpath, $suffix) = fileparse($f, ("vmdk", "img", "vhd", "vhdx", "qcow", "qcow2", "vdi", "iso"));
722
    my $subdir = "";
723
    if ($dirpath =~ /.+\/$buser(\/.+)?\//) {
724
        $subdir = $1;
725
    }
726
    $backupsize = getBackupSize($subdir, "$fname$suffix", $buser);
727
    my $ps = `/bin/ps ax`;
728

    
729
# Only fire up qemu-img etc. if image has been modified and is not being used
730
    if ((
731
        ($mtime - $lmtime)>300 &&
732
        ($status ne 'active' && $status ne 'downloading') &&
733
        !($ps =~ /$f/)) || $force
734
    ) {
735

    
736
# Special handling of vmdk's
737
        if ($suffix eq "vmdk") {
738
            my $qinfo = `/usr/bin/qemu-img info --force-share "$f"`;
739
            $qinfo =~ /virtual size:.*\((.+) bytes\)/g;
740
            $virtualsize = int($1);
741
            if ( -s ($dirpath . substr($fname,0,-1) . "-flat." . $suffix)) {
742
                my @fstatus = stat($dirpath . substr($fname,0,-1) . "-flat." . $suffix);
743
                my $fsize = $fstatus[7];
744
                my $frealsize = $fstatus[12] * 512;
745
                $size += $fsize;
746
                $virtualsize += $fsize;
747
                $realsize += $frealsize;
748
            } else {
749
#                $main::syslogit->($user, "info", "VMDK " . $dirpath . substr($fname,0,-1) . "-flat." . $suffix . " does not exist");
750
            }
751
            my $i = 1;
752
            while (@fstatus = stat($dirpath . substr($fname,0,-1) . "-s00$i." . $suffix)) {
753
                my $fsize = $fstatus[7];
754
                my $frealsize = $fstatus[12] * 512;
755
                $size += $fsize;
756
                #$virtualsize += $fsize;
757
                $realsize += $frealsize;
758

    
759
                my $cmdpath = $dirpath . substr($fname,0,-1) . "-s00$i." . $suffix;
760
                my $qinfo = `/usr/bin/qemu-img info --force-share "$cmdpath"`;
761
                $qinfo =~ /virtual size:.*\((.+) bytes\)/g;
762
                $virtualsize += int($1);
763

    
764
                $i++;
765
            }
766
# Get virtual size of qcow2 auto-grow volumes
767
        } elsif ($suffix eq "qcow2") {
768
            my $qinfo = `/usr/bin/qemu-img info --force-share "$f"`;
769
            $qinfo =~ /virtual size:.*\((.+) bytes\)/g;
770
            $virtualsize = int($1);
771
# Get virtual size of vdi auto-grow volumes
772
        } elsif ($suffix eq "vdi") {
773
            my $qinfo = `/usr/bin/VBoxManage showhdinfo "$f"`;
774
            $qinfo =~ /Logical size:\s*(\d+) MBytes/g;
775
            $virtualsize = int($1) * 1024 * 1024;
776
        }
777
# Actual used blocks times block size on disk, i.e. $realsize may be bigger than the
778
# logical size of the image file $size and the logical provisioned size of the disk $virtualsize
779
# in order to minimize confusion, we set $realsize to $size if this is the case
780
        $realsize = $size if ($realsize > $size);
781

    
782
        return ($mtime, $backupsize, $size, $realsize, $virtualsize);
783
    } else {
784
        return (0, $backupsize, $size, $realsize);
785
    }
786

    
787
}
788

    
789
sub getHypervisor {
790
	my $image = shift;
791
	# Produce a mapping of image file suffixes to hypervisors
792
	my %idreg;
793
    unless ( tie(%idreg,'Tie::DBI', Hash::Merge::merge({table=>'nodeidentities', key=>'identity'}, $Stabile::dbopts)) )
794
        {$postreply .= "Status=Error identity register could not be accessed"};
795

    
796
	my @idvalues = values %idreg;
797
	my %formats;
798
	foreach my $val (@idvalues) {
799
		my %h = %$val;
800
		foreach (split(/,/,$h{'formats'})) {
801
			$formats{lc $_} = $h{'hypervisor'}
802
		}
803
	}
804
	untie %idreg;
805

    
806
	# and then determine the hypervisor in question
807
	my $hypervisor = "vbox";
808
	my ($pathname, $path, $suffix) = fileparse($image, '\.[^\.]*');
809
	$suffix = substr $suffix, 1;
810
	my $hypervisor = $formats{lc $suffix};
811
	return $hypervisor;
812
}
813

    
814
sub Getserverbackups {
815
    my ($domuuid, $action) = @_;
816
    if ($help) {
817
        return <<END
818
GET:uuid:
819
Lists the image backups associated with a server, i.e. the backups of all the images attached to a server.
820
A server UUID should be passed as parameter. A JSON object is returned. May be called as <b>getserverbackups</b>, in
821
which case a JSON object is returned, or as <b>listserverbackups</b>, in which case a string is returned.
822
END
823
    }
824
    my $res;
825
    my @sbackups;
826
    my $backuplist;
827

    
828
    if ($domreg{$domuuid} && (($domreg{$domuuid}->{'user'} eq $user) || $isadmin)) {
829
        push @sbackups, Listbackups($domreg{$domuuid}->{'image'}, 'getbackups');
830
        push @sbackups, Listbackups($domreg{$domuuid}->{'image2'}, 'getbackups') if ($domreg{$domuuid}->{'image2'} && $domreg{$domuuid}->{'image2'} ne '--');
831
        push @sbackups, Listbackups($domreg{$domuuid}->{'image3'}, 'getbackups') if ($domreg{$domuuid}->{'image3'} && $domreg{$domuuid}->{'image3'} ne '--');
832
        push @sbackups, Listbackups($domreg{$domuuid}->{'image4'}, 'getbackups') if ($domreg{$domuuid}->{'image4'} && $domreg{$domuuid}->{'image4'} ne '--');
833
    }
834
    foreach my $sbackup (@sbackups) {
835
        my @back = @{$sbackup};
836
        my $t = $back[0]->{time};
837
        my $epoch;
838
        my $z;
839
        if ($t eq '--') {
840
            $epoch = $t;
841
        } elsif ($t =~ /(z)/) {
842
#            my $time = Time::Piece->strptime($t, "%Y-%m-%d-%H-%M-%S (z)");
843
            my $time = Time::Piece->strptime($t, "%b %d %T %Y (z)");
844
            $epoch = $time->epoch;
845
            $z = ' (z)';
846
        } else {
847
            $t = $1 if ($t =~ /\* (.*)/);
848
            my $time = Time::Piece->strptime($t, "%b %d %T %Y");
849
            $epoch = $time->epoch;
850
        }
851
        $backuplist .= "$back[-1]->{name}$z/$epoch, " if (@back && $epoch);
852
    }
853
    $backuplist = substr($backuplist,0,-2);
854

    
855
    if ($action eq 'getserverbackups') {
856
        $res .= to_json(\@sbackups, {pretty=>1});
857
    } else {
858
        $res .= header() unless ($console);
859
        $res .= $backuplist;
860
    }
861
    return $res;
862

    
863
}
864

    
865
sub Listbackups {
866
    my ($curimg, $action) = @_;
867
    if ($help) {
868
        return <<END
869
GET:image:
870
List backups on file for the give image, which may be specified as path or uuid.
871
END
872
    }
873

    
874
    my $res;
875
    my $buser = $user;
876
    $curimg = '' unless ($register{$curimg}); # Image must exist
877
    $buser = $register{$curimg}->{'user'} if ($isadmin && $curimg);
878
    my @backups;
879
    my $subdir = "";
880
    if ($curimg && $curimg ne '--') {
881
        my($bname, $dirpath) = fileparse($curimg);
882
        if ($dirpath =~ /.+\/$buser(\/.+)?\//) {
883
            $subdir = $1;
884
        }
885
        my $sbname = "$subdir/$bname";
886
        $sbname =~ s/ /\\ /g;
887
        $sbname = $1 if ($sbname =~ /(.+)/); # untaint
888
        foreach my $spool (@spools) {
889
            my $imgbasedir = $spool->{"path"};
890
            if (-d "$imgbasedir/.zfs/snapshot") {
891
                my $snaps = `/bin/ls -l --time-style=full-iso $imgbasedir/.zfs/snapshot/*/$buser$sbname 2> /dev/null`;
892
                my @snaplines = split("\n", $snaps);
893
                # -rw-r--r-- 1 root root 216174592 2012-02-19 17:51 /mnt/stabile/images/.zfs/snapshot/SNAPSHOT-20120106002116/cabo/Outlook2007.iso
894
                foreach $line (@snaplines) {
895
                    if ($line =~ /$imgbasedir\/.zfs\/snapshot\/SNAPSHOT-(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\/$buser$subdir\/$bname$/) {
896
                        my $timestamp = timelocal($6,$5,$4,$3,$2-1,$1); #$sec,$min,$hour,$mday,$mon,$year
897
                        my $t = localtime($timestamp)->strftime("%b %e %H:%M:%S %Y");
898
                        # my %incr = ("increment", "SNAPSHOT-$1$2$3$4$5$6", "time", "$1-$2-$3-$4-$5-$6 (z)", "pool", $imgbasedir);
899
                        my %incr = ("increment", "SNAPSHOT-$1$2$3$4$5$6", "time", "$t (z)", "pool", $imgbasedir);
900
                        unshift (@backups, \%incr);
901
                    };
902
                }
903
            }
904
        }
905
        # Also include ZFS snapshots transferred from nodes
906
        $imgbasedir = "/stabile-backup";
907
        my $snaps = `/bin/ls -l --time-style=full-iso $imgbasedir/node-*/.zfs/snapshot/*/$buser$sbname 2> /dev/null`;
908
        my @snaplines = split("\n", $snaps);
909
        foreach $line (@snaplines) {
910
            if ($line =~ /($imgbasedir\/node-.+)\/.zfs\/snapshot\/SNAPSHOT-(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\/$buser$subdir\/$bname$/) {
911
                my $timestamp = timelocal($7,$6,$5,$4,$3-1,$2); #$sec,$min,$hour,$mday,$mon,$year
912
                my $t = localtime($timestamp)->strftime("%b %e %H:%M:%S %Y");
913
                # my %incr = ("increment", "SNAPSHOT-$2$3$4$5$6$7", "time", "$2-$3-$4-$5-$6-$7 (z)", "pool", $1);
914
                my %incr = ("increment", "SNAPSHOT-$2$3$4$5$6$7", "time", "$t (z)", "pool", $1);
915
                unshift (@backups, \%incr);
916
            };
917
        }
918
        my $bpath = "$backupdir/$buser$subdir/$bname";
919
        $bpath = $1 if ($bpath =~ /(.+)/); # untaint
920
        if (-d "$bpath") {
921
            my $rdiffs = `/usr/bin/rdiff-backup -l "$bpath"`;
922
            my @mlines = split("\n", $rdiffs);
923
            my $curmirror;
924
            foreach my $line (@mlines) {
925
                if ($line =~ /\s+increments\.(\S+)\.dir\s+\S\S\S (.+)$/) {
926
                    my %incr = ("increment", $1, "time", $2);
927
                    if (-e "$bpath/rdiff-backup-data/increments/$bname.$1.diff.gz"
928
                    ) {
929
                        unshift (@backups, \%incr);
930
                    }
931
                };
932
                if ($line =~ /Current mirror: \S\S\S (.+)$/) {
933
                    $curmirror = $1;
934
                };
935
            }
936
            if ($curmirror) {
937
                my %incr = ("increment", "mirror", "time", "* $curmirror");
938
                unshift @backups, \%incr;
939
            }
940
            my %incr = ("increment", "--", "time", "--", "name", $bname);
941
            push @backups, \%incr;
942
        } else {
943
            my %incr = ("increment", "--", "time", "--", "name", $bname);
944
            push @backups, \%incr;
945
        }
946
    }
947

    
948
    if ($action eq 'getbackups') {
949
        return \@backups;
950
    } elsif ($console) {
951
        my $t2 = Text::SimpleTable->new(28,28);
952
        $t2->row('increment', 'time');
953
        $t2->hr;
954
        foreach my $fref (@backups) {
955
            $t2->row($fref->{'increment'}, scalar localtime( $fref->{'time'} )) unless ($fref->{'increment'} eq '--');
956
        }
957
        return $t2->draw;
958
    } else {
959
        $res .= header('application/json');
960
        my $json_text = to_json(\@backups, {pretty=>1});
961
        $res .= qq|{"identifier": "increment", "label": "time", "items": $json_text }|;
962
        return $res;
963
    }
964
}
965

    
966
# Get the timestamp of latest backup of an image
967
sub getBtime {
968
    my $curimg = shift;
969
    my $buser = shift || $user;
970
    return unless ($buser eq $user || $isadmin);
971
    $buser = 'common' if ($register{$curimg}->{user} eq 'common' && $isadmin);
972
    my $subdir = "";
973
    my $lastbtimestamp;
974
    my($bname, $dirpath) = fileparse($curimg);
975
    if ($dirpath =~ /.+\/$buser(\/.+)?\//) {
976
        $subdir = $1;
977
    }
978

    
979
    #require File::Spec;
980
    #my $devnull = File::Spec->devnull();
981

    
982
    foreach my $spool (@spools) {
983
        my $imgbasedir = $spool->{"path"};
984
        if (-d "$imgbasedir/.zfs/snapshot") {
985
            my $sbname = "$subdir/$bname";
986
            $sbname =~ s/ /\\ /g;
987
            my $cmd = qq|/bin/ls -l --time-style=full-iso $imgbasedir/.zfs/snapshot/*/$buser$sbname 2>/dev/null|;
988
            my $snaps = `$cmd`;
989
            my @snaplines = split("\n", $snaps);
990
            foreach $line (@snaplines) {
991
                if ($line =~ /$imgbasedir\/.zfs\/snapshot\/SNAPSHOT-(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\/$buser$subdir\/$bname$/) {
992
                    my $timestamp = timelocal($6,$5,$4,$3,$2-1,$1); #$sec,$min,$hour,$mday,$mon,$year
993
                    $lastbtimestamp = $timestamp if ($timestamp > $lastbtimestamp);
994
                };
995
            }
996
        }
997
    }
998
    # Also include ZFS snapshots transferred from nodes
999
    $imgbasedir = "/stabile-backup";
1000
    my $snaps = `/bin/ls -l --time-style=full-iso $imgbasedir/node-*/.zfs/snapshot/*/$buser/$bname 2> /dev/null`;
1001
    my @snaplines = split("\n", $snaps);
1002
    foreach $line (@snaplines) {
1003
        if ($line =~ /$imgbasedir\/node-.+\/.zfs\/snapshot\/SNAPSHOT-(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\/$buser$subdir\/$bname$/) {
1004
            my $timestamp = timelocal($6,$5,$4,$3,$2-1,$1); #$sec,$min,$hour,$mday,$mon,$year
1005
            $lastbtimestamp = $timestamp if ($timestamp > $lastbtimestamp);
1006
        };
1007
    }
1008
    my $bpath = "$backupdir/$buser$subdir/$bname";
1009
    $bpath = $1 if ($bpath =~ /(.+)/);
1010
    if (-d "$bpath") {
1011
        my $rdiffs = `/usr/bin/rdiff-backup --parsable-output -l "$bpath"`;
1012
        my @mlines = split("\n", $rdiffs);
1013
        foreach my $line (@mlines) {
1014
            if ($line =~ /(\d+) (\S+)$/) {
1015
                my $timestamp = $1;
1016
                $lastbtimestamp = $timestamp if ($timestamp > $lastbtimestamp);
1017
            };
1018
        }
1019
    }
1020
    return $lastbtimestamp;
1021
}
1022

    
1023
sub Unmount {
1024
    my $path = shift;
1025
	my $action = shift;
1026
    if ($help) {
1027
        return <<END
1028
GET:image: Unmounts a previously mounted image.
1029
END
1030
    }
1031
    my($bname, $dirpath, $suffix) = fileparse($path, (".vmdk", ".img", ".vhd", ".vhdx", ".qcow", ".qcow2", ".vdi", ".iso"));
1032
    my $mountpath = "$dirpath.$bname$suffix";
1033
#    my $mounts = decode('ascii-escape', `/bin/cat /proc/mounts`);
1034
    my $mounts = `/bin/cat /proc/mounts`;
1035
    my $mounted = ($mounts =~ /$mountpath/);
1036

    
1037
#    eval {`/bin/umount "$mountpath"` if ($mounted); 1;}
1038
#    eval {`/bin/fusermount -u "$mountpath"` if ($mounted); 1;}
1039
#        or do {$postreply .= "Status=ERROR Problem mounting image $@\n";};
1040

    
1041
    if ($mounted) {
1042
        $cmd = qq|/bin/fusermount -u "$mountpath" 2>&1|;
1043
        my $mes = qx($cmd);
1044
        my $xc = $? >> 8;
1045
        $main::syslogit->($user, 'info', "Unmounted $curimg $xc");
1046
        if ($xc) {
1047
            $postreply .= "Status=ERROR Problem unmounting image ($mes). ";
1048
            return $postreply;
1049
        }
1050
    }
1051
#    my $mounts2 = decode('ascii-escape', `/bin/cat /proc/mounts`);
1052
    my $mounts2 = `/bin/cat /proc/mounts`;
1053
    $mounts2 = String::Escape::unbackslash($mounts2);
1054
    my $mounted2 = ($mounts2 =~ /$mountpath/);
1055
    eval {`/bin/rmdir "$mountpath"` if (!$mounted2 && -e $mountpath); 1;}
1056
        or do {$postreply .= "Status=ERROR Problem removing mount point $@\n";};
1057

    
1058
    if ($mounted) {
1059
        if ($mounted2) {
1060
            $postreply .= "Status=ERROR Unable to unmount $register{$path}->{'name'}\n";
1061
            return $postreply;
1062
        } else {
1063
            $postreply .= "Status=OK Unmounted image $register{$path}->{'name'}\n";
1064
            return $postreply;
1065
        }
1066
    } else {
1067
        $postreply .= "Status=OK Image $path not mounted\n";
1068
        return $postreply;
1069
    }
1070
}
1071

    
1072
sub unmountAll {
1073
    my @mounts = split(/\n/, `/bin/cat /proc/mounts`);
1074
    foreach my $mount (@mounts) {
1075
        foreach my $spool (@spools) {
1076
            my $pooldir = $spool->{"path"};
1077
            if ($mount =~ /($pooldir\/$user\/\S+) / || ($mount =~ /($pooldir\/common\/\S+) / && $isadmin)) {
1078
#                $mountpath = decode('ascii-escape', $1);
1079
                $mountpath =  $1;
1080
                $rpath = $mountpath;
1081
                $rpath =~ s/\/\./\//;
1082
                my $processes = `/bin/ps`;
1083
#                if ($register{$rpath} && !($processes =~ /steamExec.+$rpath/)) {
1084
                    $postreply .= "Status=OK Unmounting $rpath\n";
1085
                    Unmount($rpath);
1086
#                }
1087
            }
1088
        }
1089
    }
1090
    return;
1091
}
1092

    
1093
sub Mount {
1094
    my $path = shift;
1095
	my $action = shift;
1096
    if ($help) {
1097
        return <<END
1098
GET:image:
1099
Tries to mount an image on admin server for listfiles/restorefiles operations.
1100
END
1101
    }
1102
    my($bname, $dirpath, $suffix) = fileparse($path, (".vmdk", ".img", ".vhd", ".vhdx", ".qcow", ".qcow2", ".vdi", ".iso"));
1103
    my $mountpath = "$dirpath.$bname$suffix";
1104
    my $mounts = `/bin/cat /proc/mounts`;
1105
    $mounts = String::Escape::unbackslash($mounts);
1106
    my $mounted = ($mounts =~ /$mountpath/);
1107
    if ($mounted) {
1108
        unless (`ls "$mountpath"`) { # Check if really mounted
1109
            Unmount($mountpath);
1110
            $mounted = 0;
1111
        }
1112
    }
1113

    
1114
    if ($mounted) {
1115
        $postreply .= "Status=OK Image $register{$path}->{'name'} already mounted\n";
1116
        return $postreply;
1117
    } else {
1118
        `/bin/mkdir "$mountpath"` unless (-e "$mountpath");
1119
        `/bin/chown www-data:www-data  "$mountpath"`;
1120
        my $cmd;
1121

    
1122
        if (lc $suffix eq '.iso') {
1123
            #eval {`/bin/mount -o allow_other,ro,loop "$path" "$mountpath"`; 1;}
1124
            #eval {`/usr/bin/fuseiso -n "$path" "$mountpath" -o user=www-data`; 1;}
1125
            eval {`/usr/bin/fuseiso -n "$path" "$mountpath" -o allow_other`; 1;}
1126
            or do {
1127
                $postreply .= header('text/html', '500 Internal Server Error') unless ($console);
1128
                $postreply .= "Status=ERROR Problem mounting image $@\n";
1129
                return $postreply;
1130
            };
1131
        } else {
1132
            # First try to mount using autodiscover -i. If that fails, try to mount /dev/sda1
1133
            $cmd = qq|/usr/bin/guestmount --ro -o allow_other -a "$path" "$mountpath" -i 2>&1|;
1134
            my $mes = qx($cmd);
1135
            my $xc = $? >> 8;
1136
            $main::syslogit->($user, 'info', "Trying to mount $curimg $xc");
1137
            if ($xc) {
1138
                $cmd = qq|/usr/bin/guestmount --ro -o allow_other -a "$path" "$mountpath"  -m /dev/sda1:/ 2>&1|;
1139
                $mes = qx($cmd);
1140
                $xc = $? >> 8;
1141
                $main::syslogit->($user, 'info', "Trying to mount $curimg $xc");
1142
                if ($xc) {
1143
                    $postreply = header('text/html', '500 Internal Server Error') . $postreply unless ($console);
1144
                    chomp $mes;
1145
                    $postreply .= "Status=Error Problem mounting image ($mes).\n$cmd\n";
1146
                    return $postreply;
1147
                }
1148
            }
1149
        }
1150

    
1151
        my $mounts2;
1152
        for (my $i=0; $i<5; $i++) {
1153
            $mounts2 = `/bin/cat /proc/mounts`;
1154
            $mounts2 = String::Escape::unbackslash($mounts2);
1155
            next if ( $mounts2 =~ /$mountpath/);
1156
            sleep 2;
1157
        }
1158
        if ( $mounts2 =~ /$mountpath/) {
1159
            $postreply .= "Status=OK Mounted image $register{$path}->{'name'}\n";
1160
            return $postreply;
1161
        } else {
1162
            $postreply .= header('text/html', '500 Internal Server Error') unless ($console);
1163
            $postreply .= "Status=ERROR Giving up mounting image $register{$path}->{'name'}\n";
1164
            return $postreply;
1165
        }
1166
    }
1167
}
1168

    
1169
sub Updatebackingfile {
1170
    my ($img, $action) = @_;
1171
    if ($help) {
1172
        return <<END
1173
GET:image:
1174
END
1175
    }
1176
    my $f = $img || $curimg;
1177
    return "Status=Error Image $f not found\n" unless (-e $f);
1178
    my $vinfo = `qemu-img info --force-share "$f"`;
1179
    my $master = $1 if ($vinfo =~ /backing file: (.+)/);
1180
    (my $fname, my $fdir) = fileparse($f);
1181
    if (!$master) {
1182
        $register{$f}->{'master'} = '';
1183
        $postreply .=  "Status=OK Image $f does not use a backing file\n";
1184
    } elsif (-e $master){ # Master OK
1185
        $register{$f}->{'master'} = $master;
1186
        $postreply .=  "Status=OK $master exists, no changes to $f.\n";
1187
    } elsif (-e "$fdir/$master") { # Master OK
1188
        $register{$f}->{'master'} = "$fdir/$master";
1189
        $postreply .=  "Status=OK $master exists in $fdir. No changes to $f.\n"
1190
    } else {
1191
        # Master not immediately found, look for it
1192
        (my $master, my $mdir) = fileparse($master);
1193
        my @busers = @users;
1194
        push (@busers, $billto) if ($billto); # We include images from 'parent' user
1195
        foreach my $u (@busers) {
1196
            foreach my $spool (@spools) {
1197
                my $pooldir = $spool->{"path"};
1198
                my $masterpath = "$pooldir/$u/$master";
1199
                if (-e $masterpath) {
1200
                    my $cmd = qq|qemu-img rebase -f qcow2 -u -b "$masterpath" -F qcow2 "$f"|;
1201
                    $register{$f}->{'master'} = $masterpath;
1202
                    $postreply .= "Status=OK found $masterpath, rebasing from $mdir to $pooldir/$u ";
1203
                    $postreply .= `$cmd` . "\n";
1204
                    last;
1205
                }
1206
            }
1207
        }
1208
        if (!$postreply) { # master not found, we rebase to main storage pool, hoping it will appear there
1209
            my $pooldir = $spools[0]->{"path"};
1210
            my $masterpath = "$pooldir/common/$master";
1211
            my $cmd = qq|qemu-img rebase -f qcow2 -u -b "$masterpath" -F qcow2 "$f"|;
1212
            $register{$f}->{'master'} = $masterpath;
1213
            $postreply .= "Status=Error $master not found in any user dir. You must provide this backing file to use this image.\n";
1214
            $postreply .= `$cmd` . "\n";
1215
        }
1216
    }
1217
    tied(%register)->commit;
1218
    return $postreply;
1219
}
1220

    
1221
# List files in a mounted image. Mount image if not mounted.
1222
sub Listfiles {
1223
    my ($curimg, $action, $obj) = @_;
1224
    if ($help) {
1225
        return <<END
1226
GET:image,path:
1227
Try to mount the file system on the given image, and list the files from the given path in the mounted file system.
1228
The image must contain a bootable file system, in order to locate a mount point.
1229
END
1230
    }
1231
    my $res;
1232
    my $curpath = $obj->{'restorepath'};
1233
    $res .= header('application/json') unless ($console);
1234

    
1235
    my($bname, $dirpath, $suffix) = fileparse($curimg, (".vmdk", ".img", ".vhd", ".vhdx", ".qcow", ".qcow2", ".vdi", ".iso"));
1236
    my $mountpath = "$dirpath.$bname$suffix";
1237
	my @files;
1238
	my @dirs;
1239
    my $mounted = (Mount($curimg) =~ /\w=OK/);
1240

    
1241
    if ($mounted) {
1242
        my @patterns = ('');
1243
        $curpath .= '/' unless ($curpath =~ /\/$/);
1244
        $mountpath .= "$curpath";
1245
        if (-d $mountpath) { # We are listing a directory
1246
            # loop through the files contained in the directory
1247
            @patterns = ('*', '.*');
1248
        }
1249
        foreach $pat (@patterns) {
1250
            for my $f (bsd_glob($mountpath.$pat)) {
1251
                my %fhash;
1252
                ($bname, $dirpath) = fileparse($f);
1253
                my @stat = stat($f);
1254
                my $size = $stat[7];
1255
                my $realsize = $stat[12] * 512;
1256
                my $mtime = $stat[9];
1257

    
1258
                $fhash{'name'} = $bname;
1259
                $fhash{'mtime'} = $mtime;
1260
                ## if the file is a directory
1261
                if( -d $f) {
1262
                    $fhash{'size'} = 0;
1263
                    $fhash{'fullpath'} = $f . '/';
1264
                    $fhash{'path'} = $curpath . $bname . '/';
1265
                    push(@dirs, \%fhash) unless ($bname eq '.' || $bname eq '..');
1266
                } else {
1267
                    $fhash{'size'} = $size;
1268
                    $fhash{'fullpath'} = $f;
1269
                    $fhash{'path'} = $curpath . $bname;
1270
                    push(@files, \%fhash);
1271
                }
1272
            }
1273
        }
1274

    
1275
        if ($console) {
1276
            my $t2 = Text::SimpleTable->new(48,16,28);
1277
            $t2->row('name', 'size', 'mtime');
1278
            $t2->hr;
1279
            foreach my $fref (@dirs) {
1280
                $t2->row($fref->{'name'}, $fref->{'size'}, scalar localtime( $fref->{'mtime'} )) unless ($bname eq '.' || $bname eq '..');
1281
            }
1282
            foreach my $fref (@files) {
1283
                $t2->row($fref->{'name'}, $fref->{'size'}, scalar localtime( $fref->{'mtime'} ) ) unless ($bname eq '.' || $bname eq '..');
1284
            }
1285
            return $t2->draw;
1286
        } else {
1287
            my @comb = (@dirs, @files);
1288
            $res .= to_json(\@comb, {pretty => 1});
1289
        }
1290
    } else {
1291
        $res .= qq|{"status": "Error", "message": "Image $curimg not mounted. Mount first."}|;
1292
    }
1293
    return $res;
1294
}
1295

    
1296
sub Restorefiles {
1297
    my ($path, $action, $obj) = @_;
1298
    if ($help) {
1299
        return <<END
1300
GET:image,files:
1301
Restores files from the given path in the given image to a newly created ISO image. The given image must be mountable.
1302
END
1303
    }
1304
    my $res;
1305
    $curfiles = $obj->{'files'};
1306
    $path = $path || $curimg;
1307

    
1308
    return "Status=ERROR Your account does not have the necessary privileges\n" if ($isreadonly);
1309
    return "Status=ERROR You must specify which files you want to restore\n" unless ($curfiles);
1310

    
1311
    my $name = $register{$path}->{'name'};
1312
    my($bname, $dirpath, $suffix) = fileparse($path, (".vmdk", ".img", ".vhd", ".vhdx", ".qcow", ".qcow2", ".vdi", ".iso"));
1313
    my $mountpath = "$dirpath.$bname$suffix";
1314
#    my $mounts = decode('ascii-escape', `/bin/cat /proc/mounts`);
1315
    my $mounts = `/bin/cat /proc/mounts`;
1316
    my $mmounts = `/bin/df`;
1317
    my $mounted = ($mounts =~ /$mountpath/ && $mmounts =~ /$mountpath/);
1318
    my $restorepath = "$dirpath$bname.iso";
1319

    
1320
    if (-e $restorepath) {
1321
        my $i = 1;
1322
        while (-e "$dirpath$bname.$i.iso") {$i++;}
1323
        $restorepath = "$dirpath$bname.$i.iso";
1324
    }
1325

    
1326
    my $uistatus = "frestoring";
1327
    if ($mounted && $curfiles) {
1328
        my $ug = new Data::UUID;
1329
        my $newuuid = $ug->create_str();
1330
        $register{$restorepath} = {
1331
                            uuid=>$newuuid,
1332
                            status=>$uistatus,
1333
                            name=>"Files from: $name",
1334
                            size=>0,
1335
                            realsize=>0,
1336
                            virtualsize=>0,
1337
                            type=>"iso",
1338
                            user=>$user
1339
                        };
1340

    
1341
        eval {
1342
                my $oldstatus = $register{$path}->{'status'};
1343
#                my $cmd = qq|steamExec $user $uistatus $oldstatus "$path" "$curfiles"|;
1344
#                my $cmdres = `$cmd`;
1345
            if ($mounted) {
1346
                $res .= "Restoring files to: /tmp/restore/$user/$bname$suffix -> $restorepath\n";
1347
                $res .= `/bin/echo $status > "$restorepath.meta"`;
1348

    
1349
                `/bin/mkdir -p "/tmp/restore/$user/$bname$suffix"` unless (-e "/tmp/restore/$user/$bname$suffix");
1350
                my @files = split(/:/, uri_unescape($curfiles));
1351
                foreach $f (@files) {
1352
                    if (-e "$mountpath$f" && chdir($mountpath)) {
1353
                        $f = substr($f,1) if ($f =~ /^\//);
1354
                        eval {`/usr/bin/rsync -aR --sparse "$f" /tmp/restore/$user/$bname$suffix`; 1;}
1355
                            or do {$e=1; $res .= "ERROR Problem restoring files $@\n";};
1356
                    } else {
1357
                        $res .= "Status=Error $f not found in $mountpath\n";
1358
                    }
1359
                }
1360
                if (chdir "/tmp/restore/$user/$bname$suffix") {
1361
                    eval {$res .= `/usr/bin/genisoimage -o "$restorepath" -iso-level 4 .`; 1;}
1362
                        or do {$e=1; $res .= "Status=ERROR Problem restoring files $@\n";};
1363
                    $res .= `/bin/rm -rf /tmp/restore/$user/$bname$suffix`;
1364
                    $res .= "Status=OK Restored files from /tmp/restore/$user/$bname$suffix to $restorepath\n";
1365
                } else {
1366
                    $res .= "Status=ERROR Unable to chdir to /tmp/restore/$user/$bname$suffix\n";
1367
                }
1368
                $main::updateUI->({tab=>"images", user=>$user, type=>"update"});
1369

    
1370
                # Update billing
1371
                my $newvirtualsize = getVirtualSize($restorepath);
1372
                unlink "$restorepath.meta";
1373
                $res .= Unmount($path);
1374
                $register{$restorepath}->{'status'} = 'unused';
1375
                $register{$restorepath}->{'virtualsize'} = $newvirtualsize;
1376
                $register{$restorepath}->{'realsize'} = $newvirtualsize;
1377
                $register{$restorepath}->{'size'} = $newvirtualsize;
1378
                $postmsg = "OK - restored your files into a new ISO.";
1379
            } else {
1380
                $res .= "Status=Error You must mount image on $mountpath before restoring\n";
1381
            }
1382
            $res .=  "Status=OK $uistatus files from $name to iso, $newuuid, $cmd\n";
1383
            $main::syslogit->($user, "info", "$uistatus files from $path to iso, $newuuid");
1384
            1;
1385
        } or do {$res .= "Status=ERROR $@\n";}
1386

    
1387
    } else {
1388
        $res .= "Status=ERROR Image not mounted, mount before restoring: ". $curfiles ."\n";
1389
    }
1390
    return $res;
1391
}
1392

    
1393
sub trim{
1394
   my $string = shift;
1395
   $string =~ s/^\s+|\s+$//g;
1396
   return $string;
1397
}
1398

    
1399
sub do_overquota {
1400
    my ($path, $action, $obj) = @_;
1401
    if ($help) {
1402
        return <<END
1403
GET:inc,onnode:
1404
Check if 'inc' bytes will bring you over your storage quota. Set onnode to 1 to check node storage quota.
1405
END
1406
    }
1407
    if (overQuotas($obj->{inc}, $obj->{onnode})) {
1408
        return "Status=Error Over storage quota\n";
1409
    } else {
1410
        return "Status=OK Not over storage quota\n";
1411
    }
1412
}
1413

    
1414
sub overQuotas {
1415
    my $inc = shift;
1416
    my $onnode = shift;
1417
	my $usedstorage = 0;
1418
	my $overquota = 0;
1419
    return 0 if ($Stabile::userprivileges =~ /a/); # Don't enforce quotas for admins
1420
	my $storagequota = ($onnode)?$Stabile::usernodestoragequota:$Stabile::userstoragequota;
1421

    
1422
	if (!$storagequota) { # 0 or empty quota means use defaults
1423
        $storagequota = (($onnode)?$Stabile::config->get('NODESTORAGE_QUOTA'):$Stabile::config->get('STORAGE_QUOTA')) + 0;
1424
	}
1425
    return 0 if ($storagequota == -1); # -1 means no quota
1426

    
1427
    my @regkeys = (tied %register)->select_where("user = '$user'");
1428
    foreach my $k (@regkeys) {
1429
        my $val = $register{$k};
1430
		if ($val->{'user'} eq $user) {
1431
		    $usedstorage += $val->{'virtualsize'} if ((!$onnode &&  $val->{'storagepool'}!=-1) || ($onnode &&  $val->{'storagepool'}==-1));
1432
		}
1433
	}
1434
    if ($usedstorage+$inc > $storagequota * 1024 *1024) {
1435
        $overquota = $usedstorage+$inc;
1436
    }
1437
	return $overquota;
1438
}
1439

    
1440
sub overStorage {
1441
    my ($reqstor, $spool, $mac) = @_;
1442
    my $storfree;
1443
    if ($spool == -1) {
1444
        if ($mac) {
1445
            unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
1446
            $storfree = $nodereg{$mac}->{'storfree'};
1447
            $storfree = $storfree *1024 * $nodestorageovercommission;
1448
            untie %nodereg;
1449
        } else {
1450
            return 1;
1451
        }
1452
    } else {
1453
        my $storpath = $spools[$spool]->{'path'};
1454
        $storfree = `df $storpath`;
1455
        $storfree =~ m/(\d\d\d\d+)(\s+)(\d\d*)(\s+)(\d\d+)(\s+)(\S+)/i;
1456
        my $stortotal = $1;
1457
        my $storused = $3;
1458
        $storfree = $5 *1024;
1459
    }
1460
    return ($reqstor > $storfree);
1461
}
1462

    
1463
sub updateBilling {
1464
    my $event = shift;
1465
    my %billing;
1466

    
1467
    my @regkeys = (tied %register)->select_where("user = '$user'");
1468
    foreach my $k (@regkeys) {
1469
        my $valref = $register{$k};
1470
        my %val = %{$valref}; # Deference and assign to new array, effectively cloning object
1471
        $val{'virtualsize'} += 0;
1472
        $val{'realsize'} += 0;
1473
        $val{'backupsize'} += 0;
1474

    
1475
        if ($val{'user'} eq $user && (defined $spools[$val{'storagepool'}]->{'id'} || $val{'storagepool'}==-1)) {
1476
            $billing{$val{'storagepool'}}->{'virtualsize'} += $val{'virtualsize'};
1477
            $billing{$val{'storagepool'}}->{'realsize'} += $val{'realsize'};
1478
            $billing{$val{'storagepool'}}->{'backupsize'} += $val{'backupsize'};
1479
        }
1480
    }
1481

    
1482
    my %billingreg;
1483

    
1484
    unless (tie %billingreg,'Tie::DBI', {
1485
            db=>'mysql:steamregister',
1486
            table=>'billing_images',
1487
            key=>'userstoragepooltime',
1488
            autocommit=>0,
1489
            CLOBBER=>3,
1490
            user=>$dbiuser,
1491
            password=>$dbipasswd}) {throw Error::Simple("Stroke=Error Billing register (images) could not be accessed")};
1492

    
1493
    my $monthtimestamp = timelocal(0,0,0,1,$mon,$year); #$sec,$min,$hour,$mday,$mon,$year
1494

    
1495
    unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'billing_images', key=>'userstoragepooltime'}, $Stabile::dbopts)) )
1496
        {throw Error::Simple("Status=Error Billing register could not be accessed")};
1497

    
1498
    my %pool = ("hostpath", "--",
1499
                "path", "--",
1500
                "name", "local",
1501
                "rdiffenabled", 1,
1502
                "id", -1);
1503
    my @bspools = @spools;
1504
    push @bspools, \%pool;
1505

    
1506
    foreach my $spool (@bspools) {
1507
        my $storagepool = $spool->{"id"};
1508
        my $b = $billing{$storagepool};
1509
        my $virtualsize = $b->{'virtualsize'} +0;
1510
        my $realsize = $b->{'realsize'} +0;
1511
        my $backupsize = $b->{'backupsize'} +0;
1512

    
1513
# Setting default start averages for use when no row found under the assumption that we entered a new month
1514
        my $startvirtualsizeavg = 0;
1515
        my $virtualsizeavg = 0;
1516
        my $startrealsizeavg = 0;
1517
        my $realsizeavg = 0;
1518
        my $startbackupsizeavg = 0;
1519
        my $backupsizeavg = 0;
1520
        my $starttimestamp = $current_time;
1521
# We have proably entered a new month if less than 4 hours since change of month, since this is run hourly
1522
        if ($current_time - $monthtimestamp < 4*3600) {
1523
            $starttimestamp = $monthtimestamp;
1524
            $startvirtualsizeavg = $virtualsizeavg = $virtualsize;
1525
            $startrealsizeavg = $realsizeavg = $realsize;
1526
            $startbackupsizeavg = $backupsizeavg = $backupsize;
1527
        }
1528
        # Update existing row
1529
        if ($billingreg{"$user-$storagepool-$year-$month"}) {
1530
            if (
1531
                ($virtualsize != $billingreg{"$user-$storagepool-$year-$month"}->{'virtualsize'})
1532
                || ($realsize != $billingreg{"$user-$storagepool-$year-$month"}->{'realsize'})
1533
                || ($backupsize != $billingreg{"$user-$storagepool-$year-$month"}->{'backupsize'})
1534
            )
1535
            {
1536
            # Sizes changed, update start averages and time, i.e. move the marker
1537
            # Averages and start averages are the same when a change has occurred
1538
                $startvirtualsizeavg = $virtualsizeavg = $billingreg{"$user-$storagepool-$year-$month"}->{'virtualsizeavg'};
1539
                $startrealsizeavg = $realsizeavg = $billingreg{"$user-$storagepool-$year-$month"}->{'realsizeavg'};
1540
                $startbackupsizeavg = $backupsizeavg = $billingreg{"$user-$storagepool-$year-$month"}->{'backupsizeavg'};
1541
                $starttimestamp = $current_time;
1542
            } else {
1543
            # Update averages and timestamp when no change on existing row
1544
                $startvirtualsizeavg = $billingreg{"$user-$storagepool-$year-$month"}->{'startvirtualsizeavg'};
1545
                $startrealsizeavg = $billingreg{"$user-$storagepool-$year-$month"}->{'startrealsizeavg'};
1546
                $startbackupsizeavg = $billingreg{"$user-$storagepool-$year-$month"}->{'startbackupsizeavg'};
1547
                $starttimestamp = $billingreg{"$user-$storagepool-$year-$month"}->{'starttimestamp'};
1548

    
1549
                $virtualsizeavg = ($startvirtualsizeavg*($starttimestamp - $monthtimestamp) + $virtualsize*($current_time - $starttimestamp)) /
1550
                                ($current_time - $monthtimestamp);
1551
                $realsizeavg = ($startrealsizeavg*($starttimestamp - $monthtimestamp) + $realsize*($current_time - $starttimestamp)) /
1552
                                ($current_time - $monthtimestamp);
1553
                $backupsizeavg = ($startbackupsizeavg*($starttimestamp - $monthtimestamp) + $backupsize*($current_time - $starttimestamp)) /
1554
                                ($current_time - $monthtimestamp);
1555
            }
1556
            # Update sizes in DB
1557
                $billingreg{"$user-$storagepool-$year-$month"}->{'virtualsize'} = $virtualsize;
1558
                $billingreg{"$user-$storagepool-$year-$month"}->{'realsize'} = $realsize;
1559
                $billingreg{"$user-$storagepool-$year-$month"}->{'backupsize'} = $backupsize;
1560
            # Update start averages
1561
                $billingreg{"$user-$storagepool-$year-$month"}->{'startvirtualsizeavg'} = $startvirtualsizeavg;
1562
                $billingreg{"$user-$storagepool-$year-$month"}->{'startrealsizeavg'} = $startrealsizeavg;
1563
                $billingreg{"$user-$storagepool-$year-$month"}->{'startbackupsizeavg'} = $startbackupsizeavg;
1564
            # Update current averages with values just calculated
1565
                $billingreg{"$user-$storagepool-$year-$month"}->{'virtualsizeavg'} = $virtualsizeavg;
1566
                $billingreg{"$user-$storagepool-$year-$month"}->{'realsizeavg'} = $realsizeavg;
1567
                $billingreg{"$user-$storagepool-$year-$month"}->{'backupsizeavg'} = $backupsizeavg;
1568
            # Update time stamps and inc
1569
                $billingreg{"$user-$storagepool-$year-$month"}->{'timestamp'} = $current_time;
1570
                $billingreg{"$user-$storagepool-$year-$month"}->{'starttimestamp'} = $starttimestamp;
1571
                $billingreg{"$user-$storagepool-$year-$month"}->{'inc'}++;
1572

    
1573
        # Write new row
1574
        } else {
1575
            $billingreg{"$user-$storagepool-$year-$month"} = {
1576
                virtualsize=>$virtualsize+0,
1577
                realsize=>$realsize+0,
1578
                backupsize=>$backupsize+0,
1579

    
1580
                virtualsizeavg=>$virtualsizeavg,
1581
                realsizeavg=>$realsizeavg,
1582
                backupsizeavg=>$backupsizeavg,
1583

    
1584
                startvirtualsizeavg=>$startvirtualsizeavg,
1585
                startrealsizeavg=>$startrealsizeavg,
1586
                startbackupsizeavg=>$startbackupsizeavg,
1587

    
1588
                timestamp=>$current_time,
1589
                starttimestamp=>$starttimestamp,
1590
                event=>$event,
1591
                inc=>1,
1592
            };
1593
        }
1594
    }
1595
    tied(%billingreg)->commit;
1596
    untie %billingreg;
1597
}
1598

    
1599
sub Removeuserimages {
1600
    my ($path, $action, $obj) = @_;
1601
    if ($help) {
1602
        return <<END
1603
GET::
1604
Removes all images belonging to a user from storage, i.e. completely deletes the image and its backups (be careful).
1605
END
1606
    }
1607

    
1608
    $postreply = removeUserImages($user) unless ($isreadonly);
1609
    return $postreply;
1610
}
1611

    
1612
sub removeUserImages {
1613
    my $username = shift;
1614
    return unless ($username && ($isadmin || $user eq $username) && !$isreadonly);
1615
    $user = $username;
1616
    foreach my $path (keys %register) {
1617
        if ($register{$path}->{'user'} eq $user) {
1618
            $postreply .=  "Status=OK Removing " . ($Stabile::preserveimagesonremove?"(preserving) ":"") . " $username image $register{$path}->{'name'}, $register{$path}->{'uuid'}" . ($console?'':'<br>') . "\n";
1619
            Remove($path, 'remove', 0, $Stabile::preserveimagesonremove);
1620
        }
1621
    }
1622
    $postreply .= "Status=Error No storage pools!\n" unless (@spools);
1623
    foreach my $spool (@spools) {
1624
        my $pooldir = $spool->{"path"};
1625
        unless (-e $pooldir) {
1626
            $postreply .= "Status=Error Storage $pooldir, $spool->{name} does not exist\n" unless (@spools);
1627
            next;
1628
        }
1629

    
1630
        $postreply .= "Status=OK Removing user dir $pooldir/$username ";
1631
        $postreply .= `/bin/rm "$pooldir/$username/.htaccess"` if (-e "$pooldir/$username/.htaccess");
1632
        $postreply .= `/bin/rmdir --ignore-fail-on-non-empty "$pooldir/$username/fuel"` if (-e "$pooldir/$username/fuel");
1633
        $postreply .= `/bin/rmdir --ignore-fail-on-non-empty "$pooldir/$username"` if (-e "$pooldir/$username");
1634
        $postreply .= "\n";
1635
    }
1636

    
1637
    unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
1638

    
1639
    foreach $mac (keys %nodereg) {
1640
        $macip = $nodereg{$mac}->{'ip'};
1641
        my $esc_path = "/mnt/stabile/node/$username";
1642
        $esc_path =~ s/([ ])/\\$1/g;
1643
        if (!$Stabile::preserveimagesonremove) {
1644
            `$sshcmd $macip "/bin/rmdir $esc_path"`;
1645
            $postreply .= "Status=OK Removing node user dir /mnt/stabile/node/$username on node $mac\n";
1646
        }
1647
    }
1648
    untie %nodereg;
1649

    
1650
    return $postreply;
1651
}
1652

    
1653
sub Remove {
1654
    my ($path, $action, $obj, $preserve, $mac) = @_;
1655
    if ($help) {
1656
        return <<END
1657
DELETE:image,mac:
1658
Removes an image from storage, i.e. completely deletes the image and its backups (be careful).
1659
END
1660
    }
1661
    $path = $imagereg{$path}->{'path'} if ($imagereg{$path}); # Check if we were passed a uuid
1662
    $path = $curimg if (!$path && $register{$curimg});
1663
    if (!$curimg && $path && !($path =~ /^\//)) {
1664
        $curimg = $path;
1665
        $path = '';
1666
    }
1667
    if (!$path && $curimg && !($curimg =~ /\//) ) { # Allow passing only image name if we are deleting an app master
1668
        my $dspool = $stackspool;
1669
        $dspool = $spools[0]->{'path'} unless ($engineid eq $valve001id);
1670
        if ($curimg =~ /\.master.qcow2$/ && $register{"$dspool/$user/$curimg"}) {
1671
            $path = "$dspool/$user/$curimg";
1672
        } elsif ($isadmin && $curimg =~ /\.master.qcow2$/ && $register{"$dspool/common/$curimg"}) {
1673
            $path = "$dspool/common/$curimg";
1674
        }
1675
    }
1676
    utf8::decode($path);
1677

    
1678
    my $img = $register{$path};
1679
    my $status = $img->{'status'};
1680
    $mac = $mac || $obj->{mac} || $img->{'mac'}; # Remove an image from a specific node
1681
    my $name = $img->{'name'};
1682
    my $uuid = $img->{'uuid'};
1683
    utf8::decode($name);
1684
    my $type = $img->{'type'};
1685
    my $username = $img->{'user'};
1686

    
1687
    unless ($username && ($isadmin || $user eq $username) && !$isreadonly) {
1688
        return qq|[]|;
1689
#        $postmsg = "Cannot delete image";
1690
#        $postreply .= "Status=Error $postmsg\n";
1691
#        return $postreply;
1692
    }
1693

    
1694
    $uistatus = "deleting";
1695
    if ($status eq "unused" || $status eq "uploading" || $path =~ /(.+)\.master\.$type/) {
1696
        my $haschildren;
1697
        my $child;
1698
        my $hasprimary;
1699
        my $primary;
1700
        my $master = ($img->{'master'} && $img->{'master'} ne '--')?$img->{'master'}:'';
1701
        my $usedmaster = '';
1702
        my @regvalues = values %register;
1703
        foreach my $valref (@regvalues) {
1704
            if ($valref->{'master'} eq $path) {
1705
                $haschildren = 1;
1706
                $child = $valref->{'name'};
1707
            #    last;
1708
            }
1709
            if ($master) {
1710
                $usedmaster = 1 if ($valref->{'master'} eq $master && $valref->{'path'} ne $path); # Check if another image is also using this master
1711
            }
1712
        }
1713
        if ($master && !$usedmaster && $register{$master}) {
1714
            $register{$master}->{'status'} = 'unused';
1715
            $main::syslogit->($user, "info", "Freeing master $master");
1716
        }
1717
        if ($type eq "qcow2") {
1718
            my @regkeys = (tied %register)->select_where("image2 = '$path'");
1719
            foreach my $k (@regkeys) {
1720
                my $val = $register{$k};
1721
                if ($val->{'image2'} eq $path) {
1722
                    $hasprimary = 1;
1723
                    $primary = $val->{'name'};
1724
                    last;
1725
                }
1726
            }
1727
        }
1728

    
1729
        if ($haschildren) {
1730
            $postmsg = "Cannot delete image. This image is used as master by: $child";
1731
            $postreply .= "Status=Error $postmsg\n";
1732
#        } elsif ($hasprimary) {
1733
#            $postmsg = "Cannot delete image. This image is used as secondary image by: $primary";
1734
#            $postreply .= "Status=Error $postmsg\n";
1735
        } else {
1736
            if ($mac && $path =~ /\/mnt\/stabile\/node\//) {
1737
                unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return "Status=Error Cannot connect to DB\n";};
1738
                $macip = $nodereg{$mac}->{'ip'};
1739
                my $wakenode = ($nodereg{$mac}->{'status'} eq 'asleep' || $nodereg{$mac}->{'status'} eq 'waking');
1740

    
1741
                if ($wakenode) {
1742
                    my $tasks = $nodereg{$mac}->{'tasks'};
1743
                    my $upath = URI::Escape::uri_escape($path);
1744
                    $tasks .= "REMOVE $upath $user\n";
1745
                    $nodereg{$mac}->{'tasks'} = $tasks;
1746
                    tied(%nodereg)->commit;
1747
                    $postmsg = "We are waking up the node your image $name is on - it will be removed shortly";
1748
                    if ($nodereg{$mac}->{'status'} eq 'asleep') {
1749
                        require "$Stabile::basedir/cgi/nodes.cgi";
1750
                        $Stabile::Nodes::console = 1;
1751
                        Stabile::Nodes::wake($mac);
1752
                    }
1753
                    $register{$path}->{'status'} = $uistatus;
1754
                } else {
1755
                    my $esc_path = $path;
1756
                    $esc_path =~ s/([ ])/\\$1/g;
1757
                    if ($preserve) {
1758
                        `$sshcmd $macip "/bin/mv $esc_path $esc_path.bak"`;
1759
                    } else {
1760
                        `$sshcmd $macip "/usr/bin/unlink $esc_path"`;
1761
                    }
1762
                    `$sshcmd $macip "/usr/bin/unlink $esc_path.meta"`;
1763
                    delete $register{$path};
1764
                }
1765
                untie %nodereg;
1766

    
1767
            } else {
1768
                if ($preserve) {
1769
                    `/bin/mv "$path" "$path.bak"`;
1770
                } else {
1771
                    unlink $path;
1772
                }
1773
                if (substr($path,-5) eq '.vmdk') {
1774
                    if ( -s (substr($path,0,-5) . "-flat.vmdk")) {
1775
                        my $flat = substr($path,0,-5) . "-flat.vmdk";
1776
                        if ($preserve) {
1777
                            `/bin/mv $flat "$flat.bak"`;
1778
                        } else {
1779
                            unlink($flat);
1780
                        }
1781
                    } elsif ( -e (substr($path,0,-5) . "-s001.vmdk")) {
1782
                        my $i = 1;
1783
                        my $rmpath = substr($path,0,-5);
1784
                        while (-e "$rmpath-s00$i.vmdk") {
1785
                            if ($preserve) {
1786
                                `/bin/mv "$rmpath-s00$i.vmdk" "$rmpath-s00$i.vmdk.bak"`;
1787
                            } else {
1788
                                unlink("$rmpath-s00$i.vmdk");
1789
                            }
1790
                            $i++;
1791
                        }
1792
                    }
1793
                }
1794
                unlink "$path.meta" if (-e "$path.meta");
1795
                delete $register{$path};
1796
            }
1797

    
1798
            my $subdir = "";
1799
            my($bname, $dirpath) = fileparse($path);
1800
            if ($dirpath =~ /.+\/$buser(\/.+)?\//) {
1801
                $subdir = $1;
1802
            }
1803
            my $bpath = "$backupdir/$user$subdir/$bname";
1804
            $bpath = $1 if ($bpath =~ /(.+)/);
1805
            # Remove backup of image if it exists
1806
            if (-d "$bpath") {
1807
                `/bin/rm -rf "$bpath"`;
1808
            }
1809

    
1810
#            $postmsg = "Deleted image $name ($path, $uuid, $mac)";
1811
#            $postreply =  "[]";
1812
#            $postreply .=  "Status=deleting OK $postmsg\n";
1813
            updateBilling("delete $path");
1814
            $main::syslogit->($user, "info", "$uistatus $type image: $name: $path");
1815
            if ($status eq 'downloading') {
1816
                my $daemon = Proc::Daemon->new(
1817
                    work_dir => '/usr/local/bin',
1818
                    exec_command => qq|pkill -f "$path"|
1819
                ) or do {$postreply .= "Status=ERROR $@\n";};
1820
                my $pid = $daemon->Init();
1821
            }
1822
            sleep 1;
1823
        }
1824
    } else {
1825
        $postmsg = "Cannot delete $type image with status: $status";
1826
        $postreply .= "Status=ERROR $postmsg\n";
1827
    }
1828
    return "[]";
1829
}
1830

    
1831
# Clone image $path to destination storage pool $istoragepool, possibly changing backup schedule $bschedule
1832
sub Clone {
1833
    my ($path, $action, $obj, $istoragepool, $imac, $name, $bschedule, $buildsystem, $managementlink, $appid, $wait, $vcpu, $mem) = @_;
1834
    if ($help) {
1835
        return <<END
1836
GET:image,name,storagepool,wait:
1837
Clones an image. In the case of cloning a master image, a child is produced.
1838
Only cloning to same storagepool is supported, with the exception of cloning to nodes (storagepool -1).
1839
If you want to perform the clone synchronously, set wait to 1;
1840
END
1841
    }
1842
    $postreply = "" if ($buildsystem);
1843
    return "Status=Error no valid user\n" unless ($user);
1844

    
1845
    unless ($register{$path} && ($register{$path}->{'user'} eq $user
1846
                || $register{$path}->{'user'} eq 'common'
1847
                || $register{$path}->{'user'} eq $billto
1848
                || $register{$path}->{'user'} eq $Stabile::Systems::billto
1849
                || $isadmin)) {
1850
        $postreply .= "Status=ERROR Cannot clone!\n";
1851
        return;
1852
    }
1853
    $istoragepool = $istoragepool || $obj->{storagepool};
1854
    $name = $name || $obj->{name};
1855
    $wait = $wait || $obj->{wait};
1856
    my $status = $register{$path}->{'status'};
1857
    my $type = $register{$path}->{'type'};
1858
    my $master = $register{$path}->{'master'};
1859
    my $notes = $register{$path}->{'notes'};
1860
    my $image2 = $register{$path}->{'image2'};
1861
    my $snap1 = $register{$path}->{'snap1'};
1862
    $managementlink = $register{$path}->{'managementlink'} unless ($managementlink);
1863
    $appid = $register{$path}->{'appid'} unless ($appid);
1864
    my $upgradelink = $register{$path}->{'upgradelink'} || '';
1865
    my $terminallink = $register{$path}->{'terminallink'} || '';
1866
    my $version = $register{$path}->{'version'} || '';
1867
    my $regmac = $register{$path}->{'mac'};
1868

    
1869
    my $virtualsize = $register{$path}->{'virtualsize'};
1870
    my $dindex = 0;
1871

    
1872
    my($bname, $dirpath, $suffix) = fileparse($path, (".vmdk", ".img", ".vhd", ".vhdx", ".qcow", ".qcow2", ".vdi", ".iso"));
1873
    $path =~ /(.+)\.$type/;
1874
    my $namepath = $1;
1875
    if ($namepath =~ /(.+)\.master/) {
1876
        $namepath = $1;
1877
    }
1878
    if ($namepath =~ /(.+)\.clone\d+/) {
1879
        $namepath = $1;
1880
    }
1881
    if ($namepath =~ /.+\/common\/(.+)/) { # Support one subdir
1882
        $namepath = $1;
1883
    } elsif ($namepath =~ /.+\/$user\/(.+)/) { # Support one subdir
1884
        $namepath = $1;
1885
    } elsif ($namepath =~ /.+\/(.+)/) { # Extract only the name
1886
        $namepath = $1;
1887
    }
1888

    
1889
    # Find unique path in DB across storage pools
1890
    my $upath;
1891
    my $npath = "/mnt/stabile/node/$user/$namepath"; # Also check for uniqueness on nodes
1892
    my $i = 1;
1893
    foreach my $spool (@spools) {
1894
        $upath = $spool->{'path'} . "/$user/$namepath";
1895
        while ($register{"$upath.clone$i.$type"} || $register{"$npath.clone$i.$type"}) {$i++;};
1896
    }
1897
    $upath = "$spools[$istoragepool]->{'path'}/$user/$namepath";
1898

    
1899
    my $iname = $register{$path}->{'name'};
1900
    $iname = "$name" if ($name); # Used when name supplied when building a system
1901
    $iname =~ /(.+)( \(master\))/;
1902
    $iname = $1 if $2;
1903
    $iname =~ /(.+)( \(clone\d*\))/;
1904
    $iname = $1 if $2;
1905
    $iname =~ /(.+)( \(child\d*\))/;
1906
    $iname = $1 if $2;
1907
    my $ippath = $path;
1908
    my $macip;
1909
    my $ug = new Data::UUID;
1910
    my $newuuid = $ug->create_str();
1911
    my $wakenode;
1912
    my $identity;
1913

    
1914
    # We only support cloning images to nodes - not the other way round
1915
    if ($imac && $regmac && $imac ne $regmac) {
1916
        $postreply .= "Status=ERROR Cloning from a node not supported\n";
1917
        return $postreply;
1918
    }
1919

    
1920
    if ($istoragepool==-1) {
1921
    # Find the ip address of target node
1922
        ($imac, $macip, $dindex, $wakenode, $identity) = locateNode($virtualsize, $imac, $vcpu, $mem);
1923
        if ($identity eq 'local_kvm') {
1924
            $postreply .= "Status=OK Cloning to local node with index: $dindex\n";
1925
            $istoragepool = 0; # cloning to local node
1926
            $upath = "$spools[$istoragepool]->{'path'}/$user/$namepath";
1927
        } elsif (!$macip) {
1928
            $postreply .= "Status=ERROR Unable to locate node with sufficient ressources\n";
1929
            $postmsg = "Unable to locate node with sufficient ressources!";
1930
            $main::updateUI->({tab=>"images", user=>$user, type=>"message", message=>$postmsg});
1931
            return $postreply;
1932
        } else {
1933
            $postreply .= "Status=OK Cloning to $macip with index: $dindex\n";
1934
            $ippath = "$macip:$path";
1935
            $upath = "/mnt/stabile/node/$user/$namepath";
1936
        }
1937
    }
1938
    my $ipath = "$upath.clone$i.$type";
1939

    
1940
    if ($bschedule eq 'daily7' || $bschedule eq 'daily14') {
1941
         $bschedule = "manually" if ($istoragepool!=-1 && (!$spools[$istoragepool]->{'rdiffenabled'} || !$spools[$istoragepool]->{'lvm'}));
1942
    } elsif ($bschedule ne 'manually') {
1943
        $bschedule = '';
1944
    }
1945

    
1946
# Find storage pool with space
1947
    my $foundstorage = 1;
1948
    if (overStorage($virtualsize, $istoragepool, $imac)) {
1949
        $foundstorage = 0;
1950
        foreach my $p (@spools) {
1951
            if (overStorage($virtualsize, $p->{'id'}, $imac)) {
1952
                ;
1953
            } else {
1954
                $istoragepool = $p->{'id'};
1955
                $foundstorage = 1;
1956
                last;
1957
            }
1958
        }
1959
    }
1960

    
1961
# We allow multiple clone operations on master images
1962
    if ($status ne "used" && $status ne "unused" && $status ne "paused" && $path !~ /(.+)\.master\.$type/) {
1963
        $postreply .= "Status=ERROR Please shut down your virtual machine before cloning\n";
1964

    
1965
    } elsif ($type eq 'vmdk' && (-e "$dirpath$bname-s001$suffix" || -e "$dirpath$bname-flat$suffix")) {
1966
        $postreply .= "Status=ERROR Cannot clone this image - please convert first!\n";
1967

    
1968
    } elsif (overQuotas($virtualsize, ($istoragepool==-1))) {
1969
        $postreply .= "Status=ERROR Over quota (". overQuotas($virtualsize, ($istoragepool==-1)) . ") cloning: $name\n";
1970

    
1971
    } elsif (!$foundstorage) {
1972
        $postreply .= "Status=ERROR Not enough storage ($virtualsize) in destination pool $istoragepool $imac cloning: $name\n";
1973

    
1974
    } elsif ($wakenode && !($path =~ /(.+)\.master\.$type/)) { # For now we dont support simply copying images on sleeping nodes
1975
        $postreply .= "Status=ERROR We are waking up the node your image $name is on, please try again later\n";
1976
        require "$Stabile::basedir/cgi/nodes.cgi";
1977
        $Stabile::Nodes::console = 1;
1978
        Stabile::Nodes::wake($imac);
1979
    } elsif ($type eq "img" || $type eq "qcow2" || $type eq "vmdk") {
1980
        my $masterimage2 = $register{"$path"}->{'image2'};
1981
    # Cloning a master produces a child
1982
        if ($type eq "qcow2" && $path =~ /(.+)\.master\.$type/) {
1983
            $uistatus = "cloning";
1984
    # VBoxManage probably does a more efficient job at cloning than simply copying
1985
        } elsif ($type eq "vdi" || $type eq "vhd" || $type eq "vhdx") {
1986
            $uistatus = "vcloning";
1987
    # Cloning another child produces a sibling with the same master
1988
        } else {
1989
            $uistatus = "copying";
1990
        }
1991
        $uipath = $path;
1992
        eval {
1993
            $register{$ipath} = {
1994
                uuid=>$newuuid,
1995
                master=>($uistatus eq 'cloning')?$path:$master,
1996
                name=>"$iname (clone$i)",
1997
                notes=>$notes,
1998
                image2=>$image2,
1999
                snap1=>($uistatus eq 'copying')?$snap1:'',
2000
                storagepool=>$istoragepool,
2001
                status=>$uistatus,
2002
                mac=>($istoragepool == -1)?$imac:"",
2003
                size=>0,
2004
                realsize=>0,
2005
                virtualsize=>$virtualsize,
2006
                bschedule=>$bschedule,
2007
                type=>"qcow2",
2008
                created=>$current_time,
2009
                user=>$user
2010
            };
2011
            $register{$ipath}->{'managementlink'} = $managementlink if ($managementlink);
2012
            $register{$ipath}->{'appid'} = $appid if ($appid);
2013
            $register{$ipath}->{'upgradelink'} = $upgradelink if ($upgradelink);
2014
            $register{$ipath}->{'terminallink'} = $terminallink if ($terminallink);
2015
            $register{$ipath}->{'version'} = $version if ($version);
2016
            $register{$path}->{'status'} = $uistatus;
2017
            my $dstatus = ($buildsystem)?'bcloning':$uistatus;
2018
            if ($wakenode) { # We are waking a node for clone operation, so ask movepiston to do the work
2019
                unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
2020
                my $tasks = $nodereg{$imac}->{'tasks'};
2021
                $upath = URI::Escape::uri_escape($ipath);
2022
                $tasks .= "BCLONE $upath $user\n";
2023
                $nodereg{$imac}->{'tasks'} = $tasks;
2024
                tied(%nodereg)->commit;
2025
                untie %nodereg;
2026
            } elsif ($wait) {
2027
                my $cmd = qq|steamExec $user $dstatus $status "$ippath" "$ipath"|;
2028
                $cmd = $1 if ($cmd =~ /(.+)/);
2029
                `$cmd`;
2030
            } else {
2031
                my $daemon = Proc::Daemon->new(
2032
                        work_dir => '/usr/local/bin',
2033
                        exec_command => "perl -U steamExec $user $dstatus $status \"$ippath\" \"$ipath\""
2034
                    ) or do {$postreply .= "Status=ERROR $@\n";};
2035
                my $pid = $daemon->Init();
2036
            }
2037
            $postreply .= "Status=$uistatus OK $uistatus to: $iname (clone$i)" . ($isadmin? " -> $ipath ":"") . "\n";
2038
            $postreply .= "Status=OK uuid: $newuuid\n"; # if ($console || $api);
2039
            $postreply .= "Status=OK path: $ipath\n"; # if ($console || $api);
2040
            $postreply .= "Status=OK mac: $imac\n"; # if ($console || $api);
2041
            $postreply .= "Status=OK wakenode: $wakenode\n"; # if ($console || $api);
2042
            $main::syslogit->($user, "info", "$uistatus $wakenode $type image: $name $uuid to $ipath");
2043
            1;
2044
        } or do {$postreply .= "Status=ERROR $@\n";}
2045

    
2046
    } else {
2047
        $postreply .= "Status=ERROR Not a valid type: $type\n";
2048
    }
2049
    tied(%register)->commit;
2050
    $main::updateUI->({tab=>"images", user=>$user, type=>"update"});
2051
    return $postreply;
2052
}
2053

    
2054

    
2055
# Link master image to fuel
2056
sub Linkmaster {
2057
    my ($mpath, $action) = @_;
2058
    if ($help) {
2059
        return <<END
2060
GET:image:
2061
Link master image to fuel
2062
END
2063
    }
2064
    my $res;
2065

    
2066
    return "Your account does not have the necessary privileges\n" if ($isreadonly);
2067
    return "Please specify master image to link\n" unless ($mpath);
2068

    
2069
    unless ($mpath =~ /^\//) { # We did not get an absolute path, look for it in users storagepools
2070
        foreach my $p (@spools) {
2071
            my $dir = $p->{'path'};
2072
            my $cpath = "$dir/common/$mpath";
2073
            my $upath = "$dir/$user/$mpath";
2074
            if (-e $cpath) {
2075
                $mpath = $cpath;
2076
                last;
2077
            } elsif (-e $upath) {
2078
                $mpath = $upath;
2079
                last;
2080
            }
2081
        }
2082
    }
2083
    my $img = $register{$mpath};
2084
    $mpath = $img->{"path"};
2085
    $imguser = $img->{"user"};
2086
    if (!$mpath || ($imguser ne $user && $imguser ne 'common' && !$isadmin)) {
2087
        $postreply = qq|{"status": "Error", "message": "No privs. or not found @_[0]"}|;
2088
        return $postreply;
2089
    }
2090
    my $status = $img->{"status"};
2091
    my $type = $img->{"type"};
2092
    $mpath =~ /(.+)\/(.+)\.master\.$type$/;
2093
    my $namepath = $2;
2094
    my $msg;
2095
    if ($status ne "unused" && $status ne "used") {
2096
        $res .= qq|{"status": "Error", "message": "Only used and unused images may be linked ($status, $mpath)."}|;
2097
    } elsif (!( $mpath =~ /(.+)\.master\.$type$/ ) ) {
2098
        $res .= qq|{"status": "Error", "message": "You can only link master images"}|;
2099
    } elsif ($type eq "qcow2") {
2100
        my $pool = $img->{'storagepool'};
2101
        `chmod 444 "$mpath"`;
2102
        my $linkpath = $tenderpathslist[$pool] . "/$user/fuel/$namepath.link.master.$type";
2103
        my $fuellinkpath = "/mnt/fuel/pool$pool/$namepath.link.master.$type";
2104
        if (-e $tenderpathslist[$pool] . "/$user/fuel") { # master should be on fuel-enabled storage
2105
            unlink ($linkpath) if (-e $linkpath);
2106
            `ln "$mpath" "$linkpath"`;
2107
        } else {
2108
            foreach my $p (@spools) {
2109
                my $dir = $p->{'path'};
2110
                my $poolid = $p->{'id'};
2111
                if (-e "$dir/$user/fuel") {
2112
                    $linkpath = "$dir/$user/fuel/$namepath.copy.master.$type";
2113
                    $fuellinkpath = "/mnt/fuel/pool$poolid/$namepath.copy.master.$type";
2114
                    unlink ($linkpath) if (-e $linkpath);
2115
                    `cp "$mpath" "$linkpath"`;
2116
                    $msg = "Different file systems, master copied";
2117
                    last;
2118
                }
2119
            }
2120
        }
2121
        $res .= qq|{"status": "OK", "message": "$msg", "path": "$fuellinkpath", "linkpath": "$linkpath", "masterpath": "$mpath"}|;
2122
    } else {
2123
        $res .= qq|{"status": "Error", "message": "You can only link qcow2 images"}|;
2124
    }
2125
    $postreply = $res;
2126
    return $res;
2127
}
2128

    
2129
# Link master image to fuel
2130
sub unlinkMaster {
2131
    my $mpath = shift;
2132
    unless ($mpath =~ /^\//) { # We did not get an absolute path, look for it in users storagepools
2133
        foreach my $p (@spools) {
2134
            my $dir = $p->{'path'};
2135
            my $upath = "$dir/$user/fuel/$mpath";
2136
            if (-e $upath) {
2137
                $mpath = "/mnt/fuel/pool$p->{id}/$mpath";
2138
                last;
2139
            }
2140
        }
2141
    }
2142

    
2143
    $mpath =~ /\/pool(\d+)\/(.+)\.link\.master\.qcow2$/;
2144
    my $pool = $1;
2145
    my $namepath = $2;
2146
    if (!( $mpath =~ /\/pool(\d+)\/(.+)\.link\.master\.qcow2$/ ) ) {
2147
        $postreply = qq|{"status": "Error", "message": "You can only unlink linked master images ($mpath)"}|;
2148
    } else {
2149
        my $linkpath = $tenderpathslist[$pool] . "/$user/fuel/$namepath.link.master.qcow2";
2150
        if (-e $linkpath) {
2151
            `chmod 644 "$linkpath"`;
2152
            `rm "$linkpath"`;
2153
            $postreply = qq|{"status": "OK", "message": "Link removed", "path": "/mnt/fuel/pool$pool/$namepath.qcow2", "linkpath": "$linkpath"}|;
2154
        } else {
2155
            $postreply = qq|{"status": "Error", "message": "Link $linkpath does not exists."}|;
2156
        }
2157
    }
2158
}
2159

    
2160
#sub do_getstatus {
2161
#    my ($img, $action) = @_;
2162
#    if ($help) {
2163
#        return <<END
2164
#GET::
2165
#END
2166
#    }
2167
#    # Allow passing only image name if we are dealing with an app master
2168
#    my $dspool = $stackspool;
2169
#    my $masteruser = $params{'masteruser'};
2170
#    my $destuser = $params{'destuser'};
2171
#    my $destpath;
2172
#    $dspool = $spools[0]->{'path'} unless ($engineid eq $valve001id);
2173
#    if (!$register{$img} && $img && !($img =~ /\//) && $masteruser) {
2174
#        if ($img =~ /\.master\.qcow2$/ && $register{"$dspool/$masteruser/$img"}) {
2175
#            if ($ismanager || $isadmin
2176
#                || ($userreg{$masteruser}->{'billto'} eq $user)
2177
#            ) {
2178
#                $img = "$dspool/$masteruser/$img";
2179
#            }
2180
#        }
2181
#    }
2182
#    my $status = $register{$img}->{'status'};
2183
#    if ($status) {
2184
#        my $iuser = $register{$img}->{'user'};
2185
#        # First check if user is allowed to access image
2186
#        if ($iuser ne $user && $iuser ne 'common' && $userreg{$iuser}->{'billto'} ne $user) {
2187
#            $status = '' unless ($isadmin || $ismanager);
2188
#        }
2189
#        if ($destuser) { # User is OK, now check if destination exists
2190
#            my ($dest, $folder) = fileparse($img);
2191
#            $destpath = "$dspool/$destuser/$dest";
2192
#            $status = 'exists' if ($register{$destpath} || -e ($destpath));
2193
#        }
2194
#    }
2195
#    my $res;
2196
#    $res .= $Stabile::q->header('text/plain') unless ($console);
2197
#    $res .= "$status";
2198
#    return $res;
2199
#}
2200

    
2201
sub do_move {
2202
    my ($image, $action, $obj) = @_;
2203
    if ($help) {
2204
        return <<END
2205
GET:image,user,storagepool,mac,precreate:
2206
Move image to a different storage pool or user
2207
END
2208
    }
2209
    return "Your account does not have the necessary privileges\n" if ($isreadonly);
2210
#    $postreply = qq/"$curimg || $image, $obj->{user} || $user, $obj->{storagepool}, $obj->{mac}, 0, $obj->{precreate}, $nodereg->{$obj->{mac}}->{name}"/;
2211
#    return $postreply;
2212
    my $res = Move($curimg || $image, $obj->{user} || $user, $obj->{storagepool}, $obj->{mac},0, $obj->{precreate});
2213
    return header() . $res;
2214
}
2215

    
2216
sub Move {
2217
    my ($path, $iuser, $istoragepool, $mac, $force, $precreate) = @_;
2218
    # Allow passing only image name if we are deleting an app master
2219
    my $dspool = $stackspool;
2220
    my $masteruser = $params{'masteruser'};
2221
    $dspool = $spools[0]->{'path'} unless ($engineid eq $valve001id);
2222
    unless ( tie(%userreg,'Tie::DBI', Hash::Merge::merge({table=>'users', key=>'username', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
2223
    if (!$register{$path} && $path && !($path =~ /\//) && $masteruser) {
2224
        if ($path =~ /\.master\.qcow2$/ && $register{"$dspool/$masteruser/$path"}) {
2225
            if ($ismanager || $isadmin
2226
                || ($userreg{$masteruser}->{'billto'} eq $user && $iuser eq $user)
2227
                || ($masteruser eq $user && $userreg{$iuser}->{'billto'} eq $user)
2228
            ) {
2229
                $path = "$dspool/$masteruser/$path";
2230
            }
2231
        }
2232
    }
2233
    my $regimg = $register{$path};
2234
    $istoragepool = ($istoragepool eq '0' || $istoragepool)? $istoragepool: $regimg->{'storagepool'};
2235
    $mac = $mac || $regimg->{'mac'}; # destination mac
2236
    my $bschedule = $regimg->{'bschedule'};
2237
    my $name = $regimg->{'name'};
2238
    my $status = $regimg->{'status'};
2239
    my $type = $regimg->{'type'};
2240
    my $reguser = $regimg->{'user'};
2241
    my $regstoragepool = $regimg->{'storagepool'};
2242
    my $virtualsize = $regimg->{'virtualsize'};
2243

    
2244
    my $newpath;
2245
    my $newdirpath;
2246
    my $oldpath = $path;
2247
    my $olddirpath = $path;
2248
    my $newuser = $reguser;
2249
    my $newstoragepool = $regstoragepool;
2250
    my $haschildren;
2251
    my $hasprimary;
2252
    my $child;
2253
    my $primary;
2254
    my $macip;
2255
    my $alreadyexists;
2256
    my $subdir;
2257
#    $subdir = $1 if ($path =~ /\/$reguser(\/.+)\//);
2258
    $subdir = $1 if ($path =~ /.+\/$reguser(\/.+)?\//);
2259
    my $restpath;
2260
    $restpath = $1 if ($path =~ /.+\/$reguser\/(.+)/);
2261

    
2262
    if ($type eq "qcow2" && $path =~ /(.+)\.master\.$type/) {
2263
        my @regkeys = (tied %register)->select_where("master = '$path'");
2264
        foreach my $k (@regkeys) {
2265
            my $val = $register{$k};
2266
            if ($val->{'master'} eq $path) {
2267
                $haschildren = 1;
2268
                $child = $val->{'name'};
2269
                last;
2270
            }
2271
        }
2272
    }
2273
    if ($type eq "qcow2") {
2274
        my @regkeys = (tied %register)->select_where("image2 = '$path'");
2275
        foreach my $k (@regkeys) {
2276
            my $val = $register{$k};
2277
            if ($val->{'image2'} eq $path) {
2278
                $hasprimary = 1;
2279
                $primary = $val->{'name'};
2280
                last;
2281
            }
2282
        }
2283
    }
2284
    if (!$register{$path}) {
2285
        $postreply .= "Status=ERROR Unable to move $path (invalid path, $path, $masteruser)\n" unless ($istoragepool eq '--' || $regstoragepool eq '--');
2286
    } elsif ($type eq 'vmdk' && -s (substr($path,0,-5) . "-flat.vmdk") || -s (substr($path,0,-5) . "-s001.vmdk")) {
2287
        $postreply .= "Status=Error Cannot move this image. Please convert before moving\n";
2288
    } elsif ($precreate && ($register{$path}->{snap1} && $register{$path}->{snap1} ne '--') && !$force) {
2289
        $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$register{$path}->{'uuid'}, status=>$status, message=>"Please remove snapshots from image $name before stormoving server."});
2290
        $postreply .= "Status=Error Cannot stormove an image with snapshots\n";
2291
# Moving an image to a different users dir
2292
    } elsif ($iuser ne $reguser && ($status eq "unused" || $status eq "used")) {
2293
        unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'users', key=>'username', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
2294
        my @accounts = split(/,\s*/, $userreg{$tktuser}->{'accounts'});
2295
        my @accountsprivs = split(/,\s*/, $userreg{$tktuser}->{'accountsprivileges'});
2296
        %ahash = ($tktuser, $userreg{$tktuser}->{'privileges'} || 'r' ); # Include tktuser in accounts hash
2297
        for my $i (0 .. scalar @accounts)
2298
        {
2299
            next unless $accounts[$i];
2300
            $ahash{$accounts[$i]} = $accountsprivs[$i] || 'u';
2301
        }
2302

    
2303
        if ((($isadmin || $ismanager ) && $iuser eq 'common') # Check if user is allowed to access account
2304
                || ($isadmin && $userreg{$iuser})
2305
                || ($user eq $engineuser)
2306
                || ($userreg{$iuser}->{'billto'} eq $user)
2307
                || ($ahash{$iuser} && !($ahash{$iuser} =~ /r/))
2308
        ) {
2309
            if ($haschildren) {
2310
                $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$register{$path}->{'uuid'}, status=>$status, message=>"Error Cannot move image. This image is used as master by: $child"});
2311
                $postreply .= "Status=Error Cannot move image. This image is used as master by: $child\n";
2312
            } elsif ($hasprimary) {
2313
                $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$register{$path}->{'uuid'}, status=>$status, message=>"Error Cannot move image. This image is used as secondary image by: $primary"});
2314
                $postreply .= "Status=Error Cannot move image. This image is used as secondary image by: $primary\n";
2315
            } else {
2316
                if ($regstoragepool == -1) { # The image is located on a node
2317
                    my $uprivs = $userreg{$iuser}->{'privileges'};
2318
                    if ($uprivs =~ /[an]/) {
2319
                        unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
2320
                        $macip = $nodereg{$mac}->{'ip'};
2321
                        my $oldmacip = $nodereg{$regimg->{'mac'}}->{'ip'};
2322
                        untie %nodereg;
2323
                        $oldpath = "$oldmacip:/mnt/stabile/node/$reguser/$restpath";
2324
                        $newdirpath = "/mnt/stabile/node/$iuser/$restpath";
2325
                        $newpath = "$macip:$newdirpath";
2326
                        $newuser = $iuser;
2327
                        $newstoragepool = $istoragepool;
2328
                # Check if image already exists in target dir
2329
                        $alreadyexists = `ssh -l irigo -i /var/www/.ssh/id_rsa_www -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no $macip "perl -e 'print 1 if -e q{/mnt/stabile/node/$iuser/$restpath}'"`;
2330
                    } else {
2331
                        $postreply .= "Status=Error Target account $iuser cannot use node storage\n";
2332
                    }
2333
                } else {
2334
                    my $reguser = $userreg{$iuser};
2335
                    my $upools = $reguser->{'storagepools'} || $Stabile::config->get('STORAGE_POOLS_DEFAULTS') || "0";
2336
                    my @nspools = split(/, ?/, $upools);
2337
                    my %ispools = map {$_=>1} @nspools; # Build a hash with destination users storagepools
2338
                    if ($ispools{$regstoragepool}) { # Destination user has access to image's storagepool
2339
                        $newpath = "$spools[$regstoragepool]->{'path'}/$iuser/$restpath";
2340
                    } else {
2341
                        $newpath = "$spools[0]->{'path'}/$iuser/$restpath";
2342
                    }
2343
                    $newdirpath = $newpath;
2344
                    $newuser = $iuser;
2345
            # Check if image already exists in target dir
2346
                    $alreadyexists = -e $newpath;
2347
                }
2348
            }
2349
        } else {
2350
            $postreply .= "Status=Error Cannot move image to account $iuser $ahash{$iuser} - not allowed\n";
2351
        }
2352
# Moving an image to a different storage pool
2353
    } elsif ($istoragepool ne '--' &&  $regstoragepool ne '--' && $istoragepool ne $regstoragepool
2354
            && ($status eq "unused" || $status eq "used" || $status eq "paused" || ($status eq "active" && $precreate))) {
2355

    
2356
        my $dindex;
2357
        my $wakenode;
2358
        if ($istoragepool == -1 && $regstoragepool != -1) {
2359
            ($mac, $macip, $dindex, $wakenode) = locateNode($virtualsize, $mac);
2360
        }
2361

    
2362
        $main::syslogit->($user, "info", "Moving $name from $regstoragepool to $istoragepool $macip $wakenode");
2363

    
2364
        if ($haschildren) {
2365
            $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$register{$path}->{'uuid'}, status=>$status, message=>"ERROR Unable to move $name (has children)"});
2366
            $postreply .= "Status=ERROR Unable to move $name (has children)\n";
2367
        } elsif ($hasprimary) {
2368
            $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$register{$path}->{'uuid'}, status=>$status, message=>"Error Cannot move image. This image is used as secondary image by: $primary"});
2369
            $postreply .= "Status=Error Cannot move image. This image is used as secondary image by: $primary\n";
2370
        } elsif ($wakenode) {
2371
            $postreply .= "Status=ERROR All available nodes are asleep moving $name, waking $mac, please try again later\n";
2372
            $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$register{$path}->{'uuid'}, status=>$status, message=>"All available nodes are asleep moving $name, waking $mac, please try again later"});
2373
            require "$Stabile::basedir/cgi/nodes.cgi";
2374
            $Stabile::Nodes::console = 1;
2375
            Stabile::Nodes::wake($mac);
2376
        } elsif (overStorage($virtualsize, $istoragepool+0, $mac)) {
2377
            $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$register{$path}->{'uuid'}, status=>$status, message=>"ERROR Out of storage in destination pool $istoragepool $mac moving: $name"});
2378
            $postreply .= "Status=ERROR Out of storage in destination pool $istoragepool $mac moving: $name\n";
2379
        } elsif (overQuotas($virtualsize, ($istoragepool==-1))) {
2380
            $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$register{$path}->{'uuid'}, status=>$status, message=>"ERROR Over quota (". overQuotas($virtualsize, ($istoragepool==-1)) . ") moving: $name"});
2381
            $postreply .= "Status=ERROR Over quota (". overQuotas($virtualsize, ($istoragepool==-1)) . ") moving: $name\n";
2382
        } elsif ($istoragepool == -1 && $regstoragepool != -1 && $path =~ /\.master\.$type/) {
2383
            $postreply .= "Status=ERROR Unable to move $name (master images are not supported on node storage)\n";
2384
            $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$register{$path}->{'uuid'}, status=>$status, message=>"Unable to move $name (master images are not supported on node storage)"});
2385
    # Moving to node
2386
        } elsif ($istoragepool == -1 && $regstoragepool != -1) {
2387
            if (index($privileges,"a")!=-1 || index($privileges,"n")!=-1 || index($Stabile::privileges,"a")!=-1 || index($Stabile::privileges,"n")!=-1) { # Privilege "n" means user may use node storage
2388
                if ($macip) {
2389
                    $newdirpath = "/mnt/stabile/node/$reguser/$restpath";
2390
                    $newpath = "$macip:$newdirpath";
2391
                    $newstoragepool = $istoragepool;
2392
            # Check if image already exists in target dir
2393
                    $alreadyexists = `ssh -l irigo -i /var/www/.ssh/id_rsa_www -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no $macip "perl -e 'print 1 if -e q{/mnt/stabile/node/$reguser/$restpath}'"`;
2394

    
2395
                } else {
2396
                    $postreply .= "Status=ERROR Unable to move $name (not enough space)\n";
2397
                }
2398
            } else {
2399
                $postreply .= "Status=ERROR Unable to move $name (no node privileges)\n";
2400
            }
2401
    # Moving from node
2402
        } elsif ($regstoragepool == -1 && $istoragepool != -1 && $spools[$istoragepool]) {
2403
            if (index($privileges,"a")!=-1 || index($privileges,"n")!=-1 && $mac || index($Stabile::privileges,"a")!=-1 || index($Stabile::privileges,"n")!=-1 && $mac) { # Privilege "n" means user may use node storage
2404
                unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
2405
                $macip = $nodereg{$mac}->{'ip'}; # $mac is set to existing image's mac since no destination mac was specified
2406
                untie %nodereg;
2407
                $newpath = "$spools[$istoragepool]->{'path'}/$reguser/$restpath";
2408
                $newdirpath = $newpath;
2409
                $oldpath = "$macip:/mnt/stabile/node/$reguser/$restpath";
2410
                $newstoragepool = $istoragepool;
2411
        # Check if image already exists in target dir
2412
                $alreadyexists = -e $newpath;
2413
            } else {
2414
                $postreply .= "Status=ERROR Unable to move $name - you must specify a node\n";
2415
            }
2416
        } elsif ($spools[$istoragepool]) { # User has access to storagepool
2417
            $newpath = "$spools[$istoragepool]->{'path'}/$reguser/$restpath";
2418
            $newdirpath = $newpath;
2419
            $newstoragepool = $istoragepool;
2420
            $alreadyexists = -e $newpath && -s $newpath;
2421
        } else {
2422
            $postreply .= "Status=ERROR Cannot move image. This image is used as master by: $child\n";
2423
        }
2424
    } else {
2425
        $postreply .= "Status=ERROR Unable to move $path (bad status or pool $status, $reguser, $iuser, $regstoragepool, $istoragepool)\n" unless ($istoragepool eq '--' || $regstoragepool eq '--');
2426
    }
2427
    untie %userreg;
2428

    
2429
    if ($alreadyexists && !$force) {
2430
        $postreply = "Status=ERROR Image \"$name\" already exists in destination\n";
2431
        return $postreply;
2432
    }
2433
# Request actual move operation
2434
    elsif ($newpath) {
2435
        if ($newstoragepool == -1) {
2436
            my $diruser = $iuser || $reguser;
2437
            `ssh -l irigo -i /var/www/.ssh/id_rsa_www -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no $macip "/bin/mkdir -v /mnt/stabile/node/$diruser"`; # rsync will create the last dir if needed
2438
        }
2439
        if ($subdir && $istoragepool != -1) {
2440
            my $fulldir = "$spools[$istoragepool]->{'path'}/$reguser$subdir";
2441
            `/bin/mkdir -p "$fulldir"` unless -d $fulldir;
2442
        }
2443
        $uistatus = "moving";
2444
        if ($precreate) {
2445
            $uistatus = "stormoving";
2446
        }
2447

    
2448
        my $ug = new Data::UUID;
2449
        my $tempuuid = $ug->create_str();
2450

    
2451
        $register{$path}->{'status'} = $uistatus;
2452
        $register{$newdirpath} = \%{$register{$path}}; # Clone db entry
2453
        $register{$newdirpath}->{'snap1'} = '' if ($precreate && $force); # Snapshots are not preserved when live migrating storage
2454

    
2455

    
2456
        if ($bschedule eq 'daily7' || $bschedule eq 'daily14') {
2457
             $bschedule = "manually" if (!$spools[$regstoragepool]->{'rdiffenabled'} || !$spools[$regstoragepool]->{'lvm'});
2458
        } elsif ($bschedule ne 'manually') {
2459
            $bschedule = '';
2460
        }
2461

    
2462
        $register{$path}->{'uuid'} = $tempuuid; # Use new temp uuid for old image
2463
        $register{$newdirpath}->{'storagepool'} = $newstoragepool;
2464
        if ($newstoragepool == -1) {
2465
            $register{$newdirpath}->{'mac'} = $mac;
2466
        } else {
2467
            $register{$newdirpath}->{'mac'} = '';
2468
        }
2469
        $register{$newdirpath}->{'user'} = $newuser;
2470
        tied(%register)->commit;
2471
        my $domuuid = $register{$path}->{'domains'};
2472
        if ($status eq "used" || $status eq "paused" || $status eq "moving" || $status eq "stormoving" || $status eq "active") {
2473
            my $dom = $domreg{$domuuid};
2474
            if ($dom->{'image'} eq $olddirpath) {
2475
                $dom->{'image'} = $newdirpath;
2476
            } elsif ($dom->{'image2'} eq $olddirpath) {
2477
                $dom->{'image2'} = $newdirpath;
2478
            } elsif ($dom->{'image3'} eq $olddirpath) {
2479
                $dom->{'image3'} = $newdirpath;
2480
            } elsif ($dom->{'image4'} eq $olddirpath) {
2481
                $dom->{'image4'} = $newdirpath;
2482
            }
2483
            # Moving an image to a node effectively ties the associated domain to that node. When live migrating this should not be done until after move is completed.
2484
            $dom->{'mac'} = $mac if ($newstoragepool == -1 && !$precreate);
2485
            if ($dom->{'system'} && $dom->{'system'} ne '--') {
2486
                unless (tie(%sysreg,'Tie::DBI', Hash::Merge::merge({table=>'systems'}, $Stabile::dbopts)) ) {$res .= qq|{"status": "Error": "message": "Unable to access systems register"}|; return $res;};
2487
                my $sys = $sysreg{$dom->{'system'}};
2488
                $sys->{'image'} = $newdirpath if ($sys->{'image'} eq $olddirpath);
2489
                untie %sysreg;
2490
            }
2491
        }
2492
        my $cmd = qq|/usr/local/bin/steamExec $user $uistatus $status "$oldpath" "$newpath"|;
2493
        `$cmd`;
2494
        $main::syslogit->($user, "info", "$uistatus $type image $name ($oldpath -> $newpath) ($regstoragepool -> $istoragepool)");
2495
        return "$newdirpath\n";
2496
    } else {
2497
        return $postreply;
2498
    }
2499

    
2500
}
2501

    
2502
sub locateNode {
2503
    my ($virtualsize, $mac, $vcpu, $mem) = @_;
2504
    $vcpu = $vcpu || 1;
2505
    unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac'}, $Stabile::dbopts)) ) {return 0};
2506
    my $macip;
2507
    my $dmac;
2508
    my $dindex;
2509
    my $asleep;
2510
    my $identity;
2511
    my $node;
2512
    if ($mac && $mac ne "--") { # A node was specified
2513
        if (1024 * $nodestorageovercommission * $nodereg{$mac}->{'storfree'} > $virtualsize && $nodereg{$mac}->{'status'} eq 'running') {
2514
            $node = $nodereg{$mac};
2515
        }
2516
    } else { # Locate a node
2517
        require "$Stabile::basedir/cgi/servers.cgi";
2518
        $Stabile::Servers::console = 1;
2519
        my ($temp1, $temp2, $temp3, $temp4, $ahashref) = Stabile::Servers::locateTargetNode();
2520
        my @avalues = values %$ahashref;
2521
        my @sorted_values = (sort {$b->{'index'} <=> $a->{'index'}} @avalues);
2522
        foreach my $snode (@sorted_values) {
2523
            if (
2524
                (1024 * $nodestorageovercommission * $snode->{'storfree'} > $virtualsize)
2525
                && ($snode->{'cpuindex'} > $vcpu)
2526
                && ($snode->{'memfree'} > $mem+512*1024)
2527
                && !($snode->{'maintenance'})
2528
                && ($snode->{'status'} eq 'running' || $snode->{'status'} eq 'asleep' || $snode->{'status'} eq 'waking')
2529
                && ($snode->{'index'} > 0)
2530
            ) {
2531
                next if (!($mem) && $snode->{'identity'} eq 'local_kvm'); # Ugly hack - prevent moving images from default storage to local_kvm node
2532
                $node = $snode;
2533
                last;
2534
            }
2535
        }
2536
    }
2537
    $macip = $node->{'ip'};
2538
    $dmac = $node->{'mac'};
2539
    $dindex = $node->{'index'};
2540
    $asleep = ($node->{'status'} eq 'asleep' || $node->{'status'} eq 'waking');
2541
    $identity = $node->{'identity'};
2542
    untie %nodereg;
2543
    return ($dmac, $macip, $dindex, $asleep, $identity);
2544
}
2545

    
2546
sub do_getimagestatus {
2547
    my ($image, $action) = @_;
2548
    if ($help) {
2549
        return <<END
2550
GET:image:
2551
Check if image already exists. Pass image name including suffix.
2552
END
2553
    }
2554
    my $res;
2555
    $imagename = $params{'name'} || $image;
2556
    if ($register{"/mnt/stabile/node/$user/$imagename"}) {
2557
        $res .= q|Status=OK Image /mnt/stabile/node/$imagename found with status | . $register{"/mnt/stabile/node/$user/$imagename"}->{status}. "\n";
2558
    }
2559
    foreach my $spool (@spools) {
2560
        my $ipath = $spool->{'path'} . "/$user/$imagename";
2561
        if ($register{$ipath}) {
2562
            $res .= "Status=OK Image $ipath found with status $register{$ipath}->{'status'}\n";
2563
        } elsif (-f "$ipath" && -s "$ipath") {
2564
            $res .= "Status=OK Image $ipath found on disk, please wait for it to be updated in DB\n";
2565
        }
2566
    }
2567
    $res .= "Status=ERROR Image $imagename not found\n" unless ($res);
2568
    return $res;;
2569
}
2570

    
2571
# Check if image already exists.
2572
# Pass image name including suffix.
2573
sub imageExists {
2574
    my $imagename = shift;
2575
    foreach my $spool (@spools) {
2576
        my $ipath = $spool->{'path'} . "/$user/$imagename";
2577
        if ($register{$ipath}) {
2578
            return $register{$ipath}->{'status'} || 1;
2579
        } elsif (-e "$ipath") {
2580
            return 1
2581
        }
2582
    }
2583
    return '';
2584
}
2585

    
2586
# Pass image name including suffix.
2587
# Returns incremented name of an image which does not already exist.
2588
sub getValidName {
2589
    my $imagename = shift;
2590
    my $name = $imagename;
2591
    my $type;
2592
    if ($imagename =~ /(.+)\.(.+)/) {
2593
        $name = $1;
2594
        $type = $2;
2595
    }
2596
    if (imageExists($imagename)) {
2597
        my $i = 1;
2598
        while (imageExists("$name.$i.$type")) {$i++;};
2599
        $imagename = "$name.$i.$type";
2600
    }
2601
    return $imagename;
2602
}
2603

    
2604
# Print list of available actions on objects
2605
sub do_plainhelp {
2606
    my $res;
2607
    $res .= header('text/plain') unless $console;
2608
    $res .= <<END
2609
* new [size="size", name="name"]: Creates a new image
2610
* clone: Creates new clone of an image. A clone of a master image is a child of the master. A clone of a child or regular
2611
image is a regular copy.
2612
* convert: Creates a copy of a non-qcow2 image in qcow2 format
2613
* snapshot: Takes a qcow2 snapshot of the image. Server can not be running.
2614
* unsnap: Removes a qcow2 snapshot.
2615
* revert: Applies a snapshot, reverting the image to the state it was in, when the snapshot was taken.
2616
* master: Turns an image into a master image which child images may be cloned from. Image can not be in use.
2617
* unmaster: Turns a master image into a regular image, which can not be used to clone child images from.
2618
* backup: Backs up an image using rdiff-backup. Rdiff-backup must be enabled in admin server configuration. This is a
2619
very expensive operation, since typically the entire image must be read.
2620
* buildsystem [master="master image"]: Constructs one or optionally multiple servers, images and networks and assembles
2621
them in one app.
2622
* restore [backup="backup"]: Restores an image from a backup. The restore is named after the backup.
2623
* delete: Deletes an image. Use with care. Image can not be in use.
2624
* mount: Mounts an image for restorefiles and listfiles operations.
2625
* unmount: Unmounts an image
2626
END
2627
    ;
2628
    return $res;
2629
}
2630

    
2631
# Print list of images
2632
# Showing a single image is also handled by specifying uuid or path in $curuuid or $curimg
2633
# When showing a single image a single action may be performed on image
2634
sub do_list {
2635
    my ($img, $action, $obj) = @_;
2636
    if ($help) {
2637
        return <<END
2638
GET:image,uuid:
2639
Lists all the images a user has access to. This is also the default action for the endpoint, so if no action is specified this is what you get.
2640
The returned list may be filtered by specifying storagepool, type, name, path or uuid, like e.g.:
2641

    
2642
<a href="/stabile/images/type:user" target="_blank">/stabile/images/type:user</a>
2643
<a href="/stabile/images/name:test* AND storagepool:shared" target="_blank">/stabile/images/name:test* AND storagepool:shared</a>
2644
<a href="/stabile/images/storagepool:shared AND path:test*" target="_blank">/stabile/images/storagepool:shared AND path:test*</a>
2645
<a href="/stabile/images/name:* AND storagepool:all AND type:usercdroms" target="_blank">/stabile/images/name:* AND storagepool:all AND type:usercdroms</a>
2646
<a href="/stabile/images/[uuid]" target="_blank">/stabile/images/[uuid]</a>
2647

    
2648
storagepool may be either of: all, node, shared
2649
type may be either of: user, usermasters, commonmasters, usercdroms
2650

    
2651
May also be called as tablelist or tablelistall, for use by stash.
2652

    
2653
END
2654
    }
2655
    my $res;
2656
    my $filter;
2657
    my $storagepoolfilter;
2658
    my $typefilter;
2659
    my $pathfilter;
2660
    my $uuidfilter;
2661
    $curimg = $img if ($img);
2662
    my $regimg = $register{$curimg};
2663
#    if ($curimg && ($isadmin || $regimg->{'user'} eq $user || $regimg->{'user'} eq 'common') ) {
2664
    if ($curimg) { # security is enforced below, we hope...
2665
        $pathfilter = $curimg;
2666
    } elsif ($uripath =~ /images(\.cgi)?\/(\?|)(name|storagepool|type|path)/) {
2667
        $filter = $3 if ($uripath =~ /images(\.cgi)?\/.*name(:|=)(.+)/);
2668
        $filter = $1 if ($filter =~ /(.*) AND storagepool/);
2669
        $filter = $1 if ($filter =~ /(.*) AND type/);
2670
        $filter = $1 if ($filter =~ /(.*)\*$/);
2671
        $storagepoolfilter = $2 if ($uripath =~ /images(\.cgi)?\/.*storagepool:(\w+)/);
2672
        $typefilter = $2 if ($uripath =~ /images(\.cgi)?\/.*type:(\w+)/);
2673
        $typefilter = $2 if ($uripath =~ /images(\.cgi)?\/.*type=(\w+)/);
2674
        $pathfilter = $2 if ($uripath =~ /images(\.cgi)?\/.*path:(.+)/);
2675
        $pathfilter = $2 if ($uripath =~ /images(\.cgi)?\/.*path=(.+)/);
2676
    } elsif ($uripath =~ /images(\.cgi)?\/(\w{8}-\w{4}-\w{4}-\w{4}-\w{12})\/?(\w*)/) {
2677
        $uuidfilter = $2;
2678
        $curaction = lc $3;
2679
    }
2680
    $uuidfilter = $options{u} unless $uuidfilter;
2681

    
2682
    if ($uuidfilter && $curaction) {
2683
        if ($imagereg{$uuidfilter}) {
2684
            $curuuid = $uuidfilter;
2685
            my $obj = getObj(%params);
2686
            # Now perform the requested action
2687
            my $objfunc = "obj_$curaction";
2688
            if (defined &$objfunc) { # If a function named objfunc exists, call it
2689
                $res = $objfunc->($obj);
2690
                chomp $postreply;
2691
                unless ($res) {
2692
                    $res .= qq|{"status": "OK", "message": "$postreply"}|;
2693
                    $res = join(", ", split("\n", $res));
2694
                }
2695
                unless ($curaction eq 'download') {
2696
                    $res = header('application/json; charset=UTF8') . $res unless ($console);
2697
                }
2698
            } else {
2699
                $res .= header('application/json') unless $console;
2700
                $res .= qq|{"status": "Error", "message": "Unknown image action: $curaction"}|;
2701
            }
2702
        } else {
2703
            $res .= header('application/json') unless $console;
2704
            $res .= qq|{"status": "Error", "message": "Unknown image $uuidfilter"}|;
2705
        }
2706
        return $res;
2707
    }
2708

    
2709

    
2710
    my %userregister; # User specific register
2711

    
2712
    $res .= header('application/json; charset=UTF8') unless $console;
2713
    unless (tie(%userreg,'Tie::DBI', Hash::Merge::merge({table=>'users', key=>'username', CLOBBER=>1}, $Stabile::dbopts)) ) {$res .= qq|{"status": "Error": "message": "Unable to access user register"}|; return $res;};
2714

    
2715
    my @busers = @users;
2716
    my @billusers = (tied %userreg)->select_where("billto = '$user'");
2717
    push (@busers, $billto) if ($billto && $billto ne '--'); # We include images from 'parent' user
2718
    push (@busers, @billusers) if (@billusers); # We include images from 'child' users
2719
    untie %userreg;
2720
    unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
2721
    foreach my $u (@busers) {
2722
        my @regkeys = (tied %register)->select_where("user = '$u'");
2723
        foreach my $k (@regkeys) {
2724
            my $valref = $register{$k};
2725
            # Only update info for images the user has access to.
2726
            if ($valref->{'user'} eq $u && (defined $spools[$valref->{'storagepool'}]->{'id'} || $valref->{'storagepool'}==-1)) {
2727
                # Only list installable master images from billto account
2728
                next if ($billto && ($billto ne $user) && ($u eq $billto) && ($valref->{'type'} ne 'qcow2' || $valref->{'installable'} ne 'true'));
2729
                my $path = $valref->{'path'};
2730
                my %val = %{$valref}; # Deference and assign to new array, effectively cloning object
2731
                my $spool = $spools[$val{'storagepool'}];
2732
                # Skip images which are in DB e.g. because of change of storage pool difinitions
2733
                next unless ($val{'storagepool'}==-1 || $val{'path'} =~ /$spool->{'path'}/);
2734
                $val{'virtualsize'} += 0;
2735
                $val{'realsize'} += 0;
2736
                $val{'size'} += 0;
2737
                #$val{'lvm'} = 0+( (($spools[$val{'storagepool'}]->{"hostpath"} eq "local") && $spools[$val{'storagepool'}]->{"rdiffenabled"}) || $val{'storagepool'}==-1);
2738
                if ($val{'storagepool'}==-1) {
2739
                    my $node = $nodereg{$val{'mac'}};
2740
                    $val{'lvm'} = 0+($node->{stor} eq 'lvm');
2741
                } else {
2742
                    $val{'lvm'} = 0+$spool->{"lvm"};
2743
                }
2744
                # If image has a master, update the master with child info.
2745
                # This info is specific to each user, so we don't store it in the db
2746
                if ($valref->{'master'} && $register{$valref->{'master'}} && ((grep $_ eq $valref->{'user'}, @users))) {
2747
                    $register{$valref->{'master'}}->{'status'} = 'used';
2748
                    unless ($userregister{$val{'master'}}) { # If we have not yet parsed master, it is not yet in userregister, so put it there
2749
                        my %mval = %{$register{$val{'master'}}};
2750
                        $userregister{$val{'master'}} = \%mval;
2751
                    }
2752
                    #   $userregister{$val{'master'}}->{'user'} = $u;
2753
                    $userregister{$val{'master'}}->{'status'} = 'used';
2754
                    if ($val{'domains'}) {
2755
                        $userregister{$val{'master'}}->{'domainnames'} .= ", " if ($userregister{$val{'master'}}->{'domainnames'});
2756
                        $userregister{$val{'master'}}->{'domainnames'} .= $val{'domainnames'};
2757
                        $userregister{$val{'master'}}->{'domainnames'} .= " (".$val{'user'}.")" if (index($privileges,"a")!=-1);
2758

    
2759
                        $userregister{$val{'master'}}->{'domains'} .= ", " if ($userregister{$val{'master'}}->{'domains'});
2760
                        $userregister{$val{'master'}}->{'domains'} .= $val{'domains'};
2761
                    }
2762
                }
2763
                my $status = $valref->{'status'};
2764
                if ($rdiffenabled && ($userrdiffenabled || index($privileges,"a")!=-1) &&
2765
                    ( ($spools[$valref->{'storagepool'}]->{'rdiffenabled'} &&
2766
                        ($spools[$valref->{'storagepool'}]->{'lvm'} || $status eq 'unused' || $status eq 'used' || $status eq 'paused') )
2767
                        || $valref->{'storagepool'}==-1 )
2768
                ) {
2769
                    $val{'backup'} = "" ;
2770
                } else {
2771
                    $val{'backup'} = "disabled" ;
2772
                }
2773
                $val{'status'} = 'backingup' if ($status =~ /backingup/);
2774
                Updateregister($k, "updateregister") if ($status =~ /(downloading|uploading)/);
2775
                $userregister{$path} = \%val unless ($userregister{$path});
2776
            }
2777
        }
2778
    }
2779
    untie(%nodereg);
2780

    
2781
    my @uservalues;
2782
    if ($filter || $storagepoolfilter || $typefilter || $pathfilter || $uuidfilter) { # List filtered images
2783
        foreach $uvalref (values %userregister) {
2784
            my $fmatch;
2785
            my $smatch;
2786
            my $tmatch;
2787
            my $pmatch;
2788
            my $umatch;
2789
            $fmatch = 1 if (!$filter || $uvalref->{'name'}=~/$filter/i);
2790
            $smatch = 1 if (!$storagepoolfilter || $storagepoolfilter eq 'all'
2791
                || ($storagepoolfilter eq 'node' && $uvalref->{'storagepool'}==-1)
2792
                || ($storagepoolfilter eq 'shared' && $uvalref->{'storagepool'}>=0)
2793
            );
2794
            $tmatch = 1 if (!$typefilter || $typefilter eq 'all'
2795
                || ($typefilter eq 'user' && $uvalref->{'user'} eq $user
2796
                # && $uvalref->{'type'} ne 'iso'
2797
                # && $uvalref->{'path'} !~ /\.master\.qcow2$/
2798
                    )
2799
                || ($typefilter eq 'usermasters' && $uvalref->{'user'} eq $user && $uvalref->{'path'} =~ /\.master\.qcow2$/)
2800
                || ($typefilter eq 'usercdroms' && $uvalref->{'user'} eq $user && $uvalref->{'type'} eq 'iso')
2801
                || ($typefilter eq 'commonmasters' && $uvalref->{'user'} ne $user && $uvalref->{'path'} =~ /\.master\.qcow2$/)
2802
                || ($typefilter eq 'commoncdroms' && $uvalref->{'user'} ne $user && $uvalref->{'type'} eq 'iso')
2803
            );
2804
            $pmatch = 1 if ($pathfilter && $uvalref->{'path'}=~/$pathfilter/i);
2805
            $umatch = 1 if ($uvalref->{'uuid'} eq $uuidfilter);
2806
            if ((!$pathfilter &&!$uuidfilter && $fmatch && $smatch && $tmatch) || $pmatch) {
2807
                push @uservalues,$uvalref if ($uvalref->{'uuid'});
2808
            } elsif ($umatch && $uvalref->{'uuid'}) {
2809
                push @uservalues,$uvalref;
2810
                last;
2811
            }
2812
        }
2813
    } else {
2814
        @uservalues = values %userregister;
2815
    }
2816

    
2817
    # Sort @uservalues
2818
    @uservalues = (sort {$a->{'name'} cmp $b->{'name'}} @uservalues); # Always sort by name first
2819
    my $sort = 'status';
2820
    $sort = $2 if ($uripath =~ /sort\((\+|\-)(\S+)\)/);
2821
    my $reverse;
2822
    $reverse = 1 if ($1 eq '-');
2823
    if ($reverse) { # sort reverse
2824
        if ($sort =~ /realsize|virtualsize|size/) {
2825
            @uservalues = (sort {$b->{$sort} <=> $a->{$sort}} @uservalues); # Sort as number
2826
        } else {
2827
            @uservalues = (sort {$b->{$sort} cmp $a->{$sort}} @uservalues); # Sort as string
2828
        }
2829
    } else {
2830
        if ($sort =~ /realsize|virtualsize|size/) {
2831
            @uservalues = (sort {$a->{$sort} <=> $b->{$sort}} @uservalues); # Sort as number
2832
        } else {
2833
            @uservalues = (sort {$a->{$sort} cmp $b->{$sort}} @uservalues); # Sort as string
2834
        }
2835
    }
2836

    
2837
    if ($uuidfilter || $curimg) {
2838
        if (scalar @uservalues > 1) { # prioritize user's own images
2839
            foreach my $val (@uservalues) {
2840
                if ($val->{'user'} eq 'common') {
2841
                    next;
2842
                } else {
2843
                    $json_text = to_json($val, {pretty => 1});
2844
                }
2845
            }
2846
        } else {
2847
            $json_text = to_json($uservalues[0], {pretty => 1}) if (@uservalues);
2848
        }
2849
    } else {
2850
    #    $json_text = JSON->new->canonical(1)->pretty(1)->encode(\@uservalues) if (@uservalues);
2851
        $json_text = to_json(\@uservalues, {pretty => 1}) if (@uservalues);
2852
    }
2853
    $json_text = "{}" unless $json_text;
2854
    $json_text =~ s/""/"--"/g;
2855
    $json_text =~ s/null/"--"/g;
2856
    $json_text =~ s/"notes" {0,1}: {0,1}"--"/"notes":""/g;
2857
    $json_text =~ s/"installable" {0,1}: {0,1}"(true|false)"/"installable":$1/g;
2858

    
2859
    if ($action eq 'tablelist' || $action eq 'tablelistall') {
2860
        my $t2 = Text::SimpleTable->new(36,26,5,20,14,10,7);
2861
        $t2->row('uuid', 'name', 'type', 'domainnames', 'virtualsize', 'user', 'status');
2862
        $t2->hr;
2863
        my $pattern = $options{m};
2864
        foreach $rowref (@uservalues){
2865
            next unless ($action eq 'tablelistall' || $rowref->{'user'} eq $user);
2866
            if ($pattern) {
2867
                my $rowtext = $rowref->{'uuid'} . " " . $rowref->{'name'} . " " . $rowref->{'type'} . " " . $rowref->{'domainnames'}
2868
                    . " " .  $rowref->{'virtualsize'} . " " . $rowref->{'user'} . " " . $rowref->{'status'};
2869
                $rowtext .= " " . $rowref->{'mac'} if ($isadmin);
2870
                next unless ($rowtext =~ /$pattern/i);
2871
            }
2872
            $t2->row($rowref->{'uuid'}, $rowref->{'name'}, $rowref->{'type'}, $rowref->{'domainnames'}||'--',
2873
                $rowref->{'virtualsize'}, $rowref->{'user'}, $rowref->{'status'});
2874
        }
2875
        $res .= $t2->draw;
2876
    } elsif ($console) {
2877
        $res .= Dumper(\@uservalues);
2878
    } else {
2879
        $res .= $json_text;
2880
    }
2881
    return $res;
2882
}
2883

    
2884
# Internal action for looking up a uuid or part of a uuid and returning the complete uuid
2885
sub do_uuidlookup {
2886
    my ($img, $action) = @_;
2887
    if ($help) {
2888
        return <<END
2889
GET:image,path:
2890
END
2891
    }
2892
    my $res;
2893
    $res .= header('text/plain') unless $console;
2894
    my $u = $options{u};
2895
    $u = $curuuid unless ($u || $u eq '0');
2896
    my $ruuid;
2897
    if ($u || $u eq '0') {
2898
        foreach my $uuid (keys %register) {
2899
            if (($register{$uuid}->{'user'} eq $user || $register{$uuid}->{'user'} eq 'common' || $fulllist)
2900
                && ($register{$uuid}->{'uuid'} =~ /^$u/ || $register{$uuid}->{'name'} =~ /^$u/)) {
2901
                $ruuid = $register{$uuid}->{'uuid'};
2902
                last;
2903
            }
2904
        }
2905
        if (!$ruuid && $isadmin) { # If no match and user is admin, do comprehensive lookup
2906
            foreach $uuid (keys %register) {
2907
                if ($register{$uuid}->{'uuid'} =~ /^$u/ || $register{$uuid}->{'name'} =~ /^$u/) {
2908
                    $ruuid = $register{$uuid}->{'uuid'};
2909
                    last;
2910
                }
2911
            }
2912
        }
2913
    }
2914
    $res .= "$ruuid\n" if ($ruuid);
2915
    return $res;
2916
}
2917

    
2918
# Internal action for showing a single image
2919
sub do_uuidshow {
2920
    my ($img, $action) = @_;
2921
    if ($help) {
2922
        return <<END
2923
GET:image,path:
2924
END
2925
    }
2926
    my $res;
2927
    $res .= header('text/plain') unless $console;
2928
    my $u = $options{u};
2929
    $u = $curuuid unless ($u || $u eq '0');
2930
    if ($u || $u eq '0') {
2931
        foreach my $uuid (keys %register) {
2932
            if (($register{$uuid}->{'user'} eq $user || $register{$uuid}->{'user'} eq 'common' || index($privileges,"a")!=-1)
2933
                && $register{$uuid}->{'uuid'} =~ /^$u/) {
2934
                my %hash = %{$register{$uuid}};
2935
                delete $hash{'action'};
2936
                my $dump = Dumper(\%hash);
2937
                $dump =~ s/undef/"--"/g;
2938
                $res .= $dump;
2939
                last;
2940
            }
2941
        }
2942
    }
2943
    return $res;
2944
}
2945

    
2946
sub do_updatebilling {
2947
    my ($img, $action) = @_;
2948
    if ($help) {
2949
        return <<END
2950
GET:image,path:
2951
END
2952
    }
2953
    my $res;
2954
    $res .= header('text/plain') unless ($console);
2955
    updateBilling($params{"event"});
2956
    $res .= "Status=OK Updated billing for $user\n";
2957
    return $res;
2958
}
2959

    
2960
# If used with the -f switch ($fulllist) from console, all users images are updated in the db
2961
# If used with the -p switch ($fullupdate), also updates status information (ressource intensive - runs through all domains)
2962
sub dont_updateregister {
2963
    my ($img, $action) = @_;
2964
    my $res;
2965
    if ($help) {
2966
        return <<END
2967
GET:image,path:
2968
END
2969
    }
2970
    return "Status=ERROR You must be an admin to do this!\n" unless ($isadmin);
2971
    $fullupdate = 1 if ((!$fullupdate && $params{'fullupdate'}) || $action eq 'fullupdateregister');
2972
    my $force = $params{'force'};
2973
    Updateregister($force);
2974
    $res .= "Status=OK Updated image register for " . join(', ', @users) . "\n";
2975
}
2976

    
2977
sub do_urlupload {
2978
    my ($img, $action) = @_;
2979
    if ($help) {
2980
        return <<END
2981
GET:image,path:
2982
END
2983
    }
2984
    my $res;
2985
    $res .= header('application/json') unless ($console);
2986
    if ($params{'probe'} && $params{'url'}) {
2987
        my $url = $params{'url'};
2988
        my $cmd = qq!curl --http1.1 -kIL "$url" 2>&1!;
2989
        my $headers = `$cmd`;
2990
        my $filename;
2991
        my $filesize = 0;
2992
        $filename = $1 if ($headers =~ /content-disposition: .+filename="(.+)"/i);
2993
        $filesize = $1 if ($headers =~ /content-length: (\d+)/i);
2994
        my $ok;
2995
        if (!$filename) {
2996
            my $cmd = qq[curl --http1.1 -kIL "$url" 2>&1 | grep -i " 200 OK"];
2997
            $ok =  `$cmd`; chomp $ok;
2998
            $filename = `basename "$url"` if ($ok);
2999
            chomp $filename;
3000
        }
3001
        if ($filename =~ /\S+\.(vmdk|img|vhd|vhdx|qcow|qcow2|vdi|iso)$/) {
3002
            $filename = $2 if ($filename =~ /(=|\?)(.+)/);
3003
            $filename = $2 if ($filename =~ /(=|\?)(.+)/);
3004
            $filename = getValidName($filename);
3005
            my $filepath = $spools[0]->{'path'} . "/$user/$filename";
3006
            $res .= qq|{"status": "OK", "name": "$filename", "message": "200 OK", "size": $filesize, "path": "$filepath"}|;
3007
        } else {
3008
            $res .= qq|{"status": "ERROR", "message": "An image file cannot be downloaded from this URL.", "url": "$url", "filename": "$filename"}|;
3009
        }
3010
    } elsif ($params{'path'} && $params{'url'} && $params{'name'} && defined $params{'size'}) {
3011
        my $imagepath = $params{'path'};
3012
        my $imagename = $params{'name'};
3013
        my $imagesize = $params{'size'};
3014
        my $imageurl = $params{'url'};
3015
        if (-e "$imagepath.meta" && $imagepath =~ /\.master\.qcow2$/) { # This image is being downloaded by pressurecontrol
3016
            $res .= qq|{"status": "OK", "name": "$imagename", "message": "Now downloading master", "path": "$imagepath"}|;
3017
        } elsif (-e $imagepath) {
3018
            $res .= qq|{"status": "OK", "message": "An image file with this name already exists on the server.", "name": "$imagename", "path": "$imagepath"}|;
3019
            `/bin/echo "uploading" > "$imagepath.meta"`;
3020
            my $ksize = $imagesize / 1024;
3021
            `/bin/echo "$ksize" . "K 100%" >> "$imagepath.meta"`;
3022
            `/bin/echo "" >> "$imagepath.meta"`;
3023
        } elsif ($imagepath !~ /^$spools[0]->{'path'}\/$user\/.+/) {
3024
            $res .= qq|{"status": "ERROR", "message": "Invalid path"}|;
3025
        } elsif (overQuotas($virtualsize)) {
3026
            $res .= qq|{"status": "ERROR", "message": "Over quota (". overQuotas($virtualsize) . ") uploading: $imagename"}|;
3027
        } elsif (overStorage($imagesize, 0)) {
3028
            $res .= qq|{"status": "ERROR", "message": "Out of storage in destination pool uploading: $imagename"}|;
3029
        } elsif ($imagepath =~ /^$spools[0]->{'path'}.+\.(vmdk|img|vhd|vhdx|qcow|qcow2|vdi|iso)$/) {
3030
            my $imagetype = $1;
3031
            my $ug = new Data::UUID;
3032
            my $newuuid = $ug->create_str();
3033
            my $name = $imagename;
3034
            $name = $1 if ($name =~ /(.+)\.(vmdk|img|vhd|vhdx|qcow|qcow2|vdi|iso)$/);
3035
            $register{$imagepath} = {
3036
                uuid => $newuuid,
3037
                path => $imagepath,
3038
                name => $name,
3039
                user => $user,
3040
                type => $imagetype,
3041
                virtualsize => $imagesize,
3042
                realsize => $imagesize,
3043
                size => $imagesize,
3044
                storagepool => 0,
3045
                status => 'uploading'
3046
            };
3047
            `/bin/echo uploading > "$imagepath.meta"`;
3048
            eval {
3049
                my $daemon = Proc::Daemon->new(
3050
                    work_dir => '/usr/local/bin',
3051
                    exec_command => "perl -U steamExec $user urluploading unused \"$imagepath\" \"$imageurl\""
3052
                ) or do {$postreply .= "Status=ERROR $@\n";};
3053
                my $pid = $daemon->Init();
3054
                $main::syslogit->($user, "info", "urlupload $imageurl, $imagepath");
3055
                1;
3056
            } or do {$res .= qq|{"status": "ERROR", "message": "ERROR $@"}|;};
3057
            $res .= qq|{"status": "OK", "name": "$imagename", "message": "Now uploading", "path": "$imagepath"}|;
3058
        }
3059
    } elsif ($params{'path'} && $params{'getsize'}) {
3060
        my $imagepath = $params{'path'};
3061
        if (-e "$imagepath.meta") {
3062
            my $imagesize = `grep -Po '\\d+K' "$imagepath.meta" | tail -n1`;
3063
            chomp $imagesize;
3064
            $imagesize = 1024 * $imagesize;
3065
            $res .= qq|{"status": "OK", "size": $imagesize, "path": "$imagepath"}|;
3066
        } else {
3067
            if (!(-e $imagepath)) {
3068
                $res .= qq|{"status": "ERROR", "message": "Image not found.", "path": "$imagepath"}|;
3069
            } elsif ($imagepath !~ /^$spools[0]->{'path'}\/$user\/.+/  && $imagepath !~ /^$spools[0]->{'path'}\/common\/.+/) {
3070
                $res .= qq|{"status": "ERROR", "message": "Invalid path"}|;
3071
            } else {
3072
                my @stat = stat($imagepath);
3073
                my $imagesize = $stat[7];
3074
                $res .= qq|{"status": "OK", "size": $imagesize, "path": "$imagepath"}|;
3075
            }
3076

    
3077
        }
3078
    }
3079
    return $res;
3080
}
3081

    
3082
sub do_upload {
3083
    my ($img, $action) = @_;
3084
    if ($help) {
3085
        return <<END
3086
POST:image,path:
3087
END
3088
    }
3089
    my $res;
3090
    $res .= header("text/html") unless ($console);
3091

    
3092
    my $uname = $params{'name'};
3093

    
3094
    my($name, $dirpath, $suffix) = fileparse($uname, (".vmdk", ".img", ".vhd", ".vhdx", ".qcow", ".qcow2", ".vdi", ".iso"));
3095

    
3096
    $name = $1 if ($name =~ /^\.+(.*)/); # Don't allow hidden files
3097
    #        my $f = lc $name;
3098
    my $f = $name;
3099
    $f = $spools[0]->{'path'} . "/$user/$f$suffix";
3100

    
3101
    my $chunk = int($params{'chunk'});
3102
    my $chunks = int($params{'chunks'});
3103

    
3104
    if ($chunk == 0 && -e $f) {
3105
        $res .= qq|Error: File $f already exists $name|;
3106
    } else {
3107
        open (FILE, ">>$f");
3108

    
3109
        if ($params{'file'}) {
3110
            my $uh = $Stabile::q->upload("file");
3111
            while ( <$uh> ) {
3112
                print FILE;
3113
            }
3114
            close FILE;
3115

    
3116
            if ($chunk == 0) {
3117
                `/usr/local/bin/steamExec updateimagestatus "$f" uploading`;
3118
            }
3119
            if ($chunk >= ($chunks - 1) ) { # Done
3120
                unlink("$f.meta");
3121
                `/usr/local/bin/steamExec updateimagestatus "$f" unused`;
3122
            } else {
3123
                my $upload_meta_data = "status=uploading&chunk=$chunk&chunks=$chunks";
3124
                `echo "$upload_meta_data" > "$f.meta"`;
3125
            }
3126
            $res .= qq|OK: Chunk $chunk uploaded of $name|;
3127
        } else {
3128
            $res .= qq|OK: No file $name.|;
3129
        }
3130
    }
3131
    return $res;
3132
}
3133

    
3134
# .htaccess files are created hourly, giving the image user access
3135
# when download is clicked by another user (in @users, so with permission), this user is also given access until .htaccess is rewritten
3136
sub Download {
3137
    my ($f, $action, $argref) = @_;
3138
    #    my ($name, $managementlink, $upgradelink, $terminallink, $version) = @{$argref};
3139
    if ($help) {
3140
        return <<END
3141
GET:image,console:
3142
Returns http redirection with URL to download image
3143
END
3144
    }
3145
    $baseurl = $argref->{baseurl} || $baseurl;
3146
    my %uargs = %{$argref};
3147
    $f = $uargs{'image'} unless ($f);
3148
    $baseurl = $uargs{'baseurl'} || $baseurl;
3149
    $console = $console || $uargs{'console'};
3150
    my $res;
3151
    my $uf =  URI::Escape::uri_unescape($f);
3152
    if (! $f) {
3153
        $res .= header('text/html', '500 Internal Server Error') unless ($console);
3154
        $res .= "Status=ERROR You must specify an image.\n";
3155
    }
3156
    my $txt = <<EOT
3157
order deny,allow
3158
AuthName "Download"
3159
AuthType None
3160
TKTAuthLoginURL $baseurl/login/
3161
TKTAuthIgnoreIP on
3162
deny from all
3163
Satisfy any
3164
require user $user
3165
require user $tktuser
3166
Options -Indexes
3167
EOT
3168
    ;
3169
    my $fid;
3170
    my $fpath;
3171
    foreach my $p (@spools) {
3172
        foreach my $suser (@users) {
3173
            my $dir = $p->{'path'};
3174
            my $id = $p->{'id'};
3175
            if (-d "$dir/$suser" && $uf =~ /\/$suser\//) {
3176
                if ($uf =~ /$dir\/(.+)\/(.+)/) {
3177
                    my $filename = $2;
3178
                    utf8::encode($filename);
3179
                    utf8::decode($filename);
3180
                    $fpath = "$1/" . URI::Escape::uri_escape($filename);
3181
                    #$fpath = "$1/" . $filename;
3182
                    `chmod o+rw "$uf"`;
3183
                    `/bin/echo "$txt" > "$dir/$suser/.htaccess"`;
3184
                    `chmod 644 "$dir/$suser/.htaccess"`;
3185
                    `/bin/mkdir "$Stabile::basedir/download"` unless (-e "$Stabile::basedir/download");
3186
                    `/bin/ln -s "$dir" "$Stabile::basedir/download/$id"` unless (-e "$Stabile::basedir/download/$id");
3187
                    $fid = $id;
3188
                    last;
3189
                }
3190
            }
3191
        }
3192
    }
3193
    if (($fid || $fid eq '0') && $fpath && -e "$f") {
3194
        my $fileurl = "$baseurl/download/$fid/$fpath";
3195
        if ($console) {
3196
            $res .= header(). $fileurl;
3197
        } else {
3198
            $res .= "Status: 302 Moved\nLocation: $fileurl\n\n";
3199
            $res .= "$fileurl\n";
3200
        }
3201
    } else {
3202
        $res .= header('text/html', '500 Internal Server Error') unless ($console);
3203
        $res .= "Status=ERROR File not found $f, $fid, $fpath, $uargs{image}\n";
3204
    }
3205
    return $res;
3206
}
3207

    
3208

    
3209
sub Liststoragedevices {
3210
    my ($image, $action, $obj) = @_;
3211
    if ($help) {
3212
        return <<END
3213
GET::
3214
Returns available physical disks and partitions.
3215
Partitions currently used for holding backup and primary images directories are marked as such.
3216
May also be called as 'getimagesdevice', 'getbackupdevice', 'listimagesdevices' or 'listbackupdevices'.
3217
END
3218
    }
3219
    unless ($isadmin || ($user eq $engineuser)) {
3220
        return '' if ($action eq 'getimagesdevice' || $action eq 'getbackupdevice');
3221
        return qq|[]|;
3222
    }
3223
    my %devs;
3224
    # Check if we have unmounted ZFS file systems
3225
#    if (`grep "stabile-images" /etc/stabile/config.cfg` && !(`df` =~ /stabile-images/)) {
3226
    if (!(`df` =~ /stabile-images/)) {
3227
        `zpool import stabile-images 2>/dev/null`;
3228
        `zfs mount stabile-images 2>/dev/null`;
3229
        `zfs mount stabile-images/images 2>/dev/null`;
3230
    }
3231
    if (!(`df` =~ /stabile-backup/)) {
3232
        `zpool import stabile-backup 2>/dev/null`;
3233
        `zfs mount stabile-backup 2>/dev/null`;
3234
        `zfs mount stabile-backup/images 2>/dev/null`;
3235
        `zfs mount stabile-backup/backup 2>/dev/null`;
3236
    }
3237
    # Add active and mounted filesystems
3238
    my %filesystems;
3239
    $cmd = q/LANG=en df -hT | tr -s ' ' ',' | jq -nR '[( input | split(",") ) as $keys | ( inputs | split(",") ) as $vals | [ [$keys, $vals] | transpose[] | {key:.[0],value:.[1]} ] | from_entries ]'/;
3240
    my $json = `$cmd`;
3241
    my $jobj = JSON::from_json($json);
3242
    my $rootdev;
3243
    my $backupdev;
3244
    my $imagesdev;
3245
    foreach my $fs (sort {$a->{'Filesystem'} cmp $b->{'Filesystem'}} @{$jobj}) {
3246
        # Note that physical disk devicess in general may be either disks, partitions with regular file systems (like ext4) or zfs pools, which may contain many file systems
3247
        if ($fs->{Filesystem} =~ /\/dev\/(.+)/) {
3248
            next if ($fs->{Type} eq 'squashfs');
3249
            next if ($fs->{Filesystem} =~ /\/dev\/loop/);
3250
            my $name = $1;
3251
            if ($name =~ /mapper\/(\w+-)(.+)/) {
3252
                $name = "$1$2";
3253
            }
3254
            $fs->{Name} = $name;
3255
            delete $fs->{on};
3256
            my $mp = $fs->{Mounted};
3257
            if ($fs->{Mounted} eq '/') {
3258
                $rootdev = $name;
3259
            } else {
3260
                if ($backupdir =~ /^$fs->{Mounted}/) {
3261
                    next if ($action eq 'listimagesdevices'); # Current backup dev is not available as images dev
3262
                    $fs->{isbackupdev} = 1;
3263
                    $backupdev = $name;
3264
                    return $name if ($action eq 'getbackupdevice');
3265
                }
3266
                if ($tenderpathslist[0] =~ /^$fs->{Mounted}/) {
3267
                    next if ($action eq 'listbackupdevices'); # Current images dev is not available as backup dev
3268
                    $fs->{isimagesdev} = 1;
3269
                    $imagesdev = $name;
3270
                    return $name if ($action eq 'getimagesdevice');
3271
                }
3272
            }
3273
            $fs->{dev} = $name;
3274
            $fs->{nametype} = "$name ($fs->{Type} - " .  ($mp?$mp:"not mounted") . " $fs->{Size})";
3275
            $filesystems{$name} = $fs;
3276
        } elsif ( $fs->{Type} eq 'zfs') {
3277
            my $name = $fs->{Filesystem};
3278
            # only include zfs pools but look for use as backup and images, exclude shapshots
3279
            if ($name =~ /(.+)\/(.+)/
3280
                && !($name =~ /SNAPSHOT/)
3281
                && !($name =~ /stabile-backup\/images/)
3282
                && !($name =~ /stabile-backup\/node/)
3283
            ) {
3284
                $name = $1;
3285
                if ($fs->{Mounted} eq $backupdir) {
3286
                    if ($action eq 'listimagesdevices') {
3287
                        delete $filesystems{$name}; # not available for images - used for backup
3288
                    } else {
3289
                        $filesystems{$name}->{isbackupdev} = 1;
3290
                        $fs->{isbackupdev} = 1;
3291
                        $backupdev = $name;
3292
                    }
3293
                    return $name if ($action eq 'getbackupdevice');
3294
                } elsif ($fs->{Mounted} eq $tenderpathslist[0]) {
3295
                    if ($action eq 'listbackupdevices') {
3296
                        delete $filesystems{$name}; # not available for backup - used for images
3297
                    } else {
3298
                        $filesystems{$name}->{isimagesdev} = 1;
3299
                        $fs->{isimagesdev} = 1;
3300
                        $imagesdev = $name;
3301
                    }
3302
                    return $name if ($action eq 'getimagesdevice');
3303
                }
3304
                $fs->{Name} = $name;
3305
                $fs->{nametype} = "$name ($fs->{Type} $fs->{Size})";
3306
                delete $fs->{on};
3307
                $filesystems{$name} = $fs;
3308
            }
3309
        }
3310
    }
3311
    if ($action eq 'getbackupdevice' || $action eq 'getimagesdevice') {
3312
        return $rootdev;
3313
    }
3314
    $filesystems{$rootdev}->{isbackupdev} = 1 unless ($backupdev || $action eq 'listimagesdevices');
3315
    $filesystems{$rootdev}->{isimagesdev} = 1 unless ($imagesdev || $action eq 'listbackupdevices');
3316
    # Lowercase keys
3317
    foreach my $k (keys %filesystems) {
3318
        my %hash = %{$filesystems{$k}};
3319
        %hash = map { lc $_ => $hash{$_} } keys %hash;
3320
        $filesystems{$k} = \%hash;
3321
    }
3322
    # Identify physical devices used for zfs
3323
    $cmd = "zpool list -vH";
3324
    my $zpools = `$cmd`;
3325
    my $zdev;
3326
    my %zdevs;
3327

    
3328
    # Now parse the rather strange output with every other line representing physical dev
3329
    foreach my $line (split "\n", $zpools) {
3330
        my ($zname, $zsize, $zalloc) = split "\t", $line;
3331
        if (!$zdev) {
3332
            if ($zname =~ /stabile-/) {
3333
                $zdev = {
3334
                    name=>$zname,
3335
                    size=>$zsize,
3336
                    alloc=>$zalloc
3337
                }
3338
            }
3339
        } else {
3340
            my $dev = $zsize;
3341
            $zdev->{dev} = $dev;
3342
            if ( $filesystems{$zdev->{name}}) {
3343
                if (
3344
                    ($action eq 'listimagesdevices' && $zdev->{name} =~ /backup/) ||
3345
                        ($action eq 'listbackupdevices' && $zdev->{name} =~ /images/)
3346
                ) {
3347
                    delete $filesystems{$zdev->{name}}; # Don't include backup devs in images listing and vice-versa
3348
                } else {
3349
                    if ($filesystems{$zdev->{name}}->{dev}) {
3350
                        $filesystems{$zdev->{name}}->{dev} .= " $dev";
3351
                    } else {
3352
                        $filesystems{$zdev->{name}}->{dev} = $dev;
3353
                    }
3354
        #            $filesystems{$zdev->{name}}->{nametype} =~ s/zfs/zfs pool/;
3355
                }
3356
            }
3357
            $zdevs{$dev} = $zdev->{name};
3358
        #    $zdev = '';
3359
        }
3360
    }
3361

    
3362
    # Add blockdevices
3363
    $cmd = q|lsblk --json|;
3364
    my $json2 = `$cmd`;
3365
    my $jobj2 = JSON::from_json($json2);
3366
    foreach my $fs (@{$jobj2->{blockdevices}}) {
3367
        my $rootdev = $1 if ($fs->{name} =~ /([A-Za-z]+)\d*/);
3368
        if ($fs->{children}) {
3369
            foreach my $fs2 (@{$fs->{children}}) {
3370
                next if ($fs2->{type} eq 'loop');
3371
                next if ($fs2->{type} eq 'squashfs');
3372
                next if ($fs2->{size} =~ /K$/);
3373
                if ($filesystems{$fs2->{name}}) {
3374
                    $filesystems{$fs2->{name}}->{blocksize} = $fs2->{size};
3375
                } elsif (!$zdevs{$fs2->{name}} && !$zdevs{$rootdev}) { # Don't add partitions already used for ZFS
3376
                    next if (($action eq 'listimagesdevices' || $action eq 'listbackupdevices') && $fs2->{mountpoint} eq '/');
3377
                    my $mp = $fs2->{mountpoint};
3378
                    $filesystems{$fs2->{name}} = {
3379
                        name=>$fs2->{name},
3380
                        blocksize=>$fs2->{size},
3381
                        mountpoint=>$mp,
3382
                        type=>$fs2->{type},
3383
                        nametype=> "$fs2->{name} ($fs2->{type} - " . ($mp?$mp:"not mounted") . " $fs2->{size})",
3384
                        dev=>$fs2->{name}
3385
                    }
3386
                }
3387
            }
3388
        } elsif (!$zdevs{$fs->{name}}) { # Don't add disks already used for ZFS
3389
            next if ($fs->{type} eq 'loop');
3390
            next if ($fs->{type} eq 'squashfs');
3391
            my $mp = $fs->{mountpoint};
3392
            next if ($fs->{type} eq 'rom');
3393
            $filesystems{$fs->{name}} = {
3394
                name=>$fs->{name},
3395
                blocksize=>$fs->{size},
3396
                mountpoint=>$fs->{mountpoint},
3397
                type=>$fs->{type},
3398
                nametype=> "$fs->{name} ($fs->{type} - " . ($mp?$mp:"not mounted") . " $fs->{size})",
3399
            }
3400
        }
3401
    }
3402

    
3403
    # Identify physical devices used for lvm
3404
    $cmd = "pvdisplay -c";
3405
    my $pvs = `$cmd`;
3406
    my @backupdevs; my @imagesdevs;
3407
    foreach my $line (split "\n", $pvs) {
3408
        my ($pvdev, $vgname) = split ":", $line;
3409
        $pvdev = $1 if ($pvdev =~ /\s+(\S+)/);
3410
        $pvdev = $1 if ($pvdev =~ /\/dev\/(\S+)/);
3411
        if ($filesystems{"$vgname-backupvol"}) {
3412
            push @backupdevs, $pvdev unless ($action eq 'listimagesdevices');
3413
        } elsif ($filesystems{"$vgname-imagesvol"}) {
3414
            push @imagesdevs, $pvdev unless ($action eq 'listbackupdevices');
3415
        }
3416
        if (@backupdevs) {
3417
            $filesystems{"$vgname-backupvol"}->{dev} = join(" ", @backupdevs);
3418
            $filesystems{"$vgname-backupvol"}->{nametype} = $filesystems{"$vgname-backupvol"}->{name} . " (lvm with " . $filesystems{"$vgname-backupvol"}->{type} . " on " . join(" ", @backupdevs) . " " . $filesystems{"$vgname-backupvol"}->{size} . ")";
3419
        }
3420
        if (@imagesdevs) {
3421
            $filesystems{"$vgname-imagesvol"}->{dev} = join(" ", @imagesdevs);
3422
            $filesystems{"$vgname-imagesvol"}->{nametype} = $filesystems{"$vgname-imagesvol"}->{name} . " (lvm with " . $filesystems{"$vgname-imagesvol"}->{type} . " on " . join(" ", @imagesdevs) . " " . $filesystems{"$vgname-imagesvol"}->{size} . ")";
3423
        }
3424
        delete $filesystems{$pvdev} if ($filesystems{$pvdev}); # Don't also list as physical device
3425
    }
3426
    my $jsonreply;
3427
    if ($action eq 'getbackupdevice' || $action eq 'getimagesdevice') {
3428
        return ''; # We should not get here
3429
    } elsif ($action eq 'getstoragedevices') {
3430
        return \%filesystems;
3431
    } elsif ($action eq 'listimagesdevices') {
3432
        $jsonreply .= qq|{"identifier": "name", "label": "nametype", "action": "$action", "items": |;
3433
        my @vals = sort {$b->{'isimagesdev'} cmp $a->{'isimagesdev'}} values %filesystems;
3434
        $jsonreply .= JSON->new->canonical(1)->pretty(1)->encode(\@vals);
3435
        $jsonreply .= "}";
3436
    } elsif ($action eq 'listbackupdevices') {
3437
        $jsonreply .= qq|{"identifier": "name", "label": "nametype", "action": "$action", "items": |;
3438
        my @vals = sort {$b->{'isbackupdev'} cmp $a->{'isbackupdev'}} values %filesystems;
3439
        $jsonreply .= JSON->new->canonical(1)->pretty(1)->encode(\@vals);
3440
        $jsonreply .= "}";
3441
    } else {
3442
        $jsonreply .= JSON->new->canonical(1)->pretty(1)->encode(\%filesystems);
3443
    }
3444
    return $jsonreply;
3445
}
3446

    
3447
sub do_liststoragepools {
3448
    my ($image, $action) = @_;
3449
    if ($help) {
3450
        return <<END
3451
GET:dojo:
3452
Returns available storage pools. If parameter dojo is set, JSON is padded for Dojo use.
3453
END
3454
    }
3455
    my %npool = (
3456
        "hostpath", "node",
3457
        "path", "--",
3458
        "name", "On node",
3459
        "rdiffenabled", 1,
3460
        "id", "-1");
3461
    my @p = @spools;
3462
    # Present node storage pool if user has sufficient privileges
3463
    if (index($privileges,"a")!=-1 || index($privileges,"n")!=-1) {
3464
        @p = (\%npool);
3465
        push @p, @spools;
3466
    }
3467

    
3468
    my $jsonreply;
3469
    $jsonreply .= "{\"identifier\": \"id\", \"label\": \"name\", \"items\":" if ($params{'dojo'});
3470
    $jsonreply .= to_json(\@p, {pretty=>1});
3471
    $jsonreply .= "}" if ($params{'dojo'});
3472
    return $jsonreply;
3473
}
3474

    
3475
# List images available for attaching to server
3476
sub do_listimages {
3477
    my ($img, $action) = @_;
3478
    if ($help) {
3479
        return <<END
3480
GET:image,image1:
3481
List images available for attaching to server. This is different from [list] since images must be unused and e.g. master images cannot be attached to a server.
3482
An image may be passed as parameter. This image is assumed to be already attached to the server, so it is included, even though it is not unused.
3483
If image1 is passed, we assume user is selecting an optional second image for the server, and an empty entry is included in the response, in order for the user to select "no image".
3484
END
3485
    }
3486
    my $res;
3487
    $res .= header('application/json') unless ($console);
3488
    my $curimg1 = URI::Escape::uri_unescape($params{'image1'});
3489
    my @filteredfiles;
3490
    my @curusers = @users;
3491
    # If an admin user is looking at a server not belonging to him, allow him to see the server
3492
    # users images
3493
    if ($isadmin && $img && $img ne '--' && $register{$img} && $register{$img}->{'user'} ne $user) {
3494
        @curusers = ($register{$img}->{'user'}, "common");
3495
    }
3496

    
3497
    foreach my $u (@curusers) {
3498
        my @regkeys = (tied %register)->select_where("user = '$u'");
3499
        foreach my $k (@regkeys) {
3500
            my $val = $register{$k};
3501
            if ($val->{'user'} eq $u && (defined $spools[$val->{'storagepool'}]->{'id'} || $val->{'storagepool'}==-1)) {
3502
                my $f = $val->{'path'};
3503
                next if ($f =~ /\/images\/dummy.qcow2/);
3504
                my $itype = $val->{'type'};
3505
                if ($itype eq "vmdk" || $itype eq "img" || $itype eq "vhd" || $itype eq "vhdx" || $itype eq "qcow" || $itype eq "qcow2" || $itype eq "vdi") {
3506
                    my $hit = 0;
3507
                    if ($f =~ /(.+)\.master\.$itype/) {$hit = 1;} # don't list master images for user selections
3508
                    if ($f =~ /(.+)\/common\//) {$hit = 1;} # don't list common images for user selections
3509
                    my $dbstatus = $val->{'status'};
3510
                    if ($dbstatus ne "unused") {$hit = 1;} # Image is in a transitional state - do not use
3511
                    if ($hit == 0 || $img eq $f) {
3512
                        my $hypervisor = ($itype eq "vmdk" || $itype eq "vhd" || $itype eq "vhdx" || $itype eq "vdi")?"vbox":"kvm";
3513
                        my $notes = $val->{'notes'};
3514
                        $notes = "" if $notes eq "--";
3515
                        my %img = ("path", $f, "name", $val->{'name'}, "hypervisor", $hypervisor, "notes", $notes,
3516
                            "uuid", $val->{'uuid'}, "master", $val->{'master'}, "managementlink", $val->{'managementlink'}||"",
3517
                            "upgradelink", $val->{'upgradelink'}||"", "terminallink", $val->{'terminallink'}||"", "version", $val->{'version'}||"",
3518
                            "appid", $val->{'appid'}||"");
3519
                        push @filteredfiles, \%img;
3520
                    }
3521
                }
3522
            }
3523
        }
3524
    }
3525
    my %img = ("path", "--", "name", "--", "hypervisor", "kvm,vbox");
3526
    if ($curimg1) {
3527
        push @filteredfiles, \%img;
3528
    }
3529
    my $json_text = to_json(\@filteredfiles, {pretty=>1});
3530
    $res .= qq/{"identifier": "path", "label": "name", "items": $json_text }/;
3531
    return $res;
3532
}
3533

    
3534
sub Listcdroms {
3535
    my ($image, $action) = @_;
3536
    if ($help) {
3537
        return <<END
3538
GET::
3539
Lists the CD roms a user has access to.
3540
END
3541
    }
3542
    my $res;
3543
    $res .= header('application/json') unless ($console);
3544
    my @filteredfiles;
3545
    foreach my $u (@users) {
3546
        my @regkeys = (tied %register)->select_where("user = '$u'");
3547
        foreach my $k (@regkeys) {
3548
            my $val = $register{$k};
3549
            my $f = $val->{'path'};
3550
            if ($val->{'user'} eq $u && (defined $spools[$val->{'storagepool'}]->{'id'} || $val->{'storagepool'}==-1)) {
3551
                my $itype = $val->{'type'};
3552
                if ($itype eq "iso" || $itype eq "toast") {
3553
                    $notes = $val->{'notes'} || '';
3554
                    if ($u eq $user) {
3555
                        $installable = "true";
3556
                    #    $notes = "This CD/DVD may work just fine, however it has not been tested to work with Irigo Servers.";
3557
                    } else {
3558
                        $installable = $val->{'installable'} || 'false';
3559
                    #    $notes = "This CD/DVD has been tested to work with Irigo Servers." unless $notes;
3560
                    }
3561
                    my %img = ("path", $f, "name", $val->{'name'}, "installable", $installable, "notes", $notes);
3562
                    push @filteredfiles, \%img;
3563
                }
3564
            }
3565
        }
3566
    }
3567
    my %ioimg = ("path", "virtio", "name", "-- VirtIO disk (dummy) --");
3568
    push @filteredfiles, \%ioimg;
3569
    my %dummyimg = ("path", "--", "name", "-- No CD --");
3570
    push @filteredfiles, \%dummyimg;
3571
    #        @filteredfiles = (sort {$a->{'name'} cmp $b->{'name'}} @filteredfiles); # Sort by status
3572
    my $json_text = to_json(\@filteredfiles, {pretty=>1});
3573
    $res .= qq/{"identifier": "path", "label": "name", "items": $json_text }/;
3574
    return $res;
3575
}
3576

    
3577
sub do_listmasterimages {
3578
    my ($image, $action, $obj) = @_;
3579
    if ($help) {
3580
        return <<END
3581
GET::
3582
Lists master images available to the current user.
3583
END
3584
    }
3585
    my $res;
3586
    $res .= header('application/json') unless ($console);
3587

    
3588
    my @filteredfiles;
3589
    my @busers = @users;
3590
    push (@busers, $billto) if ($billto && $billto ne $user); # We include images from 'parent' user
3591

    
3592
    foreach my $u (@busers) {
3593
        my @regkeys = (tied %register)->select_where("user = '$u'");
3594
        foreach my $k (@regkeys) {
3595
            my $valref = $register{$k};
3596
            my $f = $valref->{'path'};
3597
            if ($valref->{'user'} eq $u && (defined $spools[$valref->{'storagepool'}]->{'id'} || $valref->{'storagepool'}==-1)) {
3598
                # Only list installable master images from billto account
3599
                next if ($billto && $u eq $billto && $valref->{'installable'} ne 'true');
3600

    
3601
                my $itype = $valref->{'type'};
3602
                if ($itype eq "qcow2" && $f =~ /(.+)\.master\.$itype/) {
3603
                    my $installable;
3604
                    my $status = $valref->{'status'};
3605
                    my $notes;
3606
                    if ($u eq $user) {
3607
                        $installable = "true";
3608
                        $notes = "This master image may work just fine, however it has not been tested to work with Stabile.";
3609
                    } else {
3610
                        $installable = $valref->{'installable'} || '';
3611
                        $notes = $valref->{'notes'};
3612
                        $notes = "This master image has been tested to work with Stabile." unless $notes;
3613
                    }
3614
                    my %img = (
3615
                        "path", $f,
3616
                        "name", $valref->{'name'},
3617
                        "installable", $installable,
3618
                        "notes", $notes,
3619
                        "managementlink", $valref->{'managementlink'}||"",
3620
                        "upgradelink", $valref->{'upgradelink'}||"",
3621
                        "terminallink", $valref->{'terminallink'}||"",
3622
                        "image2", $valref->{'image2'}||"",
3623
                        "version", $valref->{'version'}||"",
3624
                        "appid", $valref->{'appid'}||"",
3625
                        "status", $status,
3626
                        "user", $valref->{'user'}
3627
                    );
3628
                    push @filteredfiles, \%img;
3629
                }
3630
            }
3631
        }
3632
    }
3633
    my %img = ("path", "--", "name", "--", "installable", "true", "status", "unused");
3634
    push @filteredfiles, \%img;
3635
    if ($obj->{raw}) {
3636
        return \@filteredfiles;
3637
    } else {
3638
        my $json_text = JSON::to_json(\@filteredfiles);
3639
        $res .= qq/{"identifier": "path", "label": "name", "items": $json_text }/;
3640
        return $res;
3641
    }
3642
}
3643

    
3644
sub Updatebtime {
3645
    my ($img, $action, $obj) = @_;
3646
    if ($help) {
3647
        return <<END
3648
GET:image:
3649
END
3650
    }
3651
    my $res;
3652
    $curimg = $curimg || $img;
3653
    my $imguser = $register{$curimg}->{'user'};
3654
    if ($isadmin || $imguser eq $user) {
3655
        my $btime;
3656
        $btime = getBtime($curimg, $imguser) if ($imguser);
3657
        if ($btime) {
3658
            $register{$curimg}->{'btime'} = $btime ;
3659
            $res .= "Status=OK $curimg has btime: " . scalar localtime( $btime ) . "\n";
3660
        } else {
3661
            $register{$curimg}->{'btime'} = '' ;
3662
            $res .= "Status=OK $curimg has no btime\n";
3663
        }
3664
    } else {
3665
        $res .= "Status=Error no access to $curimg\n";
3666
    }
3667
    return $res;
3668
}
3669

    
3670
sub Updateallbtimes {
3671
    my ($img, $action) = @_;
3672
    if ($help) {
3673
        return <<END
3674
GET::
3675
END
3676
    }
3677
    if ($isadmin) {
3678
        foreach my $path (keys %register) {
3679
            my $imguser = $register{$path}->{'user'};
3680
            my $btime = getBtime($path, $imguser);
3681
            if ($btime) {
3682
                $register{$path}->{'btime'} = $btime ;
3683
                $postreply .= "Status=OK $register{$path}->{'name'} ($path) has btime: " . scalar localtime( $btime ) . "\n";
3684
            } else {
3685
                $postreply .= "Status=OK $register{$path}->{'name'} ($path) has no btime\n";
3686
            }
3687
        }
3688
    } else {
3689
        $postreply .= "Status=ERROR you are not allowed to do this.\n";
3690
    }
3691
    return $postreply;
3692
}
3693

    
3694
# Activate image from fuel
3695
sub Activate {
3696
    my ($curimg, $action, $argref) = @_;
3697
    if ($help) {
3698
        return <<END
3699
GET:image, name, managementlink, upgradelink, terminallink, force:
3700
Activate an image from fuel storage, making it available for regular use.
3701
END
3702
    }
3703
    my %uargs = %{$argref};
3704
    my $name = URI::Escape::uri_unescape($uargs{'name'});
3705
    my $managementlink = URI::Escape::uri_unescape($uargs{'managementlink'});
3706
    my $upgradelink = URI::Escape::uri_unescape($uargs{'upgradelink'});
3707
    my $terminallink = URI::Escape::uri_unescape($uargs{'terminallink'});
3708
    my $version = URI::Escape::uri_unescape($uargs{'version'}) || '1.0b';
3709
    my $image2 =  URI::Escape::uri_unescape($uargs{'image2'});
3710
    my $force = $uargs{'force'};
3711

    
3712
    return "Status=ERROR image must be in fuel storage ($curimg)\n" unless ($curimg =~ /^\/mnt\/fuel\/pool(\d+)\/(.+)/);
3713
    my $pool = $1;
3714
    my $ipath = $2;
3715
    return "Status=ERROR image is not a qcow2 image ($curimg, $ipath)\n" unless ($ipath =~ /(.+\.qcow2$)/);
3716
    my $npath = $1;
3717
    my $ppath = '';
3718
    if ($npath =~ /(.*\/)(.+\.qcow2$)/) {
3719
        $npath = $2;
3720
        $ppath = $1;
3721
    }
3722
    my $imagepath = $tenderpathslist[$pool] . "/$user/fuel/$ipath";
3723
    my $newpath = $tenderpathslist[$pool] . "/$user/$npath";
3724
    return "Status=ERROR image not found ($imagepath)\n" unless (-e $imagepath);
3725
    return "Status=ERROR image already exists in destination ($newpath)\n" if (-e $newpath && !$force);
3726
    return "Status=ERROR image is in use ($newpath)\n" if (-e $newpath && $register{$newpath} && $register{$newpath}->{'status'} ne 'unused');
3727

    
3728
    my $virtualsize = `qemu-img info --force-share "$imagepath" | sed -n -e 's/^virtual size: .*(//p' | sed -n -e 's/ bytes)//p'`;
3729
    chomp $virtualsize;
3730
#    my $master = `qemu-img info --force-share "$imagepath" | sed -n -e 's/^backing file: //p' | sed -n -e 's/ (actual path:.*)\$//p'`;
3731
    my $master = `qemu-img info --force-share "$imagepath" | sed -n -e 's/^backing file: //p'`;
3732
    chomp $master;
3733

    
3734
    # Now deal with image2
3735
    my $newpath2 = '';
3736
    if ($image2) {
3737
        $image2 = "/mnt/fuel/pool$pool/$ppath$image2" unless ($image2 =~ /^\//);
3738
        return "Status=ERROR image2 must be in fuel storage ($image2)\n" unless ($image2 =~ /^\/mnt\/fuel\/pool$pool\/(.+)/);
3739
        $ipath = $1;
3740
        return "Status=ERROR image is not a qcow2 image\n" unless ($ipath =~ /(.+\.qcow2$)/);
3741
        $npath = $1;
3742
        $npath = $1 if ($npath =~ /.*\/(.+\.qcow2$)/);
3743
        my $image2path = $tenderpathslist[$pool] . "/$user/fuel/$ipath";
3744
        $newpath2 = $tenderpathslist[$pool] . "/$user/$npath";
3745
        return "Status=ERROR image2 not found ($image2path)\n" unless (-e $image2path);
3746
        return "Status=ERROR image2 already exists in destination ($newpath2)\n" if (-e $newpath2 && !$force);
3747
        return "Status=ERROR image2 is in use ($newpath2)\n" if (-e $newpath2 && $register{$newpath2} && $register{$newpath2}->{'status'} ne 'unused');
3748

    
3749
        my $virtualsize2 = `qemu-img info --force-share "$image2path" | sed -n -e 's/^virtual size: .*(//p' | sed -n -e 's/ bytes)//p'`;
3750
        chomp $virtualsize2;
3751
#        my $master2 = `qemu-img info --force-share "$image2path" | sed -n -e 's/^backing file: //p' | sed -n -e 's/ (actual path:.*)\$//p'`;
3752
        my $master2 = `qemu-img info --force-share "$image2path" | sed -n -e 's/^backing file: //p'`;
3753
        chomp $master2;
3754
        if ($register{$master2}) {
3755
            $register{$master2}->{'status'} = 'used';
3756
        }
3757
        `mv "$image2path" "$newpath2"`;
3758
        if (-e $newpath2) {
3759
            my $ug = new Data::UUID;
3760
            my $newuuid = $ug->create_str();
3761
            unless ($name) {
3762
                $name = $npath if ($npath);
3763
                $name = $1 if ($name =~ /(.+)\.(qcow2)$/);
3764
            }
3765
            $register{$newpath2} = {
3766
                uuid => $newuuid,
3767
                path => $newpath2,
3768
                master => $master2,
3769
                name => "$name (data)",
3770
                user => $user,
3771
                storagepool => $pool,
3772
                type => 'qcow2',
3773
                status => 'unused',
3774
                version => $version,
3775
                virtualsize => $virtualsize2
3776
            };
3777
            $postreply .= "Status=OK Activated data image $newpath2, $name (data), $newuuid\n";
3778
        } else {
3779
            $postreply .=  "Status=ERROR Unable to activate $image2path, $newpath2\n";
3780
        }
3781
    }
3782

    
3783
    # Finish up primary image
3784
    if ($register{$master}) {
3785
        $register{$master}->{'status'} = 'used';
3786
    }
3787
    `mv "$imagepath" "$newpath"`;
3788
    if (-e $newpath) {
3789
        my $ug = new Data::UUID;
3790
        my $newuuid = $ug->create_str();
3791
        unless ($name) {
3792
            $name = $npath if ($npath);
3793
            $name = $1 if ($name =~ /(.+)\.(qcow2)$/);
3794
        }
3795
        $register{$newpath} = {
3796
            uuid => $newuuid,
3797
            path => $newpath,
3798
            master => $master,
3799
            name => $name,
3800
            user => $user,
3801
            storagepool => $pool,
3802
            image2 => $newpath2,
3803
            type => 'qcow2',
3804
            status => 'unused',
3805
            installable => 'true',
3806
            managementlink => $managementlink || '/stabile/pipe/http://{uuid}:10000/stabile/',
3807
            upgradelink => $upgradelink,
3808
            terminallink => $terminallink,
3809
            version => $version,
3810
            virtualsize => $virtualsize
3811
        };
3812
        $postreply .=  "Status=OK Activated $newpath, $name, $newuuid\n";
3813
    } else {
3814
        $postreply .=  "Status=ERROR Unable to activate $imagepath to $newpath\n";
3815
    }
3816
    return $postreply;
3817
}
3818

    
3819
sub Uploadtoregistry {
3820
    my ($path, $action, $obj) = @_;
3821
    if ($help) {
3822
        return <<END
3823
GET:image, force:
3824
Upload an image to the registry. Set [force] if you want to force overwrite images in registry - use with caution.
3825
END
3826
    }
3827
    $force = $obj->{'force'};
3828
    if (-e $path && ($register{$path}->{'user'} eq $user || $isadmin)) {
3829
        $postreply .= $main::uploadToOrigo->($engineid, $path, $force);
3830
    } else {
3831
        $postreply .= "Status=Error Not allowed\n";
3832
    }
3833
    return $postreply;
3834
}
3835

    
3836
sub Publish {
3837
    my ($uuid, $action, $parms) = @_;
3838
    if ($help) {
3839
        return <<END
3840
GET:image,appid,appstore,force:
3841
Publish a stack to registry. Set [force] if you want to force overwrite images in registry - use with caution.
3842
END
3843
    }
3844
    my $res;
3845
    $uuid = $parms->{'uuid'} if ($uuid =~ /^\// || !$uuid);
3846
    my $force = $parms->{'force'};
3847
    my $freshen = $parms->{'freshen'};
3848

    
3849
    if ($isreadonly) {
3850
        $res .= "Status=ERROR Your account does not have the necessary privilege.s\n";
3851
    } elsif (!$uuid || !$imagereg{$uuid}) {
3852
        $res .= "Status=ERROR At least specify activated master image uuid [uuid or path] to publish.\n";
3853
    } elsif ($imagereg{$uuid}->{'user'} ne $user && !$isadmin) {
3854
        $res .= "Status=ERROR Your account does not have the necessary privileges.\n";
3855
    } elsif ($imagereg{$uuid}->{'path'} =~ /.+\.master\.qcow2$/) {
3856
        if ($engineid eq $valve001id) { # On valve001 - check if meta file exists
3857
            if (-e $imagereg{$uuid}->{'path'} . ".meta") {
3858
                $res .= "On valve001. Found meta file $imagereg{$uuid}->{'path'}.meta\n";
3859
                my $appid = `cat $imagereg{$uuid}->{'path'}.meta | sed -n -e 's/^APPID=//p'`;
3860
                chomp $appid;
3861
                if ($appid) {
3862
                    $parms->{'appid'} = $appid;
3863
                    $register{$imagereg{$uuid}->{'path'}}->{'appid'} = $appid;
3864
                    tied(%register)->commit;
3865
                }
3866
            }
3867
        # On valve001 - move image to stacks
3868
            if ($imagereg{$uuid}->{'storagepool'} ne '0') {
3869
                $res .= "Status=OK Moving image: " . Move($imagereg{$uuid}->{'path'}, $user, 0) . "\n";
3870
            } else {
3871
                $res .= "Status=OK Image is already available in registry\n";
3872
            }
3873
        } else {
3874
        #    $console = 1;
3875
        #    my $link = Download($imagereg{$uuid}->{'path'});
3876
        #    chomp $link;
3877
        #    $parms->{'downloadlink'} = $link; # We now upload instead
3878
        #    $res .= "Status=OK Asking registry to download $parms->{'APPID'} image: $link\n";
3879
            if ($appstores) {
3880
                $parms->{'appstore'} = $appstores;
3881
            } elsif ($appstoreurl =~ /www\.(.+)\//) {
3882
                $parms->{'appstore'} = $1;
3883
                $res .= "Status=OK Adding registry: $1\n";
3884
            }
3885
        }
3886
#        $parms->{'appstore'} = 1 if ($freshen);
3887

    
3888
        my %imgref = %{$imagereg{$uuid}};
3889
        $parms = Hash::Merge::merge($parms, \%imgref);
3890
        my $postdata = to_json($parms);
3891
        my $postres = $main::postToOrigo->($engineid, 'publishapp', $postdata);
3892
        $res .= $postres;
3893
        my $appid;
3894
        $appid = $1 if ($postres =~ /appid: (\d+)/);
3895
        my $path = $imagereg{$uuid}->{'path'};
3896
        if ($freshen && $appid) {
3897
            $res .= "Status=OK Freshened the stack description\n";
3898
        } elsif ($appid) {
3899
            $register{$path}->{'appid'} = $appid if ($register{$path});
3900
            $res .= "Status=OK Received appid $appid for $path, uploading image to registry, hang on...\n";
3901
            my $upres .= $main::uploadToOrigo->($engineid, $path, $force);
3902
            $res .= $upres;
3903
            my $image2 = $register{$path}->{'image2'} if ($register{$path});
3904
            if ($upres =~ /Status=OK/ && $image2 && $image2 ne '--') { # Stack has a data image
3905
                $res .= $main::uploadToOrigo->($engineid, $image2, $force);
3906
            }
3907
        } else {
3908
            $res .= "Status=Error Did not get an appid\n";
3909
        }
3910
    } else {
3911
        $res .= "Status=ERROR You can only publish a master image.\n";
3912
    }
3913
    return $res;
3914
}
3915

    
3916
sub Release {
3917
    my ($uuid, $action, $parms) = @_;
3918
    if ($help) {
3919
        return <<END
3920
GET:image,appid,appstore,force,unrelease:
3921
Releases a stack in the registry, i.e. moves it from being a private stack only owner and owner's users can see and use to being a public stack, everyone can use. Set [force] if you want to force overwrite images in registry - use with caution.
3922
END
3923
    }
3924
    my $res;
3925
    $uuid = $parms->{'uuid'} if ($uuid =~ /^\// || !$uuid);
3926
    my $force = $parms->{'force'};
3927
    my $unrelease = $parms->{'unrelease'};
3928

    
3929
    if (!$uuid || !$imagereg{$uuid}) {
3930
        $res .= "Status=ERROR At least specify master image uuid [uuid or path] to release.\n";
3931
    } elsif (!$isadmin) {
3932
        $res .= "Status=ERROR Your account does not have the necessary privileges.\n";
3933
    } elsif ($imagereg{$uuid}->{'path'} =~ /.+\.master\.qcow2$/ && $imagereg{$uuid}->{'appid'}) {
3934
        my $action = 'release';
3935
        my $targetuser = 'common';
3936
        if ($unrelease) {
3937
            $action = 'unrelease';
3938
            $targetuser = $user;
3939
        }
3940
        if ($appstores) {
3941
            $parms->{'appstore'} = $appstores;
3942
        } elsif ($appstoreurl =~ /www\.(.+)\//) {
3943
            $parms->{'appstore'} = $1;
3944
            $res .= "Status=OK Adding registry: $1\n";
3945
        }
3946
        $parms->{'appid'} = $imagereg{$uuid}->{'appid'};
3947
        $parms->{'force'} = $force if ($force);
3948
        $parms->{'unrelease'} = $unrelease if ($unrelease);
3949
        my $postdata = to_json($parms);
3950
        my $postres = $main::postToOrigo->($engineid, 'releaseapp', $postdata);
3951
        $res .= $postres;
3952
        my $appid;
3953
        $appid = $1 if ($postres =~ /Status=OK Moved (\d+)/);
3954
        my $path = $imagereg{$uuid}->{'path'};
3955
        if ($appid) {
3956
            $res.= "Now moving local stack to $targetuser\n";
3957
            # First move data image
3958
            my $image2 = $register{$path}->{'image2'} if ($register{$path});
3959
            my $newimage2 = $image2;
3960
            if ($image2 && $image2 ne '--' && $register{$image2}) { # Stack has a data image
3961
                if ($unrelease) {
3962
                    $newimage2 =~ s/common/$register{$image2}->{'user'}/;
3963
                } else {
3964
                    $newimage2 =~ s/$register{$image2}->{'user'}/common/;
3965
                }
3966
                $register{$path}->{'image2'} = $newimage2;
3967
                tied(%register)->commit;
3968
                $res .= Move($image2, $targetuser, '', '', 1);
3969
            }
3970
            # Move image
3971
            $res .= Move($path, $targetuser, '', '', 1);
3972
            $res .= "Status=OK $action $appid\n";
3973
        } else {
3974
            $res .= "Status=Error $action failed\n";
3975
        }
3976
    } else {
3977
        $res .= "Status=ERROR You can only $action a master image that has been published.\n";
3978
    }
3979
    return $res;
3980
}
3981

    
3982
sub do_unlinkmaster {
3983
    my ($img, $action) = @_;
3984
    if ($help) {
3985
        return <<END
3986
GET:image,path:
3987
END
3988
    }
3989
    my $res;
3990
    $res .= header('text/html') unless ($console);
3991
    if ($isreadonly) {
3992
        $res .= "Your account does not have the necessary privileges\n";
3993
    } elsif ($curimg) {
3994
        $res .= unlinkMaster($curimg) . "\n";
3995
    } else {
3996
        $res .= "Please specify master image to link\n";
3997
    }
3998
    return $res;
3999
}
4000

    
4001
# Simple action for unmounting all images
4002
sub do_unmountall {
4003
    my ($img, $action) = @_;
4004
    if ($help) {
4005
        return <<END
4006
GET:image,path:
4007
END
4008
    }
4009
    return "Your account does not have the necessary privileges\n" if ($isreadonly);
4010
    my $res;
4011
    $res .= header('text/plain') unless ($console);
4012
    $res .= "Unmounting all images for $user\n";
4013
    unmountAll();
4014
    $res .= "\n$postreply" if ($postreply);
4015
    return $res;
4016
}
4017

    
4018
sub Updatedownloads {
4019
    my ($img, $action) = @_;
4020
    if ($help) {
4021
        return <<END
4022
GET:image,path:
4023
END
4024
    }
4025
    my $res;
4026
    $res .= header('text/html') unless ($console);
4027
    my $txt1 = <<EOT
4028
Options -Indexes
4029
EOT
4030
    ;
4031
    `/bin/mkdir "$Stabile::basedir/download"` unless (-e "$Stabile::basedir/download");
4032
    $res .= "Writing .htaccess: -> $Stabile::basedir/download/.htaccess\n";
4033
    unlink("$Stabile::basedir/download/.htaccess");
4034
    `chown www-data:www-data "$Stabile::basedir/download"`;
4035
    `/bin/echo "$txt1" | sudo -u www-data tee "$Stabile::basedir/download/.htaccess"`; #This ugliness is needed because of ownership issues with Synology NFS
4036
    `chmod 644 "$Stabile::basedir/download/.htaccess"`;
4037
    foreach my $p (@spools) {
4038
        my $dir = $p->{'path'};
4039
        my $id = $p->{'id'};
4040
        `/bin/rm "$Stabile::basedir/download/$id"; /bin/ln -s "$dir" "$Stabile::basedir/download/$id"`;
4041
        $res .= "Writing .htaccess: $id -> $dir/.htaccess\n";
4042
        unlink("$dir/.htaccess");
4043
        `/bin/echo "$txt1" | tee "$dir/.htaccess"`;
4044
        `chown www-data:www-data "$dir/.htaccess"`;
4045
        `chmod 644 "$dir/.htaccess"`;
4046
    }
4047

    
4048
    unless ( tie(%userreg,'Tie::DBI', Hash::Merge::merge({table=>'users', key=>'username', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
4049

    
4050
    foreach my $username (keys %userreg) {
4051
        my $require = '';
4052
        my $txt = <<EOT
4053
order deny,allow
4054
AuthName "Download"
4055
AuthType None
4056
TKTAuthLoginURL $baseurl/auth/login.cgi
4057
TKTAuthIgnoreIP on
4058
deny from all
4059
Satisfy any
4060
require user $username
4061
Options -Indexes
4062
EOT
4063
        ;
4064
        foreach my $p (@spools) {
4065
            my $dir = $p->{'path'};
4066
            my $id = $p->{'id'};
4067
            if (-d "$dir/$username") {
4068
                $res .= "Writing .htaccess: $id -> $dir/$username/.htaccess\n";
4069
                unlink("$dir/$username/.htaccess");
4070
                `/bin/echo "$txt1" | sudo -u www-data tee $dir/$username/.htaccess`;
4071
                if ($tenderlist[$p->{'id'}] eq 'local') {
4072
                    if (!(-e "$dir/$username/fuel") && -e "$dir/$username") {
4073
                        `mkdir "$dir/$username/fuel"`;
4074
                        `chmod 777 "$dir/$username/fuel"`;
4075
                    }
4076
                }
4077
            }
4078
        }
4079
    }
4080
    untie %userreg;
4081
    return $res;
4082
}
4083

    
4084
sub do_listpackages($action) {
4085
    my ($image, $action) = @_;
4086
    if ($help) {
4087
        return <<END
4088
GET:image:
4089
Tries to mount and list software packages installed on the operating system on an image. The image must be mountable and contain a valid operating system.
4090
END
4091
    }
4092
    my $res;
4093
    $res .= header('text/plain') unless ($console);
4094

    
4095
    my $mac = $register{$image}->{'mac'};
4096
    my $macip;
4097
    if ($mac && $mac ne '--') {
4098
        unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
4099
        $macip = $nodereg{$mac}->{'ip'};
4100
        untie %nodereg;
4101
    }
4102
    $image =~ /(.+)/; $image = $1;
4103
    my $apps;
4104

    
4105
    if ($macip && $macip ne '--') {
4106
        my $cmd = qq[eval \$(/usr/bin/guestfish --ro -a "$image" --i --listen); ]; # sets $GUESTFISH_PID shell var
4107
        $cmd .= qq[root="\$(/usr/bin/guestfish --remote inspect-get-roots)"; ];
4108
        $cmd .= qq[guestfish --remote inspect-list-applications "\$root"; ];
4109
        $cmd .= qq[guestfish --remote inspect-get-product-name "\$root"; ];
4110
        $cmd .= qq[guestfish --remote exit];
4111
        $cmd = "$sshcmd $macip '$cmd'";
4112
        $apps = `$cmd`;
4113
    } else {
4114
        my $cmd;
4115
        #        my $pid = open my $cmdpipe, "-|",qq[/usr/bin/guestfish --ro -a "$image" --i --listen];
4116
        $cmd .= qq[eval \$(/usr/bin/guestfish --ro -a "$image" --i --listen); ];
4117
        # Start listening guestfish
4118
        my $daemon = Proc::Daemon->new(
4119
            work_dir => '/usr/local/bin',
4120
            setuid => 'www-data',
4121
            exec_command => $cmd
4122
        ) or do {$postreply .= "Status=ERROR $@\n";};
4123
        my $pid = $daemon->Init();
4124
        while ($daemon->Status($pid)) {
4125
            sleep 1;
4126
        }
4127
        # Find pid of the listening guestfish
4128
        my $pid2;
4129
        my $t = new Proc::ProcessTable;
4130
        foreach $p ( @{$t->table} ){
4131
            my $pcmd = $p->cmndline;
4132
            if ($pcmd =~ /guestfish.+$image/) {
4133
                $pid2 = $p->pid;
4134
                last;
4135
            }
4136
        }
4137

    
4138
        my $cmd2;
4139
        if ($pid2) {
4140
            $cmd2 .= qq[root="\$(/usr/bin/guestfish --remote=$pid2 inspect-get-roots)"; ];
4141
            $cmd2 .= qq[guestfish --remote=$pid2 inspect-list-applications "\$root"; ];
4142
            $cmd2 .= qq[guestfish --remote=$pid2 inspect-get-product-name "\$root"; ];
4143
            $cmd2 .= qq[guestfish --remote=$pid2 exit];
4144
        }
4145
        $apps = `$cmd2`;
4146
    }
4147
    if ($console) {
4148
        $res .= $apps;
4149
    } else {
4150
        my @packages;
4151
        my @packages2;
4152
        open my $fh, '<', \$apps or die $!;
4153
        my $i;
4154
        while (<$fh>) {
4155
            if ($_ =~ /\[(\d+)\]/) {
4156
                push @packages2, $packages[$i];
4157
                $i = $1;
4158
            } elsif ($_ =~ /(\S+): (.+)/ && $2) {
4159
                $packages[$i]->{$1} = $2;
4160
            }
4161
        }
4162
        close $fh or die $!;
4163
        $res .= to_json(\@packages, {pretty => 1});
4164
    }
4165
    return $res;
4166
}
4167

    
4168
sub Inject {
4169
    my ($image, $action, $obj) = @_;
4170
    if ($help) {
4171
        return <<END
4172
GET:image:
4173
Tries to inject drivers into a qcow2 image with a Windows OS installed on it. Image must not be in use.
4174
END
4175
    }
4176
    $uistatus = "injecting";
4177
    my $path = $obj->{path} || $curimg;
4178
    my $status = $obj->{status};
4179
    my $esc_localpath = shell_esc_chars($path);
4180

    
4181
    # Find out if we are dealing with a Windows image
4182
    # my $xml = `bash -c '/usr/bin/virt-inspector -a $esc_localpath'`;
4183
    my $xml = `bash -c '/usr/bin/virt-inspector -a $esc_localpath' 2>&1`;
4184
    # $res .= $xml . "\n";
4185
    my $xmlref;
4186
    my $osname;
4187
    $xmlref = XMLin($xml) if ($xml =~ /^<\?xml/);
4188
    $osname = $xmlref->{operatingsystem}->{name} if ($xmlref);
4189
    if ($xmlref && $osname eq 'windows') {
4190
    #    my $upath = $esc_localpath;
4191
        my $upath = $path;
4192
        # We need write privileges
4193
        $res .= `chmod 666 "$upath"`;
4194
        # First try to merge storage registry keys into Windows registry. If not a windows vm it simply fails.
4195
        $res .= `bash -c 'cat /usr/share/stabile/mergeide.reg | /usr/bin/virt-win-reg --merge "$upath"' 2>&1`;
4196
        # Then try to merge the critical device keys. This has been removed in win8 and 2012, so will simply fail for these.
4197
        $res .= `bash -c 'cat /usr/share/stabile/mergeide-CDDB.reg | /usr/bin/virt-win-reg --merge "$upath"' 2>&1`;
4198
        if ($res) { $main::syslogit->($user, "info", $res); $res = ''; }
4199

    
4200
        # Try to copy viostor.sys into image
4201
        my @winpaths = (
4202
            '/Windows/System32/drivers',
4203
            '/WINDOWS/system32/drivers',
4204
            '/WINDOWS/System32/drivers',
4205
            '/WINNT/system32/drivers'
4206
        );
4207
        foreach my $winpath (@winpaths) {
4208
            my $lscmd = qq|bash -c 'virt-ls -a "$upath" "$winpath"'|;
4209
            my $drivers = `$lscmd`;
4210
            if ($drivers =~ /viostor/i) {
4211
                $postreply .= "Status=$status viostor already installed in $winpath in $upath\n";
4212
                $main::syslogit->($user, "info", "viostor already installed in $winpath in $upath");
4213
                last;
4214
            } elsif ($drivers) {
4215
                `umount "$upath"`; # Unmount if mounted by browse operation or similar
4216
                my $cmd = qq|bash -c 'guestfish --rw -i -a "$upath" upload /usr/share/stabile/VIOSTOR.SYS $winpath/viostor.sys' 2>&1|;
4217
                my $error = `$cmd`;
4218
                if ($error) {
4219
                    $postreply .= "$cmd\n";
4220
                    $postreply .= "Status=ERROR Problem injecting virtio drivers into $winpath on $upath: $error\n";
4221
                    $main::syslogit->($user, "info", "Error injecting virtio drivers into $upath: $error");
4222
                } else {
4223
                    $postreply .= "Status=$status Injected virtio drivers into $upath\n";
4224
                    $main::syslogit->($user, "info", "Injected virtio drivers into $upath");
4225
                }
4226
                last;
4227
            } else {
4228
                $postreply .= "Status=ERROR No drivers found in $winpath\n";
4229
            }
4230
        }
4231

    
4232
    } else {
4233
        $postreply .= "Status=ERROR No Windows OS found in $osname image, not injecting drivers.\n";
4234
        $main::syslogit->($user, "info", "No Windows OS found ($osname) in image, not injecting drivers.");
4235
    }
4236
    my $msg = $postreply;
4237
    $msg = $1 if ($msg =~ /\w+=\w+ (.+)/);
4238
    chomp $msg;
4239
    $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$obj->{'uuid'}, status=>$status, message=>$msg});
4240
    $postreply .=  "Status=$uistatus $obj->{type} image: $obj->{name}\n";
4241
    $main::syslogit->($user, "info", "$uistatus $obj->{type} image: $obj->{name}: $uuid");
4242
    return $postreply;
4243
}
4244

    
4245
sub Convert {
4246
    my ($image, $action, $obj) = @_;
4247
    if ($help) {
4248
        return <<END
4249
GET:image:
4250
Converts an image to qcow2 format. Image must not be in use.
4251
END
4252
    }
4253
    my $path = $obj->{path};
4254
    $uistatus = "converting";
4255
    $uipath = $path;
4256
    if ($obj->{status} ne "unused" && $obj->{status} ne "used" && $obj->{status} ne "paused") {
4257
        $postreply .= "Status=ERROR Problem $uistatus $obj->{type} image: $obj->{name}\n";
4258
    } elsif ($obj->{type} eq "img" || $obj->{type} eq "vmdk" || $obj->{type} eq "vhd" || $obj->{type} eq "vhdx") {
4259
        my $oldpath = $path;
4260
        my $newpath = "$path.qcow2";
4261
        if ($obj->{mac} && $path =~ /\/mnt\/stabile\/node\//) {
4262
            unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
4263
            $macip = $nodereg{$obj->{mac}}->{'ip'};
4264
            untie %nodereg;
4265
            $oldpath = "$macip:$path";
4266
        } else { # We are not on a node - check that image is not on a read-only filesystem
4267
            my ($fname, $destfolder) = fileparse($path);
4268
            my $ro = `touch "$destfolder/test.tmp" && { rm "$destfolder/test.tmp"; } || echo "read-only" 2>/dev/null`;
4269
            if ($ro) { # Destinationfolder is not writable
4270
                my $npath = "$spools[0]->{'path'}/$register{$path}->{'user'}/$fname.qcow2";
4271
                $newpath = $npath;
4272
            }
4273
            if (-e $newpath) { # Don't overwrite existing file
4274
                my $subpath = substr($newpath,0,-6);
4275
                my $i = 1;
4276
                if ($newpath =~ /(.+)\.(\d+)\.qcow2/) {
4277
                    $i = $2;
4278
                    $subpath = $1;
4279
                }
4280
                while (-e $newpath) {
4281
                    $newpath = $subpath . ".$i.qcow2";
4282
                    $i++;
4283
                }
4284
            }
4285
        }
4286
        eval {
4287
            my $ug = new Data::UUID;
4288
            my $newuuid = $ug->create_str();
4289

    
4290
            $register{$newpath} = {
4291
                uuid=>$newuuid,
4292
                name=>"$obj->{name} (converted)",
4293
                notes=>$obj->{notes},
4294
                image2=>$obj->{image2},
4295
                managementlink=>$obj->{managementlink},
4296
                upgradelink=>$obj->{managementlink},
4297
                terminallink=>$obj->{terminallink},
4298
                storagepool=>$obj->{regstoragepool},
4299
                status=>$uistatus,
4300
                mac=>($obj->{regstoragepool} == -1)?$obj->{mac}:"",
4301
                size=>0,
4302
                realsize=>0,
4303
                virtualsize=>$obj->{virtualsize},
4304
                type=>"qcow2",
4305
                user=>$user
4306
            };
4307
            $register{$path}->{'status'} = $uistatus;
4308

    
4309
            my $daemon = Proc::Daemon->new(
4310
                work_dir => '/usr/local/bin',
4311
                exec_command => "perl -U steamExec $user $uistatus $obj->{status} \"$oldpath\" \"$newpath\""
4312
            ) or do {$postreply .= "Status=ERROR $@\n";};
4313
            my $pid = $daemon->Init() or do {$postreply .= "Status=ERROR $@\n";};
4314
            $postreply .=  "Status=OK $uistatus $obj->{type} image: $obj->{name}\n";
4315
            $main::syslogit->($user, "info", "$uistatus $obj->{type} image: $obj->{name}: $uuid");
4316
        } or do {$postreply .= "Status=ERROR $@\n";};
4317
        $main::updateUI->({tab=>"images", user=>$user, type=>"update"});
4318
    } else {
4319
        $postreply .= "Status=ERROR Only img and vmdk images can be converted\n";
4320
    }
4321
    return $postreply;
4322
}
4323

    
4324
sub Snapshot {
4325
    my ($image, $action, $obj) = @_;
4326
    if ($help) {
4327
        return <<END
4328
GET:image:
4329
Adds a snapshot to a qcow2 image. Image can not be in use by a running server.
4330
END
4331
    }
4332
    my $status = $obj->{status};
4333
    my $path = $obj->{path};
4334
    my $macip;
4335
    $uistatus = "snapshotting";
4336
    $uiuuid = $obj->{uuid};
4337
    if ($status ne "unused" && $status ne "used") {
4338
        $postreply .= "Status=ERROR Problem $uistatus $obj->{type} image: $obj->{name}\n";
4339
    } elsif ($obj->{type} eq "qcow2") {
4340
        my $newpath = $path;
4341
        my $hassnap;
4342
        my $snaptime = time;
4343
        if ($obj->{mac} && $path =~ /\/mnt\/stabile\/node\//) {
4344
            unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
4345
            $macip = $nodereg{$obj->{mac}}->{'ip'};
4346
            untie %nodereg;
4347
            $newpath = "$macip:$path";
4348
            my $esc_path = $path;
4349
            $esc_path =~ s/([ ])/\\$1/g;
4350
            my $qinfo = `$sshcmd $macip "sudo /usr/bin/qemu-img snapshot -l $esc_path"`;
4351
            $hassnap = ($qinfo =~ /snap1/g);
4352
            $postreply .= `$sshcmd $macip "sudo /usr/bin/qemu-img snapshot -d snap1 $esc_path"` if ($hassnap);
4353
        } else {
4354
            my $qinfo = `/usr/bin/qemu-img snapshot -l "$path"`;
4355
            $hassnap = ($qinfo =~ /snap1/g);
4356
            $postreply .= `/usr/bin/qemu-img snapshot -d snap1 "$path\n"` if ($hassnap);
4357
        }
4358
        eval {
4359
            if ($hassnap) {
4360
                $postreply .= "Status=Error Only one snapshot per image is supported for $obj->{type} image: $obj->{name} ";
4361
            } else {
4362
                $register{$path}->{'status'} = $uistatus;
4363
                $register{$path}->{'snap1'} = $snaptime;
4364

    
4365
                if ($macip) {
4366
                    my $esc_localpath = shell_esc_chars($path);
4367
                    $res .= `$sshcmd $macip "sudo /usr/bin/qemu-img snapshot -c snap1 $esc_localpath"`;
4368
                } else {
4369
                    $res .= `/usr/bin/qemu-img snapshot -c snap1 "$path"`;
4370
                }
4371
                $register{$path}->{'status'} = $status;
4372
                $postreply .=  "Status=$uistatus OK $uistatus $obj->{type} image: $obj->{name}\n";
4373
                $main::syslogit->($user, "info", "$uistatus $obj->{type} image: $obj->{name}: $uuid");
4374
            }
4375
            1;
4376
        } or do {$postreply .= "Status=ERROR $@\n";};
4377
        $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$obj->{'uuid'}, status=>$status, snap1=>$snaptime});
4378
    } else {
4379
        $postreply .= "Status=ERROR Only qcow2 images can be snapshotted\n";
4380
    }
4381
    return $postreply;
4382
}
4383

    
4384
sub Unsnap {
4385
    my ($image, $action, $obj) = @_;
4386
    if ($help) {
4387
        return <<END
4388
GET:image:
4389
Removes a snapshot from a qcow2 image. Image can not be in use by a running server.
4390
END
4391
    }
4392
    my $status = $obj->{status};
4393
    my $path = $obj->{path};
4394
    $uistatus = "unsnapping";
4395
    $uiuuid = $obj->{uuid};
4396
    my $macip;
4397

    
4398
    if ($status ne "unused" && $status ne "used") {
4399
        $postreply .= "Status=ERROR Problem $uistatus $obj->{type} image: $obj->{name}\n";
4400
    } elsif ($obj->{type} eq "qcow2") {
4401
        my $newpath = $path;
4402
        my $hassnap;
4403
        my $qinfo;
4404
        my $esc_path;
4405
        if ($obj->{mac} && $path =~ /\/mnt\/stabile\/node\//) {
4406
            unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
4407
            $macip = $nodereg{$obj->{mac}}->{'ip'};
4408
            untie %nodereg;
4409
            $newpath = "$macip:$path";
4410
            $esc_path = $path;
4411
            $esc_path =~ s/([ ])/\\$1/g;
4412
            $qinfo = `$sshcmd $macip "sudo /usr/bin/qemu-img snapshot -l $esc_path"`;
4413
            $hassnap = ($qinfo =~ /snap1/g);
4414
        } else {
4415
            $qinfo = `/usr/bin/qemu-img snapshot -l "$path"`;
4416
            $hassnap = ($qinfo =~ /snap1/g);
4417
        }
4418
        eval {
4419
            my $snaptime = time;
4420
            if ($hassnap) {
4421
                delete $register{$path}->{'snap1'};
4422
                $register{$path}->{'status'} = $uistatus;
4423
                if ($macip) {
4424
                    my $esc_localpath = shell_esc_chars($path);
4425
                    $res .= `$sshcmd $macip "sudo /usr/bin/qemu-img snapshot -d snap1 $esc_localpath"`;
4426
                } else {
4427
                    $res .= `/usr/bin/qemu-img snapshot -d snap1 "$path"`;
4428
                }
4429
                $register{$path}->{'status'} = $status;
4430
                $postreply .=  "Status=$uistatus OK $uistatus $obj->{type} image: $obj->{name}\n";
4431
                $main::syslogit->($user, "info", "$uistatus $obj->{type} image: $obj->{name}: $uuid");
4432
            } else {
4433
                $postreply .= "Status=ERROR No snapshot found in $obj->{name}\n";
4434
                delete $register{$path}->{'snap1'};
4435
                $uistatus = $status;
4436
            }
4437
            1;
4438
        } or do {$postreply .= "Status=ERROR $@\n";};
4439
        $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$obj->{'uuid'}, status=>$status, snap1=>'--'});
4440
    } else {
4441
        $postreply .= "Status=ERROR Only qcow2 images can be unsnapped\n";
4442
    }
4443
    return $postreply;
4444
}
4445

    
4446
sub Revert {
4447
    my ($image, $action, $obj) = @_;
4448
    if ($help) {
4449
        return <<END
4450
GET:image:
4451
Applies a snapshot to a qcow2 image, i.e. the image is reverted to the state it was in when the snapshot was taken. Image can not be in use by a running server.
4452
END
4453
    }
4454
    my $status = $obj->{status};
4455
    my $path = $obj->{path};
4456
    $uistatus = "reverting";
4457
    $uipath = $path;
4458
    my $macip;
4459
    if ($status ne "used" && $status ne "unused") {
4460
        $postreply .= "Status=ERROR Please shut down or pause your virtual machine before reverting\n";
4461
    } elsif ($obj->{type} eq "qcow2") {
4462
        my $newpath = $path;
4463
        my $hassnap;
4464
        if ($obj->{mac} && $path =~ /\/mnt\/stabile\/node\//) {
4465
            unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
4466
            $macip = $nodereg{$obj->{mac}}->{'ip'};
4467
            untie %nodereg;
4468
            $newpath = "$macip:$path";
4469
            my $esc_path = $path;
4470
            $esc_path =~ s/([ ])/\\$1/g;
4471
            my $qinfo = `ssh -l irigo -i /var/www/.ssh/id_rsa_www -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no $macip "/usr/bin/qemu-img snapshot -l $esc_path"`;
4472
            $hassnap = ($qinfo =~ /snap1/g);
4473
        } else {
4474
            my $qinfo = `/usr/bin/qemu-img snapshot -l "$path"`;
4475
            $hassnap = ($qinfo =~ /snap1/g);
4476
        }
4477
        eval {
4478
            if ($hassnap) {
4479
                $register{$path}->{'status'} = $uistatus;
4480
                if ($macip) {
4481
                    my $esc_localpath = shell_esc_chars($path);
4482
                    $res .= `$sshcmd $macip "sudo /usr/bin/qemu-img snapshot -a snap1 $esc_localpath"`;
4483
                } else {
4484
                    $res .= `/usr/bin/qemu-img snapshot -a snap1 "$path"`;
4485
                }
4486
                $register{$path}->{'status'} = $status;
4487
                $postreply .=  "Status=OK $uistatus $obj->{type} image: $obj->{name}\n";
4488
                $main::syslogit->($user, "info", "$uistatus $obj->{type} image: $obj->{name}: $uuid");
4489
            } else {
4490
                $postreply .= "Status=ERROR no snapshot found\n";
4491
                $uistatus = $status;
4492
            }
4493
            1;
4494
        } or do {$postreply .= "Status=ERROR $@\n";};
4495
        $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$obj->{'uuid'}, status=>$status, snap1=>'--'});
4496
    } else {
4497
        $postreply .= "Status=ERROR Only qcow2 images can be reverted\n";
4498
    }
4499
    return;
4500
}
4501

    
4502
sub Zbackup {
4503
    my ($image, $action, $obj) = @_;
4504
    if ($help) {
4505
        return <<END
4506
GET:mac, storagepool, synconly, snaponly, imageretention, backupretention:
4507
Backs all images on ZFS storage up by taking a storage snapshot. By default all shared storagepools are backed up.
4508
If storagepool -1 is specified, all ZFS node storages is backed up. If "mac" is specified, only specific node is backed up.
4509
If "synconly" is set, no new snapshots are taken - only syncing of snapshots is performed.
4510
If "snaponly" is set, only local active storage snapshot is taken - no sending to backup storage is done.
4511
"xretention" can be either simply number of snapshots to keep, or max age of snapshot to keep in seconds [s], hours [h] or days [d],
4512
e.g. "imageretention=10" will keep 10 image snapshots, "imageretention=600s" will purte image snapshots older than 600 seconds if possible, or "backretention=14d" will purge backup snapshots older than 14 days.
4513
END
4514
    }
4515
    if ($isadmin) {
4516
        my $synconly = $obj->{'synconly'};
4517
        my $snaponly = $obj->{'snaponly'};
4518
        my $mac = $obj->{'mac'};
4519
        my $storagepool = $obj->{'storagepool'};
4520
        $storagepool = -1 if ($mac);
4521
        my $imageretention = $obj->{'imageretention'} || $imageretention;
4522
        my $backupretention = $obj->{'backupretention'} || $backupretention;
4523

    
4524
        my $basepath = "stabile-backup";
4525
        my $bpath = $basepath;
4526
        my $mounts = `/bin/cat /proc/mounts`;
4527
        my $zbackupavailable = (($mounts =~ /$bpath (\S+) zfs/)?$1:'');
4528
        unless ($zbackupavailable) {$postreply .= "Status=OK ZFS backup not available, only doing local snapshots\n";}
4529
        my $zfscmd = "zfs";
4530
        my $macip;
4531
        my $ipath = $spools[0]->{'zfs'} || 'stabile-images/images';
4532
        my @nspools = @spools;
4533
        if (!(defined $obj->{'storagepool'}) || $storagepool == -1) {
4534
            @nspools = () if ($storagepool == -1); # Only do node backups
4535
            unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
4536
#            my $nipath = $ipath;
4537
#            $nipath = "$1/node" if ($nipath =~ /(.+)\/(.+)/);
4538
            my $nipath = 'stabile-node/node';
4539
            foreach my $node (values %nodereg) {
4540
                push @nspools, {
4541
                    mac=>$node->{'mac'},
4542
                    macip=>$node->{'ip'},
4543
                    zfs=>$nipath,
4544
                    id=>-1
4545
                } if ($node->{'stor'} eq 'zfs' && (!$mac || $node->{'mac'} eq $mac))
4546
            }
4547
            untie %nodereg;
4548
        }
4549
        if (`pgrep zfs`) {
4550
            $postreply .= "Status=ERROR Another ZFS backup is running. Please wait a minute...\n";
4551
            $postmsg = "ERROR ERROR Another ZFS backup is running. Please wait a minute...";
4552
            return $postreply;
4553
        }
4554
        $postreply .= "Status=OK Performing ZFS backup on " . (scalar @nspools) . " storage pools with image retention $imageretention, backup retention $backupretention\n";
4555

    
4556
        foreach my $spool (@nspools) {
4557
            $ipath = $spool->{'zfs'};
4558
            if ($spool->{'id'} == -1) { # We're doing a node backup
4559
                $mac = $spool->{'mac'};
4560
                $macip = $spool->{'macip'};
4561
                $bpath = "$basepath/node-$mac";
4562
            } else {
4563
                next unless ($ipath);
4564
                next if (($storagepool || $storagepool eq '0') && $storagepool ne $spool->{'id'});
4565
                $bpath = "$basepath/$1" if ($ipath =~ /.+\/(.+)/);
4566
                $mac = '';
4567
                $macip = '';
4568
            }
4569
            if ($macip) {$zfscmd = "$sshcmd $macip sudo zfs";}
4570
            else {$zfscmd = "zfs";}
4571

    
4572
            $postreply .= "Status=OK Commencing ZFS backup of $ipath $macip, storagepool=$storagepool, synconly=$synconly, snaponly=$snaponly\n";
4573
            my $res;
4574
            my $cmd;
4575
            my @imagesnaps;
4576
            my @backupsnaps;
4577

    
4578
            # example: stabile-images/images@SNAPSHOT-20200524172901
4579
            $cmd = qq/$zfscmd list -t snapshot | grep '$ipath'/;
4580
            my $snaplist = `$cmd`;
4581
            my @snaplines = split("\n", $snaplist);
4582
            foreach my $snap (@snaplines) {
4583
                push @imagesnaps, $2 if ($snap =~ /(.*)\@SNAPSHOT-(\d+)/);
4584
            }
4585
            if ($zbackupavailable) {
4586
                $cmd = qq/zfs list -t snapshot | grep '$bpath'/;
4587
                $snaplist = `$cmd`;
4588
                @snaplines = split("\n", $snaplist);
4589
                foreach my $snap (@snaplines) {
4590
                    push @backupsnaps, $2 if ($snap =~ /(.*)\@SNAPSHOT-(\d+)/);
4591
                }
4592
            }
4593
        # Find matching snapshots
4594
            my $matches=0;
4595
            my $matchbase = 0;
4596
            foreach my $bsnap (@backupsnaps) {
4597
                if ($bsnap eq $imagesnaps[$matchbase + $matches]) { # matching snapshot found
4598
                    $matches++;
4599
                } elsif ($matches) { # backup snapshots are ahead of image snapshots - correct manually, i.e. delete them.
4600
                    $postreply .= "Status=ERROR Snapshots are out of sync.\n";
4601
                    $postmsg = "ERROR Snapshots are out of sync";
4602
                    $main::syslogit->($user, 'info', "ERROR snapshots of $ipath and $bpath are out of sync.");
4603
                    return $postreply;
4604
                } elsif (!$matchbase) { # Possibly there are image snapshots older than there are backup snapshots, find the match base i.e. first match in @imagesnaps
4605
                    my $mb=0;
4606
                    foreach my $isnap (@imagesnaps) {
4607
                        if ($bsnap eq $isnap) { # matching snapshot found
4608
                            $matchbase = $mb;
4609
                            $matches++;
4610
                            last;
4611
                        }
4612
                        $mb++;
4613
                    }
4614
                }
4615
            }
4616

    
4617
            my $lastisnap = $imagesnaps[scalar @imagesnaps -1];
4618
            my $lastisnaptime = timelocal($6,$5,$4,$3,$2-1,$1) if ($lastisnap =~ /(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/);
4619
            my $td = ($current_time - $lastisnaptime);
4620
            if ($td<=5) {
4621
                $postreply .= "Status=ERROR Last backup was taken $td seconds ago. Please wait a minute...\n";
4622
                $postmsg = "ERROR ERROR Last backup was taken $td seconds ago. Please wait a minute...";
4623
                return $postreply;
4624
            }
4625
            my $ni = scalar @imagesnaps;
4626
            my $nb = scalar @backupsnaps;
4627

    
4628
            # If there are unsynced image snaps - sync them
4629
            if ($zbackupavailable && !$snaponly) {
4630
                if (scalar @imagesnaps > $matches+$matchbase) {
4631
                    if ($matches > 0) { # We must have at least one common shapshot to sync
4632
                        for (my $j=$matches+$matchbase; $j < scalar @imagesnaps; $j++) {
4633
                            if ($macip) {
4634
                                $cmd = qq[$zfscmd "send -i $ipath\@SNAPSHOT-$imagesnaps[$j-1] $ipath\@SNAPSHOT-$imagesnaps[$j] | ssh 10.0.0.1 sudo zfs receive $bpath"]; # -R
4635
                            } else {
4636
                                $cmd = qq[zfs send -i $ipath\@SNAPSHOT-$imagesnaps[$j-1] $ipath\@SNAPSHOT-$imagesnaps[$j] | zfs receive $bpath]; # -R
4637
                            }
4638
                            $res = `$cmd 2>&1`;
4639
                            unless (
4640
                                ($res && !$macip) #ssh will warn about adding to list of known hosts
4641
                                    || $res =~ /cannot receive/
4642
                            ) {
4643
                                $matches++;
4644
                                $nb++;
4645
                                $postreply .= "Status=OK Sending ZFS snapshot $j $imagesnaps[$j-1]->$imagesnaps[$j] of $macip $ipath to $bpath $res\n";
4646
                                $main::syslogit->($user, 'info', "OK Sending ZFS snapshot $imagesnaps[$j-1]->$imagesnaps[$j] of $macip $ipath to $bpath $res");
4647
                            } else {
4648
                                $postreply .= "Status=Error Problem sending ZFS snapshot $j $imagesnaps[$j-1]->$imagesnaps[$j] of $macip $ipath to $bpath $res\n";
4649
                                $main::syslogit->($user, 'info', "Error Problem sending ZFS snapshot $imagesnaps[$j-1]->$imagesnaps[$j] of $macip $ipath to $bpath $res");
4650
                            }
4651
                        }
4652
                    } else {
4653
                        $postreply .= "Status=OK Unable to sync $ni snapshots, no common snapshot, trying to start from scratch.\n";
4654
                    }
4655
                }
4656
            }
4657
            $res = '';
4658

    
4659
            if ($matches && !$synconly) { # There was at least one match, snapshots are now assumed to be in sync
4660
        # Then perform the actual snapshot
4661
                my $snap1 = sprintf "%4d%02d%02d%02d%02d%02d",$year,$mon+1,$mday,$hour,$min,$sec;
4662
                my $oldsnap = $imagesnaps[$matches+$matchbase-1];
4663
                $cmd = qq|$zfscmd snapshot -r $ipath\@SNAPSHOT-$snap1|;
4664
                $postreply .= "Status=OK Performing ZFS snapshot with $matches matches and base $matchbase $res\n";
4665
                $res = `$cmd 2>&1`;
4666
                unless ($res && !$macip) {
4667
                    $ni++;
4668
                    push @imagesnaps, $snap1;
4669
                }
4670
        # Send it to backup if asked to
4671
                unless ($snaponly || !$zbackupavailable) {
4672
                    if ($macip) {
4673
                        $cmd = qq[$zfscmd "send -i $ipath\@SNAPSHOT-$oldsnap $ipath\@SNAPSHOT-$snap1 | ssh 10.0.0.1 sudo zfs receive $bpath"];
4674
                    } else {
4675
                        $cmd = qq[zfs send -i $ipath\@SNAPSHOT-$oldsnap $ipath\@SNAPSHOT-$snap1 | zfs receive $bpath]; # -R
4676
                    }
4677
                    $res .= `$cmd 2>&1`;
4678
                    unless ($res && !$macip) {
4679
                        $matches++;
4680
                        $nb++;
4681
                        push @backupsnaps, $snap1;
4682
                    }
4683
                    $postreply .= "Status=OK Sending ZFS snapshot of $macip $ipath $oldsnap->$snap1 to $bpath $res\n";
4684
                    $main::syslogit->($user, 'info', "OK Sending ZFS snapshot of $macip $ipath $oldsnap->$snap1 to $bpath $res");
4685
                }
4686
                $postreply .= "Status=OK Synced $matches ZFS snapshots. There are now $ni image snapshots, $nb backup snapshots.\n";
4687
            } elsif ($matches) {
4688
                $postreply .= "Status=OK Synced $matches ZFS snapshots. There are $ni image snapshots, $nb backup snapshots.\n";
4689
#            } elsif ($ni==0 && $nb==0) { # We start from a blank slate
4690
            } elsif ($nb==0) { # We start from a blank slate
4691
                my $snap1 = sprintf "%4d%02d%02d%02d%02d%02d",$year,$mon+1,$mday,$hour,$min,$sec;
4692
                $cmd = qq|$zfscmd snapshot -r $ipath\@SNAPSHOT-$snap1|;
4693
                $res = `$cmd 2>&1`;
4694
                $postreply .= "Status=OK Performing ZFS snapshot from scratch $res $macip\n";
4695
        # Send it to backup by creating new filesystem (created autotically)
4696
                unless ($snaponly || !$zbackupavailable) {
4697
                    if ($macip) {
4698
                        $cmd = qq[$zfscmd "send $ipath\@SNAPSHOT-$snap1 | ssh 10.0.0.1 sudo zfs receive $bpath"];
4699
                        $res .= `$cmd 2>&1`;
4700
                        $cmd = qq|zfs set readonly=on $bpath|;
4701
                        $res .= `$cmd 2>&1`;
4702
                        $cmd = qq|zfs mount $bpath|;
4703
                        $res .= `$cmd 2>&1`;
4704
                    } else {
4705
                        $cmd = qq[zfs send -R $ipath\@SNAPSHOT-$snap1 | zfs receive $bpath];
4706
                        $res .= `$cmd 2>&1`;
4707
                        $cmd = qq|zfs set readonly=on $bpath|;
4708
                        $res .= `$cmd 2>&1`;
4709
                    }
4710
                    $postreply .= "Status=OK Sending complete ZFS snapshot of $macip:$ipath\@$snap1 to $bpath $res\n";
4711
                    $main::syslogit->($user, 'info', "OK Sending complete ZFS snapshot of $macip:$ipath\@$snap1 to $bpath $res");
4712
                    $matches++;
4713
                    $nb++;
4714
                }
4715
                $ni++;
4716
                $postreply .= "Status=OK Synced 0 ZFS snapshots. There are $ni image snapshots, $nb backup snapshots.\n";
4717
            } else {
4718
                $postreply .= "Status=ERROR Unable to sync snapshots.\n";
4719
                $postmsg = "ERROR Unable to sync snapshots";
4720
            }
4721
            my $i=0;
4722
        # Purge image snapshots if asked to
4723
            if ($imageretention && $matches>1) {
4724
                my $rtime;
4725
                if ($imageretention =~ /(\d+)(s|h|d)/) {
4726
                    $rtime = $1;
4727
                    $rtime = $1*60*60 if ($2 eq 'h');
4728
                    $rtime = $1*60*60*24 if ($2 eq 'd');
4729
                    $postreply .= "Status=OK Keeping image snapshots newer than $imageretention out of $ni.\n";
4730
                } elsif ($imageretention =~ /(\d+)$/) {
4731
                    $postreply .= "Status=OK Keeping " . (($imageretention>$ni)?$ni:$imageretention) . " image snapshots out of $ni.\n";
4732
                } else {
4733
                    $imageretention = 0;
4734
                }
4735
                if ($imageretention) {
4736
                    foreach my $isnap (@imagesnaps) {
4737
                        my $purge;
4738
                        if ($rtime) {
4739
                            my $snaptime = timelocal($6,$5,$4,$3,$2-1,$1) if ($isnap =~ /(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/);
4740
                            my $tdiff = ($current_time - $snaptime);
4741
                            if ( $matches>1 && $tdiff>$rtime )
4742
                                {$purge = 1;}
4743
                            else
4744
                                {last;}
4745
                        } else { # a simple number was specified
4746
#                            if ( $matches>1 && $matches+$matchbase>$imageretention )
4747
                            if ( $matches>1 && $ni>$imageretention )
4748
                                {$purge = 1;}
4749
                            else
4750
                                {last;}
4751
                        }
4752
                        if ($purge) {
4753
                            $cmd = qq|$zfscmd destroy $ipath\@SNAPSHOT-$isnap|;
4754
                            $res = `$cmd 2>&1`;
4755
                            $postreply .= "Status=OK Purging image snapshot $isnap from $ipath.\n";
4756
                            $main::syslogit->($user, 'info', "OK Purging image snapshot $isnap from $ipath");
4757
                            $matches-- if ($i>=$matchbase);
4758
                            $ni--;
4759
                        }
4760
                        $i++;
4761
                    }
4762
                }
4763
            }
4764
            # Purge backup snapshots if asked to
4765
            if ($backupretention && $matches) {
4766
                my $rtime;
4767
                if ($backupretention =~ /(\d+)(s|h|d)/) {
4768
                    $rtime = $1;
4769
                    $rtime = $1*60*60 if ($2 eq 'h');
4770
                    $rtime = $1*60*60*24 if ($2 eq 'd');
4771
                    $postreply .= "Status=OK Keeping backup snapshots newer than $backupretention out of $nb.\n";
4772
                } elsif ($backupretention =~ /(\d+)$/) {
4773
                    $postreply .= "Status=OK Keeping " . (($backupretention>$nb)?$nb:$backupretention) . " backup snapshots out of $nb.\n";
4774
                } else {
4775
                    $backupretention = 0;
4776
                }
4777
                if ($backupretention && $zbackupavailable) {
4778
                    foreach my $bsnap (@backupsnaps) {
4779
                        my $purge;
4780
                        if ($bsnap eq $imagesnaps[$matchbase+$matches-1]) { # We need to keep the last snapshot synced
4781
                            $postreply .= "Status=OK Not purging backup snapshot $matchbase $bsnap.\n";
4782
                            last;
4783
                        } else {
4784
                            if ($rtime) {
4785
                                my $snaptime = timelocal($6,$5,$4,$3,$2-1,$1) if ($bsnap =~ /(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/);
4786
                                my $tdiff = ($current_time - $snaptime);
4787
                                if ( $matches>1 && $tdiff>$rtime )
4788
                                    {$purge = 1;}
4789
                            } else {
4790
                                if ( $nb>$backupretention )
4791
                                    {$purge = 1;}
4792
                            }
4793
                            if ($purge) {
4794
                                $cmd = qq|zfs destroy $bpath\@SNAPSHOT-$bsnap|;
4795
                                $res = `$cmd 2>&1`;
4796
                                $postreply .= "Status=OK Purging backup snapshot $bsnap from $bpath.\n";
4797
                                $main::syslogit->($user, 'info', "OK Purging backup snapshot $bsnap from $bpath");
4798
                                $nb--;
4799
                            } else {
4800
                                last;
4801
                            }
4802
                        }
4803
                    }
4804
                }
4805
            }
4806
            $postmsg .= "OK Performing ZFS backup of $bpath. There are $ni image snapshots and $nb backup snapshots. ";
4807
        }
4808
        $postreply .= "Status=OK Updating all btimes\n";
4809
        Updateallbtimes();
4810
    } else {
4811
        $postreply .= "Status=ERROR Not allowed\n";
4812
        $postmsg = "ERROR Not allowed";
4813
    }
4814
    $main::updateUI->({tab=>"images", user=>$user, type=>"message", message=>$postmsg});
4815
    return $postreply;
4816
}
4817

    
4818
sub Backupfuel {
4819
    my ($image, $action, $obj) = @_;
4820
    if ($help) {
4821
        return <<END
4822
GET:username, dozfs:
4823
Backs up a user's fuel storage. If [dozfs] is set, fuel on ZFS volumes is backed up, even if it should be handled by regular ZFS backups.
4824
END
4825
    }
4826
    my $username = $obj->{'username'} || $user;
4827
    return "Status=Error Not allowed\n" unless ($isadmin || $username eq $user);
4828

    
4829
    my $remolder = "14D";
4830
    my $stordevs = Liststoragedevices('', 'getstoragedevices');
4831
    my $backupdev = Getbackupdevice('', 'getbackupdevice');
4832
    my $backupdevtype = $stordevs->{$backupdev}->{type};
4833
    foreach my $spool (@spools) {
4834
        my $ppath = $spool->{"path"};
4835
        my $pid = $spool->{"id"};
4836
        if (($spool->{"zfs"} && $backupdevtype eq 'zfs') && !$obj->{'dozfs'}) {
4837
            $postreply .= "Status=OK Skipping fuel on ZFS storage: $ppath/$username/fuel\n";
4838
        } elsif ($pid eq '-1') {
4839
            ;
4840
        } elsif (!$backupdir || !(-d $backupdir)) {
4841
            $postreply .= "Status=OK Backup dir $backupdir does not exist\n";
4842
        } elsif (-d "$ppath/$username/fuel" && !is_folder_empty("$ppath/$username/fuel")) {
4843
            my $srcdir = "$ppath/$username/fuel";
4844
            my $destdir = "$backupdir/$username/fuel/$pid";
4845

    
4846
            `mkdir -p "$destdir"` unless (-e "$destdir");
4847
            # Do the backup
4848
            my $cmd = qq|/usr/bin/rdiff-backup --print-statistics "$srcdir" "$destdir"|;
4849
            my $res = `$cmd`;
4850
            $cmd = qq|/usr/bin/rdiff-backup --print-statistics --force --remove-older-than $remolder "$destdir"|;
4851
            $res .= `$cmd`;
4852
            if ($res =~ /Errors 0/) {
4853
                my $change = $1 if ($res =~ /TotalDestinationSizeChange \d+ \((.+)\)/);
4854
                $postreply .= "Status=OK Backed up $change, $srcdir -> $destdir\n";
4855
                $main::syslogit->($user, "info", "OK backed up $change, $srcdir -> $destdir") if ($change);
4856
            } else {
4857
                $res =~ s/\n/ /g;
4858
                $postreply .= "Status=Error There was a problem backup up $srcdir -> $destdir: $res\n";
4859
                $main::syslogit->($user, "there was a problem backup up $srcdir -> $destdir");
4860
            }
4861
        } else {
4862
            $postreply .= "Status=OK Skipping empty fuel on: $ppath/$username/fuel\n";
4863
        }
4864
    }
4865
    return $postreply;
4866
}
4867

    
4868
sub is_folder_empty {
4869
    my $dirname = shift;
4870
    opendir(my $dh, $dirname) or die "Not a directory";
4871
    return scalar(grep { $_ ne "." && $_ ne ".." } readdir($dh)) == 0;
4872
}
4873

    
4874
sub Backup {
4875
    my ($image, $action, $obj) = @_;
4876
    if ($help) {
4877
        return <<END
4878
GET:image, skipzfs:
4879
Backs an image up. Set [skipzfs] if ZFS backup is configured, and you want to skip images on ZFS storage.
4880
END
4881
    }
4882
    my $path = $obj->{path} || $image;
4883
    my $status = $obj->{status};
4884
    my $skipzfs = $obj->{skipzfs};
4885
    $uistatus = "backingup";
4886
    $uipath = $path;
4887
    my $remolder;
4888
    $remolder = "14D" if ($obj->{bschedule} eq "daily14");;
4889
    $remolder = "7D" if ($obj->{bschedule} eq "daily7");
4890
    my $breply = '';
4891

    
4892
    my $stordevs = Liststoragedevices('', 'getstoragedevices');
4893
    my $backupdev = Getbackupdevice('', 'getbackupdevice');
4894
    my $backupdevtype = $stordevs->{$backupdev}->{type};
4895
    # Nodes are assumed to alwasy use ZFS
4896
    if ($backupdevtype eq 'zfs' && $skipzfs && ($obj->{regstoragepool} == -1 || $spools[$obj->{regstoragepool}]->{'zfs'})) {
4897
        return "Status=OK Skipping image on ZFS $path\n";
4898
    }
4899
    if ($status eq "snapshotting" || $status eq "unsnapping" || $status eq "reverting" || $status eq "cloning" ||
4900
        $status eq "moving" || $status eq "converting") {
4901
        $breply .= "Status=ERROR Problem backing up $obj->{type} image $obj->{name}\n";
4902
    } elsif ($obj->{regstoragepool} == -1) {
4903
        my $res = createNodeTask($obj->{mac}, "BACKUP $user $uistatus $status \"$path\" \"$backupdir\" $remolder", $status,  '', $path);
4904
        if ($res) {
4905
            $breply .= "Status=ERROR Suspend serverer befora backing up (image $obj->{name} is not on an LVM partition)\n";
4906
        } else {
4907
            $register{$path}->{'status'} = $uistatus;
4908
            $uistatus = "lbackingup" if ($status eq "active"); # Do lvm snapshot before backing up
4909
            $main::syslogit->($user, "info", "$uistatus $obj->{type} image: $obj->{name}: $uuid");
4910
            $breply .= "Status=backingup OK backingup image: $obj->{name} (on node)\n";
4911
        }
4912
    } elsif (!$spools[$obj->{regstoragepool}]->{'rdiffenabled'}) {
4913
        $breply .= "Status=ERROR Rdiff-backup has not been enabled for this storagepool ($spools[$obj->{regstoragepool}]->{'name'})\n";
4914
    } else {
4915
        if ($spools[$obj->{regstoragepool}]->{'hostpath'} eq "local" && $status eq "active") {
4916
            my $poolpath = $spools[$obj->{regstoragepool}]->{'path'};
4917
            # We only need to worry about taking an LVM snapshot if the image is in active use
4918
            # We also check if the images is actually on an LVM partition
4919
            my $qi = `/bin/cat /proc/mounts | grep "$poolpath"`; # Find the lvm volume mounted on /mnt/images
4920
            ($qi =~ m/\/dev\/mapper\/(\S+)-(\S+) $pool.+/g)[-1]; # Select last match
4921
            my $lvolgroup = $1;
4922
            my $lvol = $2;
4923
            if ($lvolgroup && $lvol) {
4924
                $uistatus = "lbackingup";
4925
            }
4926
        }
4927
        if ($uistatus ne "lbackingup" && $status eq "active") {
4928
            $breply .= "Status=ERROR Suspend serverer befora backing up (image $obj->{name} is not on an LVM partition)\n";
4929
        #    $main::updateUI->({tab=>"images", user=>$user, type=>"update", path=>$path, status=>$uistatus, message=>"Image $obj->{name} is not on an LVM partition - suspend before backing up"});
4930
        } else {
4931
            my $buser;
4932
            my $bname;
4933
            if ($path =~ /.*\/(common|$user)\/(.+)/) {
4934
                $buser = $1;
4935
                $bname = $2;
4936
            }
4937
            if ($buser && $bname) {
4938
                my $dirpath = $spools[$obj->{regstoragepool}]->{'path'};
4939
                #chop $dirpath; # Remove last /
4940
                eval {
4941
                    $register{$path}->{'status'} = $uistatus;
4942
                    my $daemon = Proc::Daemon->new(
4943
                        work_dir => '/usr/local/bin',
4944
                        exec_command => "perl -U steamExec $buser $uistatus $status \"$bname\" \"$dirpath\" \"$backupdir\" $remolder"
4945
                    ) or do {$breply .= "Status=ERROR $@\n";};
4946
                    my $pid = $daemon->Init();
4947
                    $breply .=  "Status=backingup OK backingup image: $obj->{name}\n";
4948
                    $main::syslogit->($user, "info", "$uistatus $obj->{type} image: $obj->{name}: $bname");
4949
                    1;
4950
                } or do {$breply .= "Status=ERROR $@\n";}
4951
            } else {
4952
                $breply .= "Status=ERROR Problem backing up $path\n";
4953
            }
4954
        }
4955
    }
4956
    return $breply;
4957
}
4958

    
4959
sub Restore {
4960
    my ($image, $action, $obj) = @_;
4961
    if ($help) {
4962
        return <<END
4963
GET:image:
4964
Backs an image up.
4965
END
4966
    }
4967
    my $path = $obj->{path};
4968
    my $status = $obj->{status};
4969
    $uistatus = "restoring";
4970
    my($bname, $dirpath, $suffix) = fileparse($path, (".vmdk", ".img", ".vhd", ".vhdx", ".qcow", ".qcow2", ".vdi", ".iso"));
4971
    my $backup = $params{"backup"} || $obj->{backup};
4972
    my $pool = $register{$path}->{'storagepool'};
4973
    $pool = "0" if ($pool == -1);
4974
    my $poolpath = $spools[$pool]->{'path'};
4975
    my $restorefromdir = $backupdir;
4976
    my $inc = $backup;
4977
    my $subdir; # 1 level of subdirs supported
4978
    $subdir = $1 if ($dirpath =~ /.+\/$obj->{user}(\/.+)?\//);
4979

    
4980
    if ($backup =~ /^SNAPSHOT-(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/) { # We are dealing with a zfs restore
4981
        $inc = "$1-$2-$3-$4-$5-$6";
4982
        foreach my $spool (@spools) {
4983
            my $ppath = $spool->{"path"};
4984
            if (-e "$ppath/.zfs/snapshot/$backup/$obj->{user}$subdir/$bname$suffix") {
4985
                $restorefromdir = "$ppath/.zfs/snapshot/$backup";
4986
                last;
4987
            }
4988
        }
4989
    } else {
4990
        if ($backup eq "mirror") {
4991
            my $mir = `/bin/ls "$backupdir/$obj->{user}/$bname$suffix/rdiff-backup-data" | grep current_mirror`;
4992
            if ($mir =~ /current_mirror\.(\S+)\.data/) {
4993
                $inc = $1;
4994
            }
4995
        }
4996
        $inc =~ tr/:T/-/; # qemu-img does not like colons in file names - go figure...
4997
        $inc = substr($inc,0,-6);
4998
    }
4999
    $uipath = "$poolpath/$obj->{user}$subdir/$bname.$inc$suffix";
5000
    my $i;
5001
    if (-e $uipath) {
5002
        $i = 1;
5003
        while (-e "$poolpath/$obj->{user}$subdir/$bname.$inc.$i$suffix") {$i++;}
5004
        $uipath = "$poolpath/$obj->{user}$subdir/$bname.$inc.$i$suffix";
5005
    }
5006

    
5007
    if (-e $uipath) {
5008
        $postreply .= "Status=ERROR This image is already being restored\n";
5009
    } elsif ($obj->{user} ne $user && !$isadmin) {
5010
        $postreply .= "Status=ERROR No restore privs\n";
5011
    } elsif (!$backup || $backup eq "--") {
5012
        $postreply .= "Status=ERROR No backup selected\n";
5013
    } elsif (overQuotas($obj->{virtualsize})) {
5014
        $postreply .= "Status=ERROR Over quota (". overQuotas($obj->{virtualsize}) . ") restoring: $obj->{name}\n";
5015
    } elsif (overStorage($obj->{ksize}*1024, $pool+0)) {
5016
        $postreply .= "Status=ERROR Out of storage in destination pool restoring: $obj->{name}\n";
5017
    } else {
5018
        my $ug = new Data::UUID;
5019
        my $newuuid = $ug->create_str();
5020
        $register{$uipath} = {
5021
            uuid=>$newuuid,
5022
            status=>"restoring",
5023
            name=>"$obj->{name} ($inc)" . (($i)?" $i":''),
5024
            notes=>$obj->{notes},
5025
            image2=>$obj->{image2},
5026
            managementlink=>$obj->{managementlink},
5027
            upgradelink=>$obj->{upgradelink},
5028
            terminallink=>$obj->{terminallink},
5029
            size=>0,
5030
            realsize=>0,
5031
            virtualsize=>$obj->{virtualsize},
5032
            type=>$obj->{type},
5033
            user=>$user
5034
        };
5035
        eval {
5036
            $register{$path}->{'status'} = $uistatus;
5037
            my $daemon = Proc::Daemon->new(
5038
                work_dir => '/usr/local/bin',
5039
                exec_command => "perl -U steamExec $obj->{user} $uistatus $status \"$path\" \"$restorefromdir\" \"$backup\" \"$uipath\""
5040
            ) or do {$postreply .= "Status=ERROR $@\n";};
5041
            my $pid = $daemon->Init();
5042
            $postreply .=  "Status=$uistatus OK $uistatus $obj->{type} image: $obj->{name} ($inc)". ($console?", $newuuid\n":"\n");
5043
            $main::syslogit->($user, "info", "$uistatus $obj->{type} image: $obj->{name} ($inc), $uipath, $newuuid: $uuid");
5044
            1;
5045
        } or do {$postreply .= "Status=ERROR $@\n";};
5046
        $main::updateUI->({tab=>"images", user=>$user, type=>"update"});
5047
    }
5048
    return $postreply;
5049
}
5050

    
5051
sub Master {
5052
    my ($image, $action, $obj) = @_;
5053
    if ($help) {
5054
        return <<END
5055
GET:image:
5056
Converts an image to a master image. Image must not be in use.
5057
END
5058
    }
5059
    my $path = $obj->{path};
5060
    my $status = $register{$path}->{status};
5061
    $path =~ /(.+)\.$obj->{type}$/;
5062
    my $namepath = $1;
5063
    my $uiname;
5064
    if (!$register{$path}) {
5065
        $postreply .= "Status=ERROR Image $path not found\n";
5066
    } elsif ($status ne "unused") {
5067
        $postreply .= "Status=ERROR Only unused images may be mastered\n";
5068
#    } elsif ($namepath =~ /(.+)\.master/ || $register{$path}->{'master'}) {
5069
#        $postreply .= "Status=ERROR Only one level of mastering is supported\n";
5070
    } elsif ($obj->{istoragepool} == -1 || $obj->{regstoragepool} == -1) {
5071
        $postreply .= "Status=ERROR Unable to master $obj->{name} (master images are not supported on node storage)\n";
5072
    } elsif ($obj->{type} eq "qcow2") {
5073
        # Promoting a regular image to master
5074
        # First find an unused path
5075
        if (-e "$namepath.master.$obj->{type}") {
5076
            my $i = 1;
5077
            while ($register{"$namepath.$i.master.$obj->{type}"} || -e "$namepath.$i.master.$obj->{type}") {$i++;};
5078
            $uinewpath = "$namepath.$i.master.$obj->{type}";
5079
        } else {
5080
            $uinewpath = "$namepath.master.$obj->{type}";
5081
        }
5082

    
5083
        $uipath = $path;
5084
        $uiname = "$obj->{name}";
5085
        eval {
5086
            my $qinfo = `/bin/mv -iv "$path" "$uinewpath"`;
5087
            $register{$path}->{'name'} = $uiname;
5088
            $register{$uinewpath} = $register{$path};
5089
            delete $register{$path};
5090
            $postreply .= "Status=$status Mastered $obj->{type} image: $obj->{name}\n";
5091
            chop $qinfo;
5092
            $main::syslogit->($user, "info", $qinfo);
5093
            1;
5094
        } or do {$postreply .= "Status=ERROR $@\n";};
5095
        sleep 1;
5096
        $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$obj->{'uuid'}, newpath=>$uinewpath, status=>$status, name=>$uiname});
5097
    } else {
5098
        $postreply .= "Status=ERROR Only qcow2 images may be mastered\n";
5099
    }
5100
    return $postreply;
5101
}
5102

    
5103
sub Unmaster {
5104
    my ($image, $action, $obj) = @_;
5105
    if ($help) {
5106
        return <<END
5107
GET:image:
5108
Converts a master image to a regular image. Image must not be in use.
5109
END
5110
    }
5111
    my $path = $obj->{path};
5112
    my $status = $register{$path}->{status};
5113
    $path =~ /(.+)\.$obj->{type}$/;
5114
    my $namepath = $1;
5115
    my $haschildren = 0;
5116
    my $child;
5117
    my $uinewpath;
5118
    my $iname;
5119
    my @regvalues = values %register;
5120
    foreach my $val (@regvalues) {
5121
        if ($val->{'master'} eq $path) {
5122
            $haschildren = 1;
5123
            $child = $val->{'name'};
5124
            last;
5125
        }
5126
    }
5127
    if (!$register{$path}) {
5128
        $postreply .= "Status=ERROR Image $path not found\n";
5129
    } elsif ($haschildren) {
5130
        $postreply .= "Status=Error Cannot unmaster image. This image is used as master by: $child\n";
5131
    } elsif ($status ne "unused" && $status ne "used") {
5132
        $postreply .= "Status=ERROR Only used and unused images may be unmastered\n";
5133
    } elsif (!( ($namepath =~ /(.+)\.master/) || ($obj->{master} && $obj->{master} ne "--")) ) {
5134
        $postreply .= "Status=ERROR You can only unmaster master or child images\n";
5135
    } elsif (($obj->{istoragepool} == -1 || $obj->{regstoragepool} == -1) && $namepath =~ /(.+)\.master/) {
5136
        $postreply .= "Status=ERROR Unable to unmaster $obj->{name} (master images are not supported on node storage)\n";
5137
    } elsif ($obj->{type} eq "qcow2") {
5138
        # Demoting a master to regular image
5139
        if ($action eq 'unmaster' && $namepath =~ /(.+)\.master$/) {
5140
            $namepath = $1;
5141
            $uipath = $path;
5142
            # First find an unused path
5143
            if (-e "$namepath.$obj->{type}") {
5144
                my $i = 1;
5145
                while ($register{"$namepath.$i.$obj->{type}"} || -e "$namepath.$i.$obj->{type}") {$i++;};
5146
                $uinewpath = "$namepath.$i.$obj->{type}";
5147
            } else {
5148
                $uinewpath = "$namepath.$obj->{type}";
5149
            }
5150

    
5151
            $iname = $obj->{name};
5152
            $iname =~ /(.+)( \(master\))/;
5153
            $iname = $1 if $2;
5154
            eval {
5155
                my $qinfo = `/bin/mv -iv "$path" "$uinewpath"`;
5156
                $register{$path}->{'name'} = $iname;
5157
                $register{$uinewpath} = $register{$path};
5158
                delete $register{$path};
5159
                $postreply .=  "Status=$status Unmastered $obj->{type} image: $obj->{name}\n";
5160
                chomp $qinfo;
5161
                $main::syslogit->($user, "info", $qinfo);
5162
                1;
5163
            } or do {$postreply .= "Status=ERROR $@\n";}
5164
    # Rebasing a child image
5165
        } elsif ($action eq 'rebase' && $obj->{master} && $obj->{master} ne "--") {
5166
            $uistatus = "rebasing";
5167
            $uipath = $path;
5168
            $iname = $obj->{name};
5169
            $iname =~ /(.+)( \(child\d*\))/;
5170
            $iname = $1 if $2;
5171
            my $temppath = "$path.temp";
5172
            $uipath = $path;
5173
            $uimaster = "--";
5174
            my $macip;
5175

    
5176
            if ($obj->{mac} && $path =~ /\/mnt\/stabile\/node\//) {
5177
                unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
5178
                $macip = $nodereg{$obj->{mac}}->{'ip'};
5179
                untie %nodereg;
5180
            }
5181
            eval {
5182
                my $master = $register{$path}->{'master'};
5183
                my $usedmaster = '';
5184
#                @regvalues = values %register;
5185
                if ($master && $master ne '--') {
5186
                    foreach my $valref (@regvalues) {
5187
                        $usedmaster = 1 if ($valref->{'master'} eq $master && $valref->{'path'} ne $path); # Check if another image is also using this master
5188
                    }
5189
                }
5190
                $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$obj->{'uuid'}, status=>$uistatus});
5191
                $register{$path} = {
5192
                    master=>"",
5193
                    name=>"$iname",
5194
                    notes=>$obj->{notes},
5195
                    status=>$uistatus,
5196
                    snap1=>$obj->{snap1},
5197
                    managementlink=>$obj->{managementlink},
5198
                    upgradelink=>$obj->{upgradelink},
5199
                    terminallink=>$obj->{terminallink},
5200
                    image2=>$obj->{image2},
5201
                    storagepool=>$obj->{istoragepool},
5202
                    status=>$uistatus
5203
                };
5204

    
5205
                if ($macip) {
5206
                    my $esc_localpath = shell_esc_chars($path);
5207
                    my $esc_localpath2 = shell_esc_chars($temppath);
5208
                    $res .= `$sshcmd $macip "/usr/bin/qemu-img convert $esc_localpath -O qcow2 $esc_localpath2"`;
5209
                    $res .= `$sshcmd $macip "if [ -f $esc_localpath2 ]; then /bin/mv -v $esc_localpath2 $esc_localpath; fi"`;
5210
                } else {
5211
                    $res .= `/usr/bin/qemu-img convert -O qcow2 "$path" "$temppath"`;
5212
                    $res .= `if [ -f "$temppath" ]; then /bin/mv -v "$temppath" "$path"; fi`;
5213
                }
5214
                if ($master && !$usedmaster) {
5215
                    $register{$master}->{'status'} = 'unused';
5216
                    $main::syslogit->('info', "Freeing master $master");
5217
                }
5218
                $register{$path}->{'master'} = '';
5219
                $register{$path}->{'status'} = $status;
5220

    
5221
                $postreply .= "Status=OK $uistatus $obj->{type} image: $obj->{name}\n";
5222
                $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$obj->{'uuid'}, status=>$status});
5223
                $main::syslogit->($user, "info", "$uistatus $obj->{type} image: $obj->{name}: $uuid");
5224
                1;
5225
            } or do {$postreply .= "Status=ERROR $@\n";}
5226
        } else {
5227
            $postreply .= "Status=ERROR Not a master, not a child \"$obj->{name}\"\n";
5228
        }
5229
        sleep 1;
5230
        $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$obj->{'uuid'}, newpath=>$uinewpath, name=>$iname, status=>$status});
5231
    } else {
5232
        $postreply .= "Status=ERROR Only qcow2 images may be unmastered\n";
5233
    }
5234
    return $postreply;
5235
}
5236

    
5237
# Save or create new image
5238
sub Save {
5239
    my ($img, $action, $obj) = @_;
5240
    if ($help) {
5241
        return <<END
5242
POST:path, uuid, name, type, virtualsize, storagepool, user:
5243
To save a collection of images you either PUT or POST a JSON array to the main endpoint with objects representing the images with the changes you want.
5244
Depending on your privileges not all changes are permitted. If you save without specifying a uuid or path, a new image is created.
5245
END
5246
    }
5247
    my $path = $obj->{path};
5248
    my $uuid = $obj->{uuid};
5249
    my $status = $obj->{status};
5250
    if ($status eq "new") {
5251
        # Create new image
5252
        my $ug = new Data::UUID;
5253
        if (!$uuid || $uuid eq '--') {
5254
            $uuid = $ug->create_str();
5255
        } else { # Validate
5256
            my $valuuid  = $ug->from_string($uuid);
5257
            if ($ug->to_string($valuuid) eq $uuid) {
5258
                ;
5259
            } else {
5260
                $uuid = $ug->create_str();
5261
            }
5262
        }
5263
        my $newuuid = $uuid;
5264
        my $pooldir = $spools[$obj->{storagepool}]->{'path'};
5265
        my $cmd;
5266
        my $name = $obj->{name};
5267
        $name =~ s/\./_/g; # Remove unwanted chars
5268
        $name =~ s/\//_/g;
5269
        eval {
5270
            my $ipath = "$pooldir/$user/$name.$obj->{type}";
5271
            $obj->{type} = "qcow2" unless ($obj->{type});
5272
            # Find an unused path
5273
            if ($register{$ipath} || -e "$ipath") {
5274
                my $i = 1;
5275
                while ($register{"$pooldir/$user/$name.$i.$obj->{type}"} || -e "$pooldir/$user/$name.$i.$obj->{type}") {$i++;};
5276
                $ipath = "$pooldir/$user/$name.$i.$obj->{type}";
5277
                $name = "$name.$i";
5278
            }
5279

    
5280
            if ($obj->{type} eq 'qcow2' || $obj->{type} eq 'vmdk') {
5281
                my $size = ($obj->{msize})."M";
5282
                my $format = "qcow2";
5283
                $format = "vmdk" if ($path1 =~ /\.vmdk$/);
5284
                $cmd = qq|/usr/bin/qemu-img create -f $format "$ipath" "$size"|;
5285
            } elsif ($obj->{type} eq 'img') {
5286
                my $size = ($obj->{msize})."M";
5287
                $cmd = qq|/usr/bin/qemu-img create -f raw "$ipath" "$size"|;
5288
            } elsif ($obj->{type} eq 'vdi') {
5289
                my $size = $obj->{msize};
5290
                $cmd = qq|/usr/bin/VBoxManage createhd --filename "$ipath" --size "$size" --format VDI|;
5291
            }
5292
            $obj->{name} = 'New Image' if (!$obj->{name} || $obj->{name} eq '--' || $obj->{name} =~ /^\./ || $obj->{name} =~ /\//);
5293
            if (-e $ipath) {
5294
                $postreply .= "Status=ERROR Image already exists: \"$obj->{name}\" in \"$ipath\”\n";
5295
            } elsif (overQuotas($obj->{ksize}*1024)) {
5296
                $postreply .= "Status=ERROR Over quota (". overQuotas($obj->{ksize}*1024) . ") creating: $obj->{name}\n";
5297
                $main::updateUI->({tab=>"images", user=>$user, type=>"update", message=>"Over quota in storage pool $obj->{storagepool}"});
5298
                $main::syslogit->($user, "info", "Over quota in storage pool $obj->{storagepool}, not creating $obj->{type} image $obj->{name}");
5299
            } elsif (overStorage($obj->{ksize}*1024, $obj->{storagepool}+0)) {
5300
                $postreply .= "Status=ERROR Out of storage in destination pool creating: $obj->{name}\n";
5301
                $main::updateUI->({tab=>"images", user=>$user, type=>"update", message=>"Out of storage in storage pool $obj->{storagepool}"});
5302
                $main::syslogit->($user, "info", "Out of storage in storage pool $obj->{storagepool}, not creating $obj->{type} image $obj->{name}");
5303
            } elsif ($obj->{virtualsize} > 10*1024*1024 && $obj->{name} && $obj->{name} ne '--') {
5304
                $register{$ipath} = {
5305
                    uuid=>$newuuid,
5306
                    name=>$obj->{name},
5307
                    user=>$user,
5308
                    notes=>$obj->{notes},
5309
                    type=>$obj->{type},
5310
                    size=>0,
5311
                    realsize=>0,
5312
                    virtualsize=>$obj->{virtualsize},
5313
                    storagepool=>$spools[0]->{'id'},
5314
                    created=>$current_time,
5315
                    managementlink=>$obj->{managementlink},
5316
                    upgradelink=>$obj->{upgradelink},
5317
                    terminallink=>$obj->{terminallink},
5318
                    status=>"creating"
5319
                };
5320
                $uipath = $ipath;
5321
                my $res = `$cmd`;
5322
                $register{$ipath}->{'status'} = 'unused';
5323

    
5324
                $postreply .= "Status=OK Created $obj->{type} image: $obj->{name}\n";
5325
                $postreply .= "Status=OK uuid: $newuuid\n"; # if ($console || $api);
5326
                $postreply .= "Status=OK path: $ipath\n"; # if ($console || $api);
5327
                sleep 1; # Needed to give updateUI a chance to reload
5328
                $main::updateUI->({tab=>"images", user=>$user, type=>"update"});
5329
#                $main::updateUI->({tab=>"images", uuid=>$newuuid, user=>$user, type=>"update", name=>$obj->{name}, path=>$obj->{path}});
5330
                $main::syslogit->($user, "info", "Created $obj->{type} image: $obj->{name}: $newuuid");
5331
                updateBilling("New image: $obj->{name}");
5332
            } else {
5333
                $postreply .= "Status=ERROR Problem creating image: $obj->{name} of size $obj->{virtualsize}\n";
5334
            }
5335
            1;
5336
        } or do {$postreply .= "Status=ERROR $@\n";}
5337
    } else {
5338
        # Moving images because of owner change or storagepool change
5339
        if ($obj->{user} ne $obj->{reguser} || $obj->{storagepool} ne $obj->{regstoragepool}) {
5340
            $uipath = Move($path, $obj->{user}, $obj->{storagepool}, $obj->{mac});
5341
    # Resize a qcow2 image
5342
        } elsif ($obj->{virtualsize} != $register{$path}->{'virtualsize'} &&
5343
            ($obj->{user} eq $obj->{reguser} || index($privileges,"a")!=-1)) {
5344
            if ($status eq "active" || $status eq "paused") {
5345
                $postreply .= "Status=ERROR Cannot resize active images $path, $status.\n";
5346
                $main::updateUI->({tab=>"images", user=>$user, type=>"update", status=>'ERROR', message=>"ERROR Cannot resize active images"});
5347
            } elsif ($obj->{type} eq "qcow2" || $obj->{type} eq "img") {
5348
                if ($obj->{virtualsize} < $register{$path}->{'virtualsize'}) {
5349
                    $postreply .= "Status=ERROR Only growing of images supported.\n";
5350
                } elsif (overQuotas($obj->{virtualsize}, ($register{$path}->{'storagepool'}==-1))) {
5351
                    $postreply .= "Status=ERROR Over quota (". overQuotas($obj->{virtualsize}, ($register{$path}->{'storagepool'}==-1)) . ") resizing: $obj->{name}\n";
5352
                } elsif (overStorage(
5353
                    $obj->{virtualsize},
5354
                    $register{$path}->{'storagepool'},
5355
                    $register{$path}->{'mac'}
5356
                )) {
5357
                    $postreply .= "Status=ERROR Not enough storage ($obj->{virtualsize}) in destination pool $obj->{storagepool} resizing: $obj->{name}\n";
5358
                } else {
5359
                    $uistatus = "resizing";
5360
                    $uipath = $path;
5361
                    my $mpath = $path;
5362
                    if ($obj->{mac} && $obj->{mac} ne '--') {
5363
                        unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
5364
                        $macip = $nodereg{$obj->{mac}}->{'ip'};
5365
                        untie %nodereg;
5366
                    }
5367
                    $mpath = "$macip:$mpath" if ($macip && $macip ne '--');
5368
                    $register{$path}->{'status'} = $uistatus;
5369
                    $register{$path}->{'virtualsize'} = $obj->{virtualsize};
5370
                    my $cmd = qq|steamExec $user $uistatus $status "$mpath" "$obj->{ksize}"|;
5371
                    if ($action eq 'sync_save') { # We wait for result
5372
                        my $res = `$cmd`;
5373
                        $res =~ s/\n/ /g; $res = lc $res;
5374
                        $postreply .= "Status=OK $res\n";
5375
                    } else {
5376
                        my $daemon = Proc::Daemon->new(
5377
                            work_dir => '/usr/local/bin',
5378
                            exec_command => $cmd,
5379
#                            exec_command => "suidperl -U steamExec $user $uistatus $status \"$mpath\" \"$obj->{ksize}\""
5380
                        ) or do {$postreply .= "Status=ERROR $@\n";};
5381
                        my $pid = $daemon->Init();
5382
                    }
5383
                    $postreply .=  "Status=OK $uistatus $obj->{type} image: $obj->{name} ($obj->{ksize}k)\n";
5384
                    $main::syslogit->($user, "info", "$uistatus $obj->{type} image $obj->{name} $uuid $mpath ($obj->{virtualsize})");
5385
                }
5386
            } else {
5387
                $postreply .= "Status=ERROR Can only resize .qcow2 and .img images.\n";
5388
            }
5389
        } else {
5390
            # Regular save
5391
            if ($obj->{user} eq $obj->{reguser} || $isadmin) {
5392
                my $qinfo;
5393
                my $e;
5394
                $obj->{bschedule} = "" if ($obj->{bschedule} eq "--");
5395
                if ($obj->{bschedule}) {
5396
                    # Remove backups
5397
                    if ($obj->{bschedule} eq "none") {
5398
                        if ($spools[$obj->{regstoragepool}]->{'rdiffenabled'}) {
5399
                            my($bname, $dirpath) = fileparse($path);
5400
                            if ($path =~ /\/($user|common)\/(.+)/) {
5401
                                my $buser = $1;
5402
                                if (-d "$backupdir/$buser/$bname" && $backupdir && $bname && $buser) {
5403
                                    eval {
5404
                                        $qinfo = `/bin/rm -rf "$backupdir/$buser/$bname"`;
5405
                                        1;
5406
                                    } or do {$postreply .= "Status=ERROR $@\n"; $e=1;};
5407
                                    if (!$e) {
5408
                                        $postreply .=  "Status=OK Removed all rdiff backups of $obj->{name}\n";
5409
                                        chomp $qinfo;
5410
                                        $register{$path} = {backupsize=>0};
5411
                                        $main::syslogit->($user, "info", "Removed all backups of $obj->{name}: $path: $qinfo");
5412
                                        $main::updateUI->({
5413
                                            user=>$user,
5414
                                            message=>"Removed all backups of $obj->{name}",
5415
                                            backup=>$path
5416
                                        });
5417
                                        updateBilling("no backup $path");
5418
                                        delete $register{$path}->{'btime'};
5419
                                    }
5420
                                }
5421
                            }
5422
                        }
5423
                        $obj->{bschedule} = "manually";
5424
                        $register{$path}->{'bschedule'} = $obj->{bschedule};
5425
                    }
5426
                }
5427

    
5428
                $register{$path} = {
5429
                    name=>$obj->{name},
5430
                    user=>$obj->{user},
5431
                    notes=>$obj->{notes},
5432
                    bschedule=>$obj->{bschedule},
5433
                    installable=>$obj->{installable},
5434
                    managementlink=>$obj->{managementlink},
5435
                    upgradelink=>$obj->{upgradelink},
5436
                    terminallink=>$obj->{terminallink},
5437
                    action=>""
5438
                };
5439
                my $domains = $register{$path}->{'domains'};
5440
                if ($status eq 'downloading') {
5441
                    unless (`pgrep $obj->{name}`) { # Check if image is in fact being downloaded
5442
                        $status = 'unused';
5443
                        $register{$path}->{'status'} = $status;
5444
                        unlink ("$path.meta") if (-e "$path.meta");
5445
                    }
5446
                }
5447
                elsif ($status ne 'unused') {
5448
                    my $match;
5449
                    if ($path =~ /\.master\.qcow2$/) {
5450
                        my @regkeys = (tied %register)->select_where("master = '$path'");
5451
                        $match = 2 if (@regkeys);
5452
                    } else {
5453
                        if (!$domreg{$domains}) { # Referenced domain no longer exists
5454
                            ;
5455
                        } else { # Verify if referenced domain still uses image
5456
                            my @imgkeys = ('image', 'image2', 'image3', 'image4');
5457
                            for (my $i=0; $i<4; $i++) {
5458
                                $match = 1 if ($domreg{$domains}->{$imgkeys[$i]} eq $path);
5459
                            }
5460
                        }
5461
                    }
5462
                    unless ($match) {
5463
                        $status = 'unused';
5464
                        $register{$path}->{'status'} = $status;
5465
                    }
5466
                }
5467
                if ($status eq 'unused') {
5468
                    delete $register{$path}->{'domains'};
5469
                    delete $register{$path}->{'domainnames'};
5470
                }
5471
                $uipath = $path;
5472
                $postreply .= "Status=OK Saved $obj->{name} ($uuid)\n";
5473
                $main::updateUI->({tab=>"images", user=>$user, type=>"update", path=>$path, name=>  $obj->{name}, status=>$status});
5474
            } else {
5475
                $postreply .= "Status=ERROR Unable to save $obj->{name}\n";
5476
            }
5477
        }
5478
    }
5479
    if ($postreply) {
5480
        $postmsg = $postreply;
5481
    } else {
5482
        $postreply = to_json(\%{$register{$uipath}}, {pretty=>1}) if ($uipath && $register{$uipath});
5483
        $postreply =~ s/""/"--"/g;
5484
        $postreply =~ s/null/"--"/g;
5485
        $postreply =~ s/"notes" {0,1}: {0,1}"--"/"notes":""/g;
5486
        $postreply =~ s/"installable" {0,1}: {0,1}"(true|false)"/"installable":$1/g;
5487
    }
5488
    return $postreply || "Status=OK Saved $uipath\n";
5489
}
5490

    
5491
sub Setstoragedevice {
5492
    my ($image, $action, $obj) = @_;
5493
    if ($help) {
5494
        return <<END
5495
GET:device,type:
5496
Changes the device - disk or partition, used for images or backup storage.
5497
[type] is either images or backup.
5498
END
5499
    }
5500
    my $dev = $obj->{device};
5501
    my $force = $obj->{force};
5502
    my $type = 'backup';
5503
    $type = 'images' if ($obj->{type} eq 'images');
5504
    return "Status=Error Not allowed\n" unless ($isadmin);
5505
    my $backupdevice = Getbackupdevice('', 'getbackupdevice');
5506
    my $imagesdevice = Getimagesdevice('', 'getimagesdevice');
5507
    my $devices_obj = from_json(Liststoragedevices('', 'liststoragedevices'));
5508
    my %devices = %$devices_obj;
5509
    my $backupdev = $devices{$backupdevice}->{dev};
5510
    my $imagesdev = $devices{$imagesdevice}->{dev};
5511
    if (!$devices{$dev}) {
5512
        $postreply = "Status=Error You must specify a valid device ($dev)\n";
5513
        return $postreply;
5514
    }
5515
    if (!$force && (($backupdev =~ /$dev/) || ($imagesdev =~ /$dev/))  && $dev !~ /vda/ && $dev !~ /sda/) { # make exception to allow returning to default setup
5516
        $postreply = "Status=Error $dev is already in use as images or backup device\n";
5517
        return $postreply;
5518
    }
5519
    my $stordir = $tenderpathslist[0];
5520
    my $stordevice = $imagesdevice;
5521
    if ($type eq 'backup') {
5522
        $stordir = $backupdir;
5523
        $stordevice = $backupdevice;
5524
    }
5525
    return "Status=Error Storage device not found\n" unless ($stordevice);
5526
    my $mp = $devices{$dev}->{mounted};
5527
    my $newstordir;
5528
    # my $oldstordir;
5529
    if ($devices{$dev}->{type} eq 'zfs') {
5530
        my $cmd = qq|zfs list stabile-$type/$type -Ho mountpoint|;
5531
        my $zmp = `$cmd`;
5532
        chomp $zmp;
5533
        if ($zmp =~ /^\//) {
5534
            `zfs mount stabile-$type/$type`;
5535
            $mp = $zmp;
5536
            $newstordir = $mp;
5537
        } else {
5538
            `zfs create stabile-$type/$type`;
5539
            $mp = "/stabile-$type/$type";
5540
            $newstordir = $mp;
5541
        }
5542
    } else {
5543
        $newstordir = (($type eq 'images')?"$mp/images":"$mp/backups");
5544
        $newstordir = $1 if ($newstordir =~ /(.+\/images)\/images$/);
5545
        $newstordir = $1 if ($newstordir =~ /(.+\/backups)\/backups$/);
5546
    }
5547
    if ($mp eq '/') {
5548
        $newstordir = (($type eq 'images')?"/mnt/stabile/images":"/mnt/stabile/backups");
5549
        `umount "$newstordir"`; # in case it's mounted
5550
    }
5551
    `mkdir "$newstordir"` unless (-e $newstordir);
5552
    `chmod 777 "$newstordir"`;
5553

    
5554
    my $cfg = new Config::Simple("/etc/stabile/config.cfg");
5555
    if ($type eq 'backup') {
5556
        $cfg->param('STORAGE_BACKUPDIR', $newstordir);
5557
        $cfg->save();
5558
    } elsif ($type eq 'images') {
5559

    
5560
    # Handle shared storage config
5561
    #    $oldstordir = $stordir;
5562
        my $i = 0;
5563
        for($i = 0; $i <= $#tenderpathslist; $i++) {
5564
            my $dir = $tenderpathslist[$i];
5565
            last if ($dir eq $newstordir);
5566
        }
5567
        # $tenderpathslist[0] = $newstordir;
5568
        splice(@tenderpathslist, $i,1); # Remove existing entry
5569
        unshift(@tenderpathslist, $newstordir); # Then add the new path
5570
        $cfg->param('STORAGE_POOLS_LOCAL_PATHS', join(',', @tenderpathslist));
5571

    
5572
        # $tenderlist[0] = 'local';
5573
        splice(@tenderlist, $i,1);
5574
        unshift(@tenderlist, 'local');
5575
        $cfg->param('STORAGE_POOLS_ADDRESS_PATHS', join(',', @tenderlist));
5576

    
5577
        # $tendernameslist[0] = 'Default';
5578
        splice(@tendernameslist, $i,1);
5579
        unshift(@tendernameslist, 'Default');
5580

    
5581
        if ($i) { # We've actually changed storage device
5582
            my $oldstorname = $tenderpathslist[1];
5583
            $oldstorname = $1 if ($oldstorname =~ /.*\/(.+)/);
5584
            $tendernameslist[1] = "$oldstorname on $imagesdevice"; # Give the previous default pool a fitting name
5585

    
5586
            $storagepools = "$storagepools,$i" unless ($storagepools =~ /,\s*$i,?/ || $storagepools =~ /,\s*$i$/ || $storagepools =~ /^$i$/);
5587
            $cfg->param('STORAGE_POOLS_DEFAULTS', $storagepools);
5588
        }
5589
        $cfg->param('STORAGE_POOLS_NAMES', join(',', @tendernameslist));
5590

    
5591
        $cfg->save();
5592

    
5593

    
5594
    # Handle node storage configs
5595
        unless ( tie(%idreg,'Tie::DBI', Hash::Merge::merge({table=>'nodeidentities',key=>'identity',CLOBBER=>3}, $Stabile::dbopts)) ) {return "Unable to access id register"};
5596
        # Build hash of known node config files
5597
        my @nodeconfigs;
5598
        push @nodeconfigs, "/etc/stabile/nodeconfig.cfg";
5599
        foreach my $valref (values %idreg) {
5600
            my $nodeconfigfile = $valref->{'path'} . "/casper/filesystem.dir/etc/stabile/nodeconfig.cfg";
5601
            next if ($nodeconfigs{$nodeconfigfile}); # Node identities may share basedir and node config file
5602
            if (-e $nodeconfigfile) {
5603
                push @nodeconfigs, $nodeconfigfile;
5604
            }
5605
        }
5606
        untie %idreg;
5607
        foreach my $nodeconfig (@nodeconfigs) {
5608
            my $nodecfg = new Config::Simple($nodeconfig);
5609
            my @ltenderlist = $nodecfg->param('STORAGE_SERVERS_ADDRESS_PATHS');
5610
            my $ltenders = join(", ", @ltenderlist);
5611
            next if ($ltenders =~ /10\.0\.0\.1:$newstordir$/ || $ltenders =~ /10\.0\.0\.1:$newstordir,/); # This entry already exists
5612
            #my @ltenderlist = split(/,\s*/, $ltenders);
5613
            #$ltenderlist[0] = "10.0.0.1:$newstordir";
5614
            unshift(@ltenderlist, "10.0.0.1:$newstordir");
5615
            $nodecfg->param('STORAGE_SERVERS_ADDRESS_PATHS', join(',', @ltenderlist));
5616
            my @ltenderpathslist = $nodecfg->param('STORAGE_SERVERS_LOCAL_PATHS');
5617
            my $ltenderpaths = join(", ", @ltenderpathslist);
5618
            #my @ltenderpathslist = split(/,\s*/, $ltenderpaths);
5619
            #$ltenderpathslist[0] = $newstordir;
5620
            unshift(@ltenderpathslist, $newstordir);
5621
            $nodecfg->param('STORAGE_SERVERS_LOCAL_PATHS', join(',', @ltenderpathslist));
5622
            $nodecfg->save();
5623
        }
5624
        unless (`grep "$newstordir 10" /etc/exports`) {
5625
            `echo "$newstordir 10.0.0.0/255.255.255.0(sync,no_subtree_check,no_root_squash,rw)" >> /etc/exports`;
5626
            `/usr/sbin/exportfs -r`; #Reexport nfs shares
5627
        }
5628
# We no longer undefine storage pools - we add them
5629
#        $oldstordir =~ s/\//\\\//g;
5630
#        `perl -pi -e 's/$oldstordir 10.*\\\n//s;' /etc/exports` if ($oldstordir);
5631

    
5632
        `mkdir "$newstordir/common"` unless (-e "$newstordir/common");
5633
        `cp "$stordir/ejectcdrom.xml" "$newstordir/ejectcdrom.xml"` unless (-e "$newstordir/ejectcdrom.xml");
5634
        `cp "$stordir/mountvirtio.xml" "$newstordir/mountvirtio.xml"` unless (-e "$newstordir/mountvirtio.xml");
5635
        `cp "$stordir/dummy.qcow2" "$newstordir/dummy.qcow2"` unless (-e "$newstordir/dummy.qcow2");
5636
    }
5637
    Updatedownloads();
5638

    
5639
    # Update /etc/stabile/cgconfig.conf
5640
    my $devs = $devices{$dev}->{dev};
5641
    my @pdevs = split(" ", $devs);
5642
    my $majmins;
5643
    foreach my $dev (@pdevs) {
5644
        # It seems that cgroups cannot handle individual partitions for blkio
5645
        my $physdev = $1 if ($dev =~ /(\w+)\d+/);
5646
        if ($physdev && -d "/sys/fs/cgroup" ) {
5647
            my $blkline = `lsblk -l /dev/$physdev`;
5648
            my $majmin = '';
5649
            $majmin = $1 if ($blkline =~ /$physdev +(\d+:\d+)/);
5650
            $postreply .= "Status=OK Setting cgroups block device to $majmin\n";
5651
            if ($majmin) {
5652
                $majmins .= ($majmins)?" $majmin":$majmin;
5653
            }
5654
        }
5655
    }
5656
    setCgroupsBlkDevice($majmins) if ($majmins);
5657

    
5658
    $Stabile::Nodes::console = 1;
5659
    require "$Stabile::basedir/cgi/nodes.cgi";
5660
    $postreply .= Stabile::Nodes::do_reloadall('','reloadall');
5661

    
5662
    # Update config on stabile.io
5663
    require "$Stabile::basedir/cgi/users.cgi";
5664
    $Stabile::Users::console = 1;
5665
    Stabile::Users::Updateengine('', 'updateengine');
5666

    
5667
    my $msg = "OK Now using $newstordir for $type on $obj->{device}";
5668
    $main::updateUI->({tab=>'home', user=>$user, type=>'update', message=>$msg});
5669
    $postreply .= "Status=OK Now using $newstordir for $type on $dev\n";
5670
    return $postreply;
5671
}
5672

    
5673
sub Initializestorage {
5674
    my ($image, $action, $obj) = @_;
5675
    if ($help) {
5676
        return <<END
5677
GET:device,type,fs,activate,force:
5678
Initializes a local disk or partition, and optionally formats it with ZFS and creates a ZFS pool to use as image storage or backup storage.
5679
[device] is a local disk device in /dev like e.g. 'sdd'. [type] may be either 'images' (default) or 'backup'. [fs] may be 'lvm' (default) or 'zfs'.
5680
Set [activate] if you want to put the device into use immediately. Set [force] if you want to destroy existing ZFS pool and recreate (obviously use with care).
5681
END
5682
    }
5683
    my $fs = $obj->{fs} || 'zfs';
5684
    my $dev = $obj->{device};
5685
    my $force = $obj->{force};
5686
    my $activate = $obj->{activate};
5687
    my $type = 'backup';
5688
    $type = 'images' if ($obj->{type} eq 'images');
5689
    return "Status=Error Not allowed\n" unless ($isadmin);
5690
    my $backupdevice = Getbackupdevice('', 'getbackupdevice');
5691
    my $imagesdevice = Getimagesdevice('', 'getimagesdevice');
5692
    my $devices_obj = from_json(Liststoragedevices('', 'liststoragedevices'));
5693
    my %devices = %$devices_obj;
5694
    my $backupdev = $devices{$backupdevice}->{dev};
5695
    my $imagesdev = $devices{$imagesdevice}->{dev};
5696
    if (!$dev || !(-e "/dev/$dev")) {
5697
        $postreply = "Status=Error You must specify a valid device\n";
5698
        return $postreply;
5699
    }
5700
    if (($backupdev =~ /$dev/) || ($imagesdev =~ /$dev/)) {
5701
        $postreply = "Status=Error $dev is already in use as images or backup device\n";
5702
        return $postreply;
5703
    }
5704
    my $stordir = "/stabile-$type/$type";
5705
    if ($fs eq 'lvm') {
5706
        if ($type eq 'backup') {
5707
            $stordir = "/mnt/stabile/backups";
5708
        } else {
5709
            $stordir = "/mnt/stabile/images";
5710
        }
5711
    }
5712
    `chmod 666 /dev/zfs` if (-e '/dev/zfs'); # TODO: This should be removed once we upgrade to Bionic and zfs allow is supported
5713

    
5714
    my $vol = $type . "vol";
5715
    my $mounts = `cat /proc/mounts`;
5716
    my $zpools = `zpool list -v`;
5717
    my $pvs = `pvdisplay -c`;
5718
    my $z;
5719
    $postreply = '';
5720
    # Unconfigure existing zfs or lvm if $force and zfs/lvm configured or device is in use by either
5721
    if ($zpools =~ /stabile-$type/ || $mounts =~ /dev\/mapper\/stabile$type/ || $zpools =~ /$dev/ || $pvs =~ /$dev/) {
5722
        if ($fs eq 'zfs' || $zpools =~ /$dev/) {
5723
            if ($force) { # ZFS needs to be unconfigured
5724
                my $umount = `LANG=en_US.UTF-8 umount -v "/stabile-$type/$type" 2>&1`;
5725
                unless ($umount =~ /(unmounted|not mounted|no mount point)/) {
5726
                    $postreply .= "Status=Error Unable to unmount zfs $type storage on $dev - $umount\n";
5727
                    return $postreply;
5728
                }
5729
                `umount "/stabile-$type"`;
5730
                my $res = `zpool destroy "stabile-$type" 2>&1`;
5731
                chomp $res;
5732
                $postreply .= "Status=OK Unconfigured zfs - $res\n";
5733
            } else {
5734
                $postreply .= "Status=Error ZFS is already configured for $type\n";
5735
                $z = 1;
5736
            #    return $postreply;
5737
            }
5738
        }
5739
        if ($fs eq 'lvm' || $pvs =~ /$dev/) {
5740
            if ($force) {
5741
                my $udir = (($type eq 'backup')?"/mnt/stabile/backups":"/mnt/stabile/images");
5742
                my $umount = `umount -v "$udir" 2>&1`;
5743
                unless ($umount =~ /unmounted|not mounted|no mount point/) {
5744
                    $postreply .= "Status=Error Unable to unmount lvm $type storage - $umount\n";
5745
                    return $postreply;
5746
                }
5747
                my $res = `lvremove --yes /dev/stabile$type/$vol  2>&1`;
5748
                chomp $res;
5749
                $res .= `vgremove -f stabile$type 2>&1`;
5750
                chomp $res;
5751
                my $pdev = "/dev/$dev";
5752
                $pdev .= '1' unless ($pdev =~ /1$/);
5753
                $res .= `pvremove $pdev 2>&1`;
5754
                chomp $res;
5755
                $postreply .= "Status=OK Unconfigured lvm - $res\n";
5756
            } else {
5757
                $postreply .= "Status=Error LVM is already configured for $type\n";
5758
                return $postreply;
5759
            }
5760
        }
5761
    }
5762
    # Check if $dev is still in use
5763
    $mounts = `cat /proc/mounts`;
5764
    $zpools = `zpool list -v`;
5765
    $pvs = `pvdisplay -c`;
5766
    if ($mounts =~ /\/dev\/$dev/ || $pvs =~ /$dev/ || $zpools =~ /$dev/) {
5767
        $postreply .= "Status=Error $dev is already in use - use force.\n";
5768
        return $postreply;
5769
    }
5770
    # Now format
5771
    my $ispart = 1 if ($dev =~ /[a-zA-Z]+\d+/);
5772
    if ($fs eq 'zfs') { # ZFS was specified
5773
        $postreply = "Status=OK Initializing $dev disk with ZFS for $type...\n";
5774
        if (!$ispart) {
5775
            my $fres = `parted -s /dev/$dev mklabel GPT 2>&1`;
5776
            $postreply .= "Status=OK partitioned $dev: $fres\n";
5777
        }
5778
        if ($z) { # zpool already created
5779
            `zpool add stabile-$type /dev/$dev`;
5780
        } else {
5781
            `zpool create stabile-$type /dev/$dev`;
5782
            `zfs create stabile-$type/$type`;
5783
            `zfs set atime=off stabile-$type/$type`;
5784
        }
5785
#        if ($force) {
5786
#            $postreply .= "Status=OK Forcibly removing all files in $stordir to allow ZFS mount\n";
5787
#            `rm -r $stordir/*`;
5788
#        }
5789
#        `zfs set mountpoint=$stordir stabile-$type/$type`;
5790
        $stordir = "/stabile-$type/$type" if (`zfs mount stabile-$type/$type`);
5791
        `/bin/chmod 777 $stordir`;
5792
        $postreply .= "Status=OK Mounted stabile-$type/$type as $type storage on $stordir.\n";
5793
        if ($activate) {
5794
            $postreply .= "Status=OK Setting $type storage device to $dev.\n";
5795
            Setstoragedevice('', 'setstoragedevice', {device=>"stabile-$type", type=>$type});
5796
        }
5797
    } else { # Assume LVM
5798
        $postreply = "Status=OK Initializing $dev with LVM for $type...\n";
5799
        my $part = $dev;
5800
        if (!$ispart) {
5801
            $part = $dev.'1';
5802
            `/sbin/sfdisk -d /dev/$dev > /root/$dev-partition-sectors.save`;
5803
            my $fres = `sfdisk /dev/$dev << EOF\n;\nEOF`;
5804
            $postreply .= "Status=OK partitioned $dev: $fres\n";
5805
        }
5806
        `/sbin/vgcreate -f stabile$type /dev/$part`;
5807
        `/sbin/vgchange -a y stabile$type`;
5808
        my $totalpe =`/sbin/vgdisplay stabile$type | grep "Total PE"`;
5809
        $totalpe =~ /Total PE\s+(\d+)/;
5810
        my $size = $1 -2000;
5811
#        my $size = "10000";
5812
        if ($size <100) {
5813
            $postreply .= "Status=Error Volume is too small to make sense...\n";
5814
            return $postreply;
5815
        }
5816
        my $vol = $type . "vol";
5817
        `/sbin/lvcreate --yes -l $size stabile$type -n $vol`;
5818
#        `/sbin/mkfs.ext4 /dev/stabile$type/$vol`;
5819
        `mkfs.btrfs /dev/stabile$type/$vol`;
5820
        my $mounted = `mount -v /dev/stabile$type/$vol $stordir`;
5821
        `chmod 777 $stordir`;
5822
        if ($mounted) {
5823
            $postreply .= "Status=OK Mounted /dev/stabile$type/$vol as $type storage on $stordir.\n";
5824
        } else {
5825
            $postreply .= "Status=Error Could not mount /dev/stabile$type/$vol as $type storage on $stordir.\n";
5826
        }
5827
        if ($activate){
5828
            Setstoragedevice('', 'setstoragedevice', {device=>"stabile$type-$type".'vol', type=>$type});
5829
        }
5830
    }
5831
    return $postreply;
5832
}
5833

    
5834
sub setCgroupsBlkDevice {
5835
    my @majmins = split(" ", shift);
5836
    my $file = "/etc/stabile/cgconfig.conf";
5837
    my %options = (
5838
        blkio.throttle.read_bps_device => $valve_readlimit,
5839
        blkio.throttle.write_bps_device => $valve_writelimit,
5840
        blkio.throttle.read_iops_device => $valve_iopsreadlimit,
5841
        blkio.throttle.write_iops_device => $valve_iopswritelimit
5842
        );
5843
    my @groups = ('stabile', 'stabilevm');
5844
    my @newlines;
5845
    foreach my $majmin (@majmins) {
5846
        foreach my $group (@groups) {
5847
            my $mline = qq|group $group {|; push @newlines, $mline;
5848
            my $mline = qq|    blkio {|; push @newlines, $mline;
5849
            foreach my $option (keys %options) {
5850
                my $mline = qq|        $option = "$majmin $options{$option}";|;
5851
                push @newlines, $mline;
5852
            }
5853
            my $mline = qq|    }|; push @newlines, $mline;
5854
            my $mline = qq|}|; push @newlines, $mline;
5855
        }
5856
    }
5857
    unless (open(FILE, "> $file")) {
5858
        $postreply .= "Status=Error Problem opening $file\n";
5859
        return $postreply;
5860
    }
5861
    print FILE join("\n", @newlines);
5862
    close(FILE);
5863
    return;
5864
}
(2-2/9)