Project

General

Profile

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

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

    
8
package Stabile::Images;
9

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

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

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

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

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

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

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

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

    
81
1;
82

    
83
sub Init {
84

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

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

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

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

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

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

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

    
207
    *Fullupdateregister = \&Updateregister;
208

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

    
217
    untie %userreg;
218

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

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

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

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

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

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

    
279
}
280

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
787
}
788

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

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

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

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

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

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

    
863
}
864

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1482
    my %billingreg;
1483

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1650
    return $postreply;
1651
}
1652

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

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

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

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

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

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

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

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

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

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

    
1845
    unless ($register{$path} && ($register{$path}->{'user'} eq $user
1846
                || $register{$path}->{'user'} eq 'common'
1847
                || $register{$path}->{'user'} eq $billto
1848
                || $register{$path}->{'user'} eq $Stabile::Systems::billto
1849
                || $isadmin)) {
1850
        $postreply .= "Status=ERROR Cannot clone!\n";
1851
        return;
1852
    }
1853
    if ($register{$path}->{master}) { # master has a master - must exist
1854
        unless ( $register{$register{$path}->{master}} ) {
1855
            $main::syslogit->($user, "info", "Unable to clone $path - missing parent image");
1856
            $postreply .= "Status=ERROR A parent image is missing, please wait for download to finish or download again!\n";
1857
            return "Status=ERROR A parent image is missing, please wait for download to finish or download again!\n";
1858
        }
1859
    }
1860
    $istoragepool = $istoragepool || $obj->{storagepool};
1861
    $name = $name || $obj->{name};
1862
    $wait = $wait || $obj->{wait};
1863
    my $img = $register{$path};
1864
    my $status = $img->{'status'};
1865
    my $type = $img->{'type'};
1866
    my $master = $img->{'master'};
1867
    my $notes = $img->{'notes'};
1868
    my $image2 = $img->{'image2'};
1869
    my $snap1 = $img->{'snap1'};
1870
    $managementlink = $img->{'managementlink'} unless ($managementlink);
1871
    $appid = $img->{'appid'} unless ($appid);
1872
    my $upgradelink = $img->{'upgradelink'} || '';
1873
    my $terminallink = $img->{'terminallink'} || '';
1874
    my $version = $img->{'version'} || '';
1875
    my $regmac = $img->{'mac'};
1876

    
1877
    my $virtualsize = $img->{'virtualsize'};
1878
    my $dindex = 0;
1879

    
1880
    my($bname, $dirpath, $suffix) = fileparse($path, (".vmdk", ".img", ".vhd", ".vhdx", ".qcow", ".qcow2", ".vdi", ".iso"));
1881
    $path =~ /(.+)\.$type/;
1882
    my $namepath = $1;
1883
    if ($namepath =~ /(.+)\.master/) {
1884
        $namepath = $1;
1885
    }
1886
    if ($namepath =~ /(.+)\.clone\d+/) {
1887
        $namepath = $1;
1888
    }
1889
    if ($namepath =~ /.+\/common\/(.+)/) { # Support one subdir
1890
        $namepath = $1;
1891
    } elsif ($namepath =~ /.+\/$user\/(.+)/) { # Support one subdir
1892
        $namepath = $1;
1893
    } elsif ($namepath =~ /.+\/(.+)/) { # Extract only the name
1894
        $namepath = $1;
1895
    }
1896

    
1897
    # Find unique path in DB across storage pools
1898
    my $upath;
1899
    my $npath = "/mnt/stabile/node/$user/$namepath"; # Also check for uniqueness on nodes
1900
    my $i = 1;
1901
    foreach my $spool (@spools) {
1902
        $upath = $spool->{'path'} . "/$user/$namepath";
1903
        while ($register{"$upath.clone$i.$type"} || $register{"$npath.clone$i.$type"}) {$i++;};
1904
    }
1905
    $upath = "$spools[$istoragepool]->{'path'}/$user/$namepath";
1906

    
1907
    my $iname = $img->{'name'};
1908
    $iname = "$name" if ($name); # Used when name supplied when building a system
1909
    $iname =~ /(.+)( \(master\))/;
1910
    $iname = $1 if $2;
1911
    $iname =~ /(.+)( \(clone\d*\))/;
1912
    $iname = $1 if $2;
1913
    $iname =~ /(.+)( \(child\d*\))/;
1914
    $iname = $1 if $2;
1915
    my $ippath = $path;
1916
    my $macip;
1917
    my $ug = new Data::UUID;
1918
    my $newuuid = $ug->create_str();
1919
    my $wakenode;
1920
    my $identity;
1921

    
1922
    # We only support cloning images to nodes - not the other way round
1923
    if ($imac && $regmac && $imac ne $regmac) {
1924
        $postreply .= "Status=ERROR Cloning from a node not supported\n";
1925
        return $postreply;
1926
    }
1927

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

    
1948
    if ($bschedule eq 'daily7' || $bschedule eq 'daily14') {
1949
         $bschedule = "manually" if ($istoragepool!=-1 && (!$spools[$istoragepool]->{'rdiffenabled'} || !$spools[$istoragepool]->{'lvm'}));
1950
    } elsif ($bschedule ne 'manually') {
1951
        $bschedule = '';
1952
    }
1953

    
1954
# Find storage pool with space
1955
    my $foundstorage = 1;
1956
    if (overStorage($virtualsize, $istoragepool, $imac)) {
1957
        $foundstorage = 0;
1958
        foreach my $p (@spools) {
1959
            if (overStorage($virtualsize, $p->{'id'}, $imac)) {
1960
                ;
1961
            } else {
1962
                $istoragepool = $p->{'id'};
1963
                $foundstorage = 1;
1964
                last;
1965
            }
1966
        }
1967
    }
1968

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

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

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

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

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

    
2054
    } else {
2055
        $postreply .= "Status=ERROR Not a valid type: $type\n";
2056
    }
2057
    tied(%register)->commit;
2058
    $main::updateUI->({tab=>"images", user=>$user, type=>"update"});
2059
    return $postreply;
2060
}
2061

    
2062

    
2063
# Link master image to fuel
2064
sub Linkmaster {
2065
    my ($mpath, $action) = @_;
2066
    if ($help) {
2067
        return <<END
2068
GET:image:
2069
Link master image to fuel
2070
END
2071
    }
2072
    my $res;
2073

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

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

    
2137
# Link master image to fuel
2138
sub unlinkMaster {
2139
    my $mpath = shift;
2140
    unless ($mpath =~ /^\//) { # We did not get an absolute path, look for it in users storagepools
2141
        foreach my $p (@spools) {
2142
            my $dir = $p->{'path'};
2143
            my $upath = "$dir/$user/fuel/$mpath";
2144
            if (-e $upath) {
2145
                $mpath = "/mnt/fuel/pool$p->{id}/$mpath";
2146
                last;
2147
            }
2148
        }
2149
    }
2150

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

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

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

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

    
2252
    my $newpath;
2253
    my $newdirpath;
2254
    my $oldpath = $path;
2255
    my $olddirpath = $path;
2256
    my $newuser = $reguser;
2257
    my $newstoragepool = $regstoragepool;
2258
    my $haschildren;
2259
    my $hasprimary;
2260
    my $child;
2261
    my $primary;
2262
    my $macip;
2263
    my $alreadyexists;
2264
    my $subdir;
2265
#    $subdir = $1 if ($path =~ /\/$reguser(\/.+)\//);
2266
    $subdir = $1 if ($path =~ /.+\/$reguser(\/.+)?\//);
2267
    my $restpath;
2268
    $restpath = $1 if ($path =~ /.+\/$reguser\/(.+)/);
2269

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

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

    
2364
        my $dindex;
2365
        my $wakenode;
2366
        if ($istoragepool == -1 && $regstoragepool != -1) {
2367
            ($mac, $macip, $dindex, $wakenode) = locateNode($virtualsize, $mac);
2368
        }
2369

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

    
2372
        if ($haschildren) {
2373
            $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$register{$path}->{'uuid'}, status=>$status, message=>"ERROR Unable to move $name (has children)"});
2374
            $postreply .= "Status=ERROR Unable to move $name (has children)\n";
2375
        } elsif ($hasprimary) {
2376
            $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"});
2377
            $postreply .= "Status=Error Cannot move image. This image is used as secondary image by: $primary\n";
2378
        } elsif ($wakenode) {
2379
            $postreply .= "Status=ERROR All available nodes are asleep moving $name, waking $mac, please try again later\n";
2380
            $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"});
2381
            require "$Stabile::basedir/cgi/nodes.cgi";
2382
            $Stabile::Nodes::console = 1;
2383
            Stabile::Nodes::wake($mac);
2384
        } elsif (overStorage($virtualsize, $istoragepool+0, $mac)) {
2385
            $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"});
2386
            $postreply .= "Status=ERROR Out of storage in destination pool $istoragepool $mac moving: $name\n";
2387
        } elsif (overQuotas($virtualsize, ($istoragepool==-1))) {
2388
            $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$register{$path}->{'uuid'}, status=>$status, message=>"ERROR Over quota (". overQuotas($virtualsize, ($istoragepool==-1)) . ") moving: $name"});
2389
            $postreply .= "Status=ERROR Over quota (". overQuotas($virtualsize, ($istoragepool==-1)) . ") moving: $name\n";
2390
        } elsif ($istoragepool == -1 && $regstoragepool != -1 && $path =~ /\.master\.$type/) {
2391
            $postreply .= "Status=ERROR Unable to move $name (master images are not supported on node storage)\n";
2392
            $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)"});
2393
    # Moving to node
2394
        } elsif ($istoragepool == -1 && $regstoragepool != -1) {
2395
            if (index($privileges,"a")!=-1 || index($privileges,"n")!=-1 || index($Stabile::privileges,"a")!=-1 || index($Stabile::privileges,"n")!=-1) { # Privilege "n" means user may use node storage
2396
                if ($macip) {
2397
                    $newdirpath = "/mnt/stabile/node/$reguser/$restpath";
2398
                    $newpath = "$macip:$newdirpath";
2399
                    $newstoragepool = $istoragepool;
2400
            # Check if image already exists in target dir
2401
                    $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}'"`;
2402

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

    
2437
    if ($alreadyexists && !$force) {
2438
        $postreply = "Status=ERROR Image \"$name\" already exists in destination\n";
2439
        return $postreply;
2440
    }
2441
# Request actual move operation
2442
    elsif ($newpath) {
2443
        if ($newstoragepool == -1) {
2444
            my $diruser = $iuser || $reguser;
2445
            `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
2446
        }
2447
        if ($subdir && $istoragepool != -1) {
2448
            my $fulldir = "$spools[$istoragepool]->{'path'}/$reguser$subdir";
2449
            `/bin/mkdir -p "$fulldir"` unless -d $fulldir;
2450
        }
2451
        $uistatus = "moving";
2452
        if ($precreate) {
2453
            $uistatus = "stormoving";
2454
        }
2455

    
2456
        my $ug = new Data::UUID;
2457
        my $tempuuid = $ug->create_str();
2458

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

    
2463

    
2464
        if ($bschedule eq 'daily7' || $bschedule eq 'daily14') {
2465
             $bschedule = "manually" if (!$spools[$regstoragepool]->{'rdiffenabled'} || !$spools[$regstoragepool]->{'lvm'});
2466
        } elsif ($bschedule ne 'manually') {
2467
            $bschedule = '';
2468
        }
2469

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

    
2508
}
2509

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

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

    
2579
# Check if image already exists.
2580
# Pass image name including suffix.
2581
sub imageExists {
2582
    my $imagename = shift;
2583
    foreach my $spool (@spools) {
2584
        my $ipath = $spool->{'path'} . "/$user/$imagename";
2585
        if ($register{$ipath}) {
2586
            return $register{$ipath}->{'status'} || 1;
2587
        } elsif (-e "$ipath") {
2588
            return 1
2589
        }
2590
    }
2591
    return '';
2592
}
2593

    
2594
# Pass image name including suffix.
2595
# Returns incremented name of an image which does not already exist.
2596
sub getValidName {
2597
    my $imagename = shift;
2598
    my $name = $imagename;
2599
    my $type;
2600
    if ($imagename =~ /(.+)\.(.+)/) {
2601
        $name = $1;
2602
        $type = $2;
2603
    }
2604
    if (imageExists($imagename)) {
2605
        my $i = 1;
2606
        while (imageExists("$name.$i.$type")) {$i++;};
2607
        $imagename = "$name.$i.$type";
2608
    }
2609
    return $imagename;
2610
}
2611

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

    
2639
# Print list of images
2640
# Showing a single image is also handled by specifying uuid or path in $curuuid or $curimg
2641
# When showing a single image a single action may be performed on image
2642
sub do_list {
2643
    my ($img, $action, $obj) = @_;
2644
    if ($help) {
2645
        return <<END
2646
GET:image,uuid:
2647
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.
2648
The returned list may be filtered by specifying storagepool, type, name, path or uuid, like e.g.:
2649

    
2650
<a href="/stabile/images/type:user" target="_blank">/stabile/images/type:user</a>
2651
<a href="/stabile/images/name:test* AND storagepool:shared" target="_blank">/stabile/images/name:test* AND storagepool:shared</a>
2652
<a href="/stabile/images/storagepool:shared AND path:test*" target="_blank">/stabile/images/storagepool:shared AND path:test*</a>
2653
<a href="/stabile/images/name:* AND storagepool:all AND type:usercdroms" target="_blank">/stabile/images/name:* AND storagepool:all AND type:usercdroms</a>
2654
<a href="/stabile/images/[uuid]" target="_blank">/stabile/images/[uuid]</a>
2655

    
2656
storagepool may be either of: all, node, shared
2657
type may be either of: user, usermasters, commonmasters, usercdroms
2658

    
2659
May also be called as tablelist or tablelistall, for use by stash.
2660

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

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

    
2717

    
2718
    my %userregister; # User specific register
2719

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

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

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

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

    
2825
    # Sort @uservalues
2826
    @uservalues = (sort {$a->{'name'} cmp $b->{'name'}} @uservalues); # Always sort by name first
2827
    my $sort = 'status';
2828
    $sort = $2 if ($uripath =~ /sort\((\+|\-)(\S+)\)/);
2829
    my $reverse;
2830
    $reverse = 1 if ($1 eq '-');
2831
    if ($reverse) { # sort reverse
2832
        if ($sort =~ /realsize|virtualsize|size/) {
2833
            @uservalues = (sort {$b->{$sort} <=> $a->{$sort}} @uservalues); # Sort as number
2834
        } else {
2835
            @uservalues = (sort {$b->{$sort} cmp $a->{$sort}} @uservalues); # Sort as string
2836
        }
2837
    } else {
2838
        if ($sort =~ /realsize|virtualsize|size/) {
2839
            @uservalues = (sort {$a->{$sort} <=> $b->{$sort}} @uservalues); # Sort as number
2840
        } else {
2841
            @uservalues = (sort {$a->{$sort} cmp $b->{$sort}} @uservalues); # Sort as string
2842
        }
2843
    }
2844

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

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

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

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

    
2954
sub do_updatebilling {
2955
    my ($img, $action) = @_;
2956
    if ($help) {
2957
        return <<END
2958
GET:image,path:
2959
END
2960
    }
2961
    my $res;
2962
    $res .= header('text/plain') unless ($console);
2963
    updateBilling($params{"event"});
2964
    $res .= "Status=OK Updated billing for $user\n";
2965
    return $res;
2966
}
2967

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

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

    
3085
        }
3086
    }
3087
    return $res;
3088
}
3089

    
3090
sub do_upload {
3091
    my ($img, $action) = @_;
3092
    if ($help) {
3093
        return <<END
3094
POST:image,path:
3095
END
3096
    }
3097
    my $res;
3098
    $res .= header("text/html") unless ($console);
3099

    
3100
    my $uname = $params{'name'};
3101

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

    
3104
    $name = $1 if ($name =~ /^\.+(.*)/); # Don't allow hidden files
3105
    #        my $f = lc $name;
3106
    my $f = $name;
3107
    $f = $spools[0]->{'path'} . "/$user/$f$suffix";
3108

    
3109
    my $chunk = int($params{'chunk'});
3110
    my $chunks = int($params{'chunks'});
3111

    
3112
    if ($chunk == 0 && -e $f) {
3113
        $res .= qq|Error: File $f already exists $name|;
3114
    } else {
3115
        open (FILE, ">>$f");
3116

    
3117
        if ($params{'file'}) {
3118
            my $uh = $Stabile::q->upload("file");
3119
            while ( <$uh> ) {
3120
                print FILE;
3121
            }
3122
            close FILE;
3123

    
3124
            if ($chunk == 0) {
3125
                `/usr/local/bin/steamExec updateimagestatus "$f" uploading`;
3126
            }
3127
            if ($chunk >= ($chunks - 1) ) { # Done
3128
                unlink("$f.meta");
3129
                `/usr/local/bin/steamExec updateimagestatus "$f" unused`;
3130
            } else {
3131
                my $upload_meta_data = "status=uploading&chunk=$chunk&chunks=$chunks";
3132
                `echo "$upload_meta_data" > "$f.meta"`;
3133
            }
3134
            $res .= qq|OK: Chunk $chunk uploaded of $name|;
3135
        } else {
3136
            $res .= qq|OK: No file $name.|;
3137
        }
3138
    }
3139
    return $res;
3140
}
3141

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

    
3216

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

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

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

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

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

    
3476
    my $jsonreply;
3477
    $jsonreply .= "{\"identifier\": \"id\", \"label\": \"name\", \"items\":" if ($params{'dojo'});
3478
    $jsonreply .= to_json(\@p, {pretty=>1});
3479
    $jsonreply .= "}" if ($params{'dojo'});
3480
    return $jsonreply;
3481
}
3482

    
3483
# List images available for attaching to server
3484
sub do_listimages {
3485
    my ($img, $action) = @_;
3486
    if ($help) {
3487
        return <<END
3488
GET:image,image1:
3489
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.
3490
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.
3491
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".
3492
END
3493
    }
3494
    my $res;
3495
    $res .= header('application/json') unless ($console);
3496
    my $curimg1 = URI::Escape::uri_unescape($params{'image1'});
3497
    my @filteredfiles;
3498
    my @curusers = @users;
3499
    # If an admin user is looking at a server not belonging to him, allow him to see the server
3500
    # users images
3501
    if ($isadmin && $img && $img ne '--' && $register{$img} && $register{$img}->{'user'} ne $user) {
3502
        @curusers = ($register{$img}->{'user'}, "common");
3503
    }
3504

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

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

    
3585
sub do_listmasterimages {
3586
    my ($image, $action, $obj) = @_;
3587
    if ($help) {
3588
        return <<END
3589
GET::
3590
Lists master images available to the current user.
3591
END
3592
    }
3593
    my $res;
3594
    $res .= header('application/json') unless ($console);
3595

    
3596
    my @filteredfiles;
3597
    my @busers = @users;
3598
    push (@busers, $billto) if ($billto && $billto ne $user); # We include images from 'parent' user
3599

    
3600
    foreach my $u (@busers) {
3601
        my @regkeys = (tied %register)->select_where("user = '$u'");
3602
        foreach my $k (@regkeys) {
3603
            my $valref = $register{$k};
3604
            my $f = $valref->{'path'};
3605
            if ($valref->{'user'} eq $u && (defined $spools[$valref->{'storagepool'}]->{'id'} || $valref->{'storagepool'}==-1)) {
3606
                # Only list installable master images from billto account
3607
                next if ($billto && $u eq $billto && $valref->{'installable'} ne 'true');
3608

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

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

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

    
3702
# Activate image from fuel
3703
sub Activate {
3704
    my ($curimg, $action, $argref) = @_;
3705
    if ($help) {
3706
        return <<END
3707
GET:image, name, managementlink, upgradelink, terminallink, force:
3708
Activate an image from fuel storage, making it available for regular use.
3709
END
3710
    }
3711
    my %uargs = %{$argref};
3712
    my $name = URI::Escape::uri_unescape($uargs{'name'});
3713
    my $managementlink = URI::Escape::uri_unescape($uargs{'managementlink'});
3714
    my $upgradelink = URI::Escape::uri_unescape($uargs{'upgradelink'});
3715
    my $terminallink = URI::Escape::uri_unescape($uargs{'terminallink'});
3716
    my $version = URI::Escape::uri_unescape($uargs{'version'}) || '1.0b';
3717
    my $image2 =  URI::Escape::uri_unescape($uargs{'image2'});
3718
    my $force = $uargs{'force'};
3719

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

    
3736
    my $virtualsize = `qemu-img info --force-share "$imagepath" | sed -n -e 's/^virtual size: .*(//p' | sed -n -e 's/ bytes)//p'`;
3737
    chomp $virtualsize;
3738
#    my $master = `qemu-img info --force-share "$imagepath" | sed -n -e 's/^backing file: //p' | sed -n -e 's/ (actual path:.*)\$//p'`;
3739
    my $master = `qemu-img info --force-share "$imagepath" | sed -n -e 's/^backing file: //p'`;
3740
    chomp $master;
3741

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

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

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

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

    
3844
sub Publish {
3845
    my ($uuid, $action, $parms) = @_;
3846
    if ($help) {
3847
        return <<END
3848
GET:image,appid,appstore,force:
3849
Publish a stack to registry. Set [force] if you want to force overwrite images in registry - use with caution.
3850
END
3851
    }
3852
    my $res;
3853
    $uuid = $parms->{'uuid'} if ($uuid =~ /^\// || !$uuid);
3854
    my $force = $parms->{'force'};
3855
    my $freshen = $parms->{'freshen'};
3856

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

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

    
3924
sub Release {
3925
    my ($uuid, $action, $parms) = @_;
3926
    if ($help) {
3927
        return <<END
3928
GET:image,appid,appstore,force,unrelease:
3929
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.
3930
END
3931
    }
3932
    my $res;
3933
    $uuid = $parms->{'uuid'} if ($uuid =~ /^\// || !$uuid);
3934
    my $force = $parms->{'force'};
3935
    my $unrelease = $parms->{'unrelease'};
3936

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

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

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

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

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

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

    
4092
sub do_listpackages($action) {
4093
    my ($image, $action) = @_;
4094
    if ($help) {
4095
        return <<END
4096
GET:image:
4097
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.
4098
END
4099
    }
4100
    my $res;
4101
    $res .= header('text/plain') unless ($console);
4102

    
4103
    my $mac = $register{$image}->{'mac'};
4104
    my $macip;
4105
    if ($mac && $mac ne '--') {
4106
        unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
4107
        $macip = $nodereg{$mac}->{'ip'};
4108
        untie %nodereg;
4109
    }
4110
    $image =~ /(.+)/; $image = $1;
4111
    my $apps;
4112

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

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

    
4176
sub Inject {
4177
    my ($image, $action, $obj) = @_;
4178
    if ($help) {
4179
        return <<END
4180
GET:image:
4181
Tries to inject drivers into a qcow2 image with a Windows OS installed on it. Image must not be in use.
4182
END
4183
    }
4184
    $uistatus = "injecting";
4185
    my $path = $obj->{path} || $curimg;
4186
    my $status = $obj->{status};
4187
    my $esc_localpath = shell_esc_chars($path);
4188

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

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

    
4240
    } else {
4241
        $postreply .= "Status=ERROR No Windows OS found in $osname image, not injecting drivers.\n";
4242
        $main::syslogit->($user, "info", "No Windows OS found ($osname) in image, not injecting drivers.");
4243
    }
4244
    my $msg = $postreply;
4245
    $msg = $1 if ($msg =~ /\w+=\w+ (.+)/);
4246
    chomp $msg;
4247
    $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$obj->{'uuid'}, status=>$status, message=>$msg});
4248
    $postreply .=  "Status=$uistatus $obj->{type} image: $obj->{name}\n";
4249
    $main::syslogit->($user, "info", "$uistatus $obj->{type} image: $obj->{name}: $uuid");
4250
    return $postreply;
4251
}
4252

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

    
4298
            $register{$newpath} = {
4299
                uuid=>$newuuid,
4300
                name=>"$obj->{name} (converted)",
4301
                notes=>$obj->{notes},
4302
                image2=>$obj->{image2},
4303
                managementlink=>$obj->{managementlink},
4304
                upgradelink=>$obj->{managementlink},
4305
                terminallink=>$obj->{terminallink},
4306
                storagepool=>$obj->{regstoragepool},
4307
                status=>$uistatus,
4308
                mac=>($obj->{regstoragepool} == -1)?$obj->{mac}:"",
4309
                size=>0,
4310
                realsize=>0,
4311
                virtualsize=>$obj->{virtualsize},
4312
                type=>"qcow2",
4313
                user=>$user
4314
            };
4315
            $register{$path}->{'status'} = $uistatus;
4316

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

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

    
4373
                if ($macip) {
4374
                    my $esc_localpath = shell_esc_chars($path);
4375
                    $res .= `$sshcmd $macip "sudo /usr/bin/qemu-img snapshot -c snap1 $esc_localpath"`;
4376
                } else {
4377
                    $res .= `/usr/bin/qemu-img snapshot -c 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
            }
4383
            1;
4384
        } or do {$postreply .= "Status=ERROR $@\n";};
4385
        $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$obj->{'uuid'}, status=>$status, snap1=>$snaptime});
4386
    } else {
4387
        $postreply .= "Status=ERROR Only qcow2 images can be snapshotted\n";
4388
    }
4389
    return $postreply;
4390
}
4391

    
4392
sub Unsnap {
4393
    my ($image, $action, $obj) = @_;
4394
    if ($help) {
4395
        return <<END
4396
GET:image:
4397
Removes a snapshot from a qcow2 image. Image can not be in use by a running server.
4398
END
4399
    }
4400
    my $status = $obj->{status};
4401
    my $path = $obj->{path};
4402
    $uistatus = "unsnapping";
4403
    $uiuuid = $obj->{uuid};
4404
    my $macip;
4405

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

    
4454
sub Revert {
4455
    my ($image, $action, $obj) = @_;
4456
    if ($help) {
4457
        return <<END
4458
GET:image:
4459
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.
4460
END
4461
    }
4462
    my $status = $obj->{status};
4463
    my $path = $obj->{path};
4464
    $uistatus = "reverting";
4465
    $uipath = $path;
4466
    my $macip;
4467
    if ($status ne "used" && $status ne "unused") {
4468
        $postreply .= "Status=ERROR Please shut down or pause your virtual machine before reverting\n";
4469
    } elsif ($obj->{type} eq "qcow2") {
4470
        my $newpath = $path;
4471
        my $hassnap;
4472
        if ($obj->{mac} && $path =~ /\/mnt\/stabile\/node\//) {
4473
            unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac', CLOBBER=>1}, $Stabile::dbopts)) ) {return 0};
4474
            $macip = $nodereg{$obj->{mac}}->{'ip'};
4475
            untie %nodereg;
4476
            $newpath = "$macip:$path";
4477
            my $esc_path = $path;
4478
            $esc_path =~ s/([ ])/\\$1/g;
4479
            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"`;
4480
            $hassnap = ($qinfo =~ /snap1/g);
4481
        } else {
4482
            my $qinfo = `/usr/bin/qemu-img snapshot -l "$path"`;
4483
            $hassnap = ($qinfo =~ /snap1/g);
4484
        }
4485
        eval {
4486
            if ($hassnap) {
4487
                $register{$path}->{'status'} = $uistatus;
4488
                if ($macip) {
4489
                    my $esc_localpath = shell_esc_chars($path);
4490
                    $res .= `$sshcmd $macip "sudo /usr/bin/qemu-img snapshot -a snap1 $esc_localpath"`;
4491
                } else {
4492
                    $res .= `/usr/bin/qemu-img snapshot -a snap1 "$path"`;
4493
                }
4494
                $register{$path}->{'status'} = $status;
4495
                $postreply .=  "Status=OK $uistatus $obj->{type} image: $obj->{name}\n";
4496
                $main::syslogit->($user, "info", "$uistatus $obj->{type} image: $obj->{name}: $uuid");
4497
            } else {
4498
                $postreply .= "Status=ERROR no snapshot found\n";
4499
                $uistatus = $status;
4500
            }
4501
            1;
4502
        } or do {$postreply .= "Status=ERROR $@\n";};
4503
        $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$obj->{'uuid'}, status=>$status, snap1=>'--'});
4504
    } else {
4505
        $postreply .= "Status=ERROR Only qcow2 images can be reverted\n";
4506
    }
4507
    return;
4508
}
4509

    
4510
sub Zbackup {
4511
    my ($image, $action, $obj) = @_;
4512
    if ($help) {
4513
        return <<END
4514
GET:mac, storagepool, synconly, snaponly, imageretention, backupretention:
4515
Backs all images on ZFS storage up by taking a storage snapshot. By default all shared storagepools are backed up.
4516
If storagepool -1 is specified, all ZFS node storages is backed up. If "mac" is specified, only specific node is backed up.
4517
If "synconly" is set, no new snapshots are taken - only syncing of snapshots is performed.
4518
If "snaponly" is set, only local active storage snapshot is taken - no sending to backup storage is done.
4519
"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],
4520
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.
4521
END
4522
    }
4523
    if ($isadmin) {
4524
        my $synconly = $obj->{'synconly'};
4525
        my $snaponly = $obj->{'snaponly'};
4526
        my $mac = $obj->{'mac'};
4527
        my $storagepool = $obj->{'storagepool'};
4528
        $storagepool = -1 if ($mac);
4529
        my $imageretention = $obj->{'imageretention'} || $imageretention;
4530
        my $backupretention = $obj->{'backupretention'} || $backupretention;
4531

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

    
4564
        foreach my $spool (@nspools) {
4565
            $ipath = $spool->{'zfs'};
4566
            if ($spool->{'id'} == -1) { # We're doing a node backup
4567
                $mac = $spool->{'mac'};
4568
                $macip = $spool->{'macip'};
4569
                $bpath = "$basepath/node-$mac";
4570
            } else {
4571
                next unless ($ipath);
4572
                next if (($storagepool || $storagepool eq '0') && $storagepool ne $spool->{'id'});
4573
                $bpath = "$basepath/$1" if ($ipath =~ /.+\/(.+)/);
4574
                $mac = '';
4575
                $macip = '';
4576
            }
4577
            if ($macip) {$zfscmd = "$sshcmd $macip sudo zfs";}
4578
            else {$zfscmd = "zfs";}
4579

    
4580
            $postreply .= "Status=OK Commencing ZFS backup of $ipath $macip, storagepool=$storagepool, synconly=$synconly, snaponly=$snaponly\n";
4581
            my $res;
4582
            my $cmd;
4583
            my @imagesnaps;
4584
            my @backupsnaps;
4585

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

    
4625
            my $lastisnap = $imagesnaps[scalar @imagesnaps -1];
4626
            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)/);
4627
            my $td = ($current_time - $lastisnaptime);
4628
            if ($td<=5) {
4629
                $postreply .= "Status=ERROR Last backup was taken $td seconds ago. Please wait a minute...\n";
4630
                $postmsg = "ERROR ERROR Last backup was taken $td seconds ago. Please wait a minute...";
4631
                return $postreply;
4632
            }
4633
            my $ni = scalar @imagesnaps;
4634
            my $nb = scalar @backupsnaps;
4635

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

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

    
4826
sub Backupfuel {
4827
    my ($image, $action, $obj) = @_;
4828
    if ($help) {
4829
        return <<END
4830
GET:username, dozfs:
4831
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.
4832
END
4833
    }
4834
    my $username = $obj->{'username'} || $user;
4835
    return "Status=Error Not allowed\n" unless ($isadmin || $username eq $user);
4836

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

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

    
4876
sub is_folder_empty {
4877
    my $dirname = shift;
4878
    opendir(my $dh, $dirname) or die "Not a directory";
4879
    return scalar(grep { $_ ne "." && $_ ne ".." } readdir($dh)) == 0;
4880
}
4881

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

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

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

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

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

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

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

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

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

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

    
5213
                if ($macip) {
5214
                    my $esc_localpath = shell_esc_chars($path);
5215
                    my $esc_localpath2 = shell_esc_chars($temppath);
5216
                    $res .= `$sshcmd $macip "/usr/bin/qemu-img convert $esc_localpath -O qcow2 $esc_localpath2"`;
5217
                    $res .= `$sshcmd $macip "if [ -f $esc_localpath2 ]; then /bin/mv -v $esc_localpath2 $esc_localpath; fi"`;
5218
                } else {
5219
                    $res .= `/usr/bin/qemu-img convert -O qcow2 "$path" "$temppath"`;
5220
                    $res .= `if [ -f "$temppath" ]; then /bin/mv -v "$temppath" "$path"; fi`;
5221
                }
5222
                if ($master && !$usedmaster) {
5223
                    $register{$master}->{'status'} = 'unused';
5224
                    $main::syslogit->('info', "Freeing master $master");
5225
                }
5226
                $register{$path}->{'master'} = '';
5227
                $register{$path}->{'status'} = $status;
5228

    
5229
                $postreply .= "Status=OK $uistatus $obj->{type} image: $obj->{name}\n";
5230
                $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$obj->{'uuid'}, status=>$status});
5231
                $main::syslogit->($user, "info", "$uistatus $obj->{type} image: $obj->{name}: $uuid");
5232
                1;
5233
            } or do {$postreply .= "Status=ERROR $@\n";}
5234
        } else {
5235
            $postreply .= "Status=ERROR Not a master, not a child \"$obj->{name}\"\n";
5236
        }
5237
        sleep 1;
5238
        $main::updateUI->({tab=>"images", user=>$user, type=>"update", uuid=>$obj->{'uuid'}, newpath=>$uinewpath, name=>$iname, status=>$status});
5239
    } else {
5240
        $postreply .= "Status=ERROR Only qcow2 images may be unmastered\n";
5241
    }
5242
    return $postreply;
5243
}
5244

    
5245
# Save or create new image
5246
sub Save {
5247
    my ($img, $action, $obj) = @_;
5248
    if ($help) {
5249
        return <<END
5250
POST:path, uuid, name, type, virtualsize, storagepool, user:
5251
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.
5252
Depending on your privileges not all changes are permitted. If you save without specifying a uuid or path, a new image is created.
5253
END
5254
    }
5255
    my $path = $obj->{path};
5256
    my $uuid = $obj->{uuid};
5257
    my $status = $obj->{status};
5258
    if ($status eq "new") {
5259
        # Create new image
5260
        my $ug = new Data::UUID;
5261
        if (!$uuid || $uuid eq '--') {
5262
            $uuid = $ug->create_str();
5263
        } else { # Validate
5264
            my $valuuid  = $ug->from_string($uuid);
5265
            if ($ug->to_string($valuuid) eq $uuid) {
5266
                ;
5267
            } else {
5268
                $uuid = $ug->create_str();
5269
            }
5270
        }
5271
        my $newuuid = $uuid;
5272
        my $pooldir = $spools[$obj->{storagepool}]->{'path'};
5273
        my $cmd;
5274
        my $name = $obj->{name};
5275
        $name =~ s/\./_/g; # Remove unwanted chars
5276
        $name =~ s/\//_/g;
5277
        eval {
5278
            my $ipath = "$pooldir/$user/$name.$obj->{type}";
5279
            $obj->{type} = "qcow2" unless ($obj->{type});
5280
            # Find an unused path
5281
            if ($register{$ipath} || -e "$ipath") {
5282
                my $i = 1;
5283
                while ($register{"$pooldir/$user/$name.$i.$obj->{type}"} || -e "$pooldir/$user/$name.$i.$obj->{type}") {$i++;};
5284
                $ipath = "$pooldir/$user/$name.$i.$obj->{type}";
5285
                $name = "$name.$i";
5286
            }
5287

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

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

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

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

    
5562
    my $cfg = new Config::Simple("/etc/stabile/config.cfg");
5563
    if ($type eq 'backup') {
5564
        $cfg->param('STORAGE_BACKUPDIR', $newstordir);
5565
        $cfg->save();
5566
    } elsif ($type eq 'images') {
5567

    
5568
    # Handle shared storage config
5569
    #    $oldstordir = $stordir;
5570
        my $i = 0;
5571
        for($i = 0; $i <= $#tenderpathslist; $i++) {
5572
            my $dir = $tenderpathslist[$i];
5573
            last if ($dir eq $newstordir);
5574
        }
5575
        # $tenderpathslist[0] = $newstordir;
5576
        splice(@tenderpathslist, $i,1); # Remove existing entry
5577
        unshift(@tenderpathslist, $newstordir); # Then add the new path
5578
        $cfg->param('STORAGE_POOLS_LOCAL_PATHS', join(',', @tenderpathslist));
5579

    
5580
        # $tenderlist[0] = 'local';
5581
        splice(@tenderlist, $i,1);
5582
        unshift(@tenderlist, 'local');
5583
        $cfg->param('STORAGE_POOLS_ADDRESS_PATHS', join(',', @tenderlist));
5584

    
5585
        # $tendernameslist[0] = 'Default';
5586
        splice(@tendernameslist, $i,1);
5587
        unshift(@tendernameslist, 'Default');
5588

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

    
5594
            $storagepools = "$storagepools,$i" unless ($storagepools =~ /,\s*$i,?/ || $storagepools =~ /,\s*$i$/ || $storagepools =~ /^$i$/);
5595
            $cfg->param('STORAGE_POOLS_DEFAULTS', $storagepools);
5596
        }
5597
        $cfg->param('STORAGE_POOLS_NAMES', join(',', @tendernameslist));
5598

    
5599
        $cfg->save();
5600

    
5601

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

    
5640
        `mkdir "$newstordir/common"` unless (-e "$newstordir/common");
5641
        `cp "$stordir/ejectcdrom.xml" "$newstordir/ejectcdrom.xml"` unless (-e "$newstordir/ejectcdrom.xml");
5642
        `cp "$stordir/mountvirtio.xml" "$newstordir/mountvirtio.xml"` unless (-e "$newstordir/mountvirtio.xml");
5643
        `cp "$stordir/dummy.qcow2" "$newstordir/dummy.qcow2"` unless (-e "$newstordir/dummy.qcow2");
5644
    }
5645
    Updatedownloads();
5646

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

    
5666
    $Stabile::Nodes::console = 1;
5667
    require "$Stabile::basedir/cgi/nodes.cgi";
5668
    $postreply .= Stabile::Nodes::do_reloadall('','reloadall');
5669

    
5670
    # Update config on stabile.io
5671
    require "$Stabile::basedir/cgi/users.cgi";
5672
    $Stabile::Users::console = 1;
5673
    Stabile::Users::Updateengine('', 'updateengine');
5674

    
5675
    my $msg = "OK Now using $newstordir for $type on $obj->{device}";
5676
    $main::updateUI->({tab=>'home', user=>$user, type=>'update', message=>$msg});
5677
    $postreply .= "Status=OK Now using $newstordir for $type on $dev\n";
5678
    return $postreply;
5679
}
5680

    
5681
sub Initializestorage {
5682
    my ($image, $action, $obj) = @_;
5683
    if ($help) {
5684
        return <<END
5685
GET:device,type,fs,activate,force:
5686
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.
5687
[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'.
5688
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).
5689
END
5690
    }
5691
    my $fs = $obj->{fs} || 'zfs';
5692
    my $dev = $obj->{device};
5693
    my $force = $obj->{force};
5694
    my $activate = $obj->{activate};
5695
    my $type = 'backup';
5696
    $type = 'images' if ($obj->{type} eq 'images');
5697
    return "Status=Error Not allowed\n" unless ($isadmin);
5698
    my $backupdevice = Getbackupdevice('', 'getbackupdevice');
5699
    my $imagesdevice = Getimagesdevice('', 'getimagesdevice');
5700
    my $devices_obj = from_json(Liststoragedevices('', 'liststoragedevices'));
5701
    my %devices = %$devices_obj;
5702
    my $backupdev = $devices{$backupdevice}->{dev};
5703
    my $imagesdev = $devices{$imagesdevice}->{dev};
5704
    if (!$dev || !(-e "/dev/$dev")) {
5705
        $postreply = "Status=Error You must specify a valid device\n";
5706
        return $postreply;
5707
    }
5708
    if (($backupdev =~ /$dev/) || ($imagesdev =~ /$dev/)) {
5709
        $postreply = "Status=Error $dev is already in use as images or backup device\n";
5710
        return $postreply;
5711
    }
5712
    my $stordir = "/stabile-$type/$type";
5713
    if ($fs eq 'lvm') {
5714
        if ($type eq 'backup') {
5715
            $stordir = "/mnt/stabile/backups";
5716
        } else {
5717
            $stordir = "/mnt/stabile/images";
5718
        }
5719
    }
5720
    `chmod 666 /dev/zfs` if (-e '/dev/zfs'); # TODO: This should be removed once we upgrade to Bionic and zfs allow is supported
5721

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

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