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
: [10, _('Received build request')],
64 download_imagebuilder
: [20, _('Downloading ImageBuilder archive')],
65 unpack_imagebuilder
: [40, _('Setting Up ImageBuilder')],
66 calculate_packages_hash
: [60, _('Validate package selection')],
67 building_image
: [80, _('Generating firmware image')],
87 selectImage: function (images
) {
88 let firmware
= this.firmware
;
90 var filesystemFilter = function(e
) {
91 return (e
.filesystem
== firmware
.filesystem
);
93 var typeFilter = function(e
) {
94 if (firmware
.target
.indexOf("x86") != -1) {
95 // x86 images can be combined-efi (EFI) or combined (BIOS)
97 return (e
.type
== 'combined-efi');
99 return (e
.type
== 'combined');
102 return (e
.type
== 'sysupgrade' || e
.type
== 'combined');
105 return images
.filter(filesystemFilter
).filter(typeFilter
)[0];
108 handle200: function (response
) {
109 response
= response
.json();
110 let image
= this.selectImage(response
.images
);
112 if (image
.name
!= undefined) {
113 this.data
.sha256_unsigned
= image
.sha256_unsigned
;
114 let sysupgrade_url
= `${this.data.url}/store/${response.bin_dir}/${image.name}`;
116 let keep
= E('input', { type
: 'checkbox' });
121 `${response.version_number} ${response.version_code}`,
126 if (this.data
.advanced_mode
== 1) {
143 E('a', { href
: sysupgrade_url
}, _('Download firmware image'))
145 if (this.data
.rebuilder
) {
146 fields
.push(_('Rebuilds'), E('div', { id
: 'rebuilder_status' }));
149 let table
= E('div', { class: 'table' });
151 for (let i
= 0; i
< fields
.length
; i
+= 2) {
153 E('tr', { class: 'tr' }, [
154 E('td', { class: 'td left', width
: '33%' }, [fields
[i
]]),
155 E('td', { class: 'td left' }, [fields
[i
+ 1]]),
165 E('label', { class: 'btn' }, [
168 _('Keep settings and retain the current configuration'),
171 E('div', { class: 'right' }, [
172 E('div', { class: 'btn', click
: ui
.hideModal
}, _('Cancel')),
177 class: 'btn cbi-button cbi-button-positive important',
178 click
: ui
.createHandlerFn(this, function () {
179 this.handleInstall(sysupgrade_url
, keep
.checked
, image
.sha256
);
182 _('Install firmware image')
187 ui
.showModal(_('Successfully created firmware image'), modal_body
);
188 if (this.data
.rebuilder
) {
189 this.handleRebuilder();
194 handle202: function (response
) {
195 response
= response
.json();
196 this.data
.request_hash
= response
.request_hash
;
198 if ('queue_position' in response
) {
199 ui
.showModal(_('Queued...'), [
202 { class: 'spinning' },
203 _('Request in build queue position %s').format(
204 response
.queue_position
209 ui
.showModal(_('Building Firmware...'), [
212 { class: 'spinning' },
213 _('Progress: %s%% %s').format(
214 this.steps
[response
.imagebuilder_status
][0],
215 this.steps
[response
.imagebuilder_status
][1]
222 handleError: function (response
) {
223 response
= response
.json();
225 E('p', {}, _('Server response: %s').format(response
.detail
)),
228 { href
: 'https://github.com/openwrt/asu/issues' },
229 _('Please report the error message and request')
231 E('p', {}, _('Request Data:')),
232 E('pre', {}, JSON
.stringify({ ...this.data
, ...this.firmware
}, null, 4)),
235 if (response
.stdout
) {
236 body
.push(E('b', {}, 'STDOUT:'));
237 body
.push(E('pre', {}, response
.stdout
));
240 if (response
.stderr
) {
241 body
.push(E('b', {}, 'STDERR:'));
242 body
.push(E('pre', {}, response
.stderr
));
246 E('div', { class: 'right' }, [
247 E('div', { class: 'btn', click
: ui
.hideModal
}, _('Close')),
251 ui
.showModal(_('Error building the firmware image'), body
);
254 handleRequest: function (server
, main
) {
255 let request_url
= `${server}/api/v1/build`;
257 let content
= this.firmware
;
260 * If `request_hash` is available use a GET request instead of
261 * sending the entire object.
263 if (this.data
.request_hash
&& main
== true) {
264 request_url
+= `/${this.data.request_hash}`;
270 .request(request_url
, { method
: method
, content
: content
})
271 .then((response
) => {
272 switch (response
.status
) {
275 this.handle202(response
);
277 response
= response
.json();
279 let view
= document
.getElementById(server
);
280 view
.innerText
= `⏳ (${
281 this.steps[response.imagebuilder_status][0]
287 poll
.remove(this.pollFn
);
288 this.handle200(response
);
290 poll
.remove(this.rebuilder_polls
[server
]);
291 response
= response
.json();
292 let view
= document
.getElementById(server
);
293 let image
= this.selectImage(response
.images
);
294 if (image
.sha256_unsigned
== this.data
.sha256_unsigned
) {
295 view
.innerText
= '✅ %s'.format(server
);
297 view
.innerHTML
= `⚠️ ${server} (<a href="${server}/store/${
299 }/${image.name}">${_('Download')}</a>)`;
303 case 400: // bad request
304 case 422: // bad package
305 case 500: // build failed
307 poll
.remove(this.pollFn
);
308 this.handleError(response
);
311 poll
.remove(this.rebuilder_polls
[server
]);
312 document
.getElementById(server
).innerText
= '🚫 %s'.format(
320 handleRebuilder: function () {
321 this.rebuilder_polls
= {};
322 for (let rebuilder
of this.data
.rebuilder
) {
323 this.rebuilder_polls
[rebuilder
] = L
.bind(
329 poll
.add(this.rebuilder_polls
[rebuilder
], 5);
330 document
.getElementById(
332 ).innerHTML
+= `<p id="${rebuilder}">⏳ ${rebuilder}</p>`;
337 handleInstall: function (url
, keep
, sha256
) {
338 ui
.showModal(_('Downloading...'), [
341 { class: 'spinning' },
342 _('Downloading firmware from server to browser')
349 'Content-Type': 'application/x-www-form-urlencoded',
351 responseType
: 'blob',
353 .then((response
) => {
354 let form_data
= new FormData();
355 form_data
.append('sessionid', rpc
.getSessionID());
356 form_data
.append('filename', '/tmp/firmware.bin');
357 form_data
.append('filemode', 600);
358 form_data
.append('filedata', response
.blob());
360 ui
.showModal(_('Uploading...'), [
363 { class: 'spinning' },
364 _('Uploading firmware from browser to device')
369 .get(`${L.env.cgi_base}/cgi-upload`, {
373 .then((response
) => response
.json())
374 .then((response
) => {
375 if (response
.sha256sum
!= sha256
) {
376 ui
.showModal(_('Wrong checksum'), [
379 _('Error during download of firmware. Please try again')
381 E('div', { class: 'btn', click
: ui
.hideModal
}, _('Close')),
384 ui
.showModal(_('Installing...'), [
387 { class: 'spinning' },
388 _('Installing the sysupgrade. Do not unpower device!')
392 L
.resolveDefault(callUpgradeStart(keep
), {}).then((response
) => {
394 ui
.awaitReconnect(window
.location
.host
);
396 ui
.awaitReconnect('192.168.1.1', 'openwrt.lan');
404 handleCheck: function () {
405 let { url
, revision
} = this.data
;
406 let { version
, target
} = this.firmware
;
408 let request_url
= `${url}/api/overview`;
409 if (version
.endsWith('SNAPSHOT')) {
410 request_url
= `${url}/api/v1/revision/${version}/${target}`;
413 ui
.showModal(_('Searching...'), [
416 { class: 'spinning' },
417 _('Searching for an available sysupgrade of %s - %s').format(
424 L
.resolveDefault(request
.get(request_url
)).then((response
) => {
426 ui
.showModal(_('Error connecting to upgrade server'), [
430 _('Could not reach API at "%s". Please try again later.').format(
434 E('pre', {}, response
.responseText
),
435 E('div', { class: 'right' }, [
436 E('div', { class: 'btn', click
: ui
.hideModal
}, _('Close')),
441 if (version
.endsWith('SNAPSHOT')) {
442 const remote_revision
= response
.json().revision
;
444 get_revision_count(revision
) < get_revision_count(remote_revision
)
446 candidates
.push([version
, remote_revision
]);
449 const latest
= response
.json().latest
;
451 for (let remote_version
of latest
) {
452 let remote_branch
= get_branch(remote_version
);
454 // already latest version installed
455 if (version
== remote_version
) {
459 // skip branch upgrades outside the advanced mode
461 this.data
.branch
!= remote_branch
&&
462 this.data
.advanced_mode
== 0
467 candidates
.unshift([remote_version
, null]);
469 // don't offer branches older than the current
470 if (this.data
.branch
== remote_branch
) {
476 // allow to re-install running firmware in advanced mode
477 if (this.data
.advanced_mode
== 1) {
478 candidates
.unshift([version
, revision
]);
481 if (candidates
.length
) {
486 profile
: this.firmware
.profile
,
487 version
: candidates
[0][0],
488 packages
: Object
.keys(this.firmware
.packages
).sort(),
492 let map
= new form
.JSONMap(mapdata
, '');
499 'Use defaults for the safest update'
501 o
= s
.option(form
.ListValue
, 'version', 'Select firmware version');
502 for (let candidate
of candidates
) {
503 if (candidate
[0] == version
&& candidate
[1] == revision
) {
506 _('[installed] %s').format(
508 ? `${candidate[0]} - ${candidate[1]}`
515 candidate
[1] ? `${candidate[0]} - ${candidate[1]}` : candidate
[0]
520 if (this.data
.advanced_mode
== 1) {
521 o
= s
.option(form
.Value
, 'profile', _('Board Name / Profile'));
522 o
= s
.option(form
.DynamicList
, 'packages', _('Packages'));
525 L
.resolveDefault(map
.render()).then((form_rendered
) => {
526 ui
.showModal(_('New firmware upgrade available'), [
529 _('Currently running: %s - %s').format(
530 this.firmware
.version
,
535 E('div', { class: 'right' }, [
536 E('div', { class: 'btn', click
: ui
.hideModal
}, _('Cancel')),
541 class: 'btn cbi-button cbi-button-positive important',
542 click
: ui
.createHandlerFn(this, function () {
543 map
.save().then(() => {
544 this.firmware
.packages
= mapdata
.request
.packages
;
545 this.firmware
.version
= mapdata
.request
.version
;
546 this.firmware
.profile
= mapdata
.request
.profile
;
547 this.pollFn
= L
.bind(function () {
548 this.handleRequest(this.data
.url
, true);
550 poll
.add(this.pollFn
, 5);
555 _('Request firmware image')
561 ui
.showModal(_('No upgrade available'), [
564 _('The device runs the latest firmware version %s - %s').format(
569 E('div', { class: 'right' }, [
570 E('div', { class: 'btn', click
: ui
.hideModal
}, _('Close')),
579 L
.resolveDefault(callPackagelist(), {}),
580 L
.resolveDefault(callSystemBoard(), {}),
581 L
.resolveDefault(fs
.stat('/sys/firmware/efi'), null),
582 uci
.load('attendedsysupgrade'),
586 render: function (response
) {
587 this.firmware
.client
=
588 'luci/' + response
[0].packages
['luci-app-attendedsysupgrade'];
589 this.firmware
.packages
= response
[0].packages
;
591 this.firmware
.profile
= response
[1].board_name
;
592 this.firmware
.target
= response
[1].release
.target
;
593 this.firmware
.version
= response
[1].release
.version
;
594 this.data
.branch
= get_branch(response
[1].release
.version
);
595 this.firmware
.filesystem
= response
[1].rootfs_type
;
596 this.data
.revision
= response
[1].release
.revision
;
598 this.data
.efi
= response
[2];
600 this.data
.url
= uci
.get_first('attendedsysupgrade', 'server', 'url');
601 this.data
.advanced_mode
=
602 uci
.get_first('attendedsysupgrade', 'client', 'advanced_mode') || 0;
603 this.data
.rebuilder
= uci
.get_first(
604 'attendedsysupgrade',
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(
626 this.firmware
.version
,
633 class: 'btn cbi-button cbi-button-positive important',
634 click
: ui
.createHandlerFn(this, this.handleCheck
),
636 _('Search for firmware upgrade')
640 handleSaveApply
: null,