0e6ae9d20cca100e8f9e8a6b7c2bfabb893d82cd
[openwrt-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 let callPackagelist = rpc.declare({
13         object: 'rpc-sys',
14         method: 'packagelist',
15 });
16
17 let callSystemBoard = rpc.declare({
18         object: 'system',
19         method: 'board',
20 });
21
22 let 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, _('Setting Up ImageBuilder')],
66                 calculate_packages_hash: [60, _('Validate package selection')],
67                 building_image: [80, _('Generating firmware image')],
68         },
69
70         request_hash: '',
71         sha256_unsigned: '',
72
73         selectImage: function (images, data, firmware) {
74                 var filesystemFilter = function(e) {
75                         return (e.filesystem == firmware.filesystem);
76                 }
77                 var typeFilter = function(e) {
78                         if (firmware.target.indexOf("x86") != -1) {
79                                 // x86 images can be combined-efi (EFI) or combined (BIOS)
80                                 if (data.efi) {
81                                         return (e.type == 'combined-efi');
82                                 } else {
83                                         return (e.type == 'combined');
84                                 }
85                         } else {
86                                 return (e.type == 'sysupgrade' || e.type == 'combined');
87                         }
88                 }
89                 return images.filter(filesystemFilter).filter(typeFilter)[0];
90         },
91
92         handle200: function (response, content, data, firmware) {
93                 response = response.json();
94                 let image = this.selectImage(response.images, data, firmware);
95
96                 if (image.name != undefined) {
97                         this.sha256_unsigned = image.sha256_unsigned;
98                         let sysupgrade_url = `${data.url}/store/${response.bin_dir}/${image.name}`;
99
100                         let keep = E('input', { type: 'checkbox' });
101                         keep.checked = true;
102
103                         let fields = [
104                                 _('Version'),
105                                 `${response.version_number} ${response.version_code}`,
106                                 _('SHA256'),
107                                 image.sha256,
108                         ];
109
110                         if (data.advanced_mode == 1) {
111                                 fields.push(
112                                         _('Profile'),
113                                         response.id,
114                                         _('Target'),
115                                         response.target,
116                                         _('Build Date'),
117                                         response.build_at,
118                                         _('Filename'),
119                                         image.name,
120                                         _('Filesystem'),
121                                         image.filesystem
122                                 );
123                         }
124
125                         fields.push(
126                                 '',
127                                 E('a', { href: sysupgrade_url }, _('Download firmware image'))
128                         );
129                         if (data.rebuilder) {
130                                 fields.push(_('Rebuilds'), E('div', { id: 'rebuilder_status' }));
131                         }
132
133                         let table = E('div', { class: 'table' });
134
135                         for (let i = 0; i < fields.length; i += 2) {
136                                 table.appendChild(
137                                         E('tr', { class: 'tr' }, [
138                                                 E('td', { class: 'td left', width: '33%' }, [fields[i]]),
139                                                 E('td', { class: 'td left' }, [fields[i + 1]]),
140                                         ])
141                                 );
142                         }
143
144                         let modal_body = [
145                                 table,
146                                 E(
147                                         'p',
148                                         { class: 'mt-2' },
149                                         E('label', { class: 'btn' }, [
150                                                 keep,
151                                                 ' ',
152                                                 _('Keep settings and retain the current configuration'),
153                                         ])
154                                 ),
155                                 E('div', { class: 'right' }, [
156                                         E('div', { class: 'btn', click: ui.hideModal }, _('Cancel')),
157                                         ' ',
158                                         E(
159                                                 'button',
160                                                 {
161                                                         class: 'btn cbi-button cbi-button-positive important',
162                                                         click: ui.createHandlerFn(this, function () {
163                                                                 this.handleInstall(sysupgrade_url, keep.checked, image.sha256);
164                                                         }),
165                                                 },
166                                                 _('Install firmware image')
167                                         ),
168                                 ]),
169                         ];
170
171                         ui.showModal(_('Successfully created firmware image'), modal_body);
172                         if (data.rebuilder) {
173                                 this.handleRebuilder(content, data, firmware);
174                         }
175                 }
176         },
177
178         handle202: function (response) {
179                 response = response.json();
180                 this.request_hash = response.request_hash;
181
182                 if ('queue_position' in response) {
183                         ui.showModal(_('Queued...'), [
184                                 E(
185                                         'p',
186                                         { class: 'spinning' },
187                                         _('Request in build queue position %s').format(
188                                                 response.queue_position
189                                         )
190                                 ),
191                         ]);
192                 } else {
193                         ui.showModal(_('Building Firmware...'), [
194                                 E(
195                                         'p',
196                                         { class: 'spinning' },
197                                         _('Progress: %s%% %s').format(
198                                                 this.steps[response.imagebuilder_status][0],
199                                                 this.steps[response.imagebuilder_status][1]
200                                         )
201                                 ),
202                         ]);
203                 }
204         },
205
206         handleError: function (response, data, firmware) {
207                 response = response.json();
208                 const request_data = {
209                         ...data,
210                         request_hash: this.request_hash,
211                         sha256_unsigned: this.sha256_unsigned,
212                         ...firmware
213                 };
214                 let body = [
215                         E('p', {}, _('Server response: %s').format(response.detail)),
216                         E(
217                                 'a',
218                                 { href: 'https://github.com/openwrt/asu/issues' },
219                                 _('Please report the error message and request')
220                         ),
221                         E('p', {}, _('Request Data:')),
222                         E('pre', {}, JSON.stringify({ ...request_data }, null, 4)),
223                 ];
224
225                 if (response.stdout) {
226                         body.push(E('b', {}, 'STDOUT:'));
227                         body.push(E('pre', {}, response.stdout));
228                 }
229
230                 if (response.stderr) {
231                         body.push(E('b', {}, 'STDERR:'));
232                         body.push(E('pre', {}, response.stderr));
233                 }
234
235                 body = body.concat([
236                         E('div', { class: 'right' }, [
237                                 E('div', { class: 'btn', click: ui.hideModal }, _('Close')),
238                         ]),
239                 ]);
240
241                 ui.showModal(_('Error building the firmware image'), body);
242         },
243
244         handleRequest: function (server, main, content, data, firmware) {
245                 let request_url = `${server}/api/v1/build`;
246                 let method = 'POST';
247                 let local_content = content;
248
249                 /**
250                  * If `request_hash` is available use a GET request instead of
251                  * sending the entire object.
252                  */
253                 if (this.request_hash && main == true) {
254                         request_url += `/${this.request_hash}`;
255                         local_content = {};
256                         method = 'GET';
257                 }
258
259                 request
260                         .request(request_url, { method: method, content: local_content })
261                         .then((response) => {
262                                 switch (response.status) {
263                                         case 202:
264                                                 if (main) {
265                                                         this.handle202(response);
266                                                 } else {
267                                                         response = response.json();
268
269                                                         let view = document.getElementById(server);
270                                                         view.innerText = `⏳   (${
271                                                                 this.steps[response.imagebuilder_status][0]
272                                                         }%) ${server}`;
273                                                 }
274                                                 break;
275                                         case 200:
276                                                 if (main == true) {
277                                                         poll.remove(this.pollFn);
278                                                         this.handle200(response, content, data, firmware);
279                                                 } else {
280                                                         poll.remove(this.rebuilder_polls[server]);
281                                                         response = response.json();
282                                                         let view = document.getElementById(server);
283                                                         let image = this.selectImage(response.images, data, firmware);
284                                                         if (image.sha256_unsigned == this.sha256_unsigned) {
285                                                                 view.innerText = '✅ %s'.format(server);
286                                                         } else {
287                                                                 view.innerHTML = `⚠️ ${server} (<a href="${server}/store/${
288                                                                         response.bin_dir
289                                                                 }/${image.name}">${_('Download')}</a>)`;
290                                                         }
291                                                 }
292                                                 break;
293                                         case 400: // bad request
294                                         case 422: // bad package
295                                         case 500: // build failed
296                                                 if (main == true) {
297                                                         poll.remove(this.pollFn);
298                                                         this.handleError(response, data, firmware);
299                                                         break;
300                                                 } else {
301                                                         poll.remove(this.rebuilder_polls[server]);
302                                                         document.getElementById(server).innerText = '🚫 %s'.format(
303                                                                 server
304                                                         );
305                                                 }
306                                 }
307                         });
308         },
309
310         handleRebuilder: function (content, data, firmware) {
311                 this.rebuilder_polls = {};
312                 for (let rebuilder of data.rebuilder) {
313                         this.rebuilder_polls[rebuilder] = L.bind(
314                                 this.handleRequest,
315                                 this,
316                                 rebuilder,
317                                 false,
318                                 content,
319                                 data,
320                                 firmware
321                         );
322                         poll.add(this.rebuilder_polls[rebuilder], 5);
323                         document.getElementById(
324                                 'rebuilder_status'
325                         ).innerHTML += `<p id="${rebuilder}">⏳ ${rebuilder}</p>`;
326                 }
327                 poll.start();
328         },
329
330         handleInstall: function (url, keep, sha256) {
331                 ui.showModal(_('Downloading...'), [
332                         E(
333                                 'p',
334                                 { class: 'spinning' },
335                                 _('Downloading firmware from server to browser')
336                         ),
337                 ]);
338
339                 request
340                         .get(url, {
341                                 headers: {
342                                         'Content-Type': 'application/x-www-form-urlencoded',
343                                 },
344                                 responseType: 'blob',
345                         })
346                         .then((response) => {
347                                 let form_data = new FormData();
348                                 form_data.append('sessionid', rpc.getSessionID());
349                                 form_data.append('filename', '/tmp/firmware.bin');
350                                 form_data.append('filemode', 600);
351                                 form_data.append('filedata', response.blob());
352
353                                 ui.showModal(_('Uploading...'), [
354                                         E(
355                                                 'p',
356                                                 { class: 'spinning' },
357                                                 _('Uploading firmware from browser to device')
358                                         ),
359                                 ]);
360
361                                 request
362                                         .get(`${L.env.cgi_base}/cgi-upload`, {
363                                                 method: 'PUT',
364                                                 content: form_data,
365                                         })
366                                         .then((response) => response.json())
367                                         .then((response) => {
368                                                 if (response.sha256sum != sha256) {
369                                                         ui.showModal(_('Wrong checksum'), [
370                                                                 E(
371                                                                         'p',
372                                                                         _('Error during download of firmware. Please try again')
373                                                                 ),
374                                                                 E('div', { class: 'btn', click: ui.hideModal }, _('Close')),
375                                                         ]);
376                                                 } else {
377                                                         ui.showModal(_('Installing...'), [
378                                                                 E(
379                                                                         'p',
380                                                                         { class: 'spinning' },
381                                                                         _('Installing the sysupgrade. Do not unpower device!')
382                                                                 ),
383                                                         ]);
384
385                                                         L.resolveDefault(callUpgradeStart(keep), {}).then((response) => {
386                                                                 if (keep) {
387                                                                         ui.awaitReconnect(window.location.host);
388                                                                 } else {
389                                                                         ui.awaitReconnect('192.168.1.1', 'openwrt.lan');
390                                                                 }
391                                                         });
392                                                 }
393                                         });
394                         });
395         },
396
397         handleCheck: function (data, firmware) {
398                 this.request_hash = '';
399                 let { url, revision, advanced_mode, branch } = data;
400                 let { version, target, profile, packages } = firmware;
401                 let candidates = [];
402                 let request_url = `${url}/api/overview`;
403                 if (version.endsWith('SNAPSHOT')) {
404                         request_url = `${url}/api/v1/revision/${version}/${target}`;
405                 }
406
407                 ui.showModal(_('Searching...'), [
408                         E(
409                                 'p',
410                                 { class: 'spinning' },
411                                 _('Searching for an available sysupgrade of %s - %s').format(
412                                         version,
413                                         revision
414                                 )
415                         ),
416                 ]);
417
418                 L.resolveDefault(request.get(request_url)).then((response) => {
419                         if (!response.ok) {
420                                 ui.showModal(_('Error connecting to upgrade server'), [
421                                         E(
422                                                 'p',
423                                                 {},
424                                                 _('Could not reach API at "%s". Please try again later.').format(
425                                                         response.url
426                                                 )
427                                         ),
428                                         E('pre', {}, response.responseText),
429                                         E('div', { class: 'right' }, [
430                                                 E('div', { class: 'btn', click: ui.hideModal }, _('Close')),
431                                         ]),
432                                 ]);
433                                 return;
434                         }
435                         if (version.endsWith('SNAPSHOT')) {
436                                 const remote_revision = response.json().revision;
437                                 if (
438                                         get_revision_count(revision) < get_revision_count(remote_revision)
439                                 ) {
440                                         candidates.push([version, remote_revision]);
441                                 }
442                         } else {
443                                 const latest = response.json().latest;
444
445                                 for (let remote_version of latest) {
446                                         let remote_branch = get_branch(remote_version);
447
448                                         // already latest version installed
449                                         if (version == remote_version) {
450                                                 break;
451                                         }
452
453                                         // skip branch upgrades outside the advanced mode
454                                         if (branch != remote_branch && advanced_mode == 0) {
455                                                 continue;
456                                         }
457
458                                         candidates.unshift([remote_version, null]);
459
460                                         // don't offer branches older than the current
461                                         if (branch == remote_branch) {
462                                                 break;
463                                         }
464                                 }
465                         }
466
467                         // allow to re-install running firmware in advanced mode
468                         if (advanced_mode == 1) {
469                                 candidates.unshift([version, revision]);
470                         }
471
472                         if (candidates.length) {
473                                 let s, o;
474
475                                 let mapdata = {
476                                         request: {
477                                                 profile,
478                                                 version: candidates[0][0],
479                                                 packages: Object.keys(packages).sort(),
480                                         },
481                                 };
482
483                                 let map = new form.JSONMap(mapdata, '');
484
485                                 s = map.section(
486                                         form.NamedSection,
487                                         'request',
488                                         '',
489                                         '',
490                                         'Use defaults for the safest update'
491                                 );
492                                 o = s.option(form.ListValue, 'version', 'Select firmware version');
493                                 for (let candidate of candidates) {
494                                         if (candidate[0] == version && candidate[1] == revision) {
495                                                 o.value(
496                                                         candidate[0],
497                                                         _('[installed] %s').format(
498                                                                 candidate[1]
499                                                                         ? `${candidate[0]} - ${candidate[1]}`
500                                                                         : candidate[0]
501                                                         )
502                                                 );
503                                         } else {
504                                                 o.value(
505                                                         candidate[0],
506                                                         candidate[1] ? `${candidate[0]} - ${candidate[1]}` : candidate[0]
507                                                 );
508                                         }
509                                 }
510
511                                 if (advanced_mode == 1) {
512                                         o = s.option(form.Value, 'profile', _('Board Name / Profile'));
513                                         o = s.option(form.DynamicList, 'packages', _('Packages'));
514                                 }
515
516                                 L.resolveDefault(map.render()).then((form_rendered) => {
517                                         ui.showModal(_('New firmware upgrade available'), [
518                                                 E(
519                                                         'p',
520                                                         _('Currently running: %s - %s').format(
521                                                                 version,
522                                                                 revision
523                                                         )
524                                                 ),
525                                                 form_rendered,
526                                                 E('div', { class: 'right' }, [
527                                                         E('div', { class: 'btn', click: ui.hideModal }, _('Cancel')),
528                                                         ' ',
529                                                         E(
530                                                                 'button',
531                                                                 {
532                                                                         class: 'btn cbi-button cbi-button-positive important',
533                                                                         click: ui.createHandlerFn(this, function () {
534                                                                                 map.save().then(() => {
535                                                                                         const content = {
536                                                                                                 ...firmware,
537                                                                                                 packages: mapdata.request.packages,
538                                                                                                 version: mapdata.request.version,
539                                                                                                 profile: mapdata.request.profile
540                                                                                         };
541                                                                                         this.pollFn = L.bind(function () {
542                                                                                                 this.handleRequest(url, true, content, data, firmware);
543                                                                                         }, this);
544                                                                                         poll.add(this.pollFn, 5);
545                                                                                         poll.start();
546                                                                                 });
547                                                                         }),
548                                                                 },
549                                                                 _('Request firmware image')
550                                                         ),
551                                                 ]),
552                                         ]);
553                                 });
554                         } else {
555                                 ui.showModal(_('No upgrade available'), [
556                                         E(
557                                                 'p',
558                                                 _('The device runs the latest firmware version %s - %s').format(
559                                                         version,
560                                                         revision
561                                                 )
562                                         ),
563                                         E('div', { class: 'right' }, [
564                                                 E('div', { class: 'btn', click: ui.hideModal }, _('Close')),
565                                         ]),
566                                 ]);
567                         }
568                 });
569         },
570
571         load: async function () {
572                 const promises = await Promise.all([
573                         L.resolveDefault(callPackagelist(), {}),
574                         L.resolveDefault(callSystemBoard(), {}),
575                         L.resolveDefault(fs.stat('/sys/firmware/efi'), null),
576                         uci.load('attendedsysupgrade'),
577                 ]);
578                 const data = {
579                         url: uci.get_first('attendedsysupgrade', 'server', 'url'),
580                         branch: get_branch(promises[1].release.version),
581                         revision: promises[1].release.revision,
582                         efi: promises[2],
583                         advanced_mode: uci.get_first('attendedsysupgrade', 'client', 'advanced_mode') || 0,
584                         rebuilder: uci.get_first('attendedsysupgrade', 'server', 'rebuilder')
585                 };
586                 const firmware = {
587                         client: 'luci/' + promises[0].packages['luci-app-attendedsysupgrade'],
588                         packages: promises[0].packages,
589                         profile: promises[1].board_name,
590                         target: promises[1].release.target,
591                         version: promises[1].release.version,
592                         diff_packages: true,
593                         filesystem: promises[1].rootfs_type
594                 };
595                 return [data, firmware];
596         },
597
598         render: function (response) {
599                 const data = response[0];
600                 const firmware = response[1];
601
602                 return E('p', [
603                         E('h2', _('Attended Sysupgrade')),
604                         E(
605                                 'p',
606                                 _(
607                                         'The attended sysupgrade service allows to easily upgrade vanilla and custom firmware images.'
608                                 )
609                         ),
610                         E(
611                                 'p',
612                                 _(
613                                         'This is done by building a new firmware on demand via an online service.'
614                                 )
615                         ),
616                         E(
617                                 'p',
618                                 _('Currently running: %s - %s').format(
619                                         firmware.version,
620                                         data.revision
621                                 )
622                         ),
623                         E(
624                                 'button',
625                                 {
626                                         class: 'btn cbi-button cbi-button-positive important',
627                                         click: ui.createHandlerFn(this, this.handleCheck, data, firmware),
628                                 },
629                                 _('Search for firmware upgrade')
630                         ),
631                 ]);
632         },
633         handleSaveApply: null,
634         handleSave: null,
635         handleReset: null,
636 });
git clone https://git.99rst.org/PROJECT