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,46 +22,220 @@ 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',
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')
},
[
E('div', {
class: 'btn',
click: ui.hideModal,
data: {
url: '',
revision: '',
advanced_mode: 0,
},
_('Close')),
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;
}
}
}
}
if (image.name != undefined) {
var sysupgrade_url = `${this.data.url}/store/${res.bin_dir}/${image.name}`;
var keep = E('input', { type: 'checkbox' });
keep.checked = true;
var fields = [
_('Version'), `${res.version_number} ${res.version_code}`,
_('SHA256'), image.sha256,
];
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,
)
}
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]]),
]));
}
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);
}
},
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]))
]);
}
},
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 install_sysupgrade(url, keep, sha256) {
displayStatus('notice spinning',
E('p', _('Downloading firmware from server to browser')));
request
.get(url, {
ui.showModal(_('Error building the firmware image'), body);
},
handleRequest: function () {
var request_url = `${this.data.url}/api/v1/build`;
var method = "POST"
var content = this.firmware;
/**
* 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"
}
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',
},
@ -73,8 +248,10 @@ function install_sysupgrade(url, keep, sha256) {
form_data.append('filemode', 600);
form_data.append('filedata', response.blob());
displayStatus('notice spinning',
E('p', _('Uploading firmware from browser to device')));
ui.showModal(_('Uploading...'), [
E('p', { 'class': 'spinning' }, _('Uploading firmware from browser to device'))
]);
request
.get(`${L.env.cgi_base}/cgi-upload`, {
method: 'PUT',
@ -83,21 +260,16 @@ function install_sysupgrade(url, keep, sha256) {
.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')),
ui.showModal(_('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!')));
ui.showModal(_('Installing...'), [
E('p', { class: 'spinning' }, _('Installing the sysupgrade. Do not unpower device!'))
]);
L.resolveDefault(callUpgradeStart(keep), {})
.then((response) => {
if (keep) {
@ -109,202 +281,41 @@ function install_sysupgrade(url, keep, sha256) {
}
});
});
}
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;
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',
});
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)),
];
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));
}
body = body.concat([
E('div', {
class: 'right',
},
[
E('div', {
class: 'btn',
click: ui.hideModal,
},
_('Close')),
]),
]);
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;
handleCheck: function () {
var { url, revision } = this.data
var { version, target } = this.firmware
var candidates = [];
var response;
displayStatus('notice spinning',
E('p', _('Searching for an available sysupgrade of %s - %s')
.format(version, revision)));
var request_url = `${url}/api/overview`;
if (version.endsWith('SNAPSHOT')) {
response =
await request.get(`${server_url}/api/v1/revision/${version}/${target}`);
if (!response.ok) {
error_api_connect(response);
return;
request_url = `${url}/api/v1/revision/${version}/${target}`;
}
const remote_revision = response.json().revision;
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);
candidates.push([version, remote_revision]);
}
} 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) {
@ -316,14 +327,14 @@ async function check_sysupgrade(server_url, system_board, packages) {
}
// skip branch upgrades outside the advanced mode
if (current_branch != remote_branch && advanced_mode == 0) {
if (this.data.branch != remote_branch && this.data.advanced_mode == 0) {
continue;
}
candidates.unshift(remote_version);
candidates.unshift([remote_version, null]);
// don't offer branches older than the current
if (current_branch == remote_branch) {
if (this.data.branch == remote_branch) {
break;
}
}
@ -334,119 +345,99 @@ async function check_sysupgrade(server_url, system_board, packages) {
var mapdata = {
request: {
board_name: board_name,
target: target,
version: candidates[0],
packages: Object.keys(packages).sort(),
profile: this.firmware.profile,
version: candidates[0][0],
packages: Object.keys(this.firmware.packages).sort(),
},
};
m = new form.JSONMap(mapdata, '');
var map = new form.JSONMap(mapdata, '');
s = m.section(form.NamedSection, 'request', 'example', '',
'Use defaults for the safest update');
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, candidate);
o.value(candidate[0], candidate[1] ? `${candidate[0]} - ${candidate[1]}` : candidate[0]);
}
if (advanced_mode == 1) {
o = s.option(form.Value, 'board_name', 'Board Name / Profile');
o = s.option(form.DynamicList, 'packages', 'Packages');
if (this.data.advanced_mode == 1) {
o = s.option(form.Value, 'profile', _('Board Name / Profile'));
o = s.option(form.DynamicList, 'packages', _('Packages'));
}
m.render().then(function(form_rendered) {
ui.showModal(_('New upgrade available'), [
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('div', {
class: 'btn cbi-button-action',
click: function() {
m.save().then((foo) => {
request_sysupgrade(server_url, mapdata.request);
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 Sysupgrade')),
})
}, _('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')),
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')),
]),
]);
}
}
function displayStatus(type, content) {
if (type) {
var message = ui.showModal('', '');
});
},
message.classList.add('alert-message');
DOMTokenList.prototype.add.apply(message.classList, type.split(/\s+/));
if (content)
dom.content(message, content);
} else {
ui.hideModal();
}
}
return view.extend({
load: function() {
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"
]