From ed615a90a3766c80889043fc61fa257cf40fdea6 Mon Sep 17 00:00:00 2001 From: Steve Russell Date: Mon, 8 Apr 2024 22:24:33 -0700 Subject: [PATCH] INFRA-3218 Initial commit of ip_compare() Initial commit of implementation and tests for ip_compare(), designed for use with Puppet's `sort()` to sort a series of IP addresses into binary-numerical order, regardless of the human-readable representation in which they were originally written. --- .fixtures.yml | 2 +- lib/puppet/functions/ipcalc/ip_compare.rb | 67 +++++++++++++++++++++++ spec/functions/ip_compare_spec.rb | 13 +++++ 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 lib/puppet/functions/ipcalc/ip_compare.rb create mode 100644 spec/functions/ip_compare_spec.rb diff --git a/.fixtures.yml b/.fixtures.yml index 2296adb..6470f44 100644 --- a/.fixtures.yml +++ b/.fixtures.yml @@ -3,4 +3,4 @@ --- fixtures: forge_modules: -# stdlib: "puppetlabs/stdlib" + stdlib: "puppetlabs/stdlib" diff --git a/lib/puppet/functions/ipcalc/ip_compare.rb b/lib/puppet/functions/ipcalc/ip_compare.rb new file mode 100644 index 0000000..622690a --- /dev/null +++ b/lib/puppet/functions/ipcalc/ip_compare.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'ipaddr' + +Puppet::Functions.create_function(:"ipcalc::ip_compare") do + # Compare two IP addresses and return a `sort()`-compatible Integer comparison result. 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 all bits of the address are equivalent. + # @param left + # The left address for comparison. If no netmask is given, a full-width mask is assumed. + # @param right + # The right address for comparison. If no netmask is given, a full-width mask is assumed. + # @return [Integer[-1, 1]] + # Either 1, 0, or -1 if the left operand is larger than, equal to, or smaller than the right, + # respectively. + # @example Usage with `sort()` + # $my_addr_array.sort |$left, $right| { + # ipcalc::ip_compare($left, $right) + # } + dispatch :ip_compare do + param 'Stdlib::IP::Address::V4', :left + param 'Stdlib::IP::Address::V4', :right + return_type 'Integer[-1, 1]' + end + + dispatch :ip_compare do + param 'Stdlib::IP::Address::V6', :left + param 'Stdlib::IP::Address::V6', :right + return_type 'Integer[-1, 1]' + end + + # We refuse to handle addresses in different families. There isn't a clear precedence between + # them, and if needed, any relative ordering required can be handled in your Puppet code by + # assessing the types handed to `sort()`. There are examples of this in the documentation for the + # Puppet `sort()` function, but a specific example for sorting all IPv6 addresses before all IPv4 + # address is included for reference. + # @example Usage with `sort()` where IPv6 sorts above IPv4 + # $my_ip_array.sort |$left, $right| { + # case [$left, $right] { + # [Stdlib::IP::Address::V6, Stdlib::IP::Address::V4]: { 1 } + # [Stdlib::IP::Address::V4, Stdlib::IP::Address::V6]: { -1 } + # default: { ipcalc::ip_compare($left, $right) } + # } + # } + argument_mismatch :mixed_families do + param 'Stdlib::IP::Address::V4', :left + param 'Stdlib::IP::Address::V6', :right + end + + argument_mismatch :mixed_families do + param 'Stdlib::IP::Address::V6', :left + param 'Stdlib::IP::Address::V4', :right + end + + def ip_compare(left, right) + left_addr = IPAddr.new(left) + right_addr = IPAddr.new(right) + + left_addr <=> right_addr + end + + def mixed_families(*) + 'both addresses must be in the same family' + end +end diff --git a/spec/functions/ip_compare_spec.rb b/spec/functions/ip_compare_spec.rb new file mode 100644 index 0000000..8ab5526 --- /dev/null +++ b/spec/functions/ip_compare_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'ipcalc::ip_compare' do + it { is_expected.to run.with_params('127.0.0.1', '127.0.0.2').and_return(-1) } + it { is_expected.to run.with_params('127.0.0.2', '127.0.0.1').and_return(1) } + it { is_expected.to run.with_params('fe80::1', 'fe80::2').and_return(-1) } + it { is_expected.to run.with_params('fe80::2', 'fe80::1').and_return(1) } + 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