1
|
#!/usr/bin/perl
|
2
|
|
3
|
# All rights reserved and Copyright (c) 2020 Origo Systems ApS.
|
4
|
# This file is provided with no warranty, and is subject to the terms and conditions defined in the license file LICENSE.md.
|
5
|
# The license file is part of this source code package and its content is also available at:
|
6
|
# https://www.origo.io/info/stabiledocs/licensing/stabile-open-source-license
|
7
|
|
8
|
# Clear up tainted environment
|
9
|
$ENV{PATH} = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin';
|
10
|
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
|
11
|
|
12
|
#use warnings FATAL => 'all';
|
13
|
use CGI::Carp qw(fatalsToBrowser);
|
14
|
use CGI qw(:standard);
|
15
|
use Getopt::Std;
|
16
|
use JSON;
|
17
|
use URI::Escape qw(uri_escape uri_unescape);
|
18
|
use Tie::DBI;
|
19
|
use Data::Dumper;
|
20
|
use Encode;
|
21
|
use Text::SimpleTable;
|
22
|
use ConfigReader::Simple;
|
23
|
use Sys::Syslog qw( :DEFAULT setlogsock);
|
24
|
use Digest::SHA qw(sha512_base64 sha512_hex);
|
25
|
use utf8;
|
26
|
use Hash::Merge qw( merge );
|
27
|
use Storable qw(freeze thaw);
|
28
|
use Gearman::Client;
|
29
|
use Proc::ProcessTable;
|
30
|
use HTTP::Async;
|
31
|
use HTTP::Request::Common;
|
32
|
use LWP::Simple qw(!head);
|
33
|
use Error::Simple;
|
34
|
|
35
|
our %options=();
|
36
|
# -a action -h help -f full list -p full update -u uuid -i image -m match pattern -k keywords -g args to gearman task
|
37
|
# -v verbose, include HTTP headers -s impersonate subaccount -t target [uuid or image] -c force console
|
38
|
Getopt::Std::getopts("a:hfpu:i:g:m:k:vs:t:c", \%options);
|
39
|
|
40
|
$Stabile::config = ConfigReader::Simple->new("/etc/stabile/config.cfg",
|
41
|
[qw(
|
42
|
AMT_PASSWD
|
43
|
DBI_PASSWD
|
44
|
DBI_USER
|
45
|
DO_DNS
|
46
|
DNS_DOMAIN
|
47
|
DO_XMPP
|
48
|
ENGINEID
|
49
|
ENGINENAME
|
50
|
ENGINE_DATA_NIC
|
51
|
ENGINE_LINKED
|
52
|
EXTERNAL_IP_RANGE_START
|
53
|
EXTERNAL_IP_RANGE_END
|
54
|
EXTERNAL_IP_QUOTA
|
55
|
EXTERNAL_NIC
|
56
|
EXTERNAL_SUBNET_SIZE
|
57
|
MEMORY_QUOTA
|
58
|
NODE_STORAGE_OVERCOMMISSION
|
59
|
NODESTORAGE_QUOTA
|
60
|
PROXY_GW
|
61
|
PROXY_IP
|
62
|
PROXY_IP_RANGE_END
|
63
|
PROXY_IP_RANGE_START
|
64
|
PROXY_SUBNET_SIZE
|
65
|
RDIFF-BACKUP_ENABLED
|
66
|
RDIFF-BACKUP_USERS
|
67
|
RX_QUOTA
|
68
|
SHOW_COST
|
69
|
STORAGE_BACKUPDIR
|
70
|
STORAGE_POOLS_ADDRESS_PATHS
|
71
|
STORAGE_POOLS_DEFAULTS
|
72
|
STORAGE_POOLS_LOCAL_PATHS
|
73
|
STORAGE_POOLS_NAMES
|
74
|
STORAGE_POOLS_RDIFF-BACKUP_ENABLED
|
75
|
STORAGE_QUOTA
|
76
|
Z_IMAGE_RETENTION
|
77
|
Z_BACKUP_RETENTION
|
78
|
TX_QUOTA
|
79
|
VCPU_QUOTA
|
80
|
VLAN_RANGE_START
|
81
|
VLAN_RANGE_END
|
82
|
VERSION
|
83
|
)]);
|
84
|
|
85
|
$dbiuser = $Stabile::config->get('DBI_USER') || "irigo";
|
86
|
$dbipasswd = $Stabile::config->get('DBI_PASSWD') || "";
|
87
|
$dnsdomain = $Stabile::config->get('DNS_DOMAIN') || "stabile.io";
|
88
|
$appstoreurl = $Stabile::config->get('APPSTORE_URL') || "https://www.origo.io/registry";
|
89
|
$appstores = $Stabile::config->get('APPSTORES') || "stabile.io"; # Used for publishing apps
|
90
|
$engineuser = $Stabile::config->get('ENGINEUSER') || "";
|
91
|
$imageretention = $Stabile::config->get('Z_IMAGE_RETENTION') || "";
|
92
|
$backupretention = $Stabile::config->get('Z_BACKUP_RETENTION') || "";
|
93
|
$enginelinked = $Stabile::config->get('ENGINE_LINKED') || "";
|
94
|
$downloadmasters = $Stabile::config->get('DOWNLOAD_MASTERS') || "";
|
95
|
$disablesnat = $Stabile::config->get('DISABLE_SNAT') || "";
|
96
|
our $engineid = $Stabile::config->get('ENGINEID') || "";
|
97
|
|
98
|
$Stabile::dbopts = {db=>'mysql:steamregister', key=>'uuid', autocommit=>0, CLOBBER=>2, user=>$dbiuser, password=>$dbipasswd};
|
99
|
$Stabile::auth_tkt_conf = "/etc/apache2/conf-available/auth_tkt_cgi.conf";
|
100
|
|
101
|
my $base = "/var/www/stabile";
|
102
|
$base = `cat /etc/stabile/basedir` if (-e "/etc/stabile/basedir");
|
103
|
chomp $base;
|
104
|
$base =~ /(.+)/; $base = $1; #untaint
|
105
|
$main::logfile = "/var/log/stabile/steam.log";
|
106
|
|
107
|
$current_time = time;
|
108
|
($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($current_time);
|
109
|
$year += 1900;
|
110
|
$month = substr("0" . ($mon+1), -2);
|
111
|
$pretty_time = sprintf "%4d-%02d-%02d@%02d:%02d:%02d",$year,$mon+1,$mday,$hour,$min,$sec;
|
112
|
|
113
|
if ($ENV{'HTTP_HOST'} && !($ENV{'HTTP_HOST'} =~ /^10\./) && $ENV{'HTTP_HOST'} ne 'localhost' && !($ENV{'HTTP_HOST'} =~ /^127/)) {
|
114
|
$baseurl = "https://$ENV{'HTTP_HOST'}/stabile";
|
115
|
`echo "$baseurl" > /tmp/baseurl` if ((! -e "/tmp/baseurl") && $baseurl);
|
116
|
} else {
|
117
|
if (!$baseurl && (-e "/tmp/baseurl" || -e "/etc/stabile/baseurl")) {
|
118
|
if (-e "/etc/stabile/baseurl") {
|
119
|
$baseurl = `cat /etc/stabile/baseurl`;
|
120
|
} else {
|
121
|
$baseurl = `cat /tmp/baseurl`;
|
122
|
chomp $baseurl;
|
123
|
`echo "$baseurl" >/etc/stabile/baseurl` unless (-e "/etc/stabile/baseurl");
|
124
|
}
|
125
|
}
|
126
|
}
|
127
|
if (!$baseurl) {
|
128
|
my $hostname = `hostname`; chomp $hostname;
|
129
|
$baseurl = "https://$hostname/stabile";
|
130
|
}
|
131
|
$baseurl = $1 if ($baseurl =~ /(.+)/); #untaint
|
132
|
|
133
|
$Stabile::basedir = "/var/www/stabile";
|
134
|
$Stabile::basedir = `cat /etc/stabile/basedir` if -e "/etc/stabile/basedir";
|
135
|
chomp $Stabile::basedir;
|
136
|
$Stabile::basedir = $1 if ($Stabile::basedir =~ /(.+)/); #untaint
|
137
|
|
138
|
$package = substr(lc __PACKAGE__, length "Stabile::");
|
139
|
$programname = "Stabile";
|
140
|
|
141
|
$sshcmd = qq|ssh -l irigo -i /var/www/.ssh/id_rsa_www -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no|;
|
142
|
|
143
|
$ENV{'REQUEST_METHOD'} = $ENV{'REQUEST_METHOD'} || 'GET';
|
144
|
|
145
|
preInit();
|
146
|
1;
|
147
|
|
148
|
$main::syslogit = sub {
|
149
|
my ($user, $p, $msg) = @_;
|
150
|
my $priority = ($p eq 'syslog')?'info':$p;
|
151
|
|
152
|
$current_time = time;
|
153
|
($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($current_time);
|
154
|
$year += 1900;
|
155
|
$month = substr("0" . ($mon+1), -2);
|
156
|
my $pretty_time = sprintf "%4d-%02d-%02d@%02d:%02d:%02d",$year,$mon+1,$mday,$hour,$min,$sec;
|
157
|
|
158
|
my $loguser = (!$tktuser || $tktuser eq $user)?"$user":"$user ($tktuser)";
|
159
|
if ($msg && $msg ne '') {
|
160
|
utf8::decode($msg);
|
161
|
unless (open(TEMP3, ">>$main::logfile")) {$posterror .= "Status=Error log file '$main::logfile' could not be written";}
|
162
|
$msg =~ /(.+)/; $msg = $1; #untaint
|
163
|
print TEMP3 $pretty_time, " : $loguser : $msg\n";
|
164
|
close(TEMP3);
|
165
|
}
|
166
|
return 0 unless ($priority =~ /err|debug/);
|
167
|
setlogsock('unix');
|
168
|
# $programname is assumed to be a global. Also log the PID
|
169
|
# and to CONSole if there's a problem. Use facility 'user'.
|
170
|
openlog($programname, 'pid,cons', 'user');
|
171
|
syslog($priority, "($loguser) $msg");
|
172
|
closelog();
|
173
|
return 1;
|
174
|
};
|
175
|
|
176
|
|
177
|
$main::postToOrigo = sub {
|
178
|
my ($engineid, $postaction, $postcontent, $postkey, $callback) = @_;
|
179
|
my $tktcfg = ConfigReader::Simple->new($Stabile::auth_tkt_conf, [qw(TKTAuthSecret)]);
|
180
|
my $tktkey = $tktcfg->get('TKTAuthSecret') || '';
|
181
|
my $ret;
|
182
|
|
183
|
if ($tktkey && $engineid) {
|
184
|
my $browser = LWP::UserAgent->new;
|
185
|
$browser->timeout(15);
|
186
|
$browser->agent('pressurecontrol/1.0b');
|
187
|
$browser->protocols_allowed( [ 'http','https'] );
|
188
|
|
189
|
my $postreq;
|
190
|
$postreq->{'engineid'} = $engineid;
|
191
|
$postreq->{'enginetkthash'} = sha512_hex($tktkey) if ($enginelinked);
|
192
|
$postreq->{'appuser'} = $user;
|
193
|
$postreq->{'callback'} .= $callback if ($callback);
|
194
|
$postkey = 'POSTDATA' unless ($postkey);
|
195
|
$postreq->{$postkey} = $postcontent;
|
196
|
my $posturl = "https://www.origo.io/irigo/engine.cgi?action=$postaction";
|
197
|
my $content = $browser->post($posturl, $postreq)->content();
|
198
|
my $ok = ($content =~ /OK: (.*)/i);
|
199
|
$ret .= $content;
|
200
|
} else {
|
201
|
$main::syslogit->('pressurecontrol', 'info', "Unable to get engine tktkey...");
|
202
|
$ret .= "Unable to get engine tktkey...";
|
203
|
}
|
204
|
return $ret;
|
205
|
};
|
206
|
|
207
|
$main::uploadToOrigo = sub {
|
208
|
my ($engineid, $filepath, $force) = @_;
|
209
|
my $tktcfg = ConfigReader::Simple->new($Stabile::auth_tkt_conf, [qw(TKTAuthSecret)]);
|
210
|
my $tktkey = $tktcfg->get('TKTAuthSecret') || '';
|
211
|
my $ret;
|
212
|
|
213
|
if (!$filepath || !(-e $filepath)) {
|
214
|
$ret = "Status=Error Invalid file path\n";
|
215
|
} elsif ($tktkey && $engineid) {
|
216
|
$HTTP::Request::Common::DYNAMIC_FILE_UPLOAD = 1;
|
217
|
my $browser = LWP::UserAgent->new;
|
218
|
$browser->timeout(15 * 60); # 15 min
|
219
|
$browser->agent('pressurecontrol/1.0b');
|
220
|
$browser->protocols_allowed( [ 'http','https'] );
|
221
|
my $fname = $1 if ($filepath =~ /.*\/(.+\.qcow2)$/);
|
222
|
return "Status=Error Invalid file\n" unless ($fname);
|
223
|
my $posturl = "https://www.origo.io/irigo/engine.cgi?action=uploadimage";
|
224
|
|
225
|
# -- using ->post
|
226
|
# my $postreq = [
|
227
|
# 'file' => [ $filepath ],
|
228
|
# 'filename' => $fname,
|
229
|
# 'engineid' => $engineid,
|
230
|
# 'enginetkthash' => sha512_hex($tktkey),
|
231
|
# 'appuser' => $user,
|
232
|
# 'force' => $force
|
233
|
# ];
|
234
|
# my $content = $browser->post($posturl, $postreq, 'Content_Type' => 'form-data')->content;
|
235
|
# $ret .= $content;
|
236
|
|
237
|
# -- using ->request
|
238
|
my $req = POST $posturl,
|
239
|
Content_Type => 'form-data',
|
240
|
Content => [
|
241
|
'file' => [ $filepath ],
|
242
|
'filename' => $fname,
|
243
|
'engineid' => $engineid,
|
244
|
'enginetkthash' => sha512_hex($tktkey),
|
245
|
'appuser' => $user,
|
246
|
'force' => $force
|
247
|
];
|
248
|
my $total;
|
249
|
my $callback = $req->content;
|
250
|
if (ref($callback) eq "CODE") {
|
251
|
my $size = $req->header('content-length');
|
252
|
my $counter = 0;
|
253
|
my $progress = '';
|
254
|
$req->content(
|
255
|
sub {
|
256
|
my $chunk = $callback->();
|
257
|
if ($chunk) {
|
258
|
my $length = length $chunk;
|
259
|
$total += $length;
|
260
|
if ($total / $size * 100 > $counter) {
|
261
|
$counter = 1+ int $total / $size * 100;
|
262
|
$progress .= "#";
|
263
|
`echo "$progress$counter" >> /tmp/upload-$fname`;
|
264
|
}
|
265
|
# printf "%+5d = %5.1f%%\n", $length, $total / $size * 100;
|
266
|
# printf "%5.1f%%\n", $total / $size * 100;
|
267
|
|
268
|
} else {
|
269
|
# print "Done\n";
|
270
|
}
|
271
|
$chunk;
|
272
|
}
|
273
|
);
|
274
|
my $resp = $browser->request($req)->content();
|
275
|
$ret .= $resp;
|
276
|
$ret .= "Status=OK $progress\n";
|
277
|
} else {
|
278
|
$ret .= "Status=Error Did not get a callback";
|
279
|
}
|
280
|
} else {
|
281
|
$ret .= "Status=Error Unable to get engine tktkey...";
|
282
|
}
|
283
|
return $ret;
|
284
|
};
|
285
|
|
286
|
$main::postAsyncToOrigo = sub {
|
287
|
my ($engineid, $postaction, $json_text) = @_;
|
288
|
my $tktcfg = ConfigReader::Simple->new($Stabile::auth_tkt_conf, [qw(TKTAuthSecret)]);
|
289
|
my $tktkey = $tktcfg->get('TKTAuthSecret') || '';
|
290
|
my $ret;
|
291
|
|
292
|
if ($tktkey && $engineid) {
|
293
|
my $browser = LWP::UserAgent->new;
|
294
|
$browser->timeout(15);
|
295
|
$browser->agent('pressurecontrol/1.0b');
|
296
|
$browser->protocols_allowed( [ 'http','https'] );
|
297
|
|
298
|
$ret .= "Posting $postaction to origo.io\n";
|
299
|
|
300
|
my $postreq;
|
301
|
$postreq->{'engineid'} = $engineid;
|
302
|
$postreq->{'enginetkthash'} = sha512_hex($tktkey);
|
303
|
$postreq->{'POSTDATA'} = $json_text;
|
304
|
# my $content = $browser->post("https://www.origo.io/irigo/engine.cgi?action=$postaction", $postreq)->content();
|
305
|
# my $ok = ($content =~ /OK: (.*)/i);
|
306
|
# $ret .= $content;
|
307
|
|
308
|
my $async = HTTP::Async->new;
|
309
|
my $post = POST "https://www.origo.io/irigo/engine.cgi?action=$postaction",
|
310
|
[ engineid => $engineid,
|
311
|
enginetkthash => sha512_hex($tktkey),
|
312
|
POSTDATA => $json_text
|
313
|
];
|
314
|
$async->add( $post );
|
315
|
# while ( my $response = $async->wait_for_next_response ) {
|
316
|
# $ret .= $response->decoded_content;
|
317
|
# }
|
318
|
} else {
|
319
|
$main::syslogit->('pressurecontrol', 'info', "Unable to get engine tktkey...");
|
320
|
$ret .= "Unable to get engine tktkey...";
|
321
|
}
|
322
|
return $ret;
|
323
|
};
|
324
|
|
325
|
$main::dnsCreate = sub {
|
326
|
my ($engineid, $name, $value, $type, $username) = @_;
|
327
|
my $res;
|
328
|
my $dnssubdomain = substr($engineid, 0, 8);
|
329
|
$type = uc $type;
|
330
|
$type || 'CNAME';
|
331
|
$name = $1 if ($name =~ /(.+)\.$dnsdomain/);
|
332
|
# $name =$1 if ($name =~ /(.+)\.$dnssubdomain/);
|
333
|
if ($type eq 'A') { # Look for initial registrations and format correctly
|
334
|
if (!$name && $value) { # If no name provided assume we are creating initial A-record
|
335
|
$name = $value;
|
336
|
} elsif ($name =~ /^(\d+\.\d+\.\d+\.\d+)/) { # Looks like an IP address - must be same as value
|
337
|
if ($1 eq $value) { # Keep some order in registrations
|
338
|
$name = "$value.$dnssubdomain"; # The way we format initial registrations
|
339
|
} else {
|
340
|
$name = '';
|
341
|
}
|
342
|
}
|
343
|
}
|
344
|
# Only allow creation of records corresponding to user's own networks when username is supplied
|
345
|
# When username is not supplied, we assume checking has been done
|
346
|
if ($username) {
|
347
|
my $checkval = $value;
|
348
|
# Remove any trailing period
|
349
|
$checkval = $1 if ($checkval =~ /(.+)\.$/);
|
350
|
if ($type eq 'TXT') {
|
351
|
$checkval = '';
|
352
|
} elsif ($type eq 'A') {
|
353
|
$checkval = $value;
|
354
|
} else {
|
355
|
$checkval = $1 if ($checkval =~ /(\d+\.\d+\.\d+\.\d+)\.$dnssubdomain\.$dnsdomain$/);
|
356
|
$checkval = $1 if ($checkval =~ /(\d+\.\d+\.\d+\.\d+)\.$dnsdomain$/);
|
357
|
$checkval = $1 if ($checkval =~ /(\d+\.\d+\.\d+\.\d+)$/);
|
358
|
}
|
359
|
if ($checkval) {
|
360
|
unless (tie %networkreg,'Tie::DBI', {
|
361
|
db=>'mysql:steamregister',
|
362
|
table=>'networks',
|
363
|
key=>'uuid',
|
364
|
autocommit=>0,
|
365
|
CLOBBER=>0,
|
366
|
user=>$dbiuser,
|
367
|
password=>$dbipasswd}) {throw Error::Simple("Error Register could not be accessed")};
|
368
|
my @regkeys = (tied %networkreg)->select_where("externalip = '$checkval'");
|
369
|
if (scalar @regkeys == 1) {
|
370
|
if ($register{$regkeys[0]}->{'user'} eq $username) {
|
371
|
; # OK
|
372
|
} else {
|
373
|
return qq|{"status": "Error", "message": "Invalid value $checkval, not allowed"}|;
|
374
|
}
|
375
|
} elsif (scalar @regkeys >1) {
|
376
|
return qq|{"status": "Error", "message": "Invalid value $checkval"}|;
|
377
|
}
|
378
|
untie %networkreg;
|
379
|
if ($type eq 'A') {
|
380
|
# $name = "$checkval.$dnssubdomain"; # Only allow this type of A-records...?
|
381
|
} else {
|
382
|
$value = "$checkval.$dnssubdomain";
|
383
|
}
|
384
|
}
|
385
|
}
|
386
|
|
387
|
if ($type ne 'MX' && $type ne 'TXT' && `host $name.$dnsdomain authns1.cabocomm.dk` =~ /has address/) {
|
388
|
return qq|{"status": "Error", "message": "$name is already registered"}|;
|
389
|
};
|
390
|
|
391
|
if ($enginelinked && $name && $value) {
|
392
|
require LWP::Simple;
|
393
|
my $browser = LWP::UserAgent->new;
|
394
|
$browser->agent('Stabile/1.0b');
|
395
|
$browser->protocols_allowed( [ 'http','https'] );
|
396
|
$browser->timeout(10);
|
397
|
my $tktcfg = ConfigReader::Simple->new($Stabile::auth_tkt_conf, [qw(TKTAuthSecret)]);
|
398
|
my $tktkey = $tktcfg->get('TKTAuthSecret') || '';
|
399
|
my $tkthash = sha512_hex($tktkey);
|
400
|
my $posturl = "https://www.origo.io/irigo/engine.cgi?action=dnscreate";
|
401
|
|
402
|
my $async = HTTP::Async->new;
|
403
|
my $post = POST $posturl,
|
404
|
[ engineid => $engineid,
|
405
|
enginetkthash => $tkthash,
|
406
|
name => $name,
|
407
|
domain => $dnsdomain,
|
408
|
value => $value,
|
409
|
type => $type,
|
410
|
username => $username || $user
|
411
|
];
|
412
|
# We fire this asynchronously and hope for the best. Waiting for an answer is just too erratic for now
|
413
|
$async->add( $post );
|
414
|
|
415
|
if ($username) {
|
416
|
my $response;
|
417
|
while ( $response = $async->wait_for_next_response ) {
|
418
|
$ret .= $response->decoded_content;
|
419
|
}
|
420
|
foreach my $line (split /\n/, $ret) {
|
421
|
$res .= $line unless ($line =~ /^\d/);
|
422
|
}
|
423
|
}
|
424
|
# $res =~ s/://g;
|
425
|
return "$res\n";
|
426
|
|
427
|
} else {
|
428
|
return qq|{"status": "Error", "message": "Problem creating dns record with data $name, $value.| . ($enginelinked?"":" Engine is not linked!") . qq|"}|;
|
429
|
}
|
430
|
};
|
431
|
|
432
|
$main::dnsDelete = sub {
|
433
|
my ($engineid, $name, $value, $type, $username) = @_;
|
434
|
my $dnssubdomain = substr($engineid, 0, 8);
|
435
|
$name = $1 if ($name =~ /(.+)\.$dnsdomain$/);
|
436
|
# $name =$1 if ($name =~ /(.+)\.$dnssubdomain/);
|
437
|
if ($name =~ /^(\d+\.\d+\.\d+\.\d+)$/) {
|
438
|
$name = "$1.$dnssubdomain";
|
439
|
$type = $type || 'A';
|
440
|
}
|
441
|
|
442
|
$main::syslogit->($user, "info", "Deleting DNS entry $type $name $dnsdomain");
|
443
|
if ($enginelinked && $name) {
|
444
|
require LWP::Simple;
|
445
|
my $browser = LWP::UserAgent->new;
|
446
|
$browser->agent('Stabile/1.0b');
|
447
|
$browser->protocols_allowed( [ 'http','https'] );
|
448
|
my $tktcfg = ConfigReader::Simple->new($Stabile::auth_tkt_conf, [qw(TKTAuthSecret)]);
|
449
|
my $tktkey = $tktcfg->get('TKTAuthSecret') || '';
|
450
|
my $tkthash = sha512_hex($tktkey);
|
451
|
my $posturl = "https://www.origo.io/irigo/engine.cgi?action=dnsdelete";
|
452
|
|
453
|
my $postreq = ();
|
454
|
$postreq->{'engineid'} = $engineid;
|
455
|
$postreq->{'enginetkthash'} = $tkthash;
|
456
|
$postreq->{'name'} = $name;
|
457
|
$postreq->{'value'} = $value;
|
458
|
$postreq->{'type'} = $type;
|
459
|
$postreq->{'username'} = $username || $user;
|
460
|
$postreq->{'domain'} = "$dnsdomain";
|
461
|
$content = $browser->post($posturl, $postreq)->content();
|
462
|
# $content =~ s/://g;
|
463
|
return $content;
|
464
|
} else {
|
465
|
return "ERROR Invalid data $name." . ($enginelinked?"":" Engine is not linked!") . "\n";
|
466
|
}
|
467
|
};
|
468
|
|
469
|
$main::dnsUpdate = sub {
|
470
|
my ($engineid, $name, $value, $type, $oldname, $oldvalue, $username) = @_;
|
471
|
$name = $1 if ($name =~ /(.+)\.$dnsdomain/);
|
472
|
$type = uc $type;
|
473
|
$type || 'CNAME';
|
474
|
|
475
|
# Only allow deletion of records corresponding to user's own networks when username is supplied
|
476
|
# When username is not supplied, we assume checking has been done
|
477
|
# Obsolete
|
478
|
# my $checkval;
|
479
|
# if ($username) {
|
480
|
# if ($name =~ /\d+\.\d+\.\d+\.\d+/) {
|
481
|
# $checkval = $name;
|
482
|
# } else {
|
483
|
# my $checkname = $name;
|
484
|
# # Remove trailing period
|
485
|
# $checkname = $1 if ($checkname =~ /(.+)\.$/);
|
486
|
# $checkname = "$checkname.$dnsdomain" unless ($checkname =~ /(.+)\.$dnsdomain$/);
|
487
|
# $checkval = $1 if (`host $checkname authns1.cabocomm.dk` =~ /has address (\d+\.\d+\.\d+\.\d+)/);
|
488
|
# return "ERROR Invalid value $checkname\n" unless ($checkval);
|
489
|
# }
|
490
|
#
|
491
|
# unless (tie %networkreg,'Tie::DBI', {
|
492
|
# db=>'mysql:steamregister',
|
493
|
# table=>'networks',
|
494
|
# key=>'uuid',
|
495
|
# autocommit=>0,
|
496
|
# CLOBBER=>0,
|
497
|
# user=>$dbiuser,
|
498
|
# password=>$dbipasswd}) {throw Error::Simple("Error Register could not be accessed")};
|
499
|
# my @regkeys = (tied %networkreg)->select_where("externalip = '$checkval' OR internalip = '$checkval'");
|
500
|
# if ($isadmin || (scalar @regkeys == 1 && $register{$regkeys[0]}->{'user'} eq $username)) {
|
501
|
# ; # OK
|
502
|
# } else {
|
503
|
# return "ERROR Invalid user for $checkval, not allowed\n";
|
504
|
# }
|
505
|
# untie %networkreg;
|
506
|
# }
|
507
|
|
508
|
$main::syslogit->($user, "info", "Updating DNS entries for $name $dnsdomain");
|
509
|
if ($enginelinked && $name) {
|
510
|
require LWP::Simple;
|
511
|
my $browser = LWP::UserAgent->new;
|
512
|
$browser->agent('Stabile/1.0b');
|
513
|
$browser->protocols_allowed( [ 'http','https'] );
|
514
|
my $tktcfg = ConfigReader::Simple->new($Stabile::auth_tkt_conf, [qw(TKTAuthSecret)]);
|
515
|
my $tktkey = $tktcfg->get('TKTAuthSecret') || '';
|
516
|
my $tkthash = sha512_hex($tktkey);
|
517
|
my $posturl = "https://www.origo.io/irigo/engine.cgi?action=dnsupdate";
|
518
|
|
519
|
my $postreq = ();
|
520
|
$postreq->{'engineid'} = $engineid;
|
521
|
$postreq->{'enginetkthash'} = $tkthash;
|
522
|
$postreq->{'name'} = $name;
|
523
|
$postreq->{'value'} = $value;
|
524
|
$postreq->{'type'} = $type;
|
525
|
$postreq->{'oldname'} = $oldname if ($oldname);
|
526
|
$postreq->{'oldvalue'} = $oldvalue if ($oldvalue);
|
527
|
$postreq->{'username'} = $username || $user;
|
528
|
$postreq->{'domain'} = $dnsdomain;
|
529
|
$content = $browser->post($posturl, $postreq)->content();
|
530
|
return $content;
|
531
|
} else {
|
532
|
return "ERROR Invalid data $name." . ($enginelinked?"":" Engine is not linked!") . "\n";
|
533
|
}
|
534
|
};
|
535
|
|
536
|
$main::dnsList = sub {
|
537
|
my ($engineid, $username, $domain) = @_;
|
538
|
if ($enginelinked) {
|
539
|
require LWP::Simple;
|
540
|
my $browser = LWP::UserAgent->new;
|
541
|
$browser->agent('Stabile/1.0b');
|
542
|
$browser->protocols_allowed( [ 'http','https'] );
|
543
|
my $tktcfg = ConfigReader::Simple->new($Stabile::auth_tkt_conf, [qw(TKTAuthSecret)]);
|
544
|
my $tktkey = $tktcfg->get('TKTAuthSecret') || '';
|
545
|
my $tkthash = sha512_hex($tktkey);
|
546
|
my $posturl = "https://www.origo.io/irigo/engine.cgi?action=dnslist";
|
547
|
$domain = $domain || $dnsdomain;
|
548
|
|
549
|
my $postreq = ();
|
550
|
$postreq->{'engineid'} = $engineid;
|
551
|
$postreq->{'enginetkthash'} = $tkthash;
|
552
|
$postreq->{'domain'} = $domain;
|
553
|
$postreq->{'username'} = $username || $user;
|
554
|
$content = $browser->post($posturl, $postreq)->content();
|
555
|
# $content =~ s/://g;
|
556
|
return $content;
|
557
|
} else {
|
558
|
return "ERROR Engine is not linked!\n";
|
559
|
}
|
560
|
};
|
561
|
|
562
|
$main::dnsClean = sub {
|
563
|
my ($engineid, $username) = @_;
|
564
|
if ($enginelinked) {
|
565
|
require LWP::Simple;
|
566
|
my $browser = LWP::UserAgent->new;
|
567
|
$browser->agent('Stabile/1.0b');
|
568
|
$browser->protocols_allowed( [ 'http','https'] );
|
569
|
my $tktcfg = ConfigReader::Simple->new($Stabile::auth_tkt_conf, [qw(TKTAuthSecret)]);
|
570
|
my $tktkey = $tktcfg->get('TKTAuthSecret') || '';
|
571
|
my $tkthash = sha512_hex($tktkey);
|
572
|
my $posturl = "https://www.origo.io/irigo/engine.cgi?action=dnsclean";
|
573
|
my $postreq = ();
|
574
|
$postreq->{'engineid'} = $engineid;
|
575
|
$postreq->{'enginetkthash'} = $tkthash;
|
576
|
$postreq->{'domain'} = $dnsdomain;
|
577
|
$content = $browser->post($posturl, $postreq)->content();
|
578
|
$content =~ s/://g;
|
579
|
return $content;
|
580
|
} else {
|
581
|
return "ERROR Engine is not linked!\n";
|
582
|
}
|
583
|
};
|
584
|
|
585
|
$main::xmppSend = sub {
|
586
|
my ($to, $msg, $engineid, $sysuuid) = @_;
|
587
|
$engineid = `cat /etc/stabile/config.cfg | sed -n -e 's/^ENGINEID=//p'` unless ($engineid);
|
588
|
my $doxmpp = `cat /etc/stabile/config.cfg | sed -n -e 's/^DO_XMPP=//p'`;
|
589
|
if (!$doxmpp) {
|
590
|
return "INFO: DO_XMPP not enabled in config\n";
|
591
|
|
592
|
} elsif ($to && $msg) {
|
593
|
my $xdom;
|
594
|
$xdom = $1 if ($to =~ /\@(.+)$/);
|
595
|
if ($xdom && `host -t SRV _xmpp-server._tcp.$xdom` !~ /NXDOMAIN/) {
|
596
|
require LWP::Simple;
|
597
|
my $browser = LWP::UserAgent->new;
|
598
|
$browser->agent('Stabile/1.0b');
|
599
|
$browser->protocols_allowed( [ 'http','https'] );
|
600
|
$browser->timeout(10);
|
601
|
my $tktcfg = ConfigReader::Simple->new($Stabile::auth_tkt_conf, [qw(TKTAuthSecret)]);
|
602
|
my $tktkey = $tktcfg->get('TKTAuthSecret') || '';
|
603
|
my $tkthash = sha512_hex($tktkey);
|
604
|
my $posturl = "https://www.origo.io/irigo/engine.cgi?action=xmppsend";
|
605
|
|
606
|
my $async = HTTP::Async->new;
|
607
|
my $post = POST $posturl,
|
608
|
[ engineid => $engineid,
|
609
|
enginetkthash => $tkthash,
|
610
|
sysuuid => $sysuuid,
|
611
|
to => $to,
|
612
|
msg => $msg
|
613
|
];
|
614
|
$async->add( $post );
|
615
|
|
616
|
#my $postreq = ();
|
617
|
#$postreq->{'engineid'} = $engineid;
|
618
|
#$postreq->{'enginetkthash'} = $tkthash;
|
619
|
#$postreq->{'to'} = $to;
|
620
|
#$postreq->{'msg'} = $msg;
|
621
|
#$content = $browser->post($posturl, $postreq)->content();
|
622
|
|
623
|
return "Status=OK Sent xmpp message to $to\n";
|
624
|
} else {
|
625
|
return "Status=ERROR XMPP srv records not found for domain \"$xdom\"\n";
|
626
|
}
|
627
|
|
628
|
} else {
|
629
|
return "Status=ERROR Invalid xmpp data $to, $msg\n";
|
630
|
}
|
631
|
};
|
632
|
|
633
|
# Enumerate and return network interfaces
|
634
|
$main::getNics = sub {
|
635
|
my $internalnic = $Stabile::config->get('ENGINE_DATA_NIC');
|
636
|
my $externalnic = $Stabile::config->get('EXTERNAL_NIC');
|
637
|
if (!$externalnic) {
|
638
|
my $droute = `ip route show default`;
|
639
|
$externalnic = $1 if ($droute =~ /default via .+ dev (.+) proto/);
|
640
|
}
|
641
|
my @nics = ();
|
642
|
if (!$externalnic || !$internalnic) {
|
643
|
my $niclist = `ifconfig | grep flags= | sed -n -e 's/: .*//p'`;
|
644
|
if (-e "/mnt/stabile/tftp/bionic") { # If a piston root exists, assume we will be providing boot services over secondary NIC even if it has no link
|
645
|
$niclist = `ifconfig -a | grep flags= | sed -n -e 's/: .*//p'`;
|
646
|
}
|
647
|
# my $niclist = `netstat -in`;
|
648
|
push @nics, $externalnic if ($externalnic);
|
649
|
foreach my $line (split("\n", $niclist)) {
|
650
|
if ($line =~ /^(\w+)$/) {
|
651
|
my $nic = $1;
|
652
|
push(@nics, $nic) if ($nic ne 'lo' && $nic ne $externalnic && !($nic=~/^virbr/) && !($nic=~/^docker/) && !($nic=~/^br/) && !($nic=~/^vnet/) && !($nic=~/^Name/) && !($nic=~/^Kernel/) && !($nic=~/^Iface/) && !($nic=~/(\.|\:)/));
|
653
|
}
|
654
|
}
|
655
|
}
|
656
|
$externalnic = $externalnic || $nics[0];
|
657
|
$internalnic = $internalnic || $nics[1] || $externalnic;
|
658
|
return ($internalnic, $externalnic);
|
659
|
};
|
660
|
|
661
|
$main::updateUI = sub {
|
662
|
my @parslist = @_;
|
663
|
my $newtasks;
|
664
|
my $tab;
|
665
|
my $duser;
|
666
|
foreach my $pars (@parslist) {
|
667
|
my $type = $pars->{type};
|
668
|
my $duuid = $pars->{uuid};
|
669
|
my $domuuid = $pars->{domuuid};
|
670
|
my $dstatus = $pars->{status};
|
671
|
my $message = $pars->{message};
|
672
|
$message =~ s/"/\\"/g;
|
673
|
$message =~ s/'/\\'/g;
|
674
|
my $newpath = $pars->{newpath};
|
675
|
my $displayip = $pars->{displayip};
|
676
|
my $displayport = $pars->{displayport};
|
677
|
my $name = $pars->{name};
|
678
|
my $master = $pars->{master};
|
679
|
my $mac = $pars->{mac};
|
680
|
my $macname = $pars->{macname};
|
681
|
my $progress = $pars->{progress};
|
682
|
my $title = $pars->{title};
|
683
|
my $managementlink = $pars->{managementlink};
|
684
|
my $backup = $pars->{backup};
|
685
|
my $download = $pars->{download};
|
686
|
my $size = $pars->{size};
|
687
|
my $sender = $pars->{sender};
|
688
|
my $path = $pars->{path};
|
689
|
my $snap1 = $pars->{snap1};
|
690
|
my $username = $pars->{username};
|
691
|
|
692
|
$tab = $pars->{tab};
|
693
|
$duser = $pars->{user};
|
694
|
$duser = "irigo" if ($duser eq "--");
|
695
|
$tab = $tab || substr(lc __PACKAGE__, 9);
|
696
|
$type = $type || ($message?'message':'update');
|
697
|
$sender = $sender || "stabile:$package";
|
698
|
|
699
|
if ($package eq 'users' && $pars->{'uuid'}) {
|
700
|
my %u = %{$register{$pars->{'uuid'}}};
|
701
|
delete $u{'password'};
|
702
|
$u{'user'} = $duser;
|
703
|
$u{'type'} = 'update';
|
704
|
$u{'status'} = ($u{'privileges'} =~ /d/)?'disabled':'enabled';
|
705
|
$u{'tab'} = $package;
|
706
|
$u{'timestamp'} = $current_time;
|
707
|
$newtasks .= to_json(\%u) . ", ";
|
708
|
} else {
|
709
|
$newtasks .= "{\"type\":\"$type\",\"tab\":\"$tab\",\"timestamp\":$current_time" .
|
710
|
($duuid?",\"uuid\":\"$duuid\"":"") .
|
711
|
($domuuid?",\"domuuid\":\"$domuuid\"":"") .
|
712
|
($duser?",\"user\":\"$duser\"":"") .
|
713
|
($dstatus?",\"status\":\"$dstatus\"":"") .
|
714
|
($message?",\"message\":\"$message\"":"") .
|
715
|
($newpath?",\"path\":\"$newpath\"":"") .
|
716
|
($displayip?",\"displayip\":\"$displayip\"":"") .
|
717
|
($displayport?",\"displayport\":\"$displayport\"":"") .
|
718
|
($name?",\"name\":\"$name\"":"") .
|
719
|
($backup?",\"backup\":\"$backup\"":"") .
|
720
|
($download?",\"download\":\"$download\"":"") .
|
721
|
($size?",\"size\":\"$size\"":"") .
|
722
|
($mac?",\"mac\":\"$mac\"":"") .
|
723
|
($macname?",\"macname\":\"$macname\"":"") .
|
724
|
($progress?",\"progress\":$progress":"") . # This must be a number between 0 and 100
|
725
|
($title?",\"title\":\"$title\"":"") .
|
726
|
($managementlink?",\"managementlink\":\"$managementlink\"":"") .
|
727
|
($master?",\"master\":\"$master\"":"") .
|
728
|
($snap1?",\"snap1\":\"$snap1\"":"") .
|
729
|
($username?",\"username\":\"$username\"":"") .
|
730
|
($path?",\"path\":\"$path\"":"") .
|
731
|
",\"sender\":\"$sender\"}, ";
|
732
|
}
|
733
|
}
|
734
|
$newtasks = $1 if ($newtasks =~ /(.+)/); #untaint
|
735
|
my $res;
|
736
|
eval {
|
737
|
opendir my($dh), '/tmp' or die "Couldn't open '/tmp': $!";
|
738
|
my @files;
|
739
|
if ($tab eq 'nodes' || $duser eq 'irigo') {
|
740
|
# write tasks to all admin user's session task pipes
|
741
|
@files = grep { /.*~A-.*\.tasks$/ } readdir $dh;
|
742
|
} else {
|
743
|
# write tasks to all the user's session task pipes
|
744
|
@files = grep { /^$duser~.*\.tasks$/ } readdir $dh;
|
745
|
}
|
746
|
closedir $dh;
|
747
|
my $t = new Proc::ProcessTable;
|
748
|
my @ptable = @{$t->table};
|
749
|
my @pfiles;
|
750
|
my $cmnds;
|
751
|
foreach my $f (@files) {
|
752
|
# my $n = `pgrep -fc "$f"`;
|
753
|
# chomp $n;
|
754
|
foreach my $p ( @ptable ){
|
755
|
my $pcmd = $p->cmndline;
|
756
|
$cmnds .= $pcmd . "\n" if ($pcmd =~ /tmp/);
|
757
|
if ($pcmd =~ /\/tmp\/$f/) { # Only include pipes with active listeners
|
758
|
push @pfiles, "/tmp/$f";
|
759
|
last;
|
760
|
}
|
761
|
}
|
762
|
};
|
763
|
my $tasksfiles = join(' ', @pfiles);
|
764
|
$tasksfiles = $1 if ($tasksfiles =~ /(.+)/); #untaint
|
765
|
# Write to users named pipes if user is logged in and session file found
|
766
|
if ($tasksfiles) {
|
767
|
$res = `/bin/echo \'$newtasks\' | /usr/bin/tee $tasksfiles \&`;
|
768
|
} else {
|
769
|
# If session file not found, append to orphan tasks file wait a sec and reload
|
770
|
$res = `/bin/echo \'$newtasks\' >> /tmp/$duser.tasks`;
|
771
|
$res .= `chown www-data:www-data /tmp/$duser.tasks`;
|
772
|
# sleep 1;
|
773
|
eval {`/usr/bin/pkill -HUP -f ui_update`; 1;} or do {;};
|
774
|
# `echo "duh: $duser" >> /tmp/duh`;
|
775
|
}
|
776
|
# eval {`/usr/bin/pkill -HUP -f $duser~ui_update`; 1;} or do {;};
|
777
|
} or do {$e=1; $res .= "ERROR Problem writing to tasks pipe $@\n";};
|
778
|
return 1;
|
779
|
};
|
780
|
|
781
|
sub action {
|
782
|
my ($target, $action, $obj) = @_;
|
783
|
my $res;
|
784
|
my $func = ucfirst $action;
|
785
|
# If a function named $action (with first letter uppercased) exists, call it and return the result
|
786
|
if (defined &{$func}) {
|
787
|
$res .= &{$func}($target, $action, $obj);
|
788
|
}
|
789
|
return $res;
|
790
|
}
|
791
|
|
792
|
sub privileged_action {
|
793
|
my ($target, $action, $obj) = @_;
|
794
|
return "Status=ERROR Your account does not have the necessary privileges\n" if ($isreadonly);
|
795
|
return action($target, $action) if ($help);
|
796
|
my $res;
|
797
|
$obj = {} unless ($obj);
|
798
|
$obj->{'console'} = 1 if ($console || $options{c});
|
799
|
$obj->{'baseurl'} = $baseurl if ($baseurl);
|
800
|
my $client = Gearman::Client->new;
|
801
|
$client->job_servers('127.0.0.1:4730');
|
802
|
# Gearman server will try to call a method named "do_gear_$action"
|
803
|
$res = $client->do_task(steamexec => freeze({package=>$package, tktuser=>$tktuser, user=>$user, target=>$target, action=>$action, args=>$obj}));
|
804
|
$res = ${ $res };
|
805
|
return $res;
|
806
|
}
|
807
|
|
808
|
sub privileged_action_async {
|
809
|
my ($target, $action, $obj) = @_;
|
810
|
return "Status=ERROR Your account does not have the necessary privileges\n" if ($isreadonly);
|
811
|
return action($target, $action) if ($help);
|
812
|
my $client = Gearman::Client->new;
|
813
|
$client->job_servers('127.0.0.1:4730');
|
814
|
my $tasks = $client->new_task_set;
|
815
|
$obj = {} unless ($obj);
|
816
|
$obj->{'console'} = 1 if ($console || $options{c});
|
817
|
# Gearman server will try to call a method named "do_gear_$action"
|
818
|
if (scalar(keys %{$obj}) > 2 ) {
|
819
|
my $handle = $tasks->add_task(steamexec => freeze({package=>$package, tktuser=>$tktuser, user=>$user, target=>$target, action=>$action, args=>$obj}));
|
820
|
} else {
|
821
|
my $handle = $tasks->add_task(steamexec => freeze({package=>$package, tktuser=>$tktuser, user=>$user, target=>$target, action=>$action}));
|
822
|
}
|
823
|
my $regtarget = $register{$target};
|
824
|
my $imgregtarget = $imagereg{$target};
|
825
|
$uistatus = $regtarget->{status} || "$action".'ing';
|
826
|
$uistatus = 'cloning' if ($action eq 'clone');
|
827
|
$uistatus = 'snapshotting' if ($action eq 'snapshot');
|
828
|
$uistatus = 'unsnapping' if ($action eq 'unsnap');
|
829
|
$uistatus = 'mastering' if ($action eq 'master');
|
830
|
$uistatus = 'unmastering' if ($action eq 'unmaster');
|
831
|
$uistatus = 'backingup' if ($action eq 'backup');
|
832
|
$uistatus = 'restoring' if ($action eq 'restore');
|
833
|
$uistatus = 'saving' if ($action eq 'save');
|
834
|
$uistatus = 'venting' if ($action eq 'releasepressure');
|
835
|
my $name = $regtarget->{name} || $imgregtarget->{name};
|
836
|
if ($action eq 'save') {
|
837
|
if ($package eq 'images') {
|
838
|
if ($obj->{status} eq 'new') {
|
839
|
$obj->{status} = 'unused';
|
840
|
}
|
841
|
elsif ($obj->{regstoragepool} ne $obj->{storagepool}) {
|
842
|
$obj->{'status'} = $uistatus = 'moving';
|
843
|
}
|
844
|
}
|
845
|
$postreply = to_json($obj, {pretty=>1});
|
846
|
$postreply = encode('utf8', $postreply);
|
847
|
$postreply =~ s/""/"--"/g;
|
848
|
$postreply =~ s/null/"--"/g;
|
849
|
$postreply =~ s/"notes" {0,1}: {0,1}"--"/"notes":""/g;
|
850
|
$postreply =~ s/"installable" {0,1}: {0,1}"(true|false)"/"installable":$1/g;
|
851
|
return $postreply;
|
852
|
} else {
|
853
|
return "Status=$uistatus OK $action $name (deferred)\n";
|
854
|
}
|
855
|
}
|
856
|
|
857
|
sub do_gear_action {
|
858
|
my ($target, $action ,$obj) = @_;
|
859
|
$target = encode("iso-8859-1", $target); # MySQL uses Latin1 as default charset
|
860
|
$action = $1 if ($action =~ /gear_(.+)/);
|
861
|
my $res;
|
862
|
return "This only works with elevated privileges\n" if ($>);
|
863
|
if ($register{$target}
|
864
|
|| $action =~ /all$|save|^monitors|^packages|^changemonitoremail|^buildsystem|^removesystem|^updateaccountinfo|^updateengineinfo|^removeusersystems|^removeuserimages/
|
865
|
|| $action =~ /^updateamtinfo|^updatedownloads|^releasepressure|linkmaster$|activate$|engine$|^syncusers|^deletesystem|^getserverbackups|^listserverbackups|^fullstats/
|
866
|
|| $action =~ /^zbackup|^updateallbtimes|^initializestorage|^liststoragedevices|^getbackupdevice|^getimagesdevice|^listbackupdevices|^listimagesdevices/
|
867
|
|| $action =~ /^setstoragedevice|^updateui|configurecgroups|backup|sync_backup/
|
868
|
|| ($action eq 'remove' && $package eq 'images' && $target =~ /\.master\.qcow2$/) # We allow removing master images by name only
|
869
|
) {
|
870
|
my $func = ucfirst $action;
|
871
|
# If a function named $action (with first letter uppercased) exists, call it and return the result
|
872
|
if (defined &{$func}) {
|
873
|
if ($obj) {
|
874
|
$console = $obj->{'console'} if ($obj->{'console'});
|
875
|
$target = $obj->{uuid} if (!$target && $obj->{uuid}); # backwards compat with apps calling removesystem
|
876
|
$res .= &{$func}($target, $action, $obj);
|
877
|
} else {
|
878
|
$res .= &{$func}($target, $action);
|
879
|
}
|
880
|
} else {
|
881
|
$res .= "Status=ERROR Unable to $action $target - function not found in $package\n";
|
882
|
}
|
883
|
} else {
|
884
|
$res .= "Status=ERROR Unable to $action $target - target not found in $package\n";
|
885
|
}
|
886
|
return $res;
|
887
|
}
|
888
|
|
889
|
sub preInit {
|
890
|
# Set global vars: $user, $tktuser, $curuuid and if applicable: $curdomuuid, $cursysuuid, $curimg
|
891
|
# Identify and validate user, read user prefs from DB
|
892
|
unless ( tie(%userreg,'Tie::DBI', Hash::Merge::merge({table=>'users', key=>'username'}, $Stabile::dbopts)) ) {throw Error::Simple("Status=Error User register could not be accessed")};
|
893
|
|
894
|
$user = $user || $Stabile::user || $ENV{'REMOTE_USER'};
|
895
|
$user = 'irigo' if ($package eq 'steamexec');
|
896
|
$remoteip = $ENV{'REMOTE_ADDR'};
|
897
|
# If request is coming from a running server from an internal ip, identify user requesting access
|
898
|
if (!$user && $remoteip && $remoteip =~ /^10\.\d+\.\d+\.\d+/) {
|
899
|
unless ( tie(%networkreg,'Tie::DBI', Hash::Merge::merge({table=>'networks', CLOBBER=>3}, $Stabile::dbopts)) ) {throw Error::Simple("Status=Error Network register could not be accessed")};
|
900
|
unless ( tie(%domreg,'Tie::DBI', Hash::Merge::merge({table=>'domains', CLOBBER=>3}, $Stabile::dbopts)) ) {throw Error::Simple("Status=Error Domain register could not be accessed")};
|
901
|
my @regkeys = (tied %networkreg)->select_where("internalip = '$remoteip'");
|
902
|
foreach my $k (@regkeys) {
|
903
|
my $network = $networkreg{$k};
|
904
|
my @domregkeys = (tied %domreg)->select_where("networkuuid1 = '$network->{uuid}'");
|
905
|
my $dom = $domreg{$network->{'domains'}} || $domreg{$domregkeys[0]}; # Sometimes domains is lost in network - compensate
|
906
|
# Request is coming from a running server from an internal ip - accept
|
907
|
if ($network->{'internalip'} eq $remoteip) {
|
908
|
$user = $network->{'user'};
|
909
|
# my $dom = $domreg{$network->{'domains'}};
|
910
|
if ($package eq 'networks') {
|
911
|
$curuuid = $network->{'uuid'};
|
912
|
$curdomuuid = $network->{'domains'};
|
913
|
$cursysuuid = $dom->{'system'};
|
914
|
} elsif ($package eq 'images') {
|
915
|
$curimg = $dom->{'image'} unless ($curimg);
|
916
|
} elsif ($package eq 'systems') {
|
917
|
$curuuid = $dom->{'system'} || $dom->{'uuid'} unless ($curuuid);
|
918
|
$cursysuuid = $dom->{'system'};
|
919
|
$curdomuuid = $dom->{'uuid'};
|
920
|
} elsif ($package eq 'servers') {
|
921
|
$curuuid = $dom->{'uuid'} unless ($curuuid);
|
922
|
$cursysuuid = $dom->{'system'};
|
923
|
}
|
924
|
if (!$userreg{$user}->{'allowinternalapi'}) {
|
925
|
$user = ''; # Internal API access is not enabled, disallow access
|
926
|
}
|
927
|
last;
|
928
|
}
|
929
|
}
|
930
|
untie %networkreg;
|
931
|
untie %domreg;
|
932
|
} else { # Check authorized referers to mitigate CSRF attacks. If no referer in ENV we let it pass to allow API access.
|
933
|
if (-e "/etc/stabile/basereferers"
|
934
|
&& $ENV{HTTP_REFERER}
|
935
|
) {
|
936
|
my $basereferers = `cat /etc/stabile/basereferers`;
|
937
|
chomp $basereferers;
|
938
|
my @baserefs = split(/\s+/, $basereferers);
|
939
|
my $match = 0;
|
940
|
foreach my $ref (@baserefs) {
|
941
|
if ($ENV{HTTP_REFERER} =~ /$ref/) {
|
942
|
$match = 1;
|
943
|
last;
|
944
|
}
|
945
|
}
|
946
|
$user = '' unless ($match);
|
947
|
}
|
948
|
}
|
949
|
$user = $1 if $user =~ /(.+)/; #untaint
|
950
|
$tktuser = $user;
|
951
|
$Stabile::tktuser = $tktuser;
|
952
|
|
953
|
# Initalize CGI
|
954
|
$Stabile::q = new CGI;
|
955
|
|
956
|
# Load params
|
957
|
%params = $Stabile::q->Vars;
|
958
|
$uripath = URI::Escape::uri_unescape($ENV{'REQUEST_URI'});
|
959
|
if ($options{s}) {
|
960
|
$account = $options{s};
|
961
|
} else {
|
962
|
$account = $Stabile::q->cookie('steamaccount');
|
963
|
}
|
964
|
$user = 'guest' if (!$user && $params{'action'} eq 'help');
|
965
|
die "No active user. Please authenticate or provide user through REMOTE_USER environment variable." unless ($user);
|
966
|
|
967
|
my $u = $userreg{$user};
|
968
|
my @accounts = split(/,\s*/, $u->{'accounts'}) if ($u->{'accounts'});
|
969
|
my @accountsprivs = split(/,\s*/, $u->{'accountsprivileges'}) if ($u->{'accountsprivileges'});
|
970
|
for my $i (0 .. $#accounts)
|
971
|
{ $ahash{$accounts[$i]} = $accountsprivs[$i] || 'r'; }
|
972
|
|
973
|
$privileges = '';
|
974
|
# User is requesting access to another account - check privs
|
975
|
if ($account && $account ne $user) {
|
976
|
if ($ahash{$account}) {
|
977
|
$user = $account;
|
978
|
$main::account = $account;
|
979
|
# Only allow users whose base account is admin to get admin privs
|
980
|
$ahash{$account} =~ s/a// unless ($userreg{$tktuser}->{'privileges'} =~ /a/);
|
981
|
$privileges = $ahash{$account};
|
982
|
$u = $userreg{$account};
|
983
|
}
|
984
|
}
|
985
|
|
986
|
$Stabile::user = $user;
|
987
|
|
988
|
$defaultmemoryquota = $Stabile::config->get('MEMORY_QUOTA') + 0;
|
989
|
$defaultstoragequota = $Stabile::config->get('STORAGE_QUOTA') + 0;
|
990
|
$defaultnodestoragequota = $Stabile::config->get('NODESTORAGE_QUOTA') + 0;
|
991
|
$defaultvcpuquota = $Stabile::config->get('VCPU_QUOTA') + 0;
|
992
|
$defaultexternalipquota = $Stabile::config->get('EXTERNAL_IP_QUOTA') + 0;
|
993
|
$defaultrxquota = $Stabile::config->get('RX_QUOTA') + 0;
|
994
|
$defaulttxquota = $Stabile::config->get('TX_QUOTA') + 0;
|
995
|
|
996
|
# Read quotas and privileges from db
|
997
|
$Stabile::userstoragequota = 0+ $u->{'storagequota'} if ($u->{'storagequota'});
|
998
|
$Stabile::usernodestoragequota = 0+ $u->{'nodestoragequota'} if ($u->{'storagequota'});
|
999
|
$usermemoryquota = 0+ $u->{'memoryquota'} if ($u->{'memoryquota'});
|
1000
|
$uservcpuquota = 0+ $u->{'vcpuquota'} if ($u->{'vcpuquota'});
|
1001
|
$Stabile::userexternalipquota = 0+ $u->{'externalipquota'} if ($u->{'externalipquota'});
|
1002
|
$Stabile::userrxquota = 0+ $u->{'rxquota'} if ( $u->{'rxquota'});
|
1003
|
$Stabile::usertxquota = 0+ $u->{'txquota'} if ($u->{'txquota'});
|
1004
|
|
1005
|
$billto = $u->{'billto'};
|
1006
|
$Stabile::userprivileges = $u->{'privileges'};
|
1007
|
$privileges = $Stabile::userprivileges if (!$privileges && $Stabile::userprivileges);
|
1008
|
$isadmin = index($privileges,"a")!=-1;
|
1009
|
$ismanager = index($privileges,"m")!=-1;
|
1010
|
$isreadonly = index($privileges,"r")!=-1;
|
1011
|
$preserveimagesonremove = index($privileges,"p")!=-1;
|
1012
|
$fulllist = $options{f} && $isadmin;
|
1013
|
$fullupdate = $options{p} && $isadmin;
|
1014
|
|
1015
|
my $bto = $userreg{$billto};
|
1016
|
my @bdnsdomains = split(/, ?/, $bto->{'dnsdomains'});
|
1017
|
my @udnsdomains = split(/, ?/, $u->{'dnsdomains'});
|
1018
|
$dnsdomain = '' if ($dnsdomain eq '--'); # TODO - ugly
|
1019
|
$udnsdomains[0] = '' if ($udnsdomains[0] eq '--');
|
1020
|
$bdnsdomains[0] = '' if ($bdnsdomains[0] eq '--');
|
1021
|
$dnsdomain = $udnsdomains[0] || $bdnsdomains[0] || $dnsdomain; # override config value
|
1022
|
|
1023
|
my $bstoreurl = $bto->{'appstoreurl'};
|
1024
|
$bstoreurl = '' if ($bstoreurl eq '--');
|
1025
|
my $ustoreurl = $u->{'appstoreurl'};
|
1026
|
$ustoreurl = '' if ($ustoreurl eq '--');
|
1027
|
$appstoreurl = $bstoreurl || $ustoreurl || $appstoreurl; # override config value
|
1028
|
|
1029
|
$Stabile::sshcmd = $sshcmd;
|
1030
|
$Stabile::disablesnat = $disablesnat;
|
1031
|
$Stabile::privileges = $privileges;
|
1032
|
$Stabile::isadmin = $isadmin;
|
1033
|
|
1034
|
$storagepools = $u->{'storagepools'}; # Prioritized list of users storage pools as numbers, e.g. "0,2,1"
|
1035
|
my $dbuser = $u->{'username'};
|
1036
|
untie %userreg;
|
1037
|
|
1038
|
# If params are passed in URI for a POST og PUT request, we try to parse them out
|
1039
|
if (($ENV{'REQUEST_METHOD'} ne 'GET') && !$isreadonly) {
|
1040
|
$action = $1 if (!$action && $uripath =~ /action=(\w+)/);
|
1041
|
if ($uripath =~ /$package(\.cgi)?\/(.+)$/ && !$isreadonly) {
|
1042
|
my $uuid = $2;
|
1043
|
if (!(%params) && !$curuuid && $uuid =~ /^\?/) {
|
1044
|
%params = split /[=&]/, substr($uuid,1);
|
1045
|
$curuuid = $params{uuid};
|
1046
|
} else {
|
1047
|
$curuuid = $uuid;
|
1048
|
}
|
1049
|
$curuuid = $1 if ($curuuid =~ /\/(.+)/);
|
1050
|
}
|
1051
|
}
|
1052
|
|
1053
|
# Parse out params from g option if called from cmdline
|
1054
|
my $args = $options{g};
|
1055
|
if ($args && !%params) {
|
1056
|
my $obj = from_json( uri_unescape ($args));
|
1057
|
if (ref($obj) eq 'HASH') {
|
1058
|
%params = %{$obj};
|
1059
|
} else {
|
1060
|
%params = {};
|
1061
|
$params{'POSTDATA'} = $args;
|
1062
|
}
|
1063
|
$console = $obj->{'console'} if ($obj->{'console'});
|
1064
|
$curuuid = $obj->{uuid} if (!$curuuid && $obj->{uuid}); # backwards compat with apps calling removesystem
|
1065
|
}
|
1066
|
|
1067
|
# Action may be via on command line switch -a
|
1068
|
if (!$action) {
|
1069
|
$action = $options{a};
|
1070
|
if ($action) { # Set a few options if we are called from command line
|
1071
|
$console = 1 unless ($options{v} && !$options{c});
|
1072
|
$Data::Dumper::Varname = $package;
|
1073
|
$Data::Dumper::Pair = ' : ';
|
1074
|
$Data::Dumper::Terse = 1;
|
1075
|
$Data::Dumper::Useqq = 1;
|
1076
|
}
|
1077
|
}
|
1078
|
# Parse out $action - i.e. find out what action is requested
|
1079
|
$action = $action || $params{'action'}; # $action may have been set above to 'remove' by DELETE request
|
1080
|
|
1081
|
# Handling of action given as part of addressable API
|
1082
|
# Special cases for systems, monitors, etc.
|
1083
|
if (!$action && $uripath =~ /$package\/(.+)(\/|\?)/ && !$params{'path'}) {
|
1084
|
$action = $1;
|
1085
|
$action = $1 if ($action =~ /([^\/]+)\/(.*)/);
|
1086
|
}
|
1087
|
$curuuid = $curuuid || $params{'uuid'} || $params{'id'} || $params{'system'} || $params{'serveruuid'};
|
1088
|
# Handling of target given as part of addressable API
|
1089
|
# if ($uripath =~ /$package(\.cgi)?\/($action\/)?(\w{8}-\w{4}-\w{4}-\w{4}-\w{12})(:\w+)?/) {
|
1090
|
if ($uripath =~ /$package\/(\w{8}-\w{4}-\w{4}-\w{4}-\w{12})(:\w+)?/) {
|
1091
|
$curuuid = "$1$2";
|
1092
|
} elsif ($package eq 'nodes' && $uripath =~ /$package\/(\w{12})(:\w+)?/) {
|
1093
|
$curuuid = "$1$2";
|
1094
|
}
|
1095
|
|
1096
|
$action = lc $action;
|
1097
|
if (!$params && $options{k}) {
|
1098
|
$params{'keywords'} = URI::Escape::uri_unescape($options{k});
|
1099
|
$console = 1 unless ($options{v} && !$options{c});
|
1100
|
}
|
1101
|
$action = (($action)?$action.'_':'') . 'remove' if ($ENV{'REQUEST_METHOD'} eq 'DELETE' && $action ne 'remove');
|
1102
|
# -f should only set $fulllisting and not trigger any keyword actions
|
1103
|
delete $params{'keywords'} if ($params{'keywords'} eq '-f');
|
1104
|
|
1105
|
# Regular read - we send out JSON version of directory list
|
1106
|
if (!$action && (!$ENV{'REQUEST_METHOD'} || $ENV{'REQUEST_METHOD'} eq 'GET')) {
|
1107
|
if (!($package)) {
|
1108
|
; # If we get called as a library this is were we end - do nothing...
|
1109
|
} elsif ($params{'keywords'}) {
|
1110
|
; # If param keywords is provided treat as a post
|
1111
|
} else {
|
1112
|
$action = 'list';
|
1113
|
}
|
1114
|
}
|
1115
|
|
1116
|
### Main security check
|
1117
|
unless ($package eq 'pressurecontrol' || $dbuser || ($user eq 'common' && $action =~ /^updatebtime|^list/)) {throw Error::Simple("Status=Error $action: Unknown user $user [$remoteip]")};
|
1118
|
if (index($privileges,"d")!=-1 && $action ne 'help') {throw Error::Simple("Status=Error Disabled user")};
|
1119
|
|
1120
|
$curuuid = $curuuid || URI::Escape::uri_unescape($params{'uuid'}); # $curuuid may have been set above for DELETE requests
|
1121
|
$curuuid = "" if ($curuuid eq "--");
|
1122
|
$curuuid = $options{u} unless $curuuid;
|
1123
|
if ($package eq 'images') {
|
1124
|
$curimg = URI::Escape::uri_unescape($params{'image'} || $params{'path'}) unless ($action eq 'listfiles');
|
1125
|
$curimg = "" if ($curimg eq "--");
|
1126
|
$curimg = $1 if ($curimg =~ /(.*)\*$/); # Handle Dojo peculiarity
|
1127
|
$curimg = URI::Escape::uri_unescape($options{i}) unless $curimg;
|
1128
|
unless (tie(%imagereg,'Tie::DBI', Hash::Merge::merge({table=>'images', CLOBBER=>1}, $Stabile::dbopts)) ) {throw Error::Simple("Stroke=Error Image UUID register could not be accessed")};
|
1129
|
if ($curimg && !$curuuid && $curimg =~ /(\w{8}-\w{4}-\w{4}-\w{4}-\w{12})/) {
|
1130
|
$curuuid = $curimg;
|
1131
|
$curimg = $imagereg{$curuuid}->{'path'} if ($imagereg{$curuuid});
|
1132
|
# } elsif ($target && !$curimg && !$curuuid) {
|
1133
|
# if ($target =~ /(\w{8}-\w{4}-\w{4}-\w{4}-\w{12})/) {
|
1134
|
# $curuuid = $1;
|
1135
|
# $curimg = $imagereg{$curuuid}->{'path'};
|
1136
|
# } else {
|
1137
|
# $curimg = $target;
|
1138
|
# }
|
1139
|
} elsif (!$curimg && $curuuid) {
|
1140
|
$curimg = $imagereg{$curuuid}->{'path'} if ($imagereg{$curuuid});
|
1141
|
}
|
1142
|
untie %imagereg;
|
1143
|
}
|
1144
|
}
|
1145
|
|
1146
|
sub process {
|
1147
|
my $target = $params{'target'} || $options{t} || $curuuid;
|
1148
|
# We may receive utf8 strings either from browser or command line - convert them to native Perl to avoid double encodings
|
1149
|
utf8::decode($target) if ( $target =~ /[^\x00-\x7f]/ );# true if string contains any non-ascii character
|
1150
|
my $uipath;
|
1151
|
# my $uistatus;
|
1152
|
# Special handling
|
1153
|
if ($package eq 'images') {
|
1154
|
$target = $curimg || $params{'path'} || $params{'image'} || $target unless ($target =~ /^\/.+/);
|
1155
|
$params{'restorepath'} = $params{'path'} if ($action eq 'listfiles');
|
1156
|
$params{'baseurl'} = "https://$ENV{'HTTP_HOST'}/stabile" if ($action eq 'download' && $ENV{'HTTP_HOST'} && !($baseurl =~ /\./)); # send baseurl if configured value not valid
|
1157
|
} elsif ($package eq 'systems') {
|
1158
|
$target = $params{'id'} || $target if ($action =~ /^monitors_/);
|
1159
|
} elsif ($package eq 'nodes') {
|
1160
|
$target = $target || $params{'mac'};
|
1161
|
} elsif ($package eq 'users') {
|
1162
|
$target = $target || $params{'username'};
|
1163
|
}
|
1164
|
# Named action - we got a request for an action
|
1165
|
my $obj;
|
1166
|
if ($action && (defined &{"do_$action"}) && ($ENV{'REQUEST_METHOD'} ne 'POST' || $action eq 'upload' || $action eq 'restorefiles')) {
|
1167
|
# If a function named do_$action (only lowercase allowed) exists, call it and print the result
|
1168
|
if ($action =~ /^monitors/) {
|
1169
|
if ($params{'PUTDATA'}) {
|
1170
|
$obj = $params{'PUTDATA'};
|
1171
|
$action = 'monitors_save' unless ($action =~ /monitors_.+/);
|
1172
|
} else {
|
1173
|
$obj = { action => $action, id => $target };
|
1174
|
}
|
1175
|
} else {
|
1176
|
unless (%params) {
|
1177
|
if ($package eq 'images' && $target =~ /^\//) {
|
1178
|
%params = ("path", $target);
|
1179
|
delete $params{"uuid"};
|
1180
|
} else{
|
1181
|
%params = ("uuid", $target);
|
1182
|
}
|
1183
|
}
|
1184
|
if ($curuuid || $target) {
|
1185
|
$params{uuid} = $curuuid || $target unless ($params{uuid} || $params{path} || ($params{image} && $package eq 'images'));
|
1186
|
}
|
1187
|
$obj = getObj(\%params);
|
1188
|
}
|
1189
|
$obj->{'console'} = $console if ($console);
|
1190
|
$obj->{'baseurl'} = $params{baseurl} if ($params{baseurl});
|
1191
|
# Perform the action
|
1192
|
$postreply = &{"do_$action"}($target, $action, $obj);
|
1193
|
if (!$postreply) { # We expect some kind of reply
|
1194
|
$postreply .= header('text/plain', '500 Internal Server Error because no reply') unless ($console);
|
1195
|
$main::syslogit->($user, 'info', "Could not $action $target ($package)") unless ($action eq 'uuidlookup');
|
1196
|
} elsif (! ($postreply =~ /^(Content-type|Status|Location):/i) ) {
|
1197
|
if ($postreply =~ /Content-type:/) {
|
1198
|
;
|
1199
|
} elsif (!$postreply || $postreply =~ /Status=/ || $postreply =~ /^</ || $postreply =~ /^\w/) {
|
1200
|
$postreply = header('text/plain; charset=UTF8') . $postreply unless ($console);
|
1201
|
} else {
|
1202
|
$postreply = header('application/json; charset=UTF8') . $postreply unless ($console);
|
1203
|
}
|
1204
|
}
|
1205
|
print "$postreply";
|
1206
|
|
1207
|
} elsif (($params{'PUTDATA'} || $params{"keywords"} || $params{"POSTDATA"}) && !$isreadonly) {
|
1208
|
# We got a save post with JSON. Look for interesting stuff and perform action or save
|
1209
|
my @json_array;
|
1210
|
if ($params{'PUTDATA'}) {
|
1211
|
my $json_text = $params{'PUTDATA'};
|
1212
|
utf8::decode($json_text);
|
1213
|
$json_text =~ s/\x/ /g;
|
1214
|
$json_text =~ s/\[\]/\"\"/g;
|
1215
|
@json_array = from_json($json_text);
|
1216
|
} elsif ($params{"keywords"} || $params{"POSTDATA"}) {
|
1217
|
my $json_text = $params{"keywords"} || $params{'POSTDATA'};
|
1218
|
$json_text = uri_unescape($json_text);
|
1219
|
utf8::decode($json_text);
|
1220
|
$json_text =~ s/\x/ /g;
|
1221
|
$json_text =~ s/\[\]/\"\"/g;
|
1222
|
my $json_obj = from_json($json_text);
|
1223
|
if (ref $json_obj eq 'ARRAY') {
|
1224
|
@json_array = @$json_obj;
|
1225
|
} elsif (ref $json_obj eq 'HASH') {
|
1226
|
my %json_hash = %$json_obj;
|
1227
|
my $json_array_ref = [\%json_hash];
|
1228
|
if ($json_hash{"items"}) {
|
1229
|
$json_array_ref = $json_hash{"items"};
|
1230
|
}
|
1231
|
@json_array = @$json_array_ref;
|
1232
|
}
|
1233
|
}
|
1234
|
|
1235
|
foreach (@json_array) {
|
1236
|
my %h = %$_;
|
1237
|
$console = 1 if $h{"console"};
|
1238
|
my $objaction = $h{'action'} || $action;
|
1239
|
$objaction = 'save' if (!$objaction || $objaction eq "--");
|
1240
|
$h{'action'} = $objaction = $action.'_'.$objaction if ($action eq "monitors" || $action eq "packages"); # Allow sending e.g. disable action to monitors by calling monitors_disable
|
1241
|
$h{'action'} = $objaction if ($objaction && !$h{'action'});
|
1242
|
my $obj = getObj(\%h);
|
1243
|
next unless $obj;
|
1244
|
$obj->{'console'} = $console if ($console);
|
1245
|
# Now build the requested action
|
1246
|
my $objfunc = "do_$objaction";
|
1247
|
# If a function named objfunc exists, call it
|
1248
|
if (defined &$objfunc) {
|
1249
|
$target = $h{'uuid'} || $h{'id'};
|
1250
|
$uiuuid = $target;
|
1251
|
my $targetimg = $imagereg{$target};
|
1252
|
# Special handling
|
1253
|
if ($package eq 'images') {
|
1254
|
$target = $targetimg->{'path'} || $h{'image'} || $h{'path'} || $target;
|
1255
|
}
|
1256
|
# Perform the action
|
1257
|
$postreply = &{$objfunc}($target, $objaction, $obj);
|
1258
|
# $uistatus = $1 if ($postreply =~ /\w+=(.\w+) /);
|
1259
|
# Special handling
|
1260
|
if ($package eq 'images') {
|
1261
|
if ($h{'status'} eq 'new') {
|
1262
|
# $uistatus = 'new';
|
1263
|
# $uiuuid = ''; # Refresh entire view
|
1264
|
}
|
1265
|
}
|
1266
|
my $node = $nodereg{$mac};
|
1267
|
my $updateEntry = {
|
1268
|
tab=>$tab,
|
1269
|
user=>$user,
|
1270
|
uuid=>$uiuuid,
|
1271
|
status=>$uistatus,
|
1272
|
mac=>$mac,
|
1273
|
macname=>$node->{'name'},
|
1274
|
displayip=>$uidisplayip,
|
1275
|
displayport=>$uidisplayport,
|
1276
|
type=>$uiupdatetype,
|
1277
|
message=>$postmsg
|
1278
|
};
|
1279
|
# Special handling
|
1280
|
if ($package eq 'images') {
|
1281
|
$obj->{'uuid'} = '' if ($uistatus eq 'new');
|
1282
|
$uipath = $obj->{'path'};
|
1283
|
$updateEntry->{'path'} = $uipath;
|
1284
|
$uiname = $obj->{'name'};
|
1285
|
}
|
1286
|
if ($uiname) {
|
1287
|
$updateEntry->{'name'} = $uiname;
|
1288
|
}
|
1289
|
if ($uiuuid || $postmsg || $uistatus) {
|
1290
|
push (@updateList, $updateEntry);
|
1291
|
}
|
1292
|
} else {
|
1293
|
$postreply .= "Status=ERROR Unknown $package action: $objaction\n";
|
1294
|
}
|
1295
|
}
|
1296
|
|
1297
|
if (! ($postreply =~ /^(Content-type|Status|Location):/i) ) {
|
1298
|
if (!$postreply || $postreply =~ /Status=/) {
|
1299
|
$postreply = header('text/plain; charset=UTF8') . $postreply unless ($console);
|
1300
|
} else {
|
1301
|
$postreply = header('application/json; charset=UTF8') . $postreply unless ($console);
|
1302
|
}
|
1303
|
}
|
1304
|
print $postreply;
|
1305
|
} else {
|
1306
|
$postreply .= "Status=Error Unknown $ENV{'REQUEST_METHOD'} $package action: $action\n";
|
1307
|
print header('text/html', '500 Internal Server Error') unless ($console);
|
1308
|
print $postreply;
|
1309
|
}
|
1310
|
# Functions called via aliases to privileged_action or privileged_action_async cannot update $postmsg or $uistatus
|
1311
|
# so updateUI must be called internally in these functions.
|
1312
|
if (@updateList) {
|
1313
|
$main::updateUI->(@updateList);
|
1314
|
}
|
1315
|
}
|
1316
|
|
1317
|
|
1318
|
# Print list of available actions
|
1319
|
sub Help {
|
1320
|
$help = 1;
|
1321
|
no strict 'refs';
|
1322
|
my %fdescriptions;
|
1323
|
my %fmethods;
|
1324
|
my %fparams;
|
1325
|
my @fnames;
|
1326
|
|
1327
|
my $res = header() unless ($console);
|
1328
|
# my $tempuuid = "484d7852-90d2-43f1-8bd6-e29e234848b0";
|
1329
|
my $tempuuid = "";
|
1330
|
unless ($console) {
|
1331
|
$res .= <<END
|
1332
|
<!DOCTYPE html>
|
1333
|
<html>
|
1334
|
<head>
|
1335
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
|
1336
|
<!-- script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script -->
|
1337
|
<!-- script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script -->
|
1338
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
|
1339
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
|
1340
|
<style>
|
1341
|
.form-control {display: inline-block; width: auto; margin: 2px; }
|
1342
|
input.form-control {width: 180px;}
|
1343
|
pre {
|
1344
|
overflow-x: auto;
|
1345
|
white-space: pre-wrap;
|
1346
|
white-space: -moz-pre-wrap;
|
1347
|
white-space: -pre-wrap;
|
1348
|
white-space: -o-pre-wrap;
|
1349
|
word-wrap: break-word;
|
1350
|
}
|
1351
|
</style>
|
1352
|
</head>
|
1353
|
<body style="margin:1.25rem;">
|
1354
|
<div>
|
1355
|
<table style="width:100%;"><tr><td>
|
1356
|
<select class="form-control" id="scopeaction" name="scopeaction" placeholder="action" onchange="data.scopeaction=this.value; dofields();" autocomplete="off"></select>
|
1357
|
<span id="scopeinputs">
|
1358
|
<input class="form-control" id="scopeuuid" name="scopeuuid" placeholder="uuid" onchange="data.scopedata.uuid=this.value; update();" value="$tempuuid" autocomplete="off" size="34">
|
1359
|
</span>
|
1360
|
<button class="btn btn-primary" href="#" onclick="doit();">Try it</button>
|
1361
|
<pre>
|
1362
|
\$.ajax({
|
1363
|
url: "<span class='scopeurl'>/stabile/$package?uuid=$tempuuid&action=activate</span>",
|
1364
|
type: "<span class='scopemethod'>GET</span>", <span id="dataspan" style="display:none;"><br /> data: "<span class="scopedata"></span>",</span>
|
1365
|
success: function(result) {\$("#scoperesult").text(result);}
|
1366
|
});
|
1367
|
</pre>
|
1368
|
</td><td width="50%"><textarea id="scoperesult" style="width:100%; height: 200px;"></textarea></td>
|
1369
|
</tr>
|
1370
|
</table>
|
1371
|
</div>
|
1372
|
<script>
|
1373
|
data = {"scopemethod": "GET", "scopeaction": "activate", "scopeuuid": "$tempuuid", "scopeurl": "/stabile/$package?uuid=$tempuuid&action=activate"};
|
1374
|
function doit() {
|
1375
|
var obj = {
|
1376
|
url: data.scopeurl,
|
1377
|
type: data.scopemethod,
|
1378
|
success: handleResult,
|
1379
|
error: handleResult
|
1380
|
}
|
1381
|
if (data.scopemethod != 'GET') obj.data = JSON.stringify(data.scopedata);
|
1382
|
\$.ajax(obj);
|
1383
|
\$("#scoperesult").text("");
|
1384
|
return true;
|
1385
|
function handleResult(data, textStatus, jqXHR) {
|
1386
|
if (jqXHR == 'Unauthorized') {
|
1387
|
\$("#scoperesult").text(jqXHR + ": You must log in before you can call API methods.");
|
1388
|
} else if (jqXHR.responseText) {
|
1389
|
\$("#scoperesult").text(jqXHR.responseText);
|
1390
|
} else {
|
1391
|
\$("#scoperesult").text("No result received");
|
1392
|
}
|
1393
|
}
|
1394
|
}
|
1395
|
function dofields() {
|
1396
|
if (scopeparams[data.scopeaction].length==0) {
|
1397
|
\$("#scopeinputs").hide();
|
1398
|
} else {
|
1399
|
var fields = "";
|
1400
|
\$.each(scopeparams[data.scopeaction], function (i, item) {
|
1401
|
var itemname = "scope" + item;
|
1402
|
if (\$("#"+itemname).val()) data[itemname] = \$("#"+itemname).val();
|
1403
|
fields += '<input class="form-control" id="' + itemname + '" placeholder="' + item + '" value="' + ((data[itemname])?data[itemname]:'') + '" size="34" onchange="update();"> ';
|
1404
|
});
|
1405
|
\$("#scopeinputs").empty();
|
1406
|
\$("#scopeinputs").append(fields);
|
1407
|
\$("#scopeinputs").show();
|
1408
|
}
|
1409
|
update();
|
1410
|
}
|
1411
|
function update() {
|
1412
|
data.scopemethod = scopemethods[data.scopeaction];
|
1413
|
if (data.scopemethod == "POST") {
|
1414
|
\$("#dataspan").show();
|
1415
|
data.scopeurl = "/stabile/$package";
|
1416
|
data.scopedata = {"items": [{"action":data.scopeaction}]};
|
1417
|
\$.each(scopeparams[data.scopeaction], function (i, item) {
|
1418
|
var val = \$("#scope"+item).val();
|
1419
|
if (val) data.scopedata.items[0][item] = val;
|
1420
|
});
|
1421
|
} else if (data.scopemethod == "PUT") {
|
1422
|
\$("#dataspan").show();
|
1423
|
data.scopeurl = "/stabile/$package";
|
1424
|
data.scopedata = [{"action":data.scopeaction}];
|
1425
|
\$.each(scopeparams[data.scopeaction], function (i, item) {
|
1426
|
var val = \$("#scope"+item).val();
|
1427
|
if (val) data.scopedata[0][item] = val;
|
1428
|
});
|
1429
|
} else {
|
1430
|
\$("#dataspan").hide();
|
1431
|
data.scopeurl = "/stabile/$package?action="+data.scopeaction;
|
1432
|
\$.each(scopeparams[data.scopeaction], function (i, item) {
|
1433
|
var val = \$("#scope"+item).val();
|
1434
|
if (val) data.scopeurl += "&" + item + "=" + val;
|
1435
|
});
|
1436
|
data.scopedata = '';
|
1437
|
}
|
1438
|
\$(".scopemethod").text(data.scopemethod);
|
1439
|
\$(".scopeurl").text(data.scopeurl);
|
1440
|
\$(".scopedata").text(JSON.stringify(data.scopedata, null, ' ').replace(/\\n/g,'').replace(/ /g,''));
|
1441
|
}
|
1442
|
\$( document ).ready(function() {
|
1443
|
data.scopeaction=\$("#scopeaction").val(); dofields()
|
1444
|
});
|
1445
|
END
|
1446
|
;
|
1447
|
$res .= qq|var scopeparams = {};\n|;
|
1448
|
$res .= qq|var scopemethods = {};\n|;
|
1449
|
$res .= qq|var package="$package"\n|;
|
1450
|
}
|
1451
|
my @entries;
|
1452
|
if ($package eq 'networks') {
|
1453
|
@entries = sort keys %Stabile::Networks::;
|
1454
|
} elsif ($package eq 'images') {
|
1455
|
@entries = sort keys %Stabile::Images::;
|
1456
|
} elsif ($package eq 'servers') {
|
1457
|
@entries = sort keys %Stabile::Servers::;
|
1458
|
} elsif ($package eq 'nodes') {
|
1459
|
@entries = sort keys %Stabile::Nodes::;
|
1460
|
} elsif ($package eq 'users') {
|
1461
|
@entries = sort keys %Stabile::Users::;
|
1462
|
} elsif ($package eq 'systems') {
|
1463
|
@entries = sort keys %Stabile::Systems::;
|
1464
|
}
|
1465
|
|
1466
|
foreach my $entry (@entries) {
|
1467
|
if (defined &{"$entry"} && $entry !~ /help/i && $entry =~ /^do_(.+)/) {
|
1468
|
my $fname = $1;
|
1469
|
# Ask function for help - $help is on
|
1470
|
my $helptext = &{"$entry"}(0, $fname);
|
1471
|
my @helplist = split(":", $helptext, 3);
|
1472
|
chomp $helptext;
|
1473
|
unless ($fname =~ /^gear_/) {
|
1474
|
$fmethods{$fname} = $helplist[0];
|
1475
|
$fparams{$fname} = $helplist[1];
|
1476
|
$fdescriptions{$fname} = $helplist[2];
|
1477
|
$fdescriptions{$fname} =~ s/\n// unless ($console);
|
1478
|
$fdescriptions{$fname} =~ s/\n/\n<br>/g unless ($console);
|
1479
|
my @plist = split(/, ?/, $fparams{$fname});
|
1480
|
unless ($console) {
|
1481
|
$res .= qq|scopeparams["$fname"] = |.to_json(\@plist).";\n";
|
1482
|
$res .= qq|\$("#scopeaction").append(new Option("$fname", "$fname"));\n|;
|
1483
|
$res .= qq|scopemethods["$fname"] = "$helplist[0]";\n|;
|
1484
|
}
|
1485
|
}
|
1486
|
}
|
1487
|
}
|
1488
|
@fnames = sort (keys %fdescriptions);
|
1489
|
|
1490
|
unless ($console) {
|
1491
|
$res .= "\n</script>\n";
|
1492
|
$res .= <<END
|
1493
|
<div class="table-responsive" style="margin-top:1.5rem; noheight: 65vh; overflow-y: scroll;">
|
1494
|
<table class="table table-striped table-sm">
|
1495
|
<thead>
|
1496
|
<tr>
|
1497
|
<th>Name</th>
|
1498
|
<th>Method</th>
|
1499
|
<th>Parameters</th>
|
1500
|
<th style="width:60%;">Description</th>
|
1501
|
</tr>
|
1502
|
</thead>
|
1503
|
<tbody>
|
1504
|
END
|
1505
|
;
|
1506
|
foreach my $fname (@fnames) {
|
1507
|
my $fp = ($fparams{$fname}) ? "$fparams{$fname}" : '';
|
1508
|
$res .= <<END
|
1509
|
<tr>
|
1510
|
<td><a href="#" onclick="data.scopeaction=this.text; \$('#scopeaction option[value=$fname]').prop('selected', true); dofields();">$fname</a></td>
|
1511
|
<td>$fmethods{$fname}</td>
|
1512
|
<td>$fp</td>
|
1513
|
<td>$fdescriptions{$fname}</td>
|
1514
|
</tr>
|
1515
|
END
|
1516
|
;
|
1517
|
}
|
1518
|
$res .= <<END
|
1519
|
</tbody>
|
1520
|
</table>
|
1521
|
</div>
|
1522
|
END
|
1523
|
;
|
1524
|
$res .= qq|</body>\n</html>|;
|
1525
|
} else {
|
1526
|
foreach my $fname (@fnames) {
|
1527
|
my $fp = ($fparams{$fname}) ? "[$fparams{$fname}]" : '';
|
1528
|
$res .= <<END
|
1529
|
* $fname ($fmethods{$fname}) $fp $fdescriptions{$fname}
|
1530
|
END
|
1531
|
;
|
1532
|
}
|
1533
|
}
|
1534
|
|
1535
|
return $res;
|
1536
|
}
|
1537
|
|
1538
|
sub getBackupSize {
|
1539
|
my ($subdir, $img, $imguser) = @_; # $subdir, if specified, includes leading slash
|
1540
|
$imguser = $imguser || $user;
|
1541
|
my $backupsize = 0;
|
1542
|
my @bdirs = ("$backupdir/$imguser$subdir/$img");
|
1543
|
if ($backupdir =~ /^\/stabile-backup\//) { # ZFS backup is enabled - we need to scan more dirs
|
1544
|
@bdirs = (
|
1545
|
"/stabile-backup/*/$imguser$subdir/" . shell_esc_chars($img),
|
1546
|
"/stabile-backup/*/.zfs/snapshot/*/$imguser$subdir/". shell_esc_chars($img)
|
1547
|
);
|
1548
|
}
|
1549
|
foreach my $bdir (@bdirs) {
|
1550
|
my $bdu = `/usr/bin/du -bs $bdir 2>/dev/null`;
|
1551
|
my @blines = split("\n", $bdu);
|
1552
|
# only count size from last snapshot
|
1553
|
my $bline = pop @blines;
|
1554
|
# foreach my $bline (@blines) {
|
1555
|
$bline =~ /(\d+)\s+/;
|
1556
|
$backupsize += $1;
|
1557
|
# }
|
1558
|
}
|
1559
|
return $backupsize;
|
1560
|
}
|
1561
|
|
1562
|
sub shell_esc_chars {
|
1563
|
my $str = shift;
|
1564
|
$str =~ s/([;<>\*\|`&\$!#\(\)\[\]\{\}:'" ])/\\$1/g;
|
1565
|
return $str;
|
1566
|
}
|