File Coverage

commands.pm
Criterion Covered Total %
statement 243 267 91.0
total 243 267 91.0


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' => \&current_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;