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