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, _('Setup 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,