/
ipam.go
384 lines (330 loc) · 14.1 KB
/
ipam.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
/*
*
* MIT License
*
* (C) Copyright 2022-2023 Hewlett Packard Enterprise Development LP
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
*/
package ipam
import (
"encoding/binary"
"errors"
"fmt"
"math"
sls_client "github.com/Cray-HPE/cani/pkg/sls-client"
"github.com/Cray-HPE/hms-xname/xnames"
"github.com/rs/zerolog/log"
"inet.af/netaddr"
)
func ExistingIPAddresses(slsSubnet sls_client.NetworkIpv4Subnet) (*netaddr.IPSet, error) {
var existingIPAddresses netaddr.IPSetBuilder
gatewayIP, err := netaddr.ParseIP(slsSubnet.Gateway)
if err != nil {
return nil, errors.Join(fmt.Errorf("failed to parse gateway IP (%v)", slsSubnet.Gateway), err)
}
existingIPAddresses.Add(gatewayIP)
for _, ipReservation := range slsSubnet.IPReservations {
ip, err := netaddr.ParseIP(ipReservation.IPAddress)
if err != nil {
return nil, errors.Join(fmt.Errorf("failed to parse IPReservation IP (%v)", ipReservation.IPAddress), err)
}
existingIPAddresses.Add(ip)
}
return existingIPAddresses.IPSet()
}
func FindNextAvailableIP(slsNetwork sls_client.Network, slsSubnet sls_client.NetworkIpv4Subnet) (netaddr.IP, error) {
// TODO this function should have guardrails to ensure that the IPs are within the static range.
subnet, err := netaddr.ParseIPPrefix(slsSubnet.CIDR)
if err != nil {
return netaddr.IP{}, errors.Join(fmt.Errorf("failed to parse subnet CIDR (%v)", slsSubnet.CIDR), err)
}
// If the subnet has been supernet hacked, then the unhacked CIDR will be returned. Otherwise none will be returned.
if correctedSubnet, correctedGateway, err := IsSupernetHacked(slsNetwork, slsSubnet); correctedSubnet != nil {
log.Info().Msgf("Info the %s subnet in the %s network has been supernet hacked! Changing CIDR from %v to %v for IP address calculation", slsSubnet.Name, slsNetwork.Name, subnet, correctedSubnet)
subnet = *correctedSubnet
// This won't alter the incoming SLS object, as it is passed in by value
slsSubnet.CIDR = correctedSubnet.String()
slsSubnet.Gateway = correctedGateway.String()
} else if err != nil {
return netaddr.IP{}, errors.Join(fmt.Errorf("failed to detect supernet hack on subnet %s in network %s", slsSubnet.Name, slsNetwork.Name), err)
}
existingIPAddressesSet, err := ExistingIPAddresses(slsSubnet)
if err != nil {
return netaddr.IP{}, err
}
startingIP := subnet.Range().From().Next() // Start at the first usable available IP in the subnet.
endingIP := subnet.Range().To() // This is the broadcast IP
for ip := startingIP; ip.Less(endingIP); ip = ip.Next() {
if !existingIPAddressesSet.Contains(ip) {
return ip, nil
}
}
return netaddr.IP{}, fmt.Errorf("subnet has no available IPs")
}
func AdvanceIP(ip netaddr.IP, n uint32) (netaddr.IP, error) {
if ip.Is6() {
return netaddr.IP{}, fmt.Errorf("IPv6 is not supported")
}
if ip.IsZero() {
return netaddr.IP{}, fmt.Errorf("empty IP address provided")
}
// This is kind of crude hack, but if it works it works.
ipOctets := ip.As4()
ipRaw := binary.BigEndian.Uint32(ipOctets[:])
// Advance the IP by n
ipRaw += n
// Now put it back into an netaddr.IP
var updatedIPOctets [4]byte
binary.BigEndian.PutUint32(updatedIPOctets[:], ipRaw)
return netaddr.IPFrom4(updatedIPOctets), nil
}
func SplitNetwork(network netaddr.IPPrefix, subnetMaskOneBits uint8) ([]netaddr.IPPrefix, error) {
// TODO why only allow this range?
if subnetMaskOneBits < 1 || 30 < subnetMaskOneBits {
return nil, fmt.Errorf("invalid subnet mask provided /%d", subnetMaskOneBits)
}
// Verify that the network can be split (this is allowing a split of the same size)
if subnetMaskOneBits < network.Bits() {
return nil, fmt.Errorf("provided subnet mask bits /%d is larger than starting network subnet mask /%d", subnetMaskOneBits, network.Bits())
}
subnetStartIP := network.Range().From()
// TODO add a counter to prevent this loop from going in forever!
var subnets []netaddr.IPPrefix
for {
subnets = append(subnets, netaddr.IPPrefixFrom(subnetStartIP, subnetMaskOneBits))
advanceBy := uint32(math.Pow(2, float64(32-subnetMaskOneBits)))
// Now advance!
var err error
subnetStartIP, err = AdvanceIP(subnetStartIP, advanceBy)
if err != nil {
return nil, err
}
if network.Range().To().Less(subnetStartIP) {
break
}
}
return subnets, nil
}
func FindNextAvailableSubnet(slsNetwork sls_client.NetworkExtraProperties, desiredSubnetMaskBits uint8) (netaddr.IPPrefix, error) {
// TODO make the /22 configurable
var existingSubnets netaddr.IPSetBuilder
for _, slsSubnet := range slsNetwork.Subnets {
subnet, err := netaddr.ParseIPPrefix(slsSubnet.CIDR)
if err != nil {
return netaddr.IPPrefix{}, errors.Join(fmt.Errorf("failed to parse subnet CIDR (%v)", slsSubnet.CIDR), err)
}
existingSubnets.AddPrefix(subnet)
}
existingSubnetsSet, err := existingSubnets.IPSet()
if err != nil {
return netaddr.IPPrefix{}, err
}
network, err := netaddr.ParseIPPrefix(slsNetwork.CIDR)
if err != nil {
return netaddr.IPPrefix{}, errors.Join(fmt.Errorf("failed to parse network CIDR (%s)", slsNetwork.CIDR), err)
}
availableSubnets, err := SplitNetwork(network, desiredSubnetMaskBits)
if err != nil {
return netaddr.IPPrefix{}, errors.Join(fmt.Errorf("failed to split network CIDR (%s)", slsNetwork.CIDR), err)
}
for _, subnet := range availableSubnets {
if existingSubnetsSet.Contains(subnet.IP()) {
continue
}
return subnet, nil
}
return netaddr.IPPrefix{}, fmt.Errorf("network space has been exhausted")
}
func AllocateCabinetSubnet(networkName string, slsNetwork sls_client.NetworkExtraProperties, xname xnames.Cabinet, desiredSubnetMaskBits uint8, vlanOverride *int32) (sls_client.NetworkIpv4Subnet, error) {
cabinetSubnet, err := FindNextAvailableSubnet(slsNetwork, desiredSubnetMaskBits)
if err != nil {
return sls_client.NetworkIpv4Subnet{}, errors.Join(fmt.Errorf("failed to allocate cabinet subnet for (%s) in CIDR (%s)", xname.String(), slsNetwork.CIDR), err)
}
// Verify this subnet is new
subnetName := fmt.Sprintf("cabinet_%d", xname.Cabinet)
for _, otherSubnet := range slsNetwork.Subnets {
if otherSubnet.Name == subnetName {
return sls_client.NetworkIpv4Subnet{}, fmt.Errorf("subnet (%s) already exists", subnetName)
}
}
// Calculate VLAN if one was not provided
// TODO This needs to be updated to calculate only the NMN_MTN network for Cray EX cabinets. River both HMN_RVR and NMN_RVR networks
vlan := int32(-1)
if vlanOverride != nil {
vlan = *vlanOverride
} else {
// Look at other cabinets in the subnet and pick one.
// Determine the current vlans in use by other cabinets
vlansInUse := map[int32]bool{}
for _, existingSubnet := range slsNetwork.Subnets {
vlansInUse[existingSubnet.VlanID] = true
}
// Now lest find the smallest free Vlan!
var vlanLow int32 = -1
var vlanHigh int32 = -1
if networkName == "HMN_RVR" {
// The following values are defined here in CSI: https://github.com/Cray-HPE/cray-site-init/blob/4ead6fccd0ba0710e7250357f1c3a2525996d293/cmd/init.go#L160
vlanLow = 1513
vlanHigh = 1769
} else if networkName == "NMN_RVR" {
// The following values are defined here in CSI: https://github.com/Cray-HPE/cray-site-init/blob/4ead6fccd0ba0710e7250357f1c3a2525996d293/cmd/init.go#L189
vlanLow = 1770
vlanHigh = 1999
} else if networkName == "HMN_MTN" {
// The following values are defined here in CSI: https://github.com/Cray-HPE/cray-site-init/blob/4ead6fccd0ba0710e7250357f1c3a2525996d293/cmd/init.go#L148
vlanLow = 3000
vlanHigh = 3999
} else if networkName == "NMN_MTN" {
// The following values are defined here in CSI: https://github.com/Cray-HPE/cray-site-init/blob/4ead6fccd0ba0710e7250357f1c3a2525996d293/cmd/init.go#L176
vlanLow = 2000
vlanHigh = 2999
} else {
return sls_client.NetworkIpv4Subnet{}, fmt.Errorf("unsupported network (%s) unable to allocate vlan for cabinet subnet", networkName)
}
for vlanCandidate := vlanLow; vlanCandidate <= vlanHigh; vlanCandidate++ {
if vlansInUse[vlanCandidate] {
// currently in use
continue
}
vlan = vlanCandidate
break
}
}
if vlan == -1 {
return sls_client.NetworkIpv4Subnet{}, fmt.Errorf("failed to allocate cabinet subnet for (%s) no subnets available", subnetName)
}
// DHCP starts 10 into the subnet
dhcpStart, err := AdvanceIP(cabinetSubnet.Range().From(), 10)
if err != nil {
return sls_client.NetworkIpv4Subnet{}, fmt.Errorf("failed to determine DHCP start in CIDR (%s)", cabinetSubnet.String())
}
return sls_client.NetworkIpv4Subnet{
Name: subnetName,
CIDR: cabinetSubnet.String(),
VlanID: vlan,
Gateway: cabinetSubnet.Range().From().Next().IPAddr().IP.String(),
DHCPStart: dhcpStart.IPAddr().IP.String(),
DHCPEnd: cabinetSubnet.Range().To().Prior().IPAddr().IP.String(),
}, nil
}
func AllocateIP(slsNetwork sls_client.Network, slsSubnet sls_client.NetworkIpv4Subnet, xname xnames.Xname, alias string) (sls_client.NetworkIpReservation, error) {
ip, err := FindNextAvailableIP(slsNetwork, slsSubnet)
if err != nil {
return sls_client.NetworkIpReservation{}, errors.Join(
fmt.Errorf("failed to allocate ip for hardware (%s) in subnet (%s)", xname.String(), slsSubnet.CIDR),
err,
)
}
// Verify this switch is unique within the subnet
for _, ipReservation := range slsSubnet.IPReservations {
matchingAlias := ipReservation.Name == alias
matchingXName := ipReservation.Comment == xname.String()
if matchingAlias && matchingXName {
// IP reservation already exists
return sls_client.NetworkIpReservation{}, fmt.Errorf("ip reservation with name (%v) and xname (%v) already exists with IP (%s)", ipReservation.Name, ipReservation.Comment, ipReservation.IPAddress)
} else if matchingAlias {
return sls_client.NetworkIpReservation{}, fmt.Errorf("ip reservation with name (%v) already exists on (%v) with IP (%s)", alias, ipReservation.Comment, ipReservation.IPAddress)
} else if matchingXName {
return sls_client.NetworkIpReservation{}, fmt.Errorf("ip reservation with xname (%v) already exists with name (%v) with IP (%s)", xname.String(), ipReservation.Name, ipReservation.IPAddress)
}
}
// TODO Move this outside this function? So this function just gives back IP within the subnet, and then have outside logic
// Verify that the IP is actually valid ie within the DHCP range, and if not in the DHCP range expand it and verify nothing is
// using the IP address.
// Verify IP is within the static IP range. The static range
if slsSubnet.DHCPStart != "" {
dhcpStart, err := netaddr.ParseIP(slsSubnet.DHCPStart)
if err != nil {
return sls_client.NetworkIpReservation{}, errors.Join(fmt.Errorf("failed to parse DHCP Start IP (%s) address", slsSubnet.DHCPStart), err)
}
if !ip.Less(dhcpStart) {
return sls_client.NetworkIpReservation{}, fmt.Errorf("ip reservation with xname (%v) and IP %s is outside the static IP address range, with starting DHCP IP of %s", xname.String(), ip.String(), slsSubnet.DHCPStart)
}
}
return sls_client.NetworkIpReservation{
Comment: xname.String(),
IPAddress: ip.IPAddr().IP.String(),
Name: alias,
}, nil
}
//
// The following functions may be needed when adding hardware like application or management nodes
//
// func FreeIPsInStaticRange(slsSubnet sls_client.NetworkIpv4Subnet) (uint32, error) {
// // Probably need to steal some of the logic for allocate IP. Need to share the logic between the two
//
// subnet, err := netaddr.ParseIPPrefix(slsSubnet.CIDR)
// if err != nil {
// return 0, fmt.Errorf("failed to parse subnet CIDR (%v): %w", slsSubnet.CIDR, err)
// }
//
// existingIPAddressesSet, err := ExistingIPAddresses(slsSubnet)
// if err != nil {
// return 0, err
// }
//
// startingIP := subnet.Range().From().Next() // Start at the first usable available IP in the subnet.
// endingIP, err := netaddr.ParseIP(slsSubnet.DHCPStart)
// if err != nil {
// return 0, fmt.Errorf("failed to convert DHCP Start IP address to netaddr struct")
// }
//
// var count uint32
// for ip := startingIP; ip.Less(endingIP); ip = ip.Next() {
// if existingIPAddressesSet.Contains(ip) {
// // IP address currently in use
// continue
// }
// count++
// }
//
// return count, nil
// }
// func ExpandSubnetStaticRange(slsSubnet *sls_common.IPV4Subnet, count uint32) error {
// if slsSubnet.DHCPStart == nil || slsSubnet.DHCPEnd == nil {
// return fmt.Errorf("subnet does not have DHCP range")
// }
//
// dhcpStart, ok := netaddr.FromStdIP(slsSubnet.DHCPStart)
// if !ok {
// return fmt.Errorf("failed to convert DHCP Start IP address to netaddr struct")
// }
//
// dhcpEnd, ok := netaddr.FromStdIP(slsSubnet.DHCPEnd)
// if !ok {
// return fmt.Errorf("failed to convert DHCP END IP address to netaddr struct")
// }
//
// // Move it forward!
// dhcpStart, err := AdvanceIP(dhcpStart, count)
// if err != nil {
// return fmt.Errorf("failed to advice DHCP Start IP address: %w", err)
// }
//
// // Verify the DHCP Start address is smaller than the end address
// if !dhcpStart.Less(dhcpEnd) {
// return fmt.Errorf("new DHCP Start address %v is equal or larger then the DHCP End address %v", dhcpStart, dhcpEnd)
// }
//
// // Now update the SLS subnet
// slsSubnet.DHCPStart = dhcpStart.IPAddr().IP
// return nil
// }