luci-app-attendedsysupgrade: support revision checks
authorPaul Spooren <mail@aparcar.org>
Sun, 29 Aug 2021 10:37:31 +0000 (00:37 -1000)
committerPaul Spooren <mail@aparcar.org>
Mon, 30 Aug 2021 20:20:12 +0000 (10:20 -1000)
SNAPSHOTS are not real releases and therefore the app always offers an
upgrade, even if running the latest build. To prevent that all SNAPSHOTS
now check for the running revision and if a newer one is available.

Also do a bunch of refactoring based on JavaScript I learned over the
last week.

Signed-off-by: Paul Spooren <mail@aparcar.org>
applications/luci-app-attendedsysupgrade/htdocs/luci-static/resources/view/attendedsysupgrade/overview.js

index fc179212b1c470bcac97b9a0311837ee52bcd518..b148e793b276ec1253930dd995ba78f21f4ea73a 100644 (file)
 'require dom';
 
 var callPackagelist = rpc.declare({
-       object: 'rpc-sys',
-       method: 'packagelist',
+        object: 'rpc-sys',
+        method: 'packagelist',
 });
 
 var callSystemBoard = rpc.declare({
-       object: 'system',
-       method: 'board'
+        object: 'system',
+        method: 'board'
 });
 
 var callUpgradeStart = rpc.declare({
-       object: 'rpc-sys',
-       method: 'upgrade_start',
-       params: ["keep"]
+        object: 'rpc-sys',
+        method: 'upgrade_start',
+        params: ["keep"]
 });
 
 function get_branch(version) {
-       // determine branch of a version
-       // SNAPSHOT -> SNAPSHOT
-       // 21.02-SNAPSHOT -> 21.02
-       // 21.02.0-rc1 -> 21.02
-       // 19.07.8 -> 19.07
-       return version.replace("-SNAPSHOT", "").split(".").slice(0, 2).join(".");
+        // determine branch of a version
+        // SNAPSHOT -> SNAPSHOT
+        // 21.02-SNAPSHOT -> 21.02
+        // 21.02.0-rc1 -> 21.02
+        // 19.07.8 -> 19.07
+        return version.replace("-SNAPSHOT", "").split(".").slice(0, 2).join(".");
+}
+
+function get_revision_count(revision) {
+        return parseInt(revision.substring(1).split("-")[0])
+}
+
+function error_api_connect(response) {
+        console.log(response)
+        ui.showModal(_('Error connecting to upgrade server'), [
+                E('p', {}, _(`Could not reach API at "${response.url}. Please try again later.`)),
+                E('pre', {}, response.responseText),
+                E('div', {
+                        'class': 'right'
+                }, [
+                        E('div', {
+                                'class': 'btn',
+                                'click': ui.hideModal
+                        }, _('Close'))
+                ])
+        ]);
 }
 
 function install_sysupgrade(url, keep, sha256) {
-       displayStatus("notice spinning", E('p', _('Downloading firmware from server to browser')));
-       request.get(url, {
-                       headers: {
-                               'Content-Type': 'application/x-www-form-urlencoded'
-                       },
-                       responseType: 'blob'
-               })
-               .then(response => {
-                       var form_data = new FormData();
-                       form_data.append("sessionid", rpc.getSessionID());
-                       form_data.append("filename", "/tmp/firmware.bin");
-                       form_data.append("filemode", 600);
-                       form_data.append("filedata", response.blob());
-
-                       displayStatus("notice spinning", E('p', _('Uploading firmware from browser to device')));
-                       request.get(L.env.cgi_base + "/cgi-upload", {
-                                       method: 'PUT',
-                                       content: form_data
-                               })
-                               .then(response => response.json())
-                               .then(response => {
-                                       if (response.sha256sum != sha256) {
-                                               displayStatus("warning", [
-                                                       E('b', _('Wrong checksum')),
-                                                       E('p', _('Error during download of firmware. Please try again')),
-                                                       E('div', {
-                                                               'class': 'btn',
-                                                               'click': ui.hideModal
-                                                       }, _('Close'))
-                                               ]);
-                                       } else {
-                                               displayStatus('warning spinning', E('p', _('Installing the sysupgrade. Do not unpower device!')));
-                                               L.resolveDefault(callUpgradeStart(keep), {}).then(response => {
-                                                       if (keep) {
-                                                               ui.awaitReconnect(window.location.host);
-                                                       } else {
-                                                               ui.awaitReconnect('192.168.1.1', 'openwrt.lan');
-                                                       }
-                                               });
-                                       }
-                               });
-               });
+        displayStatus("notice spinning", E('p', _('Downloading firmware from server to browser')));
+        request.get(url, {
+                        headers: {
+                                'Content-Type': 'application/x-www-form-urlencoded'
+                        },
+                        responseType: 'blob'
+                })
+                .then(response => {
+                        var form_data = new FormData();
+                        form_data.append("sessionid", rpc.getSessionID());
+                        form_data.append("filename", "/tmp/firmware.bin");
+                        form_data.append("filemode", 600);
+                        form_data.append("filedata", response.blob());
+
+                        displayStatus("notice spinning", E('p', _('Uploading firmware from browser to device')));
+                        request.get(`${L.env.cgi_base}/cgi-upload`, {
+                                        method: 'PUT',
+                                        content: form_data
+                                })
+                                .then(response => response.json())
+                                .then(response => {
+                                        if (response.sha256sum != sha256) {
+                                                displayStatus("warning", [
+                                                        E('b', _('Wrong checksum')),
+                                                        E('p', _('Error during download of firmware. Please try again')),
+                                                        E('div', {
+                                                                'class': 'btn',
+                                                                'click': ui.hideModal
+                                                        }, _('Close'))
+                                                ]);
+                                        } else {
+                                                displayStatus('warning spinning', E('p', _('Installing the sysupgrade. Do not unpower device!')));
+                                                L.resolveDefault(callUpgradeStart(keep), {}).then(response => {
+                                                        if (keep) {
+                                                                ui.awaitReconnect(window.location.host);
+                                                        } else {
+                                                                ui.awaitReconnect('192.168.1.1', 'openwrt.lan');
+                                                        }
+                                                });
+                                        }
+                                });
+                });
 }
 
 function request_sysupgrade(server_url, data) {
-       var res, req;
-
-       if (data.request_hash) {
-               req = request.get(server_url + "/api/build/" + data.request_hash)
-       } else {
-               req = request.post(server_url + "/api/build", {
-                       profile: data.board_name,
-                       target: data.target,
-                       version: data.version,
-                       packages: data.packages,
-                       diff_packages: true,
-               })
-       }
-
-       req.then(response => {
-               switch (response.status) {
-                       case 200:
-                               var res = response.json()
-                               var image;
-                               for (image of res.images) {
-                                       if (image.type == "sysupgrade") {
-                                               break;
-                                       }
-                               }
-                               if (image.name != undefined) {
-                                       var sysupgrade_url = server_url + "/store/" + res.bin_dir + "/" + image.name;
-
-                                       var keep = E('input', {
-                                               type: 'checkbox'
-                                       })
-                                       keep.checked = true;
-
-                                       var fields = [
-                                               _('Version'), res.version_number + ' ' + res.version_code,
-                                               _('File'), E('a', {
-                                                       'href': sysupgrade_url
-                                               }, image.name),
-                                               _('SHA256'), image.sha256,
-                                               _('Build Date'), res.build_at,
-                                               _('Target'), res.target,
-                                       ];
-
-                                       var table = E('div', {
-                                               'class': 'table'
-                                       });
-
-                                       for (var i = 0; i < fields.length; i += 2) {
-                                               table.appendChild(E('div', {
-                                                       'class': 'tr'
-                                               }, [
-                                                       E('div', {
-                                                               'class': 'td left',
-                                                               'width': '33%'
-                                                       }, [fields[i]]),
-                                                       E('div', {
-                                                               'class': 'td left'
-                                                       }, [(fields[i + 1] != null) ? fields[i + 1] : '?'])
-                                               ]));
-                                       }
-
-                                       var modal_body = [
-                                               table,
-                                               E('p', {}, E('label', {
-                                                       'class': 'btn'
-                                               }, [
-                                                       keep, ' ', _('Keep settings and retain the current configuration')
-                                               ])),
-                                               E('div', {
-                                                       'class': 'right'
-                                               }, [
-                                                       E('div', {
-                                                               'class': 'btn',
-                                                               'click': ui.hideModal
-                                                       }, _('Cancel')),
-                                                       ' ',
-                                                       E('div', {
-                                                               'class': 'btn cbi-button-action',
-                                                               'click': function() {
-                                                                       install_sysupgrade(sysupgrade_url, keep.checked, image.sha256)
-                                                               }
-                                                       }, _('Install Sysupgrade'))
-                                               ])
-                                       ]
-
-                                       ui.showModal(_('Successfully created sysupgrade image'), modal_body);
-                               }
-
-                               break;
-                       case 202:
-                               res = response.json()
-                               data.request_hash = res.request_hash;
-
-                               if ("queue_position" in res)
-                                       displayStatus("notice spinning", E('p', _('Request in build queue position %d'.format(res.queue_position))));
-                               else
-                                       displayStatus("notice spinning", E('p', _('Building firmware sysupgrade image')));
-
-                               setTimeout(function() {
-                                       request_sysupgrade(server_url, data);
-                               }, 5000);
-                               break;
-                       case 400: // bad request
-                       case 422: // bad package
-                       case 500: // build failed
-                               res = response.json()
-                               var body = [
-                                       E('p', {}, res.detail),
-                                       E('p', {}, _("Please report the error message and request")),
-                                       E('b', {}, _("Request to server:")),
-                                       E('pre', {}, JSON.stringify(data, null, 4)),
-
-                               ]
-
-                               if (res.stdout) {
-                                       body.push(E('b', {}, "STDOUT:"))
-                                       body.push(E('pre', {}, res.stdout))
-
-                               }
-
-                               if (res.stderr) {
-                                       body.push(E('b', {}, "STDERR:"))
-                                       body.push(E('pre', {}, res.stderr))
-
-                               }
-
-                               body = body.concat([
-                                       E('div', {
-                                               'class': 'right'
-                                       }, [
-                                               E('div', {
-                                                       'class': 'btn',
-                                                       'click': ui.hideModal
-                                               }, _('Close'))
-                                       ])
-                               ]);
-                               ui.showModal(_('Error building the sysupgrade'), body);
-                               break;
-               }
-       });
+        var res, req;
+
+        if (data.request_hash) {
+                req = request.get(`${server_url}/api/v1/build/${data.request_hash}`)
+        } else {
+                req = request.post(`${server_url}/api/v1/build`, {
+                        profile: data.board_name,
+                        target: data.target,
+                        version: data.version,
+                        packages: data.packages,
+                        diff_packages: true,
+                })
+        }
+
+        req.then(response => {
+                switch (response.status) {
+                        case 200:
+                                res = response.json()
+                                var image;
+                                for (image of res.images) {
+                                        if (image.type == "sysupgrade") {
+                                                break;
+                                        }
+                                }
+                                if (image.name != undefined) {
+                                        var sysupgrade_url = `${server_url}/store/${res.bin_dir}/${image.name}`;
+
+                                        var keep = E('input', {
+                                                type: 'checkbox'
+                                        })
+                                        keep.checked = true;
+
+                                        var fields = [
+                                                _('Version'), `${res.version_number} ${res.version_code}`,
+                                                _('File'), E('a', {
+                                                        'href': sysupgrade_url
+                                                }, image.name),
+                                                _('SHA256'), image.sha256,
+                                                _('Build Date'), res.build_at,
+                                                _('Target'), res.target,
+                                        ];
+
+                                        var table = E('div', {
+                                                'class': 'table'
+                                        });
+
+                                        for (var i = 0; i < fields.length; i += 2) {
+                                                table.appendChild(E('div', {
+                                                        'class': 'tr'
+                                                }, [
+                                                        E('div', {
+                                                                'class': 'td left',
+                                                                'width': '33%'
+                                                        }, [fields[i]]),
+                                                        E('div', {
+                                                                'class': 'td left'
+                                                        }, [(fields[i + 1] != null) ? fields[i + 1] : '?'])
+                                                ]));
+                                        }
+
+                                        var modal_body = [
+                                                table,
+                                                E('p', {}, E('label', {
+                                                        'class': 'btn'
+                                                }, [
+                                                        keep, ' ', _('Keep settings and retain the current configuration')
+                                                ])),
+                                                E('div', {
+                                                        'class': 'right'
+                                                }, [
+                                                        E('div', {
+                                                                'class': 'btn',
+                                                                'click': ui.hideModal
+                                                        }, _('Cancel')),
+                                                        ' ',
+                                                        E('div', {
+                                                                'class': 'btn cbi-button-action',
+                                                                'click': function() {
+                                                                        install_sysupgrade(sysupgrade_url, keep.checked, image.sha256)
+                                                                }
+                                                        }, _('Install Sysupgrade'))
+                                                ])
+                                        ]
+
+                                        ui.showModal(_('Successfully created sysupgrade image'), modal_body);
+                                }
+
+                                break;
+                        case 202:
+                                res = response.json()
+                                data.request_hash = res.request_hash;
+
+                                if ("queue_position" in res)
+                                        displayStatus("notice spinning", E('p', _('Request in build queue position %d'.format(res.queue_position))));
+                                else
+                                        displayStatus("notice spinning", E('p', _('Building firmware sysupgrade image')));
+
+                                setTimeout(function() {
+                                        request_sysupgrade(server_url, data);
+                                }, 5000);
+                                break;
+                        case 400: // bad request
+                        case 422: // bad package
+                        case 500: // build failed
+                                res = response.json()
+                                var body = [
+                                        E('p', {}, res.detail),
+                                        E('p', {}, _("Please report the error message and request")),
+                                        E('b', {}, _("Request to server:")),
+                                        E('pre', {}, JSON.stringify(data, null, 4)),
+
+                                ]
+
+                                if (res.stdout) {
+                                        body.push(E('b', {}, "STDOUT:"))
+                                        body.push(E('pre', {}, res.stdout))
+
+                                }
+
+                                if (res.stderr) {
+                                        body.push(E('b', {}, "STDERR:"))
+                                        body.push(E('pre', {}, res.stderr))
+
+                                }
+
+                                body = body.concat([
+                                        E('div', {
+                                                'class': 'right'
+                                        }, [
+                                                E('div', {
+                                                        'class': 'btn',
+                                                        'click': ui.hideModal
+                                                }, _('Close'))
+                                        ])
+                                ]);
+                                ui.showModal(_('Error building the sysupgrade'), body);
+                                break;
+                }
+        });
 }
 
-function check_sysupgrade(server_url, current_version, target, board_name, packages) {
-       displayStatus("notice spinning", E('p', _('Searching for an available sysupgrade')));
-       var current_branch = get_branch(current_version);
-       var advanced_mode = uci.get_first('attendedsysupgrade', 'client', 'advanced_mode') || 0;
-       var candidates = [];
-
-       request.get(server_url + "/json/latest.json", {
-                       timeout: 8000
-               })
-               .then(response => response.json())
-               .then(response => {
-                       if (current_version == "SNAPSHOT") {
-                               candidates.push("SNAPSHOT");
-                       } else {
-                               for (let version of response["latest"]) {
-                                       var branch = get_branch(version);
-
-                                       // already latest version installed
-                                       if (current_version == version) {
-                                               break;
-                                       }
-
-                                       // skip branch upgrades outside the advanced mode
-                                       if (current_branch != branch && advanced_mode == 0) {
-                                               continue;
-                                       }
-
-                                       candidates.unshift(version);
-
-                                       // don't offer branches older than the current
-                                       if (current_branch == branch) {
-                                               break;
-                                       }
-                               }
-                       }
-                       if (candidates.length) {
-                               var m, s, o;
-
-                               var mapdata = {
-                                       request: {
-                                               board_name: board_name,
-                                               target: target,
-                                               version: candidates[0],
-                                               packages: Object.keys(packages).sort(),
-                                       }
-                               }
-
-                               m = new form.JSONMap(mapdata, '');
-
-                               s = m.section(form.NamedSection, 'request', 'example', '',
-                                       'Use defaults for the safest update');
-                               o = s.option(form.ListValue, 'version', 'Select firmware version');
-                               for (let candidate of candidates) {
-                                       o.value(candidate, candidate);
-                               }
-
-                               if (advanced_mode == 1) {
-                                       o = s.option(form.Value, 'board_name', 'Board Name / Profile');
-                                       o = s.option(form.DynamicList, 'packages', 'Packages');
-                               }
-
-
-                               m.render()
-                                       .then(function(form_rendered) {
-                                               ui.showModal(_('New upgrade available'), [
-                                                       form_rendered,
-                                                       E('div', {
-                                                               'class': 'right'
-                                                       }, [
-                                                               E('div', {
-                                                                       'class': 'btn',
-                                                                       'click': ui.hideModal
-                                                               }, _('Cancel')),
-                                                               ' ',
-                                                               E('div', {
-                                                                       'class': 'btn cbi-button-action',
-                                                                       'click': function() {
-                                                                               m.save().then(foo => {
-                                                                                       request_sysupgrade(
-                                                                                               server_url, mapdata.request
-                                                                                       )
-                                                                               });
-                                                                       }
-                                                               }, _('Request Sysupgrade'))
-                                                       ])
-                                               ]);
-                                       });
-                       } else {
-                               ui.showModal(_('No upgrade available'), [
-                                       E('p', {}, _("The device runs the latest firmware version")),
-                                       E('div', {
-                                               'class': 'right'
-                                       }, [
-                                               E('div', {
-                                                       'class': 'btn',
-                                                       'click': ui.hideModal
-                                               }, _('Close'))
-                                       ])
-                               ]);
-                       }
-               })
-               .catch(error => {
-                       ui.showModal(_('Error connecting to upgrade server'), [
-                               E('p', {}, _('Could not reach API at "%s". Please try again later.'.format(server_url))),
-                               E('pre', {}, error),
-                               E('div', {
-                                       'class': 'right'
-                               }, [
-                                       E('div', {
-                                               'class': 'btn',
-                                               'click': ui.hideModal
-                                       }, _('Close'))
-                               ])
-                       ]);
-               });
+async function check_sysupgrade(server_url, system_board, packages) {
+        var {
+                board_name
+        } = system_board;
+        var {
+                target,
+                version,
+                revision
+        } = system_board.release;
+        var current_branch = get_branch(version);
+        var advanced_mode = uci.get_first('attendedsysupgrade', 'client', 'advanced_mode') || 0;
+        var candidates = [];
+        var response;
+
+        displayStatus("notice spinning", E('p', _(`Searching for an available sysupgrade of ${version} - ${revision}`)));
+
+        if (version.endsWith("SNAPSHOT")) {
+                response = await request.get(`${server_url}/api/v1/revision/${version}/${target}`)
+                if (!response.ok) {
+                        error_api_connect(response);
+                        return;
+                }
+
+                const remote_revision = response.json().revision;
+
+                if (get_revision_count(revision) < get_revision_count(remote_revision)) {
+                        candidates.push(version);
+                }
+        } else {
+                response = await request.get(`${server_url}/api/overview`, {
+                        timeout: 8000
+                });
+
+                if (!response.ok) {
+                        error_api_connect(response);
+                        return;
+                }
+
+                const latest = response.json().latest
+
+                for (let remote_version of latest) {
+                        var remote_branch = get_branch(remote_version);
+
+                        // already latest version installed
+                        if (version == remote_version) {
+                                break;
+                        }
+
+                        // skip branch upgrades outside the advanced mode
+                        if (current_branch != remote_branch && advanced_mode == 0) {
+                                continue;
+                        }
+
+                        candidates.unshift(remote_version);
+
+                        // don't offer branches older than the current
+                        if (current_branch == remote_branch) {
+                                break;
+                        }
+                }
+        }
+
+        if (candidates.length) {
+                var m, s, o;
+
+                var mapdata = {
+                        request: {
+                                board_name: board_name,
+                                target: target,
+                                version: candidates[0],
+                                packages: Object.keys(packages).sort(),
+                        }
+                }
+
+                m = new form.JSONMap(mapdata, '');
+
+                s = m.section(form.NamedSection, 'request', 'example', '',
+                        'Use defaults for the safest update');
+                o = s.option(form.ListValue, 'version', 'Select firmware version');
+                for (let candidate of candidates) {
+                        o.value(candidate, candidate);
+                }
+
+                if (advanced_mode == 1) {
+                        o = s.option(form.Value, 'board_name', 'Board Name / Profile');
+                        o = s.option(form.DynamicList, 'packages', 'Packages');
+                }
+
+
+                m.render()
+                        .then(function(form_rendered) {
+                                ui.showModal(_('New upgrade available'), [
+                                        form_rendered,
+                                        E('div', {
+                                                'class': 'right'
+                                        }, [
+                                                E('div', {
+                                                        'class': 'btn',
+                                                        'click': ui.hideModal
+                                                }, _('Cancel')),
+                                                ' ',
+                                                E('div', {
+                                                        'class': 'btn cbi-button-action',
+                                                        'click': function() {
+                                                                m.save().then(foo => {
+                                                                        request_sysupgrade(
+                                                                                server_url, mapdata.request
+                                                                        )
+                                                                });
+                                                        }
+                                                }, _('Request Sysupgrade'))
+                                        ])
+                                ]);
+                        });
+        } else {
+                ui.showModal(_('No upgrade available'), [
+                        E('p', {}, _(`The device runs the latest firmware version ${version} - ${revision}`)),
+                        E('div', {
+                                'class': 'right'
+                        }, [
+                                E('div', {
+                                        'class': 'btn',
+                                        'click': ui.hideModal
+                                }, _('Close'))
+                        ])
+                ]);
+        }
 }
 
 function displayStatus(type, content) {
-       if (type) {
-               var message = ui.showModal('', '');
+        if (type) {
+                var message = ui.showModal('', '');
 
-               message.classList.add('alert-message');
-               DOMTokenList.prototype.add.apply(message.classList, type.split(/\s+/));
+                message.classList.add('alert-message');
+                DOMTokenList.prototype.add.apply(message.classList, type.split(/\s+/));
 
-               if (content)
-                       dom.content(message, content);
-       } else {
-               ui.hideModal();
-       }
+                if (content)
+                        dom.content(message, content);
+        } else {
+                ui.hideModal();
+        }
 }
 
 return view.extend({
-       load: function() {
-               return Promise.all([
-                       L.resolveDefault(callPackagelist(), {}),
-                       L.resolveDefault(callSystemBoard(), {}),
-                       uci.load('attendedsysupgrade')
-               ]);
-       },
-       render: function(res) {
-               var packages = res[0].packages;
-               var current_version = res[1].release.version;
-               var target = res[1].release.target;
-               var board_name = res[1].board_name;
-               var auto_search = uci.get_first('attendedsysupgrade', 'client', 'auto_search') || 1;
-               var server_url = uci.get_first('attendedsysupgrade', 'server', 'url');
-
-               var view = [
-                       E('h2', _("Attended Sysupgrade")),
-                       E('p', _('The attended sysupgrade service allows to easily upgrade vanilla and custom firmware images.')),
-                       E('p', _('This is done by building a new firmware on demand via an online service.'))
-               ];
-
-               if (auto_search == 1) {
-                       check_sysupgrade(server_url, current_version, target, board_name, packages)
-               }
-
-               view.push(E('p', {
-                       'class': 'btn cbi-button-positive',
-                       'click': function() {
-                               check_sysupgrade(server_url, current_version, target, board_name, packages)
-                       }
-               }, _('Search for sysupgrade')));
-
-               return view;
-       },
+        load: function() {
+                return Promise.all([
+                        L.resolveDefault(callPackagelist(), {}),
+                        L.resolveDefault(callSystemBoard(), {}),
+                        uci.load('attendedsysupgrade')
+                ]);
+        },
+        render: function(res) {
+                var packages = res[0].packages;
+                var system_board = res[1];
+                var auto_search = uci.get_first('attendedsysupgrade', 'client', 'auto_search') || 1;
+                var server_url = uci.get_first('attendedsysupgrade', 'server', 'url');
+
+                var view = [
+                        E('h2', _("Attended Sysupgrade")),
+                        E('p', _('The attended sysupgrade service allows to easily upgrade vanilla and custom firmware images.')),
+                        E('p', _('This is done by building a new firmware on demand via an online service.'))
+                ];
+
+                if (auto_search == 1) {
+                        check_sysupgrade(server_url, system_board, packages)
+                }
+
+                view.push(E('p', {
+                        'class': 'btn cbi-button-positive',
+                        'click': function() {
+                                check_sysupgrade(server_url, system_board, packages)
+                        }
+                }, _('Search for sysupgrade')));
+
+                return view;
+        },
 
 });