1package hcloud
2
3import (
4	"bytes"
5	"context"
6	"encoding/json"
7	"errors"
8	"fmt"
9	"net"
10	"net/url"
11	"strconv"
12	"time"
13
14	"github.com/hetznercloud/hcloud-go/hcloud/schema"
15)
16
17// NetworkZone specifies a network zone.
18type NetworkZone string
19
20// List of available Network Zones.
21const (
22	NetworkZoneEUCentral NetworkZone = "eu-central"
23)
24
25// NetworkSubnetType specifies a type of a subnet.
26type NetworkSubnetType string
27
28// List of available network subnet types.
29const (
30	NetworkSubnetTypeCloud   NetworkSubnetType = "cloud"
31	NetworkSubnetTypeServer  NetworkSubnetType = "server"
32	NetworkSubnetTypeVSwitch NetworkSubnetType = "vswitch"
33)
34
35// Network represents a network in the Hetzner Cloud.
36type Network struct {
37	ID         int
38	Name       string
39	Created    time.Time
40	IPRange    *net.IPNet
41	Subnets    []NetworkSubnet
42	Routes     []NetworkRoute
43	Servers    []*Server
44	Protection NetworkProtection
45	Labels     map[string]string
46}
47
48// NetworkSubnet represents a subnet of a network in the Hetzner Cloud.
49type NetworkSubnet struct {
50	Type        NetworkSubnetType
51	IPRange     *net.IPNet
52	NetworkZone NetworkZone
53	Gateway     net.IP
54	VSwitchID   int
55}
56
57// NetworkRoute represents a route of a network.
58type NetworkRoute struct {
59	Destination *net.IPNet
60	Gateway     net.IP
61}
62
63// NetworkProtection represents the protection level of a network.
64type NetworkProtection struct {
65	Delete bool
66}
67
68// NetworkClient is a client for the network API.
69type NetworkClient struct {
70	client *Client
71}
72
73// GetByID retrieves a network by its ID. If the network does not exist, nil is returned.
74func (c *NetworkClient) GetByID(ctx context.Context, id int) (*Network, *Response, error) {
75	req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/networks/%d", id), nil)
76	if err != nil {
77		return nil, nil, err
78	}
79
80	var body schema.NetworkGetResponse
81	resp, err := c.client.Do(req, &body)
82	if err != nil {
83		if IsError(err, ErrorCodeNotFound) {
84			return nil, resp, nil
85		}
86		return nil, nil, err
87	}
88	return NetworkFromSchema(body.Network), resp, nil
89}
90
91// GetByName retrieves a network by its name. If the network does not exist, nil is returned.
92func (c *NetworkClient) GetByName(ctx context.Context, name string) (*Network, *Response, error) {
93	if name == "" {
94		return nil, nil, nil
95	}
96	Networks, response, err := c.List(ctx, NetworkListOpts{Name: name})
97	if len(Networks) == 0 {
98		return nil, response, err
99	}
100	return Networks[0], response, err
101}
102
103// Get retrieves a network by its ID if the input can be parsed as an integer, otherwise it
104// retrieves a network by its name. If the network does not exist, nil is returned.
105func (c *NetworkClient) Get(ctx context.Context, idOrName string) (*Network, *Response, error) {
106	if id, err := strconv.Atoi(idOrName); err == nil {
107		return c.GetByID(ctx, int(id))
108	}
109	return c.GetByName(ctx, idOrName)
110}
111
112// NetworkListOpts specifies options for listing networks.
113type NetworkListOpts struct {
114	ListOpts
115	Name string
116}
117
118func (l NetworkListOpts) values() url.Values {
119	vals := l.ListOpts.values()
120	if l.Name != "" {
121		vals.Add("name", l.Name)
122	}
123	return vals
124}
125
126// List returns a list of networks for a specific page.
127//
128// Please note that filters specified in opts are not taken into account
129// when their value corresponds to their zero value or when they are empty.
130func (c *NetworkClient) List(ctx context.Context, opts NetworkListOpts) ([]*Network, *Response, error) {
131	path := "/networks?" + opts.values().Encode()
132	req, err := c.client.NewRequest(ctx, "GET", path, nil)
133	if err != nil {
134		return nil, nil, err
135	}
136
137	var body schema.NetworkListResponse
138	resp, err := c.client.Do(req, &body)
139	if err != nil {
140		return nil, nil, err
141	}
142	Networks := make([]*Network, 0, len(body.Networks))
143	for _, s := range body.Networks {
144		Networks = append(Networks, NetworkFromSchema(s))
145	}
146	return Networks, resp, nil
147}
148
149// All returns all networks.
150func (c *NetworkClient) All(ctx context.Context) ([]*Network, error) {
151	return c.AllWithOpts(ctx, NetworkListOpts{ListOpts: ListOpts{PerPage: 50}})
152}
153
154// AllWithOpts returns all networks for the given options.
155func (c *NetworkClient) AllWithOpts(ctx context.Context, opts NetworkListOpts) ([]*Network, error) {
156	var allNetworks []*Network
157
158	err := c.client.all(func(page int) (*Response, error) {
159		opts.Page = page
160		Networks, resp, err := c.List(ctx, opts)
161		if err != nil {
162			return resp, err
163		}
164		allNetworks = append(allNetworks, Networks...)
165		return resp, nil
166	})
167	if err != nil {
168		return nil, err
169	}
170
171	return allNetworks, nil
172}
173
174// Delete deletes a network.
175func (c *NetworkClient) Delete(ctx context.Context, network *Network) (*Response, error) {
176	req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/networks/%d", network.ID), nil)
177	if err != nil {
178		return nil, err
179	}
180	return c.client.Do(req, nil)
181}
182
183// NetworkUpdateOpts specifies options for updating a network.
184type NetworkUpdateOpts struct {
185	Name   string
186	Labels map[string]string
187}
188
189// Update updates a network.
190func (c *NetworkClient) Update(ctx context.Context, network *Network, opts NetworkUpdateOpts) (*Network, *Response, error) {
191	reqBody := schema.NetworkUpdateRequest{
192		Name: opts.Name,
193	}
194	if opts.Labels != nil {
195		reqBody.Labels = &opts.Labels
196	}
197	reqBodyData, err := json.Marshal(reqBody)
198	if err != nil {
199		return nil, nil, err
200	}
201
202	path := fmt.Sprintf("/networks/%d", network.ID)
203	req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData))
204	if err != nil {
205		return nil, nil, err
206	}
207
208	respBody := schema.NetworkUpdateResponse{}
209	resp, err := c.client.Do(req, &respBody)
210	if err != nil {
211		return nil, resp, err
212	}
213	return NetworkFromSchema(respBody.Network), resp, nil
214}
215
216// NetworkCreateOpts specifies options for creating a new network.
217type NetworkCreateOpts struct {
218	Name    string
219	IPRange *net.IPNet
220	Subnets []NetworkSubnet
221	Routes  []NetworkRoute
222	Labels  map[string]string
223}
224
225// Validate checks if options are valid.
226func (o NetworkCreateOpts) Validate() error {
227	if o.Name == "" {
228		return errors.New("missing name")
229	}
230	if o.IPRange == nil || o.IPRange.String() == "" {
231		return errors.New("missing IP range")
232	}
233	return nil
234}
235
236// Create creates a new network.
237func (c *NetworkClient) Create(ctx context.Context, opts NetworkCreateOpts) (*Network, *Response, error) {
238	if err := opts.Validate(); err != nil {
239		return nil, nil, err
240	}
241	reqBody := schema.NetworkCreateRequest{
242		Name:    opts.Name,
243		IPRange: opts.IPRange.String(),
244	}
245	for _, subnet := range opts.Subnets {
246		s := schema.NetworkSubnet{
247			Type:        string(subnet.Type),
248			IPRange:     subnet.IPRange.String(),
249			NetworkZone: string(subnet.NetworkZone),
250		}
251		if subnet.VSwitchID != 0 {
252			s.VSwitchID = subnet.VSwitchID
253		}
254		reqBody.Subnets = append(reqBody.Subnets, s)
255	}
256	for _, route := range opts.Routes {
257		reqBody.Routes = append(reqBody.Routes, schema.NetworkRoute{
258			Destination: route.Destination.String(),
259			Gateway:     route.Gateway.String(),
260		})
261	}
262	if opts.Labels != nil {
263		reqBody.Labels = &opts.Labels
264	}
265	reqBodyData, err := json.Marshal(reqBody)
266	if err != nil {
267		return nil, nil, err
268	}
269	req, err := c.client.NewRequest(ctx, "POST", "/networks", bytes.NewReader(reqBodyData))
270	if err != nil {
271		return nil, nil, err
272	}
273
274	respBody := schema.NetworkCreateResponse{}
275	resp, err := c.client.Do(req, &respBody)
276	if err != nil {
277		return nil, resp, err
278	}
279	return NetworkFromSchema(respBody.Network), resp, nil
280}
281
282// NetworkChangeIPRangeOpts specifies options for changing the IP range of a network.
283type NetworkChangeIPRangeOpts struct {
284	IPRange *net.IPNet
285}
286
287// ChangeIPRange changes the IP range of a network.
288func (c *NetworkClient) ChangeIPRange(ctx context.Context, network *Network, opts NetworkChangeIPRangeOpts) (*Action, *Response, error) {
289	reqBody := schema.NetworkActionChangeIPRangeRequest{
290		IPRange: opts.IPRange.String(),
291	}
292	reqBodyData, err := json.Marshal(reqBody)
293	if err != nil {
294		return nil, nil, err
295	}
296
297	path := fmt.Sprintf("/networks/%d/actions/change_ip_range", network.ID)
298	req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
299	if err != nil {
300		return nil, nil, err
301	}
302
303	respBody := schema.NetworkActionChangeIPRangeResponse{}
304	resp, err := c.client.Do(req, &respBody)
305	if err != nil {
306		return nil, resp, err
307	}
308	return ActionFromSchema(respBody.Action), resp, nil
309}
310
311// NetworkAddSubnetOpts specifies options for adding a subnet to a network.
312type NetworkAddSubnetOpts struct {
313	Subnet NetworkSubnet
314}
315
316// AddSubnet adds a subnet to a network.
317func (c *NetworkClient) AddSubnet(ctx context.Context, network *Network, opts NetworkAddSubnetOpts) (*Action, *Response, error) {
318	reqBody := schema.NetworkActionAddSubnetRequest{
319		Type:        string(opts.Subnet.Type),
320		NetworkZone: string(opts.Subnet.NetworkZone),
321	}
322	if opts.Subnet.IPRange != nil {
323		reqBody.IPRange = opts.Subnet.IPRange.String()
324	}
325	if opts.Subnet.VSwitchID != 0 {
326		reqBody.VSwitchID = opts.Subnet.VSwitchID
327	}
328	reqBodyData, err := json.Marshal(reqBody)
329	if err != nil {
330		return nil, nil, err
331	}
332
333	path := fmt.Sprintf("/networks/%d/actions/add_subnet", network.ID)
334	req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
335	if err != nil {
336		return nil, nil, err
337	}
338
339	respBody := schema.NetworkActionAddSubnetResponse{}
340	resp, err := c.client.Do(req, &respBody)
341	if err != nil {
342		return nil, resp, err
343	}
344	return ActionFromSchema(respBody.Action), resp, nil
345}
346
347// NetworkDeleteSubnetOpts specifies options for deleting a subnet from a network.
348type NetworkDeleteSubnetOpts struct {
349	Subnet NetworkSubnet
350}
351
352// DeleteSubnet deletes a subnet from a network.
353func (c *NetworkClient) DeleteSubnet(ctx context.Context, network *Network, opts NetworkDeleteSubnetOpts) (*Action, *Response, error) {
354	reqBody := schema.NetworkActionDeleteSubnetRequest{
355		IPRange: opts.Subnet.IPRange.String(),
356	}
357	reqBodyData, err := json.Marshal(reqBody)
358	if err != nil {
359		return nil, nil, err
360	}
361
362	path := fmt.Sprintf("/networks/%d/actions/delete_subnet", network.ID)
363	req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
364	if err != nil {
365		return nil, nil, err
366	}
367
368	respBody := schema.NetworkActionDeleteSubnetResponse{}
369	resp, err := c.client.Do(req, &respBody)
370	if err != nil {
371		return nil, resp, err
372	}
373	return ActionFromSchema(respBody.Action), resp, nil
374}
375
376// NetworkAddRouteOpts specifies options for adding a route to a network.
377type NetworkAddRouteOpts struct {
378	Route NetworkRoute
379}
380
381// AddRoute adds a route to a network.
382func (c *NetworkClient) AddRoute(ctx context.Context, network *Network, opts NetworkAddRouteOpts) (*Action, *Response, error) {
383	reqBody := schema.NetworkActionAddRouteRequest{
384		Destination: opts.Route.Destination.String(),
385		Gateway:     opts.Route.Gateway.String(),
386	}
387	reqBodyData, err := json.Marshal(reqBody)
388	if err != nil {
389		return nil, nil, err
390	}
391
392	path := fmt.Sprintf("/networks/%d/actions/add_route", network.ID)
393	req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
394	if err != nil {
395		return nil, nil, err
396	}
397
398	respBody := schema.NetworkActionAddSubnetResponse{}
399	resp, err := c.client.Do(req, &respBody)
400	if err != nil {
401		return nil, resp, err
402	}
403	return ActionFromSchema(respBody.Action), resp, nil
404}
405
406// NetworkDeleteRouteOpts specifies options for deleting a route from a network.
407type NetworkDeleteRouteOpts struct {
408	Route NetworkRoute
409}
410
411// DeleteRoute deletes a route from a network.
412func (c *NetworkClient) DeleteRoute(ctx context.Context, network *Network, opts NetworkDeleteRouteOpts) (*Action, *Response, error) {
413	reqBody := schema.NetworkActionDeleteRouteRequest{
414		Destination: opts.Route.Destination.String(),
415		Gateway:     opts.Route.Gateway.String(),
416	}
417	reqBodyData, err := json.Marshal(reqBody)
418	if err != nil {
419		return nil, nil, err
420	}
421
422	path := fmt.Sprintf("/networks/%d/actions/delete_route", network.ID)
423	req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
424	if err != nil {
425		return nil, nil, err
426	}
427
428	respBody := schema.NetworkActionDeleteSubnetResponse{}
429	resp, err := c.client.Do(req, &respBody)
430	if err != nil {
431		return nil, resp, err
432	}
433	return ActionFromSchema(respBody.Action), resp, nil
434}
435
436// NetworkChangeProtectionOpts specifies options for changing the resource protection level of a network.
437type NetworkChangeProtectionOpts struct {
438	Delete *bool
439}
440
441// ChangeProtection changes the resource protection level of a network.
442func (c *NetworkClient) ChangeProtection(ctx context.Context, network *Network, opts NetworkChangeProtectionOpts) (*Action, *Response, error) {
443	reqBody := schema.NetworkActionChangeProtectionRequest{
444		Delete: opts.Delete,
445	}
446	reqBodyData, err := json.Marshal(reqBody)
447	if err != nil {
448		return nil, nil, err
449	}
450
451	path := fmt.Sprintf("/networks/%d/actions/change_protection", network.ID)
452	req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
453	if err != nil {
454		return nil, nil, err
455	}
456
457	respBody := schema.NetworkActionChangeProtectionResponse{}
458	resp, err := c.client.Do(req, &respBody)
459	if err != nil {
460		return nil, resp, err
461	}
462	return ActionFromSchema(respBody.Action), resp, err
463}
464