From db4ba3c2cb2614402f07063e06cb557dd57f85b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3hannes=20Nordal?= <johannesnordal88@gmail.com> Date: Mon, 24 Jun 2024 16:25:52 +0000 Subject: [PATCH] update: webapp --- Web_App/.gitkeep | 0 Web_App/2023-10-17/.gitkeep | 0 Web_App/2023-10-17/index.php | 250 ------ Web_App/2023-10-17/lamec_config.json | 194 ----- Web_App/2023-11-13/.gitkeep | 0 .../2021-01-Logo-RGB-RAISE_standard.png | Bin 34331 -> 0 bytes Web_App/2023-11-13/index.php | 422 ---------- Web_App/2023-11-13/lamec_config_new.json | 300 ------- Web_App/2023-11-20/.gitkeep | 0 Web_App/2023-11-20/index.php | 416 ---------- Web_App/index-thor.php | 80 -- Web_App/index_tmp.php | 134 ---- Web_App/index_v1.php | 57 -- about.php | 6 + base.php | 145 ++++ data/form-schema.json | 747 ++++++++++++++++++ data/form-schema.json_ | 747 ++++++++++++++++++ data/status.json | 61 ++ html/about.html | 31 + html/form.html | 6 + html/output.html | 4 + html/status.html | 12 + .../logo.png | Bin index.php | 444 +---------- js/form.js | 307 +++++++ js/status.js | 31 + lamec.py | 29 +- render.php | 8 + scripts/MockHPCSystem/MockSoftware/lamec.json | 3 + .../MockHPCSystem/MockSoftware/template.sh | 5 + scripts/MockHPCSystem/sysinfo.json | 7 + status.php | 25 + update.py | 2 +- 33 files changed, 2204 insertions(+), 2269 deletions(-) delete mode 100644 Web_App/.gitkeep delete mode 100644 Web_App/2023-10-17/.gitkeep delete mode 100644 Web_App/2023-10-17/index.php delete mode 100644 Web_App/2023-10-17/lamec_config.json delete mode 100644 Web_App/2023-11-13/.gitkeep delete mode 100644 Web_App/2023-11-13/2021-01-Logo-RGB-RAISE_standard.png delete mode 100644 Web_App/2023-11-13/index.php delete mode 100644 Web_App/2023-11-13/lamec_config_new.json delete mode 100644 Web_App/2023-11-20/.gitkeep delete mode 100644 Web_App/2023-11-20/index.php delete mode 100644 Web_App/index-thor.php delete mode 100644 Web_App/index_tmp.php delete mode 100644 Web_App/index_v1.php create mode 100644 about.php create mode 100644 base.php create mode 100644 data/form-schema.json create mode 100644 data/form-schema.json_ create mode 100644 data/status.json create mode 100644 html/about.html create mode 100644 html/form.html create mode 100644 html/output.html create mode 100644 html/status.html rename 2021-01-Logo-RGB-RAISE_standard.png => images/logo.png (100%) create mode 100644 js/form.js create mode 100644 js/status.js create mode 100644 render.php create mode 100644 scripts/MockHPCSystem/MockSoftware/lamec.json create mode 100644 scripts/MockHPCSystem/MockSoftware/template.sh create mode 100644 scripts/MockHPCSystem/sysinfo.json create mode 100644 status.php diff --git a/Web_App/.gitkeep b/Web_App/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Web_App/2023-10-17/.gitkeep b/Web_App/2023-10-17/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Web_App/2023-10-17/index.php b/Web_App/2023-10-17/index.php deleted file mode 100644 index 4436c47..0000000 --- a/Web_App/2023-10-17/index.php +++ /dev/null @@ -1,250 +0,0 @@ -<?php - // For testing purposes, output all PHP errors - ini_set('display_errors', 1); - ini_set('display_startup_errors', 1); - error_reporting(E_ALL); - - - // Read configuration data from JSON file - $filename = "lamec_config.json"; - $config_json = file_get_contents($filename); - // Remove line breaks so the JSON can be embedded as a string in the JS code - $config_json_str = preg_replace("/\r|\n/", "", $config_json); -?> -<!doctype html> -<html> -<head> - <meta charset="utf-8"> - <meta name="viewport" content="width=device-width, initial-scale=1"> - <title>Load AI Modules, Environments, and Containers (LAMEC) API</title> - <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous"> - <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script> - <script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script> - - <script> - window.onload = function() { - // Get json string with configuration data - var json_config = JSON.parse('<?=$config_json_str?>'); - - // Reference form select objects - var sys_sel = document.getElementById("sys"); - var par_sel = document.getElementById("par"); - var sw_sel = document.getElementById("sw"); - - // Method to populate the partitions form select element - function getPartitions() { - selected_system = sys_sel.value; - $.each(json_config.systems, function(key, system) { - if (system.name == selected_system) { - $.each(system.partitions, function(p_key, partition) { - par_sel.options[par_sel.options.length] = new Option(partition, partition); - }); - } - }); - } - - // Method to populate the software form select element - function getSoftware() { - selected_system = sys_sel.value; - $.each(json_config.systems, function(key, system) { - if (system.name == selected_system) { - $.each(system.software, function(p_key, software) { - sw_sel.options[sw_sel.options.length] = new Option(software, software); - }); - } - }); - } - - // Method to add the other input fields for this system - function getFields() { - // Remove the input fields present - $("#additionalFields").empty(); - - selected_system = sys_sel.value; - $.each(json_config.systems, function(key, system) { - if (system.name == selected_system) { - $.each(system.fields, function(p_key, field) { - new_input = '<div class="mb-3"><label for="'; - new_input += field.fieldname; - new_input += '" class="form-label"><b>'; - new_input += field.name; - new_input += '</b></label><div id="'; - new_input += field.fieldname; - new_input += 'Help" class="form-text">'; - new_input += field.description; - new_input += '</div>'; - - if(field.type == 'string') { - new_input += '<input type="text" placeholder="' + field.placeholder + '" '; - } - else if(field.type == 'number') { - new_input += '<input type="number" min="' + field.min + '" max="' + field.max + '" '; - new_input += 'value="' + field.default + '" '; - } - - new_input += 'name="' + field.fieldname + '" '; - new_input += 'aria-describedby="' + field.fieldname + '" '; - new_input += 'class="form-control">'; - new_input += '</div>'; - $("#additionalFields").append(new_input); - }); - } - }); - } - - // Method to update link to system ducomentation - function updateSystemDocumentation() { - selected_system = sys_sel.value; - $.each(json_config.systems, function(key, system) { - if(system.name == selected_system) { - if(!system.documentation) { - // No documentation link available - hide system documentation line - $("#system-documentation").hide(); - } - else { - $("#system-name").text(system.name); - $("#system-documentation-url").attr("href", system.documentation); - $("#system-documentation").show(); - } - } - }); - } - - // Populate the system form select element with available systems and - // create a link to the system documentation of the first system - $.each(json_config.systems, function(key, system) { - sys_sel.options[sys_sel.options.length] = new Option(system.name, system.name); - }); - updateSystemDocumentation(); - - // Populate the partition and software form select elements and insert input fields - getPartitions(); - getSoftware(); - getFields(); - - // Capture the onChange event of the system form select element and update - // the available partitions accordingly and clear the software select element - sys_sel.onchange = function() { - par_sel.length = 0; - sw_sel.length = 0; - - updateSystemDocumentation(); - - getPartitions(); - getSoftware(); - getFields(); - - } - } -</script> - <style> - .form-control::placeholder { - color: var(--bs-dark-bg-subtle); - } - </style> -</head> -<body> - <nav class="navbar"> - <div class="container justify-content-center mt-3"> - <a class="navbar-brand" href="https://www.coe-raise.eu"> - <img src="/2021-01-Logo-RGB-RAISE_standard.png" - height="160" - alt="RAISE Logo" - loading="lazy" /> - </a> - </div> - </nav> - <div class="container my-5" style="max-width: 980px;"> - <div class="container justify-content-center"> - <h1 class="display-6" style="text-align:center; color: rgb(4,132,196);">Load AI Modules, Environments, and Containers (LAMEC) API</h1> - </div> - <br><br> - <?php - if(isset( $_POST['Submit'])) { - // Convert JSON confid settings to array - $config = json_decode($config_json, true); - - // Check input data, and replace any empty values with default values, if specified - foreach($config["systems"] as $system) { - if($system["name"] == $_POST["sys"]) { - foreach($system["fields"] as $field) { - if(array_key_exists("default", $field) && $_POST[$field["fieldname"]] == "") { - $_POST[$field["fieldname"]] = $field["default"]; - } - } - break; - } - } - - putenv('PYTHONPATH="/var/www/apps/jsc/lamec"'); - $Phrase = "/var/www/apps/jsc/lamec/lamec_ml.py gen -a " . $_POST["acc"] . " -par " . $_POST["par"] . " -n " . $_POST["nnodes"] . " -e " . $_POST["exe"] . " -sys " . $_POST["sys"] . " -sw " . $_POST["sw"]; - $command = escapeshellcmd($Phrase); - $output = shell_exec($command); - ?> - <div class="mb-3"> - <label for="output" class="form-label"><b>Your start script:</b></label> - <textarea rows="20" class="form-control"><?=$output?></textarea> - </div> - <?php - } - else { - ?> - <form action="index.php", method="post"> - <div class="mb-1"> - <label for="sys" class="form-label"><b>System</b></label> - <div id="sysHelp" class="form-text">Select the computing system on which you want to submit your job.</div> - <select name="sys" id="sys" class="form-select" aria-describedby="sysHelp"></select> - </div> - - <div class="mb-1" id="system-documentation"> - <small class="text-body-secondary"> - <span id="system-name">System name</span> - <a id="system-documentation-url" href="#" target="_blank">documentation</a> - <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="bi bi-box-arrow-up-right" viewBox="0 0 16 16"> - <path fill-rule="evenodd" d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5z"/> - <path fill-rule="evenodd" d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0v-5z"/> - </svg> - </small> - </div> - - <div class="mb-3 mt-3"> - <label for="par" class="form-label"><b>Partition</b></label> - <div id="sysHelp" class="form-text">Select the partition on which you want to submit your job.</div> - <select name="par" id="par" class="form-select" aria-describedby="sysHelp"></select> - </div> - - <div class="mb-3"> - <label for="sw" class="form-label"><b>Software</b></label> - <div id="swHelp" class="form-text">Select the software that your job depends on.</div> - <select name="sw" id="sw" class="form-select" aria-describedby="swHelp"></select> - </div> - - <div id="additionalFields"></div> - <button type="submit" name="Submit" class="btn btn-primary">Submit</button> - </form> - <?php - } - ?> - <br> - </div> - <div class="container-fluid" style="background-color: rgb(158,196,243);"> - <div class="container justify-content-center" style="max-width: 980px; text-align:center;"> - <!-- If we want to show the menu links in the footer... - <ul class="nav justify-content-center pt-1 pb-3 mb-3"> - <li class="nav-item px-4"> - <a href="https://www.coe-raise.eu/contact" class="nav-link text-body-secondary">Contact</a> - </li> - <li class="nav-item px-4"> - <a href="https://www.coe-raise.eu/imprint" class="nav-link text-body-secondary">Imprint</a> - </li> - <li class="nav-item px-4"> - <a href="https://www.coe-raise.eu/privacy-policy" class="nav-link text-body-secondary">Privacy Policy</a> - </li> - </ul> - --> - <div class="py-3">The CoE RAISE project have received funding from the European Union’s Horizon 2020 – Research and Innovation Framework Programme H2020-INFRAEDI-2019-1 under grant agreement no. 951733</div> - <div class="py-2">©2021 CoE RAISE.</div> - </div> - </div> -</body> -</html> diff --git a/Web_App/2023-10-17/lamec_config.json b/Web_App/2023-10-17/lamec_config.json deleted file mode 100644 index 87cef7b..0000000 --- a/Web_App/2023-10-17/lamec_config.json +++ /dev/null @@ -1,194 +0,0 @@ -{ - "systems": [ - { - "name": "JURECA", - "documentation": "https://apps.fz-juelich.de/jsc/hps/jureca/index.html", - "partitions": ["dc-cpu", "dc-gpu-devel"], - "software": ["Pytorch-DDP", "Horovod", "DeepSpeed", "HeAT"], - "fields": [ - { - "name": "Executable", - "description": "Specify the executable of your application.", - "type": "string", - "placeholder": "./executable", - "fieldname": "exe", - "default": "YOUR_EXECUTABLE_HERE", - "arg": "e" - }, - { - "name": "Number of nodes", - "description": "Specify the number of nodes", - "type": "number", - "min": 1, - "max": 2400, - "fieldname": "nnodes", - "default": 1, - "arg": "n" - }, - { - "name": "Account", - "description": "Specify the account for your job.", - "type": "string", - "placeholder": "accountName", - "fieldname": "acc", - "default": "YOUR_ACCOUNT_NAME_HERE", - "arg": "a" - } - ] - }, - { - "name": "DEEP", - "documentation": "https://deeptrac.zam.kfa-juelich.de:8443/trac/wiki/Public/User_Guide", - "partitions": ["dp-esb", "dp-dam"], - "software": ["Pytorch-DDP", "Horovod", "DeepSpeed", "HeAT"], - "fields": [ - { - "name": "Executable", - "description": "Specify the executable of your application.", - "type": "string", - "placeholder": "./executable", - "fieldname": "exe", - "default": "YOUR_EXECUTABLE_HERE", - "arg": "e" - }, - { - "name": "Number of nodes", - "description": "Specify the number of nodes", - "type": "number", - "min": 1, - "max": 2400, - "fieldname": "nnodes", - "default": 1, - "arg": "n" - }, - { - "name": "Account", - "description": "Specify the account for your job.", - "type": "string", - "placeholder": "accountName", - "fieldname": "acc", - "default": "YOUR_ACCOUNT_NAME_HERE", - "arg": "a" - } - ] - - }, - { - "name": "JUWELS", - "documentation": "https://apps.fz-juelich.de/jsc/hps/juwels/index.html", - "partitions": ["develbooster", "develgpus", "gpus"], - "software": ["Pytorch-DDP"], - "fields": [ - { - "name": "Executable", - "description": "Specify the executable of your application.", - "type": "string", - "placeholder": "./executable", - "fieldname": "exe", - "default": "YOUR_EXECUTABLE_HERE", - "arg": "e" - }, - { - "name": "Number of nodes", - "description": "Specify the number of nodes", - "type": "number", - "min": 1, - "max": 2400, - "fieldname": "nnodes", - "default": 1, - "arg": "n" - }, - { - "name": "Account", - "description": "Specify the account for your job.", - "type": "string", - "placeholder": "accountName", - "fieldname": "acc", - "default": "YOUR_ACCOUNT_NAME_HERE", - "arg": "a" - } - ] - }, - { - "name": "LUMI", - "documentation": "https://docs.lumi-supercomputer.eu/software/", - "partitions": ["dev-g", "small-g", "standard-g"], - "software": ["Pytorch-DDP"], - "fields": [ - { - "name": "Executable", - "description": "Specify the executable of your application.", - "type": "string", - "placeholder": "./executable", - "fieldname": "exe", - "default": "YOUR_EXECUTABLE_HERE", - "arg": "e" - }, - { - "name": "Number of nodes", - "description": "Specify the number of nodes", - "type": "number", - "min": 1, - "max": 2400, - "fieldname": "nnodes", - "default": 1, - "arg": "n" - }, - { - "name": "Account", - "description": "Specify the account for your job.", - "type": "string", - "placeholder": "accountName", - "fieldname": "acc", - "default": "YOUR_ACCOUNT_NAME_HERE", - "arg": "a" - } - ] - }, - { - "name": "VEGA", - "documentation": "https://doc.vega.izum.si", - "partitions": ["cpu"], - "software": ["Basilisk"], - "fields": [ - { - "name": "Dummy field", - "description": "Just a dummy field for testing purposes.", - "type": "string", - "placeholder": "dummyString", - "fieldname": "dummy", - "default": "DUMMY_TEXT", - "arg": "dummy" - }, - { - "name": "Executable", - "description": "Specify the executable of your application.", - "type": "string", - "placeholder": "./executable", - "fieldname": "exe", - "default": "YOUR_EXECUTABLE_HERE", - "arg": "e" - }, - { - "name": "Number of nodes", - "description": "Specify the number of nodes", - "type": "number", - "min": 1, - "max": 2400, - "fieldname": "nnodes", - "default": 1, - "arg": "n" - }, - { - "name": "Account", - "description": "Specify the account for your job.", - "type": "string", - "placeholder": "accountName", - "fieldname": "acc", - "default": "YOUR_ACCOUNT_NAME_HERE", - "arg": "a" - } - ] - } - ] -} \ No newline at end of file diff --git a/Web_App/2023-11-13/.gitkeep b/Web_App/2023-11-13/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Web_App/2023-11-13/2021-01-Logo-RGB-RAISE_standard.png b/Web_App/2023-11-13/2021-01-Logo-RGB-RAISE_standard.png deleted file mode 100644 index 22f20b42a426f2ef22138439f9bc693871f46bc6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34331 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°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 diff --git a/Web_App/2023-11-13/index.php b/Web_App/2023-11-13/index.php deleted file mode 100644 index d49c9d1..0000000 --- a/Web_App/2023-11-13/index.php +++ /dev/null @@ -1,422 +0,0 @@ -<?php - // For testing purposes, output all PHP errors - ini_set('display_errors', 1); - ini_set('display_startup_errors', 1); - error_reporting(E_ALL); - - // - // Read configuration data from JSON file - // (TBD get output from LAMEC) - // - $filename = "lamec_config_new.json"; - $config_json = file_get_contents($filename); - // Remove line breaks so the JSON can be embedded as a string in the JS code - $config_json_str = json_encode($config_json); -?> -<!doctype html> -<html> -<head> - <meta charset="utf-8"> - <meta name="viewport" content="width=device-width, initial-scale=1"> - <title>Load AI Modules, Environments, and Containers (LAMEC) API</title> - <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous"> - <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script> - <script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script> - - <script> - window.onload = function() { - // All configuration settings from LAMEC - var json_config = JSON.parse(<?=$config_json_str?>); - - // Default html container name for fields - defaultFieldPosition = $("#formFields") - - // - // Set up the form dynamically with all fields from the - // LAMEC configuration - // - $.each(json_config.fields, function(key, field) { - // - // Add the header for the field, containing the field name - // and a description of the field - // - addFieldHeaderElement(field.name, field.desc); - - // - // Add an empty field element as specified in the JSON data. - // Check for the special case where field type is string, but there is - // a restriction of type 'option', in that case the field is SELECT. - // - fieldType = field.type; - if(fieldType == 'string' && field.restriction && field.restriction.type == 'option') { - fieldType = 'select'; - } - addFieldElement(field.name, fieldType); - - // - // Process the settings for the field that was created above and add - // the settings as a data attribute to each form field for later use. - // - if(field.restriction) { - $("#" + field.name).data("restriction", field.restriction); - } - if(field.scope) { - $("#" + field.name).data("scope", field.scope); - } - if(field.default) { - $("#" + field.name).data("default", field.default); - } - - // - // Check if there is documentation available for this field, and if there is then - // add it as a data attribute to the relevant form field for later reference, - // then add the html element. - // - if(json_config.documentation && json_config.documentation[field.name]) { - $("#" + field.name).data("documentation", json_config.documentation[field.name]); - addDocumentationElement(field.name); - } - - // - // Process this field's data and update the form element accordingly, - // taking into account all restrictions, dependencies and scope. - // - setFieldAttributes(field.name); - }); - - - // - // Adds the html header for a field. - // - function addFieldHeaderElement(fieldName, fieldDescription, fieldPosition = defaultFieldPosition) { - new_field = '<div class="mt-4" '; - new_field += 'id="' + fieldName + 'Header" '; - new_field += '><label for="'; - new_field += fieldName; - new_field += '" class="form-label"><b>'; - new_field += fieldName[0].toUpperCase() + fieldName.slice(1).replace(/_/g, ' '); - new_field += '</b></label><div id="'; - new_field += fieldName; - new_field += 'Help" class="form-text mt-0 mb-1">'; - new_field += fieldDescription; - new_field += '</div></div>'; - fieldPosition.append(new_field); - } - - // - // Adds an empty field element. - // - function addFieldElement(fieldName, fieldType, fieldPosition = defaultFieldPosition) { - new_field = '<div class="my-0" '; - new_field += 'id="' + fieldName + 'Element">'; - if(fieldType == 'select') { - new_field += '<select class="form-select" '; - } - else if(fieldType == 'number') { - new_field += '<input class="form-control" type="number" '; - } - else if(fieldType == 'string') { - new_field += '<input class="form-control" type="text" '; - } - - new_field += 'id="' + fieldName + '" '; - new_field += 'name="' + fieldName + '" '; - new_field += 'aria-describedby="' + fieldName + 'Help" >'; - - if(fieldType == 'select') { - new_field += '</select>'; - } - new_field += '</div>'; - fieldPosition.append(new_field); - } - - // - // Add an html documentation element. - // - function addDocumentationElement(fieldName, fieldPosition = defaultFieldPosition) { - new_field = '<div class="mt-1 mb-0" id="' + fieldName + '-documentation">'; - new_field += '<small class="text-body-secondary">'; - new_field += '<span id="' + fieldName + '-name"></span> '; - new_field += '<a id="' + fieldName + '-documentation-url" href="#" target="_blank">documentation</a> '; - new_field += '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="bi bi-box-arrow-up-right" viewBox="0 0 16 16">'; - new_field += '<path fill-rule="evenodd" d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5z"/>'; - new_field += '<path fill-rule="evenodd" d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0v-5z"/>'; - new_field += '</svg></small></div>'; - fieldPosition.append(new_field); - } - - // - // Process the field's data and update the form element accordingly, - // taking into account all restrictions, dependencies and scope. - // - function setFieldAttributes(fieldName) { - fieldElement = $("#" + fieldName); - - if(fieldElement.data("scope")) { - // - // The field has a specific scope, show or hide - // the field accordingly. - // - if(isFieldWithinScope(fieldElement.data("scope"))) { - $("#" + fieldName + "Header").show(); - $("#" + fieldName + "Element").show(); - } - else { - $("#" + fieldName + "Header").hide(); - $("#" + fieldName + "Element").hide(); - } - } - - restriction = fieldElement.data("restriction"); - if(restriction) { - // - // Process the field's restrictions. - // - restrictionType = restriction.type; - - // - // The field is a select field, delete all options present - // and then repopulate the field, taking into account restrictions - // and dependencies. - // - if(restrictionType == 'option') { - $("#" + fieldName).length = 0; - } - - if(restriction.value.depends_on) { - dependantFields = restriction.value.depends_on.fields; - dependantResolution = restriction.value.depends_on.resolution; - - $.each(dependantResolution, function(index, resolution) { - // Track the number of resolutions that are met - resolutionCount = 0; - for(i=0; i<resolution.key.length; i++) { - if(resolution.key[i] == document.getElementById(dependantFields[i]).value) { - // one resolution met - resolutionCount++; - } - else { - // a resolution not met, skip to next - // continue - return(true); - } - } - - if(resolutionCount == i) { - // All dependant resolutions met - if(restrictionType == 'range') { - fieldElement.attr("min", resolution.value[0]); - fieldElement.attr("max", resolution.value[1]); - fieldElement.val(resolution.value[0]); - } - else if(restrictionType == 'option') { - populateFieldElementOptions(fieldName, resolution.value); - } - // As all resolutions were met, no need to search further. - // break - return(false); - } - }); - } - else { - if(restrictionType == 'option') { - populateFieldElementOptions(fieldName, restriction.value); - } - else if (restrictionType == 'range'){ - fieldElement.attr("min", restriction.value[0]); - fieldElement.attr("max", restriction.value[1]); - fieldElement.val(restriction.value[0]); - } - } - } - - // Set defaut field value, if defined. - if(fieldElement.data("default")) { - fieldElement.val(fieldElement.data("default")); - } - - if(fieldElement.data("documentation")) { - // Show documentation if selected option has documentation available - updateDocumentation(fieldName); - } - } - - // - // Method to update link to documentation for selected option - // - function updateDocumentation(fieldName) { - let documentationFound = false; - $.each($("#" + fieldName).data("documentation"), function(key, val) { - if(document.getElementById(fieldName).value == key) { - $("#" + fieldName + "-name").text(key); - $("#" + fieldName + "-documentation-url").attr("href", val); - documentationFound = true; - // break - return(false); - } - }); - if(documentationFound) { - $("#" + fieldName + "-documentation").show(); - } - else { - $("#" + fieldName + "-documentation").hide(); - } - } - - // - // Method to pupolate the options of a select element - // - function populateFieldElementOptions(fieldName, fieldValues) { - sel_element = document.getElementById(fieldName); - sel_element.length = 0; - $.each(fieldValues, function(key, val) { - sel_element.options[sel_element.options.length] = new Option(val, val); - }); - } - - // - // Method to check if a field is within scope. - // - function isFieldWithinScope(fieldScope) { - let isWithinScope = false; - if(fieldScope) { - $.each(fieldScope, function(key, scope) { - $.each(scope.values, function(key, vals) { - let matchedVals = 0; - for(i = 0; i < vals.length; i++) { - if(vals[i] == document.getElementById(scope.fields[i]).value) { - matchedVals++; - } - else { - continue; - } - } - if(matchedVals == vals.length) { - isWithinScope = true; - return(false); - } - }) - }); - return(isWithinScope); - } - return(false); - } - - // - // Catch all changes to select elements and update other fields accordingly, - // based on the rules of dependencies, scope and available documentation - // - $(".form-select").on('change', function() { - changedField = this.name; - - // If field has documentation data, then it needs updating - if($("#" + changedField).data("documentation")) { - updateDocumentation(changedField); - } - - $.each(json_config.fields, function(key, field) { - if(field.scope) { - setFieldAttributes(field.name); - } - else if(field.restriction && field.restriction.value && field.restriction.value.depends_on) { - $.each(field.restriction.value.depends_on.fields, function(key, val) { - if(val == changedField) { - // Update the field, according to set dependencies - setFieldAttributes(field.name); - } - }); - } - }); - - }); - } - </script> - <style> - .form-control::placeholder { - color: var(--bs-dark-bg-subtle); - } - </style> -</head> -<body> - <nav class="navbar"> - <div class="container justify-content-center mt-3"> - <a class="navbar-brand" href="https://www.coe-raise.eu"> - <img src="/2021-01-Logo-RGB-RAISE_standard.png" - height="160" - alt="RAISE Logo" - loading="lazy" /> - </a> - </div> - </nav> - <div class="container my-5" style="max-width: 980px;"> - <div class="container justify-content-center"> - <h1 class="display-6" style="text-align:center; color: rgb(4,132,196);">Load AI Modules, Environments, and Containers (LAMEC) API</h1> - </div> - <br><br> - <?php - if(isset( $_POST['Submit'])) { - // Convert JSON confid settings to array - $config = json_decode($config_json, true); - - // Check input data, and replace any empty values with default values, if specified - // OLD CODE - TO BE CHANGED TO INTERFACE WITH LAMEC - /** - foreach($config["systems"] as $system) { - if($system["name"] == $_POST["sys"]) { - foreach($system["fields"] as $field) { - if(array_key_exists("default", $field) && $_POST[$field["fieldname"]] == "") { - $_POST[$field["fieldname"]] = $field["default"]; - } - } - break; - } - } - **/ - - /** - putenv('PYTHONPATH="/var/www/apps/jsc/lamec"'); - $Phrase = "/var/www/apps/jsc/lamec/lamec_ml.py gen -a " . $_POST["acc"] . " -par " . $_POST["par"] . " -n " . $_POST["nnodes"] . " -e " . $_POST["exe"] . " -sys " . $_POST["sys"] . " -sw " . $_POST["sw"]; - $command = escapeshellcmd($Phrase); - $output = shell_exec($command); - **/ - - // For testing purposes - $output = var_export($_REQUEST, true); - ?> - <div class="mb-3"> - <label for="output" class="form-label"><b>Your start script:</b></label> - <textarea rows="20" class="form-control"><?=$output?></textarea> - </div> - <?php - } - else { - ?> - <div id="formFields"></div> - <button type="submit" name="Submit" class="btn btn-primary mt-5">Create start script</button> - </form> - <?php - } - ?> - <br> - </div> - <div class="container-fluid" style="background-color: rgb(158,196,243);"> - <div class="container justify-content-center" style="max-width: 980px; text-align:center;"> - <!-- If we want to show the menu links in the footer... - <ul class="nav justify-content-center pt-1 pb-3 mb-3"> - <li class="nav-item px-4"> - <a href="https://www.coe-raise.eu/contact" class="nav-link text-body-secondary">Contact</a> - </li> - <li class="nav-item px-4"> - <a href="https://www.coe-raise.eu/imprint" class="nav-link text-body-secondary">Imprint</a> - </li> - <li class="nav-item px-4"> - <a href="https://www.coe-raise.eu/privacy-policy" class="nav-link text-body-secondary">Privacy Policy</a> - </li> - </ul> - --> - <div class="py-3">The CoE RAISE project have received funding from the European Union’s Horizon 2020 – Research and Innovation Framework Programme H2020-INFRAEDI-2019-1 under grant agreement no. 951733</div> - <div class="py-2">©2021 CoE RAISE.</div> - </div> - </div> -</body> -</html> diff --git a/Web_App/2023-11-13/lamec_config_new.json b/Web_App/2023-11-13/lamec_config_new.json deleted file mode 100644 index 0b9bc8e..0000000 --- a/Web_App/2023-11-13/lamec_config_new.json +++ /dev/null @@ -1,300 +0,0 @@ -{ - "fields": [ - { - "name": "system", - "type": "string", - "desc": "Select the computing system on which you want to submit your job.", - "restriction": { - "type": "option", - "value": [ - "deep", - "jureca", - "juwels", - "lumi", - "vega" - ] - } - }, - { - "name": "software", - "type": "string", - "desc": "Select the software that your job depends on.", - "restriction": { - "type": "option", - "value": { - "depends_on": { - "fields": ["system"], - "resolution": [ - { - "key": ["deep"], - "value": [ - "ddp", - "deepspeed", - "heat", - "horovod" - ] - }, - { - "key": ["jureca"], - "value": [ - "ddp", - "deepspeed", - "heat", - "horovod" - ] - }, - { - "key": ["lumi"], - "value": ["ddp"] - }, - { - "key": ["vega"], - "value": ["basilisk"] - } - ] - } - } - } - }, - { - "name": "partition", - "desc": "Specify the partition for your job.", - "type": "string", - "restriction": { - "type": ["option"], - "value": { - "depends_on": { - "fields": ["system"], - "resolution": [ - { - "key": ["deep"], - "value": [ - "dp-esb", - "dp-dam" - ] - }, - { - "key": ["jureca"], - "value": [ - "dc-cpu", - "dc-gpu", - "dc-cpu-bigmem", - "dc-gpu-devel", - "dc-cpu-devel", - "dc-gpu-devel" - ] - }, - { - "key": ["juwels"], - "value": [ - "batch", - "mem192", - "devel", - "gpus", - "develgpus", - "booster", - "develbooster" - ] - }, - { - "key": ["lumi"], - "value": [ - "standard-g", - "standard", - "dev-g", - "debug", - "small-g", - "small", - "largemem" - ] - }, - { - "key": ["vega"], - "value": [ - "dev", - "cpu", - "longcpu", - "gpu", - "largemem" - ] - } - ] - } - } - } - }, - { - "name": "number_of_nodes", - "desc": "Specify the number of nodes.", - "type": "number", - "restriction": { - "type": ["range"], - "value": { - "depends_on": { - "fields": ["system", "partition"], - "resolution": [ - { - "key": ["deep", "dp-esb"], - "value": [1, 75] - }, - { - "key": ["deep", "dp-dam"], - "value": [1, 16] - }, - { - "key": ["jureca", "dc-cpu"], - "value": [1, 128] - }, - { - "key": ["jureca", "dc-gpu"], - "value": [1, 24] - }, - { - "key": ["jureca", "dc-cpu-bigmem"], - "value": [1, 48] - }, - { - "key": ["jureca", "dc-cpu-devel"], - "value": [1, 4] - }, - { - "key": ["jureca", "dc-gpu-devel"], - "value": [1, 4] - }, - { - "key": ["juwels", "batch"], - "value": [1, 1024] - }, - { - "key": ["juwels", "mem192"], - "value": [1, 64] - }, - { - "key": ["juwels", "devel"], - "value": [1, 8] - }, - { - "key": ["juwels", "gpus"], - "value": [1, 46] - }, - { - "key": ["juwels", "develgpus"], - "value": [1, 2] - }, - { - "key": ["juwels", "booster"], - "value": [1, 384] - }, - { - "key": ["juwels", "develbooster"], - "value": [1, 4] - }, - { - "key": ["lumi", "standard-g"], - "value": [1, 1024] - }, - { - "key": ["lumi", "standard"], - "value": [1, 512] - }, - { - "key": ["lumi", "dev-g"], - "value": [1, 32] - }, - { - "key": ["lumi", "debug"], - "value": [1, 4] - }, - { - "key": ["lumi", "small-g"], - "value": [1, 4] - }, - { - "key": ["lumi", "small"], - "value": [1, 4] - }, - { - "key": ["lumi", "largemem"], - "value": [1, 1] - }, - { - "key": ["vega", "dev"], - "value": [1, 8] - }, - { - "key": ["vega", "cpu"], - "value": [1, 960] - }, - { - "key": ["vega", "longcpu"], - "value": [1, 6] - }, - { - "key": ["vega", "gpu"], - "value": [1, 60] - }, - { - "key": ["vega", "largemem"], - "value": [1, 192] - } - ] - } - } - }, - "default": 1 - }, - { - "name": "account", - "type": "string", - "desc": "Specify the account for your job." - }, - { - "name": "executable", - "type": "string", - "desc": "Specify an executable for your job.", - "default": "app" - }, - { - "name": "dummy1", - "type": "string", - "desc": "Dummy field for testing, only shown when system is 'jureca'.", - "scope": [ - { - "fields": ["system"], - "values": [ - ["jureca"] - ] - } - ] - }, - { - "name": "dummy2", - "type": "string", - "desc": "Only shown when system is 'jureca' or 'deep' and software is 'ddp'.", - "scope": [ - { - "fields": ["system", "software"], - "values": [ - ["deep", "ddp"], - ["jureca", "ddp"] - ] - } - ] - } - ], - "documentation": { - "system": { - "jureca": "https://apps.fz-juelich.de/jsc/hps/jureca/index.html", - "deep": "https://deeptrac.zam.kfa-juelich.de:8443/trac/wiki/Public/User_Guide", - "juwels": "https://apps.fz-juelich.de/jsc/hps/juwels/index.html", - "lumi": "https://docs.lumi-supercomputer.eu/software/", - "vega": "https://doc.vega.izum.si" - }, - "software": { - "ddp": "https://pytorch.org/tutorials/intermediate/ddp_tutorial.html", - "horovod": "https://horovod.readthedocs.io/en/stable/", - "deepspeed": "https://deepspeed.readthedocs.io/en/latest/", - "heat": "https://heat.readthedocs.io/en/stable/" - } - } -} diff --git a/Web_App/2023-11-20/.gitkeep b/Web_App/2023-11-20/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Web_App/2023-11-20/index.php b/Web_App/2023-11-20/index.php deleted file mode 100644 index 33c4c44..0000000 --- a/Web_App/2023-11-20/index.php +++ /dev/null @@ -1,416 +0,0 @@ -<?php - // For testing purposes, output all PHP errors - ini_set('display_errors', 1); - ini_set('display_startup_errors', 1); - error_reporting(E_ALL); - - // - // Read configuration data from JSON file - // (TBD get output from LAMEC) - // - $filename = "lamec_config_new.json"; - $config_json = file_get_contents($filename); - // Remove line breaks so the JSON can be embedded as a string in the JS code - $config_json_str = json_encode($config_json); -?> -<!doctype html> -<html> -<head> - <meta charset="utf-8"> - <meta name="viewport" content="width=device-width, initial-scale=1"> - <title>Load AI Modules, Environments, and Containers (LAMEC) API</title> - <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous"> - <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script> - <script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script> - - <script> - window.onload = function() { - // All configuration settings from LAMEC - var json_config = JSON.parse(<?=$config_json_str?>); - - // Default html container name for fields - defaultFieldPosition = $("#formFields") - - // - // Set up the form dynamically with all fields from the - // LAMEC configuration - // - $.each(json_config.fields, function(key, field) { - // - // Add the header for the field, containing the field name - // and a description of the field - // - addFieldHeaderElement(field.name, field.desc); - - // - // Add an empty field element as specified in the JSON data. - // Check for the special case where field type is string, but there is - // a restriction of type 'option', in that case the field is SELECT. - // - fieldType = field.type; - if(fieldType == 'string' && field.restriction && field.restriction.type == 'option') { - fieldType = 'select'; - } - addFieldElement(field.name, fieldType); - - // - // Process the settings for the field that was created above and add - // the settings as a data attribute to each form field for later use. - // - if(field.restriction) { - $("#" + field.name).data("restriction", field.restriction); - } - if(field.scope) { - $("#" + field.name).data("scope", field.scope); - } - if(field.default) { - $("#" + field.name).data("default", field.default); - } - - // - // Check if there is documentation available for this field, and if there is then - // add it as a data attribute to the relevant form field for later reference, - // then add the html element. - // - if(json_config.documentation && json_config.documentation[field.name]) { - $("#" + field.name).data("documentation", json_config.documentation[field.name]); - addDocumentationElement(field.name); - } - - // - // Process this field's data and update the form element accordingly, - // taking into account all restrictions, dependencies and scope. - // - setFieldAttributes(field.name); - }); - - - // - // Adds the html header for a field. - // - function addFieldHeaderElement(fieldName, fieldDescription, fieldPosition = defaultFieldPosition) { - new_field = '<div class="mt-4" '; - new_field += 'id="' + fieldName + 'Header" '; - new_field += '><label for="'; - new_field += fieldName; - new_field += '" class="form-label"><b>'; - new_field += fieldName[0].toUpperCase() + fieldName.slice(1).replace(/_/g, ' '); - new_field += '</b></label><div id="'; - new_field += fieldName; - new_field += 'Help" class="form-text mt-0 mb-1">'; - new_field += fieldDescription; - new_field += '</div></div>'; - fieldPosition.append(new_field); - } - - // - // Adds an empty field element. - // - function addFieldElement(fieldName, fieldType, fieldPosition = defaultFieldPosition) { - new_field = '<div class="my-0" '; - new_field += 'id="' + fieldName + 'Element">'; - if(fieldType == 'select') { - new_field += '<select class="form-select" '; - } - else if(fieldType == 'number') { - new_field += '<input class="form-control" type="number" '; - } - else if(fieldType == 'string') { - new_field += '<input class="form-control" type="text" '; - } - - new_field += 'id="' + fieldName + '" '; - new_field += 'name="' + fieldName + '" '; - new_field += 'aria-describedby="' + fieldName + 'Help" >'; - - if(fieldType == 'select') { - new_field += '</select>'; - } - new_field += '</div>'; - fieldPosition.append(new_field); - } - - // - // Add an html documentation element. - // - function addDocumentationElement(fieldName, fieldPosition = defaultFieldPosition) { - new_field = '<div class="mt-1 mb-0" id="' + fieldName + '-documentation">'; - new_field += '<small class="text-body-secondary">'; - new_field += '<span id="' + fieldName + '-name"></span> '; - new_field += '<a id="' + fieldName + '-documentation-url" href="#" target="_blank">documentation</a> '; - new_field += '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="bi bi-box-arrow-up-right" viewBox="0 0 16 16">'; - new_field += '<path fill-rule="evenodd" d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5z"/>'; - new_field += '<path fill-rule="evenodd" d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0v-5z"/>'; - new_field += '</svg></small></div>'; - fieldPosition.append(new_field); - } - - // - // Process the field's data and update the form element accordingly, - // taking into account all restrictions, dependencies and scope. - // - function setFieldAttributes(fieldName) { - fieldElement = $("#" + fieldName); - - if(fieldElement.data("scope")) { - // - // The field has a specific scope, show or hide - // the field accordingly. - // - if(isFieldWithinScope(fieldElement.data("scope"))) { - $("#" + fieldName + "Header").show(); - $("#" + fieldName + "Element").show(); - } - else { - $("#" + fieldName + "Header").hide(); - $("#" + fieldName + "Element").hide(); - } - } - - restriction = fieldElement.data("restriction"); - if(restriction) { - // - // Process the field's restrictions. - // - restrictionType = restriction.type; - - // - // The field is a select field, delete all options present - // and then repopulate the field, taking into account restrictions - // and dependencies. - // - if(restrictionType == 'option') { - $("#" + fieldName).length = 0; - } - - if(restriction.value.depends_on) { - dependantFields = restriction.value.depends_on.fields; - dependantResolution = restriction.value.depends_on.resolution; - - $.each(dependantResolution, function(index, resolution) { - // Track the number of resolutions that are met - resolutionCount = 0; - for(i=0; i<resolution.key.length; i++) { - if(resolution.key[i] == document.getElementById(dependantFields[i]).value) { - // one resolution met - resolutionCount++; - } - else { - // a resolution not met, skip to next - // continue - return(true); - } - } - - if(resolutionCount == i) { - // All dependant resolutions met - if(restrictionType == 'range') { - fieldElement.attr("min", resolution.value[0]); - fieldElement.attr("max", resolution.value[1]); - fieldElement.val(resolution.value[0]); - } - else if(restrictionType == 'option') { - populateFieldElementOptions(fieldName, resolution.value); - } - // As all resolutions were met, no need to search further. - // break - return(false); - } - }); - } - else { - if(restrictionType == 'option') { - populateFieldElementOptions(fieldName, restriction.value); - } - else if (restrictionType == 'range'){ - fieldElement.attr("min", restriction.value[0]); - fieldElement.attr("max", restriction.value[1]); - fieldElement.val(restriction.value[0]); - } - } - } - - // Set defaut field value, if defined. - if(fieldElement.data("default")) { - fieldElement.val(fieldElement.data("default")); - } - - if(fieldElement.data("documentation")) { - // Show documentation if selected option has documentation available - updateDocumentation(fieldName); - } - } - - // - // Method to update link to documentation for selected option - // - function updateDocumentation(fieldName) { - let documentationFound = false; - $.each($("#" + fieldName).data("documentation"), function(key, val) { - if(document.getElementById(fieldName).value == key) { - $("#" + fieldName + "-name").text(key); - $("#" + fieldName + "-documentation-url").attr("href", val); - documentationFound = true; - // break - return(false); - } - }); - if(documentationFound) { - $("#" + fieldName + "-documentation").show(); - } - else { - $("#" + fieldName + "-documentation").hide(); - } - } - - // - // Method to pupolate the options of a select element - // - function populateFieldElementOptions(fieldName, fieldValues) { - sel_element = document.getElementById(fieldName); - sel_element.length = 0; - $.each(fieldValues, function(key, val) { - sel_element.options[sel_element.options.length] = new Option(val, val); - }); - } - - // - // Method to check if a field is within scope. - // - function isFieldWithinScope(fieldScope) { - let isWithinScope = false; - if(fieldScope) { - $.each(fieldScope, function(key, scope) { - $.each(scope.values, function(key, vals) { - let matchedVals = 0; - for(i = 0; i < vals.length; i++) { - if(vals[i] == document.getElementById(scope.fields[i]).value) { - matchedVals++; - } - else { - continue; - } - } - if(matchedVals == vals.length) { - isWithinScope = true; - return(false); - } - }) - }); - return(isWithinScope); - } - return(false); - } - - // - // Catch all changes to select elements and update other fields accordingly, - // based on the rules of dependencies, scope and available documentation - // - $(".form-select").on('change', function() { - changedField = this.name; - - // If field has documentation data, then it needs updating - if($("#" + changedField).data("documentation")) { - updateDocumentation(changedField); - } - - $.each(json_config.fields, function(key, field) { - if(field.scope) { - setFieldAttributes(field.name); - } - else if(field.restriction && field.restriction.value && field.restriction.value.depends_on) { - $.each(field.restriction.value.depends_on.fields, function(key, val) { - if(val == changedField) { - // Update the field, according to set dependencies - setFieldAttributes(field.name); - } - }); - } - }); - - }); - } - </script> - <style> - .form-control::placeholder { - color: var(--bs-dark-bg-subtle); - } - </style> -</head> -<body> - <nav class="navbar"> - <div class="container justify-content-center mt-3"> - <a class="navbar-brand" href="https://www.coe-raise.eu"> - <img src="/2021-01-Logo-RGB-RAISE_standard.png" - height="160" - alt="RAISE Logo" - loading="lazy" /> - </a> - </div> - </nav> - <div class="container my-5" style="max-width: 980px;"> - <div class="container justify-content-center"> - <h1 class="display-6" style="text-align:center; color: rgb(4,132,196);">Load AI Modules, Environments, and Containers (LAMEC) API</h1> - </div> - <br><br> - <?php - if(isset( $_POST['Submit'])) { - // Convert JSON confid settings to array - // $config = json_decode($config_json, true); - // Can be used if user input has to be verified - - - // - // Create JSON payload to pass on to LAMEC - // - $payload = array(); - foreach($_POST as $key => $val) { - if($val != "") { - $payload[$key] = $val; - } - } - $json_payload = json_encode($payload); - - putenv('PYTHONPATH="/var/www/apps/jsc/lamec"'); - $Phrase = "/var/www/apps/jsc/lamec/lamec_ml.py ".$json_payload; - $command = escapeshellcmd($Phrase); - $output = shell_exec($command); - ?> - <div class="mb-3"> - <label for="output" class="form-label"><b>Your start script:</b></label> - <textarea rows="20" class="form-control"><?=$output?></textarea> - </div> - <?php - } - else { - ?> - <form action="index.php", method="post"> - <div id="formFields"></div> - <button type="submit" name="Submit" class="btn btn-primary mt-5">Create start script</button> - </form> - <?php - } - ?> - <br> - </div> - <div class="container-fluid" style="background-color: rgb(158,196,243);"> - <div class="container justify-content-center" style="max-width: 980px; text-align:center;"> - <!-- If we want to show the menu links in the footer... - <ul class="nav justify-content-center pt-1 pb-3 mb-3"> - <li class="nav-item px-4"> - <a href="https://www.coe-raise.eu/contact" class="nav-link text-body-secondary">Contact</a> - </li> - <li class="nav-item px-4"> - <a href="https://www.coe-raise.eu/imprint" class="nav-link text-body-secondary">Imprint</a> - </li> - <li class="nav-item px-4"> - <a href="https://www.coe-raise.eu/privacy-policy" class="nav-link text-body-secondary">Privacy Policy</a> - </li> - </ul> - --> - <div class="py-3">The CoE RAISE project have received funding from the European Union’s Horizon 2020 – Research and Innovation Framework Programme H2020-INFRAEDI-2019-1 under grant agreement no. 951733</div> - <div class="py-2">©2021 CoE RAISE.</div> - </div> - </div> -</body> -</html> diff --git a/Web_App/index-thor.php b/Web_App/index-thor.php deleted file mode 100644 index 5533493..0000000 --- a/Web_App/index-thor.php +++ /dev/null @@ -1,80 +0,0 @@ -<?php - // For testing purposes, output all PHP errors - ini_set('display_errors', 1); - ini_set('display_startup_errors', 1); - error_reporting(E_ALL); -?> -<!doctype html> -<html> -<head> - <meta charset="utf-8"> - <meta name="viewport" content="width=device-width, initial-scale=1"> - <title>Load AI Modules, Environments, and Containers (LAMEC) API</title> - <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous"> - <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script> -</head> -<body> - <div class="container my-5"> - <h4>Load AI Modules, Environments, and Containers (LAMEC) API </h4> - <br><br> - <?php - if(isset( $_POST['Submit'])) { - putenv('PYTHONPATH="/var/www/apps/jsc/lamec-api"'); - $Phrase = "/domains/thorcurtis.com/public_html/test/lamec_ml.py gen -a " . $_POST["acc"] . " -n " . $_POST["nnodes"] . " -e " . $_POST["exe"] . " -sys " . $_POST["sys"] . " -sw " . $_POST["sw"]; - $command = escapeshellcmd($Phrase); - $output = shell_exec($command); - ?> - <div class="mb-3"> - <label for="output" class="form-label"><b>Your start script:</b></label> - <textarea rows="20" class="form-control"><?=$output?></textarea> - </div> - <?php - } - else { - ?> - <form action="index.php", method="post"> - <div class="mb-3"> - <label for="sys" class="form-label"><b>System/Partition</b></label> - <div id="sysHelp" class="form-text">Select the computing system on which you want to submit your job.</div> - <select name="sys" id="sys" class="form-control" aria-describedby="sysHelp"> - <option value="jureca">JURECA</option> - <option value="deep">DEEP</option> - <!-- - <option value="juwels">JUWELS</option> - --> - </select> - </div> - <div class="mb-3"> - <label for="sw" class="form-label"><b>Software</b></label> - <div id="swHelp" class="form-text">Select the software that your job depends on.</div> - <select name="sw" id="sw" class="form-control" aria-describedby="swHelp"> - <option value="ddp">Pytorch-DDP</option> - <option value="horovod">Horovod</option> - <option value="deepspeed">DeepSpeed</option> - <option value="heat">HeAT</option> - </select> - </div> - <div class="mb-3"> - <label for="exe" class="form-label"><b>Executable</b></label> - <div id="exeHelp" class="form-text">Specify the executable of your application.</div> - <input type="text" name="exe" placeholder="./executable" class="form-control" aria-describedby="exeHelp"> - </div> - <div class="mb-3"> - <label for="nnodes" class="form-label"><b>Number of nodes</b></label> - <div id="nnodesHelp" class="form-text">Specify the number of nodes.</div> - <input type="number" name="nnodes" min="1" value="1" max="2400" class="form-control" aria-describedby="nnodesHelp"> - </div> - <div class="mb-3"> - <label for="acc" class="form-label"><b>Account</b></label> - <div id="accHelp" class="form-text">Specify the account for your job.</div> - <input type="text" name="acc" placeholder="accountName" class="form-control" aria-describedby="accHelp"> - </div> - <button type="submit" name="Submit" class="btn btn-primary">Submit</button> - </form> - <?php - } - ?> - <br> - </div> -</body> -</html> \ No newline at end of file diff --git a/Web_App/index_tmp.php b/Web_App/index_tmp.php deleted file mode 100644 index 486c825..0000000 --- a/Web_App/index_tmp.php +++ /dev/null @@ -1,134 +0,0 @@ -<?php - // For testing purposes, output all PHP errors - ini_set('display_errors', 1); - ini_set('display_startup_errors', 1); - error_reporting(E_ALL); -?> -<!doctype html> -<html> -<head> - <meta charset="utf-8"> - <meta name="viewport" content="width=device-width, initial-scale=1"> - <title>Load AI Modules, Environments, and Containers (LAMEC) API</title> - <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous"> -<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script> - <script> - var sys_Object = { - "JURECA": { - "dc-gpu": ["Pytorch-DDP", "Horovod", "DeepSpeed", "HeAT"], - "dc-gpu-devel":["Pytorch-DDP", "Horovod", "DeepSpeed", "HeAT"] - }, - "DEEP": { - "dp-esb": ["Pytorch-DDP", "Horovod", "DeepSpeed", "HeAT"], - "dp-dam":["Pytorch-DDP", "Horovod", "DeepSpeed", "HeAT"] - }, - "JUWELS": { - "develbooster": ["Pytorch-DDP"], - "develgpus":["Pytorch-DDP"], - "gpus":["Pytorch-DDP"] - }, - "LUMI": { - "dev-g": ["Pytorch-DDP"], - "small-g":["Pytorch-DDP"], - "standard-g":["Pytorch-DDP"] - }, - "VEGA": { - "cpu": ["Basilisk"] - } - } - -window.onload = function() { - var sys_sel = document.getElementById("sys"); - var par_sel = document.getElementById("par"); - var sw_sel = document.getElementById("sw"); - for (var x in sys_Object) { - sys_sel.options[sys_sel.options.length] = new Option(x, x); - } - sys_sel.onchange = function() { - //empty par and sw dropdowns - par_sel.length = 1; - sw_sel.length = 1; - //display correct values - for (var y in sys_Object[this.value]) { - par_sel.options[par_sel.options.length] = new Option(y, y); - } - } - par_sel.onchange = function() { - //empty sw dropdown - sw_sel.length = 1; - //display correct values - var z = sys_Object[sys_sel.value][this.value]; - for (var i = 0; i < z.length; i++) { - sw_sel.options[sw_sel.options.length] = new Option(z[i], z[i]); - } - } -} -</script> -</head> -<body> - <div class="container my-5"> - <h4>Load AI Modules, Environments, and Containers (LAMEC) API </h4> - <br><br> - <?php - if(isset( $_POST['Submit'])) { - putenv('PYTHONPATH="/var/www/apps/jsc/lamec"'); - $Phrase = "/var/www/apps/jsc/lamec/lamec_ml.py gen -a " . $_POST["acc"] . " -par " . $_POST["par"] . " -n " . $_POST["nnodes"] . " -e " . $_POST["exe"] . " -sys " . $_POST["sys"] . " -sw " . $_POST["sw"]; - $command = escapeshellcmd($Phrase); - $output = shell_exec($command); - ?> - <div class="mb-3"> - <label for="output" class="form-label"><b>Your start script:</b></label> - <textarea rows="20" class="form-control"><?=$output?></textarea> - </div> - <?php - } - else { - ?> - <form action="index.php", method="post"> - <div class="mb-3"> - <label for="sys" class="form-label"><b>System</b></label> - <div id="sysHelp" class="form-text">Select the computing system on which you want to submit your job.</div> - <select name="sys" id="sys" class="form-control" aria-describedby="sysHelp"> - <option value="" selected="selected">Please select system</option> - </select> - </div> - - <div class="mb-3"> - <label for="par" class="form-label"><b>Partition</b></label> - <div id="sysHelp" class="form-text">Select the partition on which you want to submit your job.</div> - <select name="par" id="par" class="form-control" aria-describedby="sysHelp"> - <option value="" selected="selected">please select partition</option> - </select> - </div> - - <div class="mb-3"> - <label for="sw" class="form-label"><b>Software</b></label> - <div id="swHelp" class="form-text">Select the software that your job depends on.</div> - <select name="sw" id="sw" class="form-control" aria-describedby="swHelp"> - <option value="" selected="selected"> Please select software</option> - </select> - </div> - <div class="mb-3"> - <label for="exe" class="form-label"><b>Executable</b></label> - <div id="exeHelp" class="form-text">Specify the executable of your application.</div> - <input type="text" name="exe" placeholder="./executable" class="form-control" aria-describedby="exeHelp"> - </div> - <div class="mb-3"> - <label for="nnodes" class="form-label"><b>Number of nodes</b></label> - <div id="nnodesHelp" class="form-text">Specify the number of nodes.</div> - <input type="number" name="nnodes" min="1" value="1" max="2400" class="form-control" aria-describedby="nnodesHelp"> - </div> - <div class="mb-3"> - <label for="acc" class="form-label"><b>Account</b></label> - <div id="accHelp" class="form-text">Specify the account for your job.</div> - <input type="text" name="acc" placeholder="accountName" class="form-control" aria-describedby="accHelp"> - </div> - <button type="submit" name="Submit" class="btn btn-primary">Submit</button> - </form> - <?php - } - ?> - <br> - </div> -</body> -</html> diff --git a/Web_App/index_v1.php b/Web_App/index_v1.php deleted file mode 100644 index 122b545..0000000 --- a/Web_App/index_v1.php +++ /dev/null @@ -1,57 +0,0 @@ -<html> -<body> - -<h1>Load AI Modules, Environments, and Containers (LAMEC) API </h1> - - <form action="index.php", method="post"> - <label for="sys">System/Partition:</label> - <select name="sys" id="sys"> - <option value="jureca">JURECA</option> - <option value="deep">DEEP</option> - <!-- - <option value="juwels">JUWELS</option> - --> - </select> - <br><br> - <label for="sw">Software:</label> - <select name="sw" id="sw"> - <option value="ddp">Pytorch-DDP</option> - <option value="horovod">Horovod</option> - <option value="deepspeed">DeepSpeed</option> - <option value="heat">HeAT</option> - </select> - <br><br> - Executable: <input type="text" name="exe"> - <br><br> - number of nodes: <input type="text" name="nnodes"> - <br><br> - Account: <input type="text" name="acc"> - <br><br> - <!-- - Wall time: <input type="text" name = "wtime"> - <br><br> - <input type="submit"> - --> - <input name="Submit" type="submit" class="submitbtn" value="Submit" /> - </form> - <br> -<?php - -//ini_set('display_errors', 1); -//ini_set('display_startup_errors', 1); -//error_reporting(E_ALL); -putenv('PYTHONPATH="/var/www/apps/jsc/lamec-api"'); - -if (isset( $_POST['Submit'])) -{ -$Phrase = "/var/www/apps/jsc/lamec-api/lamec_ml.py gen -a " . $_POST["acc"] . " -n " . $_POST["nnodes"] . " -e " . $_POST["exe"] . " -sys " . $_POST["sys"] . " -sw " . $_POST["sw"]; -//echo $Phrase; -$command = escapeshellcmd($Phrase); -$output = shell_exec($command); -echo $output; -} - -?> - -</body> -</html> diff --git a/about.php b/about.php new file mode 100644 index 0000000..fc2df89 --- /dev/null +++ b/about.php @@ -0,0 +1,6 @@ +<?php +include 'render.php'; +$active = 'about'; +$content = loadFragment('html/about.html'); +include 'base.php'; +?> diff --git a/base.php b/base.php new file mode 100644 index 0000000..5897afa --- /dev/null +++ b/base.php @@ -0,0 +1,145 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <meta name="viewport" + content="width=device-width, initial-scale=1, shrink-to-fit=no"> + + <!-- LOAD Bootstrap CSS --> + <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" + rel="stylesheet" + integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" + crossorigin="anonymous"> + + <?php if ($active == 'status'): ?> + <link rel="stylesheet" + href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" + crossorigin="anonymous"> + <?php endif; ?> + + <style> + .h-color { + color: rgb(4,132,196); + } + .w-980p { + max-width: 980px; + } + #jobscript-output { + font-family: monospace, monospace; + } + </style> + + <?php if ($active == 'form' || $active == 'status'): ?> + <!-- LOAD jQuery --> + <script src="https://code.jquery.com/jquery-3.7.1.min.js" + integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" + crossorigin="anonymous"> + </script> + <?php endif; ?> + + <!-- LOAD Popper --> + <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" + integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r" + crossorigin="anonymous"> + </script> + + <!-- LOAD Bootstrap JS --> + <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js" + integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy" + crossorigin="anonymous"> + </script> + + <?php if ($active == 'form'): ?> + <script src="js/form.js"></script> + <?php endif; ?> + + <?php if ($active == 'status'): ?> + <script src="js/status.js"></script> + <?php endif; ?> + + </head> + + <body class="d-flex flex-column min-vh-100"> + + <!-- BEGIN Header --> + <header> + + <!-- BEGIN Logo --> + <div class="container text-center mt-4 mb-4"> + <a href="https://www.coe-raise.eu" target="_blank"> + <img src="images/logo.png" + height="160" + alt="Raise Logo" + loading="lazy"> + </a> + </div> + <!-- END Logo --> + + <!-- BEGIN Navigation Bar --> + <nav class="navbar navbar-light navbar-expand-sm navbar-light bg-light"> + <div class="container justify-content-center w-980p"> + <a href="index.php" class="navbar-brand h-color"> + LAMEC + </a> + <button class="navbar-toggler" + type="button" + data-bs-toggle="collapse" + data-bs-target="#navbarNav" + aria-controls="navbarNav" + aria-expanded="false" + aria-label="Toggle navigation"> + <span class="navbar-toggler-icon"></span> + </button> + <div class="collapse navbar-collapse flex-grow-0" id="navbarNav"> + <ul class="navbar-nav"> + <li class="nav-item"> + <a href="index.php" + class="nav-link <?php echo ($active == 'form' ? 'active' : ''); ?>"> + Jobscript Generator + </a> + </li> + <li class="nav-item"> + <a href="status.php" + class="nav-link <?php echo ($active == 'status' ? 'active' : ''); ?>"> + Status + </a> + </li> + <li class="nav-item"> + <a href="about.php" + class="nav-link <?php echo ($active == 'about' ? 'active' : ''); ?>"> + About + </a> + </li> + </ul> + </div> + </div> + </nav> + <!-- END Navigation Bar --> + + </header> + <!-- END Header --> + + <!-- BEGIN Main Section --> + <main id="main-section" class="container my-4 w-980p"> + <?php echo $content ?> + </main> + <!-- END Main Section --> + + <!-- BEGIN Footer --> + <footer class="mt-auto py-3 bg-light"> + <div class="container text-center justify-content-center w-980p"> + <span class="text-body-secondary"> + <div class="py-3"> + The + <a href="https://www.coe-raise.eu" target"_blank"> + CoE RAISE project + </a> + has received funding from the European Union’s Horizon 2020 – Research and Innovation Framework Programme H2020-INFRAEDI-2019-1 under grant agreement no. 951733</div> + <div class="py-2">©2021 CoE RAISE.</div> + </span> + </div> + </footer> + <!-- END Footer --> + + </body> +</html> diff --git a/data/form-schema.json b/data/form-schema.json new file mode 100644 index 0000000..1cb0832 --- /dev/null +++ b/data/form-schema.json @@ -0,0 +1,747 @@ +{ + "fields": [ + { + "name": "system", + "desc": "Select the computing system on which you want to submit your job.", + "type": "string", + "restriction": { + "type": "option", + "value": [ + "Cyclone", + "DEEP", + "JURECA", + "JUWELS", + "LUMI", + "MockHPCSystem", + "VEGA" + ] + } + }, + { + "name": "software", + "desc": "Select the software your job depends on.", + "type": "string", + "restriction": { + "type": "option", + "value": { + "depends_on": { + "fields": [ + "system" + ], + "resolution": [ + { + "key": [ + "VEGA" + ], + "value": [ + "Basilisk" + ] + }, + { + "key": [ + "MockHPCSystem" + ], + "value": [ + "MockSoftware" + ] + }, + { + "key": [ + "Cyclone" + ], + "value": [ + "Basilisk", + "Horovod" + ] + }, + { + "key": [ + "JUWELS" + ], + "value": [ + "HeAT", + "Horovod", + "Pytorch-DDP", + "DeepSpeed" + ] + }, + { + "key": [ + "LUMI" + ], + "value": [ + "HeAT", + "Pytorch-DDP" + ] + }, + { + "key": [ + "JURECA" + ], + "value": [ + "HeAT", + "Horovod", + "Pytorch-DDP", + "DeepSpeed" + ] + }, + { + "key": [ + "DEEP" + ], + "value": [ + "HeAT", + "Horovod", + "Pytorch-DDP", + "DeepSpeed" + ] + } + ] + } + } + } + }, + { + "name": "partition", + "desc": "Select the partition for your job.", + "type": "string", + "restriction": { + "type": "option", + "value": { + "depends_on": { + "fields": [ + "system" + ], + "resolution": [ + { + "key": [ + "VEGA" + ], + "value": [ + "dev", + "cpu", + "longcpu", + "gpu", + "largemem" + ] + }, + { + "key": [ + "MockHPCSystem" + ], + "value": [ + "base" + ] + }, + { + "key": [ + "Cyclone" + ], + "value": [ + "milan", + "skylake", + "nehalem", + "cpu", + "p100", + "a100", + "gpu" + ] + }, + { + "key": [ + "JUWELS" + ], + "value": [ + "batch", + "mem192", + "devel", + "gpus", + "develgpus", + "booster", + "develbooster" + ] + }, + { + "key": [ + "LUMI" + ], + "value": [ + "standard-g", + "standard", + "dev-g", + "debug", + "small-g", + "small", + "largemem" + ] + }, + { + "key": [ + "JURECA" + ], + "value": [ + "dc-cpu", + "dc-cpu-bigmem", + "dc-cpu-devel", + "dc-gpu", + "dc-gpu-devel" + ] + }, + { + "key": [ + "DEEP" + ], + "value": [ + "dp-esb", + "dp-dam" + ] + } + ] + } + } + } + }, + { + "name": "nodes", + "desc": "Select the number of nodes.", + "type": "number", + "restriction": { + "type": "range", + "value": { + "depends_on": { + "fields": [ + "system", + "partition" + ], + "resolution": [ + { + "key": [ + "VEGA", + "dev" + ], + "value": [ + 1, + 8 + ] + }, + { + "key": [ + "VEGA", + "cpu" + ], + "value": [ + 1, + 960 + ] + }, + { + "key": [ + "VEGA", + "longcpu" + ], + "value": [ + 1, + 6 + ] + }, + { + "key": [ + "VEGA", + "gpu" + ], + "value": [ + 1, + 60 + ] + }, + { + "key": [ + "VEGA", + "largemem" + ], + "value": [ + 1, + 192 + ] + }, + { + "key": [ + "MockHPCSystem", + "base" + ], + "value": [ + 1, + 10 + ] + }, + { + "key": [ + "Cyclone", + "milan" + ], + "value": [ + 1, + 34 + ] + }, + { + "key": [ + "Cyclone", + "skylake" + ], + "value": [ + 1, + 3 + ] + }, + { + "key": [ + "Cyclone", + "nehalem" + ], + "value": [ + 1, + 3 + ] + }, + { + "key": [ + "Cyclone", + "cpu" + ], + "value": [ + 1, + 13 + ] + }, + { + "key": [ + "Cyclone", + "p100" + ], + "value": [ + 1, + 8 + ] + }, + { + "key": [ + "Cyclone", + "a100" + ], + "value": [ + 1, + 6 + ] + }, + { + "key": [ + "Cyclone", + "gpu" + ], + "value": [ + 1, + 16 + ] + }, + { + "key": [ + "JUWELS", + "batch" + ], + "value": [ + 1, + 1024 + ] + }, + { + "key": [ + "JUWELS", + "mem192" + ], + "value": [ + 1, + 64 + ] + }, + { + "key": [ + "JUWELS", + "devel" + ], + "value": [ + 1, + 8 + ] + }, + { + "key": [ + "JUWELS", + "gpus" + ], + "value": [ + 1, + 46 + ] + }, + { + "key": [ + "JUWELS", + "develgpus" + ], + "value": [ + 1, + 2 + ] + }, + { + "key": [ + "JUWELS", + "booster" + ], + "value": [ + 1, + 384 + ] + }, + { + "key": [ + "JUWELS", + "develbooster" + ], + "value": [ + 1, + 4 + ] + }, + { + "key": [ + "LUMI", + "standard-g" + ], + "value": [ + 1, + 1024 + ] + }, + { + "key": [ + "LUMI", + "standard" + ], + "value": [ + 1, + 512 + ] + }, + { + "key": [ + "LUMI", + "dev-g" + ], + "value": [ + 1, + 32 + ] + }, + { + "key": [ + "LUMI", + "debug" + ], + "value": [ + 1, + 4 + ] + }, + { + "key": [ + "LUMI", + "small-g" + ], + "value": [ + 1, + 4 + ] + }, + { + "key": [ + "LUMI", + "small" + ], + "value": [ + 1, + 4 + ] + }, + { + "key": [ + "LUMI", + "largemem" + ], + "value": [ + 1, + 1 + ] + }, + { + "key": [ + "JURECA", + "dc-cpu" + ], + "value": [ + 1, + 128 + ] + }, + { + "key": [ + "JURECA", + "dc-cpu-bigmem" + ], + "value": [ + 1, + 48 + ] + }, + { + "key": [ + "JURECA", + "dc-cpu-devel" + ], + "value": [ + 1, + 4 + ] + }, + { + "key": [ + "JURECA", + "dc-gpu" + ], + "value": [ + 1, + 24 + ] + }, + { + "key": [ + "JURECA", + "dc-gpu-devel" + ], + "value": [ + 1, + 4 + ] + }, + { + "key": [ + "DEEP", + "dp-esb" + ], + "value": [ + 1, + 75 + ] + }, + { + "key": [ + "DEEP", + "dp-dam" + ], + "value": [ + 1, + 16 + ] + } + ] + } + } + } + }, + { + "name": "account", + "desc": "Specify the account for your job.", + "type": "string", + "default": "Account", + "scope": [ + { + "fields": [ + "system", + "software" + ], + "values": [ + [ + "VEGA", + "Basilisk" + ], + [ + "MockHPCSystem", + "MockSoftware" + ], + [ + "Cyclone", + "Basilisk" + ], + [ + "Cyclone", + "Horovod" + ], + [ + "JUWELS", + "HeAT" + ], + [ + "JUWELS", + "Horovod" + ], + [ + "JUWELS", + "Pytorch-DDP" + ], + [ + "JUWELS", + "DeepSpeed" + ], + [ + "LUMI", + "HeAT" + ], + [ + "JURECA", + "HeAT" + ], + [ + "JURECA", + "Horovod" + ], + [ + "JURECA", + "Pytorch-DDP" + ], + [ + "JURECA", + "DeepSpeed" + ], + [ + "DEEP", + "HeAT" + ], + [ + "DEEP", + "Horovod" + ], + [ + "DEEP", + "Pytorch-DDP" + ], + [ + "DEEP", + "DeepSpeed" + ] + ] + } + ] + }, + { + "name": "executable", + "desc": "Specify an executable for your job.", + "type": "string", + "default": "app", + "scope": [ + { + "fields": [ + "system", + "software" + ], + "values": [ + [ + "VEGA", + "Basilisk" + ], + [ + "MockHPCSystem", + "MockSoftware" + ], + [ + "Cyclone", + "Basilisk" + ], + [ + "Cyclone", + "Horovod" + ], + [ + "JUWELS", + "HeAT" + ], + [ + "JUWELS", + "Horovod" + ], + [ + "JUWELS", + "Pytorch-DDP" + ], + [ + "JUWELS", + "DeepSpeed" + ], + [ + "LUMI", + "HeAT" + ], + [ + "JURECA", + "HeAT" + ], + [ + "JURECA", + "Horovod" + ], + [ + "JURECA", + "Pytorch-DDP" + ], + [ + "JURECA", + "DeepSpeed" + ], + [ + "DEEP", + "HeAT" + ], + [ + "DEEP", + "Horovod" + ], + [ + "DEEP", + "Pytorch-DDP" + ], + [ + "DEEP", + "DeepSpeed" + ] + ] + } + ] + } + ], + "documentation": { + "system": { + "JURECA": "https://apps.fz-juelich.de/jsc/hps/jureca/index.html", + "DEEP": "https://deeptrac.zam.kfa-juelich.de:8443/trac/wiki/Public/User_Guide", + "JUWELS": "https://apps.fz-juelich.de/jsc/hps/juwels/index.html", + "LUMI": "https://docs.lumi-supercomputer.eu/software/", + "VEGA": "https://doc.vega.izum.si", + "Cyclone": "https://hpcf.cyi.ac.cy/documentation/" + }, + "software": { + "Pytorch-DDP": "https://pytorch.org/tutorials/intermediate/ddp_tutorial.html", + "Horovod": "https://horovod.readthedocs.io/en/stable/", + "DeepSpeed": "https://deepspeed.readthedocs.io/en/latest/", + "HeAT": "https://heat.readthedocs.io/en/stable/" + } + } +} \ No newline at end of file diff --git a/data/form-schema.json_ b/data/form-schema.json_ new file mode 100644 index 0000000..1cb0832 --- /dev/null +++ b/data/form-schema.json_ @@ -0,0 +1,747 @@ +{ + "fields": [ + { + "name": "system", + "desc": "Select the computing system on which you want to submit your job.", + "type": "string", + "restriction": { + "type": "option", + "value": [ + "Cyclone", + "DEEP", + "JURECA", + "JUWELS", + "LUMI", + "MockHPCSystem", + "VEGA" + ] + } + }, + { + "name": "software", + "desc": "Select the software your job depends on.", + "type": "string", + "restriction": { + "type": "option", + "value": { + "depends_on": { + "fields": [ + "system" + ], + "resolution": [ + { + "key": [ + "VEGA" + ], + "value": [ + "Basilisk" + ] + }, + { + "key": [ + "MockHPCSystem" + ], + "value": [ + "MockSoftware" + ] + }, + { + "key": [ + "Cyclone" + ], + "value": [ + "Basilisk", + "Horovod" + ] + }, + { + "key": [ + "JUWELS" + ], + "value": [ + "HeAT", + "Horovod", + "Pytorch-DDP", + "DeepSpeed" + ] + }, + { + "key": [ + "LUMI" + ], + "value": [ + "HeAT", + "Pytorch-DDP" + ] + }, + { + "key": [ + "JURECA" + ], + "value": [ + "HeAT", + "Horovod", + "Pytorch-DDP", + "DeepSpeed" + ] + }, + { + "key": [ + "DEEP" + ], + "value": [ + "HeAT", + "Horovod", + "Pytorch-DDP", + "DeepSpeed" + ] + } + ] + } + } + } + }, + { + "name": "partition", + "desc": "Select the partition for your job.", + "type": "string", + "restriction": { + "type": "option", + "value": { + "depends_on": { + "fields": [ + "system" + ], + "resolution": [ + { + "key": [ + "VEGA" + ], + "value": [ + "dev", + "cpu", + "longcpu", + "gpu", + "largemem" + ] + }, + { + "key": [ + "MockHPCSystem" + ], + "value": [ + "base" + ] + }, + { + "key": [ + "Cyclone" + ], + "value": [ + "milan", + "skylake", + "nehalem", + "cpu", + "p100", + "a100", + "gpu" + ] + }, + { + "key": [ + "JUWELS" + ], + "value": [ + "batch", + "mem192", + "devel", + "gpus", + "develgpus", + "booster", + "develbooster" + ] + }, + { + "key": [ + "LUMI" + ], + "value": [ + "standard-g", + "standard", + "dev-g", + "debug", + "small-g", + "small", + "largemem" + ] + }, + { + "key": [ + "JURECA" + ], + "value": [ + "dc-cpu", + "dc-cpu-bigmem", + "dc-cpu-devel", + "dc-gpu", + "dc-gpu-devel" + ] + }, + { + "key": [ + "DEEP" + ], + "value": [ + "dp-esb", + "dp-dam" + ] + } + ] + } + } + } + }, + { + "name": "nodes", + "desc": "Select the number of nodes.", + "type": "number", + "restriction": { + "type": "range", + "value": { + "depends_on": { + "fields": [ + "system", + "partition" + ], + "resolution": [ + { + "key": [ + "VEGA", + "dev" + ], + "value": [ + 1, + 8 + ] + }, + { + "key": [ + "VEGA", + "cpu" + ], + "value": [ + 1, + 960 + ] + }, + { + "key": [ + "VEGA", + "longcpu" + ], + "value": [ + 1, + 6 + ] + }, + { + "key": [ + "VEGA", + "gpu" + ], + "value": [ + 1, + 60 + ] + }, + { + "key": [ + "VEGA", + "largemem" + ], + "value": [ + 1, + 192 + ] + }, + { + "key": [ + "MockHPCSystem", + "base" + ], + "value": [ + 1, + 10 + ] + }, + { + "key": [ + "Cyclone", + "milan" + ], + "value": [ + 1, + 34 + ] + }, + { + "key": [ + "Cyclone", + "skylake" + ], + "value": [ + 1, + 3 + ] + }, + { + "key": [ + "Cyclone", + "nehalem" + ], + "value": [ + 1, + 3 + ] + }, + { + "key": [ + "Cyclone", + "cpu" + ], + "value": [ + 1, + 13 + ] + }, + { + "key": [ + "Cyclone", + "p100" + ], + "value": [ + 1, + 8 + ] + }, + { + "key": [ + "Cyclone", + "a100" + ], + "value": [ + 1, + 6 + ] + }, + { + "key": [ + "Cyclone", + "gpu" + ], + "value": [ + 1, + 16 + ] + }, + { + "key": [ + "JUWELS", + "batch" + ], + "value": [ + 1, + 1024 + ] + }, + { + "key": [ + "JUWELS", + "mem192" + ], + "value": [ + 1, + 64 + ] + }, + { + "key": [ + "JUWELS", + "devel" + ], + "value": [ + 1, + 8 + ] + }, + { + "key": [ + "JUWELS", + "gpus" + ], + "value": [ + 1, + 46 + ] + }, + { + "key": [ + "JUWELS", + "develgpus" + ], + "value": [ + 1, + 2 + ] + }, + { + "key": [ + "JUWELS", + "booster" + ], + "value": [ + 1, + 384 + ] + }, + { + "key": [ + "JUWELS", + "develbooster" + ], + "value": [ + 1, + 4 + ] + }, + { + "key": [ + "LUMI", + "standard-g" + ], + "value": [ + 1, + 1024 + ] + }, + { + "key": [ + "LUMI", + "standard" + ], + "value": [ + 1, + 512 + ] + }, + { + "key": [ + "LUMI", + "dev-g" + ], + "value": [ + 1, + 32 + ] + }, + { + "key": [ + "LUMI", + "debug" + ], + "value": [ + 1, + 4 + ] + }, + { + "key": [ + "LUMI", + "small-g" + ], + "value": [ + 1, + 4 + ] + }, + { + "key": [ + "LUMI", + "small" + ], + "value": [ + 1, + 4 + ] + }, + { + "key": [ + "LUMI", + "largemem" + ], + "value": [ + 1, + 1 + ] + }, + { + "key": [ + "JURECA", + "dc-cpu" + ], + "value": [ + 1, + 128 + ] + }, + { + "key": [ + "JURECA", + "dc-cpu-bigmem" + ], + "value": [ + 1, + 48 + ] + }, + { + "key": [ + "JURECA", + "dc-cpu-devel" + ], + "value": [ + 1, + 4 + ] + }, + { + "key": [ + "JURECA", + "dc-gpu" + ], + "value": [ + 1, + 24 + ] + }, + { + "key": [ + "JURECA", + "dc-gpu-devel" + ], + "value": [ + 1, + 4 + ] + }, + { + "key": [ + "DEEP", + "dp-esb" + ], + "value": [ + 1, + 75 + ] + }, + { + "key": [ + "DEEP", + "dp-dam" + ], + "value": [ + 1, + 16 + ] + } + ] + } + } + } + }, + { + "name": "account", + "desc": "Specify the account for your job.", + "type": "string", + "default": "Account", + "scope": [ + { + "fields": [ + "system", + "software" + ], + "values": [ + [ + "VEGA", + "Basilisk" + ], + [ + "MockHPCSystem", + "MockSoftware" + ], + [ + "Cyclone", + "Basilisk" + ], + [ + "Cyclone", + "Horovod" + ], + [ + "JUWELS", + "HeAT" + ], + [ + "JUWELS", + "Horovod" + ], + [ + "JUWELS", + "Pytorch-DDP" + ], + [ + "JUWELS", + "DeepSpeed" + ], + [ + "LUMI", + "HeAT" + ], + [ + "JURECA", + "HeAT" + ], + [ + "JURECA", + "Horovod" + ], + [ + "JURECA", + "Pytorch-DDP" + ], + [ + "JURECA", + "DeepSpeed" + ], + [ + "DEEP", + "HeAT" + ], + [ + "DEEP", + "Horovod" + ], + [ + "DEEP", + "Pytorch-DDP" + ], + [ + "DEEP", + "DeepSpeed" + ] + ] + } + ] + }, + { + "name": "executable", + "desc": "Specify an executable for your job.", + "type": "string", + "default": "app", + "scope": [ + { + "fields": [ + "system", + "software" + ], + "values": [ + [ + "VEGA", + "Basilisk" + ], + [ + "MockHPCSystem", + "MockSoftware" + ], + [ + "Cyclone", + "Basilisk" + ], + [ + "Cyclone", + "Horovod" + ], + [ + "JUWELS", + "HeAT" + ], + [ + "JUWELS", + "Horovod" + ], + [ + "JUWELS", + "Pytorch-DDP" + ], + [ + "JUWELS", + "DeepSpeed" + ], + [ + "LUMI", + "HeAT" + ], + [ + "JURECA", + "HeAT" + ], + [ + "JURECA", + "Horovod" + ], + [ + "JURECA", + "Pytorch-DDP" + ], + [ + "JURECA", + "DeepSpeed" + ], + [ + "DEEP", + "HeAT" + ], + [ + "DEEP", + "Horovod" + ], + [ + "DEEP", + "Pytorch-DDP" + ], + [ + "DEEP", + "DeepSpeed" + ] + ] + } + ] + } + ], + "documentation": { + "system": { + "JURECA": "https://apps.fz-juelich.de/jsc/hps/jureca/index.html", + "DEEP": "https://deeptrac.zam.kfa-juelich.de:8443/trac/wiki/Public/User_Guide", + "JUWELS": "https://apps.fz-juelich.de/jsc/hps/juwels/index.html", + "LUMI": "https://docs.lumi-supercomputer.eu/software/", + "VEGA": "https://doc.vega.izum.si", + "Cyclone": "https://hpcf.cyi.ac.cy/documentation/" + }, + "software": { + "Pytorch-DDP": "https://pytorch.org/tutorials/intermediate/ddp_tutorial.html", + "Horovod": "https://horovod.readthedocs.io/en/stable/", + "DeepSpeed": "https://deepspeed.readthedocs.io/en/latest/", + "HeAT": "https://heat.readthedocs.io/en/stable/" + } + } +} \ No newline at end of file diff --git a/data/status.json b/data/status.json new file mode 100644 index 0000000..3d7e29e --- /dev/null +++ b/data/status.json @@ -0,0 +1,61 @@ +{ + "Cyclone": { + "Basilisk": { + }, + "Horovod": { + "passed": true + } + }, + "DEEP": { + "DeepSpeed": { + "passed": true + }, + "HeAT": { + "passed": true + }, + "Horovod": { + "passed": true + }, + "Pytorch-DDP": { + "passed": true + } + }, + "JUWELS": { + "DeepSpeed": { + "passed": true + }, + "HeAT": { + "passed": false + }, + "Horovod": { + "passed": true + }, + "Pytorch-DDP": { + "passed": true + } + }, + "JURECA": { + "DeepSpeed": { + "passed": true + }, + "HeAT": { + "passed": true + }, + "Horovod": { + "passed": true + }, + "Pytorch-DDP": { + "passed": true + } + }, + "LUMI": { + "Pytorch-DDP": { + "passed": true + } + }, + "VEGA": { + "Basilisk": { + "passed": true + } + } +} diff --git a/html/about.html b/html/about.html new file mode 100644 index 0000000..5cdabba --- /dev/null +++ b/html/about.html @@ -0,0 +1,31 @@ +<h1 class="display-6 mt-4 mb-5 h-color"> + <b>L</b>oad + <b>A</b>I + <b>M</b>odules, + <b>E</b>nvironments & + <b>C</b>ontainers<br> +</h1> +<section> + <p>The <strong>LAMEC</strong> API ( + <b>L</b>oad + <b>A</b>I + <b>M</b>odules, + <b>E</b>nvironments and + <b>C</b>ontainers) + is a tool that allows HPC developers and researchers to share their + configurations, setups and jobscripts of commonly used frameworks + and libraries, so they can be used to generate up to date + jobscripts. Take a look at the + <a href="index.php">jobscript generator</a> on this site + to see what systems and software are available. If you want to + contribute or learn about the structure of the project, please + read the + <a href="https://gitlab.jsc.fz-juelich.de/CoE-RAISE/FZJ/lamec-oa/-/wikis/LAMEC-Project-Overview" target="_blank"> + wiki + </a> + in the GitLab + <a href="https://gitlab.jsc.fz-juelich.de/CoE-RAISE/FZJ/lamec-oa" target="_blank"> + repository. + </a> + </p> +</section> diff --git a/html/form.html b/html/form.html new file mode 100644 index 0000000..da453fa --- /dev/null +++ b/html/form.html @@ -0,0 +1,6 @@ +<form action="index.php", method="post"> + <div id="formFields"></div> + <button type="submit" name="Submit" class="btn btn-primary mt-5"> + Generate jobscript + </button> +</form> diff --git a/html/output.html b/html/output.html new file mode 100644 index 0000000..01014a4 --- /dev/null +++ b/html/output.html @@ -0,0 +1,4 @@ +<div class="mb-3"> + <label for="output" class="form-label"><b>Your start script:</b></label> + <textarea id="jobscript-output" rows="20" class="form-control"><?php echo $output ?></textarea> +</div> diff --git a/html/status.html b/html/status.html new file mode 100644 index 0000000..793cba6 --- /dev/null +++ b/html/status.html @@ -0,0 +1,12 @@ +<table class="table table-striped table-bordered table-hover my-5"> + <thead> + <tr> + <th scope="col">System</th> + <th scope="col">Software</th> + <th scope="col">Status</th> + </tr> + </thead> + <tbody id="table-body"> + </tbody> +</table> + diff --git a/2021-01-Logo-RGB-RAISE_standard.png b/images/logo.png similarity index 100% rename from 2021-01-Logo-RGB-RAISE_standard.png rename to images/logo.png diff --git a/index.php b/index.php index 143a6a8..97e4c9c 100644 --- a/index.php +++ b/index.php @@ -1,416 +1,32 @@ <?php - // For testing purposes, output all PHP errors - ini_set('display_errors', 1); - ini_set('display_startup_errors', 1); - error_reporting(E_ALL); - - // - // Read configuration data from JSON file - // (TBD get output from LAMEC) - // - $filename = "form_schema.json"; - $config_json = file_get_contents($filename); - // Remove line breaks so the JSON can be embedded as a string in the JS code - $config_json_str = json_encode($config_json); +include 'render.php'; + +if(isset( $_POST['Submit'])) { + // Convert JSON confid settings to array + // $config = json_decode($config_json, true); + // Can be used if user input has to be verified + + // + // Create JSON payload to pass on to LAMEC + // + $payload = array(); + foreach($_POST as $key => $val) { + if($val != "") { + $payload[$key] = $val; + } + } + $json_payload = json_encode($payload); + + // putenv('PYTHONPATH="/var/www/apps/jsc/lamec"'); + $Phrase = "./lamec.py '$json_payload'"; + // $command = escapeshellcmd($Phrase); + $output = shell_exec($Phrase); + $active = 'form'; + $content = loadFragment('html/output.html', ['output' => $output]); + include 'base.php'; +} else { + $active = 'form'; + $content = loadFragment('html/form.html'); + include 'base.php'; +} ?> -<!doctype html> -<html> -<head> - <meta charset="utf-8"> - <meta name="viewport" content="width=device-width, initial-scale=1"> - <title>Load AI Modules, Environments, and Containers (LAMEC) API</title> - <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous"> - <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script> - <script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script> - - <script> - window.onload = function() { - // All configuration settings from LAMEC - var json_config = JSON.parse(<?=$config_json_str?>); - - // Default html container name for fields - defaultFieldPosition = $("#formFields") - - // - // Set up the form dynamically with all fields from the - // LAMEC configuration - // - $.each(json_config.fields, function(key, field) { - // - // Add the header for the field, containing the field name - // and a description of the field - // - addFieldHeaderElement(field.name, field.desc); - - // - // Add an empty field element as specified in the JSON data. - // Check for the special case where field type is string, but there is - // a restriction of type 'option', in that case the field is SELECT. - // - fieldType = field.type; - if(fieldType == 'string' && field.restriction && field.restriction.type == 'option') { - fieldType = 'select'; - } - addFieldElement(field.name, fieldType); - - // - // Process the settings for the field that was created above and add - // the settings as a data attribute to each form field for later use. - // - if(field.restriction) { - $("#" + field.name).data("restriction", field.restriction); - } - if(field.scope) { - $("#" + field.name).data("scope", field.scope); - } - if(field.default) { - $("#" + field.name).data("default", field.default); - } - - // - // Check if there is documentation available for this field, and if there is then - // add it as a data attribute to the relevant form field for later reference, - // then add the html element. - // - if(json_config.documentation && json_config.documentation[field.name]) { - $("#" + field.name).data("documentation", json_config.documentation[field.name]); - addDocumentationElement(field.name); - } - - // - // Process this field's data and update the form element accordingly, - // taking into account all restrictions, dependencies and scope. - // - setFieldAttributes(field.name); - }); - - - // - // Adds the html header for a field. - // - function addFieldHeaderElement(fieldName, fieldDescription, fieldPosition = defaultFieldPosition) { - new_field = '<div class="mt-4" '; - new_field += 'id="' + fieldName + 'Header" '; - new_field += '><label for="'; - new_field += fieldName; - new_field += '" class="form-label"><b>'; - new_field += fieldName[0].toUpperCase() + fieldName.slice(1).replace(/_/g, ' '); - new_field += '</b></label><div id="'; - new_field += fieldName; - new_field += 'Help" class="form-text mt-0 mb-1">'; - new_field += fieldDescription; - new_field += '</div></div>'; - fieldPosition.append(new_field); - } - - // - // Adds an empty field element. - // - function addFieldElement(fieldName, fieldType, fieldPosition = defaultFieldPosition) { - new_field = '<div class="my-0" '; - new_field += 'id="' + fieldName + 'Element">'; - if(fieldType == 'select') { - new_field += '<select class="form-select" '; - } - else if(fieldType == 'number') { - new_field += '<input class="form-control" type="number" '; - } - else if(fieldType == 'string') { - new_field += '<input class="form-control" type="text" '; - } - - new_field += 'id="' + fieldName + '" '; - new_field += 'name="' + fieldName + '" '; - new_field += 'aria-describedby="' + fieldName + 'Help" >'; - - if(fieldType == 'select') { - new_field += '</select>'; - } - new_field += '</div>'; - fieldPosition.append(new_field); - } - - // - // Add an html documentation element. - // - function addDocumentationElement(fieldName, fieldPosition = defaultFieldPosition) { - new_field = '<div class="mt-1 mb-0" id="' + fieldName + '-documentation">'; - new_field += '<small class="text-body-secondary">'; - new_field += '<span id="' + fieldName + '-name"></span> '; - new_field += '<a id="' + fieldName + '-documentation-url" href="#" target="_blank">documentation</a> '; - new_field += '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="bi bi-box-arrow-up-right" viewBox="0 0 16 16">'; - new_field += '<path fill-rule="evenodd" d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5z"/>'; - new_field += '<path fill-rule="evenodd" d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0v-5z"/>'; - new_field += '</svg></small></div>'; - fieldPosition.append(new_field); - } - - // - // Process the field's data and update the form element accordingly, - // taking into account all restrictions, dependencies and scope. - // - function setFieldAttributes(fieldName) { - fieldElement = $("#" + fieldName); - - if(fieldElement.data("scope")) { - // - // The field has a specific scope, show or hide - // the field accordingly. - // - if(isFieldWithinScope(fieldElement.data("scope"))) { - $("#" + fieldName + "Header").show(); - $("#" + fieldName + "Element").show(); - } - else { - $("#" + fieldName + "Header").hide(); - $("#" + fieldName + "Element").hide(); - } - } - - restriction = fieldElement.data("restriction"); - if(restriction) { - // - // Process the field's restrictions. - // - restrictionType = restriction.type; - - // - // The field is a select field, delete all options present - // and then repopulate the field, taking into account restrictions - // and dependencies. - // - if(restrictionType == 'option') { - $("#" + fieldName).length = 0; - } - - if(restriction.value.depends_on) { - dependantFields = restriction.value.depends_on.fields; - dependantResolution = restriction.value.depends_on.resolution; - - $.each(dependantResolution, function(index, resolution) { - // Track the number of resolutions that are met - resolutionCount = 0; - for(i=0; i<resolution.key.length; i++) { - if(resolution.key[i] == document.getElementById(dependantFields[i]).value) { - // one resolution met - resolutionCount++; - } - else { - // a resolution not met, skip to next - // continue - return(true); - } - } - - if(resolutionCount == i) { - // All dependant resolutions met - if(restrictionType == 'range') { - fieldElement.attr("min", resolution.value[0]); - fieldElement.attr("max", resolution.value[1]); - fieldElement.val(resolution.value[0]); - } - else if(restrictionType == 'option') { - populateFieldElementOptions(fieldName, resolution.value); - } - // As all resolutions were met, no need to search further. - // break - return(false); - } - }); - } - else { - if(restrictionType == 'option') { - populateFieldElementOptions(fieldName, restriction.value); - } - else if (restrictionType == 'range'){ - fieldElement.attr("min", restriction.value[0]); - fieldElement.attr("max", restriction.value[1]); - fieldElement.val(restriction.value[0]); - } - } - } - - // Set defaut field value, if defined. - if(fieldElement.data("default")) { - fieldElement.val(fieldElement.data("default")); - } - - if(fieldElement.data("documentation")) { - // Show documentation if selected option has documentation available - updateDocumentation(fieldName); - } - } - - // - // Method to update link to documentation for selected option - // - function updateDocumentation(fieldName) { - let documentationFound = false; - $.each($("#" + fieldName).data("documentation"), function(key, val) { - if(document.getElementById(fieldName).value == key) { - $("#" + fieldName + "-name").text(key); - $("#" + fieldName + "-documentation-url").attr("href", val); - documentationFound = true; - // break - return(false); - } - }); - if(documentationFound) { - $("#" + fieldName + "-documentation").show(); - } - else { - $("#" + fieldName + "-documentation").hide(); - } - } - - // - // Method to pupolate the options of a select element - // - function populateFieldElementOptions(fieldName, fieldValues) { - sel_element = document.getElementById(fieldName); - sel_element.length = 0; - $.each(fieldValues, function(key, val) { - sel_element.options[sel_element.options.length] = new Option(val, val); - }); - } - - // - // Method to check if a field is within scope. - // - function isFieldWithinScope(fieldScope) { - let isWithinScope = false; - if(fieldScope) { - $.each(fieldScope, function(key, scope) { - $.each(scope.values, function(key, vals) { - let matchedVals = 0; - for(i = 0; i < vals.length; i++) { - if(vals[i] == document.getElementById(scope.fields[i]).value) { - matchedVals++; - } - else { - continue; - } - } - if(matchedVals == vals.length) { - isWithinScope = true; - return(false); - } - }) - }); - return(isWithinScope); - } - return(false); - } - - // - // Catch all changes to select elements and update other fields accordingly, - // based on the rules of dependencies, scope and available documentation - // - $(".form-select").on('change', function() { - changedField = this.name; - - // If field has documentation data, then it needs updating - if($("#" + changedField).data("documentation")) { - updateDocumentation(changedField); - } - - $.each(json_config.fields, function(key, field) { - if(field.scope) { - setFieldAttributes(field.name); - } - else if(field.restriction && field.restriction.value && field.restriction.value.depends_on) { - $.each(field.restriction.value.depends_on.fields, function(key, val) { - if(val == changedField) { - // Update the field, according to set dependencies - setFieldAttributes(field.name); - } - }); - } - }); - - }); - } - </script> - <style> - .form-control::placeholder { - color: var(--bs-dark-bg-subtle); - } - </style> -</head> -<body> - <nav class="navbar"> - <div class="container justify-content-center mt-3"> - <a class="navbar-brand" href="https://www.coe-raise.eu"> - <img src="/2021-01-Logo-RGB-RAISE_standard.png" - height="160" - alt="RAISE Logo" - loading="lazy" /> - </a> - </div> - </nav> - <div class="container my-5" style="max-width: 980px;"> - <div class="container justify-content-center"> - <h1 class="display-6" style="text-align:center; color: rgb(4,132,196);">Load AI Modules, Environments, and Containers (LAMEC) API</h1> - </div> - <br><br> - <?php - if(isset( $_POST['Submit'])) { - // Convert JSON confid settings to array - // $config = json_decode($config_json, true); - // Can be used if user input has to be verified - - - // - // Create JSON payload to pass on to LAMEC - // - $payload = array(); - foreach($_POST as $key => $val) { - if($val != "") { - $payload[$key] = $val; - } - } - $json_payload = json_encode($payload); - - // putenv('PYTHONPATH="/var/www/apps/jsc/lamec"'); - $Phrase = "./lamec.py '$json_payload'"; - // $command = escapeshellcmd($Phrase); - $output = shell_exec($Phrase); - ?> - <div class="mb-3"> - <label for="output" class="form-label"><b>Your start script:</b></label> - <textarea rows="20" class="form-control"><?=$output?></textarea> - </div> - <?php - } - else { - ?> - <form action="index.php", method="post"> - <div id="formFields"></div> - <button type="submit" name="Submit" class="btn btn-primary mt-5">Create start script</button> - </form> - <?php - } - ?> - <br> - </div> - <div class="container-fluid" style="background-color: rgb(158,196,243);"> - <div class="container justify-content-center" style="max-width: 980px; text-align:center;"> - <!-- If we want to show the menu links in the footer... - <ul class="nav justify-content-center pt-1 pb-3 mb-3"> - <li class="nav-item px-4"> - <a href="https://www.coe-raise.eu/contact" class="nav-link text-body-secondary">Contact</a> - </li> - <li class="nav-item px-4"> - <a href="https://www.coe-raise.eu/imprint" class="nav-link text-body-secondary">Imprint</a> - </li> - <li class="nav-item px-4"> - <a href="https://www.coe-raise.eu/privacy-policy" class="nav-link text-body-secondary">Privacy Policy</a> - </li> - </ul> - --> - <div class="py-3">The CoE RAISE project have received funding from the European Union’s Horizon 2020 – Research and Innovation Framework Programme H2020-INFRAEDI-2019-1 under grant agreement no. 951733</div> - <div class="py-2">©2021 CoE RAISE.</div> - </div> - </div> -</body> -</html> diff --git a/js/form.js b/js/form.js new file mode 100644 index 0000000..d255bc3 --- /dev/null +++ b/js/form.js @@ -0,0 +1,307 @@ +window.addEventListener("load", (event) => { + $.getJSON("data/form-schema.json", init); +}); + +function init(json_config) { + // Default html container name for fields + defaultFieldPosition = $("#formFields") + + // + // Set up the form dynamically with all fields from the + // LAMEC configuration + // + $.each(json_config.fields, function(key, field) { + // + // Add the header for the field, containing the field name + // and a description of the field + // + addFieldHeaderElement(field.name, field.desc); + + // + // Add an empty field element as specified in the JSON data. + // Check for the special case where field type is string, but there is + // a restriction of type 'option', in that case the field is SELECT. + // + fieldType = field.type; + if(fieldType == 'string' && field.restriction && field.restriction.type == 'option') { + fieldType = 'select'; + } + addFieldElement(field.name, fieldType); + + // + // Process the settings for the field that was created above and add + // the settings as a data attribute to each form field for later use. + // + if(field.restriction) { + $("#" + field.name).data("restriction", field.restriction); + } + if(field.scope) { + $("#" + field.name).data("scope", field.scope); + } + if(field.default) { + $("#" + field.name).data("default", field.default); + } + + // + // Check if there is documentation available for this field, and if there is then + // add it as a data attribute to the relevant form field for later reference, + // then add the html element. + // + if(json_config.documentation && json_config.documentation[field.name]) { + $("#" + field.name).data("documentation", json_config.documentation[field.name]); + addDocumentationElement(field.name); + } + + // + // Process this field's data and update the form element accordingly, + // taking into account all restrictions, dependencies and scope. + // + setFieldAttributes(field.name); + }); + + + // + // Adds the html header for a field. + // + function addFieldHeaderElement(fieldName, fieldDescription, fieldPosition = defaultFieldPosition) { + new_field = '<div class="mt-4" '; + new_field += 'id="' + fieldName + 'Header" '; + new_field += '><label for="'; + new_field += fieldName; + new_field += '" class="form-label"><b>'; + new_field += fieldName[0].toUpperCase() + fieldName.slice(1).replace(/_/g, ' '); + new_field += '</b></label><div id="'; + new_field += fieldName; + new_field += 'Help" class="form-text mt-0 mb-1">'; + new_field += fieldDescription; + new_field += '</div></div>'; + fieldPosition.append(new_field); + } + + // + // Adds an empty field element. + // + function addFieldElement(fieldName, fieldType, fieldPosition = defaultFieldPosition) { + new_field = '<div class="my-0" '; + new_field += 'id="' + fieldName + 'Element">'; + if(fieldType == 'select') { + new_field += '<select class="form-select" '; + } + else if(fieldType == 'number') { + new_field += '<input class="form-control" type="number" '; + } + else if(fieldType == 'string') { + new_field += '<input class="form-control" type="text" '; + } + + new_field += 'id="' + fieldName + '" '; + new_field += 'name="' + fieldName + '" '; + new_field += 'aria-describedby="' + fieldName + 'Help" >'; + + if(fieldType == 'select') { + new_field += '</select>'; + } + new_field += '</div>'; + fieldPosition.append(new_field); + } + + // + // Add an html documentation element. + // + function addDocumentationElement(fieldName, fieldPosition = defaultFieldPosition) { + new_field = '<div class="mt-1 mb-0" id="' + fieldName + '-documentation">'; + new_field += '<small class="text-body-secondary">'; + new_field += '<span id="' + fieldName + '-name"></span> '; + new_field += '<a id="' + fieldName + '-documentation-url" href="#" target="_blank">documentation</a> '; + new_field += '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="bi bi-box-arrow-up-right" viewBox="0 0 16 16">'; + new_field += '<path fill-rule="evenodd" d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5z"/>'; + new_field += '<path fill-rule="evenodd" d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0v-5z"/>'; + new_field += '</svg></small></div>'; + fieldPosition.append(new_field); + } + + // + // Process the field's data and update the form element accordingly, + // taking into account all restrictions, dependencies and scope. + // + function setFieldAttributes(fieldName) { + fieldElement = $("#" + fieldName); + + if(fieldElement.data("scope")) { + // + // The field has a specific scope, show or hide + // the field accordingly. + // + if(isFieldWithinScope(fieldElement.data("scope"))) { + $("#" + fieldName + "Header").show(); + $("#" + fieldName + "Element").show(); + } + else { + $("#" + fieldName + "Header").hide(); + $("#" + fieldName + "Element").hide(); + } + } + + restriction = fieldElement.data("restriction"); + if(restriction) { + // + // Process the field's restrictions. + // + restrictionType = restriction.type; + + // + // The field is a select field, delete all options present + // and then repopulate the field, taking into account restrictions + // and dependencies. + // + if(restrictionType == 'option') { + $("#" + fieldName).length = 0; + } + + if(restriction.value.depends_on) { + dependantFields = restriction.value.depends_on.fields; + dependantResolution = restriction.value.depends_on.resolution; + + $.each(dependantResolution, function(index, resolution) { + // Track the number of resolutions that are met + resolutionCount = 0; + for(i=0; i<resolution.key.length; i++) { + if(resolution.key[i] == document.getElementById(dependantFields[i]).value) { + // one resolution met + resolutionCount++; + } + else { + // a resolution not met, skip to next + // continue + return(true); + } + } + + if(resolutionCount == i) { + // All dependant resolutions met + if(restrictionType == 'range') { + fieldElement.attr("min", resolution.value[0]); + fieldElement.attr("max", resolution.value[1]); + fieldElement.val(resolution.value[0]); + } + else if(restrictionType == 'option') { + populateFieldElementOptions(fieldName, resolution.value); + } + // As all resolutions were met, no need to search further. + // break + return(false); + } + }); + } + else { + if(restrictionType == 'option') { + populateFieldElementOptions(fieldName, restriction.value); + } + else if (restrictionType == 'range'){ + fieldElement.attr("min", restriction.value[0]); + fieldElement.attr("max", restriction.value[1]); + fieldElement.val(restriction.value[0]); + } + } + } + + // Set defaut field value, if defined. + if(fieldElement.data("default")) { + fieldElement.val(fieldElement.data("default")); + } + + if(fieldElement.data("documentation")) { + // Show documentation if selected option has documentation available + updateDocumentation(fieldName); + } + } + + // + // Method to update link to documentation for selected option + // + function updateDocumentation(fieldName) { + let documentationFound = false; + $.each($("#" + fieldName).data("documentation"), function(key, val) { + if(document.getElementById(fieldName).value == key) { + $("#" + fieldName + "-name").text(key); + $("#" + fieldName + "-documentation-url").attr("href", val); + documentationFound = true; + // break + return(false); + } + }); + if(documentationFound) { + $("#" + fieldName + "-documentation").show(); + } + else { + $("#" + fieldName + "-documentation").hide(); + } + } + + // + // Method to pupolate the options of a select element + // + function populateFieldElementOptions(fieldName, fieldValues) { + sel_element = document.getElementById(fieldName); + sel_element.length = 0; + $.each(fieldValues, function(key, val) { + sel_element.options[sel_element.options.length] = new Option(val, val); + }); + } + + // + // Method to check if a field is within scope. + // + function isFieldWithinScope(fieldScope) { + let isWithinScope = false; + if(fieldScope) { + $.each(fieldScope, function(key, scope) { + $.each(scope.values, function(key, vals) { + let matchedVals = 0; + for(i = 0; i < vals.length; i++) { + if(vals[i] == document.getElementById(scope.fields[i]).value) { + matchedVals++; + } + else { + continue; + } + } + if(matchedVals == vals.length) { + isWithinScope = true; + return(false); + } + }) + }); + return(isWithinScope); + } + return(false); + } + + // + // Catch all changes to select elements and update other fields accordingly, + // based on the rules of dependencies, scope and available documentation + // + $(".form-select").on('change', function() { + changedField = this.name; + + // If field has documentation data, then it needs updating + if($("#" + changedField).data("documentation")) { + updateDocumentation(changedField); + } + + $.each(json_config.fields, function(key, field) { + if(field.scope) { + setFieldAttributes(field.name); + } + else if(field.restriction && field.restriction.value && field.restriction.value.depends_on) { + $.each(field.restriction.value.depends_on.fields, function(key, val) { + if(val == changedField) { + // Update the field, according to set dependencies + setFieldAttributes(field.name); + } + }); + } + }); + + }); +} diff --git a/js/status.js b/js/status.js new file mode 100644 index 0000000..370c6ee --- /dev/null +++ b/js/status.js @@ -0,0 +1,31 @@ +window.addEventListener("load", (event) => { + $.getJSON("/data/status.json", init); +}); + +function passedIcon(passed) { + if (passed !== undefined) { + var color = passed ? 'green' : 'red'; + } else { + var color = 'grey'; + } + return `<i class="fa fa-solid fa-circle" style="color: ${color}"></i>`; +} + +function addTableElement(tableBody, system, software, passed) { + tableBody.append(` + <tr> + <td>${system}</td> + <td>${software}</td> + <td>${passedIcon(passed)}</td> + </tr> + `); +} + +function init(data) { + let tableBody = $("#table-body"); + $.each(data, (system, software_list) => { + $.each(software_list, (software, value) => { + addTableElement(tableBody, system, software, value.passed); + }); + }); +} diff --git a/lamec.py b/lamec.py index 7988436..dac6875 100755 --- a/lamec.py +++ b/lamec.py @@ -5,9 +5,36 @@ import json import os import re import update +import http.client scriptpath = f'{sys.path[0]}/scripts' +def report(system, software, passed): + server = 'localhost:5000' + endpoint = '/data/status.json' + + data = { + 'system': system, + 'software': software, + 'passed': passed + } + + json_data = json.dumps(data) + + conn = http.client.HTTPConnection(server) + + headers = { + 'Content-type': 'application/json', + 'Content-length': str(len(json_data)) + } + + conn.request('POST', endpoint, body=json_data, headers=headers) + response = conn.getresponse() + response_data = response.read() + print(response_data) + + conn.close() + class Module: """ Global variables @@ -185,7 +212,7 @@ def main(): else: args = json.loads(sys.argv[1]) - path = os.path.join('scripts', f"{args['system']}/{args['software']}") + path = os.path.join(scriptpath, f"{args['system']}/{args['software']}") with open(os.path.join(path, 'lamec.json'), 'r') as f: config = json.load(f) if 'startscript' in config: diff --git a/render.php b/render.php new file mode 100644 index 0000000..3672cb0 --- /dev/null +++ b/render.php @@ -0,0 +1,8 @@ +<?php +function loadFragment($filename, $variables = []) { + extract($variables); + ob_start(); + include $filename; + return ob_get_clean(); +} +?> diff --git a/scripts/MockHPCSystem/MockSoftware/lamec.json b/scripts/MockHPCSystem/MockSoftware/lamec.json new file mode 100644 index 0000000..e82ad0e --- /dev/null +++ b/scripts/MockHPCSystem/MockSoftware/lamec.json @@ -0,0 +1,3 @@ +{ + "template": "template.sh" +} diff --git a/scripts/MockHPCSystem/MockSoftware/template.sh b/scripts/MockHPCSystem/MockSoftware/template.sh new file mode 100644 index 0000000..1e26e12 --- /dev/null +++ b/scripts/MockHPCSystem/MockSoftware/template.sh @@ -0,0 +1,5 @@ +#SBATCH --account=%account% +#SBATCH --partition=%partition% +#SBATCH --nodes=%nodes% + +srun %executable% diff --git a/scripts/MockHPCSystem/sysinfo.json b/scripts/MockHPCSystem/sysinfo.json new file mode 100644 index 0000000..84f3f5f --- /dev/null +++ b/scripts/MockHPCSystem/sysinfo.json @@ -0,0 +1,7 @@ +{ + "partition": { + "base": { + "nodes": 10 + } + } +} diff --git a/status.php b/status.php new file mode 100644 index 0000000..19a5637 --- /dev/null +++ b/status.php @@ -0,0 +1,25 @@ +<?php +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $raw_data = file_get_contents('php://input'); + $json_data = json_decode($raw_data, true); + $file = fopen('/data/status.json', 'r'); + $data = json_decode(fread($file, filesize('/data/status.json')), true); + if (array_key_exists($json_data['system'], $data)) { + if (array_key_exists($json_data['software'], + $data[$json_data['system']])) { + $data[$json_data['system']][$json_data['software']]['passed'] + = $json_data['passed']; + } + } + fclose($file); + + $file = fopen('/data/status.json', 'w'); + fwrite($file, json_encode($data, JSON_PRETTY_PRINT)); + fclose($file); +} else { + include 'render.php'; + $active = 'status'; + $content = loadFragment('html/status.html'); + include 'base.php'; +} +?> diff --git a/update.py b/update.py index 15bf44f..ef338d9 100644 --- a/update.py +++ b/update.py @@ -172,7 +172,7 @@ def update_all(): fields.append(get_free_variable(var, defs)) form_schema = {'fields': fields, 'documentation': info['docs']} set_scope(fields) - with open('form_schema.json', 'w') as out: + with open('data/form-schema.json', 'w') as out: json.dump(form_schema, out, indent=4) def main(): -- GitLab