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