luci-app-attendedsysupgrade: support revision checks

SNAPSHOTS are not real releases and therefore the app always offers an
upgrade, even if running the latest build. To prevent that all SNAPSHOTS
now check for the running revision and if a newer one is available.

Also do a bunch of refactoring based on JavaScript I learned over the
last week.

Signed-off-by: Paul Spooren <mail@aparcar.org>
This commit is contained in:
Paul Spooren 2021-08-29 00:37:31 -10:00
parent ab2f8b8b04
commit f799d550b6

View file

@ -9,382 +9,412 @@
'require dom'; 'require dom';
var callPackagelist = rpc.declare({ var callPackagelist = rpc.declare({
object: 'rpc-sys', object: 'rpc-sys',
method: 'packagelist', method: 'packagelist',
}); });
var callSystemBoard = rpc.declare({ var callSystemBoard = rpc.declare({
object: 'system', object: 'system',
method: 'board' method: 'board'
}); });
var callUpgradeStart = rpc.declare({ var callUpgradeStart = rpc.declare({
object: 'rpc-sys', object: 'rpc-sys',
method: 'upgrade_start', method: 'upgrade_start',
params: ["keep"] params: ["keep"]
}); });
function get_branch(version) { function get_branch(version) {
// determine branch of a version // determine branch of a version
// SNAPSHOT -> SNAPSHOT // SNAPSHOT -> SNAPSHOT
// 21.02-SNAPSHOT -> 21.02 // 21.02-SNAPSHOT -> 21.02
// 21.02.0-rc1 -> 21.02 // 21.02.0-rc1 -> 21.02
// 19.07.8 -> 19.07 // 19.07.8 -> 19.07
return version.replace("-SNAPSHOT", "").split(".").slice(0, 2).join("."); return version.replace("-SNAPSHOT", "").split(".").slice(0, 2).join(".");
}
function get_revision_count(revision) {
return parseInt(revision.substring(1).split("-")[0])
}
function error_api_connect(response) {
console.log(response)
ui.showModal(_('Error connecting to upgrade server'), [
E('p', {}, _(`Could not reach API at "${response.url}. Please try again later.`)),
E('pre', {}, response.responseText),
E('div', {
'class': 'right'
}, [
E('div', {
'class': 'btn',
'click': ui.hideModal
}, _('Close'))
])
]);
} }
function install_sysupgrade(url, keep, sha256) { function install_sysupgrade(url, keep, sha256) {
displayStatus("notice spinning", E('p', _('Downloading firmware from server to browser'))); displayStatus("notice spinning", E('p', _('Downloading firmware from server to browser')));
request.get(url, { request.get(url, {
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded' 'Content-Type': 'application/x-www-form-urlencoded'
}, },
responseType: 'blob' responseType: 'blob'
}) })
.then(response => { .then(response => {
var form_data = new FormData(); var form_data = new FormData();
form_data.append("sessionid", rpc.getSessionID()); form_data.append("sessionid", rpc.getSessionID());
form_data.append("filename", "/tmp/firmware.bin"); form_data.append("filename", "/tmp/firmware.bin");
form_data.append("filemode", 600); form_data.append("filemode", 600);
form_data.append("filedata", response.blob()); form_data.append("filedata", response.blob());
displayStatus("notice spinning", E('p', _('Uploading firmware from browser to device'))); displayStatus("notice spinning", E('p', _('Uploading firmware from browser to device')));
request.get(L.env.cgi_base + "/cgi-upload", { request.get(`${L.env.cgi_base}/cgi-upload`, {
method: 'PUT', method: 'PUT',
content: form_data content: form_data
}) })
.then(response => response.json()) .then(response => response.json())
.then(response => { .then(response => {
if (response.sha256sum != sha256) { if (response.sha256sum != sha256) {
displayStatus("warning", [ displayStatus("warning", [
E('b', _('Wrong checksum')), E('b', _('Wrong checksum')),
E('p', _('Error during download of firmware. Please try again')), E('p', _('Error during download of firmware. Please try again')),
E('div', { E('div', {
'class': 'btn', 'class': 'btn',
'click': ui.hideModal 'click': ui.hideModal
}, _('Close')) }, _('Close'))
]); ]);
} else { } else {
displayStatus('warning spinning', E('p', _('Installing the sysupgrade. Do not unpower device!'))); displayStatus('warning spinning', E('p', _('Installing the sysupgrade. Do not unpower device!')));
L.resolveDefault(callUpgradeStart(keep), {}).then(response => { L.resolveDefault(callUpgradeStart(keep), {}).then(response => {
if (keep) { if (keep) {
ui.awaitReconnect(window.location.host); ui.awaitReconnect(window.location.host);
} else { } else {
ui.awaitReconnect('192.168.1.1', 'openwrt.lan'); ui.awaitReconnect('192.168.1.1', 'openwrt.lan');
} }
}); });
} }
}); });
}); });
} }
function request_sysupgrade(server_url, data) { function request_sysupgrade(server_url, data) {
var res, req; var res, req;
if (data.request_hash) { if (data.request_hash) {
req = request.get(server_url + "/api/build/" + data.request_hash) req = request.get(`${server_url}/api/v1/build/${data.request_hash}`)
} else { } else {
req = request.post(server_url + "/api/build", { req = request.post(`${server_url}/api/v1/build`, {
profile: data.board_name, profile: data.board_name,
target: data.target, target: data.target,
version: data.version, version: data.version,
packages: data.packages, packages: data.packages,
diff_packages: true, diff_packages: true,
}) })
} }
req.then(response => { req.then(response => {
switch (response.status) { switch (response.status) {
case 200: case 200:
var res = response.json() res = response.json()
var image; var image;
for (image of res.images) { for (image of res.images) {
if (image.type == "sysupgrade") { if (image.type == "sysupgrade") {
break; break;
} }
} }
if (image.name != undefined) { if (image.name != undefined) {
var sysupgrade_url = server_url + "/store/" + res.bin_dir + "/" + image.name; var sysupgrade_url = `${server_url}/store/${res.bin_dir}/${image.name}`;
var keep = E('input', { var keep = E('input', {
type: 'checkbox' type: 'checkbox'
}) })
keep.checked = true; keep.checked = true;
var fields = [ var fields = [
_('Version'), res.version_number + ' ' + res.version_code, _('Version'), `${res.version_number} ${res.version_code}`,
_('File'), E('a', { _('File'), E('a', {
'href': sysupgrade_url 'href': sysupgrade_url
}, image.name), }, image.name),
_('SHA256'), image.sha256, _('SHA256'), image.sha256,
_('Build Date'), res.build_at, _('Build Date'), res.build_at,
_('Target'), res.target, _('Target'), res.target,
]; ];
var table = E('div', { var table = E('div', {
'class': 'table' 'class': 'table'
}); });
for (var i = 0; i < fields.length; i += 2) { for (var i = 0; i < fields.length; i += 2) {
table.appendChild(E('div', { table.appendChild(E('div', {
'class': 'tr' 'class': 'tr'
}, [ }, [
E('div', { E('div', {
'class': 'td left', 'class': 'td left',
'width': '33%' 'width': '33%'
}, [fields[i]]), }, [fields[i]]),
E('div', { E('div', {
'class': 'td left' 'class': 'td left'
}, [(fields[i + 1] != null) ? fields[i + 1] : '?']) }, [(fields[i + 1] != null) ? fields[i + 1] : '?'])
])); ]));
} }
var modal_body = [ var modal_body = [
table, table,
E('p', {}, E('label', { E('p', {}, E('label', {
'class': 'btn' 'class': 'btn'
}, [ }, [
keep, ' ', _('Keep settings and retain the current configuration') keep, ' ', _('Keep settings and retain the current configuration')
])), ])),
E('div', { E('div', {
'class': 'right' 'class': 'right'
}, [ }, [
E('div', { E('div', {
'class': 'btn', 'class': 'btn',
'click': ui.hideModal 'click': ui.hideModal
}, _('Cancel')), }, _('Cancel')),
' ', ' ',
E('div', { E('div', {
'class': 'btn cbi-button-action', 'class': 'btn cbi-button-action',
'click': function() { 'click': function() {
install_sysupgrade(sysupgrade_url, keep.checked, image.sha256) install_sysupgrade(sysupgrade_url, keep.checked, image.sha256)
} }
}, _('Install Sysupgrade')) }, _('Install Sysupgrade'))
]) ])
] ]
ui.showModal(_('Successfully created sysupgrade image'), modal_body); ui.showModal(_('Successfully created sysupgrade image'), modal_body);
} }
break; break;
case 202: case 202:
res = response.json() res = response.json()
data.request_hash = res.request_hash; data.request_hash = res.request_hash;
if ("queue_position" in res) if ("queue_position" in res)
displayStatus("notice spinning", E('p', _('Request in build queue position %d'.format(res.queue_position)))); displayStatus("notice spinning", E('p', _('Request in build queue position %d'.format(res.queue_position))));
else else
displayStatus("notice spinning", E('p', _('Building firmware sysupgrade image'))); displayStatus("notice spinning", E('p', _('Building firmware sysupgrade image')));
setTimeout(function() { setTimeout(function() {
request_sysupgrade(server_url, data); request_sysupgrade(server_url, data);
}, 5000); }, 5000);
break; break;
case 400: // bad request case 400: // bad request
case 422: // bad package case 422: // bad package
case 500: // build failed case 500: // build failed
res = response.json() res = response.json()
var body = [ var body = [
E('p', {}, res.detail), E('p', {}, res.detail),
E('p', {}, _("Please report the error message and request")), E('p', {}, _("Please report the error message and request")),
E('b', {}, _("Request to server:")), E('b', {}, _("Request to server:")),
E('pre', {}, JSON.stringify(data, null, 4)), E('pre', {}, JSON.stringify(data, null, 4)),
] ]
if (res.stdout) { if (res.stdout) {
body.push(E('b', {}, "STDOUT:")) body.push(E('b', {}, "STDOUT:"))
body.push(E('pre', {}, res.stdout)) body.push(E('pre', {}, res.stdout))
} }
if (res.stderr) { if (res.stderr) {
body.push(E('b', {}, "STDERR:")) body.push(E('b', {}, "STDERR:"))
body.push(E('pre', {}, res.stderr)) body.push(E('pre', {}, res.stderr))
} }
body = body.concat([ body = body.concat([
E('div', { E('div', {
'class': 'right' 'class': 'right'
}, [ }, [
E('div', { E('div', {
'class': 'btn', 'class': 'btn',
'click': ui.hideModal 'click': ui.hideModal
}, _('Close')) }, _('Close'))
]) ])
]); ]);
ui.showModal(_('Error building the sysupgrade'), body); ui.showModal(_('Error building the sysupgrade'), body);
break; break;
} }
}); });
} }
function check_sysupgrade(server_url, current_version, target, board_name, packages) { async function check_sysupgrade(server_url, system_board, packages) {
displayStatus("notice spinning", E('p', _('Searching for an available sysupgrade'))); var {
var current_branch = get_branch(current_version); board_name
var advanced_mode = uci.get_first('attendedsysupgrade', 'client', 'advanced_mode') || 0; } = system_board;
var candidates = []; var {
target,
version,
revision
} = system_board.release;
var current_branch = get_branch(version);
var advanced_mode = uci.get_first('attendedsysupgrade', 'client', 'advanced_mode') || 0;
var candidates = [];
var response;
request.get(server_url + "/json/latest.json", { displayStatus("notice spinning", E('p', _(`Searching for an available sysupgrade of ${version} - ${revision}`)));
timeout: 8000
})
.then(response => response.json())
.then(response => {
if (current_version == "SNAPSHOT") {
candidates.push("SNAPSHOT");
} else {
for (let version of response["latest"]) {
var branch = get_branch(version);
// already latest version installed if (version.endsWith("SNAPSHOT")) {
if (current_version == version) { response = await request.get(`${server_url}/api/v1/revision/${version}/${target}`)
break; if (!response.ok) {
} error_api_connect(response);
return;
}
// skip branch upgrades outside the advanced mode const remote_revision = response.json().revision;
if (current_branch != branch && advanced_mode == 0) {
continue;
}
candidates.unshift(version); if (get_revision_count(revision) < get_revision_count(remote_revision)) {
candidates.push(version);
}
} else {
response = await request.get(`${server_url}/api/overview`, {
timeout: 8000
});
// don't offer branches older than the current if (!response.ok) {
if (current_branch == branch) { error_api_connect(response);
break; return;
} }
}
}
if (candidates.length) {
var m, s, o;
var mapdata = { const latest = response.json().latest
request: {
board_name: board_name,
target: target,
version: candidates[0],
packages: Object.keys(packages).sort(),
}
}
m = new form.JSONMap(mapdata, ''); for (let remote_version of latest) {
var remote_branch = get_branch(remote_version);
s = m.section(form.NamedSection, 'request', 'example', '', // already latest version installed
'Use defaults for the safest update'); if (version == remote_version) {
o = s.option(form.ListValue, 'version', 'Select firmware version'); break;
for (let candidate of candidates) { }
o.value(candidate, candidate);
}
if (advanced_mode == 1) { // skip branch upgrades outside the advanced mode
o = s.option(form.Value, 'board_name', 'Board Name / Profile'); if (current_branch != remote_branch && advanced_mode == 0) {
o = s.option(form.DynamicList, 'packages', 'Packages'); continue;
} }
candidates.unshift(remote_version);
// don't offer branches older than the current
if (current_branch == remote_branch) {
break;
}
}
}
if (candidates.length) {
var m, s, o;
var mapdata = {
request: {
board_name: board_name,
target: target,
version: candidates[0],
packages: Object.keys(packages).sort(),
}
}
m = new form.JSONMap(mapdata, '');
s = m.section(form.NamedSection, 'request', 'example', '',
'Use defaults for the safest update');
o = s.option(form.ListValue, 'version', 'Select firmware version');
for (let candidate of candidates) {
o.value(candidate, candidate);
}
if (advanced_mode == 1) {
o = s.option(form.Value, 'board_name', 'Board Name / Profile');
o = s.option(form.DynamicList, 'packages', 'Packages');
}
m.render() m.render()
.then(function(form_rendered) { .then(function(form_rendered) {
ui.showModal(_('New upgrade available'), [ ui.showModal(_('New upgrade available'), [
form_rendered, form_rendered,
E('div', { E('div', {
'class': 'right' 'class': 'right'
}, [ }, [
E('div', { E('div', {
'class': 'btn', 'class': 'btn',
'click': ui.hideModal 'click': ui.hideModal
}, _('Cancel')), }, _('Cancel')),
' ', ' ',
E('div', { E('div', {
'class': 'btn cbi-button-action', 'class': 'btn cbi-button-action',
'click': function() { 'click': function() {
m.save().then(foo => { m.save().then(foo => {
request_sysupgrade( request_sysupgrade(
server_url, mapdata.request server_url, mapdata.request
) )
}); });
} }
}, _('Request Sysupgrade')) }, _('Request Sysupgrade'))
]) ])
]); ]);
}); });
} else { } else {
ui.showModal(_('No upgrade available'), [ ui.showModal(_('No upgrade available'), [
E('p', {}, _("The device runs the latest firmware version")), E('p', {}, _(`The device runs the latest firmware version ${version} - ${revision}`)),
E('div', { E('div', {
'class': 'right' 'class': 'right'
}, [ }, [
E('div', { E('div', {
'class': 'btn', 'class': 'btn',
'click': ui.hideModal 'click': ui.hideModal
}, _('Close')) }, _('Close'))
]) ])
]); ]);
} }
})
.catch(error => {
ui.showModal(_('Error connecting to upgrade server'), [
E('p', {}, _('Could not reach API at "%s". Please try again later.'.format(server_url))),
E('pre', {}, error),
E('div', {
'class': 'right'
}, [
E('div', {
'class': 'btn',
'click': ui.hideModal
}, _('Close'))
])
]);
});
} }
function displayStatus(type, content) { function displayStatus(type, content) {
if (type) { if (type) {
var message = ui.showModal('', ''); var message = ui.showModal('', '');
message.classList.add('alert-message'); message.classList.add('alert-message');
DOMTokenList.prototype.add.apply(message.classList, type.split(/\s+/)); DOMTokenList.prototype.add.apply(message.classList, type.split(/\s+/));
if (content) if (content)
dom.content(message, content); dom.content(message, content);
} else { } else {
ui.hideModal(); ui.hideModal();
} }
} }
return view.extend({ return view.extend({
load: function() { load: function() {
return Promise.all([ return Promise.all([
L.resolveDefault(callPackagelist(), {}), L.resolveDefault(callPackagelist(), {}),
L.resolveDefault(callSystemBoard(), {}), L.resolveDefault(callSystemBoard(), {}),
uci.load('attendedsysupgrade') uci.load('attendedsysupgrade')
]); ]);
}, },
render: function(res) { render: function(res) {
var packages = res[0].packages; var packages = res[0].packages;
var current_version = res[1].release.version; var system_board = res[1];
var target = res[1].release.target; var auto_search = uci.get_first('attendedsysupgrade', 'client', 'auto_search') || 1;
var board_name = res[1].board_name; var server_url = uci.get_first('attendedsysupgrade', 'server', 'url');
var auto_search = uci.get_first('attendedsysupgrade', 'client', 'auto_search') || 1;
var server_url = uci.get_first('attendedsysupgrade', 'server', 'url');
var view = [ var view = [
E('h2', _("Attended Sysupgrade")), E('h2', _("Attended Sysupgrade")),
E('p', _('The attended sysupgrade service allows to easily upgrade vanilla and custom firmware images.')), E('p', _('The attended sysupgrade service allows to easily upgrade vanilla and custom firmware images.')),
E('p', _('This is done by building a new firmware on demand via an online service.')) E('p', _('This is done by building a new firmware on demand via an online service.'))
]; ];
if (auto_search == 1) { if (auto_search == 1) {
check_sysupgrade(server_url, current_version, target, board_name, packages) check_sysupgrade(server_url, system_board, packages)
} }
view.push(E('p', { view.push(E('p', {
'class': 'btn cbi-button-positive', 'class': 'btn cbi-button-positive',
'click': function() { 'click': function() {
check_sysupgrade(server_url, current_version, target, board_name, packages) check_sysupgrade(server_url, system_board, packages)
} }
}, _('Search for sysupgrade'))); }, _('Search for sysupgrade')));
return view; return view;
}, },
}); });