From f84ab99b7da68a53218b1762a66a563f220d2e5c Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Thu, 19 Sep 2019 19:06:15 -0700 Subject: [PATCH] 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. --- lang/funcs/cidr.go | 89 +++++++++++++++ lang/funcs/cidr_test.go | 104 ++++++++++++++++++ lang/functions.go | 1 + lang/functions_test.go | 12 ++ .../functions/cidrsubnet.html.md | 7 ++ .../functions/cidrsubnets.html.md | 99 +++++++++++++++++ 6 files changed, 312 insertions(+) create mode 100644 website/docs/configuration/functions/cidrsubnets.html.md diff --git a/lang/funcs/cidr.go b/lang/funcs/cidr.go index 6ce8aa9fa..8c0751489 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 e9fd6c7a3..824a928e2 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 602b23daa..8c089a552 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 64652ab58..7f43b06cd 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 8b0b51c58..6988ce3f6 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 000000000..b7fb9e956 --- /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.