diff --git a/lang/funcs/cidr.go b/lang/funcs/cidr.go index 6ce8aa9fa2..8c07514896 100644 --- a/lang/funcs/cidr.go +++ b/lang/funcs/cidr.go @@ -113,6 +113,86 @@ var CidrSubnetFunc = function.New(&function.Spec{ }, }) +// CidrSubnetsFunc is similar to CidrSubnetFunc but calculates many consecutive +// subnet addresses at once, rather than just a single subnet extension. +var CidrSubnetsFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "prefix", + Type: cty.String, + }, + }, + VarParam: &function.Parameter{ + Name: "newbits", + Type: cty.Number, + }, + Type: function.StaticReturnType(cty.List(cty.String)), + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + _, network, err := net.ParseCIDR(args[0].AsString()) + if err != nil { + return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "invalid CIDR expression: %s", err) + } + startPrefixLen, _ := network.Mask.Size() + + prefixLengthArgs := args[1:] + if len(prefixLengthArgs) == 0 { + return cty.ListValEmpty(cty.String), nil + } + + var firstLength int + if err := gocty.FromCtyValue(prefixLengthArgs[0], &firstLength); err != nil { + return cty.UnknownVal(cty.String), function.NewArgError(1, err) + } + firstLength += startPrefixLen + + retVals := make([]cty.Value, len(prefixLengthArgs)) + + current, _ := cidr.PreviousSubnet(network, firstLength) + for i, lengthArg := range prefixLengthArgs { + var length int + if err := gocty.FromCtyValue(lengthArg, &length); err != nil { + return cty.UnknownVal(cty.String), function.NewArgError(i+1, err) + } + + if length < 1 { + return cty.UnknownVal(cty.String), function.NewArgErrorf(i+1, "must extend prefix by at least one bit") + } + // For portability with 32-bit systems where the subnet number + // will be a 32-bit int, we only allow extension of 32 bits in + // one call even if we're running on a 64-bit machine. + // (Of course, this is significant only for IPv6.) + if length > 32 { + return cty.UnknownVal(cty.String), function.NewArgErrorf(i+1, "may not extend prefix by more than 32 bits") + } + length += startPrefixLen + if length > (len(network.IP) * 8) { + protocol := "IP" + switch len(network.IP) * 8 { + case 32: + protocol = "IPv4" + case 128: + protocol = "IPv6" + } + 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) + } + + next, rollover := cidr.NextSubnet(current, length) + if rollover || !network.Contains(next.IP) { + // If we run out of suffix bits in the base CIDR prefix then + // NextSubnet will start incrementing the prefix bits, which + // we don't allow because it would then allocate addresses + // outside of the caller's given prefix. + 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()) + } + + current = next + retVals[i] = cty.StringVal(current.String()) + } + + return cty.ListVal(retVals), nil + }, +}) + // CidrHost calculates a full host IP address within a given IP network address prefix. func CidrHost(prefix, hostnum cty.Value) (cty.Value, error) { return CidrHostFunc.Call([]cty.Value{prefix, hostnum}) @@ -127,3 +207,12 @@ func CidrNetmask(prefix cty.Value) (cty.Value, error) { func CidrSubnet(prefix, newbits, netnum cty.Value) (cty.Value, error) { return CidrSubnetFunc.Call([]cty.Value{prefix, newbits, netnum}) } + +// CidrSubnets calculates a sequence of consecutive subnet prefixes that may +// be of different prefix lengths under a common base prefix. +func CidrSubnets(prefix cty.Value, newbits ...cty.Value) (cty.Value, error) { + args := make([]cty.Value, len(newbits)+1) + args[0] = prefix + copy(args[1:], newbits) + return CidrSubnetsFunc.Call(args) +} diff --git a/lang/funcs/cidr_test.go b/lang/funcs/cidr_test.go index e9fd6c7a3d..824a928e2e 100644 --- a/lang/funcs/cidr_test.go +++ b/lang/funcs/cidr_test.go @@ -214,3 +214,107 @@ func TestCidrSubnet(t *testing.T) { }) } } +func TestCidrSubnets(t *testing.T) { + tests := []struct { + Prefix cty.Value + Newbits []cty.Value + Want cty.Value + Err string + }{ + { + cty.StringVal("10.0.0.0/21"), + []cty.Value{ + cty.NumberIntVal(3), + cty.NumberIntVal(3), + cty.NumberIntVal(3), + cty.NumberIntVal(4), + cty.NumberIntVal(4), + cty.NumberIntVal(4), + cty.NumberIntVal(7), + cty.NumberIntVal(7), + cty.NumberIntVal(7), + }, + cty.ListVal([]cty.Value{ + cty.StringVal("10.0.0.0/24"), + cty.StringVal("10.0.1.0/24"), + cty.StringVal("10.0.2.0/24"), + cty.StringVal("10.0.3.0/25"), + cty.StringVal("10.0.3.128/25"), + cty.StringVal("10.0.4.0/25"), + cty.StringVal("10.0.4.128/28"), + cty.StringVal("10.0.4.144/28"), + cty.StringVal("10.0.4.160/28"), + }), + ``, + }, + { + cty.StringVal("10.0.0.0/30"), + []cty.Value{ + cty.NumberIntVal(1), + cty.NumberIntVal(3), + }, + cty.UnknownVal(cty.List(cty.String)), + `would extend prefix to 33 bits, which is too long for an IPv4 address`, + }, + { + cty.StringVal("10.0.0.0/8"), + []cty.Value{ + cty.NumberIntVal(1), + cty.NumberIntVal(1), + cty.NumberIntVal(1), + }, + cty.UnknownVal(cty.List(cty.String)), + `not enough remaining address space for a subnet with a prefix of 9 bits after 10.128.0.0/9`, + }, + { + cty.StringVal("10.0.0.0/8"), + []cty.Value{ + cty.NumberIntVal(1), + cty.NumberIntVal(0), + }, + cty.UnknownVal(cty.List(cty.String)), + `must extend prefix by at least one bit`, + }, + { + cty.StringVal("10.0.0.0/8"), + []cty.Value{ + cty.NumberIntVal(1), + cty.NumberIntVal(-1), + }, + cty.UnknownVal(cty.List(cty.String)), + `must extend prefix by at least one bit`, + }, + { + cty.StringVal("fe80::/48"), + []cty.Value{ + cty.NumberIntVal(1), + cty.NumberIntVal(33), + }, + cty.UnknownVal(cty.List(cty.String)), + `may not extend prefix by more than 32 bits`, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("cidrsubnets(%#v, %#v)", test.Prefix, test.Newbits), func(t *testing.T) { + got, err := CidrSubnets(test.Prefix, test.Newbits...) + wantErr := test.Err != "" + + if wantErr { + if err == nil { + t.Fatal("succeeded; want error") + } + if err.Error() != test.Err { + t.Fatalf("wrong error\ngot: %s\nwant: %s", err.Error(), test.Err) + } + return + } else if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !got.RawEquals(test.Want) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) + } + }) + } +} diff --git a/lang/functions.go b/lang/functions.go index 602b23daad..8c089a5529 100644 --- a/lang/functions.go +++ b/lang/functions.go @@ -44,6 +44,7 @@ func (s *Scope) Functions() map[string]function.Function { "cidrhost": funcs.CidrHostFunc, "cidrnetmask": funcs.CidrNetmaskFunc, "cidrsubnet": funcs.CidrSubnetFunc, + "cidrsubnets": funcs.CidrSubnetsFunc, "coalesce": funcs.CoalesceFunc, "coalescelist": funcs.CoalesceListFunc, "compact": funcs.CompactFunc, diff --git a/lang/functions_test.go b/lang/functions_test.go index 64652ab588..7f43b06cdb 100644 --- a/lang/functions_test.go +++ b/lang/functions_test.go @@ -161,6 +161,18 @@ func TestFunctions(t *testing.T) { }, }, + "cidrsubnets": { + { + `cidrsubnets("10.0.0.0/8", 8, 8, 16, 8)`, + cty.ListVal([]cty.Value{ + cty.StringVal("10.0.0.0/16"), + cty.StringVal("10.1.0.0/16"), + cty.StringVal("10.2.0.0/24"), + cty.StringVal("10.3.0.0/16"), + }), + }, + }, + "coalesce": { { `coalesce("first", "second", "third")`, diff --git a/website/docs/configuration/functions/cidrsubnet.html.md b/website/docs/configuration/functions/cidrsubnet.html.md index 8b0b51c587..6988ce3f61 100644 --- a/website/docs/configuration/functions/cidrsubnet.html.md +++ b/website/docs/configuration/functions/cidrsubnet.html.md @@ -33,6 +33,11 @@ additional bits added to the prefix. This function accepts both IPv6 and IPv4 prefixes, and the result always uses the same addressing scheme as the given prefix. +Unlike the related function [`cidrsubnets`](./cidrsubnets.html), `cidrsubnet` +allows you to give a specific network number to use. `cidrsubnets` can allocate +multiple network addresses at once, but numbers them automatically starting +with zero. + ## Examples ``` @@ -163,3 +168,5 @@ For more information on CIDR notation and subnetting, see within a given network address prefix. * [`cidrnetmask`](./cidrnetmask.html) converts an IPv4 network prefix in CIDR notation into netmask notation. +* [`cidrsubnets`](./cidrsubnets.html) can allocate multiple consecutive + addresses under a prefix at once, numbering them automatically. diff --git a/website/docs/configuration/functions/cidrsubnets.html.md b/website/docs/configuration/functions/cidrsubnets.html.md new file mode 100644 index 0000000000..b7fb9e9568 --- /dev/null +++ b/website/docs/configuration/functions/cidrsubnets.html.md @@ -0,0 +1,99 @@ +--- +layout: "functions" +page_title: "cidrsubnets - Functions - Configuration Language" +sidebar_current: "docs-funcs-ipnet-cidrsubnets" +description: |- + The cidrsubnets function calculates a sequence of consecutive IP address + ranges within a particular CIDR prefix. +--- + +# `cidrsubnet` Function + +-> **Note:** This page is about Terraform 0.12 and later. For Terraform 0.11 and +earlier, see +[0.11 Configuration Language: Interpolation Syntax](../../configuration-0-11/interpolation.html). + +`cidrsubnet` calculates a sequence of consecutive IP address ranges within +a particular CIDR prefix. + +```hcl +cidrsubnet(prefix, newbits...) +``` + +`prefix` must be given in CIDR notation, as defined in +[RFC 4632 section 3.1](https://tools.ietf.org/html/rfc4632#section-3.1). + +The remaining arguments, indicated as `newbits` above, each specify the number +of additional network prefix bits for one returned address range. The return +value is therefore a list with one element per `newbits` argument, each +a string containing an address range in CIDR notation. + +For more information on IP addressing concepts, see the documentation for the +related function [`cidrsubnet`](./cidrsubnet.html). `cidrsubnet` calculates +a single subnet address within a prefix while allowing you to specify its +subnet number, while `cidrsubnets` can calculate many at once, potentially of +different sizes, and assigns subnet numbers automatically. + +When using this function to partition an address space as part of a network +address plan, you must not change any of the existing arguments once network +addresses have been assigned to real infrastructure, or else later address +assignments will be invalidated. However, you _can_ append new arguments to +existing calls safely, as long as there is sufficient address space available. + +This function accepts both IPv6 and IPv4 prefixes, and the result always uses +the same addressing scheme as the given prefix. + +## Examples + +``` +> cidrsubnets("10.1.0.0/16", 4, 4, 8, 4) +[ + "10.1.0.0/20", + "10.1.16.0/20", + "10.1.32.0/24", + "10.1.48.0/20", +] + +> cidrsubnets("fd00:fd12:3456:7890::/56", 16, 16, 16, 32) +[ + "fd00:fd12:3456:7800::/72", + "fd00:fd12:3456:7800:100::/72", + "fd00:fd12:3456:7800:200::/72", + "fd00:fd12:3456:7800:300::/88", +] +``` + +You can use nested `cidrsubnets` calls with +[`for` expressions](/docs/configuration/expressions.html#for-expressions) +to concisely allocate groups of network address blocks: + +``` +> [for cidr_block in cidrsubnets("10.0.0.0/8", 8, 8, 8, 8) : cidrsubnets(cidr_block, 4, 4)] +[ + [ + "10.0.0.0/20", + "10.0.16.0/20", + ], + [ + "10.1.0.0/20", + "10.1.16.0/20", + ], + [ + "10.2.0.0/20", + "10.2.16.0/20", + ], + [ + "10.3.0.0/20", + "10.3.16.0/20", + ], +] +``` + +## Related Functions + +* [`cidrhost`](./cidrhost.html) calculates the IP address for a single host + within a given network address prefix. +* [`cidrnetmask`](./cidrnetmask.html) converts an IPv4 network prefix in CIDR + notation into netmask notation. +* [`cidrsubnet`](./cidrsubnet.html) calculates a single subnet address, allowing + you to specify its network number.