9054b78878d0f5d20428a9fc2dbdf39e607aadc7
[project/luci.git] /
1 'use strict';
2 'require view';
3 'require form';
4 'require uci';
5 'require rpc';
6 'require ui';
7 'require poll';
8 'require request';
9 'require dom';
10 'require fs';
11
12 var callPackagelist = rpc.declare({
13 object: 'rpc-sys',
14 method: 'packagelist',
15 });
16
17 var callSystemBoard = rpc.declare({
18 object: 'system',
19 method: 'board',
20 });
21
22 var callUpgradeStart = rpc.declare({
23 object: 'rpc-sys',
24 method: 'upgrade_start',
25 params: ['keep'],
26 });
27
28 /**
29 * Returns the branch of a given version. This helps to offer upgrades
30 * for point releases (aka within the branch).
31 *
32 * Logic:
33 * SNAPSHOT -> SNAPSHOT
34 * 21.02-SNAPSHOT -> 21.02
35 * 21.02.0-rc1 -> 21.02
36 * 19.07.8 -> 19.07
37 *
38 * @param {string} version
39 * Input version from which to determine the branch
40 * @returns {string}
41 * The determined branch
42 */
43 function get_branch(version) {
44 return version.replace('-SNAPSHOT', '').split('.').slice(0, 2).join('.');
45 }
46
47 /**
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.
51 *
52 * @param {string} revision
53 * Revision string of a OpenWrt device
54 * @returns {integer}
55 * The number of commits since OpenWrt/LEDE reboot
56 */
57 function get_revision_count(revision) {
58 return parseInt(revision.substring(1).split('-')[0]);
59 }
60
61 return view.extend({
62 steps: {
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')
68 },
69
70 data: {
71 url: '',
72 revision: '',
73 advanced_mode: 0,
74 },
75
76 firmware: {
77 profile: '',
78 target: '',
79 version: '',
80 packages: [],
81 diff_packages: true,
82 filesystem: '',
83 },
84
85 handle200: function (response) {
86 res = response.json();
87 var image;
88 for (image of res.images) {
89 if (this.firmware.filesystem == image.filesystem) {
90 if (this.data.efi) {
91 if (image.type == 'combined-efi') {
92 break;
93 }
94 } else {
95 if (image.type == 'sysupgrade' || image.type == 'combined') {
96 break;
97 }
98 }
99 }
100 }
101
102 if (image.name != undefined) {
103 var sysupgrade_url = `${this.data.url}/store/${res.bin_dir}/${image.name}`;
104
105 var keep = E('input', { type: 'checkbox' });
106 keep.checked = true;
107
108 var fields = [
109 _('Version'), `${res.version_number} ${res.version_code}`,
110 _('SHA256'), image.sha256,
111 ];
112
113 if (this.data.advanced_mode == 1) {
114 fields.push(
115 _('Profile'), res.id,
116 _('Target'), res.target,
117 _('Build Date'), res.build_at,
118 _('Filename'), image.name,
119 _('Filesystem'), image.filesystem,
120 )
121 }
122
123 fields.push('', E('a', { href: sysupgrade_url }, _('Download firmware image')))
124
125 var table = E('div', { class: 'table' });
126
127 for (var i = 0; i < fields.length; i += 2) {
128 table.appendChild(E('tr', { class: 'tr' }, [
129 E('td', { class: 'td left', width: '33%' }, [fields[i]]),
130 E('td', { class: 'td left' }, [fields[i + 1]]),
131 ]));
132 }
133
134 var modal_body = [
135 table,
136 E('p', { class: 'mt-2' },
137 E('label', { class: 'btn' }, [
138 keep, ' ',
139 _('Keep settings and retain the current configuration')
140 ])),
141 E('div', { class: 'right' }, [
142 E('div', { class: 'btn', click: ui.hideModal }, _('Cancel')), ' ',
143 E('button', {
144 'class': 'btn cbi-button cbi-button-positive important',
145 'click': ui.createHandlerFn(this, function () {
146 this.handleInstall(sysupgrade_url, keep.checked, image.sha256)
147 })
148 }, _('Install firmware image')),
149 ]),
150 ];
151
152 ui.showModal(_('Successfully created firmware image'), modal_body);
153 }
154 },
155
156 handle202: function (response) {
157 response = response.json();
158 this.data.request_hash = res.request_hash;
159
160 if ('queue_position' in response) {
161 ui.showModal(_('Queued...'), [
162 E('p', { 'class': 'spinning' }, _('Request in build queue position %s').format(response.queue_position))
163 ]);
164 } else {
165 ui.showModal(_('Building Firmware...'), [
166 E('p', { 'class': 'spinning' }, _('Progress: %s').format(this.steps[response.imagebuilder_status]))
167 ]);
168 }
169 },
170
171 handleError: function (response) {
172 response = response.json();
173 var body = [
174 E('p', {}, _('Server response: %s').format(response.detail)),
175 E('a', { href: 'https://github.com/openwrt/asu/issues' }, _('Please report the error message and request')),
176 E('p', {}, _('Request Data:')),
177 E('pre', {}, JSON.stringify({ ...this.data, ...this.firmware }, null, 4)),
178 ];
179
180 if (response.stdout) {
181 body.push(E('b', {}, 'STDOUT:'));
182 body.push(E('pre', {}, response.stdout));
183 }
184
185 if (response.stderr) {
186 body.push(E('b', {}, 'STDERR:'));
187 body.push(E('pre', {}, response.stderr));
188 }
189
190 body = body.concat([
191 E('div', { class: 'right' }, [
192 E('div', { class: 'btn', click: ui.hideModal }, _('Close')),
193 ]),
194 ]);
195
196 ui.showModal(_('Error building the firmware image'), body);
197 },
198
199 handleRequest: function () {
200 var request_url = `${this.data.url}/api/v1/build`;
201 var method = "POST"
202 var content = this.firmware;
203
204 /**
205 * If `request_hash` is available use a GET request instead of
206 * sending the entire object.
207 */
208 if (this.data.request_hash) {
209 request_url += `/${this.data.request_hash}`;
210 content = {};
211 method = "GET"
212 }
213
214 request.request(request_url, { method: method, content: content })
215 .then((response) => {
216 switch (response.status) {
217 case 202:
218 this.handle202(response);
219 break;
220 case 200:
221 poll.stop();
222 this.handle200(response);
223 break;
224 case 400: // bad request
225 case 422: // bad package
226 case 500: // build failed
227 poll.stop();
228 this.handleError(response);
229 break;
230 }
231 });
232 },
233
234 handleInstall: function (url, keep, sha256) {
235 ui.showModal(_('Downloading...'), [
236 E('p', { 'class': 'spinning' }, _('Downloading firmware from server to browser'))
237 ]);
238
239 request.get(url, {
240 headers: {
241 'Content-Type': 'application/x-www-form-urlencoded',
242 },
243 responseType: 'blob',
244 })
245 .then((response) => {
246 var form_data = new FormData();
247 form_data.append('sessionid', rpc.getSessionID());
248 form_data.append('filename', '/tmp/firmware.bin');
249 form_data.append('filemode', 600);
250 form_data.append('filedata', response.blob());
251
252 ui.showModal(_('Uploading...'), [
253 E('p', { 'class': 'spinning' }, _('Uploading firmware from browser to device'))
254 ]);
255
256 request
257 .get(`${L.env.cgi_base}/cgi-upload`, {
258 method: 'PUT',
259 content: form_data,
260 })
261 .then((response) => response.json())
262 .then((response) => {
263 if (response.sha256sum != sha256) {
264
265 ui.showModal(_('Wrong checksum'), [
266 E('p', _('Error during download of firmware. Please try again')),
267 E('div', { class: 'btn', click: ui.hideModal }, _('Close'))
268 ]);
269 } else {
270 ui.showModal(_('Installing...'), [
271 E('p', { class: 'spinning' }, _('Installing the sysupgrade. Do not unpower device!'))
272 ]);
273
274 L.resolveDefault(callUpgradeStart(keep), {})
275 .then((response) => {
276 if (keep) {
277 ui.awaitReconnect(window.location.host);
278 } else {
279 ui.awaitReconnect('192.168.1.1', 'openwrt.lan');
280 }
281 });
282 }
283 });
284 });
285 },
286
287 handleCheck: function () {
288 var { url, revision } = this.data
289 var { version, target } = this.firmware
290 var candidates = [];
291 var response;
292 var request_url = `${url}/api/overview`;
293 if (version.endsWith('SNAPSHOT')) {
294 request_url = `${url}/api/v1/revision/${version}/${target}`;
295 }
296
297 ui.showModal(_('Searching...'), [
298 E('p', { 'class': 'spinning' },
299 _('Searching for an available sysupgrade of %s - %s').format(version, revision))
300 ]);
301
302 L.resolveDefault(request.get(request_url))
303 .then(response => {
304 if (!response.ok) {
305 ui.showModal(_('Error connecting to upgrade server'), [
306 E('p', {}, _('Could not reach API at "%s". Please try again later.').format(response.url)),
307 E('pre', {}, response.responseText),
308 E('div', { class: 'right' }, [
309 E('div', { class: 'btn', click: ui.hideModal }, _('Close'))
310 ]),
311 ]);
312 return;
313 }
314 if (version.endsWith('SNAPSHOT')) {
315 const remote_revision = response.json().revision;
316 if (get_revision_count(revision) < get_revision_count(remote_revision)) {
317 candidates.push([version, remote_revision]);
318 }
319 } else {
320 const latest = response.json().latest;
321
322 for (let remote_version of latest) {
323 var remote_branch = get_branch(remote_version);
324
325 // already latest version installed
326 if (version == remote_version) {
327 break;
328 }
329
330 // skip branch upgrades outside the advanced mode
331 if (this.data.branch != remote_branch && this.data.advanced_mode == 0) {
332 continue;
333 }
334
335 candidates.unshift([remote_version, null]);
336
337 // don't offer branches older than the current
338 if (this.data.branch == remote_branch) {
339 break;
340 }
341 }
342 }
343
344 // allow to re-install running firmware in advanced mode
345 if (this.data.advanced_mode == 1) {
346 candidates.unshift([version, revision])
347 }
348
349 if (candidates.length) {
350 var m, s, o;
351
352 var mapdata = {
353 request: {
354 profile: this.firmware.profile,
355 version: candidates[0][0],
356 packages: Object.keys(this.firmware.packages).sort(),
357 },
358 };
359
360 var map = new form.JSONMap(mapdata, '');
361
362 s = map.section(form.NamedSection, 'request', '', '', 'Use defaults for the safest update');
363 o = s.option(form.ListValue, 'version', 'Select firmware version');
364 for (let candidate of candidates) {
365 if (candidate[0] == version && candidate[1] == revision) {
366 o.value(candidate[0], _('[installed] %s')
367 .format(candidate[1] ? `${candidate[0]} - ${candidate[1]}` : candidate[0]));
368 } else {
369 o.value(candidate[0], candidate[1] ? `${candidate[0]} - ${candidate[1]}` : candidate[0]);
370 }
371 }
372
373 if (this.data.advanced_mode == 1) {
374 o = s.option(form.Value, 'profile', _('Board Name / Profile'));
375 o = s.option(form.DynamicList, 'packages', _('Packages'));
376 }
377
378 L.resolveDefault(map.render()).
379 then(form_rendered => {
380 ui.showModal(_('New firmware upgrade available'), [
381 E('p', _('Currently running: %s - %s').format(this.firmware.version, this.data.revision)),
382 form_rendered,
383 E('div', { class: 'right' }, [
384 E('div', { class: 'btn', click: ui.hideModal }, _('Cancel')), ' ',
385 E('button', {
386 'class': 'btn cbi-button cbi-button-positive important',
387 'click': ui.createHandlerFn(this, function () {
388 map.save().then(() => {
389 this.firmware.packages = mapdata.request.packages;
390 this.firmware.version = mapdata.request.version;
391 this.firmware.profile = mapdata.request.profile;
392 poll.add(L.bind(this.handleRequest, this), 5);
393 });
394 })
395 }, _('Request firmware image')),
396 ]),
397 ]);
398 });
399 } else {
400 ui.showModal(_('No upgrade available'), [
401 E('p', _('The device runs the latest firmware version %s - %s').format(version, revision)),
402 E('div', { class: 'right' }, [
403 E('div', { class: 'btn', click: ui.hideModal }, _('Close')),
404 ]),
405 ]);
406 }
407
408 });
409 },
410
411 load: function () {
412 return Promise.all([
413 L.resolveDefault(callPackagelist(), {}),
414 L.resolveDefault(callSystemBoard(), {}),
415 L.resolveDefault(fs.stat("/sys/firmware/efi"), null),
416 uci.load('attendedsysupgrade'),
417 ]);
418 },
419
420 render: function (res) {
421 this.firmware.client = 'luci/' + res[0].packages['luci-app-attendedsysupgrade'];
422 this.firmware.packages = res[0].packages;
423
424 this.firmware.profile = res[1].board_name;
425 this.firmware.target = res[1].release.target;
426 this.firmware.version = res[1].release.version;
427 this.data.branch = get_branch(res[1].release.version);
428 this.firmware.filesystem = res[1].rootfs_type;
429 this.data.revision = res[1].release.revision;
430
431 this.data.efi = res[2];
432
433 this.data.url = uci.get_first('attendedsysupgrade', 'server', 'url');
434 this.data.advanced_mode = uci.get_first('attendedsysupgrade', 'client', 'advanced_mode') || 0
435
436 return E('p', [
437 E('h2', _('Attended Sysupgrade')),
438 E('p', _('The attended sysupgrade service allows to easily upgrade vanilla and custom firmware images.')),
439 E('p', _('This is done by building a new firmware on demand via an online service.')),
440 E('p', _('Currently running: %s - %s').format(this.firmware.version, this.data.revision)),
441 E('button', {
442 'class': 'btn cbi-button cbi-button-positive important',
443 'click': ui.createHandlerFn(this, this.handleCheck)
444 }, _('Search for firmware upgrade'))
445 ]);
446 },
447 handleSaveApply: null,
448 handleSave: null,
449 handleReset: null
450 });