1package rest
2
3import (
4	"errors"
5	"fmt"
6	"net/http"
7
8	"gopkg.in/ns1/ns1-go.v2/rest/model/ipam"
9)
10
11// IPAMService handles the 'ipam' endpoint.
12type IPAMService service
13
14// ListAddrs returns a list of all root addresses (i.e. Parent == 0) in all
15// networks.
16//
17// NS1 API docs: https://ns1.com/api#getview-a-list-of-root-addresses
18func (s *IPAMService) ListAddrs() ([]ipam.Address, *http.Response, error) {
19	req, err := s.client.NewRequest(http.MethodGet, "ipam/address", nil)
20	if err != nil {
21		return nil, nil, err
22	}
23
24	addrs := []ipam.Address{}
25	var resp *http.Response
26	if s.client.FollowPagination {
27		resp, err = s.client.DoWithPagination(req, &addrs, s.nextAddrs)
28	} else {
29		resp, err = s.client.Do(req, &addrs)
30	}
31	if err != nil {
32		return nil, resp, err
33	}
34
35	return addrs, resp, nil
36}
37
38// GetSubnet returns the subnet corresponding to the provided address ID.
39//
40// NS1 API docs: https://ns1.com/api#getview-a-subnet
41func (s *IPAMService) GetSubnet(addrID int) (*ipam.Address, *http.Response, error) {
42	reqPath := fmt.Sprintf("ipam/address/%d", addrID)
43	req, err := s.client.NewRequest(http.MethodGet, reqPath, nil)
44	if err != nil {
45		return nil, nil, err
46	}
47
48	addr := &ipam.Address{}
49	var resp *http.Response
50	resp, err = s.client.Do(req, addr)
51	if err != nil {
52		return nil, resp, err
53	}
54
55	return addr, resp, nil
56}
57
58// GetChildren requests a list of all child addresses (or subnets) for the
59// specified IP address.
60//
61// NS1 API docs: https://ns1.com/api#getview-address-children
62func (s *IPAMService) GetChildren(addrID int) ([]*ipam.Address, *http.Response, error) {
63	reqPath := fmt.Sprintf("ipam/address/%d/children", addrID)
64	req, err := s.client.NewRequest(http.MethodGet, reqPath, nil)
65	if err != nil {
66		return nil, nil, err
67	}
68
69	addrs := []*ipam.Address{}
70	var resp *http.Response
71	if s.client.FollowPagination {
72		resp, err = s.client.DoWithPagination(req, &addrs, s.nextAddrs)
73	} else {
74		resp, err = s.client.Do(req, &addrs)
75	}
76	if err != nil {
77		return nil, resp, err
78	}
79
80	return addrs, resp, nil
81}
82
83// GetParent fetches the addresses parent.
84//
85// NS1 API docs: https://ns1.com/api#getview-address-parent
86func (s *IPAMService) GetParent(addrID int) (*ipam.Address, *http.Response, error) {
87	reqPath := fmt.Sprintf("ipam/address/%d/parent", addrID)
88	req, err := s.client.NewRequest(http.MethodGet, reqPath, nil)
89	if err != nil {
90		return nil, nil, err
91	}
92
93	addr := &ipam.Address{}
94	var resp *http.Response
95	resp, err = s.client.Do(req, addr)
96	if err != nil {
97		return nil, resp, err
98	}
99
100	return addr, resp, nil
101}
102
103// CreateSubnet creates an address or subnet.
104// The Prefix and Network fields are required.
105//
106// NS1 API docs: https://ns1.com/api#putcreate-a-subnet
107func (s *IPAMService) CreateSubnet(addr *ipam.Address) (*ipam.Address, *http.Response, error) {
108	switch {
109	case addr.Prefix == "":
110		return nil, nil, errors.New("the Prefix field is required")
111	case addr.Network == 0:
112		return nil, nil, errors.New("the Network field is required")
113	}
114
115	req, err := s.client.NewRequest(http.MethodPut, "ipam/address", addr)
116	if err != nil {
117		return nil, nil, err
118	}
119
120	respAddr := &ipam.Address{}
121	var resp *http.Response
122	resp, err = s.client.Do(req, respAddr)
123	if err != nil {
124		return nil, resp, err
125	}
126
127	return respAddr, resp, nil
128}
129
130// EditSubnet updates an existing subnet.
131// The ID field is required.
132// Parent is whether or not to include the parent in the parent field.
133//
134// NS1 API docs: https://ns1.com/api#postedit-a-subnet
135func (s *IPAMService) EditSubnet(addr *ipam.Address, parent bool) (newAddr, parentAddr *ipam.Address, resp *http.Response, err error) {
136	if addr.ID == 0 {
137		return nil, nil, nil, errors.New("the ID field is required")
138	}
139
140	reqPath := fmt.Sprintf("ipam/address/%d", addr.ID)
141	req, err := s.client.NewRequest(http.MethodPost, reqPath, addr)
142	if err != nil {
143		return nil, nil, nil, err
144	}
145	if parent {
146		q := req.URL.Query()
147		q.Add("parent", "true")
148		req.URL.RawQuery = q.Encode()
149	}
150
151	data := struct {
152		ipam.Address
153		Parent ipam.Address `json:"parent"`
154	}{}
155	resp, err = s.client.Do(req, &data)
156	if err != nil {
157		return nil, nil, resp, err
158	}
159
160	if parent {
161		return &data.Address, &data.Parent, resp, nil
162	}
163	return &data.Address, nil, resp, nil
164}
165
166// SplitSubnet splits a block of unassigned IP space into equal pieces.
167// This will not function with ranges or individual hosts. Normal breaking out
168// of a subnet is done with the standard PUT route. (Eg. root address is a /24
169// and request for /29s will break it into 32 /29s)
170//
171//    - Only planned subnets can be split
172//    - Name and description will be unset on children
173//    - KVPS and options will be copied; tags will be inherited
174//
175// NS1 API docs: https://ns1.com/api#postsplit-a-subnet
176func (s *IPAMService) SplitSubnet(id, prefix int) (rootAddr int, prefixIDs []int, resp *http.Response, err error) {
177	reqPath := fmt.Sprintf("ipam/address/%d/split", id)
178	req, err := s.client.NewRequest(http.MethodPost, reqPath, struct {
179		Prefix int `json:"prefix"`
180	}{
181		Prefix: prefix,
182	})
183	if err != nil {
184		return 0, nil, nil, err
185	}
186
187	data := &struct {
188		RootAddr  int   `json:"root_address_id"`
189		PrefixIDs []int `json:"prefix_ids"`
190	}{}
191	resp, err = s.client.Do(req, &data)
192	return data.RootAddr, data.PrefixIDs, resp, err
193}
194
195// MergeSubnet merges several subnets together.
196//
197// NS1 API docs: https://ns1.com/api#postmerge-a-subnet
198func (s *IPAMService) MergeSubnet(rootID, mergeID int) (*ipam.Address, *http.Response, error) {
199	req, err := s.client.NewRequest(http.MethodPost, "ipam/address/merge", struct {
200		Root  int `json:"root_address_id"`
201		Merge int `json:"merged_address_id"`
202	}{
203		Root:  rootID,
204		Merge: mergeID,
205	})
206	if err != nil {
207		return nil, nil, err
208	}
209
210	addr := &ipam.Address{}
211	resp, err := s.client.Do(req, &addr)
212	return addr, resp, err
213}
214
215// DeleteSubnet removes a subnet entirely.
216//
217// NS1 API docs: https://ns1.com/api#deletedelete-a-subnet
218func (s *IPAMService) DeleteSubnet(id int) (*http.Response, error) {
219	reqPath := fmt.Sprintf("ipam/address/%d", id)
220	req, err := s.client.NewRequest(http.MethodDelete, reqPath, nil)
221	if err != nil {
222		return nil, err
223	}
224
225	return s.client.Do(req, nil)
226}
227
228// nextAddrs is a pagination helper than gets and appends another list of
229// addresses to the passed list.
230func (s *IPAMService) nextAddrs(v *interface{}, uri string) (*http.Response, error) {
231	addrs := []*ipam.Address{}
232	resp, err := s.client.getURI(&addrs, uri)
233	if err != nil {
234		return resp, err
235	}
236	addrList, ok := (*v).(*[]*ipam.Address)
237	if !ok {
238		return nil, fmt.Errorf(
239			"incorrect value for v, expected value of type *[]ipam.Address, got: %T", v,
240		)
241	}
242	*addrList = append(*addrList, addrs...)
243	return resp, nil
244}
245