scaled 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +145 -58
- data/examples/topology_graph.rb +157 -0
- data/lib/scaled/client.rb +12 -0
- data/lib/scaled/resources/acl.rb +23 -0
- data/lib/scaled/resources/device_routes.rb +23 -0
- data/lib/scaled/version.rb +1 -1
- data/sig/scaled.rbs +12 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2a020a9c01e0e1d4412044cbd43e4d839d125820ccaa2f5d2c47f21d0117b0ef
|
|
4
|
+
data.tar.gz: f1caec44fa505d4587c9cef1e18b4f337e490147c9880274bc106c31d683f2c1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6d3ed8af11e8d86bb04b59fc605522bcce575a96ff49049636953fc23c5343f45ebc0060dd0d239e3c8421f746a3112815897b3e2ce6e58087516774efe6e27d
|
|
7
|
+
data.tar.gz: 82440e425e2c3d25be7d43f5cb248f331fca4203535e9183fc3a8a18599b92df8fdaf4ea9cf810c322b23fc3fa1049d1d68267159e314872385afc78b8b0d033
|
data/README.md
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
Current scope of the gem:
|
|
8
8
|
- devices inventory (`list`, `get`)
|
|
9
|
+
- device routes (`device_routes.get`) — advertised/enabled subnet routes and exit nodes
|
|
9
10
|
- keys metadata (`list`, `get`)
|
|
10
11
|
- logs (`configuration`, `network`)
|
|
12
|
+
- policy file / ACL (`acl.get`) — `acls`/`grants`, `tagOwners`, `groups`
|
|
11
13
|
|
|
12
14
|
No create/update/delete actions are exposed in resource wrappers.
|
|
13
15
|
|
|
@@ -187,78 +189,66 @@ curl -sS \
|
|
|
187
189
|
"https://api.tailscale.com/api/v2/tailnet/-/devices"
|
|
188
190
|
```
|
|
189
191
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
```ruby
|
|
193
|
-
response = client.devices.get("device-id")
|
|
194
|
-
```
|
|
192
|
+
Example server response:
|
|
195
193
|
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
194
|
+
```json
|
|
195
|
+
{
|
|
196
|
+
"devices": [
|
|
197
|
+
{
|
|
198
|
+
"id": "n123456CNTRL",
|
|
199
|
+
"name": "macbook-pro.tailnet.ts.net",
|
|
200
|
+
"addresses": ["100.101.102.103", "fd7a:115c:a1e0::abcd:1234"],
|
|
201
|
+
"user": "user@example.com",
|
|
202
|
+
"os": "macOS",
|
|
203
|
+
"created": "2026-03-12T07:12:30Z",
|
|
204
|
+
"lastSeen": "2026-03-12T08:25:44Z",
|
|
205
|
+
"authorized": true
|
|
206
|
+
}
|
|
207
|
+
]
|
|
208
|
+
}
|
|
200
209
|
```
|
|
201
210
|
|
|
202
|
-
###
|
|
211
|
+
### Get one device
|
|
203
212
|
|
|
204
213
|
```ruby
|
|
205
|
-
response = client.
|
|
214
|
+
response = client.devices.get("device-id")
|
|
206
215
|
```
|
|
207
216
|
|
|
208
217
|
```bash
|
|
209
218
|
curl -sS \
|
|
210
219
|
-H "Authorization: Bearer ${TAILSCALE_API_TOKEN}" \
|
|
211
|
-
"https://api.tailscale.com/api/v2/
|
|
220
|
+
"https://api.tailscale.com/api/v2/device/device-id"
|
|
212
221
|
```
|
|
213
222
|
|
|
214
|
-
|
|
223
|
+
Example server response:
|
|
215
224
|
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
"
|
|
225
|
+
```json
|
|
226
|
+
{
|
|
227
|
+
"id": "n123456CNTRL",
|
|
228
|
+
"name": "macbook-pro.tailnet.ts.net",
|
|
229
|
+
"hostname": "macbook-pro",
|
|
230
|
+
"addresses": ["100.101.102.103", "fd7a:115c:a1e0::abcd:1234"],
|
|
231
|
+
"user": "user@example.com",
|
|
232
|
+
"os": "macOS",
|
|
233
|
+
"created": "2026-03-12T07:12:30Z",
|
|
234
|
+
"lastSeen": "2026-03-12T08:25:44Z",
|
|
235
|
+
"authorized": true
|
|
236
|
+
}
|
|
224
237
|
```
|
|
225
238
|
|
|
226
|
-
###
|
|
239
|
+
### List keys
|
|
227
240
|
|
|
228
241
|
```ruby
|
|
229
|
-
response = client.
|
|
242
|
+
response = client.keys.list
|
|
230
243
|
```
|
|
231
244
|
|
|
232
245
|
```bash
|
|
233
246
|
curl -sS \
|
|
234
247
|
-H "Authorization: Bearer ${TAILSCALE_API_TOKEN}" \
|
|
235
|
-
"https://api.tailscale.com/api/v2/tailnet/-/
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
## Example responses
|
|
239
|
-
|
|
240
|
-
Response shapes vary by account features and scopes. Examples:
|
|
241
|
-
|
|
242
|
-
### Devices list (`client.devices.list`)
|
|
243
|
-
|
|
244
|
-
```json
|
|
245
|
-
{
|
|
246
|
-
"devices": [
|
|
247
|
-
{
|
|
248
|
-
"id": "n123456CNTRL",
|
|
249
|
-
"name": "macbook-pro.tailnet.ts.net",
|
|
250
|
-
"addresses": ["100.101.102.103", "fd7a:115c:a1e0::abcd:1234"],
|
|
251
|
-
"user": "user@example.com",
|
|
252
|
-
"os": "macOS",
|
|
253
|
-
"created": "2026-03-12T07:12:30Z",
|
|
254
|
-
"lastSeen": "2026-03-12T08:25:44Z",
|
|
255
|
-
"authorized": true
|
|
256
|
-
}
|
|
257
|
-
]
|
|
258
|
-
}
|
|
248
|
+
"https://api.tailscale.com/api/v2/tailnet/-/keys"
|
|
259
249
|
```
|
|
260
250
|
|
|
261
|
-
|
|
251
|
+
Example server response:
|
|
262
252
|
|
|
263
253
|
```json
|
|
264
254
|
{
|
|
@@ -281,7 +271,19 @@ Response shapes vary by account features and scopes. Examples:
|
|
|
281
271
|
}
|
|
282
272
|
```
|
|
283
273
|
|
|
284
|
-
###
|
|
274
|
+
### Read configuration logs
|
|
275
|
+
|
|
276
|
+
```ruby
|
|
277
|
+
response = client.logs.configuration(query: { limit: 100 })
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
curl -sS \
|
|
282
|
+
-H "Authorization: Bearer ${TAILSCALE_API_TOKEN}" \
|
|
283
|
+
"https://api.tailscale.com/api/v2/tailnet/-/logging/configuration?limit=100"
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Example server response:
|
|
285
287
|
|
|
286
288
|
```json
|
|
287
289
|
{
|
|
@@ -299,24 +301,109 @@ Response shapes vary by account features and scopes. Examples:
|
|
|
299
301
|
}
|
|
300
302
|
```
|
|
301
303
|
|
|
302
|
-
###
|
|
304
|
+
### Read network logs
|
|
305
|
+
|
|
306
|
+
Network flow logs require an RFC 3339 `start` and `end` time window (there is no
|
|
307
|
+
`limit` parameter). Source and destination are reported as `addr:port` strings.
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
response = client.logs.network(
|
|
311
|
+
query: { start: "2026-03-12T00:00:00Z", end: "2026-03-12T23:59:59Z" }
|
|
312
|
+
)
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
curl -sS \
|
|
317
|
+
-H "Authorization: Bearer ${TAILSCALE_API_TOKEN}" \
|
|
318
|
+
"https://api.tailscale.com/api/v2/tailnet/-/logging/network?start=2026-03-12T00:00:00Z&end=2026-03-12T23:59:59Z"
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Example server response (`logs` is an array of `NetworkFlowLog` objects):
|
|
303
322
|
|
|
304
323
|
```json
|
|
305
324
|
{
|
|
306
|
-
"
|
|
325
|
+
"logs": [
|
|
307
326
|
{
|
|
308
|
-
"
|
|
309
|
-
"
|
|
310
|
-
"
|
|
311
|
-
"
|
|
312
|
-
"
|
|
313
|
-
|
|
314
|
-
|
|
327
|
+
"logged": "2026-03-12T08:15:26Z",
|
|
328
|
+
"nodeId": "n123456CNTRL",
|
|
329
|
+
"start": "2026-03-12T08:15:25Z",
|
|
330
|
+
"end": "2026-03-12T08:15:26Z",
|
|
331
|
+
"virtualTraffic": [
|
|
332
|
+
{
|
|
333
|
+
"proto": "tcp",
|
|
334
|
+
"src": "100.101.102.103:52343",
|
|
335
|
+
"dst": "100.110.120.130:443",
|
|
336
|
+
"txPkts": 10,
|
|
337
|
+
"txBytes": 1200,
|
|
338
|
+
"rxPkts": 8,
|
|
339
|
+
"rxBytes": 900
|
|
340
|
+
}
|
|
341
|
+
],
|
|
342
|
+
"subnetTraffic": [],
|
|
343
|
+
"exitTraffic": [],
|
|
344
|
+
"physicalTraffic": []
|
|
315
345
|
}
|
|
316
346
|
]
|
|
317
347
|
}
|
|
318
348
|
```
|
|
319
349
|
|
|
350
|
+
Traffic is split into `virtualTraffic` (tailnet peer-to-peer), `subnetTraffic`
|
|
351
|
+
(through subnet routers), `exitTraffic` (through exit nodes), and
|
|
352
|
+
`physicalTraffic` (underlying physical endpoints).
|
|
353
|
+
|
|
354
|
+
### Read device routes
|
|
355
|
+
|
|
356
|
+
```ruby
|
|
357
|
+
response = client.device_routes.get("device-id")
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
curl -sS \
|
|
362
|
+
-H "Authorization: Bearer ${TAILSCALE_API_TOKEN}" \
|
|
363
|
+
"https://api.tailscale.com/api/v2/device/device-id/routes"
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Example server response:
|
|
367
|
+
|
|
368
|
+
```json
|
|
369
|
+
{
|
|
370
|
+
"advertisedRoutes": ["10.0.0.0/24", "0.0.0.0/0", "::/0"],
|
|
371
|
+
"enabledRoutes": ["10.0.0.0/24"]
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Read policy file (ACL)
|
|
376
|
+
|
|
377
|
+
By default the policy file is returned as JSON. Pass `details: true` to get a
|
|
378
|
+
JSON object with a base64-encoded huJSON `acl` plus `warnings` and `errors`.
|
|
379
|
+
|
|
380
|
+
```ruby
|
|
381
|
+
policy = client.acl.get
|
|
382
|
+
detailed = client.acl.get(query: { details: true })
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
```bash
|
|
386
|
+
curl -sS \
|
|
387
|
+
-H "Authorization: Bearer ${TAILSCALE_API_TOKEN}" \
|
|
388
|
+
-H "Accept: application/json" \
|
|
389
|
+
"https://api.tailscale.com/api/v2/tailnet/-/acl"
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Example server response:
|
|
393
|
+
|
|
394
|
+
```json
|
|
395
|
+
{
|
|
396
|
+
"groups": { "group:eng": ["alice@example.com"] },
|
|
397
|
+
"tagOwners": { "tag:server": ["group:eng"] },
|
|
398
|
+
"acls": [
|
|
399
|
+
{ "action": "accept", "src": ["group:eng"], "dst": ["tag:server:443"] }
|
|
400
|
+
]
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
OAuth note: reading routes needs `devices:routes:read`; reading the ACL needs an
|
|
405
|
+
ACL read scope (`acl:read`). Add them to your OAuth scopes when using those calls.
|
|
406
|
+
|
|
320
407
|
## Integration smoke tests
|
|
321
408
|
|
|
322
409
|
Integration tests are opt-in and run only when `RUN_INTEGRATION=1`.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Build a network-topology graph of a Tailscale tailnet using the read-only
|
|
4
|
+
# Scaled client and export it as Graphviz DOT + JSON.
|
|
5
|
+
#
|
|
6
|
+
# Будує граф топології мережі tailnet за допомогою read-only клієнта Scaled
|
|
7
|
+
# та експортує його у Graphviz DOT + JSON.
|
|
8
|
+
#
|
|
9
|
+
# Endpoints used (verified against https://tailscale.com/api OpenAPI):
|
|
10
|
+
# 1. GET /tailnet/{tailnet}/devices?fields=all -> вузли + tags, routes, addresses
|
|
11
|
+
# 2. GET /tailnet/{tailnet}/logging/network -> NetworkFlowLog[] (virtualTraffic)
|
|
12
|
+
# requires RFC3339 `start` & `end` (NOT `limit`); src/dst are "ip:port".
|
|
13
|
+
# 3. GET /tailnet/{tailnet}/acl?details=true -> policy (tagOwners, groups) context
|
|
14
|
+
#
|
|
15
|
+
# Usage / Запуск:
|
|
16
|
+
# TAILSCALE_API_TOKEN=tskey-api-... TAILNET=- \
|
|
17
|
+
# LOG_START=2026-06-20T00:00:00Z LOG_END=2026-06-21T00:00:00Z \
|
|
18
|
+
# ruby examples/topology_graph.rb
|
|
19
|
+
# # -> tmp/topology.dot, tmp/topology.json
|
|
20
|
+
# # dot -Tsvg tmp/topology.dot -o tmp/topology.svg
|
|
21
|
+
#
|
|
22
|
+
# OAuth scopes needed: devices:core:read, logs:network:read (+ acl:read for ACL).
|
|
23
|
+
|
|
24
|
+
require "json"
|
|
25
|
+
require "fileutils"
|
|
26
|
+
require_relative "../lib/scaled"
|
|
27
|
+
|
|
28
|
+
module TopologyGraph
|
|
29
|
+
module_function
|
|
30
|
+
|
|
31
|
+
def build_client
|
|
32
|
+
if ENV["TAILSCALE_API_TOKEN"]
|
|
33
|
+
Scaled.client(api_token: ENV.fetch("TAILSCALE_API_TOKEN"), tailnet: ENV.fetch("TAILNET", "-"))
|
|
34
|
+
else
|
|
35
|
+
Scaled.client(
|
|
36
|
+
oauth: {
|
|
37
|
+
client_id: ENV.fetch("TAILSCALE_OAUTH_CLIENT_ID"),
|
|
38
|
+
client_secret: ENV.fetch("TAILSCALE_OAUTH_CLIENT_SECRET"),
|
|
39
|
+
scopes: %w[devices:core:read logs:network:read acl:read]
|
|
40
|
+
},
|
|
41
|
+
tailnet: ENV.fetch("TAILNET", "-")
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Vertices: one node per device, enriched with tags / routes / addresses.
|
|
47
|
+
# Also build an IP -> deviceId index to resolve flow-log endpoints.
|
|
48
|
+
# Вершини: вузол на кожен device + індекс IP -> deviceId для зіставлення логів.
|
|
49
|
+
def collect_nodes(client)
|
|
50
|
+
# `fields=all` returns tags, enabledRoutes, advertisedRoutes, addresses, clientConnectivity.
|
|
51
|
+
devices = client.devices.list(query: { fields: "all" }).fetch("devices", [])
|
|
52
|
+
ip_index = {}
|
|
53
|
+
|
|
54
|
+
nodes = devices.each_with_object({}) do |device, acc|
|
|
55
|
+
id = device.fetch("id")
|
|
56
|
+
(device["addresses"] || []).each { |addr| ip_index[addr] = id }
|
|
57
|
+
acc[id] = {
|
|
58
|
+
id: id,
|
|
59
|
+
name: device["name"] || device["hostname"],
|
|
60
|
+
user: device["user"],
|
|
61
|
+
os: device["os"],
|
|
62
|
+
addresses: device["addresses"] || [],
|
|
63
|
+
tags: device["tags"] || [], # ACL-теги вузла
|
|
64
|
+
advertised_routes: device["advertisedRoutes"] || [], # subnet routes / exit node
|
|
65
|
+
enabled_routes: device["enabledRoutes"] || []
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
[nodes, ip_index]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Edges: aggregate tailnet peer-to-peer flows (virtualTraffic) from network logs.
|
|
73
|
+
# src/dst are "ip:port"; the ip is resolved back to a device via ip_index.
|
|
74
|
+
# Ребра: агрегує tailnet-потоки (virtualTraffic) у зв'язки device -> device.
|
|
75
|
+
def collect_edges(client, ip_index)
|
|
76
|
+
logs = client.logs.network(query: { start: log_start, end: log_end }).fetch("logs", [])
|
|
77
|
+
edges = Hash.new { |hash, key| hash[key] = { pkts: 0, bytes: 0 } }
|
|
78
|
+
|
|
79
|
+
logs.each do |entry|
|
|
80
|
+
Array(entry["virtualTraffic"]).each do |flow|
|
|
81
|
+
src = ip_index[host(flow["src"])]
|
|
82
|
+
dst = ip_index[host(flow["dst"])]
|
|
83
|
+
next unless src && dst
|
|
84
|
+
|
|
85
|
+
agg = edges[[src, dst, flow["proto"], port(flow["dst"])]]
|
|
86
|
+
agg[:pkts] += flow["txPkts"].to_i + flow["rxPkts"].to_i
|
|
87
|
+
agg[:bytes] += flow["txBytes"].to_i + flow["rxBytes"].to_i
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
edges
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Optional: tagOwners / groups from the policy file for legend context.
|
|
95
|
+
# Note: `details=true` returns { acl: <base64 huJSON>, warnings, errors }.
|
|
96
|
+
def collect_policy(client)
|
|
97
|
+
client.acl.get(query: { details: true })
|
|
98
|
+
rescue Scaled::Error => e
|
|
99
|
+
warn "ACL fetch skipped (#{e.class}: #{e.message})"
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def to_dot(nodes, edges)
|
|
104
|
+
lines = ["digraph tailnet {", " rankdir=LR;", " node [shape=box, style=rounded];"]
|
|
105
|
+
|
|
106
|
+
nodes.each_value do |node|
|
|
107
|
+
label = [node[:name], node[:os], (node[:tags].join(",") unless node[:tags].empty?)].compact.join("\\n")
|
|
108
|
+
color = node[:tags].empty? ? "black" : "darkgreen" # tagged hosts highlighted
|
|
109
|
+
lines << %( "#{node[:id]}" [label="#{label}", color="#{color}"];)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
edges.each do |(src, dst, proto, dport), agg|
|
|
113
|
+
lines << %( "#{src}" -> "#{dst}" [label="#{proto}:#{dport} #{agg[:bytes]}B"];)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
"#{lines.join("\n")}\n}\n"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def host(addr_port)
|
|
120
|
+
return nil unless addr_port
|
|
121
|
+
|
|
122
|
+
addr_port.rpartition(":").first # strips :port, keeps IPv4/IPv6 host
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def port(addr_port)
|
|
126
|
+
addr_port&.rpartition(":")&.last
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def log_start
|
|
130
|
+
ENV.fetch("LOG_START") { raise "set LOG_START (RFC3339), e.g. 2026-06-20T00:00:00Z" }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def log_end
|
|
134
|
+
ENV.fetch("LOG_END") { raise "set LOG_END (RFC3339), e.g. 2026-06-21T00:00:00Z" }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def run
|
|
138
|
+
client = build_client
|
|
139
|
+
nodes, ip_index = collect_nodes(client)
|
|
140
|
+
edges = collect_edges(client, ip_index)
|
|
141
|
+
policy = collect_policy(client)
|
|
142
|
+
|
|
143
|
+
FileUtils.mkdir_p("tmp")
|
|
144
|
+
File.write("tmp/topology.dot", to_dot(nodes, edges))
|
|
145
|
+
File.write("tmp/topology.json", JSON.pretty_generate(
|
|
146
|
+
nodes: nodes.values,
|
|
147
|
+
edges: edges.map do |(s, d, pr, dp), a|
|
|
148
|
+
{ src: s, dst: d, proto: pr, dst_port: dp, **a }
|
|
149
|
+
end,
|
|
150
|
+
tag_owners: policy.is_a?(Hash) ? policy["tagOwners"] : nil
|
|
151
|
+
))
|
|
152
|
+
|
|
153
|
+
puts "nodes=#{nodes.size} edges=#{edges.size} -> tmp/topology.dot, tmp/topology.json"
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
TopologyGraph.run if $PROGRAM_NAME == __FILE__
|
data/lib/scaled/client.rb
CHANGED
|
@@ -6,6 +6,8 @@ require_relative "http"
|
|
|
6
6
|
require_relative "resources/devices"
|
|
7
7
|
require_relative "resources/keys"
|
|
8
8
|
require_relative "resources/logs"
|
|
9
|
+
require_relative "resources/acl"
|
|
10
|
+
require_relative "resources/device_routes"
|
|
9
11
|
|
|
10
12
|
module Scaled
|
|
11
13
|
# Main entry point for interacting with Tailscale API.
|
|
@@ -82,6 +84,16 @@ module Scaled
|
|
|
82
84
|
@logs ||= Resources::Logs.new(self)
|
|
83
85
|
end
|
|
84
86
|
|
|
87
|
+
# @return [Scaled::Resources::Acl]
|
|
88
|
+
def acl
|
|
89
|
+
@acl ||= Resources::Acl.new(self)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @return [Scaled::Resources::DeviceRoutes]
|
|
93
|
+
def device_routes
|
|
94
|
+
@device_routes ||= Resources::DeviceRoutes.new(self)
|
|
95
|
+
end
|
|
96
|
+
|
|
85
97
|
private
|
|
86
98
|
|
|
87
99
|
# @param api_token [String, nil]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Scaled
|
|
4
|
+
module Resources
|
|
5
|
+
# API wrapper for the tailnet policy file (ACL).
|
|
6
|
+
# API-обгортка для policy-файлу tailnet (ACL).
|
|
7
|
+
class Acl
|
|
8
|
+
# @param client [Scaled::Client] configured API client
|
|
9
|
+
# @return [void]
|
|
10
|
+
def initialize(client)
|
|
11
|
+
@client = client
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @param query [Hash, nil] optional query params (e.g. { details: 1 })
|
|
15
|
+
# @return [Hash, Array, String, nil] parsed policy file (acls/grants, tagOwners, groups, ...)
|
|
16
|
+
# Note: read-only; returns the current ACL/policy document for the tailnet.
|
|
17
|
+
# Нотатка: read-only; повертає поточний policy-файл (ACL) для tailnet.
|
|
18
|
+
def get(query: nil)
|
|
19
|
+
@client.get("/tailnet/#{@client.tailnet}/acl", query: query)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Scaled
|
|
4
|
+
module Resources
|
|
5
|
+
# API wrapper for per-device subnet routes.
|
|
6
|
+
# API-обгортка для маршрутів (subnet routes) окремого device.
|
|
7
|
+
class DeviceRoutes
|
|
8
|
+
# @param client [Scaled::Client] configured API client
|
|
9
|
+
# @return [void]
|
|
10
|
+
def initialize(client)
|
|
11
|
+
@client = client
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @param device_id [String] Tailscale device identifier
|
|
15
|
+
# @return [Hash, Array, String, nil] parsed response with advertisedRoutes/enabledRoutes
|
|
16
|
+
# Note: read-only; reveals subnet routers and exit nodes for the device.
|
|
17
|
+
# Нотатка: read-only; показує subnet-routers та exit-nodes для device.
|
|
18
|
+
def get(device_id)
|
|
19
|
+
@client.get("/device/#{device_id}/routes")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/scaled/version.rb
CHANGED
data/sig/scaled.rbs
CHANGED
|
@@ -39,6 +39,8 @@ module Scaled
|
|
|
39
39
|
def devices: () -> Resources::Devices
|
|
40
40
|
def keys: () -> Resources::Keys
|
|
41
41
|
def logs: () -> Resources::Logs
|
|
42
|
+
def acl: () -> Resources::Acl
|
|
43
|
+
def device_routes: () -> Resources::DeviceRoutes
|
|
42
44
|
end
|
|
43
45
|
|
|
44
46
|
module Resources
|
|
@@ -59,5 +61,15 @@ module Scaled
|
|
|
59
61
|
def configuration: (?query: Hash[Symbol | String, untyped]?) -> untyped
|
|
60
62
|
def network: (?query: Hash[Symbol | String, untyped]?) -> untyped
|
|
61
63
|
end
|
|
64
|
+
|
|
65
|
+
class Acl
|
|
66
|
+
def initialize: (Client client) -> void
|
|
67
|
+
def get: (?query: Hash[Symbol | String, untyped]?) -> untyped
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class DeviceRoutes
|
|
71
|
+
def initialize: (Client client) -> void
|
|
72
|
+
def get: (String device_id) -> untyped
|
|
73
|
+
end
|
|
62
74
|
end
|
|
63
75
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: scaled
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Voloshyn Ruslan
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-06-21 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
12
|
description: 'Scaled provides a read-only Ruby SDK for Tailscale API endpoints: devices,
|
|
13
13
|
keys, and logs. Supports API token and OAuth client credentials authentication.'
|
|
@@ -24,12 +24,15 @@ files:
|
|
|
24
24
|
- Rakefile
|
|
25
25
|
- examples/rails/scaled_initializer.rb
|
|
26
26
|
- examples/rails/tailscale_client.rb
|
|
27
|
+
- examples/topology_graph.rb
|
|
27
28
|
- lib/scaled.rb
|
|
28
29
|
- lib/scaled/auth/api_token.rb
|
|
29
30
|
- lib/scaled/auth/oauth_client_credentials.rb
|
|
30
31
|
- lib/scaled/client.rb
|
|
31
32
|
- lib/scaled/errors.rb
|
|
32
33
|
- lib/scaled/http.rb
|
|
34
|
+
- lib/scaled/resources/acl.rb
|
|
35
|
+
- lib/scaled/resources/device_routes.rb
|
|
33
36
|
- lib/scaled/resources/devices.rb
|
|
34
37
|
- lib/scaled/resources/keys.rb
|
|
35
38
|
- lib/scaled/resources/logs.rb
|