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 <awly@tailscale.com>
This commit is contained in:
Andrew Lytvynov 2023-11-09 18:46:16 -07:00 committed by GitHub
parent 45be37cb01
commit 1f4a38ed49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 82 additions and 0 deletions

View File

@ -180,6 +180,8 @@ func (up *Updater) getUpdateFunction() (fn updateFunction, canAutoUpdate bool) {
// plugin manager to be persistent. // plugin manager to be persistent.
// TODO(awly): implement Unraid updates using the 'plugin' CLI. // TODO(awly): implement Unraid updates using the 'plugin' CLI.
return nil, false return nil, false
case distro.QNAP:
return up.updateQNAP, true
} }
switch { switch {
case haveExecutable("pacman"): case haveExecutable("pacman"):
@ -1067,6 +1069,77 @@ func (up *Updater) unpackLinuxTarball(path string) error {
return nil 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 { func writeFile(r io.Reader, path string, perm os.FileMode) error {
if err := os.Remove(path); err != nil && !os.IsNotExist(err) { if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove existing file at %q: %w", path, err) return fmt.Errorf("failed to remove existing file at %q: %w", path, err)

View File

@ -31,6 +31,7 @@
"tailscale.com/util/httpm" "tailscale.com/util/httpm"
"tailscale.com/util/syspolicy" "tailscale.com/util/syspolicy"
"tailscale.com/version" "tailscale.com/version"
"tailscale.com/version/distro"
) )
var c2nLogHeap func(http.ResponseWriter, *http.Request) // non-nil on most platforms (c2n_pprof.go) 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" { if self == "/usr/local/sbin/tailscaled" || self == "/usr/local/bin/tailscaled" {
ts = "/usr/local/bin/tailscale" 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": case "windows":
ts = filepath.Join(filepath.Dir(self), "tailscale.exe") ts = filepath.Join(filepath.Dir(self), "tailscale.exe")
case "freebsd": case "freebsd":