12 var callPackagelist = rpc.declare({
14 method: 'packagelist',
17 var callSystemBoard = rpc.declare({
22 var 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')
84 handle200: function (response) {
85 res = response.json();
87 for (image of res.images) {
88 if (this.data.rootfs_type == image.filesystem) {
90 if (image.type == 'combined-efi') {
94 if (image.type == 'sysupgrade' || image.type == 'combined') {
101 if (image.name != undefined) {
102 var sysupgrade_url = `${this.data.url}/store/${res.bin_dir}/${image.name}`;
104 var keep = E('input', { type: 'checkbox' });
108 _('Version'), `${res.version_number} ${res.version_code}`,
109 _('SHA256'), image.sha256,
112 if (this.data.advanced_mode == 1) {
114 _('Profile'), res.id,
115 _('Target'), res.target,
116 _('Build Date'), res.build_at,
117 _('Filename'), image.name,
118 _('Filesystem'), image.filesystem,
122 fields.push('', E('a', { href: sysupgrade_url }, _('Download firmware image')))
124 var table = E('div', { class: 'table' });
126 for (var i = 0; i < fields.length; i += 2) {
127 table.appendChild(E('tr', { class: 'tr' }, [
128 E('td', { class: 'td left', width: '33%' }, [fields[i]]),
129 E('td', { class: 'td left' }, [fields[i + 1]]),
135 E('p', { class: 'mt-2' },
136 E('label', { class: 'btn' }, [
138 _('Keep settings and retain the current configuration')
140 E('div', { class: 'right' }, [
141 E('div', { class: 'btn', click: ui.hideModal }, _('Cancel')), ' ',
143 'class': 'btn cbi-button cbi-button-positive important',
144 'click': ui.createHandlerFn(this, function () {
145 this.handleInstall(sysupgrade_url, keep.checked, image.sha256)
147 }, _('Install firmware image')),
151 ui.showModal(_('Successfully created firmware image'), modal_body);
155 handle202: function (response) {
156 response = response.json();
157 this.data.request_hash = res.request_hash;
159 if ('queue_position' in response) {
160 ui.showModal(_('Queued...'), [
161 E('p', { 'class': 'spinning' }, _('Request in build queue position %s').format(response.queue_position))
164 ui.showModal(_('Building Firmware...'), [
165 E('p', { 'class': 'spinning' }, _('Progress: %s').format(this.steps[response.imagebuilder_status]))
170 handleError: function (response) {
171 response = response.json();
173 E('p', {}, _('Server response: %s').format(response.detail)),
174 E('a', { href: 'https://github.com/openwrt/asu/issues' }, _('Please report the error message and request')),
175 E('p', {}, _('Request Data:')),
176 E('pre', {}, JSON.stringify({ ...this.data, ...this.firmware }, null, 4)),
179 if (response.stdout) {
180 body.push(E('b', {}, 'STDOUT:'));
181 body.push(E('pre', {}, response.stdout));
184 if (response.stderr) {
185 body.push(E('b', {}, 'STDERR:'));
186 body.push(E('pre', {}, response.stderr));
190 E('div', { class: 'right' }, [
191 E('div', { class: 'btn', click: ui.hideModal }, _('Close')),
195 ui.showModal(_('Error building the firmware image'), body);
198 handleRequest: function () {
199 var request_url = `${this.data.url}/api/v1/build`;
201 var content = this.firmware;
204 * If `request_hash` is available use a GET request instead of
205 * sending the entire object.
207 if (this.data.request_hash) {
208 request_url += `/${this.data.request_hash}`;
213 request.request(request_url, { method: method, content: content })
214 .then((response) => {
215 switch (response.status) {
217 this.handle202(response);
221 this.handle200(response);
223 case 400: // bad request
224 case 422: // bad package
225 case 500: // build failed
227 this.handleError(response);
233 handleInstall: function (url, keep, sha256) {
234 ui.showModal(_('Downloading...'), [
235 E('p', { 'class': 'spinning' }, _('Downloading firmware from server to browser'))
240 'Content-Type': 'application/x-www-form-urlencoded',
242 responseType: 'blob',
244 .then((response) => {
245 var form_data = new FormData();
246 form_data.append('sessionid', rpc.getSessionID());
247 form_data.append('filename', '/tmp/firmware.bin');
248 form_data.append('filemode', 600);
249 form_data.append('filedata', response.blob());
251 ui.showModal(_('Uploading...'), [
252 E('p', { 'class': 'spinning' }, _('Uploading firmware from browser to device'))
256 .get(`${L.env.cgi_base}/cgi-upload`, {
260 .then((response) => response.json())
261 .then((response) => {
262 if (response.sha256sum != sha256) {
264 ui.showModal(_('Wrong checksum'), [
265 E('p', _('Error during download of firmware. Please try again')),
266 E('div', { class: 'btn', click: ui.hideModal }, _('Close'))
269 ui.showModal(_('Installing...'), [
270 E('p', { class: 'spinning' }, _('Installing the sysupgrade. Do not unpower device!'))
273 L.resolveDefault(callUpgradeStart(keep), {})
274 .then((response) => {
276 ui.awaitReconnect(window.location.host);
278 ui.awaitReconnect('192.168.1.1', 'openwrt.lan');
286 handleCheck: function () {
287 var { url, revision } = this.data
288 var { version, target } = this.firmware
291 var request_url = `${url}/api/overview`;
292 if (version.endsWith('SNAPSHOT')) {
293 request_url = `${url}/api/v1/revision/${version}/${target}`;
296 ui.showModal(_('Searching...'), [
297 E('p', { 'class': 'spinning' },
298 _('Searching for an available sysupgrade of %s - %s').format(version, revision))
301 L.resolveDefault(request.get(request_url))
304 ui.showModal(_('Error connecting to upgrade server'), [
305 E('p', {}, _('Could not reach API at "%s". Please try again later.').format(response.url)),
306 E('pre', {}, response.responseText),
307 E('div', { class: 'right' }, [
308 E('div', { class: 'btn', click: ui.hideModal }, _('Close'))
313 if (version.endsWith('SNAPSHOT')) {
314 const remote_revision = response.json().revision;
315 if (get_revision_count(revision) < get_revision_count(remote_revision)) {
316 candidates.push([version, remote_revision]);
319 const latest = response.json().latest;
321 for (let remote_version of latest) {
322 var remote_branch = get_branch(remote_version);
324 // already latest version installed
325 if (version == remote_version) {
329 // skip branch upgrades outside the advanced mode
330 if (this.data.branch != remote_branch && this.data.advanced_mode == 0) {
334 candidates.unshift([remote_version, null]);
336 // don't offer branches older than the current
337 if (this.data.branch == remote_branch) {
343 if (candidates.length) {
348 profile: this.firmware.profile,
349 version: candidates[0][0],
350 packages: Object.keys(this.firmware.packages).sort(),
354 var map = new form.JSONMap(mapdata, '');
356 s = map.section(form.NamedSection, 'request', '', '', 'Use defaults for the safest update');
357 o = s.option(form.ListValue, 'version', 'Select firmware version');
358 for (let candidate of candidates) {
359 o.value(candidate[0], candidate[1] ? `${candidate[0]} - ${candidate[1]}` : candidate[0]);
362 if (this.data.advanced_mode == 1) {
363 o = s.option(form.Value, 'profile', _('Board Name / Profile'));
364 o = s.option(form.DynamicList, 'packages', _('Packages'));
367 L.resolveDefault(map.render()).
368 then(form_rendered => {
369 ui.showModal(_('New firmware upgrade available'), [
370 E('p', _('Currently running: %s - %s').format(this.firmware.version, this.data.revision)),
372 E('div', { class: 'right' }, [
373 E('div', { class: 'btn', click: ui.hideModal }, _('Cancel')), ' ',
375 'class': 'btn cbi-button cbi-button-positive important',
376 'click': ui.createHandlerFn(this, function () {
377 map.save().then(() => {
378 this.firmware.packages = mapdata.request.packages;
379 this.firmware.version = mapdata.request.version;
380 this.firmware.profile = mapdata.request.profile;
381 poll.add(L.bind(this.handleRequest, this), 5);
384 }, _('Request firmware image')),
389 ui.showModal(_('No upgrade available'), [
390 E('p', _('The device runs the latest firmware version %s - %s').format(version, revision)),
391 E('div', { class: 'right' }, [
392 E('div', { class: 'btn', click: ui.hideModal }, _('Close')),
402 L.resolveDefault(callPackagelist(), {}),
403 L.resolveDefault(callSystemBoard(), {}),
404 fs.stat("/sys/firmware/efi"),
405 fs.read("/proc/mounts"),
406 uci.load('attendedsysupgrade'),
410 render: function (res) {
411 this.data.app_version = res[0].packages['luci-app-attendedsysupgrade'];
412 this.firmware.packages = res[0].packages;
414 this.firmware.profile = res[1].board_name;
415 this.firmware.target = res[1].release.target;
416 this.firmware.version = res[1].release.version;
417 this.data.branch = get_branch(res[1].release.version);
418 this.data.revision = res[1].release.revision;
419 this.data.efi = res[2];
420 if (res[1].rootfs_type) {
421 this.data.rootfs_type = res[1].rootfs_type;
423 this.data.rootfs_type = res[3].split(/\r?\n/)[0].split(' ')[2]
426 this.data.url = uci.get_first('attendedsysupgrade', 'server', 'url');
427 this.data.advanced_mode = uci.get_first('attendedsysupgrade', 'client', 'advanced_mode') || 0
430 E('h2', _('Attended Sysupgrade')),
431 E('p', _('The attended sysupgrade service allows to easily upgrade vanilla and custom firmware images.')),
432 E('p', _('This is done by building a new firmware on demand via an online service.')),
433 E('p', _('Currently running: %s - %s').format(this.firmware.version, this.data.revision)),
435 'class': 'btn cbi-button cbi-button-positive important',
436 'click': ui.createHandlerFn(this, this.handleCheck)
437 }, _('Search for firmware upgrade'))
440 handleSaveApply: null,