luci-app-attendedsysupgrade: LuCIfy codebase

This should make the code a bit more readable and LuCI like instead of
using plain JavaScript.

Handle the filesystem correctly to avoid installing suqashfs images on
ext4 devices and the other way around, also recognize systems running
efi.

Signed-off-by: Paul Spooren <mail@aparcar.org>
(cherry picked from commit de3e0bbffd)
This commit is contained in:
Paul Spooren 2022-03-02 01:42:13 +01:00
parent 9be6f29162
commit ce3599093d
2 changed files with 376 additions and 381 deletions

View file

@ -7,6 +7,7 @@
'require poll';
'require request';
'require dom';
'require fs';
var callPackagelist = rpc.declare({
object: 'rpc-sys',
@ -21,432 +22,422 @@ var callSystemBoard = rpc.declare({
var callUpgradeStart = rpc.declare({
object: 'rpc-sys',
method: 'upgrade_start',
params: [ 'keep' ],
params: ['keep'],
});
/**
* Returns the branch of a given version. This helps to offer upgrades
* for point releases (aka within the branch).
*
* Logic:
* SNAPSHOT -> SNAPSHOT
* 21.02-SNAPSHOT -> 21.02
* 21.02.0-rc1 -> 21.02
* 19.07.8 -> 19.07
*
* @param {string} version
* Input version from which to determine the branch
* @returns {string}
* The determined branch
*/
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('.');
}
/**
* The OpenWrt revision string contains both a hash as well as the number
* commits since the OpenWrt/LEDE reboot. It helps to determine if a
* snapshot is newer than another.
*
* @param {string} revision
* Revision string of a OpenWrt device
* @returns {integer}
* The number of commits since OpenWrt/LEDE reboot
*/
function get_revision_count(revision) {
return parseInt(revision.substring(1).split('-')[0]);
}
function error_api_connect(response) {
ui.showModal(_('Error connecting to upgrade server'), [
E('p', {},
_('Could not reach API at "%s". Please try again later.')
.format(response.url)),
E('pre', {}, response.responseText),
E('div', {
class: 'right',
},
[
E('div', {
class: 'btn',
click: ui.hideModal,
return view.extend({
steps: {
init: _('10% Received build request'),
download_imagebuilder: _('20% Downloading ImageBuilder archive'),
unpack_imagebuilder: _('40% Setup ImageBuilder'),
calculate_packages_hash: _('60% Validate package selection'),
building_image: _('80% Generating firmware image')
},
_('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',
data: {
url: '',
revision: '',
advanced_mode: 0,
},
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');
firmware: {
profile: '',
target: '',
version: '',
packages: [],
diff_packages: true,
},
handle200: function (response) {
res = response.json();
var image;
for (image of res.images) {
if (this.data.rootfs_type == image.filesystem) {
if (this.data.efi) {
if (image.type == 'combined-efi') {
break;
}
} else {
if (image.type == 'sysupgrade' || image.type == 'combined') {
break;
}
});
}
});
});
}
function request_sysupgrade(server_url, data) {
var res, req;
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:
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;
if (image.name != undefined) {
var sysupgrade_url = `${this.data.url}/store/${res.bin_dir}/${image.name}`;
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 keep = E('input', { type: 'checkbox' });
keep.checked = true;
var table = E('div', {
class: 'table',
});
for (var i = 0; i < fields.length; i += 2) {
table.appendChild(E('tr', {
class: 'tr',
},
[
E('td', {
class: 'td left',
width: '33%',
},
[ fields[i] ]),
E('td', {
class: 'td left',
},
[ fields[i + 1] ]),
]));
}
var modal_body = [
table,
E('p', {class: 'mt-2'},
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);
}
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 %s')
.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)),
var fields = [
_('Version'), `${res.version_number} ${res.version_code}`,
_('SHA256'), image.sha256,
];
if (res.stdout) {
body.push(E('b', {}, 'STDOUT:'));
body.push(E('pre', {}, res.stdout));
if (this.data.advanced_mode == 1) {
fields.push(
_('Profile'), res.id,
_('Target'), res.target,
_('Build Date'), res.build_at,
_('Filename'), image.name,
_('Filesystem'), image.filesystem,
)
}
if (res.stderr) {
body.push(E('b', {}, 'STDERR:'));
body.push(E('pre', {}, res.stderr));
fields.push('', E('a', { href: sysupgrade_url }, _('Download firmware image')))
var table = E('div', { class: 'table' });
for (var i = 0; i < fields.length; i += 2) {
table.appendChild(E('tr', { class: 'tr' }, [
E('td', { class: 'td left', width: '33%' }, [fields[i]]),
E('td', { class: 'td left' }, [fields[i + 1]]),
]));
}
body = body.concat([
E('div', {
class: 'right',
var modal_body = [
table,
E('p', { class: 'mt-2' },
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('button', {
'class': 'btn cbi-button cbi-button-positive important',
'click': ui.createHandlerFn(this, function () {
this.handleInstall(sysupgrade_url, keep.checked, image.sha256)
})
}, _('Install firmware image')),
]),
];
ui.showModal(_('Successfully created firmware image'), modal_body);
}
},
[
E('div', {
class: 'btn',
click: ui.hideModal,
},
_('Close')),
]),
handle202: function (response) {
response = response.json();
this.data.request_hash = res.request_hash;
if ('queue_position' in response) {
ui.showModal(_('Queued...'), [
E('p', { 'class': 'spinning' }, _('Request in build queue position %s').format(response.queue_position))
]);
} else {
ui.showModal(_('Building Firmware...'), [
E('p', { 'class': 'spinning' }, _('Progress: %s').format(this.steps[response.imagebuilder_status]))
]);
ui.showModal(_('Error building the sysupgrade'), body);
break;
}
});
}
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;
displayStatus('notice spinning',
E('p', _('Searching for an available sysupgrade of %s - %s')
.format(version, revision)));
if (version.endsWith('SNAPSHOT')) {
response =
await request.get(`${server_url}/api/v1/revision/${version}/${target}`);
if (!response.ok) {
error_api_connect(response);
return;
}
const remote_revision = response.json().revision;
if (get_revision_count(revision) < get_revision_count(remote_revision)) {
candidates.push(version);
}
} else {
response = await request.get(`${server_url}/api/overview`, {
timeout: 8000,
});
if (!response.ok) {
error_api_connect(response);
return;
}
const latest = response.json().latest;
for (let remote_version of latest) {
var remote_branch = get_branch(remote_version);
// already latest version installed
if (version == remote_version) {
break;
}
// 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 %s - %s')
.format(version, revision)),
E('div', {
class: 'right',
},
[
E('div', {
class: 'btn',
click: ui.hideModal,
},
_('Close')),
]),
handleError: function (response) {
response = response.json();
var body = [
E('p', {}, _('Server response: %s').format(response.detail)),
E('a', { href: 'https://github.com/openwrt/asu/issues' }, _('Please report the error message and request')),
E('p', {}, _('Request Data:')),
E('pre', {}, JSON.stringify({ ...this.data, ...this.firmware }, null, 4)),
];
if (response.stdout) {
body.push(E('b', {}, 'STDOUT:'));
body.push(E('pre', {}, response.stdout));
}
if (response.stderr) {
body.push(E('b', {}, 'STDERR:'));
body.push(E('pre', {}, response.stderr));
}
body = body.concat([
E('div', { class: 'right' }, [
E('div', { class: 'btn', click: ui.hideModal }, _('Close')),
]),
]);
}
}
function displayStatus(type, content) {
if (type) {
var message = ui.showModal('', '');
ui.showModal(_('Error building the firmware image'), body);
},
message.classList.add('alert-message');
DOMTokenList.prototype.add.apply(message.classList, type.split(/\s+/));
handleRequest: function () {
var request_url = `${this.data.url}/api/v1/build`;
var method = "POST"
var content = this.firmware;
if (content)
dom.content(message, content);
} else {
ui.hideModal();
}
}
/**
* If `request_hash` is available use a GET request instead of
* sending the entire object.
*/
if (this.data.request_hash) {
request_url += `/${this.data.request_hash}`;
content = {};
method = "GET"
}
return view.extend({
load: function() {
request.request(request_url, { method: method, content: content })
.then((response) => {
switch (response.status) {
case 202:
this.handle202(response);
break;
case 200:
poll.stop();
this.handle200(response);
break;
case 400: // bad request
case 422: // bad package
case 500: // build failed
poll.stop();
this.handleError(response);
break;
}
});
},
handleInstall: function (url, keep, sha256) {
ui.showModal(_('Downloading...'), [
E('p', { 'class': 'spinning' }, _('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());
ui.showModal(_('Uploading...'), [
E('p', { 'class': 'spinning' }, _('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) {
ui.showModal(_('Wrong checksum'), [
E('p', _('Error during download of firmware. Please try again')),
E('div', { class: 'btn', click: ui.hideModal }, _('Close'))
]);
} else {
ui.showModal(_('Installing...'), [
E('p', { class: 'spinning' }, _('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');
}
});
}
});
});
},
handleCheck: function () {
var { url, revision } = this.data
var { version, target } = this.firmware
var candidates = [];
var response;
var request_url = `${url}/api/overview`;
if (version.endsWith('SNAPSHOT')) {
request_url = `${url}/api/v1/revision/${version}/${target}`;
}
ui.showModal(_('Searching...'), [
E('p', { 'class': 'spinning' },
_('Searching for an available sysupgrade of %s - %s').format(version, revision))
]);
L.resolveDefault(request.get(request_url))
.then(response => {
if (!response.ok) {
ui.showModal(_('Error connecting to upgrade server'), [
E('p', {}, _('Could not reach API at "%s". Please try again later.').format(response.url)),
E('pre', {}, response.responseText),
E('div', { class: 'right' }, [
E('div', { class: 'btn', click: ui.hideModal }, _('Close'))
]),
]);
return;
}
if (version.endsWith('SNAPSHOT')) {
const remote_revision = response.json().revision;
if (get_revision_count(revision) < get_revision_count(remote_revision)) {
candidates.push([version, remote_revision]);
}
} else {
const latest = response.json().latest;
for (let remote_version of latest) {
var remote_branch = get_branch(remote_version);
// already latest version installed
if (version == remote_version) {
break;
}
// skip branch upgrades outside the advanced mode
if (this.data.branch != remote_branch && this.data.advanced_mode == 0) {
continue;
}
candidates.unshift([remote_version, null]);
// don't offer branches older than the current
if (this.data.branch == remote_branch) {
break;
}
}
}
if (candidates.length) {
var m, s, o;
var mapdata = {
request: {
profile: this.firmware.profile,
version: candidates[0][0],
packages: Object.keys(this.firmware.packages).sort(),
},
};
var map = new form.JSONMap(mapdata, '');
s = map.section(form.NamedSection, 'request', '', '', 'Use defaults for the safest update');
o = s.option(form.ListValue, 'version', 'Select firmware version');
for (let candidate of candidates) {
o.value(candidate[0], candidate[1] ? `${candidate[0]} - ${candidate[1]}` : candidate[0]);
}
if (this.data.advanced_mode == 1) {
o = s.option(form.Value, 'profile', _('Board Name / Profile'));
o = s.option(form.DynamicList, 'packages', _('Packages'));
}
L.resolveDefault(map.render()).
then(form_rendered => {
ui.showModal(_('New firmware upgrade available'), [
E('p', _('Currently running: %s - %s').format(this.firmware.version, this.data.revision)),
form_rendered,
E('div', { class: 'right' }, [
E('div', { class: 'btn', click: ui.hideModal }, _('Cancel')), ' ',
E('button', {
'class': 'btn cbi-button cbi-button-positive important',
'click': ui.createHandlerFn(this, function () {
map.save().then(() => {
this.firmware.packages = mapdata.request.packages;
this.firmware.version = mapdata.request.version;
this.firmware.profile = mapdata.request.profile;
poll.add(L.bind(this.handleRequest, this), 5);
});
})
}, _('Request firmware image')),
]),
]);
});
} else {
ui.showModal(_('No upgrade available'), [
E('p', _('The device runs the latest firmware version %s - %s').format(version, revision)),
E('div', { class: 'right' }, [
E('div', { class: 'btn', click: ui.hideModal }, _('Close')),
]),
]);
}
});
},
load: function () {
return Promise.all([
L.resolveDefault(callPackagelist(), {}),
L.resolveDefault(callSystemBoard(), {}),
fs.stat("/sys/firmware/efi"),
fs.read("/proc/mounts"),
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.')),
];
render: function (res) {
this.data.app_version = res[0].packages['luci-app-attendedsysupgrade'];
this.firmware.packages = res[0].packages;
if (auto_search == 1) {
check_sysupgrade(server_url, system_board, packages);
this.firmware.profile = res[1].board_name;
this.firmware.target = res[1].release.target;
this.firmware.version = res[1].release.version;
this.data.branch = get_branch(res[1].release.version);
this.data.revision = res[1].release.revision;
this.data.efi = res[2];
if (res[1].rootfs_type) {
this.data.rootfs_type = res[1].rootfs_type;
} else {
this.data.rootfs_type = res[3].split(/\r?\n/)[0].split(' ')[2]
}
view.push(E('p', {
class: 'btn cbi-button-positive',
click:
function() { check_sysupgrade(server_url, system_board, packages); },
},
_('Search for sysupgrade')));
this.data.url = uci.get_first('attendedsysupgrade', 'server', 'url');
this.data.advanced_mode = uci.get_first('attendedsysupgrade', 'client', 'advanced_mode') || 0
return view;
return E('p', [
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.')),
E('p', _('Currently running: %s - %s').format(this.firmware.version, this.data.revision)),
E('button', {
'class': 'btn cbi-button cbi-button-positive important',
'click': ui.createHandlerFn(this, this.handleCheck)
}, _('Search for firmware upgrade'))
]);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});

View file

@ -15,6 +15,10 @@
"get"
]
},
"file": {
"/sys/firmware/efi": [ "list" ],
"/proc/mounts": [ "read" ]
},
"uci": [
"attendedsysupgrade"
]