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