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