Project

General

Profile

Download (78.5 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.stabile.io/info/stabiledocs/licensing/stabile-open-source-license
7

    
8
package Stabile::Users;
9

    
10
use Error qw(:try);
11
use Time::Local;
12
# use Time::HiRes qw( time );
13
use Config::Simple;
14
use Text::CSV_XS qw( csv );
15
use Proc::Daemon;
16
use MIME::Lite;
17
use File::Basename;
18
use Data::Password qw(:all);
19
use Geo::IP;
20
use lib dirname (__FILE__);
21
use Stabile;
22

    
23
$engineid = $Stabile::config->get('ENGINEID') || "";
24
$enginename = $Stabile::config->get('ENGINENAME') || "";
25
#$enginelinked = $Stabile::config->get('ENGINE_LINKED') || "";
26
$showcost = $Stabile::config->get('SHOW_COST') || "";
27
$cur = $Stabile::config->get('CURRENCY') || "USD";
28
$engineuser = $Stabile::config->get('ENGINEUSER') || "";
29
$externaliprangestart = $Stabile::config->get('EXTERNAL_IP_RANGE_START') || "";
30
$externaliprangeend = $Stabile::config->get('EXTERNAL_IP_RANGE_END') || "";
31
$proxyiprangestart = $Stabile::config->get('PROXY_IP_RANGE_START') || "";
32
$proxyiprangeend = $Stabile::config->get('PROXY_IP_RANGE_END') || "";
33
$proxygw = $Stabile::config->get('PROXY_GW') || "";
34

    
35
$uiuuid;
36
$uistatus;
37
$help = 0; # If this is set, functions output help
38

    
39
#our %options=();
40
# -a action -h help -u uuid -m match pattern -f full list, i.e. all users
41
# -v verbose, include HTTP headers -s impersonate subaccount -t target [uuid or image]
42
# -g args to gearman task
43
#Getopt::Std::getopts("a:hfu:g:m:vs:t:", \%options);
44

    
45
try {
46
    Init(); # Perform various initalization tasks
47
    process() if ($package);
48

    
49
} catch Error with {
50
    my $ex = shift;
51
    print header('text/html', '500 Internal Server Error') unless ($console);
52
    if ($ex->{-text}) {
53
        print "Got error: ", $ex->{-text}, " on line ", $ex->{-line}, "\n";
54
    } else {
55
        print "Status=ERROR\n";
56
    }
57
} finally {
58
};
59

    
60
1;
61

    
62
sub getObj {
63
    my %h = %{@_[0]};
64
    $console = 1 if $h{"console"};
65
    $api = 1 if $h{"api"};
66
    my $username = $h{"username"} || $h{"uuid"};
67
    my $obj;
68
    $action = $action || $h{'action'};
69
    if ($action=~ /engine$|updateclientui$|updateui$/) {
70
        $obj = \%h;
71
        $obj->{pwd} = $obj->{password} if ($obj->{password});
72
    } else {
73
        $obj = $register{$username};
74
        my %hobj = %{$register{$username}};
75
        $obj = \%hobj; # We do this to get around a weird problem with freeze...
76
        my @props = qw ( restorefile engineid enginename engineurl username user password pwd fullname email
77
            opemail alertemail phone opphone opfullname allowfrom allowinternalapi privileges accounts accountsprivileges
78
            storagepools memoryquota storagequota nodestoragequota vcpuquota externalipquota rxquota txquota billto dnsdomains appstoreurl totpsecret );
79
        foreach my $prop (@props) {
80
            if (defined $h{$prop}) {
81
                $obj->{$prop} = $h{$prop};
82
            }
83
        }
84
    }
85
    return $obj;
86
}
87

    
88
sub Init {
89
    # Tie database tables to hashes
90
    unless ( tie(%register,'Tie::DBI', Hash::Merge::merge({table=>'users', key=>'username'}, $Stabile::dbopts)) ) {return "Unable to access users register"};
91

    
92
    # simplify globals initialized in Stabile.pm
93
    $tktuser = $tktuser || $Stabile::tktuser;
94
    $user = $user || $Stabile::user;
95

    
96
    $fullname = $register{$user}->{'fullname'};
97
    $email = $register{$user}->{'email'};
98
    $opemail = $register{$user}->{'opemail'};
99
    $alertemail = $register{$user}->{'alertemail'};
100
    $phone = $register{$user}->{'phone'};
101
    $opphone = $register{$user}->{'opphone'};
102
    $opfullname = $register{$user}->{'opfullname'};
103
    $allowfrom = $register{$user}->{'allowfrom'};
104
    $allowinternalapi = $register{$user}->{'allowinternalapi'};
105
    $lastlogin = $register{$user}->{'lastlogin'};
106
    $lastloginfrom = $register{$user}->{'lastloginfrom'};
107

    
108
#    if ($register{$user}->{'lastlogin'} ne $tkt) {
109
#        $register{$user}->{'lastlogin'} = time;
110
#        $register{$user}->{'lastloginfrom'} = $ENV{'REMOTE_ADDR'};
111
#        $register{$user}->{'lasttkt'} = $tkt;
112
#    }
113

    
114
    $Stabile::userstoragequota = 0+ $register{$user}->{'storagequota'};
115
    $Stabile::usernodestoragequota = 0+ $register{$user}->{'nodestoragequota'};
116
    $usermemoryquota = 0+ $register{$user}->{'memoryquota'};
117
    $uservcpuquota = 0+ $register{$user}->{'vcpuquota'};
118
    $userexternalipquota = 0+ $register{$user}->{'externalipquota'};
119
    $userrxquota = 0+ $register{$user}->{'rxquota'};
120
    $usertxquota = 0+ $register{$user}->{'txquota'};
121

    
122
    $storagequota = $Stabile::userstoragequota || $defaultstoragequota;
123
    $nodestoragequota = $Stabile::usernodestoragequota || $defaultnodestoragequota;
124
    $memoryquota = $usermemoryquota || $defaultmemoryquota;
125
    $vcpuquota = $uservcpuquota || $defaultvcpuquota;
126
    $externalipquota = $userexternalipquota || $defaultexternalipquota;
127
    $rxquota = $userrxquota || $defaultrxquota;
128
    $txquota = $usertxquota || $defaulttxquota;
129

    
130
    # Create aliases of functions
131
    *header = \&CGI::header;
132

    
133
    *Unlinkengine = \&Linkengine;
134
    *Updateengine = \&Linkengine;
135
    *Saveengine = \&Linkengine;
136
    *Syncusers = \&Linkengine;
137

    
138
    *do_help = \&action;
139
    *do_show = \&do_uuidshow;
140
    *do_delete = \&do_remove;
141
    *do_tablelist = \&do_list;
142
    *do_billingstatus = \&do_billing;
143
    *do_usage = \&do_billing;
144
    *do_usagestatus = \&do_billing;
145
    *do_billingavgstatus = \&do_billing;
146
    *do_usageavgstatus = \&do_billing;
147
    *do_upgradeengine = \&privileged_action;
148
    *do_gear_upgradeengine = \&do_gear_action;
149
    *do_backupengine = \&privileged_action;
150
    *do_gear_backupengine = \&do_gear_action;
151
    *do_restoreengine = \&privileged_action;
152
    *do_gear_restoreengine = \&do_gear_action;
153
    *do_releasepressure = \&privileged_action_async;
154
    *do_gear_releasepressure = \&do_gear_action;
155

    
156
    *do_linkengine = \&privileged_action;
157
    *do_gear_linkengine = \&do_gear_action;
158
    *do_saveengine = \&privileged_action_async;
159
    *do_gear_saveengine = \&do_gear_action;
160
    *do_unlinkengine = \&privileged_action;
161
    *do_gear_unlinkengine = \&do_gear_action;
162
    *do_updateengine = \&privileged_action;
163
    *do_syncusers = \&privileged_action;
164
    *do_gear_updateengine = \&do_gear_action;
165
    *do_gear_syncusers = \&do_gear_action;
166
    *do_deleteentirely = \&privileged_action;
167
    *do_gear_deleteentirely = \&do_gear_action;
168
    *do_vent = \&privileged_action;
169
    *do_gear_vent = \&do_gear_action;
170
    *do_updateui = \&privileged_action;
171
    *do_gear_updateui = \&do_gear_action;
172
}
173

    
174
sub do_listaccounts {
175
    my ($uuid, $action, $obj) = @_;
176
    if ($help) {
177
        return <<END
178
GET:common:
179
List other user accounts current user has access to use and switch to. This is an internal method which includes html
180
specifically for use with Dojo.
181
END
182
    }
183
    my $common = $params{'common'};
184
    my %bhash;
185
    my @accounts = split(/,\s*/, $register{$tktuser}->{'accounts'});
186
    my @accountsprivs = split(/,\s*/, $register{$tktuser}->{'accountsprivileges'});
187
    for my $i (0 .. $#accounts) {
188
        $bhash{$accounts[$i]} = {
189
            id=>$accounts[$i],
190
            privileges=>$accountsprivs[$i] || 'r'
191
        } if ($register{$accounts[$i]}); # Only include accounts that exist on this engine
192
    };
193
    $bhash{$tktuser} = {id=>$tktuser, privileges=>$privileges};
194
    delete $bhash{$user};
195
    $bhash{'common'} = {id=>'common', privileges=>'--'} if ($common);
196
    my @bvalues = values %bhash;
197
    unshift(@bvalues, {id=>$user, privileges=>$privileges});
198
    my $logout = {privileges=>'', id=>'<span class="glyphicon glyphicon-log-out" aria-hidden="true" style="font-size:15px;color:#3c3c3c; vertical-align:top; margin-top:8px;"></span> Log out '};
199
    push(@bvalues, $logout) unless ($common);
200
    $postreply = "{\"identifier\": \"id\",\"label\": \"id\", \"items\":" . JSON::to_json(\@bvalues, {pretty=>1}) . "}";
201
    return $postreply;
202
}
203

    
204
sub do_listids {
205
    my ($uuid, $action, $obj) = @_;
206
    if ($help) {
207
        return <<END
208
GET::
209
List other user accounts current user has read access to. Call with flat=1 if you want a flat array.
210
END
211
    }
212
    require "$Stabile::basedir/cgi/images.cgi";
213
    my $backupdevice = Stabile::Images::Getbackupdevice('', 'getbackupdevice');
214
    my $imagesdevice = Stabile::Images::Getimagesdevice('', 'getimagesdevice');
215
    my $mounts = `cat /proc/mounts | grep zfs`;
216
    my %engine_h;
217
    my $zbackupavailable = ( (($mounts =~ /$backupdevice\/backup (\S+) zfs/) && ($mounts =~ /$imagesdevice\/images (\S+) zfs/) )?1:'');
218
    my $jsontext = qq|{"identifier": "id","label": "id", "items":[| .
219
              qq|{"id": "$user", "privileges": "$privileges", "userprivileges": "$dbprivileges", "tktuser": "$tktuser", |.
220
              qq|"storagequota": $storagequota, "nodestoragequota": $nodestoragequota, "memoryquota": $memoryquota, "vcpuquota": $vcpuquota, |.
221
              qq|"fullname": "$fullname", "email": "$email", "opemail": "$opemail", "alertemail": "$alertemail", |.
222
              qq|"phone": "$phone", "opphone": "$opphone", "opfullname": "$opfullname", "appstoreurl": "$appstoreurl", |.
223
              qq|"allowfrom": "$allowfrom", "lastlogin": "$lastlogin", "lastloginfrom": "$lastloginfrom", "allowinternalapi": "$allowinternalapi", "billto": "$billto", |.
224
              qq|"dnsdomain": "$dnsdomain", "appstoreurl": "$appstoreurl", |;
225

    
226
    if ($isadmin && $engineid) {
227
        $engine_h{"engineid"} = $engineid;
228
        $engine_h{"engineuser"} = $engineuser;
229
        $engine_h{"externaliprangestart"} = $externaliprangestart;
230
        $engine_h{"externaliprangeend"} = $externaliprangeend;
231
        $engine_h{"proxyiprangestart"} = $proxyiprangestart;
232
        $engine_h{"proxyiprangeend"} = $proxyiprangeend;
233
        $engine_h{"proxygw"} = $proxygw;
234

    
235
        $engine_h{"disablesnat"} = $disablesnat;
236
        $engine_h{"imagesdevice"} = $imagesdevice;
237
        $engine_h{"backupdevice"} = $backupdevice;
238

    
239
        my $nodecfg = new Config::Simple("/etc/stabile/nodeconfig.cfg");
240
        my $readlimit = $nodecfg->param('VM_READ_LIMIT'); # e.g. 125829120 = 120 * 1024 * 1024 = 120 MB / s
241
        my $writelimit = $nodecfg->param('VM_WRITE_LIMIT');
242
        my $iopsreadlimit = $nodecfg->param('VM_IOPS_READ_LIMIT'); # e.g. 1000 IOPS
243
        my $iopswritelimit = $nodecfg->param('VM_IOPS_WRITE_LIMIT');
244
        $engine_h{"vmreadlimit"} = $readlimit;
245
        $engine_h{"vmwritelimit"} = $writelimit;
246
        $engine_h{"vmiopsreadlimit"} = $iopsreadlimit;
247
        $engine_h{"vmiopswritelimit"} = $iopswritelimit;
248

    
249
        $engine_h{"zfsavailable"} = $zbackupavailable;
250
        $engine_h{"downloadmasters"} = $downloadmasters;
251
    }
252
    if (-e "/var/www/stabile/static/img/logo-icon-" . $ENV{HTTP_HOST} . ".png") {
253
        $jsontext .= qq|"favicon": "/stabile/static/img/logo-icon-$ENV{HTTP_HOST}.png", |;
254
    }
255
    $engine_h{"enginename"} = $enginename;
256
    $engine_h{"enginelinked"} = $enginelinked;
257
    $jsontext .= "\"showcost\": \"$showcost\", ";
258
    $jsontext .= "\"externalipquota\": $externalipquota, \"rxquota\": $rxquota, \"txquota\": $txquota, ";
259
    $jsontext .= qq|"defaultstoragequota": $defaultstoragequota, "defaultnodestoragequota": $defaultnodestoragequota, "defaultmemoryquota": $defaultmemoryquota, "defaultvcpuquota": $defaultvcpuquota, |;
260
    $jsontext .= "\"defaultexternalipquota\": $defaultexternalipquota, \"defaultrxquota\": $defaultrxquota, \"defaulttxquota\": $defaulttxquota, ";
261
    $jsontext .= qq|"engine": | . to_json(\%engine_h);
262
    $jsontext .= "},  ";
263

    
264
    $jsontext .= "{\"id\": \"common\", \"privileges\": \"--\"," .
265
      "\"fullname\": \"--\", \"email\": \"--\"," .
266
      "\"storagequota\": 0, \"memoryquota\": 0, \"vcpuquota\": 0, \"externalipquota\": 0," .
267
      "\"rxquota\": 0, \"txquota\": 0}";
268

    
269
    $jsontext .= ", {\"id\": \"$billto\"}" if ($billto && $billto ne '--');
270

    
271
    foreach my $aid (keys %ahash) {
272
        my $privs = $ahash{$aid};
273
        $jsontext .= qq|, {"id": "$aid", "privileges": "$privs"}| unless ($aid eq $user || $aid eq $billto);
274
    }
275

    
276
    $jsontext .= "]}";
277
    # Create ui_update link in case we are logging in with a remotely generated ticket, i.e. not passing through login.cgi
278
    `/bin/ln -s ../ui_update.cgi ../cgi/ui_update/$user~ui_update.cgi` unless (-e "../cgi/ui_update/$user~ui_update.cgi");
279
    $postreply = to_json(from_json($jsontext), {pretty=>1});
280
    return $postreply;
281
}
282

    
283
sub do_listengines{
284
    my ($uuid, $action, $obj) = @_;
285
    if ($help) {
286
        return <<END
287
GET::
288
List other engines user has access to
289
END
290
    }
291
    if ($enginelinked) {
292
        require LWP::Simple;
293
        my $browser = LWP::UserAgent->new;
294
        $browser->agent('stabile/1.0b');
295
        $browser->protocols_allowed( [ 'http','https'] );
296

    
297
        my $tktcfg = ConfigReader::Simple->new($Stabile::auth_tkt_conf, [qw(TKTAuthSecret)]);
298
        my $tktkey = $tktcfg->get('TKTAuthSecret') || '';
299

    
300
        $postreq->{'engineid'} = $engineid;
301
        $postreq->{'user'} = $user;
302
        $postreq->{'enginetkthash'} = Digest::SHA::sha512_hex($tktkey);
303
        $postreq->{'api'} = $params{api};
304
        $postreq->{'usertkt'} = $params{auth_tkt};
305

    
306
        my $content = $browser->post("https://www.stabile.io/irigo/engine.cgi?action=listengines", $postreq)->content();
307
        if ($content =~ /ERROR:(.+)"/) {
308
            $postreply = qq|{"identifier": "url", "label": "name", "items": [{"url": "# $1", "name": "$enginename"}]}|;
309
        } else {
310
            $postreply = qq|{"identifier": "url", "label": "name", "items": $content}|;
311
        }
312
    } else {
313
        $postreply = qq|{"identifier": "url", "label": "name", "items": [{"url": "#", "name": "$enginename"}]}|;
314
    }
315
    return $postreply;
316
}
317

    
318
sub do_listengineconfigs{
319
    my ($uuid, $action, $obj) = @_;
320
    if ($help) {
321
        return <<END
322
GET::
323
List configs of engines user has access to
324
END
325
    }
326
    if ($enginelinked) {
327
        require LWP::Simple;
328
        my $browser = LWP::UserAgent->new;
329
        $browser->agent('stabile/1.0b');
330
        $browser->protocols_allowed( [ 'http','https'] );
331

    
332
        my $tktcfg = ConfigReader::Simple->new($Stabile::auth_tkt_conf, [qw(TKTAuthSecret)]);
333
        my $tktkey = $tktcfg->get('TKTAuthSecret') || '';
334

    
335
        $postreq->{'engineid'} = $engineid;
336
        $postreq->{'user'} = $user;
337
        $postreq->{'username'} = $params{username};
338
        $postreq->{'usertkt'} = $params{auth_tkt};
339
        $postreq->{'enginetkthash'} = Digest::SHA::sha512_hex($tktkey);
340

    
341
        my $content = $browser->post("https://www.stabile.io/irigo/engine.cgi?action=listengineconfigs", $postreq)->content();
342
        $postreply = $content;
343
    } else {
344
        $postreply = qq|{"status": "Error", "message": "Engine not linked"}|;
345
    }
346
    return $postreply;
347
}
348

    
349
sub do_billing {
350
    my ($uuid, $action, $obj) = @_;
351
    if ($help) {
352
        return <<END
353
GET:uuid,username,month,startmonth,endmonth,format:
354
List usage data, optionally for specific server/system [uuid] or user [username]. May be called as usage, usagestatus or usageavgstatus.
355
When called as "usage", format may be csv, in which case startmonth and endmonth may be specified.
356
END
357
    }
358
    my $buser = $params{'user'} || $params{'username'} || $user;
359
    my $bmonth = $params{'month'} || $month;
360
    $bmonth = substr("0$bmonth", -2);
361
    my $byear = $params{'year'} || $year;
362
    my $vcpu=0, $memory=0, $virtualsize=0, $nodevirtualsize=0, $backupsize=0, $externalip=0;
363
    my $rx = 0;
364
    my $tx = 0;
365
    my $vcpuavg = 0;
366
    my $externalipavg = 0;
367
    $uuid = '' if ($register{$uuid}); # check if $uuid was set to $user because no actual uuid passed
368

    
369
    if ($user eq $buser || index($privileges,"a")!=-1) {
370
         my %stats = collectBillingData( $uuid, $buser, $bmonth, $byear, $showcost );
371
         my $memoryquotagb = int(0.5 + 100*$memoryquota/1024)/100;
372
         my $storagequotagb = int(0.5 + 100*$storagequota/1024)/100;
373
         my $nodestoragequotagb = int(0.5 + 100*$nodestoragequota/1024)/100;
374
         my $irigo_cost = ($showcost?"showcost":"hidecost");
375

    
376
         if ($action eq "billing" || $action eq "usage") {
377
             if ($params{'format'} eq 'csv') {
378
                 $postreply = header("text/plain");
379
                 my $startmonth = $params{'startmonth'} || 1;
380
                 my $endmonth = $params{'endmonth'} || $bmonth;
381
                 my @vals;
382
                 for (my $i=$startmonth; $i<=$endmonth; $i++) {
383
                     my $m = substr("0$i", -2);
384
                     my %mstats = collectBillingData( $uuid, $buser, $m, $byear, $showcost );
385
                     push @vals, \%mstats;
386
                 }
387
                 csv(in => \@vals, out => \my $csvdata);
388
                 $postreply .= $csvdata;
389
             } else {
390
                 my $json_text = JSON::to_json(\%stats, {pretty => 1});
391
                 $postreply = "$json_text";
392
             }
393

    
394
         } elsif ($action eq "billingstatus" || $action eq "usagestatus") {
395
             my $virtualsizegb = $stats{'virtualsize'};
396
             my $backupsizegb = $stats{'backupsize'};
397
             my $externalip = $stats{'externalip'};
398
             my $memorygb = $stats{'memory'};
399
             my $nodevirtualsizegb = $stats{'nodevirtualsize'};
400
             $rx = $stats{'rx'};
401
             $tx = $stats{'tx'};
402
             $vcpu = $stats{'vcpu'};
403

    
404
             my $res;
405
             if ($params{'format'} eq 'html') {
406
                 $postreply .= header("text/html");
407
                 $res .= qq[<tr><th>Ressource</th><th>Quantity</th><th class="$irigo_cost">Cost/month</th><th>Quota</th></tr>];
408
                 $res .= qq[<tr><td>vCPU's:</td><td align="right">$vcpu</td><td align="right" class="$irigo_cost">$cur ] . int(0.5+$vcpu*$vcpuprice) . qq[</td><td align="right">$vcpuquota</td></tr>];
409
                 $res .= qq[<tr><td>Memory:</td><td align="right">$memorygb GB</td><td align="right" class="$irigo_cost">$cur ] . int(0.5+$memorygb*$memoryprice) . qq[</td><td align="right">$memoryquotagb GB</td></tr>];
410
                 $res .= qq[<tr><td>Shared storage:</td><td align="right">$virtualsizegb GB</td><td align="right" class="$irigo_cost">$cur ] . int(0.5+$virtualsizegb*$storageprice) . qq[</td><td align="right">$storagequotagb GB</td></tr>];
411
                 $res .= qq[<tr><td>Node storage:</td><td align="right">$nodevirtualsizegb GB</td><td align="right" class="$irigo_cost">$cur ] . int(0.5+$nodevirtualsizegb*$nodestorageprice) . qq[</td><td align="right">$nodestoragequotagb GB</td></tr>];
412
                 $res .= qq[<tr><td>Backup storage (est.):</td><td align="right">$backupsizegb GB</td><td align="right" class="$irigo_cost">$cur ] . int(0.5+$backupsizegb*$storageprice) . qq[</td><td align="right">&infin;</td></tr>];
413
                 $res .= qq[<tr><td>External IP addresses:</td><td align="right">$externalip</td><td align="right" class="$irigo_cost">$cur ] . int(0.5+$externalip*$externalipprice) . qq[</td><td align="right">$externalipquota</td></tr>];
414
                 if (!$uuid) {
415
                     $res .= qq[<tr><td>Network traffic out:</td><td align="right">] . $rx . qq[ GB</td><td align="right" class="$irigo_cost">$cur 0</td><td align="right">] . int(0.5 + $rxquota/1024/1024) . qq[ GB</td></tr>];
416
                     $res .= qq[<tr><td>Network traffic in:</td><td align="right">] . $tx . qq[ GB</td><td align="right" class="$irigo_cost">$cur 0</td><td align="right">] . int(0.5 + $txquota/1024/1024) . qq[ GB</td></tr>];
417
                 }
418

    
419
                 $res =~ s/-1/&infin;/g;
420
                 $res =~ s/>0 .B<\/td><\/tr>/>&infin;<\/td><\/tr>/g;
421
                 $postreply .= qq[<table cellspacing="0" noframe="void" norules="rows" class="systemTables">$res</table>];
422
             } else {
423
                 my $bill = {
424
                     vcpus => {quantity => $vcpu, quota => $vcpuquota},
425
                     memory => {quantity => $memorygb, unit => 'GB', quota => $memoryquotagb},
426
                     shared_storage => {quantity => $virtualsizegb, unit => 'GB', quota => $storagequotagb},
427
                     node_storage => {quantity => $nodevirtualsizegb, unit => 'GB', quota => $nodestoragequotagb},
428
                     backup_storage => {quantity => $backupsizegb, unit => 'GB'},
429
                     external_ips => {quantity => $externalip, quota => $externalipquota},
430
                     network_traffic_out => {quantity => $rx, unit => 'GB', quota => int(0.5 + $rxquota/1024/1024)},
431
                     network_traffic_in => {quantity => $tx, unit => 'GB', quota => int(0.5 + $txquota/1024/1024)}
432
                 };
433
                 if ($showcost) {
434
                     $bill->{vcpus}->{cost} = int(0.5+$vcpu*$vcpuprice);
435
                     $bill->{memory}->{cost} = int(0.5+$memorygb*$memoryprice);
436
                     $bill->{shared_storage}->{cost} = int(0.5+$virtualsizegb*$storageprice);
437
                     $bill->{node_storage}->{cost} = int(0.5+$nodevirtualsizegb*$nodestorageprice);
438
                     $bill->{backup_storage}->{cost} = int(0.5+$backupsizegb*$storageprice);
439
                     $bill->{external_ips}->{cost} = int(0.5+$externalip*$externalipprice);
440
                     $bill->{currency} = $cur;
441
                     $bill->{username} = $buser;
442
                 }
443
                 $postreply .= to_json($bill, {pretty=>1});
444
             }
445
         } elsif ($action eq "billingavgstatus" || $action eq "usageavgstatus") {
446
             my $virtualsizeavggb = $stats{'virtualsizeavg'};
447
             my $backupsizeavggb = $stats{'backupsizeavg'};
448
             my $memoryavggb = $stats{'memoryavg'};
449
             my $nodevirtualsizeavggb = $stats{'nodevirtualsizeavg'};
450
             $vcpuavg = $stats{'vcpuavg'};
451
             $externalipavg = $stats{'externalipavg'};
452
             $rx = $stats{'rx'};
453
             $tx = $stats{'tx'};
454
             if ($params{'format'} eq 'html') {
455
                 $postreply .= header("text/html");
456
                 my $res;
457
                 $res .= qq[<tr><th>Ressource</th><th>Quantity</th><th class="$irigo_cost">Cost/month</th><th>Quota</th></tr>];
458
                 $res .= qq[<tr><td>vCPU's:</td><td align="right">] . int(0.5+100*$vcpuavg)/100 . qq[</td><td align="right" class="$irigo_cost">$cur ] . int(0.5+$vcpuavg*$vcpuprice) . qq[</td><td align="right">$vcpuquota</td></tr>];
459
                 $res .= qq[<tr><td>Memory:</td><td align="right">$memoryavggb GB</td><td align="right" class="$irigo_cost">$cur ] . int(0.5+$memoryavggb*$memoryprice) . qq[</td><td align="right">$memoryquotagb GB</td></tr>];
460
                 $res .= qq[<tr><td>Shared storage:</td><td align="right">$virtualsizeavggb GB</td><td align="right" class="$irigo_cost">$cur ] . int(0.5+$virtualsizeavggb*$storageprice) . qq[</td><td align="right">$storagequotagb GB</td></tr>];
461
                 $res .= qq[<tr><td>Node storage:</td><td align="right">$nodevirtualsizeavggb GB</td><td align="right" class="$irigo_cost">$cur ] . int(0.5+$nodevirtualsizeavggb*$nodestorageprice) . qq[</td><td align="right">$nodestoragequotagb GB</td></tr>];
462
                 $res .= qq[<tr><td>Backup storage (est.):</td><td align="right">$backupsizeavggb GB</td><td align="right" class="$irigo_cost">$cur ] . int(0.5+$backupsizeavggb*$storageprice) . qq[</td><td align="right">&infin;</td></tr>];
463
                 $res .= qq[<tr><td>External IP addresses:</td><td align="right">] . int(0.5+100*$externalipavg)/100 . qq[</td><td align="right" class="$irigo_cost">$cur ] . int(0.5+$externalipavg*$externalipprice) . qq[</td><td align="right">$externalipquota</td></tr>];
464
                 $res .= qq[<tr><td>Network traffic in:</td><td align="right">] . int(0.5 + $rx) . qq[ GB</td><td align="right" class="$irigo_cost">$cur 0</td><td align="right">] . int(0.5 + $rxquota/1024/1024) . qq[ GB</td></tr>];
465
                 $res .= qq[<tr><td>Network traffic out:</td><td align="right">] . int(0.5 + $tx) . qq[ GB</td><td align="right" class="$irigo_cost">$cur 0</td><td align="right">] . int(0.5 + $txquota/1024/1024) . qq[ GB</td></tr>];
466

    
467
                 $res =~ s/-1/&infin;/g;
468
                 $res =~ s/>0 .B<\/td><\/tr>/>&infin;<\/td><\/tr>/g;
469
                 $postreply .= qq[<table cellspacing="0" noframe="void" norules="rows" class="systemTables">$res</table>];
470
             } else {
471
                 my $bill = {
472
                     vcpus => {quantity => $vcpuavg, quota => $vcpuquota},
473
                     memory => {quantity => $memoryavggb, unit => 'GB', quota => $memoryquotagb},
474
                     shared_storage => {quantity => $virtualsizeavggb, unit => 'GB', quota => $storagequotagb},
475
                     node_storage => {quantity => $nodevirtualsizeavggb, unit => 'GB', quota => $nodestoragequotagb},
476
                     backup_storage => {quantity => $backupsizeavggb, unit => 'GB'},
477
                     external_ips => {quantity => $externalipavg, quota => $externalipquota},
478
                     network_traffic_out => {quantity => int(0.5 + $rx), unit => 'GB', quota => int(0.5 + $rxquota/1024/1024)},
479
                     network_traffic_in => {quantity => int(0.5 + $tx), unit => 'GB', quota => int(0.5 + $txquota/1024/1024)}
480
                 };
481
                 if ($showcost) {
482
                     $bill->{vcpus}->{cost} = int(0.5+$vcpuavg*$vcpuprice);
483
                     $bill->{memory}->{cost} = int(0.5+$memoryavggb*$memoryprice);
484
                     $bill->{shared_storage}->{cost} = int(0.5+$virtualsizeavggb*$storageprice);
485
                     $bill->{node_storage}->{cost} = int(0.5+$nodevirtualsizeavggb*$nodestorageprice);
486
                     $bill->{backup_storage}->{cost} = int(0.5+$backupsizeavggb*$storageprice);
487
                     $bill->{external_ips}->{cost} = int(0.5+$externalipavg*$externalipprice);
488
                     $bill->{currency} = $cur;
489
                     $bill->{username} = $buser;
490
                 }
491
                 $postreply .= to_json($bill, {pretty=>1});
492
             }
493
        }
494
    } else {
495
        $postreply .= "Status=ERROR no privileges!!\n";
496
    }
497
    return $postreply;
498
}
499

    
500
sub do_listenginebackups {
501
    my ($uuid, $action, $obj) = @_;
502
    if ($help) {
503
        return <<END
504
GET::
505
List the backups of this engine's configuration in the registry.
506
END
507
    }
508
    if ($enginelinked) {
509
        require LWP::Simple;
510
        my $browser = LWP::UserAgent->new;
511
        $browser->agent('stabile/1.0b');
512
        $browser->protocols_allowed( [ 'http','https'] );
513

    
514
        my $tktcfg = ConfigReader::Simple->new($Stabile::auth_tkt_conf, [qw(TKTAuthSecret)]);
515
        my $tktkey = $tktcfg->get('TKTAuthSecret') || '';
516

    
517
        $postreq->{'engineid'} = $engineid;
518
        $postreq->{'enginetkthash'} = Digest::SHA::sha512_hex($tktkey);
519

    
520
        my $content = $browser->post("https://www.stabile.io/irigo/engine.cgi?action=listbackups", $postreq)->content();
521
        if ($content =~ /\[\]/) {
522
            $postreply = qq|{"identifier": "path", "label": "name", "items": [{"path": "#", "name": "No backups"}]}|;
523
        } else {
524
            $postreply = qq|{"identifier": "path", "label": "name", "items": $content}|;
525
        }
526
    } else {
527
        $postreply = qq|{"identifier": "path", "label": "name", "items": [{"path": "#", "name": "Engine not linked"}]}|;
528
    }
529
    return $postreply;
530
}
531

    
532
sub Backupengine {
533
    my ($uuid, $action, $obj) = @_;
534
    if ($help) {
535
        return <<END
536
GET::
537
Backup this engine's configuration to the registry.
538
END
539
    }
540
    my $backupname = "$enginename.$engineid.$pretty_time";
541
    $backupname =~ tr/:/-/; # tar has a problem with colons in filenames
542
    if (-e "/tmp/$backupname.tgz") {
543
        $postreply .= "Status=ERROR Engine is already being backed up";
544
    } else {
545
        $res .= `mysqldump --ignore-table=steamregister.nodeidentities steamregister > /etc/stabile/steamregister.sql`;
546
        $res .= `cp /etc/apache2/conf-available/auth_tkt_cgi.conf /etc/stabile`;
547
        $res .= `cp /etc/apache2/ssl/*.crt /etc/stabile`;
548
        $res .= `cp /etc/apache2/ssl/*.pem /etc/stabile`;
549
        $res .= `cp /etc/apache2/ssl/*.key /etc/stabile`;
550
        $res .= `cp /etc/hosts.allow /etc/stabile`;
551
        $res .= `cp /etc/mon/mon.cf /etc/stabile`;
552

    
553
        # copy default node configuration to /etc/stabile
554
        unless ( tie(%register,'Tie::DBI', Hash::Merge::merge({table=>'nodeidentities', key=>'identity'}, $Stabile::dbopts)) ) {return "Unable to access identity register"};
555

    
556
        my $defaultpath = $idreg{'default'}->{'path'} . "/casper/filesystem.dir/etc/stabile/nodeconfig.cfg";
557
        $res .= `cp $defaultpath /etc/stabile`;
558

    
559
        # Make tarball
560
        my $cmd = qq[(cd /etc/stabile; /bin/tar -czf "/tmp/$backupname.tgz" * 2>/dev/null)];
561
        $res .= `$cmd`;
562

    
563
        my $tktcfg = ConfigReader::Simple->new($Stabile::auth_tkt_conf, [qw(TKTAuthSecret)]);
564
        my $tktkey = $tktcfg->get('TKTAuthSecret') || '';
565
        my $enginetkthash = Digest::SHA::sha512_hex($tktkey);
566

    
567
        my $res = `/usr/bin/curl -k -F engineid=$engineid -F enginetkthash=$enginetkthash -F filedata=@"/tmp/$backupname.tgz" https://www.stabile.io/irigo/engine.cgi?action=backup`;
568
        if ($res =~ /OK: $backupname.tgz received/) {
569
            $postreply .= "Status=OK Engine configuration saved to the registry";
570
            $main::syslogit->($user, "info", "Engine configuration saved to the registry");
571
            unlink("/tmp/$backupname.tgz");
572
        } else {
573
            $postreply .= "Status=ERROR Problem backing configuration up to the registry\n$res\n";
574
        }
575
    }
576
    return $postreply;
577
}
578

    
579
sub Upgradeengine {
580
    my ($uuid, $action, $obj) = @_;
581
    if ($help) {
582
        return <<END
583
GET::
584
Try to upgrade this engine to latest release from the registry
585
END
586
    }
587
    $postreply = "Status=OK Requesting upgrade of Stabile\n";
588
    print header("text/plain"), $postreply;
589
    `echo "UPGRADE=1" >> /etc/stabile/config.cfg` unless ( `grep ^UPGRADE=1 /etc/stabile/config.cfg`);
590
    my $cmd = "echo 'sleep 5 ; /usr/bin/pkill pressurecontrol' | at now";
591
    system($cmd);
592
    exit 0;
593
}
594

    
595
sub do_billengine {
596
    my ($uuid, $action, $obj) = @_;
597
    if ($help) {
598
        return <<END
599
GET::
600
Submit billing data for this engine to the registry.
601
END
602
    }
603
    require LWP::Simple;
604
    my $browser = LWP::UserAgent->new;
605
    $browser->agent('stabile/1.0b');
606
    $browser->protocols_allowed( [ 'http','https'] );
607

    
608
    my $bmonth = $params{'month'} || $month;
609
    $bmonth = substr("0$bmonth", -2);
610
    my $byear = $params{'year'} || $year;
611
    $showcost = 1;
612

    
613
    my $tktcfg = ConfigReader::Simple->new($Stabile::auth_tkt_conf, [qw(TKTAuthSecret)]);
614
    my $tktkey = $tktcfg->get('TKTAuthSecret') || '';
615
    my $tkthash = Digest::SHA::sha512_hex($tktkey);
616

    
617
    my $postreq = ();
618
    my %bill;
619
    my @regvalues = values %register; # Sort by id
620
    foreach my $valref (@regvalues) {
621
        my $cuser = $valref->{'username'};
622
        my %stats = collectBillingData( '', $cuser, $bmonth, $byear, $showcost );
623
        $bill{"$cuser-$byear-$bmonth"} = \%stats;
624
    }
625
    $postreq->{'engineid'} = $engineid;
626
    $postreq->{'enginetkthash'} = $tkthash;
627
    $postreq->{'keywords'} = JSON::to_json(\%bill, {pretty=>1});
628
    my $url = "https://www.stabile.io/irigo/engine.cgi";
629
    $content = $browser->post($url, $postreq)->content();
630
    $postreply = "Status=OK Billed this engine ($engineid)\n";
631
    $postreply .= "$postreq->{'keywords'}\n$content";
632
    return $postreply;
633
}
634

    
635
sub Linkengine {
636
    my ($uuid, $action, $obj) = @_;
637
    if ($help) {
638
        return <<END
639
PUT:username,password,engineid,enginename,engineurl:
640
Links engine to the registry
641
END
642
    }
643
    return "Status=Error Not allowed\n" unless ($isadmin || ($user eq $engineuser));
644
    my $linkaction = 'update';
645
    $linkaction = 'link' if ($action eq 'linkengine');
646
    $linkaction = 'unlink' if ($action eq 'unlinkengine');
647
    $linkaction = 'update' if ($action eq 'updateengine');
648
    $linkaction = 'update' if ($action eq 'syncusers');
649

    
650
    require LWP::Simple;
651
    my $browser = LWP::UserAgent->new;
652
    $browser->agent('stabile/1.0b');
653
    $browser->protocols_allowed( [ 'http','https'] );
654

    
655
    my $tktcfg = ConfigReader::Simple->new($Stabile::auth_tkt_conf, [qw(TKTAuthSecret)]);
656
    my $tktkey = $tktcfg->get('TKTAuthSecret') || '';
657

    
658
    my $postreq = ();
659
    $postreq->{'user'} = $user || $obj->{'username'};
660
    $postreq->{'engineid'} = $obj->{'engineid'} || $engineid;
661
    $postreq->{'pwd'} = $obj->{'pwd'} if ($obj->{'pwd'});
662
    $postreq->{'enginename'} = $obj->{'enginename'} if ($obj->{'enginename'});
663
    $postreq->{'engineurl'} = $obj->{'engineurl'} if ($obj->{'engineurl'});
664
    if ($tktkey) {
665
        if ($action eq 'linkengine') {
666
            $main::syslogit->($user, "info", "Linking engine with the registry");
667
            $postreq->{'enginetktkey'} = $tktkey;
668
        } else {
669
            $postreq->{'enginetkthash'} = Digest::SHA::sha512_hex($tktkey);
670
        }
671
    }
672
    if ($action eq "saveengine") { # Save request from the registry - don't post back
673
        # Pressurecontrol reads new configuration data from the registry, simply reload it
674
        my $pressureon = !(`systemctl is-active pressurecontrol` =~ /inactive/);
675
        $postreply = ($pressureon)? "Status=OK Engine updating...\n":"Status=OK Engine not updating because pressurecontrol not active\n";
676
        $postreply .= `systemctl restart pressurecontrol` if ($pressureon);
677
    } else {
678
        my $res;
679
        my $cfg = new Config::Simple("/etc/stabile/config.cfg");
680
        if ($action eq 'linkengine' || $action eq 'syncusers') {
681
            # Send engine users to the registry
682
            my @vals = values %register;
683
            my $json = JSON::to_json(\@vals);
684
            $json =~ s/null/""/g;
685
            $json = URI::Escape::uri_escape($json);
686
            $postreq->{'POSTDATA'} = $json;
687
        }
688
        if ($action eq 'linkengine' || $action eq 'updateengine') {
689
            # Update name in config file
690
            if ($postreq->{'enginename'} && $cfg->param("ENGINENAME") ne $postreq->{'enginename'}) {
691
                $cfg->param("ENGINENAME", $postreq->{'enginename'});
692
                $cfg->save();
693
            }
694
            # Send entire engine config file to the registry
695
            my %cfghash = $cfg->vars();
696
            foreach my $param (keys %cfghash) {
697
                $param =~ /default\.(.+)/; # Get rid of default. prefix
698
                if ($1) {
699
                    my $k = $1;
700
                    my @cvals = $cfg->param($param);
701
                    my $cval = join(", ", @cvals);
702
                    $postreq->{$k} = URI::Escape::uri_escape($cval);
703
                }
704
            }
705
            # Send entire engine piston config file to the registry
706
            my $nodeconfigfile = "/mnt/stabile/tftp/bionic/casper/filesystem.dir/etc/stabile/nodeconfig.cfg";
707
            if (-e $nodeconfigfile) {
708
                my $pistoncfg = new Config::Simple($nodeconfigfile);
709
                %cfghash = $pistoncfg->vars();
710
                foreach my $param (keys %cfghash) {
711
                    $param =~ /default\.(.+)/; # Get rid of default. prefix
712
                    if ($1) {
713
                        my $k = $1;
714
                        my @cvals = $pistoncfg->param($param);
715
                        my $cval = join(", ", @cvals);
716
                        $postreq->{$k} = URI::Escape::uri_escape($cval);
717
                    }
718
                }
719
            }
720
        }
721
        if ($linkaction eq 'link' || $enginelinked) {
722
            my $content = $browser->post("https://www.stabile.io/irigo/engine.cgi?action=$linkaction", $postreq)->content();
723
            if ($content =~ /(Engine linked|Engine not linked|Engine unlinked|Engine updated|Unknown engine|Invalid credentials .+\.)/i) {
724
                $res = "Status=OK $1";
725
                my $linked = 1;
726
                $linked = 0 unless ($content =~ /Engine linked/i || $content =~ /Engine updated/i);
727
                $cfg->param("ENGINE_LINKED", $linked);
728
                $cfg->save();
729
            } elsif ($action eq 'syncusers' || $action eq 'linkengine') { # If we send user list to the registry we get merged list back
730
                if ($content =~ /^\[/) { # Sanity check to see if we got json back
731
                    $res .= "Status=OK Engine linked\n" if ($action eq 'linkengine');
732
                    # Update engine users with users from the registry
733
                    $res .= updateEngineUsers($content);
734
                    $res .= "Status=OK Users synced with registry\n";
735
                    $main::updateUI->({ tab => 'users', type=>'update', user=>$user});
736
                }
737
                $res .= "$content" unless ($res =~ /Status=OK/); # Only add if there are problems
738
            }
739
            $postreply = $res;
740
            $content =~ s/\n/ - /;
741
            $res =~ s/\n/ - /;
742
        #    $main::syslogit->($user, "info", "$content");
743
            $main::syslogit->($user, "info", "Synced users");
744
        } else {
745
            $postreply .= "Status=OK Engine not linked, saving name\n";
746
        }
747
    }
748
    return $postreply;
749
}
750

    
751
sub Releasepressure {
752
    my ($uuid, $action, $obj) = @_;
753
    if ($help) {
754
        return <<END
755
GET::
756
Restarts pressurecontrol.
757
END
758
    }
759
    my $res;
760
    unless (`systemctl is-active pressurecontrol` =~ /inactive/) {
761
        my $daemon = Proc::Daemon->new(
762
            work_dir => '/usr/local/bin',
763
            exec_command => "systemctl restart pressurecontrol"
764
        ) or do {$postreply .= "Status=ERROR $@\n";};
765
        my $pid = $daemon->Init();
766
#        $res = `systemctl restart pressurecontrol`;
767
        return "Status=OK Venting...\n";
768
    } else {
769
        return "Status=OK Not venting\n";
770
    }
771
}
772

    
773
sub do_enable {
774
    my ($uuid, $action, $obj) = @_;
775
    if ($help) {
776
        return <<END
777
GET:username:
778
Enable a user.
779
END
780
    }
781
    my $username = $obj->{'username'};
782
    return unless ($username);
783
    if ($isadmin || ($user eq $engineuser)) {
784
        # Create user on this engine if not yet created
785
        do_save($username, 'save', $obj);
786
        my $uprivileges = $register{$username}->{'privileges'};
787
        $uprivileges =~ s/d//;
788
        $uprivileges .= 'n' unless ($uprivileges =~ /n/);# These are constant sources of problems - enable by default when enabling users to alleviate situation
789
        $register{$username}->{'privileges'} = $uprivileges;
790
        $register{$username}->{'allowinternalapi'} = 1;
791
        $postreply .= "Status=OK User $username enabled\n";
792
    } else {
793
        $postreply .= "Status=ERROR Not allowed\n";
794
    }
795
    $uiuuid = $username;
796
    return $postreply;
797
}
798

    
799
sub do_disable {
800
    my ($uuid, $action, $obj) = @_;
801
    if ($help) {
802
        return <<END
803
GET:username:
804
Disable a user.
805
END
806
    }
807
    my $username = $obj->{'username'};
808
    if ($isadmin || ($user eq $engineuser)) {
809
        my $uprivileges = $register{$username}->{'privileges'};
810
        $uprivileges .= 'd' unless ($uprivileges =~ /d/);
811
        $register{$username}->{'privileges'} = $uprivileges;
812
        $postreply .= "Stream=OK User $username disabled, halting servers...\n";
813
        require "$Stabile::basedir/cgi/servers.cgi";
814
        $Stabile::Servers::console = 1;
815
        $postreply .= Stabile::Servers::destroyUserServers($username,1);
816
        `/bin/rm /tmp/$username~*.tasks`;
817
    } else {
818
        $postreply .= "Status=ERROR Not allowed\n";
819
    }
820
    $uiuuid = $username;
821
    return $postreply;
822
}
823

    
824
sub Updateui {
825
    my ($uuid, $action, $obj) = @_;
826
    if ($help) {
827
        return <<END
828
GET:username,message,tab:
829
Update the UI for given user if logged into UI.
830
END
831
    }
832
    my $username = $obj->{'username'} || $user;
833
    my $message = $obj->{'message'};
834
    my $tab = $obj->{'tab'} || 'home';
835
    if ($isadmin || ($username eq $user) || ($user eq $engineuser)) {
836
        $postreply = $main::updateUI->({ tab => $tab, user => $username, message =>$message, type=>'update'});
837
    } else {
838
        $postreply = "Status=ERROR Not allowed\n";
839
    }
840
}
841

    
842
sub do_updateclientui {
843
    my ($uuid, $action, $obj) = @_;
844
    if ($help) {
845
        return <<END
846
GET:username,message,tab,type:
847
Update the UI for given user if logged into UI.
848
END
849
    }
850
    my $username = $obj->{'username'} || $user;
851
    my $message = $obj->{'message'};
852
    my $tab = $obj->{'tab'} || 'home';
853
    my $type= $obj->{'type'} || 'update';
854
    if ($isadmin || ($username eq $user) || ($user eq $engineuser)) {
855
        $postreply = $main::updateUI->({ tab => $tab, user => $username, message =>$message, type=>$type});
856
    } else {
857
        $postreply = "Status=ERROR Not allowed\n";
858
    }
859
}
860

    
861
sub Vent {
862
    my ($uuid, $action, $obj) = @_;
863
    if ($help) {
864
        return <<END
865
GET::
866
Restart pressurecontrol.
867
END
868
    }
869
    `systemctl restart pressurecontrol`;
870
    $postreply = "Status=OK Restarting pressurecontrol\n";
871
    return $postreply;
872
}
873

    
874
sub Deleteentirely {
875
    my ($uuid, $action, $obj) = @_;
876
    if ($help) {
877
        return <<END
878
GET:username:
879
Deletes a user and all the user's servers, images, networks etc. Warning: This destroys data
880
END
881
    }
882
    my $username = $obj->{'username'};
883
    my $reply = "Status=OK Removed $username";
884
    if (($isadmin || ($user eq $engineuser)) && $register{$username} && !($register{$username}->{'privileges'} =~ /a/) && !($username eq $engineuser)) {
885
        #Never delete admins
886
        my @dusers = ($username);
887
        # Add list of subusers - does not look like a good idea
888
        # foreach my $u (values %register) {
889
        #     push @dusers, $u->{'username'} if ($u->{'billto'} && $u->{'billto'} eq $username);
890
        # };
891

    
892
        foreach my $uname (@dusers) {
893
            if ($register{$uname}->{privileges} =~ /a/) { #Never delete admins
894
                $postreply .= "Stream=OK Not deleting user $uname - demote before deleting!\n";
895
                next;
896
            }
897
            $main::updateUI->({ tab => 'users', type=>'update', user=>$user, username=>$username, status=>'deleting'});
898

    
899
            $postreply .= "Stream=OK Deleting user $uname and all associated data!!!\n";
900
            $main::syslogit->($user, "info", "Deleting user $uname and all associated data");
901

    
902
            require "$Stabile::basedir/cgi/servers.cgi";
903
            $Stabile::Servers::console = 1;
904
            $Stabile::Servers::isadmin = $isadmin;
905
            require "$Stabile::basedir/cgi/systems.cgi";
906
            $Stabile::Systems::console = 1;
907
            $Stabile::Systems::isadmin = $isadmin;
908
            Stabile::Systems::removeusersystems($uname);
909
            Stabile::Servers::removeUserServers($uname);
910

    
911
            require "$Stabile::basedir/cgi/images.cgi";
912
            $Stabile::Images::console = 1;
913
            $postreply .= Stabile::Images::removeUserImages($uname);
914

    
915
            require "$Stabile::basedir/cgi/networks.cgi";
916
            $Stabile::Networks::console = 1;
917
            $Stabile::Networks::isadmin = $isadmin;
918
            Stabile::Networks::Removeusernetworks($uname);
919
            remove($uname);
920
            $reply = "$reply\n$postreply";
921

    
922
            # Also remove billing data, so next user with same username does not get old billing data
923
            `echo "delete from billing_domains where usernodetime like '$uname-%';" | mysql steamregister`;
924
            `echo "delete from billing_images where userstoragepooltime like '$uname-%';" | mysql steamregister`;
925
            `echo "delete from billing_networks where useridtime like '$uname-%';" | mysql steamregister`;
926
        }
927
        $main::updateUI->({tab => 'users', type=>'update', user=>$user});
928

    
929
    } else {
930
        $postreply .= "Stream=ERROR Cannot delete user $username - you cannot delete administrators!\n";
931
        $reply = $postreply;
932
    }
933
    return $reply;
934
}
935

    
936
sub do_save {
937
    my ($username, $action, $obj) = @_;
938
    if ($help) {
939
        return <<END
940
POST:username, password, privileges, fullname, email, opemail, alertemail, phone, opphone, opfullname, allowfrom, allowinternalapi, accounts, accountsprivileges, storagepools, memoryquota, storagequota, nodestoragequota, vcpuquota, externalipquota, rxquota, txquota:
941
Saves a user. If [username] does not exist, it is created if privileges allow this. [password] can be plaintext or a SHA256 hash.
942
END
943
    }
944
    $username = $username || $obj->{"username"};
945
    unless ($username && (($user eq $username) || $isadmin || ($user eq $engineuser))) {
946
        $postreply = "Status=ERROR Please provide a valid username\n";
947
        return $postreply;
948
    }
949
    my $password = '';
950
    my $reguser = $register{$username};
951
    if ($obj->{"password"} && $obj->{"password"} ne '--') {
952
        if (length $obj->{'password'} == 86) {
953
            $password = $obj->{"password"}; # This is already encoded
954
        } else {
955
            $password = $obj->{"password"};
956
            $MAXLEN = 20;
957
            my $msg = IsBadPassword($password);
958
            if ($msg) {
959
                $postreply = "Status=Error $msg - please choose a stronger password\n";
960
                $postmsg = "$msg - please choose a stronger password";
961
                return $postreply;
962
            } else {
963
                $password = Digest::SHA::sha512_base64($password);
964
            }
965
        }
966
    } else {
967
        $password = $reguser->{'password'};
968
    }
969
    my $fullname = $obj->{"fullname"} || $reguser->{'fullname'};
970
    my $email = $obj->{"email"} || $reguser->{'email'};
971
    my $opemail = $obj->{"opemail"} || $reguser->{'opemail'};
972
    my $alertemail = $obj->{"alertemail"} || $reguser->{'alertemail'};
973
    my $phone = $obj->{"phone"} || $reguser->{'phone'};
974
    my $opphone = $obj->{"opphone"} || $reguser->{'opphone'};
975
    my $opfullname = $obj->{"opfullname"} || $reguser->{'opfullname'};
976
    my $allowfrom = $obj->{"allowfrom"};
977
    my $totpsecret = $reguser->{'totpsecret'};
978
    $totpsecret = $obj->{"totpsecret"} if (defined $obj->{"totpsecret"});
979
    my $allowinternalapi = $obj->{"allowinternalapi"} || $reguser->{'allowinternalapi'};
980

    
981
    if (defined $obj->{"allowfrom"}) {
982
        my @allows = split(/(,\s*|\s+)/, $allowfrom);
983
        $allowfrom = '';
984
        my %allowshash;
985
        foreach my $ip (@allows) {
986
            $allowshash{"$1$2"} = 1 if ($ip =~ /(\d+\.\d+\.\d+\.\d+)(\/\d+)?/);
987
            if ($ip =~ /\w\w/) { # Check if we are dealing with a country code
988
                $ip = uc $ip;
989
                my $geoip = Geo::IP->new(GEOIP_MEMORY_CACHE);
990
                my $tz = $geoip->time_zone($ip, '');
991
                $allowshash{$ip} = 1 if ($tz); # We have a valid country code
992
            }
993
        }
994
        $allowfrom = join(", ", sort(keys %allowshash));
995
    }
996

    
997
    my $uprivileges = $reguser->{'privileges'};
998
    my $uaccounts = $reguser->{'accounts'};
999
    my $uaccountsprivileges = $reguser->{'accountsprivileges'};
1000
    my $storagepools = $reguser->{'storagepools'};
1001
    my $memoryquota = $reguser->{'memoryquota'};
1002
    my $storagequota = $reguser->{'storagequota'};
1003
    my $nodestoragequota = $reguser->{'nodestoragequota'};
1004
    my $vcpuquota = $reguser->{'vcpuquota'};
1005
    my $externalipquota = $reguser->{'externalipquota'};
1006
    my $rxquota = $reguser->{'rxquota'};
1007
    my $txquota = $reguser->{'txquota'};
1008
    my $tasks = $reguser->{'tasks'};
1009
    my $ubillto = $reguser->{'billto'};
1010
    my $udnsdomains = $reguser->{'dnsdomains'};
1011
    my $uappstoreurl = $reguser->{'appstoreurl'}; $uappstoreurl = '' if ($uappstoreurl eq '--');
1012
    my $created = $reguser->{'created'} || $current_time; # set created timestamp for new users
1013

    
1014
    # Only allow admins to change user privileges and quotas
1015
    if ($isadmin || $user eq $engineuser) {
1016
        $uprivileges = $obj->{"privileges"} || $reguser->{'privileges'};
1017
        $uprivileges = '' if ($uprivileges eq '--');
1018
        $uprivileges = 'n' if (!$reguser->{'username'} && !$uprivileges); # Allow new users to use node storage unless explicitly disallowed
1019
        $uprivileges =~ tr/adnrpu//cd; # filter out non-valid privileges
1020
        $uprivileges =~ s/(.)(?=.*?\1)//g; # filter out duplicates using positive lookahead
1021
        $storagepools = ($obj->{"storagepools"} || $obj->{"storagepools"} eq '0')?$obj->{"storagepools"} : $reguser->{'storagepools'};
1022
        $memoryquota = (defined $obj->{"memoryquota"}) ? $obj->{"memoryquota"} : $reguser->{'memoryquota'};
1023
        $storagequota = (defined $obj->{"storagequota"}) ? $obj->{"storagequota"} : $reguser->{'storagequota'};
1024
        $nodestoragequota = (defined $obj->{"nodestoragequota"}) ? $obj->{"nodestoragequota"} : $reguser->{'nodestoragequota'};
1025
        $vcpuquota = (defined $obj->{"vcpuquota"}) ? $obj->{"vcpuquota"} : $reguser->{'vcpuquota'};
1026
        $externalipquota = (defined $obj->{"externalipquota"}) ? $obj->{"externalipquota"} : $reguser->{'externalipquota'};
1027
        $rxquota = (defined $obj->{"rxquota"}) ? $obj->{"rxquota"} : $reguser->{'rxquota'};
1028
        $txquota = (defined $obj->{"txquota"}) ? $obj->{"txquota"} : $reguser->{'txquota'};
1029
        $tasks = $obj->{"tasks"} || $reguser->{'tasks'};
1030
        $ubillto = $obj->{"billto"} || $reguser->{'billto'};
1031
        $udnsdomains = $obj->{"dnsdomains"} || $udnsdomains; $udnsdomains = '' if ($udnsdomains eq '--');
1032
        $uappstoreurl = $obj->{"appstoreurl"} || $uappstoreurl;
1033
        $uaccounts = $obj->{"accounts"} || $reguser->{'accounts'};
1034
        $uaccountsprivileges = $obj->{"accountsprivileges"} || $reguser->{'accountsprivileges'};
1035
        my @ua = split(/, ?/, $uaccounts);
1036
        my @up = split(/, ?/, $uaccountsprivileges);
1037
        my @ua2 = ();
1038
        my @up2 = ();
1039
        my $i = 0;
1040
        foreach my $u (@ua) {
1041
            if ($register{$u} && ($u ne $username)) {
1042
                push @ua2, $u;
1043
                my $uprivs = $up[$i] || 'u';
1044
                $uprivs =~ tr/adnrpu//cd; # filter out non-valid privileges
1045
                $uprivs =~ s/(.)(?=.*?\1)//g; # filter out duplicates using positive lookahead
1046
                push @up2, $uprivs;
1047
            }
1048
            $i++;
1049
        }
1050
        $uaccounts = join(", ", @ua2);
1051
        $uaccountsprivileges = join(", ", @up2);
1052
    }
1053

    
1054
    # Sanity checks
1055
    if (
1056
        ($fullname && length $fullname > 255)
1057
            || ($password && length $password > 255)
1058
    ) {
1059
        $postreply .= "Status=ERROR Bad data: $username\n";
1060
        return  $postreply;
1061
    }
1062
    # Only allow new users to be created by admins, i.e. no auto-registration
1063
    if ($reguser->{'username'} || $isadmin) {
1064
        $register{$username} = {
1065
            password           => $password,
1066
            fullname           => $fullname,
1067
            email              => $email,
1068
            opemail            => $opemail,
1069
            alertemail         => $alertemail,
1070
            phone              => $phone,
1071
            opphone            => $opphone,
1072
            opfullname         => $opfullname,
1073
            allowfrom          => $allowfrom,
1074
            totpsecret         => $totpsecret,
1075
            privileges         => $uprivileges,
1076
            accounts           => $uaccounts,
1077
            accountsprivileges => $uaccountsprivileges,
1078
            storagepools       => $storagepools,
1079
            memoryquota        => $memoryquota+0,
1080
            storagequota       => $storagequota+0,
1081
            nodestoragequota   => $nodestoragequota+0,
1082
            vcpuquota          => $vcpuquota+0,
1083
            externalipquota    => $externalipquota+0,
1084
            rxquota            => $rxquota+0,
1085
            txquota            => $txquota+0,
1086
            tasks              => $tasks,
1087
            allowinternalapi   => $allowinternalapi || 1, # specify '--' to explicitly disallow
1088
            billto             => $ubillto,
1089
            dnsdomains         => $udnsdomains,
1090
            appstoreurl        => $uappstoreurl,
1091
            created            => $created,
1092
            modified           => $current_time,
1093
            action             => ""
1094
        };
1095
        my %uref = %{$register{$username}};
1096
        $uref{result} = "OK";
1097
        $uref{password} = "";
1098
        $uref{status} = ($uprivileges =~ /d/)?'disabled':'enabled';
1099
        $postreply = JSON::to_json(\%uref, { pretty => 1 });
1100
#        $postreply =~ s/""/"--"/g;
1101
        $postreply =~ s/null/""/g;
1102
#        $postreply =~ s/\x/ /g;
1103
    }
1104
    return $postreply;
1105
}
1106

    
1107
sub do_list {
1108
    my ($uuid, $action, $obj) = @_;
1109
    if ($help) {
1110
        return <<END
1111
GET::
1112
List users registered on this engine.
1113
END
1114
    }
1115
    my $userfilter;
1116
    my $usermatch;
1117
    my $propmatch;
1118
    if ($uripath =~ /users(\.cgi)?\/(\?|)(me|this)/) {
1119
        $usermatch = $user;
1120
        $propmatch = $4 if ($uripath =~ /users(\.cgi)?\/(\?|)(me|this)\/(.+)/);
1121
    } elsif ($uripath =~ /users(\.cgi)?\/(\?|)(username)/) {
1122
        $userfilter = $3 if ($uripath =~ /users(\.cgi)?\/\??username(:|=)(.+)/);
1123
        $userfilter = $1 if ($userfilter =~ /(.*)\*/);
1124
    } elsif ($uripath =~ /users(\.cgi)?\/(\S+)/) {
1125
        $usermatch = $2;
1126
        $propmatch = $4 if ($uripath =~ /users(\.cgi)?\/(\S+)\/(.+)/);
1127
    }
1128

    
1129
    my @regvalues = (sort {$a->{'id'} <=> $b->{'id'}} values %register); # Sort by id
1130
    my @curregvalues;
1131

    
1132
    foreach my $valref (@regvalues) {
1133
        my $reguser = $valref->{'username'};
1134
        if ($user eq $reguser || $isadmin) {
1135
            next if ($reguser eq 'irigo' || $reguser eq 'guest');
1136
            my %val = %{$valref}; # Deference and assign to new ass array, effectively cloning object
1137
            $val{'password'} = '';
1138
            $val{'status'} = ($val{'privileges'} =~ /d/)?'disabled':'enabled';
1139
            if ((!$userfilter && !$usermatch) || ($userfilter && $reguser =~ /$userfilter/) || $reguser eq $usermatch) {
1140
                push @curregvalues,\%val;
1141
            }
1142
        }
1143
    }
1144
    if ($action eq 'tablelist') {
1145
        my $t2 = Text::SimpleTable->new(14,32,24,10);
1146

    
1147
        $t2->row('username', 'fullname', 'lastlogin', 'privileges');
1148
        $t2->hr;
1149
        my $pattern = $options{m};
1150
        foreach $rowref (@curregvalues){
1151
            if ($pattern) {
1152
                my $rowtext = $rowref->{'username'} . " " . $rowref->{'fullname'} . " " . $rowref->{'lastlogin'}
1153
                               . " " .  $rowref->{'privileges'};
1154
                $rowtext .= " " . $rowref->{'mac'} if ($isadmin);
1155
                next unless ($rowtext =~ /$pattern/i);
1156
            }
1157
            $t2->row($rowref->{'username'}, $rowref->{'fullname'}||'--', localtime($rowref->{'lastlogin'})||'--',
1158
            $rowref->{'privileges'}||'--');
1159
        }
1160
        #$t2->row('common', '--', '--', '--');
1161
        #$t2->row('all', '--', '--', '--') if (index($privileges,"a")!=-1);
1162
        $postreply .= $t2->draw;
1163
    } elsif ($console) {
1164
        $postreply = Dumper(\@curregvalues);
1165
    } else {
1166
        my $json_text;
1167
        if ($propmatch) {
1168
            $json_text = JSON::to_json($curregvalues[0]->{$propmatch}, {allow_nonref=>1});
1169
        } else {
1170
            $json_text = JSON::to_json(\@curregvalues, {pretty=>1});
1171
        }
1172
        $json_text =~ s/"--"/""/g;
1173
        $json_text =~ s/null/""/g;
1174
#        $json_text =~ s/\x/ /g;
1175
        $postreply = qq|{"identifier": "username", "label": "username", "items": | unless ($usermatch || $action ne 'listusers');
1176
        $postreply .= $json_text;
1177
        $postreply .= "}\n" unless ($usermatch || $action ne 'listusers');
1178
    }
1179
    return $postreply;
1180
}
1181

    
1182
sub do_uuidlookup {
1183
    if ($help) {
1184
        return <<END
1185
GET:uuid:
1186
Simple action for looking up a username (uuid) or part of a username and returning the complete username.
1187
END
1188
    }
1189
    my $u = $options{u};
1190
    $u = $params{'uuid'} unless ($u || $u eq '0');
1191
    if ($u || $u eq '0') {
1192
        foreach my $uuid (keys %register) {
1193
            if ($uuid =~ /^$u/) {
1194
                return "$uuid\n" if ($uuid eq $user || index($privileges,"a")!=-1);
1195
            }
1196
        }
1197
    }
1198
}
1199

    
1200
sub do_uuidshow {
1201
    if ($help) {
1202
        return <<END
1203
GET:uuid:
1204
Simple action for showing a single user. Pass username as uuid.
1205
END
1206
    }
1207
    my $u = $options{u};
1208
    $u = $params{'uuid'} unless ($u || $u eq '0');
1209
    if ($u eq $user || index($privileges,"a")!=-1) {
1210
        foreach my $uuid (keys %register) {
1211
            if ($uuid =~ /^$u/) {
1212
                my %hash = %{$register{$uuid}};
1213
                delete $hash{'action'};
1214
                my $dump = to_json(\%hash, {pretty=>1});
1215
                $dump =~ s/undef/"--"/g;
1216
                return $dump;
1217
            }
1218
        }
1219
    }
1220
}
1221

    
1222
sub Restoreengine {
1223
    my ($uuid, $action, $obj) = @_;
1224
    if ($help) {
1225
        return <<END
1226
GET:restorefile:
1227
Restores this engine's configuration from "restorefile", which must be one of the paths listed in listenginebackups
1228
END
1229
    }
1230
    if (!$isadmin) {
1231
        $postreply = "Status=ERROR You must be an administrator in order to restore this engine";
1232
    } else {
1233
        my $tktcfg = ConfigReader::Simple->new($Stabile::auth_tkt_conf, [qw(TKTAuthSecret)]);
1234
        my $tktkey = $tktcfg->get('TKTAuthSecret') || '';
1235
        my $enginetkthash = Digest::SHA::sha512_hex($tktkey);
1236

    
1237
        my $restoredir = "/etc";
1238
        my $dbname = "steamregister";
1239
        my $restorefile = $obj->{'restorefile'};
1240

    
1241
        if ($restorefile && !($restorefile =~ /\//)) {
1242
            my $urifile = URI::Escape::uri_escape($restorefile);
1243
            my $uri = "https://www.stabile.io/irigo/engine.cgi";
1244
            my $cmd = qq|/usr/bin/curl -f --cookie -O -L -F action=getbackup -F restorefile=$urifile -F engineid=$engineid -F enginetkthash=$enginetkthash "$uri" > "/tmp/$restorefile"|;
1245
            my $res = `$cmd`;
1246
            if (-s "/tmp/$restorefile") {
1247
                $res .= `(mkdir $restoredir/stabile; cd $restoredir/stabile; /bin/tar -zxf "/tmp/$restorefile")`;
1248
                $res .= `/usr/bin/mysql -e "create database $dbname;"`;
1249
                $res .= `/usr/bin/mysql $dbname < $restoredir/stabile/steamregister.sql`;
1250
                $res .= `cp -b $restoredir/stabile/hosts.allow /etc/hosts.allow`;
1251
                $res .= `cp -b $restoredir/stabile/auth_tkt_cgi.conf /etc/apache2/conf.d/`;
1252
                $res .= `cp -b $restoredir/stabile/*.crt /etc/apache2/ssl/`;
1253
                $res .= `cp -b $restoredir/stabile/*.key /etc/apache2/ssl/`;
1254
                $res .= `cp -b $restoredir/stabile/mon.cf /etc/mon/`;
1255
                $res .= `service apache2 reload`;
1256

    
1257
                # Restore default node configuration
1258
                unless ( tie(%idreg,'Tie::DBI', Hash::Merge::merge({table=>'nodeidentities', key=>'identity'}, $Stabile::dbopts)) ) {return "Unable to access identity register"};
1259
                my $defaultpath = $idreg{'default'}->{'path'} . "/casper/filesystem.dir/etc/stabile/nodeconfig.cfg";
1260
                untie %idreg;
1261
                $res .=  `cp $restoredir/stabile/nodeconfig.cfg $defaultpath`;
1262
                $main::syslogit->($user, "info", "Engine configuration $restorefile restored from the registry");
1263
                $postreply .= "Status=OK Engine configuration $restorefile restored from the registry - reloading UI\n";
1264
            } else {
1265
                $postreply .= "Status=ERROR Restore failed, $restorefile not found...\n";
1266
            }
1267
        } else {
1268
            $postreply .= "Status=ERROR You must select a restore file\n";
1269
        }
1270
    }
1271
    return $postreply;
1272
}
1273

    
1274
# Print list of available actions on objects
1275
sub do_plainhelp {
1276
    my $res;
1277
    $res .= header('text/plain') unless $console;
1278
    $res .= <<END
1279
new [username="name", password="password"]
1280
* enable: Enables a disabled user
1281
* disable: Disables a user, disallowing login
1282
* remove: Deletes a user, leaving servers, images, networks etc. untouched
1283
* deleteentirely: Deletes a user and all the user's servers, images, networks etc. Warning: This destroys data
1284

    
1285
END
1286
;
1287
}
1288

    
1289
sub do_cleanbillingdata {
1290
    my ($uuid, $action, $obj) = @_;
1291
    if ($help) {
1292
        return <<END
1293
GET:year,dryrun,cleanup:
1294
Deletes billing from [year]. Default is current year-2. Set dryrun to do a test run. Set cleanup to remove invalid entries.
1295
END
1296
    }
1297
    return "Status=Error Not allowed\n" unless ($isadmin);
1298

    
1299
    my $y = $params{'year'} || ($year-2);
1300
    my $dryrun = $params{'dryrun'};
1301
    my $cleanup = $params{'cleanup'};
1302
    my $pattern = qq|like '%-$y-__'|;
1303
    if ($cleanup) {
1304
        $pattern = qq|not like '%-____-__'|;
1305
        $y = '';
1306
    }
1307

    
1308
    unless ( tie(%bnetworksreg,'Tie::DBI', Hash::Merge::merge({table=>'billing_networks', key=>'useridtime'}, $Stabile::dbopts)) ) {return "Status=Error Unable to access billing register"};
1309
    my @bkeys = (tied %bnetworksreg)->select_where("useridtime $pattern");
1310
    $postreply .= "Status=OK -- this is only a test run ---\n" if ($dryrun);
1311
    $postreply .= "Status=OK Cleaning " . scalar @bkeys . " $y network rows\n";
1312
    foreach my $bkey (@bkeys) {
1313
        $postreply .= "Status=OK removing $bnetworksreg{$bkey}->{useridtime}\n";
1314
        delete($bnetworksreg{$bkey}) unless ($dryrun);
1315
    }
1316
    untie(%bnetworksreg);
1317

    
1318
    unless ( tie(%bimagesreg,'Tie::DBI', Hash::Merge::merge({table=>'billing_images', key=>'userstoragepooltime'}, $Stabile::dbopts)) ) {return "Status=Error Unable to access billing register"};
1319
    my @bkeys = (tied %bimagesreg)->select_where("userstoragepooltime $pattern");
1320
    $postreply .= "Status=OK Cleaning " . scalar @bkeys . " $y image rows\n";
1321
    foreach my $bkey (@bkeys) {
1322
        $postreply .= "Status=OK removing $bimagesreg{$bkey}->{userstoragepooltime}\n";
1323
        delete($bimagesreg{$bkey}) unless ($dryrun);
1324
    }
1325
    untie(%bimagesreg);
1326

    
1327
    unless ( tie(%bserversreg,'Tie::DBI', Hash::Merge::merge({table=>'billing_domains', key=>'usernodetime'}, $Stabile::dbopts)) ) {return "Status=Error Unable to access billing register"};
1328
    my @bkeys = (tied %bserversreg)->select_where("usernodetime $pattern");
1329
    $postreply .= "Status=OK Cleaning " . scalar @bkeys . " $y server rows\n";
1330
    foreach my $bkey (@bkeys) {
1331
        $postreply .= "Status=OK removing $bserversreg{$bkey}->{usernodetime}\n";
1332
        delete($bserversreg{$bkey}) unless ($dryrun);
1333
    }
1334
    untie(%bserversreg);
1335

    
1336
    return $postreply;
1337

    
1338
}
1339

    
1340
sub collectBillingData {
1341
    my ( $curuuid, $buser, $bmonth, $byear, $showcost ) = @_;
1342

    
1343
    my $vcpu=0;
1344
    my $rx = 0;
1345
    my $tx = 0;
1346
    my $vcpuavg = 0;
1347
    my $memory = 0;
1348
    my $memoryavg = 0;
1349
    my $backupsize = 0;
1350
    my $backupsizeavg = 0;
1351
    my $nodevirtualsize = 0;
1352
    my $nodevirtualsizeavg = 0;
1353
    my $virtualsize = 0;
1354
    my $virtualsizeavg = 0;
1355
    my $externalip = 0;
1356
    my $externalipavg = 0;
1357

    
1358
    my $prevmonth = $bmonth-1;
1359
    my $prevyear = $byear;
1360
    if ($prevmonth == 0) {$prevmonth=12; $prevyear--;};
1361
    $prevmonth = substr("0" . $prevmonth, -2);
1362
    my $prev_rx = 0;
1363
    my $prev_tx = 0;
1364
    # List pricing for a single system/server
1365
    if ($curuuid) {
1366
        unless ( tie(%domreg,'Tie::DBI', Hash::Merge::merge({table=>'domains'}, $Stabile::dbopts)) ) {return "Unable to access domains register"};
1367
        unless ( tie(%imagereg,'Tie::DBI', Hash::Merge::merge({table=>'images',key=>'path'}, $Stabile::dbopts)) ) {return "Unable to access images register"};
1368
        unless ( tie(%networkreg,'Tie::DBI', Hash::Merge::merge({table=>'networks'}, $Stabile::dbopts)) ) {return "Unable to access networks register"};
1369

    
1370
        my @domains;
1371
        my $isserver = 1 if ($domreg{$curuuid});
1372
        if ($isserver) {
1373
            @domains = $domreg{$curuuid};
1374
        } else {
1375
            @domains = values %domreg;
1376
        }
1377
        foreach my $valref (@domains) {
1378
            if ($valref->{'system'} eq $curuuid || $isserver) {
1379
                $memory += $valref->{'memory'};
1380
                $vcpu += $valref->{'vcpu'};
1381
                my $image = $valref->{'image'};
1382
                my $storagepool;
1383
                if ($imagereg{$image}) {
1384
                    $storagepool = $imagereg{$image}->{'storagepool'};
1385
                    if ($storagepool == -1) {
1386
                        $nodevirtualsize += $imagereg{$image}->{'virtualsize'};
1387
                    } else {
1388
                        $virtualsize += $imagereg{$image}->{'virtualsize'};
1389
                    }
1390
                    $backupsize += $imagereg{$image}->{'backupsize'};
1391
                }
1392
                $image = $valref->{'image2'};
1393
                if ($imagereg{$image}) {
1394
                    $storagepool = $imagereg{$image}->{'storagepool'};
1395
                    if ($storagepool == -1) {
1396
                        $nodevirtualsize += $imagereg{$image}->{'virtualsize'};
1397
                    } else {
1398
                        $virtualsize += $imagereg{$image}->{'virtualsize'};
1399
                    }
1400
                    $backupsize += $imagereg{$image}->{'backupsize'};
1401
                }
1402
                my $networkuuid = $valref->{'networkuuid1'};
1403
                my $networktype = $networkreg{$networkuuid}->{'type'};
1404
                $externalip++ if ($networktype eq 'externalip'|| $networktype eq 'ipmapping');
1405
                $networkuuid = $valref->{'networkuuid2'};
1406
                if ($networkreg{$networkuuid}) {
1407
                    $networktype = $networkreg{$networkuuid}->{'type'};
1408
                    $externalip++ if ($networktype eq 'externalip'|| $networktype eq 'ipmapping');
1409
                }
1410
            }
1411
        }
1412
        untie %domreg;
1413
        untie %imagereg;
1414
        untie %networkreg;
1415

    
1416
    # List pricing for all servers
1417
    } else {
1418
        # Network billing
1419
        unless ( tie(%bnetworksreg,'Tie::DBI', Hash::Merge::merge({table=>'billing_networks', key=>'useridtime'}, $Stabile::dbopts)) ) {return "Unable to access billing register"};
1420
        unless ( tie(%networkreg,'Tie::DBI', Hash::Merge::merge({table=>'networks'}, $Stabile::dbopts)) ) {return "Unable to access networks register"};
1421

    
1422
        # Build list of the user's network id's
1423
        my %usernetworks;
1424
        my @nkeys = (tied %networkreg)->select_where("user = '$buser'");
1425
        foreach $network (@nkeys) {
1426
            my $id = $networkreg{$network}->{'id'};
1427
            $usernetworks{$id} = $id unless ($usernetworks{$id} || $id==0 || $id==1);
1428
        }
1429
        untie %networkreg;
1430

    
1431
        foreach $id (keys %usernetworks) {
1432
            my $networkobj = $bnetworksreg{"$buser-$id-$byear-$bmonth"};
1433
            my $prevnetworkobj = $bnetworksreg{"$buser-$id-$prevyear-$prevmonth"};
1434
            $externalip += $networkobj->{'externalip'};
1435
            $externalipavg += $networkobj->{'externalipavg'};
1436
            $rx += $networkobj->{'rx'};
1437
            $tx += $networkobj->{'tx'};
1438
            $prev_rx += $prevnetworkobj->{'rx'};
1439
            $prev_tx += $prevnetworkobj->{'tx'};
1440
        }
1441
        untie %bnetworksreg;
1442

    
1443
    # Image billing
1444

    
1445
        unless ( tie(%bimagesreg,'Tie::DBI', Hash::Merge::merge({table=>'billing_images', key=>'userstoragepooltime'}, $Stabile::dbopts)) ) {return "Unable to access billing register"};
1446

    
1447
        # Build list of the users storage pools
1448
        my $storagepools = $Stabile::config->get('STORAGE_POOLS_DEFAULTS') || "0";
1449
        my $upools = $register{$buser}->{'storagepools'}; # Prioritized list of users storage pools as numbers, e.g. "0,2,1"
1450
        $storagepools = $upools if ($upools && $upools ne '--');
1451
        my @spl = split(/,\s*/, $storagepools);
1452
        my $bimageobj = $bimagesreg{"$buser--1-$byear-$bmonth"};
1453
        $backupsize = $bimageobj->{'backupsize'}+0;
1454
        $nodevirtualsize = $bimageobj->{'virtualsize'}+0;
1455
        $backupsizeavg = $bimageobj->{'backupsizeavg'}+0;
1456
        $nodevirtualsizeavg = $bimageobj->{'virtualsizeavg'}+0;
1457
        foreach $pool (@spl) {
1458
            $bimageobj = $bimagesreg{"$buser-$pool-$byear-$bmonth"};
1459
            $virtualsize += $bimageobj->{'virtualsize'};
1460
            $backupsize += $bimageobj->{'backupsize'};
1461
            $virtualsizeavg += $bimageobj->{'virtualsizeavg'};
1462
            $backupsizeavg += $bimageobj->{'backupsizeavg'};
1463
        }
1464
        untie %bimagesreg;
1465

    
1466
    # Server billing
1467

    
1468
        unless ( tie(%bserversreg,'Tie::DBI', Hash::Merge::merge({table=>'billing_domains', key=>'usernodetime'}, $Stabile::dbopts)) ) {return "Unable to access billing register"};
1469
        unless ( tie(%nodereg,'Tie::DBI', Hash::Merge::merge({table=>'nodes', key=>'mac'}, $Stabile::dbopts)) ) {return "Unable to access billing register"};
1470

    
1471
        my @usernodes = keys %nodereg;
1472
        untie %nodereg;
1473

    
1474
        my @nodebills;
1475
        foreach $mac (@usernodes) {
1476
            my $bserverobj = $bserversreg{"$buser-$mac-$byear-$bmonth"};
1477
            $vcpu += $bserverobj->{'vcpu'};
1478
            $memory += $bserverobj->{'memory'};
1479
            $vcpuavg += $bserverobj->{'vcpuavg'};
1480
            $memoryavg += $bserverobj->{'memoryavg'};
1481
        }
1482
        untie %bserversreg;
1483
    }
1484

    
1485
    my $uservcpuprice = 0+ $register{$user}->{'vcpuprice'};
1486
    my $usermemoryprice = 0+ $register{$user}->{'memoryprice'};
1487
    my $userstorageprice = 0+ $register{$user}->{'storageprice'};
1488
    my $usernodestorageprice = 0+ $register{$user}->{'nodestorageprice'};
1489
    my $userexternalipprice = 0+ $register{$user}->{'externalipprice'};
1490

    
1491
    $vcpuprice = $uservcpuprice || $Stabile::config->get('VCPU_PRICE') + 0;
1492
    $memoryprice = $usermemoryprice || $Stabile::config->get('MEMORY_PRICE') + 0;
1493
    $storageprice = $userstorageprice || $Stabile::config->get('STORAGE_PRICE') + 0;
1494
    $nodestorageprice = $usernodestorageprice || $Stabile::config->get('NODESTORAGE_PRICE') + 0;
1495
    $externalipprice = $userexternalipprice || $Stabile::config->get('EXTERNALIP_PRICE') + 0;
1496

    
1497
    my $memorygb = int(0.5 + 100*$memory/1024)/100;
1498
    my $virtualsizegb = int(0.5 + 100*$virtualsize/1024/1024/1024)/100;
1499
    my $nodevirtualsizegb = int(0.5 + 100*$nodevirtualsize/1024/1024/1024)/100;
1500
    my $backupsizegb = int(0.5 + 100*$backupsize/1024/1024/1024)/100;
1501

    
1502
    my $totalprice = int(0.5 + 100*($vcpu*$vcpuprice + $memorygb*$memoryprice + $virtualsizegb*$storageprice
1503
     + $nodevirtualsizegb*$nodestorageprice + $backupsizegb*$storageprice + $externalip*$externalipprice)) /100;
1504

    
1505
    my $memoryavggb = int(0.5 + 100*$memoryavg/1024)/100;
1506
    my $virtualsizeavggb = int(0.5 + 100*$virtualsizeavg/1024/1024/1024)/100;
1507
    my $nodevirtualsizeavggb = int(0.5 + 100*$nodevirtualsizeavg/1024/1024/1024)/100;
1508
    my $backupsizeavggb = int(0.5 + 100*$backupsizeavg/1024/1024/1024)/100;
1509

    
1510
    my $monfac = 1;
1511
    if ($bmonth == $month) {
1512
        # Find 00:00 of first day of month - http://www.perlmonks.org/?node_id=97120
1513
        my $fstamp = POSIX::mktime(0,0,0,1,$mon,$year-1900,0,0,-1);
1514
        my $lstamp = POSIX::mktime(0,0,0,1,$mon+1,$year-1900,0,0,-1);
1515
        $monfac = ($current_time-$fstamp)/($lstamp-$fstamp);
1516
    }
1517

    
1518
    my $totalpriceavg = int(0.5 + 100*$monfac * ($vcpuavg*$vcpuprice + $memoryavggb*$memoryprice + $virtualsizeavggb*$storageprice
1519
     + $nodevirtualsizeavggb*$nodestorageprice + $backupsizeavggb*$storageprice + $externalipavg*$externalipprice)) /100;
1520

    
1521
    $prev_rx = 0 if ($prev_rx>$rx); # Something is fishy
1522
    $prev_tx = 0 if ($prev_tx>$tx);
1523
    my $rxgb = int(0.5 + 100*($rx-$prev_rx)/1024**3)/100;
1524
    my $txgb = int(0.5 + 100*($tx-$prev_tx)/1024**3)/100;
1525

    
1526
    my %stats;
1527
    $stats{'virtualsize'} = $virtualsizegb;
1528
    $stats{'backupsize'} = $backupsizegb;
1529
    $stats{'externalip'} = $externalip;
1530
    $stats{'memory'} = $memorygb;
1531
    $stats{'month'} = $bmonth;
1532
    $stats{'nodevirtualsize'} = $nodevirtualsizegb;
1533
    $stats{'rx'} = $rxgb;
1534
    $stats{'tx'} = $txgb;
1535
    $stats{'username'} = $buser;
1536
    $stats{'vcpu'} = $vcpu;
1537
    $stats{'year'} = $byear;
1538
    $stats{'totalcost'} = "$cur $totalprice" if ($showcost);
1539
    $stats{'curtotal'} = $totalprice if ($showcost);
1540

    
1541
    if (!$curuuid) {
1542
        $stats{'virtualsizeavg'} = $virtualsizeavggb;
1543
        $stats{'backupsizeavg'} = $backupsizeavggb;
1544
        $stats{'memoryavg'} = $memoryavggb;
1545
        $stats{'nodevirtualsizeavg'} = $nodevirtualsizeavggb;
1546
        $stats{'vcpuavg'} = int(0.5 + 100*$vcpuavg)/100;
1547
        $stats{'externalipavg'} = int(0.5 + 100*$externalipavg)/100;
1548
        $stats{'totalcostavg'} = "$cur $totalpriceavg" if ($showcost);
1549
    }
1550
    return %stats;
1551
}
1552

    
1553
sub do_resetpassword {
1554
    my ($uuid, $action, $obj) = @_;
1555
    if ($help) {
1556
        return <<END
1557
GET:username:
1558
Sends an email to a user with a link to reset his password. The user must have a valid email address.
1559
END
1560
    }
1561
    my $username = $obj->{'username'} || $user;
1562
    if ($register{$username} && ($username eq $user || $isadmin)) {
1563
        my $mailaddrs = $register{$username}->{'email'};
1564
        $mailaddrs = $username if (!$mailaddrs && $username =~ /\@/);
1565
        if ($mailaddrs) {
1566
            require (dirname(__FILE__)) . "/../auth/Apache/AuthTkt.pm";
1567
            my $tktname = 'auth_' . substr($engineid, 0, 8);
1568
            my $at = Apache::AuthTkt->new(conf => $ENV{MOD_AUTH_TKT_CONF});
1569
            my $tkt = $at->ticket(uid => $username, digest_type => 'SHA512', tokens => '', debug => 0);
1570
#            my $valid = $at->valid_ticket($tkt);
1571

    
1572
            my $mailhtml = <<END;
1573
<!DOCTYPE html
1574
	PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
1575
	 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1576
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-US" xml:lang="en-US">
1577
	<head>
1578
		<title>Password reset</title>
1579
		<meta http-equiv="Pragma" content="no-cache" />
1580
		<link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css" />
1581
		<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
1582
	</head>
1583
	<body class="tundra">
1584
		<div>
1585
			<div class="well" style="margin:20px;">
1586
				<h3 style="color: #e74c3c!important; margin-bottom:30px;">You requested a password reset at $enginename</h3>
1587
					To log in and set a new password, please click <a href="$baseurl/auth/autologin?$tktname=$tkt\&back=#chpwd">here</a>.<br>
1588
    				<div>Thanks,<br>your friendly infrastructure services</div>
1589
				</div>
1590
			</div>
1591
		</div>
1592
	</body>
1593
</html>
1594
END
1595
            ;
1596
            my $msg = MIME::Lite->new(
1597
                From     => "$enginename",
1598
                To       => $mailaddrs,
1599
                Type     => 'multipart/alternative',
1600
                Subject  => "Password reset on $enginename",
1601
            );
1602
            # my $att_text = MIME::Lite->new(
1603
            #     Type     => 'text',
1604
            #     Data     => $mailtext,
1605
            #     Encoding => 'quoted-printable',
1606
            # );
1607
            # $att_text->attr('content-type' => 'text/plain; charset=UTF-8');
1608
            # $msg->attach($att_text);
1609
            my $att_html = MIME::Lite->new(
1610
                Type     => 'text',
1611
                Data     => $mailhtml,
1612
                Encoding => 'quoted-printable',
1613
            );
1614
            $att_html->attr('content-type' => 'text/html; charset=UTF-8');
1615
            $msg->attach($att_html);
1616
            my $res = $msg->send;
1617
            $postreply = "Status=OK Password reset email sent to $mailaddrs\n";
1618
        } else {
1619
            $postreply = "Status=Error user does not have a registered email address\n";
1620
        }
1621
    } else {
1622
        $postreply = "Status=Error invalid data submitted\n";
1623
    }
1624
    return $postreply;
1625
}
1626

    
1627
sub do_changepassword {
1628
    my ($uuid, $action, $obj) = @_;
1629
    if ($help) {
1630
        return <<END
1631
GET:username,password:
1632
Changes the password for a user.
1633
END
1634
    }
1635
    my $username = $obj->{'username'} || $user;
1636
    my $password = $obj->{'password'};
1637
    if ($password && $register{$username} && ($username eq $user || $isadmin)) {
1638
        $MAXLEN = 20;
1639
        var $msg = IsBadPassword($password);
1640
        if ($msg) {
1641
            $postreply = "Status=Error $msg - please choose a stronger password\n";
1642
        } else {
1643
            $password = Digest::SHA::sha512_base64($password);
1644
            $register{$username}->{'password'} = $password;
1645
            $postreply = "Status=OK Password changed for $username\n";
1646
        }
1647
    } else {
1648
        $postreply = "Status=Error invalid data submitted\n";
1649
    }
1650
    return $postreply;
1651
}
1652

    
1653
sub do_remove {
1654
    my ($uuid, $action, $obj) = @_;
1655
    if ($help) {
1656
        return <<END
1657
GET:username:
1658
Removes a user.
1659
END
1660
    }
1661
    my $username = $obj->{'username'};
1662
    $postreply = remove($username);
1663
    return $postreply;
1664
}
1665

    
1666
sub remove {
1667
    my $username = shift;
1668
    if (!$isadmin && ($user ne $engineuser)) {
1669
        $postreply .= "Status=ERROR You are not allowed to remove user $username\n";
1670
    } elsif ($register{$username}) {
1671
        delete $register{$username};
1672
        tied(%register)->commit;
1673
        `/bin/rm /tmp/$username~*.tasks`;
1674
        unlink "../cgi/ui_update/$username~ui_update.cgi" if (-e "../cgi/ui_update/$username~ui_update.cgi");
1675
        $main::syslogit->($user, "info", "Deleted user $username from db");
1676
        if ($console) {
1677
            $postreply .= "Status=OK Deleted user $username\n";
1678
        } else {
1679
#            $main::updateUI->({ tab => 'users', type=>'update', user=>$user});
1680
            return "{}";
1681
        }
1682
        return $postreply;
1683
    } else {
1684
        $postreply .= "Status=ERROR No such user: $username\n";
1685
    }
1686
}
1687

    
1688
# Update engine users with users received from the registry
1689
sub updateEngineUsers {
1690
    my ($json_text) = @_;
1691
    return unless ($isadmin || ($user eq $engineuser));
1692
    my $res;
1693
    my $json = JSON->new;
1694
    $json->utf8([1]);
1695
    my $json_obj = $json->decode($json_text);
1696
    my @ulist = @$json_obj;
1697
    my @efields = qw(password
1698
    	address city company country email fullname phone
1699
        state zip alertemail opemail opfullname opphone billto
1700
        memoryquota storagequota vcpuquota externalipquota rxquota txquota nodestoragequota
1701
        accounts accountsprivileges privileges modified dnsdomains appstoreurl totpsecret
1702
    );
1703
    my $ures;
1704
    my $ucount = 0;
1705
    foreach my $u (@ulist) {
1706
        my $username = $u->{'username'};
1707
        if (!$register{$username} && $u->{'password'}) {
1708
            $register{$username} = {
1709
                username => $username,
1710
                password => $u->{'password'},
1711
                allowinternalapi => 1
1712
            };
1713
            $ures .= " *";
1714
        }
1715
        next unless ($register{$username});
1716
        next if ($register{$username}->{'modified'} && $register{$username}->{'modified'} > $u->{'modified'});
1717
        foreach my $efield (@efields) {
1718
            if ($efield eq 'privileges') {
1719
                $u->{$efield} =~ tr/adnrpu//cd; # filter out non-valid privileges
1720
            }
1721
            if (defined $u->{$efield}) {
1722
                $u->{$efield} += 0 if ($efield =~ /(quota|price)$/);
1723
                $register{$username}->{$efield} = $u->{$efield};
1724
            }
1725
            delete $u->{$efield} if (defined $u->{$efield} && $u->{$efield} eq '' && $efield ne 'password')
1726
        }
1727
        $ures .= "$username ($u->{'fullname'}), ";
1728
        $ucount++;
1729
        my $uid = `id -u irigo-$username`; chomp $uid;
1730
        if (!$uid) { # Check user has system account for disk quotas
1731
            $main::syslogit->($user, "info", "Adding system user $username");
1732
            `/usr/sbin/useradd -m "irigo-$username"`;
1733
            `echo "[User]\nSystemAccount=true" > /var/lib/AccountsService/users/irigo-$username`; # Don't show in login screen
1734
        }
1735

    
1736
    }
1737
    $ures = substr($res, 0, -2) . "\n";
1738
    $res .= "Status=OK Received $ucount updates on " .(scalar(@ulist)). " registry users\n";
1739
    return $res;
1740
}
1741

    
1742
sub sendEngineUser {
1743
    my ($username) = @_;
1744
    if ($enginelinked) {
1745
    # Send engine user to the registry
1746
        require LWP::Simple;
1747
        my $browser = LWP::UserAgent->new;
1748
        $browser->agent('stabile/1.0b');
1749
        $browser->protocols_allowed( [ 'http','https'] );
1750

    
1751
        my $tktcfg = ConfigReader::Simple->new($Stabile::auth_tkt_conf, [qw(TKTAuthSecret)]);
1752
        my $tktkey = $tktcfg->get('TKTAuthSecret') || '';
1753
        my $tkthash = Digest::SHA::sha512_hex($tktkey);
1754
        my $json = '[' . JSON::to_json(\%{$register{$username}}) . ']';
1755
        $json =~ s/null/""/g;
1756
#        $json = uri_escape_utf8($json);
1757
        $json = URI::Escape::uri_escape($json);
1758
        my $posturl = "https://www.stabile.io/irigo/engine.cgi?action=update";
1759
        my $postreq = ();
1760
        $postreq->{'POSTDATA'} = $json;
1761
        $postreq->{'engineid'} = $engineid;
1762
        $postreq->{'enginetkthash'} = $tkthash;
1763

    
1764
#        my $req = HTTP::Request->new(POST => $posturl);
1765
#        $req->content_type("application/json; charset='utf8'");
1766
#        $req->content($postreq);
1767

    
1768
        $content = $browser->post($posturl, $postreq)->content();
1769
#        $content = $browser->post($posturl, 'Content-type' => 'text/plain;charset=utf-8', Content => $postreq)->content();
1770
#        $content = $browser->request($req)->content();
1771
        my $fullname = $register{$username}->{'fullname'};
1772
        $fullname = Encode::decode('utf8', $fullname);
1773
        return "Updated $fullname in registry\n";
1774
    }
1775
}
(9-9/9)