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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1288
END
1289
;
1290
}
1291

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

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

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

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

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

    
1339
    return $postreply;
1340

    
1341
}
1342

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

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

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

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

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

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

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

    
1446
    # Image billing
1447

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

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

    
1469
    # Server billing
1470

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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