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