X-Git-Url: http://git.openwrt.org/?p=project%2Fluci.git;a=blobdiff_plain;f=applications%2Fluci-app-attendedsysupgrade%2Fhtdocs%2Fluci-static%2Fresources%2Fview%2Fattendedsysupgrade%2Foverview.js;h=0e6ae9d20cca100e8f9e8a6b7c2bfabb893d82cd;hp=5d5d4bd9ba8f01dd171f06359f35f741a682e1f7;hb=HEAD;hpb=53ff7ca676d10fa253d3ab472b9b5645996527ed 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 5d5d4bd9ba..0e6ae9d20c 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,384 +7,630 @@ 'require poll'; 'require request'; 'require dom'; +'require fs'; -var callPackagelist = rpc.declare({ +let callPackagelist = rpc.declare({ object: 'rpc-sys', method: 'packagelist', }); -var callSystemBoard = rpc.declare({ +let callSystemBoard = rpc.declare({ object: 'system', - method: 'board' + method: 'board', }); -var callUpgradeStart = rpc.declare({ +let 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("."); + return version.replace('-SNAPSHOT', '').split('.').slice(0, 2).join('.'); } -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', _('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'); - } - }); - } - }); - }); +/** + * 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 request_sysupgrade(server_url, data) { - 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, - }) - } - - req.then(response => { - switch (response.status) { - case 200: - var res = response.json() - var image; - for (image of res.images) { - if (image.type == "sysupgrade") { - break; - } +return view.extend({ + steps: { + init: [10, _('Received build request')], + download_imagebuilder: [20, _('Downloading ImageBuilder archive')], + unpack_imagebuilder: [40, _('Setting Up ImageBuilder')], + calculate_packages_hash: [60, _('Validate package selection')], + building_image: [80, _('Generating firmware image')], + }, + + request_hash: '', + sha256_unsigned: '', + + selectImage: function (images, data, firmware) { + var filesystemFilter = function(e) { + return (e.filesystem == firmware.filesystem); + } + var typeFilter = function(e) { + if (firmware.target.indexOf("x86") != -1) { + // x86 images can be combined-efi (EFI) or combined (BIOS) + if (data.efi) { + return (e.type == 'combined-efi'); + } else { + return (e.type == 'combined'); } - if (image.name != undefined) { - var sysupgrade_url = server_url + "/store/" + res.bin_dir + "/" + image.name; + } else { + return (e.type == 'sysupgrade' || e.type == 'combined'); + } + } + return images.filter(filesystemFilter).filter(typeFilter)[0]; + }, - 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' - }); + handle200: function (response, content, data, firmware) { + response = response.json(); + let image = this.selectImage(response.images, data, firmware); + + if (image.name != undefined) { + this.sha256_unsigned = image.sha256_unsigned; + let sysupgrade_url = `${data.url}/store/${response.bin_dir}/${image.name}`; + + let keep = E('input', { type: 'checkbox' }); + keep.checked = true; + + let fields = [ + _('Version'), + `${response.version_number} ${response.version_code}`, + _('SHA256'), + image.sha256, + ]; + + if (data.advanced_mode == 1) { + fields.push( + _('Profile'), + response.id, + _('Target'), + response.target, + _('Build Date'), + response.build_at, + _('Filename'), + image.name, + _('Filesystem'), + image.filesystem + ); + } - 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] : '?']) - ])); - } + fields.push( + '', + E('a', { href: sysupgrade_url }, _('Download firmware image')) + ); + if (data.rebuilder) { + fields.push(_('Rebuilds'), E('div', { id: 'rebuilder_status' })); + } - 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')) - ]) - ] + let table = E('div', { class: 'table' }); - ui.showModal(_('Successfully created sysupgrade image'), modal_body); - } + for (let 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]]), + ]) + ); + } - 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'))); - - 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.message)), - 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)) + let 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); + if (data.rebuilder) { + this.handleRebuilder(content, data, firmware); + } + } + }, - } + handle202: function (response) { + response = response.json(); + this.request_hash = response.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%% %s').format( + this.steps[response.imagebuilder_status][0], + this.steps[response.imagebuilder_status][1] + ) + ), + ]); + } + }, - if (res.stderr) { - body.push(E('b', {}, "STDERR:")) - body.push(E('pre', {}, res.stderr)) + handleError: function (response, data, firmware) { + response = response.json(); + const request_data = { + ...data, + request_hash: this.request_hash, + sha256_unsigned: this.sha256_unsigned, + ...firmware + }; + let 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({ ...request_data }, 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 (server, main, content, data, firmware) { + let request_url = `${server}/api/v1/build`; + let method = 'POST'; + let local_content = content; + + /** + * If `request_hash` is available use a GET request instead of + * sending the entire object. + */ + if (this.request_hash && main == true) { + request_url += `/${this.request_hash}`; + local_content = {}; + method = 'GET'; + } + + request + .request(request_url, { method: method, content: local_content }) + .then((response) => { + switch (response.status) { + case 202: + if (main) { + this.handle202(response); + } else { + response = response.json(); + + let view = document.getElementById(server); + view.innerText = `⏳ (${ + this.steps[response.imagebuilder_status][0] + }%) ${server}`; + } + break; + case 200: + if (main == true) { + poll.remove(this.pollFn); + this.handle200(response, content, data, firmware); + } else { + poll.remove(this.rebuilder_polls[server]); + response = response.json(); + let view = document.getElementById(server); + let image = this.selectImage(response.images, data, firmware); + if (image.sha256_unsigned == this.sha256_unsigned) { + view.innerText = '✅ %s'.format(server); + } else { + view.innerHTML = `⚠️ ${server} (${_('Download')})`; + } + } + break; + case 400: // bad request + case 422: // bad package + case 500: // build failed + if (main == true) { + poll.remove(this.pollFn); + this.handleError(response, data, firmware); + break; + } else { + poll.remove(this.rebuilder_polls[server]); + document.getElementById(server).innerText = '🚫 %s'.format( + server + ); + } } + }); + }, - body = body.concat([ - E('div', { - 'class': 'right' - }, [ - E('div', { - 'class': 'btn', - 'click': ui.hideModal - }, _('Close')) - ]) + handleRebuilder: function (content, data, firmware) { + this.rebuilder_polls = {}; + for (let rebuilder of data.rebuilder) { + this.rebuilder_polls[rebuilder] = L.bind( + this.handleRequest, + this, + rebuilder, + false, + content, + data, + firmware + ); + poll.add(this.rebuilder_polls[rebuilder], 5); + document.getElementById( + 'rebuilder_status' + ).innerHTML += `

⏳ ${rebuilder}

`; + } + poll.start(); + }, + + 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) => { + let 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') + ), ]); - ui.showModal(_('Error building the sysupgrade'), body); - break; + + 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 (data, firmware) { + this.request_hash = ''; + let { url, revision, advanced_mode, branch } = data; + let { version, target, profile, packages } = firmware; + let candidates = []; + let request_url = `${url}/api/overview`; + if (version.endsWith('SNAPSHOT')) { + request_url = `${url}/api/v1/revision/${version}/${target}`; } - }); -} -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 = []; - - request.get(server_url + "/api/latest", { - timeout: 8000 - }) - .then(response => response.json()) - .then(response => { - if (current_version == "SNAPSHOT") { - candidates.push("SNAPSHOT"); + 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 { - for (let version of response["latest"]) { - var branch = get_branch(version); + const latest = response.json().latest; + + for (let remote_version of latest) { + let remote_branch = get_branch(remote_version); // already latest version installed - if (current_version == version) { + if (version == remote_version) { break; } // skip branch upgrades outside the advanced mode - if (current_branch != branch && advanced_mode == 0) { + if (branch != remote_branch && advanced_mode == 0) { continue; } - candidates.unshift(version); + candidates.unshift([remote_version, null]); // don't offer branches older than the current - if (current_branch == branch) { + if (branch == remote_branch) { break; } } } + + // allow to re-install running firmware in advanced mode + if (advanced_mode == 1) { + candidates.unshift([version, revision]); + } + if (candidates.length) { - var m, s, o; + let s, o; - var mapdata = { + let mapdata = { request: { - board_name: board_name, - target: target, - version: candidates[0], + profile, + version: candidates[0][0], packages: Object.keys(packages).sort(), - } - } - - m = new form.JSONMap(mapdata, ''); - - s = m.section(form.NamedSection, 'request', 'example', '', - 'Use defaults for the safest update'); + }, + }; + + let 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, candidate); + if (candidate[0] == version && candidate[1] == revision) { + o.value( + candidate[0], + _('[installed] %s').format( + candidate[1] + ? `${candidate[0]} - ${candidate[1]}` + : candidate[0] + ) + ); + } else { + 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'); + 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'), [ - 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 - ) + L.resolveDefault(map.render()).then((form_rendered) => { + ui.showModal(_('New firmware upgrade available'), [ + E( + 'p', + _('Currently running: %s - %s').format( + version, + 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(() => { + const content = { + ...firmware, + packages: mapdata.request.packages, + version: mapdata.request.version, + profile: mapdata.request.profile + }; + this.pollFn = L.bind(function () { + this.handleRequest(url, true, content, data, firmware); + }, this); + poll.add(this.pollFn, 5); + poll.start(); }); - } - }, _('Request Sysupgrade')) - ]) - ]); - }); + }), + }, + _('Request firmware image') + ), + ]), + ]); + }); } 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')) - ]) + 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')), + ]), ]); } - }) - .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')) - ]) - ]); }); -} - -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() { - return Promise.all([ + load: async function () { + const promises = await Promise.all([ L.resolveDefault(callPackagelist(), {}), L.resolveDefault(callSystemBoard(), {}), - uci.load('attendedsysupgrade') + L.resolveDefault(fs.stat('/sys/firmware/efi'), null), + uci.load('attendedsysupgrade'), ]); + const data = { + url: uci.get_first('attendedsysupgrade', 'server', 'url'), + branch: get_branch(promises[1].release.version), + revision: promises[1].release.revision, + efi: promises[2], + advanced_mode: uci.get_first('attendedsysupgrade', 'client', 'advanced_mode') || 0, + rebuilder: uci.get_first('attendedsysupgrade', 'server', 'rebuilder') + }; + const firmware = { + client: 'luci/' + promises[0].packages['luci-app-attendedsysupgrade'], + packages: promises[0].packages, + profile: promises[1].board_name, + target: promises[1].release.target, + version: promises[1].release.version, + diff_packages: true, + filesystem: promises[1].rootfs_type + }; + return [data, firmware]; }, - 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'); - - 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) - } - - view.push(E('p', { - 'class': 'btn cbi-button-positive', - 'click': function() { - check_sysupgrade(server_url, current_version, target, board_name, packages) - } - }, _('Search for sysupgrade'))); - - return view; + render: function (response) { + const data = response[0]; + const firmware = response[1]; + + 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( + firmware.version, + data.revision + ) + ), + E( + 'button', + { + class: 'btn cbi-button cbi-button-positive important', + click: ui.createHandlerFn(this, this.handleCheck, data, firmware), + }, + _('Search for firmware upgrade') + ), + ]); }, - + handleSaveApply: null, + handleSave: null, + handleReset: null, });