luci-app-acme: convert to client side rendering
[project/luci.git] / modules / luci-mod-system / htdocs / luci-static / resources / view / system / flash.js
1 'use strict';
2 'require view';
3 'require dom';
4 'require form';
5 'require rpc';
6 'require fs';
7 'require ui';
8
9 var isReadonlyView = !L.hasViewPermission();
10
11 var callSystemValidateFirmwareImage = rpc.declare({
12 object: 'system',
13 method: 'validate_firmware_image',
14 params: [ 'path' ],
15 expect: { '': { valid: false, forcable: true } }
16 });
17
18 function findStorageSize(procmtd, procpart) {
19 var kernsize = 0, rootsize = 0, wholesize = 0;
20
21 procmtd.split(/\n/).forEach(function(ln) {
22 var match = ln.match(/^mtd\d+: ([0-9a-f]+) [0-9a-f]+ "(.+)"$/),
23 size = match ? parseInt(match[1], 16) : 0;
24
25 switch (match ? match[2] : '') {
26 case 'linux':
27 case 'firmware':
28 if (size > wholesize)
29 wholesize = size;
30 break;
31
32 case 'kernel':
33 case 'kernel0':
34 kernsize = size;
35 break;
36
37 case 'rootfs':
38 case 'rootfs0':
39 case 'ubi':
40 case 'ubi0':
41 rootsize = size;
42 break;
43 }
44 });
45
46 if (wholesize > 0)
47 return wholesize;
48 else if (kernsize > 0 && rootsize > kernsize)
49 return kernsize + rootsize;
50
51 procpart.split(/\n/).forEach(function(ln) {
52 var match = ln.match(/^\s*\d+\s+\d+\s+(\d+)\s+(\S+)$/);
53 if (match) {
54 var size = parseInt(match[1], 10);
55
56 if (!match[2].match(/\d/) && size > 2048 && wholesize == 0)
57 wholesize = size * 1024;
58 }
59 });
60
61 return wholesize;
62 }
63
64
65 var mapdata = { actions: {}, config: {} };
66
67 return view.extend({
68 load: function() {
69 var tasks = [
70 L.resolveDefault(fs.stat('/lib/upgrade/platform.sh'), {}),
71 fs.trimmed('/proc/sys/kernel/hostname'),
72 fs.trimmed('/proc/mtd'),
73 fs.trimmed('/proc/partitions'),
74 fs.trimmed('/proc/mounts')
75 ];
76
77 return Promise.all(tasks);
78 },
79
80 handleBackup: function(ev) {
81 var form = E('form', {
82 method: 'post',
83 action: L.env.cgi_base + '/cgi-backup',
84 enctype: 'application/x-www-form-urlencoded'
85 }, E('input', { type: 'hidden', name: 'sessionid', value: rpc.getSessionID() }));
86
87 ev.currentTarget.parentNode.appendChild(form);
88
89 form.submit();
90 form.parentNode.removeChild(form);
91 },
92
93 handleFirstboot: function(ev) {
94 if (!confirm(_('Do you really want to erase all settings?')))
95 return;
96
97 ui.showModal(_('Erasing...'), [
98 E('p', { 'class': 'spinning' }, _('The system is erasing the configuration partition now and will reboot itself when finished.'))
99 ]);
100
101 /* Currently the sysupgrade rpc call will not return, hence no promise handling */
102 fs.exec('/sbin/firstboot', [ '-r', '-y' ]);
103
104 ui.awaitReconnect('192.168.1.1', 'openwrt.lan');
105 },
106
107 handleRestore: function(ev) {
108 return ui.uploadFile('/tmp/backup.tar.gz', ev.target)
109 .then(L.bind(function(btn, res) {
110 btn.firstChild.data = _('Checking archiveā€¦');
111 return fs.exec('/bin/tar', [ '-tzf', '/tmp/backup.tar.gz' ]);
112 }, this, ev.target))
113 .then(L.bind(function(btn, res) {
114 if (res.code != 0) {
115 ui.addNotification(null, E('p', _('The uploaded backup archive is not readable')));
116 return fs.remove('/tmp/backup.tar.gz');
117 }
118
119 ui.showModal(_('Apply backup?'), [
120 E('p', _('The uploaded backup archive appears to be valid and contains the files listed below. Press "Continue" to restore the backup and reboot, or "Cancel" to abort the operation.')),
121 E('pre', {}, [ res.stdout ]),
122 E('div', { 'class': 'right' }, [
123 E('button', {
124 'class': 'btn',
125 'click': ui.createHandlerFn(this, function(ev) {
126 return fs.remove('/tmp/backup.tar.gz').finally(ui.hideModal);
127 })
128 }, [ _('Cancel') ]), ' ',
129 E('button', {
130 'class': 'btn cbi-button-action important',
131 'click': ui.createHandlerFn(this, 'handleRestoreConfirm', btn)
132 }, [ _('Continue') ])
133 ])
134 ]);
135 }, this, ev.target))
136 .catch(function(e) { ui.addNotification(null, E('p', e.message)) })
137 .finally(L.bind(function(btn, input) {
138 btn.firstChild.data = _('Upload archive...');
139 }, this, ev.target));
140 },
141
142 handleRestoreConfirm: function(btn, ev) {
143 return fs.exec('/sbin/sysupgrade', [ '--restore-backup', '/tmp/backup.tar.gz' ])
144 .then(L.bind(function(btn, res) {
145 if (res.code != 0) {
146 ui.addNotification(null, [
147 E('p', _('The restore command failed with code %d').format(res.code)),
148 res.stderr ? E('pre', {}, [ res.stderr ]) : ''
149 ]);
150 L.raise('Error', 'Unpack failed');
151 }
152
153 btn.firstChild.data = _('Rebootingā€¦');
154 return fs.exec('/sbin/reboot');
155 }, this, ev.target))
156 .then(L.bind(function(res) {
157 if (res.code != 0) {
158 ui.addNotification(null, E('p', _('The reboot command failed with code %d').format(res.code)));
159 L.raise('Error', 'Reboot failed');
160 }
161
162 ui.showModal(_('Rebootingā€¦'), [
163 E('p', { 'class': 'spinning' }, _('The system is rebooting now. If the restored configuration changed the current LAN IP address, you might need to reconnect manually.'))
164 ]);
165
166 ui.awaitReconnect(window.location.host, '192.168.1.1', 'openwrt.lan');
167 }, this))
168 .catch(function(e) { ui.addNotification(null, E('p', e.message)) })
169 .finally(function() { btn.firstChild.data = _('Upload archive...') });
170 },
171
172 handleBlock: function(hostname, ev) {
173 var mtdblock = dom.parent(ev.target, '.cbi-section').querySelector('[data-name="mtdselect"] select').value;
174 var form = E('form', {
175 'method': 'post',
176 'action': L.env.cgi_base + '/cgi-download',
177 'enctype': 'application/x-www-form-urlencoded'
178 }, [
179 E('input', { 'type': 'hidden', 'name': 'sessionid', 'value': rpc.getSessionID() }),
180 E('input', { 'type': 'hidden', 'name': 'path', 'value': '/dev/mtdblock%d'.format(mtdblock) }),
181 E('input', { 'type': 'hidden', 'name': 'filename', 'value': '%s.mtd%d.bin'.format(hostname, mtdblock) })
182 ]);
183
184 ev.currentTarget.parentNode.appendChild(form);
185
186 form.submit();
187 form.parentNode.removeChild(form);
188 },
189
190 handleSysupgrade: function(storage_size, ev) {
191 return ui.uploadFile('/tmp/firmware.bin', ev.target.firstChild)
192 .then(L.bind(function(btn, reply) {
193 btn.firstChild.data = _('Checking imageā€¦');
194
195 ui.showModal(_('Checking imageā€¦'), [
196 E('span', { 'class': 'spinning' }, _('Verifying the uploaded image file.'))
197 ]);
198
199 return callSystemValidateFirmwareImage('/tmp/firmware.bin')
200 .then(function(res) { return [ reply, res ]; });
201 }, this, ev.target))
202 .then(L.bind(function(btn, reply) {
203 return fs.exec('/sbin/sysupgrade', [ '--test', '/tmp/firmware.bin' ])
204 .then(function(res) { reply.push(res); return reply; });
205 }, this, ev.target))
206 .then(L.bind(function(btn, res) {
207 var keep = E('input', { type: 'checkbox' }),
208 force = E('input', { type: 'checkbox' }),
209 is_valid = res[1].valid,
210 is_forceable = res[1].forceable,
211 allow_backup = res[1].allow_backup,
212 is_too_big = (storage_size > 0 && res[0].size > storage_size),
213 body = [];
214
215 body.push(E('p', _('The flash image was uploaded. Below is the checksum and file size listed, compare them with the original file to ensure data integrity. <br /> Click "Proceed" below to start the flash procedure.')));
216 body.push(E('ul', {}, [
217 res[0].size ? E('li', {}, '%s: %1024.2mB'.format(_('Size'), res[0].size)) : '',
218 res[0].checksum ? E('li', {}, '%s: %s'.format(_('MD5'), res[0].checksum)) : '',
219 res[0].sha256sum ? E('li', {}, '%s: %s'.format(_('SHA256'), res[0].sha256sum)) : ''
220 ]));
221
222 body.push(E('p', {}, E('label', { 'class': 'btn' }, [
223 keep, ' ', _('Keep settings and retain the current configuration')
224 ])));
225
226 if (!is_valid || is_too_big)
227 body.push(E('hr'));
228
229 if (is_too_big)
230 body.push(E('p', { 'class': 'alert-message' }, [
231 _('It appears that you are trying to flash an image that does not fit into the flash memory, please verify the image file!')
232 ]));
233
234 if (!is_valid)
235 body.push(E('p', { 'class': 'alert-message' }, [
236 res[2].stderr ? res[2].stderr : '',
237 res[2].stderr ? E('br') : '',
238 res[2].stderr ? E('br') : '',
239 _('The uploaded image file does not contain a supported format. Make sure that you choose the generic image format for your platform.')
240 ]));
241
242 if (!allow_backup)
243 body.push(E('p', { 'class': 'alert-message' }, [
244 _('The uploaded firmware does not allow keeping current configuration.')
245 ]));
246
247 if (allow_backup)
248 keep.checked = true;
249 else
250 keep.disabled = true;
251
252
253 if ((!is_valid || is_too_big) && is_forceable)
254 body.push(E('p', {}, E('label', { 'class': 'btn alert-message danger' }, [
255 force, ' ', _('Force upgrade'),
256 E('br'), E('br'),
257 _('Select \'Force upgrade\' to flash the image even if the image format check fails. Use only if you are sure that the firmware is correct and meant for your device!')
258 ])));
259
260 var cntbtn = E('button', {
261 'class': 'btn cbi-button-action important',
262 'click': ui.createHandlerFn(this, 'handleSysupgradeConfirm', btn, keep, force),
263 'disabled': (!is_valid || is_too_big) ? true : null
264 }, [ _('Continue') ]);
265
266 body.push(E('div', { 'class': 'right' }, [
267 E('button', {
268 'class': 'btn',
269 'click': ui.createHandlerFn(this, function(ev) {
270 return fs.remove('/tmp/firmware.bin').finally(ui.hideModal);
271 })
272 }, [ _('Cancel') ]), ' ', cntbtn
273 ]));
274
275 force.addEventListener('change', function(ev) {
276 cntbtn.disabled = !ev.target.checked;
277 });
278
279 ui.showModal(_('Flash image?'), body);
280 }, this, ev.target))
281 .catch(function(e) { ui.addNotification(null, E('p', e.message)) })
282 .finally(L.bind(function(btn) {
283 btn.firstChild.data = _('Flash image...');
284 }, this, ev.target));
285 },
286
287 handleSysupgradeConfirm: function(btn, keep, force, ev) {
288 btn.firstChild.data = _('Flashingā€¦');
289
290 ui.showModal(_('Flashingā€¦'), [
291 E('p', { 'class': 'spinning' }, _('The system is flashing now.<br /> DO NOT POWER OFF THE DEVICE!<br /> Wait a few minutes before you try to reconnect. It might be necessary to renew the address of your computer to reach the device again, depending on your settings.'))
292 ]);
293
294 var opts = [];
295
296 if (!keep.checked)
297 opts.push('-n');
298
299 if (force.checked)
300 opts.push('--force');
301
302 opts.push('/tmp/firmware.bin');
303
304 /* Currently the sysupgrade rpc call will not return, hence no promise handling */
305 fs.exec('/sbin/sysupgrade', opts);
306
307 if (keep.checked)
308 ui.awaitReconnect(window.location.host);
309 else
310 ui.awaitReconnect('192.168.1.1', 'openwrt.lan');
311 },
312
313 handleBackupList: function(ev) {
314 return fs.exec('/sbin/sysupgrade', [ '--list-backup' ]).then(function(res) {
315 if (res.code != 0) {
316 ui.addNotification(null, [
317 E('p', _('The sysupgrade command failed with code %d').format(res.code)),
318 res.stderr ? E('pre', {}, [ res.stderr ]) : ''
319 ]);
320 L.raise('Error', 'Sysupgrade failed');
321 }
322
323 ui.showModal(_('Backup file list'), [
324 E('p', _('Below is the determined list of files to backup. It consists of changed configuration files marked by opkg, essential base files and the user defined backup patterns.')),
325 E('ul', {}, (res.stdout || '').trim().split(/\n/).map(function(ln) { return E('li', {}, ln) })),
326 E('div', { 'class': 'right' }, [
327 E('button', {
328 'class': 'btn',
329 'click': ui.hideModal
330 }, [ _('Dismiss') ])
331 ])
332 ], 'cbi-modal');
333 });
334 },
335
336 handleBackupSave: function(m, ev) {
337 return m.save(function() {
338 return fs.write('/etc/sysupgrade.conf', mapdata.config.editlist.trim().replace(/\r\n/g, '\n') + '\n');
339 }).then(function() {
340 ui.addNotification(null, E('p', _('Contents have been saved.')), 'info');
341 }).catch(function(e) {
342 ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e)));
343 });
344 },
345
346 render: function(rpc_replies) {
347 var has_sysupgrade = (rpc_replies[0].type == 'file'),
348 hostname = rpc_replies[1],
349 procmtd = rpc_replies[2],
350 procpart = rpc_replies[3],
351 procmounts = rpc_replies[4],
352 has_rootfs_data = (procmtd.match(/"rootfs_data"/) != null) || (procmounts.match("overlayfs:\/overlay \/ ") != null),
353 storage_size = findStorageSize(procmtd, procpart),
354 m, s, o, ss;
355
356 m = new form.JSONMap(mapdata, _('Flash operations'));
357 m.tabbed = true;
358 m.readonly = isReadonlyView;
359
360 s = m.section(form.NamedSection, 'actions', _('Actions'));
361
362
363 o = s.option(form.SectionValue, 'actions', form.NamedSection, 'actions', 'actions', _('Backup'), _('Click "Generate archive" to download a tar archive of the current configuration files.'));
364 ss = o.subsection;
365
366 o = ss.option(form.Button, 'dl_backup', _('Download backup'));
367 o.inputstyle = 'action important';
368 o.inputtitle = _('Generate archive');
369 o.onclick = this.handleBackup;
370
371
372 o = s.option(form.SectionValue, 'actions', form.NamedSection, 'actions', 'actions', _('Restore'), _('To restore configuration files, you can upload a previously generated backup archive here. To reset the firmware to its initial state, click "Perform reset" (only possible with squashfs images).'));
373 ss = o.subsection;
374
375 if (has_rootfs_data) {
376 o = ss.option(form.Button, 'reset', _('Reset to defaults'));
377 o.inputstyle = 'negative important';
378 o.inputtitle = _('Perform reset');
379 o.onclick = this.handleFirstboot;
380 }
381
382 o = ss.option(form.Button, 'restore', _('Restore backup'), _('Custom files (certificates, scripts) may remain on the system. To prevent this, perform a factory-reset first.'));
383 o.inputstyle = 'action important';
384 o.inputtitle = _('Upload archive...');
385 o.onclick = L.bind(this.handleRestore, this);
386
387
388 if (procmtd.length) {
389 o = s.option(form.SectionValue, 'actions', form.NamedSection, 'actions', 'actions', _('Save mtdblock contents'), _('Click "Save mtdblock" to download specified mtdblock file. (NOTE: THIS FEATURE IS FOR PROFESSIONALS! )'));
390 ss = o.subsection;
391
392 o = ss.option(form.ListValue, 'mtdselect', _('Choose mtdblock'));
393 procmtd.split(/\n/).forEach(function(ln) {
394 var match = ln.match(/^mtd(\d+): .+ "(.+?)"$/);
395 if (match)
396 o.value(match[1], match[2]);
397 });
398
399 o = ss.option(form.Button, 'mtddownload', _('Download mtdblock'));
400 o.inputstyle = 'action important';
401 o.inputtitle = _('Save mtdblock');
402 o.onclick = L.bind(this.handleBlock, this, hostname);
403 }
404
405
406 o = s.option(form.SectionValue, 'actions', form.NamedSection, 'actions', 'actions', _('Flash new firmware image'),
407 has_sysupgrade
408 ? _('Upload a sysupgrade-compatible image here to replace the running firmware.')
409 : _('Sorry, there is no sysupgrade support present; a new firmware image must be flashed manually. Please refer to the wiki for device specific install instructions.'));
410
411 ss = o.subsection;
412
413 if (has_sysupgrade) {
414 o = ss.option(form.Button, 'sysupgrade', _('Image'));
415 o.inputstyle = 'action important';
416 o.inputtitle = _('Flash image...');
417 o.onclick = L.bind(this.handleSysupgrade, this, storage_size);
418 }
419
420
421 s = m.section(form.NamedSection, 'config', 'config', _('Configuration'), _('This is a list of shell glob patterns for matching files and directories to include during sysupgrade. Modified files in /etc/config/ and certain other configurations are automatically preserved.'));
422 s.render = L.bind(function(view /*, ... */) {
423 return form.NamedSection.prototype.render.apply(this, this.varargs(arguments, 1))
424 .then(L.bind(function(node) {
425 node.appendChild(E('div', { 'class': 'cbi-page-actions' }, [
426 E('button', {
427 'class': 'cbi-button cbi-button-save',
428 'click': ui.createHandlerFn(view, 'handleBackupSave', this.map),
429 'disabled': isReadonlyView || null
430 }, [ _('Save') ])
431 ]));
432
433 return node;
434 }, this));
435 }, s, this);
436
437 o = s.option(form.Button, 'showlist', _('Show current backup file list'));
438 o.inputstyle = 'action';
439 o.inputtitle = _('Open list...');
440 o.onclick = L.bind(this.handleBackupList, this);
441
442 o = s.option(form.TextValue, 'editlist');
443 o.forcewrite = true;
444 o.rows = 30;
445 o.load = function(section_id) {
446 return L.resolveDefault(fs.read('/etc/sysupgrade.conf'), '');
447 };
448
449
450 return m.render();
451 },
452
453 handleSaveApply: null,
454 handleSave: null,
455 handleReset: null
456 });