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