line |
stmt |
code |
1
|
|
# Copyright 2009-2013 Bernhard M. Wiedemann |
2
|
|
# Copyright 2012-2021 SUSE LLC |
3
|
|
# SPDX-License-Identifier: GPL-2.0-or-later |
4
|
|
|
5
|
|
|
6
|
|
use Mojo::Base -strict, -signatures; |
7
|
30
|
use autodie ':all'; |
|
30
|
|
|
30
|
|
8
|
30
|
|
|
30
|
|
|
30
|
|
9
|
|
require IPC::System::Simple; |
10
|
|
use Try::Tiny; |
11
|
30
|
use Socket; |
|
30
|
|
|
30
|
|
12
|
30
|
use POSIX '_exit', 'strftime'; |
|
30
|
|
|
30
|
|
13
|
30
|
use myjsonrpc; |
|
30
|
|
|
30
|
|
14
|
30
|
use bmwqemu; |
|
30
|
|
|
30
|
|
15
|
30
|
use Mojo::JSON 'to_json'; |
|
30
|
|
|
30
|
|
16
|
30
|
use Mojo::File 'path'; |
|
30
|
|
|
30
|
|
17
|
30
|
|
|
30
|
|
|
30
|
|
18
|
|
BEGIN { |
19
|
|
$ENV{MOJO_REACTOR} = 'Mojo::Reactor::Poll'; |
20
|
30
|
} |
21
|
|
|
22
|
|
use Mojolicious::Lite -signatures; |
23
|
30
|
use Mojo::IOLoop; |
|
30
|
|
|
30
|
|
24
|
30
|
use Mojo::IOLoop::ReadWriteProcess 'process'; |
|
30
|
|
|
30
|
|
25
|
30
|
use Mojo::IOLoop::ReadWriteProcess::Session 'session'; |
|
30
|
|
|
30
|
|
26
|
30
|
use Mojo::Server::Daemon; |
|
30
|
|
|
30
|
|
27
|
30
|
use File::Basename; |
|
30
|
|
|
30
|
|
28
|
30
|
use Time::HiRes 'gettimeofday'; |
|
30
|
|
|
30
|
|
29
|
30
|
|
|
30
|
|
|
30
|
|
30
|
|
# borrowed from obs with permission from mls@suse.de to license as |
31
|
|
# GPLv2+ |
32
|
|
return "07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000b00000000TRAILER!!!\0\0\0\0" if !$s; |
33
|
9
|
# magic ino |
|
9
|
|
|
9
|
|
|
9
|
|
34
|
9
|
my $h = "07070100000000"; |
35
|
|
# mode S_IFREG |
36
|
6
|
$h .= sprintf("%08x", oct(100000) | $s->[2] & oct(777)); |
37
|
|
# uid gid nlink |
38
|
6
|
$h .= "000000000000000000000001"; |
39
|
|
$h .= sprintf("%08x%08x", $s->[9], $s->[7]); |
40
|
6
|
$h .= "00000000000000000000000000000000"; |
41
|
6
|
$h .= sprintf("%08x", length($name) + 1); |
42
|
6
|
$h .= "00000000$name\0"; |
43
|
6
|
$h .= substr("\0\0\0\0", (length($h) & 3)) if length($h) & 3; |
44
|
6
|
my $pad = ''; |
45
|
6
|
$pad = substr("\0\0\0\0", ($s->[7] & 3)) if $s->[7] & 3; |
46
|
6
|
return ($h, $pad); |
47
|
6
|
} |
48
|
6
|
|
49
|
|
# send test data as cpio archive |
50
|
|
$base .= '/' if $base !~ /\/$/; |
51
|
|
return $self->reply->not_found unless -d $base; |
52
|
3
|
|
|
3
|
|
|
3
|
|
|
3
|
|
53
|
3
|
$self->res->headers->content_type('application/x-cpio'); |
54
|
3
|
|
55
|
|
my $data = ''; |
56
|
3
|
for my $file (path($base)->list_tree->each) { |
57
|
|
$file = $file->to_string(); |
58
|
3
|
my @s = stat $file; |
59
|
3
|
unless (@s) { |
60
|
6
|
$self->app->log->error("Error stating test distribution file '$file': $!"); |
61
|
6
|
next; |
62
|
6
|
} |
63
|
0
|
my $fn = 'data/' . substr($file, length($base)); |
64
|
0
|
local $/; # enable localized slurp mode |
65
|
|
my $fd; |
66
|
6
|
eval { (open($fd, '<:raw', $file)) }; |
67
|
6
|
if (my $E = $@) { |
68
|
6
|
$self->app->log->error("Error reading test distribution file '$file': $!"); |
69
|
6
|
next; |
|
6
|
|
70
|
6
|
} |
71
|
0
|
my ($header, $pad) = _makecpiohead($fn, \@s); |
72
|
0
|
$data .= $header; |
73
|
|
$data .= <$fd>; |
74
|
6
|
close $fd; |
75
|
6
|
$data .= $pad if $pad; |
76
|
6
|
} |
77
|
6
|
$data .= _makecpiohead(); |
78
|
6
|
return $self->render(data => $data); |
79
|
|
} |
80
|
3
|
|
81
|
3
|
# serve a file from within data directory |
82
|
|
my $filetype; |
83
|
|
if ($file =~ m/\.([^\.]+)$/) { |
84
|
|
my $ext = $1; |
85
|
6
|
$filetype = $self->app->types->type($ext); |
|
6
|
|
|
6
|
|
|
6
|
|
86
|
6
|
} |
87
|
6
|
|
88
|
5
|
$filetype ||= 'application/octet-stream'; |
89
|
5
|
$self->res->headers->content_type($filetype); |
90
|
|
return $self->reply->asset(Mojo::Asset::File->new(path => $file)); |
91
|
|
} |
92
|
6
|
|
93
|
6
|
return !(!defined $path || $path =~ /^(.*\/)*\.\.(\/.*)*$/); # do not allow .. in path |
94
|
6
|
} |
95
|
|
|
96
|
|
my $path = path($bmwqemu::vars{CASEDIR}, 'data'); |
97
|
15
|
my $relpath = $self->param('relpath'); |
|
15
|
|
|
15
|
|
98
|
15
|
if (defined $relpath) { |
99
|
|
return $self->reply->not_found unless _is_allowed_path($relpath); |
100
|
|
$path = $path->child($relpath); |
101
|
7
|
} |
|
7
|
|
|
7
|
|
102
|
7
|
|
103
|
7
|
$self->app->log->info("Test data requested: $path"); |
104
|
7
|
return _test_data_dir($self, $path) if -d $path; |
105
|
6
|
return _test_data_file($self, $path) if -f $path; |
106
|
6
|
return $self->reply->not_found; |
107
|
|
} |
108
|
|
|
109
|
7
|
my $asset_name = $self->param('assetname'); |
110
|
7
|
my $asset_type = $self->param('assettype'); |
111
|
4
|
return $self->reply->not_found unless _is_allowed_path($asset_name) && _is_allowed_path($asset_type); |
112
|
1
|
|
113
|
|
# check for the asset within the current working directory because the worker cache will store it here; otherwise |
114
|
|
# fallback to $bmwqemu::vars{ASSETDIR} for legacy setups (see poo#70723) |
115
|
5
|
my $relpath = $self->param('relpath'); |
|
5
|
|
|
5
|
|
116
|
5
|
my $path = path($asset_name); |
117
|
5
|
$path = path($bmwqemu::vars{ASSETDIR}, $asset_type, $asset_name) unless -f $path; |
118
|
5
|
if (defined $relpath) { |
119
|
|
return $self->reply->not_found unless _is_allowed_path($relpath); |
120
|
|
$path = $path->child($relpath); |
121
|
|
} |
122
|
3
|
|
123
|
3
|
$self->app->log->info("Asset requested: $path"); |
124
|
3
|
return _test_data_file($self, $path) if -f $path; |
125
|
3
|
return $self->reply->not_found; |
126
|
0
|
} |
127
|
0
|
|
128
|
|
# store the file in $pooldir/$target |
129
|
|
my $req = $self->req; |
130
|
3
|
return $self->render(text => (($req->error // {})->{message} // 'Limit exceeded'), status => 400) if $req->is_limit_exceeded; |
131
|
3
|
return $self->render(text => 'Upload file content missing', status => 400) unless my $upload = $req->upload('upload'); |
132
|
1
|
|
133
|
|
# choose 'target' field from curl form, otherwise default 'assets_private', assume the pool directory is the current working dir |
134
|
|
my $target = $self->param('target') || 'assets_private'; |
135
|
|
eval { mkdir $target unless -d $target }; |
136
|
5
|
if (my $error = $@) { return $self->render(text => "Unable to create directory for upload: $error", status => 500) } |
|
5
|
|
|
5
|
|
137
|
5
|
|
138
|
5
|
my $upname = $self->param('upname'); |
139
|
4
|
my $filename = basename($upname ? $upname : $self->param('filename')); |
140
|
|
# note: Only renaming the file if upname parameter is present, e.g. from upload_logs(). With this it won't rename the file in |
141
|
|
# case of upload_assert() and autoyast profiles as those are not done via upload_logs(). |
142
|
3
|
|
143
|
3
|
$upload->move_to("$target/$filename"); |
|
3
|
|
144
|
3
|
return $self->render(text => "OK: $filename\n"); |
|
1
|
|
145
|
|
} |
146
|
2
|
|
147
|
2
|
bmwqemu::load_vars(); |
148
|
|
return $self->render(json => {vars => \%bmwqemu::vars}); |
149
|
|
} |
150
|
|
|
151
|
2
|
return $self->reply->asset(Mojo::Asset::File->new(path => 'current_script')); |
152
|
2
|
} |
153
|
|
|
154
|
|
return undef unless $response->{stop_processing_isotovideo_commands}; |
155
|
0
|
|
|
0
|
|
|
0
|
|
156
|
0
|
# stop processing isotovideo commands if isotovideo says so |
157
|
0
|
$app->log->debug('cmdsrv: stop processing isotovideo commands'); |
158
|
|
$app->defaults(isotovideo => undef); |
159
|
|
} |
160
|
0
|
|
|
0
|
|
|
0
|
|
161
|
0
|
my $cmd = $mojo_lite_controller->param('command'); |
162
|
|
return $mojo_lite_controller->reply->not_found unless grep { $cmd eq $_ } @$commands; |
163
|
|
|
164
|
134
|
my $app = $mojo_lite_controller->app; |
|
134
|
|
|
134
|
|
|
134
|
|
165
|
134
|
return unless my $isotovideo = $app->defaults('isotovideo'); |
166
|
|
|
167
|
|
# send command to isotovideo and block until a response arrives |
168
|
3
|
myjsonrpc::send_json($isotovideo, {cmd => $cmd, params => $mojo_lite_controller->req->query_params->to_hash}); |
169
|
3
|
my $response = myjsonrpc::read_json($isotovideo); |
170
|
|
_handle_isotovideo_response($app, $response); |
171
|
|
|
172
|
1
|
return $mojo_lite_controller->render(json => $response); |
|
1
|
|
|
1
|
|
|
1
|
|
173
|
1
|
} |
174
|
1
|
|
|
1
|
|
175
|
|
return isotovideo_command($c, [qw(version)]); |
176
|
1
|
} |
177
|
1
|
|
178
|
|
return isotovideo_command($c, []); |
179
|
|
} |
180
|
1
|
|
181
|
1
|
my $relpath = $self->param('relpath'); |
182
|
1
|
my $path = testapi::hashed_string($relpath); |
183
|
|
return _test_data_file($self, $path) if -f $path; |
184
|
1
|
return $self->reply->not_found; |
185
|
|
} |
186
|
|
|
187
|
1
|
# allow up to 20 GiB for uploads of big hdd images |
|
1
|
|
|
1
|
|
188
|
1
|
$ENV{MOJO_MAX_MESSAGE_SIZE} //= ($bmwqemu::vars{UPLOAD_MAX_MESSAGE_SIZE_GB} // 0) * 1024**3; |
189
|
|
$ENV{MOJO_INACTIVITY_TIMEOUT} //= ($bmwqemu::vars{UPLOAD_INACTIVITY_TIMEOUT} // 300); |
190
|
|
$ENV{MOJO_TMPDIR} //= path('command-server-tmp')->make_path; |
191
|
0
|
|
|
0
|
|
|
0
|
|
192
|
0
|
# avoid leaking token |
193
|
|
app->mode('production'); |
194
|
|
app->log->level('info'); |
195
|
1
|
app->log->debug('cmdsrv: run daemon ' . $isotovideo); |
|
1
|
|
|
1
|
|
196
|
1
|
# abuse the defaults to store singletons for the server |
197
|
1
|
app->defaults(isotovideo => $isotovideo); |
198
|
1
|
app->defaults(clients => {}); |
199
|
0
|
|
200
|
|
my $r = app->routes; |
201
|
|
$r->namespaces(['OpenQA']); |
202
|
6
|
my $token_auth = $r->any("/$bmwqemu::vars{JOBTOKEN}"); |
|
6
|
|
|
6
|
|
|
6
|
|
203
|
|
|
204
|
6
|
# for access all data as CPIO archive |
205
|
6
|
$token_auth->get('/data' => \&test_data); |
206
|
6
|
|
207
|
|
# to access a single file or a subdirectory |
208
|
|
$token_auth->get('/data/*relpath' => \&test_data); |
209
|
6
|
|
210
|
6
|
# uploading log files from tests |
211
|
6
|
$token_auth->post('/uploadlog/#filename' => {target => 'ulogs'} => [target => [qw(ulogs)]] => \&upload_file); |
212
|
|
|
213
|
6
|
# uploading assets |
214
|
6
|
$token_auth->post('/upload_asset/#filename' => [target => [qw(assets_private assets_public)]] => \&upload_file); |
215
|
|
|
216
|
6
|
# to get the current bash script out of the test |
217
|
6
|
$token_auth->get('/current_script' => \¤t_script); |
218
|
6
|
|
219
|
|
# to get temporary files from the current worker |
220
|
|
$token_auth->get('/files/*relpath' => \&get_temp_file); |
221
|
6
|
|
222
|
|
# get asset |
223
|
|
$token_auth->get('/assets/#assettype/#assetname' => \&get_asset); |
224
|
6
|
$token_auth->get('/assets/#assettype/#assetname/*relpath' => \&get_asset); |
225
|
|
|
226
|
|
# get vars |
227
|
6
|
$token_auth->get('/vars' => \&get_vars); |
228
|
|
|
229
|
|
$token_auth->get('/isotovideo/#command' => \&isotovideo_get); |
230
|
6
|
$token_auth->post('/isotovideo/#command' => \&isotovideo_post); |
231
|
|
|
232
|
|
# websocket related routes |
233
|
6
|
$token_auth->websocket('/ws')->name('ws')->to('commands#start_ws'); |
234
|
|
$token_auth->post('/broadcast')->name('broadcast')->to('commands#broadcast_message_to_websocket_clients'); |
235
|
|
|
236
|
6
|
# not known by default mojolicious |
237
|
|
app->types->type(oga => 'audio/ogg'); |
238
|
|
|
239
|
6
|
# it's unlikely that we will ever use cookies, but we need a secret to shut up mojo |
240
|
6
|
app->secrets([$bmwqemu::vars{JOBTOKEN}]); |
241
|
|
|
242
|
|
# listen to all IPv4 and IPv6 interfaces (if ipv6 is supported) |
243
|
6
|
my $address = '[::]'; |
244
|
|
if (!IO::Socket::IP->new(Listen => 5, LocalAddr => $address)) { |
245
|
6
|
$address = '0.0.0.0'; |
246
|
6
|
} |
247
|
|
my $daemon = Mojo::Server::Daemon->new(app => app, listen => ["http://$address:$port"]); |
248
|
|
$daemon->silent; |
249
|
6
|
# Use same log format as isotovideo |
250
|
6
|
app->log->format(\&bmwqemu::log_format_callback); |
251
|
|
# process json messages from isotovideo |
252
|
|
Mojo::IOLoop->singleton->reactor->io($isotovideo => sub { |
253
|
6
|
my ($reactor, $writable) = @_; |
254
|
|
|
255
|
|
my @isotovideo_responses = myjsonrpc::read_json($isotovideo, undef, 1); |
256
|
6
|
my $clients = app->defaults('clients'); |
257
|
|
for my $response (@isotovideo_responses) { |
258
|
|
_handle_isotovideo_response(app, $response); |
259
|
6
|
delete $response->{json_cmd_token}; |
260
|
6
|
|
261
|
0
|
app->log->debug('cmdsrv: broadcasting message from os-autoinst to all ws clients: ' . to_json($response)); |
262
|
|
for (keys %$clients) { |
263
|
6
|
$clients->{$_}->send({json => $response}); |
264
|
6
|
} |
265
|
|
} |
266
|
6
|
})->watch($isotovideo, 1, 0); # watch only readable (and not writable) |
267
|
|
|
268
|
|
app->log->info("cmdsrv: daemon reachable under http://*:$port/$bmwqemu::vars{JOBTOKEN}/"); |
269
|
133
|
try { |
270
|
|
$daemon->run; |
271
|
133
|
} |
272
|
133
|
catch { |
273
|
133
|
print "cmdsrv: failed to run daemon $_\n"; |
274
|
133
|
_exit(1); |
275
|
133
|
}; |
276
|
|
} |
277
|
133
|
|
278
|
133
|
my ($child, $isotovideo); |
279
|
1
|
socketpair($child, $isotovideo, AF_UNIX, SOCK_STREAM, PF_UNSPEC) |
280
|
|
or die "cmdsrv: socketpair: $!"; |
281
|
|
|
282
|
6
|
$child->autoflush(1); |
283
|
|
$isotovideo->autoflush(1); |
284
|
6
|
|
285
|
|
my $process = process(sub { |
286
|
6
|
$SIG{TERM} = 'DEFAULT'; |
287
|
|
$SIG{INT} = 'DEFAULT'; |
288
|
|
$SIG{HUP} = 'DEFAULT'; |
289
|
0
|
$SIG{CHLD} = 'DEFAULT'; |
290
|
0
|
|
291
|
6
|
close($child); |
292
|
|
$0 = "$0: commands"; |
293
|
|
run_daemon($port, $isotovideo); |
294
|
21
|
Devel::Cover::report() if Devel::Cover->can('report'); |
|
21
|
|
|
21
|
|
295
|
21
|
_exit(0); |
296
|
21
|
}, |
297
|
|
sleeptime_during_kill => 0.1, |
298
|
|
total_sleeptime_during_kill => 5, |
299
|
21
|
blocking_stop => 1, |
300
|
21
|
internal_pipes => 0, |
301
|
|
set_pipes => 0)->start; |
302
|
|
|
303
|
6
|
close($isotovideo); |
304
|
6
|
$process->on(collected => sub { bmwqemu::diag("commands process exited: " . shift->exit_status); }); |
305
|
6
|
return ($process, $child); |
306
|
6
|
} |
307
|
|
|
308
|
6
|
1; |