Skip to content

Commit 4b33a1b

Browse files
apparentlymartkmoe
authored andcommitted
lang/funcs: cidrsubnets function
This is a companion to cidrsubnet that allows bulk-allocation of multiple subnet addresses at once, with automatic numbering. Unlike cidrsubnet, cidrsubnets allows each of the allocations to have a different prefix length, and will pack the networks consecutively into the given address space. cidrsubnets can potentially create more complicated addressing schemes than cidrsubnet alone can, because it's able to take into account the full set of requested prefix lengths rather than just one at a time.
1 parent e664f5b commit 4b33a1b

File tree

4 files changed

+206
-0
lines changed

4 files changed

+206
-0
lines changed

internal/lang/funcs/cidr.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,86 @@ var CidrSubnetFunc = function.New(&function.Spec{
113113
},
114114
})
115115

116+
// CidrSubnetsFunc is similar to CidrSubnetFunc but calculates many consecutive
117+
// subnet addresses at once, rather than just a single subnet extension.
118+
var CidrSubnetsFunc = function.New(&function.Spec{
119+
Params: []function.Parameter{
120+
{
121+
Name: "prefix",
122+
Type: cty.String,
123+
},
124+
},
125+
VarParam: &function.Parameter{
126+
Name: "newbits",
127+
Type: cty.Number,
128+
},
129+
Type: function.StaticReturnType(cty.List(cty.String)),
130+
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
131+
_, network, err := net.ParseCIDR(args[0].AsString())
132+
if err != nil {
133+
return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "invalid CIDR expression: %s", err)
134+
}
135+
startPrefixLen, _ := network.Mask.Size()
136+
137+
prefixLengthArgs := args[1:]
138+
if len(prefixLengthArgs) == 0 {
139+
return cty.ListValEmpty(cty.String), nil
140+
}
141+
142+
var firstLength int
143+
if err := gocty.FromCtyValue(prefixLengthArgs[0], &firstLength); err != nil {
144+
return cty.UnknownVal(cty.String), function.NewArgError(1, err)
145+
}
146+
firstLength += startPrefixLen
147+
148+
retVals := make([]cty.Value, len(prefixLengthArgs))
149+
150+
current, _ := cidr.PreviousSubnet(network, firstLength)
151+
for i, lengthArg := range prefixLengthArgs {
152+
var length int
153+
if err := gocty.FromCtyValue(lengthArg, &length); err != nil {
154+
return cty.UnknownVal(cty.String), function.NewArgError(i+1, err)
155+
}
156+
157+
if length < 1 {
158+
return cty.UnknownVal(cty.String), function.NewArgErrorf(i+1, "must extend prefix by at least one bit")
159+
}
160+
// For portability with 32-bit systems where the subnet number
161+
// will be a 32-bit int, we only allow extension of 32 bits in
162+
// one call even if we're running on a 64-bit machine.
163+
// (Of course, this is significant only for IPv6.)
164+
if length > 32 {
165+
return cty.UnknownVal(cty.String), function.NewArgErrorf(i+1, "may not extend prefix by more than 32 bits")
166+
}
167+
length += startPrefixLen
168+
if length > (len(network.IP) * 8) {
169+
protocol := "IP"
170+
switch len(network.IP) * 8 {
171+
case 32:
172+
protocol = "IPv4"
173+
case 128:
174+
protocol = "IPv6"
175+
}
176+
return cty.UnknownVal(cty.String), function.NewArgErrorf(i+1, "would extend prefix to %d bits, which is too long for an %s address", length, protocol)
177+
}
178+
179+
next, rollover := cidr.NextSubnet(current, length)
180+
if rollover || !network.Contains(next.IP) {
181+
// If we run out of suffix bits in the base CIDR prefix then
182+
// NextSubnet will start incrementing the prefix bits, which
183+
// we don't allow because it would then allocate addresses
184+
// outside of the caller's given prefix.
185+
return cty.UnknownVal(cty.String), function.NewArgErrorf(i+1, "not enough remaining address space for a subnet with a prefix of %d bits after %s", length, current.String())
186+
}
187+
188+
current = next
189+
retVals[i] = cty.StringVal(current.String())
190+
}
191+
192+
return cty.ListVal(retVals), nil
193+
},
194+
})
195+
116196
// CidrHost calculates a full host IP address within a given IP network address prefix.
117197
func CidrHost(prefix, hostnum cty.Value) (cty.Value, error) {
118198
return CidrHostFunc.Call([]cty.Value{prefix, hostnum})
@@ -127,3 +207,12 @@ func CidrNetmask(prefix cty.Value) (cty.Value, error) {
127207
func CidrSubnet(prefix, newbits, netnum cty.Value) (cty.Value, error) {
128208
return CidrSubnetFunc.Call([]cty.Value{prefix, newbits, netnum})
129209
}
210+
211+
// CidrSubnets calculates a sequence of consecutive subnet prefixes that may
212+
// be of different prefix lengths under a common base prefix.
213+
func CidrSubnets(prefix cty.Value, newbits ...cty.Value) (cty.Value, error) {
214+
args := make([]cty.Value, len(newbits)+1)
215+
args[0] = prefix
216+
copy(args[1:], newbits)
217+
return CidrSubnetsFunc.Call(args)
218+
}

internal/lang/funcs/cidr_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,3 +214,107 @@ func TestCidrSubnet(t *testing.T) {
214214
})
215215
}
216216
}
217+
func TestCidrSubnets(t *testing.T) {
218+
tests := []struct {
219+
Prefix cty.Value
220+
Newbits []cty.Value
221+
Want cty.Value
222+
Err string
223+
}{
224+
{
225+
cty.StringVal("10.0.0.0/21"),
226+
[]cty.Value{
227+
cty.NumberIntVal(3),
228+
cty.NumberIntVal(3),
229+
cty.NumberIntVal(3),
230+
cty.NumberIntVal(4),
231+
cty.NumberIntVal(4),
232+
cty.NumberIntVal(4),
233+
cty.NumberIntVal(7),
234+
cty.NumberIntVal(7),
235+
cty.NumberIntVal(7),
236+
},
237+
cty.ListVal([]cty.Value{
238+
cty.StringVal("10.0.0.0/24"),
239+
cty.StringVal("10.0.1.0/24"),
240+
cty.StringVal("10.0.2.0/24"),
241+
cty.StringVal("10.0.3.0/25"),
242+
cty.StringVal("10.0.3.128/25"),
243+
cty.StringVal("10.0.4.0/25"),
244+
cty.StringVal("10.0.4.128/28"),
245+
cty.StringVal("10.0.4.144/28"),
246+
cty.StringVal("10.0.4.160/28"),
247+
}),
248+
``,
249+
},
250+
{
251+
cty.StringVal("10.0.0.0/30"),
252+
[]cty.Value{
253+
cty.NumberIntVal(1),
254+
cty.NumberIntVal(3),
255+
},
256+
cty.UnknownVal(cty.List(cty.String)),
257+
`would extend prefix to 33 bits, which is too long for an IPv4 address`,
258+
},
259+
{
260+
cty.StringVal("10.0.0.0/8"),
261+
[]cty.Value{
262+
cty.NumberIntVal(1),
263+
cty.NumberIntVal(1),
264+
cty.NumberIntVal(1),
265+
},
266+
cty.UnknownVal(cty.List(cty.String)),
267+
`not enough remaining address space for a subnet with a prefix of 9 bits after 10.128.0.0/9`,
268+
},
269+
{
270+
cty.StringVal("10.0.0.0/8"),
271+
[]cty.Value{
272+
cty.NumberIntVal(1),
273+
cty.NumberIntVal(0),
274+
},
275+
cty.UnknownVal(cty.List(cty.String)),
276+
`must extend prefix by at least one bit`,
277+
},
278+
{
279+
cty.StringVal("10.0.0.0/8"),
280+
[]cty.Value{
281+
cty.NumberIntVal(1),
282+
cty.NumberIntVal(-1),
283+
},
284+
cty.UnknownVal(cty.List(cty.String)),
285+
`must extend prefix by at least one bit`,
286+
},
287+
{
288+
cty.StringVal("fe80::/48"),
289+
[]cty.Value{
290+
cty.NumberIntVal(1),
291+
cty.NumberIntVal(33),
292+
},
293+
cty.UnknownVal(cty.List(cty.String)),
294+
`may not extend prefix by more than 32 bits`,
295+
},
296+
}
297+
298+
for _, test := range tests {
299+
t.Run(fmt.Sprintf("cidrsubnets(%#v, %#v)", test.Prefix, test.Newbits), func(t *testing.T) {
300+
got, err := CidrSubnets(test.Prefix, test.Newbits...)
301+
wantErr := test.Err != ""
302+
303+
if wantErr {
304+
if err == nil {
305+
t.Fatal("succeeded; want error")
306+
}
307+
if err.Error() != test.Err {
308+
t.Fatalf("wrong error\ngot: %s\nwant: %s", err.Error(), test.Err)
309+
}
310+
return
311+
} else if err != nil {
312+
t.Fatalf("unexpected error: %s", err)
313+
}
314+
315+
if !got.RawEquals(test.Want) {
316+
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
317+
}
318+
})
319+
}
320+
}

internal/lang/functions.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ func (s *Scope) Functions() map[string]function.Function {
4242
"cidrhost": funcs.CidrHostFunc,
4343
"cidrnetmask": funcs.CidrNetmaskFunc,
4444
"cidrsubnet": funcs.CidrSubnetFunc,
45+
"cidrsubnets": funcs.CidrSubnetsFunc,
4546
"coalesce": funcs.CoalesceFunc,
4647
"coalescelist": funcs.CoalesceListFunc,
4748
"compact": funcs.CompactFunc,

internal/lang/functions_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,18 @@ func TestFunctions(t *testing.T) {
161161
},
162162
},
163163

164+
"cidrsubnets": {
165+
{
166+
`cidrsubnets("10.0.0.0/8", 8, 8, 16, 8)`,
167+
cty.ListVal([]cty.Value{
168+
cty.StringVal("10.0.0.0/16"),
169+
cty.StringVal("10.1.0.0/16"),
170+
cty.StringVal("10.2.0.0/24"),
171+
cty.StringVal("10.3.0.0/16"),
172+
}),
173+
},
174+
},
175+
164176
"coalesce": {
165177
{
166178
`coalesce("first", "second", "third")`,

0 commit comments

Comments
 (0)