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
) {
89 for (image
of images
) {
90 if (this.firmware
.filesystem
== image
.filesystem
) {
92 if (image
.type
== 'combined-efi') {
96 if (image
.type
== 'sysupgrade' || image
.type
== 'combined') {
105 handle200: function (response
) {
106 response
= response
.json();
107 let image
= this.selectImage(response
.images
);
109 if (image
.name
!= undefined) {
110 this.data
.sha256_unsigned
= image
.sha256_unsigned
;
111 let sysupgrade_url
= `${this.data.url}/store/${response.bin_dir}/${image.name}`;
113 let keep
= E('input', { type
: 'checkbox' });
118 `${response.version_number} ${response.version_code}`,
123 if (this.data
.advanced_mode
== 1) {
140 E('a', { href
: sysupgrade_url
}, _('Download firmware image'))
142 if (this.data
.rebuilder
) {
143 fields
.push(_('Rebuilds'), E('div', { id
: 'rebuilder_status' }));
146 let table
= E('div', { class: 'table' });
148 for (let i
= 0; i
< fields
.length
; i
+= 2) {
150 E('tr', { class: 'tr' }, [
151 E('td', { class: 'td left', width
: '33%' }, [fields
[i
]]),
152 E('td', { class: 'td left' }, [fields
[i
+ 1]]),
162 E('label', { class: 'btn' }, [
165 _('Keep settings and retain the current configuration'),
168 E('div', { class: 'right' }, [
169 E('div', { class: 'btn', click
: ui
.hideModal
}, _('Cancel')),
174 class: 'btn cbi-button cbi-button-positive important',
175 click
: ui
.createHandlerFn(this, function () {
176 this.handleInstall(sysupgrade_url
, keep
.checked
, image
.sha256
);
179 _('Install firmware image')
184 ui
.showModal(_('Successfully created firmware image'), modal_body
);
185 if (this.data
.rebuilder
) {
186 this.handleRebuilder();
191 handle202: function (response
) {
192 response
= response
.json();
193 this.data
.request_hash
= response
.request_hash
;
195 if ('queue_position' in response
) {
196 ui
.showModal(_('Queued...'), [
199 { class: 'spinning' },
200 _('Request in build queue position %s').format(
201 response
.queue_position
206 ui
.showModal(_('Building Firmware...'), [
209 { class: 'spinning' },
210 _('Progress: %s%% %s').format(
211 this.steps
[response
.imagebuilder_status
][0],
212 this.steps
[response
.imagebuilder_status
][1]
219 handleError: function (response
) {
220 response
= response
.json();
222 E('p', {}, _('Server response: %s').format(response
.detail
)),
225 { href
: 'https://github.com/openwrt/asu/issues' },
226 _('Please report the error message and request')
228 E('p', {}, _('Request Data:')),
229 E('pre', {}, JSON
.stringify({ ...this.data
, ...this.firmware
}, null, 4)),
232 if (response
.stdout
) {
233 body
.push(E('b', {}, 'STDOUT:'));
234 body
.push(E('pre', {}, response
.stdout
));
237 if (response
.stderr
) {
238 body
.push(E('b', {}, 'STDERR:'));
239 body
.push(E('pre', {}, response
.stderr
));
243 E('div', { class: 'right' }, [
244 E('div', { class: 'btn', click
: ui
.hideModal
}, _('Close')),
248 ui
.showModal(_('Error building the firmware image'), body
);
251 handleRequest: function (server
, main
) {
252 let request_url
= `${server}/api/v1/build`;
254 let content
= this.firmware
;
257 * If `request_hash` is available use a GET request instead of
258 * sending the entire object.
260 if (this.data
.request_hash
&& main
== true) {
261 request_url
+= `/${this.data.request_hash}`;
267 .request(request_url
, { method
: method
, content
: content
})
268 .then((response
) => {
269 switch (response
.status
) {
272 this.handle202(response
);
274 response
= response
.json();
276 let view
= document
.getElementById(server
);
277 view
.innerText
= `⏳ (${
278 this.steps[response.imagebuilder_status][0]
284 poll
.remove(this.pollFn
);
285 this.handle200(response
);
287 poll
.remove(this.rebuilder_polls
[server
]);
288 response
= response
.json();
289 let view
= document
.getElementById(server
);
290 let image
= this.selectImage(response
.images
);
291 if (image
.sha256_unsigned
== this.data
.sha256_unsigned
) {
292 view
.innerText
= '✅ %s'.format(server
);
294 view
.innerHTML
= `⚠️ ${server} (<a href="${server}/store/${
296 }/${image.name}">${_('Download')}</a>)`;
300 case 400: // bad request
301 case 422: // bad package
302 case 500: // build failed
304 poll
.remove(this.pollFn
);
305 this.handleError(response
);
308 poll
.remove(this.rebuilder_polls
[server
]);
309 document
.getElementById(server
).innerText
= '🚫 %s'.format(
317 handleRebuilder: function () {
318 this.rebuilder_polls
= {};
319 for (let rebuilder
of this.data
.rebuilder
) {
320 this.rebuilder_polls
[rebuilder
] = L
.bind(
326 poll
.add(this.rebuilder_polls
[rebuilder
], 5);
327 document
.getElementById(
329 ).innerHTML
+= `<p id="${rebuilder}">⏳ ${rebuilder}</p>`;
334 handleInstall: function (url
, keep
, sha256
) {
335 ui
.showModal(_('Downloading...'), [
338 { class: 'spinning' },
339 _('Downloading firmware from server to browser')
346 'Content-Type': 'application/x-www-form-urlencoded',
348 responseType
: 'blob',
350 .then((response
) => {
351 let form_data
= new FormData();
352 form_data
.append('sessionid', rpc
.getSessionID());
353 form_data
.append('filename', '/tmp/firmware.bin');
354 form_data
.append('filemode', 600);
355 form_data
.append('filedata', response
.blob());
357 ui
.showModal(_('Uploading...'), [
360 { class: 'spinning' },
361 _('Uploading firmware from browser to device')
366 .get(`${L.env.cgi_base}/cgi-upload`, {
370 .then((response
) => response
.json())
371 .then((response
) => {
372 if (response
.sha256sum
!= sha256
) {
373 ui
.showModal(_('Wrong checksum'), [
376 _('Error during download of firmware. Please try again')
378 E('div', { class: 'btn', click
: ui
.hideModal
}, _('Close')),
381 ui
.showModal(_('Installing...'), [
384 { class: 'spinning' },
385 _('Installing the sysupgrade. Do not unpower device!')
389 L
.resolveDefault(callUpgradeStart(keep
), {}).then((response
) => {
391 ui
.awaitReconnect(window
.location
.host
);
393 ui
.awaitReconnect('192.168.1.1', 'openwrt.lan');
401 handleCheck: function () {
402 let { url
, revision
} = this.data
;
403 let { version
, target
} = this.firmware
;
405 let request_url
= `${url}/api/overview`;
406 if (version
.endsWith('SNAPSHOT')) {
407 request_url
= `${url}/api/v1/revision/${version}/${target}`;
410 ui
.showModal(_('Searching...'), [
413 { class: 'spinning' },
414 _('Searching for an available sysupgrade of %s - %s').format(
421 L
.resolveDefault(request
.get(request_url
)).then((response
) => {
423 ui
.showModal(_('Error connecting to upgrade server'), [
427 _('Could not reach API at "%s". Please try again later.').format(
431 E('pre', {}, response
.responseText
),
432 E('div', { class: 'right' }, [
433 E('div', { class: 'btn', click
: ui
.hideModal
}, _('Close')),
438 if (version
.endsWith('SNAPSHOT')) {
439 const remote_revision
= response
.json().revision
;
441 get_revision_count(revision
) < get_revision_count(remote_revision
)
443 candidates
.push([version
, remote_revision
]);
446 const latest
= response
.json().latest
;
448 for (let remote_version
of latest
) {
449 let remote_branch
= get_branch(remote_version
);
451 // already latest version installed
452 if (version
== remote_version
) {
456 // skip branch upgrades outside the advanced mode
458 this.data
.branch
!= remote_branch
&&
459 this.data
.advanced_mode
== 0
464 candidates
.unshift([remote_version
, null]);
466 // don't offer branches older than the current
467 if (this.data
.branch
== remote_branch
) {
473 // allow to re-install running firmware in advanced mode
474 if (this.data
.advanced_mode
== 1) {
475 candidates
.unshift([version
, revision
]);
478 if (candidates
.length
) {
483 profile
: this.firmware
.profile
,
484 version
: candidates
[0][0],
485 packages
: Object
.keys(this.firmware
.packages
).sort(),
489 let map
= new form
.JSONMap(mapdata
, '');
496 'Use defaults for the safest update'
498 o
= s
.option(form
.ListValue
, 'version', 'Select firmware version');
499 for (let candidate
of candidates
) {
500 if (candidate
[0] == version
&& candidate
[1] == revision
) {
503 _('[installed] %s').format(
505 ? `${candidate[0]} - ${candidate[1]}`
512 candidate
[1] ? `${candidate[0]} - ${candidate[1]}` : candidate
[0]
517 if (this.data
.advanced_mode
== 1) {
518 o
= s
.option(form
.Value
, 'profile', _('Board Name / Profile'));
519 o
= s
.option(form
.DynamicList
, 'packages', _('Packages'));
522 L
.resolveDefault(map
.render()).then((form_rendered
) => {
523 ui
.showModal(_('New firmware upgrade available'), [
526 _('Currently running: %s - %s').format(
527 this.firmware
.version
,
532 E('div', { class: 'right' }, [
533 E('div', { class: 'btn', click
: ui
.hideModal
}, _('Cancel')),
538 class: 'btn cbi-button cbi-button-positive important',
539 click
: ui
.createHandlerFn(this, function () {
540 map
.save().then(() => {
541 this.firmware
.packages
= mapdata
.request
.packages
;
542 this.firmware
.version
= mapdata
.request
.version
;
543 this.firmware
.profile
= mapdata
.request
.profile
;
544 this.pollFn
= L
.bind(function () {
545 this.handleRequest(this.data
.url
, true);
547 poll
.add(this.pollFn
, 5);
552 _('Request firmware image')
558 ui
.showModal(_('No upgrade available'), [
561 _('The device runs the latest firmware version %s - %s').format(
566 E('div', { class: 'right' }, [
567 E('div', { class: 'btn', click
: ui
.hideModal
}, _('Close')),
576 L
.resolveDefault(callPackagelist(), {}),
577 L
.resolveDefault(callSystemBoard(), {}),
578 L
.resolveDefault(fs
.stat('/sys/firmware/efi'), null),
579 uci
.load('attendedsysupgrade'),
583 render: function (response
) {
584 this.firmware
.client
=
585 'luci/' + response
[0].packages
['luci-app-attendedsysupgrade'];
586 this.firmware
.packages
= response
[0].packages
;
588 this.firmware
.profile
= response
[1].board_name
;
589 this.firmware
.target
= response
[1].release
.target
;
590 this.firmware
.version
= response
[1].release
.version
;
591 this.data
.branch
= get_branch(response
[1].release
.version
);
592 this.firmware
.filesystem
= response
[1].rootfs_type
;
593 this.data
.revision
= response
[1].release
.revision
;
595 this.data
.efi
= response
[2];
597 this.data
.url
= uci
.get_first('attendedsysupgrade', 'server', 'url');
598 this.data
.advanced_mode
=
599 uci
.get_first('attendedsysupgrade', 'client', 'advanced_mode') || 0;
600 this.data
.rebuilder
= uci
.get_first(
601 'attendedsysupgrade',
607 E('h2', _('Attended Sysupgrade')),
611 'The attended sysupgrade service allows to easily upgrade vanilla and custom firmware images.'
617 'This is done by building a new firmware on demand via an online service.'
622 _('Currently running: %s - %s').format(
623 this.firmware
.version
,
630 class: 'btn cbi-button cbi-button-positive important',
631 click
: ui
.createHandlerFn(this, this.handleCheck
),
633 _('Search for firmware upgrade')
637 handleSaveApply
: null,