luci/applications/luci-app-attendedsysupgrade/htdocs/luci-static/resources/view/attendedsysupgrade/overview.js
Paul Spooren 65266c490a luci-app-attendedsysupgrade: fix missing efi path
If running on a non EFI system, the file `/sys/firmware/efi` is not
available and therefore results in an error of `fs.stat`. Wrap it with
`L.resolveDefault` to avoid the error message and make it work on
non-EFI devices again.

Signed-off-by: Paul Spooren <mail@aparcar.org>
2022-03-09 11:31:25 +01:00

443 lines
12 KiB
JavaScript

'use strict';
'require view';
'require form';
'require uci';
'require rpc';
'require ui';
'require poll';
'require request';
'require dom';
'require fs';
var callPackagelist = rpc.declare({
object: 'rpc-sys',
method: 'packagelist',
});
var callSystemBoard = rpc.declare({
object: 'system',
method: 'board',
});
var callUpgradeStart = rpc.declare({
object: 'rpc-sys',
method: 'upgrade_start',
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) {
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]);
}
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')
},
data: {
url: '',
revision: '',
advanced_mode: 0,
},
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')),
]),
]);
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',
},
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(), {}),
L.resolveDefault(fs.stat("/sys/firmware/efi"), null),
fs.read("/proc/mounts"),
uci.load('attendedsysupgrade'),
]);
},
render: function (res) {
this.data.app_version = res[0].packages['luci-app-attendedsysupgrade'];
this.firmware.packages = res[0].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]
}
this.data.url = uci.get_first('attendedsysupgrade', 'server', 'url');
this.data.advanced_mode = uci.get_first('attendedsysupgrade', 'client', 'advanced_mode') || 0
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
});