From ef161e1c1b57545e183537b14ff3f7c7e7fefae4 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Thu, 22 Oct 2015 08:10:42 -0700 Subject: [PATCH] Various interpolation functions for CIDR range manipulation. These new functions allow Terraform to be used for network address space planning tasks, and make it easier to produce reusable modules that contain or depend on network infrastructure. For example: - cidrsubnet allows an aws_subnet to derive its CIDR prefix from its parent aws_vpc. - cidrhost allows a fixed IP address for a resource to be assigned within an address range defined elsewhere. - cidrnetmask provides the dotted-decimal form of a prefix length that is accepted by some systems such as routing tables and static network interface configuration files. The bulk of the work here is done by an external library I authored called go-cidr. It is MIT licensed and was implemented primarily for the purpose of using it within Terraform. It has its own unit tests and so the unit tests within this change focus on simple success cases and on the correct handling of the various error cases. --- config/interpolate_funcs.go | 91 +++++++++++++++ config/interpolate_funcs_test.go | 109 ++++++++++++++++++ .../docs/configuration/interpolation.html.md | 16 +++ 3 files changed, 216 insertions(+) diff --git a/config/interpolate_funcs.go b/config/interpolate_funcs.go index 1b58ac93c..e98ade2f0 100644 --- a/config/interpolate_funcs.go +++ b/config/interpolate_funcs.go @@ -6,11 +6,13 @@ import ( "errors" "fmt" "io/ioutil" + "net" "regexp" "sort" "strconv" "strings" + "github.com/apparentlymart/go-cidr/cidr" "github.com/hashicorp/terraform/config/lang/ast" "github.com/mitchellh/go-homedir" ) @@ -20,6 +22,9 @@ var Funcs map[string]ast.Function func init() { Funcs = map[string]ast.Function{ + "cidrhost": interpolationFuncCidrHost(), + "cidrnetmask": interpolationFuncCidrNetmask(), + "cidrsubnet": interpolationFuncCidrSubnet(), "compact": interpolationFuncCompact(), "concat": interpolationFuncConcat(), "element": interpolationFuncElement(), @@ -54,6 +59,92 @@ func interpolationFuncCompact() ast.Function { } } +// interpolationFuncCidrHost implements the "cidrhost" function that +// fills in the host part of a CIDR range address to create a single +// host address +func interpolationFuncCidrHost() ast.Function { + return ast.Function{ + ArgTypes: []ast.Type{ + ast.TypeString, // starting CIDR mask + ast.TypeInt, // host number to insert + }, + ReturnType: ast.TypeString, + Variadic: false, + Callback: func(args []interface{}) (interface{}, error) { + hostNum := args[1].(int) + _, network, err := net.ParseCIDR(args[0].(string)) + if err != nil { + return nil, fmt.Errorf("invalid CIDR expression: %s", err) + } + + ip, err := cidr.Host(network, hostNum) + if err != nil { + return nil, err + } + + return ip.String(), nil + }, + } +} + +// interpolationFuncCidrNetmask implements the "cidrnetmask" function +// that returns the subnet mask in IP address notation. +func interpolationFuncCidrNetmask() ast.Function { + return ast.Function{ + ArgTypes: []ast.Type{ + ast.TypeString, // CIDR mask + }, + ReturnType: ast.TypeString, + Variadic: false, + Callback: func(args []interface{}) (interface{}, error) { + _, network, err := net.ParseCIDR(args[0].(string)) + if err != nil { + return nil, fmt.Errorf("invalid CIDR expression: %s", err) + } + + return net.IP(network.Mask).String(), nil + }, + } +} + +// interpolationFuncCidrSubnet implements the "cidrsubnet" function that +// adds an additional subnet of the given length onto an existing +// IP block expressed in CIDR notation. +func interpolationFuncCidrSubnet() ast.Function { + return ast.Function{ + ArgTypes: []ast.Type{ + ast.TypeString, // starting CIDR mask + ast.TypeInt, // number of bits to extend the prefix + ast.TypeInt, // network number to append to the prefix + }, + ReturnType: ast.TypeString, + Variadic: false, + Callback: func(args []interface{}) (interface{}, error) { + extraBits := args[1].(int) + subnetNum := args[2].(int) + _, network, err := net.ParseCIDR(args[0].(string)) + if err != nil { + return nil, fmt.Errorf("invalid CIDR expression: %s", err) + } + + // 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 extraBits > 32 { + return nil, fmt.Errorf("may not extend prefix by more than 32 bits") + } + + newNetwork, err := cidr.Subnet(network, extraBits, subnetNum) + if err != nil { + return nil, err + } + + return newNetwork.String(), nil + }, + } +} + // interpolationFuncConcat implements the "concat" function that // concatenates multiple strings. This isn't actually necessary anymore // since our language supports string concat natively, but for backwards diff --git a/config/interpolate_funcs_test.go b/config/interpolate_funcs_test.go index f40f56860..7b7311fd4 100644 --- a/config/interpolate_funcs_test.go +++ b/config/interpolate_funcs_test.go @@ -38,6 +38,115 @@ func TestInterpolateFuncCompact(t *testing.T) { }) } +func TestInterpolateFuncCidrHost(t *testing.T) { + testFunction(t, testFunctionConfig{ + Cases: []testFunctionCase{ + { + `${cidrhost("192.168.1.0/24", 5)}`, + "192.168.1.5", + false, + }, + { + `${cidrhost("192.168.1.0/30", 255)}`, + nil, + true, // 255 doesn't fit in two bits + }, + { + `${cidrhost("not-a-cidr", 6)}`, + nil, + true, // not a valid CIDR mask + }, + { + `${cidrhost("10.256.0.0/8", 6)}`, + nil, + true, // can't have an octet >255 + }, + }, + }) +} + +func TestInterpolateFuncCidrNetmask(t *testing.T) { + testFunction(t, testFunctionConfig{ + Cases: []testFunctionCase{ + { + `${cidrnetmask("192.168.1.0/24")}`, + "255.255.255.0", + false, + }, + { + `${cidrnetmask("192.168.1.0/32")}`, + "255.255.255.255", + false, + }, + { + `${cidrnetmask("0.0.0.0/0")}`, + "0.0.0.0", + false, + }, + { + // This doesn't really make sense for IPv6 networks + // but it ought to do something sensible anyway. + `${cidrnetmask("1::/64")}`, + "ffff:ffff:ffff:ffff::", + false, + }, + { + `${cidrnetmask("not-a-cidr")}`, + nil, + true, // not a valid CIDR mask + }, + { + `${cidrnetmask("10.256.0.0/8")}`, + nil, + true, // can't have an octet >255 + }, + }, + }) +} + +func TestInterpolateFuncCidrSubnet(t *testing.T) { + testFunction(t, testFunctionConfig{ + Cases: []testFunctionCase{ + { + `${cidrsubnet("192.168.2.0/20", 4, 6)}`, + "192.168.6.0/24", + false, + }, + { + `${cidrsubnet("fe80::/48", 16, 6)}`, + "fe80:0:0:6::/64", + false, + }, + { + // IPv4 address encoded in IPv6 syntax gets normalized + `${cidrsubnet("::ffff:192.168.0.0/112", 8, 6)}`, + "192.168.6.0/24", + false, + }, + { + `${cidrsubnet("192.168.0.0/30", 4, 6)}`, + nil, + true, // not enough bits left + }, + { + `${cidrsubnet("192.168.0.0/16", 2, 16)}`, + nil, + true, // can't encode 16 in 2 bits + }, + { + `${cidrsubnet("not-a-cidr", 4, 6)}`, + nil, + true, // not a valid CIDR mask + }, + { + `${cidrsubnet("10.256.0.0/8", 4, 6)}`, + nil, + true, // can't have an octet >255 + }, + }, + }) +} + func TestInterpolateFuncDeprecatedConcat(t *testing.T) { testFunction(t, testFunctionConfig{ Cases: []testFunctionCase{ diff --git a/website/source/docs/configuration/interpolation.html.md b/website/source/docs/configuration/interpolation.html.md index 940839076..049c71825 100644 --- a/website/source/docs/configuration/interpolation.html.md +++ b/website/source/docs/configuration/interpolation.html.md @@ -80,6 +80,22 @@ The supported built-in functions are: * `base64encode(string)` - Returns a base64-encoded representation of the given string. + * `cidrhost(iprange, hostnum)` - Takes an IP address range in CIDR notation + and creates an IP address with the given host number. For example, + ``cidrhost("10.0.0.0/8", 2)`` returns ``10.0.0.2``. + + * `cidrnetmask(iprange)` - Takes an IP address range in CIDR notation + and returns the address-formatted subnet mask format that some + systems expect for IPv4 interfaces. For example, + ``cidrmask("10.0.0.0/8")`` returns ``255.0.0.0``. Not applicable + to IPv6 networks since CIDR notation is the only valid notation for + IPv6. + + * `cidrsubnet(iprange, newbits, netnum)` - Takes an IP address range in + CIDR notation (like ``10.0.0.0/8``) and extends its prefix to include an + additional subnet number. For example, + ``cidrsubnet("10.0.0.0/8", 8, 2)`` returns ``10.2.0.0/16``. + * `compact(list)` - Removes empty string elements from a list. This can be useful in some cases, for example when passing joined lists as module variables or when parsing module outputs.