Commit 4e463b12 authored by Daniel Smith's avatar Daniel Smith

Bugfixes and documentation of REST API schema

parent 6fc69131
# REST API
This tool provides a JSON REST API inteface which can be used to schedule new jobs
or query for current jobs. All requests should be directed to the host acting
as the scheduler unless otherwise specified.
***Important***: This REST API presently performs no validation on incoming requests. Malformed requests
may produce undefined behavior.
#
## Endpoints
### **POST** /remoteSchedule
Schedule a new job.
Note optional and conditionally required fields. Conditionally required fields
are marked with the testType they are required for.
#### Request:
```
{
testType: <str>[ bisect | singleCommit | twoCommit ],
owner: (optional) <str>, // If set as an email address, the system will notify about jobs.
title: (optional) <str>, // If not set, the subject of the first sha to test is used.
status: <str>[ staged ],
branch: <str>[ any qt X.XX branch ],
module: <str>[ qtbase | qtdeclarative | qtquickcontrols | qtquickcontrols2 | qtgraphicaleffects ],
commit: (singleCommit) <str> [ any sha1 in codereview ],
patches: (optional) <str>[ comma separated list of sha1s or patchrefs from codereview ],
firstCommitBuildOnHead: (singleCommit | twoCommit) <bool>,
secondCommitBuildOnHead: (twoCommit) <bool>,
firstCommit: (twoCommit) <str> [any sha from codereview],
secondCommit: (twoCommit) <str> [any sha from codereview],
bad_commit: (bisect) <str> [any sha from codereview],
good_commit: (bisect) <str> [any sha from codereview],
openGLBackend: (win32 platforms) <str> [ desktop | Angle | software ], // ignored on non-win32 platforms
test_name: (exclusive with custom_benchmark_file) <str> [ benchmark path from qmlbench repo ],
custom_benchmark_file: (exclusive with test_name) <Buffer> [ custom qml file as binary data ],
expected_regression: (bisect) <int> [ > 5 ], // percent
test_hosts: <array<str>> | <str> [ hostname(s) of agents which should run the test ],
environment: <array<str>> | <str> [ 'key=value' pair(s) of environment variables to apply to agents ]
}
```
#### Example:
```
POST 127.0.0.1:8080/remoteSchedule
{
testType: 'singleCommit',
branch: 'dev',
module: 'qtquickcontrols2',
openGLBackend: 'desktop',
commit: '49ffc6e6af83b295c67fd119b79c925879cc292e',
test_name: 'auto/creation/quick.controls2/delegates_combobox.qml',
test_hosts: 'myTestHost1',
patches: '39d99c714bb87bd39e92d85fb1f46c52eb9f8d33,5bdf48f6c9e125c2528e3af16eda257ab0fef9c0'
}
```
#### Response:
```200: OK```
#
### **GET** /currentJobs
Get all current running and queued jobs. Request does not accept parameters or a request body.
#### Response:
```
200: OK
{
"localJobs": {
"runningJob": {},
"queuedJobs": []
},
"remoteRunningJobs": [],
"remoteQueuedJobs": []
}
```
#### Job schema:
Note: Not all fields may be populated.
```
{
jobHash: <str> SHA256
platform: <str>[ win32 | linux | macOS ]
startTime: <int> milliseconds // Time the job actually started on the agent
expectedDuration: <int> milliseconds
timestamp: <int> // Time the job was scheduled.
testType: <str>[ bisect | singleCommit | twoCommit ]
owner: <str>
title: <str>
status: <str>[ staged | running | cancelling | cancelled | finished ]
status_msg: <str>
branch: <str>
module: <str>[ qtbase | qtdeclarative | qtquickcontrols | qtquickcontrols2 | qtgraphicaleffects ]
commit: <str> 40-char sha1
patches: <array<str>> | <str>
firstCommitBuildOnHead: <bool>
secondCommitBuildOnHead: <bool>
firstCommit: <str> 40-char sha1
secondCommit: <str> 40-char sha1
bad_commit: <str> 40-char sha1
good_commit: <str> 40-char sha1
openGLBackend: <str> [ desktop | Angle | software ]
test_name: <str>
custom_benchmark_file: <buffer>
expected_regression: <int>
test_hosts: <array<str>> | <str>
environment: <array<str>> | <str> [ 'key=value' pair(s)]
remote_job: <bool>
master_host: <str> IP Address
}
```
#
### **GET** /cancelJob
Cancel a currently running or queued job.
#### Request:
```
URL Parameters:
jobHash: [SHA256 job hash to cancel]
```
#### Example:
```
GET /cancelJob?jobHash=deb167217eff98d8cad0a7baca73a8565324f3f9cd332faebbccc12eb30afb11
```
#### Response:
```200: OK```
#
### **GET** /status
Get the basic operational status of an agent. This request may be directed to agents other than
the scheduler.
#### Response:
```
200: OK
{
"freediskspace": <int> bytes free
"uptime": <float> seconds
"hostname": <str> hostname
"ip": <str> IPV4 address
}
```
......@@ -226,7 +226,6 @@ class jobNotificationHandler {
firstCommit: req.body.firstCommit,
secondCommit: req.body.secondCommit,
bad_commit: req.body.bad_commit,
good_date: req.body.good_date,
good_commit: req.body.good_commit,
openGLBackend: req.body.openGLBackend,
test_name: req.body.test_name,
......@@ -237,7 +236,6 @@ class jobNotificationHandler {
? req.files.custom_benchmark_file
: req.body.custom_benchmark_file,
expected_regression: req.body.expected_regression,
test_all: req.body.test_all,
// Only tell the agent which will execute the job about itself.
test_hosts: Array.isArray(req.body.test_hosts)
? [req.body.test_hosts[i]]
......
/* eslint-disable max-len */
"use strict";
const { spawn } = require("child_process");
const express = require("express");
const fileUpload = require("express-fileupload");
const events = require("events");
......@@ -17,7 +16,6 @@ const axios = require("axios");
const prettyBytes = require("pretty-bytes");
const fs = require("fs");
const serveIndex = require("serve-index");
const { scheduleJob } = require("node-schedule");
const fetch = require("node-fetch");
......@@ -49,7 +47,8 @@ let globals ={
webPort: envOrConfig("HTTP_PORT"),
baseURL: envOrConfig("BASE_URL"),
agents: envOrConfig("agents"),
expectedDurations: envOrConfig("expectedDurations")
expectedDurations: envOrConfig("expectedDurations"),
refreshRepos: false
}
......@@ -80,45 +79,42 @@ if (globals.standalone && globals.nolocal) {
console.log("Starting in standalone mode. Ignoring all agent hosts.");
globals.isPrimaryHost = true;
} else if (globals.nolocal) {
parseAgentsFromArgs();
if (!globals.agents) {
// verify we have at least one agent worker
console.log("ERROR: Use of 'nolocal' parameter requires addition of at least one agent, and cannot be combined with 'standalone' mode");
console.log("ERROR: Use of 'nolocal' parameter requires addition of at least one agent.");
process.exit(1);
}
console.log("Starting in nolocal mode. Will not offer self as a agent host.");
globals.isPrimaryHost = true;
console.log("Starting in nolocal mode.");
} else if (process.argv.some((e) => e === "agent")) {
// Override and empty any config of agent lists if this is not the scheduler.
console.log("Starting in agent mode.")
globals.isPrimaryHost = false;
globals.agents = [];
} else if (process.argv.some((e) => e === "refreshrepos")) {
// Refresh test names and exit.
console.log("Refreshing qmlbench test list. Process will exit after completion!")
gatherTestNames(true, function () {
process.exit(0);
});
} else if (process.argv[2] != undefined) {
// Assume all our arguments are agent worker IPs.
globals.isPrimaryHost = true;
process.argv.forEach((val, index) => {
if (index > 1) {
if (globals.agents.all((e) => e != val)) {
globals.agents.push(val);
console.log(`Got additional agent ${index - 1}: ${val}`);
}
}
});
parseAgentsFromArgs();
}
console.log(`I am ${globals.isPrimaryHost ? "" : "not "}a primary host, and I ${
globals.nolocal ? "will not" : "will"} run local jobs.`);
if (!globals.nolocal)
globals.agents.push(ip.address()); // List self as a agent
if (!globals.agents) {
console.error("Error: No agents! Exiting...");
process.exit(1);
} else {
console.log(`Have agents: ${globals.agents}`);
}
if (globals.agents)
globals.isPrimaryHost = true;
console.log(`I am ${globals.isPrimaryHost ? "" : "not "}a primary host, and I ${
globals.nolocal ? "will not" : "will"} run local jobs.`);
if (process.argv.some((e) => e === "refreshrepos")) {
// Refresh test names and exit.
console.log("Refreshing qmlbench test list.");
globals.refreshRepos = true;
}
}
......@@ -129,6 +125,19 @@ const testHandler = new TestHandler(notifyJob, globals);
const notifyHandler = new NotifyHandler(notifyJob, testHandler, globals);
// Parse command line args for valid IPs.
function parseAgentsFromArgs() {
process.argv.forEach((val, index) => {
if (
(ip.isV4Format(val) || ip.isV6Format(val))
&& globals.agents.every((e) => e != val)
) {
globals.agents.push(val);
console.log(`Got additional agent ${index - 1}: ${val}`);
}
});
}
function clearErrors(req, res) {
// Clear system errors on the web interface when running in scheduler mode.
globals.recentErrors.length = 0;
......@@ -174,20 +183,28 @@ function queryJobs(req, res) {
firstCommit: globals.runningJob.firstCommit,
secondCommit: globals.runningJob.secondCommit,
bad_commit: globals.runningJob.bad_commit,
good_date: globals.runningJob.good_date,
good_commit: globals.runningJob.good_commit,
openGLBackend: req.body.openGLBackend,
openGLBackend: globals.runningJob.openGLBackend,
test_name: globals.runningJob.test_name,
custom_benchmark_file: globals.runningJob.custom_benchmark_file,
expected_regression: globals.runningJob.expected_regression,
test_all: globals.runningJob.test_all,
test_hosts: globals.runningJob.test_hosts,
environment: globals.runningJob.environment,
remote_job: globals.runningJob.remote_job,
master_host: globals.runningJob.master_host
};
if (res)
res.send({ runningJob: tempRunningJob, queuedJobs: globals.jobQueue });
else
return { runningJob: tempRunningJob, queuedJobs: globals.jobQueue };
}
res.send({ runningJob: tempRunningJob, queuedJobs: globals.jobQueue });
function serveJobsJSON(req, res) {
res.send({
localJobs: queryJobs(),
remoteRunningJobs: globals.remoteRunningJobs,
remoteQueuedJobs: globals.remoteQueuedJobs
})
}
// Dump the scheduler's local cache of currently running and queued remote
......@@ -253,8 +270,10 @@ function cancelJob(req, res) {
);
} else {
// Job must be remote. Forward the cancel request to the agent.
let jobFound = false;
[global.remoteRunningJobs, global.remoteQueuedJobs].forEach((targetArray) => {
let targetIndex = toolbox.getIndex(targetArray, req.query.jobHash);
let targetIndex = -1;
toolbox.getIndex(targetArray, req.query.jobHash);
if (targetIndex == -1) {
return;
} else {
......@@ -276,6 +295,8 @@ function cancelJob(req, res) {
});
}
}); // End of foreach
if (!jobFound)
res.status(400).send(`Not found on any agent: ${req.query.jobHash}`)
} // End of local/remote if
}
......@@ -477,6 +498,8 @@ function serveHomePage(req, res) {
</tr>`;
readyEmitter.emit("agentFinished");
}
} else {
res.send("This agent is not running a scheduler.");
}
}
......@@ -586,18 +609,19 @@ function runServer() {
serveIndex(`${__dirname}/QtBuild/qmlbench/benchmarks/`, { view: "details" })
);
server.all("/remoteSchedule", (req, res) => notifyHandler.scheduleJobRequest(req, res));
server.all("/scheduleJob", (req, res) => serveSchedulerPage(req, res));
server.all("/queryJobs", (req, res) => queryJobs(req, res));
server.all("/refreshRemoteJobs", (req, res) => queryAgentJobs(req, res));
server.all("/pushJobUpdate", (req, res) => notifyHandler.addUpdateToQueue(req, res));
server.all("/updateLocalRunningJobStatus", (req, res) => notifyHandler.updateRunningJobStatus(req, res));
server.all("/cancelJob", (req, res) => cancelJob(req, res));
server.all("/recentErrors", (req, res) => errorsPage(req, res));
server.all("/clearRecentErrors", (req, res) => clearErrors(req, res));
server.all("/clearFinishedJobs", (req, res) => clearFinishedJobs(req, res));
server.all("/status", (req, res) => serveStatus(req, res));
server.all("/", (req, res) => serveHomePage(req, res));
server.post("/remoteSchedule", (req, res) => notifyHandler.scheduleJobRequest(req, res));
server.get("/scheduleJob", (req, res) => serveSchedulerPage(req, res));
server.get("/queryJobs", (req, res) => queryJobs(req, res));
server.get("/refreshRemoteJobs", (req, res) => queryAgentJobs(req, res));
server.get("/currentJobs", (req, res) => serveJobsJSON(req, res));
server.post("/pushJobUpdate", (req, res) => notifyHandler.addUpdateToQueue(req, res));
server.post("/updateLocalRunningJobStatus", (req, res) => notifyHandler.updateRunningJobStatus(req, res));
server.get("/cancelJob", (req, res) => cancelJob(req, res));
server.get("/recentErrors", (req, res) => errorsPage(req, res));
server.get("/clearRecentErrors", (req, res) => clearErrors(req, res));
server.get("/clearFinishedJobs", (req, res) => clearFinishedJobs(req, res));
server.get("/status", (req, res) => serveStatus(req, res));
server.get("/", (req, res) => serveHomePage(req, res));
console.log(`Starting server... Listening on port ${globals.webPort}`);
......@@ -607,7 +631,7 @@ function runServer() {
// Gather test names for partial match suggestions on the scheduler page. If QMLBench isn't already initialized, do it now and block the scheduler.
function gatherTestNames(forceUpdate, callback) {
if (!fs.existsSync(pathmodule.join(__dirname, "QtBuild", "qmlbench")) || forceUpdate) {
if (forceUpdate || !fs.existsSync(pathmodule.join(__dirname, "QtBuild", "qmlbench"))) {
globals.locked = true;
if (forceUpdate)
console.log("Updating local repositories and pulling new qmlbench tests.");
......@@ -650,7 +674,7 @@ function queryAgents(callback) {
for (let agent in globals.agents) {
axios
.get(`http://${globals.agents[agent]}:${globals.webPort}/status`, { timeout: 2000 }) // Query everyone else
.get(`http://${globals.agents[agent]}:${globals.webPort}/status`, { timeout: 1000 })
.then((response) => {
if (!globals.hosts.some((o) => o.ip == response.data.ip)) {
console.log(`Adding ${response.data.hostname} to the agent list`);
......@@ -675,6 +699,7 @@ function queryAgents(callback) {
// listening so the data is already there, if the remote hosts are online first.
function startup() {
toolbox.createLocalDirs(globals.myname);
gatherTestNames(globals.refreshRepos, (tests) => globals.testNames = tests);
if (globals.agents) {
console.log("Querying agent IPs for hostname info...");
queryAgents(function (finished) {
......@@ -689,8 +714,6 @@ function startup() {
console.error("Cannot continue with 0 agents. Please verify your configuration.");
process.exit(1);
}
gatherTestNames(undefined, (tests) => globals.testNames = tests);
}
// Call pre-flight checks, which calls the server-startup, which runs the scheduler and web interface
......
......@@ -20,7 +20,7 @@ class testHandler {
this.notifyJob.on("jobDone", this.jobDone.bind(this));
}
runTest(test, callback) {
runTest(test, callback, callbackArgs) {
let _this = this;
// Run the given test on this host
console.log("Now executing test with params:");
......@@ -174,8 +174,11 @@ class testHandler {
}
// Provide a callback option to do custom processing on job completion.
if (callback)
callback();
if (callback) {
typeof(callbackArgs) === Array
? callback(...callbackArgs)
: callback(callbackArgs)
}
// Emit the JobDone signal to do some more tasks and prep for the next job in queue.
_this.notifyJob.emit("jobDone");
......
......@@ -30,7 +30,13 @@ let smtpPort = envOrConfig(Number("SMTP_PORT"));
exports.getIndex = getIndex;
function getIndex(targetArray, jobHash) {
return targetArray[targetArray.findIndex((j) => j.jobHash == jobHash)]
let index = -1;
try {
targetArray[targetArray.findIndex((j) => j.jobHash == jobHash)]
} catch (exception) {
return index;
}
return index;
}
exports.addJob = addJobToArray;
......@@ -47,7 +53,7 @@ function addJobRecursive(viewElement, joblist, index, hosts, callback) {
viewElement += job;
index += 1;
if (index < joblist.length)
addJobRecursive(viewElement, joblist, index, callback);
addJobRecursive(viewElement, joblist, index, hosts, callback);
else
callback(viewElement);
});
......@@ -123,6 +129,7 @@ function jobFormatter(job, hosts, callback) {
<br>
${bisectResult}`;
}
view.resultString += "<br>"
}
if (job.test_name)
......@@ -152,7 +159,12 @@ function jobFormatter(job, hosts, callback) {
view.timeEstimate = `Expected to run for about ${moment
.duration(job.expectedDuration, "ms")
.format("h [hrs], m [min]")}`;
} else if (job.actualDuration) {
view.timeEstimate = `Ran for ${moment
.duration(job.actualDuration, "ms")
.format("h [hrs], m [min]")}`
}
view.timeEstimate += "<br>"
if (job.status != "staged") {
view.logButton = `<button onclick="window.location.href='${
......
......@@ -13,7 +13,6 @@ Last status message: <font color="#454545">{{statusMsg}}</font>
{{&testString}}
{{&timeEstimate}}
{{&resultString}}
<br>
{{&logButton}} {{&cancelButton}}
</li>
<br>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment