Project

General

Profile

Download (256 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/
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) = @_;
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
        return "node is is not using LVM, unable to backup active image.";
426
    } elsif ($nodereg{$mac}->{'status'} =~ /asleep|inactive/  && !$wake) {
427
        $postreply .= "Status=Error Node $mac is asleep, not waking\n";
428
        return "node is asleep, please wake first!";
429
    } else {
430
        my $tasks = $nodereg{$mac}->{'tasks'};
431
        $nodereg{$mac}->{'tasks'} = $tasks . "$newtask\n";
432
        tied(%nodereg)->commit;
433
    }
434
    untie %nodereg;
435
    return 0;
436
}
437

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

    
457
# If used with the -f switch ($fulllist) from console, all users images are updated in the db
458
# If used with the -p switch ($fullupdate), also updates status information (ressource intensive - runs through all domains)
459
sub Updateregister {
460
    my ($spath, $action) = @_;
461
    if ($help) {
462
        return <<END
463
GET:image,uuid:
464
If used with the -f switch ($fulllist) from console, all users images are updated in the db.
465
If used with the -p switch ($fullupdate), also updates status information (ressource intensive - runs through all domains)
466
Only images on shared storage are updated, images on node storage are handled on the node.
467
END
468
    }
469
    return "Status=ERROR You must be an admin to do this!\n" unless ($isadmin);
470
    $fullupdate = 1 if ((!$fullupdate && $params{'fullupdate'}) || $action eq 'fullupdateregister');
471
    my $force = $params{'force'};
472
    my %userregister;
473
    my $res;
474
    # Update size information in db
475
    foreach my $u (@users) {
476
        foreach my $spool (@spools) {
477
            my $pooldir = $spool->{"path"};
478
            my $dir = "$pooldir/$u";
479
            my @thefiles = Recurse($dir);
480
            foreach my $f (@thefiles) {
481
                next if ($spath && $spath ne $f); # Only specific image being updated
482
                if ($f =~ /(.+)(-s\d\d\d\.vmdk$)/) {
483
                #   `touch "$1.vmdk" 2>/dev/null` unless -e "$1.vmdk";
484
                } elsif ($f =~ /(.+)(-flat\.vmdk$)/) {
485
                #    `touch "$1.vmdk" 2>/dev/null` unless -e "$1.vmdk";
486
                } elsif(-s $f && $f =~ /(\.vmdk$)|(\.img$)|(\.vhd$)|(\.vhdx$)|(\.qcow$)|(\.qcow2$)|(\.vdi$)|(\.iso$)/i) {
487
                    my($fname, $dirpath, $suffix) = fileparse($f, ("vmdk", "img", "vhd", "vhdx", "qcow", "qcow2", "vdi", "iso"));
488
                    my $uuid;
489
                    my $img = $register{$f};
490
                    $uuid = $img->{'uuid'};
491
            # Create a new uuid if we are dealing with a new file in the file-system
492
                    if (!$uuid) {
493
                        my $ug = new Data::UUID;
494
                        $uuid = $ug->create_str();
495
                    }
496
                    my $storagepool = $spool->{"id"};
497
            # Deal with sizes
498
                    my ($newmtime, $newbackupsize, $newsize, $newrealsize, $newvirtualsize) =
499
                        getSizes($f, $img->{'mtime'}, $img->{'status'}, $u, $force);
500
                    my $size = $newsize || $img->{'size'};
501
                    my $realsize = $newrealsize || $img->{'realsize'};
502
                    my $virtualsize = $newvirtualsize || $img->{'virtualsize'};
503
                    my $mtime = $newmtime || $img->{'mtime'};
504
                    my $created = $img->{'created'} || $mtime;
505
                    my $name = $img->{'name'} || substr($fname,0,-1);
506
                    $register{$f} = {
507
                        path=>$f,
508
                        user=>$u,
509
                        type=>$suffix,
510
                        size=>$size,
511
                        realsize=>$realsize,
512
                        virtualsize=>$virtualsize,
513
                        backupsize=>$newbackupsize,
514
                        name=>$name,
515
                        uuid=>$uuid,
516
                    #    domains=>$domains,
517
                    #    domainnames=>$domainnames,
518
                        storagepool=>$storagepool,
519
                        backup=>"", # Only set in uservalues at runtime
520
                        created=>$created,
521
                        mtime=>$mtime
522
                    };
523
                #    $postreply .= "Status=OK $f, $size, $newbackupsize\n" if ($console);
524
                }
525
            }
526
        }
527
    }
528
    # Update status information in db
529
#    my $mounts = decode('ascii-escape', `/bin/cat /proc/mounts`);
530
    my $mounts = `/bin/cat /proc/mounts`;
531
    unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
532
    foreach my $u (@users) {
533
        my @regkeys = (tied %register)->select_where("user = '$u'");
534
        foreach my $k (@regkeys) {
535
            my $valref = $register{$k};
536
            my $path = $valref->{'path'};
537
# Only update info for images the user has access to.
538
# Remove DB entries for images on removed nodes
539
            if ($valref->{'storagepool'}==-1 && $valref->{'mac'} && $valref->{'mac'} ne '--' && !$nodereg{$valref->{'mac'}}) {
540
                delete $register{$path}; # Clean up database, remove rows which don't have corresponding file
541
                $main::updateUI->({tab=>'images', user=>$u}) unless ($u eq 'common');
542
            } elsif ($valref->{'user'} eq $u && (defined $spools[$valref->{'storagepool'}]->{'id'} || $valref->{'storagepool'}==-1)) {
543
                my $path = $valref->{'path'};
544
                next if ($spath && $spath ne $path); # Only specific image being updated
545
                my $mounted = ($mounts =~ /$path/);
546
                my $domains;
547
                my $domainnames;
548
                my $regstatus = $valref->{'status'};
549
                my $status = $regstatus;
550
                if (!$status || $status eq '--') {
551
                    $status = 'unused';
552
                }
553
                if (-e $path || $valref->{'storagepool'}==-1 || -s "$path.meta") {
554
                # Deal with status
555
                    if ($valref->{'storagepool'}!=-1 && -s "$path.meta") {
556
                        my $metastatus;
557
                        $metastatus = `/bin/cat "$path.meta" 2>/dev/null`;
558
                        chomp $metastatus;
559

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

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

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

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

    
702
sub getSizes {
703
    my ($f, $lmtime, $status, $buser, $force) = @_;
704

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

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

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

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

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

    
772
        return ($mtime, $backupsize, $size, $realsize, $virtualsize);
773
    } else {
774
        return (0, $backupsize, $size, $realsize);
775
    }
776

    
777
}
778

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

    
786
	my @idvalues = values %idreg;
787
	my %formats;
788
	foreach my $val (@idvalues) {
789
		my %h = %$val;
790
		foreach (split(/,/,$h{'formats'})) {
791
			$formats{lc $_} = $h{'hypervisor'}
792
		}
793
	}
794
	untie %idreg;
795

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

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

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

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

    
853
}
854

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

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

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

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

    
969
    #require File::Spec;
970
    #my $devnull = File::Spec->devnull();
971

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1303
    if (-e $restorepath) {
1304
        my $i = 1;
1305
        while (-e "$dirpath$bname.$i.iso") {$i++;}
1306
        $restorepath = "$dirpath$bname.$i.iso";
1307
    }
1308

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

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

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

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

    
1370
    } else {
1371
        $res .= "Status=ERROR Image not mounted, mount before restoring: ". $curfiles ."\n";
1372
    }
1373
    return $res;
1374
}
1375

    
1376
sub trim{
1377
   my $string = shift;
1378
   $string =~ s/^\s+|\s+$//g;
1379
   return $string;
1380
}
1381

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

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

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

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

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

    
1446
sub updateBilling {
1447
    my $event = shift;
1448
    my %billing;
1449

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

    
1458
        if ($val{'user'} eq $user && (defined $spools[$val{'storagepool'}]->{'id'} || $val{'storagepool'}==-1)) {
1459
            $billing{$val{'storagepool'}}->{'virtualsize'} += $val{'virtualsize'};
1460
            $billing{$val{'storagepool'}}->{'realsize'} += $val{'realsize'};
1461
            $billing{$val{'storagepool'}}->{'backupsize'} += $val{'backupsize'};
1462
        }
1463
    }
1464

    
1465
    my %billingreg;
1466

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

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

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

    
1481
    my %pool = ("hostpath", "--",
1482
                "path", "--",
1483
                "name", "local",
1484
                "rdiffenabled", 1,
1485
                "id", -1);
1486
    my @bspools = @spools;
1487
    push @bspools, \%pool;
1488

    
1489
    foreach my $spool (@bspools) {
1490
        my $storagepool = $spool->{"id"};
1491
        my $b = $billing{$storagepool};
1492
        my $virtualsize = $b->{'virtualsize'} +0;
1493
        my $realsize = $b->{'realsize'} +0;
1494
        my $backupsize = $b->{'backupsize'} +0;
1495

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

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

    
1556
        # Write new row
1557
        } else {
1558
            $billingreg{"$user-$storagepool-$year-$month"} = {
1559
                virtualsize=>$virtualsize+0,
1560
                realsize=>$realsize+0,
1561
                backupsize=>$backupsize+0,
1562

    
1563
                virtualsizeavg=>$virtualsizeavg,
1564
                realsizeavg=>$realsizeavg,
1565
                backupsizeavg=>$backupsizeavg,
1566

    
1567
                startvirtualsizeavg=>$startvirtualsizeavg,
1568
                startrealsizeavg=>$startrealsizeavg,
1569
                startbackupsizeavg=>$startbackupsizeavg,
1570

    
1571
                timestamp=>$current_time,
1572
                starttimestamp=>$starttimestamp,
1573
                event=>$event,
1574
                inc=>1,
1575
            };
1576
        }
1577
    }
1578
    tied(%billingreg)->commit;
1579
    untie %billingreg;
1580
}
1581

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

    
1591
    $postreply = removeUserImages($user) unless ($isreadonly);
1592
    return $postreply;
1593
}
1594

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

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

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

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

    
1633
    return $postreply;
1634
}
1635

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

    
1661
    my $img = $register{$path};
1662
    my $status = $img->{'status'};
1663
    my $mac = $img->{'mac'};
1664
    my $name = $img->{'name'};
1665
    my $uuid = $img->{'uuid'};
1666
    utf8::decode($name);
1667
    my $type = $img->{'type'};
1668
    my $username = $img->{'user'};
1669

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

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

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

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

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

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

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

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

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

    
1852
    my $virtualsize = $register{$path}->{'virtualsize'};
1853
    my $dindex = 0;
1854

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

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

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

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

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

    
1923
    if ($bschedule eq 'daily7' || $bschedule eq 'daily14') {
1924
         $bschedule = "manually" if ($istoragepool!=-1 && (!$spools[$istoragepool]->{'rdiffenabled'} || !$spools[$istoragepool]->{'lvm'}));
1925
    } elsif ($bschedule ne 'manually') {
1926
        $bschedule = '';
1927
    }
1928

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

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

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

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

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

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

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

    
2037

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

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

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

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

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

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

    
2184
# sub do_move {
2185
#     my ($uuid, $action) = @_;
2186
#     if ($help) {
2187
#         return <<END
2188
# GET:image,destuser,masteruser:
2189
# Move image to a different storage pool or user
2190
# END
2191
#     }
2192
#     return "Your account does not have the necessary privileges\n" if ($isreadonly);
2193
#     Move($curimg, $params{'user'});
2194
#     return $postreply;
2195
# }
2196

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

    
2225
    my $newpath;
2226
    my $newdirpath;
2227
    my $oldpath = $path;
2228
    my $olddirpath = $path;
2229
    my $newuser = $reguser;
2230
    my $newstoragepool = $regstoragepool;
2231
    my $haschildren;
2232
    my $hasprimary;
2233
    my $child;
2234
    my $primary;
2235
    my $macip;
2236
    my $alreadyexists;
2237
    my $subdir;
2238
#    $subdir = $1 if ($path =~ /\/$reguser(\/.+)\//);
2239
    $subdir = $1 if ($path =~ /.+\/$reguser(\/.+)?\//);
2240
    my $restpath;
2241
    $restpath = $1 if ($path =~ /.+\/$reguser\/(.+)/);
2242

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

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

    
2333
        my $dindex;
2334
        my $wakenode;
2335
        if ($istoragepool == -1 && $regstoragepool != -1) {
2336
            ($mac, $macip, $dindex, $wakenode) = locateNode($virtualsize, $mac);
2337
        }
2338

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

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

    
2372
                } else {
2373
                    $postreply .= "Status=ERROR Unable to move $name (not enough space)\n";
2374
                }
2375
            } else {
2376
                $postreply .= "Status=ERROR Unable to move $name (no node)\n";
2377
            }
2378
    # Moving from node
2379
        } elsif ($regstoragepool == -1 && $istoragepool != -1 && $spools[$istoragepool]) {
2380
            if (index($privileges,"a")!=-1 || index($privileges,"n")!=-1 && $mac) { # Privilege "n" means user may use node storage
2381
                unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
2382
                $macip = $nodereg{$mac}->{'ip'};
2383
                untie %nodereg;
2384
                $newpath = "$spools[$istoragepool]->{'path'}/$reguser/$restpath";
2385
                $newdirpath = $newpath;
2386
                $oldpath = "$macip:/mnt/stabile/node/$reguser/$restpath";
2387
                $newstoragepool = $istoragepool;
2388
        # Check if image already exists in target dir
2389
                $alreadyexists = -e $newpath;
2390
            } else {
2391
                $postreply .= "Status=ERROR Unable to move $name - select node\n";
2392
            }
2393
        } elsif ($spools[$istoragepool]) { # User has access to storagepool
2394
            $newpath = "$spools[$istoragepool]->{'path'}/$reguser/$restpath";
2395
            $newdirpath = $newpath;
2396
            $newstoragepool = $istoragepool;
2397
            $alreadyexists = -e $newpath && -s $newpath;
2398
        } else {
2399
            $postreply .= "Status=ERROR Cannot move image. This image is used as master by: $child\n";
2400
        }
2401
    } else {
2402
        $postreply .= "Status=ERROR Unable to move $path (bad status or pool $status, $reguser, $iuser, $regstoragepool, $istoragepool)\n" unless ($istoragepool eq '--' || $regstoragepool eq '--');
2403
    }
2404
    untie %userreg;
2405

    
2406
    if ($alreadyexists && !$force) {
2407
        $postreply = "Status=ERROR Image \"$name\" already exists in destination\n";
2408
        return $postreply;
2409
    }
2410
# Request actual move operation
2411
    elsif ($newpath) {
2412
        if ($newstoragepool == -1) {
2413
            my $diruser = $iuser || $reguser;
2414
            `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
2415
        }
2416
        if ($subdir && $istoragepool != -1) {
2417
            my $fulldir = "$spools[$istoragepool]->{'path'}/$reguser$subdir";
2418
            `/bin/mkdir -p "$fulldir"` unless -d $fulldir;
2419
        }
2420
        $uistatus = "moving";
2421
        my $ug = new Data::UUID;
2422
        my $tempuuid = $ug->create_str();
2423

    
2424
        $register{$path}->{'status'} = $uistatus;
2425
        $register{$newdirpath} = \%{$register{$path}}; # Clone db entry
2426

    
2427
        if ($bschedule eq 'daily7' || $bschedule eq 'daily14') {
2428
             $bschedule = "manually" if (!$spools[$regstoragepool]->{'rdiffenabled'} || !$spools[$regstoragepool]->{'lvm'});
2429
        } elsif ($bschedule ne 'manually') {
2430
            $bschedule = '';
2431
        }
2432

    
2433
        $register{$path}->{'uuid'} = $tempuuid; # Use new temp uuid for old image
2434
        $register{$newdirpath}->{'storagepool'} = $newstoragepool;
2435
        if ($newstoragepool == -1) {
2436
            $register{$newdirpath}->{'mac'} = $mac;
2437
        } else {
2438
            $register{$newdirpath}->{'mac'} = '';
2439
        }
2440
        $register{$newdirpath}->{'user'} = $newuser;
2441
        tied(%register)->commit;
2442
        my $domuuid = $register{$path}->{'domains'};
2443
        if ($status eq "used" || $status eq "paused" || $status eq "moving") {
2444
            my $dom = $domreg{$domuuid};
2445
            if ($dom->{'image'} eq $olddirpath) {
2446
                $dom->{'image'} = $newdirpath;
2447
            } elsif ($dom->{'image2'} eq $olddirpath) {
2448
                $dom->{'image2'} = $newdirpath;
2449
            } elsif ($dom->{'image3'} eq $olddirpath) {
2450
                $dom->{'image3'} = $newdirpath;
2451
            } elsif ($dom->{'image4'} eq $olddirpath) {
2452
                $dom->{'image4'} = $newdirpath;
2453
            }
2454
            $dom->{'mac'} = $mac if ($newstoragepool == -1);
2455
            if ($dom->{'system'} && $dom->{'system'} ne '--') {
2456
                unless (tie(%sysreg,'Tie::DBI', Hash::Merge::merge({table=>'systems'}, $Stabile::dbopts)) ) {$res .= qq|{"status": "Error": "message": "Unable to access systems register"}|; return $res;};
2457
                my $sys = $sysreg{$dom->{'system'}};
2458
                $sys->{'image'} = $newdirpath if ($sys->{'image'} eq $olddirpath);
2459
                untie %sysreg;
2460
            }
2461
        }
2462
        my $cmd = qq|/usr/local/bin/steamExec $user $uistatus $status "$oldpath" "$newpath"|;
2463
        `$cmd`;
2464
        $main::syslogit->($user, "info", "$uistatus $type image $name ($oldpath -> $newpath) ($regstoragepool -> $istoragepool) ($register{$newdirpath}->{uuid})");
2465
        return "$newdirpath\n";
2466
    } else {
2467
        return $postreply;
2468
    }
2469

    
2470
}
2471

    
2472
sub locateNode {
2473
    my ($virtualsize, $mac, $vcpu, $mem) = @_;
2474
    $vcpu = $vcpu || 1;
2475
    unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac'}, $Stabile::dbopts)) ) {return 0};
2476
    my $macip;
2477
    my $dmac;
2478
    my $dindex;
2479
    my $asleep;
2480
    my $identity;
2481
    my $node;
2482
    if ($mac && $mac ne "--") { # A node was specified
2483
        if (1024 * $nodestorageovercommission * $nodereg{$mac}->{'storfree'} > $virtualsize && $nodereg{$mac}->{'status'} eq 'running') {
2484
            $node = $nodereg{$mac};
2485
        }
2486
    } else { # Locate a node
2487
        require "$Stabile::basedir/cgi/servers.cgi";
2488
        $Stabile::Servers::console = 1;
2489
        my ($temp1, $temp2, $temp3, $temp4, $ahashref) = Stabile::Servers::locateTargetNode();
2490
        my @avalues = values %$ahashref;
2491
        my @sorted_values = (sort {$b->{'index'} <=> $a->{'index'}} @avalues);
2492
        foreach my $snode (@sorted_values) {
2493
            if (
2494
                (1024 * $nodestorageovercommission * $snode->{'storfree'} > $virtualsize)
2495
                && ($snode->{'cpuindex'} > $vcpu)
2496
                && ($snode->{'memfree'} > $mem+512*1024)
2497
                && !($snode->{'maintenance'})
2498
                && ($snode->{'status'} eq 'running' || $snode->{'status'} eq 'asleep' || $snode->{'status'} eq 'waking')
2499
                && ($snode->{'index'} > 0)
2500
            ) {
2501
                next if (!($mem) && $snode->{'identity'} eq 'local_kvm'); # Ugly hack - prevent moving images from default storage to local_kvm node
2502
                $node = $snode;
2503
                last;
2504
            }
2505
        }
2506
    }
2507
    $macip = $node->{'ip'};
2508
    $dmac = $node->{'mac'};
2509
    $dindex = $node->{'index'};
2510
    $asleep = ($node->{'status'} eq 'asleep' || $node->{'status'} eq 'waking');
2511
    $identity = $node->{'identity'};
2512
    untie %nodereg;
2513
    return ($dmac, $macip, $dindex, $asleep, $identity);
2514
}
2515

    
2516
sub do_getimagestatus {
2517
    my ($image, $action) = @_;
2518
    if ($help) {
2519
        return <<END
2520
GET:image:
2521
Check if image already exists. Pass image name including suffix.
2522
END
2523
    }
2524
    my $res;
2525
    $imagename = $params{'name'} || $image;
2526
    foreach my $spool (@spools) {
2527
        my $ipath = $spool->{'path'} . "/$user/$imagename";
2528
        if ($register{$ipath}) {
2529
            $res .= "Status=OK Image $ipath found with status $register{$ipath}->{'status'}\n";
2530
        } elsif (-f "$ipath" && -s "$ipath") {
2531
            $res .= "Status=OK Image $ipath found on disk, please wait for it to be updated in DB\n";
2532
        }
2533
    }
2534
    $res .= "Status=ERROR Image $image not found\n" unless ($res);
2535
    return $res;;
2536
}
2537

    
2538
# Check if image already exists.
2539
# Pass image name including suffix.
2540
sub imageExists {
2541
    my $imagename = shift;
2542
    foreach my $spool (@spools) {
2543
        my $ipath = $spool->{'path'} . "/$user/$imagename";
2544
        if ($register{$ipath}) {
2545
            return $register{$ipath}->{'status'} || 1;
2546
        } elsif (-e "$ipath") {
2547
            return 1
2548
        }
2549
    }
2550
    return '';
2551
}
2552

    
2553
# Pass image name including suffix.
2554
# Returns incremented name of an image which does not already exist.
2555
sub getValidName {
2556
    my $imagename = shift;
2557
    my $name = $imagename;
2558
    my $type;
2559
    if ($imagename =~ /(.+)\.(.+)/) {
2560
        $name = $1;
2561
        $type = $2;
2562
    }
2563
    if (imageExists($imagename)) {
2564
        my $i = 1;
2565
        while (imageExists("$name.$i.$type")) {$i++;};
2566
        $imagename = "$name.$i.$type";
2567
    }
2568
    return $imagename;
2569
}
2570

    
2571
# Print list of available actions on objects
2572
sub do_plainhelp {
2573
    my $res;
2574
    $res .= header('text/plain') unless $console;
2575
    $res .= <<END
2576
* new [size="size", name="name"]: Creates a new image
2577
* 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
2578
image is a regular copy.
2579
* convert: Creates a copy of a non-qcow2 image in qcow2 format
2580
* snapshot: Takes a qcow2 snapshot of the image. Server can not be running.
2581
* unsnap: Removes a qcow2 snapshot.
2582
* revert: Applies a snapshot, reverting the image to the state it was in, when the snapshot was taken.
2583
* master: Turns an image into a master image which child images may be cloned from. Image can not be in use.
2584
* unmaster: Turns a master image into a regular image, which can not be used to clone child images from.
2585
* backup: Backs up an image using rdiff-backup. Rdiff-backup must be enabled in admin server configuration. This is a
2586
very expensive operation, since typically the entire image must be read.
2587
* buildsystem [master="master image"]: Constructs one or optionally multiple servers, images and networks and assembles
2588
them in one app.
2589
* restore [backup="backup"]: Restores an image from a backup. The restore is named after the backup.
2590
* delete: Deletes an image. Use with care. Image can not be in use.
2591
* mount: Mounts an image for restorefiles and listfiles operations.
2592
* unmount: Unmounts an image
2593
END
2594
    ;
2595
    return $res;
2596
}
2597

    
2598
# Print list of images
2599
# Showing a single image is also handled by specifying uuid or path in $curuuid or $curimg
2600
# When showing a single image a single action may be performed on image
2601
sub do_list {
2602
    my ($img, $action, $obj) = @_;
2603
    if ($help) {
2604
        return <<END
2605
GET:image,uuid:
2606
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.
2607
The returned list may be filtered by specifying storagepool, type, name, path or uuid, like e.g.:
2608

    
2609
<a href="/stabile/images/type:user" target="_blank">/stabile/images/type:user</a>
2610
<a href="/stabile/images/name:test* AND storagepool:shared" target="_blank">/stabile/images/name:test* AND storagepool:shared</a>
2611
<a href="/stabile/images/storagepool:shared AND path:test*" target="_blank">/stabile/images/storagepool:shared AND path:test*</a>
2612
<a href="/stabile/images/name:* AND storagepool:all AND type:usercdroms" target="_blank">/stabile/images/name:* AND storagepool:all AND type:usercdroms</a>
2613
<a href="/stabile/images/[uuid]" target="_blank">/stabile/images/[uuid]</a>
2614

    
2615
storagepool may be either of: all, node, shared
2616
type may be either of: user, usermasters, commonmasters, usercdroms
2617

    
2618
May also be called as tablelist or tablelistall, for use by stash.
2619

    
2620
END
2621
    }
2622
    my $res;
2623
    my $filter;
2624
    my $storagepoolfilter;
2625
    my $typefilter;
2626
    my $pathfilter;
2627
    my $uuidfilter;
2628
    $curimg = $img if ($img);
2629
    my $regimg = $register{$curimg};
2630
#    if ($curimg && ($isadmin || $regimg->{'user'} eq $user || $regimg->{'user'} eq 'common') ) {
2631
    if ($curimg) { # security is enforced below, we hope...
2632
        $pathfilter = $curimg;
2633
    } elsif ($uripath =~ /images(\.cgi)?\/(\?|)(name|storagepool|type|path)/) {
2634
        $filter = $3 if ($uripath =~ /images(\.cgi)?\/.*name(:|=)(.+)/);
2635
        $filter = $1 if ($filter =~ /(.*) AND storagepool/);
2636
        $filter = $1 if ($filter =~ /(.*) AND type/);
2637
        $filter = $1 if ($filter =~ /(.*)\*$/);
2638
        $storagepoolfilter = $2 if ($uripath =~ /images(\.cgi)?\/.*storagepool:(\w+)/);
2639
        $typefilter = $2 if ($uripath =~ /images(\.cgi)?\/.*type:(\w+)/);
2640
        $typefilter = $2 if ($uripath =~ /images(\.cgi)?\/.*type=(\w+)/);
2641
        $pathfilter = $2 if ($uripath =~ /images(\.cgi)?\/.*path:(.+)/);
2642
        $pathfilter = $2 if ($uripath =~ /images(\.cgi)?\/.*path=(.+)/);
2643
    } elsif ($uripath =~ /images(\.cgi)?\/(\w{8}-\w{4}-\w{4}-\w{4}-\w{12})\/?(\w*)/) {
2644
        $uuidfilter = $2;
2645
        $curaction = lc $3;
2646
    }
2647
    $uuidfilter = $options{u} unless $uuidfilter;
2648

    
2649
    if ($uuidfilter && $curaction) {
2650
        if ($imagereg{$uuidfilter}) {
2651
            $curuuid = $uuidfilter;
2652
            my $obj = getObj(%params);
2653
            # Now perform the requested action
2654
            my $objfunc = "obj_$curaction";
2655
            if (defined &$objfunc) { # If a function named objfunc exists, call it
2656
                $res = $objfunc->($obj);
2657
                chomp $postreply;
2658
                unless ($res) {
2659
                    $res .= qq|{"status": "OK", "message": "$postreply"}|;
2660
                    $res = join(", ", split("\n", $res));
2661
                }
2662
                unless ($curaction eq 'download') {
2663
                    $res = header('application/json; charset=UTF8') . $res unless ($console);
2664
                }
2665
            } else {
2666
                $res .= header('application/json') unless $console;
2667
                $res .= qq|{"status": "Error", "message": "Unknown image action: $curaction"}|;
2668
            }
2669
        } else {
2670
            $res .= header('application/json') unless $console;
2671
            $res .= qq|{"status": "Error", "message": "Unknown image $uuidfilter"}|;
2672
        }
2673
        return $res;
2674
    }
2675

    
2676

    
2677
    my %userregister; # User specific register
2678

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

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

    
2726
                        $userregister{$val{'master'}}->{'domains'} .= ", " if ($userregister{$val{'master'}}->{'domains'});
2727
                        $userregister{$val{'master'}}->{'domains'} .= $val{'domains'};
2728
                    }
2729
                }
2730
                my $status = $valref->{'status'};
2731
                if ($rdiffenabled && ($userrdiffenabled || index($privileges,"a")!=-1) &&
2732
                    ( ($spools[$valref->{'storagepool'}]->{'rdiffenabled'} &&
2733
                        ($spools[$valref->{'storagepool'}]->{'lvm'} || $status eq 'unused' || $status eq 'used' || $status eq 'paused') )
2734
                        || $valref->{'storagepool'}==-1 )
2735
                ) {
2736
                    $val{'backup'} = "" ;
2737
                } else {
2738
                    $val{'backup'} = "disabled" ;
2739
                }
2740
                $val{'status'} = 'backingup' if ($status =~ /backingup/);
2741
                $userregister{$path} = \%val unless ($userregister{$path});
2742
            }
2743
        }
2744
    }
2745
    untie(%nodereg);
2746

    
2747
    my @uservalues;
2748
    if ($filter || $storagepoolfilter || $typefilter || $pathfilter || $uuidfilter) { # List filtered images
2749
        foreach $uvalref (values %userregister) {
2750
            my $fmatch;
2751
            my $smatch;
2752
            my $tmatch;
2753
            my $pmatch;
2754
            my $umatch;
2755
            $fmatch = 1 if (!$filter || $uvalref->{'name'}=~/$filter/i);
2756
            $smatch = 1 if (!$storagepoolfilter || $storagepoolfilter eq 'all'
2757
                || ($storagepoolfilter eq 'node' && $uvalref->{'storagepool'}==-1)
2758
                || ($storagepoolfilter eq 'shared' && $uvalref->{'storagepool'}>=0)
2759
            );
2760
            $tmatch = 1 if (!$typefilter || $typefilter eq 'all'
2761
                || ($typefilter eq 'user' && $uvalref->{'user'} eq $user
2762
                # && $uvalref->{'type'} ne 'iso'
2763
                # && $uvalref->{'path'} !~ /\.master\.qcow2$/
2764
                    )
2765
                || ($typefilter eq 'usermasters' && $uvalref->{'user'} eq $user && $uvalref->{'path'} =~ /\.master\.qcow2$/)
2766
                || ($typefilter eq 'usercdroms' && $uvalref->{'user'} eq $user && $uvalref->{'type'} eq 'iso')
2767
                || ($typefilter eq 'commonmasters' && $uvalref->{'user'} ne $user && $uvalref->{'path'} =~ /\.master\.qcow2$/)
2768
                || ($typefilter eq 'commoncdroms' && $uvalref->{'user'} ne $user && $uvalref->{'type'} eq 'iso')
2769
            );
2770
            $pmatch = 1 if ($pathfilter && $uvalref->{'path'}=~/$pathfilter/i);
2771
            $umatch = 1 if ($uvalref->{'uuid'} eq $uuidfilter);
2772
            if ((!$pathfilter &&!$uuidfilter && $fmatch && $smatch && $tmatch) || $pmatch) {
2773
                push @uservalues,$uvalref if ($uvalref->{'uuid'});
2774
            } elsif ($umatch && $uvalref->{'uuid'}) {
2775
                push @uservalues,$uvalref;
2776
                last;
2777
            }
2778
        }
2779
    } else {
2780
        @uservalues = values %userregister;
2781
    }
2782

    
2783
    # Sort @uservalues
2784
    @uservalues = (sort {$a->{'name'} cmp $b->{'name'}} @uservalues); # Always sort by name first
2785
    my $sort = 'status';
2786
    $sort = $2 if ($uripath =~ /sort\((\+|\-)(\S+)\)/);
2787
    my $reverse;
2788
    $reverse = 1 if ($1 eq '-');
2789
    if ($reverse) { # sort reverse
2790
        if ($sort =~ /realsize|virtualsize|size/) {
2791
            @uservalues = (sort {$b->{$sort} <=> $a->{$sort}} @uservalues); # Sort as number
2792
        } else {
2793
            @uservalues = (sort {$b->{$sort} cmp $a->{$sort}} @uservalues); # Sort as string
2794
        }
2795
    } else {
2796
        if ($sort =~ /realsize|virtualsize|size/) {
2797
            @uservalues = (sort {$a->{$sort} <=> $b->{$sort}} @uservalues); # Sort as number
2798
        } else {
2799
            @uservalues = (sort {$a->{$sort} cmp $b->{$sort}} @uservalues); # Sort as string
2800
        }
2801
    }
2802

    
2803
    if ($uuidfilter || $curimg) {
2804
        if (scalar @uservalues > 1) { # prioritize user's own images
2805
            foreach my $val (@uservalues) {
2806
                if ($val->{'user'} eq 'common') {
2807
                    next;
2808
                } else {
2809
                    $json_text = to_json($val, {pretty => 1});
2810
                }
2811
            }
2812
        } else {
2813
            $json_text = to_json($uservalues[0], {pretty => 1}) if (@uservalues);
2814
        }
2815
    } else {
2816
    #    $json_text = JSON->new->canonical(1)->pretty(1)->encode(\@uservalues) if (@uservalues);
2817
        $json_text = to_json(\@uservalues, {pretty => 1}) if (@uservalues);
2818
    }
2819
    $json_text = "{}" unless $json_text;
2820
    $json_text =~ s/""/"--"/g;
2821
    $json_text =~ s/null/"--"/g;
2822
    $json_text =~ s/"notes" {0,1}: {0,1}"--"/"notes":""/g;
2823
    $json_text =~ s/"installable" {0,1}: {0,1}"(true|false)"/"installable":$1/g;
2824

    
2825
    if ($action eq 'tablelist' || $action eq 'tablelistall') {
2826
        my $t2 = Text::SimpleTable->new(36,26,5,20,14,10,7);
2827
        $t2->row('uuid', 'name', 'type', 'domainnames', 'virtualsize', 'user', 'status');
2828
        $t2->hr;
2829
        my $pattern = $options{m};
2830
        foreach $rowref (@uservalues){
2831
            next unless ($action eq 'tablelistall' || $rowref->{'user'} eq $user);
2832
            if ($pattern) {
2833
                my $rowtext = $rowref->{'uuid'} . " " . $rowref->{'name'} . " " . $rowref->{'type'} . " " . $rowref->{'domainnames'}
2834
                    . " " .  $rowref->{'virtualsize'} . " " . $rowref->{'user'} . " " . $rowref->{'status'};
2835
                $rowtext .= " " . $rowref->{'mac'} if ($isadmin);
2836
                next unless ($rowtext =~ /$pattern/i);
2837
            }
2838
            $t2->row($rowref->{'uuid'}, $rowref->{'name'}, $rowref->{'type'}, $rowref->{'domainnames'}||'--',
2839
                $rowref->{'virtualsize'}, $rowref->{'user'}, $rowref->{'status'});
2840
        }
2841
        $res .= $t2->draw;
2842
    } elsif ($console) {
2843
        $res .= Dumper(\@uservalues);
2844
    } else {
2845
        $res .= $json_text;
2846
    }
2847
    return $res;
2848
}
2849

    
2850
# Internal action for looking up a uuid or part of a uuid and returning the complete uuid
2851
sub do_uuidlookup {
2852
    my ($img, $action) = @_;
2853
    if ($help) {
2854
        return <<END
2855
GET:image,path:
2856
END
2857
    }
2858
    my $res;
2859
    $res .= header('text/plain') unless $console;
2860
    my $u = $options{u};
2861
    $u = $curuuid unless ($u || $u eq '0');
2862
    my $ruuid;
2863
    if ($u || $u eq '0') {
2864
        foreach my $uuid (keys %register) {
2865
            if (($register{$uuid}->{'user'} eq $user || $register{$uuid}->{'user'} eq 'common' || $fulllist)
2866
                && ($register{$uuid}->{'uuid'} =~ /^$u/ || $register{$uuid}->{'name'} =~ /^$u/)) {
2867
                $ruuid = $register{$uuid}->{'uuid'};
2868
                last;
2869
            }
2870
        }
2871
        if (!$ruuid && $isadmin) { # If no match and user is admin, do comprehensive lookup
2872
            foreach $uuid (keys %register) {
2873
                if ($register{$uuid}->{'uuid'} =~ /^$u/ || $register{$uuid}->{'name'} =~ /^$u/) {
2874
                    $ruuid = $register{$uuid}->{'uuid'};
2875
                    last;
2876
                }
2877
            }
2878
        }
2879
    }
2880
    $res .= "$ruuid\n" if ($ruuid);
2881
    return $res;
2882
}
2883

    
2884
# Internal action for showing a single image
2885
sub do_uuidshow {
2886
    my ($img, $action) = @_;
2887
    if ($help) {
2888
        return <<END
2889
GET:image,path:
2890
END
2891
    }
2892
    my $res;
2893
    $res .= header('text/plain') unless $console;
2894
    my $u = $options{u};
2895
    $u = $curuuid unless ($u || $u eq '0');
2896
    if ($u || $u eq '0') {
2897
        foreach my $uuid (keys %register) {
2898
            if (($register{$uuid}->{'user'} eq $user || $register{$uuid}->{'user'} eq 'common' || index($privileges,"a")!=-1)
2899
                && $register{$uuid}->{'uuid'} =~ /^$u/) {
2900
                my %hash = %{$register{$uuid}};
2901
                delete $hash{'action'};
2902
                my $dump = Dumper(\%hash);
2903
                $dump =~ s/undef/"--"/g;
2904
                $res .= $dump;
2905
                last;
2906
            }
2907
        }
2908
    }
2909
    return $res;
2910
}
2911

    
2912
sub do_updatebilling {
2913
    my ($img, $action) = @_;
2914
    if ($help) {
2915
        return <<END
2916
GET:image,path:
2917
END
2918
    }
2919
    my $res;
2920
    $res .= header('text/plain') unless ($console);
2921
    updateBilling($params{"event"});
2922
    $res .= "Status=OK Updated billing for $user\n";
2923
    return $res;
2924
}
2925

    
2926
# If used with the -f switch ($fulllist) from console, all users images are updated in the db
2927
# If used with the -p switch ($fullupdate), also updates status information (ressource intensive - runs through all domains)
2928
sub dont_updateregister {
2929
    my ($img, $action) = @_;
2930
    my $res;
2931
    if ($help) {
2932
        return <<END
2933
GET:image,path:
2934
END
2935
    }
2936
    return "Status=ERROR You must be an admin to do this!\n" unless ($isadmin);
2937
    $fullupdate = 1 if ((!$fullupdate && $params{'fullupdate'}) || $action eq 'fullupdateregister');
2938
    my $force = $params{'force'};
2939
    Updateregister($force);
2940
    $res .= "Status=OK Updated image register for " . join(', ', @users) . "\n";
2941
}
2942

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

    
3036
sub do_upload {
3037
    my ($img, $action) = @_;
3038
    if ($help) {
3039
        return <<END
3040
POST:image,path:
3041
END
3042
    }
3043
    my $res;
3044
    $res .= header("text/html") unless ($console);
3045

    
3046
    my $uname = $params{'name'};
3047

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

    
3050
    $name = $1 if ($name =~ /^\.+(.*)/); # Don't allow hidden files
3051
    #        my $f = lc $name;
3052
    my $f = $name;
3053
    $f = $spools[0]->{'path'} . "/$user/$f$suffix";
3054

    
3055
    my $chunk = int($params{'chunk'});
3056
    my $chunks = int($params{'chunks'});
3057

    
3058
    if ($chunk == 0 && -e $f) {
3059
        $res .= qq|Error: File $f already exists $name|;
3060
    } else {
3061
        open (FILE, ">>$f");
3062

    
3063
        if ($params{'file'}) {
3064
            my $uh = $Stabile::q->upload("file");
3065
            while ( <$uh> ) {
3066
                print FILE;
3067
            }
3068
            close FILE;
3069

    
3070
            if ($chunk == 0) {
3071
                `/usr/local/bin/steamExec updateimagestatus "$f" uploading`;
3072
            }
3073
            if ($chunk >= ($chunks - 1) ) { # Done
3074
                unlink("$f.meta");
3075
                `/usr/local/bin/steamExec updateimagestatus "$f" unused`;
3076
            } else {
3077
                my $upload_meta_data = "status=uploading&chunk=$chunk&chunks=$chunks";
3078
                `echo "$upload_meta_data" > "$f.meta"`;
3079
            }
3080
            $res .= qq|OK: Chunk $chunk uploaded of $name|;
3081
        } else {
3082
            $res .= qq|OK: No file $name.|;
3083
        }
3084
    }
3085
    return $res;
3086
}
3087

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

    
3162

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

    
3282
    # Now parse the rather strange output with every other line representing physical dev
3283
    foreach my $line (split "\n", $zpools) {
3284
        my ($zname, $zsize, $zalloc) = split "\t", $line;
3285
        if (!$zdev) {
3286
            if ($zname =~ /stabile-/) {
3287
                $zdev = {
3288
                    name=>$zname,
3289
                    size=>$zsize,
3290
                    alloc=>$zalloc
3291
                }
3292
            }
3293
        } else {
3294
            my $dev = $zsize;
3295
            $zdev->{dev} = $dev;
3296
            if ( $filesystems{$zdev->{name}}) {
3297
                if (
3298
                    ($action eq 'listimagesdevices' && $zdev->{name} =~ /backup/) ||
3299
                        ($action eq 'listbackupdevices' && $zdev->{name} =~ /images/)
3300
                ) {
3301
                    delete $filesystems{$zdev->{name}}; # Don't include backup devs in images listing and vice-versa
3302
                } else {
3303
                    if ($filesystems{$zdev->{name}}->{dev}) {
3304
                        $filesystems{$zdev->{name}}->{dev} .= " $dev";
3305
                    } else {
3306
                        $filesystems{$zdev->{name}}->{dev} = $dev;
3307
                    }
3308
        #            $filesystems{$zdev->{name}}->{nametype} =~ s/zfs/zfs pool/;
3309
                }
3310
            }
3311
            $zdevs{$dev} = $zdev->{name};
3312
        #    $zdev = '';
3313
        }
3314
    }
3315

    
3316
    # Add blockdevices
3317
    $cmd = q|lsblk --json|;
3318
    my $json2 = `$cmd`;
3319
    my $jobj2 = JSON::from_json($json2);
3320
    foreach my $fs (@{$jobj2->{blockdevices}}) {
3321
        my $rootdev = $1 if ($fs->{name} =~ /([A-Za-z]+)\d*/);
3322
        if ($fs->{children}) {
3323
            foreach my $fs2 (@{$fs->{children}}) {
3324
                next if ($fs2->{type} eq 'loop');
3325
                next if ($fs2->{type} eq 'squashfs');
3326
                next if ($fs2->{size} =~ /K$/);
3327
                if ($filesystems{$fs2->{name}}) {
3328
                    $filesystems{$fs2->{name}}->{blocksize} = $fs2->{size};
3329
                } elsif (!$zdevs{$fs2->{name}} && !$zdevs{$rootdev}) { # Don't add partitions already used for ZFS
3330
                    next if (($action eq 'listimagesdevices' || $action eq 'listbackupdevices') && $fs2->{mountpoint} eq '/');
3331
                    my $mp = $fs2->{mountpoint};
3332
                    $filesystems{$fs2->{name}} = {
3333
                        name=>$fs2->{name},
3334
                        blocksize=>$fs2->{size},
3335
                        mountpoint=>$mp,
3336
                        type=>$fs2->{type},
3337
                        nametype=> "$fs2->{name} ($fs2->{type} - " . ($mp?$mp:"not mounted") . " $fs2->{size})",
3338
                        dev=>$fs2->{name}
3339
                    }
3340
                }
3341
            }
3342
        } elsif (!$zdevs{$fs->{name}}) { # Don't add disks already used for ZFS
3343
            next if ($fs->{type} eq 'loop');
3344
            next if ($fs->{type} eq 'squashfs');
3345
            my $mp = $fs->{mountpoint};
3346
            next if ($fs->{type} eq 'rom');
3347
            $filesystems{$fs->{name}} = {
3348
                name=>$fs->{name},
3349
                blocksize=>$fs->{size},
3350
                mountpoint=>$fs->{mountpoint},
3351
                type=>$fs->{type},
3352
                nametype=> "$fs->{name} ($fs->{type} - " . ($mp?$mp:"not mounted") . " $fs->{size})",
3353
            }
3354
        }
3355
    }
3356

    
3357
    # Identify physical devices used for lvm
3358
    $cmd = "pvdisplay -c";
3359
    my $pvs = `$cmd`;
3360
    my @backupdevs; my @imagesdevs;
3361
    foreach my $line (split "\n", $pvs) {
3362
        my ($pvdev, $vgname) = split ":", $line;
3363
        $pvdev = $1 if ($pvdev =~ /\s+(\S+)/);
3364
        $pvdev = $1 if ($pvdev =~ /\/dev\/(\S+)/);
3365
        if ($filesystems{"$vgname-backupvol"}) {
3366
            push @backupdevs, $pvdev unless ($action eq 'listimagesdevices');
3367
        } elsif ($filesystems{"$vgname-imagesvol"}) {
3368
            push @imagesdevs, $pvdev unless ($action eq 'listbackupdevices');
3369
        }
3370
        if (@backupdevs) {
3371
            $filesystems{"$vgname-backupvol"}->{dev} = join(" ", @backupdevs);
3372
            $filesystems{"$vgname-backupvol"}->{nametype} = $filesystems{"$vgname-backupvol"}->{name} . " (lvm with " . $filesystems{"$vgname-backupvol"}->{type} . " on " . join(" ", @backupdevs) . " " . $filesystems{"$vgname-backupvol"}->{size} . ")";
3373
        }
3374
        if (@imagesdevs) {
3375
            $filesystems{"$vgname-imagesvol"}->{dev} = join(" ", @imagesdevs);
3376
            $filesystems{"$vgname-imagesvol"}->{nametype} = $filesystems{"$vgname-imagesvol"}->{name} . " (lvm with " . $filesystems{"$vgname-imagesvol"}->{type} . " on " . join(" ", @imagesdevs) . " " . $filesystems{"$vgname-imagesvol"}->{size} . ")";
3377
        }
3378
        delete $filesystems{$pvdev} if ($filesystems{$pvdev}); # Don't also list as physical device
3379
    }
3380
    my $jsonreply;
3381
    if ($action eq 'getbackupdevice' || $action eq 'getimagesdevice') {
3382
        return ''; # We should not get here
3383
    } elsif ($action eq 'getstoragedevices') {
3384
        return \%filesystems;
3385
    } elsif ($action eq 'listimagesdevices') {
3386
        $jsonreply .= qq|{"identifier": "name", "label": "nametype", "action": "$action", "items": |;
3387
        my @vals = sort {$b->{'isimagesdev'} cmp $a->{'isimagesdev'}} values %filesystems;
3388
        $jsonreply .= JSON->new->canonical(1)->pretty(1)->encode(\@vals);
3389
        $jsonreply .= "}";
3390
    } elsif ($action eq 'listbackupdevices') {
3391
        $jsonreply .= qq|{"identifier": "name", "label": "nametype", "action": "$action", "items": |;
3392
        my @vals = sort {$b->{'isbackupdev'} cmp $a->{'isbackupdev'}} values %filesystems;
3393
        $jsonreply .= JSON->new->canonical(1)->pretty(1)->encode(\@vals);
3394
        $jsonreply .= "}";
3395
    } else {
3396
        $jsonreply .= JSON->new->canonical(1)->pretty(1)->encode(\%filesystems);
3397
    }
3398
    return $jsonreply;
3399
}
3400

    
3401
sub do_liststoragepools {
3402
    my ($image, $action) = @_;
3403
    if ($help) {
3404
        return <<END
3405
GET:dojo:
3406
Returns available storage pools. If parameter dojo is set, JSON is padded for Dojo use.
3407
END
3408
    }
3409
    my %npool = (
3410
        "hostpath", "node",
3411
        "path", "--",
3412
        "name", "On node",
3413
        "rdiffenabled", 1,
3414
        "id", "-1");
3415
    my @p = @spools;
3416
    # Present node storage pool if user has sufficient privileges
3417
    if (index($privileges,"a")!=-1 || index($privileges,"n")!=-1) {
3418
        @p = (\%npool);
3419
        push @p, @spools;
3420
    }
3421

    
3422
    my $jsonreply;
3423
    $jsonreply .= "{\"identifier\": \"id\", \"label\": \"name\", \"items\":" if ($params{'dojo'});
3424
    $jsonreply .= to_json(\@p, {pretty=>1});
3425
    $jsonreply .= "}" if ($params{'dojo'});
3426
    return $jsonreply;
3427
}
3428

    
3429
# List images available for attaching to server
3430
sub do_listimages {
3431
    my ($img, $action) = @_;
3432
    if ($help) {
3433
        return <<END
3434
GET:image,image1:
3435
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.
3436
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.
3437
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".
3438
END
3439
    }
3440
    my $res;
3441
    $res .= header('application/json') unless ($console);
3442
    my $curimg1 = URI::Escape::uri_unescape($params{'image1'});
3443
    my @filteredfiles;
3444
    my @curusers = @users;
3445
    # If an admin user is looking at a server not belonging to him, allow him to see the server
3446
    # users images
3447
    if ($isadmin && $img && $img ne '--' && $register{$img} && $register{$img}->{'user'} ne $user) {
3448
        @curusers = ($register{$img}->{'user'}, "common");
3449
    }
3450

    
3451
    foreach my $u (@curusers) {
3452
        my @regkeys = (tied %register)->select_where("user = '$u'");
3453
        foreach my $k (@regkeys) {
3454
            my $val = $register{$k};
3455
            if ($val->{'user'} eq $u && (defined $spools[$val->{'storagepool'}]->{'id'} || $val->{'storagepool'}==-1)) {
3456
                my $f = $val->{'path'};
3457
                next if ($f =~ /\/images\/dummy.qcow2/);
3458
                my $itype = $val->{'type'};
3459
                if ($itype eq "vmdk" || $itype eq "img" || $itype eq "vhd" || $itype eq "vhdx" || $itype eq "qcow" || $itype eq "qcow2" || $itype eq "vdi") {
3460
                    my $hit = 0;
3461
                    if ($f =~ /(.+)\.master\.$itype/) {$hit = 1;} # don't list master images for user selections
3462
                    if ($f =~ /(.+)\/common\//) {$hit = 1;} # don't list common images for user selections
3463
                    my $dbstatus = $val->{'status'};
3464
                    if ($dbstatus ne "unused") {$hit = 1;} # Image is in a transitional state - do not use
3465
                    if ($hit == 0 || $img eq $f) {
3466
                        my $hypervisor = ($itype eq "vmdk" || $itype eq "vhd" || $itype eq "vhdx" || $itype eq "vdi")?"vbox":"kvm";
3467
                        my $notes = $val->{'notes'};
3468
                        $notes = "" if $notes eq "--";
3469
                        my %img = ("path", $f, "name", $val->{'name'}, "hypervisor", $hypervisor, "notes", $notes,
3470
                            "uuid", $val->{'uuid'}, "master", $val->{'master'}, "managementlink", $val->{'managementlink'}||"",
3471
                            "upgradelink", $val->{'upgradelink'}||"", "terminallink", $val->{'terminallink'}||"", "version", $val->{'version'}||"",
3472
                            "appid", $val->{'appid'}||"");
3473
                        push @filteredfiles, \%img;
3474
                    }
3475
                }
3476
            }
3477
        }
3478
    }
3479
    my %img = ("path", "--", "name", "--", "hypervisor", "kvm,vbox");
3480
    if ($curimg1) {
3481
        push @filteredfiles, \%img;
3482
    }
3483
    my $json_text = to_json(\@filteredfiles, {pretty=>1});
3484
    $res .= qq/{"identifier": "path", "label": "name", "items": $json_text }/;
3485
    return $res;
3486
}
3487

    
3488
sub Listcdroms {
3489
    my ($image, $action) = @_;
3490
    if ($help) {
3491
        return <<END
3492
GET::
3493
Lists the CD roms a user has access to.
3494
END
3495
    }
3496
    my $res;
3497
    $res .= header('application/json') unless ($console);
3498
    my @filteredfiles;
3499
    foreach my $u (@users) {
3500
        my @regkeys = (tied %register)->select_where("user = '$u'");
3501
        foreach my $k (@regkeys) {
3502
            my $val = $register{$k};
3503
            my $f = $val->{'path'};
3504
            if ($val->{'user'} eq $u && (defined $spools[$val->{'storagepool'}]->{'id'} || $val->{'storagepool'}==-1)) {
3505
                my $itype = $val->{'type'};
3506
                if ($itype eq "iso" || $itype eq "toast") {
3507
                    $notes = $val->{'notes'} || '';
3508
                    if ($u eq $user) {
3509
                        $installable = "true";
3510
                    #    $notes = "This CD/DVD may work just fine, however it has not been tested to work with Irigo Servers.";
3511
                    } else {
3512
                        $installable = $val->{'installable'} || 'false';
3513
                    #    $notes = "This CD/DVD has been tested to work with Irigo Servers." unless $notes;
3514
                    }
3515
                    my %img = ("path", $f, "name", $val->{'name'}, "installable", $installable, "notes", $notes);
3516
                    push @filteredfiles, \%img;
3517
                }
3518
            }
3519
        }
3520
    }
3521
    my %ioimg = ("path", "virtio", "name", "-- VirtIO disk (dummy) --");
3522
    push @filteredfiles, \%ioimg;
3523
    my %dummyimg = ("path", "--", "name", "-- No CD --");
3524
    push @filteredfiles, \%dummyimg;
3525
    #        @filteredfiles = (sort {$a->{'name'} cmp $b->{'name'}} @filteredfiles); # Sort by status
3526
    my $json_text = to_json(\@filteredfiles, {pretty=>1});
3527
    $res .= qq/{"identifier": "path", "label": "name", "items": $json_text }/;
3528
    return $res;
3529
}
3530

    
3531
sub do_listmasterimages {
3532
    my ($image, $action) = @_;
3533
    if ($help) {
3534
        return <<END
3535
GET::
3536
Lists master images available to the current user.
3537
END
3538
    }
3539
    my $res;
3540
    $res .= header('application/json') unless ($console);
3541

    
3542
    my @filteredfiles;
3543
    my @busers = @users;
3544
    push (@busers, $billto) if ($billto && $billto ne $user); # We include images from 'parent' user
3545

    
3546
    foreach my $u (@busers) {
3547
        my @regkeys = (tied %register)->select_where("user = '$u'");
3548
        foreach my $k (@regkeys) {
3549
            my $valref = $register{$k};
3550
            my $f = $valref->{'path'};
3551
            if ($valref->{'user'} eq $u && (defined $spools[$valref->{'storagepool'}]->{'id'} || $valref->{'storagepool'}==-1)) {
3552
                # Only list installable master images from billto account
3553
                next if ($billto && $u eq $billto && $valref->{'installable'} ne 'true');
3554

    
3555
                my $itype = $valref->{'type'};
3556
                if ($itype eq "qcow2" && $f =~ /(.+)\.master\.$itype/) {
3557
                    my $installable;
3558
                    my $status = $valref->{'status'};
3559
                    my $notes;
3560
                    if ($u eq $user) {
3561
                        $installable = "true";
3562
                        $notes = "This master image may work just fine, however it has not been tested to work with Stabile.";
3563
                    } else {
3564
                        $installable = $valref->{'installable'} || '';
3565
                        $notes = $valref->{'notes'};
3566
                        $notes = "This master image has been tested to work with Irigo Servers." unless $notes;
3567
                    }
3568
                    my %img = (
3569
                        "path", $f,
3570
                        "name", $valref->{'name'},
3571
                        "installable", $installable,
3572
                        "notes", $notes,
3573
                        "managementlink", $valref->{'managementlink'}||"",
3574
                        "upgradelink", $valref->{'upgradelink'}||"",
3575
                        "terminallink", $valref->{'terminallink'}||"",
3576
                        "image2", $valref->{'image2'}||"",
3577
                        "version", $valref->{'version'}||"",
3578
                        "appid", $valref->{'appid'}||"",
3579
                        "status", $status,
3580
                        "user", $valref->{'user'}
3581
                    );
3582
                    push @filteredfiles, \%img;
3583
                }
3584
            }
3585
        }
3586
    }
3587
    my %img = ("path", "--", "name", "--", "installable", "true", "status", "unused");
3588
    push @filteredfiles, \%img;
3589
    my $json_text = to_json(\@filteredfiles);
3590
    $res .= qq/{"identifier": "path", "label": "name", "items": $json_text }/;
3591
    return $res;
3592
}
3593

    
3594
sub Updatebtime {
3595
    my ($img, $action, $obj) = @_;
3596
    if ($help) {
3597
        return <<END
3598
GET:image:
3599
END
3600
    }
3601
    my $res;
3602
    $curimg = $curimg || $img;
3603
    my $imguser = $register{$curimg}->{'user'};
3604
    if ($isadmin || $imguser eq $user) {
3605
        my $btime;
3606
        $btime = getBtime($curimg, $imguser) if ($imguser);
3607
        if ($btime) {
3608
            $register{$curimg}->{'btime'} = $btime ;
3609
            $res .= "Status=OK $curimg has btime: " . scalar localtime( $btime ) . "\n";
3610
        } else {
3611
            $register{$curimg}->{'btime'} = '' ;
3612
            $res .= "Status=OK $curimg has no btime\n";
3613
        }
3614
    } else {
3615
        $res .= "Status=Error no access to $curimg\n";
3616
    }
3617
    return $res;
3618
}
3619

    
3620
sub Updateallbtimes {
3621
    my ($img, $action) = @_;
3622
    if ($help) {
3623
        return <<END
3624
GET::
3625
END
3626
    }
3627
    if ($isadmin) {
3628
        foreach my $path (keys %register) {
3629
            my $imguser = $register{$path}->{'user'};
3630
            my $btime = getBtime($path, $imguser);
3631
            if ($btime) {
3632
                $register{$path}->{'btime'} = $btime ;
3633
                $postreply .= "Status=OK $register{$path}->{'name'} ($path) has btime: " . scalar localtime( $btime ) . "\n";
3634
            } else {
3635
                $postreply .= "Status=OK $register{$path}->{'name'} ($path) has no btime\n";
3636
            }
3637
        }
3638
    } else {
3639
        $postreply .= "Status=ERROR you are not allowed to do this.\n";
3640
    }
3641
    return $postreply;
3642
}
3643

    
3644
# Activate image from fuel
3645
sub Activate {
3646
    my ($curimg, $action, $argref) = @_;
3647
    if ($help) {
3648
        return <<END
3649
GET:image, name, managementlink, upgradelink, terminallink, force:
3650
Activate an image from fuel storage, making it available for regular use.
3651
END
3652
    }
3653
    my %uargs = %{$argref};
3654
    my $name = URI::Escape::uri_unescape($uargs{'name'});
3655
    my $managementlink = URI::Escape::uri_unescape($uargs{'managementlink'});
3656
    my $upgradelink = URI::Escape::uri_unescape($uargs{'upgradelink'});
3657
    my $terminallink = URI::Escape::uri_unescape($uargs{'terminallink'});
3658
    my $version = URI::Escape::uri_unescape($uargs{'version'}) || '1.0b';
3659
    my $image2 =  URI::Escape::uri_unescape($uargs{'image2'});
3660
    my $force = $uargs{'force'};
3661

    
3662
    return "Status=ERROR image must be in fuel storage ($curimg)\n" unless ($curimg =~ /^\/mnt\/fuel\/pool(\d+)\/(.+)/);
3663
    my $pool = $1;
3664
    my $ipath = $2;
3665
    return "Status=ERROR image is not a qcow2 image ($curimg, $ipath)\n" unless ($ipath =~ /(.+\.qcow2$)/);
3666
    my $npath = $1;
3667
    my $ppath = '';
3668
    if ($npath =~ /(.*\/)(.+\.qcow2$)/) {
3669
        $npath = $2;
3670
        $ppath = $1;
3671
    }
3672
    my $imagepath = $tenderpathslist[$pool] . "/$user/fuel/$ipath";
3673
    my $newpath = $tenderpathslist[$pool] . "/$user/$npath";
3674
    return "Status=ERROR image not found ($imagepath)\n" unless (-e $imagepath);
3675
    return "Status=ERROR image already exists in destination ($newpath)\n" if (-e $newpath && !$force);
3676
    return "Status=ERROR image is in use ($newpath)\n" if (-e $newpath && $register{$newpath} && $register{$newpath}->{'status'} ne 'unused');
3677

    
3678
    my $virtualsize = `qemu-img info --force-share "$imagepath" | sed -n -e 's/^virtual size: .*(//p' | sed -n -e 's/ bytes)//p'`;
3679
    chomp $virtualsize;
3680
#    my $master = `qemu-img info --force-share "$imagepath" | sed -n -e 's/^backing file: //p' | sed -n -e 's/ (actual path:.*)\$//p'`;
3681
    my $master = `qemu-img info --force-share "$imagepath" | sed -n -e 's/^backing file: //p'`;
3682
    chomp $master;
3683

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

    
3699
        my $virtualsize2 = `qemu-img info --force-share "$image2path" | sed -n -e 's/^virtual size: .*(//p' | sed -n -e 's/ bytes)//p'`;
3700
        chomp $virtualsize2;
3701
#        my $master2 = `qemu-img info --force-share "$image2path" | sed -n -e 's/^backing file: //p' | sed -n -e 's/ (actual path:.*)\$//p'`;
3702
        my $master2 = `qemu-img info --force-share "$image2path" | sed -n -e 's/^backing file: //p'`;
3703
        chomp $master2;
3704
        if ($register{$master2}) {
3705
            $register{$master2}->{'status'} = 'used';
3706
        }
3707
        `mv "$image2path" "$newpath2"`;
3708
        if (-e $newpath2) {
3709
            my $ug = new Data::UUID;
3710
            my $newuuid = $ug->create_str();
3711
            unless ($name) {
3712
                $name = $npath if ($npath);
3713
                $name = $1 if ($name =~ /(.+)\.(qcow2)$/);
3714
            }
3715
            $register{$newpath2} = {
3716
                uuid => $newuuid,
3717
                path => $newpath2,
3718
                master => $master2,
3719
                name => "$name (data)",
3720
                user => $user,
3721
                storagepool => $pool,
3722
                type => 'qcow2',
3723
                status => 'unused',
3724
                version => $version,
3725
                virtualsize => $virtualsize2
3726
            };
3727
            $postreply .= "Status=OK Activated data image $newpath2, $name (data), $newuuid\n";
3728
        } else {
3729
            $postreply .=  "Status=ERROR Unable to activate $image2path, $newpath2\n";
3730
        }
3731
    }
3732

    
3733
    # Finish up primary image
3734
    if ($register{$master}) {
3735
        $register{$master}->{'status'} = 'used';
3736
    }
3737
    `mv "$imagepath" "$newpath"`;
3738
    if (-e $newpath) {
3739
        my $ug = new Data::UUID;
3740
        my $newuuid = $ug->create_str();
3741
        unless ($name) {
3742
            $name = $npath if ($npath);
3743
            $name = $1 if ($name =~ /(.+)\.(qcow2)$/);
3744
        }
3745
        $register{$newpath} = {
3746
            uuid => $newuuid,
3747
            path => $newpath,
3748
            master => $master,
3749
            name => $name,
3750
            user => $user,
3751
            storagepool => $pool,
3752
            image2 => $newpath2,
3753
            type => 'qcow2',
3754
            status => 'unused',
3755
            installable => 'true',
3756
            managementlink => $managementlink || '/stabile/pipe/http://{uuid}:10000/stabile/',
3757
            upgradelink => $upgradelink,
3758
            terminallink => $terminallink,
3759
            version => $version,
3760
            virtualsize => $virtualsize
3761
        };
3762
        $postreply .=  "Status=OK Activated $newpath, $name, $newuuid\n";
3763
    } else {
3764
        $postreply .=  "Status=ERROR Unable to activate $imagepath to $newpath\n";
3765
    }
3766
    return $postreply;
3767
}
3768

    
3769
sub Uploadtoregistry {
3770
    my ($path, $action, $obj) = @_;
3771
    if ($help) {
3772
        return <<END
3773
GET:image, force:
3774
Upload an image to the registry. Set [force] if you want to force overwrite images in registry - use with caution.
3775
END
3776
    }
3777
    $force = $obj->{'force'};
3778
    if (-e $path && ($register{$path}->{'user'} eq $user || $isadmin)) {
3779
        $postreply .= $main::uploadToOrigo->($engineid, $path, $force);
3780
    } else {
3781
        $postreply .= "Status=Error Not allowed\n";
3782
    }
3783
    return $postreply;
3784
}
3785

    
3786
sub Publish {
3787
    my ($uuid, $action, $parms) = @_;
3788
    if ($help) {
3789
        return <<END
3790
GET:image,appid,appstore,force:
3791
Publish a stack to registry. Set [force] if you want to force overwrite images in registry - use with caution.
3792
END
3793
    }
3794
    my $res;
3795
    $uuid = $parms->{'uuid'} if ($uuid =~ /^\// || !$uuid);
3796
    my $force = $parms->{'force'};
3797
    my $freshen = $parms->{'freshen'};
3798

    
3799
    if ($isreadonly) {
3800
        $res .= "Status=ERROR Your account does not have the necessary privilege.s\n";
3801
    } elsif (!$uuid || !$imagereg{$uuid}) {
3802
        $res .= "Status=ERROR At least specify activated master image uuid [uuid or path] to publish.\n";
3803
    } elsif ($imagereg{$uuid}->{'user'} ne $user && !$isadmin) {
3804
        $res .= "Status=ERROR Your account does not have the necessary privileges.\n";
3805
    } elsif ($imagereg{$uuid}->{'path'} =~ /.+\.master\.qcow2$/) {
3806
        if ($engineid eq $valve001id) { # On valve001 - check if meta file exists
3807
            if (-e $imagereg{$uuid}->{'path'} . ".meta") {
3808
                $res .= "On valve001. Found meta file $imagereg{$uuid}->{'path'}.meta\n";
3809
                my $appid = `cat $imagereg{$uuid}->{'path'}.meta | sed -n -e 's/^APPID=//p'`;
3810
                chomp $appid;
3811
                if ($appid) {
3812
                    $parms->{'appid'} = $appid;
3813
                    $register{$imagereg{$uuid}->{'path'}}->{'appid'} = $appid;
3814
                    tied(%register)->commit;
3815
                }
3816
            }
3817
        # On valve001 - move image to stacks
3818
            if ($imagereg{$uuid}->{'storagepool'} ne '0') {
3819
                $res .= "Status=OK Moving image: " . Move($imagereg{$uuid}->{'path'}, $user, 0) . "\n";
3820
            } else {
3821
                $res .= "Status=OK Image is already available in registry\n";
3822
            }
3823
        } else {
3824
        #    $console = 1;
3825
        #    my $link = Download($imagereg{$uuid}->{'path'});
3826
        #    chomp $link;
3827
        #    $parms->{'downloadlink'} = $link; # We now upload instead
3828
        #    $res .= "Status=OK Asking registry to download $parms->{'APPID'} image: $link\n";
3829
            if ($appstores) {
3830
                $parms->{'appstore'} = $appstores;
3831
            } elsif ($appstoreurl =~ /www\.(.+)\//) {
3832
                $parms->{'appstore'} = $1;
3833
                $res .= "Status=OK Adding registry: $1\n";
3834
            }
3835
        }
3836
#        $parms->{'appstore'} = 1 if ($freshen);
3837

    
3838
        my %imgref = %{$imagereg{$uuid}};
3839
        $parms = Hash::Merge::merge($parms, \%imgref);
3840
        my $postdata = to_json($parms);
3841
        my $postres = $main::postToOrigo->($engineid, 'publishapp', $postdata);
3842
        $res .= $postres;
3843
        my $appid;
3844
        $appid = $1 if ($postres =~ /appid: (\d+)/);
3845
        my $path = $imagereg{$uuid}->{'path'};
3846
        if ($freshen && $appid) {
3847
            $res .= "Status=OK Freshened the stack description\n";
3848
        } elsif ($appid) {
3849
            $register{$path}->{'appid'} = $appid if ($register{$path});
3850
            $res .= "Status=OK Received appid $appid for $path, uploading image to registry, hang on...\n";
3851
            my $upres .= $main::uploadToOrigo->($engineid, $path, $force);
3852
            $res .= $upres;
3853
            my $image2 = $register{$path}->{'image2'} if ($register{$path});
3854
            if ($upres =~ /Status=OK/ && $image2 && $image2 ne '--') { # Stack has a data image
3855
                $res .= $main::uploadToOrigo->($engineid, $image2, $force);
3856
            }
3857
        } else {
3858
            $res .= "Status=Error Did not get an appid\n";
3859
        }
3860
    } else {
3861
        $res .= "Status=ERROR You can only publish a master image.\n";
3862
    }
3863
    return $res;
3864
}
3865

    
3866
sub Release {
3867
    my ($uuid, $action, $parms) = @_;
3868
    if ($help) {
3869
        return <<END
3870
GET:image,appid,appstore,force,unrelease:
3871
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.
3872
END
3873
    }
3874
    my $res;
3875
    $uuid = $parms->{'uuid'} if ($uuid =~ /^\// || !$uuid);
3876
    my $force = $parms->{'force'};
3877
    my $unrelease = $parms->{'unrelease'};
3878

    
3879
    if (!$uuid || !$imagereg{$uuid}) {
3880
        $res .= "Status=ERROR At least specify master image uuid [uuid or path] to release.\n";
3881
    } elsif (!$isadmin) {
3882
        $res .= "Status=ERROR Your account does not have the necessary privileges.\n";
3883
    } elsif ($imagereg{$uuid}->{'path'} =~ /.+\.master\.qcow2$/ && $imagereg{$uuid}->{'appid'}) {
3884
        my $action = 'release';
3885
        my $targetuser = 'common';
3886
        if ($unrelease) {
3887
            $action = 'unrelease';
3888
            $targetuser = $user;
3889
        }
3890
        if ($appstores) {
3891
            $parms->{'appstore'} = $appstores;
3892
        } elsif ($appstoreurl =~ /www\.(.+)\//) {
3893
            $parms->{'appstore'} = $1;
3894
            $res .= "Status=OK Adding registry: $1\n";
3895
        }
3896
        $parms->{'appid'} = $imagereg{$uuid}->{'appid'};
3897
        $parms->{'force'} = $force if ($force);
3898
        $parms->{'unrelease'} = $unrelease if ($unrelease);
3899
        my $postdata = to_json($parms);
3900
        my $postres = $main::postToOrigo->($engineid, 'releaseapp', $postdata);
3901
        $res .= $postres;
3902
        my $appid;
3903
        $appid = $1 if ($postres =~ /Status=OK Moved (\d+)/);
3904
        my $path = $imagereg{$uuid}->{'path'};
3905
        if ($appid) {
3906
            $res.= "Now moving local stack to $targetuser\n";
3907
            # First move data image
3908
            my $image2 = $register{$path}->{'image2'} if ($register{$path});
3909
            my $newimage2 = $image2;
3910
            if ($image2 && $image2 ne '--' && $register{$image2}) { # Stack has a data image
3911
                if ($unrelease) {
3912
                    $newimage2 =~ s/common/$register{$image2}->{'user'}/;
3913
                } else {
3914
                    $newimage2 =~ s/$register{$image2}->{'user'}/common/;
3915
                }
3916
                $register{$path}->{'image2'} = $newimage2;
3917
                tied(%register)->commit;
3918
                $res .= Move($image2, $targetuser, '', '', 1);
3919
            }
3920
            # Move image
3921
            $res .= Move($path, $targetuser, '', '', 1);
3922
            $res .= "Status=OK $action $appid\n";
3923
        } else {
3924
            $res .= "Status=Error $action failed\n";
3925
        }
3926
    } else {
3927
        $res .= "Status=ERROR You can only $action a master image that has been published.\n";
3928
    }
3929
    return $res;
3930
}
3931

    
3932
sub do_unlinkmaster {
3933
    my ($img, $action) = @_;
3934
    if ($help) {
3935
        return <<END
3936
GET:image,path:
3937
END
3938
    }
3939
    my $res;
3940
    $res .= header('text/html') unless ($console);
3941
    if ($isreadonly) {
3942
        $res .= "Your account does not have the necessary privileges\n";
3943
    } elsif ($curimg) {
3944
        $res .= unlinkMaster($curimg) . "\n";
3945
    } else {
3946
        $res .= "Please specify master image to link\n";
3947
    }
3948
    return $res;
3949
}
3950

    
3951
# Simple action for unmounting all images
3952
sub do_unmountall {
3953
    my ($img, $action) = @_;
3954
    if ($help) {
3955
        return <<END
3956
GET:image,path:
3957
END
3958
    }
3959
    return "Your account does not have the necessary privileges\n" if ($isreadonly);
3960
    my $res;
3961
    $res .= header('text/plain') unless ($console);
3962
    $res .= "Unmounting all images for $user\n";
3963
    unmountAll();
3964
    $res .= "\n$postreply" if ($postreply);
3965
    return $res;
3966
}
3967

    
3968
sub Updatedownloads {
3969
    my ($img, $action) = @_;
3970
    if ($help) {
3971
        return <<END
3972
GET:image,path:
3973
END
3974
    }
3975
    my $res;
3976
    $res .= header('text/html') unless ($console);
3977
    my $txt1 = <<EOT
3978
Options -Indexes
3979
EOT
3980
    ;
3981
    `/bin/mkdir "$Stabile::basedir/download"` unless (-e "$Stabile::basedir/download");
3982
    $res .= "Writing .htaccess: -> $Stabile::basedir/download/.htaccess\n";
3983
    unlink("$Stabile::basedir/download/.htaccess");
3984
    `chown www-data:www-data "$Stabile::basedir/download"`;
3985
    `/bin/echo "$txt1" | sudo -u www-data tee "$Stabile::basedir/download/.htaccess"`; #This ugliness is needed because of ownership issues with Synology NFS
3986
    `chmod 644 "$Stabile::basedir/download/.htaccess"`;
3987
    foreach my $p (@spools) {
3988
        my $dir = $p->{'path'};
3989
        my $id = $p->{'id'};
3990
        `/bin/rm "$Stabile::basedir/download/$id"; /bin/ln -s "$dir" "$Stabile::basedir/download/$id"`;
3991
        $res .= "Writing .htaccess: $id -> $dir/.htaccess\n";
3992
        unlink("$dir/.htaccess");
3993
        `/bin/echo "$txt1" | tee "$dir/.htaccess"`;
3994
        `chown www-data:www-data "$dir/.htaccess"`;
3995
        `chmod 644 "$dir/.htaccess"`;
3996
    }
3997

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

    
4000
    foreach my $username (keys %userreg) {
4001
        my $require = '';
4002
        my $txt = <<EOT
4003
order deny,allow
4004
AuthName "Download"
4005
AuthType None
4006
TKTAuthLoginURL $baseurl/auth/login.cgi
4007
TKTAuthIgnoreIP on
4008
deny from all
4009
Satisfy any
4010
require user $username
4011
Options -Indexes
4012
EOT
4013
        ;
4014
        foreach my $p (@spools) {
4015
            my $dir = $p->{'path'};
4016
            my $id = $p->{'id'};
4017
            if (-d "$dir/$username") {
4018
                $res .= "Writing .htaccess: $id -> $dir/$username/.htaccess\n";
4019
                unlink("$dir/$username/.htaccess");
4020
                `/bin/echo "$txt1" | sudo -u www-data tee $dir/$username/.htaccess`;
4021
                if ($tenderlist[$p->{'id'}] eq 'local') {
4022
                    if (!(-e "$dir/$username/fuel") && -e "$dir/$username") {
4023
                        `mkdir "$dir/$username/fuel"`;
4024
                        `chmod 777 "$dir/$username/fuel"`;
4025
                    }
4026
                }
4027
            }
4028
        }
4029
    }
4030
    untie %userreg;
4031
    return $res;
4032
}
4033

    
4034
sub do_listpackages($action) {
4035
    my ($image, $action) = @_;
4036
    if ($help) {
4037
        return <<END
4038
GET:image:
4039
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.
4040
END
4041
    }
4042
    my $res;
4043
    $res .= header('text/plain') unless ($console);
4044

    
4045
    my $mac = $register{$image}->{'mac'};
4046
    my $macip;
4047
    if ($mac && $mac ne '--') {
4048
        unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
4049
        $macip = $nodereg{$mac}->{'ip'};
4050
        untie %nodereg;
4051
    }
4052
    $image =~ /(.+)/; $image = $1;
4053
    my $apps;
4054

    
4055
    if ($macip && $macip ne '--') {
4056
        my $cmd = qq[eval \$(/usr/bin/guestfish --ro -a "$image" --i --listen); ]; # sets $GUESTFISH_PID shell var
4057
        $cmd .= qq[root="\$(/usr/bin/guestfish --remote inspect-get-roots)"; ];
4058
        $cmd .= qq[guestfish --remote inspect-list-applications "\$root"; ];
4059
        $cmd .= qq[guestfish --remote inspect-get-product-name "\$root"; ];
4060
        $cmd .= qq[guestfish --remote exit];
4061
        $cmd = "$sshcmd $macip '$cmd'";
4062
        $apps = `$cmd`;
4063
    } else {
4064
        my $cmd;
4065
        #        my $pid = open my $cmdpipe, "-|",qq[/usr/bin/guestfish --ro -a "$image" --i --listen];
4066
        $cmd .= qq[eval \$(/usr/bin/guestfish --ro -a "$image" --i --listen); ];
4067
        # Start listening guestfish
4068
        my $daemon = Proc::Daemon->new(
4069
            work_dir => '/usr/local/bin',
4070
            setuid => 'www-data',
4071
            exec_command => $cmd
4072
        ) or do {$postreply .= "Status=ERROR $@\n";};
4073
        my $pid = $daemon->Init();
4074
        while ($daemon->Status($pid)) {
4075
            sleep 1;
4076
        }
4077
        # Find pid of the listening guestfish
4078
        my $pid2;
4079
        my $t = new Proc::ProcessTable;
4080
        foreach $p ( @{$t->table} ){
4081
            my $pcmd = $p->cmndline;
4082
            if ($pcmd =~ /guestfish.+$image/) {
4083
                $pid2 = $p->pid;
4084
                last;
4085
            }
4086
        }
4087

    
4088
        my $cmd2;
4089
        if ($pid2) {
4090
            $cmd2 .= qq[root="\$(/usr/bin/guestfish --remote=$pid2 inspect-get-roots)"; ];
4091
            $cmd2 .= qq[guestfish --remote=$pid2 inspect-list-applications "\$root"; ];
4092
            $cmd2 .= qq[guestfish --remote=$pid2 inspect-get-product-name "\$root"; ];
4093
            $cmd2 .= qq[guestfish --remote=$pid2 exit];
4094
        }
4095
        $apps = `$cmd2`;
4096
    }
4097
    if ($console) {
4098
        $res .= $apps;
4099
    } else {
4100
        my @packages;
4101
        my @packages2;
4102
        open my $fh, '<', \$apps or die $!;
4103
        my $i;
4104
        while (<$fh>) {
4105
            if ($_ =~ /\[(\d+)\]/) {
4106
                push @packages2, $packages[$i];
4107
                $i = $1;
4108
            } elsif ($_ =~ /(\S+): (.+)/ && $2) {
4109
                $packages[$i]->{$1} = $2;
4110
            }
4111
        }
4112
        close $fh or die $!;
4113
        $res .= to_json(\@packages, {pretty => 1});
4114
    }
4115
    return $res;
4116
}
4117

    
4118
sub Inject {
4119
    my ($image, $action, $obj) = @_;
4120
    if ($help) {
4121
        return <<END
4122
GET:image:
4123
Tries to inject drivers into a qcow2 image with a Windows OS installed on it. Image must not be in use.
4124
END
4125
    }
4126
    $uistatus = "injecting";
4127
    my $path = $obj->{path} || $curimg;
4128
    my $status = $obj->{status};
4129
    my $esc_localpath = shell_esc_chars($path);
4130

    
4131
    # Find out if we are dealing with a Windows image
4132
    # my $xml = `bash -c '/usr/bin/virt-inspector -a $esc_localpath'`;
4133
    my $xml = `bash -c '/usr/bin/virt-inspector -a $esc_localpath' 2>&1`;
4134
    # $res .= $xml . "\n";
4135
    my $xmlref;
4136
    my $osname;
4137
    $xmlref = XMLin($xml) if ($xml =~ /^<\?xml/);
4138
    $osname = $xmlref->{operatingsystem}->{name} if ($xmlref);
4139
    if ($xmlref && $osname eq 'windows') {
4140
    #    my $upath = $esc_localpath;
4141
        my $upath = $path;
4142
        # We need write privileges
4143
        $res .= `chmod 666 "$upath"`;
4144
        # First try to merge storage registry keys into Windows registry. If not a windows vm it simply fails.
4145
        $res .= `bash -c 'cat /usr/share/stabile/mergeide.reg | /usr/bin/virt-win-reg --merge "$upath"' 2>&1`;
4146
        # Then try to merge the critical device keys. This has been removed in win8 and 2012, so will simply fail for these.
4147
        $res .= `bash -c 'cat /usr/share/stabile/mergeide-CDDB.reg | /usr/bin/virt-win-reg --merge "$upath"' 2>&1`;
4148
        if ($res) { $main::syslogit->($user, "info", $res); $res = ''; }
4149

    
4150
        # Try to copy viostor.sys into image
4151
        my @winpaths = (
4152
            '/Windows/System32/drivers',
4153
            '/WINDOWS/system32/drivers',
4154
            '/WINDOWS/System32/drivers',
4155
            '/WINNT/system32/drivers'
4156
        );
4157
        foreach my $winpath (@winpaths) {
4158
            my $lscmd = qq|bash -c 'virt-ls -a "$upath" "$winpath"'|;
4159
            my $drivers = `$lscmd`;
4160
            if ($drivers =~ /viostor/i) {
4161
                $postreply .= "Status=$status viostor already installed in $winpath in $upath\n";
4162
                $main::syslogit->($user, "info", "viostor already installed in $winpath in $upath");
4163
                last;
4164
            } elsif ($drivers) {
4165
                `umount "$upath"`; # Unmount if mounted by browse operation or similar
4166
                my $cmd = qq|bash -c 'guestfish --rw -i -a "$upath" upload /usr/share/stabile/VIOSTOR.SYS $winpath/viostor.sys' 2>&1|;
4167
                my $error = `$cmd`;
4168
                if ($error) {
4169
                    $postreply .= "$cmd\n";
4170
                    $postreply .= "Status=ERROR Problem injecting virtio drivers into $winpath on $upath: $error\n";
4171
                    $main::syslogit->($user, "info", "Error injecting virtio drivers into $upath: $error");
4172
                } else {
4173
                    $postreply .= "Status=$status Injected virtio drivers into $upath\n";
4174
                    $main::syslogit->($user, "info", "Injected virtio drivers into $upath");
4175
                }
4176
                last;
4177
            } else {
4178
                $postreply .= "Status=ERROR No drivers found in $winpath\n";
4179
            }
4180
        }
4181

    
4182
    } else {
4183
        $postreply .= "Status=ERROR No Windows OS found in $osname image, not injecting drivers.\n";
4184
        $main::syslogit->($user, "info", "No Windows OS found ($osname) in image, not injecting drivers.");
4185
    }
4186
    my $msg = $postreply;
4187
    $msg = $1 if ($msg =~ /\w+=\w+ (.+)/);
4188
    chomp $msg;
4189
    $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$obj->{'uuid'}, status=>$status, message=>$msg});
4190
    $postreply .=  "Status=$uistatus $obj->{type} image: $obj->{name}\n";
4191
    $main::syslogit->($user, "info", "$uistatus $obj->{type} image: $obj->{name}: $uuid");
4192
    return $postreply;
4193
}
4194

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

    
4240
            $register{$newpath} = {
4241
                uuid=>$newuuid,
4242
                name=>"$obj->{name} (converted)",
4243
                notes=>$obj->{notes},
4244
                image2=>$obj->{image2},
4245
                managementlink=>$obj->{managementlink},
4246
                upgradelink=>$obj->{managementlink},
4247
                terminallink=>$obj->{terminallink},
4248
                storagepool=>$obj->{regstoragepool},
4249
                status=>$uistatus,
4250
                mac=>($obj->{regstoragepool} == -1)?$obj->{mac}:"",
4251
                size=>0,
4252
                realsize=>0,
4253
                virtualsize=>$obj->{virtualsize},
4254
                type=>"qcow2",
4255
                user=>$user
4256
            };
4257
            $register{$path}->{'status'} = $uistatus;
4258

    
4259
            my $daemon = Proc::Daemon->new(
4260
                work_dir => '/usr/local/bin',
4261
                exec_command => "perl -U steamExec $user $uistatus $obj->{status} \"$oldpath\" \"$newpath\""
4262
            ) or do {$postreply .= "Status=ERROR $@\n";};
4263
            my $pid = $daemon->Init() or do {$postreply .= "Status=ERROR $@\n";};
4264
            $postreply .=  "Status=OK $uistatus $obj->{type} image: $obj->{name}\n";
4265
            $main::syslogit->($user, "info", "$uistatus $obj->{type} image: $obj->{name}: $uuid");
4266
        } or do {$postreply .= "Status=ERROR $@\n";};
4267
        $main::updateUI->({tab=>"images", user=>$user, type=>"update"});
4268
    } else {
4269
        $postreply .= "Status=ERROR Only img and vmdk images can be converted\n";
4270
    }
4271
    return $postreply;
4272
}
4273

    
4274
sub Snapshot {
4275
    my ($image, $action, $obj) = @_;
4276
    if ($help) {
4277
        return <<END
4278
GET:image:
4279
Adds a snapshot to a qcow2 image. Image can not be in use by a running server.
4280
END
4281
    }
4282
    my $status = $obj->{status};
4283
    my $path = $obj->{path};
4284
    my $macip;
4285
    $uistatus = "snapshotting";
4286
    $uiuuid = $obj->{uuid};
4287
    if ($status ne "unused" && $status ne "used") {
4288
        $postreply .= "Status=ERROR Problem $uistatus $obj->{type} image: $obj->{name}\n";
4289
    } elsif ($obj->{type} eq "qcow2") {
4290
        my $newpath = $path;
4291
        my $hassnap;
4292
        my $snaptime = time;
4293
        if ($obj->{mac} && $path =~ /\/mnt\/stabile\/node\//) {
4294
            unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
4295
            $macip = $nodereg{$obj->{mac}}->{'ip'};
4296
            untie %nodereg;
4297
            $newpath = "$macip:$path";
4298
            my $esc_path = $path;
4299
            $esc_path =~ s/([ ])/\\$1/g;
4300
            my $qinfo = `$sshcmd $macip "sudo /usr/bin/qemu-img snapshot -l $esc_path"`;
4301
            $hassnap = ($qinfo =~ /snap1/g);
4302
            $postreply .= `$sshcmd $macip "sudo /usr/bin/qemu-img snapshot -d snap1 $esc_path"` if ($hassnap);
4303
        } else {
4304
            my $qinfo = `/usr/bin/qemu-img snapshot -l "$path"`;
4305
            $hassnap = ($qinfo =~ /snap1/g);
4306
            $postreply .= `/usr/bin/qemu-img snapshot -d snap1 "$path\n"` if ($hassnap);
4307
        }
4308
        eval {
4309
            if ($hassnap) {
4310
                $postreply .= "Status=Error Only one snapshot per image is supported for $obj->{type} image: $obj->{name} ";
4311
            } else {
4312
                $register{$path}->{'status'} = $uistatus;
4313
                $register{$path}->{'snap1'} = $snaptime;
4314

    
4315
                if ($macip) {
4316
                    my $esc_localpath = shell_esc_chars($path);
4317
                    $res .= `$sshcmd $macip "sudo /usr/bin/qemu-img snapshot -c snap1 $esc_localpath"`;
4318
                } else {
4319
                    $res .= `/usr/bin/qemu-img snapshot -c snap1 "$path"`;
4320
                }
4321
                $register{$path}->{'status'} = $status;
4322
                $postreply .=  "Status=$uistatus OK $uistatus $obj->{type} image: $obj->{name}\n";
4323
                $main::syslogit->($user, "info", "$uistatus $obj->{type} image: $obj->{name}: $uuid");
4324
            }
4325
            1;
4326
        } or do {$postreply .= "Status=ERROR $@\n";};
4327
        $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$obj->{'uuid'}, status=>$status, snap1=>$snaptime});
4328
    } else {
4329
        $postreply .= "Status=ERROR Only qcow2 images can be snapshotted\n";
4330
    }
4331
    return $postreply;
4332
}
4333

    
4334
sub Unsnap {
4335
    my ($image, $action, $obj) = @_;
4336
    if ($help) {
4337
        return <<END
4338
GET:image:
4339
Removes a snapshot from a qcow2 image. Image can not be in use by a running server.
4340
END
4341
    }
4342
    my $status = $obj->{status};
4343
    my $path = $obj->{path};
4344
    $uistatus = "unsnapping";
4345
    $uiuuid = $obj->{uuid};
4346
    my $macip;
4347

    
4348
    if ($status ne "unused" && $status ne "used") {
4349
        $postreply .= "Status=ERROR Problem $uistatus $obj->{type} image: $obj->{name}\n";
4350
    } elsif ($obj->{type} eq "qcow2") {
4351
        my $newpath = $path;
4352
        my $hassnap;
4353
        my $qinfo;
4354
        my $esc_path;
4355
        if ($obj->{mac} && $path =~ /\/mnt\/stabile\/node\//) {
4356
            unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
4357
            $macip = $nodereg{$obj->{mac}}->{'ip'};
4358
            untie %nodereg;
4359
            $newpath = "$macip:$path";
4360
            $esc_path = $path;
4361
            $esc_path =~ s/([ ])/\\$1/g;
4362
            $qinfo = `$sshcmd $macip "sudo /usr/bin/qemu-img snapshot -l $esc_path"`;
4363
            $hassnap = ($qinfo =~ /snap1/g);
4364
        } else {
4365
            $qinfo = `/usr/bin/qemu-img snapshot -l "$path"`;
4366
            $hassnap = ($qinfo =~ /snap1/g);
4367
        }
4368
        eval {
4369
            my $snaptime = time;
4370
            if ($hassnap) {
4371
                delete $register{$path}->{'snap1'};
4372
                $register{$path}->{'status'} = $uistatus;
4373
                if ($macip) {
4374
                    my $esc_localpath = shell_esc_chars($path);
4375
                    $res .= `$sshcmd $macip "sudo /usr/bin/qemu-img snapshot -d snap1 $esc_localpath"`;
4376
                } else {
4377
                    $res .= `/usr/bin/qemu-img snapshot -d snap1 "$path"`;
4378
                }
4379
                $register{$path}->{'status'} = $status;
4380
                $postreply .=  "Status=$uistatus OK $uistatus $obj->{type} image: $obj->{name}\n";
4381
                $main::syslogit->($user, "info", "$uistatus $obj->{type} image: $obj->{name}: $uuid");
4382
            } else {
4383
                $postreply .= "Status=ERROR No snapshot found in $obj->{name}\n";
4384
                delete $register{$path}->{'snap1'};
4385
                $uistatus = $status;
4386
            }
4387
            1;
4388
        } or do {$postreply .= "Status=ERROR $@\n";};
4389
        $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$obj->{'uuid'}, status=>$status, snap1=>'--'});
4390
    } else {
4391
        $postreply .= "Status=ERROR Only qcow2 images can be unsnapped\n";
4392
    }
4393
    return $postreply;
4394
}
4395

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

    
4452
sub Zbackup {
4453
    my ($image, $action, $obj) = @_;
4454
    if ($help) {
4455
        return <<END
4456
GET:mac, storagepool, synconly, snaponly, imageretention, backupretention:
4457
Backs all images on ZFS storage up by taking a storage snapshot. By default all shared storagepools are backed up.
4458
If storagepool -1 is specified, all ZFS node storages is backed up. If "mac" is specified, only specific node is backed up.
4459
If "synconly" is set, no new snapshots are taken - only syncing of snapshots is performed.
4460
If "snaponly" is set, only local active storage snapshot is taken - no sending to backup storage is done.
4461
"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],
4462
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.
4463
END
4464
    }
4465
    if ($isadmin) {
4466
        my $synconly = $obj->{'synconly'};
4467
        my $snaponly = $obj->{'snaponly'};
4468
        my $mac = $obj->{'mac'};
4469
        my $storagepool = $obj->{'storagepool'};
4470
        $storagepool = -1 if ($mac);
4471
        my $imageretention = $obj->{'imageretention'} || $imageretention;
4472
        my $backupretention = $obj->{'backupretention'} || $backupretention;
4473

    
4474
        my $basepath = "stabile-backup";
4475
        my $bpath = $basepath;
4476
        my $mounts = `/bin/cat /proc/mounts`;
4477
        my $zbackupavailable = (($mounts =~ /$bpath (\S+) zfs/)?$1:'');
4478
        unless ($zbackupavailable) {$postreply .= "Status=OK ZFS backup not available, only doing local snapshots\n";}
4479
        my $zfscmd = "zfs";
4480
        my $macip;
4481
        my $ipath = $spools[0]->{'zfs'} || 'stabile-images/images';
4482
        my @nspools = @spools;
4483
        if (!(defined $obj->{'storagepool'}) || $storagepool == -1) {
4484
            @nspools = () if ($storagepool == -1); # Only do node backups
4485
            unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
4486
#            my $nipath = $ipath;
4487
#            $nipath = "$1/node" if ($nipath =~ /(.+)\/(.+)/);
4488
            my $nipath = 'stabile-node/node';
4489
            foreach my $node (values %nodereg) {
4490
                push @nspools, {
4491
                    mac=>$node->{'mac'},
4492
                    macip=>$node->{'ip'},
4493
                    zfs=>$nipath,
4494
                    id=>-1
4495
                } if ($node->{'stor'} eq 'zfs' && (!$mac || $node->{'mac'} eq $mac))
4496
            }
4497
            untie %nodereg;
4498
        }
4499
        if (`pgrep zfs`) {
4500
            $postreply .= "Status=ERROR Another ZFS backup is running. Please wait a minute...\n";
4501
            $postmsg = "ERROR ERROR Another ZFS backup is running. Please wait a minute...";
4502
            return $postreply;
4503
        }
4504
        $postreply .= "Status=OK Performing ZFS backup on " . (scalar @nspools) . " storage pools with image retention $imageretention, backup retention $backupretention\n";
4505

    
4506
        foreach my $spool (@nspools) {
4507
            $ipath = $spool->{'zfs'};
4508
            if ($spool->{'id'} == -1) { # We're doing a node backup
4509
                $mac = $spool->{'mac'};
4510
                $macip = $spool->{'macip'};
4511
                $bpath = "$basepath/node-$mac";
4512
            } else {
4513
                next unless ($ipath);
4514
                next if (($storagepool || $storagepool eq '0') && $storagepool ne $spool->{'id'});
4515
                $bpath = "$basepath/$1" if ($ipath =~ /.+\/(.+)/);
4516
                $mac = '';
4517
                $macip = '';
4518
            }
4519
            if ($macip) {$zfscmd = "$sshcmd $macip sudo zfs";}
4520
            else {$zfscmd = "zfs";}
4521

    
4522
            $postreply .= "Status=OK Commencing ZFS backup of $ipath $macip, storagepool=$storagepool, synconly=$synconly, snaponly=$snaponly\n";
4523
            my $res;
4524
            my $cmd;
4525
            my @imagesnaps;
4526
            my @backupsnaps;
4527

    
4528
            # example: stabile-images/images@SNAPSHOT-20200524172901
4529
            $cmd = qq/$zfscmd list -t snapshot | grep '$ipath'/;
4530
            my $snaplist = `$cmd`;
4531
            my @snaplines = split("\n", $snaplist);
4532
            foreach my $snap (@snaplines) {
4533
                push @imagesnaps, $2 if ($snap =~ /(.*)\@SNAPSHOT-(\d+)/);
4534
            }
4535
            if ($zbackupavailable) {
4536
                $cmd = qq/zfs list -t snapshot | grep '$bpath'/;
4537
                $snaplist = `$cmd`;
4538
                @snaplines = split("\n", $snaplist);
4539
                foreach my $snap (@snaplines) {
4540
                    push @backupsnaps, $2 if ($snap =~ /(.*)\@SNAPSHOT-(\d+)/);
4541
                }
4542
            }
4543
        # Find matching snapshots
4544
            my $matches=0;
4545
            my $matchbase = 0;
4546
            foreach my $bsnap (@backupsnaps) {
4547
                if ($bsnap eq $imagesnaps[$matchbase + $matches]) { # matching snapshot found
4548
                    $matches++;
4549
                } elsif ($matches) { # backup snapshots are ahead of image snapshots - correct manually, i.e. delete them.
4550
                    $postreply .= "Status=ERROR Snapshots are out of sync.\n";
4551
                    $postmsg = "ERROR Snapshots are out of sync";
4552
                    $main::syslogit->($user, 'info', "ERROR snapshots of $ipath and $bpath are out of sync.");
4553
                    return $postreply;
4554
                } elsif (!$matchbase) { # Possibly there are image snapshots older than there are backup snapshots, find the match base i.e. first match in @imagesnaps
4555
                    my $mb=0;
4556
                    foreach my $isnap (@imagesnaps) {
4557
                        if ($bsnap eq $isnap) { # matching snapshot found
4558
                            $matchbase = $mb;
4559
                            $matches++;
4560
                            last;
4561
                        }
4562
                        $mb++;
4563
                    }
4564
                }
4565
            }
4566

    
4567
            my $lastisnap = $imagesnaps[scalar @imagesnaps -1];
4568
            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)/);
4569
            my $td = ($current_time - $lastisnaptime);
4570
            if ($td<=5) {
4571
                $postreply .= "Status=ERROR Last backup was taken $td seconds ago. Please wait a minute...\n";
4572
                $postmsg = "ERROR ERROR Last backup was taken $td seconds ago. Please wait a minute...";
4573
                return $postreply;
4574
            }
4575
            my $ni = scalar @imagesnaps;
4576
            my $nb = scalar @backupsnaps;
4577

    
4578
            # If there are unsynced image snaps - sync them
4579
            if ($zbackupavailable && !$snaponly) {
4580
                if (scalar @imagesnaps > $matches+$matchbase) {
4581
                    if ($matches > 0) { # We must have at least one common shapshot to sync
4582
                        for (my $j=$matches+$matchbase; $j < scalar @imagesnaps; $j++) {
4583
                            if ($macip) {
4584
                                $cmd = qq[$zfscmd "send -i $ipath\@SNAPSHOT-$imagesnaps[$j-1] $ipath\@SNAPSHOT-$imagesnaps[$j] | ssh 10.0.0.1 sudo zfs receive $bpath"]; # -R
4585
                            } else {
4586
                                $cmd = qq[zfs send -i $ipath\@SNAPSHOT-$imagesnaps[$j-1] $ipath\@SNAPSHOT-$imagesnaps[$j] | zfs receive $bpath]; # -R
4587
                            }
4588
                            $res = `$cmd 2>&1`;
4589
                            unless (
4590
                                ($res && !$macip) #ssh will warn about adding to list of known hosts
4591
                                    || $res =~ /cannot receive/
4592
                            ) {
4593
                                $matches++;
4594
                                $nb++;
4595
                                $postreply .= "Status=OK Sending ZFS snapshot $j $imagesnaps[$j-1]->$imagesnaps[$j] of $macip $ipath to $bpath $res\n";
4596
                                $main::syslogit->($user, 'info', "OK Sending ZFS snapshot $imagesnaps[$j-1]->$imagesnaps[$j] of $macip $ipath to $bpath $res");
4597
                            } else {
4598
                                $postreply .= "Status=Error Problem sending ZFS snapshot $j $imagesnaps[$j-1]->$imagesnaps[$j] of $macip $ipath to $bpath $res\n";
4599
                                $main::syslogit->($user, 'info', "Error Problem sending ZFS snapshot $imagesnaps[$j-1]->$imagesnaps[$j] of $macip $ipath to $bpath $res");
4600
                            }
4601
                        }
4602
                    } else {
4603
                        $postreply .= "Status=OK Unable to sync $ni snapshots, no common snapshot, trying to start from scratch.\n";
4604
                    }
4605
                }
4606
            }
4607
            $res = '';
4608

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

    
4768
sub Backupfuel {
4769
    my ($image, $action, $obj) = @_;
4770
    if ($help) {
4771
        return <<END
4772
GET:username, dozfs:
4773
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.
4774
END
4775
    }
4776
    my $username = $obj->{'username'} || $user;
4777
    return "Status=Error Not allowed\n" unless ($isadmin || $username eq $user);
4778

    
4779
    my $remolder = "14D";
4780
    my $stordevs = Liststoragedevices('', 'getstoragedevices');
4781
    my $backupdev = Getbackupdevice('', 'getbackupdevice');
4782
    my $backupdevtype = $stordevs->{$backupdev}->{type};
4783
    foreach my $spool (@spools) {
4784
        my $ppath = $spool->{"path"};
4785
        my $pid = $spool->{"id"};
4786
        if (($spool->{"zfs"} && $backupdevtype eq 'zfs') && !$obj->{'dozfs'}) {
4787
            $postreply .= "Status=OK Skipping fuel on ZFS storage: $ppath/$username/fuel\n";
4788
        } elsif ($pid eq '-1') {
4789
            ;
4790
        } elsif (!$backupdir || !(-d $backupdir)) {
4791
            $postreply .= "Status=OK Backup dir $backupdir does not exist\n";
4792
        } elsif (-d "$ppath/$username/fuel" && !is_folder_empty("$ppath/$username/fuel")) {
4793
            my $srcdir = "$ppath/$username/fuel";
4794
            my $destdir = "$backupdir/$username/fuel/$pid";
4795

    
4796
            `mkdir -p "$destdir"` unless (-e "$destdir");
4797
            # Do the backup
4798
            my $cmd = qq|/usr/bin/rdiff-backup --print-statistics "$srcdir" "$destdir"|;
4799
            my $res = `$cmd`;
4800
            $cmd = qq|/usr/bin/rdiff-backup --print-statistics --force --remove-older-than $remolder "$destdir"|;
4801
            $res .= `$cmd`;
4802
            if ($res =~ /Errors 0/) {
4803
                my $change = $1 if ($res =~ /TotalDestinationSizeChange \d+ \((.+)\)/);
4804
                $postreply .= "Status=OK Backed up $change, $srcdir -> $destdir\n";
4805
                $main::syslogit->($user, "info", "OK backed up $change, $srcdir -> $destdir") if ($change);
4806
            } else {
4807
                $res =~ s/\n/ /g;
4808
                $postreply .= "Status=Error There was a problem backup up $srcdir -> $destdir: $res\n";
4809
                $main::syslogit->($user, "there was a problem backup up $srcdir -> $destdir");
4810
            }
4811
        } else {
4812
            $postreply .= "Status=OK Skipping empty fuel on: $ppath/$username/fuel\n";
4813
        }
4814
    }
4815
    return $postreply;
4816
}
4817

    
4818
sub is_folder_empty {
4819
    my $dirname = shift;
4820
    opendir(my $dh, $dirname) or die "Not a directory";
4821
    return scalar(grep { $_ ne "." && $_ ne ".." } readdir($dh)) == 0;
4822
}
4823

    
4824
sub Backup {
4825
    my ($image, $action, $obj) = @_;
4826
    if ($help) {
4827
        return <<END
4828
GET:image, skipzfs:
4829
Backs an image up. Set [skipzfs] if ZFS backup is configured, and you do not want to skip images on ZFS storage.
4830
END
4831
    }
4832
    my $path = $obj->{path} || $image;
4833
    my $status = $obj->{status};
4834
    my $skipzfs = $obj->{skipzfs};
4835
    $uistatus = "backingup";
4836
    $uipath = $path;
4837
    my $remolder;
4838
    $remolder = "14D" if ($obj->{bschedule} eq "daily14");;
4839
    $remolder = "7D" if ($obj->{bschedule} eq "daily7");
4840

    
4841
    my $stordevs = Liststoragedevices('', 'getstoragedevices');
4842
    my $backupdev = Getbackupdevice('', 'getbackupdevice');
4843
    my $backupdevtype = $stordevs->{$backupdev}->{type};
4844
    # Nodes are assumed to alwasy use ZFS
4845
    if ($backupdevtype eq 'zfs' && $skipzfs && ($obj->{regstoragepool} == -1 || $spools[$obj->{regstoragepool}]->{'zfs'})) {
4846
        return "Status=OK Skipping image on ZFS $path\n";
4847
    }
4848
    if ($status eq "snapshotting" || $status eq "unsnapping" || $status eq "reverting" || $status eq "cloning" ||
4849
        $status eq "moving" || $status eq "converting") {
4850
        $postreply .= "Status=ERROR Problem backing up $obj->{type} image: $obj->{name}\n";
4851
    } elsif ($obj->{regstoragepool} == -1) {
4852
        my $res = createNodeTask($obj->{mac}, "BACKUP $user $uistatus $status \"$path\" \"$backupdir\" $remolder", $status);
4853
        if ($res) {
4854
            $postreply .= "OK not backingup image: $obj->{name} (on node, $res)\n";
4855
        } else {
4856
            $register{$path}->{'status'} = $uistatus;
4857
            $uistatus = "lbackingup" if ($status eq "active"); # Do lvm snapshot before backing up
4858
            $main::syslogit->($user, "info", "$uistatus $obj->{type} image: $obj->{name}: $uuid");
4859
            $postreply .= "Status=backingup OK backingup image: $obj->{name} (on node)\n";
4860
        }
4861
    } elsif (!$spools[$obj->{regstoragepool}]->{'rdiffenabled'}) {
4862
        $postreply .= "Status=ERROR Rdiff-backup has not been enabled for this storagepool ($spools[$obj->{regstoragepool}]->{'name'})\n";
4863
    } else {
4864
        if ($spools[$obj->{regstoragepool}]->{'hostpath'} eq "local" && $status eq "active") {
4865
            my $poolpath = $spools[$obj->{regstoragepool}]->{'path'};
4866
            # We only need to worry about taking an LVM snapshot if the image is in active use
4867
            # We also check if the images is actually on an LVM partition
4868
            my $qi = `/bin/cat /proc/mounts | grep "$poolpath"`; # Find the lvm volume mounted on /mnt/images
4869
            ($qi =~ m/\/dev\/mapper\/(\S+)-(\S+) $pool.+/g)[-1]; # Select last match
4870
            my $lvolgroup = $1;
4871
            my $lvol = $2;
4872
            if ($lvolgroup && $lvol) {
4873
                $uistatus = "lbackingup";
4874
            }
4875
        }
4876
        if ($uistatus ne "lbackingup" && $status eq "active") {
4877
            $postreply .= "Status=ERROR Image is not on an LVM partition - suspend before backing up.\n";
4878
            $main::updateUI->({tab=>"images", user=>$user, type=>"update", path=>$path, status=>$uistatus, message=>"Image is not on an LVM partition - suspend before backing up"});
4879
        } else {
4880
            my $buser;
4881
            my $bname;
4882
            if ($path =~ /.*\/(common|$user)\/(.+)/) {
4883
                $buser = $1;
4884
                $bname = $2;
4885
            }
4886
            if ($buser && $bname) {
4887
                my $dirpath = $spools[$obj->{regstoragepool}]->{'path'};
4888
                #chop $dirpath; # Remove last /
4889
                eval {
4890
                    $register{$path}->{'status'} = $uistatus;
4891
                    my $daemon = Proc::Daemon->new(
4892
                        work_dir => '/usr/local/bin',
4893
                        exec_command => "perl -U steamExec $buser $uistatus $status \"$bname\" \"$dirpath\" \"$backupdir\" $remolder"
4894
                    ) or do {$postreply .= "Status=ERROR $@\n";};
4895
                    my $pid = $daemon->Init();
4896
                    $postreply .=  "Status=backingup OK backingup image: $obj->{name}\n";
4897
                    $main::syslogit->($user, "info", "$uistatus $obj->{type} image: $obj->{name}: $bname");
4898
                    1;
4899
                } or do {$postreply .= "Status=ERROR $@\n";}
4900
            } else {
4901
                $postreply .= "Status=ERROR Problem backing up $path\n";
4902
            }
4903
        }
4904
    }
4905
    return $postreply;
4906
}
4907

    
4908
sub Restore {
4909
    my ($image, $action, $obj) = @_;
4910
    if ($help) {
4911
        return <<END
4912
GET:image:
4913
Backs an image up.
4914
END
4915
    }
4916
    my $path = $obj->{path};
4917
    my $status = $obj->{status};
4918
    $uistatus = "restoring";
4919
    my($bname, $dirpath, $suffix) = fileparse($path, (".vmdk", ".img", ".vhd", ".vhdx", ".qcow", ".qcow2", ".vdi", ".iso"));
4920
    my $backup = $params{"backup"} || $obj->{backup};
4921
    my $pool = $register{$path}->{'storagepool'};
4922
    $pool = "0" if ($pool == -1);
4923
    my $poolpath = $spools[$pool]->{'path'};
4924
    my $restorefromdir = $backupdir;
4925
    my $inc = $backup;
4926
    my $subdir; # 1 level of subdirs supported
4927
    $subdir = $1 if ($dirpath =~ /.+\/$obj->{user}(\/.+)?\//);
4928

    
4929
    if ($backup =~ /^SNAPSHOT-(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/) { # We are dealing with a zfs restore
4930
        $inc = "$1-$2-$3-$4-$5-$6";
4931
        foreach my $spool (@spools) {
4932
            my $ppath = $spool->{"path"};
4933
            if (-e "$ppath/.zfs/snapshot/$backup/$obj->{user}$subdir/$bname$suffix") {
4934
                $restorefromdir = "$ppath/.zfs/snapshot/$backup";
4935
                last;
4936
            }
4937
        }
4938
    } else {
4939
        if ($backup eq "mirror") {
4940
            my $mir = `/bin/ls "$backupdir/$obj->{user}/$bname$suffix/rdiff-backup-data" | grep current_mirror`;
4941
            if ($mir =~ /current_mirror\.(\S+)\.data/) {
4942
                $inc = $1;
4943
            }
4944
        }
4945
        $inc =~ tr/:T/-/; # qemu-img does not like colons in file names - go figure...
4946
        $inc = substr($inc,0,-6);
4947
    }
4948
    $uipath = "$poolpath/$obj->{user}$subdir/$bname.$inc$suffix";
4949
    my $i;
4950
    if (-e $uipath) {
4951
        $i = 1;
4952
        while (-e "$poolpath/$obj->{user}$subdir/$bname.$inc.$i$suffix") {$i++;}
4953
        $uipath = "$poolpath/$obj->{user}$subdir/$bname.$inc.$i$suffix";
4954
    }
4955

    
4956
    if (-e $uipath) {
4957
        $postreply .= "Status=ERROR This image is already being restored\n";
4958
    } elsif ($obj->{user} ne $user && !$isadmin) {
4959
        $postreply .= "Status=ERROR No restore privs\n";
4960
    } elsif (!$backup || $backup eq "--") {
4961
        $postreply .= "Status=ERROR No backup selected\n";
4962
    } elsif (overQuotas($obj->{virtualsize})) {
4963
        $postreply .= "Status=ERROR Over quota (". overQuotas($obj->{virtualsize}) . ") restoring: $obj->{name}\n";
4964
    } elsif (overStorage($obj->{ksize}*1024, $pool+0)) {
4965
        $postreply .= "Status=ERROR Out of storage in destination pool restoring: $obj->{name}\n";
4966
    } else {
4967
        my $ug = new Data::UUID;
4968
        my $newuuid = $ug->create_str();
4969
        $register{$uipath} = {
4970
            uuid=>$newuuid,
4971
            status=>"restoring",
4972
            name=>"$obj->{name} ($inc)" . (($i)?" $i":''),
4973
            notes=>$obj->{notes},
4974
            image2=>$obj->{image2},
4975
            managementlink=>$obj->{managementlink},
4976
            upgradelink=>$obj->{upgradelink},
4977
            terminallink=>$obj->{terminallink},
4978
            size=>0,
4979
            realsize=>0,
4980
            virtualsize=>$obj->{virtualsize},
4981
            type=>$obj->{type},
4982
            user=>$user
4983
        };
4984
        eval {
4985
            $register{$path}->{'status'} = $uistatus;
4986
            my $daemon = Proc::Daemon->new(
4987
                work_dir => '/usr/local/bin',
4988
                exec_command => "perl -U steamExec $obj->{user} $uistatus $status \"$path\" \"$restorefromdir\" \"$backup\" \"$uipath\""
4989
            ) or do {$postreply .= "Status=ERROR $@\n";};
4990
            my $pid = $daemon->Init();
4991
            $postreply .=  "Status=$uistatus OK $uistatus $obj->{type} image: $obj->{name} ($inc)". ($console?", $newuuid\n":"\n");
4992
            $main::syslogit->($user, "info", "$uistatus $obj->{type} image: $obj->{name} ($inc), $uipath, $newuuid: $uuid");
4993
            1;
4994
        } or do {$postreply .= "Status=ERROR $@\n";};
4995
        $main::updateUI->({tab=>"images", user=>$user, type=>"update"});
4996
    }
4997
    return $postreply;
4998
}
4999

    
5000
sub Master {
5001
    my ($image, $action, $obj) = @_;
5002
    if ($help) {
5003
        return <<END
5004
GET:image:
5005
Converts an image to a master image. Image must not be in use.
5006
END
5007
    }
5008
    my $path = $obj->{path};
5009
    my $status = $register{$path}->{status};
5010
    $path =~ /(.+)\.$obj->{type}$/;
5011
    my $namepath = $1;
5012
    my $uiname;
5013
    if (!$register{$path}) {
5014
        $postreply .= "Status=ERROR Image $path not found\n";
5015
    } elsif ($status ne "unused") {
5016
        $postreply .= "Status=ERROR Only unused images may be mastered\n";
5017
#    } elsif ($namepath =~ /(.+)\.master/ || $register{$path}->{'master'}) {
5018
#        $postreply .= "Status=ERROR Only one level of mastering is supported\n";
5019
    } elsif ($obj->{istoragepool} == -1 || $obj->{regstoragepool} == -1) {
5020
        $postreply .= "Status=ERROR Unable to master $obj->{name} (master images are not supported on node storage)\n";
5021
    } elsif ($obj->{type} eq "qcow2") {
5022
        # Promoting a regular image to master
5023
        # First find an unused path
5024
        if (-e "$namepath.master.$obj->{type}") {
5025
            my $i = 1;
5026
            while ($register{"$namepath.$i.master.$obj->{type}"} || -e "$namepath.$i.master.$obj->{type}") {$i++;};
5027
            $uinewpath = "$namepath.$i.master.$obj->{type}";
5028
        } else {
5029
            $uinewpath = "$namepath.master.$obj->{type}";
5030
        }
5031

    
5032
        $uipath = $path;
5033
        $uiname = "$obj->{name}";
5034
        eval {
5035
            my $qinfo = `/bin/mv -iv "$path" "$uinewpath"`;
5036
            $register{$path}->{'name'} = $uiname;
5037
            $register{$uinewpath} = $register{$path};
5038
            delete $register{$path};
5039
            $postreply .= "Status=$status Mastered $obj->{type} image: $obj->{name}\n";
5040
            chop $qinfo;
5041
            $main::syslogit->($user, "info", $qinfo);
5042
            1;
5043
        } or do {$postreply .= "Status=ERROR $@\n";};
5044
        sleep 1;
5045
        $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$obj->{'uuid'}, newpath=>$uinewpath, status=>$status, name=>$uiname});
5046
    } else {
5047
        $postreply .= "Status=ERROR Only qcow2 images may be mastered\n";
5048
    }
5049
    return $postreply;
5050
}
5051

    
5052
sub Unmaster {
5053
    my ($image, $action, $obj) = @_;
5054
    if ($help) {
5055
        return <<END
5056
GET:image:
5057
Converts a master image to a regular image. Image must not be in use.
5058
END
5059
    }
5060
    my $path = $obj->{path};
5061
    my $status = $register{$path}->{status};
5062
    $path =~ /(.+)\.$obj->{type}$/;
5063
    my $namepath = $1;
5064
    my $haschildren = 0;
5065
    my $child;
5066
    my $uinewpath;
5067
    my $iname;
5068
    my @regvalues = values %register;
5069
    foreach my $val (@regvalues) {
5070
        if ($val->{'master'} eq $path) {
5071
            $haschildren = 1;
5072
            $child = $val->{'name'};
5073
            last;
5074
        }
5075
    }
5076
    if (!$register{$path}) {
5077
        $postreply .= "Status=ERROR Image $path not found\n";
5078
    } elsif ($haschildren) {
5079
        $postreply .= "Status=Error Cannot unmaster image. This image is used as master by: $child\n";
5080
    } elsif ($status ne "unused" && $status ne "used") {
5081
        $postreply .= "Status=ERROR Only used and unused images may be unmastered\n";
5082
    } elsif (!( ($namepath =~ /(.+)\.master/) || ($obj->{master} && $obj->{master} ne "--")) ) {
5083
        $postreply .= "Status=ERROR You can only unmaster master or child images\n";
5084
    } elsif (($obj->{istoragepool} == -1 || $obj->{regstoragepool} == -1) && $namepath =~ /(.+)\.master/) {
5085
        $postreply .= "Status=ERROR Unable to unmaster $obj->{name} (master images are not supported on node storage)\n";
5086
    } elsif ($obj->{type} eq "qcow2") {
5087
        # Demoting a master to regular image
5088
        if ($action eq 'unmaster' && $namepath =~ /(.+)\.master$/) {
5089
            $namepath = $1;
5090
            $uipath = $path;
5091
            # First find an unused path
5092
            if (-e "$namepath.$obj->{type}") {
5093
                my $i = 1;
5094
                while ($register{"$namepath.$i.$obj->{type}"} || -e "$namepath.$i.$obj->{type}") {$i++;};
5095
                $uinewpath = "$namepath.$i.$obj->{type}";
5096
            } else {
5097
                $uinewpath = "$namepath.$obj->{type}";
5098
            }
5099

    
5100
            $iname = $obj->{name};
5101
            $iname =~ /(.+)( \(master\))/;
5102
            $iname = $1 if $2;
5103
            eval {
5104
                my $qinfo = `/bin/mv -iv "$path" "$uinewpath"`;
5105
                $register{$path}->{'name'} = $iname;
5106
                $register{$uinewpath} = $register{$path};
5107
                delete $register{$path};
5108
                $postreply .=  "Status=$status Unmastered $obj->{type} image: $obj->{name}\n";
5109
                chomp $qinfo;
5110
                $main::syslogit->($user, "info", $qinfo);
5111
                1;
5112
            } or do {$postreply .= "Status=ERROR $@\n";}
5113
    # Rebasing a child image
5114
        } elsif ($action eq 'rebase' && $obj->{master} && $obj->{master} ne "--") {
5115
            $uistatus = "rebasing";
5116
            $uipath = $path;
5117
            $iname = $obj->{name};
5118
            $iname =~ /(.+)( \(child\d*\))/;
5119
            $iname = $1 if $2;
5120
            my $temppath = "$path.temp";
5121
            $uipath = $path;
5122
            $uimaster = "--";
5123
            my $macip;
5124

    
5125
            if ($obj->{mac} && $path =~ /\/mnt\/stabile\/node\//) {
5126
                unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
5127
                $macip = $nodereg{$obj->{mac}}->{'ip'};
5128
                untie %nodereg;
5129
            }
5130
            eval {
5131
                my $master = $register{$path}->{'master'};
5132
                my $usedmaster = '';
5133
#                @regvalues = values %register;
5134
                if ($master && $master ne '--') {
5135
                    foreach my $valref (@regvalues) {
5136
                        $usedmaster = 1 if ($valref->{'master'} eq $master && $valref->{'path'} ne $path); # Check if another image is also using this master
5137
                    }
5138
                }
5139
                $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$obj->{'uuid'}, status=>$uistatus});
5140
                $register{$path} = {
5141
                    master=>"",
5142
                    name=>"$iname",
5143
                    notes=>$obj->{notes},
5144
                    status=>$uistatus,
5145
                    snap1=>$obj->{snap1},
5146
                    managementlink=>$obj->{managementlink},
5147
                    upgradelink=>$obj->{upgradelink},
5148
                    terminallink=>$obj->{terminallink},
5149
                    image2=>$obj->{image2},
5150
                    storagepool=>$obj->{istoragepool},
5151
                    status=>$uistatus
5152
                };
5153

    
5154
                if ($macip) {
5155
                    my $esc_localpath = shell_esc_chars($path);
5156
                    my $esc_localpath2 = shell_esc_chars($temppath);
5157
                    $res .= `$sshcmd $macip "/usr/bin/qemu-img convert $esc_localpath -O qcow2 $esc_localpath2"`;
5158
                    $res .= `$sshcmd $macip "if [ -f $esc_localpath2 ]; then /bin/mv -v $esc_localpath2 $esc_localpath; fi"`;
5159
                } else {
5160
                    $res .= `/usr/bin/qemu-img convert -O qcow2 "$path" "$temppath"`;
5161
                    $res .= `if [ -f "$temppath" ]; then /bin/mv -v "$temppath" "$path"; fi`;
5162
                }
5163
                if ($master && !$usedmaster) {
5164
                    $register{$master}->{'status'} = 'unused';
5165
                    $main::syslogit->('info', "Freeing master $master");
5166
                }
5167
                $register{$path}->{'master'} = '';
5168
                $register{$path}->{'status'} = $status;
5169

    
5170
                $postreply .= "Status=OK $uistatus $obj->{type} image: $obj->{name}\n";
5171
                $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$obj->{'uuid'}, status=>$status});
5172
                $main::syslogit->($user, "info", "$uistatus $obj->{type} image: $obj->{name}: $uuid");
5173
                1;
5174
            } or do {$postreply .= "Status=ERROR $@\n";}
5175
        } else {
5176
            $postreply .= "Status=ERROR Not a master, not a child \"$obj->{name}\"\n";
5177
        }
5178
        sleep 1;
5179
        $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$obj->{'uuid'}, newpath=>$uinewpath, name=>$iname, status=>$status});
5180
    } else {
5181
        $postreply .= "Status=ERROR Only qcow2 images may be unmastered\n";
5182
    }
5183
    return $postreply;
5184
}
5185

    
5186
# Save or create new image
5187
sub Save {
5188
    my ($img, $action, $obj) = @_;
5189
    if ($help) {
5190
        return <<END
5191
POST:path, uuid, name, type, virtualsize, storagepool, user:
5192
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.
5193
Depending on your privileges not all changes are permitted. If you save without specifying a uuid or path, a new image is created.
5194
END
5195
    }
5196
    my $path = $obj->{path};
5197
    my $uuid = $obj->{uuid};
5198
    my $status = $obj->{status};
5199
    if ($status eq "new") {
5200
        # Create new image
5201
        my $ug = new Data::UUID;
5202
        if (!$uuid || $uuid eq '--') {
5203
            $uuid = $ug->create_str();
5204
        } else { # Validate
5205
            my $valuuid  = $ug->from_string($uuid);
5206
            if ($ug->to_string($valuuid) eq $uuid) {
5207
                ;
5208
            } else {
5209
                $uuid = $ug->create_str();
5210
            }
5211
        }
5212
        my $newuuid = $uuid;
5213
        my $pooldir = $spools[$obj->{storagepool}]->{'path'};
5214
        my $cmd;
5215
        my $name = $obj->{name};
5216
        $name =~ s/\./_/g; # Remove unwanted chars
5217
        $name =~ s/\//_/g;
5218
        eval {
5219
            my $ipath = "$pooldir/$user/$name.$obj->{type}";
5220
            $obj->{type} = "qcow2" unless ($obj->{type});
5221
            # Find an unused path
5222
            if ($register{$ipath} || -e "$ipath") {
5223
                my $i = 1;
5224
                while ($register{"$pooldir/$user/$name.$i.$obj->{type}"} || -e "$pooldir/$user/$name.$i.$obj->{type}") {$i++;};
5225
                $ipath = "$pooldir/$user/$name.$i.$obj->{type}";
5226
                $name = "$name.$i";
5227
            }
5228

    
5229
            if ($obj->{type} eq 'qcow2' || $obj->{type} eq 'vmdk') {
5230
                my $size = ($obj->{msize})."M";
5231
                my $format = "qcow2";
5232
                $format = "vmdk" if ($path1 =~ /\.vmdk$/);
5233
                $cmd = qq|/usr/bin/qemu-img create -f $format "$ipath" "$size"|;
5234
            } elsif ($obj->{type} eq 'img') {
5235
                my $size = ($obj->{msize})."M";
5236
                $cmd = qq|/usr/bin/qemu-img create -f raw "$ipath" "$size"|;
5237
            } elsif ($obj->{type} eq 'vdi') {
5238
                my $size = $obj->{msize};
5239
                $cmd = qq|/usr/bin/VBoxManage createhd --filename "$ipath" --size "$size" --format VDI|;
5240
            }
5241
            $obj->{name} = 'New Image' if (!$obj->{name} || $obj->{name} eq '--' || $obj->{name} =~ /^\./ || $obj->{name} =~ /\//);
5242
            if (-e $ipath) {
5243
                $postreply .= "Status=ERROR Image already exists: \"$obj->{name}\" in \"$ipath\”\n";
5244
            } elsif (overQuotas($obj->{ksize}*1024)) {
5245
                $postreply .= "Status=ERROR Over quota (". overQuotas($obj->{ksize}*1024) . ") creating: $obj->{name}\n";
5246
                $main::updateUI->({tab=>"images", user=>$user, type=>"update", message=>"Over quota in storage pool $obj->{storagepool}"});
5247
                $main::syslogit->($user, "info", "Over quota in storage pool $obj->{storagepool}, not creating $obj->{type} image $obj->{name}");
5248
            } elsif (overStorage($obj->{ksize}*1024, $obj->{storagepool}+0)) {
5249
                $postreply .= "Status=ERROR Out of storage in destination pool creating: $obj->{name}\n";
5250
                $main::updateUI->({tab=>"images", user=>$user, type=>"update", message=>"Out of storage in storage pool $obj->{storagepool}"});
5251
                $main::syslogit->($user, "info", "Out of storage in storage pool $obj->{storagepool}, not creating $obj->{type} image $obj->{name}");
5252
            } elsif ($obj->{virtualsize} > 10*1024*1024 && $obj->{name} && $obj->{name} ne '--') {
5253
                $register{$ipath} = {
5254
                    uuid=>$newuuid,
5255
                    name=>$obj->{name},
5256
                    user=>$user,
5257
                    notes=>$obj->{notes},
5258
                    type=>$obj->{type},
5259
                    size=>0,
5260
                    realsize=>0,
5261
                    virtualsize=>$obj->{virtualsize},
5262
                    storagepool=>$spools[0]->{'id'},
5263
                    created=>$current_time,
5264
                    managementlink=>$obj->{managementlink},
5265
                    upgradelink=>$obj->{upgradelink},
5266
                    terminallink=>$obj->{terminallink},
5267
                    status=>"creating"
5268
                };
5269
                $uipath = $ipath;
5270
                my $res = `$cmd`;
5271
                $register{$ipath}->{'status'} = 'unused';
5272

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

    
5377
                $register{$path} = {
5378
                    name=>$obj->{name},
5379
                    user=>$obj->{user},
5380
                    notes=>$obj->{notes},
5381
                    bschedule=>$obj->{bschedule},
5382
                    installable=>$obj->{installable},
5383
                    managementlink=>$obj->{managementlink},
5384
                    upgradelink=>$obj->{upgradelink},
5385
                    terminallink=>$obj->{terminallink},
5386
                    action=>""
5387
                };
5388
                my $domains = $register{$path}->{'domains'};
5389
                if ($status eq 'downloading') {
5390
                    unless (`pgrep $obj->{name}`) { # Check if image is in fact being downloaded
5391
                        $status = 'unused';
5392
                        $register{$path}->{'status'} = $status;
5393
                        unlink ("$path.meta") if (-e "$path.meta");
5394
                    }
5395
                }
5396
                elsif ($status ne 'unused') {
5397
                    my $match;
5398
                    if ($path =~ /\.master\.qcow2$/) {
5399
                        my @regkeys = (tied %register)->select_where("master = '$path'");
5400
                        $match = 2 if (@regkeys);
5401
                    } else {
5402
                        if (!$domreg{$domains}) { # Referenced domain no longer exists
5403
                            ;
5404
                        } else { # Verify if referenced domain still uses image
5405
                            my @imgkeys = ('image', 'image2', 'image3', 'image4');
5406
                            for (my $i=0; $i<4; $i++) {
5407
                                $match = 1 if ($domreg{$domains}->{$imgkeys[$i]} eq $path);
5408
                            }
5409
                        }
5410
                    }
5411
                    unless ($match) {
5412
                        $status = 'unused';
5413
                        $register{$path}->{'status'} = $status;
5414
                    }
5415
                }
5416
                if ($status eq 'unused') {
5417
                    delete $register{$path}->{'domains'};
5418
                    delete $register{$path}->{'domainnames'};
5419
                }
5420
                $uipath = $path;
5421
                $postreply .= "Status=OK Saved $obj->{name} ($uuid)\n";
5422
            } else {
5423
                $postreply .= "Status=ERROR Unable to save $obj->{name}\n";
5424
            }
5425
        }
5426
    }
5427
    if ($postreply) {
5428
        $postmsg = $postreply;
5429
    } else {
5430
        $postreply = to_json(\%{$register{$uipath}}, {pretty=>1}) if ($uipath && $register{$uipath});
5431
        $postreply =~ s/""/"--"/g;
5432
        $postreply =~ s/null/"--"/g;
5433
        $postreply =~ s/"notes" {0,1}: {0,1}"--"/"notes":""/g;
5434
        $postreply =~ s/"installable" {0,1}: {0,1}"(true|false)"/"installable":$1/g;
5435
    }
5436
    return $postreply || "Status=OK Saved $uipath\n";
5437
}
5438

    
5439
sub Setstoragedevice {
5440
    my ($image, $action, $obj) = @_;
5441
    if ($help) {
5442
        return <<END
5443
GET:device,type:
5444
Changes the device - disk or partition, used for images or backup storage.
5445
[type] is either images or backup.
5446
END
5447
    }
5448
    my $dev = $obj->{device};
5449
    my $force = $obj->{force};
5450
    my $type = 'backup';
5451
    $type = 'images' if ($obj->{type} eq 'images');
5452
    return "Status=Error Not allowed\n" unless ($isadmin);
5453
    my $backupdevice = Getbackupdevice('', 'getbackupdevice');
5454
    my $imagesdevice = Getimagesdevice('', 'getimagesdevice');
5455
    my $devices_obj = from_json(Liststoragedevices('', 'liststoragedevices'));
5456
    my %devices = %$devices_obj;
5457
    my $backupdev = $devices{$backupdevice}->{dev};
5458
    my $imagesdev = $devices{$imagesdevice}->{dev};
5459
    if (!$devices{$dev}) {
5460
        $postreply = "Status=Error You must specify a valid device ($dev)\n";
5461
        return $postreply;
5462
    }
5463
    if (!$force && (($backupdev =~ /$dev/) || ($imagesdev =~ /$dev/))  && $dev !~ /vda/ && $dev !~ /sda/) { # make exception to allow returning to default setup
5464
        $postreply = "Status=Error $dev is already in use as images or backup device\n";
5465
        return $postreply;
5466
    }
5467
    my $stordir = $tenderpathslist[0];
5468
    my $stordevice = $imagesdevice;
5469
    if ($type eq 'backup') {
5470
        $stordir = $backupdir;
5471
        $stordevice = $backupdevice;
5472
    }
5473
    return "Status=Error Storage device not found\n" unless ($stordevice);
5474
    my $mp = $devices{$dev}->{mounted};
5475
    my $newstordir;
5476
    # my $oldstordir;
5477
    if ($devices{$dev}->{type} eq 'zfs') {
5478
        my $cmd = qq|zfs list stabile-$type/$type -Ho mountpoint|;
5479
        my $zmp = `$cmd`;
5480
        chomp $zmp;
5481
        if ($zmp =~ /^\//) {
5482
            `zfs mount stabile-$type/$type`;
5483
            $mp = $zmp;
5484
            $newstordir = $mp;
5485
        } else {
5486
            `zfs create stabile-$type/$type`;
5487
            $mp = "/stabile-$type/$type";
5488
            $newstordir = $mp;
5489
        }
5490
    } else {
5491
        $newstordir = (($type eq 'images')?"$mp/images":"$mp/backups");
5492
        $newstordir = $1 if ($newstordir =~ /(.+\/images)\/images$/);
5493
        $newstordir = $1 if ($newstordir =~ /(.+\/backups)\/backups$/);
5494
    }
5495
    if ($mp eq '/') {
5496
        $newstordir = (($type eq 'images')?"/mnt/stabile/images":"/mnt/stabile/backups");
5497
        `umount "$newstordir"`; # in case it's mounted
5498
    }
5499
    `mkdir "$newstordir"` unless (-e $newstordir);
5500
    `chmod 777 "$newstordir"`;
5501

    
5502
    my $cfg = new Config::Simple("/etc/stabile/config.cfg");
5503
    if ($type eq 'backup') {
5504
        $cfg->param('STORAGE_BACKUPDIR', $newstordir);
5505
        $cfg->save();
5506
    } elsif ($type eq 'images') {
5507

    
5508
    # Handle shared storage config
5509
    #    $oldstordir = $stordir;
5510
        my $i = 0;
5511
        for($i = 0; $i <= $#tenderpathslist; $i++) {
5512
            my $dir = $tenderpathslist[$i];
5513
            last if ($dir eq $newstordir);
5514
        }
5515
        # $tenderpathslist[0] = $newstordir;
5516
        splice(@tenderpathslist, $i,1); # Remove existing entry
5517
        unshift(@tenderpathslist, $newstordir); # Then add the new path
5518
        $cfg->param('STORAGE_POOLS_LOCAL_PATHS', join(',', @tenderpathslist));
5519

    
5520
        # $tenderlist[0] = 'local';
5521
        splice(@tenderlist, $i,1);
5522
        unshift(@tenderlist, 'local');
5523
        $cfg->param('STORAGE_POOLS_ADDRESS_PATHS', join(',', @tenderlist));
5524

    
5525
        # $tendernameslist[0] = 'Default';
5526
        splice(@tendernameslist, $i,1);
5527
        unshift(@tendernameslist, 'Default');
5528

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

    
5534
            $storagepools = "$storagepools,$i" unless ($storagepools =~ /,\s*$i,?/ || $storagepools =~ /,\s*$i$/ || $storagepools =~ /^$i$/);
5535
            $cfg->param('STORAGE_POOLS_DEFAULTS', $storagepools);
5536
        }
5537
        $cfg->param('STORAGE_POOLS_NAMES', join(',', @tendernameslist));
5538

    
5539
        $cfg->save();
5540

    
5541

    
5542
    # Handle node storage configs
5543
        unless ( tie(%idreg,'Tie::DBI', Hash::Merge::merge({table=>'nodeidentities',key=>'identity',CLOBBER=>3}, $Stabile::dbopts)) ) {return "Unable to access id register"};
5544
        # Build hash of known node config files
5545
        my @nodeconfigs;
5546
        push @nodeconfigs, "/etc/stabile/nodeconfig.cfg";
5547
        foreach my $valref (values %idreg) {
5548
            my $nodeconfigfile = $valref->{'path'} . "/casper/filesystem.dir/etc/stabile/nodeconfig.cfg";
5549
            next if ($nodeconfigs{$nodeconfigfile}); # Node identities may share basedir and node config file
5550
            if (-e $nodeconfigfile) {
5551
                push @nodeconfigs, $nodeconfigfile;
5552
            }
5553
        }
5554
        untie %idreg;
5555
        foreach my $nodeconfig (@nodeconfigs) {
5556
            my $nodecfg = new Config::Simple($nodeconfig);
5557
            my @ltenderlist = $nodecfg->param('STORAGE_SERVERS_ADDRESS_PATHS');
5558
            my $ltenders = join(", ", @ltenderlist);
5559
            next if ($ltenders =~ /10\.0\.0\.1:$newstordir$/ || $ltenders =~ /10\.0\.0\.1:$newstordir,/); # This entry already exists
5560
            #my @ltenderlist = split(/,\s*/, $ltenders);
5561
            #$ltenderlist[0] = "10.0.0.1:$newstordir";
5562
            unshift(@ltenderlist, "10.0.0.1:$newstordir");
5563
            $nodecfg->param('STORAGE_SERVERS_ADDRESS_PATHS', join(',', @ltenderlist));
5564
            my @ltenderpathslist = $nodecfg->param('STORAGE_SERVERS_LOCAL_PATHS');
5565
            my $ltenderpaths = join(", ", @ltenderpathslist);
5566
            #my @ltenderpathslist = split(/,\s*/, $ltenderpaths);
5567
            #$ltenderpathslist[0] = $newstordir;
5568
            unshift(@ltenderpathslist, $newstordir);
5569
            $nodecfg->param('STORAGE_SERVERS_LOCAL_PATHS', join(',', @ltenderpathslist));
5570
            $nodecfg->save();
5571
        }
5572
        unless (`grep "$newstordir 10" /etc/exports`) {
5573
            `echo "$newstordir 10.0.0.0/255.255.255.0(sync,no_subtree_check,no_root_squash,rw)" >> /etc/exports`;
5574
            `/usr/sbin/exportfs -r`; #Reexport nfs shares
5575
        }
5576
# We no longer undefine storage pools - we add them
5577
#        $oldstordir =~ s/\//\\\//g;
5578
#        `perl -pi -e 's/$oldstordir 10.*\\\n//s;' /etc/exports` if ($oldstordir);
5579

    
5580
        `mkdir "$newstordir/common"` unless (-e "$newstordir/common");
5581
        `cp "$stordir/ejectcdrom.xml" "$newstordir/ejectcdrom.xml"` unless (-e "$newstordir/ejectcdrom.xml");
5582
        `cp "$stordir/mountvirtio.xml" "$newstordir/mountvirtio.xml"` unless (-e "$newstordir/mountvirtio.xml");
5583
        `cp "$stordir/dummy.qcow2" "$newstordir/dummy.qcow2"` unless (-e "$newstordir/dummy.qcow2");
5584
    }
5585
    Updatedownloads();
5586

    
5587
    # Update /etc/stabile/cgconfig.conf
5588
    my $devs = $devices{$dev}->{dev};
5589
    my @pdevs = split(" ", $devs);
5590
    my $majmins;
5591
    foreach my $dev (@pdevs) {
5592
        # It seems that cgroups cannot handle individual partitions for blkio
5593
        my $physdev = $1 if ($dev =~ /(\w+)\d+/);
5594
        if ($physdev && -d "/sys/fs/cgroup" ) {
5595
            my $blkline = `lsblk -l /dev/$physdev`;
5596
            my $majmin = '';
5597
            $majmin = $1 if ($blkline =~ /$physdev +(\d+:\d+)/);
5598
            $postreply .= "Status=OK Setting cgroups block device to $majmin\n";
5599
            if ($majmin) {
5600
                $majmins .= ($majmins)?" $majmin":$majmin;
5601
            }
5602
        }
5603
    }
5604
    setCgroupsBlkDevice($majmins) if ($majmins);
5605

    
5606
    $Stabile::Nodes::console = 1;
5607
    require "$Stabile::basedir/cgi/nodes.cgi";
5608
    $postreply .= Stabile::Nodes::do_reloadall('','reloadall');
5609

    
5610
    # Update config on stabile.io
5611
    require "$Stabile::basedir/cgi/users.cgi";
5612
    $Stabile::Users::console = 1;
5613
    Stabile::Users::Updateengine('', 'updateengine');
5614

    
5615
    my $msg = "OK Now using $newstordir for $type on $obj->{device}";
5616
    $main::updateUI->({tab=>'home', user=>$user, type=>'update', message=>$msg});
5617
    $postreply .= "Status=OK Now using $newstordir for $type on $dev\n";
5618
    return $postreply;
5619
}
5620

    
5621
sub Initializestorage {
5622
    my ($image, $action, $obj) = @_;
5623
    if ($help) {
5624
        return <<END
5625
GET:device,type,fs,activate,force:
5626
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.
5627
[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'.
5628
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).
5629
END
5630
    }
5631
    my $fs = $obj->{fs} || 'zfs';
5632
    my $dev = $obj->{device};
5633
    my $force = $obj->{force};
5634
    my $activate = $obj->{activate};
5635
    my $type = 'backup';
5636
    $type = 'images' if ($obj->{type} eq 'images');
5637
    return "Status=Error Not allowed\n" unless ($isadmin);
5638
    my $backupdevice = Getbackupdevice('', 'getbackupdevice');
5639
    my $imagesdevice = Getimagesdevice('', 'getimagesdevice');
5640
    my $devices_obj = from_json(Liststoragedevices('', 'liststoragedevices'));
5641
    my %devices = %$devices_obj;
5642
    my $backupdev = $devices{$backupdevice}->{dev};
5643
    my $imagesdev = $devices{$imagesdevice}->{dev};
5644
    if (!$dev || !(-e "/dev/$dev")) {
5645
        $postreply = "Status=Error You must specify a valid device\n";
5646
        return $postreply;
5647
    }
5648
    if (($backupdev =~ /$dev/) || ($imagesdev =~ /$dev/)) {
5649
        $postreply = "Status=Error $dev is already in use as images or backup device\n";
5650
        return $postreply;
5651
    }
5652
    my $stordir = "/stabile-$type/$type";
5653
    if ($fs eq 'lvm') {
5654
        if ($type eq 'backup') {
5655
            $stordir = "/mnt/stabile/backups";
5656
        } else {
5657
            $stordir = "/mnt/stabile/images";
5658
        }
5659
    }
5660
    `chmod 666 /dev/zfs` if (-e '/dev/zfs'); # TODO: This should be removed once we upgrade to Bionic and zfs allow is supported
5661

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

    
5782
sub setCgroupsBlkDevice {
5783
    my @majmins = split(" ", shift);
5784
    my $file = "/etc/stabile/cgconfig.conf";
5785
    my %options = (
5786
        blkio.throttle.read_bps_device => $valve_readlimit,
5787
        blkio.throttle.write_bps_device => $valve_writelimit,
5788
        blkio.throttle.read_iops_device => $valve_iopsreadlimit,
5789
        blkio.throttle.write_iops_device => $valve_iopswritelimit
5790
        );
5791
    my @groups = ('stabile', 'stabilevm');
5792
    my @newlines;
5793
    foreach my $majmin (@majmins) {
5794
        foreach my $group (@groups) {
5795
            my $mline = qq|group $group {|; push @newlines, $mline;
5796
            my $mline = qq|    blkio {|; push @newlines, $mline;
5797
            foreach my $option (keys %options) {
5798
                my $mline = qq|        $option = "$majmin $options{$option}";|;
5799
                push @newlines, $mline;
5800
            }
5801
            my $mline = qq|    }|; push @newlines, $mline;
5802
            my $mline = qq|}|; push @newlines, $mline;
5803
        }
5804
    }
5805
    unless (open(FILE, "> $file")) {
5806
        $postreply .= "Status=Error Problem opening $file\n";
5807
        return $postreply;
5808
    }
5809
    print FILE join("\n", @newlines);
5810
    close(FILE);
5811
    return;
5812
}
(2-2/9)