From ce3599093d947eda6cd93665794d6783f6568216 Mon Sep 17 00:00:00 2001 From: Paul Spooren Date: Wed, 2 Mar 2022 01:42:13 +0100 Subject: [PATCH] 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 (cherry picked from commit de3e0bbffd87a3e62f59c7206dff48bfc0467a09) --- .../view/attendedsysupgrade/overview.js | 727 +++++++++--------- .../acl.d/luci-app-attendedsysupgrade.json | 4 + 2 files changed, 363 insertions(+), 368 deletions(-) diff --git a/applications/luci-app-attendedsysupgrade/htdocs/luci-static/resources/view/attendedsysupgrade/overview.js b/applications/luci-app-attendedsysupgrade/htdocs/luci-static/resources/view/attendedsysupgrade/overview.js index b7cbdc2231..8c1a5a84b1 100644 --- a/applications/luci-app-attendedsysupgrade/htdocs/luci-static/resources/view/attendedsysupgrade/overview.js +++ b/applications/luci-app-attendedsysupgrade/htdocs/luci-static/resources/view/attendedsysupgrade/overview.js @@ -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')), - ]); + + 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 { - 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'); + 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; - - 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); - } + 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; - 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', - }, - [ - E('div', { - class: 'btn', - click: ui.hideModal, - }, - _('Close')), - ]), - ]); - ui.showModal(_('Error building the sysupgrade'), body); - break; + 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; -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; + 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])) + ]); } + }, - const remote_revision = response.json().revision; + 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 (get_revision_count(revision) < get_revision_count(remote_revision)) { - candidates.push(version); + if (response.stdout) { + body.push(E('b', {}, 'STDOUT:')); + body.push(E('pre', {}, response.stdout)); } - } else { - response = await request.get(`${server_url}/api/overview`, { - timeout: 8000, - }); - - if (!response.ok) { - error_api_connect(response); - return; + + if (response.stderr) { + body.push(E('b', {}, 'STDERR:')); + body.push(E('pre', {}, response.stderr)); } - const latest = response.json().latest; + body = body.concat([ + E('div', { class: 'right' }, [ + E('div', { class: 'btn', click: ui.hideModal }, _('Close')), + ]), + ]); - for (let remote_version of latest) { - var remote_branch = get_branch(remote_version); + ui.showModal(_('Error building the firmware image'), body); + }, - // already latest version installed - if (version == remote_version) { - break; - } + 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" + } - // skip branch upgrades outside the advanced mode - if (current_branch != remote_branch && advanced_mode == 0) { - continue; - } + 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; + } + }); + }, - candidates.unshift(remote_version); + handleInstall: function (url, keep, sha256) { + ui.showModal(_('Downloading...'), [ + E('p', { 'class': 'spinning' }, _('Downloading firmware from server to browser')) + ]); - // don't offer branches older than the current - if (current_branch == remote_branch) { - break; - } + 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}`; } - } - if (candidates.length) { - var m, s, o; + ui.showModal(_('Searching...'), [ + E('p', { 'class': 'spinning' }, + _('Searching for an available sysupgrade of %s - %s').format(version, revision)) + ]); - var mapdata = { - request: { - board_name: board_name, - target: target, - version: candidates[0], - packages: Object.keys(packages).sort(), - }, - }; + 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; - 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 (this.data.branch != remote_branch && this.data.advanced_mode == 0) { + continue; + } - 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')), - ]), - ]); - } -} + candidates.unshift([remote_version, null]); -function displayStatus(type, content) { - if (type) { - var message = ui.showModal('', ''); + // don't offer branches older than the current + if (this.data.branch == remote_branch) { + break; + } + } + } - message.classList.add('alert-message'); - DOMTokenList.prototype.add.apply(message.classList, type.split(/\s+/)); + if (candidates.length) { + var m, s, o; - if (content) - dom.content(message, content); - } else { - ui.hideModal(); - } -} + var mapdata = { + request: { + profile: this.firmware.profile, + version: candidates[0][0], + packages: Object.keys(this.firmware.packages).sort(), + }, + }; -return view.extend({ - load: function() { + 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.')), - ]; - if (auto_search == 1) { - check_sysupgrade(server_url, system_board, packages); + 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] } - 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 }); diff --git a/applications/luci-app-attendedsysupgrade/root/usr/share/rpcd/acl.d/luci-app-attendedsysupgrade.json b/applications/luci-app-attendedsysupgrade/root/usr/share/rpcd/acl.d/luci-app-attendedsysupgrade.json index ec102e3dad..378967da34 100644 --- a/applications/luci-app-attendedsysupgrade/root/usr/share/rpcd/acl.d/luci-app-attendedsysupgrade.json +++ b/applications/luci-app-attendedsysupgrade/root/usr/share/rpcd/acl.d/luci-app-attendedsysupgrade.json @@ -15,6 +15,10 @@ "get" ] }, + "file": { + "/sys/firmware/efi": [ "list" ], + "/proc/mounts": [ "read" ] + }, "uci": [ "attendedsysupgrade" ] -- 2.30.2