Passed
Push — dpa_submit21 ( e4087a )
by David
08:53
created

file_row()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
c 0
b 0
f 0
nc 1
nop 4
dl 0
loc 6
rs 10
1
<?php
2
// This file is part of BOINC.
3
// https://boinc.berkeley.edu
4
// Copyright (C) 2024 University of California
5
//
6
// BOINC is free software; you can redistribute it and/or modify it
7
// under the terms of the GNU Lesser General Public License
8
// as published by the Free Software Foundation,
9
// either version 3 of the License, or (at your option) any later version.
10
//
11
// BOINC is distributed in the hope that it will be useful,
12
// but WITHOUT ANY WARRANTY; without even the implied warranty of
13
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14
// See the GNU Lesser General Public License for more details.
15
//
16
// You should have received a copy of the GNU Lesser General Public License
17
// along with BOINC.  If not, see <http://www.gnu.org/licenses/>.
18
19
// web interface for managing BUDA science apps
20
//
21
// in the following, 'app' means BUDA science app
22
// and 'variant' means a variant of one of these (e.g. CPU, GPU)
23
24
require_once('../inc/util.inc');
25
require_once('../inc/sandbox.inc');
26
require_once('../inc/submit_util.inc');
27
28
display_errors();
29
30
$buda_root = "../../buda_apps";
31
32
// scan BUDA apps and variants, and write a file 'buda_plan_classes'
33
// in the project dir with list of plan classes
34
//
35
function write_plan_class_file() {
36
    $pcs = [];
37
    global $buda_root;
38
    if (is_dir($buda_root)) {
39
        $apps = scandir($buda_root);
40
        foreach ($apps as $app) {
41
            if ($app[0] == '.') continue;
42
            if (!is_dir("$buda_root/$app")) continue;
43
            $vars = scandir("$buda_root/$app");
44
            foreach ($vars as $var) {
45
                if ($var[0] == '.') continue;
46
                if (!is_dir("$buda_root/$app/$var")) continue;
47
                $pcs[] = $var;
48
            }
49
        }
50
    }
51
    $pcs = array_unique($pcs);
52
    file_put_contents(
53
        "../../buda_plan_classes",
54
        implode("\n", $pcs)."\n"
55
    );
56
}
57
58
// show list of BUDA apps and variants,
59
// w/ buttons for adding and deleting
60
//
61
function app_list($notice=null) {
62
    global $buda_root;
63
    if (!is_dir($buda_root)) {
64
        mkdir($buda_root);
65
    }
66
    page_head('BOINC Universal Docker App (BUDA)');
67
    if ($notice) {
68
        echo "$notice <p>\n";
69
    }
70
    text_start();
71
    echo "
72
        <p>BUDA lets you submit Docker jobs using a web interface.
73
        <a href=https://github.com/BOINC/boinc/wiki/BUDA-overview>Learn more</a>.
74
        <p>
75
        BUDA science apps:
76
    ";
77
78
    $dirs = scandir($buda_root);
79
    foreach ($dirs as $dir) {
80
        if ($dir[0] == '.') continue;
81
        panel("$dir",
82
            function() use ($dir) {
83
                show_app($dir);
84
            }
85
        );
86
    }
87
    show_button_small('buda.php?action=app_form', 'Add science app');
88
    text_end();
89
    page_tail();
90
}
91
92
function show_app($dir) {
93
    global $buda_root;
94
    start_table('table-striped');
95
    table_header('Variant<br><small>click for details</small>', 'Submit jobs');
96
    $pcs = scandir("$buda_root/$dir");
97
    $have_var = false;
98
    foreach ($pcs as $pc) {
99
        if ($pc[0] == '.') continue;
100
        $have_var = true;
101
        table_row(
102
            "<a href=buda.php?action=variant_view&app=$dir&variant=$pc>$pc</href>",
103
            button_text(
104
                "buda_submit.php?app=$dir&variant=$pc", "Submission form"
105
            )
106
        );
107
    }
108
    end_table();
109
    echo "<p>";
110
    show_button_small("buda.php?action=variant_form&app=$dir", 'Add variant');
111
    if (!$have_var) {
112
        echo "<p>";
113
        show_button(
114
            "buda.php?action=app_delete&app=$dir",
115
            "Delete app",
116
            null,
117
            'btn btn-xs btn-warning'
118
        );
119
    }
120
}
121
122
function file_row($app, $variant, $dir, $f) {
123
    [$md5, $size] = parse_info_file("$dir/.md5/$f");
124
    table_row(
125
        "<a href=buda.php?action=view_file&app=$app&variant=$variant&fname=$f>$f</a>",
126
        $size,
127
        $md5
128
    );
129
}
130
131
function variant_view() {
132
    global $buda_root;
133
    $app = get_str('app');
134
    if (!is_valid_filename($app)) die('bad arg');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
135
    $variant = get_str('variant');
136
    if (!is_valid_filename($variant)) die('bad arg');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
137
    page_head("BUDA: variant '$variant' of science app '$app'");
138
    $dir = "$buda_root/$app/$variant";
139
    $desc = json_decode(file_get_contents("$dir/variant.json"));
140
    start_table();
141
    table_header('Dockerfile', 'size', 'md5');
142
    file_row($app, $variant, $dir, $desc->dockerfile);
143
    table_header('App files', '', '');
144
    foreach ($desc->app_files as $f) {
145
        file_row($app, $variant, $dir, $f);
146
    }
147
    table_header('Auto-generated files', '', '');
148
    file_row($app, $variant, $dir, 'template_in');
149
    file_row($app, $variant, $dir, 'template_out');
150
    file_row($app, $variant, $dir, 'variant.json');
151
    end_table();
152
153
    start_table();
154
    row2(
155
        'Input filenames:',
156
        implode(',', $desc->input_file_names)
157
    );
158
    row2(
159
        'Output filenames:',
160
        implode(',', $desc->output_file_names)
161
    );
162
    if (!empty($desc->max_total)) {
163
        row2('Max total instances per job:', $desc->max_total);
164
    } else {
165
        row2('Max total instances per job:', '1');
166
    }
167
    if (!empty($desc->min_nsuccess)) {
168
        row2('Target successful instances per job:', $desc->min_nsuccess);
169
    } else {
170
        row2('Target successful instances per job:', '1');
171
    }
172
    end_table();
173
174
    echo '<p>';
175
    show_button(
176
        "buda.php?action=variant_delete&app=$app&variant=$variant",
177
        'Delete variant',
178
        null,
179
        'btn btn-xs btn-warning'
180
    );
181
    page_tail();
182
}
183
184
// form for creating app variant.
185
// Currently doesn't support indirect files.
186
// doing this would require checkboxes for indirect
187
//
188
// Could have other stuff like
189
//      - min_quorum, max_total_results
190
//      - rsc_disk_bound, rsc_memory_bound
191
// or does this belong in job submission?
192
//
193
function variant_form($user) {
194
    $sbitems = sandbox_select_items($user);
195
    $app = get_str('app');
196
    if (!is_valid_filename($app)) die('bad arg');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
197
198
    page_head_select2("Create a variant of Docker app $app");
199
    echo "
200
        Details are <a href=https://github.com/BOINC/boinc/wiki/BUDA-job-submission#adding-a-variant>here</a>.
201
    ";
202
    $sb = '<br><small>From your <a href=sandbox.php>file sandbox</a></small>';
203
    $pc = '<br><small>Specify
204
    <a href=https://github.com/BOINC/boinc/wiki/AppPlan>GPU and other requirements</a>';
205
    form_start('buda.php');
206
    form_input_hidden('app', $app);
207
    form_input_hidden('action', 'variant_action');
208
    form_input_text("Plan class$pc", 'variant');
209
    form_select("Dockerfile$sb", 'dockerfile', $sbitems);
210
    form_select2_multi("Application files$sb", 'app_files', $sbitems, null);
211
    form_input_text(
212
        'Input file names<br><small>Space-separated</small>',
213
        'input_file_names'
214
    );
215
    form_input_text(
216
        'Output file names<br><small>Space-separated</small>',
217
        'output_file_names'
218
    );
219
    form_input_text(
220
        'Run at most this many total instances of each job',
221
        'max_total',
222
        '1'
223
    );
224
    form_input_text(
225
        'Get this many successful instances of each job
226
            <br><small>(subject to the above limit)</small>
227
        ',
228
        'min_nsuccess',
229
        '1'
230
    );
231
    form_submit('OK');
232
    form_end();
233
    page_tail();
234
}
235
236
function buda_file_phys_name($app, $variant, $md5) {
237
    return sprintf('buda_%s_%s_%s', $app, $variant, $md5);
238
}
239
240
// copy file from sandbox to variant dir, and stage to download hier
241
//
242
function copy_and_stage_file($user, $fname, $dir, $app, $variant) {
243
    copy_sandbox_file($user, $fname, $dir);
244
    [$md5, $size] = parse_info_file("$dir/.md5/$fname");
245
    $phys_name = buda_file_phys_name($app, $variant, $md5);
246
    stage_file_aux("$dir/$fname", $md5, $size, $phys_name);
247
    return $phys_name;
248
}
249
250
// create templates and put them in variant dir
251
//
252
function create_templates($variant, $variant_desc, $dir) {
253
    // input template
254
    //
255
    $x = "<input_template>\n";
256
    $ninfiles = 1 + count($variant_desc->input_file_names) + count($variant_desc->app_files);
257
    for ($i=0; $i<$ninfiles; $i++) {
258
        $x .= "   <file_info>\n      <sticky/>\n      <no_delete/>\n      <executable/>\n   </file_info>\n";
259
    }
260
    $x .= "   <workunit>\n";
261
    $x .= file_ref_in($variant_desc->dockerfile);
262
    foreach ($variant_desc->app_files as $fname) {
263
        $x .= file_ref_in($fname);
264
    }
265
    foreach ($variant_desc->input_file_names as $fname) {
266
        $x .= file_ref_in($fname);
267
    }
268
    if ($variant == 'cpu') {
269
        $x .= "      <plan_class></plan_class>\n";
270
    } else {
271
        $x .= "      <plan_class>$variant</plan_class>\n";
272
    }
273
274
    // replication params
275
    //
276
    $x .= sprintf("      <target_nresults>%d</target_nresults>\n",
277
        $variant_desc->min_nsuccess
278
    );
279
    $x .= sprintf("      <min_quorum>%d</min_quorum>\n",
280
        $variant_desc->min_nsuccess
281
    );
282
    $x .= sprintf("      <max_total_results>%d</max_total_results>\n",
283
        $variant_desc->max_total
284
    );
285
286
    $x .= "   </workunit>\n<input_template>\n";
287
    file_put_contents("$dir/template_in", $x);
288
289
    // output template
290
    //
291
    $x = "<output_template>\n";
292
    $i = 0;
293
    foreach ($variant_desc->output_file_names as $fname) {
294
        $x .= file_info_out($i++);
295
    }
296
    $x .= "   <result>\n";
297
    $i = 0;
298
    foreach ($variant_desc->output_file_names as $fname) {
299
        $x .= file_ref_out($i++, $fname);
300
    }
301
    $x .= "   </result>\n</output_template>\n";
302
    file_put_contents("$dir/template_out", $x);
303
}
304
305
// create variant
306
//
307
function variant_action($user) {
308
    global $buda_root;
309
    $variant = get_str('variant');
310
    if (!$variant) $variant = 'cpu';
311
    if (!is_valid_filename($variant)) {
312
        error_page(filename_rules());
313
    }
314
    $app = get_str('app');
315
    if (!is_valid_filename($app)) die('bad arg');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
316
    $dockerfile = get_str('dockerfile');
317
    if (!is_valid_filename($dockerfile)) {
318
        error_page("Invalid dockerfile name: ".filename_rules());
319
    }
320
    $app_files = get_array('app_files');
321
    foreach ($app_files as $fname) {
322
        if (!is_valid_filename($fname)) {
323
            error_page("Invalid app file name: ".filename_rules());
324
        }
325
    }
326
    $min_nsuccess = get_int('min_nsuccess');
327
    if (!$min_nsuccess) {
328
        error_page('Must specify nonzero number of successful instances.');
329
    }
330
    $max_total = get_int('max_total');
331
    if (!$max_total) {
332
        error_page('Must specify nonzero max number of instances.');
333
    }
334
    $input_file_names = get_str('input_file_names', true);
335
    $output_file_names = explode(' ', get_str('output_file_names'));
336
    if ($input_file_names) {
337
        $input_file_names = explode(' ', $input_file_names);
338
        foreach ($input_file_names as $fname) {
339
            if (!is_valid_filename($fname)) {
340
                error_page("Invalid input file name: ".filename_rules());
341
            }
342
        }
343
    } else {
344
        $input_file_names = [];
345
    }
346
    foreach ($output_file_names as $fname) {
347
        if (!is_valid_filename($fname)) {
348
            error_page("Invalid output file name: ".filename_rules());
349
        }
350
    }
351
352
    if (file_exists("$buda_root/$app/$variant")) {
353
        error_page("Variant '$variant' already exists.");
354
    }
355
    $dir = "$buda_root/$app/$variant";
356
    mkdir($dir);
357
    mkdir("$dir/.md5");
358
359
    // collect variant params into a struct
360
    //
361
    $desc = new StdClass;
362
    $desc->dockerfile = $dockerfile;
363
    $desc->app_files = $app_files;
364
    $desc->input_file_names = $input_file_names;
365
    $desc->output_file_names = $output_file_names;
366
    $desc->min_nsuccess = $min_nsuccess;
367
    $desc->max_total = $max_total;
368
369
    // copy files from sandbox to variant dir
370
    //
371
    $pname = copy_and_stage_file($user, $dockerfile, $dir, $app, $variant);
372
    $desc->dockerfile_phys = $pname;
373
    $desc->app_files_phys = [];
374
    foreach ($app_files as $fname) {
375
        $pname = copy_and_stage_file($user, $fname, $dir, $app, $variant);
376
        $desc->app_files_phys[] = $pname;
377
    }
378
379
    // write variant params to a JSON file
380
    //
381
    file_put_contents(
382
        "$dir/variant.json",
383
        json_encode($desc, JSON_PRETTY_PRINT)
384
    );
385
386
    create_templates($variant, $desc, $dir);
387
388
    // Note: we don't currently allow indirect file access.
389
    // If we did, we'd need to create job.toml to mount project dir
390
391
    app_list("Variant $variant added for app $app.");
392
}
393
394
function variant_delete() {
395
    global $buda_root;
396
    $app = get_str('app');
397
    if (!is_valid_filename($app)) die('bad arg');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
398
    $variant = get_str('variant');
399
    if (!is_valid_filename($variant)) die('bad arg');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
400
    $confirmed = get_str('confirmed', true);
401
    if ($confirmed) {
402
        $dir = "$buda_root/$app/$variant";
403
        if (!file_exists($dir)) error_page('no such variant');
404
        // delete staged files
405
        //
406
        foreach (scandir("$dir/.md5") as $fname) {
407
            if ($fname[0] == '.') continue;
408
            [$md5, $size] = parse_info_file("$dir/.md5/$fname");
409
            $phys_name = buda_file_phys_name($app, $variant, $md5);
410
            $phys_path = download_hier_path($phys_name);
411
            unlink($phys_path);
412
            unlink("$phys_path.md5");
413
        }
414
        system("rm -r $buda_root/$app/$variant", $ret);
415
        if ($ret) {
416
            error_page("delete failed");
417
        }
418
        $notice = "Variant $variant of app $app removed.";
419
        app_list($notice);
420
    } else {
421
        page_head("Confirm");
422
        echo "<p>Are you sure you want to delete variant $variant of BUDA app $app?  <p>";
423
        show_button(
424
            "buda.php?action=variant_delete&app=$app&variant=$variant&confirmed=yes",
425
            "Yes"
426
        );
427
        page_tail();
428
    }
429
}
430
431
function app_delete() {
432
    global $buda_root;
433
    $app = get_str('app');
434
    if (!is_valid_filename($app)) die('bad arg');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
435
    $confirmed = get_str('confirmed', true);
436
    if ($confirmed) {
437
        $dir = "$buda_root/$app";
438
        if (!file_exists($dir)) error_page('no such app');
439
        foreach (scandir($dir) as $fname) {
440
            if ($fname[0] == '.') continue;
441
            error_page("You must delete all variants first.");
442
        }
443
        system("rmdir $buda_root/$app", $ret);
444
        if ($ret) {
445
            error_page('delete failed');
446
        }
447
        $notice = "App $app removed.";
448
        app_list($notice);
449
    } else {
450
        page_head('Confirm');
451
        echo "<p>Are you sure you want to delete BUDA science app $app?  <p>";
452
        show_button(
453
            "buda.php?action=app_delete&app=$app&confirmed=yes",
454
            "Yes"
455
        );
456
        page_tail();
457
    }
458
}
459
460
function app_form() {
461
    page_head('Create Docker app');
462
    form_start('buda.php');
463
    form_input_hidden('action', 'app_action');
464
    form_input_text('Name', 'name');
465
    form_submit('OK');
466
    form_end();
467
    page_tail();
468
}
469
470
function app_action() {
471
    global $buda_root;
472
    $name = get_str('name');
473
    if (!is_valid_filename($name)) {
474
        error_page(filename_rules());
475
    }
476
    $dir = "$buda_root/$name";
477
    if (file_exists($dir)) {
478
        error_page("App $name already exists.");
479
    }
480
    mkdir($dir);
481
    header("Location: buda.php");
482
}
483
484
function view_file() {
485
    global $buda_root;
486
    $app = get_str('app');
487
    if (!is_valid_filename($app)) die('bad arg');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
488
    $variant = get_str('variant');
489
    if (!is_valid_filename($variant)) die('bad arg');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
490
    $fname = get_str('fname');
491
    if (!is_valid_filename($fname)) die('bad arg');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
492
    echo "<pre>\n";
493
    $x = file_get_contents("$buda_root/$app/$variant/$fname");
494
    echo htmlspecialchars($x);
495
    echo "</pre>\n";
496
}
497
498
// check access.
499
// Anyone with submit access to BUDA can add/delete apps and variants.
500
// Might want to refine this at some point
501
502
$user = get_logged_in_user();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $user is correct as get_logged_in_user() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
503
$buda_app = BoincApp::lookup("name='buda'");
504
if (!$buda_app) error_page('no buda app');
505
if (!has_submit_access($user, $buda_app->id)) {
506
    error_page('no access');
507
}
508
509
$action = get_str('action', true);
510
switch ($action) {
511
case 'app_form':
512
    app_form(); break;
513
case 'app_action':
514
    app_action(); break;
515
case 'app_delete':
516
    app_delete(); break;
517
case 'variant_view':
518
    variant_view($user); break;
0 ignored issues
show
Unused Code introduced by
The call to variant_view() has too many arguments starting with $user. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

518
    /** @scrutinizer ignore-call */ 
519
    variant_view($user); break;

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
519
case 'variant_form':
520
    variant_form($user); break;
521
case 'variant_action':
522
    variant_action($user);
523
    write_plan_class_file();
524
    break;
525
case 'variant_delete':
526
    variant_delete();
527
    write_plan_class_file();
528
    break;
529
case 'view_file':
530
    view_file(); break;
531
case null:
532
    app_list(); break;
533
default:
0 ignored issues
show
Coding Style introduced by
DEFAULT keyword must be indented 4 spaces from SWITCH keyword
Loading history...
Coding Style introduced by
DEFAULT case must have a breaking statement
Loading history...
534
    error_page("unknown action");
535
}
536
537
?>
538