Project

General

Profile

Download (258 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
                        my $metastatus;
558
                        $metastatus = `/bin/cat "$path.meta" 2>/dev/null`;
559
                        chomp $metastatus;
560

    
561
                        if ($metastatus =~ /status=(.+)&chunk=/) {
562
                            $status = $1;
563
                        } elsif ($metastatus =~ /status=(.+)&path2:(.+)=(.+)/) {
564
                        # A move operation has been completed - update status of both involved
565
                            $status = $1;
566
                            $register{$2}->{'status'} = $3;
567
                            unless ($userregister{$2}) { # If we have not yet parsed image, it is not yet in userregister, so put it there
568
                                my %mval = %{$register{$2}};
569
                                $userregister{$2} = \%mval;
570
                            }
571
                            $userregister{$2}->{'status'} = $3;
572
                        } elsif ($metastatus =~ /status=(\w+)/) {
573
                            $status = $1;
574
                        } else {
575
                        #    $status = $metastatus; # Do nothing - this meta file contains no status info
576
                        }
577
                    } elsif (
578
                            $status eq "restoring"
579
                            || $status eq "frestoring"
580
                            || ($status eq "mounted" && $mounted)
581
                            || $status eq "snapshotting"
582
                            || $status eq "unsnapping"
583
                            || $status eq "reverting"
584
                            || $status eq "moving"
585
                            || $status eq "stormoving"
586
                            || $status eq "converting"
587
                            || $status eq "cloning"
588
                            || $status eq "copying"
589
                            || $status eq "rebasing"
590
                            || $status eq "creating"
591
                            || $status eq "resizing"
592
                        ) { # When operation is done, status is updated by piston.cgi
593
                        ; # Do nothing
594
                    } elsif ($status =~ /.(backingup)/) { # When backup is done, status is updated by steamExec
595
                        if ($valref->{'storagepool'}==-1) {
596
                        #    unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
597
                            if ($nodereg{$valref->{'mac'}}) {
598
                                my $nodestatus = $nodereg{$valref->{'mac'}}->{status};
599
                                # If node is not available, it cannot be backing up...
600
                                if ($nodestatus eq 'inactive'
601
                                    || $nodestatus eq 'asleep'
602
                                    || $nodestatus eq 'shutoff'
603
                                ) {
604
                                    $valref->{'status'} = 'unused'; # Make sure we don't end here again in endless loop
605
                                    $rstatus = Updateregister(0, $path);
606
                                    $status = $rstatus if ($rstatus);
607
                                    $main::syslogit->($user, 'info', "Updated image status for aborted backup - $user, $path, $rstatus");
608
                                }
609
                            }
610
                            #untie %nodereg;
611
                        }
612

    
613
                    } elsif ($status eq 'uploading') {
614
                        $status = 'unused' unless (-s "$path.meta");
615

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

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

    
706
sub getSizes {
707
    my ($f, $lmtime, $status, $buser, $force) = @_;
708

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

    
723
# Only fire up qemu-img etc. if image has been modified and is not being used
724
    if ((
725
        ($mtime - $lmtime)>300 &&
726
        ($status ne 'active' && $status ne 'downloading') &&
727
        !($ps =~ /$f/)) || $force
728
    ) {
729

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

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

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

    
776
        return ($mtime, $backupsize, $size, $realsize, $virtualsize);
777
    } else {
778
        return (0, $backupsize, $size, $realsize);
779
    }
780

    
781
}
782

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

    
790
	my @idvalues = values %idreg;
791
	my %formats;
792
	foreach my $val (@idvalues) {
793
		my %h = %$val;
794
		foreach (split(/,/,$h{'formats'})) {
795
			$formats{lc $_} = $h{'hypervisor'}
796
		}
797
	}
798
	untie %idreg;
799

    
800
	# and then determine the hypervisor in question
801
	my $hypervisor = "vbox";
802
	my ($pathname, $path, $suffix) = fileparse($image, '\.[^\.]*');
803
	$suffix = substr $suffix, 1;
804
	my $hypervisor = $formats{lc $suffix};
805
	return $hypervisor;
806
}
807

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

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

    
849
    if ($action eq 'getserverbackups') {
850
        $res .= to_json(\@sbackups, {pretty=>1});
851
    } else {
852
        $res .= header() unless ($console);
853
        $res .= $backuplist;
854
    }
855
    return $res;
856

    
857
}
858

    
859
sub Listbackups {
860
    my ($curimg, $action) = @_;
861
    if ($help) {
862
        return <<END
863
GET:image:
864
List backups on file for the give image, which may be specified as path or uuid.
865
END
866
    }
867

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

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

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

    
973
    #require File::Spec;
974
    #my $devnull = File::Spec->devnull();
975

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

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

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

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

    
1052
    if ($mounted) {
1053
        if ($mounted2) {
1054
            $postreply .= "Status=ERROR Unable to unmount $register{$path}->{'name'}\n";
1055
            return $postreply;
1056
        } else {
1057
            $postreply .= "Status=OK Unmounted image $register{$path}->{'name'}\n";
1058
            return $postreply;
1059
        }
1060
    } else {
1061
        $postreply .= "Status=OK Image $path not mounted\n";
1062
        return $postreply;
1063
    }
1064
}
1065

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

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

    
1108
    if ($mounted) {
1109
        $postreply .= "Status=OK Image $register{$path}->{'name'} already mounted\n";
1110
        return $postreply;
1111
    } else {
1112
        `/bin/mkdir "$mountpath"` unless (-e "$mountpath");
1113
        `/bin/chown www-data:www-data  "$mountpath"`;
1114
        my $cmd;
1115

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

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

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

    
1208
# List files in a mounted image. Mount image if not mounted.
1209
sub Listfiles {
1210
    my ($curimg, $action, $obj) = @_;
1211
    if ($help) {
1212
        return <<END
1213
GET:image,path:
1214
Try to mount the file system on the given image, and list the files from the given path in the mounted file system.
1215
The image must contain a bootable file system, in order to locate a mount point.
1216
END
1217
    }
1218
    my $res;
1219
    my $curpath = $obj->{'restorepath'};
1220
    $res .= header('application/json') unless ($console);
1221

    
1222
    my($bname, $dirpath, $suffix) = fileparse($curimg, (".vmdk", ".img", ".vhd", ".vhdx", ".qcow", ".qcow2", ".vdi", ".iso"));
1223
    my $mountpath = "$dirpath.$bname$suffix";
1224
	my @files;
1225
	my @dirs;
1226
    my $mounted = (Mount($curimg) =~ /\w=OK/);
1227

    
1228
    if ($mounted) {
1229
        my @patterns = ('');
1230
        $curpath .= '/' unless ($curpath =~ /\/$/);
1231
        $mountpath .= "$curpath";
1232
        if (-d $mountpath) { # We are listing a directory
1233
            # loop through the files contained in the directory
1234
            @patterns = ('*', '.*');
1235
        }
1236
        foreach $pat (@patterns) {
1237
            for my $f (bsd_glob($mountpath.$pat)) {
1238
                my %fhash;
1239
                ($bname, $dirpath) = fileparse($f);
1240
                my @stat = stat($f);
1241
                my $size = $stat[7];
1242
                my $realsize = $stat[12] * 512;
1243
                my $mtime = $stat[9];
1244

    
1245
                $fhash{'name'} = $bname;
1246
                $fhash{'mtime'} = $mtime;
1247
                ## if the file is a directory
1248
                if( -d $f) {
1249
                    $fhash{'size'} = 0;
1250
                    $fhash{'fullpath'} = $f . '/';
1251
                    $fhash{'path'} = $curpath . $bname . '/';
1252
                    push(@dirs, \%fhash) unless ($bname eq '.' || $bname eq '..');
1253
                } else {
1254
                    $fhash{'size'} = $size;
1255
                    $fhash{'fullpath'} = $f;
1256
                    $fhash{'path'} = $curpath . $bname;
1257
                    push(@files, \%fhash);
1258
                }
1259
            }
1260
        }
1261

    
1262
        if ($console) {
1263
            my $t2 = Text::SimpleTable->new(48,16,28);
1264
            $t2->row('name', 'size', 'mtime');
1265
            $t2->hr;
1266
            foreach my $fref (@dirs) {
1267
                $t2->row($fref->{'name'}, $fref->{'size'}, scalar localtime( $fref->{'mtime'} )) unless ($bname eq '.' || $bname eq '..');
1268
            }
1269
            foreach my $fref (@files) {
1270
                $t2->row($fref->{'name'}, $fref->{'size'}, scalar localtime( $fref->{'mtime'} ) ) unless ($bname eq '.' || $bname eq '..');
1271
            }
1272
            return $t2->draw;
1273
        } else {
1274
            my @comb = (@dirs, @files);
1275
            $res .= to_json(\@comb, {pretty => 1});
1276
        }
1277
    } else {
1278
        $res .= qq|{"status": "Error", "message": "Image $curimg not mounted. Mount first."}|;
1279
    }
1280
    return $res;
1281
}
1282

    
1283
sub Restorefiles {
1284
    my ($path, $action, $obj) = @_;
1285
    if ($help) {
1286
        return <<END
1287
GET:image,files:
1288
Restores files from the given path in the given image to a newly created ISO image. The given image must be mountable.
1289
END
1290
    }
1291
    my $res;
1292
    $curfiles = $obj->{'files'};
1293
    $path = $path || $curimg;
1294

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

    
1298
    my $name = $register{$path}->{'name'};
1299
    my($bname, $dirpath, $suffix) = fileparse($path, (".vmdk", ".img", ".vhd", ".vhdx", ".qcow", ".qcow2", ".vdi", ".iso"));
1300
    my $mountpath = "$dirpath.$bname$suffix";
1301
#    my $mounts = decode('ascii-escape', `/bin/cat /proc/mounts`);
1302
    my $mounts = `/bin/cat /proc/mounts`;
1303
    my $mmounts = `/bin/df`;
1304
    my $mounted = ($mounts =~ /$mountpath/ && $mmounts =~ /$mountpath/);
1305
    my $restorepath = "$dirpath$bname.iso";
1306

    
1307
    if (-e $restorepath) {
1308
        my $i = 1;
1309
        while (-e "$dirpath$bname.$i.iso") {$i++;}
1310
        $restorepath = "$dirpath$bname.$i.iso";
1311
    }
1312

    
1313
    my $uistatus = "frestoring";
1314
    if ($mounted && $curfiles) {
1315
        my $ug = new Data::UUID;
1316
        my $newuuid = $ug->create_str();
1317
        $register{$restorepath} = {
1318
                            uuid=>$newuuid,
1319
                            status=>$uistatus,
1320
                            name=>"Files from: $name",
1321
                            size=>0,
1322
                            realsize=>0,
1323
                            virtualsize=>0,
1324
                            type=>"iso",
1325
                            user=>$user
1326
                        };
1327

    
1328
        eval {
1329
                my $oldstatus = $register{$path}->{'status'};
1330
#                my $cmd = qq|steamExec $user $uistatus $oldstatus "$path" "$curfiles"|;
1331
#                my $cmdres = `$cmd`;
1332
            if ($mounted) {
1333
                $res .= "Restoring files to: /tmp/restore/$user/$bname$suffix -> $restorepath\n";
1334
                $res .= `/bin/echo $status > "$restorepath.meta"`;
1335

    
1336
                `/bin/mkdir -p "/tmp/restore/$user/$bname$suffix"` unless (-e "/tmp/restore/$user/$bname$suffix");
1337
                my @files = split(/:/, uri_unescape($curfiles));
1338
                foreach $f (@files) {
1339
                    if (-e "$mountpath$f" && chdir($mountpath)) {
1340
                        $f = substr($f,1) if ($f =~ /^\//);
1341
                        eval {`/usr/bin/rsync -aR --sparse "$f" /tmp/restore/$user/$bname$suffix`; 1;}
1342
                            or do {$e=1; $res .= "ERROR Problem restoring files $@\n";};
1343
                    } else {
1344
                        $res .= "Status=Error $f not found in $mountpath\n";
1345
                    }
1346
                }
1347
                if (chdir "/tmp/restore/$user/$bname$suffix") {
1348
                    eval {$res .= `/usr/bin/genisoimage -o "$restorepath" -iso-level 4 .`; 1;}
1349
                        or do {$e=1; $res .= "Status=ERROR Problem restoring files $@\n";};
1350
                    $res .= `/bin/rm -rf /tmp/restore/$user/$bname$suffix`;
1351
                    $res .= "Status=OK Restored files from /tmp/restore/$user/$bname$suffix to $restorepath\n";
1352
                } else {
1353
                    $res .= "Status=ERROR Unable to chdir to /tmp/restore/$user/$bname$suffix\n";
1354
                }
1355
                $main::updateUI->({tab=>"images", user=>$user, type=>"update"});
1356

    
1357
                # Update billing
1358
                my $newvirtualsize = getVirtualSize($restorepath);
1359
                unlink "$restorepath.meta";
1360
                $res .= Unmount($path);
1361
                $register{$restorepath}->{'status'} = 'unused';
1362
                $register{$restorepath}->{'virtualsize'} = $newvirtualsize;
1363
                $register{$restorepath}->{'realsize'} = $newvirtualsize;
1364
                $register{$restorepath}->{'size'} = $newvirtualsize;
1365
                $postmsg = "OK - restored your files into a new ISO.";
1366
            } else {
1367
                $res .= "Status=Error You must mount image on $mountpath before restoring\n";
1368
            }
1369
            $res .=  "Status=OK $uistatus files from $name to iso, $newuuid, $cmd\n";
1370
            $main::syslogit->($user, "info", "$uistatus files from $path to iso, $newuuid");
1371
            1;
1372
        } or do {$res .= "Status=ERROR $@\n";}
1373

    
1374
    } else {
1375
        $res .= "Status=ERROR Image not mounted, mount before restoring: ". $curfiles ."\n";
1376
    }
1377
    return $res;
1378
}
1379

    
1380
sub trim{
1381
   my $string = shift;
1382
   $string =~ s/^\s+|\s+$//g;
1383
   return $string;
1384
}
1385

    
1386
sub do_overquota {
1387
    my ($path, $action, $obj) = @_;
1388
    if ($help) {
1389
        return <<END
1390
GET:inc,onnode:
1391
Check if 'inc' bytes will bring you over your storage quota. Set onnode to 1 to check node storage quota.
1392
END
1393
    }
1394
    if (overQuotas($obj->{inc}, $obj->{onnode})) {
1395
        return "Status=Error Over storage quota\n";
1396
    } else {
1397
        return "Status=OK Not over storage quota\n";
1398
    }
1399
}
1400

    
1401
sub overQuotas {
1402
    my $inc = shift;
1403
    my $onnode = shift;
1404
	my $usedstorage = 0;
1405
	my $overquota = 0;
1406
    return 0 if ($Stabile::userprivileges =~ /a/); # Don't enforce quotas for admins
1407
	my $storagequota = ($onnode)?$Stabile::usernodestoragequota:$Stabile::userstoragequota;
1408

    
1409
	if (!$storagequota) { # 0 or empty quota means use defaults
1410
        $storagequota = (($onnode)?$Stabile::config->get('NODESTORAGE_QUOTA'):$Stabile::config->get('STORAGE_QUOTA')) + 0;
1411
	}
1412
    return 0 if ($storagequota == -1); # -1 means no quota
1413

    
1414
    my @regkeys = (tied %register)->select_where("user = '$user'");
1415
    foreach my $k (@regkeys) {
1416
        my $val = $register{$k};
1417
		if ($val->{'user'} eq $user) {
1418
		    $usedstorage += $val->{'virtualsize'} if ((!$onnode &&  $val->{'storagepool'}!=-1) || ($onnode &&  $val->{'storagepool'}==-1));
1419
		}
1420
	}
1421
    if ($usedstorage+$inc > $storagequota * 1024 *1024) {
1422
        $overquota = $usedstorage+$inc;
1423
    }
1424
	return $overquota;
1425
}
1426

    
1427
sub overStorage {
1428
    my ($reqstor, $spool, $mac) = @_;
1429
    my $storfree;
1430
    if ($spool == -1) {
1431
        if ($mac) {
1432
            unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
1433
            $storfree = $nodereg{$mac}->{'storfree'};
1434
            $storfree = $storfree *1024 * $nodestorageovercommission;
1435
            untie %nodereg;
1436
        } else {
1437
            return 1;
1438
        }
1439
    } else {
1440
        my $storpath = $spools[$spool]->{'path'};
1441
        $storfree = `df $storpath`;
1442
        $storfree =~ m/(\d\d\d\d+)(\s+)(\d\d*)(\s+)(\d\d+)(\s+)(\S+)/i;
1443
        my $stortotal = $1;
1444
        my $storused = $3;
1445
        $storfree = $5 *1024;
1446
    }
1447
    return ($reqstor > $storfree);
1448
}
1449

    
1450
sub updateBilling {
1451
    my $event = shift;
1452
    my %billing;
1453

    
1454
    my @regkeys = (tied %register)->select_where("user = '$user'");
1455
    foreach my $k (@regkeys) {
1456
        my $valref = $register{$k};
1457
        my %val = %{$valref}; # Deference and assign to new array, effectively cloning object
1458
        $val{'virtualsize'} += 0;
1459
        $val{'realsize'} += 0;
1460
        $val{'backupsize'} += 0;
1461

    
1462
        if ($val{'user'} eq $user && (defined $spools[$val{'storagepool'}]->{'id'} || $val{'storagepool'}==-1)) {
1463
            $billing{$val{'storagepool'}}->{'virtualsize'} += $val{'virtualsize'};
1464
            $billing{$val{'storagepool'}}->{'realsize'} += $val{'realsize'};
1465
            $billing{$val{'storagepool'}}->{'backupsize'} += $val{'backupsize'};
1466
        }
1467
    }
1468

    
1469
    my %billingreg;
1470

    
1471
    unless (tie %billingreg,'Tie::DBI', {
1472
            db=>'mysql:steamregister',
1473
            table=>'billing_images',
1474
            key=>'userstoragepooltime',
1475
            autocommit=>0,
1476
            CLOBBER=>3,
1477
            user=>$dbiuser,
1478
            password=>$dbipasswd}) {throw Error::Simple("Stroke=Error Billing register (images) could not be accessed")};
1479

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

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

    
1485
    my %pool = ("hostpath", "--",
1486
                "path", "--",
1487
                "name", "local",
1488
                "rdiffenabled", 1,
1489
                "id", -1);
1490
    my @bspools = @spools;
1491
    push @bspools, \%pool;
1492

    
1493
    foreach my $spool (@bspools) {
1494
        my $storagepool = $spool->{"id"};
1495
        my $b = $billing{$storagepool};
1496
        my $virtualsize = $b->{'virtualsize'} +0;
1497
        my $realsize = $b->{'realsize'} +0;
1498
        my $backupsize = $b->{'backupsize'} +0;
1499

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

    
1536
                $virtualsizeavg = ($startvirtualsizeavg*($starttimestamp - $monthtimestamp) + $virtualsize*($current_time - $starttimestamp)) /
1537
                                ($current_time - $monthtimestamp);
1538
                $realsizeavg = ($startrealsizeavg*($starttimestamp - $monthtimestamp) + $realsize*($current_time - $starttimestamp)) /
1539
                                ($current_time - $monthtimestamp);
1540
                $backupsizeavg = ($startbackupsizeavg*($starttimestamp - $monthtimestamp) + $backupsize*($current_time - $starttimestamp)) /
1541
                                ($current_time - $monthtimestamp);
1542
            }
1543
            # Update sizes in DB
1544
                $billingreg{"$user-$storagepool-$year-$month"}->{'virtualsize'} = $virtualsize;
1545
                $billingreg{"$user-$storagepool-$year-$month"}->{'realsize'} = $realsize;
1546
                $billingreg{"$user-$storagepool-$year-$month"}->{'backupsize'} = $backupsize;
1547
            # Update start averages
1548
                $billingreg{"$user-$storagepool-$year-$month"}->{'startvirtualsizeavg'} = $startvirtualsizeavg;
1549
                $billingreg{"$user-$storagepool-$year-$month"}->{'startrealsizeavg'} = $startrealsizeavg;
1550
                $billingreg{"$user-$storagepool-$year-$month"}->{'startbackupsizeavg'} = $startbackupsizeavg;
1551
            # Update current averages with values just calculated
1552
                $billingreg{"$user-$storagepool-$year-$month"}->{'virtualsizeavg'} = $virtualsizeavg;
1553
                $billingreg{"$user-$storagepool-$year-$month"}->{'realsizeavg'} = $realsizeavg;
1554
                $billingreg{"$user-$storagepool-$year-$month"}->{'backupsizeavg'} = $backupsizeavg;
1555
            # Update time stamps and inc
1556
                $billingreg{"$user-$storagepool-$year-$month"}->{'timestamp'} = $current_time;
1557
                $billingreg{"$user-$storagepool-$year-$month"}->{'starttimestamp'} = $starttimestamp;
1558
                $billingreg{"$user-$storagepool-$year-$month"}->{'inc'}++;
1559

    
1560
        # Write new row
1561
        } else {
1562
            $billingreg{"$user-$storagepool-$year-$month"} = {
1563
                virtualsize=>$virtualsize+0,
1564
                realsize=>$realsize+0,
1565
                backupsize=>$backupsize+0,
1566

    
1567
                virtualsizeavg=>$virtualsizeavg,
1568
                realsizeavg=>$realsizeavg,
1569
                backupsizeavg=>$backupsizeavg,
1570

    
1571
                startvirtualsizeavg=>$startvirtualsizeavg,
1572
                startrealsizeavg=>$startrealsizeavg,
1573
                startbackupsizeavg=>$startbackupsizeavg,
1574

    
1575
                timestamp=>$current_time,
1576
                starttimestamp=>$starttimestamp,
1577
                event=>$event,
1578
                inc=>1,
1579
            };
1580
        }
1581
    }
1582
    tied(%billingreg)->commit;
1583
    untie %billingreg;
1584
}
1585

    
1586
sub Removeuserimages {
1587
    my ($path, $action, $obj) = @_;
1588
    if ($help) {
1589
        return <<END
1590
GET::
1591
Removes all images belonging to a user from storage, i.e. completely deletes the image and its backups (be careful).
1592
END
1593
    }
1594

    
1595
    $postreply = removeUserImages($user) unless ($isreadonly);
1596
    return $postreply;
1597
}
1598

    
1599
sub removeUserImages {
1600
    my $username = shift;
1601
    return unless ($username && ($isadmin || $user eq $username) && !$isreadonly);
1602
    $user = $username;
1603
    foreach my $path (keys %register) {
1604
        if ($register{$path}->{'user'} eq $user) {
1605
            $postreply .=  "Status=OK Removing " . ($Stabile::preserveimagesonremove?"(preserving) ":"") . " $username image $register{$path}->{'name'}, $register{$path}->{'uuid'}" . ($console?'':'<br>') . "\n";
1606
            Remove($path, 'remove', 0, $Stabile::preserveimagesonremove);
1607
        }
1608
    }
1609
    $postreply .= "Status=Error No storage pools!\n" unless (@spools);
1610
    foreach my $spool (@spools) {
1611
        my $pooldir = $spool->{"path"};
1612
        unless (-e $pooldir) {
1613
            $postreply .= "Status=Error Storage $pooldir, $spool->{name} does not exist\n" unless (@spools);
1614
            next;
1615
        }
1616

    
1617
        $postreply .= "Status=OK Removing user dir $pooldir/$username ";
1618
        $postreply .= `/bin/rm "$pooldir/$username/.htaccess"` if (-e "$pooldir/$username/.htaccess");
1619
        $postreply .= `/bin/rmdir --ignore-fail-on-non-empty "$pooldir/$username/fuel"` if (-e "$pooldir/$username/fuel");
1620
        $postreply .= `/bin/rmdir --ignore-fail-on-non-empty "$pooldir/$username"` if (-e "$pooldir/$username");
1621
        $postreply .= "\n";
1622
    }
1623

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

    
1626
    foreach $mac (keys %nodereg) {
1627
        $macip = $nodereg{$mac}->{'ip'};
1628
        my $esc_path = "/mnt/stabile/node/$username";
1629
        $esc_path =~ s/([ ])/\\$1/g;
1630
        if (!$Stabile::preserveimagesonremove) {
1631
            `$sshcmd $macip "/bin/rmdir $esc_path"`;
1632
            $postreply .= "Status=OK Removing node user dir /mnt/stabile/node/$username on node $mac\n";
1633
        }
1634
    }
1635
    untie %nodereg;
1636

    
1637
    return $postreply;
1638
}
1639

    
1640
sub Remove {
1641
    my ($path, $action, $obj, $preserve, $mac) = @_;
1642
    if ($help) {
1643
        return <<END
1644
DELETE:image,mac:
1645
Removes an image from storage, i.e. completely deletes the image and its backups (be careful).
1646
END
1647
    }
1648
    $path = $imagereg{$path}->{'path'} if ($imagereg{$path}); # Check if we were passed a uuid
1649
    $path = $curimg if (!$path && $register{$curimg});
1650
    if (!$curimg && $path && !($path =~ /^\//)) {
1651
        $curimg = $path;
1652
        $path = '';
1653
    }
1654
    if (!$path && $curimg && !($curimg =~ /\//) ) { # Allow passing only image name if we are deleting an app master
1655
        my $dspool = $stackspool;
1656
        $dspool = $spools[0]->{'path'} unless ($engineid eq $valve001id);
1657
        if ($curimg =~ /\.master.qcow2$/ && $register{"$dspool/$user/$curimg"}) {
1658
            $path = "$dspool/$user/$curimg";
1659
        } elsif ($isadmin && $curimg =~ /\.master.qcow2$/ && $register{"$dspool/common/$curimg"}) {
1660
            $path = "$dspool/common/$curimg";
1661
        }
1662
    }
1663
    utf8::decode($path);
1664

    
1665
    my $img = $register{$path};
1666
    my $status = $img->{'status'};
1667
    $mac = $mac || $obj->{mac} || $img->{'mac'}; # Remove an image from a specific node
1668
    my $name = $img->{'name'};
1669
    my $uuid = $img->{'uuid'};
1670
    utf8::decode($name);
1671
    my $type = $img->{'type'};
1672
    my $username = $img->{'user'};
1673

    
1674
    unless ($username && ($isadmin || $user eq $username) && !$isreadonly) {
1675
        return qq|[]|;
1676
#        $postmsg = "Cannot delete image";
1677
#        $postreply .= "Status=Error $postmsg\n";
1678
#        return $postreply;
1679
    }
1680

    
1681
    $uistatus = "deleting";
1682
    if ($status eq "unused" || $status eq "uploading" || $path =~ /(.+)\.master\.$type/) {
1683
        my $haschildren;
1684
        my $child;
1685
        my $hasprimary;
1686
        my $primary;
1687
        my $master = ($img->{'master'} && $img->{'master'} ne '--')?$img->{'master'}:'';
1688
        my $usedmaster = '';
1689
        my @regvalues = values %register;
1690
        foreach my $valref (@regvalues) {
1691
            if ($valref->{'master'} eq $path) {
1692
                $haschildren = 1;
1693
                $child = $valref->{'name'};
1694
            #    last;
1695
            }
1696
            if ($master) {
1697
                $usedmaster = 1 if ($valref->{'master'} eq $master && $valref->{'path'} ne $path); # Check if another image is also using this master
1698
            }
1699
        }
1700
        if ($master && !$usedmaster && $register{$master}) {
1701
            $register{$master}->{'status'} = 'unused';
1702
            $main::syslogit->($user, "info", "Freeing master $master");
1703
        }
1704
        if ($type eq "qcow2") {
1705
            my @regkeys = (tied %register)->select_where("image2 = '$path'");
1706
            foreach my $k (@regkeys) {
1707
                my $val = $register{$k};
1708
                if ($val->{'image2'} eq $path) {
1709
                    $hasprimary = 1;
1710
                    $primary = $val->{'name'};
1711
                    last;
1712
                }
1713
            }
1714
        }
1715

    
1716
        if ($haschildren) {
1717
            $postmsg = "Cannot delete image. This image is used as master by: $child";
1718
            $postreply .= "Status=Error $postmsg\n";
1719
#        } elsif ($hasprimary) {
1720
#            $postmsg = "Cannot delete image. This image is used as secondary image by: $primary";
1721
#            $postreply .= "Status=Error $postmsg\n";
1722
        } else {
1723
            if ($mac && $path =~ /\/mnt\/stabile\/node\//) {
1724
                unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return "Status=Error Cannot connect to DB\n";};
1725
                $macip = $nodereg{$mac}->{'ip'};
1726
                my $wakenode = ($nodereg{$mac}->{'status'} eq 'asleep' || $nodereg{$mac}->{'status'} eq 'waking');
1727

    
1728
                if ($wakenode) {
1729
                    my $tasks = $nodereg{$mac}->{'tasks'};
1730
                    my $upath = URI::Escape::uri_escape($path);
1731
                    $tasks .= "REMOVE $upath $user\n";
1732
                    $nodereg{$mac}->{'tasks'} = $tasks;
1733
                    tied(%nodereg)->commit;
1734
                    $postmsg = "We are waking up the node your image $name is on - it will be removed shortly";
1735
                    if ($nodereg{$mac}->{'status'} eq 'asleep') {
1736
                        require "$Stabile::basedir/cgi/nodes.cgi";
1737
                        $Stabile::Nodes::console = 1;
1738
                        Stabile::Nodes::wake($mac);
1739
                    }
1740
                    $register{$path}->{'status'} = $uistatus;
1741
                } else {
1742
                    my $esc_path = $path;
1743
                    $esc_path =~ s/([ ])/\\$1/g;
1744
                    if ($preserve) {
1745
                        `$sshcmd $macip "/bin/mv $esc_path $esc_path.bak"`;
1746
                    } else {
1747
                        `$sshcmd $macip "/usr/bin/unlink $esc_path"`;
1748
                    }
1749
                    `$sshcmd $macip "/usr/bin/unlink $esc_path.meta"`;
1750
                    delete $register{$path};
1751
                }
1752
                untie %nodereg;
1753

    
1754
            } else {
1755
                if ($preserve) {
1756
                    `/bin/mv "$path" "$path.bak"`;
1757
                } else {
1758
                    unlink $path;
1759
                }
1760
                if (substr($path,-5) eq '.vmdk') {
1761
                    if ( -s (substr($path,0,-5) . "-flat.vmdk")) {
1762
                        my $flat = substr($path,0,-5) . "-flat.vmdk";
1763
                        if ($preserve) {
1764
                            `/bin/mv $flat "$flat.bak"`;
1765
                        } else {
1766
                            unlink($flat);
1767
                        }
1768
                    } elsif ( -e (substr($path,0,-5) . "-s001.vmdk")) {
1769
                        my $i = 1;
1770
                        my $rmpath = substr($path,0,-5);
1771
                        while (-e "$rmpath-s00$i.vmdk") {
1772
                            if ($preserve) {
1773
                                `/bin/mv "$rmpath-s00$i.vmdk" "$rmpath-s00$i.vmdk.bak"`;
1774
                            } else {
1775
                                unlink("$rmpath-s00$i.vmdk");
1776
                            }
1777
                            $i++;
1778
                        }
1779
                    }
1780
                }
1781
                unlink "$path.meta" if (-e "$path.meta");
1782
                delete $register{$path};
1783
            }
1784

    
1785
            my $subdir = "";
1786
            my($bname, $dirpath) = fileparse($path);
1787
            if ($dirpath =~ /.+\/$buser(\/.+)?\//) {
1788
                $subdir = $1;
1789
            }
1790
            my $bpath = "$backupdir/$user$subdir/$bname";
1791
            $bpath = $1 if ($bpath =~ /(.+)/);
1792
            # Remove backup of image if it exists
1793
            if (-d "$bpath") {
1794
                `/bin/rm -rf "$bpath"`;
1795
            }
1796

    
1797
#            $postmsg = "Deleted image $name ($path, $uuid, $mac)";
1798
#            $postreply =  "[]";
1799
#            $postreply .=  "Status=deleting OK $postmsg\n";
1800
            updateBilling("delete $path");
1801
            $main::syslogit->($user, "info", "$uistatus $type image: $name: $path");
1802
            if ($status eq 'downloading') {
1803
                my $daemon = Proc::Daemon->new(
1804
                    work_dir => '/usr/local/bin',
1805
                    exec_command => qq|pkill -f "$path"|
1806
                ) or do {$postreply .= "Status=ERROR $@\n";};
1807
                my $pid = $daemon->Init();
1808
            }
1809
            sleep 1;
1810
        }
1811
    } else {
1812
        $postmsg = "Cannot delete $type image with status: $status";
1813
        $postreply .= "Status=ERROR $postmsg\n";
1814
    }
1815
    return "[]";
1816
}
1817

    
1818
# Clone image $path to destination storage pool $istoragepool, possibly changing backup schedule $bschedule
1819
sub Clone {
1820
    my ($path, $action, $obj, $istoragepool, $imac, $name, $bschedule, $buildsystem, $managementlink, $appid, $wait, $vcpu, $mem) = @_;
1821
    if ($help) {
1822
        return <<END
1823
GET:image,name,storagepool,wait:
1824
Clones an image. In the case of cloning a master image, a child is produced.
1825
Only cloning to same storagepool is supported, with the exception of cloning to nodes (storagepool -1).
1826
If you want to perform the clone synchronously, set wait to 1;
1827
END
1828
    }
1829
    $postreply = "" if ($buildsystem);
1830
    return "Status=Error no valid user\n" unless ($user);
1831

    
1832
    unless ($register{$path} && ($register{$path}->{'user'} eq $user
1833
                || $register{$path}->{'user'} eq 'common'
1834
                || $register{$path}->{'user'} eq $billto
1835
                || $register{$path}->{'user'} eq $Stabile::Systems::billto
1836
                || $isadmin)) {
1837
        $postreply .= "Status=ERROR Cannot clone!\n";
1838
        return;
1839
    }
1840
    $istoragepool = $istoragepool || $obj->{storagepool};
1841
    $name = $name || $obj->{name};
1842
    $wait = $wait || $obj->{wait};
1843
    my $status = $register{$path}->{'status'};
1844
    my $type = $register{$path}->{'type'};
1845
    my $master = $register{$path}->{'master'};
1846
    my $notes = $register{$path}->{'notes'};
1847
    my $image2 = $register{$path}->{'image2'};
1848
    my $snap1 = $register{$path}->{'snap1'};
1849
    $managementlink = $register{$path}->{'managementlink'} unless ($managementlink);
1850
    $appid = $register{$path}->{'appid'} unless ($appid);
1851
    my $upgradelink = $register{$path}->{'upgradelink'} || '';
1852
    my $terminallink = $register{$path}->{'terminallink'} || '';
1853
    my $version = $register{$path}->{'version'} || '';
1854
    my $regmac = $register{$path}->{'mac'};
1855

    
1856
    my $virtualsize = $register{$path}->{'virtualsize'};
1857
    my $dindex = 0;
1858

    
1859
    my($bname, $dirpath, $suffix) = fileparse($path, (".vmdk", ".img", ".vhd", ".vhdx", ".qcow", ".qcow2", ".vdi", ".iso"));
1860
    $path =~ /(.+)\.$type/;
1861
    my $namepath = $1;
1862
    if ($namepath =~ /(.+)\.master/) {
1863
        $namepath = $1;
1864
    }
1865
    if ($namepath =~ /(.+)\.clone\d+/) {
1866
        $namepath = $1;
1867
    }
1868
    if ($namepath =~ /.+\/common\/(.+)/) { # Support one subdir
1869
        $namepath = $1;
1870
    } elsif ($namepath =~ /.+\/$user\/(.+)/) { # Support one subdir
1871
        $namepath = $1;
1872
    } elsif ($namepath =~ /.+\/(.+)/) { # Extract only the name
1873
        $namepath = $1;
1874
    }
1875

    
1876
    # Find unique path in DB across storage pools
1877
    my $upath;
1878
    my $npath = "/mnt/stabile/node/$user/$namepath"; # Also check for uniqueness on nodes
1879
    my $i = 1;
1880
    foreach my $spool (@spools) {
1881
        $upath = $spool->{'path'} . "/$user/$namepath";
1882
        while ($register{"$upath.clone$i.$type"} || $register{"$npath.clone$i.$type"}) {$i++;};
1883
    }
1884
    $upath = "$spools[$istoragepool]->{'path'}/$user/$namepath";
1885

    
1886
    my $iname = $register{$path}->{'name'};
1887
    $iname = "$name" if ($name); # Used when name supplied when building a system
1888
    $iname =~ /(.+)( \(master\))/;
1889
    $iname = $1 if $2;
1890
    $iname =~ /(.+)( \(clone\d*\))/;
1891
    $iname = $1 if $2;
1892
    $iname =~ /(.+)( \(child\d*\))/;
1893
    $iname = $1 if $2;
1894
    my $ippath = $path;
1895
    my $macip;
1896
    my $ug = new Data::UUID;
1897
    my $newuuid = $ug->create_str();
1898
    my $wakenode;
1899
    my $identity;
1900

    
1901
    # We only support cloning images to nodes - not the other way round
1902
    if ($imac && $regmac && $imac ne $regmac) {
1903
        $postreply .= "Status=ERROR Cloning from a node not supported\n";
1904
        return $postreply;
1905
    }
1906

    
1907
    if ($istoragepool==-1) {
1908
    # Find the ip address of target node
1909
        ($imac, $macip, $dindex, $wakenode, $identity) = locateNode($virtualsize, $imac, $vcpu, $mem);
1910
        if ($identity eq 'local_kvm') {
1911
            $postreply .= "Status=OK Cloning to local node with index: $dindex\n";
1912
            $istoragepool = 0; # cloning to local node
1913
            $upath = "$spools[$istoragepool]->{'path'}/$user/$namepath";
1914
        } elsif (!$macip) {
1915
            $postreply .= "Status=ERROR Unable to locate node with sufficient ressources\n";
1916
            $postmsg = "Unable to locate node with sufficient ressources!";
1917
            $main::updateUI->({tab=>"images", user=>$user, type=>"message", message=>$postmsg});
1918
            return $postreply;
1919
        } else {
1920
            $postreply .= "Status=OK Cloning to $macip with index: $dindex\n";
1921
            $ippath = "$macip:$path";
1922
            $upath = "/mnt/stabile/node/$user/$namepath";
1923
        }
1924
    }
1925
    my $ipath = "$upath.clone$i.$type";
1926

    
1927
    if ($bschedule eq 'daily7' || $bschedule eq 'daily14') {
1928
         $bschedule = "manually" if ($istoragepool!=-1 && (!$spools[$istoragepool]->{'rdiffenabled'} || !$spools[$istoragepool]->{'lvm'}));
1929
    } elsif ($bschedule ne 'manually') {
1930
        $bschedule = '';
1931
    }
1932

    
1933
# Find storage pool with space
1934
    my $foundstorage = 1;
1935
    if (overStorage($virtualsize, $istoragepool, $imac)) {
1936
        $foundstorage = 0;
1937
        foreach my $p (@spools) {
1938
            if (overStorage($virtualsize, $p->{'id'}, $imac)) {
1939
                ;
1940
            } else {
1941
                $istoragepool = $p->{'id'};
1942
                $foundstorage = 1;
1943
                last;
1944
            }
1945
        }
1946
    }
1947

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

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

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

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

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

    
2033
    } else {
2034
        $postreply .= "Status=ERROR Not a valid type: $type\n";
2035
    }
2036
    tied(%register)->commit;
2037
    $main::updateUI->({tab=>"images", user=>$user, type=>"update"});
2038
    return $postreply;
2039
}
2040

    
2041

    
2042
# Link master image to fuel
2043
sub Linkmaster {
2044
    my ($mpath, $action) = @_;
2045
    if ($help) {
2046
        return <<END
2047
GET:image:
2048
Link master image to fuel
2049
END
2050
    }
2051
    my $res;
2052

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

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

    
2116
# Link master image to fuel
2117
sub unlinkMaster {
2118
    my $mpath = shift;
2119
    unless ($mpath =~ /^\//) { # We did not get an absolute path, look for it in users storagepools
2120
        foreach my $p (@spools) {
2121
            my $dir = $p->{'path'};
2122
            my $upath = "$dir/$user/fuel/$mpath";
2123
            if (-e $upath) {
2124
                $mpath = "/mnt/fuel/pool$p->{id}/$mpath";
2125
                last;
2126
            }
2127
        }
2128
    }
2129

    
2130
    $mpath =~ /\/pool(\d+)\/(.+)\.link\.master\.qcow2$/;
2131
    my $pool = $1;
2132
    my $namepath = $2;
2133
    if (!( $mpath =~ /\/pool(\d+)\/(.+)\.link\.master\.qcow2$/ ) ) {
2134
        $postreply = qq|{"status": "Error", "message": "You can only unlink linked master images ($mpath)"}|;
2135
    } else {
2136
        my $linkpath = $tenderpathslist[$pool] . "/$user/fuel/$namepath.link.master.qcow2";
2137
        if (-e $linkpath) {
2138
            `chmod 644 "$linkpath"`;
2139
            `rm "$linkpath"`;
2140
            $postreply = qq|{"status": "OK", "message": "Link removed", "path": "/mnt/fuel/pool$pool/$namepath.qcow2", "linkpath": "$linkpath"}|;
2141
        } else {
2142
            $postreply = qq|{"status": "Error", "message": "Link $linkpath does not exists."}|;
2143
        }
2144
    }
2145
}
2146

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

    
2188
sub do_move {
2189
    my ($image, $action, $obj) = @_;
2190
    if ($help) {
2191
        return <<END
2192
GET:image,user,storagepool,mac,precreate:
2193
Move image to a different storage pool or user
2194
END
2195
    }
2196
    return "Your account does not have the necessary privileges\n" if ($isreadonly);
2197
#    $postreply = qq/"$curimg || $image, $obj->{user} || $user, $obj->{storagepool}, $obj->{mac}, 0, $obj->{precreate}, $nodereg->{$obj->{mac}}->{name}"/;
2198
#    return $postreply;
2199
    my $res = Move($curimg || $image, $obj->{user} || $user, $obj->{storagepool}, $obj->{mac},0, $obj->{precreate});
2200
    return header() . $res;
2201
}
2202

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

    
2231
    my $newpath;
2232
    my $newdirpath;
2233
    my $oldpath = $path;
2234
    my $olddirpath = $path;
2235
    my $newuser = $reguser;
2236
    my $newstoragepool = $regstoragepool;
2237
    my $haschildren;
2238
    my $hasprimary;
2239
    my $child;
2240
    my $primary;
2241
    my $macip;
2242
    my $alreadyexists;
2243
    my $subdir;
2244
#    $subdir = $1 if ($path =~ /\/$reguser(\/.+)\//);
2245
    $subdir = $1 if ($path =~ /.+\/$reguser(\/.+)?\//);
2246
    my $restpath;
2247
    $restpath = $1 if ($path =~ /.+\/$reguser\/(.+)/);
2248

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

    
2290
        if ((($isadmin || $ismanager ) && $iuser eq 'common') # Check if user is allowed to access account
2291
                || ($isadmin && $userreg{$iuser})
2292
                || ($user eq $engineuser)
2293
                || ($userreg{$iuser}->{'billto'} eq $user)
2294
                || ($ahash{$iuser} && !($ahash{$iuser} =~ /r/))
2295
        ) {
2296
            if ($haschildren) {
2297
                $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"});
2298
                $postreply .= "Status=Error Cannot move image. This image is used as master by: $child\n";
2299
            } elsif ($hasprimary) {
2300
                $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"});
2301
                $postreply .= "Status=Error Cannot move image. This image is used as secondary image by: $primary\n";
2302
            } else {
2303
                if ($regstoragepool == -1) { # The image is located on a node
2304
                    my $uprivs = $userreg{$iuser}->{'privileges'};
2305
                    if ($uprivs =~ /[an]/) {
2306
                        unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
2307
                        $macip = $nodereg{$mac}->{'ip'};
2308
                        my $oldmacip = $nodereg{$regimg->{'mac'}}->{'ip'};
2309
                        untie %nodereg;
2310
                        $oldpath = "$oldmacip:/mnt/stabile/node/$reguser/$restpath";
2311
                        $newdirpath = "/mnt/stabile/node/$iuser/$restpath";
2312
                        $newpath = "$macip:$newdirpath";
2313
                        $newuser = $iuser;
2314
                        $newstoragepool = $istoragepool;
2315
                # Check if image already exists in target dir
2316
                        $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}'"`;
2317
                    } else {
2318
                        $postreply .= "Status=Error Target account $iuser cannot use node storage\n";
2319
                    }
2320
                } else {
2321
                    my $reguser = $userreg{$iuser};
2322
                    my $upools = $reguser->{'storagepools'} || $Stabile::config->get('STORAGE_POOLS_DEFAULTS') || "0";
2323
                    my @nspools = split(/, ?/, $upools);
2324
                    my %ispools = map {$_=>1} @nspools; # Build a hash with destination users storagepools
2325
                    if ($ispools{$regstoragepool}) { # Destination user has access to image's storagepool
2326
                        $newpath = "$spools[$regstoragepool]->{'path'}/$iuser/$restpath";
2327
                    } else {
2328
                        $newpath = "$spools[0]->{'path'}/$iuser/$restpath";
2329
                    }
2330
                    $newdirpath = $newpath;
2331
                    $newuser = $iuser;
2332
            # Check if image already exists in target dir
2333
                    $alreadyexists = -e $newpath;
2334
                }
2335
            }
2336
        } else {
2337
            $postreply .= "Status=Error Cannot move image to account $iuser $ahash{$iuser} - not allowed\n";
2338
        }
2339
# Moving an image to a different storage pool
2340
    } elsif ($istoragepool ne '--' &&  $regstoragepool ne '--' && $istoragepool ne $regstoragepool
2341
            && ($status eq "unused" || $status eq "used" || $status eq "paused" || ($status eq "active" && $precreate))) {
2342

    
2343
        my $dindex;
2344
        my $wakenode;
2345
        if ($istoragepool == -1 && $regstoragepool != -1) {
2346
            ($mac, $macip, $dindex, $wakenode) = locateNode($virtualsize, $mac);
2347
        }
2348

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

    
2351
        if ($haschildren) {
2352
            $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$register{$path}->{'uuid'}, status=>$status, message=>"ERROR Unable to move $name (has children)"});
2353
            $postreply .= "Status=ERROR Unable to move $name (has children)\n";
2354
        } elsif ($hasprimary) {
2355
            $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"});
2356
            $postreply .= "Status=Error Cannot move image. This image is used as secondary image by: $primary\n";
2357
        } elsif ($wakenode) {
2358
            $postreply .= "Status=ERROR All available nodes are asleep moving $name, waking $mac, please try again later\n";
2359
            $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"});
2360
            require "$Stabile::basedir/cgi/nodes.cgi";
2361
            $Stabile::Nodes::console = 1;
2362
            Stabile::Nodes::wake($mac);
2363
        } elsif (overStorage($virtualsize, $istoragepool+0, $mac)) {
2364
            $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"});
2365
            $postreply .= "Status=ERROR Out of storage in destination pool $istoragepool $mac moving: $name\n";
2366
        } elsif (overQuotas($virtualsize, ($istoragepool==-1))) {
2367
            $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$register{$path}->{'uuid'}, status=>$status, message=>"ERROR Over quota (". overQuotas($virtualsize, ($istoragepool==-1)) . ") moving: $name"});
2368
            $postreply .= "Status=ERROR Over quota (". overQuotas($virtualsize, ($istoragepool==-1)) . ") moving: $name\n";
2369
        } elsif ($istoragepool == -1 && $regstoragepool != -1 && $path =~ /\.master\.$type/) {
2370
            $postreply .= "Status=ERROR Unable to move $name (master images are not supported on node storage)\n";
2371
            $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)"});
2372
    # Moving to node
2373
        } elsif ($istoragepool == -1 && $regstoragepool != -1) {
2374
            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
2375
                if ($macip) {
2376
                    $newdirpath = "/mnt/stabile/node/$reguser/$restpath";
2377
                    $newpath = "$macip:$newdirpath";
2378
                    $newstoragepool = $istoragepool;
2379
            # Check if image already exists in target dir
2380
                    $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}'"`;
2381

    
2382
                } else {
2383
                    $postreply .= "Status=ERROR Unable to move $name (not enough space)\n";
2384
                }
2385
            } else {
2386
                $postreply .= "Status=ERROR Unable to move $name (no node privileges)\n";
2387
            }
2388
    # Moving from node
2389
        } elsif ($regstoragepool == -1 && $istoragepool != -1 && $spools[$istoragepool]) {
2390
            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
2391
                unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
2392
                $macip = $nodereg{$mac}->{'ip'}; # $mac is set to existing image's mac since no destination mac was specified
2393
                untie %nodereg;
2394
                $newpath = "$spools[$istoragepool]->{'path'}/$reguser/$restpath";
2395
                $newdirpath = $newpath;
2396
                $oldpath = "$macip:/mnt/stabile/node/$reguser/$restpath";
2397
                $newstoragepool = $istoragepool;
2398
        # Check if image already exists in target dir
2399
                $alreadyexists = -e $newpath;
2400
            } else {
2401
                $postreply .= "Status=ERROR Unable to move $name - you must specify a node\n";
2402
            }
2403
        } elsif ($spools[$istoragepool]) { # User has access to storagepool
2404
            $newpath = "$spools[$istoragepool]->{'path'}/$reguser/$restpath";
2405
            $newdirpath = $newpath;
2406
            $newstoragepool = $istoragepool;
2407
            $alreadyexists = -e $newpath && -s $newpath;
2408
        } else {
2409
            $postreply .= "Status=ERROR Cannot move image. This image is used as master by: $child\n";
2410
        }
2411
    } else {
2412
        $postreply .= "Status=ERROR Unable to move $path (bad status or pool $status, $reguser, $iuser, $regstoragepool, $istoragepool)\n" unless ($istoragepool eq '--' || $regstoragepool eq '--');
2413
    }
2414
    untie %userreg;
2415

    
2416
    if ($alreadyexists && !$force) {
2417
        $postreply = "Status=ERROR Image \"$name\" already exists in destination\n";
2418
        return $postreply;
2419
    }
2420
# Request actual move operation
2421
    elsif ($newpath) {
2422
        if ($newstoragepool == -1) {
2423
            my $diruser = $iuser || $reguser;
2424
            `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
2425
        }
2426
        if ($subdir && $istoragepool != -1) {
2427
            my $fulldir = "$spools[$istoragepool]->{'path'}/$reguser$subdir";
2428
            `/bin/mkdir -p "$fulldir"` unless -d $fulldir;
2429
        }
2430
        $uistatus = "moving";
2431
        if ($precreate) {
2432
            $uistatus = "stormoving";
2433
        }
2434

    
2435
        my $ug = new Data::UUID;
2436
        my $tempuuid = $ug->create_str();
2437

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

    
2442

    
2443
        if ($bschedule eq 'daily7' || $bschedule eq 'daily14') {
2444
             $bschedule = "manually" if (!$spools[$regstoragepool]->{'rdiffenabled'} || !$spools[$regstoragepool]->{'lvm'});
2445
        } elsif ($bschedule ne 'manually') {
2446
            $bschedule = '';
2447
        }
2448

    
2449
        $register{$path}->{'uuid'} = $tempuuid; # Use new temp uuid for old image
2450
        $register{$newdirpath}->{'storagepool'} = $newstoragepool;
2451
        if ($newstoragepool == -1) {
2452
            $register{$newdirpath}->{'mac'} = $mac;
2453
        } else {
2454
            $register{$newdirpath}->{'mac'} = '';
2455
        }
2456
        $register{$newdirpath}->{'user'} = $newuser;
2457
        tied(%register)->commit;
2458
        my $domuuid = $register{$path}->{'domains'};
2459
        if ($status eq "used" || $status eq "paused" || $status eq "moving" || $status eq "stormoving" || $status eq "active") {
2460
            my $dom = $domreg{$domuuid};
2461
            if ($dom->{'image'} eq $olddirpath) {
2462
                $dom->{'image'} = $newdirpath;
2463
            } elsif ($dom->{'image2'} eq $olddirpath) {
2464
                $dom->{'image2'} = $newdirpath;
2465
            } elsif ($dom->{'image3'} eq $olddirpath) {
2466
                $dom->{'image3'} = $newdirpath;
2467
            } elsif ($dom->{'image4'} eq $olddirpath) {
2468
                $dom->{'image4'} = $newdirpath;
2469
            }
2470
            # 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.
2471
            $dom->{'mac'} = $mac if ($newstoragepool == -1 && !$precreate);
2472
            if ($dom->{'system'} && $dom->{'system'} ne '--') {
2473
                unless (tie(%sysreg,'Tie::DBI', Hash::Merge::merge({table=>'systems'}, $Stabile::dbopts)) ) {$res .= qq|{"status": "Error": "message": "Unable to access systems register"}|; return $res;};
2474
                my $sys = $sysreg{$dom->{'system'}};
2475
                $sys->{'image'} = $newdirpath if ($sys->{'image'} eq $olddirpath);
2476
                untie %sysreg;
2477
            }
2478
        }
2479
        my $cmd = qq|/usr/local/bin/steamExec $user $uistatus $status "$oldpath" "$newpath"|;
2480
        `$cmd`;
2481
        $main::syslogit->($user, "info", "$uistatus $type image $name ($oldpath -> $newpath) ($regstoragepool -> $istoragepool)");
2482
        return "$newdirpath\n";
2483
    } else {
2484
        return $postreply;
2485
    }
2486

    
2487
}
2488

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

    
2533
sub do_getimagestatus {
2534
    my ($image, $action) = @_;
2535
    if ($help) {
2536
        return <<END
2537
GET:image:
2538
Check if image already exists. Pass image name including suffix.
2539
END
2540
    }
2541
    my $res;
2542
    $imagename = $params{'name'} || $image;
2543
    if ($register{"/mnt/stabile/node/$user/$imagename"}) {
2544
        $res .= q|Status=OK Image /mnt/stabile/node/$imagename found with status | . $register{"/mnt/stabile/node/$user/$imagename"}->{status}. "\n";
2545
    }
2546
    foreach my $spool (@spools) {
2547
        my $ipath = $spool->{'path'} . "/$user/$imagename";
2548
        if ($register{$ipath}) {
2549
            $res .= "Status=OK Image $ipath found with status $register{$ipath}->{'status'}\n";
2550
        } elsif (-f "$ipath" && -s "$ipath") {
2551
            $res .= "Status=OK Image $ipath found on disk, please wait for it to be updated in DB\n";
2552
        }
2553
    }
2554
    $res .= "Status=ERROR Image $imagename not found\n" unless ($res);
2555
    return $res;;
2556
}
2557

    
2558
# Check if image already exists.
2559
# Pass image name including suffix.
2560
sub imageExists {
2561
    my $imagename = shift;
2562
    foreach my $spool (@spools) {
2563
        my $ipath = $spool->{'path'} . "/$user/$imagename";
2564
        if ($register{$ipath}) {
2565
            return $register{$ipath}->{'status'} || 1;
2566
        } elsif (-e "$ipath") {
2567
            return 1
2568
        }
2569
    }
2570
    return '';
2571
}
2572

    
2573
# Pass image name including suffix.
2574
# Returns incremented name of an image which does not already exist.
2575
sub getValidName {
2576
    my $imagename = shift;
2577
    my $name = $imagename;
2578
    my $type;
2579
    if ($imagename =~ /(.+)\.(.+)/) {
2580
        $name = $1;
2581
        $type = $2;
2582
    }
2583
    if (imageExists($imagename)) {
2584
        my $i = 1;
2585
        while (imageExists("$name.$i.$type")) {$i++;};
2586
        $imagename = "$name.$i.$type";
2587
    }
2588
    return $imagename;
2589
}
2590

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

    
2618
# Print list of images
2619
# Showing a single image is also handled by specifying uuid or path in $curuuid or $curimg
2620
# When showing a single image a single action may be performed on image
2621
sub do_list {
2622
    my ($img, $action, $obj) = @_;
2623
    if ($help) {
2624
        return <<END
2625
GET:image,uuid:
2626
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.
2627
The returned list may be filtered by specifying storagepool, type, name, path or uuid, like e.g.:
2628

    
2629
<a href="/stabile/images/type:user" target="_blank">/stabile/images/type:user</a>
2630
<a href="/stabile/images/name:test* AND storagepool:shared" target="_blank">/stabile/images/name:test* AND storagepool:shared</a>
2631
<a href="/stabile/images/storagepool:shared AND path:test*" target="_blank">/stabile/images/storagepool:shared AND path:test*</a>
2632
<a href="/stabile/images/name:* AND storagepool:all AND type:usercdroms" target="_blank">/stabile/images/name:* AND storagepool:all AND type:usercdroms</a>
2633
<a href="/stabile/images/[uuid]" target="_blank">/stabile/images/[uuid]</a>
2634

    
2635
storagepool may be either of: all, node, shared
2636
type may be either of: user, usermasters, commonmasters, usercdroms
2637

    
2638
May also be called as tablelist or tablelistall, for use by stash.
2639

    
2640
END
2641
    }
2642
    my $res;
2643
    my $filter;
2644
    my $storagepoolfilter;
2645
    my $typefilter;
2646
    my $pathfilter;
2647
    my $uuidfilter;
2648
    $curimg = $img if ($img);
2649
    my $regimg = $register{$curimg};
2650
#    if ($curimg && ($isadmin || $regimg->{'user'} eq $user || $regimg->{'user'} eq 'common') ) {
2651
    if ($curimg) { # security is enforced below, we hope...
2652
        $pathfilter = $curimg;
2653
    } elsif ($uripath =~ /images(\.cgi)?\/(\?|)(name|storagepool|type|path)/) {
2654
        $filter = $3 if ($uripath =~ /images(\.cgi)?\/.*name(:|=)(.+)/);
2655
        $filter = $1 if ($filter =~ /(.*) AND storagepool/);
2656
        $filter = $1 if ($filter =~ /(.*) AND type/);
2657
        $filter = $1 if ($filter =~ /(.*)\*$/);
2658
        $storagepoolfilter = $2 if ($uripath =~ /images(\.cgi)?\/.*storagepool:(\w+)/);
2659
        $typefilter = $2 if ($uripath =~ /images(\.cgi)?\/.*type:(\w+)/);
2660
        $typefilter = $2 if ($uripath =~ /images(\.cgi)?\/.*type=(\w+)/);
2661
        $pathfilter = $2 if ($uripath =~ /images(\.cgi)?\/.*path:(.+)/);
2662
        $pathfilter = $2 if ($uripath =~ /images(\.cgi)?\/.*path=(.+)/);
2663
    } elsif ($uripath =~ /images(\.cgi)?\/(\w{8}-\w{4}-\w{4}-\w{4}-\w{12})\/?(\w*)/) {
2664
        $uuidfilter = $2;
2665
        $curaction = lc $3;
2666
    }
2667
    $uuidfilter = $options{u} unless $uuidfilter;
2668

    
2669
    if ($uuidfilter && $curaction) {
2670
        if ($imagereg{$uuidfilter}) {
2671
            $curuuid = $uuidfilter;
2672
            my $obj = getObj(%params);
2673
            # Now perform the requested action
2674
            my $objfunc = "obj_$curaction";
2675
            if (defined &$objfunc) { # If a function named objfunc exists, call it
2676
                $res = $objfunc->($obj);
2677
                chomp $postreply;
2678
                unless ($res) {
2679
                    $res .= qq|{"status": "OK", "message": "$postreply"}|;
2680
                    $res = join(", ", split("\n", $res));
2681
                }
2682
                unless ($curaction eq 'download') {
2683
                    $res = header('application/json; charset=UTF8') . $res unless ($console);
2684
                }
2685
            } else {
2686
                $res .= header('application/json') unless $console;
2687
                $res .= qq|{"status": "Error", "message": "Unknown image action: $curaction"}|;
2688
            }
2689
        } else {
2690
            $res .= header('application/json') unless $console;
2691
            $res .= qq|{"status": "Error", "message": "Unknown image $uuidfilter"}|;
2692
        }
2693
        return $res;
2694
    }
2695

    
2696

    
2697
    my %userregister; # User specific register
2698

    
2699
    $res .= header('application/json; charset=UTF8') unless $console;
2700
    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;};
2701

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

    
2746
                        $userregister{$val{'master'}}->{'domains'} .= ", " if ($userregister{$val{'master'}}->{'domains'});
2747
                        $userregister{$val{'master'}}->{'domains'} .= $val{'domains'};
2748
                    }
2749
                }
2750
                my $status = $valref->{'status'};
2751
                if ($rdiffenabled && ($userrdiffenabled || index($privileges,"a")!=-1) &&
2752
                    ( ($spools[$valref->{'storagepool'}]->{'rdiffenabled'} &&
2753
                        ($spools[$valref->{'storagepool'}]->{'lvm'} || $status eq 'unused' || $status eq 'used' || $status eq 'paused') )
2754
                        || $valref->{'storagepool'}==-1 )
2755
                ) {
2756
                    $val{'backup'} = "" ;
2757
                } else {
2758
                    $val{'backup'} = "disabled" ;
2759
                }
2760
                $val{'status'} = 'backingup' if ($status =~ /backingup/);
2761
                $userregister{$path} = \%val unless ($userregister{$path});
2762
            }
2763
        }
2764
    }
2765
    untie(%nodereg);
2766

    
2767
    my @uservalues;
2768
    if ($filter || $storagepoolfilter || $typefilter || $pathfilter || $uuidfilter) { # List filtered images
2769
        foreach $uvalref (values %userregister) {
2770
            my $fmatch;
2771
            my $smatch;
2772
            my $tmatch;
2773
            my $pmatch;
2774
            my $umatch;
2775
            $fmatch = 1 if (!$filter || $uvalref->{'name'}=~/$filter/i);
2776
            $smatch = 1 if (!$storagepoolfilter || $storagepoolfilter eq 'all'
2777
                || ($storagepoolfilter eq 'node' && $uvalref->{'storagepool'}==-1)
2778
                || ($storagepoolfilter eq 'shared' && $uvalref->{'storagepool'}>=0)
2779
            );
2780
            $tmatch = 1 if (!$typefilter || $typefilter eq 'all'
2781
                || ($typefilter eq 'user' && $uvalref->{'user'} eq $user
2782
                # && $uvalref->{'type'} ne 'iso'
2783
                # && $uvalref->{'path'} !~ /\.master\.qcow2$/
2784
                    )
2785
                || ($typefilter eq 'usermasters' && $uvalref->{'user'} eq $user && $uvalref->{'path'} =~ /\.master\.qcow2$/)
2786
                || ($typefilter eq 'usercdroms' && $uvalref->{'user'} eq $user && $uvalref->{'type'} eq 'iso')
2787
                || ($typefilter eq 'commonmasters' && $uvalref->{'user'} ne $user && $uvalref->{'path'} =~ /\.master\.qcow2$/)
2788
                || ($typefilter eq 'commoncdroms' && $uvalref->{'user'} ne $user && $uvalref->{'type'} eq 'iso')
2789
            );
2790
            $pmatch = 1 if ($pathfilter && $uvalref->{'path'}=~/$pathfilter/i);
2791
            $umatch = 1 if ($uvalref->{'uuid'} eq $uuidfilter);
2792
            if ((!$pathfilter &&!$uuidfilter && $fmatch && $smatch && $tmatch) || $pmatch) {
2793
                push @uservalues,$uvalref if ($uvalref->{'uuid'});
2794
            } elsif ($umatch && $uvalref->{'uuid'}) {
2795
                push @uservalues,$uvalref;
2796
                last;
2797
            }
2798
        }
2799
    } else {
2800
        @uservalues = values %userregister;
2801
    }
2802

    
2803
    # Sort @uservalues
2804
    @uservalues = (sort {$a->{'name'} cmp $b->{'name'}} @uservalues); # Always sort by name first
2805
    my $sort = 'status';
2806
    $sort = $2 if ($uripath =~ /sort\((\+|\-)(\S+)\)/);
2807
    my $reverse;
2808
    $reverse = 1 if ($1 eq '-');
2809
    if ($reverse) { # sort reverse
2810
        if ($sort =~ /realsize|virtualsize|size/) {
2811
            @uservalues = (sort {$b->{$sort} <=> $a->{$sort}} @uservalues); # Sort as number
2812
        } else {
2813
            @uservalues = (sort {$b->{$sort} cmp $a->{$sort}} @uservalues); # Sort as string
2814
        }
2815
    } else {
2816
        if ($sort =~ /realsize|virtualsize|size/) {
2817
            @uservalues = (sort {$a->{$sort} <=> $b->{$sort}} @uservalues); # Sort as number
2818
        } else {
2819
            @uservalues = (sort {$a->{$sort} cmp $b->{$sort}} @uservalues); # Sort as string
2820
        }
2821
    }
2822

    
2823
    if ($uuidfilter || $curimg) {
2824
        if (scalar @uservalues > 1) { # prioritize user's own images
2825
            foreach my $val (@uservalues) {
2826
                if ($val->{'user'} eq 'common') {
2827
                    next;
2828
                } else {
2829
                    $json_text = to_json($val, {pretty => 1});
2830
                }
2831
            }
2832
        } else {
2833
            $json_text = to_json($uservalues[0], {pretty => 1}) if (@uservalues);
2834
        }
2835
    } else {
2836
    #    $json_text = JSON->new->canonical(1)->pretty(1)->encode(\@uservalues) if (@uservalues);
2837
        $json_text = to_json(\@uservalues, {pretty => 1}) if (@uservalues);
2838
    }
2839
    $json_text = "{}" unless $json_text;
2840
    $json_text =~ s/""/"--"/g;
2841
    $json_text =~ s/null/"--"/g;
2842
    $json_text =~ s/"notes" {0,1}: {0,1}"--"/"notes":""/g;
2843
    $json_text =~ s/"installable" {0,1}: {0,1}"(true|false)"/"installable":$1/g;
2844

    
2845
    if ($action eq 'tablelist' || $action eq 'tablelistall') {
2846
        my $t2 = Text::SimpleTable->new(36,26,5,20,14,10,7);
2847
        $t2->row('uuid', 'name', 'type', 'domainnames', 'virtualsize', 'user', 'status');
2848
        $t2->hr;
2849
        my $pattern = $options{m};
2850
        foreach $rowref (@uservalues){
2851
            next unless ($action eq 'tablelistall' || $rowref->{'user'} eq $user);
2852
            if ($pattern) {
2853
                my $rowtext = $rowref->{'uuid'} . " " . $rowref->{'name'} . " " . $rowref->{'type'} . " " . $rowref->{'domainnames'}
2854
                    . " " .  $rowref->{'virtualsize'} . " " . $rowref->{'user'} . " " . $rowref->{'status'};
2855
                $rowtext .= " " . $rowref->{'mac'} if ($isadmin);
2856
                next unless ($rowtext =~ /$pattern/i);
2857
            }
2858
            $t2->row($rowref->{'uuid'}, $rowref->{'name'}, $rowref->{'type'}, $rowref->{'domainnames'}||'--',
2859
                $rowref->{'virtualsize'}, $rowref->{'user'}, $rowref->{'status'});
2860
        }
2861
        $res .= $t2->draw;
2862
    } elsif ($console) {
2863
        $res .= Dumper(\@uservalues);
2864
    } else {
2865
        $res .= $json_text;
2866
    }
2867
    return $res;
2868
}
2869

    
2870
# Internal action for looking up a uuid or part of a uuid and returning the complete uuid
2871
sub do_uuidlookup {
2872
    my ($img, $action) = @_;
2873
    if ($help) {
2874
        return <<END
2875
GET:image,path:
2876
END
2877
    }
2878
    my $res;
2879
    $res .= header('text/plain') unless $console;
2880
    my $u = $options{u};
2881
    $u = $curuuid unless ($u || $u eq '0');
2882
    my $ruuid;
2883
    if ($u || $u eq '0') {
2884
        foreach my $uuid (keys %register) {
2885
            if (($register{$uuid}->{'user'} eq $user || $register{$uuid}->{'user'} eq 'common' || $fulllist)
2886
                && ($register{$uuid}->{'uuid'} =~ /^$u/ || $register{$uuid}->{'name'} =~ /^$u/)) {
2887
                $ruuid = $register{$uuid}->{'uuid'};
2888
                last;
2889
            }
2890
        }
2891
        if (!$ruuid && $isadmin) { # If no match and user is admin, do comprehensive lookup
2892
            foreach $uuid (keys %register) {
2893
                if ($register{$uuid}->{'uuid'} =~ /^$u/ || $register{$uuid}->{'name'} =~ /^$u/) {
2894
                    $ruuid = $register{$uuid}->{'uuid'};
2895
                    last;
2896
                }
2897
            }
2898
        }
2899
    }
2900
    $res .= "$ruuid\n" if ($ruuid);
2901
    return $res;
2902
}
2903

    
2904
# Internal action for showing a single image
2905
sub do_uuidshow {
2906
    my ($img, $action) = @_;
2907
    if ($help) {
2908
        return <<END
2909
GET:image,path:
2910
END
2911
    }
2912
    my $res;
2913
    $res .= header('text/plain') unless $console;
2914
    my $u = $options{u};
2915
    $u = $curuuid unless ($u || $u eq '0');
2916
    if ($u || $u eq '0') {
2917
        foreach my $uuid (keys %register) {
2918
            if (($register{$uuid}->{'user'} eq $user || $register{$uuid}->{'user'} eq 'common' || index($privileges,"a")!=-1)
2919
                && $register{$uuid}->{'uuid'} =~ /^$u/) {
2920
                my %hash = %{$register{$uuid}};
2921
                delete $hash{'action'};
2922
                my $dump = Dumper(\%hash);
2923
                $dump =~ s/undef/"--"/g;
2924
                $res .= $dump;
2925
                last;
2926
            }
2927
        }
2928
    }
2929
    return $res;
2930
}
2931

    
2932
sub do_updatebilling {
2933
    my ($img, $action) = @_;
2934
    if ($help) {
2935
        return <<END
2936
GET:image,path:
2937
END
2938
    }
2939
    my $res;
2940
    $res .= header('text/plain') unless ($console);
2941
    updateBilling($params{"event"});
2942
    $res .= "Status=OK Updated billing for $user\n";
2943
    return $res;
2944
}
2945

    
2946
# If used with the -f switch ($fulllist) from console, all users images are updated in the db
2947
# If used with the -p switch ($fullupdate), also updates status information (ressource intensive - runs through all domains)
2948
sub dont_updateregister {
2949
    my ($img, $action) = @_;
2950
    my $res;
2951
    if ($help) {
2952
        return <<END
2953
GET:image,path:
2954
END
2955
    }
2956
    return "Status=ERROR You must be an admin to do this!\n" unless ($isadmin);
2957
    $fullupdate = 1 if ((!$fullupdate && $params{'fullupdate'}) || $action eq 'fullupdateregister');
2958
    my $force = $params{'force'};
2959
    Updateregister($force);
2960
    $res .= "Status=OK Updated image register for " . join(', ', @users) . "\n";
2961
}
2962

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

    
3056
sub do_upload {
3057
    my ($img, $action) = @_;
3058
    if ($help) {
3059
        return <<END
3060
POST:image,path:
3061
END
3062
    }
3063
    my $res;
3064
    $res .= header("text/html") unless ($console);
3065

    
3066
    my $uname = $params{'name'};
3067

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

    
3070
    $name = $1 if ($name =~ /^\.+(.*)/); # Don't allow hidden files
3071
    #        my $f = lc $name;
3072
    my $f = $name;
3073
    $f = $spools[0]->{'path'} . "/$user/$f$suffix";
3074

    
3075
    my $chunk = int($params{'chunk'});
3076
    my $chunks = int($params{'chunks'});
3077

    
3078
    if ($chunk == 0 && -e $f) {
3079
        $res .= qq|Error: File $f already exists $name|;
3080
    } else {
3081
        open (FILE, ">>$f");
3082

    
3083
        if ($params{'file'}) {
3084
            my $uh = $Stabile::q->upload("file");
3085
            while ( <$uh> ) {
3086
                print FILE;
3087
            }
3088
            close FILE;
3089

    
3090
            if ($chunk == 0) {
3091
                `/usr/local/bin/steamExec updateimagestatus "$f" uploading`;
3092
            }
3093
            if ($chunk >= ($chunks - 1) ) { # Done
3094
                unlink("$f.meta");
3095
                `/usr/local/bin/steamExec updateimagestatus "$f" unused`;
3096
            } else {
3097
                my $upload_meta_data = "status=uploading&chunk=$chunk&chunks=$chunks";
3098
                `echo "$upload_meta_data" > "$f.meta"`;
3099
            }
3100
            $res .= qq|OK: Chunk $chunk uploaded of $name|;
3101
        } else {
3102
            $res .= qq|OK: No file $name.|;
3103
        }
3104
    }
3105
    return $res;
3106
}
3107

    
3108
# .htaccess files are created hourly, giving the image user access
3109
# when download is clicked by another user (in @users, so with permission), this user is also given access until .htaccess is rewritten
3110
sub Download {
3111
    my ($f, $action, $argref) = @_;
3112
    #    my ($name, $managementlink, $upgradelink, $terminallink, $version) = @{$argref};
3113
    if ($help) {
3114
        return <<END
3115
GET:image,console:
3116
Returns http redirection with URL to download image
3117
END
3118
    }
3119
    $baseurl = $argref->{baseurl} || $baseurl;
3120
    my %uargs = %{$argref};
3121
    $f = $uargs{'image'} unless ($f);
3122
    $baseurl = $uargs{'baseurl'} || $baseurl;
3123
    $console = $console || $uargs{'console'};
3124
    my $res;
3125
    my $uf =  URI::Escape::uri_unescape($f);
3126
    if (! $f) {
3127
        $res .= header('text/html', '500 Internal Server Error') unless ($console);
3128
        $res .= "Status=ERROR You must specify an image.\n";
3129
    }
3130
    my $txt = <<EOT
3131
order deny,allow
3132
AuthName "Download"
3133
AuthType None
3134
TKTAuthLoginURL $baseurl/login/
3135
TKTAuthIgnoreIP on
3136
deny from all
3137
Satisfy any
3138
require user $user
3139
require user $tktuser
3140
Options -Indexes
3141
EOT
3142
    ;
3143
    my $fid;
3144
    my $fpath;
3145
    foreach my $p (@spools) {
3146
        foreach my $suser (@users) {
3147
            my $dir = $p->{'path'};
3148
            my $id = $p->{'id'};
3149
            if (-d "$dir/$suser" && $uf =~ /\/$suser\//) {
3150
                if ($uf =~ /$dir\/(.+)\/(.+)/) {
3151
                    my $filename = $2;
3152
                    utf8::encode($filename);
3153
                    utf8::decode($filename);
3154
                    $fpath = "$1/" . URI::Escape::uri_escape($filename);
3155
                    #$fpath = "$1/" . $filename;
3156
                    `chmod o+rw "$uf"`;
3157
                    `/bin/echo "$txt" > "$dir/$suser/.htaccess"`;
3158
                    `chmod 644 "$dir/$suser/.htaccess"`;
3159
                    `/bin/mkdir "$Stabile::basedir/download"` unless (-e "$Stabile::basedir/download");
3160
                    `/bin/ln -s "$dir" "$Stabile::basedir/download/$id"` unless (-e "$Stabile::basedir/download/$id");
3161
                    $fid = $id;
3162
                    last;
3163
                }
3164
            }
3165
        }
3166
    }
3167
    if (($fid || $fid eq '0') && $fpath && -e "$f") {
3168
        my $fileurl = "$baseurl/download/$fid/$fpath";
3169
        if ($console) {
3170
            $res .= header(). $fileurl;
3171
        } else {
3172
            $res .= "Status: 302 Moved\nLocation: $fileurl\n\n";
3173
            $res .= "$fileurl\n";
3174
        }
3175
    } else {
3176
        $res .= header('text/html', '500 Internal Server Error') unless ($console);
3177
        $res .= "Status=ERROR File not found $f, $fid, $fpath, $uargs{image}\n";
3178
    }
3179
    return $res;
3180
}
3181

    
3182

    
3183
sub Liststoragedevices {
3184
    my ($image, $action, $obj) = @_;
3185
    if ($help) {
3186
        return <<END
3187
GET::
3188
Returns available physical disks and partitions.
3189
Partitions currently used for holding backup and primary images directories are marked as such.
3190
May also be called as 'getimagesdevice', 'getbackupdevice', 'listimagesdevices' or 'listbackupdevices'.
3191
END
3192
    }
3193
    unless ($isadmin || ($user eq $engineuser)) {
3194
        return '' if ($action eq 'getimagesdevice' || $action eq 'getbackupdevice');
3195
        return qq|[]|;
3196
    }
3197
    my %devs;
3198
    # Check if we have unmounted ZFS file systems
3199
#    if (`grep "stabile-images" /etc/stabile/config.cfg` && !(`df` =~ /stabile-images/)) {
3200
    if (!(`df` =~ /stabile-images/)) {
3201
        `zpool import stabile-images 2>/dev/null`;
3202
        `zfs mount stabile-images 2>/dev/null`;
3203
        `zfs mount stabile-images/images 2>/dev/null`;
3204
    }
3205
    if (!(`df` =~ /stabile-backup/)) {
3206
        `zpool import stabile-backup 2>/dev/null`;
3207
        `zfs mount stabile-backup 2>/dev/null`;
3208
        `zfs mount stabile-backup/images 2>/dev/null`;
3209
        `zfs mount stabile-backup/backup 2>/dev/null`;
3210
    }
3211
    # Add active and mounted filesystems
3212
    my %filesystems;
3213
    $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 ]'/;
3214
    my $json = `$cmd`;
3215
    my $jobj = JSON::from_json($json);
3216
    my $rootdev;
3217
    my $backupdev;
3218
    my $imagesdev;
3219
    foreach my $fs (sort {$a->{'Filesystem'} cmp $b->{'Filesystem'}} @{$jobj}) {
3220
        # 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
3221
        if ($fs->{Filesystem} =~ /\/dev\/(.+)/) {
3222
            next if ($fs->{Type} eq 'squashfs');
3223
            next if ($fs->{Filesystem} =~ /\/dev\/loop/);
3224
            my $name = $1;
3225
            if ($name =~ /mapper\/(\w+-)(.+)/) {
3226
                $name = "$1$2";
3227
            }
3228
            $fs->{Name} = $name;
3229
            delete $fs->{on};
3230
            my $mp = $fs->{Mounted};
3231
            if ($fs->{Mounted} eq '/') {
3232
                $rootdev = $name;
3233
            } else {
3234
                if ($backupdir =~ /^$fs->{Mounted}/) {
3235
                    next if ($action eq 'listimagesdevices'); # Current backup dev is not available as images dev
3236
                    $fs->{isbackupdev} = 1;
3237
                    $backupdev = $name;
3238
                    return $name if ($action eq 'getbackupdevice');
3239
                }
3240
                if ($tenderpathslist[0] =~ /^$fs->{Mounted}/) {
3241
                    next if ($action eq 'listbackupdevices'); # Current images dev is not available as backup dev
3242
                    $fs->{isimagesdev} = 1;
3243
                    $imagesdev = $name;
3244
                    return $name if ($action eq 'getimagesdevice');
3245
                }
3246
            }
3247
            $fs->{dev} = $name;
3248
            $fs->{nametype} = "$name ($fs->{Type} - " .  ($mp?$mp:"not mounted") . " $fs->{Size})";
3249
            $filesystems{$name} = $fs;
3250
        } elsif ( $fs->{Type} eq 'zfs') {
3251
            my $name = $fs->{Filesystem};
3252
            # only include zfs pools but look for use as backup and images, exclude shapshots
3253
            if ($name =~ /(.+)\/(.+)/
3254
                && !($name =~ /SNAPSHOT/)
3255
                && !($name =~ /stabile-backup\/images/)
3256
                && !($name =~ /stabile-backup\/node/)
3257
            ) {
3258
                $name = $1;
3259
                if ($fs->{Mounted} eq $backupdir) {
3260
                    if ($action eq 'listimagesdevices') {
3261
                        delete $filesystems{$name}; # not available for images - used for backup
3262
                    } else {
3263
                        $filesystems{$name}->{isbackupdev} = 1;
3264
                        $fs->{isbackupdev} = 1;
3265
                        $backupdev = $name;
3266
                    }
3267
                    return $name if ($action eq 'getbackupdevice');
3268
                } elsif ($fs->{Mounted} eq $tenderpathslist[0]) {
3269
                    if ($action eq 'listbackupdevices') {
3270
                        delete $filesystems{$name}; # not available for backup - used for images
3271
                    } else {
3272
                        $filesystems{$name}->{isimagesdev} = 1;
3273
                        $fs->{isimagesdev} = 1;
3274
                        $imagesdev = $name;
3275
                    }
3276
                    return $name if ($action eq 'getimagesdevice');
3277
                }
3278
                $fs->{Name} = $name;
3279
                $fs->{nametype} = "$name ($fs->{Type} $fs->{Size})";
3280
                delete $fs->{on};
3281
                $filesystems{$name} = $fs;
3282
            }
3283
        }
3284
    }
3285
    if ($action eq 'getbackupdevice' || $action eq 'getimagesdevice') {
3286
        return $rootdev;
3287
    }
3288
    $filesystems{$rootdev}->{isbackupdev} = 1 unless ($backupdev || $action eq 'listimagesdevices');
3289
    $filesystems{$rootdev}->{isimagesdev} = 1 unless ($imagesdev || $action eq 'listbackupdevices');
3290
    # Lowercase keys
3291
    foreach my $k (keys %filesystems) {
3292
        my %hash = %{$filesystems{$k}};
3293
        %hash = map { lc $_ => $hash{$_} } keys %hash;
3294
        $filesystems{$k} = \%hash;
3295
    }
3296
    # Identify physical devices used for zfs
3297
    $cmd = "zpool list -vH";
3298
    my $zpools = `$cmd`;
3299
    my $zdev;
3300
    my %zdevs;
3301

    
3302
    # Now parse the rather strange output with every other line representing physical dev
3303
    foreach my $line (split "\n", $zpools) {
3304
        my ($zname, $zsize, $zalloc) = split "\t", $line;
3305
        if (!$zdev) {
3306
            if ($zname =~ /stabile-/) {
3307
                $zdev = {
3308
                    name=>$zname,
3309
                    size=>$zsize,
3310
                    alloc=>$zalloc
3311
                }
3312
            }
3313
        } else {
3314
            my $dev = $zsize;
3315
            $zdev->{dev} = $dev;
3316
            if ( $filesystems{$zdev->{name}}) {
3317
                if (
3318
                    ($action eq 'listimagesdevices' && $zdev->{name} =~ /backup/) ||
3319
                        ($action eq 'listbackupdevices' && $zdev->{name} =~ /images/)
3320
                ) {
3321
                    delete $filesystems{$zdev->{name}}; # Don't include backup devs in images listing and vice-versa
3322
                } else {
3323
                    if ($filesystems{$zdev->{name}}->{dev}) {
3324
                        $filesystems{$zdev->{name}}->{dev} .= " $dev";
3325
                    } else {
3326
                        $filesystems{$zdev->{name}}->{dev} = $dev;
3327
                    }
3328
        #            $filesystems{$zdev->{name}}->{nametype} =~ s/zfs/zfs pool/;
3329
                }
3330
            }
3331
            $zdevs{$dev} = $zdev->{name};
3332
        #    $zdev = '';
3333
        }
3334
    }
3335

    
3336
    # Add blockdevices
3337
    $cmd = q|lsblk --json|;
3338
    my $json2 = `$cmd`;
3339
    my $jobj2 = JSON::from_json($json2);
3340
    foreach my $fs (@{$jobj2->{blockdevices}}) {
3341
        my $rootdev = $1 if ($fs->{name} =~ /([A-Za-z]+)\d*/);
3342
        if ($fs->{children}) {
3343
            foreach my $fs2 (@{$fs->{children}}) {
3344
                next if ($fs2->{type} eq 'loop');
3345
                next if ($fs2->{type} eq 'squashfs');
3346
                next if ($fs2->{size} =~ /K$/);
3347
                if ($filesystems{$fs2->{name}}) {
3348
                    $filesystems{$fs2->{name}}->{blocksize} = $fs2->{size};
3349
                } elsif (!$zdevs{$fs2->{name}} && !$zdevs{$rootdev}) { # Don't add partitions already used for ZFS
3350
                    next if (($action eq 'listimagesdevices' || $action eq 'listbackupdevices') && $fs2->{mountpoint} eq '/');
3351
                    my $mp = $fs2->{mountpoint};
3352
                    $filesystems{$fs2->{name}} = {
3353
                        name=>$fs2->{name},
3354
                        blocksize=>$fs2->{size},
3355
                        mountpoint=>$mp,
3356
                        type=>$fs2->{type},
3357
                        nametype=> "$fs2->{name} ($fs2->{type} - " . ($mp?$mp:"not mounted") . " $fs2->{size})",
3358
                        dev=>$fs2->{name}
3359
                    }
3360
                }
3361
            }
3362
        } elsif (!$zdevs{$fs->{name}}) { # Don't add disks already used for ZFS
3363
            next if ($fs->{type} eq 'loop');
3364
            next if ($fs->{type} eq 'squashfs');
3365
            my $mp = $fs->{mountpoint};
3366
            next if ($fs->{type} eq 'rom');
3367
            $filesystems{$fs->{name}} = {
3368
                name=>$fs->{name},
3369
                blocksize=>$fs->{size},
3370
                mountpoint=>$fs->{mountpoint},
3371
                type=>$fs->{type},
3372
                nametype=> "$fs->{name} ($fs->{type} - " . ($mp?$mp:"not mounted") . " $fs->{size})",
3373
            }
3374
        }
3375
    }
3376

    
3377
    # Identify physical devices used for lvm
3378
    $cmd = "pvdisplay -c";
3379
    my $pvs = `$cmd`;
3380
    my @backupdevs; my @imagesdevs;
3381
    foreach my $line (split "\n", $pvs) {
3382
        my ($pvdev, $vgname) = split ":", $line;
3383
        $pvdev = $1 if ($pvdev =~ /\s+(\S+)/);
3384
        $pvdev = $1 if ($pvdev =~ /\/dev\/(\S+)/);
3385
        if ($filesystems{"$vgname-backupvol"}) {
3386
            push @backupdevs, $pvdev unless ($action eq 'listimagesdevices');
3387
        } elsif ($filesystems{"$vgname-imagesvol"}) {
3388
            push @imagesdevs, $pvdev unless ($action eq 'listbackupdevices');
3389
        }
3390
        if (@backupdevs) {
3391
            $filesystems{"$vgname-backupvol"}->{dev} = join(" ", @backupdevs);
3392
            $filesystems{"$vgname-backupvol"}->{nametype} = $filesystems{"$vgname-backupvol"}->{name} . " (lvm with " . $filesystems{"$vgname-backupvol"}->{type} . " on " . join(" ", @backupdevs) . " " . $filesystems{"$vgname-backupvol"}->{size} . ")";
3393
        }
3394
        if (@imagesdevs) {
3395
            $filesystems{"$vgname-imagesvol"}->{dev} = join(" ", @imagesdevs);
3396
            $filesystems{"$vgname-imagesvol"}->{nametype} = $filesystems{"$vgname-imagesvol"}->{name} . " (lvm with " . $filesystems{"$vgname-imagesvol"}->{type} . " on " . join(" ", @imagesdevs) . " " . $filesystems{"$vgname-imagesvol"}->{size} . ")";
3397
        }
3398
        delete $filesystems{$pvdev} if ($filesystems{$pvdev}); # Don't also list as physical device
3399
    }
3400
    my $jsonreply;
3401
    if ($action eq 'getbackupdevice' || $action eq 'getimagesdevice') {
3402
        return ''; # We should not get here
3403
    } elsif ($action eq 'getstoragedevices') {
3404
        return \%filesystems;
3405
    } elsif ($action eq 'listimagesdevices') {
3406
        $jsonreply .= qq|{"identifier": "name", "label": "nametype", "action": "$action", "items": |;
3407
        my @vals = sort {$b->{'isimagesdev'} cmp $a->{'isimagesdev'}} values %filesystems;
3408
        $jsonreply .= JSON->new->canonical(1)->pretty(1)->encode(\@vals);
3409
        $jsonreply .= "}";
3410
    } elsif ($action eq 'listbackupdevices') {
3411
        $jsonreply .= qq|{"identifier": "name", "label": "nametype", "action": "$action", "items": |;
3412
        my @vals = sort {$b->{'isbackupdev'} cmp $a->{'isbackupdev'}} values %filesystems;
3413
        $jsonreply .= JSON->new->canonical(1)->pretty(1)->encode(\@vals);
3414
        $jsonreply .= "}";
3415
    } else {
3416
        $jsonreply .= JSON->new->canonical(1)->pretty(1)->encode(\%filesystems);
3417
    }
3418
    return $jsonreply;
3419
}
3420

    
3421
sub do_liststoragepools {
3422
    my ($image, $action) = @_;
3423
    if ($help) {
3424
        return <<END
3425
GET:dojo:
3426
Returns available storage pools. If parameter dojo is set, JSON is padded for Dojo use.
3427
END
3428
    }
3429
    my %npool = (
3430
        "hostpath", "node",
3431
        "path", "--",
3432
        "name", "On node",
3433
        "rdiffenabled", 1,
3434
        "id", "-1");
3435
    my @p = @spools;
3436
    # Present node storage pool if user has sufficient privileges
3437
    if (index($privileges,"a")!=-1 || index($privileges,"n")!=-1) {
3438
        @p = (\%npool);
3439
        push @p, @spools;
3440
    }
3441

    
3442
    my $jsonreply;
3443
    $jsonreply .= "{\"identifier\": \"id\", \"label\": \"name\", \"items\":" if ($params{'dojo'});
3444
    $jsonreply .= to_json(\@p, {pretty=>1});
3445
    $jsonreply .= "}" if ($params{'dojo'});
3446
    return $jsonreply;
3447
}
3448

    
3449
# List images available for attaching to server
3450
sub do_listimages {
3451
    my ($img, $action) = @_;
3452
    if ($help) {
3453
        return <<END
3454
GET:image,image1:
3455
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.
3456
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.
3457
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".
3458
END
3459
    }
3460
    my $res;
3461
    $res .= header('application/json') unless ($console);
3462
    my $curimg1 = URI::Escape::uri_unescape($params{'image1'});
3463
    my @filteredfiles;
3464
    my @curusers = @users;
3465
    # If an admin user is looking at a server not belonging to him, allow him to see the server
3466
    # users images
3467
    if ($isadmin && $img && $img ne '--' && $register{$img} && $register{$img}->{'user'} ne $user) {
3468
        @curusers = ($register{$img}->{'user'}, "common");
3469
    }
3470

    
3471
    foreach my $u (@curusers) {
3472
        my @regkeys = (tied %register)->select_where("user = '$u'");
3473
        foreach my $k (@regkeys) {
3474
            my $val = $register{$k};
3475
            if ($val->{'user'} eq $u && (defined $spools[$val->{'storagepool'}]->{'id'} || $val->{'storagepool'}==-1)) {
3476
                my $f = $val->{'path'};
3477
                next if ($f =~ /\/images\/dummy.qcow2/);
3478
                my $itype = $val->{'type'};
3479
                if ($itype eq "vmdk" || $itype eq "img" || $itype eq "vhd" || $itype eq "vhdx" || $itype eq "qcow" || $itype eq "qcow2" || $itype eq "vdi") {
3480
                    my $hit = 0;
3481
                    if ($f =~ /(.+)\.master\.$itype/) {$hit = 1;} # don't list master images for user selections
3482
                    if ($f =~ /(.+)\/common\//) {$hit = 1;} # don't list common images for user selections
3483
                    my $dbstatus = $val->{'status'};
3484
                    if ($dbstatus ne "unused") {$hit = 1;} # Image is in a transitional state - do not use
3485
                    if ($hit == 0 || $img eq $f) {
3486
                        my $hypervisor = ($itype eq "vmdk" || $itype eq "vhd" || $itype eq "vhdx" || $itype eq "vdi")?"vbox":"kvm";
3487
                        my $notes = $val->{'notes'};
3488
                        $notes = "" if $notes eq "--";
3489
                        my %img = ("path", $f, "name", $val->{'name'}, "hypervisor", $hypervisor, "notes", $notes,
3490
                            "uuid", $val->{'uuid'}, "master", $val->{'master'}, "managementlink", $val->{'managementlink'}||"",
3491
                            "upgradelink", $val->{'upgradelink'}||"", "terminallink", $val->{'terminallink'}||"", "version", $val->{'version'}||"",
3492
                            "appid", $val->{'appid'}||"");
3493
                        push @filteredfiles, \%img;
3494
                    }
3495
                }
3496
            }
3497
        }
3498
    }
3499
    my %img = ("path", "--", "name", "--", "hypervisor", "kvm,vbox");
3500
    if ($curimg1) {
3501
        push @filteredfiles, \%img;
3502
    }
3503
    my $json_text = to_json(\@filteredfiles, {pretty=>1});
3504
    $res .= qq/{"identifier": "path", "label": "name", "items": $json_text }/;
3505
    return $res;
3506
}
3507

    
3508
sub Listcdroms {
3509
    my ($image, $action) = @_;
3510
    if ($help) {
3511
        return <<END
3512
GET::
3513
Lists the CD roms a user has access to.
3514
END
3515
    }
3516
    my $res;
3517
    $res .= header('application/json') unless ($console);
3518
    my @filteredfiles;
3519
    foreach my $u (@users) {
3520
        my @regkeys = (tied %register)->select_where("user = '$u'");
3521
        foreach my $k (@regkeys) {
3522
            my $val = $register{$k};
3523
            my $f = $val->{'path'};
3524
            if ($val->{'user'} eq $u && (defined $spools[$val->{'storagepool'}]->{'id'} || $val->{'storagepool'}==-1)) {
3525
                my $itype = $val->{'type'};
3526
                if ($itype eq "iso" || $itype eq "toast") {
3527
                    $notes = $val->{'notes'} || '';
3528
                    if ($u eq $user) {
3529
                        $installable = "true";
3530
                    #    $notes = "This CD/DVD may work just fine, however it has not been tested to work with Irigo Servers.";
3531
                    } else {
3532
                        $installable = $val->{'installable'} || 'false';
3533
                    #    $notes = "This CD/DVD has been tested to work with Irigo Servers." unless $notes;
3534
                    }
3535
                    my %img = ("path", $f, "name", $val->{'name'}, "installable", $installable, "notes", $notes);
3536
                    push @filteredfiles, \%img;
3537
                }
3538
            }
3539
        }
3540
    }
3541
    my %ioimg = ("path", "virtio", "name", "-- VirtIO disk (dummy) --");
3542
    push @filteredfiles, \%ioimg;
3543
    my %dummyimg = ("path", "--", "name", "-- No CD --");
3544
    push @filteredfiles, \%dummyimg;
3545
    #        @filteredfiles = (sort {$a->{'name'} cmp $b->{'name'}} @filteredfiles); # Sort by status
3546
    my $json_text = to_json(\@filteredfiles, {pretty=>1});
3547
    $res .= qq/{"identifier": "path", "label": "name", "items": $json_text }/;
3548
    return $res;
3549
}
3550

    
3551
sub do_listmasterimages {
3552
    my ($image, $action) = @_;
3553
    if ($help) {
3554
        return <<END
3555
GET::
3556
Lists master images available to the current user.
3557
END
3558
    }
3559
    my $res;
3560
    $res .= header('application/json') unless ($console);
3561

    
3562
    my @filteredfiles;
3563
    my @busers = @users;
3564
    push (@busers, $billto) if ($billto && $billto ne $user); # We include images from 'parent' user
3565

    
3566
    foreach my $u (@busers) {
3567
        my @regkeys = (tied %register)->select_where("user = '$u'");
3568
        foreach my $k (@regkeys) {
3569
            my $valref = $register{$k};
3570
            my $f = $valref->{'path'};
3571
            if ($valref->{'user'} eq $u && (defined $spools[$valref->{'storagepool'}]->{'id'} || $valref->{'storagepool'}==-1)) {
3572
                # Only list installable master images from billto account
3573
                next if ($billto && $u eq $billto && $valref->{'installable'} ne 'true');
3574

    
3575
                my $itype = $valref->{'type'};
3576
                if ($itype eq "qcow2" && $f =~ /(.+)\.master\.$itype/) {
3577
                    my $installable;
3578
                    my $status = $valref->{'status'};
3579
                    my $notes;
3580
                    if ($u eq $user) {
3581
                        $installable = "true";
3582
                        $notes = "This master image may work just fine, however it has not been tested to work with Stabile.";
3583
                    } else {
3584
                        $installable = $valref->{'installable'} || '';
3585
                        $notes = $valref->{'notes'};
3586
                        $notes = "This master image has been tested to work with Irigo Servers." unless $notes;
3587
                    }
3588
                    my %img = (
3589
                        "path", $f,
3590
                        "name", $valref->{'name'},
3591
                        "installable", $installable,
3592
                        "notes", $notes,
3593
                        "managementlink", $valref->{'managementlink'}||"",
3594
                        "upgradelink", $valref->{'upgradelink'}||"",
3595
                        "terminallink", $valref->{'terminallink'}||"",
3596
                        "image2", $valref->{'image2'}||"",
3597
                        "version", $valref->{'version'}||"",
3598
                        "appid", $valref->{'appid'}||"",
3599
                        "status", $status,
3600
                        "user", $valref->{'user'}
3601
                    );
3602
                    push @filteredfiles, \%img;
3603
                }
3604
            }
3605
        }
3606
    }
3607
    my %img = ("path", "--", "name", "--", "installable", "true", "status", "unused");
3608
    push @filteredfiles, \%img;
3609
    my $json_text = to_json(\@filteredfiles);
3610
    $res .= qq/{"identifier": "path", "label": "name", "items": $json_text }/;
3611
    return $res;
3612
}
3613

    
3614
sub Updatebtime {
3615
    my ($img, $action, $obj) = @_;
3616
    if ($help) {
3617
        return <<END
3618
GET:image:
3619
END
3620
    }
3621
    my $res;
3622
    $curimg = $curimg || $img;
3623
    my $imguser = $register{$curimg}->{'user'};
3624
    if ($isadmin || $imguser eq $user) {
3625
        my $btime;
3626
        $btime = getBtime($curimg, $imguser) if ($imguser);
3627
        if ($btime) {
3628
            $register{$curimg}->{'btime'} = $btime ;
3629
            $res .= "Status=OK $curimg has btime: " . scalar localtime( $btime ) . "\n";
3630
        } else {
3631
            $register{$curimg}->{'btime'} = '' ;
3632
            $res .= "Status=OK $curimg has no btime\n";
3633
        }
3634
    } else {
3635
        $res .= "Status=Error no access to $curimg\n";
3636
    }
3637
    return $res;
3638
}
3639

    
3640
sub Updateallbtimes {
3641
    my ($img, $action) = @_;
3642
    if ($help) {
3643
        return <<END
3644
GET::
3645
END
3646
    }
3647
    if ($isadmin) {
3648
        foreach my $path (keys %register) {
3649
            my $imguser = $register{$path}->{'user'};
3650
            my $btime = getBtime($path, $imguser);
3651
            if ($btime) {
3652
                $register{$path}->{'btime'} = $btime ;
3653
                $postreply .= "Status=OK $register{$path}->{'name'} ($path) has btime: " . scalar localtime( $btime ) . "\n";
3654
            } else {
3655
                $postreply .= "Status=OK $register{$path}->{'name'} ($path) has no btime\n";
3656
            }
3657
        }
3658
    } else {
3659
        $postreply .= "Status=ERROR you are not allowed to do this.\n";
3660
    }
3661
    return $postreply;
3662
}
3663

    
3664
# Activate image from fuel
3665
sub Activate {
3666
    my ($curimg, $action, $argref) = @_;
3667
    if ($help) {
3668
        return <<END
3669
GET:image, name, managementlink, upgradelink, terminallink, force:
3670
Activate an image from fuel storage, making it available for regular use.
3671
END
3672
    }
3673
    my %uargs = %{$argref};
3674
    my $name = URI::Escape::uri_unescape($uargs{'name'});
3675
    my $managementlink = URI::Escape::uri_unescape($uargs{'managementlink'});
3676
    my $upgradelink = URI::Escape::uri_unescape($uargs{'upgradelink'});
3677
    my $terminallink = URI::Escape::uri_unescape($uargs{'terminallink'});
3678
    my $version = URI::Escape::uri_unescape($uargs{'version'}) || '1.0b';
3679
    my $image2 =  URI::Escape::uri_unescape($uargs{'image2'});
3680
    my $force = $uargs{'force'};
3681

    
3682
    return "Status=ERROR image must be in fuel storage ($curimg)\n" unless ($curimg =~ /^\/mnt\/fuel\/pool(\d+)\/(.+)/);
3683
    my $pool = $1;
3684
    my $ipath = $2;
3685
    return "Status=ERROR image is not a qcow2 image ($curimg, $ipath)\n" unless ($ipath =~ /(.+\.qcow2$)/);
3686
    my $npath = $1;
3687
    my $ppath = '';
3688
    if ($npath =~ /(.*\/)(.+\.qcow2$)/) {
3689
        $npath = $2;
3690
        $ppath = $1;
3691
    }
3692
    my $imagepath = $tenderpathslist[$pool] . "/$user/fuel/$ipath";
3693
    my $newpath = $tenderpathslist[$pool] . "/$user/$npath";
3694
    return "Status=ERROR image not found ($imagepath)\n" unless (-e $imagepath);
3695
    return "Status=ERROR image already exists in destination ($newpath)\n" if (-e $newpath && !$force);
3696
    return "Status=ERROR image is in use ($newpath)\n" if (-e $newpath && $register{$newpath} && $register{$newpath}->{'status'} ne 'unused');
3697

    
3698
    my $virtualsize = `qemu-img info --force-share "$imagepath" | sed -n -e 's/^virtual size: .*(//p' | sed -n -e 's/ bytes)//p'`;
3699
    chomp $virtualsize;
3700
#    my $master = `qemu-img info --force-share "$imagepath" | sed -n -e 's/^backing file: //p' | sed -n -e 's/ (actual path:.*)\$//p'`;
3701
    my $master = `qemu-img info --force-share "$imagepath" | sed -n -e 's/^backing file: //p'`;
3702
    chomp $master;
3703

    
3704
    # Now deal with image2
3705
    my $newpath2 = '';
3706
    if ($image2) {
3707
        $image2 = "/mnt/fuel/pool$pool/$ppath$image2" unless ($image2 =~ /^\//);
3708
        return "Status=ERROR image2 must be in fuel storage ($image2)\n" unless ($image2 =~ /^\/mnt\/fuel\/pool$pool\/(.+)/);
3709
        $ipath = $1;
3710
        return "Status=ERROR image is not a qcow2 image\n" unless ($ipath =~ /(.+\.qcow2$)/);
3711
        $npath = $1;
3712
        $npath = $1 if ($npath =~ /.*\/(.+\.qcow2$)/);
3713
        my $image2path = $tenderpathslist[$pool] . "/$user/fuel/$ipath";
3714
        $newpath2 = $tenderpathslist[$pool] . "/$user/$npath";
3715
        return "Status=ERROR image2 not found ($image2path)\n" unless (-e $image2path);
3716
        return "Status=ERROR image2 already exists in destination ($newpath2)\n" if (-e $newpath2 && !$force);
3717
        return "Status=ERROR image2 is in use ($newpath2)\n" if (-e $newpath2 && $register{$newpath2} && $register{$newpath2}->{'status'} ne 'unused');
3718

    
3719
        my $virtualsize2 = `qemu-img info --force-share "$image2path" | sed -n -e 's/^virtual size: .*(//p' | sed -n -e 's/ bytes)//p'`;
3720
        chomp $virtualsize2;
3721
#        my $master2 = `qemu-img info --force-share "$image2path" | sed -n -e 's/^backing file: //p' | sed -n -e 's/ (actual path:.*)\$//p'`;
3722
        my $master2 = `qemu-img info --force-share "$image2path" | sed -n -e 's/^backing file: //p'`;
3723
        chomp $master2;
3724
        if ($register{$master2}) {
3725
            $register{$master2}->{'status'} = 'used';
3726
        }
3727
        `mv "$image2path" "$newpath2"`;
3728
        if (-e $newpath2) {
3729
            my $ug = new Data::UUID;
3730
            my $newuuid = $ug->create_str();
3731
            unless ($name) {
3732
                $name = $npath if ($npath);
3733
                $name = $1 if ($name =~ /(.+)\.(qcow2)$/);
3734
            }
3735
            $register{$newpath2} = {
3736
                uuid => $newuuid,
3737
                path => $newpath2,
3738
                master => $master2,
3739
                name => "$name (data)",
3740
                user => $user,
3741
                storagepool => $pool,
3742
                type => 'qcow2',
3743
                status => 'unused',
3744
                version => $version,
3745
                virtualsize => $virtualsize2
3746
            };
3747
            $postreply .= "Status=OK Activated data image $newpath2, $name (data), $newuuid\n";
3748
        } else {
3749
            $postreply .=  "Status=ERROR Unable to activate $image2path, $newpath2\n";
3750
        }
3751
    }
3752

    
3753
    # Finish up primary image
3754
    if ($register{$master}) {
3755
        $register{$master}->{'status'} = 'used';
3756
    }
3757
    `mv "$imagepath" "$newpath"`;
3758
    if (-e $newpath) {
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{$newpath} = {
3766
            uuid => $newuuid,
3767
            path => $newpath,
3768
            master => $master,
3769
            name => $name,
3770
            user => $user,
3771
            storagepool => $pool,
3772
            image2 => $newpath2,
3773
            type => 'qcow2',
3774
            status => 'unused',
3775
            installable => 'true',
3776
            managementlink => $managementlink || '/stabile/pipe/http://{uuid}:10000/stabile/',
3777
            upgradelink => $upgradelink,
3778
            terminallink => $terminallink,
3779
            version => $version,
3780
            virtualsize => $virtualsize
3781
        };
3782
        $postreply .=  "Status=OK Activated $newpath, $name, $newuuid\n";
3783
    } else {
3784
        $postreply .=  "Status=ERROR Unable to activate $imagepath to $newpath\n";
3785
    }
3786
    return $postreply;
3787
}
3788

    
3789
sub Uploadtoregistry {
3790
    my ($path, $action, $obj) = @_;
3791
    if ($help) {
3792
        return <<END
3793
GET:image, force:
3794
Upload an image to the registry. Set [force] if you want to force overwrite images in registry - use with caution.
3795
END
3796
    }
3797
    $force = $obj->{'force'};
3798
    if (-e $path && ($register{$path}->{'user'} eq $user || $isadmin)) {
3799
        $postreply .= $main::uploadToOrigo->($engineid, $path, $force);
3800
    } else {
3801
        $postreply .= "Status=Error Not allowed\n";
3802
    }
3803
    return $postreply;
3804
}
3805

    
3806
sub Publish {
3807
    my ($uuid, $action, $parms) = @_;
3808
    if ($help) {
3809
        return <<END
3810
GET:image,appid,appstore,force:
3811
Publish a stack to registry. Set [force] if you want to force overwrite images in registry - use with caution.
3812
END
3813
    }
3814
    my $res;
3815
    $uuid = $parms->{'uuid'} if ($uuid =~ /^\// || !$uuid);
3816
    my $force = $parms->{'force'};
3817
    my $freshen = $parms->{'freshen'};
3818

    
3819
    if ($isreadonly) {
3820
        $res .= "Status=ERROR Your account does not have the necessary privilege.s\n";
3821
    } elsif (!$uuid || !$imagereg{$uuid}) {
3822
        $res .= "Status=ERROR At least specify activated master image uuid [uuid or path] to publish.\n";
3823
    } elsif ($imagereg{$uuid}->{'user'} ne $user && !$isadmin) {
3824
        $res .= "Status=ERROR Your account does not have the necessary privileges.\n";
3825
    } elsif ($imagereg{$uuid}->{'path'} =~ /.+\.master\.qcow2$/) {
3826
        if ($engineid eq $valve001id) { # On valve001 - check if meta file exists
3827
            if (-e $imagereg{$uuid}->{'path'} . ".meta") {
3828
                $res .= "On valve001. Found meta file $imagereg{$uuid}->{'path'}.meta\n";
3829
                my $appid = `cat $imagereg{$uuid}->{'path'}.meta | sed -n -e 's/^APPID=//p'`;
3830
                chomp $appid;
3831
                if ($appid) {
3832
                    $parms->{'appid'} = $appid;
3833
                    $register{$imagereg{$uuid}->{'path'}}->{'appid'} = $appid;
3834
                    tied(%register)->commit;
3835
                }
3836
            }
3837
        # On valve001 - move image to stacks
3838
            if ($imagereg{$uuid}->{'storagepool'} ne '0') {
3839
                $res .= "Status=OK Moving image: " . Move($imagereg{$uuid}->{'path'}, $user, 0) . "\n";
3840
            } else {
3841
                $res .= "Status=OK Image is already available in registry\n";
3842
            }
3843
        } else {
3844
        #    $console = 1;
3845
        #    my $link = Download($imagereg{$uuid}->{'path'});
3846
        #    chomp $link;
3847
        #    $parms->{'downloadlink'} = $link; # We now upload instead
3848
        #    $res .= "Status=OK Asking registry to download $parms->{'APPID'} image: $link\n";
3849
            if ($appstores) {
3850
                $parms->{'appstore'} = $appstores;
3851
            } elsif ($appstoreurl =~ /www\.(.+)\//) {
3852
                $parms->{'appstore'} = $1;
3853
                $res .= "Status=OK Adding registry: $1\n";
3854
            }
3855
        }
3856
#        $parms->{'appstore'} = 1 if ($freshen);
3857

    
3858
        my %imgref = %{$imagereg{$uuid}};
3859
        $parms = Hash::Merge::merge($parms, \%imgref);
3860
        my $postdata = to_json($parms);
3861
        my $postres = $main::postToOrigo->($engineid, 'publishapp', $postdata);
3862
        $res .= $postres;
3863
        my $appid;
3864
        $appid = $1 if ($postres =~ /appid: (\d+)/);
3865
        my $path = $imagereg{$uuid}->{'path'};
3866
        if ($freshen && $appid) {
3867
            $res .= "Status=OK Freshened the stack description\n";
3868
        } elsif ($appid) {
3869
            $register{$path}->{'appid'} = $appid if ($register{$path});
3870
            $res .= "Status=OK Received appid $appid for $path, uploading image to registry, hang on...\n";
3871
            my $upres .= $main::uploadToOrigo->($engineid, $path, $force);
3872
            $res .= $upres;
3873
            my $image2 = $register{$path}->{'image2'} if ($register{$path});
3874
            if ($upres =~ /Status=OK/ && $image2 && $image2 ne '--') { # Stack has a data image
3875
                $res .= $main::uploadToOrigo->($engineid, $image2, $force);
3876
            }
3877
        } else {
3878
            $res .= "Status=Error Did not get an appid\n";
3879
        }
3880
    } else {
3881
        $res .= "Status=ERROR You can only publish a master image.\n";
3882
    }
3883
    return $res;
3884
}
3885

    
3886
sub Release {
3887
    my ($uuid, $action, $parms) = @_;
3888
    if ($help) {
3889
        return <<END
3890
GET:image,appid,appstore,force,unrelease:
3891
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.
3892
END
3893
    }
3894
    my $res;
3895
    $uuid = $parms->{'uuid'} if ($uuid =~ /^\// || !$uuid);
3896
    my $force = $parms->{'force'};
3897
    my $unrelease = $parms->{'unrelease'};
3898

    
3899
    if (!$uuid || !$imagereg{$uuid}) {
3900
        $res .= "Status=ERROR At least specify master image uuid [uuid or path] to release.\n";
3901
    } elsif (!$isadmin) {
3902
        $res .= "Status=ERROR Your account does not have the necessary privileges.\n";
3903
    } elsif ($imagereg{$uuid}->{'path'} =~ /.+\.master\.qcow2$/ && $imagereg{$uuid}->{'appid'}) {
3904
        my $action = 'release';
3905
        my $targetuser = 'common';
3906
        if ($unrelease) {
3907
            $action = 'unrelease';
3908
            $targetuser = $user;
3909
        }
3910
        if ($appstores) {
3911
            $parms->{'appstore'} = $appstores;
3912
        } elsif ($appstoreurl =~ /www\.(.+)\//) {
3913
            $parms->{'appstore'} = $1;
3914
            $res .= "Status=OK Adding registry: $1\n";
3915
        }
3916
        $parms->{'appid'} = $imagereg{$uuid}->{'appid'};
3917
        $parms->{'force'} = $force if ($force);
3918
        $parms->{'unrelease'} = $unrelease if ($unrelease);
3919
        my $postdata = to_json($parms);
3920
        my $postres = $main::postToOrigo->($engineid, 'releaseapp', $postdata);
3921
        $res .= $postres;
3922
        my $appid;
3923
        $appid = $1 if ($postres =~ /Status=OK Moved (\d+)/);
3924
        my $path = $imagereg{$uuid}->{'path'};
3925
        if ($appid) {
3926
            $res.= "Now moving local stack to $targetuser\n";
3927
            # First move data image
3928
            my $image2 = $register{$path}->{'image2'} if ($register{$path});
3929
            my $newimage2 = $image2;
3930
            if ($image2 && $image2 ne '--' && $register{$image2}) { # Stack has a data image
3931
                if ($unrelease) {
3932
                    $newimage2 =~ s/common/$register{$image2}->{'user'}/;
3933
                } else {
3934
                    $newimage2 =~ s/$register{$image2}->{'user'}/common/;
3935
                }
3936
                $register{$path}->{'image2'} = $newimage2;
3937
                tied(%register)->commit;
3938
                $res .= Move($image2, $targetuser, '', '', 1);
3939
            }
3940
            # Move image
3941
            $res .= Move($path, $targetuser, '', '', 1);
3942
            $res .= "Status=OK $action $appid\n";
3943
        } else {
3944
            $res .= "Status=Error $action failed\n";
3945
        }
3946
    } else {
3947
        $res .= "Status=ERROR You can only $action a master image that has been published.\n";
3948
    }
3949
    return $res;
3950
}
3951

    
3952
sub do_unlinkmaster {
3953
    my ($img, $action) = @_;
3954
    if ($help) {
3955
        return <<END
3956
GET:image,path:
3957
END
3958
    }
3959
    my $res;
3960
    $res .= header('text/html') unless ($console);
3961
    if ($isreadonly) {
3962
        $res .= "Your account does not have the necessary privileges\n";
3963
    } elsif ($curimg) {
3964
        $res .= unlinkMaster($curimg) . "\n";
3965
    } else {
3966
        $res .= "Please specify master image to link\n";
3967
    }
3968
    return $res;
3969
}
3970

    
3971
# Simple action for unmounting all images
3972
sub do_unmountall {
3973
    my ($img, $action) = @_;
3974
    if ($help) {
3975
        return <<END
3976
GET:image,path:
3977
END
3978
    }
3979
    return "Your account does not have the necessary privileges\n" if ($isreadonly);
3980
    my $res;
3981
    $res .= header('text/plain') unless ($console);
3982
    $res .= "Unmounting all images for $user\n";
3983
    unmountAll();
3984
    $res .= "\n$postreply" if ($postreply);
3985
    return $res;
3986
}
3987

    
3988
sub Updatedownloads {
3989
    my ($img, $action) = @_;
3990
    if ($help) {
3991
        return <<END
3992
GET:image,path:
3993
END
3994
    }
3995
    my $res;
3996
    $res .= header('text/html') unless ($console);
3997
    my $txt1 = <<EOT
3998
Options -Indexes
3999
EOT
4000
    ;
4001
    `/bin/mkdir "$Stabile::basedir/download"` unless (-e "$Stabile::basedir/download");
4002
    $res .= "Writing .htaccess: -> $Stabile::basedir/download/.htaccess\n";
4003
    unlink("$Stabile::basedir/download/.htaccess");
4004
    `chown www-data:www-data "$Stabile::basedir/download"`;
4005
    `/bin/echo "$txt1" | sudo -u www-data tee "$Stabile::basedir/download/.htaccess"`; #This ugliness is needed because of ownership issues with Synology NFS
4006
    `chmod 644 "$Stabile::basedir/download/.htaccess"`;
4007
    foreach my $p (@spools) {
4008
        my $dir = $p->{'path'};
4009
        my $id = $p->{'id'};
4010
        `/bin/rm "$Stabile::basedir/download/$id"; /bin/ln -s "$dir" "$Stabile::basedir/download/$id"`;
4011
        $res .= "Writing .htaccess: $id -> $dir/.htaccess\n";
4012
        unlink("$dir/.htaccess");
4013
        `/bin/echo "$txt1" | tee "$dir/.htaccess"`;
4014
        `chown www-data:www-data "$dir/.htaccess"`;
4015
        `chmod 644 "$dir/.htaccess"`;
4016
    }
4017

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

    
4020
    foreach my $username (keys %userreg) {
4021
        my $require = '';
4022
        my $txt = <<EOT
4023
order deny,allow
4024
AuthName "Download"
4025
AuthType None
4026
TKTAuthLoginURL $baseurl/auth/login.cgi
4027
TKTAuthIgnoreIP on
4028
deny from all
4029
Satisfy any
4030
require user $username
4031
Options -Indexes
4032
EOT
4033
        ;
4034
        foreach my $p (@spools) {
4035
            my $dir = $p->{'path'};
4036
            my $id = $p->{'id'};
4037
            if (-d "$dir/$username") {
4038
                $res .= "Writing .htaccess: $id -> $dir/$username/.htaccess\n";
4039
                unlink("$dir/$username/.htaccess");
4040
                `/bin/echo "$txt1" | sudo -u www-data tee $dir/$username/.htaccess`;
4041
                if ($tenderlist[$p->{'id'}] eq 'local') {
4042
                    if (!(-e "$dir/$username/fuel") && -e "$dir/$username") {
4043
                        `mkdir "$dir/$username/fuel"`;
4044
                        `chmod 777 "$dir/$username/fuel"`;
4045
                    }
4046
                }
4047
            }
4048
        }
4049
    }
4050
    untie %userreg;
4051
    return $res;
4052
}
4053

    
4054
sub do_listpackages($action) {
4055
    my ($image, $action) = @_;
4056
    if ($help) {
4057
        return <<END
4058
GET:image:
4059
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.
4060
END
4061
    }
4062
    my $res;
4063
    $res .= header('text/plain') unless ($console);
4064

    
4065
    my $mac = $register{$image}->{'mac'};
4066
    my $macip;
4067
    if ($mac && $mac ne '--') {
4068
        unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
4069
        $macip = $nodereg{$mac}->{'ip'};
4070
        untie %nodereg;
4071
    }
4072
    $image =~ /(.+)/; $image = $1;
4073
    my $apps;
4074

    
4075
    if ($macip && $macip ne '--') {
4076
        my $cmd = qq[eval \$(/usr/bin/guestfish --ro -a "$image" --i --listen); ]; # sets $GUESTFISH_PID shell var
4077
        $cmd .= qq[root="\$(/usr/bin/guestfish --remote inspect-get-roots)"; ];
4078
        $cmd .= qq[guestfish --remote inspect-list-applications "\$root"; ];
4079
        $cmd .= qq[guestfish --remote inspect-get-product-name "\$root"; ];
4080
        $cmd .= qq[guestfish --remote exit];
4081
        $cmd = "$sshcmd $macip '$cmd'";
4082
        $apps = `$cmd`;
4083
    } else {
4084
        my $cmd;
4085
        #        my $pid = open my $cmdpipe, "-|",qq[/usr/bin/guestfish --ro -a "$image" --i --listen];
4086
        $cmd .= qq[eval \$(/usr/bin/guestfish --ro -a "$image" --i --listen); ];
4087
        # Start listening guestfish
4088
        my $daemon = Proc::Daemon->new(
4089
            work_dir => '/usr/local/bin',
4090
            setuid => 'www-data',
4091
            exec_command => $cmd
4092
        ) or do {$postreply .= "Status=ERROR $@\n";};
4093
        my $pid = $daemon->Init();
4094
        while ($daemon->Status($pid)) {
4095
            sleep 1;
4096
        }
4097
        # Find pid of the listening guestfish
4098
        my $pid2;
4099
        my $t = new Proc::ProcessTable;
4100
        foreach $p ( @{$t->table} ){
4101
            my $pcmd = $p->cmndline;
4102
            if ($pcmd =~ /guestfish.+$image/) {
4103
                $pid2 = $p->pid;
4104
                last;
4105
            }
4106
        }
4107

    
4108
        my $cmd2;
4109
        if ($pid2) {
4110
            $cmd2 .= qq[root="\$(/usr/bin/guestfish --remote=$pid2 inspect-get-roots)"; ];
4111
            $cmd2 .= qq[guestfish --remote=$pid2 inspect-list-applications "\$root"; ];
4112
            $cmd2 .= qq[guestfish --remote=$pid2 inspect-get-product-name "\$root"; ];
4113
            $cmd2 .= qq[guestfish --remote=$pid2 exit];
4114
        }
4115
        $apps = `$cmd2`;
4116
    }
4117
    if ($console) {
4118
        $res .= $apps;
4119
    } else {
4120
        my @packages;
4121
        my @packages2;
4122
        open my $fh, '<', \$apps or die $!;
4123
        my $i;
4124
        while (<$fh>) {
4125
            if ($_ =~ /\[(\d+)\]/) {
4126
                push @packages2, $packages[$i];
4127
                $i = $1;
4128
            } elsif ($_ =~ /(\S+): (.+)/ && $2) {
4129
                $packages[$i]->{$1} = $2;
4130
            }
4131
        }
4132
        close $fh or die $!;
4133
        $res .= to_json(\@packages, {pretty => 1});
4134
    }
4135
    return $res;
4136
}
4137

    
4138
sub Inject {
4139
    my ($image, $action, $obj) = @_;
4140
    if ($help) {
4141
        return <<END
4142
GET:image:
4143
Tries to inject drivers into a qcow2 image with a Windows OS installed on it. Image must not be in use.
4144
END
4145
    }
4146
    $uistatus = "injecting";
4147
    my $path = $obj->{path} || $curimg;
4148
    my $status = $obj->{status};
4149
    my $esc_localpath = shell_esc_chars($path);
4150

    
4151
    # Find out if we are dealing with a Windows image
4152
    # my $xml = `bash -c '/usr/bin/virt-inspector -a $esc_localpath'`;
4153
    my $xml = `bash -c '/usr/bin/virt-inspector -a $esc_localpath' 2>&1`;
4154
    # $res .= $xml . "\n";
4155
    my $xmlref;
4156
    my $osname;
4157
    $xmlref = XMLin($xml) if ($xml =~ /^<\?xml/);
4158
    $osname = $xmlref->{operatingsystem}->{name} if ($xmlref);
4159
    if ($xmlref && $osname eq 'windows') {
4160
    #    my $upath = $esc_localpath;
4161
        my $upath = $path;
4162
        # We need write privileges
4163
        $res .= `chmod 666 "$upath"`;
4164
        # First try to merge storage registry keys into Windows registry. If not a windows vm it simply fails.
4165
        $res .= `bash -c 'cat /usr/share/stabile/mergeide.reg | /usr/bin/virt-win-reg --merge "$upath"' 2>&1`;
4166
        # Then try to merge the critical device keys. This has been removed in win8 and 2012, so will simply fail for these.
4167
        $res .= `bash -c 'cat /usr/share/stabile/mergeide-CDDB.reg | /usr/bin/virt-win-reg --merge "$upath"' 2>&1`;
4168
        if ($res) { $main::syslogit->($user, "info", $res); $res = ''; }
4169

    
4170
        # Try to copy viostor.sys into image
4171
        my @winpaths = (
4172
            '/Windows/System32/drivers',
4173
            '/WINDOWS/system32/drivers',
4174
            '/WINDOWS/System32/drivers',
4175
            '/WINNT/system32/drivers'
4176
        );
4177
        foreach my $winpath (@winpaths) {
4178
            my $lscmd = qq|bash -c 'virt-ls -a "$upath" "$winpath"'|;
4179
            my $drivers = `$lscmd`;
4180
            if ($drivers =~ /viostor/i) {
4181
                $postreply .= "Status=$status viostor already installed in $winpath in $upath\n";
4182
                $main::syslogit->($user, "info", "viostor already installed in $winpath in $upath");
4183
                last;
4184
            } elsif ($drivers) {
4185
                `umount "$upath"`; # Unmount if mounted by browse operation or similar
4186
                my $cmd = qq|bash -c 'guestfish --rw -i -a "$upath" upload /usr/share/stabile/VIOSTOR.SYS $winpath/viostor.sys' 2>&1|;
4187
                my $error = `$cmd`;
4188
                if ($error) {
4189
                    $postreply .= "$cmd\n";
4190
                    $postreply .= "Status=ERROR Problem injecting virtio drivers into $winpath on $upath: $error\n";
4191
                    $main::syslogit->($user, "info", "Error injecting virtio drivers into $upath: $error");
4192
                } else {
4193
                    $postreply .= "Status=$status Injected virtio drivers into $upath\n";
4194
                    $main::syslogit->($user, "info", "Injected virtio drivers into $upath");
4195
                }
4196
                last;
4197
            } else {
4198
                $postreply .= "Status=ERROR No drivers found in $winpath\n";
4199
            }
4200
        }
4201

    
4202
    } else {
4203
        $postreply .= "Status=ERROR No Windows OS found in $osname image, not injecting drivers.\n";
4204
        $main::syslogit->($user, "info", "No Windows OS found ($osname) in image, not injecting drivers.");
4205
    }
4206
    my $msg = $postreply;
4207
    $msg = $1 if ($msg =~ /\w+=\w+ (.+)/);
4208
    chomp $msg;
4209
    $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$obj->{'uuid'}, status=>$status, message=>$msg});
4210
    $postreply .=  "Status=$uistatus $obj->{type} image: $obj->{name}\n";
4211
    $main::syslogit->($user, "info", "$uistatus $obj->{type} image: $obj->{name}: $uuid");
4212
    return $postreply;
4213
}
4214

    
4215
sub Convert {
4216
    my ($image, $action, $obj) = @_;
4217
    if ($help) {
4218
        return <<END
4219
GET:image:
4220
Converts an image to qcow2 format. Image must not be in use.
4221
END
4222
    }
4223
    my $path = $obj->{path};
4224
    $uistatus = "converting";
4225
    $uipath = $path;
4226
    if ($obj->{status} ne "unused" && $obj->{status} ne "used" && $obj->{status} ne "paused") {
4227
        $postreply .= "Status=ERROR Problem $uistatus $obj->{type} image: $obj->{name}\n";
4228
    } elsif ($obj->{type} eq "img" || $obj->{type} eq "vmdk" || $obj->{type} eq "vhd" || $obj->{type} eq "vhdx") {
4229
        my $oldpath = $path;
4230
        my $newpath = "$path.qcow2";
4231
        if ($obj->{mac} && $path =~ /\/mnt\/stabile\/node\//) {
4232
            unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
4233
            $macip = $nodereg{$obj->{mac}}->{'ip'};
4234
            untie %nodereg;
4235
            $oldpath = "$macip:$path";
4236
        } else { # We are not on a node - check that image is not on a read-only filesystem
4237
            my ($fname, $destfolder) = fileparse($path);
4238
            my $ro = `touch "$destfolder/test.tmp" && { rm "$destfolder/test.tmp"; } || echo "read-only" 2>/dev/null`;
4239
            if ($ro) { # Destinationfolder is not writable
4240
                my $npath = "$spools[0]->{'path'}/$register{$path}->{'user'}/$fname.qcow2";
4241
                $newpath = $npath;
4242
            }
4243
            if (-e $newpath) { # Don't overwrite existing file
4244
                my $subpath = substr($newpath,0,-6);
4245
                my $i = 1;
4246
                if ($newpath =~ /(.+)\.(\d+)\.qcow2/) {
4247
                    $i = $2;
4248
                    $subpath = $1;
4249
                }
4250
                while (-e $newpath) {
4251
                    $newpath = $subpath . ".$i.qcow2";
4252
                    $i++;
4253
                }
4254
            }
4255
        }
4256
        eval {
4257
            my $ug = new Data::UUID;
4258
            my $newuuid = $ug->create_str();
4259

    
4260
            $register{$newpath} = {
4261
                uuid=>$newuuid,
4262
                name=>"$obj->{name} (converted)",
4263
                notes=>$obj->{notes},
4264
                image2=>$obj->{image2},
4265
                managementlink=>$obj->{managementlink},
4266
                upgradelink=>$obj->{managementlink},
4267
                terminallink=>$obj->{terminallink},
4268
                storagepool=>$obj->{regstoragepool},
4269
                status=>$uistatus,
4270
                mac=>($obj->{regstoragepool} == -1)?$obj->{mac}:"",
4271
                size=>0,
4272
                realsize=>0,
4273
                virtualsize=>$obj->{virtualsize},
4274
                type=>"qcow2",
4275
                user=>$user
4276
            };
4277
            $register{$path}->{'status'} = $uistatus;
4278

    
4279
            my $daemon = Proc::Daemon->new(
4280
                work_dir => '/usr/local/bin',
4281
                exec_command => "perl -U steamExec $user $uistatus $obj->{status} \"$oldpath\" \"$newpath\""
4282
            ) or do {$postreply .= "Status=ERROR $@\n";};
4283
            my $pid = $daemon->Init() or do {$postreply .= "Status=ERROR $@\n";};
4284
            $postreply .=  "Status=OK $uistatus $obj->{type} image: $obj->{name}\n";
4285
            $main::syslogit->($user, "info", "$uistatus $obj->{type} image: $obj->{name}: $uuid");
4286
        } or do {$postreply .= "Status=ERROR $@\n";};
4287
        $main::updateUI->({tab=>"images", user=>$user, type=>"update"});
4288
    } else {
4289
        $postreply .= "Status=ERROR Only img and vmdk images can be converted\n";
4290
    }
4291
    return $postreply;
4292
}
4293

    
4294
sub Snapshot {
4295
    my ($image, $action, $obj) = @_;
4296
    if ($help) {
4297
        return <<END
4298
GET:image:
4299
Adds a snapshot to a qcow2 image. Image can not be in use by a running server.
4300
END
4301
    }
4302
    my $status = $obj->{status};
4303
    my $path = $obj->{path};
4304
    my $macip;
4305
    $uistatus = "snapshotting";
4306
    $uiuuid = $obj->{uuid};
4307
    if ($status ne "unused" && $status ne "used") {
4308
        $postreply .= "Status=ERROR Problem $uistatus $obj->{type} image: $obj->{name}\n";
4309
    } elsif ($obj->{type} eq "qcow2") {
4310
        my $newpath = $path;
4311
        my $hassnap;
4312
        my $snaptime = time;
4313
        if ($obj->{mac} && $path =~ /\/mnt\/stabile\/node\//) {
4314
            unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
4315
            $macip = $nodereg{$obj->{mac}}->{'ip'};
4316
            untie %nodereg;
4317
            $newpath = "$macip:$path";
4318
            my $esc_path = $path;
4319
            $esc_path =~ s/([ ])/\\$1/g;
4320
            my $qinfo = `$sshcmd $macip "sudo /usr/bin/qemu-img snapshot -l $esc_path"`;
4321
            $hassnap = ($qinfo =~ /snap1/g);
4322
            $postreply .= `$sshcmd $macip "sudo /usr/bin/qemu-img snapshot -d snap1 $esc_path"` if ($hassnap);
4323
        } else {
4324
            my $qinfo = `/usr/bin/qemu-img snapshot -l "$path"`;
4325
            $hassnap = ($qinfo =~ /snap1/g);
4326
            $postreply .= `/usr/bin/qemu-img snapshot -d snap1 "$path\n"` if ($hassnap);
4327
        }
4328
        eval {
4329
            if ($hassnap) {
4330
                $postreply .= "Status=Error Only one snapshot per image is supported for $obj->{type} image: $obj->{name} ";
4331
            } else {
4332
                $register{$path}->{'status'} = $uistatus;
4333
                $register{$path}->{'snap1'} = $snaptime;
4334

    
4335
                if ($macip) {
4336
                    my $esc_localpath = shell_esc_chars($path);
4337
                    $res .= `$sshcmd $macip "sudo /usr/bin/qemu-img snapshot -c snap1 $esc_localpath"`;
4338
                } else {
4339
                    $res .= `/usr/bin/qemu-img snapshot -c snap1 "$path"`;
4340
                }
4341
                $register{$path}->{'status'} = $status;
4342
                $postreply .=  "Status=$uistatus OK $uistatus $obj->{type} image: $obj->{name}\n";
4343
                $main::syslogit->($user, "info", "$uistatus $obj->{type} image: $obj->{name}: $uuid");
4344
            }
4345
            1;
4346
        } or do {$postreply .= "Status=ERROR $@\n";};
4347
        $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$obj->{'uuid'}, status=>$status, snap1=>$snaptime});
4348
    } else {
4349
        $postreply .= "Status=ERROR Only qcow2 images can be snapshotted\n";
4350
    }
4351
    return $postreply;
4352
}
4353

    
4354
sub Unsnap {
4355
    my ($image, $action, $obj) = @_;
4356
    if ($help) {
4357
        return <<END
4358
GET:image:
4359
Removes a snapshot from a qcow2 image. Image can not be in use by a running server.
4360
END
4361
    }
4362
    my $status = $obj->{status};
4363
    my $path = $obj->{path};
4364
    $uistatus = "unsnapping";
4365
    $uiuuid = $obj->{uuid};
4366
    my $macip;
4367

    
4368
    if ($status ne "unused" && $status ne "used") {
4369
        $postreply .= "Status=ERROR Problem $uistatus $obj->{type} image: $obj->{name}\n";
4370
    } elsif ($obj->{type} eq "qcow2") {
4371
        my $newpath = $path;
4372
        my $hassnap;
4373
        my $qinfo;
4374
        my $esc_path;
4375
        if ($obj->{mac} && $path =~ /\/mnt\/stabile\/node\//) {
4376
            unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
4377
            $macip = $nodereg{$obj->{mac}}->{'ip'};
4378
            untie %nodereg;
4379
            $newpath = "$macip:$path";
4380
            $esc_path = $path;
4381
            $esc_path =~ s/([ ])/\\$1/g;
4382
            $qinfo = `$sshcmd $macip "sudo /usr/bin/qemu-img snapshot -l $esc_path"`;
4383
            $hassnap = ($qinfo =~ /snap1/g);
4384
        } else {
4385
            $qinfo = `/usr/bin/qemu-img snapshot -l "$path"`;
4386
            $hassnap = ($qinfo =~ /snap1/g);
4387
        }
4388
        eval {
4389
            my $snaptime = time;
4390
            if ($hassnap) {
4391
                delete $register{$path}->{'snap1'};
4392
                $register{$path}->{'status'} = $uistatus;
4393
                if ($macip) {
4394
                    my $esc_localpath = shell_esc_chars($path);
4395
                    $res .= `$sshcmd $macip "sudo /usr/bin/qemu-img snapshot -d snap1 $esc_localpath"`;
4396
                } else {
4397
                    $res .= `/usr/bin/qemu-img snapshot -d snap1 "$path"`;
4398
                }
4399
                $register{$path}->{'status'} = $status;
4400
                $postreply .=  "Status=$uistatus OK $uistatus $obj->{type} image: $obj->{name}\n";
4401
                $main::syslogit->($user, "info", "$uistatus $obj->{type} image: $obj->{name}: $uuid");
4402
            } else {
4403
                $postreply .= "Status=ERROR No snapshot found in $obj->{name}\n";
4404
                delete $register{$path}->{'snap1'};
4405
                $uistatus = $status;
4406
            }
4407
            1;
4408
        } or do {$postreply .= "Status=ERROR $@\n";};
4409
        $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$obj->{'uuid'}, status=>$status, snap1=>'--'});
4410
    } else {
4411
        $postreply .= "Status=ERROR Only qcow2 images can be unsnapped\n";
4412
    }
4413
    return $postreply;
4414
}
4415

    
4416
sub Revert {
4417
    my ($image, $action, $obj) = @_;
4418
    if ($help) {
4419
        return <<END
4420
GET:image:
4421
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.
4422
END
4423
    }
4424
    my $status = $obj->{status};
4425
    my $path = $obj->{path};
4426
    $uistatus = "reverting";
4427
    $uipath = $path;
4428
    my $macip;
4429
    if ($status ne "used" && $status ne "unused") {
4430
        $postreply .= "Status=ERROR Please shut down or pause your virtual machine before reverting\n";
4431
    } elsif ($obj->{type} eq "qcow2") {
4432
        my $newpath = $path;
4433
        my $hassnap;
4434
        if ($obj->{mac} && $path =~ /\/mnt\/stabile\/node\//) {
4435
            unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
4436
            $macip = $nodereg{$obj->{mac}}->{'ip'};
4437
            untie %nodereg;
4438
            $newpath = "$macip:$path";
4439
            my $esc_path = $path;
4440
            $esc_path =~ s/([ ])/\\$1/g;
4441
            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"`;
4442
            $hassnap = ($qinfo =~ /snap1/g);
4443
        } else {
4444
            my $qinfo = `/usr/bin/qemu-img snapshot -l "$path"`;
4445
            $hassnap = ($qinfo =~ /snap1/g);
4446
        }
4447
        eval {
4448
            if ($hassnap) {
4449
                $register{$path}->{'status'} = $uistatus;
4450
                if ($macip) {
4451
                    my $esc_localpath = shell_esc_chars($path);
4452
                    $res .= `$sshcmd $macip "sudo /usr/bin/qemu-img snapshot -a snap1 $esc_localpath"`;
4453
                } else {
4454
                    $res .= `/usr/bin/qemu-img snapshot -a snap1 "$path"`;
4455
                }
4456
                $register{$path}->{'status'} = $status;
4457
                $postreply .=  "Status=OK $uistatus $obj->{type} image: $obj->{name}\n";
4458
                $main::syslogit->($user, "info", "$uistatus $obj->{type} image: $obj->{name}: $uuid");
4459
            } else {
4460
                $postreply .= "Status=ERROR no snapshot found\n";
4461
                $uistatus = $status;
4462
            }
4463
            1;
4464
        } or do {$postreply .= "Status=ERROR $@\n";};
4465
        $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$obj->{'uuid'}, status=>$status, snap1=>'--'});
4466
    } else {
4467
        $postreply .= "Status=ERROR Only qcow2 images can be reverted\n";
4468
    }
4469
    return;
4470
}
4471

    
4472
sub Zbackup {
4473
    my ($image, $action, $obj) = @_;
4474
    if ($help) {
4475
        return <<END
4476
GET:mac, storagepool, synconly, snaponly, imageretention, backupretention:
4477
Backs all images on ZFS storage up by taking a storage snapshot. By default all shared storagepools are backed up.
4478
If storagepool -1 is specified, all ZFS node storages is backed up. If "mac" is specified, only specific node is backed up.
4479
If "synconly" is set, no new snapshots are taken - only syncing of snapshots is performed.
4480
If "snaponly" is set, only local active storage snapshot is taken - no sending to backup storage is done.
4481
"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],
4482
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.
4483
END
4484
    }
4485
    if ($isadmin) {
4486
        my $synconly = $obj->{'synconly'};
4487
        my $snaponly = $obj->{'snaponly'};
4488
        my $mac = $obj->{'mac'};
4489
        my $storagepool = $obj->{'storagepool'};
4490
        $storagepool = -1 if ($mac);
4491
        my $imageretention = $obj->{'imageretention'} || $imageretention;
4492
        my $backupretention = $obj->{'backupretention'} || $backupretention;
4493

    
4494
        my $basepath = "stabile-backup";
4495
        my $bpath = $basepath;
4496
        my $mounts = `/bin/cat /proc/mounts`;
4497
        my $zbackupavailable = (($mounts =~ /$bpath (\S+) zfs/)?$1:'');
4498
        unless ($zbackupavailable) {$postreply .= "Status=OK ZFS backup not available, only doing local snapshots\n";}
4499
        my $zfscmd = "zfs";
4500
        my $macip;
4501
        my $ipath = $spools[0]->{'zfs'} || 'stabile-images/images';
4502
        my @nspools = @spools;
4503
        if (!(defined $obj->{'storagepool'}) || $storagepool == -1) {
4504
            @nspools = () if ($storagepool == -1); # Only do node backups
4505
            unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
4506
#            my $nipath = $ipath;
4507
#            $nipath = "$1/node" if ($nipath =~ /(.+)\/(.+)/);
4508
            my $nipath = 'stabile-node/node';
4509
            foreach my $node (values %nodereg) {
4510
                push @nspools, {
4511
                    mac=>$node->{'mac'},
4512
                    macip=>$node->{'ip'},
4513
                    zfs=>$nipath,
4514
                    id=>-1
4515
                } if ($node->{'stor'} eq 'zfs' && (!$mac || $node->{'mac'} eq $mac))
4516
            }
4517
            untie %nodereg;
4518
        }
4519
        if (`pgrep zfs`) {
4520
            $postreply .= "Status=ERROR Another ZFS backup is running. Please wait a minute...\n";
4521
            $postmsg = "ERROR ERROR Another ZFS backup is running. Please wait a minute...";
4522
            return $postreply;
4523
        }
4524
        $postreply .= "Status=OK Performing ZFS backup on " . (scalar @nspools) . " storage pools with image retention $imageretention, backup retention $backupretention\n";
4525

    
4526
        foreach my $spool (@nspools) {
4527
            $ipath = $spool->{'zfs'};
4528
            if ($spool->{'id'} == -1) { # We're doing a node backup
4529
                $mac = $spool->{'mac'};
4530
                $macip = $spool->{'macip'};
4531
                $bpath = "$basepath/node-$mac";
4532
            } else {
4533
                next unless ($ipath);
4534
                next if (($storagepool || $storagepool eq '0') && $storagepool ne $spool->{'id'});
4535
                $bpath = "$basepath/$1" if ($ipath =~ /.+\/(.+)/);
4536
                $mac = '';
4537
                $macip = '';
4538
            }
4539
            if ($macip) {$zfscmd = "$sshcmd $macip sudo zfs";}
4540
            else {$zfscmd = "zfs";}
4541

    
4542
            $postreply .= "Status=OK Commencing ZFS backup of $ipath $macip, storagepool=$storagepool, synconly=$synconly, snaponly=$snaponly\n";
4543
            my $res;
4544
            my $cmd;
4545
            my @imagesnaps;
4546
            my @backupsnaps;
4547

    
4548
            # example: stabile-images/images@SNAPSHOT-20200524172901
4549
            $cmd = qq/$zfscmd list -t snapshot | grep '$ipath'/;
4550
            my $snaplist = `$cmd`;
4551
            my @snaplines = split("\n", $snaplist);
4552
            foreach my $snap (@snaplines) {
4553
                push @imagesnaps, $2 if ($snap =~ /(.*)\@SNAPSHOT-(\d+)/);
4554
            }
4555
            if ($zbackupavailable) {
4556
                $cmd = qq/zfs list -t snapshot | grep '$bpath'/;
4557
                $snaplist = `$cmd`;
4558
                @snaplines = split("\n", $snaplist);
4559
                foreach my $snap (@snaplines) {
4560
                    push @backupsnaps, $2 if ($snap =~ /(.*)\@SNAPSHOT-(\d+)/);
4561
                }
4562
            }
4563
        # Find matching snapshots
4564
            my $matches=0;
4565
            my $matchbase = 0;
4566
            foreach my $bsnap (@backupsnaps) {
4567
                if ($bsnap eq $imagesnaps[$matchbase + $matches]) { # matching snapshot found
4568
                    $matches++;
4569
                } elsif ($matches) { # backup snapshots are ahead of image snapshots - correct manually, i.e. delete them.
4570
                    $postreply .= "Status=ERROR Snapshots are out of sync.\n";
4571
                    $postmsg = "ERROR Snapshots are out of sync";
4572
                    $main::syslogit->($user, 'info', "ERROR snapshots of $ipath and $bpath are out of sync.");
4573
                    return $postreply;
4574
                } elsif (!$matchbase) { # Possibly there are image snapshots older than there are backup snapshots, find the match base i.e. first match in @imagesnaps
4575
                    my $mb=0;
4576
                    foreach my $isnap (@imagesnaps) {
4577
                        if ($bsnap eq $isnap) { # matching snapshot found
4578
                            $matchbase = $mb;
4579
                            $matches++;
4580
                            last;
4581
                        }
4582
                        $mb++;
4583
                    }
4584
                }
4585
            }
4586

    
4587
            my $lastisnap = $imagesnaps[scalar @imagesnaps -1];
4588
            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)/);
4589
            my $td = ($current_time - $lastisnaptime);
4590
            if ($td<=5) {
4591
                $postreply .= "Status=ERROR Last backup was taken $td seconds ago. Please wait a minute...\n";
4592
                $postmsg = "ERROR ERROR Last backup was taken $td seconds ago. Please wait a minute...";
4593
                return $postreply;
4594
            }
4595
            my $ni = scalar @imagesnaps;
4596
            my $nb = scalar @backupsnaps;
4597

    
4598
            # If there are unsynced image snaps - sync them
4599
            if ($zbackupavailable && !$snaponly) {
4600
                if (scalar @imagesnaps > $matches+$matchbase) {
4601
                    if ($matches > 0) { # We must have at least one common shapshot to sync
4602
                        for (my $j=$matches+$matchbase; $j < scalar @imagesnaps; $j++) {
4603
                            if ($macip) {
4604
                                $cmd = qq[$zfscmd "send -i $ipath\@SNAPSHOT-$imagesnaps[$j-1] $ipath\@SNAPSHOT-$imagesnaps[$j] | ssh 10.0.0.1 sudo zfs receive $bpath"]; # -R
4605
                            } else {
4606
                                $cmd = qq[zfs send -i $ipath\@SNAPSHOT-$imagesnaps[$j-1] $ipath\@SNAPSHOT-$imagesnaps[$j] | zfs receive $bpath]; # -R
4607
                            }
4608
                            $res = `$cmd 2>&1`;
4609
                            unless (
4610
                                ($res && !$macip) #ssh will warn about adding to list of known hosts
4611
                                    || $res =~ /cannot receive/
4612
                            ) {
4613
                                $matches++;
4614
                                $nb++;
4615
                                $postreply .= "Status=OK Sending ZFS snapshot $j $imagesnaps[$j-1]->$imagesnaps[$j] of $macip $ipath to $bpath $res\n";
4616
                                $main::syslogit->($user, 'info', "OK Sending ZFS snapshot $imagesnaps[$j-1]->$imagesnaps[$j] of $macip $ipath to $bpath $res");
4617
                            } else {
4618
                                $postreply .= "Status=Error Problem sending ZFS snapshot $j $imagesnaps[$j-1]->$imagesnaps[$j] of $macip $ipath to $bpath $res\n";
4619
                                $main::syslogit->($user, 'info', "Error Problem sending ZFS snapshot $imagesnaps[$j-1]->$imagesnaps[$j] of $macip $ipath to $bpath $res");
4620
                            }
4621
                        }
4622
                    } else {
4623
                        $postreply .= "Status=OK Unable to sync $ni snapshots, no common snapshot, trying to start from scratch.\n";
4624
                    }
4625
                }
4626
            }
4627
            $res = '';
4628

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

    
4788
sub Backupfuel {
4789
    my ($image, $action, $obj) = @_;
4790
    if ($help) {
4791
        return <<END
4792
GET:username, dozfs:
4793
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.
4794
END
4795
    }
4796
    my $username = $obj->{'username'} || $user;
4797
    return "Status=Error Not allowed\n" unless ($isadmin || $username eq $user);
4798

    
4799
    my $remolder = "14D";
4800
    my $stordevs = Liststoragedevices('', 'getstoragedevices');
4801
    my $backupdev = Getbackupdevice('', 'getbackupdevice');
4802
    my $backupdevtype = $stordevs->{$backupdev}->{type};
4803
    foreach my $spool (@spools) {
4804
        my $ppath = $spool->{"path"};
4805
        my $pid = $spool->{"id"};
4806
        if (($spool->{"zfs"} && $backupdevtype eq 'zfs') && !$obj->{'dozfs'}) {
4807
            $postreply .= "Status=OK Skipping fuel on ZFS storage: $ppath/$username/fuel\n";
4808
        } elsif ($pid eq '-1') {
4809
            ;
4810
        } elsif (!$backupdir || !(-d $backupdir)) {
4811
            $postreply .= "Status=OK Backup dir $backupdir does not exist\n";
4812
        } elsif (-d "$ppath/$username/fuel" && !is_folder_empty("$ppath/$username/fuel")) {
4813
            my $srcdir = "$ppath/$username/fuel";
4814
            my $destdir = "$backupdir/$username/fuel/$pid";
4815

    
4816
            `mkdir -p "$destdir"` unless (-e "$destdir");
4817
            # Do the backup
4818
            my $cmd = qq|/usr/bin/rdiff-backup --print-statistics "$srcdir" "$destdir"|;
4819
            my $res = `$cmd`;
4820
            $cmd = qq|/usr/bin/rdiff-backup --print-statistics --force --remove-older-than $remolder "$destdir"|;
4821
            $res .= `$cmd`;
4822
            if ($res =~ /Errors 0/) {
4823
                my $change = $1 if ($res =~ /TotalDestinationSizeChange \d+ \((.+)\)/);
4824
                $postreply .= "Status=OK Backed up $change, $srcdir -> $destdir\n";
4825
                $main::syslogit->($user, "info", "OK backed up $change, $srcdir -> $destdir") if ($change);
4826
            } else {
4827
                $res =~ s/\n/ /g;
4828
                $postreply .= "Status=Error There was a problem backup up $srcdir -> $destdir: $res\n";
4829
                $main::syslogit->($user, "there was a problem backup up $srcdir -> $destdir");
4830
            }
4831
        } else {
4832
            $postreply .= "Status=OK Skipping empty fuel on: $ppath/$username/fuel\n";
4833
        }
4834
    }
4835
    return $postreply;
4836
}
4837

    
4838
sub is_folder_empty {
4839
    my $dirname = shift;
4840
    opendir(my $dh, $dirname) or die "Not a directory";
4841
    return scalar(grep { $_ ne "." && $_ ne ".." } readdir($dh)) == 0;
4842
}
4843

    
4844
sub Backup {
4845
    my ($image, $action, $obj) = @_;
4846
    if ($help) {
4847
        return <<END
4848
GET:image, skipzfs:
4849
Backs an image up. Set [skipzfs] if ZFS backup is configured, and you want to skip images on ZFS storage.
4850
END
4851
    }
4852
    my $path = $obj->{path} || $image;
4853
    my $status = $obj->{status};
4854
    my $skipzfs = $obj->{skipzfs};
4855
    $uistatus = "backingup";
4856
    $uipath = $path;
4857
    my $remolder;
4858
    $remolder = "14D" if ($obj->{bschedule} eq "daily14");;
4859
    $remolder = "7D" if ($obj->{bschedule} eq "daily7");
4860
    my $breply = '';
4861

    
4862
    my $stordevs = Liststoragedevices('', 'getstoragedevices');
4863
    my $backupdev = Getbackupdevice('', 'getbackupdevice');
4864
    my $backupdevtype = $stordevs->{$backupdev}->{type};
4865
    # Nodes are assumed to alwasy use ZFS
4866
    if ($backupdevtype eq 'zfs' && $skipzfs && ($obj->{regstoragepool} == -1 || $spools[$obj->{regstoragepool}]->{'zfs'})) {
4867
        return "Status=OK Skipping image on ZFS $path\n";
4868
    }
4869
    if ($status eq "snapshotting" || $status eq "unsnapping" || $status eq "reverting" || $status eq "cloning" ||
4870
        $status eq "moving" || $status eq "converting") {
4871
        $breply .= "Status=ERROR Problem backing up $obj->{type} image $obj->{name}\n";
4872
    } elsif ($obj->{regstoragepool} == -1) {
4873
        my $res = createNodeTask($obj->{mac}, "BACKUP $user $uistatus $status \"$path\" \"$backupdir\" $remolder", $status,  '', $path);
4874
        if ($res) {
4875
            $breply .= "Status=ERROR Suspend serverer befora backing up (image $obj->{name} is not on an LVM partition)\n";
4876
        } else {
4877
            $register{$path}->{'status'} = $uistatus;
4878
            $uistatus = "lbackingup" if ($status eq "active"); # Do lvm snapshot before backing up
4879
            $main::syslogit->($user, "info", "$uistatus $obj->{type} image: $obj->{name}: $uuid");
4880
            $breply .= "Status=backingup OK backingup image: $obj->{name} (on node)\n";
4881
        }
4882
    } elsif (!$spools[$obj->{regstoragepool}]->{'rdiffenabled'}) {
4883
        $breply .= "Status=ERROR Rdiff-backup has not been enabled for this storagepool ($spools[$obj->{regstoragepool}]->{'name'})\n";
4884
    } else {
4885
        if ($spools[$obj->{regstoragepool}]->{'hostpath'} eq "local" && $status eq "active") {
4886
            my $poolpath = $spools[$obj->{regstoragepool}]->{'path'};
4887
            # We only need to worry about taking an LVM snapshot if the image is in active use
4888
            # We also check if the images is actually on an LVM partition
4889
            my $qi = `/bin/cat /proc/mounts | grep "$poolpath"`; # Find the lvm volume mounted on /mnt/images
4890
            ($qi =~ m/\/dev\/mapper\/(\S+)-(\S+) $pool.+/g)[-1]; # Select last match
4891
            my $lvolgroup = $1;
4892
            my $lvol = $2;
4893
            if ($lvolgroup && $lvol) {
4894
                $uistatus = "lbackingup";
4895
            }
4896
        }
4897
        if ($uistatus ne "lbackingup" && $status eq "active") {
4898
            $breply .= "Status=ERROR Suspend serverer befora backing up (image $obj->{name} is not on an LVM partition)\n";
4899
        #    $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"});
4900
        } else {
4901
            my $buser;
4902
            my $bname;
4903
            if ($path =~ /.*\/(common|$user)\/(.+)/) {
4904
                $buser = $1;
4905
                $bname = $2;
4906
            }
4907
            if ($buser && $bname) {
4908
                my $dirpath = $spools[$obj->{regstoragepool}]->{'path'};
4909
                #chop $dirpath; # Remove last /
4910
                eval {
4911
                    $register{$path}->{'status'} = $uistatus;
4912
                    my $daemon = Proc::Daemon->new(
4913
                        work_dir => '/usr/local/bin',
4914
                        exec_command => "perl -U steamExec $buser $uistatus $status \"$bname\" \"$dirpath\" \"$backupdir\" $remolder"
4915
                    ) or do {$breply .= "Status=ERROR $@\n";};
4916
                    my $pid = $daemon->Init();
4917
                    $breply .=  "Status=backingup OK backingup image: $obj->{name}\n";
4918
                    $main::syslogit->($user, "info", "$uistatus $obj->{type} image: $obj->{name}: $bname");
4919
                    1;
4920
                } or do {$breply .= "Status=ERROR $@\n";}
4921
            } else {
4922
                $breply .= "Status=ERROR Problem backing up $path\n";
4923
            }
4924
        }
4925
    }
4926
    return $breply;
4927
}
4928

    
4929
sub Restore {
4930
    my ($image, $action, $obj) = @_;
4931
    if ($help) {
4932
        return <<END
4933
GET:image:
4934
Backs an image up.
4935
END
4936
    }
4937
    my $path = $obj->{path};
4938
    my $status = $obj->{status};
4939
    $uistatus = "restoring";
4940
    my($bname, $dirpath, $suffix) = fileparse($path, (".vmdk", ".img", ".vhd", ".vhdx", ".qcow", ".qcow2", ".vdi", ".iso"));
4941
    my $backup = $params{"backup"} || $obj->{backup};
4942
    my $pool = $register{$path}->{'storagepool'};
4943
    $pool = "0" if ($pool == -1);
4944
    my $poolpath = $spools[$pool]->{'path'};
4945
    my $restorefromdir = $backupdir;
4946
    my $inc = $backup;
4947
    my $subdir; # 1 level of subdirs supported
4948
    $subdir = $1 if ($dirpath =~ /.+\/$obj->{user}(\/.+)?\//);
4949

    
4950
    if ($backup =~ /^SNAPSHOT-(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/) { # We are dealing with a zfs restore
4951
        $inc = "$1-$2-$3-$4-$5-$6";
4952
        foreach my $spool (@spools) {
4953
            my $ppath = $spool->{"path"};
4954
            if (-e "$ppath/.zfs/snapshot/$backup/$obj->{user}$subdir/$bname$suffix") {
4955
                $restorefromdir = "$ppath/.zfs/snapshot/$backup";
4956
                last;
4957
            }
4958
        }
4959
    } else {
4960
        if ($backup eq "mirror") {
4961
            my $mir = `/bin/ls "$backupdir/$obj->{user}/$bname$suffix/rdiff-backup-data" | grep current_mirror`;
4962
            if ($mir =~ /current_mirror\.(\S+)\.data/) {
4963
                $inc = $1;
4964
            }
4965
        }
4966
        $inc =~ tr/:T/-/; # qemu-img does not like colons in file names - go figure...
4967
        $inc = substr($inc,0,-6);
4968
    }
4969
    $uipath = "$poolpath/$obj->{user}$subdir/$bname.$inc$suffix";
4970
    my $i;
4971
    if (-e $uipath) {
4972
        $i = 1;
4973
        while (-e "$poolpath/$obj->{user}$subdir/$bname.$inc.$i$suffix") {$i++;}
4974
        $uipath = "$poolpath/$obj->{user}$subdir/$bname.$inc.$i$suffix";
4975
    }
4976

    
4977
    if (-e $uipath) {
4978
        $postreply .= "Status=ERROR This image is already being restored\n";
4979
    } elsif ($obj->{user} ne $user && !$isadmin) {
4980
        $postreply .= "Status=ERROR No restore privs\n";
4981
    } elsif (!$backup || $backup eq "--") {
4982
        $postreply .= "Status=ERROR No backup selected\n";
4983
    } elsif (overQuotas($obj->{virtualsize})) {
4984
        $postreply .= "Status=ERROR Over quota (". overQuotas($obj->{virtualsize}) . ") restoring: $obj->{name}\n";
4985
    } elsif (overStorage($obj->{ksize}*1024, $pool+0)) {
4986
        $postreply .= "Status=ERROR Out of storage in destination pool restoring: $obj->{name}\n";
4987
    } else {
4988
        my $ug = new Data::UUID;
4989
        my $newuuid = $ug->create_str();
4990
        $register{$uipath} = {
4991
            uuid=>$newuuid,
4992
            status=>"restoring",
4993
            name=>"$obj->{name} ($inc)" . (($i)?" $i":''),
4994
            notes=>$obj->{notes},
4995
            image2=>$obj->{image2},
4996
            managementlink=>$obj->{managementlink},
4997
            upgradelink=>$obj->{upgradelink},
4998
            terminallink=>$obj->{terminallink},
4999
            size=>0,
5000
            realsize=>0,
5001
            virtualsize=>$obj->{virtualsize},
5002
            type=>$obj->{type},
5003
            user=>$user
5004
        };
5005
        eval {
5006
            $register{$path}->{'status'} = $uistatus;
5007
            my $daemon = Proc::Daemon->new(
5008
                work_dir => '/usr/local/bin',
5009
                exec_command => "perl -U steamExec $obj->{user} $uistatus $status \"$path\" \"$restorefromdir\" \"$backup\" \"$uipath\""
5010
            ) or do {$postreply .= "Status=ERROR $@\n";};
5011
            my $pid = $daemon->Init();
5012
            $postreply .=  "Status=$uistatus OK $uistatus $obj->{type} image: $obj->{name} ($inc)". ($console?", $newuuid\n":"\n");
5013
            $main::syslogit->($user, "info", "$uistatus $obj->{type} image: $obj->{name} ($inc), $uipath, $newuuid: $uuid");
5014
            1;
5015
        } or do {$postreply .= "Status=ERROR $@\n";};
5016
        $main::updateUI->({tab=>"images", user=>$user, type=>"update"});
5017
    }
5018
    return $postreply;
5019
}
5020

    
5021
sub Master {
5022
    my ($image, $action, $obj) = @_;
5023
    if ($help) {
5024
        return <<END
5025
GET:image:
5026
Converts an image to a master image. Image must not be in use.
5027
END
5028
    }
5029
    my $path = $obj->{path};
5030
    my $status = $register{$path}->{status};
5031
    $path =~ /(.+)\.$obj->{type}$/;
5032
    my $namepath = $1;
5033
    my $uiname;
5034
    if (!$register{$path}) {
5035
        $postreply .= "Status=ERROR Image $path not found\n";
5036
    } elsif ($status ne "unused") {
5037
        $postreply .= "Status=ERROR Only unused images may be mastered\n";
5038
#    } elsif ($namepath =~ /(.+)\.master/ || $register{$path}->{'master'}) {
5039
#        $postreply .= "Status=ERROR Only one level of mastering is supported\n";
5040
    } elsif ($obj->{istoragepool} == -1 || $obj->{regstoragepool} == -1) {
5041
        $postreply .= "Status=ERROR Unable to master $obj->{name} (master images are not supported on node storage)\n";
5042
    } elsif ($obj->{type} eq "qcow2") {
5043
        # Promoting a regular image to master
5044
        # First find an unused path
5045
        if (-e "$namepath.master.$obj->{type}") {
5046
            my $i = 1;
5047
            while ($register{"$namepath.$i.master.$obj->{type}"} || -e "$namepath.$i.master.$obj->{type}") {$i++;};
5048
            $uinewpath = "$namepath.$i.master.$obj->{type}";
5049
        } else {
5050
            $uinewpath = "$namepath.master.$obj->{type}";
5051
        }
5052

    
5053
        $uipath = $path;
5054
        $uiname = "$obj->{name}";
5055
        eval {
5056
            my $qinfo = `/bin/mv -iv "$path" "$uinewpath"`;
5057
            $register{$path}->{'name'} = $uiname;
5058
            $register{$uinewpath} = $register{$path};
5059
            delete $register{$path};
5060
            $postreply .= "Status=$status Mastered $obj->{type} image: $obj->{name}\n";
5061
            chop $qinfo;
5062
            $main::syslogit->($user, "info", $qinfo);
5063
            1;
5064
        } or do {$postreply .= "Status=ERROR $@\n";};
5065
        sleep 1;
5066
        $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$obj->{'uuid'}, newpath=>$uinewpath, status=>$status, name=>$uiname});
5067
    } else {
5068
        $postreply .= "Status=ERROR Only qcow2 images may be mastered\n";
5069
    }
5070
    return $postreply;
5071
}
5072

    
5073
sub Unmaster {
5074
    my ($image, $action, $obj) = @_;
5075
    if ($help) {
5076
        return <<END
5077
GET:image:
5078
Converts a master image to a regular image. Image must not be in use.
5079
END
5080
    }
5081
    my $path = $obj->{path};
5082
    my $status = $register{$path}->{status};
5083
    $path =~ /(.+)\.$obj->{type}$/;
5084
    my $namepath = $1;
5085
    my $haschildren = 0;
5086
    my $child;
5087
    my $uinewpath;
5088
    my $iname;
5089
    my @regvalues = values %register;
5090
    foreach my $val (@regvalues) {
5091
        if ($val->{'master'} eq $path) {
5092
            $haschildren = 1;
5093
            $child = $val->{'name'};
5094
            last;
5095
        }
5096
    }
5097
    if (!$register{$path}) {
5098
        $postreply .= "Status=ERROR Image $path not found\n";
5099
    } elsif ($haschildren) {
5100
        $postreply .= "Status=Error Cannot unmaster image. This image is used as master by: $child\n";
5101
    } elsif ($status ne "unused" && $status ne "used") {
5102
        $postreply .= "Status=ERROR Only used and unused images may be unmastered\n";
5103
    } elsif (!( ($namepath =~ /(.+)\.master/) || ($obj->{master} && $obj->{master} ne "--")) ) {
5104
        $postreply .= "Status=ERROR You can only unmaster master or child images\n";
5105
    } elsif (($obj->{istoragepool} == -1 || $obj->{regstoragepool} == -1) && $namepath =~ /(.+)\.master/) {
5106
        $postreply .= "Status=ERROR Unable to unmaster $obj->{name} (master images are not supported on node storage)\n";
5107
    } elsif ($obj->{type} eq "qcow2") {
5108
        # Demoting a master to regular image
5109
        if ($action eq 'unmaster' && $namepath =~ /(.+)\.master$/) {
5110
            $namepath = $1;
5111
            $uipath = $path;
5112
            # First find an unused path
5113
            if (-e "$namepath.$obj->{type}") {
5114
                my $i = 1;
5115
                while ($register{"$namepath.$i.$obj->{type}"} || -e "$namepath.$i.$obj->{type}") {$i++;};
5116
                $uinewpath = "$namepath.$i.$obj->{type}";
5117
            } else {
5118
                $uinewpath = "$namepath.$obj->{type}";
5119
            }
5120

    
5121
            $iname = $obj->{name};
5122
            $iname =~ /(.+)( \(master\))/;
5123
            $iname = $1 if $2;
5124
            eval {
5125
                my $qinfo = `/bin/mv -iv "$path" "$uinewpath"`;
5126
                $register{$path}->{'name'} = $iname;
5127
                $register{$uinewpath} = $register{$path};
5128
                delete $register{$path};
5129
                $postreply .=  "Status=$status Unmastered $obj->{type} image: $obj->{name}\n";
5130
                chomp $qinfo;
5131
                $main::syslogit->($user, "info", $qinfo);
5132
                1;
5133
            } or do {$postreply .= "Status=ERROR $@\n";}
5134
    # Rebasing a child image
5135
        } elsif ($action eq 'rebase' && $obj->{master} && $obj->{master} ne "--") {
5136
            $uistatus = "rebasing";
5137
            $uipath = $path;
5138
            $iname = $obj->{name};
5139
            $iname =~ /(.+)( \(child\d*\))/;
5140
            $iname = $1 if $2;
5141
            my $temppath = "$path.temp";
5142
            $uipath = $path;
5143
            $uimaster = "--";
5144
            my $macip;
5145

    
5146
            if ($obj->{mac} && $path =~ /\/mnt\/stabile\/node\//) {
5147
                unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
5148
                $macip = $nodereg{$obj->{mac}}->{'ip'};
5149
                untie %nodereg;
5150
            }
5151
            eval {
5152
                my $master = $register{$path}->{'master'};
5153
                my $usedmaster = '';
5154
#                @regvalues = values %register;
5155
                if ($master && $master ne '--') {
5156
                    foreach my $valref (@regvalues) {
5157
                        $usedmaster = 1 if ($valref->{'master'} eq $master && $valref->{'path'} ne $path); # Check if another image is also using this master
5158
                    }
5159
                }
5160
                $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$obj->{'uuid'}, status=>$uistatus});
5161
                $register{$path} = {
5162
                    master=>"",
5163
                    name=>"$iname",
5164
                    notes=>$obj->{notes},
5165
                    status=>$uistatus,
5166
                    snap1=>$obj->{snap1},
5167
                    managementlink=>$obj->{managementlink},
5168
                    upgradelink=>$obj->{upgradelink},
5169
                    terminallink=>$obj->{terminallink},
5170
                    image2=>$obj->{image2},
5171
                    storagepool=>$obj->{istoragepool},
5172
                    status=>$uistatus
5173
                };
5174

    
5175
                if ($macip) {
5176
                    my $esc_localpath = shell_esc_chars($path);
5177
                    my $esc_localpath2 = shell_esc_chars($temppath);
5178
                    $res .= `$sshcmd $macip "/usr/bin/qemu-img convert $esc_localpath -O qcow2 $esc_localpath2"`;
5179
                    $res .= `$sshcmd $macip "if [ -f $esc_localpath2 ]; then /bin/mv -v $esc_localpath2 $esc_localpath; fi"`;
5180
                } else {
5181
                    $res .= `/usr/bin/qemu-img convert -O qcow2 "$path" "$temppath"`;
5182
                    $res .= `if [ -f "$temppath" ]; then /bin/mv -v "$temppath" "$path"; fi`;
5183
                }
5184
                if ($master && !$usedmaster) {
5185
                    $register{$master}->{'status'} = 'unused';
5186
                    $main::syslogit->('info', "Freeing master $master");
5187
                }
5188
                $register{$path}->{'master'} = '';
5189
                $register{$path}->{'status'} = $status;
5190

    
5191
                $postreply .= "Status=OK $uistatus $obj->{type} image: $obj->{name}\n";
5192
                $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$obj->{'uuid'}, status=>$status});
5193
                $main::syslogit->($user, "info", "$uistatus $obj->{type} image: $obj->{name}: $uuid");
5194
                1;
5195
            } or do {$postreply .= "Status=ERROR $@\n";}
5196
        } else {
5197
            $postreply .= "Status=ERROR Not a master, not a child \"$obj->{name}\"\n";
5198
        }
5199
        sleep 1;
5200
        $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$obj->{'uuid'}, newpath=>$uinewpath, name=>$iname, status=>$status});
5201
    } else {
5202
        $postreply .= "Status=ERROR Only qcow2 images may be unmastered\n";
5203
    }
5204
    return $postreply;
5205
}
5206

    
5207
# Save or create new image
5208
sub Save {
5209
    my ($img, $action, $obj) = @_;
5210
    if ($help) {
5211
        return <<END
5212
POST:path, uuid, name, type, virtualsize, storagepool, user:
5213
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.
5214
Depending on your privileges not all changes are permitted. If you save without specifying a uuid or path, a new image is created.
5215
END
5216
    }
5217
    my $path = $obj->{path};
5218
    my $uuid = $obj->{uuid};
5219
    my $status = $obj->{status};
5220
    if ($status eq "new") {
5221
        # Create new image
5222
        my $ug = new Data::UUID;
5223
        if (!$uuid || $uuid eq '--') {
5224
            $uuid = $ug->create_str();
5225
        } else { # Validate
5226
            my $valuuid  = $ug->from_string($uuid);
5227
            if ($ug->to_string($valuuid) eq $uuid) {
5228
                ;
5229
            } else {
5230
                $uuid = $ug->create_str();
5231
            }
5232
        }
5233
        my $newuuid = $uuid;
5234
        my $pooldir = $spools[$obj->{storagepool}]->{'path'};
5235
        my $cmd;
5236
        my $name = $obj->{name};
5237
        $name =~ s/\./_/g; # Remove unwanted chars
5238
        $name =~ s/\//_/g;
5239
        eval {
5240
            my $ipath = "$pooldir/$user/$name.$obj->{type}";
5241
            $obj->{type} = "qcow2" unless ($obj->{type});
5242
            # Find an unused path
5243
            if ($register{$ipath} || -e "$ipath") {
5244
                my $i = 1;
5245
                while ($register{"$pooldir/$user/$name.$i.$obj->{type}"} || -e "$pooldir/$user/$name.$i.$obj->{type}") {$i++;};
5246
                $ipath = "$pooldir/$user/$name.$i.$obj->{type}";
5247
                $name = "$name.$i";
5248
            }
5249

    
5250
            if ($obj->{type} eq 'qcow2' || $obj->{type} eq 'vmdk') {
5251
                my $size = ($obj->{msize})."M";
5252
                my $format = "qcow2";
5253
                $format = "vmdk" if ($path1 =~ /\.vmdk$/);
5254
                $cmd = qq|/usr/bin/qemu-img create -f $format "$ipath" "$size"|;
5255
            } elsif ($obj->{type} eq 'img') {
5256
                my $size = ($obj->{msize})."M";
5257
                $cmd = qq|/usr/bin/qemu-img create -f raw "$ipath" "$size"|;
5258
            } elsif ($obj->{type} eq 'vdi') {
5259
                my $size = $obj->{msize};
5260
                $cmd = qq|/usr/bin/VBoxManage createhd --filename "$ipath" --size "$size" --format VDI|;
5261
            }
5262
            $obj->{name} = 'New Image' if (!$obj->{name} || $obj->{name} eq '--' || $obj->{name} =~ /^\./ || $obj->{name} =~ /\//);
5263
            if (-e $ipath) {
5264
                $postreply .= "Status=ERROR Image already exists: \"$obj->{name}\" in \"$ipath\”\n";
5265
            } elsif (overQuotas($obj->{ksize}*1024)) {
5266
                $postreply .= "Status=ERROR Over quota (". overQuotas($obj->{ksize}*1024) . ") creating: $obj->{name}\n";
5267
                $main::updateUI->({tab=>"images", user=>$user, type=>"update", message=>"Over quota in storage pool $obj->{storagepool}"});
5268
                $main::syslogit->($user, "info", "Over quota in storage pool $obj->{storagepool}, not creating $obj->{type} image $obj->{name}");
5269
            } elsif (overStorage($obj->{ksize}*1024, $obj->{storagepool}+0)) {
5270
                $postreply .= "Status=ERROR Out of storage in destination pool creating: $obj->{name}\n";
5271
                $main::updateUI->({tab=>"images", user=>$user, type=>"update", message=>"Out of storage in storage pool $obj->{storagepool}"});
5272
                $main::syslogit->($user, "info", "Out of storage in storage pool $obj->{storagepool}, not creating $obj->{type} image $obj->{name}");
5273
            } elsif ($obj->{virtualsize} > 10*1024*1024 && $obj->{name} && $obj->{name} ne '--') {
5274
                $register{$ipath} = {
5275
                    uuid=>$newuuid,
5276
                    name=>$obj->{name},
5277
                    user=>$user,
5278
                    notes=>$obj->{notes},
5279
                    type=>$obj->{type},
5280
                    size=>0,
5281
                    realsize=>0,
5282
                    virtualsize=>$obj->{virtualsize},
5283
                    storagepool=>$spools[0]->{'id'},
5284
                    created=>$current_time,
5285
                    managementlink=>$obj->{managementlink},
5286
                    upgradelink=>$obj->{upgradelink},
5287
                    terminallink=>$obj->{terminallink},
5288
                    status=>"creating"
5289
                };
5290
                $uipath = $ipath;
5291
                my $res = `$cmd`;
5292
                $register{$ipath}->{'status'} = 'unused';
5293

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

    
5398
                $register{$path} = {
5399
                    name=>$obj->{name},
5400
                    user=>$obj->{user},
5401
                    notes=>$obj->{notes},
5402
                    bschedule=>$obj->{bschedule},
5403
                    installable=>$obj->{installable},
5404
                    managementlink=>$obj->{managementlink},
5405
                    upgradelink=>$obj->{upgradelink},
5406
                    terminallink=>$obj->{terminallink},
5407
                    action=>""
5408
                };
5409
                my $domains = $register{$path}->{'domains'};
5410
                if ($status eq 'downloading') {
5411
                    unless (`pgrep $obj->{name}`) { # Check if image is in fact being downloaded
5412
                        $status = 'unused';
5413
                        $register{$path}->{'status'} = $status;
5414
                        unlink ("$path.meta") if (-e "$path.meta");
5415
                    }
5416
                }
5417
                elsif ($status ne 'unused') {
5418
                    my $match;
5419
                    if ($path =~ /\.master\.qcow2$/) {
5420
                        my @regkeys = (tied %register)->select_where("master = '$path'");
5421
                        $match = 2 if (@regkeys);
5422
                    } else {
5423
                        if (!$domreg{$domains}) { # Referenced domain no longer exists
5424
                            ;
5425
                        } else { # Verify if referenced domain still uses image
5426
                            my @imgkeys = ('image', 'image2', 'image3', 'image4');
5427
                            for (my $i=0; $i<4; $i++) {
5428
                                $match = 1 if ($domreg{$domains}->{$imgkeys[$i]} eq $path);
5429
                            }
5430
                        }
5431
                    }
5432
                    unless ($match) {
5433
                        $status = 'unused';
5434
                        $register{$path}->{'status'} = $status;
5435
                    }
5436
                }
5437
                if ($status eq 'unused') {
5438
                    delete $register{$path}->{'domains'};
5439
                    delete $register{$path}->{'domainnames'};
5440
                }
5441
                $uipath = $path;
5442
                $postreply .= "Status=OK Saved $obj->{name} ($uuid)\n";
5443
                $main::updateUI->({tab=>"images", user=>$user, type=>"update", path=>$path, name=>  $obj->{name}, status=>$status});
5444
            } else {
5445
                $postreply .= "Status=ERROR Unable to save $obj->{name}\n";
5446
            }
5447
        }
5448
    }
5449
    if ($postreply) {
5450
        $postmsg = $postreply;
5451
    } else {
5452
        $postreply = to_json(\%{$register{$uipath}}, {pretty=>1}) if ($uipath && $register{$uipath});
5453
        $postreply =~ s/""/"--"/g;
5454
        $postreply =~ s/null/"--"/g;
5455
        $postreply =~ s/"notes" {0,1}: {0,1}"--"/"notes":""/g;
5456
        $postreply =~ s/"installable" {0,1}: {0,1}"(true|false)"/"installable":$1/g;
5457
    }
5458
    return $postreply || "Status=OK Saved $uipath\n";
5459
}
5460

    
5461
sub Setstoragedevice {
5462
    my ($image, $action, $obj) = @_;
5463
    if ($help) {
5464
        return <<END
5465
GET:device,type:
5466
Changes the device - disk or partition, used for images or backup storage.
5467
[type] is either images or backup.
5468
END
5469
    }
5470
    my $dev = $obj->{device};
5471
    my $force = $obj->{force};
5472
    my $type = 'backup';
5473
    $type = 'images' if ($obj->{type} eq 'images');
5474
    return "Status=Error Not allowed\n" unless ($isadmin);
5475
    my $backupdevice = Getbackupdevice('', 'getbackupdevice');
5476
    my $imagesdevice = Getimagesdevice('', 'getimagesdevice');
5477
    my $devices_obj = from_json(Liststoragedevices('', 'liststoragedevices'));
5478
    my %devices = %$devices_obj;
5479
    my $backupdev = $devices{$backupdevice}->{dev};
5480
    my $imagesdev = $devices{$imagesdevice}->{dev};
5481
    if (!$devices{$dev}) {
5482
        $postreply = "Status=Error You must specify a valid device ($dev)\n";
5483
        return $postreply;
5484
    }
5485
    if (!$force && (($backupdev =~ /$dev/) || ($imagesdev =~ /$dev/))  && $dev !~ /vda/ && $dev !~ /sda/) { # make exception to allow returning to default setup
5486
        $postreply = "Status=Error $dev is already in use as images or backup device\n";
5487
        return $postreply;
5488
    }
5489
    my $stordir = $tenderpathslist[0];
5490
    my $stordevice = $imagesdevice;
5491
    if ($type eq 'backup') {
5492
        $stordir = $backupdir;
5493
        $stordevice = $backupdevice;
5494
    }
5495
    return "Status=Error Storage device not found\n" unless ($stordevice);
5496
    my $mp = $devices{$dev}->{mounted};
5497
    my $newstordir;
5498
    # my $oldstordir;
5499
    if ($devices{$dev}->{type} eq 'zfs') {
5500
        my $cmd = qq|zfs list stabile-$type/$type -Ho mountpoint|;
5501
        my $zmp = `$cmd`;
5502
        chomp $zmp;
5503
        if ($zmp =~ /^\//) {
5504
            `zfs mount stabile-$type/$type`;
5505
            $mp = $zmp;
5506
            $newstordir = $mp;
5507
        } else {
5508
            `zfs create stabile-$type/$type`;
5509
            $mp = "/stabile-$type/$type";
5510
            $newstordir = $mp;
5511
        }
5512
    } else {
5513
        $newstordir = (($type eq 'images')?"$mp/images":"$mp/backups");
5514
        $newstordir = $1 if ($newstordir =~ /(.+\/images)\/images$/);
5515
        $newstordir = $1 if ($newstordir =~ /(.+\/backups)\/backups$/);
5516
    }
5517
    if ($mp eq '/') {
5518
        $newstordir = (($type eq 'images')?"/mnt/stabile/images":"/mnt/stabile/backups");
5519
        `umount "$newstordir"`; # in case it's mounted
5520
    }
5521
    `mkdir "$newstordir"` unless (-e $newstordir);
5522
    `chmod 777 "$newstordir"`;
5523

    
5524
    my $cfg = new Config::Simple("/etc/stabile/config.cfg");
5525
    if ($type eq 'backup') {
5526
        $cfg->param('STORAGE_BACKUPDIR', $newstordir);
5527
        $cfg->save();
5528
    } elsif ($type eq 'images') {
5529

    
5530
    # Handle shared storage config
5531
    #    $oldstordir = $stordir;
5532
        my $i = 0;
5533
        for($i = 0; $i <= $#tenderpathslist; $i++) {
5534
            my $dir = $tenderpathslist[$i];
5535
            last if ($dir eq $newstordir);
5536
        }
5537
        # $tenderpathslist[0] = $newstordir;
5538
        splice(@tenderpathslist, $i,1); # Remove existing entry
5539
        unshift(@tenderpathslist, $newstordir); # Then add the new path
5540
        $cfg->param('STORAGE_POOLS_LOCAL_PATHS', join(',', @tenderpathslist));
5541

    
5542
        # $tenderlist[0] = 'local';
5543
        splice(@tenderlist, $i,1);
5544
        unshift(@tenderlist, 'local');
5545
        $cfg->param('STORAGE_POOLS_ADDRESS_PATHS', join(',', @tenderlist));
5546

    
5547
        # $tendernameslist[0] = 'Default';
5548
        splice(@tendernameslist, $i,1);
5549
        unshift(@tendernameslist, 'Default');
5550

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

    
5556
            $storagepools = "$storagepools,$i" unless ($storagepools =~ /,\s*$i,?/ || $storagepools =~ /,\s*$i$/ || $storagepools =~ /^$i$/);
5557
            $cfg->param('STORAGE_POOLS_DEFAULTS', $storagepools);
5558
        }
5559
        $cfg->param('STORAGE_POOLS_NAMES', join(',', @tendernameslist));
5560

    
5561
        $cfg->save();
5562

    
5563

    
5564
    # Handle node storage configs
5565
        unless ( tie(%idreg,'Tie::DBI', Hash::Merge::merge({table=>'nodeidentities',key=>'identity',CLOBBER=>3}, $Stabile::dbopts)) ) {return "Unable to access id register"};
5566
        # Build hash of known node config files
5567
        my @nodeconfigs;
5568
        push @nodeconfigs, "/etc/stabile/nodeconfig.cfg";
5569
        foreach my $valref (values %idreg) {
5570
            my $nodeconfigfile = $valref->{'path'} . "/casper/filesystem.dir/etc/stabile/nodeconfig.cfg";
5571
            next if ($nodeconfigs{$nodeconfigfile}); # Node identities may share basedir and node config file
5572
            if (-e $nodeconfigfile) {
5573
                push @nodeconfigs, $nodeconfigfile;
5574
            }
5575
        }
5576
        untie %idreg;
5577
        foreach my $nodeconfig (@nodeconfigs) {
5578
            my $nodecfg = new Config::Simple($nodeconfig);
5579
            my @ltenderlist = $nodecfg->param('STORAGE_SERVERS_ADDRESS_PATHS');
5580
            my $ltenders = join(", ", @ltenderlist);
5581
            next if ($ltenders =~ /10\.0\.0\.1:$newstordir$/ || $ltenders =~ /10\.0\.0\.1:$newstordir,/); # This entry already exists
5582
            #my @ltenderlist = split(/,\s*/, $ltenders);
5583
            #$ltenderlist[0] = "10.0.0.1:$newstordir";
5584
            unshift(@ltenderlist, "10.0.0.1:$newstordir");
5585
            $nodecfg->param('STORAGE_SERVERS_ADDRESS_PATHS', join(',', @ltenderlist));
5586
            my @ltenderpathslist = $nodecfg->param('STORAGE_SERVERS_LOCAL_PATHS');
5587
            my $ltenderpaths = join(", ", @ltenderpathslist);
5588
            #my @ltenderpathslist = split(/,\s*/, $ltenderpaths);
5589
            #$ltenderpathslist[0] = $newstordir;
5590
            unshift(@ltenderpathslist, $newstordir);
5591
            $nodecfg->param('STORAGE_SERVERS_LOCAL_PATHS', join(',', @ltenderpathslist));
5592
            $nodecfg->save();
5593
        }
5594
        unless (`grep "$newstordir 10" /etc/exports`) {
5595
            `echo "$newstordir 10.0.0.0/255.255.255.0(sync,no_subtree_check,no_root_squash,rw)" >> /etc/exports`;
5596
            `/usr/sbin/exportfs -r`; #Reexport nfs shares
5597
        }
5598
# We no longer undefine storage pools - we add them
5599
#        $oldstordir =~ s/\//\\\//g;
5600
#        `perl -pi -e 's/$oldstordir 10.*\\\n//s;' /etc/exports` if ($oldstordir);
5601

    
5602
        `mkdir "$newstordir/common"` unless (-e "$newstordir/common");
5603
        `cp "$stordir/ejectcdrom.xml" "$newstordir/ejectcdrom.xml"` unless (-e "$newstordir/ejectcdrom.xml");
5604
        `cp "$stordir/mountvirtio.xml" "$newstordir/mountvirtio.xml"` unless (-e "$newstordir/mountvirtio.xml");
5605
        `cp "$stordir/dummy.qcow2" "$newstordir/dummy.qcow2"` unless (-e "$newstordir/dummy.qcow2");
5606
    }
5607
    Updatedownloads();
5608

    
5609
    # Update /etc/stabile/cgconfig.conf
5610
    my $devs = $devices{$dev}->{dev};
5611
    my @pdevs = split(" ", $devs);
5612
    my $majmins;
5613
    foreach my $dev (@pdevs) {
5614
        # It seems that cgroups cannot handle individual partitions for blkio
5615
        my $physdev = $1 if ($dev =~ /(\w+)\d+/);
5616
        if ($physdev && -d "/sys/fs/cgroup" ) {
5617
            my $blkline = `lsblk -l /dev/$physdev`;
5618
            my $majmin = '';
5619
            $majmin = $1 if ($blkline =~ /$physdev +(\d+:\d+)/);
5620
            $postreply .= "Status=OK Setting cgroups block device to $majmin\n";
5621
            if ($majmin) {
5622
                $majmins .= ($majmins)?" $majmin":$majmin;
5623
            }
5624
        }
5625
    }
5626
    setCgroupsBlkDevice($majmins) if ($majmins);
5627

    
5628
    $Stabile::Nodes::console = 1;
5629
    require "$Stabile::basedir/cgi/nodes.cgi";
5630
    $postreply .= Stabile::Nodes::do_reloadall('','reloadall');
5631

    
5632
    # Update config on stabile.io
5633
    require "$Stabile::basedir/cgi/users.cgi";
5634
    $Stabile::Users::console = 1;
5635
    Stabile::Users::Updateengine('', 'updateengine');
5636

    
5637
    my $msg = "OK Now using $newstordir for $type on $obj->{device}";
5638
    $main::updateUI->({tab=>'home', user=>$user, type=>'update', message=>$msg});
5639
    $postreply .= "Status=OK Now using $newstordir for $type on $dev\n";
5640
    return $postreply;
5641
}
5642

    
5643
sub Initializestorage {
5644
    my ($image, $action, $obj) = @_;
5645
    if ($help) {
5646
        return <<END
5647
GET:device,type,fs,activate,force:
5648
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.
5649
[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'.
5650
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).
5651
END
5652
    }
5653
    my $fs = $obj->{fs} || 'zfs';
5654
    my $dev = $obj->{device};
5655
    my $force = $obj->{force};
5656
    my $activate = $obj->{activate};
5657
    my $type = 'backup';
5658
    $type = 'images' if ($obj->{type} eq 'images');
5659
    return "Status=Error Not allowed\n" unless ($isadmin);
5660
    my $backupdevice = Getbackupdevice('', 'getbackupdevice');
5661
    my $imagesdevice = Getimagesdevice('', 'getimagesdevice');
5662
    my $devices_obj = from_json(Liststoragedevices('', 'liststoragedevices'));
5663
    my %devices = %$devices_obj;
5664
    my $backupdev = $devices{$backupdevice}->{dev};
5665
    my $imagesdev = $devices{$imagesdevice}->{dev};
5666
    if (!$dev || !(-e "/dev/$dev")) {
5667
        $postreply = "Status=Error You must specify a valid device\n";
5668
        return $postreply;
5669
    }
5670
    if (($backupdev =~ /$dev/) || ($imagesdev =~ /$dev/)) {
5671
        $postreply = "Status=Error $dev is already in use as images or backup device\n";
5672
        return $postreply;
5673
    }
5674
    my $stordir = "/stabile-$type/$type";
5675
    if ($fs eq 'lvm') {
5676
        if ($type eq 'backup') {
5677
            $stordir = "/mnt/stabile/backups";
5678
        } else {
5679
            $stordir = "/mnt/stabile/images";
5680
        }
5681
    }
5682
    `chmod 666 /dev/zfs` if (-e '/dev/zfs'); # TODO: This should be removed once we upgrade to Bionic and zfs allow is supported
5683

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

    
5804
sub setCgroupsBlkDevice {
5805
    my @majmins = split(" ", shift);
5806
    my $file = "/etc/stabile/cgconfig.conf";
5807
    my %options = (
5808
        blkio.throttle.read_bps_device => $valve_readlimit,
5809
        blkio.throttle.write_bps_device => $valve_writelimit,
5810
        blkio.throttle.read_iops_device => $valve_iopsreadlimit,
5811
        blkio.throttle.write_iops_device => $valve_iopswritelimit
5812
        );
5813
    my @groups = ('stabile', 'stabilevm');
5814
    my @newlines;
5815
    foreach my $majmin (@majmins) {
5816
        foreach my $group (@groups) {
5817
            my $mline = qq|group $group {|; push @newlines, $mline;
5818
            my $mline = qq|    blkio {|; push @newlines, $mline;
5819
            foreach my $option (keys %options) {
5820
                my $mline = qq|        $option = "$majmin $options{$option}";|;
5821
                push @newlines, $mline;
5822
            }
5823
            my $mline = qq|    }|; push @newlines, $mline;
5824
            my $mline = qq|}|; push @newlines, $mline;
5825
        }
5826
    }
5827
    unless (open(FILE, "> $file")) {
5828
        $postreply .= "Status=Error Problem opening $file\n";
5829
        return $postreply;
5830
    }
5831
    print FILE join("\n", @newlines);
5832
    close(FILE);
5833
    return;
5834
}
(2-2/9)