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