From 1dd922d7ab90edab4f0186613ed38c220953793b Mon Sep 17 00:00:00 2001 From: Alexander Fisher Date: Tue, 25 Apr 2023 18:24:12 +0100 Subject: [PATCH] Modernise `has_interface_with` function Convert the function to the modern function API as a namespaced function and use the `networking` fact instead of legacy facts. A non-namespaced shim is also created (but marked deprecated), to preserve compatibility. --- REFERENCE.md | 78 ++++++++++ lib/puppet/functions/has_interface_with.rb | 12 ++ .../functions/stdlib/has_interface_with.rb | 48 +++++++ spec/functions/has_interface_with_spec.rb | 134 ++++++++++++++++-- 4 files changed, 258 insertions(+), 14 deletions(-) create mode 100644 lib/puppet/functions/has_interface_with.rb create mode 100644 lib/puppet/functions/stdlib/has_interface_with.rb diff --git a/REFERENCE.md b/REFERENCE.md index c6f9b3f2e..448c3d828 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -82,6 +82,7 @@ environment. * [`glob`](#glob): Uses same patterns as Dir#glob. * [`grep`](#grep): This function searches through an array and returns any elements that match the provided regular expression. +* [`has_interface_with`](#has_interface_with): DEPRECATED. Use the namespaced function [`stdlib::has_interface_with`](#stdlibhas_interface_with) instead. * [`has_interface_with`](#has_interface_with): Returns boolean based on kind and value. * [`has_ip_address`](#has_ip_address): Returns true if the client has the requested IP address on some interface. * [`has_ip_network`](#has_ip_network): Returns true if the client has an IP address within the requested network. @@ -179,6 +180,7 @@ the provided regular expression. * [`stdlib::ensure`](#stdlib--ensure): function to cast ensure parameter to resource specific value * [`stdlib::extname`](#stdlib--extname): Returns the Extension (the Portion of Filename in Path starting from the last Period). +* [`stdlib::has_interface_with`](#stdlib--has_interface_with): Returns boolean based on network interfaces present and their attribute values. * [`stdlib::ip_in_range`](#stdlib--ip_in_range): Returns true if the ipaddress is within the given CIDRs * [`stdlib::sha256`](#stdlib--sha256): Run a SHA256 calculation against a given value. * [`stdlib::start_with`](#stdlib--start_with): Returns true if str starts with one of the prefixes given. Each of the prefixes should be a String. @@ -2584,6 +2586,24 @@ Returns: `Any` array of elements that match the provided regular expression. grep(['aaa','bbb','ccc','aaaddd'], 'aaa') # Returns ['aaa','aaaddd'] ``` +### `has_interface_with` + +Type: Ruby 4.x API + +DEPRECATED. Use the namespaced function [`stdlib::has_interface_with`](#stdlibhas_interface_with) instead. + +#### `has_interface_with(Any *$args)` + +The has_interface_with function. + +Returns: `Any` + +##### `*args` + +Data type: `Any` + + + ### `has_interface_with` Type: Ruby 3.x API @@ -4881,6 +4901,64 @@ Data type: `String` The Filename +### `stdlib::has_interface_with` + +Type: Ruby 4.x API + +Can be called with one, or two arguments. + +#### `stdlib::has_interface_with(String[1] $interface)` + +The stdlib::has_interface_with function. + +Returns: `Boolean` Returns `true` if `interface` exists and `false` otherwise + +##### Examples + +###### When called with a single argument, the presence of the interface is checked + +```puppet +stdlib::has_interface_with('lo') # Returns `true` +``` + +##### `interface` + +Data type: `String[1]` + +The name of an interface + +#### `stdlib::has_interface_with(Enum['macaddress','netmask','ipaddress','network','ip','mac'] $kind, String[1] $value)` + +The stdlib::has_interface_with function. + +Returns: `Boolean` Returns `true` if any of the interfaces in the `networking` fact has a `kind` attribute with the value `value`. Otherwise returns `false` + +##### Examples + +###### Checking if an interface exists with a given mac address + +```puppet +stdlib::has_interface_with('macaddress', 'x:x:x:x:x:x') # Returns `false` +``` + +###### Checking if an interface exists with a given IP address + +```puppet +stdlib::has_interface_with('ipaddress', '127.0.0.1') # Returns `true` +``` + +##### `kind` + +Data type: `Enum['macaddress','netmask','ipaddress','network','ip','mac']` + +A supported interface attribute + +##### `value` + +Data type: `String[1]` + +The value of the attribute + ### `stdlib::ip_in_range` Type: Ruby 4.x API diff --git a/lib/puppet/functions/has_interface_with.rb b/lib/puppet/functions/has_interface_with.rb new file mode 100644 index 000000000..d166bb42f --- /dev/null +++ b/lib/puppet/functions/has_interface_with.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# @summary DEPRECATED. Use the namespaced function [`stdlib::has_interface_with`](#stdlibhas_interface_with) instead. +Puppet::Functions.create_function(:has_interface_with) do + dispatch :deprecation_gen do + repeated_param 'Any', :args + end + def deprecation_gen(*args) + call_function('deprecation', 'has_interface_with', 'This method is deprecated, please use stdlib::has_interface_with instead.') + call_function('stdlib::has_interface_with', *args) + end +end diff --git a/lib/puppet/functions/stdlib/has_interface_with.rb b/lib/puppet/functions/stdlib/has_interface_with.rb new file mode 100644 index 000000000..8ec4a04eb --- /dev/null +++ b/lib/puppet/functions/stdlib/has_interface_with.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +# @summary Returns boolean based on network interfaces present and their attribute values. +# +# Can be called with one, or two arguments. +Puppet::Functions.create_function(:'stdlib::has_interface_with') do + # @param interface + # The name of an interface + # @return [Boolean] Returns `true` if `interface` exists and `false` otherwise + # @example When called with a single argument, the presence of the interface is checked + # stdlib::has_interface_with('lo') # Returns `true` + dispatch :has_interface do + param 'String[1]', :interface + return_type 'Boolean' + end + + # @param kind + # A supported interface attribute + # @param value + # The value of the attribute + # @return [Boolean] Returns `true` if any of the interfaces in the `networking` fact has a `kind` attribute with the value `value`. Otherwise returns `false` + # @example Checking if an interface exists with a given mac address + # stdlib::has_interface_with('macaddress', 'x:x:x:x:x:x') # Returns `false` + # @example Checking if an interface exists with a given IP address + # stdlib::has_interface_with('ipaddress', '127.0.0.1') # Returns `true` + dispatch :has_interface_with do + param "Enum['macaddress','netmask','ipaddress','network','ip','mac']", :kind + param 'String[1]', :value + return_type 'Boolean' + end + + def has_interface(interface) # rubocop:disable Naming/PredicateName + interfaces.key? interface + end + + def has_interface_with(kind, value) # rubocop:disable Naming/PredicateName + # For compatibility with older version of function that used the legacy facts, alias `ip` with `ipaddress` and `mac` with `macaddress` + kind = 'ip' if kind == 'ipaddress' + kind = 'mac' if kind == 'macaddress' + + interface = interfaces.find { |_interface, params| params[kind] == value } + !interface.nil? + end + + def interfaces + closure_scope['facts']['networking']['interfaces'] + end +end diff --git a/spec/functions/has_interface_with_spec.rb b/spec/functions/has_interface_with_spec.rb index d55f58ead..45d4d9680 100644 --- a/spec/functions/has_interface_with_spec.rb +++ b/spec/functions/has_interface_with_spec.rb @@ -4,13 +4,49 @@ describe 'has_interface_with' do it { is_expected.not_to eq(nil) } - it { is_expected.to run.with_params.and_raise_error(Puppet::ParseError, %r{wrong number of arguments}i) } - it { is_expected.to run.with_params('one', 'two', 'three').and_raise_error(Puppet::ParseError, %r{wrong number of arguments}i) } + it { is_expected.to run.with_params.and_raise_error(ArgumentError, %r{expects between 1 and 2 arguments, got none}) } + it { is_expected.to run.with_params('one', 'two', 'three').and_raise_error(ArgumentError, %r{expects between 1 and 2 arguments, got 3}) } # We need to mock out the Facts so we can specify how we expect this function # to behave on different platforms. context 'when on Mac OS X Systems' do - let(:facts) { { interfaces: 'lo0,gif0,stf0,en1,p2p0,fw0,en0,vmnet1,vmnet8,utun0' } } + let(:facts) do + { + 'networking' => { + 'interfaces' => { + 'lo0' => { + 'bindings' => [ + { + 'address' => '127.0.0.1', + 'netmask' => '255.0.0.0', + 'network' => '127.0.0.0' + }, + ], + "bindings6": [ + { + 'address' => '::1', + 'netmask' => 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', + 'network' => '::1' + }, + { + 'address' => 'fe80::1', + 'netmask' => 'ffff:ffff:ffff:ffff::', + 'network' => 'fe80::' + }, + ], + 'ip' => '127.0.0.1', + 'ip6' => '::1', + 'mtu' => 16_384, + 'netmask' => '255.0.0.0', + 'netmask6' => 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', + 'network' => '127.0.0.0', + 'network6' => '::1', + 'scope6' => 'host' + }, + } + } + } + end it { is_expected.to run.with_params('lo0').and_return(true) } it { is_expected.to run.with_params('lo').and_return(false) } @@ -19,23 +55,93 @@ context 'when on Linux Systems' do let(:facts) do { - interfaces: 'eth0,lo', - ipaddress: '10.0.0.1', - ipaddress_lo: '127.0.0.1', - ipaddress_eth0: '10.0.0.1', - muppet: 'kermit', - muppet_lo: 'mspiggy', - muppet_eth0: 'kermit', + 'networking' => { + 'interfaces' => { + 'eth0' => { + 'bindings' => [ + { + 'address' => '10.0.2.15', + 'netmask' => '255.255.255.0', + 'network' => '10.0.2.0' + }, + ], + 'bindings6' => [ + { + 'address' => 'fe80::5054:ff:fe8a:fee6', + 'netmask' => 'ffff:ffff:ffff:ffff::', + 'network' => 'fe80::' + }, + ], + 'dhcp' => '10.0.2.2', + 'ip' => '10.0.2.15', + 'ip6' => 'fe80::5054:ff:fe8a:fee6', + 'mac' => '52:54:00:8a:fe:e6', + 'mtu' => 1500, + 'netmask' => '255.255.255.0', + 'netmask6' => 'ffff:ffff:ffff:ffff::', + 'network' => '10.0.2.0', + 'network6' => 'fe80::' + }, + 'eth1' => { + 'bindings' => [ + { + 'address' => '10.0.0.2', + 'netmask' => '255.255.255.0', + 'network' => '10.0.0.0' + }, + ], + 'bindings6' => [ + { + 'address' => 'fe80::a00:27ff:fed1:d28c', + 'netmask' => 'ffff:ffff:ffff:ffff::', + 'network' => 'fe80::' + }, + ], + 'ip' => '10.0.0.2', + 'ip6' => 'fe80::a00:27ff:fed1:d28c', + 'mac' => '08:00:27:d1:d2:8c', + 'mtu' => 1500, + 'netmask' => '255.255.255.0', + 'netmask6' => 'ffff:ffff:ffff:ffff::', + 'network' => '10.0.0.0', + 'network6' => 'fe80::' + }, + 'lo' => { + 'bindings' => [ + { + 'address' => '127.0.0.1', + 'netmask' => '255.0.0.0', + 'network' => '127.0.0.0' + }, + ], + 'bindings6' => [ + { + 'address' => '::1', + 'netmask' => 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', + 'network' => '::1' + }, + ], + 'ip' => '127.0.0.1', + 'ip6' => '::1', + 'mtu' => 65_536, + 'netmask' => '255.0.0.0', + 'netmask6' => 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', + 'network' => '127.0.0.0', + 'network6' => '::1' + } + }, + }, } end it { is_expected.to run.with_params('lo').and_return(true) } it { is_expected.to run.with_params('lo0').and_return(false) } it { is_expected.to run.with_params('ipaddress', '127.0.0.1').and_return(true) } - it { is_expected.to run.with_params('ipaddress', '10.0.0.1').and_return(true) } + it { is_expected.to run.with_params('ipaddress', '10.0.0.2').and_return(true) } it { is_expected.to run.with_params('ipaddress', '8.8.8.8').and_return(false) } - it { is_expected.to run.with_params('muppet', 'kermit').and_return(true) } - it { is_expected.to run.with_params('muppet', 'mspiggy').and_return(true) } - it { is_expected.to run.with_params('muppet', 'bigbird').and_return(false) } + it { is_expected.to run.with_params('netmask', '255.255.255.0').and_return(true) } + it { is_expected.to run.with_params('macaddress', '52:54:00:8a:fe:e6').and_return(true) } + it { is_expected.to run.with_params('network', '42.0.0.0').and_return(false) } + it { is_expected.to run.with_params('network', '10.0.0.0').and_return(true) } end end