From 1f4a38ed49511aca9606a57bf2ed9eab15d27874 Mon Sep 17 00:00:00 2001 From: Andrew Lytvynov Date: Thu, 9 Nov 2023 18:46:16 -0700 Subject: [PATCH] clientupdate: add support for QNAP (#10179) Use the `qpkg_cli` to check for updates and install them. There are a couple special things about this compare to other updaters: * qpkg_cli can tell you when upgrade is available, but not what the version is * qpkg_cli --add Tailscale works for new installs, upgrades and reinstalling existing version; even reinstall of existing version takes a while Updates #10178 Signed-off-by: Andrew Lytvynov --- clientupdate/clientupdate.go | 73 ++++++++++++++++++++++++++++++++++++ ipn/ipnlocal/c2n.go | 9 +++++ 2 files changed, 82 insertions(+) diff --git a/clientupdate/clientupdate.go b/clientupdate/clientupdate.go index 76a849bf1..78bc322da 100644 --- a/clientupdate/clientupdate.go +++ b/clientupdate/clientupdate.go @@ -180,6 +180,8 @@ func (up *Updater) getUpdateFunction() (fn updateFunction, canAutoUpdate bool) { // plugin manager to be persistent. // TODO(awly): implement Unraid updates using the 'plugin' CLI. return nil, false + case distro.QNAP: + return up.updateQNAP, true } switch { case haveExecutable("pacman"): @@ -1067,6 +1069,77 @@ func (up *Updater) unpackLinuxTarball(path string) error { return nil } +func (up *Updater) updateQNAP() (err error) { + if up.Version != "" { + return errors.New("installing a specific version on QNAP is not supported") + } + if err := requireRoot(); err != nil { + return err + } + + defer func() { + if err != nil { + err = fmt.Errorf(`%w; you can try updating using "qpkg_cli --add Tailscale"`, err) + } + }() + + out, err := exec.Command("qpkg_cli", "--upgradable", "Tailscale").CombinedOutput() + if err != nil { + return fmt.Errorf("failed to check if Tailscale is upgradable using qpkg_cli: %w, output: %q", err, out) + } + + // Output should look like this: + // + // $ qpkg_cli -G Tailscale + // [Tailscale] + // upgradeStatus = 1 + statusRe := regexp.MustCompile(`upgradeStatus = (\d)`) + m := statusRe.FindStringSubmatch(string(out)) + if len(m) < 2 { + return fmt.Errorf("failed to check if Tailscale is upgradable using qpkg_cli, output: %q", out) + } + status, err := strconv.Atoi(m[1]) + if err != nil { + return fmt.Errorf("cannot parse upgradeStatus from qpkg_cli output %q: %w", out, err) + } + // Possible status values: + // 0:can upgrade + // 1:can not upgrade + // 2:error + // 3:can not get rss information + // 4:qpkg not found + // 5:qpkg not installed + // + // We want status 0. + switch status { + case 0: // proceed with upgrade + case 1: + up.Logf("no update available") + return nil + case 2, 3, 4: + return fmt.Errorf("failed to check update status with qpkg_cli (upgradeStatus = %d)", status) + case 5: + return errors.New("Tailscale was not found in the QNAP App Center") + default: + return fmt.Errorf("failed to check update status with qpkg_cli (upgradeStatus = %d)", status) + } + + // There doesn't seem to be a way to fetch what the available upgrade + // version is. Use the generic "latest" version in confirmation prompt. + if up.Confirm != nil && !up.Confirm("latest") { + return nil + } + + up.Logf("c2n: running qpkg_cli --add Tailscale") + cmd := exec.Command("qpkg_cli", "--add", "Tailscale") + cmd.Stdout = up.Stdout + cmd.Stderr = up.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed tailscale update using qpkg_cli: %w", err) + } + return nil +} + func writeFile(r io.Reader, path string, perm os.FileMode) error { if err := os.Remove(path); err != nil && !os.IsNotExist(err) { return fmt.Errorf("failed to remove existing file at %q: %w", path, err) diff --git a/ipn/ipnlocal/c2n.go b/ipn/ipnlocal/c2n.go index 52184dc60..04c33c705 100644 --- a/ipn/ipnlocal/c2n.go +++ b/ipn/ipnlocal/c2n.go @@ -31,6 +31,7 @@ import ( "tailscale.com/util/httpm" "tailscale.com/util/syspolicy" "tailscale.com/version" + "tailscale.com/version/distro" ) var c2nLogHeap func(http.ResponseWriter, *http.Request) // non-nil on most platforms (c2n_pprof.go) @@ -341,6 +342,14 @@ func findCmdTailscale() (string, error) { if self == "/usr/local/sbin/tailscaled" || self == "/usr/local/bin/tailscaled" { ts = "/usr/local/bin/tailscale" } + if distro.Get() == distro.QNAP { + // The volume under /share/ where qpkg are installed is not + // predictable. But the rest of the path is. + ok, err := filepath.Match("/share/*/.qpkg/Tailscale/tailscaled", self) + if err == nil && ok { + ts = filepath.Join(filepath.Dir(self), "tailscale") + } + } case "windows": ts = filepath.Join(filepath.Dir(self), "tailscale.exe") case "freebsd":