-
Notifications
You must be signed in to change notification settings - Fork 2
/
hetzner.go
422 lines (389 loc) · 11.8 KB
/
hetzner.go
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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
package main
import (
"bufio"
"flag"
"fmt"
"github.com/dmotylev/goproperties"
"github.com/dmotylev/hetzner/api"
"io"
"log"
"net"
"os"
"sort"
"strconv"
"strings"
"sync"
"text/tabwriter"
"time"
)
const paidTimeFormat = "2006-01-02"
const (
KindNil = iota
KindHost
KindManagement
KindNetwork
KindVirtual
KindFailover
)
type Kind byte
func (k Kind) String() string {
switch k {
case KindNil:
return "<nil>"
case KindManagement:
return "Management"
case KindHost:
return "Host"
case KindVirtual:
return "Virtual"
case KindNetwork:
return "Network"
case KindFailover:
return "Failover"
default:
return "kind_" + strconv.Itoa(int(k))
}
}
type Node struct {
// Identity
Ip net.IP // (String) IP address
// Network info
Ptr string // (String) PTR record
Separate_mac net.HardwareAddr // Separate MAC address, if not set null
Server_ip net.IP // Server main IP address
Subnet *net.IPNet // Subnet
Gateway net.IP // Subnet gateway
// Hetzner product info
Dc string // (String) Datacentre number
Product string // (String) Server product name
Server_name string // (String) Server name
Server_number int // (Integer) Server id
// Billing info
Throttled bool // (Boolean) Bandwidth limit status
Status string // (String) Server order status (ready or in process)
Cancelled bool // (Boolean) Status of server cancellation
Failover bool // (Boolean) True if net is a failover net
Flatrate bool // (Boolean) Indicates if the server has a traffic flatrate (traffic overusage will not be charged but the bandwith will be reduced) or not (traffic overusage will be charged)
Locked bool // (Boolean) Status of locking
Paid_until time.Time // Paid until date
// Traffic limits
Traffic_warnings bool // (Boolean) True if traffic warnings are enabled
Traffic string // (String) Free traffic quota
Traffic_daily int // (Integer) Daily traffic limit in MB
Traffic_hourly int // (Integer) Hourly traffic limit in MB
Traffic_monthly int // (Integer) Monthly traffic limit in GB
// Failover
Active_server_ip net.IP // (String) Main IP of current destination server
// Misc
Kind Kind
}
func (n *Node) Clone() *Node {
return &Node{
Ip: n.Ip,
Ptr: n.Ptr,
Separate_mac: n.Separate_mac,
Server_ip: n.Server_ip,
Subnet: n.Subnet,
Gateway: n.Gateway,
Dc: n.Dc,
Product: n.Product,
Server_name: n.Server_name,
Server_number: n.Server_number,
Throttled: n.Throttled,
Status: n.Status,
Cancelled: n.Cancelled,
Failover: n.Failover,
Flatrate: n.Flatrate,
Locked: n.Locked,
Paid_until: n.Paid_until,
Traffic: n.Traffic,
Traffic_daily: n.Traffic_daily,
Traffic_hourly: n.Traffic_hourly,
Traffic_monthly: n.Traffic_monthly,
Traffic_warnings: n.Traffic_warnings,
Active_server_ip: n.Active_server_ip,
Kind: n.Kind,
}
}
type Nodes []*Node
func (n Nodes) Len() int { return len(n) }
func (n Nodes) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
func (n Nodes) Less(i, j int) bool {
if n[i].Server_number == n[j].Server_number {
return n[i].Kind < n[j].Kind
}
return n[i].Server_number < n[j].Server_number
}
func unbrace(s string) string {
if len(s) > 1 && (s[0] == '\'' || s[0] == '"') {
return s[1 : len(s)-1]
}
return s
}
func getHetznerData(username, password string) (Nodes, error) {
api.SetBasicAuth(username, password)
// load raw data
var (
rservers []api.Server
rips []api.Ip
rsubnets []api.Subnet
rrdns []api.Rdns
rfailover []api.Failover
wg sync.WaitGroup
)
errc := make(chan error, 5)
wg.Add(5)
go func() {
defer wg.Done()
errc <- api.Get("/server", &rservers)
}()
go func() {
defer wg.Done()
errc <- api.Get("/ip", &rips)
}()
go func() {
defer wg.Done()
errc <- api.Get("/subnet", &rsubnets)
}()
go func() {
defer wg.Done()
errc <- api.Get("/rdns", &rrdns)
}()
go func() {
defer wg.Done()
errc <- api.Get("/failover", &rfailover)
}()
wg.Wait()
for i := 0; i < cap(errc); i++ {
if err := <-errc; err != nil {
return nil, err
}
}
// build nodes
nodes := make(map[string]*Node) // Ip.String() as a key
for _, e := range rservers {
paid_until, _ := time.Parse(paidTimeFormat, e.Paid_until)
n := Node{
// copy of Server
Ip: net.IP(e.Server_ip),
Server_number: e.Server_number,
Server_name: e.Server_name,
Product: e.Product,
Dc: e.Dc,
Traffic: e.Traffic,
Flatrate: e.Flatrate,
Status: e.Status,
Throttled: e.Throttled,
Cancelled: e.Cancelled,
Paid_until: paid_until,
// Additional fields
Server_ip: net.IP(e.Server_ip),
Kind: KindHost,
}
nodes[n.Ip.String()] = &n
}
for _, e := range rips {
n, found := nodes[e.Ip.String()]
if !found {
// find host node by Server_ip, make a copy of it and set Ip
if host, found := nodes[e.Server_ip.String()]; found {
if !host.Ip.Equal(net.IP(e.Ip)) {
n = host.Clone()
n.Ip = net.IP(e.Ip)
n.Kind = KindManagement
nodes[n.Ip.String()] = n
} else {
n = host
}
} else {
panic("no host found for " + e.Ip.String())
}
}
n.Locked = e.Locked
n.Separate_mac, _ = net.ParseMAC(e.Separate_mac)
n.Traffic_warnings = e.Traffic_warnings
n.Traffic_hourly = e.Traffic_hourly
n.Traffic_daily = e.Traffic_daily
n.Traffic_monthly = e.Traffic_monthly
}
subnets := make([]*Node, 0, len(rsubnets))
for _, e := range rsubnets {
if _, found := nodes[e.Ip.String()]; found {
panic("subnet conflicted with node " + e.Ip.String()) // should not happen
}
server := nodes[e.Server_ip.String()] // it is unlikely that subnet exists without host
n := server.Clone()
n.Ip = net.IP(e.Ip)
n.Kind = KindNetwork
n.Gateway = net.IP(e.Gateway)
n.Failover = e.Failover
n.Locked = e.Locked
n.Traffic_warnings = e.Traffic_warnings
n.Traffic_hourly = e.Traffic_hourly
n.Traffic_daily = e.Traffic_daily
n.Traffic_monthly = e.Traffic_monthly
bits := 8 * net.IPv4len
if v := n.Ip.To4(); v == nil {
bits = 8 * net.IPv6len
}
n.Subnet = &net.IPNet{IP: n.Ip, Mask: net.CIDRMask(e.Mask, bits)}
nodes[n.Ip.String()] = n
subnets = append(subnets, n)
}
BindPtr:
for _, e := range rrdns {
ip := net.IP(e.Ip)
ptr := e.Ptr
if n, found := nodes[ip.String()]; found {
n.Ptr = ptr
continue
}
for _, s := range subnets {
if s.Subnet.Contains(ip) {
host := nodes[s.Server_ip.String()]
n := host.Clone()
n.Ip = ip
n.Ptr = ptr
n.Kind = KindVirtual
nodes[n.Ip.String()] = n
continue BindPtr
}
}
panic(fmt.Sprintf("should not reach that point: loosen record %s PTR %s", ip, ptr))
}
for _, e := range rfailover {
// failover is a /32 network assigned to server
n, found := nodes[e.Ip.String()]
if !found || !(n.Kind == KindNetwork && n.Failover == true) {
panic("no subnet for failover " + e.Ip.String())
}
n.Kind = KindFailover
n.Active_server_ip = net.IP(e.Active_server_ip)
}
// convert map to list sorted by server_number+kind
l := Nodes{}
for _, n := range nodes {
l = append(l, n)
}
sort.Sort(l)
return l, nil
}
type field struct {
format string
factory func(*Node) interface{}
}
var (
batchMode bool
ofields string
okinds string
delimiter string
knownFields = map[string]field{
"server_number": field{"%d", func(n *Node) interface{} { return n.Server_number }},
"kind": field{"%s", func(n *Node) interface{} { return n.Kind }},
"ip": field{"%s", func(n *Node) interface{} { return n.Ip }},
"subnet": field{"%s", func(n *Node) interface{} {
if n.Subnet == nil {
return ""
}
return n.Subnet
}},
"gateway": field{"%s", func(n *Node) interface{} { return n.Gateway }},
"server_name": field{"%s", func(n *Node) interface{} { return n.Server_name }},
"ptr": field{"%s", func(n *Node) interface{} { return n.Ptr }},
"server_ip": field{"%s", func(n *Node) interface{} { return n.Server_ip }},
"separate_mac": field{"%s", func(n *Node) interface{} { return n.Separate_mac }},
"dc": field{"%s", func(n *Node) interface{} { return n.Dc }},
"product": field{"%s", func(n *Node) interface{} { return n.Product }},
"status": field{"%s", func(n *Node) interface{} { return n.Status }},
"cancelled": field{"%t", func(n *Node) interface{} { return n.Cancelled }},
"locked": field{"%t", func(n *Node) interface{} { return n.Locked }},
"paid_until": field{"%s", func(n *Node) interface{} { return n.Paid_until.Format(paidTimeFormat) }},
"failover": field{"%t", func(n *Node) interface{} { return n.Failover }},
"flatrate": field{"%t", func(n *Node) interface{} { return n.Flatrate }},
"throttled": field{"%t", func(n *Node) interface{} { return n.Throttled }},
"traffic_warnings": field{"%t", func(n *Node) interface{} { return n.Traffic_warnings }},
"traffic": field{"%s", func(n *Node) interface{} { return n.Traffic }},
"traffic_daily": field{"%d", func(n *Node) interface{} { return n.Traffic_daily }},
"traffic_hourly": field{"%d", func(n *Node) interface{} { return n.Traffic_hourly }},
"traffic_monthly": field{"%d", func(n *Node) interface{} { return n.Traffic_monthly }},
"active_server_ip": field{"%s", func(n *Node) interface{} { return n.Active_server_ip }},
}
)
func main() {
flag.BoolVar(&batchMode, "batch", false, "output mode handy for batch processing")
flag.StringVar(&delimiter, "delimiter", ",", "field delimiter for batch mode output")
flag.StringVar(&ofields, "fields", "server_number,kind,ip,subnet,gateway,server_name,ptr,server_ip,separate_mac,dc,product,status,cancelled,locked,paid_until,failover,flatrate,throttled,traffic_warnings,traffic,traffic_daily,traffic_hourly,traffic_monthly,active_server_ip", "comma separated list of output fields")
flag.StringVar(&okinds, "kinds", "host,management,network,virtual,failover", "comma separated list of output kinds")
flag.Parse()
log.SetFlags(0)
// compile arguments
if !batchMode {
delimiter = "\t"
}
names := strings.Split(ofields, ",")
header := strings.Join(names, "\t")
format := func(a []string) string {
l := make([]string, 0, len(names))
for _, name := range names {
f, found := knownFields[strings.ToLower(strings.TrimSpace(name))]
if !found {
log.Fatalf("unknown field %s", name)
}
l = append(l, f.format)
}
return strings.Join(l, delimiter) + "\n"
}(names)
args := func(n *Node) []interface{} {
l := make([]interface{}, 0, len(names))
for _, name := range names {
// all names are known on this line
l = append(l, knownFields[strings.ToLower(strings.TrimSpace(name))].factory(n))
}
return l
}
kinds := make(map[Kind]bool)
for _, k := range strings.Split(okinds, ",") {
switch strings.ToLower(strings.TrimSpace(k)) {
case "host":
kinds[KindHost] = true
case "management":
kinds[KindManagement] = true
case "network":
kinds[KindNetwork] = true
case "virtual":
kinds[KindVirtual] = true
case "failover":
kinds[KindFailover] = true
}
}
// load credentials
rc, err := properties.Load(os.ExpandEnv("$HOME/.hetzner.rc"))
if err != nil {
log.Fatalf("no credentials: %s", err)
}
// load nodes data
var nodes Nodes
nodes, err = getHetznerData(unbrace(rc["login"]), unbrace(rc["password"]))
if err != nil {
log.Fatal(err)
}
// output data
var w interface {
io.Writer
Flush() error
}
if batchMode {
w = bufio.NewWriter(os.Stdout)
} else {
w = tabwriter.NewWriter(os.Stdout, 0, 2, 1, ' ', 0)
fmt.Fprintln(w, header)
}
for _, n := range nodes {
if _, found := kinds[n.Kind]; !found {
continue
}
a := args(n)
fmt.Fprintf(w, format, a...)
}
w.Flush()
}