From 28f5ed7ef00e7f1691510b58b7f5363184a0ee9b Mon Sep 17 00:00:00 2001 From: Steve Russell Date: Tue, 9 Apr 2024 00:42:05 -0700 Subject: [PATCH] INFRA-3218 Initial commit of boolean comparators Initial commit of greater_than, equal_or_greater_than, equal_to, equal_or_less_than, and less_than comparator functions and their unit tests. --- .../functions/ipcalc/equal_or_greater_than.rb | 64 ++++++++++++++++++ .../functions/ipcalc/equal_or_less_than.rb | 64 ++++++++++++++++++ lib/puppet/functions/ipcalc/equal_to.rb | 66 +++++++++++++++++++ lib/puppet/functions/ipcalc/greater_than.rb | 63 ++++++++++++++++++ lib/puppet/functions/ipcalc/less_than.rb | 63 ++++++++++++++++++ spec/functions/equal_or_greater_than_spec.rb | 15 +++++ spec/functions/equal_or_less_than_spec.rb | 15 +++++ spec/functions/equal_to_spec.rb | 18 +++++ spec/functions/greater_than_spec.rb | 13 ++++ spec/functions/less_than_spec.rb | 13 ++++ 10 files changed, 394 insertions(+) create mode 100644 lib/puppet/functions/ipcalc/equal_or_greater_than.rb create mode 100644 lib/puppet/functions/ipcalc/equal_or_less_than.rb create mode 100644 lib/puppet/functions/ipcalc/equal_to.rb create mode 100644 lib/puppet/functions/ipcalc/greater_than.rb create mode 100644 lib/puppet/functions/ipcalc/less_than.rb create mode 100644 spec/functions/equal_or_greater_than_spec.rb create mode 100644 spec/functions/equal_or_less_than_spec.rb create mode 100644 spec/functions/equal_to_spec.rb create mode 100644 spec/functions/greater_than_spec.rb create mode 100644 spec/functions/less_than_spec.rb diff --git a/lib/puppet/functions/ipcalc/equal_or_greater_than.rb b/lib/puppet/functions/ipcalc/equal_or_greater_than.rb new file mode 100644 index 0000000..12d03b0 --- /dev/null +++ b/lib/puppet/functions/ipcalc/equal_or_greater_than.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'ipaddr' + +Puppet::Functions.create_function(:"ipcalc::equal_or_greater_than") do + # Compare two IP addresses and return a Boolean indicating whether the first operand is equal to + # (or possibly greater than) the second. Contextually, this makes the most sense when using + # Puppet's dotted function notation, because the function name sits where a comparison operator + # would normally go. This will work with either plain addresses or CIDR-notation addresses; if a + # plain address is supplied, a full-width netmask is assumed. That is, '127.0.0.1' and + # '127.0.0.1/32' are equivalent, just as 'fe80::1' and 'fe80::1/128' are equivalent. The netmask + # IS assessed in the comparison, but is only relevant if the remainder of the address is exactly + # equivalent down to the start of the netmask. If addresses of mixed families are given, an error + # is raised and the catalog will fail. There is no obvious implied relation between the two, and + # even the underlying Ruby implementation will refuse to make comparisons between addresses of + # different families. + # @param first + # The first address for comparison. If no netmask is given, full-width is assumed. + # @param second + # The second address for comparison. If no netmask is given, full-width is assumed. + # @return [Boolean] + # `true` if the first operand is equal to (or greater than) the second, or `false` if it's less + # than the second. + # @example Use with dotted function notation + # $first_ip.equal_or_greater_than($second_ip) ? { + # true => { 'We did it, go team, first operand is equal or bigger' }, + # default => { 'I\'m afraid I have some bad news...' }, + # } + dispatch :equal_or_greater_than do + param 'Stdlib::IP::Address::V4', :first + param 'Stdlib::IP::Address::V4', :second + return_type 'Boolean' + end + + dispatch :equal_or_greater_than do + param 'Stdlib::IP::Address::V6', :first + param 'Stdlib::IP::Address::V6', :second + return_type 'Boolean' + end + + # Refuse to handle addresses in different families. There isn't a clear precedence between the + # two families; if you need to establish an order, you can use type comparison to handle this in + # your Puppet code. + argument_mismatch :mixed_families do + param 'Stdlib::IP::Address::V4', :first + param 'Stdlib::IP::Address::V6', :second + end + + argument_mismatch :mixed_families do + param 'Stdlib::IP::Address::V6', :first + param 'Stdlib::IP::Address::V4', :second + end + + def equal_or_greater_than(first, second) + first_addr = IPAddr.new(first) + second_addr = IPAddr.new(second) + + first_addr >= second_addr + end + + def mixed_families(*) + 'both addresses must be in the same family' + end +end diff --git a/lib/puppet/functions/ipcalc/equal_or_less_than.rb b/lib/puppet/functions/ipcalc/equal_or_less_than.rb new file mode 100644 index 0000000..6188aeb --- /dev/null +++ b/lib/puppet/functions/ipcalc/equal_or_less_than.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'ipaddr' + +Puppet::Functions.create_function(:"ipcalc::equal_or_less_than") do + # Compare two IP addresses and return a Boolean indicating whether the first operand is equal to + # (or possibly less than) the second. Contextually, this makes the most sense when using + # Puppet's dotted function notation, because the function name sits where a comparison operator + # would normally go. This will work with either plain addresses or CIDR-notation addresses; if a + # plain address is supplied, a full-width netmask is assumed. That is, '127.0.0.1' and + # '127.0.0.1/32' are equivalent, just as 'fe80::1' and 'fe80::1/128' are equivalent. The netmask + # IS assessed in the comparison, but is only relevant if the remainder of the address is exactly + # equivalent down to the start of the netmask. If addresses of mixed families are given, an error + # is raised and the catalog will fail. There is no obvious implied relation between the two, and + # even the underlying Ruby implementation will refuse to make comparisons between addresses of + # different families. + # @param first + # The first address for comparison. If no netmask is given, full-width is assumed. + # @param second + # The second address for comparison. If no netmask is given, full-width is assumed. + # @return [Boolean] + # `true` if the first operand is equal to (or less than) the second, or `false` if it's greater + # than the second. + # @example Use with dotted function notation + # $first_ip.equal_or_less_than($second_ip) ? { + # true => { 'We did it, go team, first operand is equal or smaller' }, + # default => { 'I\'m afraid I have some bad news...' }, + # } + dispatch :equal_or_less_than do + param 'Stdlib::IP::Address::V4', :first + param 'Stdlib::IP::Address::V4', :second + return_type 'Boolean' + end + + dispatch :equal_or_less_than do + param 'Stdlib::IP::Address::V6', :first + param 'Stdlib::IP::Address::V6', :second + return_type 'Boolean' + end + + # Refuse to handle addresses in different families. There isn't a clear precedence between the + # two families; if you need to establish an order, you can use type comparison to handle this in + # your Puppet code. + argument_mismatch :mixed_families do + param 'Stdlib::IP::Address::V4', :first + param 'Stdlib::IP::Address::V6', :second + end + + argument_mismatch :mixed_families do + param 'Stdlib::IP::Address::V6', :first + param 'Stdlib::IP::Address::V4', :second + end + + def equal_or_less_than(first, second) + first_addr = IPAddr.new(first) + second_addr = IPAddr.new(second) + + first_addr <= second_addr + end + + def mixed_families(*) + 'both addresses must be in the same family' + end +end diff --git a/lib/puppet/functions/ipcalc/equal_to.rb b/lib/puppet/functions/ipcalc/equal_to.rb new file mode 100644 index 0000000..2156c25 --- /dev/null +++ b/lib/puppet/functions/ipcalc/equal_to.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'ipaddr' + +Puppet::Functions.create_function(:"ipcalc::equal_to") do + # Compare two IP addresses and return a Boolean indicating whether the first operand is equal to + # the second. This differs from comparing the address Strings in that it compares native 32-bit + # (or for IPv6, 128-bit) binary values, regardless of their human-readable representations. For + # IPv4 addresses this is usually not much of a problem, as representations are simpler and + # zero-padded values are relatively rare (enough that Stdlib::IP::Address::V4 filters them out as + # invalid). For IPv6 addresses, however, IPv6's collapsed notation can be a very serious problem, + # and this function allows IPv6 addresses with un-collapsed runs of zeroes, leading zeroes, etc. + # to be compared meaningfully without needing to worry about normalizing their human-readable + # representations first. + # + # Contextually, this function makes the most sense when using Puppet's dotted function notation, + # because the function name sits where a comparison operator would normally go. This will work + # with either plain addresses or CIDR-notation addresses; if a plain address is supplied, a + # full-width netmask is assumed. That is, '127.0.0.1' and '127.0.0.1/32' are equivalent, just as + # 'fe80::1' and 'fe80::1/128' are equivalent. The netmask IS assessed in the comparison, but is + # only relevant if the remainder of the address is exactly equivalent down to the start of the + # netmask. Addresses of mixed families are always considered unequal. + # @param first + # The first address for comparison. If no netmask is given, full-width is assumed. + # @param second + # The second address for comparison. If no netmask is given, full-width is assumed. + # @return [Boolean] + # `true` if the operands are equal, or `false` otherwise. + # @example Use with dotted function notation + # $first_ip.equal_to($second_ip) ? { + # true => { 'We did it, go team, the operands are equal' }, + # default => { 'I\'m afraid I have some bad news...' }, + # } + dispatch :equal_to do + param 'Stdlib::IP::Address::V4', :first + param 'Stdlib::IP::Address::V4', :second + return_type 'Boolean' + end + + dispatch :equal_to do + param 'Stdlib::IP::Address::V6', :first + param 'Stdlib::IP::Address::V6', :second + return_type 'Boolean' + end + + dispatch :mixed_families do + param 'Stdlib::IP::Address::V4', :first + param 'Stdlib::IP::Address::V6', :second + end + + dispatch :mixed_families do + param 'Stdlib::IP::Address::V6', :first + param 'Stdlib::IP::Address::V4', :second + end + + def equal_to(first, second) + first_addr = IPAddr.new(first) + second_addr = IPAddr.new(second) + + first_addr == second_addr + end + + def mixed_families(*) + false + end +end diff --git a/lib/puppet/functions/ipcalc/greater_than.rb b/lib/puppet/functions/ipcalc/greater_than.rb new file mode 100644 index 0000000..92224c2 --- /dev/null +++ b/lib/puppet/functions/ipcalc/greater_than.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'ipaddr' + +Puppet::Functions.create_function(:"ipcalc::greater_than") do + # Compare two IP addresses and return a Boolean indicating whether the first operand is greater + # than the second. Contextually, this makes the most sense when using Puppet's dotted function + # notation, because the function name sits where a comparison operator would normally go. This + # will work with either plain addresses or CIDR-notation addresses; if a plain address is + # supplied, a full-width netmask is assumed. That is, '127.0.0.1' and '127.0.0.1/32' are + # equivalent, just as 'fe80::1' and 'fe80::1/128' are equivalent. The netmask IS assessed in the + # comparison, but is only relevant if the remainder of the address is exactly equivalent down to + # the start of the netmask. If addresses of mixed families are given, an error is raised and the + # catalog will fail. There is no obvious implied relation between the two, and even the + # underlying Ruby implementation will refuse to make comparisons between addresses of different + # families. + # @param first + # The first address for comparison. If no netmask is given, full-width is assumed. + # @param second + # The second address for comparison. If no netmask is given, full-width is assumed. + # @return [Boolean] + # `true` if the first operand is greater than the second, or `false` otherwise. + # @example Use with dotted function notation + # $first_ip.greater_than($second_ip) ? { + # true => { 'We did it, go team, first operand is bigger' }, + # default => { 'I\'m afraid I have some bad news...' }, + # } + dispatch :greater_than do + param 'Stdlib::IP::Address::V4', :first + param 'Stdlib::IP::Address::V4', :second + return_type 'Boolean' + end + + dispatch :greater_than do + param 'Stdlib::IP::Address::V6', :first + param 'Stdlib::IP::Address::V6', :second + return_type 'Boolean' + end + + # Refuse to handle addresses in different families. There isn't a clear precedence between the + # two families; if you need to establish an order, you can use type comparison to handle this in + # your Puppet code. + argument_mismatch :mixed_families do + param 'Stdlib::IP::Address::V4', :first + param 'Stdlib::IP::Address::V6', :second + end + + argument_mismatch :mixed_families do + param 'Stdlib::IP::Address::V6', :first + param 'Stdlib::IP::Address::V4', :second + end + + def greater_than(first, second) + first_addr = IPAddr.new(first) + second_addr = IPAddr.new(second) + + first_addr > second_addr + end + + def mixed_families(*) + 'both addresses must be in the same family' + end +end diff --git a/lib/puppet/functions/ipcalc/less_than.rb b/lib/puppet/functions/ipcalc/less_than.rb new file mode 100644 index 0000000..5319ea3 --- /dev/null +++ b/lib/puppet/functions/ipcalc/less_than.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'ipaddr' + +Puppet::Functions.create_function(:"ipcalc::less_than") do + # Compare two IP addresses and return a Boolean indicating whether the first operand is less than + # the second. Contextually, this makes the most sense when using Puppet's dotted function + # notation, because the function name sits where a comparison operator would normally go. This + # will work with either plain addresses or CIDR-notation addresses; if a plain address is + # supplied, a full-width netmask is assumed. That is, '127.0.0.1' and '127.0.0.1/32' are + # equivalent, just as 'fe80::1' and 'fe80::1/128' are equivalent. The netmask IS assessed in the + # comparison, but is only relevant if the remainder of the address is exactly equivalent down to + # the start of the netmask. If addresses of mixed families are given, an error is raised and the + # catalog will fail. There is no obvious implied relation between the two, and even the + # underlying Ruby implementation will refuse to make comparisons between addresses of different + # families. + # @param first + # The first address for comparison. If no netmask is given, full-width is assumed. + # @param second + # The second address for comparison. If no netmask is given, full-width is assumed. + # @return [Boolean] + # `true` if the first operand is less than the second, or `false` otherwise. + # @example Use with dotted function notation + # $first_ip.less_than($second_ip) ? { + # true => { 'We did it, go team, first operand is smaller' }, + # default => { 'I\'m afraid I have some bad news...' }, + # } + dispatch :less_than do + param 'Stdlib::IP::Address::V4', :first + param 'Stdlib::IP::Address::V4', :second + return_type 'Boolean' + end + + dispatch :less_than do + param 'Stdlib::IP::Address::V6', :first + param 'Stdlib::IP::Address::V6', :second + return_type 'Boolean' + end + + # Refuse to handle addresses in different families. There isn't a clear precedence between the + # two families; if you need to establish an order, you can use type comparison to handle this in + # your Puppet code. + argument_mismatch :mixed_families do + param 'Stdlib::IP::Address::V4', :first + param 'Stdlib::IP::Address::V6', :second + end + + argument_mismatch :mixed_families do + param 'Stdlib::IP::Address::V6', :first + param 'Stdlib::IP::Address::V4', :second + end + + def less_than(first, second) + first_addr = IPAddr.new(first) + second_addr = IPAddr.new(second) + + first_addr < second_addr + end + + def mixed_families(*) + 'both addresses must be in the same family' + end +end diff --git a/spec/functions/equal_or_greater_than_spec.rb b/spec/functions/equal_or_greater_than_spec.rb new file mode 100644 index 0000000..eb16324 --- /dev/null +++ b/spec/functions/equal_or_greater_than_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'ipcalc::equal_or_greater_than' do + it { is_expected.to run.with_params('127.0.0.1', '127.0.0.2').and_return(false) } + it { is_expected.to run.with_params('127.0.0.2', '127.0.0.1').and_return(true) } + it { is_expected.to run.with_params('127.0.0.1', '127.0.0.1').and_return(true) } + it { is_expected.to run.with_params('fe80::1', 'fe80::2').and_return(false) } + it { is_expected.to run.with_params('fe80::2', 'fe80::1').and_return(true) } + it { is_expected.to run.with_params('fe80::1', 'fe80::1').and_return(true) } + it { is_expected.to run.with_params(nil).and_raise_error(StandardError) } + it { is_expected.to run.with_params('127.0.0.1', 'fe80::1').and_raise_error(StandardError) } + it { is_expected.to run.with_params('fe80::1', '127.0.0.1').and_raise_error(StandardError) } +end diff --git a/spec/functions/equal_or_less_than_spec.rb b/spec/functions/equal_or_less_than_spec.rb new file mode 100644 index 0000000..b958fd3 --- /dev/null +++ b/spec/functions/equal_or_less_than_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'ipcalc::equal_or_less_than' do + it { is_expected.to run.with_params('127.0.0.1', '127.0.0.2').and_return(true) } + it { is_expected.to run.with_params('127.0.0.2', '127.0.0.1').and_return(false) } + it { is_expected.to run.with_params('127.0.0.1', '127.0.0.1').and_return(true) } + it { is_expected.to run.with_params('fe80::1', 'fe80::2').and_return(true) } + it { is_expected.to run.with_params('fe80::2', 'fe80::1').and_return(false) } + it { is_expected.to run.with_params('fe80::1', 'fe80::1').and_return(true) } + it { is_expected.to run.with_params(nil).and_raise_error(StandardError) } + it { is_expected.to run.with_params('127.0.0.1', 'fe80::1').and_raise_error(StandardError) } + it { is_expected.to run.with_params('fe80::1', '127.0.0.1').and_raise_error(StandardError) } +end diff --git a/spec/functions/equal_to_spec.rb b/spec/functions/equal_to_spec.rb new file mode 100644 index 0000000..7b36ca9 --- /dev/null +++ b/spec/functions/equal_to_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'ipcalc::equal_to' do + it { is_expected.to run.with_params('127.0.0.1', '127.0.0.2').and_return(false) } + it { is_expected.to run.with_params('127.0.0.2', '127.0.0.1').and_return(false) } + # Zero-padded values are rejected by type validation and by the underlying Ruby implementation + it { is_expected.to run.with_params('127.00.0.1', '127.0.0.1').and_raise_error(StandardError) } + it { is_expected.to run.with_params('127.0.0.1', '127.0.0.1').and_return(true) } + it { is_expected.to run.with_params('fe80::1', 'fe80::2').and_return(false) } + it { is_expected.to run.with_params('fe80::2', 'fe80::1').and_return(false) } + it { is_expected.to run.with_params('fe80:0::001', 'fe80::1').and_return(true) } + it { is_expected.to run.with_params('fe80::1', 'fe80::1').and_return(true) } + it { is_expected.to run.with_params(nil).and_raise_error(StandardError) } + it { is_expected.to run.with_params('127.0.0.1', 'fe80::1').and_return(false) } + it { is_expected.to run.with_params('fe80::1', '127.0.0.1').and_return(false) } +end diff --git a/spec/functions/greater_than_spec.rb b/spec/functions/greater_than_spec.rb new file mode 100644 index 0000000..c9fc0cc --- /dev/null +++ b/spec/functions/greater_than_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'ipcalc::greater_than' do + it { is_expected.to run.with_params('127.0.0.1', '127.0.0.2').and_return(false) } + it { is_expected.to run.with_params('127.0.0.2', '127.0.0.1').and_return(true) } + it { is_expected.to run.with_params('fe80::1', 'fe80::2').and_return(false) } + it { is_expected.to run.with_params('fe80::2', 'fe80::1').and_return(true) } + it { is_expected.to run.with_params(nil).and_raise_error(StandardError) } + it { is_expected.to run.with_params('127.0.0.1', 'fe80::1').and_raise_error(StandardError) } + it { is_expected.to run.with_params('fe80::1', '127.0.0.1').and_raise_error(StandardError) } +end diff --git a/spec/functions/less_than_spec.rb b/spec/functions/less_than_spec.rb new file mode 100644 index 0000000..d342d20 --- /dev/null +++ b/spec/functions/less_than_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: false + +require 'spec_helper' + +describe 'ipcalc::less_than' do + it { is_expected.to run.with_params('127.0.0.1', '127.0.0.2').and_return(true) } + it { is_expected.to run.with_params('127.0.0.2', '127.0.0.1').and_return(false) } + it { is_expected.to run.with_params('fe80::1', 'fe80::2').and_return(true) } + it { is_expected.to run.with_params('fe80::2', 'fe80::1').and_return(false) } + it { is_expected.to run.with_params(nil).and_raise_error(StandardError) } + it { is_expected.to run.with_params('127.0.0.1', 'fe80::1').and_raise_error(StandardError) } + it { is_expected.to run.with_params('fe80::1', '127.0.0.1').and_raise_error(StandardError) } +end