forked from jetkvm/kvm
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtailscale.go
More file actions
118 lines (100 loc) · 3.42 KB
/
tailscale.go
File metadata and controls
118 lines (100 loc) · 3.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
package kvm
import (
"context"
"encoding/json"
"fmt"
"os/exec"
"strings"
"time"
)
const tailscaleCommandTimeout = 10 * time.Second
// TailscaleStatus represents the current state of Tailscale on the device.
type TailscaleStatus struct {
Installed bool `json:"installed"`
Running bool `json:"running"`
BackendState string `json:"backendState,omitempty"`
AuthURL string `json:"authURL,omitempty"`
Self *TailscalePeer `json:"self,omitempty"`
Health []string `json:"health,omitempty"`
}
// TailscalePeer represents a Tailscale peer (including self).
type TailscalePeer struct {
HostName string `json:"hostName"`
DNSName string `json:"dnsName"`
TailscaleIPs []string `json:"tailscaleIPs"`
Online bool `json:"online"`
OS string `json:"os"`
}
// tailscaleRawStatus represents the subset of fields we parse from `tailscale status --json`.
type tailscaleRawStatus struct {
BackendState string `json:"BackendState"`
AuthURL string `json:"AuthURL"`
Self *tailscaleRawPeer `json:"Self"`
Health []string `json:"Health"`
}
type tailscaleRawPeer struct {
HostName string `json:"HostName"`
DNSName string `json:"DNSName"`
TailscaleIPs []string `json:"TailscaleIPs"`
Online bool `json:"Online"`
OS string `json:"OS"`
}
// isTailscaleInstalled checks if the tailscale binary is available on the system.
func isTailscaleInstalled() bool {
_, err := exec.LookPath("tailscale")
return err == nil
}
// execTailscaleStatus runs `tailscale status --json` and returns the raw output.
// This is a package-level var to allow test substitution.
var execTailscaleStatus = func() ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), tailscaleCommandTimeout)
defer cancel()
output, err := exec.CommandContext(ctx, "tailscale", "status", "--json").CombinedOutput()
if err != nil {
return nil, fmt.Errorf("tailscale status: %w: %s", err, strings.TrimSpace(string(output)))
}
return output, nil
}
// getTailscaleStatus queries the Tailscale daemon for current status.
// Returns a TailscaleStatus with Installed=false when the binary is not found.
func getTailscaleStatus() (*TailscaleStatus, error) {
if !isTailscaleInstalled() {
return &TailscaleStatus{Installed: false}, nil
}
output, err := execTailscaleStatus()
if err != nil {
tailscaleLogger.Warn().Err(err).Msg("failed to get tailscale status")
return &TailscaleStatus{
Installed: true,
Running: false,
}, nil
}
return parseTailscaleStatus(output)
}
// parseTailscaleStatus parses the JSON output from `tailscale status --json`.
func parseTailscaleStatus(data []byte) (*TailscaleStatus, error) {
var raw tailscaleRawStatus
if err := json.Unmarshal(data, &raw); err != nil {
return nil, fmt.Errorf("failed to parse tailscale status: %w", err)
}
status := &TailscaleStatus{
Installed: true,
Running: raw.BackendState == "Running",
BackendState: raw.BackendState,
AuthURL: raw.AuthURL,
Health: raw.Health,
}
if raw.Self != nil {
status.Self = &TailscalePeer{
HostName: raw.Self.HostName,
DNSName: raw.Self.DNSName,
TailscaleIPs: raw.Self.TailscaleIPs,
Online: raw.Self.Online,
OS: raw.Self.OS,
}
}
return status, nil
}
func rpcGetTailscaleStatus() (*TailscaleStatus, error) {
return getTailscaleStatus()
}