55ed0509e16eda799f0f2a253ef9194a306804a5
[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 var callPackagelist = rpc.declare({
13         object: 'rpc-sys',
14         method: 'packagelist',
15 });
16
17 var callSystemBoard = rpc.declare({
18         object: 'system',
19         method: 'board',
20 });
21
22 var 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% Setup 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         },
75
76         firmware: {
77                 profile: '',
78                 target: '',
79                 version: '',
80                 packages: [],
81                 diff_packages: true,
82                 filesystem: '',
83         },
84
85         handle200: function (response) {
86                 response = response.json();
87                 var image;
88                 for (image of response.images) {
89                         if (this.firmware.filesystem == image.filesystem) {
90                                 if (this.data.efi) {
91                                         if (image.type == 'combined-efi') {
92                                                 break;
93                                         }
94                                 } else {
95                                         if (image.type == 'sysupgrade' || image.type == 'combined') {
96                                                 break;
97                                         }
98                                 }
99                         }
100                 }
101
102                 if (image.name != undefined) {
103                         var sysupgrade_url = `${this.data.url}/store/${response.bin_dir}/${image.name}`;
104
105                         var keep = E('input', { type: 'checkbox' });
106                         keep.checked = true;
107
108                         var fields = [
109                                 _('Version'), `${response.version_number} ${response.version_code}`,
110                                 _('SHA256'), image.sha256,
111                         ];
112
113                         if (this.data.advanced_mode == 1) {
114                                 fields.push(
115                                         _('Profile'), response.id,
116                                         _('Target'), response.target,
117                                         _('Build Date'), response.build_at,
118                                         _('Filename'), image.name,
119                                         _('Filesystem'), image.filesystem,
120                                 )
121                         }
122
123                         fields.push('', E('a', { href: sysupgrade_url }, _('Download firmware image')))
124
125                         var table = E('div', { class: 'table' });
126
127                         for (var i = 0; i < fields.length; i += 2) {
128                                 table.appendChild(E('tr', { class: 'tr' }, [
129                                         E('td', { class: 'td left', width: '33%' }, [fields[i]]),
130                                         E('td', { class: 'td left' }, [fields[i + 1]]),
131                                 ]));
132                         }
133
134                         var modal_body = [
135                                 table,
136                                 E('p', { class: 'mt-2' },
137                                         E('label', { class: 'btn' }, [
138                                                 keep, ' ',
139                                                 _('Keep settings and retain the current configuration')
140                                         ])),
141                                 E('div', { class: 'right' }, [
142                                         E('div', { class: 'btn', click: ui.hideModal }, _('Cancel')), ' ',
143                                         E('button', {
144                                                 'class': 'btn cbi-button cbi-button-positive important',
145                                                 'click': ui.createHandlerFn(this, function () {
146                                                         this.handleInstall(sysupgrade_url, keep.checked, image.sha256)
147                                                 })
148                                         }, _('Install firmware image')),
149                                 ]),
150                         ];
151
152                         ui.showModal(_('Successfully created firmware image'), modal_body);
153                 }
154         },
155
156         handle202: function (response) {
157                 response = response.json();
158                 this.data.request_hash = response.request_hash;
159
160                 if ('queue_position' in response) {
161                         ui.showModal(_('Queued...'), [
162                                 E('p', { 'class': 'spinning' }, _('Request in build queue position %s').format(response.queue_position))
163                         ]);
164                 } else {
165                         ui.showModal(_('Building Firmware...'), [
166                                 E('p', { 'class': 'spinning' }, _('Progress: %s').format(this.steps[response.imagebuilder_status]))
167                         ]);
168                 }
169         },
170
171         handleError: function (response) {
172                 response = response.json();
173                 var body = [
174                         E('p', {}, _('Server response: %s').format(response.detail)),
175                         E('a', { href: 'https://github.com/openwrt/asu/issues' }, _('Please report the error message and request')),
176                         E('p', {}, _('Request Data:')),
177                         E('pre', {}, JSON.stringify({ ...this.data, ...this.firmware }, null, 4)),
178                 ];
179
180                 if (response.stdout) {
181                         body.push(E('b', {}, 'STDOUT:'));
182                         body.push(E('pre', {}, response.stdout));
183                 }
184
185                 if (response.stderr) {
186                         body.push(E('b', {}, 'STDERR:'));
187                         body.push(E('pre', {}, response.stderr));
188                 }
189
190                 body = body.concat([
191                         E('div', { class: 'right' }, [
192                                 E('div', { class: 'btn', click: ui.hideModal }, _('Close')),
193                         ]),
194                 ]);
195
196                 ui.showModal(_('Error building the firmware image'), body);
197         },
198
199         handleRequest: function () {
200                 var request_url = `${this.data.url}/api/v1/build`;
201                 var method = "POST"
202                 var content = this.firmware;
203
204                 /**
205                  * If `request_hash` is available use a GET request instead of 
206                  * sending the entire object.
207                  */
208                 if (this.data.request_hash) {
209                         request_url += `/${this.data.request_hash}`;
210                         content = {};
211                         method = "GET"
212                 }
213
214                 request.request(request_url, { method: method, content: content })
215                         .then((response) => {
216                                 switch (response.status) {
217                                         case 202:
218                                                 this.handle202(response);
219                                                 break;
220                                         case 200:
221                                                 poll.stop();
222                                                 this.handle200(response);
223                                                 break;
224                                         case 400: // bad request
225                                         case 422: // bad package
226                                         case 500: // build failed
227                                                 poll.stop();
228                                                 this.handleError(response);
229                                                 break;
230                                 }
231                         });
232         },
233
234         handleInstall: function (url, keep, sha256) {
235                 ui.showModal(_('Downloading...'), [
236                         E('p', { 'class': 'spinning' }, _('Downloading firmware from server to browser'))
237                 ]);
238
239                 request.get(url, {
240                         headers: {
241                                 'Content-Type': 'application/x-www-form-urlencoded',
242                         },
243                         responseType: 'blob',
244                 })
245                         .then((response) => {
246                                 var form_data = new FormData();
247                                 form_data.append('sessionid', rpc.getSessionID());
248                                 form_data.append('filename', '/tmp/firmware.bin');
249                                 form_data.append('filemode', 600);
250                                 form_data.append('filedata', response.blob());
251
252                                 ui.showModal(_('Uploading...'), [
253                                         E('p', { 'class': 'spinning' }, _('Uploading firmware from browser to device'))
254                                 ]);
255
256                                 request
257                                         .get(`${L.env.cgi_base}/cgi-upload`, {
258                                                 method: 'PUT',
259                                                 content: form_data,
260                                         })
261                                         .then((response) => response.json())
262                                         .then((response) => {
263                                                 if (response.sha256sum != sha256) {
264
265                                                         ui.showModal(_('Wrong checksum'), [
266                                                                 E('p', _('Error during download of firmware. Please try again')),
267                                                                 E('div', { class: 'btn', click: ui.hideModal }, _('Close'))
268                                                         ]);
269                                                 } else {
270                                                         ui.showModal(_('Installing...'), [
271                                                                 E('p', { class: 'spinning' }, _('Installing the sysupgrade. Do not unpower device!'))
272                                                         ]);
273
274                                                         L.resolveDefault(callUpgradeStart(keep), {})
275                                                                 .then((response) => {
276                                                                         if (keep) {
277                                                                                 ui.awaitReconnect(window.location.host);
278                                                                         } else {
279                                                                                 ui.awaitReconnect('192.168.1.1', 'openwrt.lan');
280                                                                         }
281                                                                 });
282                                                 }
283                                         });
284                         });
285         },
286
287         handleCheck: function () {
288                 var { url, revision } = this.data
289                 var { version, target } = this.firmware
290                 var candidates = [];
291                 var response;
292                 var request_url = `${url}/api/overview`;
293                 if (version.endsWith('SNAPSHOT')) {
294                         request_url = `${url}/api/v1/revision/${version}/${target}`;
295                 }
296
297                 ui.showModal(_('Searching...'), [
298                         E('p', { 'class': 'spinning' },
299                                 _('Searching for an available sysupgrade of %s - %s').format(version, revision))
300                 ]);
301
302                 L.resolveDefault(request.get(request_url))
303                         .then(response => {
304                                 if (!response.ok) {
305                                         ui.showModal(_('Error connecting to upgrade server'), [
306                                                 E('p', {}, _('Could not reach API at "%s". Please try again later.').format(response.url)),
307                                                 E('pre', {}, response.responseText),
308                                                 E('div', { class: 'right' }, [
309                                                         E('div', { class: 'btn', click: ui.hideModal }, _('Close'))
310                                                 ]),
311                                         ]);
312                                         return;
313                                 }
314                                 if (version.endsWith('SNAPSHOT')) {
315                                         const remote_revision = response.json().revision;
316                                         if (get_revision_count(revision) < get_revision_count(remote_revision)) {
317                                                 candidates.push([version, remote_revision]);
318                                         }
319                                 } else {
320                                         const latest = response.json().latest;
321
322                                         for (let remote_version of latest) {
323                                                 var remote_branch = get_branch(remote_version);
324
325                                                 // already latest version installed
326                                                 if (version == remote_version) {
327                                                         break;
328                                                 }
329
330                                                 // skip branch upgrades outside the advanced mode
331                                                 if (this.data.branch != remote_branch && this.data.advanced_mode == 0) {
332                                                         continue;
333                                                 }
334
335                                                 candidates.unshift([remote_version, null]);
336
337                                                 // don't offer branches older than the current
338                                                 if (this.data.branch == remote_branch) {
339                                                         break;
340                                                 }
341                                         }
342                                 }
343
344                                 // allow to re-install running firmware in advanced mode
345                                 if (this.data.advanced_mode == 1) {
346                                         candidates.unshift([version, revision])
347                                 }
348
349                                 if (candidates.length) {
350                                         var m, s, o;
351
352                                         var mapdata = {
353                                                 request: {
354                                                         profile: this.firmware.profile,
355                                                         version: candidates[0][0],
356                                                         packages: Object.keys(this.firmware.packages).sort(),
357                                                 },
358                                         };
359
360                                         var map = new form.JSONMap(mapdata, '');
361
362                                         s = map.section(form.NamedSection, 'request', '', '', 'Use defaults for the safest update');
363                                         o = s.option(form.ListValue, 'version', 'Select firmware version');
364                                         for (let candidate of candidates) {
365                                                 if (candidate[0] == version && candidate[1] == revision) {
366                                                         o.value(candidate[0], _('[installed] %s')
367                                                                 .format(candidate[1] ? `${candidate[0]} - ${candidate[1]}` : candidate[0]));
368                                                 } else {
369                                                         o.value(candidate[0], candidate[1] ? `${candidate[0]} - ${candidate[1]}` : candidate[0]);
370                                                 }
371                                         }
372
373                                         if (this.data.advanced_mode == 1) {
374                                                 o = s.option(form.Value, 'profile', _('Board Name / Profile'));
375                                                 o = s.option(form.DynamicList, 'packages', _('Packages'));
376                                         }
377
378                                         L.resolveDefault(map.render()).
379                                                 then(form_rendered => {
380                                                         ui.showModal(_('New firmware upgrade available'), [
381                                                                 E('p', _('Currently running: %s - %s').format(this.firmware.version, this.data.revision)),
382                                                                 form_rendered,
383                                                                 E('div', { class: 'right' }, [
384                                                                         E('div', { class: 'btn', click: ui.hideModal }, _('Cancel')), ' ',
385                                                                         E('button', {
386                                                                                 'class': 'btn cbi-button cbi-button-positive important',
387                                                                                 'click': ui.createHandlerFn(this, function () {
388                                                                                         map.save().then(() => {
389                                                                                                 this.firmware.packages = mapdata.request.packages;
390                                                                                                 this.firmware.version = mapdata.request.version;
391                                                                                                 this.firmware.profile = mapdata.request.profile;
392                                                                                                 poll.add(L.bind(this.handleRequest, this), 5);
393                                                                                         });
394                                                                                 })
395                                                                         }, _('Request firmware image')),
396                                                                 ]),
397                                                         ]);
398                                                 });
399                                 } else {
400                                         ui.showModal(_('No upgrade available'), [
401                                                 E('p', _('The device runs the latest firmware version %s - %s').format(version, revision)),
402                                                 E('div', { class: 'right' }, [
403                                                         E('div', { class: 'btn', click: ui.hideModal }, _('Close')),
404                                                 ]),
405                                         ]);
406                                 }
407
408                         });
409         },
410
411         load: function () {
412                 return Promise.all([
413                         L.resolveDefault(callPackagelist(), {}),
414                         L.resolveDefault(callSystemBoard(), {}),
415                         L.resolveDefault(fs.stat("/sys/firmware/efi"), null),
416                         uci.load('attendedsysupgrade'),
417                 ]);
418         },
419
420         render: function (response) {
421                 this.firmware.client = 'luci/' + response[0].packages['luci-app-attendedsysupgrade'];
422                 this.firmware.packages = response[0].packages;
423
424                 this.firmware.profile = response[1].board_name;
425                 this.firmware.target = response[1].release.target;
426                 this.firmware.version = response[1].release.version;
427                 this.data.branch = get_branch(response[1].release.version);
428                 this.firmware.filesystem = response[1].rootfs_type;
429                 this.data.revision = response[1].release.revision;
430
431                 this.data.efi = response[2];
432
433                 this.data.url = uci.get_first('attendedsysupgrade', 'server', 'url');
434                 this.data.advanced_mode = uci.get_first('attendedsysupgrade', 'client', 'advanced_mode') || 0
435
436                 return E('p', [
437                         E('h2', _('Attended Sysupgrade')),
438                         E('p', _('The attended sysupgrade service allows to easily upgrade vanilla and custom firmware images.')),
439                         E('p', _('This is done by building a new firmware on demand via an online service.')),
440                         E('p', _('Currently running: %s - %s').format(this.firmware.version, this.data.revision)),
441                         E('button', {
442                                 'class': 'btn cbi-button cbi-button-positive important',
443                                 'click': ui.createHandlerFn(this, this.handleCheck)
444                         }, _('Search for firmware upgrade'))
445                 ]);
446         },
447         handleSaveApply: null,
448         handleSave: null,
449         handleReset: null
450 });
git clone https://git.99rst.org/PROJECT