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