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,