12 let callPackagelist = rpc.declare({
14 method: 'packagelist',
17 let callSystemBoard = rpc.declare({
22 let callUpgradeStart = rpc.declare({
24 method: 'upgrade_start',
29 * Returns the branch of a given version. This helps to offer upgrades
30 * for point releases (aka within the branch).
33 * SNAPSHOT -> SNAPSHOT
34 * 21.02-SNAPSHOT -> 21.02
35 * 21.02.0-rc1 -> 21.02
38 * @param {string} version
39 * Input version from which to determine the branch
41 * The determined branch
43 function get_branch(version) {
44 return version.replace('-SNAPSHOT', '').split('.').slice(0, 2).join('.');
48 * The OpenWrt revision string contains both a hash as well as the number
49 * commits since the OpenWrt/LEDE reboot. It helps to determine if a
50 * snapshot is newer than another.
52 * @param {string} revision
53 * Revision string of a OpenWrt device
55 * The number of commits since OpenWrt/LEDE reboot
57 function get_revision_count(revision) {
58 return parseInt(revision.substring(1).split('-')[0]);
63 init: [ 0, _('Received build request')],
64 container_setup: [ 10, _('Setting up ImageBuilder')],
65 validate_revision: [ 20, _('Validating revision')],
66 validate_manifest: [ 30, _('Validating package selection')],
67 calculate_packages_hash: [ 40, _('Calculating package hash')],
68 building_image: [ 50, _('Generating firmware image')],
69 signing_images: [ 95, _('Signing images')],
70 done: [100, _('Completed generating firmware image')],
71 failed: [100, _('Failed to generate firmware image')],
73 /* Obsolete status values, retained for backward compatibility. */
74 download_imagebuilder: [ 20, _('Downloading ImageBuilder archive')],
75 unpack_imagebuilder: [ 40, _('Setting Up ImageBuilder')],
81 selectImage: function (images, data, firmware) {
82 var filesystemFilter = function(e) {
83 return (e.filesystem == firmware.filesystem);
85 var typeFilter = function(e) {
86 if (firmware.target.indexOf("x86") != -1) {
87 // x86 images can be combined-efi (EFI) or combined (BIOS)
89 return (e.type == 'combined-efi');
91 return (e.type == 'combined');
94 return (e.type == 'sysupgrade' || e.type == 'combined');
97 return images.filter(filesystemFilter).filter(typeFilter)[0];
100 handle200: function (response, content, data, firmware) {
101 response = response.json();
102 let image = this.selectImage(response.images, data, firmware);
104 if (image.name != undefined) {
105 this.sha256_unsigned = image.sha256_unsigned;
106 let sysupgrade_url = `${data.url}/store/${response.bin_dir}/${image.name}`;
108 let keep = E('input', { type: 'checkbox' });
113 `${response.version_number} ${response.version_code}`,
118 if (data.advanced_mode == 1) {
135 E('a', { href: sysupgrade_url }, _('Download firmware image'))
137 if (data.rebuilder) {
138 fields.push(_('Rebuilds'), E('div', { id: 'rebuilder_status' }));
141 let table = E('div', { class: 'table' });
143 for (let i = 0; i < fields.length; i += 2) {
145 E('tr', { class: 'tr' }, [
146 E('td', { class: 'td left', width: '33%' }, [fields[i]]),
147 E('td', { class: 'td left' }, [fields[i + 1]]),
157 E('label', { class: 'btn' }, [
160 _('Keep settings and retain the current configuration'),
163 E('div', { class: 'right' }, [
164 E('div', { class: 'btn', click: ui.hideModal }, _('Cancel')),
169 class: 'btn cbi-button cbi-button-positive important',
170 click: ui.createHandlerFn(this, function () {
171 this.handleInstall(sysupgrade_url, keep.checked, image.sha256);
174 _('Install firmware image')
179 ui.showModal(_('Successfully created firmware image'), modal_body);
180 if (data.rebuilder) {
181 this.handleRebuilder(content, data, firmware);
186 handle202: function (response) {
187 response = response.json();
188 this.request_hash = response.request_hash;
190 if ('queue_position' in response) {
191 ui.showModal(_('Queued...'), [
194 { class: 'spinning' },
195 _('Request in build queue position %s').format(
196 response.queue_position
201 ui.showModal(_('Building Firmware...'), [
204 { class: 'spinning' },
205 _('Progress: %s%% %s').format(
206 this.steps[response.imagebuilder_status][0],
207 this.steps[response.imagebuilder_status][1]
214 handleError: function (response, data, firmware) {
215 response = response.json();
216 const request_data = {
218 request_hash: this.request_hash,
219 sha256_unsigned: this.sha256_unsigned,
223 E('p', {}, _('Server response: %s').format(response.detail)),
226 { href: 'https://github.com/openwrt/asu/issues' },
227 _('Please report the error message and request')
229 E('p', {}, _('Request Data:')),
230 E('pre', {}, JSON.stringify({ ...request_data }, null, 4)),
233 if (response.stdout) {
234 body.push(E('b', {}, 'STDOUT:'));
235 body.push(E('pre', {}, response.stdout));
238 if (response.stderr) {
239 body.push(E('b', {}, 'STDERR:'));
240 body.push(E('pre', {}, response.stderr));
244 E('div', { class: 'right' }, [
245 E('div', { class: 'btn', click: ui.hideModal }, _('Close')),
249 ui.showModal(_('Error building the firmware image'), body);
252 handleRequest: function (server, main, content, data, firmware) {
253 let request_url = `${server}/api/v1/build`;
255 let local_content = content;
258 * If `request_hash` is available use a GET request instead of
259 * sending the entire object.
261 if (this.request_hash && main == true) {
262 request_url += `/${this.request_hash}`;
268 .request(request_url, { method: method, content: local_content })
269 .then((response) => {
270 switch (response.status) {
273 this.handle202(response);
275 response = response.json();
277 let view = document.getElementById(server);
278 view.innerText = `⏳ (${
279 this.steps[response.imagebuilder_status][0]
285 poll.remove(this.pollFn);
286 this.handle200(response, content, data, firmware);
288 poll.remove(this.rebuilder_polls[server]);
289 response = response.json();
290 let view = document.getElementById(server);
291 let image = this.selectImage(response.images, data, firmware);
292 if (image.sha256_unsigned == this.sha256_unsigned) {
293 view.innerText = '✅ %s'.format(server);
295 view.innerHTML = `⚠️ ${server} (<a href="${server}/store/${
297 }/${image.name}">${_('Download')}</a>)`;
301 case 400: // bad request
302 case 422: // bad package
303 case 500: // build failed
305 poll.remove(this.pollFn);
306 this.handleError(response, data, firmware);
309 poll.remove(this.rebuilder_polls[server]);
310 document.getElementById(server).innerText = '🚫 %s'.format(
318 handleRebuilder: function (content, data, firmware) {
319 this.rebuilder_polls = {};
320 for (let rebuilder of data.rebuilder) {
321 this.rebuilder_polls[rebuilder] = L.bind(
330 poll.add(this.rebuilder_polls[rebuilder], 5);
331 document.getElementById(
333 ).innerHTML += `<p id="${rebuilder}">⏳ ${rebuilder}</p>`;
338 handleInstall: function (url, keep, sha256) {
339 ui.showModal(_('Downloading...'), [
342 { class: 'spinning' },
343 _('Downloading firmware from server to browser')
350 'Content-Type': 'application/x-www-form-urlencoded',
352 responseType: 'blob',
354 .then((response) => {
355 let form_data = new FormData();
356 form_data.append('sessionid', rpc.getSessionID());
357 form_data.append('filename', '/tmp/firmware.bin');
358 form_data.append('filemode', 600);
359 form_data.append('filedata', response.blob());
361 ui.showModal(_('Uploading...'), [
364 { class: 'spinning' },
365 _('Uploading firmware from browser to device')
370 .get(`${L.env.cgi_base}/cgi-upload`, {
374 .then((response) => response.json())
375 .then((response) => {
376 if (response.sha256sum != sha256) {
377 ui.showModal(_('Wrong checksum'), [
380 _('Error during download of firmware. Please try again')
382 E('div', { class: 'btn', click: ui.hideModal }, _('Close')),
385 ui.showModal(_('Installing...'), [
388 { class: 'spinning' },
389 _('Installing the sysupgrade. Do not unpower device!')
393 L.resolveDefault(callUpgradeStart(keep), {}).then((response) => {
395 ui.awaitReconnect(window.location.host);
397 ui.awaitReconnect('192.168.1.1', 'openwrt.lan');
405 handleCheck: function (data, firmware) {
406 this.request_hash = '';
407 let { url, revision, advanced_mode, branch } = data;
408 let { version, target, profile, packages } = firmware;
411 const endpoint = version.endsWith('SNAPSHOT') ? `revision/${version}/${target}` : 'overview';
412 const request_url = `${url}/api/v1/${endpoint}`;
414 ui.showModal(_('Searching...'), [
417 { class: 'spinning' },
418 _('Searching for an available sysupgrade of %s - %s').format(
425 L.resolveDefault(request.get(request_url)).then((response) => {
427 ui.showModal(_('Error connecting to upgrade server'), [
431 _('Could not reach API at "%s". Please try again later.').format(
435 E('pre', {}, response.responseText),
436 E('div', { class: 'right' }, [
437 E('div', { class: 'btn', click: ui.hideModal }, _('Close')),
442 if (version.endsWith('SNAPSHOT')) {
443 const remote_revision = response.json().revision;
445 get_revision_count(revision) < get_revision_count(remote_revision)
447 candidates.push([version, remote_revision]);
450 const latest = response.json().latest;
452 for (let remote_version of latest) {
453 let remote_branch = get_branch(remote_version);
455 // already latest version installed
456 if (version == remote_version) {
460 // skip branch upgrades outside the advanced mode
461 if (branch != remote_branch && advanced_mode == 0) {
465 candidates.unshift([remote_version, null]);
467 // don't offer branches older than the current
468 if (branch == remote_branch) {
474 // allow to re-install running firmware in advanced mode
475 if (advanced_mode == 1) {
476 candidates.unshift([version, revision]);
479 if (candidates.length) {
485 version: candidates[0][0],
486 packages: Object.keys(packages).sort(),
490 let map = new form.JSONMap(mapdata, '');
497 'Use defaults for the safest update'
499 o = s.option(form.ListValue, 'version', 'Select firmware version');
500 for (let candidate of candidates) {
501 if (candidate[0] == version && candidate[1] == revision) {
504 _('[installed] %s').format(
506 ? `${candidate[0]} - ${candidate[1]}`
513 candidate[1] ? `${candidate[0]} - ${candidate[1]}` : candidate[0]
518 if (advanced_mode == 1) {
519 o = s.option(form.Value, 'profile', _('Board Name / Profile'));
520 o = s.option(form.DynamicList, 'packages', _('Packages'));
523 L.resolveDefault(map.render()).then((form_rendered) => {
524 ui.showModal(_('New firmware upgrade available'), [
527 _('Currently running: %s - %s').format(
533 E('div', { class: 'right' }, [
534 E('div', { class: 'btn', click: ui.hideModal }, _('Cancel')),
539 class: 'btn cbi-button cbi-button-positive important',
540 click: ui.createHandlerFn(this, function () {
541 map.save().then(() => {
544 packages: mapdata.request.packages,
545 version: mapdata.request.version,
546 profile: mapdata.request.profile
548 this.pollFn = L.bind(function () {
549 this.handleRequest(url, true, content, data, firmware);
551 poll.add(this.pollFn, 5);
556 _('Request firmware image')
562 ui.showModal(_('No upgrade available'), [
565 _('The device runs the latest firmware version %s - %s').format(
570 E('div', { class: 'right' }, [
571 E('div', { class: 'btn', click: ui.hideModal }, _('Close')),
578 load: async function () {
579 const promises = await Promise.all([
580 L.resolveDefault(callPackagelist(), {}),
581 L.resolveDefault(callSystemBoard(), {}),
582 L.resolveDefault(fs.stat('/sys/firmware/efi'), null),
583 uci.load('attendedsysupgrade'),
586 url: uci.get_first('attendedsysupgrade', 'server', 'url'),
587 branch: get_branch(promises[1].release.version),
588 revision: promises[1].release.revision,
590 advanced_mode: uci.get_first('attendedsysupgrade', 'client', 'advanced_mode') || 0,
591 rebuilder: uci.get_first('attendedsysupgrade', 'server', 'rebuilder')
594 client: 'luci/' + promises[0].packages['luci-app-attendedsysupgrade'],
595 packages: promises[0].packages,
596 profile: promises[1].board_name,
597 target: promises[1].release.target,
598 version: promises[1].release.version,
600 filesystem: promises[1].rootfs_type
602 return [data, firmware];
605 render: function (response) {
606 const data = response[0];
607 const firmware = response[1];
610 E('h2', _('Attended Sysupgrade')),
614 'The attended sysupgrade service allows to easily upgrade vanilla and custom firmware images.'
620 'This is done by building a new firmware on demand via an online service.'
625 _('Currently running: %s - %s').format(
633 class: 'btn cbi-button cbi-button-positive important',
634 click: ui.createHandlerFn(this, this.handleCheck, data, firmware),
636 _('Search for firmware upgrade')
640 handleSaveApply: null,