overview.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643
  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. 'require fs';
  11. let callPackagelist = rpc.declare({
  12. object: 'rpc-sys',
  13. method: 'packagelist',
  14. });
  15. let callSystemBoard = rpc.declare({
  16. object: 'system',
  17. method: 'board',
  18. });
  19. let callUpgradeStart = rpc.declare({
  20. object: 'rpc-sys',
  21. method: 'upgrade_start',
  22. params: ['keep'],
  23. });
  24. /**
  25. * Returns the branch of a given version. This helps to offer upgrades
  26. * for point releases (aka within the branch).
  27. *
  28. * Logic:
  29. * SNAPSHOT -> SNAPSHOT
  30. * 21.02-SNAPSHOT -> 21.02
  31. * 21.02.0-rc1 -> 21.02
  32. * 19.07.8 -> 19.07
  33. *
  34. * @param {string} version
  35. * Input version from which to determine the branch
  36. * @returns {string}
  37. * The determined branch
  38. */
  39. function get_branch(version) {
  40. return version.replace('-SNAPSHOT', '').split('.').slice(0, 2).join('.');
  41. }
  42. /**
  43. * The OpenWrt revision string contains both a hash as well as the number
  44. * commits since the OpenWrt/LEDE reboot. It helps to determine if a
  45. * snapshot is newer than another.
  46. *
  47. * @param {string} revision
  48. * Revision string of a OpenWrt device
  49. * @returns {integer}
  50. * The number of commits since OpenWrt/LEDE reboot
  51. */
  52. function get_revision_count(revision) {
  53. return parseInt(revision.substring(1).split('-')[0]);
  54. }
  55. return view.extend({
  56. steps: {
  57. init: [ 0, _('Received build request')],
  58. container_setup: [ 10, _('Setting up ImageBuilder')],
  59. validate_revision: [ 20, _('Validating revision')],
  60. validate_manifest: [ 30, _('Validating package selection')],
  61. calculate_packages_hash: [ 40, _('Calculating package hash')],
  62. building_image: [ 50, _('Generating firmware image')],
  63. signing_images: [ 95, _('Signing images')],
  64. done: [100, _('Completed generating firmware image')],
  65. failed: [100, _('Failed to generate firmware image')],
  66. /* Obsolete status values, retained for backward compatibility. */
  67. download_imagebuilder: [ 20, _('Downloading ImageBuilder archive')],
  68. unpack_imagebuilder: [ 40, _('Setting Up ImageBuilder')],
  69. },
  70. request_hash: '',
  71. sha256_unsigned: '',
  72. selectImage: function (images, data, firmware) {
  73. var filesystemFilter = function(e) {
  74. return (e.filesystem == firmware.filesystem);
  75. }
  76. var typeFilter = function(e) {
  77. if (firmware.target.indexOf("x86") != -1) {
  78. // x86 images can be combined-efi (EFI) or combined (BIOS)
  79. if (data.efi) {
  80. return (e.type == 'combined-efi');
  81. } else {
  82. return (e.type == 'combined');
  83. }
  84. } else {
  85. return (e.type == 'sysupgrade' || e.type == 'combined');
  86. }
  87. }
  88. return images.filter(filesystemFilter).filter(typeFilter)[0];
  89. },
  90. handle200: function (response, content, data, firmware) {
  91. response = response.json();
  92. let image = this.selectImage(response.images, data, firmware);
  93. if (image.name != undefined) {
  94. this.sha256_unsigned = image.sha256_unsigned;
  95. let sysupgrade_url = `${data.url}/store/${response.bin_dir}/${image.name}`;
  96. let keep = E('input', { type: 'checkbox' });
  97. keep.checked = true;
  98. let fields = [
  99. _('Version'),
  100. `${response.version_number} ${response.version_code}`,
  101. _('SHA256'),
  102. image.sha256,
  103. ];
  104. if (data.advanced_mode == 1) {
  105. fields.push(
  106. _('Profile'),
  107. response.id,
  108. _('Target'),
  109. response.target,
  110. _('Build Date'),
  111. response.build_at,
  112. _('Filename'),
  113. image.name,
  114. _('Filesystem'),
  115. image.filesystem
  116. );
  117. }
  118. fields.push(
  119. '',
  120. E('a', { href: sysupgrade_url }, _('Download firmware image'))
  121. );
  122. if (data.rebuilder) {
  123. fields.push(_('Rebuilds'), E('div', { id: 'rebuilder_status' }));
  124. }
  125. let table = E('div', { class: 'table' });
  126. for (let i = 0; i < fields.length; i += 2) {
  127. table.appendChild(
  128. E('tr', { class: 'tr' }, [
  129. E('td', { class: 'td left', width: '33%' }, [fields[i]]),
  130. E('td', { class: 'td left' }, [fields[i + 1]]),
  131. ])
  132. );
  133. }
  134. let modal_body = [
  135. table,
  136. E(
  137. 'p',
  138. { class: 'mt-2' },
  139. E('label', { class: 'btn' }, [
  140. keep,
  141. ' ',
  142. _('Keep settings and retain the current configuration'),
  143. ])
  144. ),
  145. E('div', { class: 'right' }, [
  146. E('div', { class: 'btn', click: ui.hideModal }, _('Cancel')),
  147. ' ',
  148. E(
  149. 'button',
  150. {
  151. class: 'btn cbi-button cbi-button-positive important',
  152. click: ui.createHandlerFn(this, function () {
  153. this.handleInstall(sysupgrade_url, keep.checked, image.sha256);
  154. }),
  155. },
  156. _('Install firmware image')
  157. ),
  158. ]),
  159. ];
  160. ui.showModal(_('Successfully created firmware image'), modal_body);
  161. if (data.rebuilder) {
  162. this.handleRebuilder(content, data, firmware);
  163. }
  164. }
  165. },
  166. handle202: function (response) {
  167. response = response.json();
  168. this.request_hash = response.request_hash;
  169. if ('queue_position' in response) {
  170. ui.showModal(_('Queued...'), [
  171. E(
  172. 'p',
  173. { class: 'spinning' },
  174. _('Request in build queue position %s').format(
  175. response.queue_position
  176. )
  177. ),
  178. ]);
  179. } else {
  180. ui.showModal(_('Building Firmware...'), [
  181. E(
  182. 'p',
  183. { class: 'spinning' },
  184. _('Progress: %s%% %s').format(
  185. this.steps[response.imagebuilder_status][0],
  186. this.steps[response.imagebuilder_status][1]
  187. )
  188. ),
  189. ]);
  190. }
  191. },
  192. handleError: function (response, data, firmware) {
  193. response = response.json();
  194. const request_data = {
  195. ...data,
  196. request_hash: this.request_hash,
  197. sha256_unsigned: this.sha256_unsigned,
  198. ...firmware
  199. };
  200. let body = [
  201. E('p', {}, _('Server response: %s').format(response.detail)),
  202. E(
  203. 'a',
  204. { href: 'https://github.com/openwrt/asu/issues' },
  205. _('Please report the error message and request')
  206. ),
  207. E('p', {}, _('Request Data:')),
  208. E('pre', {}, JSON.stringify({ ...request_data }, null, 4)),
  209. ];
  210. if (response.stdout) {
  211. body.push(E('b', {}, 'STDOUT:'));
  212. body.push(E('pre', {}, response.stdout));
  213. }
  214. if (response.stderr) {
  215. body.push(E('b', {}, 'STDERR:'));
  216. body.push(E('pre', {}, response.stderr));
  217. }
  218. body = body.concat([
  219. E('div', { class: 'right' }, [
  220. E('div', { class: 'btn', click: ui.hideModal }, _('Close')),
  221. ]),
  222. ]);
  223. ui.showModal(_('Error building the firmware image'), body);
  224. },
  225. handleRequest: function (server, main, content, data, firmware) {
  226. let request_url = `${server}/api/v1/build`;
  227. let method = 'POST';
  228. let local_content = content;
  229. /**
  230. * If `request_hash` is available use a GET request instead of
  231. * sending the entire object.
  232. */
  233. if (this.request_hash && main == true) {
  234. request_url += `/${this.request_hash}`;
  235. local_content = {};
  236. method = 'GET';
  237. }
  238. request
  239. .request(request_url, { method: method, content: local_content })
  240. .then((response) => {
  241. switch (response.status) {
  242. case 202:
  243. if (main) {
  244. this.handle202(response);
  245. } else {
  246. response = response.json();
  247. let view = document.getElementById(server);
  248. view.innerText = `⏳ (${
  249. this.steps[response.imagebuilder_status][0]
  250. }%) ${server}`;
  251. }
  252. break;
  253. case 200:
  254. if (main == true) {
  255. poll.remove(this.pollFn);
  256. this.handle200(response, content, data, firmware);
  257. } else {
  258. poll.remove(this.rebuilder_polls[server]);
  259. response = response.json();
  260. let view = document.getElementById(server);
  261. let image = this.selectImage(response.images, data, firmware);
  262. if (image.sha256_unsigned == this.sha256_unsigned) {
  263. view.innerText = '✅ %s'.format(server);
  264. } else {
  265. view.innerHTML = `⚠️ ${server} (<a href="${server}/store/${
  266. response.bin_dir
  267. }/${image.name}">${_('Download')}</a>)`;
  268. }
  269. }
  270. break;
  271. case 400: // bad request
  272. case 422: // bad package
  273. case 500: // build failed
  274. if (main == true) {
  275. poll.remove(this.pollFn);
  276. this.handleError(response, data, firmware);
  277. break;
  278. } else {
  279. poll.remove(this.rebuilder_polls[server]);
  280. document.getElementById(server).innerText = '🚫 %s'.format(
  281. server
  282. );
  283. }
  284. }
  285. });
  286. },
  287. handleRebuilder: function (content, data, firmware) {
  288. this.rebuilder_polls = {};
  289. for (let rebuilder of data.rebuilder) {
  290. this.rebuilder_polls[rebuilder] = L.bind(
  291. this.handleRequest,
  292. this,
  293. rebuilder,
  294. false,
  295. content,
  296. data,
  297. firmware
  298. );
  299. poll.add(this.rebuilder_polls[rebuilder], 5);
  300. document.getElementById(
  301. 'rebuilder_status'
  302. ).innerHTML += `<p id="${rebuilder}">⏳ ${rebuilder}</p>`;
  303. }
  304. poll.start();
  305. },
  306. handleInstall: function (url, keep, sha256) {
  307. ui.showModal(_('Downloading...'), [
  308. E(
  309. 'p',
  310. { class: 'spinning' },
  311. _('Downloading firmware from server to browser')
  312. ),
  313. ]);
  314. request
  315. .get(url, {
  316. headers: {
  317. 'Content-Type': 'application/x-www-form-urlencoded',
  318. },
  319. responseType: 'blob',
  320. })
  321. .then((response) => {
  322. let form_data = new FormData();
  323. form_data.append('sessionid', rpc.getSessionID());
  324. form_data.append('filename', '/tmp/firmware.bin');
  325. form_data.append('filemode', 600);
  326. form_data.append('filedata', response.blob());
  327. ui.showModal(_('Uploading...'), [
  328. E(
  329. 'p',
  330. { class: 'spinning' },
  331. _('Uploading firmware from browser to device')
  332. ),
  333. ]);
  334. request
  335. .get(`${L.env.cgi_base}/cgi-upload`, {
  336. method: 'PUT',
  337. content: form_data,
  338. })
  339. .then((response) => response.json())
  340. .then((response) => {
  341. if (response.sha256sum != sha256) {
  342. ui.showModal(_('Wrong checksum'), [
  343. E(
  344. 'p',
  345. _('Error during download of firmware. Please try again')
  346. ),
  347. E('div', { class: 'btn', click: ui.hideModal }, _('Close')),
  348. ]);
  349. } else {
  350. ui.showModal(_('Installing...'), [
  351. E(
  352. 'p',
  353. { class: 'spinning' },
  354. _('Installing the sysupgrade. Do not unpower device!')
  355. ),
  356. ]);
  357. L.resolveDefault(callUpgradeStart(keep), {}).then((response) => {
  358. if (keep) {
  359. ui.awaitReconnect(window.location.host);
  360. } else {
  361. ui.awaitReconnect('192.168.1.1', 'openwrt.lan');
  362. }
  363. });
  364. }
  365. });
  366. });
  367. },
  368. handleCheck: function (data, firmware) {
  369. this.request_hash = '';
  370. let { url, revision, advanced_mode, branch } = data;
  371. let { version, target, profile, packages } = firmware;
  372. let candidates = [];
  373. const endpoint = version.endsWith('SNAPSHOT') ? `revision/${version}/${target}` : 'overview';
  374. const request_url = `${url}/api/v1/${endpoint}`;
  375. ui.showModal(_('Searching...'), [
  376. E(
  377. 'p',
  378. { class: 'spinning' },
  379. _('Searching for an available sysupgrade of %s - %s').format(
  380. version,
  381. revision
  382. )
  383. ),
  384. ]);
  385. L.resolveDefault(request.get(request_url)).then((response) => {
  386. if (!response.ok) {
  387. ui.showModal(_('Error connecting to upgrade server'), [
  388. E(
  389. 'p',
  390. {},
  391. _('Could not reach API at "%s". Please try again later.').format(
  392. response.url
  393. )
  394. ),
  395. E('pre', {}, response.responseText),
  396. E('div', { class: 'right' }, [
  397. E('div', { class: 'btn', click: ui.hideModal }, _('Close')),
  398. ]),
  399. ]);
  400. return;
  401. }
  402. if (version.endsWith('SNAPSHOT')) {
  403. const remote_revision = response.json().revision;
  404. if (
  405. get_revision_count(revision) < get_revision_count(remote_revision)
  406. ) {
  407. candidates.push([version, remote_revision]);
  408. }
  409. } else {
  410. const latest = response.json().latest;
  411. for (let remote_version of latest) {
  412. let remote_branch = get_branch(remote_version);
  413. // already latest version installed
  414. if (version == remote_version) {
  415. break;
  416. }
  417. // skip branch upgrades outside the advanced mode
  418. if (branch != remote_branch && advanced_mode == 0) {
  419. continue;
  420. }
  421. candidates.unshift([remote_version, null]);
  422. // don't offer branches older than the current
  423. if (branch == remote_branch) {
  424. break;
  425. }
  426. }
  427. }
  428. // allow to re-install running firmware in advanced mode
  429. if (advanced_mode == 1) {
  430. candidates.unshift([version, revision]);
  431. }
  432. if (candidates.length) {
  433. let s, o;
  434. let mapdata = {
  435. request: {
  436. profile,
  437. version: candidates[0][0],
  438. packages: Object.keys(packages).sort(),
  439. },
  440. };
  441. let map = new form.JSONMap(mapdata, '');
  442. s = map.section(
  443. form.NamedSection,
  444. 'request',
  445. '',
  446. '',
  447. 'Use defaults for the safest update'
  448. );
  449. o = s.option(form.ListValue, 'version', 'Select firmware version');
  450. for (let candidate of candidates) {
  451. if (candidate[0] == version && candidate[1] == revision) {
  452. o.value(
  453. candidate[0],
  454. _('[installed] %s').format(
  455. candidate[1]
  456. ? `${candidate[0]} - ${candidate[1]}`
  457. : candidate[0]
  458. )
  459. );
  460. } else {
  461. o.value(
  462. candidate[0],
  463. candidate[1] ? `${candidate[0]} - ${candidate[1]}` : candidate[0]
  464. );
  465. }
  466. }
  467. if (advanced_mode == 1) {
  468. o = s.option(form.Value, 'profile', _('Board Name / Profile'));
  469. o = s.option(form.DynamicList, 'packages', _('Packages'));
  470. }
  471. L.resolveDefault(map.render()).then((form_rendered) => {
  472. ui.showModal(_('New firmware upgrade available'), [
  473. E(
  474. 'p',
  475. _('Currently running: %s - %s').format(
  476. version,
  477. revision
  478. )
  479. ),
  480. form_rendered,
  481. E('div', { class: 'right' }, [
  482. E('div', { class: 'btn', click: ui.hideModal }, _('Cancel')),
  483. ' ',
  484. E(
  485. 'button',
  486. {
  487. class: 'btn cbi-button cbi-button-positive important',
  488. click: ui.createHandlerFn(this, function () {
  489. map.save().then(() => {
  490. const content = {
  491. ...firmware,
  492. packages: mapdata.request.packages,
  493. version: mapdata.request.version,
  494. profile: mapdata.request.profile
  495. };
  496. this.pollFn = L.bind(function () {
  497. this.handleRequest(url, true, content, data, firmware);
  498. }, this);
  499. poll.add(this.pollFn, 5);
  500. poll.start();
  501. });
  502. }),
  503. },
  504. _('Request firmware image')
  505. ),
  506. ]),
  507. ]);
  508. });
  509. } else {
  510. ui.showModal(_('No upgrade available'), [
  511. E(
  512. 'p',
  513. _('The device runs the latest firmware version %s - %s').format(
  514. version,
  515. revision
  516. )
  517. ),
  518. E('div', { class: 'right' }, [
  519. E('div', { class: 'btn', click: ui.hideModal }, _('Close')),
  520. ]),
  521. ]);
  522. }
  523. });
  524. },
  525. load: async function () {
  526. const promises = await Promise.all([
  527. L.resolveDefault(callPackagelist(), {}),
  528. L.resolveDefault(callSystemBoard(), {}),
  529. L.resolveDefault(fs.stat('/sys/firmware/efi'), null),
  530. uci.load('attendedsysupgrade'),
  531. ]);
  532. const data = {
  533. url: uci.get_first('attendedsysupgrade', 'server', 'url'),
  534. branch: get_branch(promises[1].release.version),
  535. revision: promises[1].release.revision,
  536. efi: promises[2],
  537. advanced_mode: uci.get_first('attendedsysupgrade', 'client', 'advanced_mode') || 0,
  538. rebuilder: uci.get_first('attendedsysupgrade', 'server', 'rebuilder')
  539. };
  540. const firmware = {
  541. client: 'luci/' + promises[0].packages['luci-app-attendedsysupgrade'],
  542. packages: promises[0].packages,
  543. profile: promises[1].board_name,
  544. target: promises[1].release.target,
  545. version: promises[1].release.version,
  546. diff_packages: true,
  547. filesystem: promises[1].rootfs_type
  548. };
  549. return [data, firmware];
  550. },
  551. render: function (response) {
  552. const data = response[0];
  553. const firmware = response[1];
  554. return E('p', [
  555. E('h2', _('Attended Sysupgrade')),
  556. E(
  557. 'p',
  558. _(
  559. 'The attended sysupgrade service allows to easily upgrade vanilla and custom firmware images.'
  560. )
  561. ),
  562. E(
  563. 'p',
  564. _(
  565. 'This is done by building a new firmware on demand via an online service.'
  566. )
  567. ),
  568. E(
  569. 'p',
  570. _('Currently running: %s - %s').format(
  571. firmware.version,
  572. data.revision
  573. )
  574. ),
  575. E(
  576. 'button',
  577. {
  578. class: 'btn cbi-button cbi-button-positive important',
  579. click: ui.createHandlerFn(this, this.handleCheck, data, firmware),
  580. },
  581. _('Search for firmware upgrade')
  582. ),
  583. ]);
  584. },
  585. handleSaveApply: null,
  586. handleSave: null,
  587. handleReset: null,
  588. });