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