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