b1cc4d1a6f83a6e2421e01f7e42e7572cbcc7551
[project/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: L.env.cgi_base + '/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': L.env.cgi_base + '/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 });