Project

General

Profile

Download (78.9 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
        $engine_h{"enforceiolimits"} = $enforceiolimits;
249

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
407
             my $res;
408
             if ($params{'format'} eq 'html') {
409
                 $postreply .= header("text/html");
410
                 $res .= qq[<tr><th>Ressource</th><th>Quantity</th><th class="$irigo_cost">Cost/month</th><th>Quota</th></tr>];
411
                 $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>];
412
                 $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>];
413
                 $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>];
414
                 $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>];
415
                 $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>];
416
                 $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>];
417
                 if (!$uuid) {
418
                     $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>];
419
                     $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>];
420
                 }
421

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

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

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

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

    
520
        $postreq->{'engineid'} = $engineid;
521
        $postreq->{'enginetkthash'} = Digest::SHA::sha512_hex($tktkey);
522

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

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

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

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

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

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

    
570
        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`;
571
        if ($res =~ /OK: $backupname.tgz received/) {
572
            $postreply .= "Status=OK Engine configuration saved to the registry";
573
            $main::syslogit->($user, "info", "Engine configuration saved to the registry");
574
            unlink("/tmp/$backupname.tgz");
575
        } else {
576
            $postreply .= "Status=ERROR Problem backing configuration up to the registry\n$res\n";
577
        }
578
    }
579
    return $postreply;
580
}
581

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
914
            require "$Stabile::basedir/cgi/images.cgi";
915
            $Stabile::Images::console = 1;
916
            $postreply .= Stabile::Images::removeUserImages($uname);
917

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

    
925
            do_billengine(); # Send latest billing data to origo before removing user
926
            # Also remove billing data from previous months - these are assumed reported to origo for linked and billed engines
927
            `echo "delete from billing_domains where (usernodetime like '$uname-%') AND (not (usernodetime LIKE '$uname-%-$year-$month'));" | mysql steamregister`;
928
            `echo "delete from billing_images where (userstoragepooltime like '$uname-%') AND (not (userstoragepooltime LIKE '$uname-%-$year-$month'));" | mysql steamregister`;
929
            `echo "delete from billing_networks where (useridtime like '$uname-%') AND (not (useridtime LIKE '$uname-%-$year-$month'));" | mysql steamregister`;
930
        }
931
        $main::updateUI->({tab => 'users', type=>'update', user=>$user});
932

    
933
    } else {
934
        $postreply .= "Stream=ERROR Cannot delete user $username - you cannot delete administrators!\n";
935
        $reply = $postreply;
936
    }
937
    return $reply;
938
}
939

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

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

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

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

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

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

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

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

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

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

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

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

    
1241
        my $restoredir = "/etc";
1242
        my $dbname = "steamregister";
1243
        my $restorefile = $obj->{'restorefile'};
1244

    
1245
        if ($restorefile && !($restorefile =~ /\//)) {
1246
            my $urifile = URI::Escape::uri_escape($restorefile);
1247
            my $uri = "https://www.stabile.io/irigo/engine.cgi";
1248
            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"|;
1249
            my $res = `$cmd`;
1250
            if (-s "/tmp/$restorefile") {
1251
                $res .= `(mkdir $restoredir/stabile; cd $restoredir/stabile; /bin/tar -zxf "/tmp/$restorefile")`;
1252
                $res .= `/usr/bin/mysql -e "create database $dbname;"`;
1253
                $res .= `/usr/bin/mysql $dbname < $restoredir/stabile/steamregister.sql`;
1254
                $res .= `cp -b $restoredir/stabile/hosts.allow /etc/hosts.allow`;
1255
                $res .= `cp -b $restoredir/stabile/auth_tkt_cgi.conf /etc/apache2/conf.d/`;
1256
                $res .= `cp -b $restoredir/stabile/*.crt /etc/apache2/ssl/`;
1257
                $res .= `cp -b $restoredir/stabile/*.key /etc/apache2/ssl/`;
1258
                $res .= `cp -b $restoredir/stabile/mon.cf /etc/mon/`;
1259
                $res .= `service apache2 reload`;
1260

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

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

    
1289
END
1290
;
1291
}
1292

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

    
1303
    my $y = $params{'year'} || ($year-2);
1304
    my $dryrun = $params{'dryrun'};
1305
    my $cleanup = $params{'cleanup'};
1306
    my $pattern = qq|like '%-$y-__'|;
1307
    if ($cleanup) {
1308
        $pattern = qq|not like '%-____-__'|;
1309
        $y = '';
1310
    }
1311

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

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

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

    
1340
    return $postreply;
1341

    
1342
}
1343

    
1344
sub collectBillingData {
1345
    my ( $curuuid, $buser, $bmonth, $byear, $showcost ) = @_;
1346

    
1347
    my $vcpu=0;
1348
    my $rx = 0;
1349
    my $tx = 0;
1350
    my $vcpuavg = 0;
1351
    my $memory = 0;
1352
    my $memoryavg = 0;
1353
    my $backupsize = 0;
1354
    my $backupsizeavg = 0;
1355
    my $nodevirtualsize = 0;
1356
    my $nodevirtualsizeavg = 0;
1357
    my $virtualsize = 0;
1358
    my $virtualsizeavg = 0;
1359
    my $externalip = 0;
1360
    my $externalipavg = 0;
1361

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

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

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

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

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

    
1447
    # Image billing
1448

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

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

    
1470
    # Server billing
1471

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

    
1475
        my @usernodes = keys %nodereg;
1476
        untie %nodereg;
1477

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

    
1489
    my $uservcpuprice = 0+ $register{$user}->{'vcpuprice'};
1490
    my $usermemoryprice = 0+ $register{$user}->{'memoryprice'};
1491
    my $userstorageprice = 0+ $register{$user}->{'storageprice'};
1492
    my $usernodestorageprice = 0+ $register{$user}->{'nodestorageprice'};
1493
    my $userexternalipprice = 0+ $register{$user}->{'externalipprice'};
1494

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

    
1501
    my $memorygb = int(0.5 + 100*$memory/1024)/100;
1502
    my $virtualsizegb = int(0.5 + 100*$virtualsize/1024/1024/1024)/100;
1503
    my $nodevirtualsizegb = int(0.5 + 100*$nodevirtualsize/1024/1024/1024)/100;
1504
    my $backupsizegb = int(0.5 + 100*$backupsize/1024/1024/1024)/100;
1505

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

    
1509
    my $memoryavggb = int(0.5 + 100*$memoryavg/1024)/100;
1510
    my $virtualsizeavggb = int(0.5 + 100*$virtualsizeavg/1024/1024/1024)/100;
1511
    my $nodevirtualsizeavggb = int(0.5 + 100*$nodevirtualsizeavg/1024/1024/1024)/100;
1512
    my $backupsizeavggb = int(0.5 + 100*$backupsizeavg/1024/1024/1024)/100;
1513

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

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

    
1525
    $prev_rx = 0 if ($prev_rx>$rx); # Something is fishy
1526
    $prev_tx = 0 if ($prev_tx>$tx);
1527
    my $rxgb = int(0.5 + 100*($rx-$prev_rx)/1024**3)/100;
1528
    my $txgb = int(0.5 + 100*($tx-$prev_tx)/1024**3)/100;
1529

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

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

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

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

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

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

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

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

    
1740
    }
1741
    $ures = substr($res, 0, -2) . "\n";
1742
    $res .= "Status=OK Received $ucount updates on " .(scalar(@ulist)). " registry users\n";
1743
    return $res;
1744
}
1745

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

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

    
1768
#        my $req = HTTP::Request->new(POST => $posturl);
1769
#        $req->content_type("application/json; charset='utf8'");
1770
#        $req->content($postreq);
1771

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